aboutsummaryrefslogtreecommitdiffhomepage
path: root/examples
diff options
context:
space:
mode:
authorGravatar Paweł Zuzelski <pawelz@pld-linux.org>2010-02-17 18:25:49 +0100
committerGravatar Paweł Zuzelski <pawelz@pld-linux.org>2010-02-17 18:25:49 +0100
commit025babb5684e8c7544e1d9c0c899b08387a9d61e (patch)
tree14ffb30c2160284683870e1fedc150d758a47dfc /examples
parentcaa401e1642c49368d61597284defc4f61107798 (diff)
parent7bfb3b5b56e30b157cfb750657de104055fe6da3 (diff)
Merge remote branch 'Dieterbe/experimental' into eFormFiller
Diffstat (limited to 'examples')
-rw-r--r--examples/config/config389
-rw-r--r--examples/config/cookies22
-rw-r--r--examples/data/bookmarks4
-rw-r--r--examples/data/forms/bbs.archlinux.org5
-rw-r--r--examples/data/plugins/bind.py492
-rw-r--r--examples/data/plugins/cmd_expand.py42
-rw-r--r--examples/data/plugins/completion.py206
-rw-r--r--examples/data/plugins/config.py97
-rw-r--r--examples/data/plugins/keycmd.py571
-rw-r--r--examples/data/plugins/mode.py176
-rw-r--r--examples/data/plugins/on_event.py107
-rw-r--r--examples/data/plugins/plugin_template.py76
-rw-r--r--examples/data/plugins/progress_bar.py159
-rwxr-xr-xexamples/data/scripts/auth.py53
-rwxr-xr-xexamples/data/scripts/cookies.sh154
-rwxr-xr-xexamples/data/scripts/download.sh22
-rw-r--r--examples/data/scripts/extedit.js102
-rw-r--r--examples/data/scripts/follow.js269
-rw-r--r--examples/data/scripts/follower.js420
-rwxr-xr-xexamples/data/scripts/formfiller.pl99
-rwxr-xr-xexamples/data/scripts/formfiller.sh62
-rw-r--r--examples/data/scripts/hint.js26
-rwxr-xr-xexamples/data/scripts/history.sh5
-rwxr-xr-xexamples/data/scripts/insert_bookmark.sh16
-rwxr-xr-xexamples/data/scripts/instance-select-wmii.sh54
-rw-r--r--examples/data/scripts/linkfollow.js269
-rwxr-xr-xexamples/data/scripts/load_url_from_bookmarks.sh20
-rwxr-xr-xexamples/data/scripts/load_url_from_history.sh24
-rwxr-xr-xexamples/data/scripts/scheme.py24
-rw-r--r--examples/data/scripts/scroll-percentage.js68
-rwxr-xr-xexamples/data/scripts/session.sh62
-rwxr-xr-xexamples/data/scripts/uzbl-cookie-daemon664
-rwxr-xr-xexamples/data/scripts/uzbl-event-manager837
-rwxr-xr-xexamples/data/scripts/uzbl-tabbed1417
-rwxr-xr-xexamples/data/scripts/uzblcat12
-rw-r--r--examples/data/style.css25
-rw-r--r--examples/data/uzbl.pngbin0 -> 2185 bytes
37 files changed, 7050 insertions, 0 deletions
diff --git a/examples/config/config b/examples/config/config
new file mode 100644
index 0000000..4c63fe7
--- /dev/null
+++ b/examples/config/config
@@ -0,0 +1,389 @@
+# example uzbl config.
+# all settings are optional. you can use uzbl without any config at all (but it won't do much)
+
+set prefix = /usr/local
+
+# === Shortcuts / 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 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
+
+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 -----------------------------------------------
+
+# 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.
+set cookie_handler = talk_to_socket $XDG_CACHE_HOME/uzbl/cookie_daemon_socket
+set scheme_handler = sync_spawn @scripts_dir/scheme.py
+set authentication_handler = sync_spawn @scripts_dir/auth.py
+
+# Open in the same window.
+#set new_window = sh 'echo uri "$8" > $4'
+# Open a link in a new window. equivalent to default behavior
+set new_window = sh 'uzbl-browser -u $8'
+
+# --- Optional dynamic event handlers ----------------------------------------
+
+# Download handler
+@on_event DOWNLOAD_REQUEST spawn @scripts_dir/download.sh %s \@proxy_url
+
+# Load start handler
+@on_event LOAD_START @set_status <span foreground="khaki">wait</span>
+
+# Load commit handlers
+@on_event LOAD_COMMIT @set_status <span foreground="green">recv</span>
+@on_event LOAD_COMMIT script @scripts_dir/scroll-percentage.js
+# Reset the keycmd on navigation
+@on_event LOAD_COMMIT @set_mode
+
+# Load finish handlers
+@on_event LOAD_FINISH @set_status <span foreground="gold">done</span>
+@on_event LOAD_FINISH spawn @scripts_dir/history.sh
+
+# 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
+
+# Example CONFIG_CHANGED event handler
+#@on_event CONFIG_CHANGED print Config changed: %1 = %2
+
+
+# === Behaviour and appearance ===============================================
+
+set show_status = 1
+set status_top = 0
+set status_background = #303030
+
+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 completion_style = foreground="green"
+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 scroll_section = <span foreground="#606060">\@[\@scroll_message]\@</span>
+set uri_section = <span foreground="#99FF66">\@[\@uri]\@</span>
+set name_section = <span foreground="khaki">\@[\@NAME]\@</span>
+set status_section = <span foreground="orange">\@status_message</span>
+set selected_section = <span foreground="#606060">\@[\@SELECTED_URI]\@</span>
+
+set status_format = <span font_family="monospace">@mode_section @keycmd_section @progress_section @uri_section @name_section @status_section @scroll_section @selected_section</span>
+
+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 =
+
+# Or ride those spinnas'
+#@progress format = [%d%s%p]
+#@progress spinner = -\\|/
+#@progress done = -
+#@progress pending =
+
+
+# === Core settings ==========================================================
+
+set useragent = Uzbl (Webkit @WEBKIT_MAJOR.@WEBKIT_MINOR.@WEBKIT_MICRO) (@(+uname -o)@ @(+uname -m)@ [@ARCH_UZBL]) (Commit @COMMIT)
+set fifo_dir = /tmp
+set socket_dir = /tmp
+
+
+# === Key modmapping and ignoring ============================================
+
+#modmap <From> <To>
+@modmap <Control> <Ctrl>
+@modmap <ISO_Left_Tab> <Shift-Tab>
+@modmap <space> <Space>
+
+#modkey_addition <Key1> <Key2> <Result>
+@modkey_addition <Shift> <Ctrl> <Meta>
+@modkey_addition <Shift> <Tab> <Shift-Tab>
+
+#ignore_key <glob>
+@ignore_key <ISO_*>
+@ignore_key <Shift>
+
+
+# === Mode bind aliases ======================================================
+
+# Global binding alias (this is done automatically inside the bind plugin).
+#set bind = @mode_bind global
+
+# Insert mode binding alias
+set ibind = @mode_bind insert
+
+# Command mode binding alias
+set cbind = @mode_bind command
+
+# Non-insert mode bindings alias (ebind for edit-bind).
+set ebind = @mode_bind global,-insert
+
+
+# === Global & keycmd editing binds ==========================================
+
+# Resets keycmd and returns to default mode.
+@bind <Escape> = @set_mode
+
+# Commands for editing and traversing the keycmd.
+@ebind <Return> = event KEYCMD_EXEC_CURRENT
+@ebind <Home> = event SET_CURSOR_POS
+@ebind <End> = event SET_CURSOR_POS -1
+@ebind <Left> = event SET_CURSOR_POS -
+@ebind <Right> = event SET_CURSOR_POS +
+@ebind <BackSpace> = event KEYCMD_BACKSPACE
+@ebind <Delete> = event KEYCMD_DELETE
+@ebind <Tab> = event START_COMPLETION
+# Readline-ish bindings.
+@ebind <Ctrl>w = event KEYCMD_STRIP_WORD
+@ebind <Ctrl>u = event SET_KEYCMD
+@ebind <Ctrl>a = event SET_CURSOR_POS 0
+@ebind <Ctrl>e = event SET_CURSOR_POS -1
+
+# Keycmd injection/append examples.
+#@ebind <Ctrl>su = event INJECT_KEYCMD \@uri
+#@ebind <Ctrl>st = event INJECT_KEYCMD \@title
+#@ebind <Ctrl>du = event APPEND_KEYCMD \@uri
+#@ebind <Ctrl>dt = event APPEND_KEYCMD \@title
+
+
+# === 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)" > $4; fi'
+
+
+# === Keyboard bindings ======================================================
+
+# With this command you can enter in any command at runtime when prefixed with
+# a colon.
+@cbind :_ = %s
+
+# --- Page movement binds ---
+@cbind j = scroll vertical 20
+@cbind k = scroll vertical -20
+@cbind h = scroll horizontal -20
+@cbind l = scroll horizontal 20
+@cbind <Page_Up> = scroll vertical -100%
+@cbind <Page_Down> = scroll vertical 100%
+@cbind << = scroll vertical begin
+@cbind >> = scroll vertical end
+@cbind ^ = scroll horizontal begin
+@cbind $ = scroll horizontal end
+@cbind <Space> = scroll vertical end
+
+# --- Navigation binds ---
+@cbind b = back
+@cbind m = forward
+@cbind S = stop
+@cbind r = reload
+@cbind R = reload_ign_cache
+
+# --- 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 ---
+@cbind t = toggle_status
+
+# --- 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 go<uri:>_ = event NEW_TAB %s
+@cbind gY = sh 'echo "event NEW_TAB `xclip -selection primary -o`" > $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 ---
+@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 ---
+# Set function shortcut
+@cbind s<var:>_<value:>_ = set %1 = %2
+# Exit binding
+@cbind ZZ = exit
+# Dump config to stdout
+@cbind !dump = sh "echo dump_config > $4"
+# Reload config
+@cbind !reload = sh "sed '/^# === Post-load misc commands/,$d' $1 > $4"
+# Uzbl Terminal. TODO explain why this is useful
+@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 ---
+@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
+
+# --- Hard-bound bookmarks ---
+@cbind gh = uri http://www.uzbl.org
+
+# --- Yanking & pasting binds ---
+@cbind yu = sh 'echo -n $6 | xclip'
+@cbind yy = sh 'echo -n $7 | xclip'
+
+# Go the page from primary selection
+@cbind p = sh 'echo "uri `xclip -selection primary -o`" > $4'
+# Go to the page in clipboard
+@cbind P = sh 'echo "uri `xclip -selection clipboard -o`" > $4'
+# Start a new uzbl instance from the page in primary selection
+@cbind 'p = sh 'exec uzbl-browser --uri $(xclip -o)'
+
+# --- 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 ---
+@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) ---
+# Set custom keys you wish to use for navigation. Some common examples:
+set follow_hint_keys = 0123456789
+#set follow_hint_keys = qwerty
+#set follow_hint_keys = asdfghjkl;
+#set follow_hint_keys = thsnd-rcgmvwb/;789aefijkopquxyz234
+@cbind fl* = script @scripts_dir/follow.js '@follow_hint_keys %s'
+
+# --- 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
+set formfiller = spawn @scripts_dir/formfiller
+@cbind za = @{formfiller}.sh
+@cbind ze = @{formfiller}.sh edit
+@cbind zn = @{formfiller}.sh new
+@cbind zl = @{formfiller}.sh load
+# Or the more advanced implementation using perl: (could not get this to run - Dieter)
+@cbind LL = @{formfiller}.pl load
+@cbind LN = @{formfiller}.pl new
+@cbind LE = @{formfiller}.pl edit
+
+# --- External edit script configuration & binds ---
+# Edit form input fields in an external editor (gvim, emacs, urxvt -e vim, ..)
+set external_editor = gvim
+#set external_editor = xterm -e vim
+@cbind E = script @scripts_dir/extedit.js
+# And add menu option.
+menu_editable_add Open in @external_editor = script @scripts_dir/extedit.js
+
+# --- 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"'
+
+
+# === 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 =====================================================
+
+# Define some mode specific uzbl configurations.
+set command = @mode_config command
+set insert = @mode_config insert
+set stack = @mode_config stack
+
+# Command mode config.
+@command keycmd_style = foreground="red"
+@command status_background = #202020
+@command mode_indicator = Cmd
+
+# Insert mode config.
+@insert status_background = #303030
+@insert mode_indicator = Ins
+
+# Multi-stage-binding mode config.
+@stack keycmd_events = 1
+@stack modcmd_updates = 1
+@stack forward_keys = 0
+@stack keycmd_style = foreground="red"
+@stack prompt_style = foreground="#888" weight="light"
+@stack status_background = #202020
+@stack mode_indicator = Bnd
+
+set default_mode = command
+
+
+# === Post-load misc commands ===============================================
+
+# Set the "home" page.
+set uri = uzbl.org/doesitwork/@COMMIT
diff --git a/examples/config/cookies b/examples/config/cookies
new file mode 100644
index 0000000..9b7374a
--- /dev/null
+++ b/examples/config/cookies
@@ -0,0 +1,22 @@
+# This file demonstrates how one *could* manage his cookies. this file is used by the example cookie handler script.
+# stick to this format.
+# trusted -> always store what we get, send what we have (TODO: by default, or when requested?)
+# deny -> deny storing + sending
+
+# if you don't like to edit this file manually, you could even write a script that adds/removes entries using sed, and call the script from uzbl with a keybind...
+
+
+TRUSTED
+bbs.archlinux.org
+archlinux.org
+linux.com
+
+
+
+
+DENY
+www.icanhascheezburger.com
+
+
+
+# rest -> ask \ No newline at end of file
diff --git a/examples/data/bookmarks b/examples/data/bookmarks
new file mode 100644
index 0000000..13fcd48
--- /dev/null
+++ b/examples/data/bookmarks
@@ -0,0 +1,4 @@
+http://www.archlinux.org linux arch
+http://www.uzbl.org uzbl browser
+http://dieter.plaetinck.be uzbl
+http://www.icanhascheezburger.com lolcats fun
diff --git a/examples/data/forms/bbs.archlinux.org b/examples/data/forms/bbs.archlinux.org
new file mode 100644
index 0000000..73c1539
--- /dev/null
+++ b/examples/data/forms/bbs.archlinux.org
@@ -0,0 +1,5 @@
+form_sent:
+redirect_url:
+req_username: <your username>
+req_password: <password>
+login:
diff --git a/examples/data/plugins/bind.py b/examples/data/plugins/bind.py
new file mode 100644
index 0000000..a1a5d89
--- /dev/null
+++ b/examples/data/plugins/bind.py
@@ -0,0 +1,492 @@
+'''Plugin provides support for binds in uzbl.
+
+For example:
+ event BIND ZZ = exit -> bind('ZZ', 'exit')
+ event BIND o _ = uri %s -> bind('o _', 'uri %s')
+ event BIND fl* = sh 'echo %s' -> bind('fl*', "sh 'echo %s'")
+
+And it is also possible to execute a function on activation:
+ bind('DD', myhandler)
+'''
+
+import sys
+import re
+import pprint
+
+# Hold the bind dicts for each uzbl instance.
+UZBLS = {}
+
+# Commonly used regular expressions.
+MOD_START = re.compile('^<([A-Z][A-Za-z0-9-_]*)>').match
+# Matches <x:y>, <'x':y>, <:'y'>, <x!y>, <'x'!y>, ...
+PROMPTS = '<(\"[^\"]*\"|\'[^\']*\'|[^:!>]*)(:|!)(\"[^\"]*\"|\'[^\']*\'|[^>]*)>'
+FIND_PROMPTS = re.compile(PROMPTS).split
+VALID_MODE = re.compile('^(-|)[A-Za-z0-9][A-Za-z0-9_]*$').match
+
+# For accessing a bind glob stack.
+ON_EXEC, HAS_ARGS, MOD_CMD, GLOB, MORE = range(5)
+
+
+# Custom errors.
+class ArgumentError(Exception): pass
+
+
+class Bindlet(object):
+ '''Per-instance bind status/state tracker.'''
+
+ def __init__(self, uzbl):
+ self.binds = {'global': {}}
+ self.uzbl = uzbl
+ self.depth = 0
+ self.args = []
+ self.last_mode = None
+ self.after_cmds = None
+ self.stack_binds = []
+
+ # A subset of the global mode binds containing non-stack and modkey
+ # activiated binds for use in the stack mode.
+ self.globals = []
+
+
+ def __getitem__(self, key):
+ return self.get_binds(key)
+
+
+ def reset(self):
+ '''Reset the tracker state and return to last mode.'''
+
+ self.depth = 0
+ self.args = []
+ self.after_cmds = None
+ self.stack_binds = []
+
+ if self.last_mode:
+ mode, self.last_mode = self.last_mode, None
+ self.uzbl.set_mode(mode)
+
+ self.uzbl.set('keycmd_prompt')
+
+
+ def stack(self, bind, args, depth):
+ '''Enter or add new bind in the next stack level.'''
+
+ if self.depth != depth:
+ if bind not in self.stack_binds:
+ self.stack_binds.append(bind)
+
+ return
+
+ current_mode = self.uzbl.get_mode()
+ if current_mode != 'stack':
+ self.last_mode = current_mode
+ self.uzbl.set_mode('stack')
+
+ self.stack_binds = [bind,]
+ self.args += args
+ self.depth += 1
+ self.after_cmds = bind.prompts[depth]
+
+
+ def after(self):
+ '''If a stack was triggered then set the prompt and default value.'''
+
+ if self.after_cmds is None:
+ return
+
+ (prompt, is_cmd, set), self.after_cmds = self.after_cmds, None
+
+ self.uzbl.clear_keycmd()
+ if prompt:
+ self.uzbl.set('keycmd_prompt', prompt)
+
+ if set and is_cmd:
+ self.uzbl.send(set)
+
+ elif set and not is_cmd:
+ self.uzbl.send('event SET_KEYCMD %s' % set)
+
+
+ def get_binds(self, mode=None):
+ '''Return the mode binds + globals. If we are stacked then return
+ the filtered stack list and modkey & non-stack globals.'''
+
+ if mode is None:
+ mode = self.uzbl.get_mode()
+
+ if not mode:
+ mode = 'global'
+
+ if self.depth:
+ return self.stack_binds + self.globals
+
+ globals = self.binds['global']
+ if mode not in self.binds or mode == 'global':
+ return filter(None, globals.values())
+
+ binds = dict(globals.items() + self.binds[mode].items())
+ return filter(None, binds.values())
+
+
+ def add_bind(self, mode, glob, bind=None):
+ '''Insert (or override) a bind into the mode bind dict.'''
+
+ if mode not in self.binds:
+ self.binds[mode] = {glob: bind}
+ return
+
+ binds = self.binds[mode]
+ binds[glob] = bind
+
+ if mode == 'global':
+ # Regen the global-globals list.
+ self.globals = []
+ for bind in binds.values():
+ if bind is not None and bind.is_global:
+ self.globals.append(bind)
+
+
+def add_instance(uzbl, *args):
+ UZBLS[uzbl] = Bindlet(uzbl)
+
+
+def del_instance(uzbl, *args):
+ if uzbl in UZBLS:
+ del UZBLS[uzbl]
+
+
+def get_bindlet(uzbl):
+ '''Return the bind tracklet for the given uzbl instance.'''
+
+ if uzbl not in UZBLS:
+ add_instance(uzbl)
+
+ return UZBLS[uzbl]
+
+
+def ismodbind(glob):
+ '''Return True if the glob specifies a modbind.'''
+
+ return bool(MOD_START(glob))
+
+
+def split_glob(glob):
+ '''Take a string of the form "<Mod1><Mod2>cmd _" and return a list of the
+ modkeys in the glob and the command.'''
+
+ mods = set()
+ while True:
+ match = MOD_START(glob)
+ if not match:
+ break
+
+ end = match.span()[1]
+ mods.add(glob[:end])
+ glob = glob[end:]
+
+ return (mods, glob)
+
+
+def unquote(str):
+ '''Remove quotation marks around string.'''
+
+ if str and str[0] == str[-1] and str[0] in ['"', "'"]:
+ str = str[1:-1]
+
+ return str
+
+
+class Bind(object):
+
+ # Class attribute to hold the number of Bind classes created.
+ counter = [0,]
+
+ def __init__(self, glob, handler, *args, **kargs):
+ self.is_callable = callable(handler)
+ self._repr_cache = None
+
+ if not glob:
+ raise ArgumentError('glob cannot be blank')
+
+ if self.is_callable:
+ self.function = handler
+ self.args = args
+ self.kargs = kargs
+
+ elif kargs:
+ raise ArgumentError('cannot supply kargs for uzbl commands')
+
+ elif hasattr(handler, '__iter__'):
+ self.commands = handler
+
+ else:
+ self.commands = [handler,] + list(args)
+
+ self.glob = glob
+
+ # Assign unique id.
+ self.counter[0] += 1
+ self.bid = self.counter[0]
+
+ self.split = split = FIND_PROMPTS(glob)
+ self.prompts = []
+ for (prompt, cmd, set) in zip(split[1::4], split[2::4], split[3::4]):
+ prompt, set = map(unquote, [prompt, set])
+ cmd = True if cmd == '!' else False
+ if prompt and prompt[-1] != ":":
+ prompt = "%s:" % prompt
+
+ self.prompts.append((prompt, cmd, set))
+
+ # Check that there is nothing like: fl*<int:>*
+ for glob in split[:-1:4]:
+ if glob.endswith('*'):
+ msg = "token '*' not at the end of a prompt bind: %r" % split
+ raise SyntaxError(msg)
+
+ # Check that there is nothing like: fl<prompt1:><prompt2:>_
+ for glob in split[4::4]:
+ if not glob:
+ msg = 'found null segment after first prompt: %r' % split
+ raise SyntaxError(msg)
+
+ stack = []
+ for (index, glob) in enumerate(reversed(split[::4])):
+ # Is the binding a MODCMD or KEYCMD:
+ mod_cmd = ismodbind(glob)
+
+ # Do we execute on UPDATES or EXEC events?
+ on_exec = True if glob[-1] in ['!', '_'] else False
+
+ # Does the command take arguments?
+ has_args = True if glob[-1] in ['*', '_'] else False
+
+ glob = glob[:-1] if has_args or on_exec else glob
+ mods, glob = split_glob(glob)
+ stack.append((on_exec, has_args, mods, glob, index))
+
+ self.stack = list(reversed(stack))
+ self.is_global = (len(self.stack) == 1 and self.stack[0][MOD_CMD])
+
+
+ def __getitem__(self, depth):
+ '''Get bind info at a depth.'''
+
+ if self.is_global:
+ return self.stack[0]
+
+ return self.stack[depth]
+
+
+ def __repr__(self):
+ if self._repr_cache:
+ return self._repr_cache
+
+ args = ['glob=%r' % self.glob, 'bid=%d' % self.bid]
+
+ if self.is_callable:
+ args.append('function=%r' % self.function)
+ if self.args:
+ args.append('args=%r' % self.args)
+
+ if self.kargs:
+ args.append('kargs=%r' % self.kargs)
+
+ else:
+ cmdlen = len(self.commands)
+ cmds = self.commands[0] if cmdlen == 1 else self.commands
+ args.append('command%s=%r' % ('s' if cmdlen-1 else '', cmds))
+
+ self._repr_cache = '<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 = []
+ cmd_expand = uzbl.cmd_expand
+ for cmd in bind.commands:
+ cmd = cmd_expand(cmd, args)
+ uzbl.send(cmd)
+
+
+def mode_bind(uzbl, modes, glob, handler=None, *args, **kargs):
+ '''Add a mode bind.'''
+
+ bindlet = get_bindlet(uzbl)
+
+ if not hasattr(modes, '__iter__'):
+ modes = unicode(modes).split(',')
+
+ # Sort and filter binds.
+ modes = filter(None, map(unicode.strip, modes))
+
+ if callable(handler) or (handler is not None and handler.strip()):
+ bind = Bind(glob, handler, *args, **kargs)
+
+ else:
+ bind = None
+
+ for mode in modes:
+ if not VALID_MODE(mode):
+ raise NameError('invalid mode name: %r' % mode)
+
+ for mode in modes:
+ if mode[0] == '-':
+ mode, bind = mode[1:], None
+
+ bindlet.add_bind(mode, glob, bind)
+ uzbl.event('ADDED_MODE_BIND', mode, glob, bind)
+
+
+def bind(uzbl, glob, handler, *args, **kargs):
+ '''Legacy bind function.'''
+
+ mode_bind(uzbl, 'global', glob, handler, *args, **kargs)
+
+
+def parse_mode_bind(uzbl, args):
+ '''Parser for the MODE_BIND event.
+
+ Example events:
+ MODE_BIND <mode> <bind> = <command>
+ MODE_BIND command o<location:>_ = uri %s
+ MODE_BIND insert,command <BackSpace> = ...
+ MODE_BIND global ... = ...
+ MODE_BIND global,-insert ... = ...
+ '''
+
+ if not args:
+ raise ArgumentError('missing bind arguments')
+
+ split = map(unicode.strip, args.split(' ', 1))
+ if len(split) != 2:
+ raise ArgumentError('missing mode or bind section: %r' % args)
+
+ modes, args = split[0].split(','), split[1]
+ split = map(unicode.strip, args.split('=', 1))
+ if len(split) != 2:
+ raise ArgumentError('missing delimiter in bind section: %r' % args)
+
+ glob, command = split
+ mode_bind(uzbl, modes, glob, command)
+
+
+def parse_bind(uzbl, args):
+ '''Legacy parsing of the BIND event and conversion to the new format.
+
+ Example events:
+ request BIND <bind> = <command>
+ request BIND o<location:>_ = uri %s
+ request BIND <BackSpace> = ...
+ request BIND ... = ...
+ '''
+
+ parse_mode_bind(uzbl, "global %s" % args)
+
+
+def mode_changed(uzbl, mode):
+ '''Clear the stack on all non-stack mode changes.'''
+
+ if mode != 'stack':
+ get_bindlet(uzbl).reset()
+
+
+def match_and_exec(uzbl, bind, depth, keylet, bindlet):
+ (on_exec, has_args, mod_cmd, glob, more) = bind[depth]
+ cmd = keylet.modcmd if mod_cmd else keylet.keycmd
+
+ if mod_cmd and keylet.held != mod_cmd:
+ return False
+
+ if has_args:
+ if not cmd.startswith(glob):
+ return False
+
+ args = [cmd[len(glob):],]
+
+ elif cmd != glob:
+ return False
+
+ else:
+ args = []
+
+ if bind.is_global or (not more and depth == 0):
+ exec_bind(uzbl, bind, *args)
+ if not has_args:
+ uzbl.clear_current()
+
+ return True
+
+ elif more:
+ bindlet.stack(bind, args, depth)
+ (on_exec, has_args, mod_cmd, glob, more) = bind[depth+1]
+ if not on_exec and has_args and not glob and not more:
+ exec_bind(uzbl, bind, *(args+['',]))
+
+ return False
+
+ args = bindlet.args + args
+ exec_bind(uzbl, bind, *args)
+ if not has_args or on_exec:
+ uzbl.set_mode()
+ bindlet.reset()
+ uzbl.clear_current()
+
+ return True
+
+
+def key_event(uzbl, keylet, mod_cmd=False, on_exec=False):
+ bindlet = get_bindlet(uzbl)
+ depth = bindlet.depth
+ for bind in bindlet.get_binds():
+ t = bind[depth]
+ if (bool(t[MOD_CMD]) != mod_cmd) or (t[ON_EXEC] != on_exec):
+ continue
+
+ if match_and_exec(uzbl, bind, depth, keylet, bindlet):
+ return
+
+ bindlet.after()
+
+ # 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()
+
+
+def init(uzbl):
+ # Event handling hooks.
+ uzbl.connect_dict({
+ 'BIND': parse_bind,
+ 'MODE_BIND': parse_mode_bind,
+ 'MODE_CHANGED': mode_changed,
+ })
+
+ # Connect key related events to the key_event function.
+ events = [['KEYCMD_UPDATE', 'KEYCMD_EXEC'],
+ ['MODCMD_UPDATE', 'MODCMD_EXEC']]
+
+ 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))
+
+ # Function exports to the uzbl object, `function(uzbl, *args, ..)`
+ # becomes `uzbl.function(*args, ..)`.
+ uzbl.export_dict({
+ 'bind': bind,
+ 'mode_bind': mode_bind,
+ 'get_bindlet': get_bindlet,
+ })
diff --git a/examples/data/plugins/cmd_expand.py b/examples/data/plugins/cmd_expand.py
new file mode 100644
index 0000000..3f6ae2b
--- /dev/null
+++ b/examples/data/plugins/cmd_expand.py
@@ -0,0 +1,42 @@
+def escape(str):
+ for (level, char) in [(3, '\\'), (2, "'"), (2, '"'), (1, '@')]:
+ str = str.replace(char, (level * '\\') + char)
+
+ return str
+
+
+def cmd_expand(uzbl, cmd, args):
+ '''Exports a function that provides the following
+ expansions in any uzbl command string:
+
+ %s = replace('%s', ' '.join(args))
+ %r = replace('%r', "'%s'" % escaped(' '.join(args)))
+ %1 = replace('%1', arg[0])
+ %2 = replace('%2', arg[1])
+ %n = replace('%n', arg[n-1])
+ '''
+
+ # Ensure (1) all string representable and (2) correct string encoding.
+ args = map(unicode, args)
+
+ # Direct string replace.
+ if '%s' in cmd:
+ cmd = cmd.replace('%s', ' '.join(args))
+
+ # Escaped and quoted string replace.
+ if '%r' in cmd:
+ cmd = cmd.replace('%r', "'%s'" % escape(' '.join(args)))
+
+ # Arg index string replace.
+ for (index, arg) in enumerate(args):
+ index += 1
+ if '%%%d' % index in cmd:
+ cmd = cmd.replace('%%%d' % index, unicode(arg))
+
+ return cmd
+
+
+def init(uzbl):
+ # Function exports to the uzbl object, `function(uzbl, *args, ..)`
+ # becomes `uzbl.function(*args, ..)`.
+ uzbl.export('cmd_expand', cmd_expand)
diff --git a/examples/data/plugins/completion.py b/examples/data/plugins/completion.py
new file mode 100644
index 0000000..8cea203
--- /dev/null
+++ b/examples/data/plugins/completion.py
@@ -0,0 +1,206 @@
+'''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
+
+# Formats
+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()
+ left_segment = keylet.keycmd[:keylet.cursor]
+ partial = (FIND_SEGMENT(left_segment) + ['',])[0].lstrip()
+ if partial.startswith('set '):
+ return ('@%s' % partial[4:].lstrip(), True)
+
+ return (partial, False)
+
+
+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')
+
+
+def complete_completion(uzbl, partial, hint, set_completion=False):
+ '''Inject the remaining porition of the keyword into the keycmd then stop
+ the completioning.'''
+
+ if set_completion:
+ remainder = "%s = " % hint[len(partial):]
+
+ else:
+ remainder = "%s " % hint[len(partial):]
+
+ uzbl.inject_keycmd(remainder)
+ stop_completion(uzbl)
+
+
+def partial_completion(uzbl, partial, hint):
+ '''Inject a common portion of the hints into the keycmd.'''
+
+ remainder = hint[len(partial):]
+ uzbl.inject_keycmd(remainder)
+
+
+def update_completion_list(uzbl, *args):
+ '''Checks if the user still has a partially completed keyword under his
+ cursor then update the completion hints list.'''
+
+ partial = get_incomplete_keyword(uzbl)[0]
+ if not partial:
+ return stop_completion(uzbl)
+
+ d = get_completion_dict(uzbl)
+ if d['level'] < LIST:
+ return
+
+ hints = [h for h in d['completions'] if h.startswith(partial)]
+ if not hints:
+ return uzbl.set('completion_list')
+
+ j = len(partial)
+ l = [ITEM_FORMAT % (escape(h[:j]), h[j:]) for h in sorted(hints)]
+ uzbl.set('completion_list', LIST_FORMAT % ' '.join(l))
+
+
+def start_completion(uzbl, *args):
+
+ d = get_completion_dict(uzbl)
+ if d['lock']:
+ return
+
+ (partial, set_completion) = get_incomplete_keyword(uzbl)
+ if not partial:
+ return stop_completion(uzbl)
+
+ if d['level'] < COMPLETE:
+ d['level'] += 1
+
+ hints = [h for h in d['completions'] if h.startswith(partial)]
+ if not hints:
+ return
+
+ elif len(hints) == 1:
+ d['lock'] = True
+ complete_completion(uzbl, partial, hints[0], set_completion)
+ d['lock'] = False
+ return
+
+ elif partial in hints and d['level'] == COMPLETE:
+ d['lock'] = True
+ complete_completion(uzbl, partial, partial, set_completion)
+ d['lock'] = False
+ return
+
+ smalllen, smallest = sorted([(len(h), h) for h in hints])[0]
+ common = ''
+ for i in range(len(partial), smalllen):
+ char, same = smallest[i], True
+ for hint in hints:
+ if hint[i] != char:
+ same = False
+ break
+
+ if not same:
+ break
+
+ common += char
+
+ if common:
+ d['lock'] = True
+ partial_completion(uzbl, partial, partial+common)
+ d['lock'] = False
+
+ update_completion_list(uzbl)
+
+
+def add_builtins(uzbl, args):
+ '''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)
+
+
+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)
+
+
+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,
+ })
+
+ # 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,
+ })
diff --git a/examples/data/plugins/config.py b/examples/data/plugins/config.py
new file mode 100644
index 0000000..4a848a3
--- /dev/null
+++ b/examples/data/plugins/config.py
@@ -0,0 +1,97 @@
+import re
+import types
+
+__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)
+
+
+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.'''
+
+ if type(value) == types.BooleanType:
+ value = int(value)
+
+ else:
+ value = unicode(value)
+
+ if not VALIDKEY(key):
+ raise KeyError("%r" % key)
+
+ value = escape(value)
+ if '\n' in value:
+ value = value.replace("\n", "\\n")
+
+ if not force:
+ if config is None:
+ config = get_config(uzbl)
+
+ if key in config and config[key] == value:
+ return
+
+ uzbl.send('set %s = %s' % (key, value))
+
+
+class ConfigDict(dict):
+ def __init__(self, uzbl):
+ self._uzbl = uzbl
+
+ def __setitem__(self, key, value):
+ '''Makes "config[key] = value" a wrapper for the set function.'''
+
+ set(self._uzbl, key, value, config=self)
+
+
+def add_instance(uzbl, *args):
+ UZBLS[uzbl] = ConfigDict(uzbl)
+
+
+def del_instance(uzbl, *args):
+ if uzbl in UZBLS:
+ del uzbl
+
+
+def get_config(uzbl):
+ if uzbl not in UZBLS:
+ add_instance(uzbl)
+
+ return UZBLS[uzbl]
+
+
+def variable_set(uzbl, args):
+ config = get_config(uzbl)
+
+ key, type, value = list(args.split(' ', 2) + ['',])[:3]
+ old = config[key] if key in config else None
+ value = TYPECONVERT[type](value)
+
+ dict.__setitem__(config, key, value)
+
+ if old != value:
+ uzbl.event("CONFIG_CHANGED", key, value)
+
+
+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,
+ })
diff --git a/examples/data/plugins/keycmd.py b/examples/data/plugins/keycmd.py
new file mode 100644
index 0000000..c119077
--- /dev/null
+++ b/examples/data/plugins/keycmd.py
@@ -0,0 +1,571 @@
+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>"
+
+
+def escape(str):
+ for char in ['\\', '@']:
+ str = str.replace(char, '\\'+char)
+
+ return str
+
+
+def uzbl_escape(str):
+ return "@[%s]@" % escape(str) if str else ''
+
+
+class Keylet(object):
+ '''Small per-instance object that tracks all the keys held and characters
+ typed.'''
+
+ def __init__(self):
+ # Modcmd tracking
+ self.held = set()
+ self.ignored = set()
+ self.modcmd = ''
+ self.is_modcmd = False
+
+ # Keycmd tracking
+ self.keycmd = ''
+ self.cursor = 0
+
+ self.modmaps = {}
+ self.ignores = {}
+ self.additions = {}
+
+ # 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(self.held) + self.modcmd
+
+
+ def modmap_key(self, key):
+ '''Make some obscure names for some keys friendlier.'''
+
+ if key in self.modmaps:
+ return self.modmaps[key]
+
+ elif key.endswith('_L') or key.endswith('_R'):
+ # Remove left-right discrimination and try again.
+ return self.modmap_key(key[:-2])
+
+ else:
+ return key
+
+
+ def find_addition(self, modkey):
+ '''Key has just been pressed, check if this key + the held list
+ results in a modkey addition. Return that addition and remove all
+ modkeys that created it.'''
+
+ # Intersection of (held list + modkey) and additions.
+ added = (self.held | set([modkey])) & set(self.additions.keys())
+ for key in added:
+ if key == modkey or modkey in self.additions[key]:
+ self.held -= self.additions[key]
+ return key
+
+ # Held list + ignored list + modkey.
+ modkeys = self.held | self.ignored | set([modkey])
+ for (key, value) in self.additions.items():
+ if modkeys.issuperset(value):
+ self.held -= value
+ return key
+
+ return modkey
+
+
+ def key_ignored(self, key):
+ '''Check if the given key is ignored by any ignore rules.'''
+
+ for (glob, match) in self.ignores.items():
+ if match(key):
+ return True
+
+ return False
+
+
+ def __repr__(self):
+ '''Return a string representation of the keylet.'''
+
+ 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(sorted(self.held)))
+
+ if self.keycmd:
+ l.append('keycmd=%r' % self.get_keycmd())
+
+ self._repr_cache = '<Keylet(%s)>' % ', '.join(l)
+ return self._repr_cache
+
+
+def add_modmap(uzbl, key, map):
+ '''Add modmaps.
+
+ Examples:
+ set modmap = request MODMAP
+ @modmap <Control> <Ctrl>
+ @modmap <ISO_Left_Tab> <Shift-Tab>
+ ...
+
+ Then:
+ @bind <Shift-Tab> = <command1>
+ @bind <Ctrl>x = <command2>
+ ...
+
+ '''
+
+ assert len(key)
+ modmaps = get_keylet(uzbl).modmaps
+
+ if key[0] == "<" and key[-1] == ">":
+ key = key[1:-1]
+
+ modmaps[key] = map
+ uzbl.event("NEW_MODMAP", key, map)
+
+
+def modmap_parse(uzbl, map):
+ '''Parse a modmap definiton.'''
+
+ split = [s.strip() for s in map.split(' ') if s.split()]
+
+ if not split or len(split) > 2:
+ raise Exception('Invalid modmap arugments: %r' % map)
+
+ add_modmap(uzbl, *split)
+
+
+def add_key_ignore(uzbl, glob):
+ '''Add an ignore definition.
+
+ Examples:
+ set ignore_key = request IGNORE_KEY
+ @ignore_key <Shift>
+ @ignore_key <ISO_*>
+ ...
+ '''
+
+ assert len(glob) > 1
+ ignores = get_keylet(uzbl).ignores
+
+ glob = "<%s>" % glob.strip("<> ")
+ restr = glob.replace('*', '[^\s]*')
+ match = re.compile(restr).match
+
+ ignores[glob] = match
+ uzbl.event('NEW_KEY_IGNORE', glob)
+
+
+def add_modkey_addition(uzbl, modkeys, result):
+ '''Add a modkey addition definition.
+
+ Examples:
+ set mod_addition = request MODKEY_ADDITION
+ @mod_addition <Shift> <Control> <Meta>
+ @mod_addition <Left> <Up> <Left-Up>
+ @mod_addition <Right> <Up> <Right-Up>
+ ...
+
+ Then:
+ @bind <Right-Up> = <command1>
+ @bind <Meta>o = <command2>
+ ...
+ '''
+
+ additions = get_keylet(uzbl).additions
+ modkeys = set(modkeys)
+
+ assert len(modkeys) and result and result not in modkeys
+
+ for (existing_result, existing_modkeys) in additions.items():
+ if existing_result != result:
+ assert modkeys != existing_modkeys
+
+ additions[result] = modkeys
+ uzbl.event('NEW_MODKEY_ADDITION', modkeys, result)
+
+
+def modkey_addition_parse(uzbl, modkeys):
+ '''Parse modkey addition definition.'''
+
+ keys = filter(None, map(unicode.strip, modkeys.split(" ")))
+ keys = ['<%s>' % key.strip("<>") for key in keys if key.strip("<>")]
+
+ assert len(keys) > 1
+ 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.keycmd = ''
+ k.cursor = 0
+ k._repr_cache = False
+ uzbl.set('keycmd')
+ uzbl.set('raw_keycmd')
+ uzbl.event('KEYCMD_CLEARED')
+
+
+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.ignored = set()
+ k.held = set()
+
+ uzbl.set('modcmd')
+ uzbl.set('raw_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:
+ 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:
+ uzbl.set('modcmd', config=config)
+ uzbl.set('raw_modcmd', config=config)
+
+ elif new_modcmd == modcmd:
+ uzbl.set('raw_modcmd', escape(modcmd), config=config)
+ uzbl.set('modcmd', MODCMD_FORMAT % uzbl_escape(modcmd),
+ config=config)
+
+ if 'keycmd_events' in config and config['keycmd_events'] != '1':
+ return
+
+ new_keycmd = k.get_keycmd()
+ if not new_keycmd:
+ uzbl.set('keycmd', config=config)
+ uzbl.set('raw_keycmd', config=config)
+
+ 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)
+
+
+def inject_str(str, index, inj):
+ '''Inject a string into string at at given index.'''
+
+ return "%s%s%s" % (str[:index], inj, str[index:])
+
+
+def get_keylet_and_key(uzbl, key, add=True):
+ '''Return the keylet and apply any transformations to the key as defined
+ by the modmapping or modkey addition rules. Return None if the key is
+ ignored.'''
+
+ keylet = get_keylet(uzbl)
+ key = keylet.modmap_key(key)
+ if len(key) == 1:
+ return (keylet, key)
+
+ modkey = "<%s>" % key.strip("<>")
+
+ if keylet.key_ignored(modkey):
+ if add:
+ keylet.ignored.add(modkey)
+
+ elif modkey in keylet.ignored:
+ keylet.ignored.remove(modkey)
+
+ modkey = keylet.find_addition(modkey)
+
+ if keylet.key_ignored(modkey):
+ return (keylet, None)
+
+ return (keylet, modkey)
+
+
+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.'''
+
+ (k, key) = get_keylet_and_key(uzbl, key.strip())
+ if not key:
+ return
+
+ if key.lower() == '<space>' and not k.held and k.keycmd:
+ k.keycmd = inject_str(k.keycmd, k.cursor, ' ')
+ 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':
+ k.keycmd = ''
+ k.cursor = 0
+ uzbl.set('keycmd', config=config)
+ uzbl.set('raw_keycmd', config=config)
+ return
+
+ k.keycmd = inject_str(k.keycmd, k.cursor, key)
+ k.cursor += 1
+
+ elif len(key) > 1:
+ k.is_modcmd = True
+ if key not in k.held:
+ k.held.add(key)
+
+ 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.'''
+
+ (k, key) = get_keylet_and_key(uzbl, key.strip(), add=False)
+
+ if key in k.held:
+ if k.is_modcmd:
+ uzbl.event('MODCMD_EXEC', k)
+
+ k.held.remove(key)
+ 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 inject_keycmd(uzbl, keycmd):
+ '''Allow injecting of a string into the keycmd at the cursor position.'''
+
+ k = get_keylet(uzbl)
+ k.keycmd = inject_str(k.keycmd, k.cursor, keycmd)
+ k._repr_cache = None
+ k.cursor += len(keycmd)
+ update_event(uzbl, k, False)
+
+
+def append_keycmd(uzbl, keycmd):
+ '''Allow appening of a string to the keycmd.'''
+
+ k = get_keylet(uzbl)
+ k.keycmd += keycmd
+ k._repr_cache = None
+ k.cursor = len(k.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.'''
+
+ # Event handling hooks.
+ uzbl.connect_dict({
+ '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,
+ 'KEYCMD_STRIP_WORD': keycmd_strip_word,
+ 'KEY_PRESS': key_press,
+ 'KEY_RELEASE': key_release,
+ 'MODKEY_ADDITION': modkey_addition_parse,
+ 'MODMAP': modmap_parse,
+ 'SET_CURSOR_POS': set_cursor_pos,
+ 'SET_KEYCMD': set_keycmd,
+ })
+
+ # Function exports to the uzbl object, `function(uzbl, *args, ..)`
+ # becomes `uzbl.function(*args, ..)`.
+ uzbl.export_dict({
+ 'add_key_ignore': add_key_ignore,
+ 'add_modkey_addition': add_modkey_addition,
+ 'add_modmap': add_modmap,
+ 'append_keycmd': append_keycmd,
+ 'clear_current': clear_current,
+ 'clear_keycmd': clear_keycmd,
+ 'clear_modcmd': clear_modcmd,
+ 'get_keylet': get_keylet,
+ 'inject_keycmd': inject_keycmd,
+ 'set_cursor_pos': set_cursor_pos,
+ 'set_keycmd': set_keycmd,
+ })
diff --git a/examples/data/plugins/mode.py b/examples/data/plugins/mode.py
new file mode 100644
index 0000000..54d865a
--- /dev/null
+++ b/examples/data/plugins/mode.py
@@ -0,0 +1,176 @@
+import sys
+import re
+
+__export__ = ['set_mode', 'get_mode', 'set_mode_config', 'get_mode_config']
+
+UZBLS = {}
+
+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'}}}
+
+FINDSPACES = re.compile("\s+")
+VALID_KEY = re.compile("^[\w_]+$").match
+
+
+def add_instance(uzbl, *args):
+ UZBLS[uzbl] = dict(DEFAULTS)
+
+
+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:
+ 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
+
+ elif mode_dict['mode'] != mode:
+ mode_dict['mode'] = mode
+ uzbl.event("MODE_CHANGED", mode)
+
+
+def config_changed(uzbl, key, value):
+ '''Check for mode related config changes.'''
+
+ 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])
+
+
+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,
+ })
+
+ # 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,
+ })
diff --git a/examples/data/plugins/on_event.py b/examples/data/plugins/on_event.py
new file mode 100644
index 0000000..b9c504a
--- /dev/null
+++ b/examples/data/plugins/on_event.py
@@ -0,0 +1,107 @@
+'''Plugin provides arbitrary binding of uzbl events to uzbl commands.
+
+Formatting options:
+ %s = space separated string of the arguments
+ %r = escaped and quoted version of %s
+ %1 = argument 1
+ %2 = argument 2
+ %n = argument n
+
+Usage:
+ request ON_EVENT LINK_HOVER set selected_uri = $1
+ --> LINK_HOVER http://uzbl.org/
+ <-- set selected_uri = http://uzbl.org/
+
+ request ON_EVENT CONFIG_CHANGED print Config changed: %1 = %2
+ --> CONFIG_CHANGED selected_uri http://uzbl.org/
+ <-- print Config changed: selected_uri = http://uzbl.org/
+'''
+
+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)
+ event = kargs['on_event']
+ if event not in events:
+ return
+
+ commands = events[event]
+ cmd_expand = uzbl.cmd_expand
+ for cmd in commands:
+ cmd = cmd_expand(cmd, args)
+ uzbl.send(cmd)
+
+
+def on_event(uzbl, event, cmd):
+ '''Add a new event to watch and respond to.'''
+
+ event = event.upper()
+ events = get_on_events(uzbl)
+ if event not in events:
+ uzbl.connect(event, event_handler, on_event=event)
+ events[event] = []
+
+ cmds = events[event]
+ if cmd not in cmds:
+ cmds.append(cmd)
+
+
+def parse_on_event(uzbl, args):
+ '''Parse ON_EVENT events and pass them to the on_event function.
+
+ 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)
+
+ event, cmd = split
+ on_event(uzbl, event, cmd)
+
+
+def init(uzbl):
+ # Event handling hooks.
+ uzbl.connect_dict({
+ 'INSTANCE_EXIT': del_instance,
+ 'INSTANCE_START': add_instance,
+ '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,
+ })
diff --git a/examples/data/plugins/plugin_template.py b/examples/data/plugins/plugin_template.py
new file mode 100644
index 0000000..565a999
--- /dev/null
+++ b/examples/data/plugins/plugin_template.py
@@ -0,0 +1,76 @@
+'''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
new file mode 100644
index 0000000..89ba175
--- /dev/null
+++ b/examples/data/plugins/progress_bar.py
@@ -0,0 +1,159 @@
+import sys
+
+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.
+
+ The current substitution options are:
+ %d = done char * done
+ %p = pending char * remaining
+ %c = percent done
+ %i = int done
+ %s = -\|/ spinner
+ %t = percent pending
+ %o = int pending
+ %r = sprites
+ '''
+
+ prog_config = get_progress_config(uzbl)
+ config = uzbl.get_config()
+
+ if prog is None:
+ prog = prog_config['progress']
+
+ else:
+ prog = int(prog)
+ prog_config['progress'] = prog
+
+ prog_config['updates'] += 1
+ format = prog_config['format']
+ width = prog_config['width']
+
+ # 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
+
+ if '%t' in format or '%o' in format:
+ count = format.count('%t') + format.count('%o')
+ width += (3-len(str(100-prog))) * count
+
+ done = int(((prog/100.0)*width)+0.5)
+ pending = width - done
+
+ if '%d' in format:
+ format = format.replace('%d', prog_config['done']*done)
+
+ if '%p' in format:
+ format = format.replace('%p', prog_config['pending']*pending)
+
+ if '%c' in format:
+ format = format.replace('%c', '%d%%' % prog)
+
+ if '%i' in format:
+ format = format.replace('%i', '%d' % prog)
+
+ if '%t' in format:
+ format = format.replace('%t', '%d%%' % (100-prog))
+
+ if '%o' in format:
+ format = format.replace('%o', '%d' % (100-prog))
+
+ 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)
+
+ if '%r' in format:
+ sprites = prog_config['sprites']
+ sprites = '-' if not sprites else sprites
+ index = int(((prog/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)
+
+
+def init(uzbl):
+ # Event handling hooks.
+ uzbl.connect_dict({
+ 'INSTANCE_EXIT': del_instance,
+ 'INSTANCE_START': add_instance,
+ 'LOAD_COMMIT': reset_progress,
+ 'LOAD_PROGRESS': update_progress,
+ 'PROGRESS_CONFIG': progress_config,
+ })
diff --git a/examples/data/scripts/auth.py b/examples/data/scripts/auth.py
new file mode 100755
index 0000000..4feb90b
--- /dev/null
+++ b/examples/data/scripts/auth.py
@@ -0,0 +1,53 @@
+#!/usr/bin/python
+
+import gtk
+import sys
+
+def responseToDialog(entry, dialog, response):
+ dialog.response(response)
+
+def getText(authInfo, authHost, authRealm):
+ dialog = gtk.MessageDialog(
+ None,
+ gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
+ gtk.MESSAGE_QUESTION,
+ gtk.BUTTONS_OK_CANCEL,
+ None)
+ dialog.set_markup('%s at %s' % (authRealm, authHost))
+
+ login = gtk.Entry()
+ password = gtk.Entry()
+ password.set_visibility(False)
+
+ login.connect("activate", responseToDialog, dialog, gtk.RESPONSE_OK)
+ password.connect("activate", responseToDialog, dialog, gtk.RESPONSE_OK)
+
+ hbox = gtk.HBox();
+
+ vbox_entries = gtk.VBox();
+ vbox_labels = gtk.VBox();
+
+ vbox_labels.pack_start(gtk.Label("Login:"), False, 5, 5)
+ vbox_labels.pack_end(gtk.Label("Password:"), False, 5, 5)
+
+ vbox_entries.pack_start(login)
+ vbox_entries.pack_end(password)
+
+ dialog.format_secondary_markup("Please enter username and password:")
+ hbox.pack_start(vbox_labels, True, True, 0)
+ hbox.pack_end(vbox_entries, True, True, 0)
+
+ dialog.vbox.pack_start(hbox)
+ dialog.show_all()
+ rv = dialog.run()
+
+ output = login.get_text() + "\n" + password.get_text()
+ dialog.destroy()
+ return rv, output
+
+if __name__ == '__main__':
+ rv, output = getText(sys.argv[8], sys.argv[9], sys.argv[10])
+ if (rv == gtk.RESPONSE_OK):
+ print output;
+ else:
+ exit(1)
diff --git a/examples/data/scripts/cookies.sh b/examples/data/scripts/cookies.sh
new file mode 100755
index 0000000..ee2ce51
--- /dev/null
+++ b/examples/data/scripts/cookies.sh
@@ -0,0 +1,154 @@
+#!/bin/sh
+
+set -n;
+
+# THIS IS EXPERIMENTAL AND COULD BE INSECURE !!!!!!
+
+# this is an example bash script of how you could manage your cookies. it is very raw and basic and not as good as uzbl-cookie-daemon
+# we use the cookies.txt format (See http://kb.mozillazine.org/Cookies.txt)
+# This is one textfile with entries like this:
+# kb.mozillazine.org FALSE / FALSE 1146030396 wikiUserID 16993
+# domain alow-read-other-subdomains path http-required expiration name value
+# you probably want your cookies config file in your $XDG_CONFIG_HOME ( eg $HOME/.config/uzbl/cookies)
+# Note. in uzbl there is no strict definition on what a session is. it's YOUR job to clear cookies marked as end_session if you want to keep cookies only valid during a "session"
+# MAYBE TODO: allow user to edit cookie before saving. this cannot be done with zenity :(
+# TODO: different cookie paths per config (eg per group of uzbl instances)
+
+# TODO: correct implementation.
+# see http://curl.haxx.se/rfc/cookie_spec.html
+# http://en.wikipedia.org/wiki/HTTP_cookie
+
+# TODO : check expires= before sending.
+# write sample script that cleans up cookies dir based on expires attribute.
+# TODO: check uri against domain attribute. and path also.
+# implement secure attribute.
+# support blocking or not for 3rd parties
+# http://kb.mozillazine.org/Cookies.txt
+# don't always append cookies, sometimes we need to overwrite
+
+cookie_config=${XDG_CONFIG_HOME:-${HOME}/.config}/uzbl/cookies
+[ "x$cookie_config" = x ] && exit 1
+[ -d "${XDG_DATA_HOME:-${HOME}/.local/share}/uzbl/" ] &&\
+cookie_data=${XDG_DATA_HOME:-${HOME}/.local/share}/uzbl/cookies.txt || exit 1
+
+notifier=
+#notifier=notify-send
+#notify_wrapper () {
+# echo "$@" >> $HOME/cookielog
+#}
+#notifier=notifier_wrapper
+
+# if this variable is set, we will use it to inform you when and which cookies we store, and when/which we send.
+# it's primarily used for debugging
+notifier=
+which zenity &>/dev/null || exit 2
+
+# Example cookie:
+# test_cookie=CheckForPermission; expires=Thu, 07-May-2009 19:17:55 GMT; path=/; domain=.doubleclick.net
+
+# uri=$6
+# uri=${uri/http:\/\/} # strip 'http://' part
+# host=${uri/\/*/}
+action=$8 # GET/PUT
+shift
+host=$9
+shift
+path=$9
+shift
+cookie=$9
+
+field_domain=$host
+field_path=$path
+field_name=
+field_value=
+field_exp='end_session'
+
+notify() {
+ [ -n "$notifier" ] && $notifier "$@"
+}
+
+
+# FOR NOW LETS KEEP IT SIMPLE AND JUST ALWAYS PUT AND ALWAYS GET
+parse_cookie() {
+ IFS=$';'
+ first_pair=1
+ for pair in $cookie
+ do
+ if [ "x$first_pair" = x1 ]
+ then
+ field_name=${pair%%=*}
+ field_value=${pair#*=}
+ first_pair=0
+ else
+ echo "$pair" | read -r pair #strip leading/trailing wite space
+ key=${pair%%=*}
+ val=${pair#*=}
+ [ "$key" == expires ] && field_exp=`date -u -d "$val" +'%s'`
+ # TODO: domain
+ [ "$key" == path ] && field_path=$val
+ fi
+ done
+ unset IFS
+}
+
+# match cookies in cookies.txt against hostname and path
+get_cookie() {
+ path_esc=${path//\//\\/}
+ search="^[^\t]*$host\t[^\t]*\t$path_esc"
+ cookie=`awk "/$search/" $cookie_data 2>/dev/null | tail -n 1`
+ if [ -z "$cookie" ]
+ then
+ notify "Get_cookie: search: $search in $cookie_data -> no result"
+ false
+ else
+ notify "Get_cookie: search: $search in $cookie_data -> result: $cookie"
+ echo "$cookie" | \
+ read domain alow_read_other_subdomains path http_required expiration name \
+ value;
+ cookie="$name=$value"
+ true
+ fi
+}
+
+save_cookie() {
+ if parse_cookie
+ then
+ data="$field_domain\tFALSE\t$field_path\tFALSE\t$field_exp\t$field_name\t$field_value"
+ notify "save_cookie: adding $data to $cookie_data"
+ echo -e "$data" >> $cookie_data
+ else
+ notify "not saving a cookie. since we don't have policies yet, parse_cookie must have returned false. this is a bug"
+ fi
+}
+
+[ "x$action" = xPUT ] && save_cookie
+[ "x$action" = xGET ] && get_cookie && echo "$cookie"
+
+exit
+
+
+# TODO: implement this later.
+# $1 = section (TRUSTED or DENY)
+# $2 =url
+match() {
+ sed -n "/$1/,/^\$/p" $cookie_config 2>/dev/null | grep -q "^$host"
+}
+
+fetch_cookie() {
+ cookie=`cat $cookie_data`
+}
+
+store_cookie() {
+ echo $cookie > $cookie_data
+}
+
+if match TRUSTED $host
+then
+ [ "x$action" = xPUT ] && store_cookie $host
+ [ "x$action" = xGET ] && fetch_cookie && echo "$cookie"
+elif ! match DENY $host
+then
+ [ "x$action" = xPUT ] && cookie=`zenity --entry --title 'Uzbl Cookie handler' --text "Accept this cookie from $host ?" --entry-text="$cookie"` && store_cookie $host
+ [ "x$action" = xGET ] && fetch_cookie && cookie=`zenity --entry --title 'Uzbl Cookie handler' --text "Submit this cookie to $host ?" --entry-text="$cookie"` && echo $cookie
+fi
+exit 0
diff --git a/examples/data/scripts/download.sh b/examples/data/scripts/download.sh
new file mode 100755
index 0000000..1c7d039
--- /dev/null
+++ b/examples/data/scripts/download.sh
@@ -0,0 +1,22 @@
+#!/bin/sh
+# just an example of how you could handle your downloads
+# try some pattern matching on the uri to determine what we should do
+
+# Some sites block the default wget --user-agent..
+GET="wget --user-agent=Firefox"
+
+dest="$HOME"
+url="$8"
+
+http_proxy="$9"
+export http_proxy
+
+test "x$url" = "x" && { echo "you must supply a url! ($url)"; exit 1; }
+
+# only changes the dir for the $get sub process
+if echo "$url" | grep -E '.*\.torrent' >/dev/null;
+then
+ ( cd "$dest"; $GET "$url")
+else
+ ( cd "$dest"; $GET "$url")
+fi
diff --git a/examples/data/scripts/extedit.js b/examples/data/scripts/extedit.js
new file mode 100644
index 0000000..8ed346d
--- /dev/null
+++ b/examples/data/scripts/extedit.js
@@ -0,0 +1,102 @@
+/*
+ * Edit forms in external editor
+ *
+ * (c) 2009, Robert Manea
+ * utf8 functions are (c) by Webtoolkit.info (http://www.webtoolkit.info/)
+ *
+ *
+ * Installation:
+ * - Copy this script to $HOME/.local/share/uzbl/scripts
+ * - Add the following to $HOME/.config/uzbl/config:
+ * @bind E = script @scripts_dir/extedit.js
+ * - Set your preferred editor
+ * set editor = gvim
+ * - non-GUI editors
+ * set editor = xterm -e vim
+ *
+ * Usage:
+ * Select (click) an editable form, go to command mode and hit E
+ *
+*/
+
+
+function utf8_decode ( str_data ) {
+ var tmp_arr = [], i = 0, ac = 0, c1 = 0, c2 = 0, c3 = 0;
+
+ str_data += '';
+
+ while ( i < str_data.length ) {
+ c1 = str_data.charCodeAt(i);
+ if (c1 < 128) {
+ tmp_arr[ac++] = String.fromCharCode(c1);
+ i++;
+ } else if ((c1 > 191) && (c1 < 224)) {
+ c2 = str_data.charCodeAt(i+1);
+ tmp_arr[ac++] = String.fromCharCode(((c1 & 31) << 6) | (c2 & 63));
+ i += 2;
+ } else {
+ c2 = str_data.charCodeAt(i+1);
+ c3 = str_data.charCodeAt(i+2);
+ tmp_arr[ac++] = String.fromCharCode(((c1 & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
+ i += 3;
+ }
+ }
+
+ return tmp_arr.join('');
+}
+
+
+function utf8_encode ( argString ) {
+ var string = (argString+''); // .replace(/\r\n/g, "\n").replace(/\r/g, "\n");
+
+ var utftext = "";
+ var start, end;
+ var stringl = 0;
+
+ start = end = 0;
+ stringl = string.length;
+ for (var n = 0; n < stringl; n++) {
+ var c1 = string.charCodeAt(n);
+ var enc = null;
+
+ if (c1 < 128) {
+ end++;
+ } else if (c1 > 127 && c1 < 2048) {
+ enc = String.fromCharCode((c1 >> 6) | 192) + String.fromCharCode((c1 & 63) | 128);
+ } else {
+ enc = String.fromCharCode((c1 >> 12) | 224) + String.fromCharCode(((c1 >> 6) & 63) | 128) + String.fromCharCode((c1 & 63) | 128);
+ }
+ if (enc !== null) {
+ if (end > start) {
+ utftext += string.substring(start, end);
+ }
+ utftext += enc;
+ start = end = n+1;
+ }
+ }
+
+ if (end > start) {
+ utftext += string.substring(start, string.length);
+ }
+
+ return utftext;
+}
+
+
+(function() {
+ var actelem = document.activeElement;
+
+ if(actelem.type == 'text' || actelem.type == 'textarea') {
+ var editor = Uzbl.run("print @external_editor") || "gvim";
+ var filename = Uzbl.run("print @(mktemp /tmp/uzbl_edit.XXXXXX)@");
+
+ if(actelem.value)
+ Uzbl.run("sh 'echo " + window.btoa(utf8_encode(actelem.value)) + " | base64 -d > " + filename + "'");
+
+ Uzbl.run("sync_sh '" + editor + " " + filename + "'");
+ actelem.value = utf8_decode(window.atob(Uzbl.run("print @(base64 -w 0 " + filename + ")@")));
+
+ Uzbl.run("sh 'rm -f " + filename + "'");
+ }
+
+ })();
diff --git a/examples/data/scripts/follow.js b/examples/data/scripts/follow.js
new file mode 100644
index 0000000..a42447c
--- /dev/null
+++ b/examples/data/scripts/follow.js
@@ -0,0 +1,269 @@
+/* This is the basic linkfollowing script.
+ * Its pretty stable, and you configure which keys to use for hinting
+ *
+ * TODO: Some pages mess around a lot with the zIndex which
+ * lets some hints in the background.
+ * TODO: Some positions are not calculated correctly (mostly
+ * because of uber-fancy-designed-webpages. Basic HTML and CSS
+ * works good
+ * TODO: Still some links can't be followed/unexpected things
+ * happen. Blame some freaky webdesigners ;)
+ */
+
+//Just some shortcuts and globals
+var uzblid = 'uzbl_link_hint';
+var uzbldivid = uzblid + '_div_container';
+var doc = document;
+var win = window;
+var links = document.links;
+var forms = document.forms;
+//Make onlick-links "clickable"
+try {
+ HTMLElement.prototype.click = function() {
+ if (typeof this.onclick == 'function') {
+ this.onclick({
+ type: 'click'
+ });
+ }
+ };
+} catch(e) {}
+//Catch the ESC keypress to stop linkfollowing
+function keyPressHandler(e) {
+ var kC = window.event ? event.keyCode: e.keyCode;
+ var Esc = window.event ? 27 : e.DOM_VK_ESCAPE;
+ if (kC == Esc) {
+ removeAllHints();
+ }
+}
+//Calculate element position to draw the hint
+//Pretty accurate but on fails in some very fancy cases
+function elementPosition(el) {
+ var up = el.offsetTop;
+ var left = el.offsetLeft;
+ var width = el.offsetWidth;
+ var height = el.offsetHeight;
+ while (el.offsetParent) {
+ el = el.offsetParent;
+ up += el.offsetTop;
+ left += el.offsetLeft;
+ }
+ return [up, left, width, height];
+}
+//Calculate if an element is visible
+function isVisible(el) {
+ if (el == doc) {
+ return true;
+ }
+ if (!el) {
+ return false;
+ }
+ if (!el.parentNode) {
+ return false;
+ }
+ if (el.style) {
+ if (el.style.display == 'none') {
+ return false;
+ }
+ if (el.style.visibility == 'hidden') {
+ return false;
+ }
+ }
+ return isVisible(el.parentNode);
+}
+//Calculate if an element is on the viewport.
+function elementInViewport(el) {
+ offset = elementPosition(el);
+ var up = offset[0];
+ var left = offset[1];
+ var width = offset[2];
+ var height = offset[3];
+ return up < window.pageYOffset + window.innerHeight && left < window.pageXOffset + window.innerWidth && (up + height) > window.pageYOffset && (left + width) > window.pageXOffset;
+}
+//Removes all hints/leftovers that might be generated
+//by this script.
+function removeAllHints() {
+ var elements = doc.getElementById(uzbldivid);
+ if (elements) {
+ elements.parentNode.removeChild(elements);
+ }
+}
+//Generate a hint for an element with the given label
+//Here you can play around with the style of the hints!
+function generateHint(el, label) {
+ var pos = elementPosition(el);
+ var hint = doc.createElement('div');
+ hint.setAttribute('name', uzblid);
+ hint.innerText = label;
+ hint.style.display = 'inline';
+ hint.style.backgroundColor = '#B9FF00';
+ hint.style.border = '2px solid #4A6600';
+ hint.style.color = 'black';
+ hint.style.fontSize = '9px';
+ hint.style.fontWeight = 'bold';
+ hint.style.lineHeight = '9px';
+ hint.style.margin = '0px';
+ hint.style.width = 'auto'; // fix broken rendering on w3schools.com
+ hint.style.padding = '1px';
+ hint.style.position = 'absolute';
+ hint.style.zIndex = '1000';
+ // hint.style.textTransform = 'uppercase';
+ hint.style.left = pos[1] + 'px';
+ hint.style.top = pos[0] + 'px';
+ // var img = el.getElementsByTagName('img');
+ // if (img.length > 0) {
+ // hint.style.top = pos[1] + img[0].height / 2 - 6 + 'px';
+ // }
+ hint.style.textDecoration = 'none';
+ // hint.style.webkitBorderRadius = '6px'; // slow
+ // Play around with this, pretty funny things to do :)
+ // hint.style.webkitTransform = 'scale(1) rotate(0deg) translate(-6px,-5px)';
+ return hint;
+}
+//Here we choose what to do with an element if we
+//want to "follow" it. On form elements we "select"
+//or pass the focus, on links we try to perform a click,
+//but at least set the href of the link. (needs some improvements)
+function clickElem(item) {
+ removeAllHints();
+ if (item) {
+ var name = item.tagName;
+ if (name == 'A') {
+ item.click();
+ window.location = item.href;
+ } else if (name == 'INPUT') {
+ var type = item.getAttribute('type').toUpperCase();
+ if (type == 'TEXT' || type == 'FILE' || type == 'PASSWORD') {
+ item.focus();
+ item.select();
+ } else {
+ item.click();
+ }
+ } else if (name == 'TEXTAREA' || name == 'SELECT') {
+ item.focus();
+ item.select();
+ } else {
+ item.click();
+ window.location = item.href;
+ }
+ }
+}
+//Returns a list of all links (in this version
+//just the elements itself, but in other versions, we
+//add the label here.
+function addLinks() {
+ res = [[], []];
+ for (var l = 0; l < links.length; l++) {
+ var li = links[l];
+ if (isVisible(li) && elementInViewport(li)) {
+ res[0].push(li);
+ }
+ }
+ return res;
+}
+//Same as above, just for the form elements
+function addFormElems() {
+ res = [[], []];
+ for (var f = 0; f < forms.length; f++) {
+ for (var e = 0; e < forms[f].elements.length; e++) {
+ var el = forms[f].elements[e];
+ if (el && ['INPUT', 'TEXTAREA', 'SELECT'].indexOf(el.tagName) + 1 && isVisible(el) && elementInViewport(el)) {
+ res[0].push(el);
+ }
+ }
+ }
+ return res;
+}
+//Draw all hints for all elements passed. "len" is for
+//the number of chars we should use to avoid collisions
+function reDrawHints(elems, chars) {
+ removeAllHints();
+ var hintdiv = doc.createElement('div');
+ hintdiv.setAttribute('id', uzbldivid);
+ for (var i = 0; i < elems[0].length; i++) {
+ if (elems[0][i]) {
+ var label = elems[1][i].substring(chars);
+ var h = generateHint(elems[0][i], label);
+ hintdiv.appendChild(h);
+ }
+ }
+ if (document.body) {
+ document.body.appendChild(hintdiv);
+ }
+}
+// pass: number of keys
+// returns: key length
+function labelLength(n) {
+ var oldn = n;
+ var keylen = 0;
+ if(n < 2) {
+ return 1;
+ }
+ n -= 1; // our highest key will be n-1
+ while(n) {
+ keylen += 1;
+ n = Math.floor(n / charset.length);
+ }
+ return keylen;
+}
+// pass: number
+// returns: label
+function intToLabel(n) {
+ var label = '';
+ do {
+ label = charset.charAt(n % charset.length) + label;
+ n = Math.floor(n / charset.length);
+ } while(n);
+ return label;
+}
+// pass: label
+// returns: number
+function labelToInt(label) {
+ var n = 0;
+ var i;
+ for(i = 0; i < label.length; ++i) {
+ n *= charset.length;
+ n += charset.indexOf(label[i]);
+ }
+ return n;
+}
+//Put it all together
+function followLinks(follow) {
+ // if(follow.charAt(0) == 'l') {
+ // follow = follow.substr(1);
+ // charset = 'thsnlrcgfdbmwvz-/';
+ // }
+ var s = follow.split('');
+ var linknr = labelToInt(follow);
+ if (document.body) document.body.setAttribute('onkeyup', 'keyPressHandler(event)');
+ var linkelems = addLinks();
+ var formelems = addFormElems();
+ var elems = [linkelems[0].concat(formelems[0]), linkelems[1].concat(formelems[1])];
+ var len = labelLength(elems[0].length);
+ var oldDiv = doc.getElementById(uzbldivid);
+ var leftover = [[], []];
+ if (s.length == len && linknr < elems[0].length && linknr >= 0) {
+ clickElem(elems[0][linknr]);
+ } else {
+ for (var j = 0; j < elems[0].length; j++) {
+ var b = true;
+ var label = intToLabel(j);
+ var n = label.length;
+ for (n; n < len; n++) {
+ label = charset.charAt(0) + label;
+ }
+ for (var k = 0; k < s.length; k++) {
+ b = b && label.charAt(k) == s[k];
+ }
+ if (b) {
+ leftover[0].push(elems[0][j]);
+ leftover[1].push(label);
+ }
+ }
+ reDrawHints(leftover, s.length);
+ }
+}
+
+//Parse input: first argument is follow keys, second is user input.
+var args = '%s'.split(' ');
+var charset = args[0];
+followLinks(args[1]);
diff --git a/examples/data/scripts/follower.js b/examples/data/scripts/follower.js
new file mode 100644
index 0000000..604b779
--- /dev/null
+++ b/examples/data/scripts/follower.js
@@ -0,0 +1,420 @@
+// A Link Follower for Uzbl.
+// P.C. Shyamshankar <sykora@lucentbeing.com>
+//
+// WARNING: this script depends on the Uzbl object which is now disabled for
+// WARNING security reasons. So the script currently doesn't work but it's
+// WARNING interesting nonetheless
+//
+// Based extensively (like copy-paste) on the follow_numbers.js and
+// linkfollow.js included with uzbl, but modified to be more customizable and
+// extensible.
+//
+// Usage
+// -----
+//
+// First, you'll need to make sure the script is loaded on each page. This can
+// be done with:
+//
+// @on_event LOAD_COMMIT script /path/to/follower.js
+//
+// Then you can bind it to a key:
+//
+// @bind f* = js follower.follow('%s', matchSpec, handler, hintStyler)
+//
+// where matchSpec, handler and hintStyler are parameters which control the
+// operation of follower. If you don't want to customize any further, you can
+// set these to follower.genericMatchSpec, follower.genericHandler and
+// follower.genericHintStyler respectively.
+//
+// For example,
+//
+// @bind f* = js follower.follow('%s', follower.genericMatchSpec, follower.genericHandler, follower.genericHintStyler)
+// @bind F* = js follower.follow('%s', follower.onlyLinksMatchSpec, follower.newPageHandler, follower.newPageHintStyler)
+//
+// In order to make hints disappear when pressing a key (the Escape key, for
+// example), you can do this:
+//
+// @bind <Escape> = js follower.clearHints()
+//
+// If your Escape is already bound to something like command mode, chain it.
+//
+// Alternatively, you can tell your <Escape> key to emit an event, and handle
+// that instead.
+//
+// @bind <Escape> = event ESCAPE
+// @on_event ESCAPE js follower.clearHints()
+//
+// Customization
+// -------------
+//
+// If however you do want to customize, 3 Aspects of the link follower can be
+// customized with minimal pain or alteration to the existing code base:
+//
+// * What elements are hinted.
+// * The style of the hints displayed.
+// * How the hints are handled.
+//
+// In order to customize behavior, write an alternative, and pass that in to
+// follower.follow invocation. You _will_ have to modify this script, but only
+// locally, it beats having to copy the entire script under a new name and
+// modify.
+//
+// TODO:
+// * Whatever all the other TODOs in the file say.
+// * Find out how to do default arguments in Javascript.
+// * Abstract out the hints into a Hint object, make hintables a list of hint
+// objects instead of two lists.
+
+// Helpers
+String.prototype.lpad = function(padding, length) {
+ var padded = this;
+ while (padded.length < length) {
+ padded = padding + padded;
+ }
+
+ return padded;
+}
+
+function Follower() {
+
+ // Globals
+ var uzblID = 'uzbl-follow'; // ID to apply to each hint.
+ var uzblContainerID = 'uzbl-follow-container'; // ID to apply to the div containing hints.
+
+ // Translation table, used to display something other than numbers as hint
+ // labels. Typically set to the ten keys of the home row.
+ //
+ // Must have exactly 10 elements.
+ //
+ // I haven't parameterized this, to make it customizable. Should I? Do
+ // people really use more than one set of keys at a time?
+ var translation = ["a", "r", "s", "t", "d", "h", "n", "e", "i", "o"];
+
+ // MatchSpecs
+ // These are XPath expressions which indicate which elements will be hinted.
+ // Use multiple expressions for different situations, like hinting only form
+ // elements, or only links, etc.
+ //
+ // TODO: Check that these XPath expressions are correct, and optimize/make
+ // them more elegant. Preferably by someone who actually knows XPath, unlike
+ // me.
+
+ // Vimperator default (copy-pasted, I never used vimperator).
+ this.genericMatchSpec = " //*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or @class='lk' or @role='link' or @href] | //input[not(@type='hidden')] | //a | //area | //iframe | //textarea | //button | //select";
+
+ // Matches only links, suitable for opening in a new instance (I think).
+ this.onlyLinksMatchSpec = " //*[@href] | //a | //area";
+
+ // Follow Handlers
+ // These decide how an element should be 'followed'. The handler is passed
+ // the element in question.
+
+ // Generic Handler, opens links in the same instance, emits the FORM_ACTIVE
+ // event if a form element was chosen. Also clears the keycmd.
+ this.genericHandler = function(node) {
+ if (node) {
+ if (window.itemClicker != undefined) {
+ window.itemClicker(node);
+ } else {
+ var tag = node.tagName.toLowerCase();
+ if (tag == 'a') {
+ node.click();
+ window.location = node.href;
+ } else if (tag == 'input') {
+ var inputType = node.getAttribute('type');
+ if (inputType == undefined)
+ inputType = 'text';
+
+ inputType = inputType.toLowerCase();
+
+ if (inputType == 'text' || inputType == 'file' || inputType == 'password') {
+ node.focus();
+ node.select();
+ } else {
+ node.click();
+ }
+ Uzbl.run("event FORM_ACTIVE");
+ } else if (tag == 'textarea'|| tag == 'select') {
+ node.focus();
+ node.select();
+ Uzbl.run("event FORM_ACTIVE");
+ } else {
+ node.click();
+ if ((node.href != undefined) && node.href)
+ window.location = node.href;
+ }
+ }
+ }
+ Uzbl.run("event SET_KEYCMD");
+ }
+
+ // Handler to open links in a new page. The rest is the same as before.
+ this.newPageHandler = function(node) {
+ if (node) {
+ if (window.itemClicker != undefined) {
+ window.itemClicker(node);
+ } else {
+ var tag = node.tagName.toLowerCase();
+ if (tag == 'a') {
+ node.click();
+ Uzbl.run("@new_window " + node.href);
+ } else if (tag == 'input') {
+ var inputType = node.getAttribute('type');
+ if (inputType == undefined)
+ inputType = 'text';
+
+ inputType = inputType.toLowerCase();
+
+ if (inputType == 'text' || inputType == 'file' || inputType == 'password') {
+ node.focus();
+ node.select();
+ } else {
+ node.click();
+ }
+ Uzbl.run("event FORM_ACTIVE");
+ } else if (tag == 'textarea'|| tag == 'select') {
+ node.focus();
+ node.select();
+ Uzbl.run("event FORM_ACTIVE");
+ } else {
+ node.click();
+ if ((node.href != undefined) && node.href)
+ window.location = node.href;
+ }
+ }
+ }
+ Uzbl.run("event SET_KEYCMD");
+ };
+
+ // Hint styling.
+ // Pretty much any attribute of the hint object can be modified here, but it
+ // was meant to change the styling. Useful to differentiate between hints
+ // with different handlers.
+ //
+ // Hint stylers are applied at the end of hint creation, so that they
+ // override the defaults.
+
+ this.genericHintStyler = function(hint) {
+ hint.style.backgroundColor = '#AAAAAA';
+ hint.style.border = '2px solid #4A6600';
+ hint.style.color = 'black';
+ hint.style.fontSize = '10px';
+ hint.style.fontWeight = 'bold';
+ hint.style.lineHeight = '12px';
+ return hint;
+ };
+
+ this.newPageHintStyler = function(hint) {
+ hint.style.backgroundColor = '#FFCC00';
+ hint.style.border = '2px solid #4A6600';
+ hint.style.color = 'black';
+ hint.style.fontSize = '10px';
+ hint.style.fontWeight = 'bold';
+ hint.style.lineHeight = '12px';
+ return hint;
+ };
+
+ // Beyond lies a jungle of pasta and verbosity.
+
+ // Translate a numeric label using the translation table.
+ function translate(digitLabel, translationTable) {
+ translatedLabel = '';
+ for (var i = 0; i < digitLabel.length; i++) {
+ translatedLabel += translationTable[digitLabel.charAt(i)];
+ }
+
+ return translatedLabel;
+ }
+
+ function computeElementPosition(element) {
+ var up = element.offsetTop;
+ var left = element.offsetLeft;
+ var width = element.offsetWidth;
+ var height = element.offsetHeight;
+
+ while (element.offsetParent) {
+ element = element.offsetParent;
+ up += element.offsetTop;
+ left += element.offsetLeft;
+ }
+
+ return {up: up, left: left, width: width, height: height};
+ }
+
+ // Pretty much copy-pasted from every other link following script.
+ function isInViewport(element) {
+ offset = computeElementPosition(element);
+
+ var up = offset.up;
+ var left = offset.left;
+ var width = offset.width;
+ var height = offset.height;
+
+ return up < window.pageYOffset + window.innerHeight &&
+ left < window.pageXOffset + window.innerWidth &&
+ (up + height) > window.pageYOffset &&
+ (left + width) > window.pageXOffset;
+ }
+
+ function isVisible(element) {
+ if (element == document) {
+ return true;
+ }
+
+ if (!element){
+ return false;
+ }
+
+ if (element.style) {
+ if (element.style.display == 'none' || element.style.visibiilty == 'hidden') {
+ return false;
+ }
+ }
+
+ return isVisible(element.parentNode);
+ }
+
+ function generateHintContainer() {
+ var container = document.getElementById(uzblContainerID);
+ if (container) {
+ container.parentNode.removeChild(container);
+ }
+
+ container = document.createElement('div');
+ container.id = uzblContainerID;
+
+ if (document.body) {
+ document.body.appendChild(container);
+ }
+ return container;
+ }
+
+ // Generate everything that is to be hinted, as per the given matchSpec.
+ // hintables[0] refers to the items, hintables[1] to their labels.
+ function generateHintables(matchSpec) {
+ var hintables = [[], []];
+
+ var itemsFromXPath = document.evaluate(matchSpec, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
+
+ for (var i = 0; i < itemsFromXPath.snapshotLength; ++i) {
+ var element = itemsFromXPath.snapshotItem(i);
+ if (element && isVisible(element) && isInViewport(element)) {
+ hintables[0].push(element);
+ }
+ }
+
+ // Assign labels to each hintable. Can't be combined with the previous
+ // step, because we didn't know how many there were at that time.
+ var hintLength = hintables.length;
+ for (var i = 0; i < hintables[0].length; ++i) {
+ var code = translate(i.toString(), translation);
+ hintables[1].push(code.lpad(translation[0], hintLength));
+ }
+
+ return hintables;
+ }
+
+ // Filter the hintables based on input from the user. Makes the screen less
+ // cluttered after the user has typed some prefix of hint labels.
+ function filterHintables(hintables, target) {
+ var filtered = [[], []];
+
+ var targetPattern = new RegExp("^" + target);
+
+ for (var i = 0; i < hintables[0].length; i++) {
+ if (hintables[1][i].match(targetPattern)) {
+ filtered[0].push(hintables[0][i]);
+ filtered[1].push(hintables[1][i].substring(target.length));
+ }
+ }
+
+ return filtered;
+ }
+
+ // TODO make this use the container variable from main, instead of searching
+ // for it?
+ function clearHints() {
+ var container = document.getElementById(uzblContainerID);
+ if (container) {
+ container.parentNode.removeChild(container);
+ }
+ }
+
+ // So that we can offer this as a separate function.
+ this.clearHints = clearHints;
+
+ function makeHint(node, code, styler) {
+ var position = computeElementPosition(node);
+ var hint = document.createElement('div');
+
+ hint.name = uzblID;
+ hint.innerText = code;
+ hint.style.display = 'inline';
+
+ hint.style.margin = '0px';
+ hint.style.padding = '1px';
+ hint.style.position = 'absolute';
+ hint.style.zIndex = '10000';
+
+ hint.style.left = position.left + 'px';
+ hint.style.top = position.up + 'px';
+
+ var img = node.getElementsByTagName('img');
+ if (img.length > 0) {
+ hint.style.left = position.left + img[0].width / 2 + 'px';
+ }
+
+ hint.style.textDecoration = 'none';
+ hint.style.webkitBorderRadius = '6px';
+ hint.style.webkitTransform = 'scale(1) rotate(0deg) translate(-6px, -5px)';
+
+ hint = styler(hint); // So that custom hint stylers can override the above.
+ return hint;
+ }
+
+
+ function drawHints(container, hintables, styler) {
+ for (var i = 0; i < hintables[0].length; i++) {
+ hint = makeHint(hintables[0][i], hintables[1][i], styler);
+ container.appendChild(hint);
+ }
+
+ if (document.body) {
+ document.body.appendChild(container);
+ }
+ }
+
+ // The main hinting function. I don't know how to do default values to
+ // functions, so all arguments must be specified. Use generics if you must.
+ this.follow = function(target, matchSpec, handler, hintStyler) {
+ var container = generateHintContainer(); // Get a container to hold all hints.
+ var allHintables = generateHintables(matchSpec); // Get all items that can be hinted.
+ hintables = filterHintables(allHintables, target); // Filter them based on current input.
+
+ clearHints(); // Clear existing hints, if any.
+
+ if (hintables[0].length == 0) {
+ // Nothing was hinted, user pressed an unknown key, maybe?
+ // Do nothing.
+ } else if (hintables[0].length == 1) {
+ handler(hintables[0][0]); // Only one hint remains, handle it.
+ } else {
+ drawHints(container, hintables, hintStyler); // Draw whatever hints remain.
+ }
+
+ return;
+ };
+}
+
+// Make on-click links clickable.
+try {
+ HTMLElement.prototype.click = function() {
+ if (typeof this.onclick == 'function') {
+ this.onclick({
+ type: 'click'
+ });
+ }
+ };
+} catch(e) {}
+
+follower = new Follower();
diff --git a/examples/data/scripts/formfiller.pl b/examples/data/scripts/formfiller.pl
new file mode 100755
index 0000000..74dcc80
--- /dev/null
+++ b/examples/data/scripts/formfiller.pl
@@ -0,0 +1,99 @@
+#!/usr/bin/perl
+
+# a slightly more advanced form filler
+#
+# uses settings file like: $keydir/<domain>
+#TODO: fallback to $HOME/.local/share
+# user arg 1:
+# edit: force editing of the file (fetches if file is missing)
+# load: fill forms from file (fetches if file is missing)
+# new: fetch new file
+
+# usage example:
+# bind LL = spawn /usr/share/uzbl/examples/data/scripts/formfiller.pl load
+# bind LN = spawn /usr/share/uzbl/examples/data/scripts/formfiller.pl new
+# bind LE = spawn /usr/share/uzbl/examples/data/scripts/formfiller.pl edit
+
+use strict;
+use warnings;
+
+my $keydir = $ENV{XDG_CONFIG_HOME} . "/uzbl/forms";
+my ($config,$pid,$xid,$fifoname,$socket,$url,$title,$cmd) = @ARGV;
+if (!defined $fifoname || $fifoname eq "") { die "No fifo"; }
+
+sub domain {
+ my ($url) = @_;
+ $url =~ s#http(s)?://([A-Za-z0-9\.-]+)(/.*)?#$2#;
+ return $url;
+};
+
+my $editor = "xterm -e vim";
+#my $editor = "gvim";
+
+# ideally, there would be some way to ask uzbl for the html content instead of having to redownload it with
+# Also, you may need to fake the user-agent on some sites (like facebook)
+ my $downloader = "curl -A 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.10) Gecko/2009042810 GranParadiso/3.0.10' ";
+#my $downloader = "curl -s";
+
+my @fields = ("type","name","value");
+
+my %command;
+
+$command{load} = sub {
+ my ($domain) = @_;
+ my $filename = "$keydir/$domain";
+ if (-e $filename){
+ open(my $file, $filename) or die "Failed to open $filename: $!";
+ my (@lines) = <$file>;
+ close($file);
+ $|++;
+ open(my $fifo, ">>", $fifoname) or die "Failed to open $fifoname: $!";
+ foreach my $line (@lines) {
+ next if ($line =~ m/^#/);
+ my ($type,$name,$value) = ($line =~ /^\s*(\w+)\s*\|\s*(.*?)\s*\|\s*(.*?)\s*$/);
+ if ($type eq "checkbox")
+ {
+ printf $fifo 'js document.getElementsByName("%s")[0].checked = %s;', $name, $value;
+ } elsif ($type eq "submit")
+ {
+ printf $fifo 'js function fs (n) {try{n.submit()} catch (e){fs(n.parentNode)}}; fs(document.getElementsByName("%s")[0]);', $name;
+ } elsif ($type ne "")
+ {
+ printf $fifo 'js document.getElementsByName("%s")[0].value = "%s";', $name, $value;
+ }
+ print $fifo "\n";
+ }
+ $|--;
+ } else {
+ $command{new}->($domain);
+ $command{edit}->($domain);
+ }
+};
+$command{edit} = sub {
+ my ($domain) = @_;
+ my $file = "$keydir/$domain";
+ if(-e $file){
+ system ($editor, $file);
+ } else {
+ $command{new}->($domain);
+ }
+};
+$command{new} = sub {
+ my ($domain) = @_;
+ my $filename = "$keydir/$domain";
+ open (my $file,">>", $filename) or die "Failed to open $filename: $!";
+ $|++;
+ print $file "# Make sure that there are no extra submits, since it may trigger the wrong one.\n";
+ printf $file "#%-10s | %-10s | %s\n", @fields;
+ print $file "#------------------------------\n";
+ my @data = `$downloader $url`;
+ foreach my $line (@data){
+ if($line =~ m/<input ([^>].*?)>/i){
+ $line =~ s/.*(<input ([^>].*?)>).*/$1/;
+ printf $file " %-10s | %-10s | %s\n", map { my ($r) = $line =~ /.*$_=["'](.*?)["']/;$r } @fields;
+ };
+ };
+ $|--;
+};
+
+$command{$cmd}->(domain($url));
diff --git a/examples/data/scripts/formfiller.sh b/examples/data/scripts/formfiller.sh
new file mode 100755
index 0000000..10afaba
--- /dev/null
+++ b/examples/data/scripts/formfiller.sh
@@ -0,0 +1,62 @@
+#!/bin/bash
+
+# simple html form (eg for logins) filler (and manager) for uzbl.
+# uses settings files like: $keydir/<domain>
+# files contain lines like: <fieldname>: <value>
+
+
+# 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
+
+# something else (or empty): if file not available: new, otherwise load.
+
+keydir=${XDG_DATA_HOME:-$HOME/.local/share}/uzbl/forms
+[ -d "`dirname $keydir`" ] || exit 1
+[ -d "$keydir" ] || mkdir "$keydir"
+
+editor=${VISUAL}
+if [[ -z ${editor} ]]; then
+ #editor='gvim'
+ editor='urxvt -e vim'
+fi
+
+config=$1; shift
+pid=$1; shift
+xid=$1; shift
+fifo=$1; shift
+socket=$1; shift
+url=$1; shift
+title=$1; shift
+action=$1
+
+[ -d $keydir ] || mkdir $keydir || exit 1
+
+if [ "$action" != 'edit' -a "$action" != 'new' -a "$action" != 'load' ]
+then
+ action=new
+ [[ -e $keydir/$domain ]] && action=load
+elif [ "$action" == 'edit' ] && [[ ! -e $keydir/$domain ]]
+then
+ action=new
+fi
+domain=$(echo $url | sed -re 's|(http\|https)+://([A-Za-z0-9\.]+)/.*|\2|')
+
+
+#regex='s|.*<input.*?name="([[:graph:]]+)".*?/>.*|\1: |p' # sscj's first version, does not work on http://wiki.archlinux.org/index.php?title=Special:UserLogin&returnto=Main_Page
+ regex='s|.*<input.*?name="([^"]*)".*|\1: |p' #works on arch wiki, but not on http://lists.uzbl.org/listinfo.cgi/uzbl-dev-uzbl.org TODO: improve
+
+
+if [ "$action" = 'load' ]
+then
+ [[ -e $keydir/$domain ]] || exit 2
+ gawk -F': ' '{ print "js document.getElementsByName(\"" $1 "\")[0].value = \"" $2 "\";"}' $keydir/$domain >> $fifo
+else
+ if [ "$action" == 'new' ]
+ then
+ curl "$url" | grep '<input' | sed -nre "$regex" > $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
+fi
diff --git a/examples/data/scripts/hint.js b/examples/data/scripts/hint.js
new file mode 100644
index 0000000..ec7f1e2
--- /dev/null
+++ b/examples/data/scripts/hint.js
@@ -0,0 +1,26 @@
+for (var i=0; i < document.links.length; i++) {
+ var uzblid = 'uzbl_link_hint_';
+ var li = document.links[i];
+ var pre = document.getElementById(uzblid+i);
+
+ if (pre) {
+ li.removeChild(pre);
+ } else {
+ var hint = document.createElement('div');
+ hint.setAttribute('id',uzblid+i);
+ hint.innerHTML = i;
+ hint.style.display='inline';
+ hint.style.lineHeight='90%';
+ hint.style.backgroundColor='red';
+ hint.style.color='white';
+ hint.style.fontSize='small-xx';
+ hint.style.fontWeight='light';
+ hint.style.margin='0px';
+ hint.style.padding='2px';
+ hint.style.position='absolute';
+ hint.style.textDecoration='none';
+ hint.style.left=li.style.left;
+ hint.style.top=li.style.top;
+ li.insertAdjacentElement('afterBegin',hint);
+ }
+}
diff --git a/examples/data/scripts/history.sh b/examples/data/scripts/history.sh
new file mode 100755
index 0000000..7c83aa6
--- /dev/null
+++ b/examples/data/scripts/history.sh
@@ -0,0 +1,5 @@
+#!/bin/sh
+
+file=${XDG_DATA_HOME:-$HOME/.local/share}/uzbl/history
+[ -d `dirname $file` ] || exit 1
+echo `date +'%Y-%m-%d %H:%M:%S'`" $6 $7" >> $file
diff --git a/examples/data/scripts/insert_bookmark.sh b/examples/data/scripts/insert_bookmark.sh
new file mode 100755
index 0000000..c34e7db
--- /dev/null
+++ b/examples/data/scripts/insert_bookmark.sh
@@ -0,0 +1,16 @@
+#!/bin/sh
+
+[ -d "${XDG_DATA_HOME:-$HOME/.local/share}/uzbl" ] || exit 1
+file=${XDG_DATA_HOME:-$HOME/.local/share}/uzbl/bookmarks
+
+which zenity &>/dev/null || exit 2
+
+entry=`zenity --entry --text="Add bookmark. add tags after the '\t', separated by spaces" --entry-text="$6 $7\t"`
+exitstatus=$?
+if [ $exitstatus -ne 0 ]; then exit $exitstatus; fi
+url=`echo $entry | awk '{print $1}'`
+
+# TODO: check if already exists, if so, and tags are different: ask if you want to replace tags
+echo "$entry" >/dev/null #for some reason we need this.. don't ask me why
+echo -e "$entry" >> $file
+true
diff --git a/examples/data/scripts/instance-select-wmii.sh b/examples/data/scripts/instance-select-wmii.sh
new file mode 100755
index 0000000..2bf13ba
--- /dev/null
+++ b/examples/data/scripts/instance-select-wmii.sh
@@ -0,0 +1,54 @@
+#!/bin/sh
+
+
+# This script allows you to focus another uzbl window
+# It considers all uzbl windows in the current tag
+# you can select one from a list, or go to the next/previous one
+# It does not change the layout (stacked/tiled/floating) nor does it
+# changes the size or viewing mode of a uzbl window
+# When your current uzbl window is maximized, the one you change to
+# will be maximized as well.
+# See http://www.uzbl.org/wiki/wmii for more info
+# $1 must be one of 'list', 'next', 'prev'
+
+COLORS=" -nb #303030 -nf khaki -sb #CCFFAA -sf #303030"
+
+if dmenu --help 2>&1 | grep -q '\[-rs\] \[-ni\] \[-nl\] \[-xs\]'
+then
+ DMENU="dmenu -i -xs -rs -l 10" # vertical patch
+else
+ DMENU="dmenu -i"
+fi
+
+if [ "$1" == 'list' ]
+then
+ list=
+ # get window id's of uzbl clients. we could also get the label in one shot but it's pretty tricky
+ for i in $(wmiir read /tag/sel/index | grep uzbl |cut -d ' ' -f2)
+ do
+ label=$(wmiir read /client/$i/label)
+ list="$list$i : $label\n"
+ done
+ window=$(echo -e "$list" | $DMENU $COLORS | cut -d ' ' -f1)
+ wmiir xwrite /tag/sel/ctl "select client $window"
+elif [ "$1" == 'next' ]
+then
+ current=$(wmiir read /client/sel/ctl | head -n 1)
+ # find the next uzbl window and focus it
+ next=$(wmiir read /tag/sel/index | grep -A 10000 " $current " | grep -m 1 uzbl | cut -d ' ' -f2)
+ if [ x"$next" != "x" ]
+ then
+ wmiir xwrite /tag/sel/ctl "select client $next"
+ fi
+elif [ "$1" == 'prev' ]
+then
+ current=$(wmiir read /client/sel/ctl | head -n 1)
+ prev=$(wmiir read /tag/sel/index | grep -B 10000 " $current " | tac | grep -m 1 uzbl | cut -d ' ' -f2)
+ if [ x"$prev" != "x" ]
+ then
+ wmiir xwrite /tag/sel/ctl "select client $prev"
+ fi
+else
+ echo "\$1 not valid" >&2
+ exit 2
+fi
diff --git a/examples/data/scripts/linkfollow.js b/examples/data/scripts/linkfollow.js
new file mode 100644
index 0000000..3109cda
--- /dev/null
+++ b/examples/data/scripts/linkfollow.js
@@ -0,0 +1,269 @@
+// link follower for uzbl
+// requires http://github.com/DuClare/uzbl/commit/6c11777067bdb8aac09bba78d54caea04f85e059
+//
+// first, it needs to be loaded before every time it is used.
+// One way would be to use the load_commit_handler:
+// set load_commit_handler = sh 'echo "script /usr/share/uzbl/examples/data/scripts/linkfollow.js" > "$4"'
+//
+// when script is loaded, it can be invoked with
+// bind f* = js hints.set("%s", hints.open)
+// bind f_ = js hints.follow("%s",hints.open)
+//
+// At the moment, it may be useful to have way of forcing uzbl to load the script
+// bind :lf = script /usr/share/uzbl/examples/data/scripts/linkfollow.js
+//
+// The default style for the hints are pretty ugly, so it is recommended to add the following
+// to config file
+// set stylesheet_uri = /usr/share/uzbl/examples/data/style.css
+//
+// based on follow_Numbers.js
+//
+// TODO: fix styling for the first element
+// TODO: emulate mouseover events when visiting some elements
+// TODO: rewrite the element->action handling
+
+
+function Hints(){
+
+ // Settings
+ ////////////////////////////////////////////////////////////////////////////
+
+ // if set to true, you must explicitly call hints.follow(), otherwise it will
+ // follow the link if there is only one matching result
+ var requireReturn = true;
+
+ // Case sensitivity flag
+ var matchCase = "i";
+
+ // For case sensitive matching, uncomment:
+ // var matchCase = "";
+
+
+ var uzblid = 'uzbl_hint';
+ var uzblclass = 'uzbl_highlight';
+ var uzblclassfirst = 'uzbl_h_first';
+ var doc = document;
+ var visible = [];
+ var hintdiv;
+
+ this.set = hint;
+ this.follow = follow;
+ this.keyPressHandler = keyPressHandler;
+
+ function elementPosition(el) {
+ var up = el.offsetTop;
+ var left = el.offsetLeft; var width = el.offsetWidth;
+ var height = el.offsetHeight;
+
+ while (el.offsetParent) {
+ el = el.offsetParent;
+ up += el.offsetTop;
+ left += el.offsetLeft;
+ }
+ return {up: up, left: left, width: width, height: height};
+ }
+
+ function elementInViewport(p) {
+ return (p.up < window.pageYOffset + window.innerHeight &&
+ p.left < window.pageXOffset + window.innerWidth &&
+ (p.up + p.height) > window.pageYOffset &&
+ (p.left + p.width) > window.pageXOffset);
+ }
+
+ function isVisible(el) {
+ if (el == doc) { return true; }
+ if (!el) { return false; }
+ if (!el.parentNode) { return false; }
+ if (el.style) {
+ if (el.style.display == 'none') {
+ return false;
+ }
+ if (el.style.visibility == 'hidden') {
+ return false;
+ }
+ }
+ return isVisible(el.parentNode);
+ }
+
+ // the vimperator defaults minus the xhtml elements, since it gave DOM errors
+ var hintable = " //*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or @class='lk' or @role='link' or @href] | //input[not(@type='hidden')] | //a | //area | //iframe | //textarea | //button | //select";
+
+ function Matcher(str){
+ var numbers = str.replace(/[^\d]/g,"");
+ var words = str.replace(/\d/g,"").split(/\s+/).map(function (n) { return new RegExp(n,matchCase)});
+ this.test = test;
+ this.toString = toString;
+ this.numbers = numbers;
+ function matchAgainst(element){
+ if(element.node.nodeName == "INPUT"){
+ return element.node.value;
+ } else {
+ return element.node.textContent;
+ }
+ }
+ function test(element) {
+ // test all the regexp
+ var item = matchAgainst(element);
+ return words.every(function (regex) { return item.match(regex)});
+ }
+ }
+
+ function HintElement(node,pos){
+
+ this.node = node;
+ this.isHinted = false;
+ this.position = pos;
+ this.num = 0;
+
+ this.addHint = function (labelNum) {
+ // TODO: fix uzblclassfirst
+ if(!this.isHinted){
+ this.node.className += " " + uzblclass;
+ }
+ this.isHinted = true;
+
+ // create hint
+ var hintNode = doc.createElement('div');
+ hintNode.name = uzblid;
+ hintNode.innerText = labelNum;
+ hintNode.style.left = this.position.left + 'px';
+ hintNode.style.top = this.position.up + 'px';
+ hintNode.style.position = "absolute";
+ doc.body.firstChild.appendChild(hintNode);
+
+ }
+ this.removeHint = function(){
+ if(this.isHinted){
+ var s = (this.num)?uzblclassfirst:uzblclass;
+ this.node.className = this.node.className.replace(new RegExp(" "+s,"g"),"");
+ this.isHinted = false;
+ }
+ }
+ }
+
+ function createHintDiv(){
+ var hintdiv = doc.getElementById(uzblid);
+ if(hintdiv){
+ hintdiv.parentNode.removeChild(hintdiv);
+ }
+ hintdiv = doc.createElement("div");
+ hintdiv.setAttribute('id',uzblid);
+ doc.body.insertBefore(hintdiv,doc.body.firstChild);
+ return hintdiv;
+ }
+
+ function init(){
+ // WHAT?
+ doc.body.setAttribute("onkeyup","hints.keyPressHandler(event)");
+ hintdiv = createHintDiv();
+ visible = [];
+
+ var items = doc.evaluate(hintable,doc,null,XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,null);
+ for (var i = 0;i<items.snapshotLength;i++){
+ var item = items.snapshotItem(i);
+ var pos = elementPosition(item);
+ if(isVisible && elementInViewport(elementPosition(item))){
+ visible.push(new HintElement(item,pos));
+ }
+ }
+ }
+
+ function clear(){
+
+ visible.forEach(function (n) { n.removeHint(); } );
+ hintdiv = doc.getElementById(uzblid);
+ while(hintdiv){
+ hintdiv.parentNode.removeChild(hintdiv);
+ hintdiv = doc.getElementById(uzblid);
+ }
+ }
+
+ function update(str,openFun) {
+ var match = new Matcher(str);
+ hintdiv = createHintDiv();
+ var i = 1;
+ visible.forEach(function (n) {
+ if(match.test(n)) {
+ n.addHint(i);
+ i++;
+ } else {
+ n.removeHint();
+ }});
+ if(!requireReturn){
+ if(i==2){ //only been incremented once
+ follow(str,openFun);
+ }
+ }
+ }
+
+ function hint(str,openFun){
+ if(str.length == 0) init();
+ update(str,openFun);
+ }
+
+ function keyPressHandler(e) {
+ var kC = window.event ? event.keyCode: e.keyCode;
+ var Esc = window.event ? 27 : e.DOM_VK_ESCAPE;
+ if (kC == Esc) {
+ clear();
+ doc.body.removeAttribute("onkeyup");
+ }
+ }
+
+ this.openNewWindow = function(item){
+ // TODO: this doesn't work yet
+ item.className += " uzbl_follow";
+ window.open(item.href,"uzblnew","");
+ }
+ this.open = function(item){
+ simulateMouseOver(item);
+ item.className += " uzbl_follow";
+ window.location = item.href;
+ }
+
+ function simulateMouseOver(item){
+ var evt = doc.createEvent("MouseEvents");
+ evt.initMouseEvent("MouseOver",true,true,
+ doc.defaultView,1,0,0,0,0,
+ false,false,false,false,0,null);
+ return item.dispatchEvent(evt);
+ }
+
+
+ function follow(str,openFunction){
+ var m = new Matcher(str);
+ var items = visible.filter(function (n) { return n.isHinted });
+ clear();
+ var num = parseInt(m.numbers,10);
+ if(num){
+ var item = items[num-1].node;
+ } else {
+ var item = items[0].node;
+ }
+ if (item) {
+ var name = item.tagName;
+ if (name == 'A') {
+ if(item.click) {item.click()};
+ openFunction(item);
+ } else if (name == 'INPUT') {
+ var type = item.getAttribute('type').toUpperCase();
+ if (type == 'TEXT' || type == 'FILE' || type == 'PASSWORD') {
+ item.focus();
+ item.select();
+ } else {
+ item.click();
+ }
+ } else if (name == 'TEXTAREA' || name == 'SELECT') {
+ item.focus();
+ item.select();
+ } else {
+ item.click();
+ openFunction(item);
+ }
+ }
+ }
+}
+
+var hints = new Hints();
+
+// vim:set et sw=2:
diff --git a/examples/data/scripts/load_url_from_bookmarks.sh b/examples/data/scripts/load_url_from_bookmarks.sh
new file mode 100755
index 0000000..1e9f9e7
--- /dev/null
+++ b/examples/data/scripts/load_url_from_bookmarks.sh
@@ -0,0 +1,20 @@
+#!/bin/bash
+
+#NOTE: it's the job of the script that inserts bookmarks to make sure there are no dupes.
+
+file=${XDG_DATA_HOME:-$HOME/.local/share}/uzbl/bookmarks
+[ -r "$file" ] || exit
+COLORS=" -nb #303030 -nf khaki -sb #CCFFAA -sf #303030"
+if dmenu --help 2>&1 | grep -q '\[-rs\] \[-ni\] \[-nl\] \[-xs\]'
+then
+ DMENU="dmenu -i -xs -rs -l 10" # vertical patch
+ # show tags as well
+ goto=`$DMENU $COLORS < $file | awk '{print $1}'`
+else
+ DMENU="dmenu -i"
+ # because they are all after each other, just show the url, not their tags.
+ goto=`awk '{print $1}' $file | $DMENU $COLORS`
+fi
+
+#[ -n "$goto" ] && echo "uri $goto" > $4
+[ -n "$goto" ] && echo "uri $goto" | socat - unix-connect:$5
diff --git a/examples/data/scripts/load_url_from_history.sh b/examples/data/scripts/load_url_from_history.sh
new file mode 100755
index 0000000..62e02ac
--- /dev/null
+++ b/examples/data/scripts/load_url_from_history.sh
@@ -0,0 +1,24 @@
+#!/bin/sh
+
+history_file=${XDG_DATA_HOME:-$HOME/.local/share}/uzbl/history
+[ -r "$history_file" ] || exit 1
+
+# choose from all entries, sorted and uniqued
+# goto=`awk '{print $3}' $history_file | sort -u | dmenu -i`
+COLORS=" -nb #303030 -nf khaki -sb #CCFFAA -sf #303030"
+if dmenu --help 2>&1 | grep -q '\[-rs\] \[-ni\] \[-nl\] \[-xs\]';
+then
+ DMENU="dmenu -i -xs -rs -l 10" # vertical patch
+ # choose an item in reverse order, showing also the date and page titles
+ # pick the last field from the first 3 fields. this way you can pick a url (prefixed with date & time) or type just a new url.
+ goto=`tac $history_file | $DMENU $COLORS | cut -d ' ' -f -3 | awk '{print $NF}'`
+else
+ DMENU="dmenu -i"
+ # choose from all entries (no date or title), the first one being current url, and after that all others, sorted and uniqued, in ascending order
+ current=`tail -n 1 $history_file | awk '{print $3}'`;
+ goto=`(echo $current; awk '{print $3}' $history_file | grep -v "^$current\$" \
+ | sort -u) | $DMENU $COLORS`
+fi
+
+[ -n "$goto" ] && echo "uri $goto" > $4
+#[ -n "$goto" ] && echo "uri $goto" | socat - unix-connect:$5
diff --git a/examples/data/scripts/scheme.py b/examples/data/scripts/scheme.py
new file mode 100755
index 0000000..0916466
--- /dev/null
+++ b/examples/data/scripts/scheme.py
@@ -0,0 +1,24 @@
+#!/usr/bin/env python
+
+import os, subprocess, sys, urlparse
+
+def detach_open(cmd):
+ # Thanks to the vast knowledge of Laurence Withers (lwithers) and this message:
+ # http://mail.python.org/pipermail/python-list/2006-November/587523.html
+ if not os.fork():
+ null = os.open(os.devnull,os.O_WRONLY)
+ for i in range(3): os.dup2(null,i)
+ os.close(null)
+ subprocess.Popen(cmd)
+ print 'USED'
+
+if __name__ == '__main__':
+ uri = sys.argv[8]
+ u = urlparse.urlparse(uri)
+ if u.scheme == 'mailto':
+ detach_open(['xterm', '-e', 'mail', u.path])
+ elif u.scheme == 'xmpp':
+ # Someone check for safe arguments to gajim-remote
+ detach_open(['gajim-remote', 'open_chat', uri])
+ elif u.scheme == 'git':
+ detach_open(['git', 'clone', '--', uri], cwd=os.path.expanduser('~/src'))
diff --git a/examples/data/scripts/scroll-percentage.js b/examples/data/scripts/scroll-percentage.js
new file mode 100644
index 0000000..c9a51aa
--- /dev/null
+++ b/examples/data/scripts/scroll-percentage.js
@@ -0,0 +1,68 @@
+// VIM ruler style scroll message
+(function() {
+ var run = Uzbl.run;
+ var update_message = function() {
+ var innerHeight = window.innerHeight;
+ var scrollY = window.scrollY;
+ var height = document.height;
+ var message;
+
+ if (UzblZoom.type === "full") {
+ var zoom_level = UzblZoom.level;
+ innerHeight = Math.ceil(innerHeight * zoom_level);
+ scrollY = Math.ceil(scrollY * zoom_level);
+ height -= 1;
+ }
+
+ if (! height) {
+ message = "";
+ }
+ else if (height <= innerHeight) {
+ message = run("print @scroll_all_indicator") || "All";
+ }
+ else if (scrollY === 0) {
+ message = run("print @scroll_top_indicator") || "Top";
+ }
+ else if (scrollY + innerHeight >= height) {
+ message = run("print @scroll_bottom_indicator") || "Bot";
+ }
+ else {
+ var percentage = Math.round(scrollY / (height - innerHeight) * 100);
+ message = percentage + "%";
+ }
+ run("set scroll_message=" + message);
+ };
+
+ self.UzblZoom = {
+ get level() {
+ return Number(run("print @zoom_level")) || 1;
+ },
+ set level(level) {
+ if (typeof level === "number" && level > 0) {
+ run("set zoom_level = " + level);
+ update_message();
+ }
+ },
+ get type() {
+ return run("print @zoom_type") || "text";
+ },
+ set type(type) {
+ if ((type === "text" || type === "full") && this.type != type) {
+ run("toggle_zoom_type");
+ run("set zoom_type = " + type);
+ update_message();
+ }
+ },
+ toggle_type: function() {
+ this.type = (this.type === "text" ? "full" : "text");
+ }
+ };
+
+ window.addEventListener("DOMContentLoaded", update_message, false);
+ window.addEventListener("load", update_message, false);
+ window.addEventListener("resize", update_message, false);
+ window.addEventListener("scroll", update_message, false);
+ update_message();
+})();
+
+// vim: set noet ff=unix
diff --git a/examples/data/scripts/session.sh b/examples/data/scripts/session.sh
new file mode 100755
index 0000000..1059b5e
--- /dev/null
+++ b/examples/data/scripts/session.sh
@@ -0,0 +1,62 @@
+#!/bin/sh
+
+# Very simple session manager for uzbl-browser. When called with "endsession" as the
+# argument, it'll backup $sessionfile, look for fifos in $fifodir and
+# instruct each of them to store their current url in $sessionfile and
+# terminate themselves. Run with "launch" as the argument and an instance of
+# uzbl-browser will be launched for each stored url. "endinstance" is used internally
+# and doesn't need to be called manually at any point.
+# Add a line like 'bind quit = /path/to/session.sh endsession' to your config
+
+[ -d ${XDG_DATA_HOME:-$HOME/.local/share}/uzbl ] || exit 1
+scriptfile=$0 # this script
+sessionfile=${XDG_DATA_HOME:-$HOME/.local/share}/uzbl/browser-session # the file in which the "session" (i.e. urls) are stored
+configfile=${XDG_DATA_HOME:-$HOME/.local/share}/uzbl/config # uzbl configuration file
+UZBL="uzbl-browser -c $configfile" # add custom flags and whatever here.
+
+fifodir=/tmp # remember to change this if you instructed uzbl to put its fifos elsewhere
+thisfifo="$4"
+act="$8"
+url="$6"
+
+if [ "$act." = "." ]; then
+ act="$1"
+fi
+
+
+case $act in
+ "launch" )
+ urls=`cat $sessionfile`
+ if [ "$urls." = "." ]; then
+ $UZBL
+ else
+ for url in $urls; do
+ $UZBL --uri "$url" &
+ done
+ fi
+ exit 0
+ ;;
+
+ "endinstance" )
+ if [ "$url" != "(null)" ]; then
+ echo "$url" >> $sessionfile;
+ fi
+ echo "exit" > "$thisfifo"
+ ;;
+
+ "endsession" )
+ mv "$sessionfile" "$sessionfile~"
+ for fifo in $fifodir/uzbl_fifo_*; do
+ if [ "$fifo" != "$thisfifo" ]; then
+ echo "spawn $scriptfile endinstance" > "$fifo"
+ fi
+ done
+ echo "spawn $scriptfile endinstance" > "$thisfifo"
+ ;;
+
+ * ) echo "session manager: bad action"
+ echo "Usage: $scriptfile [COMMAND] where commands are:"
+ echo " launch - Restore a saved session or start a new one"
+ echo " endsession - Quit the running session. Must be called from uzbl"
+ ;;
+esac
diff --git a/examples/data/scripts/uzbl-cookie-daemon b/examples/data/scripts/uzbl-cookie-daemon
new file mode 100755
index 0000000..fde8b8e
--- /dev/null
+++ b/examples/data/scripts/uzbl-cookie-daemon
@@ -0,0 +1,664 @@
+#!/usr/bin/env python
+
+# The Python Cookie Daemon for Uzbl.
+# Copyright (c) 2009, Tom Adams <tom@holizz.com>
+# Copyright (c) 2009, Dieter Plaetinck <dieter@plaetinck.be>
+# Copyright (c) 2009, Mason Larobina <mason.larobina@gmail.com>
+# Copyright (c) 2009, Michael Fiano <axionix@gmail.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+'''
+The Python Cookie Daemon
+========================
+
+This daemon is a re-write of the original cookies.py script found in uzbl's
+master branch. This script provides more functionality than the original
+cookies.py by adding numerous command line options to specify different cookie
+jar locations, socket locations, verbose output, etc. This functionality is
+very useful as it allows you to run multiple daemons at once serving cookies
+to different groups of uzbl instances as required.
+
+Keeping up to date
+==================
+
+Check the cookie daemon uzbl-wiki page for more information on where to
+find the latest version of the cookie_daemon.py
+
+ http://www.uzbl.org/wiki/cookie_daemon.py
+
+Command line options
+====================
+
+Use the following command to get a full list of the cookie_daemon.py command
+line options:
+
+ ./cookie_daemon.py --help
+
+Talking with uzbl
+=================
+
+In order to get uzbl to talk to a running cookie daemon you add the following
+to your uzbl config:
+
+ set cookie_handler = talk_to_socket $XDG_CACHE_HOME/uzbl/cookie_daemon_socket
+
+Or if you prefer using the $HOME variable:
+
+ set cookie_handler = talk_to_socket $HOME/.cache/uzbl/cookie_daemon_socket
+
+Todo list
+=========
+
+ - Use a pid file to make force killing a running daemon possible.
+
+Reporting bugs / getting help
+=============================
+
+The best way to report bugs and or get help with the cookie daemon is to
+contact the maintainers it the #uzbl irc channel found on the Freenode IRC
+network (irc.freenode.org).
+'''
+
+import cookielib
+import os
+import sys
+import urllib2
+import select
+import socket
+import time
+import atexit
+from traceback import print_exc
+from signal import signal, SIGTERM
+from optparse import OptionParser
+from os.path import join
+
+try:
+ import cStringIO as StringIO
+
+except ImportError:
+ import StringIO
+
+
+# ============================================================================
+# ::: Default configuration section ::::::::::::::::::::::::::::::::::::::::::
+# ============================================================================
+
+def xdghome(key, default):
+ '''Attempts to use the environ XDG_*_HOME paths if they exist otherwise
+ use $HOME and the default path.'''
+
+ xdgkey = "XDG_%s_HOME" % key
+ if xdgkey in os.environ.keys() and os.environ[xdgkey]:
+ return os.environ[xdgkey]
+
+ return join(os.environ['HOME'], default)
+
+# Setup xdg paths.
+CACHE_DIR = join(xdghome('CACHE', '.cache/'), 'uzbl/')
+DATA_DIR = join(xdghome('DATA', '.local/share/'), 'uzbl/')
+CONFIG_DIR = join(xdghome('CONFIG', '.config/'), 'uzbl/')
+
+# Ensure data paths exist.
+for path in [CACHE_DIR, DATA_DIR, CONFIG_DIR]:
+ if not os.path.exists(path):
+ os.makedirs(path)
+
+# Default config
+config = {
+
+ # Default cookie jar, whitelist, and daemon socket locations.
+ 'cookie_jar': join(DATA_DIR, 'cookies.txt'),
+ 'cookie_whitelist': join(CONFIG_DIR, 'cookie_whitelist'),
+ 'cookie_socket': join(CACHE_DIR, 'cookie_daemon_socket'),
+
+ # Don't use a cookie whitelist policy by default.
+ 'use_whitelist': False,
+
+ # Time out after x seconds of inactivity (set to 0 for never time out).
+ # WARNING: Do not use this option if you are manually launching the daemon.
+ 'daemon_timeout': 0,
+
+ # Daemonise by default.
+ 'daemon_mode': True,
+
+ # Optionally print helpful debugging messages to the terminal.
+ 'verbose': False,
+
+} # End of config dictionary.
+
+
+# ============================================================================
+# ::: End of configuration section :::::::::::::::::::::::::::::::::::::::::::
+# ============================================================================
+
+
+_SCRIPTNAME = os.path.basename(sys.argv[0])
+def echo(msg):
+ '''Prints only if the verbose flag has been set.'''
+
+ if config['verbose']:
+ sys.stderr.write("%s: %s\n" % (_SCRIPTNAME, msg))
+
+
+def error(msg):
+ '''Prints error message and exits.'''
+
+ sys.stderr.write("%s: error: %s\n" % (_SCRIPTNAME, msg))
+ sys.exit(1)
+
+
+def mkbasedir(filepath):
+ '''Create the base directories of the file in the file-path if the dirs
+ don't exist.'''
+
+ dirname = os.path.dirname(filepath)
+ if not os.path.exists(dirname):
+ echo("creating dirs: %r" % dirname)
+ os.makedirs(dirname)
+
+
+def daemon_running(cookie_socket):
+ '''Check if another process (hopefully a cookie_daemon.py) is listening
+ on the cookie daemon socket. If another process is found to be
+ listening on the socket exit the daemon immediately and leave the
+ socket alone. If the connect fails assume the socket has been abandoned
+ and delete it (to be re-created in the create socket function).'''
+
+ if not os.path.exists(cookie_socket):
+ return False
+
+ if os.path.isfile(cookie_socket):
+ raise Exception("regular file at %r is not a socket" % cookie_socket)
+
+
+ if os.path.isdir(cookie_socket):
+ raise Exception("directory at %r is not a socket" % cookie_socket)
+
+ try:
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_SEQPACKET)
+ sock.connect(cookie_socket)
+ sock.close()
+ echo("detected daemon listening on %r" % cookie_socket)
+ return True
+
+ except socket.error:
+ # Failed to connect to cookie_socket so assume it has been
+ # abandoned by another cookie daemon process.
+ if os.path.exists(cookie_socket):
+ echo("deleting abandoned socket at %r" % cookie_socket)
+ os.remove(cookie_socket)
+
+ return False
+
+
+def send_command(cookie_socket, cmd):
+ '''Send a command to a running cookie daemon.'''
+
+ if not daemon_running(cookie_socket):
+ return False
+
+ try:
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_SEQPACKET)
+ sock.connect(cookie_socket)
+ sock.send(cmd)
+ sock.close()
+ echo("sent command %r to %r" % (cmd, cookie_socket))
+ return True
+
+ except socket.error:
+ print_exc()
+ error("failed to send message %r to %r" % (cmd, cookie_socket))
+ return False
+
+
+def kill_daemon(cookie_socket):
+ '''Send the "EXIT" command to running cookie_daemon.'''
+
+ if send_command(cookie_socket, "EXIT"):
+ # Now ensure the cookie_socket is cleaned up.
+ start = time.time()
+ while os.path.exists(cookie_socket):
+ time.sleep(0.1)
+ if (time.time() - start) > 5:
+ error("force deleting socket %r" % cookie_socket)
+ os.remove(cookie_socket)
+ return
+
+ echo("stopped daemon listening on %r"% cookie_socket)
+
+ else:
+ if os.path.exists(cookie_socket):
+ os.remove(cookie_socket)
+ echo("removed abandoned/broken socket %r" % cookie_socket)
+
+
+def daemonize():
+ '''Daemonize the process using the Stevens' double-fork magic.'''
+
+ try:
+ if os.fork():
+ os._exit(0)
+
+ except OSError:
+ print_exc()
+ sys.stderr.write("fork #1 failed")
+ sys.exit(1)
+
+ os.chdir('/')
+ os.setsid()
+ os.umask(0)
+
+ try:
+ if os.fork():
+ os._exit(0)
+
+ except OSError:
+ print_exc()
+ sys.stderr.write("fork #2 failed")
+ sys.exit(1)
+
+ sys.stdout.flush()
+ sys.stderr.flush()
+
+ devnull = '/dev/null'
+ stdin = file(devnull, 'r')
+ stdout = file(devnull, 'a+')
+ stderr = file(devnull, 'a+', 0)
+
+ os.dup2(stdin.fileno(), sys.stdin.fileno())
+ os.dup2(stdout.fileno(), sys.stdout.fileno())
+ os.dup2(stderr.fileno(), sys.stderr.fileno())
+
+
+class CookieMonster:
+ '''The uzbl cookie daemon class.'''
+
+ def __init__(self):
+ '''Initialise class variables.'''
+
+ self.server_socket = None
+ self.jar = None
+ self.last_request = time.time()
+ self._running = False
+
+
+ def run(self):
+ '''Start the daemon.'''
+
+ # The check healthy function will exit if another daemon is detected
+ # listening on the cookie socket and remove the abandoned socket if
+ # there isnt.
+ if os.path.exists(config['cookie_socket']):
+ if daemon_running(config['cookie_socket']):
+ sys.exit(1)
+
+ # Create cookie daemon socket.
+ self.create_socket()
+
+ # Daemonize process.
+ if config['daemon_mode']:
+ echo("entering daemon mode")
+ daemonize()
+
+ # Register a function to cleanup on exit.
+ atexit.register(self.quit)
+
+ # Make SIGTERM act orderly.
+ signal(SIGTERM, lambda signum, stack_frame: sys.exit(1))
+
+ # Create cookie jar object from file.
+ self.open_cookie_jar()
+
+ # Create a way to exit nested loops by setting a running flag.
+ self._running = True
+
+ while self._running:
+ try:
+ # Enter main listen loop.
+ self.listen()
+
+ except KeyboardInterrupt:
+ self._running = False
+ print
+
+ except socket.error:
+ print_exc()
+
+ except:
+ # Clean up
+ self.del_socket()
+
+ # Raise exception
+ raise
+
+ # Always delete the socket before calling create again.
+ self.del_socket()
+ # Create cookie daemon socket.
+ self.create_socket()
+
+
+ def load_whitelist(self):
+ '''Load the cookie jar whitelist policy.'''
+
+ cookie_whitelist = config['cookie_whitelist']
+
+ if cookie_whitelist:
+ mkbasedir(cookie_whitelist)
+
+ # Create cookie whitelist file if it does not exist.
+ if not os.path.exists(cookie_whitelist):
+ open(cookie_whitelist, 'w').close()
+
+ # Read cookie whitelist file into list.
+ file = open(cookie_whitelist,'r')
+ domain_list = [line.rstrip('\n') for line in file]
+ file.close()
+
+ # Define policy of allowed domains
+ policy = cookielib.DefaultCookiePolicy(allowed_domains=domain_list)
+ self.jar.set_policy(policy)
+
+ # Save the last modified time of the whitelist.
+ self._whitelistmtime = os.stat(cookie_whitelist).st_mtime
+
+
+ def open_cookie_jar(self):
+ '''Open the cookie jar.'''
+
+ cookie_jar = config['cookie_jar']
+ cookie_whitelist = config['cookie_whitelist']
+
+ if cookie_jar:
+ mkbasedir(cookie_jar)
+
+ # Create cookie jar object from file.
+ self.jar = cookielib.MozillaCookieJar(cookie_jar)
+
+ # Load cookie whitelist policy.
+ if config['use_whitelist']:
+ self.load_whitelist()
+
+ if cookie_jar:
+ try:
+ # Attempt to load cookies from the cookie jar.
+ self.jar.load(ignore_discard=True)
+
+ # Ensure restrictive permissions are set on the cookie jar
+ # to prevent other users on the system from hi-jacking your
+ # authenticated sessions simply by copying your cookie jar.
+ os.chmod(cookie_jar, 0600)
+
+ except:
+ pass
+
+
+ def reload_whitelist(self):
+ '''Reload the cookie whitelist.'''
+
+ cookie_whitelist = config['cookie_whitelist']
+ if os.path.exists(cookie_whitelist):
+ echo("reloading whitelist %r" % cookie_whitelist)
+ self.open_cookie_jar()
+
+
+ def create_socket(self):
+ '''Create AF_UNIX socket for communication with uzbl instances.'''
+
+ cookie_socket = config['cookie_socket']
+ mkbasedir(cookie_socket)
+
+ self.server_socket = socket.socket(socket.AF_UNIX,
+ socket.SOCK_SEQPACKET)
+
+ self.server_socket.bind(cookie_socket)
+
+ # Set restrictive permissions on the cookie socket to prevent other
+ # users on the system from data-mining your cookies.
+ os.chmod(cookie_socket, 0600)
+
+
+ def listen(self):
+ '''Listen for incoming cookie PUT and GET requests.'''
+
+ daemon_timeout = config['daemon_timeout']
+ echo("listening on %r" % config['cookie_socket'])
+
+ while self._running:
+ # This line tells the socket how many pending incoming connections
+ # to enqueue at once. Raising this number may or may not increase
+ # performance.
+ self.server_socket.listen(1)
+
+ if bool(select.select([self.server_socket], [], [], 1)[0]):
+ client_socket, _ = self.server_socket.accept()
+ self.handle_request(client_socket)
+ self.last_request = time.time()
+ client_socket.close()
+ continue
+
+ if daemon_timeout:
+ # Checks if the daemon has been idling for too long.
+ idle = time.time() - self.last_request
+ if idle > daemon_timeout:
+ self._running = False
+
+
+ def handle_request(self, client_socket):
+ '''Connection made, now to serve a cookie PUT or GET request.'''
+
+ # Receive cookie request from client.
+ data = client_socket.recv(8192)
+ if not data:
+ return
+
+ # Cookie argument list in packet is null separated.
+ argv = data.split("\0")
+ action = argv[0].upper().strip()
+
+ # Catch the EXIT command sent to kill running daemons.
+ if action == "EXIT":
+ self._running = False
+ return
+
+ # Catch whitelist RELOAD command.
+ elif action == "RELOAD":
+ self.reload_whitelist()
+ return
+
+ # Return if command unknown.
+ elif action not in ['GET', 'PUT']:
+ error("unknown command %r." % argv)
+ return
+
+ # Determine whether or not to print cookie data to terminal.
+ print_cookie = (config['verbose'] and not config['daemon_mode'])
+ if print_cookie:
+ print ' '.join(argv[:4])
+
+ uri = urllib2.urlparse.ParseResult(
+ scheme=argv[1],
+ netloc=argv[2],
+ path=argv[3],
+ params='',
+ query='',
+ fragment='').geturl()
+
+ req = urllib2.Request(uri)
+
+ if action == "GET":
+ self.jar.add_cookie_header(req)
+ if req.has_header('Cookie'):
+ cookie = req.get_header('Cookie')
+ client_socket.send(cookie)
+ if print_cookie:
+ print cookie
+
+ else:
+ client_socket.send("\0")
+
+ elif action == "PUT":
+ cookie = argv[4] if len(argv) > 3 else None
+ if print_cookie:
+ print cookie
+
+ self.put_cookie(req, cookie)
+
+ if print_cookie:
+ print
+
+
+ def put_cookie(self, req, cookie=None):
+ '''Put a cookie in the cookie jar.'''
+
+ hdr = urllib2.httplib.HTTPMessage(\
+ StringIO.StringIO('Set-Cookie: %s' % cookie))
+ res = urllib2.addinfourl(StringIO.StringIO(), hdr,
+ req.get_full_url())
+ self.jar.extract_cookies(res, req)
+ if config['cookie_jar']:
+ self.jar.save(ignore_discard=True)
+
+
+ def del_socket(self):
+ '''Remove the cookie_socket file on exit. In a way the cookie_socket
+ is the daemons pid file equivalent.'''
+
+ if self.server_socket:
+ try:
+ self.server_socket.close()
+
+ except:
+ pass
+
+ self.server_socket = None
+
+ cookie_socket = config['cookie_socket']
+ if os.path.exists(cookie_socket):
+ echo("deleting socket %r" % cookie_socket)
+ os.remove(cookie_socket)
+
+
+ def quit(self):
+ '''Called on exit to make sure all loose ends are tied up.'''
+
+ self.del_socket()
+ sys.exit(0)
+
+
+def main():
+ '''Main function.'''
+
+ # Define command line parameters.
+ usage = "usage: %prog [options] {start|stop|restart|reload}"
+ parser = OptionParser(usage=usage)
+ parser.add_option('-n', '--no-daemon', dest='no_daemon',
+ action='store_true', help="don't daemonise the process.")
+
+ parser.add_option('-v', '--verbose', dest="verbose",
+ action='store_true', help="print verbose output.")
+
+ parser.add_option('-t', '--daemon-timeout', dest='daemon_timeout',
+ action="store", metavar="SECONDS", help="shutdown the daemon after x "\
+ "seconds inactivity. WARNING: Do not use this when launching the "\
+ "cookie daemon manually.")
+
+ parser.add_option('-s', '--cookie-socket', dest="cookie_socket",
+ metavar="SOCKET", help="manually specify the socket location.")
+
+ parser.add_option('-j', '--cookie-jar', dest='cookie_jar',
+ metavar="FILE", help="manually specify the cookie jar location.")
+
+ parser.add_option('-m', '--memory', dest='memory', action='store_true',
+ help="store cookies in memory only - do not write to disk")
+
+ parser.add_option('-u', '--use-whitelist', dest='usewhitelist',
+ action='store_true', help="use cookie whitelist policy")
+
+ parser.add_option('-w', '--cookie-whitelist', dest='whitelist',
+ action='store', help="manually specify whitelist location",
+ metavar='FILE')
+
+ # Parse the command line arguments.
+ (options, args) = parser.parse_args()
+
+ expand = lambda p: os.path.realpath(os.path.expandvars(p))
+
+ initcommands = ['start', 'stop', 'restart', 'reload']
+ for arg in args:
+ if arg not in initcommands:
+ error("unknown argument %r" % args[0])
+ sys.exit(1)
+
+ if len(args) > 1:
+ error("the daemon only accepts one {%s} action at a time."
+ % '|'.join(initcommands))
+ sys.exit(1)
+
+ if len(args):
+ action = args[0]
+
+ else:
+ action = "start"
+
+ if options.no_daemon:
+ config['daemon_mode'] = False
+
+ if options.cookie_socket:
+ config['cookie_socket'] = expand(options.cookie_socket)
+
+ if options.cookie_jar:
+ config['cookie_jar'] = expand(options.cookie_jar)
+
+ if options.memory:
+ config['cookie_jar'] = None
+
+ if options.whitelist:
+ config['cookie_whitelist'] = expand(options.whitelist)
+
+ if options.whitelist or options.usewhitelist:
+ config['use_whitelist'] = True
+
+ if options.daemon_timeout:
+ try:
+ config['daemon_timeout'] = int(options.daemon_timeout)
+
+ except ValueError:
+ error("expected int argument for -t, --daemon-timeout")
+
+ # Expand $VAR's in config keys that relate to paths.
+ for key in ['cookie_socket', 'cookie_jar', 'cookie_whitelist']:
+ if config[key]:
+ config[key] = os.path.expandvars(config[key])
+
+ if options.verbose:
+ config['verbose'] = True
+ import pprint
+ sys.stderr.write("%s\n" % pprint.pformat(config))
+
+ # It would be better if we didn't need to start this python process just
+ # to send a command to the socket, but unfortunately socat doesn't seem
+ # to support SEQPACKET.
+ if action == "reload":
+ send_command(config['cookie_socket'], "RELOAD")
+
+ if action in ['stop', 'restart']:
+ kill_daemon(config['cookie_socket'])
+
+ if action in ['start', 'restart']:
+ CookieMonster().run()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/data/scripts/uzbl-event-manager b/examples/data/scripts/uzbl-event-manager
new file mode 100755
index 0000000..9624b14
--- /dev/null
+++ b/examples/data/scripts/uzbl-event-manager
@@ -0,0 +1,837 @@
+#!/usr/bin/env python
+
+# Event Manager for Uzbl
+# Copyright (c) 2009, 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
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+'''
+
+E V E N T _ M A N A G E R . P Y
+===============================
+
+Event manager for uzbl written in python.
+
+'''
+
+import imp
+import os
+import sys
+import re
+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
+from functools import partial
+
+
+def xdghome(key, default):
+ '''Attempts to use the environ XDG_*_HOME paths if they exist otherwise
+ use $HOME and the default path.'''
+
+ xdgkey = "XDG_%s_HOME" % key
+ if xdgkey in os.environ.keys() and os.environ[xdgkey]:
+ return os.environ[xdgkey]
+
+ 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/'
+
+# Setup xdg paths.
+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 counter():
+ '''Generate unique object id's.'''
+
+ i = 0
+ while True:
+ i += 1
+ yield i
+
+
+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 load_plugins(plugin_dirs, load=None, ignore=None):
+ '''Load event manager plugins found in the plugin_dirs.'''
+
+ 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 daemonize():
+ '''Daemonize the process using the Stevens' double-fork magic.'''
+
+ try:
+ if os.fork():
+ os._exit(0)
+
+ except OSError:
+ print_exc()
+ sys.stderr.write("fork #1 failed")
+ sys.exit(1)
+
+ os.chdir('/')
+ os.setsid()
+ os.umask(0)
+
+ try:
+ if os.fork():
+ os._exit(0)
+
+ except OSError:
+ print_exc()
+ sys.stderr.write("fork #2 failed")
+ sys.exit(1)
+
+ sys.stdout.flush()
+ sys.stderr.flush()
+
+ devnull = '/dev/null'
+ stdin = file(devnull, 'r')
+ stdout = file(devnull, 'a+')
+ stderr = file(devnull, 'a+', 0)
+
+ os.dup2(stdin.fileno(), sys.stdin.fileno())
+ os.dup2(stdout.fileno(), sys.stdout.fileno())
+ os.dup2(stderr.fileno(), sys.stderr.fileno())
+
+
+def make_dirs(path):
+ '''Make all basedirs recursively as required.'''
+
+ try:
+ dirname = os.path.dirname(path)
+ if not os.path.isdir(dirname):
+ os.makedirs(dirname)
+
+ except OSError:
+ print_exc()
+
+
+def make_pid_file(pid_file):
+ '''Make pid file at given pid_file location.'''
+
+ make_dirs(pid_file)
+ fileobj = open(pid_file, 'w')
+ fileobj.write('%d' % os.getpid())
+ fileobj.close()
+
+
+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 get_pid(pid_file):
+ '''Read pid from pid_file.'''
+
+ try:
+ fileobj = open(pid_file, 'r')
+ pid = int(fileobj.read())
+ fileobj.close()
+ return pid
+
+ except IOError, ValueError:
+ print_exc()
+ return None
+
+
+def pid_running(pid):
+ '''Returns True if a process with the given pid is running.'''
+
+ try:
+ os.kill(pid, 0)
+
+ 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 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
+
+ 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('')
+
+ event, args = cmd[2], cmd[3]
+ if not event:
+ return
+
+ try:
+ uzbl.event(event, args)
+
+ except:
+ print_exc()
+
+
+class EventHandler(object):
+
+ nexthid = counter().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)
+
+ 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,
+ "function=%r" % self.function]
+
+ if self.args:
+ args.append(u"args=%r" % unicode(self.args))
+
+ if self.kargs:
+ args.append(u"kargs=%r" % unicode(self.kargs))
+
+ return u"<EventHandler(%s)>" % ', '.join(args)
+
+
+class UzblInstance(object):
+
+ # Give all plugins access to the main config dict.
+ config = CONFIG
+
+ def __init__(self, parent, client_socket):
+
+ # Internal variables.
+ self.exports = {}
+ self.handlers = {}
+ self.parent = parent
+ self.client_socket = client_socket
+
+ self.depth = 0
+ self.buffer = ''
+ self.pid = None
+
+ # 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 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, name, function):
+ '''Export `function(uzbl, *args, ..)` inside a plugin to the uzbl
+ object like so `uzbl.function(*args, ..)`. This will allow other
+ plugins to call functions inside the current plugin (which is currently
+ calling this function) via the uzbl object.'''
+
+ self.__dict__.__setitem__(name, partial(function, self))
+
+
+ def export_dict(self, export_dict):
+ '''Export multiple (name, function)'s at once inside a dict of the
+ form `{name1: function1, name2: function2, ...}`.'''
+
+ for (name, function) in export_dict.items():
+ self.export(name, function)
+
+
+ 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.'''
+
+ event = event.upper().strip()
+ assert event and ' ' not in event
+
+ if event not in self.handlers.keys():
+ self.handlers[event] = []
+
+ handlerobj = EventHandler(event, handler, *args, **kargs)
+ self.handlers[event].append(handlerobj)
+ print handlerobj
+
+
+ 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
+ function.'''
+
+ for (event, handler) in connect_dict.items():
+ self.connect(event, handler)
+
+
+ def remove_by_id(self, hid):
+ '''Remove connected event handler by unique handler id.'''
+
+ for (event, handlers) in self.handlers.items():
+ for handler in list(handlers):
+ if hid != handler.hid:
+ continue
+
+ echo("removed %r" % handler)
+ handlers.remove(handler)
+ return
+
+ echo('unable to find & remove handler with id: %d' % hid)
+
+
+ def remove(self, handler):
+ '''Remove connected event handler.'''
+
+ for (event, handlers) in self.handlers.items():
+ if handler in handlers:
+ echo("removed %r" % handler)
+ handlers.remove(handler)
+ return
+
+ echo('unable to find & remove handler: %r' % handler)
+
+
+ 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 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 event == "INSTANCE_START" and args:
+ self.pid = int(args[0])
+
+ if event not in self.handlers:
+ return
+
+ for handler in self.handlers[event]:
+ self.depth += 1
+ try:
+ self.exec_handler(handler, *args, **kargs)
+
+ except:
+ print_exc()
+
+ self.depth -= 1
+
+
+ def close(self):
+ '''Close the client socket and clean up.'''
+
+ try:
+ self.client_socket.close()
+
+ except:
+ pass
+
+ for (name, plugin) in self.parent['plugins'].items():
+ if hasattr(plugin, 'cleanup'):
+ plugin.cleanup(self)
+
+
+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:
+ pass
+
+
+ def run(self):
+ '''Main event daemon loop.'''
+
+ # Create event daemon socket.
+ self._create_server_socket()
+ echo('listening on: %s' % self.socket_location)
+
+ if CONFIG['daemon_mode']:
+ echo('entering daemon mode.')
+ 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()
+
+ # Clean up.
+ self.quit()
+
+
+ def listen(self):
+ '''Accept incoming connections and constantly poll instance sockets
+ for incoming data.'''
+
+ self.running = True
+ while self.running:
+
+ sockets = [self.server_socket] + self['uzbls'].keys()
+
+ reads, _, errors = select(sockets, [], sockets, 1)
+
+ if self.server_socket in reads:
+ self.accept_connection()
+ reads.remove(self.server_socket)
+
+ for client in reads:
+ self.read_socket(client)
+
+ for client in errors:
+ 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.'''
+
+ uzbl = self['uzbls'][client]
+ try:
+ raw = unicode(client.recv(8192), 'utf-8', 'ignore')
+
+ except:
+ print_exc()
+ raw = None
+
+ 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:
+ try:
+ parse_msg(uzbl, msg.strip())
+
+ except:
+ print_exc()
+
+
+ def accept_connection(self):
+ '''Accept incoming connection to the server socket.'''
+
+ client_socket = self.server_socket.accept()[0]
+
+ uzbl = UzblInstance(self, client_socket)
+ self['uzbls'][client_socket] = uzbl
+
+
+ def close_connection(self, client):
+ '''Clean up after instance close.'''
+
+ try:
+ if client in self['uzbls']:
+ uzbl = self['uzbls'][client]
+ uzbl.close()
+ del self['uzbls'][client]
+
+ except:
+ print_exc()
+
+ if not len(self['uzbls']) and CONFIG['auto_close']:
+ echo('auto closing event manager.')
+ self.running = False
+
+
+ def quit(self):
+ '''Close all instance socket objects, server socket and delete the
+ pid file.'''
+
+ echo('shutting down event manager.')
+
+ for client in self['uzbls'].keys():
+ self.close_connection(client)
+
+ 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 stop_action():
+ '''Stop the event manager daemon.'''
+
+ pid_file = CONFIG['pid_file']
+ if not os.path.isfile(pid_file):
+ return echo('no running daemon found.')
+
+ 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)
+
+ echo('stopped event daemon.')
+
+
+def start_action():
+ '''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_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.'''
+
+ plugins = find_plugins(CONFIG['plugin_dirs'])
+ dirs = {}
+
+ for (plugin, plugin_dir) in plugins.items():
+ if plugin_dir not in dirs:
+ dirs[plugin_dir] = []
+
+ dirs[plugin_dir].append(plugin)
+
+ for (index, (plugin_dir, plugin_list)) in enumerate(sorted(dirs.items())):
+ if index:
+ print
+
+ print "%s:" % plugin_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-dirs', dest='plugin_dirs', action="store",
+ metavar="DIRS", help="Specify plugin directories in the form of "\
+ "'dir1:dir2:dir3'.")
+
+ 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('-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.")
+
+ PARSER.add_option('-a', '--auto-close', dest='autoclose',
+ action='store_true', help='auto close after all instances disconnect.')
+
+ (OPTIONS, 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}
+
+ if not ARGS:
+ ACTION = 'start'
+
+ elif len(ARGS) == 1:
+ ACTION = ARGS[0]
+ if ACTION not in DAEMON_ACTIONS:
+ raise ArgumentError("unknown argument: %r" % ACTION)
+
+ else:
+ raise ArgumentError("too many arguments: %r" % ARGS)
+
+ # parse other flags & options.
+ if OPTIONS.verbose:
+ CONFIG['verbose'] = True
+
+ if OPTIONS.plugin_dirs:
+ PLUGIN_DIRS = []
+ for DIR in OPTIONS.plugin_dirs.split(':'):
+ if not DIR:
+ continue
+
+ PLUGIN_DIRS.append(os.path.realpath(DIR))
+
+ 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.")
+ sys.exit(1)
+
+ elif OPTIONS.load:
+ LOAD = CONFIG['plugins_load']
+ for PLUGIN in OPTIONS.load.split(','):
+ if PLUGIN.strip():
+ LOAD.append(PLUGIN.strip())
+
+ echo('only loading plugin(s): %s' % ', '.join(LOAD))
+
+ elif OPTIONS.ignore:
+ IGNORE = CONFIG['plugins_ignore']
+ for PLUGIN in OPTIONS.ignore.split(','):
+ if PLUGIN.strip():
+ IGNORE.append(PLUGIN.strip())
+
+ echo('ignoring plugin(s): %s' % ', '.join(IGNORE))
+
+ if OPTIONS.autoclose:
+ CONFIG['auto_close'] = True
+ echo('will auto close.')
+
+ if OPTIONS.pid:
+ CONFIG['pid_file'] = os.path.realpath(OPTIONS.pid)
+ echo("pid file location: %r" % CONFIG['pid_file'])
+
+ if OPTIONS.socket:
+ CONFIG['server_socket'] = os.path.realpath(OPTIONS.socket)
+ echo("daemon socket location: %s" % CONFIG['server_socket'])
+
+ if OPTIONS.daemon:
+ CONFIG['daemon_mode'] = False
+
+ # Now {start|stop|...}
+ DAEMON_ACTIONS[ACTION]()
diff --git a/examples/data/scripts/uzbl-tabbed b/examples/data/scripts/uzbl-tabbed
new file mode 100755
index 0000000..7bd90d5
--- /dev/null
+++ b/examples/data/scripts/uzbl-tabbed
@@ -0,0 +1,1417 @@
+#!/usr/bin/env python
+
+# Uzbl tabbing wrapper using a fifo socket interface
+# Copyright (c) 2009, Tom Adams <tom@holizz.com>
+# Copyright (c) 2009, Chris van Dijk <cn.vandijk@hotmail.com>
+# Copyright (c) 2009, Mason Larobina <mason.larobina@gmail.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+# Author(s):
+# Tom Adams <tom@holizz.com>
+# Wrote the original uzbl_tabbed.py as a proof of concept.
+#
+# Chris van Dijk (quigybo) <cn.vandijk@hotmail.com>
+# Made signifigant headway on the old uzbl_tabbing.py script on the
+# uzbl wiki <http://www.uzbl.org/wiki/uzbl_tabbed>
+#
+# Mason Larobina <mason.larobina@gmail.com>
+# Rewrite of the uzbl_tabbing.py script to use a fifo socket interface
+# and inherit configuration options from the user's uzbl config.
+#
+# Contributor(s):
+# mxey <mxey@ghosthacking.net>
+# uzbl_config path now honors XDG_CONFIG_HOME if it exists.
+#
+# Romain Bignon <romain@peerfuse.org>
+# Fix for session restoration code.
+#
+# Jake Probst <jake.probst@gmail.com>
+# Wrote a patch that overflows tabs in the tablist on to new lines when
+# running of room.
+#
+# Devon Jones <devon.jones@gmail.com>
+# Fifo command bring_to_front which brings the gtk window to focus.
+#
+# Simon Lipp (sloonz)
+# Various
+
+
+# Dependencies:
+# pygtk - python bindings for gtk.
+# pango - python bindings needed for text rendering & layout in gtk widgets.
+# pygobject - GLib's GObject bindings for python.
+#
+# Optional dependencies:
+# simplejson - save uzbl_tabbed.py sessions & presets in json.
+#
+# Note: I haven't included version numbers with this dependency list because
+# I've only ever tested uzbl_tabbed.py on the latest stable versions of these
+# packages in Gentoo's portage. Package names may vary on different systems.
+
+
+# Configuration:
+# Because this version of uzbl_tabbed is able to inherit options from your main
+# uzbl configuration file you may wish to configure uzbl tabbed from there.
+# Here is a list of configuration options that can be customised and some
+# example values for each:
+#
+# General tabbing options:
+# show_tablist = 1
+# show_gtk_tabs = 0
+# tablist_top = 1
+# gtk_tab_pos = (top|left|bottom|right)
+# gtk_refresh = 1000
+# switch_to_new_tabs = 1
+# capture_new_windows = 1
+# multiline_tabs = 1
+#
+# Tab title options:
+# tab_titles = 1
+# tab_indexes = 1
+# new_tab_title = Loading
+# max_title_len = 50
+# show_ellipsis = 1
+#
+# Session options:
+# save_session = 1
+# json_session = 0
+# session_file = $HOME/.local/share/uzbl/session
+#
+# Inherited uzbl options:
+# fifo_dir = /tmp
+# socket_dir = /tmp
+# icon_path = $HOME/.local/share/uzbl/uzbl.png
+# status_background = #303030
+#
+# Misc options:
+# window_size = 800,800
+# verbose = 0
+#
+# And uzbl_tabbed.py takes care of the actual binding of the commands via each
+# instances fifo socket.
+#
+# Custom tab styling:
+# tab_colours = foreground = "#888" background = "#303030"
+# tab_text_colours = foreground = "#bbb"
+# selected_tab = foreground = "#fff"
+# selected_tab_text = foreground = "green"
+# tab_indicate_https = 1
+# https_colours = foreground = "#888"
+# https_text_colours = foreground = "#9c8e2d"
+# selected_https = foreground = "#fff"
+# selected_https_text = foreground = "gold"
+#
+# How these styling values are used are soley defined by the syling policy
+# handler below (the function in the config section). So you can for example
+# turn the tab text colour Firetruck-Red in the event "error" appears in the
+# tab title or some other arbitrary event. You may wish to make a trusted
+# hosts file and turn tab titles of tabs visiting trusted hosts purple.
+
+
+# Issues:
+# - new windows are not caught and opened in a new tab.
+# - when uzbl_tabbed.py crashes it takes all the children with it.
+# - when a new tab is opened when using gtk tabs the tab button itself
+# grabs focus from its child for a few seconds.
+# - when switch_to_new_tabs is not selected the notebook page is
+# maintained but the new window grabs focus (try as I might to stop it).
+
+
+# Todo:
+# - add command line options to use a different session file, not use a
+# session file and or open a uri on starup.
+# - ellipsize individual tab titles when the tab-list becomes over-crowded
+# - add "<" & ">" arrows to tablist to indicate that only a subset of the
+# currently open tabs are being displayed on the tablist.
+# - add the small tab-list display when both gtk tabs and text vim-like
+# tablist are hidden (I.e. [ 1 2 3 4 5 ])
+# - check spelling.
+# - pass a uzbl socketid to uzbl_tabbed.py and have it assimilated into
+# the collective. Resistance is futile!
+
+
+import pygtk
+import gtk
+import subprocess
+import os
+import re
+import time
+import getopt
+import pango
+import select
+import sys
+import gobject
+import socket
+import random
+import hashlib
+import atexit
+import types
+
+from gobject import io_add_watch, source_remove, timeout_add, IO_IN, IO_HUP
+from signal import signal, SIGTERM, SIGINT
+from optparse import OptionParser, OptionGroup
+
+
+pygtk.require('2.0')
+
+_SCRIPTNAME = os.path.basename(sys.argv[0])
+def error(msg):
+ sys.stderr.write("%s: error: %s\n" % (_SCRIPTNAME, msg))
+
+# ============================================================================
+# ::: Default configuration section ::::::::::::::::::::::::::::::::::::::::::
+# ============================================================================
+
+def xdghome(key, default):
+ '''Attempts to use the environ XDG_*_HOME paths if they exist otherwise
+ use $HOME and the default path.'''
+
+ xdgkey = "XDG_%s_HOME" % key
+ if xdgkey in os.environ.keys() and os.environ[xdgkey]:
+ return os.environ[xdgkey]
+
+ return os.path.join(os.environ['HOME'], default)
+
+# Setup xdg paths.
+DATA_DIR = os.path.join(xdghome('DATA', '.local/share/'), 'uzbl/')
+
+# Ensure uzbl xdg paths exist
+if not os.path.exists(DATA_DIR):
+ os.makedirs(DATA_DIR)
+
+# All of these settings can be inherited from your uzbl config file.
+config = {
+ # Tab options
+ 'show_tablist': True, # Show text uzbl like statusbar tab-list
+ 'show_gtk_tabs': False, # Show gtk notebook tabs
+ 'tablist_top': True, # Display tab-list at top of window
+ 'gtk_tab_pos': 'top', # Gtk tab position (top|left|bottom|right)
+ 'gtk_refresh': 1000, # Tablist refresh millisecond interval
+ 'switch_to_new_tabs': True, # Upon opening a new tab switch to it
+ 'capture_new_windows': True, # Use uzbl_tabbed to catch new windows
+ 'multiline_tabs': True, # Tabs overflow onto new tablist lines.
+
+ # Tab title options
+ 'tab_titles': True, # Display tab titles (else only tab-nums)
+ 'tab_indexes': True, # Display tab nums (else only tab titles)
+ 'new_tab_title': 'Loading', # New tab title
+ 'max_title_len': 50, # Truncate title at n characters
+ 'show_ellipsis': True, # Show ellipsis when truncating titles
+
+ # Session options
+ 'save_session': True, # Save session in file when quit
+ 'json_session': False, # Use json to save session.
+ 'saved_sessions_dir': os.path.join(DATA_DIR, 'sessions/'),
+ 'session_file': os.path.join(DATA_DIR, 'session'),
+
+ # Inherited uzbl options
+ 'fifo_dir': '/tmp', # Path to look for uzbl fifo.
+ 'socket_dir': '/tmp', # Path to look for uzbl socket.
+ 'icon_path': os.path.join(DATA_DIR, 'uzbl.png'),
+ 'status_background': "#303030", # Default background for all panels.
+
+ # Misc options
+ 'window_size': "800,800", # width,height in pixels.
+ 'verbose': False, # Print verbose output.
+
+ # Add custom tab style definitions to be used by the tab colour policy
+ # handler here. Because these are added to the config dictionary like
+ # any other uzbl_tabbed configuration option remember that they can
+ # be superseeded from your main uzbl config file.
+ 'tab_colours': 'foreground = "#888" background = "#303030"',
+ 'tab_text_colours': 'foreground = "#bbb"',
+ 'selected_tab': 'foreground = "#fff"',
+ 'selected_tab_text': 'foreground = "green"',
+ 'tab_indicate_https': True,
+ 'https_colours': 'foreground = "#888"',
+ 'https_text_colours': 'foreground = "#9c8e2d"',
+ 'selected_https': 'foreground = "#fff"',
+ 'selected_https_text': 'foreground = "gold"',
+
+} # End of config dict.
+
+UZBL_TABBED_VARS = config.keys()
+
+# This is the tab style policy handler. Every time the tablist is updated
+# this function is called to determine how to colourise that specific tab
+# according the simple/complex rules as defined here. You may even wish to
+# move this function into another python script and import it using:
+# from mycustomtabbingconfig import colour_selector
+# Remember to rename, delete or comment out this function if you do that.
+
+def colour_selector(tabindex, currentpage, uzbl):
+ '''Tablist styling policy handler. This function must return a tuple of
+ the form (tab style, text style).'''
+
+ # Just as an example:
+ # if 'error' in uzbl.title:
+ # if tabindex == currentpage:
+ # return ('foreground="#fff"', 'foreground="red"')
+ # return ('foreground="#888"', 'foreground="red"')
+
+ # Style tabs to indicate connected via https.
+ if config['tab_indicate_https'] and uzbl.uri.startswith("https://"):
+ if tabindex == currentpage:
+ return (config['selected_https'], config['selected_https_text'])
+ return (config['https_colours'], config['https_text_colours'])
+
+ # Style to indicate selected.
+ if tabindex == currentpage:
+ return (config['selected_tab'], config['selected_tab_text'])
+
+ # Default tab style.
+ return (config['tab_colours'], config['tab_text_colours'])
+
+# ============================================================================
+# ::: End of configuration section :::::::::::::::::::::::::::::::::::::::::::
+# ============================================================================
+
+def echo(msg):
+ if config['verbose']:
+ sys.stderr.write("%s: %s\n" % (_SCRIPTNAME, msg))
+
+
+def counter():
+ '''To infinity and beyond!'''
+
+ i = 0
+ while True:
+ i += 1
+ yield i
+
+
+def escape(s):
+ '''Replaces html markup in tab titles that screw around with pango.'''
+
+ for (split, glue) in [('&','&amp;'), ('<', '&lt;'), ('>', '&gt;')]:
+ s = s.replace(split, glue)
+ return s
+
+
+class SocketClient:
+ '''Represents a Uzbl instance, which is not necessarly linked with a UzblInstance'''
+
+ # List of UzblInstance objects not already linked with a SocketClient
+ instances_queue = {}
+
+ def __init__(self, socket):
+ self._buffer = ""
+ self._socket = socket
+ self._watchers = [io_add_watch(socket, IO_IN, self._socket_recv),\
+ io_add_watch(socket, IO_HUP, self._socket_closed)]
+ self.uzbl = None
+
+
+ def _socket_recv(self, fd, condition):
+ '''Data available on socket, process it'''
+
+ self._feed(self._socket.recv(1024)) #TODO: is io_add_watch edge or level-triggered ?
+ return True
+
+
+ def _socket_closed(self, fd, condition):
+ '''Remote client exited'''
+ self.uzbl.close()
+ return False
+
+
+ def _feed(self, data):
+ '''An Uzbl instance sent some data, parse it'''
+
+ self._buffer += data
+ if self.uzbl:
+ if "\n" in self._buffer:
+ cmds = self._buffer.split("\n")
+
+ if cmds[-1]: # Last command has been received incomplete, don't process it
+ self._buffer, cmds = cmds[-1], cmds[:-1]
+ else:
+ self._buffer = ""
+
+ for cmd in cmds:
+ if cmd:
+ self.uzbl.parse_command(cmd)
+ else:
+ name = re.findall('^EVENT \[(\d+-\d+)\] INSTANCE_START \d+$', self._buffer, re.M)
+ uzbl = self.instances_queue.get(name[0])
+ if uzbl:
+ del self.instances_queue[name[0]]
+ self.uzbl = uzbl
+ self.uzbl.got_socket(self)
+ self._feed("")
+
+ def send(self, data):
+ '''Child socket send function.'''
+
+ self._socket.send(data + "\n")
+
+ def close(self):
+ '''Close the connection'''
+
+ if self._socket:
+ self._socket.close()
+ self._socket = None
+ map(source_remove, self._watchers)
+ self._watchers = []
+
+
+class UzblInstance:
+ '''Uzbl instance meta-data/meta-action object.'''
+
+ def __init__(self, parent, tab, name, uri, title, switch):
+
+ self.parent = parent
+ self.tab = tab
+ self.name = name
+ self.title = title
+ self.tabtitle = ""
+ self.uri = uri
+ self._client = None
+ self._switch = switch # Switch to tab after loading ?
+ self.title_changed()
+
+
+ def got_socket(self, client):
+ '''Uzbl instance is now connected'''
+
+ self._client = client
+ self.parent.config_uzbl(self)
+ if self._switch:
+ tabid = self.parent.notebook.page_num(self.tab)
+ self.parent.goto_tab(tabid)
+
+
+ def title_changed(self, gtk_only = True): # GTK-only is for indexes
+ '''self.title has changed, update the tabs list'''
+
+ tab_titles = config['tab_titles']
+ tab_indexes = config['tab_indexes']
+ show_ellipsis = config['show_ellipsis']
+ max_title_len = config['max_title_len']
+
+ # Unicode heavy strings do not like being truncated/sliced so by
+ # re-encoding the string sliced of limbs are removed.
+ self.tabtitle = self.title[:max_title_len + int(show_ellipsis)]
+ if type(self.tabtitle) != types.UnicodeType:
+ self.tabtitle = unicode(self.tabtitle, 'utf-8', 'ignore')
+
+ self.tabtitle = self.tabtitle.encode('utf-8', 'ignore').strip()
+
+ if show_ellipsis and len(self.tabtitle) != len(self.title):
+ self.tabtitle += "\xe2\x80\xa6"
+
+ gtk_tab_format = "%d %s"
+ index = self.parent.notebook.page_num(self.tab)
+ if tab_titles and tab_indexes:
+ self.parent.notebook.set_tab_label_text(self.tab,
+ gtk_tab_format % (index, self.tabtitle))
+ elif tab_titles:
+ self.parent.notebook.set_tab_label_text(self.tab, self.tabtitle)
+ else:
+ self.parent.notebook.set_tab_label_text(self.tab, str(index))
+
+ # If instance is current tab, update window title
+ if index == self.parent.notebook.get_current_page():
+ title_format = "%s - Uzbl Browser"
+ self.parent.window.set_title(title_format % self.title)
+
+ # Non-GTK tabs
+ if not gtk_only:
+ self.parent.update_tablist()
+
+
+ def set(self, key, val):
+ ''' Send the SET command to Uzbl '''
+
+ if self._client:
+ self._client.send('set %s = %s') #TODO: escape chars ?
+
+
+ def exit(self):
+ ''' Ask the Uzbl instance to close '''
+
+ if self._client:
+ self._client.send('exit')
+
+
+ def parse_command(self, cmd):
+ ''' Parse event givent by the Uzbl instance '''
+
+ type, _, args = cmd.split(" ", 2)
+ if type == "EVENT":
+ type, args = args.split(" ", 1)
+ if type == "TITLE_CHANGED":
+ self.title = args
+ self.title_changed()
+ elif type == "VARIABLE_SET":
+ var, _, val = args.split(" ", 2)
+ try:
+ val = int(val)
+ except:
+ pass
+
+ if var in UZBL_TABBED_VARS:
+ if config[var] != val:
+ config[var] = val
+ if var == "show_gtk_tabs":
+ self.parent.notebook.set_show_tabs(bool(val))
+ elif var == "show_tablist" or var == "tablist_top":
+ self.parent.update_tablist_display()
+ elif var == "gtk_tab_pos":
+ self.parent.update_gtk_tab_pos()
+ elif var == "status_background":
+ col = gtk.gdk.color_parse(config['status_background'])
+ self.parent.ebox.modify_bg(gtk.STATE_NORMAL, col)
+ elif var == "tab_titles" or var == "tab_indexes":
+ for tab in self.parent.notebook:
+ self.parent.tabs[tab].title_changed(True)
+
+ self.parent.update_tablist()
+ else:
+ config[var] = val
+
+ if var == "uri":
+ self.uri = var
+ self.parent.update_tablist()
+ elif type == "NEW_TAB":
+ self.parent.new_tab(args)
+ elif type == "NEXT_TAB":
+ if args:
+ self.parent.next_tab(int(args))
+ else:
+ self.parent.next_tab()
+ elif type == "PREV_TAB":
+ if args:
+ self.parent.prev_tab(int(args))
+ else:
+ self.parent.prev_tab()
+ elif type == "GOTO_TAB":
+ self.parent.goto_tab(int(args))
+ elif type == "FIRST_TAB":
+ self.parent.goto_tab(0)
+ elif type == "LAST_TAB":
+ self.parent.goto_tab(-1)
+ elif type == "PRESET_TABS":
+ self.parent.parse_command(["preset"] + args.split())
+ elif type == "BRING_TO_FRONT":
+ self.parent.window.present()
+ elif type == "CLEAN_TABS":
+ self.parent.clean_slate()
+ elif type == "EXIT_ALL_TABS":
+ self.parent.quitrequest()
+
+
+ def close(self):
+ '''The remote instance exited'''
+
+ if self._client:
+ self._client.close()
+ self._client = None
+
+
+class UzblTabbed:
+ '''A tabbed version of uzbl using gtk.Notebook'''
+
+ def __init__(self):
+ '''Create tablist, window and notebook.'''
+
+ self._timers = {}
+ self._buffer = ""
+ self._killed = False
+
+ # A list of the recently closed tabs
+ self._closed = []
+
+ # Holds metadata on the uzbl childen open.
+ self.tabs = {}
+
+ # Uzbl sockets (socket => SocketClient)
+ self.clients = {}
+
+ # Generates a unique id for uzbl socket filenames.
+ self.next_pid = counter().next
+
+ # Create main window
+ self.window = gtk.Window()
+ try:
+ window_size = map(int, config['window_size'].split(','))
+ self.window.set_default_size(*window_size)
+
+ except:
+ error("Invalid value for default_size in config file.")
+
+ self.window.set_title("Uzbl Browser")
+ self.window.set_border_width(0)
+
+ # Set main window icon
+ icon_path = config['icon_path']
+ if os.path.exists(icon_path):
+ self.window.set_icon(gtk.gdk.pixbuf_new_from_file(icon_path))
+
+ else:
+ icon_path = '/usr/share/uzbl/examples/data/uzbl.png'
+ if os.path.exists(icon_path):
+ self.window.set_icon(gtk.gdk.pixbuf_new_from_file(icon_path))
+
+ # Attach main window event handlers
+ self.window.connect("delete-event", self.quitrequest)
+
+ # Create tab list
+ vbox = gtk.VBox()
+ self.vbox = vbox
+ self.window.add(vbox)
+ ebox = gtk.EventBox()
+ self.ebox = ebox
+ self.tablist = gtk.Label()
+
+ self.tablist.set_use_markup(True)
+ self.tablist.set_justify(gtk.JUSTIFY_LEFT)
+ self.tablist.set_line_wrap(False)
+ self.tablist.set_selectable(False)
+ self.tablist.set_padding(2,2)
+ self.tablist.set_alignment(0,0)
+ self.tablist.set_ellipsize(pango.ELLIPSIZE_END)
+ self.tablist.set_text(" ")
+ self.tablist.show()
+ ebox.add(self.tablist)
+ ebox.show()
+ bgcolor = gtk.gdk.color_parse(config['status_background'])
+ ebox.modify_bg(gtk.STATE_NORMAL, bgcolor)
+
+ # Create notebook
+ self.notebook = gtk.Notebook()
+ self.notebook.set_show_tabs(config['show_gtk_tabs'])
+
+ # Set tab position
+ self.update_gtk_tab_pos()
+
+ self.notebook.set_show_border(False)
+ self.notebook.set_scrollable(True)
+ self.notebook.set_border_width(0)
+
+ self.notebook.connect("page-removed", self.tab_closed)
+ self.notebook.connect("switch-page", self.tab_changed)
+ self.notebook.connect("page-added", self.tab_opened)
+
+ self.notebook.show()
+ vbox.pack_start(self.notebook, True, True, 0)
+ vbox.reorder_child(self.notebook, 1)
+ self.update_tablist_display()
+
+ self.vbox.show()
+ self.window.show()
+ self.wid = self.notebook.window.xid
+
+ # Store information about the applications fifo and socket.
+ fifo_filename = 'uzbltabbed_%d.fifo' % os.getpid()
+ socket_filename = 'uzbltabbed_%d.socket' % os.getpid()
+ self._fifo = None
+ self._socket = None
+ self.fifo_path = os.path.join(config['fifo_dir'], fifo_filename)
+ self.socket_path = os.path.join(config['socket_dir'], socket_filename)
+
+ # Now initialise the fifo and the socket
+ self.init_fifo()
+ self.init_socket()
+
+ # If we are using sessions then load the last one if it exists.
+ if config['save_session']:
+ self.load_session()
+
+
+ def run(self):
+ '''UzblTabbed main function that calls the gtk loop.'''
+
+ if not self.clients and not SocketClient.instances_queue and not self.tabs:
+ self.new_tab()
+
+ gtk_refresh = int(config['gtk_refresh'])
+ if gtk_refresh < 100:
+ gtk_refresh = 100
+
+ # Make SIGTERM act orderly.
+ signal(SIGTERM, lambda signum, stack_frame: self.terminate(SIGTERM))
+
+ # Catch keyboard interrupts
+ signal(SIGINT, lambda signum, stack_frame: self.terminate(SIGINT))
+
+ try:
+ gtk.main()
+
+ except:
+ error("encounted error %r" % sys.exc_info()[1])
+
+ # Unlink fifo socket
+ self.unlink_fifo()
+ self.close_socket()
+
+ # Attempt to close all uzbl instances nicely.
+ self.quitrequest()
+
+ # Allow time for all the uzbl instances to quit.
+ time.sleep(1)
+
+ raise
+
+
+ def terminate(self, termsig=None):
+ '''Handle termination signals and exit safely and cleanly.'''
+
+ # Not required but at least it lets the user know what killed his
+ # browsing session.
+ if termsig == SIGTERM:
+ error("caught SIGTERM signal")
+
+ elif termsig == SIGINT:
+ error("caught keyboard interrupt")
+
+ else:
+ error("caught unknown signal")
+
+ error("commencing infanticide!")
+
+ # Sends the exit signal to all uzbl instances.
+ self.quitrequest()
+
+
+ def init_socket(self):
+ '''Create interprocess communication socket.'''
+
+ def accept(sock, condition):
+ '''A new uzbl instance was created'''
+
+ client, _ = sock.accept()
+ self.clients[client] = SocketClient(client)
+
+ return True
+
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ sock.bind(self.socket_path)
+ sock.listen(1)
+
+ # Add event handler for IO_IN event.
+ self._socket = (sock, io_add_watch(sock, IO_IN, accept))
+
+ echo("[socket] listening at %r" % self.socket_path)
+
+ # Add atexit register to destroy the socket on program termination.
+ atexit.register(self.close_socket)
+
+
+ def close_socket(self):
+ '''Close the socket when closing the application'''
+
+ if self._socket:
+ (fd, watcher) = self._socket
+ source_remove(watcher)
+ fd.close()
+ os.unlink(self.socket_path)
+ self._socket = None
+
+
+ def init_fifo(self):
+ '''Create interprocess communication fifo.'''
+
+ if os.path.exists(self.fifo_path):
+ if not os.access(self.fifo_path, os.F_OK | os.R_OK | os.W_OK):
+ os.mkfifo(self.fifo_path)
+
+ else:
+ basedir = os.path.dirname(self.fifo_path)
+ if not os.path.exists(basedir):
+ os.makedirs(basedir)
+
+ os.mkfifo(self.fifo_path)
+
+ # Add event handlers for IO_IN & IO_HUP events.
+ self.setup_fifo_watchers()
+
+ echo("[fifo] listening at %r" % self.fifo_path)
+
+ # Add atexit register to destroy the fifo on program termination.
+ atexit.register(self.unlink_fifo)
+
+
+ def unlink_fifo(self):
+ '''Unlink the fifo socket. Note: This function is called automatically
+ on exit by an atexit register.'''
+
+ # Make sure the fifo fd is closed.
+ self.close_fifo()
+
+ # And unlink if the real fifo exists.
+ if os.path.exists(self.fifo_path):
+ os.unlink(self.fifo_path)
+ echo("unlinked %r" % self.fifo_path)
+
+
+ def close_fifo(self):
+ '''Remove all event handlers watching the fifo and close the fd.'''
+
+ # Already closed
+ if self._fifo is None: return
+
+ (fd, watchers) = self._fifo
+ os.close(fd)
+
+ # Stop all gobject io watchers watching the fifo.
+ for gid in watchers:
+ source_remove(gid)
+
+ self._fifo = None
+
+
+ def setup_fifo_watchers(self):
+ '''Open fifo socket fd and setup gobject IO_IN & IO_HUP event
+ handlers.'''
+
+ # Close currently open fifo fd and kill all watchers
+ self.close_fifo()
+
+ fd = os.open(self.fifo_path, os.O_RDONLY | os.O_NONBLOCK)
+
+ # Add gobject io event handlers to the fifo socket.
+ watchers = [io_add_watch(fd, IO_IN, self.main_fifo_read),\
+ io_add_watch(fd, IO_HUP, self.main_fifo_hangup)]
+
+ self._fifo = (fd, watchers)
+
+
+ def main_fifo_hangup(self, fd, cb_condition):
+ '''Handle main fifo socket hangups.'''
+
+ # Close old fd, open new fifo socket and add io event handlers.
+ self.setup_fifo_watchers()
+
+ # Kill the gobject event handler calling this handler function.
+ return False
+
+
+ def main_fifo_read(self, fd, cb_condition):
+ '''Read from main fifo socket.'''
+
+ self._buffer = os.read(fd, 1024)
+ temp = self._buffer.split("\n")
+ self._buffer = temp.pop()
+ cmds = [s.strip().split() for s in temp if len(s.strip())]
+
+ for cmd in cmds:
+ try:
+ #print cmd
+ self.parse_command(cmd)
+
+ except:
+ error("parse_command: invalid command %s" % ' '.join(cmd))
+ raise
+
+ return True
+
+
+ def parse_command(self, cmd):
+ '''Parse instructions from uzbl child processes.'''
+
+ # Commands ( [] = optional, {} = required )
+ # new [uri]
+ # open new tab and head to optional uri.
+ # close [tab-num]
+ # close current tab or close via tab id.
+ # next [n-tabs]
+ # open next tab or n tabs down. Supports negative indexing.
+ # prev [n-tabs]
+ # open prev tab or n tabs down. Supports negative indexing.
+ # goto {tab-n}
+ # goto tab n.
+ # first
+ # goto first tab.
+ # last
+ # goto last tab.
+ # title {pid} {document-title}
+ # updates tablist title.
+ # uri {pid} {document-location}
+ # updates tablist uri
+ # bring_to_front
+ # brings the gtk window to focus.
+ # exit
+ # exits uzbl_tabbed.py
+
+ if cmd[0] == "new":
+ if len(cmd) == 2:
+ self.new_tab(cmd[1])
+
+ else:
+ self.new_tab()
+
+ elif cmd[0] == "newfromclip":
+ uri = subprocess.Popen(['xclip','-selection','clipboard','-o'],\
+ stdout=subprocess.PIPE).communicate()[0]
+ if uri:
+ self.new_tab(uri)
+
+ elif cmd[0] == "close":
+ if len(cmd) == 2:
+ self.close_tab(int(cmd[1]))
+
+ else:
+ self.close_tab()
+
+ elif cmd[0] == "next":
+ if len(cmd) == 2:
+ self.next_tab(int(cmd[1]))
+
+ else:
+ self.next_tab()
+
+ elif cmd[0] == "prev":
+ if len(cmd) == 2:
+ self.prev_tab(int(cmd[1]))
+
+ else:
+ self.prev_tab()
+
+ elif cmd[0] == "goto":
+ self.goto_tab(int(cmd[1]))
+
+ elif cmd[0] == "first":
+ self.goto_tab(0)
+
+ elif cmd[0] == "last":
+ self.goto_tab(-1)
+
+ elif cmd[0] in ["title", "uri"]:
+ if len(cmd) > 2:
+ uzbl = self.get_tab_by_name(int(cmd[1]))
+ if uzbl:
+ old = getattr(uzbl, cmd[0])
+ new = ' '.join(cmd[2:])
+ setattr(uzbl, cmd[0], new)
+ if old != new:
+ self.update_tablist()
+
+ else:
+ error("parse_command: no uzbl with name %r" % int(cmd[1]))
+
+ elif cmd[0] == "preset":
+ if len(cmd) < 3:
+ error("parse_command: invalid preset command")
+
+ elif cmd[1] == "save":
+ path = os.path.join(config['saved_sessions_dir'], cmd[2])
+ self.save_session(path)
+
+ elif cmd[1] == "load":
+ path = os.path.join(config['saved_sessions_dir'], cmd[2])
+ self.load_session(path)
+
+ elif cmd[1] == "del":
+ path = os.path.join(config['saved_sessions_dir'], cmd[2])
+ if os.path.isfile(path):
+ os.remove(path)
+
+ else:
+ error("parse_command: preset %r does not exist." % path)
+
+ elif cmd[1] == "list":
+ uzbl = self.get_tab_by_name(int(cmd[2]))
+ if uzbl:
+ if not os.path.isdir(config['saved_sessions_dir']):
+ js = "js alert('No saved presets.');"
+ uzbl._client.send(js)
+
+ else:
+ listdir = os.listdir(config['saved_sessions_dir'])
+ listdir = "\\n".join(listdir)
+ js = "js alert('Session presets:\\n\\n%s');" % listdir
+ uzbl._client.send(js)
+
+ else:
+ error("parse_command: unknown tab name.")
+
+ else:
+ error("parse_command: unknown parse command %r"\
+ % ' '.join(cmd))
+
+ elif cmd[0] == "bring_to_front":
+ self.window.present()
+
+ elif cmd[0] == "clean":
+ self.clean_slate()
+
+ elif cmd[0] == "exit":
+ self.quitrequest()
+
+ else:
+ error("parse_command: unknown command %r" % ' '.join(cmd))
+
+
+ def get_tab_by_name(self, name):
+ '''Return uzbl instance by name.'''
+
+ for (tab, uzbl) in self.tabs.items():
+ if uzbl.name == name:
+ return uzbl
+
+ return False
+
+
+ def new_tab(self, uri='', title='', switch=None):
+ '''Add a new tab to the notebook and start a new instance of uzbl.
+ Use the switch option to negate config['switch_to_new_tabs'] option
+ when you need to load multiple tabs at a time (I.e. like when
+ restoring a session from a file).'''
+
+ tab = gtk.Socket()
+ tab.show()
+ self.notebook.append_page(tab)
+ sid = tab.get_id()
+ uri = uri.strip()
+ name = "%d-%d" % (os.getpid(), self.next_pid())
+
+ if switch is None:
+ switch = config['switch_to_new_tabs']
+
+ if not title:
+ title = config['new_tab_title']
+
+ cmd = ['uzbl-browser', '-n', name, '-s', str(sid),
+ '--connect-socket', self.socket_path, '--uri', uri]
+ subprocess.Popen(cmd) # TODO: do i need close_fds=True ?
+
+ uzbl = UzblInstance(self, tab, name, uri, title, switch)
+ SocketClient.instances_queue[name] = uzbl
+ self.tabs[tab] = uzbl
+
+
+ def clean_slate(self):
+ '''Close all open tabs and open a fresh brand new one.'''
+
+ self.new_tab()
+ tabs = self.tabs.keys()
+ for tab in list(self.notebook)[:-1]:
+ if tab not in tabs: continue
+ uzbl = self.tabs[tab]
+ uzbl.exit()
+
+
+ def config_uzbl(self, uzbl):
+ '''Send bind commands for tab new/close/next/prev to a uzbl
+ instance.'''
+
+ # Set definitions here
+ # set(key, command back to fifo)
+ if config['capture_new_windows']:
+ uzbl.set("new_window", r'new $8')
+
+
+ def goto_tab(self, index):
+ '''Goto tab n (supports negative indexing).'''
+
+ title_format = "%s - Uzbl Browser"
+
+ tabs = list(self.notebook)
+ if 0 <= index < len(tabs):
+ self.notebook.set_current_page(index)
+ uzbl = self.tabs[self.notebook.get_nth_page(index)]
+ self.window.set_title(title_format % uzbl.title)
+ self.update_tablist()
+ return None
+
+ try:
+ tab = tabs[index]
+ # Update index because index might have previously been a
+ # negative index.
+ index = tabs.index(tab)
+ self.notebook.set_current_page(index)
+ uzbl = self.tabs[self.notebook.get_nth_page(index)]
+ self.window.set_title(title_format % uzbl.title)
+ self.update_tablist()
+
+ except IndexError:
+ pass
+
+
+ def next_tab(self, step=1):
+ '''Switch to next tab or n tabs right.'''
+
+ if step < 1:
+ error("next_tab: invalid step %r" % step)
+ return None
+
+ ntabs = self.notebook.get_n_pages()
+ tabn = (self.notebook.get_current_page() + step) % ntabs
+ self.goto_tab(tabn)
+
+
+ def prev_tab(self, step=1):
+ '''Switch to prev tab or n tabs left.'''
+
+ if step < 1:
+ error("prev_tab: invalid step %r" % step)
+ return None
+
+ ntabs = self.notebook.get_n_pages()
+ tabn = self.notebook.get_current_page() - step
+ while tabn < 0: tabn += ntabs
+ self.goto_tab(tabn)
+
+
+ def close_tab(self, tabn=None):
+ '''Closes current tab. Supports negative indexing.'''
+
+ if tabn is None:
+ tabn = self.notebook.get_current_page()
+
+ else:
+ try:
+ tab = list(self.notebook)[tabn]
+
+ except IndexError:
+ error("close_tab: invalid index %r" % tabn)
+ return None
+
+ self.notebook.remove_page(tabn)
+
+
+ def tab_opened(self, notebook, tab, index):
+ '''Called upon tab creation. Called by page-added signal.'''
+
+ if config['switch_to_new_tabs']:
+ self.notebook.set_focus_child(tab)
+
+ else:
+ oldindex = self.notebook.get_current_page()
+ oldtab = self.notebook.get_nth_page(oldindex)
+ self.notebook.set_focus_child(oldtab)
+
+
+ def tab_closed(self, notebook, tab, index):
+ '''Close the window if no tabs are left. Called by page-removed
+ signal.'''
+
+ if tab in self.tabs.keys():
+ uzbl = self.tabs[tab]
+ uzbl.close()
+
+ self._closed.append((uzbl.uri, uzbl.title))
+ self._closed = self._closed[-10:]
+ del self.tabs[tab]
+
+ if self.notebook.get_n_pages() == 0:
+ if not self._killed and config['save_session']:
+ if os.path.exists(config['session_file']):
+ os.remove(config['session_file'])
+
+ self.quit()
+
+ for tab in self.notebook:
+ self.tabs[tab].title_changed(True)
+ self.update_tablist()
+
+ return True
+
+
+ def tab_changed(self, notebook, page, index):
+ '''Refresh tab list. Called by switch-page signal.'''
+
+ tab = self.notebook.get_nth_page(index)
+ self.notebook.set_focus_child(tab)
+ self.update_tablist(index)
+ return True
+
+
+ def update_tablist_display(self):
+ '''Called when show_tablist or tablist_top has changed'''
+
+ if self.ebox in self.vbox.get_children():
+ self.vbox.remove(self.ebox)
+
+ if config['show_tablist']:
+ self.vbox.pack_start(self.ebox, False, False, 0)
+ if config['tablist_top']:
+ self.vbox.reorder_child(self.ebox, 0)
+ else:
+ self.vbox.reorder_child(self.ebox, 2)
+
+ def update_gtk_tab_pos(self):
+ ''' Called when gtk_tab_pos has changed '''
+
+ allposes = {'left': gtk.POS_LEFT, 'right':gtk.POS_RIGHT,
+ 'top':gtk.POS_TOP, 'bottom':gtk.POS_BOTTOM}
+ if config['gtk_tab_pos'] in allposes.keys():
+ self.notebook.set_tab_pos(allposes[config['gtk_tab_pos']])
+
+
+ def update_tablist(self, curpage=None):
+ '''Upate tablist status bar.'''
+
+ if not config['show_tablist']:
+ return True
+
+ tab_titles = config['tab_titles']
+ tab_indexes = config['tab_indexes']
+ multiline_tabs = config['multiline_tabs']
+
+ if multiline_tabs:
+ multiline = []
+
+ tabs = self.tabs.keys()
+ if curpage is None:
+ curpage = self.notebook.get_current_page()
+
+ pango = ""
+ normal = (config['tab_colours'], config['tab_text_colours'])
+ selected = (config['selected_tab'], config['selected_tab_text'])
+
+ if tab_titles and tab_indexes:
+ tab_format = "<span %(tabc)s> [ %(index)d <span %(textc)s> %(title)s</span> ] </span>"
+ elif tab_titles:
+ tab_format = "<span %(tabc)s> [ <span %(textc)s>%(title)s</span> ] </span>"
+ else:
+ tab_format = "<span %(tabc)s> [ <span %(textc)s>%(index)d</span> ] </span>"
+
+ for index, tab in enumerate(self.notebook):
+ if tab not in tabs: continue
+ uzbl = self.tabs[tab]
+ title = escape(uzbl.tabtitle)
+
+ style = colour_selector(index, curpage, uzbl)
+ (tabc, textc) = style
+
+ if multiline_tabs:
+ opango = pango
+
+ pango += tab_format % locals()
+
+ self.tablist.set_markup(pango)
+ listwidth = self.tablist.get_layout().get_pixel_size()[0]
+ winwidth = self.window.get_size()[0]
+
+ if listwidth > (winwidth - 20):
+ multiline.append(opango)
+ pango = tab_format % locals()
+ else:
+ pango += tab_format % locals()
+
+ if multiline_tabs:
+ multiline.append(pango)
+ self.tablist.set_markup('&#10;'.join(multiline))
+
+ else:
+ self.tablist.set_markup(pango)
+
+ return True
+
+
+ def save_session(self, session_file=None):
+ '''Save the current session to file for restoration on next load.'''
+
+ strip = str.strip
+
+ if session_file is None:
+ session_file = config['session_file']
+
+ tabs = self.tabs.keys()
+ state = []
+ for tab in list(self.notebook):
+ if tab not in tabs: continue
+ uzbl = self.tabs[tab]
+ if not uzbl.uri: continue
+ state += [(uzbl.uri, uzbl.title),]
+
+ session = {'curtab': self.notebook.get_current_page(),
+ 'tabs': state}
+
+ if config['json_session']:
+ raw = json.dumps(session)
+
+ else:
+ lines = ["curtab = %d" % session['curtab'],]
+ for (uri, title) in session['tabs']:
+ lines += ["%s\t%s" % (strip(uri), strip(title)),]
+
+ raw = "\n".join(lines)
+
+ if not os.path.isfile(session_file):
+ dirname = os.path.dirname(session_file)
+ if not os.path.isdir(dirname):
+ os.makedirs(dirname)
+
+ h = open(session_file, 'w')
+ h.write(raw)
+ h.close()
+
+
+ def load_session(self, session_file=None):
+ '''Load a saved session from file.'''
+
+ default_path = False
+ strip = str.strip
+ json_session = config['json_session']
+ delete_loaded = False
+
+ if session_file is None:
+ default_path = True
+ delete_loaded = True
+ session_file = config['session_file']
+
+ if not os.path.isfile(session_file):
+ return False
+
+ h = open(session_file, 'r')
+ raw = h.read()
+ h.close()
+ if json_session:
+ if sum([1 for s in raw.split("\n") if strip(s)]) != 1:
+ error("Warning: The session file %r does not look json. "\
+ "Trying to load it as a non-json session file."\
+ % session_file)
+ json_session = False
+
+ if json_session:
+ try:
+ session = json.loads(raw)
+ curtab, tabs = session['curtab'], session['tabs']
+
+ except:
+ error("Failed to load jsonifed session from %r"\
+ % session_file)
+ return None
+
+ else:
+ tabs = []
+ strip = str.strip
+ curtab, tabs = 0, []
+ lines = [s for s in raw.split("\n") if strip(s)]
+ if len(lines) < 2:
+ error("Warning: The non-json session file %r looks invalid."\
+ % session_file)
+ return None
+
+ try:
+ for line in lines:
+ if line.startswith("curtab"):
+ curtab = int(line.split()[-1])
+
+ else:
+ uri, title = line.split("\t",1)
+ tabs += [(strip(uri), strip(title)),]
+
+ except:
+ error("Warning: failed to load session file %r" % session_file)
+ return None
+
+ session = {'curtab': curtab, 'tabs': tabs}
+
+ # Now populate notebook with the loaded session.
+ for (index, (uri, title)) in enumerate(tabs):
+ self.new_tab(uri=uri, title=title, switch=(curtab==index))
+
+ # A saved session has been loaded now delete it.
+ if delete_loaded and os.path.exists(session_file):
+ os.remove(session_file)
+
+ # There may be other state information in the session dict of use to
+ # other functions. Of course however the non-json session object is
+ # just a dummy object of no use to no one.
+ return session
+
+
+ def quitrequest(self, *args):
+ '''Attempt to close all uzbl instances nicely and exit.'''
+
+ self._killed = True
+
+ if config['save_session']:
+ if len(list(self.notebook)) > 1:
+ self.save_session()
+
+ else:
+ # Notebook has one page open so delete the session file.
+ if os.path.isfile(config['session_file']):
+ os.remove(config['session_file'])
+
+ for (tab, uzbl) in self.tabs.items():
+ uzbl.exit()
+
+ # Add a gobject timer to make sure the application force-quits after a
+ # reasonable period. Calling quit when all the tabs haven't had time to
+ # close should be a last resort.
+ timer = "force-quit"
+ timerid = timeout_add(5000, self.quit, timer)
+ self._timers[timer] = timerid
+
+
+ def quit(self, *args):
+ '''Cleanup and quit. Called by delete-event signal.'''
+
+ # Close the fifo socket, remove any gobject io event handlers and
+ # delete socket.
+ self.unlink_fifo()
+ self.close_socket()
+
+ # Remove all gobject timers that are still ticking.
+ for (timerid, gid) in self._timers.items():
+ source_remove(gid)
+ del self._timers[timerid]
+
+ try:
+ gtk.main_quit()
+
+ except:
+ pass
+
+
+if __name__ == "__main__":
+
+ # Build command line parser
+ usage = "usage: %prog [OPTIONS] {URIS}..."
+ parser = OptionParser(usage=usage)
+ parser.add_option('-n', '--no-session', dest='nosession',
+ action='store_true', help="ignore session saving a loading.")
+ parser.add_option('-v', '--verbose', dest='verbose',
+ action='store_true', help='print verbose output.')
+
+ # Parse command line options
+ (options, uris) = parser.parse_args()
+
+ if options.nosession:
+ config['save_session'] = False
+
+ if options.verbose:
+ config['verbose'] = True
+
+ if config['json_session']:
+ try:
+ import simplejson as json
+
+ except:
+ error("Warning: json_session set but cannot import the python "\
+ "module simplejson. Fix: \"set json_session = 0\" or "\
+ "install the simplejson python module to remove this warning.")
+ config['json_session'] = False
+
+ if config['verbose']:
+ import pprint
+ sys.stderr.write("%s\n" % pprint.pformat(config))
+
+ uzbl = UzblTabbed()
+
+ # All extra arguments given to uzbl_tabbed.py are interpreted as
+ # web-locations to opened in new tabs.
+ lasturi = len(uris)-1
+ for (index,uri) in enumerate(uris):
+ uzbl.new_tab(uri, switch=(index==lasturi))
+
+ uzbl.run()
diff --git a/examples/data/scripts/uzblcat b/examples/data/scripts/uzblcat
new file mode 100755
index 0000000..e955608
--- /dev/null
+++ b/examples/data/scripts/uzblcat
@@ -0,0 +1,12 @@
+#!/usr/bin/env python
+# uzblcat - safely push html to uzbl
+# See http://www.uzbl.org/wiki/html-mode
+
+from sys import stdin, stdout
+
+stdout.write("uri data:text/html,")
+for line in stdin:
+ stdout.write(line[0:-1])
+
+# vim: set noet ff=unix
+
diff --git a/examples/data/style.css b/examples/data/style.css
new file mode 100644
index 0000000..f9b111e
--- /dev/null
+++ b/examples/data/style.css
@@ -0,0 +1,25 @@
+.uzbl_highlight { background-color: yellow;}
+.uzbl_h_first { background-color: lightgreen;}
+
+.uzbl_follow { border-style: dotted;
+ border-width: thin;
+}
+
+#uzbl_hint > div {
+ display: inline;
+ border: 2px solid #4a6600;
+ background-color: #b9ff00;
+ color: black;
+ font-size: 9px;
+ font-weight: bold;
+ line-height: 9px;
+ margin: 0px;
+ padding: 0px;
+ position: absolute;
+ z-index: 1000;
+ -webkit-border-radius: 6px;
+ text-decoration: none;
+ -wekit-transform: scale(1) rotate(0deg) translate(-6px,-5px);
+}
+
+/* vim:set et ts=4: */
diff --git a/examples/data/uzbl.png b/examples/data/uzbl.png
new file mode 100644
index 0000000..773ea84
--- /dev/null
+++ b/examples/data/uzbl.png
Binary files differ