diff options
author | keis <keijser@gmail.com> | 2010-08-20 14:58:03 +0200 |
---|---|---|
committer | keis <keijser@gmail.com> | 2010-08-20 14:58:03 +0200 |
commit | 814f96d9d9ee984bb7912cebf8e616bb61dafdcc (patch) | |
tree | 963ea62f599e48dbcb872eb9ff83921edbdde480 /examples | |
parent | 0d91f22c0eaa97d60752c1a7787f58f527db340e (diff) | |
parent | 9cc39cb5c9396be013b5dc2ba7e4b3eaa647e975 (diff) |
Merge branch 'master' of git://github.com/Dieterbe/uzbl into history
Conflicts:
examples/config/config
Diffstat (limited to 'examples')
-rw-r--r-- | examples/config/config | 188 | ||||
-rw-r--r-- | examples/data/plugins/bind.py | 63 | ||||
-rw-r--r-- | examples/data/plugins/cmd_expand.py | 6 | ||||
-rw-r--r-- | examples/data/plugins/completion.py | 129 | ||||
-rw-r--r-- | examples/data/plugins/config.py | 129 | ||||
-rw-r--r-- | examples/data/plugins/keycmd.py | 121 | ||||
-rw-r--r-- | examples/data/plugins/mode.py | 204 | ||||
-rw-r--r-- | examples/data/plugins/on_event.py | 69 | ||||
-rw-r--r-- | examples/data/plugins/on_set.py | 92 | ||||
-rw-r--r-- | examples/data/plugins/plugin_template.py | 76 | ||||
-rw-r--r-- | examples/data/plugins/progress_bar.py | 152 | ||||
-rwxr-xr-x | examples/data/scripts/formfiller.sh | 175 | ||||
-rwxr-xr-x | examples/data/scripts/uzbl-event-manager | 1132 |
13 files changed, 1215 insertions, 1321 deletions
diff --git a/examples/config/config b/examples/config/config index 2e05b99..bb2c066 100644 --- a/examples/config/config +++ b/examples/config/config @@ -1,43 +1,42 @@ -# example uzbl config. -# all settings are optional. you can use uzbl without any config at all (but it won't do much) +# Example uzbl config. All settings are optional. You can use uzbl without +# any config at all (but it won't do much). +# === Core settings ========================================================== + +# Install location prefix. set prefix = /usr/local -# === Shortcuts / Aliases =================================================== +# Interface paths. +set fifo_dir = /tmp +set socket_dir = /tmp + +set shell_cmd = sh -c + +# === General config aliases ================================================= # Config related events (use the request function): -# request BIND <bind cmd> = <command> -set bind = request BIND -# request MODE_BIND <mode> <bind cmd> = <command> -set mode_bind = request MODE_BIND # request MODE_CONFIG <mode> <key> = <value> set mode_config = request MODE_CONFIG # request ON_EVENT <EVENT_NAME> <command> set on_event = request ON_EVENT -# request PROGRESS_CONFIG <key> = <value> -set progress = request PROGRESS_CONFIG +# request ON_SET <key/glob> <command> +set on_set = request ON_SET # request MODMAP <From> <To> set modmap = request MODMAP # request IGNORE_KEY <glob> set ignore_key = request IGNORE_KEY # request MODKEY_ADDITION <key1> <key2> <keyn> <result> set modkey_addition = request MODKEY_ADDITION - -# Action related events (use the event function): -# event TOGGLE_MODES <mode1> <mode2> ... <moden> -set toggle_modes = event TOGGLE_MODES +# request TOGGLE_MODES <mode1> <mode2> ... <moden> +set toggle_modes = request TOGGLE_MODES set set_mode = set mode = set set_status = set status_message = -set shell_cmd = sh -c # Spawn path shortcuts. In spawn the first dir+path match is used in "dir1:dir2:dir3:executable" set scripts_dir = $XDG_DATA_HOME/uzbl:@prefix/share/uzbl/examples/data:scripts - -# === Handlers =============================================================== - -# --- Hardcoded event handlers ----------------------------------------------- +# === Hardcoded handlers ===================================================== # These handlers can't be moved to the new event system yet as we don't # support events that can wait for a response from a script. @@ -45,7 +44,7 @@ set cookie_handler = talk_to_socket $XDG_CACHE_HOME/uzbl/cookie_daemon_sock set scheme_handler = sync_spawn @scripts_dir/scheme.py set authentication_handler = sync_spawn @scripts_dir/auth.py -# --- Optional dynamic event handlers ---------------------------------------- +# === Dynamic event handlers ================================================= # Open link in new window @on_event NEW_WINDOW sh 'uzbl-browser -u "$8"' %r @@ -77,7 +76,6 @@ set authentication_handler = sync_spawn @scripts_dir/auth.py # Example CONFIG_CHANGED event handler #@on_event CONFIG_CHANGED print Config changed: %1 = %2 - # === Behaviour and appearance =============================================== set show_status = 1 @@ -93,7 +91,7 @@ set hint_style = weight="bold" set mode_section = <span background="khaki" foreground="black">[\@[\@mode_indicator]\@]</span> set keycmd_section = [<span \@prompt_style>\@[\@keycmd_prompt]\@</span><span \@modcmd_style>\@modcmd</span><span \@keycmd_style>\@keycmd</span><span \@completion_style>\@completion_list</span>] -set progress_section = <span foreground="#606060">\@[\@progress_format]\@</span> +set progress_section = <span foreground="#606060">\@[\@progress.output]\@</span> set scroll_section = <span foreground="#606060">\@[\@scroll_message]\@</span> set uri_section = <span foreground="#99FF66">\@[\@uri]\@</span> set name_section = <span foreground="khaki">\@[\@NAME]\@</span> @@ -105,28 +103,19 @@ set status_format = <span font_family="monospace">@mode_section @keycmd_sect set title_format_long = \@keycmd_prompt \@raw_modcmd \@raw_keycmd \@TITLE - Uzbl browser <\@NAME> \@SELECTED_URI # Progress bar config -@progress width = 8 # %d = done, %p = pending %c = percent done, %i = int done, %s = spinner, # %t = percent pending, %o = int pending, %r = sprite scroll -@progress format = [%d>%p]%c -@progress done = = -@progress pending = +set progress.width = 8 +set progress.format = [%d>%p]%c +set progress.done = = +set progress.pending = -# Or ride those spinnas' -#@progress format = [%d%s%p] -#@progress spinner = -\\|/ -#@progress done = - -#@progress pending = - - -# === Core settings ========================================================== +# === Useragent setup ======================================================== set useragent = Uzbl (Webkit @{WEBKIT_MAJOR}.@{WEBKIT_MINOR}.@{WEBKIT_MICRO}) (@(+uname -sm)@ [@ARCH_UZBL]) (Commit @COMMIT) -set fifo_dir = /tmp -set socket_dir = /tmp - -# === Key modmapping and ignoring ============================================ +# === Key binding configuration ============================================== +# --- Internal modmapping and ignoring --------------------------------------- #modmap <From> <To> @modmap <Control> <Ctrl> @@ -142,23 +131,24 @@ set socket_dir = /tmp @ignore_key <ISO_*> @ignore_key <Shift> +# --- Bind aliases ----------------------------------------------------------- -# === Mode bind aliases ====================================================== +# request BIND <bind cmd> = <command> +set bind = request MODE_BIND global -# Global binding alias (this is done automatically inside the bind plugin). -#set bind = @mode_bind global +# request MODE_BIND <mode> <bind cmd> = <command> +set mode_bind = request MODE_BIND # Insert mode binding alias -set ibind = @mode_bind insert +set ibind = @mode_bind insert # Command mode binding alias -set cbind = @mode_bind command +set cbind = @mode_bind command # Non-insert mode bindings alias (ebind for edit-bind). -set ebind = @mode_bind global,-insert +set ebind = @mode_bind global,-insert - -# === Global & keycmd editing binds ========================================== +# --- Global & keycmd editing binds ------------------------------------------ # Resets keycmd and returns to default mode. @on_event ESCAPE @set_mode @@ -192,16 +182,15 @@ set ebind = @mode_bind global,-insert # === Mouse bindings ========================================================= # Middle click open in new window -@bind <Button2> = sh 'if [ "\@SELECTED_URI" ]; then uzbl-browser -u "\@SELECTED_URI"; else echo "uri $(xclip -o | sed s/\\\@/%40/g)" > $4; fi' - +@bind <Button2> = sh 'if [ "$8" ]; then uzbl-browser -u "$8"; else echo "uri $(xclip -o | sed s/\\\@/%40/g)" > $4; fi' \@SELECTED_URI -# === Keyboard bindings ====================================================== +# --- Keyboard bindings ------------------------------------------------------ # With this command you can enter in any command at runtime when prefixed with # a colon. @cbind :_ = %s -# --- Page movement binds --- +# Page movement binds @cbind j = scroll vertical 20 @cbind k = scroll vertical -20 @cbind h = scroll horizontal -20 @@ -214,59 +203,36 @@ set ebind = @mode_bind global,-insert @cbind $ = scroll horizontal end @cbind <Space> = scroll vertical end -# --- Navigation binds --- +# Navigation binds @cbind b = back @cbind m = forward @cbind S = stop @cbind r = reload @cbind R = reload_ign_cache -# --- Zoom binds --- +# Zoom binds @cbind + = zoom_in @cbind - = zoom_out @cbind T = toggle_zoom_type @cbind 1 = set zoom_level = 1.0 @cbind 2 = set zoom_level = 2.0 -# --- Appearance binds --- +# Appearance binds @cbind t = toggle_status -# --- Page searching binds --- +# Page searching binds @cbind /* = search %s @cbind ?* = search_reverse %s # Jump to next and previous items @cbind n = search @cbind N = search_reverse -# --- Uzbl tabbed binds --- -# Tab opening -@cbind gn = event NEW_TAB -@cbind gN = event NEW_TAB_NEXT -@cbind go<uri:>_ = event NEW_TAB %s -@cbind gO<uri:>_ = event NEW_TAB_NEXT %s -@cbind gY = sh 'echo "event NEW_TAB `xclip -selection primary -o | sed s/\\\@/%40/g`" > $4' -# Closing / resting -@cbind gC = exit -@cbind gQ = event CLEAN_TABS -# Tab navigating -@cbind g< = event FIRST_TAB -@cbind g> = event LAST_TAB -@cbind gt = event NEXT_TAB -@cbind gT = event PREV_TAB -@cbind gi<index:>_ = event GOTO_TAB %s -# Preset loading -set preset = event PRESET_TABS -@cbind gs<preset save:>_ = @preset save %s -@cbind glo<preset load:>_ = @preset load %s -@cbind gd<preset del:>_ = @preset del %s -@cbind gli = @preset list - -# --- Web searching binds --- +# Web searching binds @cbind gg<Google:>_ = uri http://www.google.com/search?q=\@<encodeURIComponent(%r)>\@ @cbind \\awiki<Archwiki:>_ = uri http://wiki.archlinux.org/index.php/Special:Search?search=\@<encodeURIComponent(%r)>\@&go=Go @cbind \\wiki<Wikipedia:>_ = uri http://en.wikipedia.org/w/index.php?title=Special:Search&search=\@<encodeURIComponent(%r)>\@&go=Go -# --- Handy binds --- +# Handy binds # Set function shortcut @cbind s<var:>_<value:>_ = set %1 = %2 # Exit binding @@ -281,24 +247,19 @@ set preset = event PRESET_TABS @cbind <Ctrl><Alt>t = sh 'xterm -e "socat unix-connect:$5 -"' #@cbind <Ctrl><Alt>t = sh 'urxvt -e socat unix-connect:$5 -' -# --- Uri opening prompts --- +# Uri opening prompts @cbind o<uri:>_ = uri %s # Or have it load the current uri into the keycmd for editing @cbind O<uri:\@uri>_ = uri %s -# --- Mode setting binds --- -# Changing mode via set. -@cbind I = @set_mode insert -# Or toggle between modes by raising the toggle event. -set toggle_cmd_ins = @toggle_modes command insert -@cbind i = @toggle_cmd_ins -# And the global toggle bind. -@bind <Ctrl>i = @toggle_cmd_ins +# Mode setting binds +@cbind i = @set_mode insert +@bind <Ctrl>i = @set_mode insert -# --- Hard-bound bookmarks --- +# Hard-bound bookmarks @cbind gh = uri http://www.uzbl.org -# --- Yanking & pasting binds --- +# Yanking & pasting binds @cbind yu = sh 'echo -n $6 | xclip' @cbind yy = sh 'echo -n $7 | xclip' @@ -311,16 +272,16 @@ set toggle_cmd_ins = @toggle_modes command insert # paste primary selection into keycmd at the cursor position @bind <Shift-Insert> = sh 'echo "event INJECT_KEYCMD `xclip -o | sed s/\\\@/%40/g`" > $4' -# --- Bookmark inserting binds --- +# Bookmark inserting binds @cbind <Ctrl>b<tags:>_ = sh 'echo -e "$6 %s" >> $XDG_DATA_HOME/uzbl/bookmarks' # Or use a script to insert a bookmark. @cbind B = spawn @scripts_dir/insert_bookmark.sh -# --- Bookmark/history loading --- +# Bookmark/history loading @cbind U = spawn @scripts_dir/load_url_from_history.sh @cbind u = spawn @scripts_dir/load_url_from_bookmarks.sh -# --- Link following (similar to vimperator and konqueror) --- +# Link following (similar to vimperator and konqueror) # Set custom keys you wish to use for navigation. Some common examples: set follow_hint_keys = 0123456789 #set follow_hint_keys = qwerty @@ -328,7 +289,7 @@ set follow_hint_keys = 0123456789 #set follow_hint_keys = thsnd-rcgmvwb/;789aefijkopquxyz234 @cbind fl* = script @scripts_dir/follow.js '@follow_hint_keys %s' -# --- Form filler binds --- +# Form filler binds # This script allows you to configure (per domain) values to fill in form # fields (eg login information) and to fill in these values automatically. # This implementation allows you to save multiple profiles for each form @@ -339,11 +300,32 @@ set formfiller = spawn @scripts_dir/formfiller.sh @cbind zn = @formfiller new @cbind zl = @formfiller load -# --- Examples --- -# Example showing how to use uzbl's fifo to execute a command. -#@bind X1 = sh 'echo "set zoom_level = 1.0" > "$4"' -#@bind X2 = sh 'echo "js alert (\\"This is sent by the shell via a fifo\\")" > "$4"' +# --- Uzbl tabbed binds ------------------------------------------------------ + +# Tab opening +@cbind gn = event NEW_TAB +@cbind gN = event NEW_TAB_NEXT +@cbind go<uri:>_ = event NEW_TAB %s +@cbind gO<uri:>_ = event NEW_TAB_NEXT %s +@cbind gY = sh 'echo "event NEW_TAB `xclip -selection primary -o | sed s/\\\@/%40/g`" > $4' + +# Closing / resting +@cbind gC = exit +@cbind gQ = event CLEAN_TABS + +# Tab navigating +@cbind g< = event FIRST_TAB +@cbind g> = event LAST_TAB +@cbind gt = event NEXT_TAB +@cbind gT = event PREV_TAB +@cbind gi<index:>_ = event GOTO_TAB %s +# Preset loading +set preset = event PRESET_TABS +@cbind gs<preset save:>_ = @preset save %s +@cbind glo<preset load:>_ = @preset load %s +@cbind gd<preset del:>_ = @preset del %s +@cbind gli = @preset list # === Context menu items ===================================================== @@ -356,7 +338,6 @@ menu_add Quit uzbl = exit # Link context menu menu_link_add Print Link = print \@SELECTED_URI - # === Mode configuration ===================================================== # Define some mode specific uzbl configurations. @@ -368,10 +349,16 @@ set stack = @mode_config stack @command keycmd_style = foreground="red" @command status_background = #202020 @command mode_indicator = Cmd +@command keycmd_events = 1 +@command forward_keys = 0 +@command modcmd_updates = 1 # Insert mode config. @insert status_background = #303030 @insert mode_indicator = Ins +@insert forward_keys = 1 +@insert keycmd_events = 0 +@insert modcmd_updates = 0 # Multi-stage-binding mode config. @stack keycmd_events = 1 @@ -384,8 +371,9 @@ set stack = @mode_config stack set default_mode = command - -# === Post-load misc commands =============================================== +# === Post-load misc commands ================================================ # Set the "home" page. set uri = uzbl.org/doesitwork/@COMMIT + +# vim: set fdm=syntax: diff --git a/examples/data/plugins/bind.py b/examples/data/plugins/bind.py index a1a5d89..5b13476 100644 --- a/examples/data/plugins/bind.py +++ b/examples/data/plugins/bind.py @@ -11,10 +11,6 @@ And it is also possible to execute a function on activation: 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 @@ -62,9 +58,9 @@ class Bindlet(object): if self.last_mode: mode, self.last_mode = self.last_mode, None - self.uzbl.set_mode(mode) + self.uzbl.config['mode'] = mode - self.uzbl.set('keycmd_prompt') + del self.uzbl.config['keycmd_prompt'] def stack(self, bind, args, depth): @@ -76,10 +72,10 @@ class Bindlet(object): return - current_mode = self.uzbl.get_mode() - if current_mode != 'stack': - self.last_mode = current_mode - self.uzbl.set_mode('stack') + mode = self.uzbl.config.get('mode', None) + if mode != 'stack': + self.last_mode = mode + self.uzbl.config['mode'] = 'stack' self.stack_binds = [bind,] self.args += args @@ -97,7 +93,7 @@ class Bindlet(object): self.uzbl.clear_keycmd() if prompt: - self.uzbl.set('keycmd_prompt', prompt) + self.uzbl.config['keycmd_prompt'] = prompt if set and is_cmd: self.uzbl.send(set) @@ -111,7 +107,7 @@ class Bindlet(object): the filtered stack list and modkey & non-stack globals.''' if mode is None: - mode = self.uzbl.get_mode() + mode = self.uzbl.config.get('mode', None) if not mode: mode = 'global' @@ -145,24 +141,6 @@ class Bindlet(object): 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.''' @@ -324,7 +302,7 @@ def exec_bind(uzbl, bind, *args, **kargs): def mode_bind(uzbl, modes, glob, handler=None, *args, **kargs): '''Add a mode bind.''' - bindlet = get_bindlet(uzbl) + bindlet = uzbl.bindlet if not hasattr(modes, '__iter__'): modes = unicode(modes).split(',') @@ -400,7 +378,8 @@ def mode_changed(uzbl, mode): '''Clear the stack on all non-stack mode changes.''' if mode != 'stack': - get_bindlet(uzbl).reset() + uzbl.bindlet.reset() + uzbl.clear_keycmd() def match_and_exec(uzbl, bind, depth, keylet, bindlet): @@ -440,7 +419,7 @@ def match_and_exec(uzbl, bind, depth, keylet, bindlet): args = bindlet.args + args exec_bind(uzbl, bind, *args) if not has_args or on_exec: - uzbl.set_mode() + del uzbl.config['mode'] bindlet.reset() uzbl.clear_current() @@ -448,7 +427,7 @@ def match_and_exec(uzbl, bind, depth, keylet, bindlet): def key_event(uzbl, keylet, mod_cmd=False, on_exec=False): - bindlet = get_bindlet(uzbl) + bindlet = uzbl.bindlet depth = bindlet.depth for bind in bindlet.get_binds(): t = bind[depth] @@ -463,12 +442,14 @@ def key_event(uzbl, keylet, mod_cmd=False, on_exec=False): # Return to the previous mode if the KEYCMD_EXEC keycmd doesn't match any # binds in the stack mode. if on_exec and not mod_cmd and depth and depth == bindlet.depth: - uzbl.set_mode() + del uzbl.config['mode'] +# plugin init hook def init(uzbl): - # Event handling hooks. - uzbl.connect_dict({ + '''Export functions and connect handlers to events.''' + + connect_dict(uzbl, { 'BIND': parse_bind, 'MODE_BIND': parse_mode_bind, 'MODE_CHANGED': mode_changed, @@ -481,12 +462,10 @@ def init(uzbl): for mod_cmd in range(2): for on_exec in range(2): event = events[mod_cmd][on_exec] - uzbl.connect(event, key_event, bool(mod_cmd), bool(on_exec)) + connect(uzbl, event, key_event, bool(mod_cmd), bool(on_exec)) - # Function exports to the uzbl object, `function(uzbl, *args, ..)` - # becomes `uzbl.function(*args, ..)`. - uzbl.export_dict({ + export_dict(uzbl, { 'bind': bind, 'mode_bind': mode_bind, - 'get_bindlet': get_bindlet, + 'bindlet': Bindlet(uzbl), }) diff --git a/examples/data/plugins/cmd_expand.py b/examples/data/plugins/cmd_expand.py index 3f6ae2b..b007975 100644 --- a/examples/data/plugins/cmd_expand.py +++ b/examples/data/plugins/cmd_expand.py @@ -35,8 +35,6 @@ def cmd_expand(uzbl, cmd, args): return cmd - +# plugin init hook def init(uzbl): - # Function exports to the uzbl object, `function(uzbl, *args, ..)` - # becomes `uzbl.function(*args, ..)`. - uzbl.export('cmd_expand', cmd_expand) + export(uzbl, 'cmd_expand', cmd_expand) diff --git a/examples/data/plugins/completion.py b/examples/data/plugins/completion.py index 8cea203..e8c7f34 100644 --- a/examples/data/plugins/completion.py +++ b/examples/data/plugins/completion.py @@ -1,19 +1,10 @@ '''Keycmd completion.''' -# A list of functions this plugin exports to be used via uzbl object. -__export__ = ['start_completion', 'get_completion_dict'] - import re -# Holds the per-instance completion dicts. -UZBLS = {} - # Completion level NONE, ONCE, LIST, COMPLETE = range(4) -# Default instance dict. -DEFAULTS = {'completions': [], 'level': NONE, 'lock': False} - # The reverse keyword finding re. FIND_SEGMENT = re.compile("(\@[\w_]+|set[\s]+[\w_]+|[\w_]+)$").findall @@ -21,39 +12,17 @@ FIND_SEGMENT = re.compile("(\@[\w_]+|set[\s]+[\w_]+|[\w_]+)$").findall LIST_FORMAT = "<span> %s </span>" ITEM_FORMAT = "<span @hint_style>%s</span>%s" - def escape(str): return str.replace("@", "\@") -def add_instance(uzbl, *args): - UZBLS[uzbl] = dict(DEFAULTS) - - # Make sure the config keys for all possible completions are known. - uzbl.send('dump_config_as_events') - - -def del_instance(uzbl, *args): - if uzbl in UZBLS: - del UZBLS[uzbl] - - -def get_completion_dict(uzbl): - '''Get data stored for an instance.''' - - if uzbl not in UZBLS: - add_instance(uzbl) - - return UZBLS[uzbl] - - def get_incomplete_keyword(uzbl): '''Gets the segment of the keycmd leading up to the cursor position and uses a regular expression to search backwards finding parially completed keywords or @variables. Returns a null string if the correct completion conditions aren't met.''' - keylet = uzbl.get_keylet() + keylet = uzbl.keylet left_segment = keylet.keycmd[:keylet.cursor] partial = (FIND_SEGMENT(left_segment) + ['',])[0].lstrip() if partial.startswith('set '): @@ -65,9 +34,8 @@ def get_incomplete_keyword(uzbl): def stop_completion(uzbl, *args): '''Stop command completion and return the level to NONE.''' - d = get_completion_dict(uzbl) - d['level'] = NONE - uzbl.set('completion_list') + uzbl.completion.level = NONE + del uzbl.config['completion_list'] def complete_completion(uzbl, partial, hint, set_completion=False): @@ -99,46 +67,46 @@ def update_completion_list(uzbl, *args): if not partial: return stop_completion(uzbl) - d = get_completion_dict(uzbl) - if d['level'] < LIST: + if uzbl.completion.level < LIST: return - hints = [h for h in d['completions'] if h.startswith(partial)] + hints = filter(lambda h: h.startswith(partial), uzbl.completion) if not hints: - return uzbl.set('completion_list') + del uzbl.config['completion_list'] + return j = len(partial) l = [ITEM_FORMAT % (escape(h[:j]), h[j:]) for h in sorted(hints)] - uzbl.set('completion_list', LIST_FORMAT % ' '.join(l)) + uzbl.config['completion_list'] = LIST_FORMAT % ' '.join(l) def start_completion(uzbl, *args): - d = get_completion_dict(uzbl) - if d['lock']: + comp = uzbl.completion + if comp.locked: return (partial, set_completion) = get_incomplete_keyword(uzbl) if not partial: return stop_completion(uzbl) - if d['level'] < COMPLETE: - d['level'] += 1 + if comp.level < COMPLETE: + comp.level += 1 - hints = [h for h in d['completions'] if h.startswith(partial)] + hints = filter(lambda h: h.startswith(partial), comp) if not hints: return elif len(hints) == 1: - d['lock'] = True + comp.lock() complete_completion(uzbl, partial, hints[0], set_completion) - d['lock'] = False + comp.unlock() return - elif partial in hints and d['level'] == COMPLETE: - d['lock'] = True + elif partial in hints and comp.level == COMPLETE: + comp.lock() complete_completion(uzbl, partial, partial, set_completion) - d['lock'] = False + comp.unlock() return smalllen, smallest = sorted([(len(h), h) for h in hints])[0] @@ -156,51 +124,56 @@ def start_completion(uzbl, *args): common += char if common: - d['lock'] = True + comp.lock() partial_completion(uzbl, partial, partial+common) - d['lock'] = False + comp.unlock() update_completion_list(uzbl) -def add_builtins(uzbl, args): +def add_builtins(uzbl, builtins): '''Pump the space delimited list of builtin commands into the builtin list.''' - completions = get_completion_dict(uzbl)['completions'] - builtins = filter(None, map(unicode.strip, args.split(" "))) - for builtin in builtins: - if builtin not in completions: - completions.append(builtin) + uzbl.completion.update(builtins.split()) def add_config_key(uzbl, key, value): '''Listen on the CONFIG_CHANGED event and add config keys to the variable list for @var<Tab> like expansion support.''' - completions = get_completion_dict(uzbl)['completions'] - key = "@%s" % key - if key not in completions: - completions.append(key) + uzbl.completion.add("@%s" % key) + + +class Completions(set): + def __init__(self): + set.__init__(self) + self.locked = False + self.level = NONE + + def lock(self): + self.locked = True + + def unlock(self): + self.locked = False def init(uzbl): - # Event handling hooks. - uzbl.connect_dict({ - 'BUILTINS': add_builtins, - 'CONFIG_CHANGED': add_config_key, - 'INSTANCE_EXIT': del_instance, - 'INSTANCE_START': add_instance, - 'KEYCMD_CLEARED': stop_completion, - 'KEYCMD_EXEC': stop_completion, - 'KEYCMD_UPDATE': update_completion_list, - 'START_COMPLETION': start_completion, - 'STOP_COMPLETION': stop_completion, + '''Export functions and connect handlers to events.''' + + export_dict(uzbl, { + 'completion': Completions(), + 'start_completion': start_completion, }) - # Function exports to the uzbl object, `function(uzbl, *args, ..)` - # becomes `uzbl.function(*args, ..)`. - uzbl.export_dict({ - 'get_completion_dict': get_completion_dict, - 'start_completion': start_completion, + connect_dict(uzbl, { + 'BUILTINS': add_builtins, + 'CONFIG_CHANGED': add_config_key, + 'KEYCMD_CLEARED': stop_completion, + 'KEYCMD_EXEC': stop_completion, + 'KEYCMD_UPDATE': update_completion_list, + 'START_COMPLETION': start_completion, + 'STOP_COMPLETION': stop_completion, }) + + uzbl.send('dump_config_as_events') diff --git a/examples/data/plugins/config.py b/examples/data/plugins/config.py index 4a848a3..ed2d761 100644 --- a/examples/data/plugins/config.py +++ b/examples/data/plugins/config.py @@ -1,97 +1,90 @@ -import re -import types +from re import compile +from types import BooleanType +from UserDict import DictMixin -__export__ = ['set', 'get_config'] - -VALIDKEY = re.compile("^[a-zA-Z][a-zA-Z0-9_]*$").match -TYPECONVERT = {'int': int, 'float': float, 'str': unicode} - -UZBLS = {} - - -def escape(value): - '''A real escaping function may be required.''' - - return unicode(value) +valid_key = compile('^[A-Za-z0-9_\.]+$').match +types = {'int': int, 'float': float, 'str': unicode} +escape = lambda s: unicode(s).replace('\n', '\\n') +class Config(DictMixin): + def __init__(self, uzbl): + self.uzbl = uzbl -def set(uzbl, key, value='', config=None, force=False): - '''Sends a: "set key = value" command to the uzbl instance. If force is - False then only send a set command if the values aren't equal.''' + # Create the base dict and map allowed methods to `self`. + self.data = data = {} - if type(value) == types.BooleanType: - value = int(value) + methods = ['__contains__', '__getitem__', '__iter__', + '__len__', 'get', 'has_key', 'items', 'iteritems', + 'iterkeys', 'itervalues', 'values'] - else: - value = unicode(value) + for method in methods: + setattr(self, method, getattr(data, method)) - if not VALIDKEY(key): - raise KeyError("%r" % key) - value = escape(value) - if '\n' in value: - value = value.replace("\n", "\\n") + def __setitem__(self, key, value): + self.set(key, value) - if not force: - if config is None: - config = get_config(uzbl) + def __delitem__(self, key): + self.set(key) - if key in config and config[key] == value: - return + def update(self, other=None, **kwargs): + if other is None: + other = {} - uzbl.send('set %s = %s' % (key, value)) + for (key, value) in dict(other).items() + kwargs.items(): + self[key] = value -class ConfigDict(dict): - def __init__(self, uzbl): - self._uzbl = uzbl + def set(self, key, value='', force=False): + '''Generates a `set <key> = <value>` command string to send to the + current uzbl instance. - def __setitem__(self, key, value): - '''Makes "config[key] = value" a wrapper for the set function.''' + Note that the config dict isn't updated by this function. The config + dict is only updated after a successful `VARIABLE_SET ..` event + returns from the uzbl instance.''' - set(self._uzbl, key, value, config=self) + assert valid_key(key) + if type(value) == BooleanType: + value = int(value) -def add_instance(uzbl, *args): - UZBLS[uzbl] = ConfigDict(uzbl) + else: + value = escape(value) + if not force and key in self and self[key] == value: + return -def del_instance(uzbl, *args): - if uzbl in UZBLS: - del uzbl + self.uzbl.send(u'set %s = %s' % (key, value)) -def get_config(uzbl): - if uzbl not in UZBLS: - add_instance(uzbl) +def parse_set_event(uzbl, args): + '''Parse `VARIABLE_SET <var> <type> <value>` event and load the + (key, value) pair into the `uzbl.config` dict.''' - return UZBLS[uzbl] + (key, type, raw_value) = (args.split(' ', 2) + ['',])[:3] + assert valid_key(key) + assert type in types -def variable_set(uzbl, args): - config = get_config(uzbl) + new_value = types[type](raw_value) + old_value = uzbl.config.get(key, None) - key, type, value = list(args.split(' ', 2) + ['',])[:3] - old = config[key] if key in config else None - value = TYPECONVERT[type](value) + # Update new value. + uzbl.config.data[key] = new_value - dict.__setitem__(config, key, value) + if old_value != new_value: + uzbl.event('CONFIG_CHANGED', key, new_value) - if old != value: - uzbl.event("CONFIG_CHANGED", key, value) + # Cleanup null config values. + if type == 'str' and not new_value: + del uzbl.config.data[key] +# plugin init hook def init(uzbl): - # Event handling hooks. - uzbl.connect_dict({ - 'INSTANCE_EXIT': del_instance, - 'INSTANCE_START': add_instance, - 'VARIABLE_SET': variable_set, - }) - - # Function exports to the uzbl object, `function(uzbl, *args, ..)` - # becomes `uzbl.function(*args, ..)`. - uzbl.export_dict({ - 'get_config': get_config, - 'set': set, - }) + export(uzbl, 'config', Config(uzbl)) + connect(uzbl, 'VARIABLE_SET', parse_set_event) + +# plugin cleanup hook +def cleanup(uzbl): + uzbl.config.data.clear() diff --git a/examples/data/plugins/keycmd.py b/examples/data/plugins/keycmd.py index c119077..b600afe 100644 --- a/examples/data/plugins/keycmd.py +++ b/examples/data/plugins/keycmd.py @@ -1,8 +1,5 @@ import re -# Hold the keylets. -UZBLS = {} - # Keycmd format which includes the markup for the cursor. KEYCMD_FORMAT = "%s<span @cursor_style>%s</span>%s" MODCMD_FORMAT = "<span> %s </span>" @@ -38,9 +35,6 @@ class Keylet(object): self.ignores = {} self.additions = {} - # Keylet string repr cache. - self._repr_cache = None - def get_keycmd(self): '''Get the keycmd-part of the keylet.''' @@ -106,9 +100,6 @@ class Keylet(object): 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()) @@ -119,8 +110,7 @@ class Keylet(object): if self.keycmd: l.append('keycmd=%r' % self.get_keycmd()) - self._repr_cache = '<Keylet(%s)>' % ', '.join(l) - return self._repr_cache + return '<keylet(%s)>' % ', '.join(l) def add_modmap(uzbl, key, map): @@ -140,7 +130,7 @@ def add_modmap(uzbl, key, map): ''' assert len(key) - modmaps = get_keylet(uzbl).modmaps + modmaps = uzbl.keylet.modmaps if key[0] == "<" and key[-1] == ">": key = key[1:-1] @@ -171,7 +161,7 @@ def add_key_ignore(uzbl, glob): ''' assert len(glob) > 1 - ignores = get_keylet(uzbl).ignores + ignores = uzbl.keylet.ignores glob = "<%s>" % glob.strip("<> ") restr = glob.replace('*', '[^\s]*') @@ -197,7 +187,7 @@ def add_modkey_addition(uzbl, modkeys, result): ... ''' - additions = get_keylet(uzbl).additions + additions = uzbl.keylet.additions modkeys = set(modkeys) assert len(modkeys) and result and result not in modkeys @@ -220,65 +210,34 @@ def modkey_addition_parse(uzbl, modkeys): add_modkey_addition(uzbl, keys[:-1], keys[-1]) -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 = uzbl.keylet k.keycmd = '' k.cursor = 0 - k._repr_cache = False - uzbl.set('keycmd') - uzbl.set('raw_keycmd') + del uzbl.config['keycmd'] uzbl.event('KEYCMD_CLEARED') def clear_modcmd(uzbl, clear_held=False): '''Clear the modcmd for this uzbl instance.''' - k = get_keylet(uzbl) + k = uzbl.keylet k.modcmd = '' k.is_modcmd = False - k._repr_cache = False if clear_held: k.ignored = set() k.held = set() - uzbl.set('modcmd') - uzbl.set('raw_modcmd') + del uzbl.config['modcmd'] uzbl.event('MODCMD_CLEARED') def clear_current(uzbl): '''Clear the modcmd if is_modcmd else clear keycmd.''' - k = get_keylet(uzbl) - if k.is_modcmd: + if uzbl.keylet.is_modcmd: clear_modcmd(uzbl) else: @@ -296,7 +255,6 @@ def focus_changed(uzbl, *args): 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: @@ -305,32 +263,28 @@ def update_event(uzbl, k, execute=True): else: uzbl.event('KEYCMD_UPDATE', k) - if 'modcmd_updates' not in config or config['modcmd_updates'] == '1': + if uzbl.config.get('modcmd_updates', '1') == '1': new_modcmd = k.get_modcmd() if not new_modcmd: - uzbl.set('modcmd', config=config) - uzbl.set('raw_modcmd', config=config) + del uzbl.config['modcmd'] elif new_modcmd == modcmd: - uzbl.set('raw_modcmd', escape(modcmd), config=config) - uzbl.set('modcmd', MODCMD_FORMAT % uzbl_escape(modcmd), - config=config) + uzbl.config['modcmd'] = MODCMD_FORMAT % uzbl_escape(modcmd) - if 'keycmd_events' in config and config['keycmd_events'] != '1': + if uzbl.config.get('keycmd_events', '1') != '1': return new_keycmd = k.get_keycmd() if not new_keycmd: - uzbl.set('keycmd', config=config) - uzbl.set('raw_keycmd', config=config) + 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.set('keycmd', value, config=config) - uzbl.set('raw_keycmd', escape(keycmd), config=config) + + uzbl.config['keycmd'] = value def inject_str(str, index, inj): @@ -344,7 +298,7 @@ def get_keylet_and_key(uzbl, key, add=True): by the modmapping or modkey addition rules. Return None if the key is ignored.''' - keylet = get_keylet(uzbl) + keylet = uzbl.keylet key = keylet.modmap_key(key) if len(key) == 1: return (keylet, key) @@ -385,12 +339,11 @@ def key_press(uzbl, key): k.cursor += 1 elif not k.held and len(key) == 1: - config = uzbl.get_config() - if 'keycmd_events' in config and config['keycmd_events'] != '1': + + if uzbl.config.get('keycmd_events', '1') != '1': k.keycmd = '' k.cursor = 0 - uzbl.set('keycmd', config=config) - uzbl.set('raw_keycmd', config=config) + del uzbl.config['keycmd'] return k.keycmd = inject_str(k.keycmd, k.cursor, key) @@ -429,9 +382,8 @@ def key_release(uzbl, key): def set_keycmd(uzbl, keycmd): '''Allow setting of the keycmd externally.''' - k = get_keylet(uzbl) + k = uzbl.keylet k.keycmd = keycmd - k._repr_cache = None k.cursor = len(keycmd) update_event(uzbl, k, False) @@ -439,9 +391,8 @@ def set_keycmd(uzbl, keycmd): def inject_keycmd(uzbl, keycmd): '''Allow injecting of a string into the keycmd at the cursor position.''' - k = get_keylet(uzbl) + k = uzbl.keylet k.keycmd = inject_str(k.keycmd, k.cursor, keycmd) - k._repr_cache = None k.cursor += len(keycmd) update_event(uzbl, k, False) @@ -449,9 +400,8 @@ def inject_keycmd(uzbl, keycmd): def append_keycmd(uzbl, keycmd): '''Allow appening of a string to the keycmd.''' - k = get_keylet(uzbl) + k = uzbl.keylet k.keycmd += keycmd - k._repr_cache = None k.cursor = len(k.keycmd) update_event(uzbl, k, False) @@ -460,7 +410,7 @@ def keycmd_strip_word(uzbl, sep): ''' Removes the last word from the keycmd, similar to readline ^W ''' sep = sep or ' ' - k = get_keylet(uzbl) + k = uzbl.keylet if not k.keycmd: return @@ -475,7 +425,7 @@ def keycmd_strip_word(uzbl, sep): def keycmd_backspace(uzbl, *args): '''Removes the character at the cursor position in the keycmd.''' - k = get_keylet(uzbl) + k = uzbl.keylet if not k.keycmd: return @@ -487,7 +437,7 @@ def keycmd_backspace(uzbl, *args): def keycmd_delete(uzbl, *args): '''Removes the character after the cursor position in the keycmd.''' - k = get_keylet(uzbl) + k = uzbl.keylet if not k.keycmd: return @@ -499,8 +449,7 @@ 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) + uzbl.event('KEYCMD_EXEC', uzbl.keylet) clear_keycmd(uzbl) @@ -508,7 +457,7 @@ 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) + k = uzbl.keylet if index == '-': cursor = k.cursor - 1 @@ -530,18 +479,16 @@ def set_cursor_pos(uzbl, index): update_event(uzbl, k, False) +# plugin init hook def init(uzbl): - '''Connect handlers to uzbl events.''' + '''Export functions and connect handlers to events.''' - # Event handling hooks. - uzbl.connect_dict({ + connect_dict(uzbl, { 'APPEND_KEYCMD': append_keycmd, 'FOCUS_GAINED': focus_changed, 'FOCUS_LOST': focus_changed, 'IGNORE_KEY': add_key_ignore, 'INJECT_KEYCMD': inject_keycmd, - 'INSTANCE_EXIT': del_instance, - 'INSTANCE_START': add_instance, 'KEYCMD_BACKSPACE': keycmd_backspace, 'KEYCMD_DELETE': keycmd_delete, 'KEYCMD_EXEC_CURRENT': keycmd_exec_current, @@ -554,9 +501,7 @@ def init(uzbl): 'SET_KEYCMD': set_keycmd, }) - # Function exports to the uzbl object, `function(uzbl, *args, ..)` - # becomes `uzbl.function(*args, ..)`. - uzbl.export_dict({ + export_dict(uzbl, { 'add_key_ignore': add_key_ignore, 'add_modkey_addition': add_modkey_addition, 'add_modmap': add_modmap, @@ -564,8 +509,8 @@ def init(uzbl): 'clear_current': clear_current, 'clear_keycmd': clear_keycmd, 'clear_modcmd': clear_modcmd, - 'get_keylet': get_keylet, 'inject_keycmd': inject_keycmd, + 'keylet': Keylet(), 'set_cursor_pos': set_cursor_pos, 'set_keycmd': set_keycmd, }) diff --git a/examples/data/plugins/mode.py b/examples/data/plugins/mode.py index 54d865a..e0de706 100644 --- a/examples/data/plugins/mode.py +++ b/examples/data/plugins/mode.py @@ -1,176 +1,68 @@ -import sys -import re +from collections import defaultdict -__export__ = ['set_mode', 'get_mode', 'set_mode_config', 'get_mode_config'] +def parse_mode_config(uzbl, args): + '''Parse `MODE_CONFIG <mode> <var> = <value>` event and update config if + the `<mode>` is the current mode.''' -UZBLS = {} + ustrip = unicode.strip + args = unicode(args) -DEFAULTS = { - 'mode': '', - 'modes': { - 'insert': { - 'forward_keys': True, - 'keycmd_events': False, - 'modcmd_updates': False, - 'mode_indicator': 'I'}, - 'command': { - 'forward_keys': False, - 'keycmd_events': True, - 'modcmd_updates': True, - 'mode_indicator': 'C'}}} + assert args.strip(), "missing mode config args" + (mode, args) = map(ustrip, (args.strip().split(' ', 1) + ['',])[:2]) -FINDSPACES = re.compile("\s+") -VALID_KEY = re.compile("^[\w_]+$").match + assert args.strip(), "missing mode config set arg" + (key, value) = map(ustrip, (args.strip().split('=', 1) + [None,])[:2]) + assert key and value is not None, "invalid mode config set syntax" + uzbl.mode_config[mode][key] = value + if uzbl.config.get('mode', None) == mode: + uzbl.config[key] = value -def add_instance(uzbl, *args): - UZBLS[uzbl] = dict(DEFAULTS) +def default_mode_updated(uzbl, var, mode): + if mode and not uzbl.config.get('mode', None): + logger.debug('setting mode to default %r' % mode) + uzbl.config['mode'] = mode -def del_instance(uzbl, *args): - if uzbl in UZBLS: - del UZBLS[uzbl] - -def get_mode_dict(uzbl): - '''Return the mode dict for an instance.''' - - if uzbl not in UZBLS: - add_instance(uzbl) - - return UZBLS[uzbl] - - -def get_mode_config(uzbl, mode): - '''Return the mode config for a given mode.''' - - modes = get_mode_dict(uzbl)['modes'] - if mode not in modes: - modes[mode] = {} - - return modes[mode] - - -def get_mode(uzbl): - return get_mode_dict(uzbl)['mode'] - - -def mode_changed(uzbl, mode): - '''The mode has just been changed, now set the per-mode config.''' - - if get_mode(uzbl) != mode: +def mode_updated(uzbl, var, mode): + if not mode: + mode = uzbl.config.get('default_mode', 'command') + logger.debug('setting mode to default %r' % mode) + uzbl.config['mode'] = mode return - config = uzbl.get_config() - mode_config = get_mode_config(uzbl, mode) - for (key, value) in mode_config.items(): - uzbl.set(key, value, config=config) - - if 'mode_indicator' not in mode_config: - config['mode_indicator'] = mode - - uzbl.clear_keycmd() - uzbl.clear_modcmd() - - -def set_mode(uzbl, mode=None): - '''Set the mode and raise the MODE_CHANGED event if the mode has changed. - Fallback on the default mode if no mode argument was given and the default - mode is not null.''' - - config = uzbl.get_config() - mode_dict = get_mode_dict(uzbl) - if mode is None: - mode_dict['mode'] = '' - if 'default_mode' in config: - mode = config['default_mode'] - - else: - mode = 'command' - - if not VALID_KEY(mode): - raise KeyError("invalid mode name: %r" % mode) - - if 'mode' not in config or config['mode'] != mode: - config['mode'] = mode + # Load mode config + mode_config = uzbl.mode_config.get(mode, None) + if mode_config: + uzbl.config.update(mode_config) - elif mode_dict['mode'] != mode: - mode_dict['mode'] = mode - uzbl.event("MODE_CHANGED", mode) + uzbl.send('event MODE_CONFIRM %s' % mode) -def config_changed(uzbl, key, value): - '''Check for mode related config changes.''' +def confirm_change(uzbl, mode): + if mode and uzbl.config.get('mode', None) == mode: + uzbl.event('MODE_CHANGED', mode) - value = None if not value else value - if key == 'default_mode': - if not get_mode(uzbl): - set_mode(uzbl, value) - elif key == 'mode': - set_mode(uzbl, value) - - -def set_mode_config(uzbl, mode, key, value): - '''Set mode specific configs. If the mode being modified is the current - mode then apply the changes on the go.''' - - assert VALID_KEY(mode) and VALID_KEY(key) - - mode_config = get_mode_config(uzbl, mode) - mode_config[key] = value - - if get_mode(uzbl) == mode: - uzbl.set(key, value) - - -def mode_config(uzbl, args): - '''Parse mode config events.''' - - split = map(unicode.strip, FINDSPACES.split(args.lstrip(), 1)) - if len(split) != 2: - raise SyntaxError('invalid mode config syntax: %r' % args) - - mode, set = split - split = map(unicode.strip, set.split('=', 1)) - if len(split) != 2: - raise SyntaxError('invalid set syntax: %r' % args) - - key, value = split - set_mode_config(uzbl, mode, key, value) - - -def toggle_modes(uzbl, modes): - '''Toggle or cycle between or through a list of modes.''' - - assert len(modes.strip()) - - modelist = filter(None, map(unicode.strip, modes.split(' '))) - mode = get_mode(uzbl) - - index = 0 - if mode in modelist: - index = (modelist.index(mode)+1) % len(modelist) - - set_mode(uzbl, modelist[index]) +# plugin init hook +def init(uzbl): + require('config') + require('on_set') + # Usage `uzbl.mode_config[mode][key] = value` + export(uzbl, 'mode_config', defaultdict(dict)) -def init(uzbl): - # Event handling hooks. - uzbl.connect_dict({ - 'CONFIG_CHANGED': config_changed, - 'INSTANCE_EXIT': del_instance, - 'INSTANCE_START': add_instance, - 'MODE_CHANGED': mode_changed, - 'MODE_CONFIG': mode_config, - 'TOGGLE_MODES': toggle_modes, + connect_dict(uzbl, { + 'MODE_CONFIG': parse_mode_config, + 'MODE_CONFIRM': confirm_change, }) - # Function exports to the uzbl object, `function(uzbl, *args, ..)` - # becomes `uzbl.function(*args, ..)`. - uzbl.export_dict({ - 'get_mode': get_mode, - 'get_mode_config': get_mode_config, - 'set_mode': set_mode, - 'set_mode_config': set_mode_config, - }) +# plugin after hook +def after(uzbl): + uzbl.on_set('mode', mode_updated) + uzbl.on_set('default_mode', default_mode_updated) + +# plugin cleanup hook +def cleanup(uzbl): + uzbl.mode_config.clear() diff --git a/examples/data/plugins/on_event.py b/examples/data/plugins/on_event.py index b9c504a..5142275 100644 --- a/examples/data/plugins/on_event.py +++ b/examples/data/plugins/on_event.py @@ -20,36 +20,11 @@ Usage: import sys import re -__export__ = ['get_on_events', 'on_event'] - -UZBLS = {} - - -def error(msg): - sys.stderr.write('on_event plugin: error: %s\n' % msg) - - -def add_instance(uzbl, *args): - UZBLS[uzbl] = {} - - -def del_instance(uzbl, *args): - if uzbl in UZBLS: - del UZBLS[uzbl] - - -def get_on_events(uzbl): - if uzbl not in UZBLS: - add_instance(uzbl) - - return UZBLS[uzbl] - - def event_handler(uzbl, *args, **kargs): '''This function handles all the events being watched by various on_event definitions and responds accordingly.''' - events = get_on_events(uzbl) + events = uzbl.on_events event = kargs['on_event'] if event not in events: return @@ -65,9 +40,9 @@ def on_event(uzbl, event, cmd): '''Add a new event to watch and respond to.''' event = event.upper() - events = get_on_events(uzbl) + events = uzbl.on_events if event not in events: - uzbl.connect(event, event_handler, on_event=event) + connect(uzbl, event, event_handler, on_event=event) events[event] = [] cmds = events[event] @@ -80,28 +55,28 @@ def parse_on_event(uzbl, args): Syntax: "event ON_EVENT <EVENT_NAME> commands".''' - if not args: - return error("missing on_event arguments") - - split = args.split(' ', 1) - if len(split) != 2: - return error("invalid ON_EVENT syntax: %r" % args) + args = args.strip() + assert args, 'missing on event arguments' - event, cmd = split - on_event(uzbl, event, cmd) + (event, command) = (args.split(' ', 1) + ['',])[:2] + assert event and command, 'missing on event command' + on_event(uzbl, event, command) +# plugin init hook def init(uzbl): - # Event handling hooks. - uzbl.connect_dict({ - 'INSTANCE_EXIT': del_instance, - 'INSTANCE_START': add_instance, - 'ON_EVENT': parse_on_event, - }) + '''Export functions and connect handlers to events.''' + + connect(uzbl, 'ON_EVENT', parse_on_event) - # Function exports to the uzbl object, `function(uzbl, *args, ..)` - # becomes `uzbl.function(*args, ..)`. - uzbl.export_dict({ - 'get_on_events': get_on_events, - 'on_event': on_event, + export_dict(uzbl, { + 'on_event': on_event, + 'on_events': {}, }) + +# plugin cleanup hook +def cleanup(uzbl): + for handlers in uzbl.on_events.values(): + del handlers[:] + + uzbl.on_events.clear() diff --git a/examples/data/plugins/on_set.py b/examples/data/plugins/on_set.py new file mode 100644 index 0000000..130b816 --- /dev/null +++ b/examples/data/plugins/on_set.py @@ -0,0 +1,92 @@ +from re import compile +from functools import partial + +valid_glob = compile('^[A-Za-z0-9_\*\.]+$').match + +def make_matcher(glob): + '''Make matcher function from simple glob.''' + + pattern = "^%s$" % glob.replace('*', '[^\s]*') + return compile(pattern).match + + +def exec_handlers(uzbl, handlers, key, arg): + '''Execute the on_set handlers that matched the key.''' + + for handler in handlers: + if callable(handler): + handler(key, arg) + + else: + uzbl.send(uzbl.cmd_expand(handler, [key, arg])) + + +def check_for_handlers(uzbl, key, arg): + '''Check for handlers for the current key.''' + + for (matcher, handlers) in uzbl.on_sets.values(): + if matcher(key): + exec_handlers(uzbl, handlers, key, arg) + + +def on_set(uzbl, glob, handler, prepend=True): + '''Add a new handler for a config key change. + + Structure of the `uzbl.on_sets` dict: + { glob : ( glob matcher function, handlers list ), .. } + ''' + + assert valid_glob(glob) + + while '**' in glob: + glob = glob.replace('**', '*') + + if callable(handler): + orig_handler = handler + if prepend: + handler = partial(handler, uzbl) + + else: + orig_handler = handler = unicode(handler) + + if glob in uzbl.on_sets: + (matcher, handlers) = uzbl.on_sets[glob] + handlers.append(handler) + + else: + matcher = make_matcher(glob) + uzbl.on_sets[glob] = (matcher, [handler,]) + + uzbl.logger.info('on set %r call %r' % (glob, orig_handler)) + + +def parse_on_set(uzbl, args): + '''Parse `ON_SET <glob> <command>` event then pass arguments to the + `on_set(..)` function.''' + + (glob, command) = (args.split(' ', 1) + [None,])[:2] + assert glob and command and valid_glob(glob) + on_set(uzbl, glob, command) + + +# plugins init hook +def init(uzbl): + require('config') + require('cmd_expand') + + export_dict(uzbl, { + 'on_sets': {}, + 'on_set': on_set, + }) + + connect_dict(uzbl, { + 'ON_SET': parse_on_set, + 'CONFIG_CHANGED': check_for_handlers, + }) + +# plugins cleanup hook +def cleanup(uzbl): + for (matcher, handlers) in uzbl.on_sets.values(): + del handlers[:] + + uzbl.on_sets.clear() diff --git a/examples/data/plugins/plugin_template.py b/examples/data/plugins/plugin_template.py deleted file mode 100644 index 565a999..0000000 --- a/examples/data/plugins/plugin_template.py +++ /dev/null @@ -1,76 +0,0 @@ -'''Plugin template.''' - -# Holds the per-instance data dict. -UZBLS = {} - -# The default instance dict. -DEFAULTS = {} - - -def add_instance(uzbl, *args): - '''Add a new instance with default config options.''' - - UZBLS[uzbl] = dict(DEFAULTS) - - -def del_instance(uzbl, *args): - '''Delete data stored for an instance.''' - - if uzbl in UZBLS: - del UZBLS[uzbl] - - -def get_myplugin_dict(uzbl): - '''Get data stored for an instance.''' - - if uzbl not in UZBLS: - add_instance(uzbl) - - return UZBLS[uzbl] - - -def myplugin_function(uzbl, *args, **kargs): - '''Custom plugin function which is exported by the __export__ list at the - top of the file for use by other functions/callbacks.''' - - print "My plugin function arguments:", args, kargs - - # Get the per-instance data object. - data = get_myplugin_dict(uzbl) - - # Function logic goes here. - - -def myplugin_event_parser(uzbl, args): - '''Parses MYPLUGIN_EVENT raised by uzbl or another plugin.''' - - print "Got MYPLUGIN_EVENT with arguments: %r" % args - - # Parsing logic goes here. - - -def init(uzbl): - '''The main function of the plugin which is used to attach all the event - hooks that are going to be used throughout the plugins life. This function - is called each time a UzblInstance() object is created in the event - manager.''' - - # Make a dictionary comprising of {"EVENT_NAME": handler, ..} to the event - # handler stack: - uzbl.connect_dict({ - # event name function - 'INSTANCE_START': add_instance, - 'INSTANCE_EXIT': del_instance, - 'MYPLUGIN_EVENT': myplugin_event_parser, - }) - - # Or connect a handler to an event manually and supply additional optional - # arguments: - #uzbl.connect("MYOTHER_EVENT", myother_event_parser, True, limit=20) - - # Function exports to the uzbl object, `function(uzbl, *args, ..)` - # becomes `uzbl.function(*args, ..)`. - uzbl.connect_dict({ - # external name function - 'myplugin_function': myplugin_function, - }) diff --git a/examples/data/plugins/progress_bar.py b/examples/data/plugins/progress_bar.py index 89ba175..b2edffc 100644 --- a/examples/data/plugins/progress_bar.py +++ b/examples/data/plugins/progress_bar.py @@ -1,39 +1,7 @@ -import sys +UPDATES = 0 -UZBLS = {} - -DEFAULTS = {'width': 8, - 'done': '=', - 'pending': '.', - 'format': '[%d%a%p]%c', - 'spinner': '-\\|/', - 'sprites': 'loading', - 'updates': 0, - 'progress': 100} - - -def error(msg): - sys.stderr.write("progress_bar plugin: error: %s\n" % msg) - - -def add_instance(uzbl, *args): - UZBLS[uzbl] = dict(DEFAULTS) - - -def del_instance(uzbl, *args): - if uzbl in UZBLS: - del UZBLS[uzbl] - - -def get_progress_config(uzbl): - if uzbl not in UZBLS: - add_instance(uzbl) - - return UZBLS[uzbl] - - -def update_progress(uzbl, prog=None): - '''Updates the progress_format variable on LOAD_PROGRESS update. +def update_progress(uzbl, progress=None): + '''Updates the progress.output variable on LOAD_PROGRESS update. The current substitution options are: %d = done char * done @@ -44,116 +12,82 @@ def update_progress(uzbl, prog=None): %t = percent pending %o = int pending %r = sprites + + Default configuration options: + progress.format = [%d>%p]%c + progress.width = 8 + progress.done = = + progress.pending = + progress.spinner = -\|/ + progress.sprites = loading ''' - prog_config = get_progress_config(uzbl) - config = uzbl.get_config() + global UPDATES - if prog is None: - prog = prog_config['progress'] + if progress is None: + UPDATES = 0 + progress = 100 else: - prog = int(prog) - prog_config['progress'] = prog + UPDATES += 1 + progress = int(progress) - prog_config['updates'] += 1 - format = prog_config['format'] - width = prog_config['width'] + # Get progress config vars. + format = uzbl.config.get('progress.format', '[%d>%p]%c') + width = int(uzbl.config.get('progress.width', 8)) + done_symbol = uzbl.config.get('progress.done', '=') + pend = uzbl.config.get('progress.pending', None) + pending_symbol = pend if pend else ' ' # Inflate the done and pending bars to stop the progress bar # jumping around. if '%c' in format or '%i' in format: count = format.count('%c') + format.count('%i') - width += (3-len(str(prog))) * count + width += (3-len(str(progress))) * count if '%t' in format or '%o' in format: count = format.count('%t') + format.count('%o') - width += (3-len(str(100-prog))) * count + width += (3-len(str(100-progress))) * count - done = int(((prog/100.0)*width)+0.5) + done = int(((progress/100.0)*width)+0.5) pending = width - done if '%d' in format: - format = format.replace('%d', prog_config['done']*done) + format = format.replace('%d', done_symbol * done) if '%p' in format: - format = format.replace('%p', prog_config['pending']*pending) + format = format.replace('%p', pending_symbol * pending) if '%c' in format: - format = format.replace('%c', '%d%%' % prog) + format = format.replace('%c', '%d%%' % progress) if '%i' in format: - format = format.replace('%i', '%d' % prog) + format = format.replace('%i', '%d' % progress) if '%t' in format: - format = format.replace('%t', '%d%%' % (100-prog)) + format = format.replace('%t', '%d%%' % (100-progress)) if '%o' in format: - format = format.replace('%o', '%d' % (100-prog)) + format = format.replace('%o', '%d' % (100-progress)) if '%s' in format: - spinner = prog_config['spinner'] - spin = '-' if not spinner else spinner - index = 0 if prog == 100 else prog_config['updates'] % len(spin) - char = '\\\\' if spin[index] == '\\' else spin[index] - format = format.replace('%s', char) + spinner = uzbl.config.get('progress.spinner', '-\\|/') + index = 0 if progress == 100 else UPDATES % len(spinner) + spin = '\\\\' if spinner[index] == '\\' else spinner[index] + format = format.replace('%s', spin) if '%r' in format: - sprites = prog_config['sprites'] - sprites = '-' if not sprites else sprites - index = int(((prog/100.0)*len(sprites))+0.5)-1 + sprites = uzbl.config.get('progress.sprites', 'loading') + index = int(((progress/100.0)*len(sprites))+0.5)-1 sprite = '\\\\' if sprites[index] == '\\' else sprites[index] format = format.replace('%r', sprite) - if 'progress_format' not in config or config['progress_format'] != format: - config['progress_format'] = format - - -def progress_config(uzbl, args): - '''Parse PROGRESS_CONFIG events from the uzbl instance. - - Syntax: event PROGRESS_CONFIG <key> = <value> - ''' - - split = args.split('=', 1) - if len(split) != 2: - return error("invalid syntax: %r" % args) - - key, value = map(unicode.strip, split) - prog_config = get_progress_config(uzbl) - - if key not in prog_config: - return error("key error: %r" % args) - - if type(prog_config[key]) == type(1): - try: - value = int(value) - - except: - return error("invalid type: %r" % args) - - elif not value: - value = ' ' - - prog_config[key] = value - update_progress(uzbl) - - -def reset_progress(uzbl, args): - '''Reset the spinner counter, reset the progress int and re-draw the - progress bar on LOAD_COMMIT.''' - - prog_dict = get_progress_config(uzbl) - prog_dict['updates'] = prog_dict['progress'] = 0 - update_progress(uzbl) - + if uzbl.config.get('progress.output', None) != format: + uzbl.config['progress.output'] = format +# plugin init hook def init(uzbl): - # Event handling hooks. - uzbl.connect_dict({ - 'INSTANCE_EXIT': del_instance, - 'INSTANCE_START': add_instance, - 'LOAD_COMMIT': reset_progress, + connect_dict(uzbl, { + 'LOAD_COMMIT': lambda uzbl, uri: update_progress(uzbl), 'LOAD_PROGRESS': update_progress, - 'PROGRESS_CONFIG': progress_config, }) diff --git a/examples/data/scripts/formfiller.sh b/examples/data/scripts/formfiller.sh index 1408006..69eca17 100755 --- a/examples/data/scripts/formfiller.sh +++ b/examples/data/scripts/formfiller.sh @@ -5,16 +5,37 @@ # uses settings files like: $keydir/<domain> # files contain lines like: !profile=<profile_name> # <fieldname>(fieldtype): <value> -# profile_name should be replaced with a name that will tell sth about that profile -# fieldtype can be text or password - only for information pupropse (auto-generated) - don't change that +# profile_name should be replaced with a name that will tell sth about that +# profile +# fieldtype can be checkbox, text or password, textarea - only for information +# pupropse (auto-generated) - don't change that +# +# Texteares: for textareas edited text can be now splitted into more lines. +# If there will be text, that doesn't match key line: +# <fieldname>(fieldtype):<value> +# then it will be considered as a multiline for the first field above it +# Keep in mind, that if you make more than one line for fileds like input +# text fields, then all lines will be inserted into as one line +# +# Checkboxes/radio-buttons: to uncheck it type on of the following after the +# colon: +# no +# off +# 0 +# unchecked +# false +# or leave it blank, even without spaces +# otherwise it will be considered as checked # # user arg 1: # edit: force editing the file (falls back to new if not found) # new: start with a new file. # load: try to load from file into form # add: try to add another profile to an existing file +# once: edit form using external editor # # something else (or empty): if file not available: new, otherwise load. +# # config dmenu colors and prompt NB="#0f0f0f" @@ -30,15 +51,16 @@ else fi PROMPT="Choose profile" +MODELINE="> vim:ft=formfiller" keydir=${XDG_DATA_HOME:-$HOME/.local/share}/uzbl/dforms [ -d "`dirname $keydir`" ] || exit 1 [ -d "$keydir" ] || mkdir "$keydir" -editor=${VISUAL} -if [ -z ${editor} ]; then - if [ -z ${EDITOR} ]; then +editor="${VISUAL}" +if [ -z "${editor}" ]; then + if [ -z "${EDITOR}" ]; then editor='xterm -e vim' else editor="xterm -e ${EDITOR}" @@ -69,11 +91,67 @@ if [ "$action" != 'edit' -a "$action" != 'new' -a "$action" != 'load' -a "$acti then action="new" [ -e "$keydir/$domain" ] && action="load" -elif [ "$action" == 'edit' ] && [ ! -e "$keydir/$domain" ] +elif [ "$action" = 'edit' ] && [ ! -e "$keydir/$domain" ] then action="new" fi +dumpFunction="function dump() { \ + var rv=''; \ + var allFrames = new Array(window); \ + for(f=0;f<window.frames.length;f=f+1) { \ + allFrames.push(window.frames[f]); \ + } \ + for(j=0;j<allFrames.length;j=j+1) { \ + try { \ + var xp_res=allFrames[j].document.evaluate('//input', allFrames[j].document.documentElement, null, XPathResult.ANY_TYPE,null); \ + var input; \ + while(input=xp_res.iterateNext()) { \ + var type=(input.type?input.type:text); \ + if(type == 'text' || type == 'password' || type == 'search') { \ + rv += input.name + '(' + type + '):' + input.value + '\\\\n'; \ + } \ + else if(type == 'checkbox' || type == 'radio') { \ + rv += input.name + '{' + input.value + '}(' + type + '):' + (input.checked?'ON':'OFF') + '\\\\n'; \ + } \ + } \ + xp_res=allFrames[j].document.evaluate('//textarea', allFrames[j].document.documentElement, null, XPathResult.ANY_TYPE,null); \ + var input; \ + while(input=xp_res.iterateNext()) { \ + rv += input.name + '(textarea):' + input.value + '\\\\n'; \ + } \ + } \ + catch(err) { } \ + } \ + return rv; \ +}; " + +insertFunction="function insert(fname, ftype, fvalue, fchecked) { \ + var allFrames = new Array(window); \ + for(f=0;f<window.frames.length;f=f+1) { \ + allFrames.push(window.frames[f]); \ + } \ + for(j=0;j<allFrames.length;j=j+1) { \ + try { \ + if(ftype == 'text' || ftype == 'password' || ftype == 'search' || ftype == 'textarea') { \ + allFrames[j].document.getElementsByName(fname)[0].value = fvalue; \ + } \ + else if(ftype == 'checkbox') { \ + allFrames[j].document.getElementsByName(fname)[0].checked = fchecked;\ + } \ + else if(ftype == 'radio') { \ + var radios = allFrames[j].document.getElementsByName(fname); \ + for(r=0;r<radios.length;r+=1) { \ + if(radios[r].value == fvalue) { \ + radios[r].checked = fchecked; \ + } \ + } \ + } \ + } \ + catch(err) { } \ + } \ +}; " + if [ "$action" = 'load' ] then [ -e $keydir/$domain ] || exit 2 @@ -84,45 +162,55 @@ then option=`echo -e -n "$menu"| dmenu ${LINES} -nb "${NB}" -nf "${NF}" -sb "${SB}" -sf "${SF}" -p "${PROMPT}"` fi - cat $keydir/$domain | \ - sed -n -e "/^!profile=${option}/,/^!profile=/p" | \ - sed -n -e 's/\([^(]\+\)([^)]\+):[ ]*\(.\+\)$/js if(window.frames.length > 0) { for(i=0;i<window.frames.length;i=i+1) { try { var e = window.frames[i].document.getElementsByName("\1"); if(e.length > 0) { e[0].value="\2"; } } catch(err) { } } }; document.getElementsByName("\1")[0].value="\2"/p' | \ - sed -e 's/@/\\@/g' >> $fifo + # Remove comments + sed '/^>/d' -i $tmpfile + + sed 's/^\([^{]\+\){\([^}]*\)}(\(radio\|checkbox\)):\(off\|no\|false\|unchecked\|0\|$\)/\1{\2}(\3):0/I;s/^\([^{]\+\){\([^}]*\)}(\(radio\|checkbox\)):[^0]\+/\1{\2}(\3):1/I' -i $keydir/$domain + fields=`cat $keydir/$domain | \ + sed -n "/^!profile=${option}/,/^!profile=/p" | \ + sed '/^!profile=/d' | \ + sed 's/^\([^(]\+(\)\(radio\|checkbox\|text\|search\|textarea\|password\)):/%{>\1\2):<}%/' | \ + sed 's/^\(.\+\)$/<{br}>\1/' | \ + tr -d '\n' | \ + sed 's/<{br}>%{>\([^(]\+(\)\(radio\|checkbox\|text\|search\|textarea\|password\)):<}%/\\n\1\2):/g'` + printf '%s\n' "${fields}" | \ + sed -n -e "s/\([^(]\+\)(\(password\|text\|search\|textarea\)\+):[ ]*\(.\+\)/js $insertFunction; insert('\1', '\2', '\3', 0);/p" | \ + sed -e 's/@/\\@/g;s/<{br}>/\\\\n/g' > $fifo + printf '%s\n' "${fields}" | \ + sed -n -e "s/\([^{]\+\){\([^}]*\)}(\(radio\|checkbox\)):[ ]*\(.\+\)/js $insertFunction; insert('\1', '\3', '\2', \4);/p" | \ + sed -e 's/@/\\@/g' > $fifo elif [ "$action" = "once" ] then tmpfile=`mktemp` - html=`echo 'js if(window.frames.length > 0) { for(j=0;j<window.frames.length;j=j+1) { try { window.frames[j].document.documentElement.outerHTML; } catch(err) { } } }' | \ - socat - unix-connect:$socket` - html=${html}" "`echo 'js document.documentElement.outerHTML' | \ - socat - unix-connect:$socket` - html=`echo ${html} | \ - tr -d '\n' | \ - sed 's/>/>\n/g' | \ - sed 's/<input/<input type="text"/g' | \ - sed 's/type="text"\(.*\)type="\([^"]\+\)"/type="\2" \1 /g'` - echo "${html}" | \ - sed -n 's/.*\(<input[^>]\+>\).*/\1/;/type="\(password\|text\)"/Ip' | \ - sed 's/\(.*\)\(type="[^"]\+"\)\(.*\)\(name="[^"]\+"\)\(.*\)/\1\4\3\2\5/I' | \ - sed 's/.*name="\([^"]\+\)".*type="\([^"]\+\)".*/\1(\2):/I' >> $tmpfile - echo "${html}" | \ - sed -n 's/.*<textarea.*name="\([^"]\+\)".*/\1(textarea):/Ip' >> $tmpfile + printf 'js %s dump(); \n' "$dumpFunction" | \ + socat - unix-connect:$socket | \ + sed -n '/^[^(]\+([^)]\+):/p' > $tmpfile + echo "$MODELINE" >> $tmpfile ${editor} $tmpfile [ -e $tmpfile ] || exit 2 - cat $tmpfile | \ - sed -n -e 's/\([^(]\+\)([^)]\+):[ ]*\(.\+\)/js if(window.frames.length > 0) { for(i=0;i<window.frames.length;i=i+1) { try { var e = window.frames[i].document.getElementsByName("\1"); if(e.length > 0) { e[0].value="\2" } } catch(err) { } } }; document.getElementsByName("\1")[0].value="\2"/p' | \ - sed -e 's/@/\\@/g' >> $fifo + # Remove comments + sed '/^>/d' -i $tmpfile + + sed 's/^\([^{]\+\){\([^}]*\)}(\(radio\|checkbox\)):\(off\|no\|false\|unchecked\|0\|$\)/\1{\2}(\3):0/I;s/^\([^{]\+\){\([^}]*\)}(\(radio\|checkbox\)):[^0]\+/\1{\2}(\3):1/I' -i $tmpfile + fields=`cat $tmpfile | \ + sed 's/^\([^(]\+(\)\(radio\|checkbox\|text\|search\|textarea\|password\)):/%{>\1\2):<}%/' | \ + sed 's/^\(.\+\)$/<{br}>\1/' | \ + tr -d '\n' | \ + sed 's/<{br}>%{>\([^(]\+(\)\(radio\|checkbox\|text\|search\|textarea\|password\)):<}%/\\n\1\2):/g'` + printf '%s\n' "${fields}" | \ + sed -n -e "s/\([^(]\+\)(\(password\|text\|search\|textarea\)\+):[ ]*\(.\+\)/js $insertFunction; insert('\1', '\2', '\3', 0);/p" | \ + sed -e 's/@/\\@/g;s/<{br}>/\\\\n/g' > $fifo + printf '%s\n' "${fields}" | \ + sed -n -e "s/\([^{]\+\){\([^}]*\)}(\(radio\|checkbox\)):[ ]*\(.\+\)/js $insertFunction; insert('\1', '\3', '\2', \4);/p" | \ + sed -e 's/@/\\@/g' > $fifo rm -f $tmpfile else - if [ "$action" == 'new' -o "$action" == 'add' ] + if [ "$action" = 'new' -o "$action" = 'add' ] then - if [ "$action" == 'new' ] - then - echo "!profile=NAME_THIS_PROFILE$RANDOM" > $keydir/$domain - else - echo "!profile=NAME_THIS_PROFILE$RANDOM" >> $keydir/$domain - fi + [ "$action" = 'new' ] && echo "$MODELINE" > $keydir/$domain + echo "!profile=NAME_THIS_PROFILE$RANDOM" >> $keydir/$domain # # 2. and 3. line (tr -d and sed) are because, on gmail login for example, # <input > tag is splited into lines @@ -131,7 +219,7 @@ else # type="text" # value=""> # So, tr removes all new lines, and sed inserts new line after each > - # Next sed selects only <input> tags and only with type == "text" or == "password" + # Next sed selects only <input> tags and only with type = "text" or = "password" # If type is first and name is second, then another sed will change their order # so the last sed will make output # text_from_the_name_attr(text or password): @@ -139,18 +227,9 @@ else # login(text): # passwd(password): # - html=`echo 'js if(window.frames.length > 0) { for(i=0;i<window.frames.length;i=i+1) { try { window.frames[i].document.documentElement.outerHTML; } catch(err) { } } }' | \ - socat - unix-connect:$socket` - html=${html}" "`echo 'js document.documentElement.outerHTML' | \ - socat - unix-connect:$socket` - echo ${html} | \ - tr -d '\n' | \ - sed 's/>/>\n/g' | \ - sed 's/<input/<input type="text"/g' | \ - sed 's/type="text"\(.*\)type="\([^"]\+\)"/type="\2" \1 /g' | \ - sed -n 's/.*\(<input[^>]\+>\).*/\1/;/type="\(password\|text\)"/Ip' | \ - sed 's/\(.*\)\(type="[^"]\+"\)\(.*\)\(name="[^"]\+"\)\(.*\)/\1\4\3\2\5/I' | \ - sed 's/.*name="\([^"]\+\)".*type="\([^"]\+\)".*/\1(\2):/I' >> $keydir/$domain + printf 'js %s dump(); \n' "$dumpFunction" | \ + socat - unix-connect:$socket | \ + sed -n '/^[^(]\+([^)]\+):/p' >> $keydir/$domain fi [ -e "$keydir/$domain" ] || exit 3 #this should never happen, but you never know. $editor "$keydir/$domain" #TODO: if user aborts save in editor, the file is already overwritten diff --git a/examples/data/scripts/uzbl-event-manager b/examples/data/scripts/uzbl-event-manager index 7fa4a09..ab13fbb 100755 --- a/examples/data/scripts/uzbl-event-manager +++ b/examples/data/scripts/uzbl-event-manager @@ -1,7 +1,7 @@ #!/usr/bin/env python # Event Manager for Uzbl -# Copyright (c) 2009, Mason Larobina <mason.larobina@gmail.com> +# Copyright (c) 2009-2010, Mason Larobina <mason.larobina@gmail.com> # Copyright (c) 2009, Dieter Plaetinck <dieter@plaetinck.be> # # This program is free software: you can redistribute it and/or modify @@ -26,21 +26,23 @@ Event manager for uzbl written in python. ''' +import atexit import imp +import logging import os -import sys -import re import socket -import pprint +import sys import time -import atexit -from select import select -from signal import signal, SIGTERM -from optparse import OptionParser -from traceback import print_exc +import weakref +from collections import defaultdict from functools import partial +from glob import glob from itertools import count - +from optparse import OptionParser +from select import select +from signal import signal, SIGTERM, SIGINT +from socket import socket, AF_UNIX, SOCK_STREAM +from traceback import format_exc def xdghome(key, default): '''Attempts to use the environ XDG_*_HOME paths if they exist otherwise @@ -52,11 +54,6 @@ def xdghome(key, default): return os.path.join(os.environ['HOME'], default) - -# ============================================================================ -# ::: Default configuration section :::::::::::::::::::::::::::::::::::::::::: -# ============================================================================ - # `make install` will put the correct value here for your system PREFIX = '/usr/local/' @@ -64,120 +61,34 @@ PREFIX = '/usr/local/' DATA_DIR = os.path.join(xdghome('DATA', '.local/share/'), 'uzbl/') CACHE_DIR = os.path.join(xdghome('CACHE', '.cache/'), 'uzbl/') -# Event manager config dictionary. This is not to be confused with the config -# dict that tracks variables in the uzbl instance. -CONFIG = { - 'verbose': False, - 'daemon_mode': True, - 'auto_close': False, - - 'plugins_load': [], - 'plugins_ignore': [], - - 'plugin_dirs': [os.path.join(DATA_DIR, 'plugins/'), - os.path.join(PREFIX, 'share/uzbl/examples/data/plugins/')], - - 'server_socket': os.path.join(CACHE_DIR, 'event_daemon'), - 'pid_file': os.path.join(CACHE_DIR, 'event_daemon.pid'), -} - -# ============================================================================ -# ::: End of configuration section ::::::::::::::::::::::::::::::::::::::::::: -# ============================================================================ - - # Define some globals. SCRIPTNAME = os.path.basename(sys.argv[0]) -FINDSPACES = re.compile("\s+") - - -class ArgumentError(Exception): - pass - - -def echo(msg): - '''Prints only if the verbose flag has been set.''' - - if CONFIG['verbose']: - sys.stdout.write("%s: %s\n" % (SCRIPTNAME, msg)) - - -def error(msg): - '''Prints error messages to stderr.''' - - sys.stderr.write("%s: error: %s\n" % (SCRIPTNAME, msg)) - - -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', ...}''' - - 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 filename in os.listdir(plugin_dir): - if not filename.lower().endswith('.py'): - continue - - path = os.path.join(plugin_dir, filename) - if not os.path.isfile(path): - continue - - if filename not in plugins: - plugins[filename] = plugin_dir - - return plugins +def get_exc(): + '''Format `format_exc` for logging.''' + return "\n%s" % format_exc().rstrip() -def load_plugins(plugin_dirs, load=None, ignore=None): - '''Load event manager plugins found in the plugin_dirs.''' +def expandpath(path): + '''Expand and realpath paths.''' + return os.path.realpath(os.path.expandvars(path)) - load = [] if load is None else load - ignore = [] if ignore is None else ignore - - # Find the plugins in the plugin_dirs. - found = find_plugins(plugin_dirs) - - if load: - # Ignore anything not in the load list. - for plugin in found.keys(): - if plugin not in load: - del found[plugin] - - if ignore: - # Ignore anything in the ignore list. - for plugin in found.keys(): - if plugin in ignore: - del found[plugin] - - # Print plugin list to be loaded. - pprint.pprint(found) - - loaded = {} - # Load all found plugins into the loaded dict. - for (filename, plugin_dir) in found.items(): - name = filename[:-3] - info = imp.find_module(name, [plugin_dir]) - plugin = imp.load_module(name, *info) - loaded[(plugin_dir, filename)] = plugin - - return loaded +def ascii(u): + '''Convert unicode strings into ascii for transmission over + ascii-only streams/sockets/devices.''' + return u.encode('utf-8') def daemonize(): '''Daemonize the process using the Stevens' double-fork magic.''' + logger.info('entering daemon mode') + try: if os.fork(): os._exit(0) except OSError: - print_exc() - sys.stderr.write("fork #1 failed") + logger.critical(get_exc()) sys.exit(1) os.chdir('/') @@ -189,8 +100,7 @@ def daemonize(): os._exit(0) except OSError: - print_exc() - sys.stderr.write("fork #2 failed") + logger.critical(get_exc()) sys.exit(1) if sys.stdout.isatty(): @@ -206,6 +116,8 @@ def daemonize(): os.dup2(stdout.fileno(), sys.stdout.fileno()) os.dup2(stderr.fileno(), sys.stderr.fileno()) + logger.info('entered daemon mode') + def make_dirs(path): '''Make all basedirs recursively as required.''' @@ -213,637 +125,847 @@ def make_dirs(path): try: dirname = os.path.dirname(path) if not os.path.isdir(dirname): + logger.debug('creating directories %r' % dirname) os.makedirs(dirname) except OSError: - print_exc() + logger.error(get_exc()) -def make_pid_file(pid_file): - '''Make pid file at given pid_file location.''' +class EventHandler(object): + '''Event handler class. Used to store args and kwargs which are merged + come time to call the callback with the event args and kwargs.''' - make_dirs(pid_file) - fileobj = open(pid_file, 'w') - fileobj.write('%d' % os.getpid()) - fileobj.close() + nextid = count().next + def __init__(self, plugin, event, callback, args, kwargs): + self.id = self.nextid() + self.plugin = plugin + self.event = event + self.callback = callback + self.args = args + self.kwargs = kwargs -def del_pid_file(pid_file): - '''Delete pid file at given pid_file location.''' - if os.path.isfile(pid_file): - os.remove(pid_file) + def __repr__(self): + elems = ['id=%d' % self.id, 'event=%s' % self.event, + 'callback=%r' % self.callback] + if self.args: + elems.append('args=%s' % repr(self.args)) -def get_pid(pid_file): - '''Read pid from pid_file.''' + if self.kwargs: + elems.append('kwargs=%s' % repr(self.kwargs)) - try: - fileobj = open(pid_file, 'r') - pid = int(fileobj.read()) - fileobj.close() - return pid + elems.append('plugin=%s' % self.plugin.name) + return u'<handler(%s)>' % ', '.join(elems) - except IOError, ValueError: - print_exc() - return None + def call(self, uzbl, *args, **kwargs): + '''Execute the handler function and merge argument lists.''' -def pid_running(pid): - '''Returns True if a process with the given pid is running.''' + args = args + self.args + kwargs = dict(self.kwargs.items() + kwargs.items()) + self.callback(uzbl, *args, **kwargs) - try: - os.kill(pid, 0) - except OSError: - return False +class Plugin(object): + '''Plugin module wrapper object.''' - else: - return True + # Special functions exported from the Plugin instance to the + # plugin namespace. + special_functions = ['require', 'export', 'export_dict', 'connect', + 'connect_dict', 'logger'] -def term_process(pid): - '''Send a SIGTERM signal to the process with the given pid.''' + def __init__(self, parent, name, path, plugin): + self.parent = parent + self.name = name + self.path = path + self.plugin = plugin + self.logger = get_logger('plugin.%s' % name) - if not pid_running(pid): - return False + # Weakrefs to all handlers created by this plugin + self.handlers = set([]) - os.kill(pid, SIGTERM) + # Plugins init hook + init = getattr(plugin, 'init', None) + self.init = init if callable(init) else None - start = time.time() - while True: - if not pid_running(pid): - return True + # Plugins optional after hook + after = getattr(plugin, 'after', None) + self.after = after if callable(after) else None - if time.time() - start > 5: - raise OSError('failed to stop process with pid: %d' % pid) + # Plugins optional cleanup hook + cleanup = getattr(plugin, 'cleanup', None) + self.cleanup = cleanup if callable(cleanup) else None - time.sleep(0.25) + assert init or after or cleanup, "missing hooks in plugin" + # Export plugin's instance methods to plugin namespace + for attr in self.special_functions: + plugin.__dict__[attr] = getattr(self, attr) -def parse_msg(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.''' - if not msg: - return + def __repr__(self): + return u'<plugin(%r)>' % self.plugin - cmd = FINDSPACES.split(msg, 3) - if not cmd or cmd[0] != 'EVENT': - # Not an event message. - print '---', msg.encode('utf-8') - return - while len(cmd) < 4: - cmd.append('') + def export(self, uzbl, attr, object, prepend=True): + '''Attach `object` to `uzbl` instance. This is the preferred method + of sharing functionality, functions, data and objects between + plugins. - event, args = cmd[2], cmd[3] - if not event: - return + If the object is callable you may wish to turn the callable object + in to a meta-instance-method by prepending `uzbl` to the call stack. + You can change this behaviour with the `prepend` argument. + ''' - try: - uzbl.event(event, args) + assert attr not in uzbl.exports, "attr %r already exported by %r" %\ + (attr, uzbl.exports[attr][0]) - except: - print_exc() + prepend = True if prepend and callable(object) else False + uzbl.__dict__[attr] = partial(object, uzbl) if prepend else object + uzbl.exports[attr] = (self, object, prepend) + uzbl.logger.info('exported %r to %r by plugin %r, prepended %r' + % (object, 'uzbl.%s' % attr, self.name, prepend)) -class EventHandler(object): + def export_dict(self, uzbl, exports): + for (attr, object) in exports.items(): + self.export(uzbl, attr, object) - nexthid = count().next - def __init__(self, event, handler, *args, **kargs): - if not callable(handler): - raise ArgumentError("EventHandler object requires a callable " - "object function for the handler argument not: %r" % handler) + def find_handler(self, event, callback, args, kwargs): + '''Check if a handler with the identical callback and arguments + exists and return it.''' - self.function = handler - self.args = args - self.kargs = kargs - self.event = event - self.hid = self.nexthid() + # Remove dead refs + self.handlers -= set(filter(lambda ref: not ref(), self.handlers)) + # Find existing identical handler + for handler in [ref() for ref in self.handlers]: + if handler.event == event and handler.callback == callback \ + and handler.args == args and handler.kwargs == kwargs: + return handler - def __repr__(self): - args = ["event=%s" % self.event, "hid=%d" % self.hid, - "function=%r" % self.function] - if self.args: - args.append(u"args=%r" % unicode(self.args)) + def connect(self, uzbl, event, callback, *args, **kwargs): + '''Create an event handler object which handles `event` events. - if self.kargs: - args.append(u"kargs=%r" % unicode(self.kargs)) + Arguments passed to the connect function (`args` and `kwargs`) are + stored in the handler object and merged with the event arguments + come handler execution. - return u"<EventHandler(%s)>" % ', '.join(args) + All handler functions must behave like a `uzbl` instance-method (that + means `uzbl` is prepended to the callback call arguments).''' + # Sanitise and check event name + event = event.upper().strip() + assert event and ' ' not in event -class UzblInstance(object): + assert callable(callback), 'callback must be callable' - # Give all plugins access to the main config dict. - global_config = CONFIG + # Check if an identical handler already exists + handler = self.find_handler(event, callback, args, kwargs) + if not handler: + # Create a new handler + handler = EventHandler(self, event, callback, args, kwargs) + self.handlers.add(weakref.ref(handler)) + self.logger.info('new %r' % handler) - def __init__(self, parent, client_socket): + uzbl.handlers[event].append(handler) + uzbl.logger.info('connected %r' % handler) + return handler - # Internal variables. - self.exports = {} - self.handlers = {} - self.parent = parent - self.client_socket = client_socket - self.depth = 0 - self.buffer = '' - self.pid = None + def connect_dict(self, uzbl, connects): + for (event, callback) in connects.items(): + self.connect(uzbl, event, callback) - # Call the init function in every plugin. The init function in each - # plugin is where that plugin connects functions to events and exports - # functions to the uzbl object. - for plugin in self.parent['plugins'].values(): - try: - plugin.init(self) - except: - raise + def require(self, plugin): + '''Check that plugin with name `plugin` has been loaded. Use this to + ensure that your plugins dependencies have been met.''' + assert plugin in self.parent.plugins, self.logger.critical( + 'plugin %r required by plugin %r' (plugin, self.name)) - def send(self, msg): - '''Send a command to the uzbl instance via the socket file.''' - msg = msg.strip() - if self.client_socket: - print (u'%s<-- %s' % (' ' * self.depth, msg)).encode('utf-8') - self.client_socket.send(("%s\n" % msg).encode('utf-8')) - - else: - print (u'%s!-- %s' % (' ' * self.depth, msg)).encode('utf-8') - - - def export(self, attr, object, prepend=True): - '''Attach an object to the current class instance. This is the - preferred method of sharing functionality, functions and objects - between plugins. - - If the object is callable you may wish to turn the callable object in - to an "instance method call" by using the `functools.partial(..)` - tool to prepend the `self` object to the callable objects argument - list. - - Example session from a plugins POV: - >>> config_dict = {'foo': 'data..', 'bar': 'other data..'} - >>> uzbl.export('config', config_dict) - >>> uzbl.config is config_dict - True - >>> print uzbl.config['foo'] - data.. - >>> uzbl.export('get', lambda uzbl, key: uzbl.config[key]) - >>> print uzbl.get('bar') - other data.. - ''' +class Uzbl(object): + def __init__(self, parent, child_socket): + self.opts = opts + self.parent = parent + self.child_socket = child_socket + self.time = time.time() + self.pid = None + self.name = None - if prepend and callable(object): - object = partial(object, self) + # Flag if the instance has raised the INSTANCE_START event. + self.instance_start = False - self.__dict__.__setitem__(attr, object) + # Use name "unknown" until name is discovered. + self.logger = get_logger('uzbl-instance[]') + # Track plugin event handlers and exported functions. + self.exports = {} + self.handlers = defaultdict(list) - def export_dict(self, export_dict): - '''Export multiple (attr, object)'s at once inside a dict of the - form `{attr1: object1, attr2: object2, ...}`.''' + # Internal vars + self._depth = 0 + self._buffer = '' - for (attr, object) in export_dict.items(): - self.export(attr, object) + def __repr__(self): + return '<uzbl(%s)>' % ', '.join([ + 'pid=%s' % (self.pid if self.pid else "Unknown"), + 'name=%s' % ('%r' % self.name if self.name else "Unknown"), + 'uptime=%f' % (time.time()-self.time), + '%d exports' % len(self.exports.keys()), + '%d handlers' % sum([len(l) for l in self.handlers.values()])]) + + + def init_plugins(self): + '''Call the init and after hooks in all loaded plugins for this + instance.''' + + # Initialise each plugin with the current uzbl instance. + for plugin in self.parent.plugins.values(): + if plugin.init: + self.logger.debug('calling %r plugin init hook' % plugin.name) + plugin.init(self) - def connect(self, event, handler, *args, **kargs): - '''Connect a uzbl event with a handler. Handlers can either be a - function or a uzbl command string.''' + # Allow plugins to use exported features of other plugins by calling an + # optional `after` function in the plugins namespace. + for plugin in self.parent.plugins.values(): + if plugin.after: + self.logger.debug('calling %r plugin after hook'%plugin.name) + plugin.after(self) - event = event.upper().strip() - assert event and ' ' not in event - if event not in self.handlers.keys(): - self.handlers[event] = [] + def send(self, msg): + '''Send a command to the uzbl instance via the child socket + instance.''' + + msg = msg.strip() + assert self.child_socket, "socket inactive" - handlerobj = EventHandler(event, handler, *args, **kargs) - self.handlers[event].append(handlerobj) - print handlerobj + if opts.print_events: + print ascii(u'%s<-- %s' % (' ' * self._depth, msg)) + self.child_socket.send(ascii("%s\n" % msg)) - def connect_dict(self, connect_dict): - '''Connect a dictionary comprising of {"EVENT_NAME": handler, ..} to - the event handler stack. - If you need to supply args or kargs to an event use the normal connect + def read(self): + '''Read data from the child socket and pass lines to the parse_msg function.''' - for (event, handler) in connect_dict.items(): - self.connect(event, handler) - + try: + raw = unicode(self.child_socket.recv(8192), 'utf-8', 'ignore') + if not raw: + self.logger.debug('read null byte') + return self.close() - def remove_by_id(self, hid): - '''Remove connected event handler by unique handler id.''' + except: + self.logger.error(get_exc()) + return self.close() - for (event, handlers) in self.handlers.items(): - for handler in list(handlers): - if hid != handler.hid: - continue + lines = (self._buffer + raw).split('\n') + self._buffer = lines.pop() - echo("removed %r" % handler) - handlers.remove(handler) - return + for line in filter(None, map(unicode.strip, lines)): + try: + self.parse_msg(line.strip()) - echo('unable to find & remove handler with id: %d' % hid) + except: + self.logger.error(get_exc()) + self.logger.error('erroneous event: %r' % line) - def remove(self, handler): - '''Remove connected event handler.''' + def parse_msg(self, line): + '''Parse an incoming message from a uzbl instance. Event strings + will be parsed into `self.event(event, args)`.''' - for (event, handlers) in self.handlers.items(): - if handler in handlers: - echo("removed %r" % handler) - handlers.remove(handler) - return + # Split by spaces (and fill missing with nulls) + elems = (line.split(' ', 3) + ['',]*3)[:4] - echo('unable to find & remove handler: %r' % handler) + # Ignore non-event messages. + if elems[0] != 'EVENT': + logger.info('non-event message: %r' % line) + if opts.print_events: + print '--- %s' % ascii(line) + return + # Check event string elements + (name, event, args) = elems[1:] + assert name and event, 'event string missing elements' + if not self.name: + self.name = name + self.logger = get_logger('uzbl-instance%s' % name) + self.logger.info('found instance name %r' % name) - def exec_handler(self, handler, *args, **kargs): - '''Execute event handler function.''' + assert self.name == name, 'instance name mismatch' - args += handler.args - kargs = dict(handler.kargs.items()+kargs.items()) - handler.function(self, *args, **kargs) + # Handle the event with the event handlers through the event method + self.event(event, args) def event(self, event, *args, **kargs): '''Raise an event.''' event = event.upper() - elems = [event,] - if args: elems.append(unicode(args)) - if kargs: elems.append(unicode(kargs)) - print (u'%s--> %s' % (' ' * self.depth, ' '.join(elems))).encode('utf-8') + + if not opts.daemon_mode and opts.print_events: + elems = [event,] + if args: elems.append(unicode(args)) + if kargs: elems.append(unicode(kargs)) + print ascii(u'%s--> %s' % (' ' * self._depth, ' '.join(elems))) if event == "INSTANCE_START" and args: + assert not self.instance_start, 'instance already started' + self.pid = int(args[0]) + self.logger.info('found instance pid %r' % self.pid) + + self.init_plugins() + + elif event == "INSTANCE_EXIT": + self.logger.info('uzbl instance exit') + self.close() if event not in self.handlers: return for handler in self.handlers[event]: - self.depth += 1 + self._depth += 1 try: - self.exec_handler(handler, *args, **kargs) + handler.call(self, *args, **kargs) except: - print_exc() + self.logger.error(get_exc()) + + self._depth -= 1 + - self.depth -= 1 + def close_connection(self, child_socket): + '''Close child socket and delete the uzbl instance created for that + child socket connection.''' def close(self): - '''Close the client socket and clean up.''' + '''Close the client socket and call the plugin cleanup hooks.''' + + self.logger.debug('called close method') + + # Remove self from parent uzbls dict. + if self.child_socket in self.parent.uzbls: + self.logger.debug('removing self from uzbls list') + del self.parent.uzbls[self.child_socket] try: - self.client_socket.close() + if self.child_socket: + self.logger.debug('closing child socket') + self.child_socket.close() except: - pass + self.logger.error(get_exc()) + + finally: + self.child_socket = None - for (name, plugin) in self.parent['plugins'].items(): - if hasattr(plugin, 'cleanup'): + # Call plugins cleanup hooks. + for plugin in self.parent.plugins.values(): + if plugin.cleanup: + self.logger.debug('calling %r plugin cleanup hook' + % plugin.name) plugin.cleanup(self) + logger.info('removed %r' % self) -class UzblEventDaemon(dict): - def __init__(self): - # Init variables and dict keys. - dict.__init__(self, {'uzbls': {}}) - self.running = None +class UzblEventDaemon(object): + def __init__(self): + self.opts = opts self.server_socket = None - self.socket_location = None + self._quit = False + + # Hold uzbl instances + # {child socket: Uzbl instance, ..} + self.uzbls = {} + + # Hold plugins + # {plugin name: Plugin instance, ..} + self.plugins = {} # Register that the event daemon server has started by creating the # pid file. - make_pid_file(CONFIG['pid_file']) + make_pid_file(opts.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)) + # Add signal handlers. + for sigint in [SIGTERM, SIGINT]: + signal(sigint, self.quit) - # 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']) + # Load plugins into self.plugins + self.load_plugins(opts.plugins) - def _create_server_socket(self): - '''Create the event manager daemon socket for uzbl instance duplex - communication.''' + def load_plugins(self, plugins): + '''Load event manager plugins.''' - server_socket = CONFIG['server_socket'] - server_socket = os.path.realpath(os.path.expandvars(server_socket)) - self.socket_location = server_socket + for path in plugins: + logger.debug('loading plugin %r' % path) + (dir, file) = os.path.split(path) + name = file[:-3] if file.lower().endswith('.py') else file - # Delete socket if it exists. - if os.path.exists(server_socket): - os.remove(server_socket) + info = imp.find_module(name, [dir,]) + module = imp.load_module(name, *info) - self.server_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - self.server_socket.bind(server_socket) - self.server_socket.listen(5) + # Check if the plugin has a callable hook. + hooks = filter(callable, [getattr(module, attr, None) \ + for attr in ['init', 'after', 'cleanup']]) + assert hooks, "no hooks in plugin %r" % module + logger.debug('creating plugin instance for %r plugin' % name) + plugin = Plugin(self, name, path, module) + self.plugins[name] = plugin + logger.info('new %r' % plugin) - def _close_server_socket(self): - '''Close and delete the server socket.''' - try: - self.server_socket.close() - self.server_socket = None + def create_server_socket(self): + '''Create the event manager daemon socket for uzbl instance duplex + communication.''' - if os.path.exists(self.socket_location): - os.remove(self.socket_location) + # Close old socket. + self.close_server_socket() - except: - pass + sock = socket(AF_UNIX, SOCK_STREAM) + sock.bind(opts.server_socket) + sock.listen(5) + + self.server_socket = sock + logger.debug('bound server socket to %r' % opts.server_socket) def run(self): '''Main event daemon loop.''' - # Create event daemon socket. - self._create_server_socket() - echo('listening on: %s' % self.socket_location) + logger.debug('entering main loop') + + # Create and listen on the server socket + self.create_server_socket() - if CONFIG['daemon_mode']: - echo('entering daemon mode.') + if opts.daemon_mode: + # Daemonize the process daemonize() - # The pid has changed so update the pid file. - make_pid_file(CONFIG['pid_file']) - # Now listen for incoming connections and or data. - self.listen() + # Update the pid file + make_pid_file(opts.pid_file) - # Clean up. + try: + # Accept incoming connections and listen for incoming data + self.listen() + + except: + if not self._quit: + logger.critical(get_exc()) + + # Clean up and exit self.quit() + logger.debug('exiting main loop') + def listen(self): '''Accept incoming connections and constantly poll instance sockets for incoming data.''' - self.running = True - while self.running: + logger.info('listening on %r' % opts.server_socket) - sockets = [self.server_socket] + self['uzbls'].keys() + # Count accepted connections + connections = 0 - reads, _, errors = select(sockets, [], sockets, 1) + while (self.uzbls or not connections) or (not opts.auto_close): + socks = [self.server_socket] + self.uzbls.keys() + reads, _, errors = select(socks, [], socks, 1) if self.server_socket in reads: - self.accept_connection() reads.remove(self.server_socket) - for client in reads: - self.read_socket(client) + # Accept connection and create uzbl instance. + child_socket = self.server_socket.accept()[0] + self.uzbls[child_socket] = Uzbl(self, child_socket) + connections += 1 + + for uzbl in [self.uzbls[s] for s in reads]: + uzbl.read() + + for uzbl in [self.uzbls[s] for s in errors]: + uzbl.logger.error('socket read error') + uzbl.close() - for client in errors: - error('Unknown error on socket: %r' % client) - self.close_connection(client) + logger.info('auto closing') - def read_socket(self, client): - '''Read data from an instance socket and pass to the uzbl objects - event handler function.''' + def close_server_socket(self): + '''Close and delete the server socket.''' - uzbl = self['uzbls'][client] try: - raw = unicode(client.recv(8192), 'utf-8', 'ignore') + if self.server_socket: + logger.debug('closing server socket') + self.server_socket.close() + self.server_socket = None + + if os.path.exists(opts.server_socket): + logger.info('unlinking %r' % opts.server_socket) + os.unlink(opts.server_socket) except: - print_exc() - raw = None + logger.error(get_exc()) - 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() + def quit(self, sigint=None, *args): + '''Close all instance socket objects, server socket and delete the + pid file.''' - for msg in msgs: - try: - parse_msg(uzbl, msg.strip()) + if sigint == SIGTERM: + logger.critical('caught SIGTERM, exiting') - except: - print_exc() + elif sigint == SIGINT: + logger.critical('caught SIGINT, exiting') + elif not self._quit: + logger.debug('shutting down event manager') - def accept_connection(self): - '''Accept incoming connection to the server socket.''' + self.close_server_socket() - client_socket = self.server_socket.accept()[0] + for uzbl in self.uzbls.values(): + uzbl.close() - uzbl = UzblInstance(self, client_socket) - self['uzbls'][client_socket] = uzbl + del_pid_file(opts.pid_file) + if not self._quit: + logger.info('event manager shut down') + self._quit = True - def close_connection(self, client): - '''Clean up after instance close.''' +def make_pid_file(pid_file): + '''Creates a pid file at `pid_file`, fails silently.''' + + try: + logger.debug('creating pid file %r' % pid_file) + make_dirs(pid_file) + pid = os.getpid() + fileobj = open(pid_file, 'w') + fileobj.write('%d' % pid) + fileobj.close() + logger.info('created pid file %r with pid %d' % (pid_file, pid)) + + except: + logger.error(get_exc()) + + +def del_pid_file(pid_file): + '''Deletes a pid file at `pid_file`, fails silently.''' + + if os.path.isfile(pid_file): try: - if client in self['uzbls']: - uzbl = self['uzbls'][client] - uzbl.close() - del self['uzbls'][client] + logger.debug('deleting pid file %r' % pid_file) + os.remove(pid_file) + logger.info('deleted pid file %r' % pid_file) except: - print_exc() + logger.error(get_exc()) + - if not len(self['uzbls']) and CONFIG['auto_close']: - echo('auto closing event manager.') - self.running = False +def get_pid(pid_file): + '''Reads a pid from pid file `pid_file`, fails None.''' + try: + logger.debug('reading pid file %r' % pid_file) + fileobj = open(pid_file, 'r') + pid = int(fileobj.read()) + fileobj.close() + logger.info('read pid %d from pid file %r' % (pid, pid_file)) + return pid - def quit(self): - '''Close all instance socket objects, server socket and delete the - pid file.''' + except (IOError, ValueError): + logger.error(get_exc()) + return None - echo('shutting down event manager.') - for client in self['uzbls'].keys(): - self.close_connection(client) +def pid_running(pid): + '''Checks if a process with a pid `pid` is running.''' + + try: + os.kill(pid, 0) + except OSError: + return False + else: + return True + + +def term_process(pid): + '''Asks nicely then forces process with pid `pid` to exit.''' + + try: + logger.info('sending SIGTERM to process with pid %r' % pid) + os.kill(pid, SIGTERM) + + except OSError: + logger.error(get_exc()) + + logger.debug('waiting for process with pid %r to exit' % pid) + start = time.time() + while True: + if not pid_running(pid): + logger.debug('process with pid %d exit' % pid) + return True + + if (time.time()-start) > 5: + logger.warning('process with pid %d failed to exit' % pid) + logger.info('sending SIGKILL to process with pid %d' % pid) + try: + os.kill(pid, SIGKILL) + except: + logger.critical(get_exc()) + raise - echo('unlinking: %r' % self.socket_location) - self._close_server_socket() + if (time.time()-start) > 10: + logger.critical('unable to kill process with pid %d' % pid) + raise OSError - echo('deleting pid file: %r' % CONFIG['pid_file']) - del_pid_file(CONFIG['pid_file']) + time.sleep(0.25) def stop_action(): '''Stop the event manager daemon.''' - pid_file = CONFIG['pid_file'] + pid_file = opts.pid_file if not os.path.isfile(pid_file): - return echo('no running daemon found.') + logger.error('could not find running event manager with pid file %r' + % opts.pid_file) + return - 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) + logger.debug('no process with pid %r' % pid) + del_pid_file(pid_file) + return - echo("terminating process with pid: %d" % pid) + logger.debug('terminating process with pid %r' % pid) term_process(pid) - if os.path.isfile(pid_file): - os.remove(pid_file) - - echo('stopped event daemon.') + del_pid_file(pid_file) + logger.info('stopped event manager process with pid %d' % pid) def start_action(): '''Start the event manager daemon.''' - pid_file = CONFIG['pid_file'] + pid_file = opts.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) + logger.error('event manager already started with pid %d' % pid) + return - echo('no process with pid: %d' % pid) - os.remove(pid_file) + logger.info('no process with pid %d' % pid) + del_pid_file(pid_file) - echo('starting event manager.') UzblEventDaemon().run() def restart_action(): '''Restart the event manager daemon.''' - echo('restarting event manager daemon.') stop_action() start_action() def list_action(): - '''List all the plugins being loaded by the event daemon.''' + '''List all the plugins that would be loaded in the current search + dirs.''' - plugins = find_plugins(CONFIG['plugin_dirs']) - dirs = {} + names = {} + for plugin in opts.plugins: + (head, tail) = os.path.split(plugin) + if tail not in names: + names[tail] = plugin - for (plugin, plugin_dir) in plugins.items(): - if plugin_dir not in dirs: - dirs[plugin_dir] = [] + for plugin in sorted(names.values()): + print plugin - dirs[plugin_dir].append(plugin) - for (index, (plugin_dir, plugin_list)) in enumerate(sorted(dirs.items())): - if index: - print +if __name__ == "__main__": + parser = OptionParser('usage: %prog [options] {start|stop|restart|list}') + add = parser.add_option - print "%s:" % plugin_dir - for plugin in sorted(plugin_list): - print " %s" % plugin + add('-v', '--verbose', + dest='verbose', default=2, action='count', + help='increase verbosity') + add('-d', '--plugin-dir', + dest='plugin_dirs', action='append', metavar="DIR", default=[], + help='add extra plugin search dir, same as `-l "DIR/*.py"`') -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.") + add('-l', '--load-plugin', + dest='load_plugins', action='append', metavar="PLUGIN", default=[], + help='load plugin, loads before plugins in search dirs') - PARSER.add_option('-d', '--plugin-dirs', dest='plugin_dirs', action="store", - metavar="DIRS", help="Specify plugin directories in the form of "\ - "'dir1:dir2:dir3'.") + socket_location = os.path.join(CACHE_DIR, 'event_daemon') + add('-s', '--server-socket', + dest='server_socket', metavar="SOCKET", default=socket_location, + help='server AF_UNIX socket location') - PARSER.add_option('-l', '--load-plugins', dest="load", action="store", - metavar="PLUGINS", help="comma separated list of plugins to load") + add('-p', '--pid-file', + metavar="FILE", dest='pid_file', + help='pid file location, defaults to server socket + .pid') - PARSER.add_option('-i', '--ignore-plugins', dest="ignore", action="store", - metavar="PLUGINS", help="comma separated list of plugins to ignore") + add('-n', '--no-daemon', + dest='daemon_mode', action='store_false', default=True, + help='daemonize the process') - PARSER.add_option('-p', '--pid-file', dest='pid', action='store', - metavar='FILE', help="specify pid file location") + add('-a', '--auto-close', + dest='auto_close', action='store_true', default=False, + help='auto close after all instances disconnect') - PARSER.add_option('-s', '--server-socket', dest='socket', action='store', - metavar='SOCKET', help="specify the daemon socket location") + add('-i', '--no-default-dirs', + dest='default_dirs', action='store_false', default=True, + help='ignore the default plugin search dirs') - PARSER.add_option('-n', '--no-daemon', dest="daemon", - action="store_true", help="don't enter daemon mode.") + add('-o', '--log-file', + dest='log_file', metavar='FILE', + help='write logging output to a file, defaults to server socket +' + ' .log') - PARSER.add_option('-a', '--auto-close', dest='autoclose', - action='store_true', help='auto close after all instances disconnect.') + add('-q', '--quiet-events', + dest='print_events', action="store_false", default=True, + help="silence the printing of events to stdout") - (OPTIONS, ARGS) = PARSER.parse_args() + (opts, args) = parser.parse_args() - # init like {start|stop|..} daemon actions dict. - DAEMON_ACTIONS = {'start': start_action, 'stop': stop_action, - 'restart': restart_action, 'list': list_action} + opts.server_socket = expandpath(opts.server_socket) + + # Set default pid file location + if not opts.pid_file: + opts.pid_file = "%s.pid" % opts.server_socket - if not ARGS: - ACTION = 'start' + else: + opts.pid_file = expandpath(opts.pid_file) - elif len(ARGS) == 1: - ACTION = ARGS[0] - if ACTION not in DAEMON_ACTIONS: - raise ArgumentError("unknown argument: %r" % ACTION) + # Set default log file location + if not opts.log_file: + opts.log_file = "%s.log" % opts.server_socket else: - raise ArgumentError("too many arguments: %r" % ARGS) + opts.log_file = expandpath(opts.log_file) + + # Logging setup + log_level = logging.CRITICAL - opts.verbose*10 + + # Console logging handler + ch = logging.StreamHandler() + ch.setLevel(max(log_level+10, 10)) + ch.setFormatter(logging.Formatter( + '%(name)s: %(levelname)s: %(message)s')) + + # File logging handler + fh = logging.FileHandler(opts.log_file, 'w', 'utf-8', 1) + fh.setLevel(max(log_level, 10)) + fh.setFormatter(logging.Formatter( + '[%(created)f] %(name)s: %(levelname)s: %(message)s')) + + # logging.getLogger wrapper which sets the levels and adds the + # file and console handlers automagically + def get_logger(name): + handlers = [ch, fh] + level = [max(log_level, 10),] + logger = logging.getLogger(name) + logger.setLevel(level[0]) + for handler in handlers: + logger.addHandler(handler) + + return logger + + # Get main logger + logger = get_logger(SCRIPTNAME) + logger.info('logging to %r' % opts.log_file) - # parse other flags & options. - if OPTIONS.verbose: - CONFIG['verbose'] = True + plugins = {} - if OPTIONS.plugin_dirs: - PLUGIN_DIRS = [] - for DIR in OPTIONS.plugin_dirs.split(':'): - if not DIR: - continue + # Load all `opts.load_plugins` into the plugins list + for path in opts.load_plugins: + path = expandpath(path) + matches = glob(path) + if not matches: + parser.error('cannot find plugin(s): %r' % path) - PLUGIN_DIRS.append(os.path.realpath(DIR)) + for plugin in matches: + (head, tail) = os.path.split(plugin) + if tail not in plugins: + logger.debug('found plugin: %r' % plugin) + plugins[tail] = plugin - CONFIG['plugin_dirs'] = PLUGIN_DIRS - echo("plugin search dirs: %r" % PLUGIN_DIRS) + else: + logger.debug('ignoring plugin: %r' % plugin) - if OPTIONS.load and OPTIONS.ignore: - error("you can't load and ignore at the same time.") - sys.exit(1) + # Add default plugin locations + if opts.default_dirs: + logger.debug('adding default plugin dirs to plugin dirs list') + opts.plugin_dirs += [os.path.join(DATA_DIR, 'plugins/'), + os.path.join(PREFIX, 'share/uzbl/examples/data/plugins/')] + + else: + logger.debug('ignoring default plugin dirs') - elif OPTIONS.load: - LOAD = CONFIG['plugins_load'] - for PLUGIN in OPTIONS.load.split(','): - if PLUGIN.strip(): - LOAD.append(PLUGIN.strip()) + # Load all plugins in `opts.plugin_dirs` into the plugins list + for dir in opts.plugin_dirs: + dir = expandpath(dir) + logger.debug('searching plugin dir: %r' % dir) + for plugin in glob(os.path.join(dir, '*.py')): + (head, tail) = os.path.split(plugin) + if tail not in plugins: + logger.debug('found plugin: %r' % plugin) + plugins[tail] = plugin - echo('only loading plugin(s): %s' % ', '.join(LOAD)) + else: + logger.debug('ignoring plugin: %r' % plugin) - elif OPTIONS.ignore: - IGNORE = CONFIG['plugins_ignore'] - for PLUGIN in OPTIONS.ignore.split(','): - if PLUGIN.strip(): - IGNORE.append(PLUGIN.strip()) + plugins = plugins.values() - echo('ignoring plugin(s): %s' % ', '.join(IGNORE)) + # Check all the paths in the plugins list are files + for plugin in plugins: + if not os.path.isfile(plugin): + parser.error('plugin not a file: %r' % plugin) - if OPTIONS.autoclose: - CONFIG['auto_close'] = True - echo('will auto close.') + if opts.auto_close: logger.debug('will auto close') + else: logger.debug('will not auto close') - if OPTIONS.pid: - CONFIG['pid_file'] = os.path.realpath(OPTIONS.pid) - echo("pid file location: %r" % CONFIG['pid_file']) + if opts.daemon_mode: logger.debug('will daemonize') + else: logger.debug('will not daemonize') - if OPTIONS.socket: - CONFIG['server_socket'] = os.path.realpath(OPTIONS.socket) - echo("daemon socket location: %s" % CONFIG['server_socket']) + opts.plugins = plugins + + # init like {start|stop|..} daemon actions + daemon_actions = {'start': start_action, 'stop': stop_action, + 'restart': restart_action, 'list': list_action} + + if len(args) == 1: + action = args[0] + if action not in daemon_actions: + parser.error('invalid action: %r' % action) + + elif not args: + logger.warning('no daemon action given, assuming %r' % 'start') + action = 'start' + + else: + parser.error('invalid action argument: %r' % args) - if OPTIONS.daemon: - CONFIG['daemon_mode'] = False + logger.info('daemon action %r' % action) + # Do action + daemon_actions[action]() - # Now {start|stop|...} - DAEMON_ACTIONS[ACTION]() + logger.debug('process CPU time: %f' % time.clock()) |