diff options
| author | Tim Redfern <tim@eclectronics.org> | 2013-11-18 14:40:41 +0000 |
|---|---|---|
| committer | Tim Redfern <tim@eclectronics.org> | 2013-11-18 14:40:41 +0000 |
| commit | a36735b4585521218268da5fed2214ba239b4c2a (patch) | |
| tree | 77f951acb62d61c88b0b01d4bcb34367e44abc91 /lyricstimer | |
| parent | 6c5ba0ff0a4301b932c70aee6b4c87b2092d8bff (diff) | |
simple lyrics script
Diffstat (limited to 'lyricstimer')
| -rw-r--r-- | lyricstimer/keyview.py | 295 | ||||
| -rw-r--r-- | lyricstimer/pyxhook.py | 359 | ||||
| -rwxr-xr-x | lyricstimer/timelyrics | 60 |
3 files changed, 714 insertions, 0 deletions
diff --git a/lyricstimer/keyview.py b/lyricstimer/keyview.py new file mode 100644 index 0000000..ea4c3f3 --- /dev/null +++ b/lyricstimer/keyview.py @@ -0,0 +1,295 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from xml.sax.saxutils import escape, quoteattr +import gtk, gtk.glade +from time import time + +import pyxhook + +# Want to handle/show these in a nice emacsy way +MODIFIERS = ( + 'Control_L', + 'Control_R', + 'Alt_L', + 'Alt_R', + 'Super_L', + 'Mode_switch' +) + +CHORD_PREFIXES = ( + 'f1-', + 'f2-' +) + +SHIFT_KEYS = ( + 'Shift_L', + 'Shift_R', +) + +# Alter the appearance of some key events +KEY_MAP = { + 'F1':'f1-', + 'F2':'f2-', + 'F3':'f3', + 'F4':'f4', + 'F5':'f5', + 'F6':'f6', + 'F7':'f7', + 'F8':'f8', + 'F9':'f9', + 'F10':'f10', + 'F11':'f11', + 'F12':'f12', + 'Return':'↲', + 'Right': '→', + 'Left': '←', + 'Up': '↑', + 'Down': '↓', + 'Control_L':'Ctrl-', + 'Control_R':'Ctrl-', + 'Mode_switch':'Mod4-', + 'Alt_L':'Alt-', + 'Alt_R':'Alt-', + 'Shift_L':'⇪-', + 'Shift_R':'⇪-', + 'space': ' ', + 'parenleft': '(', + 'parenright': ')', + 'bracketleft': '[', + 'bracketright': ']', + 'braceleft': '{', + 'braceright': '}', + 'BackSpace': '⇤', + 'Delete': 'DEL', + 'Tab': '↹', + 'bar': '|', + 'minus': '-', + 'plus': '+', + 'asterisk': '*', + 'equal': '=', + 'less': '<', + 'greater': '>', + 'semicolon': ';', + 'colon': ':', + 'comma': ',', + 'apostrophe': "'", + 'quotedbl' : '"', + 'underscore' : '_', + 'numbersign' : '#', + 'percent' : '%', + 'exclam' : '!', + 'period' : '.', + 'slash' : '/', + 'backslash' : '\\', + 'question' : '?', + 'adiaeresis': 'ä', + 'odiaeresis': 'ö', + 'udiaeresis': 'ü', + 'ssharp': 'ß', + 'ampersand': '&', + 'section': '§', +} + +def get_hook_manager(): + hm = pyxhook.HookManager() + hm.HookKeyboard() + hm.HookMouse() + #hm.KeyDown = hm.printevent + #hm.KeyUp = self.hook_manager_event #hm.printevent + #hm.MouseAllButtonsDown = hm.printevent + #hm.MouseAllButtonsUp = hm.printevent + hm.start() + return hm + + +class GTKKeyView: + def __init__(self, hm): + + xml = gtk.glade.XML('keyview.libglade') + self.window = xml.get_widget('window1') + self.window.connect('destroy', self.quit) + self.key_strokes = xml.get_widget('label1') + self.key_strokes.set_alignment(0, 0) + self.menu = xml.get_widget('config-menu') + self.font_dialog = xml.get_widget('fontselectiondialog1') + self.font = 'Courier Bold 30' #None # text of font description from selection dialog + self.init_menu() + self.pressed_modifiers = {} # keep track of whats pushed + self.hm = hm + self.active = True + hm.KeyDown = self.hook_manager_down_event + hm.KeyUp = self.hook_manager_up_event + xml.signal_autoconnect(self) + self.max_lines = 3 + self.show_backspace = True + self.show_shift = False + self.keys = [] #stack of keys typed + self.last_key = 0 # keep track of time + + def init_menu(self): + self.font_item = gtk.MenuItem('set font...') + self.font_item.connect('activate', self.font_select) + + self.on_item = gtk.CheckMenuItem('listening') + self.on_item.set_active(True) + self.on_item.connect('activate', self.toggle_active) + + self.quit_item = gtk.MenuItem('quit') + self.quit_item.connect('activate', self.quit) + + for item in [self.font_item, self.on_item, self.quit_item]: + self.menu.append(item) + item.show() + + def font_select(self, widget): + response = self.font_dialog.run() + self.font_dialog.hide() + self.font = self.font_dialog.get_font_name() + cur_text = self.key_strokes.get_text() + self.update_text(cur_text, self.font) + + + def toggle_active(self, widget): + # active is state after click + self.active = widget.get_active() + + + def quit(self, widget): + self.hm.cancel() + gtk.main_quit() + + + def on_eventbox1_popup_menu(self, *args): + self.menu.show() + + def on_eventbox1_button_press_event(self, widget, event): + if event.type == gtk.gdk.BUTTON_PRESS and event.button == 3: + self.menu.popup(None, None, None, event.button, event.get_time()) + + def update_text(self, text, font_desc=None): + """ + see + http://www.pygtk.org/docs/pygtk/class-gtklabel.html + and + http://www.pygtk.org/docs/pygtk/pango-markup-language.html + """ + if font_desc: + font_desc_text = 'font_desc=%s' % quoteattr(font_desc) + else: + font_desc_text = '' + pango_markup = """<span %s>%s</span>""" % (font_desc_text, escape(text)) + self.key_strokes.set_markup(pango_markup) + + def hook_manager_up_event(self, event): + if event.Key in self.pressed_modifiers: + del self.pressed_modifiers[event.Key] + + def hook_manager_down_event(self, event): + #hm.printevent(event) + if self.active: + e_key = event.Key + + # hack to deal with ctr modifiers in emacsy way + modifiers = [] + postfix = '' + for modifier in self.pressed_modifiers.keys(): + mod = KEY_MAP.get(modifier, modifier) + if self.keys and not self.keys[-1].text == mod: + modifiers.append(mod) + postfix = ' ' + + if e_key in MODIFIERS: + self.pressed_modifiers[e_key] = 1 + elif e_key == 'BackSpace' and not self.show_backspace: + if self.keys: + self.keys.pop() + elif e_key in SHIFT_KEYS: + if self.show_shift: + self.pressed_modifiers[e_key] = 1 + else: + txt = KEY_MAP.get(e_key, e_key) + + prefix = ''.join(modifiers) + txt = Text(txt, prefix, postfix) + + isseq = ( + len(self.keys) > 0 and + (self.keys[-1].is_chord_prefix + or + self.keys[-1].is_char and time() - self.last_key < 1) + ) + + if not (txt.is_char and isseq): + self.keys.append(Text("\n")) + + self.keys.append(txt) + + self.last_key = time() + + + # limit line lengths + self.keys = limit_text(self.keys, self.max_lines) + new_text = ''.join([repr(x) for x in self.keys]) + + self.update_text(new_text, self.font) + +def limit_text(text_list, max_lines): + r""" + >>> lines = [Text('\n')]*4 + >>> len(limit_text(lines, 2)) #only 1 newline is possible since we're limiting to two lines + 1 + >>> lines = [Text(x) for x in 'foo\nbar\nbaz'] + >>> len(limit_text(lines, 2)) #from bar to end + 7 + + """ + # limit line lengths + new_line_idx = [i for i,x in enumerate(text_list) if x.text == '\n'] + if len(new_line_idx) >= max_lines: + new_start = new_line_idx[-max_lines] + 1 + text_list = text_list[new_start:] + return text_list + +class Text(object): + """Simple class to hold text and pre/postfix for it + """ + def __init__(self, text, prefix='', postfix=''): + self.text = text + self.prefix = prefix + self.postfix = postfix + + def __repr__(self): + return '%s%s%s' %(self.prefix, self.text, self.postfix) + + @property + def is_char(self): + t = self.text + return len(t) == 1 and (t.isalnum() or t.isspace()) + @property + def is_chord_prefix(self): + return self.text in CHORD_PREFIXES + +def main(): + gtk.gdk.threads_init() + hm = get_hook_manager() + view = GTKKeyView(hm) + w = view.window + w.resize(360, 150) + w.set_keep_above(True) #ensure visibility + w.show() + + try: + gtk.main() + except KeyboardInterrupt, e: + # kill the hook manager thread + view.hm.cancel() + +def test(): + import doctest + doctest.testmod() + +if __name__ == '__main__': + main() + + + diff --git a/lyricstimer/pyxhook.py b/lyricstimer/pyxhook.py new file mode 100644 index 0000000..387eb86 --- /dev/null +++ b/lyricstimer/pyxhook.py @@ -0,0 +1,359 @@ +#!/usr/bin/python +# from pykeylogger http://pykeylogger.sourceforge.net/ +# +# pyxhook -- an extension to emulate some of the PyHook library on linux. +# +# Copyright (C) 2008 Tim Alexander <dragonfyre13@gmail.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Thanks to Alex Badea <vamposdecampos@gmail.com> for writing the Record +# demo for the xlib libraries. It helped me immensely working with these +# in this library. +# +# Thanks to the python-xlib team. This wouldn't have been possible without +# your code. +# +# This requires: +# at least python-xlib 1.4 +# xwindows must have the "record" extension present, and active. +# +# This file has now been somewhat extensively modified by +# Daniel Folkinshteyn <nanotube@users.sf.net> +# So if there are any bugs, they are probably my fault. :) + +import sys +import os +import re +import time +import threading + +from Xlib import X, XK, display, error +from Xlib.ext import record +from Xlib.protocol import rq + +####################################################################### +########################START CLASS DEF################################ +####################################################################### + +class HookManager(threading.Thread): + """This is the main class. Instantiate it, and you can hand it KeyDown and KeyUp (functions in your own code) which execute to parse the pyxhookkeyevent class that is returned. + + This simply takes these two values for now: + KeyDown = The function to execute when a key is pressed, if it returns anything. It hands the function an argument that is the pyxhookkeyevent class. + KeyUp = The function to execute when a key is released, if it returns anything. It hands the function an argument that is the pyxhookkeyevent class. + """ + + def __init__(self): + threading.Thread.__init__(self) + self.finished = threading.Event() + + # Give these some initial values + self.mouse_position_x = 0 + self.mouse_position_y = 0 + self.ison = {"shift":False, "caps":False} + + # Compile our regex statements. + self.isshift = re.compile('^Shift') + self.iscaps = re.compile('^Caps_Lock') + self.shiftablechar = re.compile('^[a-z0-9]$|^minus$|^equal$|^bracketleft$|^bracketright$|^semicolon$|^backslash$|^apostrophe$|^comma$|^period$|^slash$|^grave$') + self.logrelease = re.compile('.*') + self.isspace = re.compile('^space$') + + # Assign default function actions (do nothing). + self.KeyDown = lambda x: True + self.KeyUp = lambda x: True + self.MouseAllButtonsDown = lambda x: True + self.MouseAllButtonsUp = lambda x: True + + self.contextEventMask = [X.KeyPress,X.MotionNotify] + + # Hook to our display. + self.local_dpy = display.Display() + self.record_dpy = display.Display() + + def run(self): + # Check if the extension is present + if not self.record_dpy.has_extension("RECORD"): + print "RECORD extension not found" + sys.exit(1) + r = self.record_dpy.record_get_version(0, 0) + print "RECORD extension version %d.%d" % (r.major_version, r.minor_version) + + # Create a recording context; we only want key and mouse events + self.ctx = self.record_dpy.record_create_context( + 0, + [record.AllClients], + [{ + 'core_requests': (0, 0), + 'core_replies': (0, 0), + 'ext_requests': (0, 0, 0, 0), + 'ext_replies': (0, 0, 0, 0), + 'delivered_events': (0, 0), + 'device_events': tuple(self.contextEventMask), #(X.KeyPress, X.ButtonPress), + 'errors': (0, 0), + 'client_started': False, + 'client_died': False, + }]) + + # Enable the context; this only returns after a call to record_disable_context, + # while calling the callback function in the meantime + self.record_dpy.record_enable_context(self.ctx, self.processevents) + # Finally free the context + self.record_dpy.record_free_context(self.ctx) + + def cancel(self): + self.finished.set() + self.local_dpy.record_disable_context(self.ctx) + self.local_dpy.flush() + + def printevent(self, event): + print event + + def HookKeyboard(self): + pass + # We don't need to do anything here anymore, since the default mask + # is now set to contain X.KeyPress + #self.contextEventMask[0] = X.KeyPress + + def HookMouse(self): + pass + # We don't need to do anything here anymore, since the default mask + # is now set to contain X.MotionNotify + + # need mouse motion to track pointer position, since ButtonPress events + # don't carry that info. + #self.contextEventMask[1] = X.MotionNotify + + def processevents(self, reply): + if reply.category != record.FromServer: + return + if reply.client_swapped: + print "* received swapped protocol data, cowardly ignored" + return + if not len(reply.data) or ord(reply.data[0]) < 2: + # not an event + return + data = reply.data + while len(data): + event, data = rq.EventField(None).parse_binary_value(data, self.record_dpy.display, None, None) + if event.type == X.KeyPress: + hookevent = self.keypressevent(event) + self.KeyDown(hookevent) + elif event.type == X.KeyRelease: + hookevent = self.keyreleaseevent(event) + self.KeyUp(hookevent) + elif event.type == X.ButtonPress: + hookevent = self.buttonpressevent(event) + self.MouseAllButtonsDown(hookevent) + elif event.type == X.ButtonRelease: + hookevent = self.buttonreleaseevent(event) + self.MouseAllButtonsUp(hookevent) + elif event.type == X.MotionNotify: + # use mouse moves to record mouse position, since press and release events + # do not give mouse position info (event.root_x and event.root_y have + # bogus info). + self.mousemoveevent(event) + + #print "processing events...", event.type + + def keypressevent(self, event): + matchto = self.lookup_keysym(self.local_dpy.keycode_to_keysym(event.detail, 0)) + if self.shiftablechar.match(self.lookup_keysym(self.local_dpy.keycode_to_keysym(event.detail, 0))): ## This is a character that can be typed. + if self.ison["shift"] == False: + keysym = self.local_dpy.keycode_to_keysym(event.detail, 0) + return self.makekeyhookevent(keysym, event) + else: + keysym = self.local_dpy.keycode_to_keysym(event.detail, 1) + return self.makekeyhookevent(keysym, event) + else: ## Not a typable character. + keysym = self.local_dpy.keycode_to_keysym(event.detail, 0) + if self.isshift.match(matchto): + self.ison["shift"] = self.ison["shift"] + 1 + elif self.iscaps.match(matchto): + if self.ison["caps"] == False: + self.ison["shift"] = self.ison["shift"] + 1 + self.ison["caps"] = True + if self.ison["caps"] == True: + self.ison["shift"] = self.ison["shift"] - 1 + self.ison["caps"] = False + return self.makekeyhookevent(keysym, event) + + def keyreleaseevent(self, event): + if self.shiftablechar.match(self.lookup_keysym(self.local_dpy.keycode_to_keysym(event.detail, 0))): + if self.ison["shift"] == False: + keysym = self.local_dpy.keycode_to_keysym(event.detail, 0) + else: + keysym = self.local_dpy.keycode_to_keysym(event.detail, 1) + else: + keysym = self.local_dpy.keycode_to_keysym(event.detail, 0) + matchto = self.lookup_keysym(keysym) + if self.isshift.match(matchto): + self.ison["shift"] = self.ison["shift"] - 1 + return self.makekeyhookevent(keysym, event) + + def buttonpressevent(self, event): + #self.clickx = self.rootx + #self.clicky = self.rooty + return self.makemousehookevent(event) + + def buttonreleaseevent(self, event): + #if (self.clickx == self.rootx) and (self.clicky == self.rooty): + ##print "ButtonClick " + str(event.detail) + " x=" + str(self.rootx) + " y=" + str(self.rooty) + #if (event.detail == 1) or (event.detail == 2) or (event.detail == 3): + #self.captureclick() + #else: + #pass + + return self.makemousehookevent(event) + + # sys.stdout.write("ButtonDown " + str(event.detail) + " x=" + str(self.clickx) + " y=" + str(self.clicky) + "\n") + # sys.stdout.write("ButtonUp " + str(event.detail) + " x=" + str(self.rootx) + " y=" + str(self.rooty) + "\n") + #sys.stdout.flush() + + def mousemoveevent(self, event): + self.mouse_position_x = event.root_x + self.mouse_position_y = event.root_y + + # need the following because XK.keysym_to_string() only does printable chars + # rather than being the correct inverse of XK.string_to_keysym() + def lookup_keysym(self, keysym): + for name in dir(XK): + if name.startswith("XK_") and getattr(XK, name) == keysym: + return name.lstrip("XK_") + return "[%d]" % keysym + + def asciivalue(self, keysym): + asciinum = XK.string_to_keysym(self.lookup_keysym(keysym)) + if asciinum < 256: + return asciinum + else: + return 0 + + def makekeyhookevent(self, keysym, event): + storewm = self.xwindowinfo() + if event.type == X.KeyPress: + MessageName = "key down" + elif event.type == X.KeyRelease: + MessageName = "key up" + return pyxhookkeyevent(storewm["handle"], storewm["name"], storewm["class"], self.lookup_keysym(keysym), self.asciivalue(keysym), False, event.detail, MessageName) + + def makemousehookevent(self, event): + storewm = self.xwindowinfo() + if event.detail == 1: + MessageName = "mouse left " + elif event.detail == 3: + MessageName = "mouse right " + elif event.detail == 2: + MessageName = "mouse middle " + elif event.detail == 5: + MessageName = "mouse wheel down " + elif event.detail == 4: + MessageName = "mouse wheel up " + else: + MessageName = "mouse " + str(event.detail) + " " + + if event.type == X.ButtonPress: + MessageName = MessageName + "down" + elif event.type == X.ButtonRelease: + MessageName = MessageName + "up" + return pyxhookmouseevent(storewm["handle"], storewm["name"], storewm["class"], (self.mouse_position_x, self.mouse_position_y), MessageName) + + def xwindowinfo(self): + try: + windowvar = self.local_dpy.get_input_focus().focus + wmname = windowvar.get_wm_name() + wmclass = windowvar.get_wm_class() + wmhandle = str(windowvar)[20:30] + except: + ## This is to keep things running smoothly. It almost never happens, but still... + return {"name":None, "class":None, "handle":None} + if (wmname == None) and (wmclass == None): + try: + windowvar = windowvar.query_tree().parent + wmname = windowvar.get_wm_name() + wmclass = windowvar.get_wm_class() + wmhandle = str(windowvar)[20:30] + except: + ## This is to keep things running smoothly. It almost never happens, but still... + return {"name":None, "class":None, "handle":None} + if wmclass == None: + return {"name":wmname, "class":wmclass, "handle":wmhandle} + else: + return {"name":wmname, "class":wmclass[0], "handle":wmhandle} + +class pyxhookkeyevent: + """This is the class that is returned with each key event.f + It simply creates the variables below in the class. + + Window = The handle of the window. + WindowName = The name of the window. + WindowProcName = The backend process for the window. + Key = The key pressed, shifted to the correct caps value. + Ascii = An ascii representation of the key. It returns 0 if the ascii value is not between 31 and 256. + KeyID = This is just False for now. Under windows, it is the Virtual Key Code, but that's a windows-only thing. + ScanCode = Please don't use this. It differs for pretty much every type of keyboard. X11 abstracts this information anyway. + MessageName = "key down", "key up". + """ + + def __init__(self, Window, WindowName, WindowProcName, Key, Ascii, KeyID, ScanCode, MessageName): + self.Window = Window + self.WindowName = WindowName + self.WindowProcName = WindowProcName + self.Key = Key + self.Ascii = Ascii + self.KeyID = KeyID + self.ScanCode = ScanCode + self.MessageName = MessageName + + def __str__(self): + return "Window Handle: " + str(self.Window) + "\nWindow Name: " + str(self.WindowName) + "\nWindow's Process Name: " + str(self.WindowProcName) + "\nKey Pressed: " + str(self.Key) + "\nAscii Value: " + str(self.Ascii) + "\nKeyID: " + str(self.KeyID) + "\nScanCode: " + str(self.ScanCode) + "\nMessageName: " + str(self.MessageName) + "\n" + +class pyxhookmouseevent: + """This is the class that is returned with each key event.f + It simply creates the variables below in the class. + + Window = The handle of the window. + WindowName = The name of the window. + WindowProcName = The backend process for the window. + Position = 2-tuple (x,y) coordinates of the mouse click + MessageName = "mouse left|right|middle down", "mouse left|right|middle up". + """ + + def __init__(self, Window, WindowName, WindowProcName, Position, MessageName): + self.Window = Window + self.WindowName = WindowName + self.WindowProcName = WindowProcName + self.Position = Position + self.MessageName = MessageName + + def __str__(self): + return "Window Handle: " + str(self.Window) + "\nWindow Name: " + str(self.WindowName) + "\nWindow's Process Name: " + str(self.WindowProcName) + "\nPosition: " + str(self.Position) + "\nMessageName: " + str(self.MessageName) + "\n" + +####################################################################### +#########################END CLASS DEF################################# +####################################################################### + +if __name__ == '__main__': + hm = HookManager() + hm.HookKeyboard() + hm.HookMouse() + hm.KeyDown = hm.printevent + hm.KeyUp = hm.printevent + hm.MouseAllButtonsDown = hm.printevent + hm.MouseAllButtonsUp = hm.printevent + hm.start() + time.sleep(10) + hm.cancel() diff --git a/lyricstimer/timelyrics b/lyricstimer/timelyrics new file mode 100755 index 0000000..2d6e071 --- /dev/null +++ b/lyricstimer/timelyrics @@ -0,0 +1,60 @@ +#!/usr/bin/python + +from pyxhook import HookManager +from time import time +import termios, sys +import atexit + +if len(sys.argv)<2: + print "usage: timelyrics audio_filename" + exit() + +import pygame +pygame.mixer.init() +pygame.mixer.music.load(sys.argv[1]) +pygame.mixer.music.play() + +for arg in sys.argv: + print arg + + +def enable_echo(fd, enabled): + (iflag, oflag, cflag, lflag, ispeed, ospeed, cc) \ + = termios.tcgetattr(fd) + if enabled: + lflag |= termios.ECHO + else: + lflag &= ~termios.ECHO + new_attr = [iflag, oflag, cflag, lflag, ispeed, ospeed, cc] + termios.tcsetattr(fd, termios.TCSANOW, new_attr) + +atexit.register(enable_echo, sys.stdin.fileno(), True) + +enable_echo( sys.stdin.fileno(),False) + +start=0.0 +down=0.0 +state="up" +count=0 + +def handle_up (event): + global start,down,state,count + if count>0: + print "[",down,",",time()-(start+down),"]" + state="up" + count+=1 + +def handle_down (event): + global start,down,state + if state=="up": + down=time()-start + state="down" + + +hm = HookManager() +hm.HookKeyboard() +hm.KeyUp = handle_up +hm.KeyDown = handle_down +start=time() +print "started:",start +hm.start()
\ No newline at end of file |
