import re # Keycmd format which includes the markup for the cursor. KEYCMD_FORMAT = "%s%s%s" MODCMD_FORMAT = " %s " def escape(str): for char in ['\\', '@']: str = str.replace(char, '\\'+char) return str def uzbl_escape(str): return "@[%s]@" % escape(str) if str else '' class Keylet(object): '''Small per-instance object that tracks characters typed.''' def __init__(self): # Modcmd tracking self.modcmd = '' self.is_modcmd = False # Keycmd tracking self.keycmd = '' self.cursor = 0 self.modmaps = {} self.ignores = {} def get_keycmd(self): '''Get the keycmd-part of the keylet.''' return self.keycmd def get_modcmd(self): '''Get the modcmd-part of the keylet.''' if not self.is_modcmd: return '' return self.modcmd def modmap_key(self, key): '''Make some obscure names for some keys friendlier.''' if key in self.modmaps: return self.modmaps[key] elif key.endswith('_L') or key.endswith('_R'): # Remove left-right discrimination and try again. return self.modmap_key(key[:-2]) else: return key def key_ignored(self, key): '''Check if the given key is ignored by any ignore rules.''' for (glob, match) in self.ignores.items(): if match(key): return True return False def __repr__(self): '''Return a string representation of the keylet.''' l = [] if self.is_modcmd: l.append('modcmd=%r' % self.get_modcmd()) if self.keycmd: l.append('keycmd=%r' % self.get_keycmd()) return '' % ', '.join(l) def add_modmap(uzbl, key, map): '''Add modmaps. Examples: set modmap = request MODMAP @modmap @modmap ... Then: @bind = @bind x = ... ''' assert len(key) modmaps = uzbl.keylet.modmaps modmaps[key.strip('<>')] = map.strip('<>') uzbl.event("NEW_MODMAP", key, map) def modmap_parse(uzbl, map): '''Parse a modmap definiton.''' split = [s.strip() for s in map.split(' ') if s.split()] if not split or len(split) > 2: raise Exception('Invalid modmap arugments: %r' % map) add_modmap(uzbl, *split) def add_key_ignore(uzbl, glob): '''Add an ignore definition. Examples: set ignore_key = request IGNORE_KEY @ignore_key @ignore_key ... ''' assert len(glob) > 1 ignores = uzbl.keylet.ignores glob = "<%s>" % glob.strip("<> ") restr = glob.replace('*', '[^\s]*') match = re.compile(restr).match ignores[glob] = match uzbl.event('NEW_KEY_IGNORE', glob) def clear_keycmd(uzbl, *args): '''Clear the keycmd for this uzbl instance.''' k = uzbl.keylet k.keycmd = '' k.cursor = 0 del uzbl.config['keycmd'] uzbl.event('KEYCMD_CLEARED') def clear_modcmd(uzbl): '''Clear the modcmd for this uzbl instance.''' k = uzbl.keylet k.modcmd = '' k.is_modcmd = False del uzbl.config['modcmd'] uzbl.event('MODCMD_CLEARED') def clear_current(uzbl): '''Clear the modcmd if is_modcmd else clear keycmd.''' if uzbl.keylet.is_modcmd: clear_modcmd(uzbl) else: clear_keycmd(uzbl) def update_event(uzbl, modstate, k, execute=True): '''Raise keycmd & modcmd update events.''' keycmd, modcmd = k.get_keycmd(), ''.join(modstate) + k.get_modcmd() if k.is_modcmd: logger.debug('modcmd_update, %s' % modcmd) uzbl.event('MODCMD_UPDATE', modstate, k) else: logger.debug('keycmd_update, %s' % keycmd) uzbl.event('KEYCMD_UPDATE', modstate, k) if uzbl.config.get('modcmd_updates', '1') == '1': new_modcmd = ''.join(modstate) + k.get_modcmd() if not new_modcmd or not k.is_modcmd: del uzbl.config['modcmd'] elif new_modcmd == modcmd: uzbl.config['modcmd'] = MODCMD_FORMAT % uzbl_escape(modcmd) if uzbl.config.get('keycmd_events', '1') != '1': return new_keycmd = k.get_keycmd() if not new_keycmd: del uzbl.config['keycmd'] elif new_keycmd == keycmd: # Generate the pango markup for the cursor in the keycmd. curchar = keycmd[k.cursor] if k.cursor < len(keycmd) else ' ' chunks = [keycmd[:k.cursor], curchar, keycmd[k.cursor+1:]] value = KEYCMD_FORMAT % tuple(map(uzbl_escape, chunks)) uzbl.config['keycmd'] = value def inject_str(str, index, inj): '''Inject a string into string at at given index.''' return "%s%s%s" % (str[:index], inj, str[index:]) def parse_key_event(uzbl, key): ''' Build a set from the modstate part of the event, and pass all keys through modmap ''' keylet = uzbl.keylet modstate, key = splitquoted(key) modstate = set(['<%s>' % keylet.modmap_key(k) for k in modstate.split('|') if k]) key = keylet.modmap_key(key) return modstate, key def key_press(uzbl, key): '''Handle KEY_PRESS events. Things done by this function include: 1. Ignore all shift key presses (shift can be detected by capital chars) 2. In non-modcmd mode: a. append char to keycmd 3. If not in modcmd mode and a modkey was pressed set modcmd mode. 4. Keycmd is updated and events raised if anything is changed.''' k = uzbl.keylet modstate, key = parse_key_event(uzbl, key) k.is_modcmd = any(not k.key_ignored(m) for m in modstate) logger.debug('key press modstate=%s' % str(modstate)) if key.lower() == 'space' and not k.is_modcmd and k.keycmd: k.keycmd = inject_str(k.keycmd, k.cursor, ' ') k.cursor += 1 elif not k.is_modcmd and len(key) == 1: if uzbl.config.get('keycmd_events', '1') != '1': # TODO, make a note on what's going on here k.keycmd = '' k.cursor = 0 del uzbl.config['keycmd'] return k.keycmd = inject_str(k.keycmd, k.cursor, key) k.cursor += 1 elif len(key) == 1: k.modcmd += key else: if not k.key_ignored('<%s>' % key): modstate.add('<%s>' % key) k.is_modcmd = True update_event(uzbl, modstate, k) def key_release(uzbl, key): '''Respond to KEY_RELEASE event. Things done by this function include: 1. If in a mod-command then raise a MODCMD_EXEC. 2. Update the keycmd uzbl variable if anything changed.''' k = uzbl.keylet modstate, key = parse_key_event(uzbl, key) if len(key) > 1: if k.is_modcmd: uzbl.event('MODCMD_EXEC', modstate, k) clear_modcmd(uzbl) def set_keycmd(uzbl, keycmd): '''Allow setting of the keycmd externally.''' k = uzbl.keylet k.keycmd = keycmd k.cursor = len(keycmd) update_event(uzbl, set(), k, False) def inject_keycmd(uzbl, keycmd): '''Allow injecting of a string into the keycmd at the cursor position.''' k = uzbl.keylet k.keycmd = inject_str(k.keycmd, k.cursor, keycmd) k.cursor += len(keycmd) update_event(uzbl, set(), k, False) def append_keycmd(uzbl, keycmd): '''Allow appening of a string to the keycmd.''' k = uzbl.keylet k.keycmd += keycmd k.cursor = len(k.keycmd) update_event(uzbl, set(), k, False) def keycmd_strip_word(uzbl, seps): ''' Removes the last word from the keycmd, similar to readline ^W ''' seps = seps or ' ' k = uzbl.keylet if not k.keycmd: return head, tail = k.keycmd[:k.cursor].rstrip(seps), k.keycmd[k.cursor:] rfind = -1 for sep in seps: p = head.rfind(sep) if p >= 0 and rfind < p + 1: rfind = p + 1 if rfind == len(head) and head[-1] in seps: rfind -= 1 head = head[:rfind] if rfind + 1 else '' k.keycmd = head + tail k.cursor = len(head) update_event(uzbl, set(), k, False) def keycmd_backspace(uzbl, *args): '''Removes the character at the cursor position in the keycmd.''' k = uzbl.keylet if not k.keycmd or not k.cursor: return k.keycmd = k.keycmd[:k.cursor-1] + k.keycmd[k.cursor:] k.cursor -= 1 update_event(uzbl, set(), k, False) def keycmd_delete(uzbl, *args): '''Removes the character after the cursor position in the keycmd.''' k = uzbl.keylet if not k.keycmd: return k.keycmd = k.keycmd[:k.cursor] + k.keycmd[k.cursor+1:] update_event(uzbl, set(), k, False) def keycmd_exec_current(uzbl, *args): '''Raise a KEYCMD_EXEC with the current keylet and then clear the keycmd.''' uzbl.event('KEYCMD_EXEC', set(), uzbl.keylet) clear_keycmd(uzbl) def set_cursor_pos(uzbl, index): '''Allow setting of the cursor position externally. Supports negative indexing and relative stepping with '+' and '-'.''' k = uzbl.keylet if index == '-': cursor = k.cursor - 1 elif index == '+': cursor = k.cursor + 1 else: cursor = int(index.strip()) if cursor < 0: cursor = len(k.keycmd) + cursor + 1 if cursor < 0: cursor = 0 if cursor > len(k.keycmd): cursor = len(k.keycmd) k.cursor = cursor update_event(uzbl, set(), k, False) # plugin init hook def init(uzbl): '''Export functions and connect handlers to events.''' connect_dict(uzbl, { 'APPEND_KEYCMD': append_keycmd, 'IGNORE_KEY': add_key_ignore, 'INJECT_KEYCMD': inject_keycmd, 'KEYCMD_BACKSPACE': keycmd_backspace, 'KEYCMD_DELETE': keycmd_delete, 'KEYCMD_EXEC_CURRENT': keycmd_exec_current, 'KEYCMD_STRIP_WORD': keycmd_strip_word, 'KEYCMD_CLEAR': clear_keycmd, 'KEY_PRESS': key_press, 'KEY_RELEASE': key_release, 'MOD_PRESS': key_press, 'MOD_RELEASE': key_release, 'MODMAP': modmap_parse, 'SET_CURSOR_POS': set_cursor_pos, 'SET_KEYCMD': set_keycmd, }) export_dict(uzbl, { 'add_key_ignore': add_key_ignore, 'add_modmap': add_modmap, 'append_keycmd': append_keycmd, 'clear_current': clear_current, 'clear_keycmd': clear_keycmd, 'clear_modcmd': clear_modcmd, 'inject_keycmd': inject_keycmd, 'keylet': Keylet(), 'set_cursor_pos': set_cursor_pos, 'set_keycmd': set_keycmd, }) # vi: set et ts=4: