'''Plugin provides support for binds in uzbl. For example: event BIND ZZ = exit -> bind('ZZ', 'exit') event BIND o _ = uri %s -> bind('o _', 'uri %s') event BIND fl* = sh 'echo %s' -> bind('fl*', "sh 'echo %s'") And it is also possible to execute a function on activation: bind('DD', myhandler) ''' import sys import re import pprint # Hold the bind dicts for each uzbl instance. UZBLS = {} # Commonly used regular expressions. MOD_START = re.compile('^<([A-Z][A-Za-z0-9-_]*)>').match # Matches , <'x':y>, <:'y'>, , <'x'!y>, ... PROMPTS = '<(\"[^\"]*\"|\'[^\']*\'|[^:!>]*)(:|!)(\"[^\"]*\"|\'[^\']*\'|[^>]*)>' FIND_PROMPTS = re.compile(PROMPTS).split VALID_MODE = re.compile('^(-|)[A-Za-z0-9][A-Za-z0-9_]*$').match # For accessing a bind glob stack. ON_EXEC, HAS_ARGS, MOD_CMD, GLOB, MORE = range(5) # Custom errors. class ArgumentError(Exception): pass class Bindlet(object): '''Per-instance bind status/state tracker.''' def __init__(self, uzbl): self.binds = {'global': {}} self.uzbl = uzbl self.depth = 0 self.args = [] self.last_mode = None self.after_cmds = None self.stack_binds = [] # A subset of the global mode binds containing non-stack and modkey # activiated binds for use in the stack mode. self.globals = [] def __getitem__(self, key): return self.get_binds(key) def reset(self): '''Reset the tracker state and return to last mode.''' self.depth = 0 self.args = [] self.after_cmds = None self.stack_binds = [] if self.last_mode: mode, self.last_mode = self.last_mode, None self.uzbl.set_mode(mode) self.uzbl.set('keycmd_prompt') def stack(self, bind, args, depth): '''Enter or add new bind in the next stack level.''' if self.depth != depth: if bind not in self.stack_binds: self.stack_binds.append(bind) return current_mode = self.uzbl.get_mode() if current_mode != 'stack': self.last_mode = current_mode self.uzbl.set_mode('stack') self.stack_binds = [bind,] self.args += args self.depth += 1 self.after_cmds = bind.prompts[depth] def after(self): '''If a stack was triggered then set the prompt and default value.''' if self.after_cmds is None: return (prompt, is_cmd, set), self.after_cmds = self.after_cmds, None self.uzbl.clear_keycmd() if prompt: self.uzbl.set('keycmd_prompt', prompt) if set and is_cmd: self.uzbl.send(set) elif set and not is_cmd: self.uzbl.send('event SET_KEYCMD %s' % set) def get_binds(self, mode=None): '''Return the mode binds + globals. If we are stacked then return the filtered stack list and modkey & non-stack globals.''' if mode is None: mode = self.uzbl.get_mode() if not mode: mode = 'global' if self.depth: return self.stack_binds + self.globals globals = self.binds['global'] if mode not in self.binds or mode == 'global': return filter(None, globals.values()) binds = dict(globals.items() + self.binds[mode].items()) return filter(None, binds.values()) def add_bind(self, mode, glob, bind=None): '''Insert (or override) a bind into the mode bind dict.''' if mode not in self.binds: self.binds[mode] = {glob: bind} return binds = self.binds[mode] binds[glob] = bind if mode == 'global': # Regen the global-globals list. self.globals = [] for bind in binds.values(): if bind is not None and bind.is_global: self.globals.append(bind) def add_instance(uzbl, *args): UZBLS[uzbl] = Bindlet(uzbl) def del_instance(uzbl, *args): if uzbl in UZBLS: del UZBLS[uzbl] def get_bindlet(uzbl): '''Return the bind tracklet for the given uzbl instance.''' if uzbl not in UZBLS: add_instance(uzbl) return UZBLS[uzbl] def ismodbind(glob): '''Return True if the glob specifies a modbind.''' return bool(MOD_START(glob)) def split_glob(glob): '''Take a string of the form "cmd _" and return a list of the modkeys in the glob and the command.''' mods = set() while True: match = MOD_START(glob) if not match: break end = match.span()[1] mods.add(glob[:end]) glob = glob[end:] return (mods, glob) def unquote(str): '''Remove quotation marks around string.''' if str and str[0] == str[-1] and str[0] in ['"', "'"]: str = str[1:-1] return str class Bind(object): # Class attribute to hold the number of Bind classes created. counter = [0,] def __init__(self, glob, handler, *args, **kargs): self.is_callable = callable(handler) self._repr_cache = None if not glob: raise ArgumentError('glob cannot be blank') if self.is_callable: self.function = handler self.args = args self.kargs = kargs elif kargs: raise ArgumentError('cannot supply kargs for uzbl commands') elif hasattr(handler, '__iter__'): self.commands = handler else: self.commands = [handler,] + list(args) self.glob = glob # Assign unique id. self.counter[0] += 1 self.bid = self.counter[0] self.split = split = FIND_PROMPTS(glob) self.prompts = [] for (prompt, cmd, set) in zip(split[1::4], split[2::4], split[3::4]): prompt, set = map(unquote, [prompt, set]) cmd = True if cmd == '!' else False if prompt and prompt[-1] != ":": prompt = "%s:" % prompt self.prompts.append((prompt, cmd, set)) # Check that there is nothing like: fl** for glob in split[:-1:4]: if glob.endswith('*'): msg = "token '*' not at the end of a prompt bind: %r" % split raise SyntaxError(msg) # Check that there is nothing like: fl_ for glob in split[4::4]: if not glob: msg = 'found null segment after first prompt: %r' % split raise SyntaxError(msg) stack = [] for (index, glob) in enumerate(reversed(split[::4])): # Is the binding a MODCMD or KEYCMD: mod_cmd = ismodbind(glob) # Do we execute on UPDATES or EXEC events? on_exec = True if glob[-1] in ['!', '_'] else False # Does the command take arguments? has_args = True if glob[-1] in ['*', '_'] else False glob = glob[:-1] if has_args or on_exec else glob mods, glob = split_glob(glob) stack.append((on_exec, has_args, mods, glob, index)) self.stack = list(reversed(stack)) self.is_global = (len(self.stack) == 1 and self.stack[0][MOD_CMD]) def __getitem__(self, depth): '''Get bind info at a depth.''' if self.is_global: return self.stack[0] return self.stack[depth] def __repr__(self): if self._repr_cache: return self._repr_cache args = ['glob=%r' % self.glob, 'bid=%d' % self.bid] if self.is_callable: args.append('function=%r' % self.function) if self.args: args.append('args=%r' % self.args) if self.kargs: args.append('kargs=%r' % self.kargs) else: cmdlen = len(self.commands) cmds = self.commands[0] if cmdlen == 1 else self.commands args.append('command%s=%r' % ('s' if cmdlen-1 else '', cmds)) self._repr_cache = '' % ', '.join(args) return self._repr_cache def exec_bind(uzbl, bind, *args, **kargs): '''Execute bind objects.''' uzbl.event("EXEC_BIND", bind, args, kargs) if bind.is_callable: args += bind.args kargs = dict(bind.kargs.items()+kargs.items()) bind.function(uzbl, *args, **kargs) return if kargs: raise ArgumentError('cannot supply kargs for uzbl commands') commands = [] cmd_expand = uzbl.cmd_expand for cmd in bind.commands: cmd = cmd_expand(cmd, args) uzbl.send(cmd) def mode_bind(uzbl, modes, glob, handler=None, *args, **kargs): '''Add a mode bind.''' bindlet = get_bindlet(uzbl) if not hasattr(modes, '__iter__'): modes = unicode(modes).split(',') # Sort and filter binds. modes = filter(None, map(unicode.strip, modes)) if callable(handler) or (handler is not None and handler.strip()): bind = Bind(glob, handler, *args, **kargs) else: bind = None for mode in modes: if not VALID_MODE(mode): raise NameError('invalid mode name: %r' % mode) for mode in modes: if mode[0] == '-': mode, bind = mode[1:], None bindlet.add_bind(mode, glob, bind) uzbl.event('ADDED_MODE_BIND', mode, glob, bind) def bind(uzbl, glob, handler, *args, **kargs): '''Legacy bind function.''' mode_bind(uzbl, 'global', glob, handler, *args, **kargs) def parse_mode_bind(uzbl, args): '''Parser for the MODE_BIND event. Example events: MODE_BIND = MODE_BIND command o_ = uri %s MODE_BIND insert,command = ... MODE_BIND global ... = ... MODE_BIND global,-insert ... = ... ''' if not args: raise ArgumentError('missing bind arguments') split = map(unicode.strip, args.split(' ', 1)) if len(split) != 2: raise ArgumentError('missing mode or bind section: %r' % args) modes, args = split[0].split(','), split[1] split = map(unicode.strip, args.split('=', 1)) if len(split) != 2: raise ArgumentError('missing delimiter in bind section: %r' % args) glob, command = split mode_bind(uzbl, modes, glob, command) def parse_bind(uzbl, args): '''Legacy parsing of the BIND event and conversion to the new format. Example events: request BIND = request BIND o_ = uri %s request BIND = ... request BIND ... = ... ''' parse_mode_bind(uzbl, "global %s" % args) def mode_changed(uzbl, mode): '''Clear the stack on all non-stack mode changes.''' if mode != 'stack': get_bindlet(uzbl).reset() def match_and_exec(uzbl, bind, depth, keylet, bindlet): (on_exec, has_args, mod_cmd, glob, more) = bind[depth] cmd = keylet.modcmd if mod_cmd else keylet.keycmd if mod_cmd and keylet.held != mod_cmd: return False if has_args: if not cmd.startswith(glob): return False args = [cmd[len(glob):],] elif cmd != glob: return False else: args = [] if bind.is_global or (not more and depth == 0): exec_bind(uzbl, bind, *args) if not has_args: uzbl.clear_current() return True elif more: bindlet.stack(bind, args, depth) return False args = bindlet.args + args exec_bind(uzbl, bind, *args) uzbl.set_mode() if not has_args: bindlet.reset() uzbl.clear_current() return True def keycmd_update(uzbl, keylet): bindlet = get_bindlet(uzbl) depth = bindlet.depth for bind in bindlet.get_binds(): t = bind[depth] if t[MOD_CMD] or t[ON_EXEC]: continue if match_and_exec(uzbl, bind, depth, keylet, bindlet): return bindlet.after() def keycmd_exec(uzbl, keylet): bindlet = get_bindlet(uzbl) depth = bindlet.depth for bind in bindlet.get_binds(): t = bind[depth] if t[MOD_CMD] or not t[ON_EXEC]: continue if match_and_exec(uzbl, bind, depth, keylet, bindlet): return uzbl.clear_keycmd() bindlet.after() def modcmd_update(uzbl, keylet): bindlet = get_bindlet(uzbl) depth = bindlet.depth for bind in bindlet.get_binds(): t = bind[depth] if not t[MOD_CMD] or t[ON_EXEC]: continue if match_and_exec(uzbl, bind, depth, keylet, bindlet): return bindlet.after() def modcmd_exec(uzbl, keylet): bindlet = get_bindlet(uzbl) depth = bindlet.depth for bind in bindlet.get_binds(): t = bind[depth] if not t[MOD_CMD] or not t[ON_EXEC]: continue if match_and_exec(uzbl, bind, depth, keylet, bindlet): return uzbl.clear_modcmd() bindlet.after() def init(uzbl): # Event handling hooks. uzbl.connect_dict({ 'BIND': parse_bind, 'KEYCMD_EXEC': keycmd_exec, 'KEYCMD_UPDATE': keycmd_update, 'MODCMD_EXEC': modcmd_exec, 'MODCMD_UPDATE': modcmd_update, 'MODE_BIND': parse_mode_bind, 'MODE_CHANGED': mode_changed, }) # Function exports to the uzbl object, `function(uzbl, *args, ..)` # becomes `uzbl.function(*args, ..)`. uzbl.export_dict({ 'bind': bind, 'mode_bind': mode_bind, 'get_bindlet': get_bindlet, })