diff options
author | David Keijser <keijser@gmail.com> | 2009-10-25 01:26:48 +0200 |
---|---|---|
committer | David Keijser <keijser@gmail.com> | 2009-10-25 01:26:48 +0200 |
commit | f5ca0aa5c05df75dd3268e85feb8947b92486aaf (patch) | |
tree | f89f52338bafdfcbaeb9d589de78659545219de9 /examples | |
parent | fdca08d7d7ae0d6ec41cc23220f8cdd89785e370 (diff) | |
parent | 75c771ecf35605045dd7760269395ca276e8d989 (diff) |
Merge branch 'experimental' of git://github.com/Dieterbe/uzbl into prompt
Conflicts:
examples/data/uzbl/plugins/bind.py
Diffstat (limited to 'examples')
-rw-r--r-- | examples/config/uzbl/config | 57 | ||||
-rw-r--r-- | examples/data/uzbl/plugins/bind.py (renamed from examples/data/uzbl/scripts/plugins/bind.py) | 123 | ||||
-rw-r--r-- | examples/data/uzbl/plugins/config.py (renamed from examples/data/uzbl/scripts/plugins/config.py) | 0 | ||||
-rw-r--r-- | examples/data/uzbl/plugins/keycmd.py | 395 | ||||
-rw-r--r-- | examples/data/uzbl/plugins/mode.py (renamed from examples/data/uzbl/scripts/plugins/mode.py) | 0 | ||||
-rw-r--r-- | examples/data/uzbl/plugins/on_event.py (renamed from examples/data/uzbl/scripts/plugins/on_event.py) | 0 | ||||
-rw-r--r-- | examples/data/uzbl/plugins/plugin_template.py (renamed from examples/data/uzbl/scripts/plugins/plugin_template.py) | 0 | ||||
-rw-r--r-- | examples/data/uzbl/plugins/progress_bar.py (renamed from examples/data/uzbl/scripts/plugins/progress_bar.py) | 0 | ||||
-rwxr-xr-x | examples/data/uzbl/scripts/event_manager.py | 871 | ||||
-rw-r--r-- | examples/data/uzbl/scripts/plugins/keycmd.py | 382 |
10 files changed, 1023 insertions, 805 deletions
diff --git a/examples/config/uzbl/config b/examples/config/uzbl/config index fae36a2..e5b3c6a 100644 --- a/examples/config/uzbl/config +++ b/examples/config/uzbl/config @@ -32,7 +32,7 @@ set scheme_handler = spawn @scripts_dir/scheme.py #set new_window = sh 'echo uri "$8" > $4' # open in same window set new_window = sh 'uzbl-browser -u $8' # equivalent to the default behaviour -# Load start handler +# Load start handlers @on_event LOAD_START @set_status <span foreground="khaki">wait</span> # Load commit handler @@ -42,6 +42,15 @@ set new_window = sh 'uzbl-browser -u $8' # equivalent to the default beh @on_event LOAD_FINISH @set_status <span foreground="gold">done</span> @on_event LOAD_FINISH spawn @scripts_dir/history.sh +# Generate a FORM_ACTIVE event if an editable +# element on the loaded site has initial focus +@on_event LOAD_FINISH js if(document.activeElement.type == 'text') {Uzbl.run("event FORM_ACTIVE");} + +# Switch to insert mode if a (editable) html form is clicked +@on_event FORM_ACTIVE @set_mode insert +# Switch to command mode if anything else is clicked +@on_event ROOT_ACTIVE @set_mode command + # Misc on_event handlers #@on_event CONFIG_CHANGED print Config changed: %1 = %2 @@ -52,12 +61,13 @@ set show_status = 1 set status_top = 0 set status_background = #303030 -set keycmd_style = weight="bold" foreground="red" +set modcmd_style = weight="bold" foreground="red" +set keycmd_style = weight="light" foreground="red" set prompt_style = foreground="grey" set cursor_style = underline="single" set mode_section = <span background="khaki" foreground="black">[\@[\@mode_indicator]\@]</span> -set keycmd_section = [<span \@prompt_style>\@[\@keycmd_prompt]\@</span><span \@keycmd_style>\@keycmd</span>] +set keycmd_section = [<span \@prompt_style>\@[\@keycmd_prompt]\@</span><span \@modcmd_style>\@modcmd</span><span \@keycmd_style>\@keycmd</span>] set progress_section = <span foreground="#606060">\@[\@progress_format]\@</span> set uri_section = <span foreground="#99FF66">\@[\@uri]\@</span> set name_section = <span foreground="khaki">\@[\@NAME]\@</span> @@ -183,8 +193,41 @@ set formfiller = spawn @scripts_dir/formfiller @bind <Ctrl>b<name:>_<tags:>_ = sh 'echo -e "$6 $7 %s %s" >> $XDG_DATA_HOME/uzbl/bookmarks' # Multi-stage bindings with blank prompts (similar behaviour to emacs M-c M-s bindings?) -@bind <Ctrl>a<:>q = exit -@bind <Ctrl>a<:>h = uri http://uzbl.org/ +@bind <Ctrl>a<:><Ctrl>q = exit +@bind <Ctrl>a<:><Ctrl>h = uri http://uzbl.org/ + + +# === command editing configuration ========================================== + +# you'll want this at the very least +@bind <Return> = event KEYCMD_EXEC_CURRENT +@bind <Escape> = event @set_mode + +# basic searching +@bind <Home> = event SET_CURSOR_POS 0 +@bind <End> = event SET_CURSOR_POS -1 +@bind <Left> = event SET_CURSOR_POS - +@bind <Right> = event SET_CURSOR_POS + +@bind <BackSpace> = event KEYCMD_BACKSPACE +@bind <Delete> = event KEYCMD_DELETE + +# readline-ish bindings +@bind <Ctrl>w = event KEYCMD_STRIP_WORD +@bind <Ctrl>u = event SET_KEYCMD +@bind <Ctrl>a = event SET_CURSOR_POS 0 +@bind <Ctrl>e = event SET_CURSOR_POS -1 + + +# === Context menu items ===================================================== + +# Default context menu +menu_add Google = set uri = http://google.com +menu_add Go Home = set uri = http://uzbl.org +menu_separator separator_1 +menu_add Quit uzbl = exit + +# Link context menu +menu_link_add Print Link = print \@SELECTED_URI # === Mode configuration ===================================================== @@ -195,7 +238,7 @@ set insert = @mode_config insert set stack = @mode_config stack # Command mode config. -@command keycmd_style = weight="bold" foreground="red" +@command keycmd_style = foreground="red" @command status_background = #202020 @command mode_indicator = Cmd @@ -221,7 +264,7 @@ set default_mode = command set toggle_cmd_ins = @toggle_modes command insert @bind i = @toggle_cmd_ins -@bind <Ctrl>i = @toggle_cmd_ins +@bind <Ctrl><i> = @toggle_cmd_ins # === Post-load misc commands =============================================== diff --git a/examples/data/uzbl/scripts/plugins/bind.py b/examples/data/uzbl/plugins/bind.py index 16c8148..8cd0702 100644 --- a/examples/data/uzbl/scripts/plugins/bind.py +++ b/examples/data/uzbl/plugins/bind.py @@ -20,7 +20,7 @@ __export__ = ['bind', 'del_bind', 'del_bind_by_glob', 'get_binds'] UZBLS = {} # Commonly used regular expressions. -starts_with_mod = re.compile('^<([A-Z][A-Za-z0-9-_]+)>') +starts_with_mod = re.compile('^<([A-Z][A-Za-z0-9-_]*)>') find_prompts = re.compile('<([^:>]*):(\"[^>]*\"|)>').split # For accessing a bind glob stack. @@ -135,12 +135,13 @@ class Bind(object): nextbid = counter().next def __init__(self, glob, handler, *args, **kargs): - self.callable = iscallable(handler) + self.is_callable = iscallable(handler) + self._repr_cache = None if not glob: raise ArgumentError('glob cannot be blank') - if self.callable: + if self.is_callable: self.function = handler self.args = args self.kargs = kargs @@ -172,9 +173,8 @@ class Bind(object): msg = 'found null segment after first prompt: %r' % split raise BindParseError(msg) - self.stack = [] - - for glob in split[::3]: + stack = [] + for (index, glob) in enumerate(reversed(split[::3])): # Is the binding a MODCMD or KEYCMD: mod_cmd = ismodbind(glob) @@ -185,13 +185,28 @@ class Bind(object): has_args = True if glob[-1] in ['*', '_'] else False glob = glob[:-1] if has_args else glob - self.stack.append((mod_cmd, on_exec, has_args, glob)) + stack.append((mod_cmd, on_exec, has_args, glob, index)) + + self.stack = list(reversed(stack)) + self.is_global = len(self.stack) == 1 + + + 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.callable: + if self.is_callable: args.append('function=%r' % self.function) if self.args: args.append('args=%r' % self.args) @@ -204,7 +219,35 @@ class Bind(object): cmds = self.commands[0] if cmdlen == 1 else self.commands args.append('command%s=%r' % ('s' if cmdlen-1 else '', cmds)) - return '<Bind(%s)>' % ', '.join(args) + self._repr_cache = '<Bind(%s)>' % ', '.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 = [] + for cmd in bind.commands: + if '%s' in cmd: + if len(args) > 1: + for arg in args: + cmd = cmd.replace('%s', arg, 1) + + elif len(args) == 1: + cmd = cmd.replace('%s', args[0]) + + uzbl.send(cmd) def bind(uzbl, glob, handler, *args, **kargs): @@ -279,93 +322,101 @@ def filter_bind(uzbl, bind_dict, bind): def match_and_exec(uzbl, bind, depth, keycmd): bind_dict = get_bind_dict(uzbl) - mode_cmd, on_exec, has_args, glob = bind.stack[depth] + (mod_cmd, on_exec, has_args, glob, more) = bind[depth] if has_args: if not keycmd.startswith(glob): - filter_bind(uzbl, bind_dict, bind) + if not mod_cmd: + filter_bind(uzbl, bind_dict, bind) + return False args = [keycmd[len(glob):],] elif keycmd != glob: - filter_bind(uzbl, bind_dict, bind) + if not mod_cmd: + filter_bind(uzbl, bind_dict, bind) + return False else: args = [] - execindex = len(bind.stack)-1 - if execindex == depth == 0: - uzbl.exec_handler(bind, *args) + if bind.is_global or (not more and depth == 0): + exec_bind(uzbl, bind, *args) if not has_args: - uzbl.clear_keycmd() + uzbl.clear_current() return True - elif depth != execindex: + elif more: if bind_dict['depth'] == depth: - bind_dict['filter'] = [bind,] + globalcmds = [cmd for cmd in bind_dict['binds'] if cmd.is_global] + bind_dict['filter'] = [bind,] + globalcmds bind_dict['args'] += args bind_dict['depth'] = depth + 1 - else: - if bind not in bind_dict['filter']: - bind_dict['filter'].append(bind) + elif bind not in bind_dict['filter']: + bind_dict['filter'].append(bind) set_stack_mode(uzbl, bind.prompts[depth]) return False args = bind_dict['args'] + args - uzbl.exec_handler(bind, *args) - if on_exec: - uzbl.set_mode() + exec_bind(uzbl, bind, *args) + uzbl.set_mode() + if not has_args: + uzbl.clear_current() return True def keycmd_update(uzbl, keylet): depth = get_stack_depth(uzbl) - keycmd = keylet.to_string() + keycmd = keylet.get_keycmd() for bind in get_filtered_binds(uzbl): - t = bind.stack[depth] + t = bind[depth] if t[MOD_CMD] or t[ON_EXEC]: continue - match_and_exec(uzbl, bind, depth, keycmd) + if match_and_exec(uzbl, bind, depth, keycmd): + return def keycmd_exec(uzbl, keylet): depth = get_stack_depth(uzbl) - keycmd = keylet.to_string() + keycmd = keylet.get_keycmd() for bind in get_filtered_binds(uzbl): - t = bind.stack[depth] + t = bind[depth] if t[MOD_CMD] or not t[ON_EXEC]: continue - match_and_exec(uzbl, bind, depth, keycmd) + if match_and_exec(uzbl, bind, depth, keycmd): + return uzbl.clear_keycmd() def modcmd_update(uzbl, keylet): depth = get_stack_depth(uzbl) - keycmd = keylet.to_string() + keycmd = keylet.get_modcmd() for bind in get_filtered_binds(uzbl): - t = bind.stack[depth] + t = bind[depth] if not t[MOD_CMD] or t[ON_EXEC]: continue - match_and_exec(uzbl, bind, depth, keycmd) + if match_and_exec(uzbl, bind, depth, keycmd): + return def modcmd_exec(uzbl, keylet): depth = get_stack_depth(uzbl) - keycmd = keylet.to_string() + keycmd = keylet.get_modcmd() for bind in get_filtered_binds(uzbl): - t = bind.stack[depth] + t = bind[depth] if not t[MOD_CMD] or not t[ON_EXEC]: continue - match_and_exec(uzbl, bind, depth, keycmd) + if match_and_exec(uzbl, bind, depth, keycmd): + return uzbl.clear_modcmd() def init(uzbl): diff --git a/examples/data/uzbl/scripts/plugins/config.py b/examples/data/uzbl/plugins/config.py index 22803b4..22803b4 100644 --- a/examples/data/uzbl/scripts/plugins/config.py +++ b/examples/data/uzbl/plugins/config.py diff --git a/examples/data/uzbl/plugins/keycmd.py b/examples/data/uzbl/plugins/keycmd.py new file mode 100644 index 0000000..109bb38 --- /dev/null +++ b/examples/data/uzbl/plugins/keycmd.py @@ -0,0 +1,395 @@ +import re + +# Map these functions/variables in the plugins namespace to the uzbl object. +__export__ = ['clear_keycmd', 'set_keycmd', 'set_cursor_pos', 'get_keylet', + 'clear_current', 'clear_modcmd'] + +# Hold the keylets. +UZBLS = {} + +# Simple key names map. +SIMPLEKEYS = { + 'Control': 'Ctrl', + 'ISO_Left_Tab': 'Shift-Tab', + 'space':'Space', +} + +# Keycmd format which includes the markup for the cursor. +KEYCMD_FORMAT = "%s<span @cursor_style>%s</span>%s" + + +def uzbl_escape(str): + '''Prevent outgoing keycmd values from expanding inside the + status_format.''' + + if not str: + return '' + + for char in ['\\', '@']: + if char in str: + str = str.replace(char, '\\'+char) + + return "@[%s]@" % str + + +class Keylet(object): + '''Small per-instance object that tracks all the keys held and characters + typed.''' + + def __init__(self): + # Modcmd tracking + self.held = [] + self.modcmd = '' + self.is_modcmd = False + + # Keycmd tracking + self.keycmd = '' + self.cursor = 0 + + # Keylet string repr cache. + self._repr_cache = None + + + 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 ''.join(['<%s>' % key for key in self.held]) + self.modcmd + + + def __repr__(self): + '''Return a string representation of the keylet.''' + + if self._repr_cache: + return self._repr_cache + + l = [] + if self.is_modcmd: + l.append('modcmd=%r' % self.get_modcmd()) + + elif self.held: + l.append('held=%r' % ''.join(['<%s>'%key for key in self.held])) + + if self.keycmd: + l.append('keycmd=%r' % self.get_keycmd()) + + self._repr_cache = '<Keylet(%s)>' % ', '.join(l) + return self._repr_cache + + +def make_simple(key): + '''Make some obscure names for some keys friendlier.''' + + # Remove left-right discrimination. + if key.endswith('_L') or key.endswith('_R'): + key = key[:-2] + + if key in SIMPLEKEYS: + key = SIMPLEKEYS[key] + + return key + + +def add_instance(uzbl, *args): + '''Create the Keylet object for this uzbl instance.''' + + UZBLS[uzbl] = Keylet() + + +def del_instance(uzbl, *args): + '''Delete the Keylet object for this uzbl instance.''' + + if uzbl in UZBLS: + del UZBLS[uzbl] + + +def get_keylet(uzbl): + '''Return the corresponding keylet for this uzbl instance.''' + + # Startup events are not correctly captured and sent over the uzbl socket + # yet so this line is needed because the INSTANCE_START event is lost. + if uzbl not in UZBLS: + add_instance(uzbl) + + keylet = UZBLS[uzbl] + keylet._repr_cache = False + return keylet + + +def clear_keycmd(uzbl): + '''Clear the keycmd for this uzbl instance.''' + + k = get_keylet(uzbl) + k.keycmd = '' + k.cursor = 0 + k._repr_cache = False + config = uzbl.get_config() + if 'keycmd' not in config or config['keycmd'] != '': + uzbl.set('keycmd', '') + uzbl.send('update_gui') + + uzbl.event('KEYCMD_CLEAR') + + +def clear_modcmd(uzbl, clear_held=False): + '''Clear the modcmd for this uzbl instance.''' + + k = get_keylet(uzbl) + k.modcmd = '' + k.is_modcmd = False + k._repr_cache = False + if clear_held: + k.held = [] + + config = uzbl.get_config() + if 'modcmd' not in config or config['modcmd'] != '': + uzbl.set('modcmd', '') + uzbl.send('update_gui') + + uzbl.event('MODCMD_CLEAR') + + +def clear_current(uzbl): + '''Clear the modcmd if is_modcmd else clear keycmd.''' + + k = get_keylet(uzbl) + if k.is_modcmd: + clear_modcmd(uzbl) + + else: + clear_keycmd(uzbl) + + +def focus_changed(uzbl, *args): + '''Focus to the uzbl instance has now been lost which means all currently + held keys in the held list will not get a KEY_RELEASE event so clear the + entire held list.''' + + clear_modcmd(uzbl, clear_held=True) + + +def update_event(uzbl, k, execute=True): + '''Raise keycmd & modcmd update events.''' + + config = uzbl.get_config() + keycmd, modcmd = k.get_keycmd(), k.get_modcmd() + + if k.is_modcmd: + uzbl.event('MODCMD_UPDATE', k) + + else: + uzbl.event('KEYCMD_UPDATE', k) + + if 'modcmd_updates' not in config or config['modcmd_updates'] == '1': + new_modcmd = k.get_modcmd() + if not new_modcmd or new_modcmd == modcmd: + uzbl.set('modcmd', uzbl_escape(new_modcmd)) + + if 'keycmd_events' in config and config['keycmd_events'] != '1': + return uzbl.send('update_gui') + + new_keycmd = k.get_keycmd() + if not new_keycmd or new_keycmd != keycmd: + uzbl.set('keycmd', '') + return uzbl.send('update_gui') + + + # 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:]] + uzbl.set('keycmd', KEYCMD_FORMAT % tuple(map(uzbl_escape, chunks))) + uzbl.send('update_gui') + + +def inject_char(str, index, char): + '''Inject character into string at at given index.''' + + assert len(char) == 1 + return "%s%s%s" % (str[:index], char, str[index:]) + + +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) + 3. In non-modcmd mode: + a. append char to keycmd + 4. If not in modcmd mode and a modkey was pressed set modcmd mode. + 5. If in modcmd mode the pressed key is added to the held keys list. + 6. Keycmd is updated and events raised if anything is changed.''' + + if key.startswith('Shift_'): + return + + if len(key) > 1: + key = make_simple(key) + + k = get_keylet(uzbl) + if key == 'Space' and not k.held and k.keycmd: + k.keycmd = inject_char(k.keycmd, k.cursor, ' ') + k.cursor += 1 + + elif not k.held and len(key) == 1: + config = uzbl.get_config() + if 'keycmd_events' not in config or config['keycmd_events'] == '1': + k.keycmd = inject_char(k.keycmd, k.cursor, key) + k.cursor += 1 + + elif k.keycmd: + k.keycmd = '' + k.cursor = 0 + + elif len(key) > 1: + k.is_modcmd = True + if key == 'Shift-Tab' and 'Tab' in k.held: + k.held.remove('Tab') + + if key not in k.held: + k.held.append(key) + k.held.sort() + + else: + k.is_modcmd = True + k.modcmd += key + + update_event(uzbl, k) + + +def key_release(uzbl, key): + '''Respond to KEY_RELEASE event. Things done by this function include: + + 1. Remove the key from the keylet held list. + 2. If in a mod-command then raise a MODCMD_EXEC. + 3. Check if any modkey is held, if so set modcmd mode. + 4. Update the keycmd uzbl variable if anything changed.''' + + if len(key) > 1: + key = make_simple(key) + + k = get_keylet(uzbl) + if key in ['Shift', 'Tab'] and 'Shift-Tab' in k.held: + key = 'Shift-Tab' + + elif key in ['Shift', 'Alt'] and 'Meta' in k.held: + key = 'Meta' + + if key in k.held: + if k.is_modcmd: + uzbl.event('MODCMD_EXEC', k) + + k.held.remove(key) + #k.is_modcmd = False + #k.modcmd = '' + #update_event(uzbl, k) + clear_modcmd(uzbl) + + +def set_keycmd(uzbl, keycmd): + '''Allow setting of the keycmd externally.''' + + k = get_keylet(uzbl) + k.keycmd = keycmd + k._repr_cache = None + k.cursor = len(keycmd) + update_event(uzbl, k, False) + + +def keycmd_strip_word(uzbl, sep): + ''' Removes the last word from the keycmd, similar to readline ^W ''' + + sep = sep or ' ' + k = get_keylet(uzbl) + if not k.keycmd: + return + + head, tail = k.keycmd[:k.cursor].rstrip(sep), k.keycmd[k.cursor:] + rfind = head.rfind(sep) + head = head[:rfind] if rfind + 1 else '' + k.keycmd = head + tail + k.cursor = len(head) + update_event(uzbl, k, False) + + +def keycmd_backspace(uzbl, *args): + '''Removes the character at the cursor position in the keycmd.''' + + k = get_keylet(uzbl) + if not k.keycmd: + return + + k.keycmd = k.keycmd[:k.cursor-1] + k.keycmd[k.cursor:] + k.cursor -= 1 + update_event(uzbl, k, False) + + +def keycmd_delete(uzbl, *args): + '''Removes the character after the cursor position in the keycmd.''' + + k = get_keylet(uzbl) + if not k.keycmd: + return + + k.keycmd = k.keycmd[:k.cursor] + k.keycmd[k.cursor+1:] + update_event(uzbl, k, False) + + +def keycmd_exec_current(uzbl, *args): + '''Raise a KEYCMD_EXEC with the current keylet and then clear the + keycmd.''' + + k = get_keylet(uzbl) + uzbl.event('KEYCMD_EXEC', k) + 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 = get_keylet(uzbl) + 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, k, False) + + +def init(uzbl): + '''Connect handlers to uzbl events.''' + + connects = {'INSTANCE_START': add_instance, + 'INSTANCE_EXIT': del_instance, + 'KEY_PRESS': key_press, + 'KEY_RELEASE': key_release, + 'SET_KEYCMD': set_keycmd, + 'KEYCMD_STRIP_WORD': keycmd_strip_word, + 'KEYCMD_BACKSPACE': keycmd_backspace, + 'KEYCMD_DELETE': keycmd_delete, + 'KEYCMD_EXEC_CURRENT': keycmd_exec_current, + 'SET_CURSOR_POS': set_cursor_pos, + 'FOCUS_LOST': focus_changed, + 'FOCUS_GAINED': focus_changed} + + uzbl.connect_dict(connects) diff --git a/examples/data/uzbl/scripts/plugins/mode.py b/examples/data/uzbl/plugins/mode.py index ad0d9a8..ad0d9a8 100644 --- a/examples/data/uzbl/scripts/plugins/mode.py +++ b/examples/data/uzbl/plugins/mode.py diff --git a/examples/data/uzbl/scripts/plugins/on_event.py b/examples/data/uzbl/plugins/on_event.py index 242f9b0..242f9b0 100644 --- a/examples/data/uzbl/scripts/plugins/on_event.py +++ b/examples/data/uzbl/plugins/on_event.py diff --git a/examples/data/uzbl/scripts/plugins/plugin_template.py b/examples/data/uzbl/plugins/plugin_template.py index 03cb748..03cb748 100644 --- a/examples/data/uzbl/scripts/plugins/plugin_template.py +++ b/examples/data/uzbl/plugins/plugin_template.py diff --git a/examples/data/uzbl/scripts/plugins/progress_bar.py b/examples/data/uzbl/plugins/progress_bar.py index b6fcb1b..b6fcb1b 100644 --- a/examples/data/uzbl/scripts/plugins/progress_bar.py +++ b/examples/data/uzbl/plugins/progress_bar.py diff --git a/examples/data/uzbl/scripts/event_manager.py b/examples/data/uzbl/scripts/event_manager.py index 2e84ded..391fb84 100755 --- a/examples/data/uzbl/scripts/event_manager.py +++ b/examples/data/uzbl/scripts/event_manager.py @@ -24,30 +24,19 @@ E V E N T _ M A N A G E R . P Y Event manager for uzbl written in python. -Usage -===== - - uzbl | $XDG_DATA_HOME/uzbl/scripts/event_manager.py - -Todo -==== - - - Command line options including supplying a list of plugins to load or not - load (default is load all plugins in the plugin_dir). - - Spell checking. - - ''' import imp import os import sys -import select import re import types import socket import pprint import time +import atexit +from select import select +from signal import signal, SIGTERM from optparse import OptionParser from traceback import print_exc @@ -69,13 +58,21 @@ def xdghome(key, default): # Setup xdg paths. DATA_DIR = os.path.join(xdghome('DATA', '.local/share/'), 'uzbl/') +CACHE_DIR = os.path.join(xdghome('CACHE', '.cache/'), 'uzbl/') # Config dict (NOT the same as the uzbl.config). config = { - 'verbose': False, - 'plugin_dir': "$XDG_DATA_HOME/uzbl/scripts/plugins/", - 'plugins_load': [], - 'plugins_ignore': [], + 'verbose': False, + 'daemon_mode': True, + + 'plugins_load': [], + 'plugins_ignore': [], + + 'plugin_dirs': [os.path.join(DATA_DIR, 'plugins/'), + '/usr/local/share/uzbl/examples/data/uzbl/plugins/'], + + 'server_socket': os.path.join(CACHE_DIR, 'event_daemon'), + 'pid_file': os.path.join(CACHE_DIR, 'event_daemon.pid'), } @@ -122,223 +119,265 @@ def isiterable(obj): return hasattr(obj, "__iter__") -class PluginManager(dict): - def __init__(self): +def find_plugins(plugin_dirs): + '''Find all event manager plugins in the plugin dirs and return a + dictionary of {'plugin-name.py': '/full/path/to/plugin-name.py', ...}''' - plugin_dir = os.path.expandvars(config['plugin_dir']) - self.plugin_dir = os.path.realpath(plugin_dir) - if not os.path.exists(self.plugin_dir): - os.makedirs(self.plugin_dir) + plugins = {} - self.load_plugins() + for plugin_dir in plugin_dirs: + plugin_dir = os.path.realpath(os.path.expandvars(plugin_dir)) + if not os.path.isdir(plugin_dir): + continue + for file in os.listdir(plugin_dir): + if not file.lower().endswith('.py'): + continue - def _find_all_plugins(self): - '''Find all python scripts in plugin dir and return a list of - locations and imp moduleinfo's.''' + path = os.path.join(plugin_dir, file) + if not os.path.isfile(path): + continue - dirlist = os.listdir(self.plugin_dir) - pythonfiles = filter(lambda s: s.endswith('.py'), dirlist) + if file not in plugins: + plugins[file] = plugin_dir - plugins = [] + return plugins - for filename in pythonfiles: - plugins.append(filename[:-3]) - return plugins +def load_plugins(plugin_dirs, load=[], ignore=[]): + '''Load event manager plugins found in the plugin_dirs.''' + # Find the plugins in the plugin_dirs. + found = find_plugins(plugin_dirs) - def _unload_plugin(self, name, remove_pyc=True): - '''Unload specific plugin and remove all waste in sys.modules + if load: + # Ignore anything not in the load list. + for plugin in found.keys(): + if plugin not in load: + del found[plugin] - Notice: manual manipulation of sys.modules is very un-pythonic but I - see no other way to make sure you have 100% unloaded the module. Also - this allows us to implement a reload plugins function.''' + if ignore: + # Ignore anything in the ignore list. + for plugin in found.keys(): + if plugin in ignore: + del found[plugin] - allmodules = sys.modules.keys() - allrefs = filter(lambda s: s.startswith("%s." % name), allmodules) + # Print plugin list to be loaded. + pprint.pprint(found) - for ref in allrefs: - del sys.modules[ref] + loaded = {} + # Load all found plugins into the loaded dict. + for (filename, dir) in found.items(): + name = filename[:-3] + info = imp.find_module(name, [dir,]) + plugin = imp.load_module(name, *info) + loaded[(dir, filename)] = plugin - if name in sys.modules.keys(): - del sys.modules[name] + return loaded - if name in self: - del self[name] - if remove_pyc: - pyc = os.path.join(self.plugin_dir, '%s.pyc' % name) - if os.path.exists(pyc): - os.remove(pyc) +def daemonize(): + '''Daemonize the process using the Stevens' double-fork magic.''' + try: + if os.fork(): + os._exit(0) - def load_plugins(self): + except OSError: + print_exc() + sys.stderr.write("fork #1 failed") + sys.exit(1) - if config['plugins_load']: - pluginlist = config['plugins_load'] + os.chdir('/') + os.setsid() + os.umask(0) - else: - pluginlist = self._find_all_plugins() - for name in config['plugins_ignore']: - if name in pluginlist: - pluginlist.remove(name) + try: + if os.fork(): + os._exit(0) - for name in pluginlist: - # Make sure the plugin isn't already loaded. - self._unload_plugin(name) + except OSError: + print_exc() + sys.stderr.write("fork #2 failed") + sys.exit(1) - try: - moduleinfo = imp.find_module(name, [self.plugin_dir,]) - plugin = imp.load_module(name, *moduleinfo) - self[name] = plugin + sys.stdout.flush() + sys.stderr.flush() - except: - raise + devnull = '/dev/null' + stdin = file(devnull, 'r') + stdout = file(devnull, 'a+') + stderr = file(devnull, 'a+', 0) - if self.keys(): - echo("loaded plugin(s): %s" % ', '.join(self.keys())) + os.dup2(stdin.fileno(), sys.stdin.fileno()) + os.dup2(stdout.fileno(), sys.stdout.fileno()) + os.dup2(stderr.fileno(), sys.stderr.fileno()) - def reload_plugins(self): - '''Unload all loaded plugins then run load_plugins() again. +def make_dirs(path): + '''Make all basedirs recursively as required.''' - IMPORTANT: It is crucial that the event handler be deleted if you - are going to unload any modules because there is now way to track - which module created wich handler.''' + dirname = os.path.dirname(path) + if not os.path.isdir(dirname): + os.makedirs(dirname) - for plugin in self.keys(): - self._unload_plugin(plugin) - self.load_plugins() +def make_pid_file(pid_file): + '''Make pid file at given pid_file location.''' + make_dirs(pid_file) + file = open(pid_file, 'w') + file.write('%d' % os.getpid()) + file.close() -class CallPrepender(object): - '''Execution argument modifier. Takes (arg, function) then modifies the - function call: - -> function(*args, **kargs) -> function(arg, *args, **kargs) ->''' +def del_pid_file(pid_file): + '''Delete pid file at given pid_file location.''' - def __init__(self, uzbl, function): - self.function = function - self.uzbl = uzbl + if os.path.isfile(pid_file): + os.remove(pid_file) - def call(self, *args, **kargs): - return self.function(self.uzbl, *args, **kargs) +def get_pid(pid_file): + '''Read pid from pid_file.''' -class Handler(object): + try: + file = open(pid_file, 'r') + strpid = file.read() + file.close() + pid = int(strpid.strip()) + return pid - nexthid = counter().next + except: + print_exc() + return None - def __init__(self, event, handler, *args, **kargs): - self.callable = iscallable(handler) - if self.callable: - self.function = handler - self.args = args - self.kargs = kargs - elif kargs: - raise ArgumentError("cannot supply kargs with a uzbl command") +def pid_running(pid): + '''Returns True if a process with the given pid is running.''' - elif isiterable(handler): - self.commands = handler + try: + os.kill(pid, 0) - else: - self.commands = [handler,] + list(args) + except OSError: + return False + + else: + return True + +def term_process(pid): + '''Send a SIGTERM signal to the process with the given pid.''' + + if not pid_running(pid): + return False + + os.kill(pid, SIGTERM) + + start = time.time() + while True: + if not pid_running(pid): + return True + + if time.time() - start > 5: + raise OSError('failed to stop process with pid: %d' % pid) + + time.sleep(0.25) + + +def prepender(function, *pre_args): + '''Creates a wrapper around a callable object injecting a list of + arguments before the called arguments.''' + + locals = (function, pre_args) + def _prepender(*args, **kargs): + (function, pre_args) = locals + return function(*(pre_args + args), **kargs) + + return _prepender + + +class EventHandler(object): + + nexthid = counter().next + + def __init__(self, event, handler, *args, **kargs): + if not iscallable(handler): + raise ArgumentError("EventHandler object requires a callable " + "object function for the handler argument not: %r" % handler) + + self.function = handler + self.args = args + self.kargs = kargs self.event = event self.hid = self.nexthid() def __repr__(self): - args = ["event=%s" % self.event, "hid=%d" % self.hid] - - if self.callable: - args.append("function=%r" % self.function) - if self.args: - args.append("args=%r" % self.args) + args = ["event=%s" % self.event, "hid=%d" % self.hid, + "function=%r" % self.function] - if self.kargs: - args.append("kargs=%r" % self.kargs) + if self.args: + args.append("args=%r" % self.args) - 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)) + if self.kargs: + args.append("kargs=%r" % self.kargs) return "<EventHandler(%s)>" % ', '.join(args) class UzblInstance(object): - '''Event manager for a uzbl instance.''' - - # Singleton plugin manager. - plugins = None - - def __init__(self): - '''Initialise event manager.''' + def __init__(self, parent, client_socket): - # Hold functions exported by plugins. + # Internal variables. self._exports = {} - self._running = None - self._buffer = '' - self._handlers = {} + self._parent = parent + self._client_socket = client_socket - # Variables needed for fifo & socket communication with uzbl. - self.uzbl_fifo = None - self.uzbl_socket = None - self._fifo_cmd_queue = [] - self._socket_cmd_queue = [] - self._socket = None - self.send = self._send_socket - - if not self.plugins: - self.plugins = PluginManager() + self.buffer = '' - # Call the init() function in every plugin which then setup their - # respective hooks (event handlers, binds or timers). + # Call the init() function in every plugin. Inside the init function + # is where the plugins insert the hooks into the event system. self._init_plugins() - def __getattribute__(self, name): + def __getattribute__(self, attr): '''Expose any exported functions before class functions.''' - if not name.startswith('_'): + if not attr.startswith('_'): exports = object.__getattribute__(self, '_exports') - if name in exports: - return exports[name] + if attr in exports: + return exports[attr] - return object.__getattribute__(self, name) + return object.__getattribute__(self, attr) def _init_plugins(self): '''Call the init() function in every plugin and expose all exposable functions in the plugins root namespace.''' + plugins = self._parent['plugins'] + # Map all plugin exports - for (name, plugin) in self.plugins.items(): + for (name, plugin) in plugins.items(): if not hasattr(plugin, '__export__'): continue for export in plugin.__export__: if export in self._exports: - orig = self._exports[export] - raise KeyError("already exported attribute: %r" % export) + raise KeyError("conflicting export: %r" % export) obj = getattr(plugin, export) if iscallable(obj): - # Wrap the function in the CallPrepender object to make - # the exposed functions act like instance methods. - obj = CallPrepender(self, obj).call + obj = prepender(obj, self) self._exports[export] = obj echo("exposed attribute(s): %s" % ', '.join(self._exports.keys())) # Now call the init function in all plugins. - for (name, plugin) in self.plugins.items(): + for (name, plugin) in plugins.items(): try: plugin.init(self) @@ -347,82 +386,15 @@ class UzblInstance(object): raise - def _init_uzbl_socket(self, uzbl_socket=None, timeout=None): - '''Store socket location and open socket connection to uzbl socket.''' - - if uzbl_socket is None: - uzbl_socket = self.uzbl_socket - - if not uzbl_socket: - error("no socket location.") - return - - if not os.path.exists(uzbl_socket): - if timeout is None: - error("uzbl socket doesn't exist: %r" % uzbl_socket) - return - - waitlimit = time.time() + timeout - echo("waiting for uzbl socket: %r" % uzbl_socket) - while not os.path.exists(uzbl_socket): - time.sleep(0.25) - if time.time() > waitlimit: - error("timed out waiting for socket: %r" % uzbl_socket) - return - - self.uzbl_socket = uzbl_socket - sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - sock.connect(self.uzbl_socket) - self._socket = sock - - - def _close_socket(self): - '''Close the socket used for communication with the uzbl instance. - This function is normally called upon receiving the INSTANCE_EXIT - event.''' - - if self._socket: - self._socket.close() - - self.uzbl_socket = self._socket = None - - - def _flush(self): - '''Flush messages from the outgoing queue to the uzbl instance.''' - - if len(self._fifo_cmd_queue) and self.uzbl_fifo: - if os.path.exists(self.uzbl_fifo): - h = open(self.uzbl_fifo, 'w') - while len(self._fifo_cmd_queue): - msg = self._fifo_cmd_queue.pop(0) - print '<-- %s' % msg - h.write(("%s\n" % msg).encode('utf-8')) - - h.close() - - if len(self._socket_cmd_queue) and self.uzbl_socket: - if not self._socket and os.path.exists(self.uzbl_socket): - self._init_uzbl_socket() - - if self._socket: - while len(self._socket_cmd_queue): - msg = self._socket_cmd_queue.pop(0) - print '<-- %s' % msg - self._socket.send(("%s\n" % msg).encode('utf-8')) - - - def _send_fifo(self, msg): - '''Send a command to the uzbl instance via the fifo socket.''' - - self._fifo_cmd_queue.append(msg) - self._flush() - - - def _send_socket(self, msg): + def send(self, msg): '''Send a command to the uzbl instance via the socket file.''' - self._socket_cmd_queue.append(msg) - self._flush() + if self._client_socket: + print '<-- %s' % msg + self._client_socket.send(("%s\n" % msg).encode('utf-8')) + + else: + print '!-- %s' % msg def connect(self, event, handler, *args, **kargs): @@ -432,11 +404,9 @@ class UzblInstance(object): if event not in self._handlers.keys(): self._handlers[event] = [] - handler = Handler(event, handler, *args, **kargs) - self._handlers[event].append(handler) - - print handler - return handler + handlerobj = EventHandler(event, handler, *args, **kargs) + self._handlers[event].append(handlerobj) + print handlerobj def connect_dict(self, connect_dict): @@ -477,216 +447,362 @@ class UzblInstance(object): echo('unable to find & remove handler: %r' % handler) - def listen_from_fd(self, fd): - '''Polls for event messages from fd.''' + def exec_handler(self, handler, *args, **kargs): + '''Execute event handler function.''' + + args += handler.args + kargs = dict(handler.kargs.items()+kargs.items()) + handler.function(self, *args, **kargs) + + + def event(self, event, *args, **kargs): + '''Raise a custom event.''' + + # Silence _printing_ of geo events while debugging. + if event != "GEOMETRY_CHANGED": + print "--> %s %s %s" % (event, args, '' if not kargs else kargs) + + if event not in self._handlers: + return + + for handler in self._handlers[event]: + try: + self.exec_handler(handler, *args, **kargs) + + except: + print_exc() + + + def close(self): + '''Close the client socket and clean up.''' try: - self._running = True - while self._running: - if select.select([fd,], [], [], 1)[0]: - self.read_from_fd(fd) - continue + self._client_socket.close() + + except: + pass - self._flush() + for (name, plugin) in self._parent['plugins'].items(): + if hasattr(plugin, 'cleanup'): + plugin.cleanup(self) - except KeyboardInterrupt: - print + del self._exports + del self._handlers + del self._client_socket + + +class UzblEventDaemon(dict): + def __init__(self): + + # Init variables and dict keys. + dict.__init__(self, {'uzbls': {}}) + self.running = None + self.server_socket = None + self.socket_location = None + + # Register that the event daemon server has started by creating the + # pid file. + make_pid_file(config['pid_file']) + + # Register a function to clean up the socket and pid file on exit. + atexit.register(self.quit) + + # Make SIGTERM act orderly. + signal(SIGTERM, lambda signum, stack_frame: sys.exit(1)) + + # Load plugins, first-build of the plugins may be a costly operation. + self['plugins'] = load_plugins(config['plugin_dirs'], + config['plugins_load'], config['plugins_ignore']) + + + def _create_server_socket(self): + '''Create the event manager daemon socket for uzbl instance duplex + communication.''' + + server_socket = config['server_socket'] + server_socket = os.path.realpath(os.path.expandvars(server_socket)) + self.socket_location = server_socket + + # Delete socket if it exists. + if os.path.exists(server_socket): + os.remove(server_socket) + + self.server_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self.server_socket.bind(server_socket) + self.server_socket.listen(5) + + + def _close_server_socket(self): + '''Close and delete the server socket.''' + + try: + self.server_socket.close() + self.server_socket = None + + if os.path.exists(self.socket_location): + os.remove(self.socket_location) except: - #print_exc() - raise + pass - def read_from_fd(self, fd): - '''Reads event messages from a single fd.''' + def run(self): + '''Main event daemon loop.''' - raw = fd.readline() - if not raw: - # Read null byte (i.e. uzbl closed). - self._running = False - return + if config['daemon_mode']: + echo('entering daemon mode.') + daemonize() + # The pid has changed so update the pid file. + make_pid_file(config['pid_file']) - msg = raw.strip().split(' ', 3) + # Create event daemon socket. + self._create_server_socket() + echo('listening on: %s' % self.socket_location) - if not msg or msg[0] != "EVENT": - # Not an event message - print "---", raw.rstrip() - return + # Now listen for incoming connections and or data. + self.listen() - event, args = msg[1], msg[3] - self.handle_event(event, args) + # Clean up. + self.quit() - def listen_from_uzbl_socket(self, uzbl_socket): - '''Polls for event messages from a single uzbl socket.''' + def listen(self): + '''Accept incoming connections and constantly poll instance sockets + for incoming data.''' - self._init_uzbl_socket(uzbl_socket, 10) + self.running = True + while self.running: - if not self._socket: - error("failed to init socket: %r" % uzbl_socket) - return + sockets = [self.server_socket,] + self['uzbls'].keys() + + read, _, error = select(sockets, [], sockets, 1) + + if self.server_socket in read: + self.accept_connection() + read.remove(self.server_socket) + + for client in read: + self.read_socket(client) + + for client in error: + error('Unknown error on socket: %r' % client) + self.close_connection(client) + + + def read_socket(self, client): + '''Read data from an instance socket and pass to the uzbl objects + event handler function.''' - self._flush() try: - self._running = True - while self._running: - if select.select([self._socket], [], [], 1): - self.read_from_uzbl_socket() - continue + uzbl = self['uzbls'][client] + try: + raw = unicode(client.recv(8192), 'utf-8', 'ignore') - self._flush() + except: + print_exc() + raw = None - except KeyboardInterrupt: - print + if not raw: + # Read null byte, close socket. + return self.close_connection(client) + + uzbl.buffer += raw + msgs = uzbl.buffer.split('\n') + uzbl.buffer = msgs.pop() + + for msg in msgs: + self.parse_msg(uzbl, msg) except: - #print_exc() raise - def read_from_uzbl_socket(self): - '''Reads event messages from a uzbl socket.''' + def parse_msg(self, uzbl, msg): + '''Parse an incoming msg from a uzbl instance. All non-event messages + will be printed here and not be passed to the uzbl instance event + handler function.''' - raw = unicode(self._socket.recv(8192), 'utf-8', 'ignore') - if not raw: - # Read null byte - self._running = False + msg = msg.strip() + if not msg: return - self._buffer += raw - msgs = self._buffer.split("\n") - self._buffer = msgs.pop() + cmd = _RE_FINDSPACES.split(msg, 3) + if not cmd or cmd[0] != 'EVENT': + # Not an event message. + print '---', msg + return - for msg in msgs: - msg = msg.rstrip() - if not msg: - continue + if len(cmd) < 4: + cmd.append('') - cmd = _RE_FINDSPACES.split(msg, 3) - if not cmd or cmd[0] != "EVENT": - # Not an event message - print msg.rstrip() - continue + event, args = cmd[2], cmd[3] - if len(cmd) < 4: - cmd.append('') + try: + uzbl.event(event, args) - event, args = cmd[2], cmd[3] - try: - self.handle_event(event, args) + except: + print_exc() - except: - #print_exc() - raise + def accept_connection(self): + '''Accept incoming connection to the server socket.''' - def handle_event(self, event, args): - '''Handle uzbl events internally before dispatch.''' + client_socket = self.server_socket.accept()[0] - if event == 'FIFO_SET': - self.uzbl_fifo = args - self._flush() + uzbl = UzblInstance(self, client_socket) + self['uzbls'][client_socket] = uzbl - elif event == 'SOCKET_SET': - if not self.uzbl_socket or not self._socket: - self._init_uzbl_socket(args) - self._flush() - elif event == 'INSTANCE_EXIT': - self._close_socket() - self._running = False - for (name, plugin) in self.plugins.items(): - if hasattr(plugin, "cleanup"): - plugin.cleanup(uzbl) + def close_connection(self, client): + '''Clean up after instance close.''' - # Now handle the event "publically". - self.event(event, args) + try: + if client not in self['uzbls']: + return + uzbl = self['uzbls'][client] + uzbl.close() + del self['uzbls'][client] - def exec_handler(self, handler, *args, **kargs): - '''Execute either the handler function or send the handlers uzbl - commands via the socket.''' + except: + print_exc() - if handler.callable: - args = args + handler.args - kargs = dict(handler.kargs.items()+kargs.items()) - handler.function(uzbl, *args, **kargs) - else: - if kargs: - raise ArgumentError('cannot supply kargs for uzbl commands') + def quit(self): + '''Close all instance socket objects, server socket and delete the + pid file.''' - for command in handler.commands: - if '%s' in command: - if len(args) > 1: - for arg in args: - command = command.replace('%s', arg, 1) + echo('shutting down event manager.') - elif len(args) == 1: - command = command.replace('%s', args[0]) + for client in self['uzbls'].keys(): + self.close_connection(client) - uzbl.send(command) + echo('unlinking: %r' % self.socket_location) + self._close_server_socket() + echo('deleting pid file: %r' % config['pid_file']) + del_pid_file(config['pid_file']) - def event(self, event, *args, **kargs): - '''Raise a custom event.''' - # Silence _printing_ of geo events while still debugging. - if event != "GEOMETRY_CHANGED": - print "--> %s %s %s" % (event, args, '' if not kargs else kargs) +def stop(): + '''Stop the event manager daemon.''' - if event in self._handlers: - for handler in self._handlers[event]: - try: - self.exec_handler(handler, *args, **kargs) + pid_file = config['pid_file'] + if not os.path.isfile(pid_file): + return echo('no running daemon found.') - except: - #print_exc() - raise + echo('found pid file: %r' % pid_file) + pid = get_pid(pid_file) + if not pid_running(pid): + echo('no process with pid: %d' % pid) + return os.remove(pid_file) + echo("terminating process with pid: %d" % pid) + term_process(pid) + if os.path.isfile(pid_file): + os.remove(pid_file) -if __name__ == "__main__": - #uzbl = UzblInstance().listen_from_fd(sys.stdin) + echo('stopped event daemon.') + + +def start(): + '''Start the event manager daemon.''' + + pid_file = config['pid_file'] + if os.path.isfile(pid_file): + echo('found pid file: %r' % pid_file) + pid = get_pid(pid_file) + if pid_running(pid): + return echo('event daemon already started with pid: %d' % pid) + + echo('no process with pid: %d' % pid) + os.remove(pid_file) + + echo('starting event manager.') + UzblEventDaemon().run() + + +def restart(): + '''Restart the event manager daemon.''' + + echo('restarting event manager daemon.') + stop() + start() + + +def list_plugins(): + '''List all the plugins being loaded by the event daemon.''' - parser = OptionParser() - parser.add_option('-s', '--uzbl-socket', dest='socket', - action="store", metavar="SOCKET", - help="read event messages from uzbl socket.") + plugins = find_plugins(config['plugin_dirs']) + dirs = {} + for (plugin, dir) in plugins.items(): + if dir not in dirs: + dirs[dir] = [] + + dirs[dir].append(plugin) + + for (index, (dir, plugin_list)) in enumerate(sorted(dirs.items())): + if index: + print + + print "%s:" % dir + for plugin in sorted(plugin_list): + print " %s" % plugin + + +if __name__ == "__main__": + usage = "usage: %prog [options] {start|stop|restart|list}" + parser = OptionParser(usage=usage) parser.add_option('-v', '--verbose', dest='verbose', action="store_true", help="print verbose output.") - parser.add_option('-d', '--plugin-dir', dest='plugin_dir', action="store", - metavar="DIR", help="change plugin directory.") + parser.add_option('-d', '--plugin-dirs', dest='plugin_dirs', action="store", + metavar="DIRS", help="Specify plugin directories in the form of "\ + "'dir1:dir2:dir3'.") - parser.add_option('-p', '--load-plugins', dest="load", action="store", + parser.add_option('-l', '--load-plugins', dest="load", action="store", metavar="PLUGINS", help="comma separated list of plugins to load") parser.add_option('-i', '--ignore-plugins', dest="ignore", action="store", metavar="PLUGINS", help="comma separated list of plugins to ignore") - parser.add_option('-l', '--list-plugins', dest='list', action='store_true', - help="list all the plugins in the plugin dir.") + parser.add_option('-p', '--pid-file', dest='pid', action='store', + metavar='FILE', help="specify pid file location") + + parser.add_option('-s', '--server-socket', dest='socket', action='store', + metavar='SOCKET', help="specify the daemon socket location") + + parser.add_option('-n', '--no-daemon', dest="daemon", + action="store_true", help="don't enter daemon mode.") (options, args) = parser.parse_args() - if len(args): - for arg in args: - error("unknown argument: %r" % arg) + # init like {start|stop|..} daemon control section. + daemon_controls = {'start': start, 'stop': stop, 'restart': restart, + 'list': list_plugins} + + if len(args) == 1: + action = args[0] + if action not in daemon_controls: + error('unknown action: %r' % action) + sys.exit(1) + + elif len(args) > 1: + error("too many arguments: %r" % args) + sys.exit(1) - raise ArgumentError + else: + action = 'start' + # parse other flags & options. if options.verbose: config['verbose'] = True - if options.plugin_dir: - plugin_dir = os.path.expandvars(options.plugin_dir) - if not os.path.isdir(plugin_dir): - error("%r is not a directory" % plugin_dir) - sys.exit(1) - - config['plugin_dir'] = plugin_dir - echo("changed plugin dir: %r" % plugin_dir) + if options.plugin_dirs: + plugin_dirs = map(str.strip, options.plugin_dirs.split(':')) + config['plugin_dirs'] = plugin_dirs + echo("plugin search dirs: %r" % plugin_dirs) if options.load and options.ignore: error("you can't load and ignore at the same time.") @@ -708,21 +824,16 @@ if __name__ == "__main__": echo('ignoring plugin(s): %s' % ', '.join(plugins_ignore)) + if options.pid: + config['pid_file'] = options.pid + echo("pid file location: %r" % config['pid_file']) - if options.list: - plugin_dir = os.path.expandvars(config['plugin_dir']) - if not os.path.isdir(plugin_dir): - error("not a directory: %r" % plugin_dir) - sys.exit(1) + if options.socket: + config['server_socket'] = options.socket + echo("daemon socket location: %s" % config['server_socket']) - dirlist = filter(lambda p: p.endswith('.py'), os.listdir(plugin_dir)) - print ', '.join([p[:-3] for p in dirlist]) + if options.daemon: + config['daemon_mode'] = False - else: - uzbl = UzblInstance() - if options.socket: - echo("listen from uzbl socket: %r" % options.socket) - uzbl.listen_from_uzbl_socket(options.socket) - - else: - uzbl.listen_from_fd(sys.stdin) + # Now {start|stop|...} + daemon_controls[action]() diff --git a/examples/data/uzbl/scripts/plugins/keycmd.py b/examples/data/uzbl/scripts/plugins/keycmd.py deleted file mode 100644 index 3dd6f37..0000000 --- a/examples/data/uzbl/scripts/plugins/keycmd.py +++ /dev/null @@ -1,382 +0,0 @@ -import re - -# Map these functions/variables in the plugins namespace to the uzbl object. -__export__ = ['clear_keycmd', 'set_keycmd', 'set_cursor_pos', 'get_keylet'] - -# Regular expression compile cache. -_RE_CACHE = {} - -# Hold the keylets. -UZBLS = {} - -# Simple key names map. -_SIMPLEKEYS = { - 'Control': 'Ctrl', - 'ISO_Left_Tab': 'Shift-Tab', - 'space':'Space', -} - -# Keycmd format which includes the markup for the cursor. -KEYCMD_FORMAT = "%s<span @cursor_style>%s</span>%s" - - -def escape(str): - '''Prevent outgoing keycmd values from expanding inside the - status_format.''' - - if not str: - return '' - - for char in ['\\', '@']: - if char in str: - str = str.replace(char, '\\'+char) - - return "@[%s]@" % str - - -def get_regex(regex): - '''Compiling regular expressions is a very time consuming so return a - pre-compiled regex match object if possible.''' - - if regex not in _RE_CACHE: - _RE_CACHE[regex] = re.compile(regex).match - - return _RE_CACHE[regex] - - -class Keylet(object): - '''Small per-instance object that tracks all the keys held and characters - typed.''' - - def __init__(self): - self.cmd = '' - self.cursor = 0 - self.held = [] - - # to_string() string building cache. - self._to_string = None - - self.modcmd = False - self.wasmod = False - - def __repr__(self): - return '<Keycmd(%r)>' % self.to_string() - - - def to_string(self): - '''Return a string representation of the keys held and pressed that - have been recorded.''' - - if self._to_string is not None: - # Return cached keycmd string. - return self._to_string - - if not self.held: - self._to_string = self.cmd - - else: - self._to_string = ''.join(['<%s>' % key for key in self.held]) - if self.cmd: - self._to_string += self.cmd - - return self._to_string - - - def match(self, regex): - '''See if the keycmd string matches the given regex.''' - - return bool(get_regex(regex)(self.to_string())) - - -def make_simple(key): - '''Make some obscure names for some keys friendlier.''' - - # Remove left-right discrimination. - if key.endswith('_L') or key.endswith('_R'): - key = key[:-2] - - if key in _SIMPLEKEYS: - key = _SIMPLEKEYS[key] - - return key - - -def add_instance(uzbl, *args): - '''Create the Keylet object for this uzbl instance.''' - - UZBLS[uzbl] = Keylet() - - -def del_instance(uzbl, *args): - '''Delete the Keylet object for this uzbl instance.''' - - if uzbl in UZBLS: - del UZBLS[uzbl] - - -def get_keylet(uzbl): - '''Return the corresponding keylet for this uzbl instance.''' - - # Startup events are not correctly captured and sent over the uzbl socket - # yet so this line is needed because the INSTANCE_START event is lost. - if uzbl not in UZBLS: - add_instance(uzbl) - - keylet = UZBLS[uzbl] - keylet._to_string = None - return keylet - - -def clear_keycmd(uzbl): - '''Clear the keycmd for this uzbl instance.''' - - k = get_keylet(uzbl) - k.cmd = '' - k.cursor = 0 - k._to_string = None - - if k.modcmd: - k.wasmod = True - - k.modcmd = False - config = uzbl.get_config() - if 'keycmd' not in config or config['keycmd'] != '': - config['keycmd'] = '' - - uzbl.event('KEYCMD_CLEAR') - - -def update_event(uzbl, k): - '''Raise keycmd & modcmd update events.''' - - config = uzbl.get_config() - if k.modcmd: - keycmd = k.to_string() - uzbl.event('MODCMD_UPDATE', k) - if keycmd != k.to_string(): - return - - if 'modcmd_updates' in config and config['modcmd_updates'] != '1': - return - - return uzbl.set('keycmd', escape(keycmd)) - - if 'keycmd_events' in config and config['keycmd_events'] != '1': - return - - keycmd = k.cmd - uzbl.event('KEYCMD_UPDATE', k) - if keycmd != k.cmd: - return - - if not k.cmd: - return uzbl.set('keycmd', '') - - # Generate the pango markup for the cursor in the keycmd. - if k.cursor < len(k.cmd): - cursor = k.cmd[k.cursor] - - else: - cursor = ' ' - - chunks = map(escape, [k.cmd[:k.cursor], cursor, k.cmd[k.cursor+1:]]) - uzbl.set('keycmd', KEYCMD_FORMAT % tuple(chunks)) - - -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. Re-enable modcmd var if the user presses another key with at least one - modkey still held from the previous modcmd (I.e. <Ctrl>+t, clear & - <Ctrl>+o without having to re-press <Ctrl>) - 3. In non-modcmd mode: - a. BackSpace deletes the character before the cursor position. - b. Delete deletes the character at the cursor position. - c. End moves the cursor to the end of the keycmd. - d. Home moves the cursor to the beginning of the keycmd. - e. Return raises a KEYCMD_EXEC event then clears the keycmd. - f. Escape clears the keycmd. - 4. If keycmd and held keys are both empty/null and a modkey was pressed - set modcmd mode. - 5. If in modcmd mode only mod keys are added to the held keys list. - 6. Keycmd is updated and events raised if anything is changed.''' - - if key.startswith('Shift_'): - return - - if len(key) > 1: - key = make_simple(key) - - k = get_keylet(uzbl) - cmdmod = False - if k.held and k.wasmod: - k.modcmd = True - k.wasmod = False - cmdmod = True - - if k.cmd and key == 'Space': - k.cmd = "%s %s" % (k.cmd[:k.cursor], k.cmd[k.cursor:]) - k.cursor += 1 - cmdmod = True - - elif not k.modcmd and k.cmd and key in ['BackSpace', 'Delete']: - if key == 'BackSpace' and k.cursor > 0: - k.cursor -= 1 - k.cmd = k.cmd[:k.cursor] + k.cmd[k.cursor+1:] - - elif key == 'Delete': - cmd = k.cmd - k.cmd = k.cmd[:k.cursor] + k.cmd[k.cursor+1:] - if k.cmd != cmd: - cmdmod = True - - if not k.cmd: - clear_keycmd(uzbl) - - elif key == 'BackSpace': - cmdmod = True - - elif not k.modcmd and key == 'Return': - if k.cmd: - uzbl.event('KEYCMD_EXEC', k) - - clear_keycmd(uzbl) - - elif not k.modcmd and key == 'Escape': - clear_keycmd(uzbl) - - elif not k.modcmd and k.cmd and key == 'Left': - if k.cursor > 0: - k.cursor -= 1 - cmdmod = True - - elif not k.modcmd and k.cmd and key == 'Right': - if k.cursor < len(k.cmd): - k.cursor += 1 - cmdmod = True - - elif not k.modcmd and k.cmd and key == 'End': - if k.cursor != len(k.cmd): - k.cursor = len(k.cmd) - cmdmod = True - - elif not k.modcmd and k.cmd and key == 'Home': - if k.cursor: - k.cursor = 0 - cmdmod = True - - elif not k.held and not k.cmd and len(key) > 1: - k.modcmd = True - k.held.append(key) - cmdmod = True - - elif k.modcmd: - cmdmod = True - if len(key) > 1: - if key == 'Shift-Tab' and 'Tab' in k.held: - k.held.remove('Tab') - - if key not in k.held: - k.held.append(key) - k.held.sort() - - else: - k.cmd = "%s%s%s" % (k.cmd[:k.cursor], key, k.cmd[k.cursor:]) - k.cursor += 1 - - else: - config = uzbl.get_config() - if 'keycmd_events' not in config or config['keycmd_events'] == '1': - if len(key) == 1: - cmdmod = True - k.cmd = "%s%s%s" % (k.cmd[:k.cursor], key, k.cmd[k.cursor:]) - k.cursor += 1 - - elif k.cmd: - cmdmod = True - k.cmd = '' - k.cursor = 0 - - if cmdmod: - update_event(uzbl, k) - - -def key_release(uzbl, key): - '''Respond to KEY_RELEASE event. Things done by this function include: - - 1. Remove the key from the keylet held list. - 2. If the key removed was a mod key and it was in a mod-command then - raise a MODCMD_EXEC event then clear the keycmd. - 3. Stop trying to restore mod-command status with wasmod if both the - keycmd and held list are empty/null. - 4. Update the keycmd uzbl variable if anything changed.''' - - if len(key) > 1: - key = make_simple(key) - - k = get_keylet(uzbl) - - cmdmod = False - if key in ['Shift', 'Tab'] and 'Shift-Tab' in k.held: - key = 'Shift-Tab' - - elif key in ['Shift', 'Alt'] and 'Meta' in k.held: - key = 'Meta' - - if key in k.held: - if k.modcmd: - uzbl.event('MODCMD_EXEC', k) - - k.held.remove(key) - clear_keycmd(uzbl) - - if not k.held and not k.cmd and k.wasmod: - k.wasmod = False - - if cmdmod: - update_event(uzbl, k) - - -def set_keycmd(uzbl, keycmd): - '''Allow setting of the keycmd externally.''' - - k = get_keylet(uzbl) - k.wasmod = k.modcmd = False - k._to_string = None - k.cmd = keycmd - k.cursor = len(keycmd) - update_event(uzbl, k) - - -def set_cursor_pos(uzbl, index): - '''Allow setting of the cursor position externally. Supports negative - indexing.''' - - cursor = int(index.strip()) - k = get_keylet(uzbl) - - if cursor < 0: - cursor = len(k.cmd) + cursor - - if cursor < 0: - cursor = 0 - - if cursor > len(k.cmd): - cursor = len(k.cmd) - - k.cursor = cursor - update_event(uzbl, k) - - -def init(uzbl): - '''Connect handlers to uzbl events.''' - - connects = {'INSTANCE_START': add_instance, - 'INSTANCE_EXIT': del_instance, - 'KEY_PRESS': key_press, - 'KEY_RELEASE': key_release, - 'SET_KEYCMD': set_keycmd, - 'SET_CURSOR_POS': set_cursor_pos} - - uzbl.connect_dict(connects) |