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