diff options
author | Paweł Zuzelski <pawelz@pld-linux.org> | 2010-02-17 18:25:49 +0100 |
---|---|---|
committer | Paweł Zuzelski <pawelz@pld-linux.org> | 2010-02-17 18:25:49 +0100 |
commit | 025babb5684e8c7544e1d9c0c899b08387a9d61e (patch) | |
tree | 14ffb30c2160284683870e1fedc150d758a47dfc /examples | |
parent | caa401e1642c49368d61597284defc4f61107798 (diff) | |
parent | 7bfb3b5b56e30b157cfb750657de104055fe6da3 (diff) |
Merge remote branch 'Dieterbe/experimental' into eFormFiller
Diffstat (limited to 'examples')
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 [('&','&'), ('<', '<'), ('>', '>')]: + 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(' '.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 Binary files differnew file mode 100644 index 0000000..773ea84 --- /dev/null +++ b/examples/data/uzbl.png |