aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.gitattributes2
-rw-r--r--.gitignore5
-rw-r--r--AUTHORS74
-rw-r--r--Makefile135
-rw-r--r--README782
-rw-r--r--docs/COMMUNITY33
-rw-r--r--docs/CONTRIBUTING108
-rw-r--r--docs/FAQ146
-rw-r--r--docs/INSTALL62
-rw-r--r--docs/TODO81
-rw-r--r--docs/config-syntax21
-rw-r--r--docs/multiple-instances-management41
-rw-r--r--docs/performance9
-rw-r--r--docs/url-editing9
-rw-r--r--examples/config/config389
-rw-r--r--examples/config/cookies22
-rw-r--r--examples/data/bookmarks4
-rw-r--r--examples/data/forms/bbs.archlinux.org5
-rw-r--r--examples/data/plugins/bind.py492
-rw-r--r--examples/data/plugins/cmd_expand.py42
-rw-r--r--examples/data/plugins/completion.py206
-rw-r--r--examples/data/plugins/config.py97
-rw-r--r--examples/data/plugins/keycmd.py571
-rw-r--r--examples/data/plugins/mode.py176
-rw-r--r--examples/data/plugins/on_event.py107
-rw-r--r--examples/data/plugins/plugin_template.py76
-rw-r--r--examples/data/plugins/progress_bar.py159
-rwxr-xr-xexamples/data/scripts/auth.py53
-rwxr-xr-xexamples/data/scripts/cookies.sh154
-rwxr-xr-xexamples/data/scripts/download.sh22
-rw-r--r--examples/data/scripts/extedit.js102
-rw-r--r--examples/data/scripts/follow.js269
-rw-r--r--examples/data/scripts/follower.js420
-rwxr-xr-xexamples/data/scripts/formfiller.pl99
-rwxr-xr-xexamples/data/scripts/formfiller.sh62
-rw-r--r--examples/data/scripts/hint.js26
-rwxr-xr-xexamples/data/scripts/history.sh5
-rwxr-xr-xexamples/data/scripts/insert_bookmark.sh16
-rwxr-xr-xexamples/data/scripts/instance-select-wmii.sh54
-rw-r--r--examples/data/scripts/linkfollow.js269
-rwxr-xr-xexamples/data/scripts/load_url_from_bookmarks.sh20
-rwxr-xr-xexamples/data/scripts/load_url_from_history.sh24
-rwxr-xr-xexamples/data/scripts/scheme.py24
-rw-r--r--examples/data/scripts/scroll-percentage.js68
-rwxr-xr-xexamples/data/scripts/session.sh62
-rwxr-xr-xexamples/data/scripts/uzbl-cookie-daemon664
-rwxr-xr-xexamples/data/scripts/uzbl-event-manager837
-rwxr-xr-xexamples/data/scripts/uzbl-tabbed1417
-rwxr-xr-xexamples/data/scripts/uzblcat12
-rw-r--r--examples/data/style.css25
-rw-r--r--examples/data/uzbl.pngbin0 -> 2185 bytes
-rwxr-xr-xmisc/cleanprocs-files.sh10
-rwxr-xr-xmisc/dmenu-performancetest-bench.sh17
-rwxr-xr-xmisc/dmenu-performancetest-generate-dummy-history-file.sh12
-rwxr-xr-xmisc/env.sh23
-rwxr-xr-xmisc/fifotest.sh20
-rwxr-xr-xmisc/hash.sh25
-rw-r--r--src/callbacks.c781
-rw-r--r--src/callbacks.h210
-rw-r--r--src/config.h11
-rw-r--r--src/events.c208
-rw-r--r--src/events.h37
-rw-r--r--src/inspector.c103
-rw-r--r--src/inspector.h7
-rwxr-xr-xsrc/uzbl-browser66
-rw-r--r--src/uzbl-core.c2709
-rw-r--r--src/uzbl-core.h504
-rw-r--r--tests/Makefile17
-rw-r--r--tests/test-command.c377
-rw-r--r--tests/test-expand.c209
70 files changed, 13904 insertions, 0 deletions
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..9c6b849
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+examples/data/uzbl/forms/bbs.archlinux.org whitespace=-trailing-space
+hash.sh export-subst
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..078164f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+uzbl-core
+*.o
+*.pyc
+*~
+tags
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000..e66d399
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,74 @@
+Uzbl would never be what it is today without the help of these people.
+
+
+Special roles:
+- global/C code management/QA: Dieter, Rob
+- python code management/QA: Mason, Tom
+- website/mailinglist/bugtracker/...: Dieter, Michael W.
+
+In alphabetical order:
+
+ (bobpaul) - session script patches
+ (dequis) - Uzbl.run, birectional socket, javascript commands
+ (evocallaghan) - tiny patches
+ (israellevin) - toggle_zoom_type
+ (kmeaw) - fix for multibyte utf8 characters segfault
+ (mxf) - uzblcat
+ (neutralinsomniac) - load_progress = 0 fix
+ (salinasv) - move some variables to heap
+ (sentientswitch) - Cleaned up code. Added some commands.
+ (tczy) - patches
+ Aaron Griffin (phrakture) - Makefile patches to build on OSX
+ Abel Camarillo (00z) - various portability fixes, such as BSD fixes for Makefile and posix shell scripts
+ Aldrik Dunbar (n30n) - scroll % script
+ Andraž 'ruskie' Levstik - font_family patch
+ Andrea Marchesini <baku@ippolita.net> - resource-request-starting
+ Andy Spencer - security fixes
+ Barak A. Pearlmutter - typo fix
+ Brendan Taylor (bct) - various bugfixes, making misc variables much better using expand(), refactoring some internal var stuff
+ Ciprian Dorin, Craciun - patches
+ Chris Mason - code snippets such as basic cookie handler
+ Chris van Dijk (quigybo) - work on uzbl_tabbed.py
+ Damien Leon - misc
+ Daniel M. Hackney - documentation cleanups
+ David Keijser - various C and python patches.
+ Devon Jones <devon.jones@gmail.com> - uzbl_tabbed: bring_to_front
+ Dieter Plaetinck (Dieter@be) <dieter AT plaetinck.be> - several contributions
+ Dusan Popovic (dusanx) - many contributions to early uzbl
+ Evgeny Grablyk - libsoup settings
+ Helmut Grohne (helmut) - move void **ptr to union, various fixes
+ Henri Kemppainen (DuClare) <email is akarinotengoku AT THE DOMAIN OF gmail.com> - many contributions, mostly old handler code
+ Igor Bogomazov - mouse ptr events
+ Jake Probst <jake.probst@gmail.com> - uzbl_tabbed: multiline tablist
+ James S Wheaton (uranther) - zoom level, test framework
+ Jan Kolkmeier (jouz) - scrolling, link following
+ Jason Woofenden (JasonWoof) - geometry=maximized, link following
+ Laurence Withers (lwithers) - talk_to_socket
+ Mark Nevill - misc patches
+ Mason Larobina - uzbl-tabbed.py, cookie-daemon, event-manager&plugins, ...
+ Maximilian Gaß (mxey) - several small patches
+ Michael Fiano (axionix) - added cookie_daemon.py whitelist
+ Michael Walker (Barrucadu) <mike AT barrucadu.co.uk> - contributions to early uzbl
+ Moritz Lenz - small doc fix
+ Nicolas Pouillard - refactored scroll command
+ Olivier Schwander - auto file:// prepend
+ Paweł Zuzelski (pawelz) - http auth handler, misc patches
+ Peter Suschlik - backwards searching
+ Přemysl Hrubý (anydot) <email is dfenze AT gmail.com> - several C contributions and cleanups
+ Robert Manea (robm) <email is rob DOT manea AT gmail DOT com> - C code all over the place
+ Sergey Shepelev (temoto) - doc patch
+ Simon Lipp (sloonz) - various patches, EM contributions
+ Sylvester Johansson (scj) - form filler script & different take on link follower
+ Tassilo Horn (tsdh) - $VISUAL patch
+ Thorsten Wilms - logo design
+ Tom Adams (holizz) - few patches, cookies.py, gtkplug/socket & proof of concept uzbl_tabbed.py, scheme_handler
+ Uli Schlachter (psychon) - basic mime_policy_cb & Makefile patch
+ Zane Ashby (HashBox) - Rewrote FIFO interface. Fixed various bugs.
+
+Also, thanks to all people who've posted useful things to the mailing list and/or wiki.
+
+
+Originaly based on http://trac.webkit.org/browser/trunk/WebKitTools/GtkLauncher/main.c
+Which is copyrighted:
+2006, 2007 Apple Inc
+2007 Alp Toker <alp@atoker.com>
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..1b77f6f
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,135 @@
+# first entries are for gnu make, 2nd for BSD make. see http://lists.uzbl.org/pipermail/uzbl-dev-uzbl.org/2009-July/000177.html
+
+CFLAGS:=-std=c99 $(shell pkg-config --cflags gtk+-2.0 webkit-1.0 libsoup-2.4 gthread-2.0) -ggdb -Wall -W -DARCH="\"$(shell uname -m)\"" -lgthread-2.0 -DCOMMIT="\"$(shell ./misc/hash.sh)\"" $(CPPFLAGS) -fPIC -W -Wall -Wextra -pedantic
+CFLAGS!=echo -std=c99 `pkg-config --cflags gtk+-2.0 webkit-1.0 libsoup-2.4 gthread-2.0` -ggdb -Wall -W -DARCH='"\""'`uname -m`'"\""' -lgthread-2.0 -DCOMMIT='"\""'`./misc/hash.sh`'"\""' $(CPPFLAGS) -fPIC -W -Wall -Wextra -pedantic
+
+LDFLAGS:=$(shell pkg-config --libs gtk+-2.0 webkit-1.0 libsoup-2.4 gthread-2.0) -pthread $(LDFLAGS)
+LDFLAGS!=echo `pkg-config --libs gtk+-2.0 webkit-1.0 libsoup-2.4 gthread-2.0` -pthread $(LDFLAGS)
+
+SRC = $(wildcard src/*.c)
+HEAD = $(wildcard src/*.h)
+TOBJ = $(SRC:.c=.o)
+OBJ = $(foreach obj, $(TOBJ), $(notdir $(obj)))
+
+all: uzbl-browser options
+
+options:
+ @echo
+ @echo BUILD OPTIONS:
+ @echo "CFLAGS = ${CFLAGS}"
+ @echo "LDFLAGS = ${LDFLAGS}"
+ @echo
+ @echo See the README file for usage instructions.
+
+
+.c.o:
+ @echo COMPILING $<
+ @${CC} -c ${CFLAGS} $<
+ @echo ... done.
+
+${OBJ}: ${HEAD}
+
+uzbl-core: ${TOBJ} # why doesn't ${OBJ} work?
+ @echo
+ @echo LINKING object files
+ @${CC} -o $@ ${OBJ} ${LDFLAGS}
+ @echo ... done.
+
+
+uzbl-browser: uzbl-core
+
+# packagers, set DESTDIR to your "package directory" and PREFIX to the prefix you want to have on the end-user system
+# end-users who build from source: don't care about DESTDIR, update PREFIX if you want to
+# RUN_PREFIX : what the prefix is when the software is run. usually the same as PREFIX
+PREFIX?=/usr/local
+INSTALLDIR?=$(DESTDIR)$(PREFIX)
+RUN_PREFIX?=$(PREFIX)
+
+# the 'tests' target can never be up to date
+.PHONY: tests
+force:
+
+# When compiling unit tests, compile uzbl as a library first
+tests: ${OBJ} force
+ $(CC) -shared -Wl ${OBJ} -o ./tests/libuzbl-core.so
+ cd ./tests/; $(MAKE)
+
+test-uzbl-core: uzbl-core
+ ./uzbl-core --uri http://www.uzbl.org --verbose
+
+test-uzbl-browser: uzbl-browser
+ ./src/uzbl-browser --uri http://www.uzbl.org --verbose
+
+test-uzbl-core-sandbox: uzbl-core
+ make DESTDIR=./sandbox RUN_PREFIX=`pwd`/sandbox/usr/local install-uzbl-core
+ make DESTDIR=./sandbox RUN_PREFIX=`pwd`/sandbox/usr/local install-example-data
+ cp -np ./misc/env.sh ./sandbox/env.sh
+ source ./sandbox/env.sh && uzbl-core --uri http://www.uzbl.org --verbose
+ make DESTDIR=./sandbox uninstall
+ rm -rf ./sandbox/usr
+
+test-uzbl-browser-sandbox: uzbl-browser
+ make DESTDIR=./sandbox RUN_PREFIX=`pwd`/sandbox/usr/local install-uzbl-core
+ make DESTDIR=./sandbox RUN_PREFIX=`pwd`/sandbox/usr/local install-uzbl-browser
+ make DESTDIR=./sandbox RUN_PREFIX=`pwd`/sandbox/usr/local install-example-data
+ cp -np ./misc/env.sh ./sandbox/env.sh
+ source ./sandbox/env.sh && uzbl-cookie-daemon restart -nv &
+ source ./sandbox/env.sh && uzbl-event-manager restart -nav &
+ source ./sandbox/env.sh && uzbl-browser --uri http://www.uzbl.org --verbose
+ source ./sandbox/env.sh && uzbl-cookie-daemon stop -v
+ source ./sandbox/env.sh && uzbl-event-manager stop -v
+ make DESTDIR=./sandbox uninstall
+ rm -rf ./sandbox/usr
+
+clean:
+ rm -f uzbl-core
+ rm -f uzbl-core.o
+ rm -f events.o
+ rm -f callbacks.o
+ rm -f inspector.o
+ find ./examples/ -name "*.pyc" -delete
+ cd ./tests/; $(MAKE) clean
+ rm -rf ./sandbox/
+
+strip:
+ @echo Stripping binary
+ @strip uzbl-core
+ @echo ... done.
+
+install: install-uzbl-core install-uzbl-browser install-uzbl-tabbed
+
+install-uzbl-core: all
+ install -d $(INSTALLDIR)/bin
+ install -d $(INSTALLDIR)/share/uzbl/docs
+ install -d $(INSTALLDIR)/share/uzbl/examples
+ cp -rp docs $(INSTALLDIR)/share/uzbl/
+ cp -rp src/config.h $(INSTALLDIR)/share/uzbl/docs/
+ cp -rp examples $(INSTALLDIR)/share/uzbl/
+ install -m755 uzbl-core $(INSTALLDIR)/bin/uzbl-core
+ install -m644 AUTHORS $(INSTALLDIR)/share/uzbl/docs
+ install -m644 README $(INSTALLDIR)/share/uzbl/docs
+ sed -i 's#^set prefix.*=.*#set prefix = $(RUN_PREFIX)#' $(INSTALLDIR)/share/uzbl/examples/config/config
+
+install-uzbl-browser:
+ install -d $(INSTALLDIR)/bin
+ install -m755 src/uzbl-browser $(INSTALLDIR)/bin/uzbl-browser
+ install -m755 examples/data/scripts/uzbl-cookie-daemon $(INSTALLDIR)/bin/uzbl-cookie-daemon
+ install -m755 examples/data/scripts/uzbl-event-manager $(INSTALLDIR)/bin/uzbl-event-manager
+ sed -i 's#^PREFIX=.*#PREFIX=$(RUN_PREFIX)#' $(INSTALLDIR)/bin/uzbl-browser
+ sed -i "s#^PREFIX = .*#PREFIX = '$(RUN_PREFIX)'#" $(INSTALLDIR)/bin/uzbl-event-manager
+
+install-uzbl-tabbed:
+ install -d $(INSTALLDIR)/bin
+ install -m755 examples/data/scripts/uzbl-tabbed $(INSTALLDIR)/bin/uzbl-tabbed
+
+# you probably only want to do this manually when testing and/or to the sandbox. not meant for distributors
+install-example-data:
+ install -d $(INSTALLDIR)/home/.config/uzbl
+ install -d $(INSTALLDIR)/home/.cache/uzbl
+ install -d $(INSTALLDIR)/home/.local/share/uzbl
+ cp -rp examples/config/* $(INSTALLDIR)/home/.config/uzbl/
+ cp -rp examples/data/* $(INSTALLDIR)/home/.local/share/uzbl/
+
+uninstall:
+ rm -rf $(INSTALLDIR)/bin/uzbl-*
+ rm -rf $(INSTALLDIR)/share/uzbl
diff --git a/README b/README
new file mode 100644
index 0000000..11146ef
--- /dev/null
+++ b/README
@@ -0,0 +1,782 @@
+### INTRODUCTION
+
+Any program can only be really useful if it complies with the Unix
+philosophy. Web browsers (and other tools that work with HTML, such as feed
+readers) are frequent violators of this principle:
+
+* They build in way too much things into one (complex) program, dramatically
+ decreasing the options to do things the way you want.
+* They store things in way too fancy formats (XML, RDF, SQLite, etc.) which are
+ hard to store under version control, reuse in other scripts, and so on.
+
+The Uzbl project was started as an attempt to resolve this.
+
+### EDITIONS
+
+"Uzbl" is an umbrella project consisting of different flavors. In the future
+more things may come, but for now:
+
+#### uzbl-core: main component meant for integration with other tools and scripts
+
+* Uses WebKitGtk+ for rendering and network interaction (libsoup). CSS,
+ JavaScript, and plugin support come for free.
+* Provides interfaces to get data in (commands/configuration) and out (events):
+ stdin/stdout/fifo/Unix sockets.
+* You see a WebKit view and (optionally) a statusbar which gets populated
+ externally.
+* No built-in means for URL changing, loading/saving of bookmarks, saving
+ history, keybinds, downloads, etc.
+* Extra functionality: many sample scripts come with it. More are available on
+ the [Uzbl wiki](http://www.uzbl.org/wiki/scripts) or you can write them
+ yourself.
+* Entire configuration/state can be changed at runtime.
+* Uzbl keeps it simple, and puts **you** in charge.
+
+#### uzbl-browser: a complete browser experience based on uzbl-core
+
+* Uses a set of scripts (mostly Python) that will fit most people, so things
+ work out of the box; yet plenty of room for customization.
+* Brings everything you expect: URL changing, history, downloads, form filling,
+ link navigation, cookies, event management etc. However: one page per
+ instance.
+* Advanced, customizable keyboard interface with support for modes, modkeys,
+ multichars, variables (keywords) etc. (eg you can tweak the interface to be
+ Vi-like, Emacs-like or any-other-program-like).
+* Adequate default configuration.
+* Focus on plaintext storage for your data and configs in simple, parseable
+ formats and adherence to the XDG basedir spec.
+* Visually, similar to `uzbl-core` except that the statusbar contains useful
+ information. One window per webpage.
+
+#### uzbl-tabbed: wraps around uzbl-browser and multiplexes it
+
+* Spawns one window containing multiple tabs, each tab containing a full
+ embedded `uzbl-browser`.
+* Ideal as a quick and simple solution to manage multiple `uzbl-browser`
+ instances without getting lost.
+
+Throughout the documentation, when referring to `uzbl` we mean `uzbl-core`,
+unless otherwise specified.
+
+### CONFIGURATION / CONTROL:
+
+The general idea is that Uzbl by default is very bare bones. You can send it
+commands to update settings and perform actions, through various
+interfaces. There is a limited default configuration. Please see `config.h` to
+see what it contains. By default, there are *no* keybinds defined at
+all. (Default keybinds would work counterproductive when you try to
+customize). For examples of the possibilities what you can do, please see the
+sample config(s), and uzbl wiki page.
+
+There are several interfaces to interact with Uzbl:
+
+* `uzbl --config <filename>`: `<filename>` will be read line by line, and the
+ commands in it will be executed. Useful to configure Uzbl at startup. If you
+ have a file in `$XDG_CONFIG_HOME/uzbl/config` (this expands to
+ `~/.config/uzbl/config` on most systems), it will be automatically recognized.
+* `stdin`: to write commands into `stdin`, use `--config -` (or `-c -`).
+* Interactive: you can enter commands (and bind them to shortcuts, even at
+ runtime) By default, the behaviour is modal (Vi style):
+
+ - command mode: every keystroke is interpreted to run commands
+ - insert mode: keystrokes are not interpreted so you can enter text into html
+ forms
+
+ There is also support for "chained" commands (multiple characters long), and
+ keyworded commands. Also you can have incremental matching on commands or
+ match after pressing return. See the sample configuration file for more info.
+
+ Also, copy and paste works when typing commands:
+
+ - `insert` (paste X cliboard)
+ - `shift insert` (paste primary selection buffer)
+
+* FIFO & socket files: If enabled by setting their paths through one of the
+ above means, you can have socket and fifo files available which are very
+ useful to programmatically control `uzbl` (from scripts etc).
+
+ - The advantage of the FIFO is you can write plaintext commands to it, but
+ it's half duplex only (`uzbl` cannot send a response to you).
+ - The socket is full duplex but you need a socket-compatible wrapper such as
+ `socat` to work with it. For example:
+
+ echo <command> | socat - unix-connect:<socketfile>
+
+When `uzbl` forks a new instance (eg "open in new window") it will use the same
+command line arguments (eg the same `--config <file>`), except `--uri` and
+`--name`. If you made changes to the configuration at runtime, these are not
+passed on to the child.
+
+#### Uzbl-browser
+
+* default config
+* Event Manager
+* Event Manager plugins
+* bindings/events/requests
+
+#### Uzbl-tabbed
+
+* also has some of its own keybindings.
+
+### COMMAND SYNTAX
+
+Uzbl will read commands via standard input, named FIFO pipe (if `fifo_dir` is
+set) and Unix socket (when `socket_dir` is set). For convenience, `uzbl` can
+also be instructed to read commands from a file on startup by using the
+`--config` option. Indeed, the config file is nothing more than a list of
+commands.
+
+Each command starts with the name of a command or a `uzbl` variable that expands
+to it. A command is terminated by a newline. Empty lines and lines that start
+with the hash sign are ignored by the parser. Command names are always written
+in lowercase.
+
+The following commands are recognized:
+
+* `back`
+ - Navigate to the previous URI in the history.
+* `forward`
+ - Navigate to the next URI in the history.
+* `scroll <vertical|horizontal> <argument>`
+ - argument can be `begin`, `end`, or an amount given in pixels(?) or as a
+ percentage of the size of the view
+ - set the amount to 100% to scroll a whole page
+* `reload`
+ - Reload the current page.
+* `reload_ign_cache`
+ - Reload the current page, discarding any cached resources.
+* `stop`
+ - Stop loading the current page.
+* `zoom_in`
+ - Increase the zoom level.
+* `zoom_out`
+ - Decrease the zoom level.
+* `toggle_zoom_type`
+ - Toggles the variable `zoom_type` between "full-content" and "text-only"
+ zoom. In "text-only" zoom, only the text of the page is zoomed, while in
+ "full-content" zoom, images and other page elements are zoomed along with
+ the text.
+* `uri <address>`
+ - Attempt to load `<address>`. This is equivalent to `set uri = <address>`.
+* `js <body>`
+ - Execute the JavaScript in `<body>`.
+ - Remember that the commands must not contain line breaks.
+* `script <file>`
+ - Execute the JavaScript in `<file>`.
+* `toggle_status`
+ - Toggle the display of the status bar.
+* `spawn <executable> <additional args>` TODO explain path-alike expansion
+ - Runs a command; see EXTERNAL SCRIPTS for details.
+ - `$PATH` is searched, so giving the full path to commands is not necessary.
+ - Note that the arguments as specified in "EXTERNAL SCRIPTS" are appended at
+ the end, so the argument numbers will be higher.
+* `sync_spawn <executable> <additional args>`
+ - A synchronous variant of `spawn`, which means `uzbl` will wait for it to
+ return.
+ - You should only need to use this manually if you want to use a `chain`
+ command in a handler that wants output from the command it runs.
+* `sh <command>`
+ - Runs a shell command by expanding `%s` in the `shell_cmd` variable with the
+ specified command; primarily useful as a shortcut for `spawn sh -c
+ <command>`
+ - Note that the arguments as specified in "EXTERNAL SCRIPTS" are appended at
+ the end, so the argument numbers will be higher.
+* `sync_sh <command>`
+ - Synchronous version of `sh`, See `sync_spawn`.
+* `talk_to_socket <socketfile> <tokens>`
+ - Send a message to `<socketfile>` and wait for a response. `<tokens>` are
+ concatenated and separated by ASCII NUL bytes.
+ - Expects the socket type to be `SOCK_SEQPACKET` (see `connect(2)`).
+ - Waits for 500ms for a response.
+* `exit`
+ - Closes `uzbl`.
+* `search <string>`
+ - Search forward for `<string>`. With no string, search for the next
+ occurrence of the previously searched string.
+* `search_reverse <string>`
+ - Like `search`, but searches backward.
+* `search_clear`
+ - Dehighlight matches and clear the search string.
+* `dehilight`
+ - Remove highlighting of search matches.
+* `set <key> = <value>`
+ - Sets `<key>` to `<value>`.
+ - The changes are effective immediately; for example, setting the variable
+ `uri` will make `uzbl` start loading, and changing `status_format` will make
+ the status bar react immediately.
+ - If you want to unset a string, use `set` with one space after the equals
+ sign.
+* `dump_config`
+ - Dumps the current config (which may have been changed at runtime) to stdout.
+ - Uses a format which can be piped into `uzbl` again or saved as a config
+ file.
+* `dump_config_as_events`
+ - Dump the current config as a series of `VARIABLE_SET` events, which can be
+ handled by an event manager.
+* `chain <command> <command> ...`
+ - Used for chaining multiple commands.
+ - Remember to quote the commands; one command must come as one parameter.
+ - If you use `chain` with a handler script which must return some output (such
+ as a cookie handler -- `uzbl` will wait for and use its output), use
+ `sync_spawn` or `sync_sh` instead of `spawn` or `sh` in the command that
+ should give the output.
+* `print <string>`
+ - Expands variables in `<string>` and prints its output to stdout. Is useful
+ for getting the value of variables.
+* `event <event_name> [event_details]`
+ - Send a custom event.
+* `request <request_name> [request_details]`
+ - Send a custom request (same idea as events, but to be processed by the event
+ manager, not `uzbl-core`).
+* `menu_add <label> = <command>`
+ - Add a new entry `<label>` to the default right-click menu that will execute
+ `<command>`.
+* `menu_link_add <label> = <command>`
+ - Add a new entry `<label>`, executing `<command>` to the right-click menu for
+ links.
+* `menu_image_add <label> = <command>`
+ - Same as `menu_add`, but for images.
+* `menu_editable_add <label> = <command>`
+ - Same as `menu_add`, but for editable text areas.
+* `menu_separator <label>`
+ - Add a separator, named `<label>` to the default right-click menu.
+* `menu_link_separator <label>`
+ - Same as `menu_separator`, but for links.
+* `menu_image_separator <label>`
+ - Same as `menu_separator`, but for images.
+* `menu_editable_separator <label>`
+ - Same as `menu_separator`, but for editable text areas.
+* `menu_remove <label>`
+ - Removes the menu entry `<label>` from the default right-click menu.
+* `menu_link_remove <label>`
+ - Same as `menu_remove`, but for links.
+* `menu_image_remove <label>`
+ - Same as `menu_remove`, but for images.
+* `menu_editable_remove <label>`
+ - Same as `menu_remove`, but for editable text areas.
+* `hardcopy`
+ - Open the print dialog.
+* `include <file>`
+ - Read contents of `<file>` and interpret as a set of `uzbl` commands.
+
+### VARIABLES AND CONSTANTS
+
+Uzbl has a lot of internal variables and constants. You can get the values
+(using the `print` command, see above), and for variables you can also change
+the value at runtime. Some of the values can be passed at start up through
+commandline arguments, others need to be set by using commands (eg in config
+file).
+
+* Some of them have default values (see config.h)
+* Some variables have callback functions which will get called after setting the
+ variable to perform some additional logic (see below).
+* Besides the builtin variables you can also define your own ones and use them
+ in the exact same way as the builtin ones.
+
+#### Variables
+
+* `uri`: The URI of the current page. (callback: load the uri)
+* `verbose`: Controls the verbosity printed to `stdout`.
+* `inject_html`: Inject an HTML string, navigating to the URI "about:blank" and
+ rendering the HTML sting given.
+* `geometry`: Geometry and position of the Uzbl window. Format is
+ "<width>x<height>+<x-offset>+<y-offset>".
+* `keycmd`: Holds the input buffer (callback: update input buffer).
+* `show_status`: Show statusbar or not.
+* `status_top`: statusbar on top?
+* `status_format`: Marked up, to be expanded string for statusbar (callback:
+ update statusbar).
+* `status_background`: color which can be used to override Gtk theme.
+* `title_format_long`: titlebar string when no statusbar shown (will be
+ expanded).
+* `title_format_short`: titlebar string when statusbar shown (will be expanded).
+* `icon`: path to icon for Gtk.
+* `forward_keys`: Whether `uzbl-core` should send key events to the webkit view.
+* `download_handler`: Handler called when page requests a download. In addition
+ to the standard handler arguments, appends the following extra arguments:
+ - `url`: The URL of the item to be downloaded.
+ - `proxy`: (optional) The URL of an HTTP proxy.
+* `cookie_handler`: Handler called when the page requests a cookie to be
+ retrieved or set. Appends the following arguments to the standard handler
+ arguments.
+ - `op`: Either "GET" if the browser requests a cookie to be sent to the server
+ or "PUT" if the server requests the browser save a cookie.
+ - `scheme`: The request address scheme ("http" or "https").
+ - `host`: The host requesting the cookie.
+ - `path`: The request address path.
+ - `data`: The cookie data. Only included for "PUT" requests.
+* `new_window`: handler to execute to invoke new uzbl window (TODO better name)
+* `scheme_handler`: handler to execute for each URI navigated to - the
+ navigation request will be ignored if handler prints "USED\n"
+* `fifo_dir`: location to store FIFOs.
+* `socket_dir`: location to store sockets.
+* `http_debug`: HTTP debug mode (value 0-3).
+* `scrollbars_visible`: set to 1 to have GTK scrollbars if the document
+ doesn't fit into the window (defaults to 0)
+* `shell_cmd`: Alias which will be expanded to use shell commands (eg `sh -c`).
+* `print_events`: show events on stdout
+* `proxy_url`: HTTP traffic SOCKS proxy (eg: `http://<host>:<port>`).
+* `max_conns`: Max simultaneous connections (default: 100).
+* `max_conns_host`: max simultaneous connections per hostname (default: 6)
+* `view_source`: Set the browser in "view source" mode (default 0). Any URI
+ visited while "view_source" is 1 will display the page source rather than the
+ rendered content.
+* `useragent`: The User-Agent to send to the browser, expands variables in its
+ definition.
+* `zoom_level`: The factor by which elements in the page are scaled with respect
+ to their original size. Setting this will resize the currently displayed page.
+* `zoom_type`: Whether to use "full-content" zoom (defaults to true). With
+ full-content zoom on, all page content, not just text, is zoomed. When
+ full-content zoom is off, only the text of a page is zoomed.
+* `font_size`: The default font size.
+* `default_font_family`: The default font family used to display text.
+* `monospace_font_family`: The default font family used to display monospace
+ text.
+* `cursive_font_family`: The default Cursive font family used to display text.
+* `sans_serif_font_family`: The default Sans Serif font family used to display
+ text.
+* `serif_font_family`: The default Serif font family used to display text.
+* `fantasy_font_family`: The default Fantasy font family used to display text.
+* `monospace_size`: The default size of monospaced font (default 1).
+* `minimum_font_size`: The minimum font size used to display text (default 1).
+* `disable_plugins`: Disable embedded plugin objects (default 0).
+* `disable_scripts`: Disable embedded scripting languages (default 0).
+* `autoload_images`: Automatically load images (default 1).
+* `autoshrink_images`: Shrink images to window size (default 0).
+* `enable_spellcheck`: Whether to enable spell checking while typing (default
+ 0).
+* `enable_private`: Whether to enable private browsing mode (default 0).
+* `print_backgrounds`: Print background images? (default 0).
+* `stylesheet_uri`: Use this to override the pagelayout with a custom
+ stylesheet.
+* `resizable_text_areas`: Whether text areas can be resized (default 0).
+* `default_encoding`: The default text encoding (default "iso-8859-1").
+* `enforce_96_dpi`: Enforce a resolution of 96 DPI (default 1).
+* `caret_browsing`: Whether the caret is enabled in the text portion of pages
+ (default 0).
+* `follow_hint_keys`: keys for keyboard-based navigation and link
+ highlighting
+
+#### Constants (not dumpable or writeable)
+
+* `WEBKIT_MAJOR`: WebKit major version number.
+* `WEBKIT_MINOR`: WebKit minor version number.
+* `WEBKIT_MICRO`: WebKit micro version number.
+* `ARCH_UZBL`: Processor architecture for which Uzbl is compiled, set at compile
+ time.
+* `COMMIT`: ID of the current Git commit, set at compile time.
+* `TITLE`: The current page title or "(no title)" if no title exists for the
+ current page.
+* `SELECTED_URI`: The URL currently hovered over by the mouse.
+* `NAME`: name of the uzbl instance (TODO: can't we make this a variable?)
+ - default: Xorg window id
+ - overridable with cmdline arg
+ - in GtkSocket mode, this is a random number to prevent name clashes
+* `PID`: The process ID of this Uzbl instance.
+
+### VARIABLE EXPANSION AND COMMAND / JAVASCRIPT SUBSTITUTION
+
+Variable expansion works pretty much as known from shell interpreters (sh, bash,
+etc.). This means you can construct strings with uzbl variables in them and have
+uzbl replace the variable name with its contents.
+
+In order to let uzbl know what to expand you'll need to prepend @ to the
+variable name:
+
+ print The variable \@show_status contains @show_status
+
+The above example demonstrates two things:
+
+* `\` is treated as escape character and will use the character immediately
+ following it literally this means `\@show_status` will not expand to the
+ variable content but be rather printed as `@show_status`
+* prepending the variable with `@` will expand to its contents
+* like in the shell you can use `@{uzbl_var}` to denote the beginning/end of the
+ variable name in cases where it is not obvious what belongs to the name and
+ what not. E.g. `print @{show_status}foobar`
+
+Command substitution will launch any commands and substitute the call with the
+return value of the command. There are two methods:
+
+* Through a shell: enclose commands with `@( )@` (quote escaping is handled by
+ Uzbl):
+
+ print Command substitution: @(uname -a)@
+
+This method allows you to use POSIX shell syntax in your commands.
+
+* directly:
+
+ print Command substitution: @(+uname -a)@
+
+This example will execute uname directly.
+
+Note that you can access any `uzbl` variable from within a command substitution:
+
+ print @(echo -n 'Accessing the show_status var from an external script, value: @show_status')@
+
+JavaScript substitution works in the exact same way as command substitution but
+you will need to enclose the JavaScript in `@< >@`.
+
+ print The currently viewed document contains @<document.links.length>@ links
+
+The `@<>@` substitution can also load JavaScript from a file, syntax: `@<+filename>@`
+
+ print JS return value from file: @<+/path/to/file.js>@
+
+Variable expansion also works within a JavaScript substitution.
+
+When a piece of text needs to be XML escaped after it is expanded (for example,
+in the status bar format), you can use `@[ ]@` substitution:
+
+ print This text is XML escaped: @[<&>]@
+
+ # prints: This text is XML escaped: &lt;&amp;&gt;
+
+NOTE: If you need to use literal `@` or `\` characters you will need to escape
+them:
+
+ print At sign: \@ and backslash: \\
+
+### TITLE AND STATUS BAR EVALUATION
+
+The contents of the status bar can be customized by setting the `status_format`
+variable. The contents of the window title can be customized by setting the
+`title_format_short` variable (which is used when the status bar is displayed)
+and the `title_format_long` variable (which is used when the status bar is not
+displayed). Their values can be set using the expansion and substitution
+techniques described above.
+
+These variables are expanded in multiple stages; once when the variable is set,
+and again every time that the status bar or window title are updated. Expansions
+that should be evaluated on every update need to be escaped:
+
+ set title_format_short = @(date)@
+ # this expansion will be evaluated when the variable is set.
+ # the title will stay constant with the date that the variable was set.
+
+ set title_format_short = \@(date)\@
+ # this expansion will be evaluated when the window title is updated.
+ # the date in the title will change when you change pages, for example.
+
+ set title_format_short = \\\@(date)\\\@
+ # the title will stay constant as a literal "@(date)@"
+
+The `status_format` variable can contain
+[Pango](http://library.gnome.org/devel/pango/stable/PangoMarkupFormat.html)
+markup . In the `status_format`, variables that might contain characters like
+`<`, `&` and `>`, should be wrapped in a `@[ ]@` substitution so that they don't
+interfere with the status bar's markup; see the sample config for examples.
+
+### EXTERNAL SCRIPTS
+
+You can use external scripts with Uzbl the following ways:
+
+* Let `uzbl` call them. These scripts are called "handlers" in the `uzbl`
+ config. Used for handling cookies, starting a new download, and more.
+* Call them yourself from inside `uzbl`. You can bind keys for this. Examples:
+ add new bookmark, load new URL.
+* You could also use `xbindkeys` or your WM config to trigger scripts if `uzbl`
+ does not have focus.
+
+Have a look at the sample configs and scripts!
+
+Handler scripts that are called by `uzbl` are passed the following arguments:
+
+* `$1 config`: The configuration file loaded by this `uzbl` instance.
+* `$2 pid`: The process ID of this `uzbl` instance.
+* `$3 x_id`: The X Windows ID of the process.
+* `$4 fifo`: The filename of the FIFO being used, if any.
+* `$5 socket`: The filename of the Unix socket being used, if any.
+* `$6 uri`: The URI of the current page.
+* `$7 title`: The current page title.
+* `.. [ script specific ] (optional)`
+
+The script specific arguments are:
+
+* download
+
+ - `$8 url`: The URL of the item to be downloaded.
+ - `$9 proxy`: (optional) The URL of an HTTP proxy.
+
+* cookie handler
+
+ - `$8 GET/PUT`: Whether a cookie should be sent to the server (`GET`) or
+ stored by the browser (`PUT`).
+ - `$9 scheme`: Either `http` or `https`.
+ - `$10 host`: If current page URL is `www.example.com/somepage`, this could be
+ something else than `example.com`, eg advertising from another host.
+ - `$11 path`: The request address path.
+ - `$12 data`: The cookie data. Only included for `PUT` requests.
+
+* scheme handler
+
+ - `$8 URI` of the page to be navigated to
+
+* authentication handler:
+
+ $8 authentication zone unique identifier
+ $9 domain part of URL that requests authentication
+ $10 authentication realm
+ $11 FALSE if this is the first attempt to authenticate, TRUE otherwise
+
+Custom, userdefined scripts (`spawn foo bar`) get first the arguments as
+specified in the config and then the above 7 are added at the end.
+
+### HTTP/BASIC AUTHENTICATION
+
+You can use the authentication_handler variable to denote how http
+authentication should be handled.
+If this variable is:
+
+* not set or empty: use webkit internal auth dialog
+* a valid handler (i.e. {sh,sync}_spawn correct_script), use this handler
+* innvalid handler (spawn, some other command, uses script that does not
+ print anything): skip authentication.
+
+Example:
+
+ set authentication_handler = sync_spawn /patch/to/your/script
+
+Script will be executed on each authentication request.
+It will receive four auth-related parameters:
+
+ $8 authentication zone unique identifier (may be used as 'key')
+ $9 domain part of URL that requests authentication
+ $10 authentication realm
+ $11 FALSE if this is the first attempt to authenticate, TRUE otherwise
+
+Script is expected to print exactly two lines of text on stdout (that means
+its output must contain exactly two '\n' bytes).
+The first line contains username, the second one - password.
+If authentication fails, script will be executed again (with $11 = TRUE).
+Non-interactive scripts should handle this case and do not try to
+authenticate again to avoid loops. If number of '\n' characters in scripts
+output does not equal 2, authentication will fail.
+That means 401 error will be displayed and uzbl won't try to authenticate anymore.
+
+The simplest example of authentication handler script is:
+
+#!/bin/sh
+[ "$11" == "TRUE ] && exit
+echo alice
+echo wonderland
+
+This script tries to authenticate as user alice with password wonderland once
+and never retries authentication.
+See examples for more sofisticated, interactive authentication handler.
+
+### JAVASCRIPT HELPER OBJECT DISABLED BECAUSE OF SECURITY LEAK
+
+JavaScript code run from `uzbl` is given a special object in the global
+namespace which gives special privileges to these scripts. This object is called
+`Uzbl`, and it is added and removed before and after the script execution so
+that it is hidden to web JavaScript code (there is no race condition, since all
+the JavaScript code runs in a single thread).
+
+Currently, the `Uzbl` object provides only one function:
+
+* `Uzbl.run( <command> )`
+ - Command is any `uzbl` command as defined above.
+ - Return value: a string, either empty or containing the output of the
+ command. Very few commands return their output currently, including `js`,
+ `script`, and `print`.
+ - Examples:
+ * `Uzbl.run("spawn insert_bookmark.sh")`
+ * `uri = Uzbl.run("print @uri")` (see variable expansion below)
+
+### EVENTS
+
+Unlike commands, events are not handled in `uzbl` itself, but are propagated
+(dispatched) asynchronously through a text stream on `stdout` and/or through a
+socket. You'll usually use uzbl by piping it's output to a so-called "event
+manager" (EM), or by having the EM listen to a socket.
+
+The EM allows:
+
+* Use of whichever language you want for event handling (Python, Perl, Bash,
+ ... you name it). You'll usually send commands (see above) back to `uzbl`
+ through its FIFO or socket.
+* Keybindings use X keysyms.
+* Many fine-grained events (`hover_over_link`, `key_press`, `key_release`,..)
+* See example `uzbl-event-manager`.
+
+**Note**: Cookie events are not sent to an event handler but handled internally
+ through the cookie handler because of their synchronous nature. Cookie events
+ are really something completely different from all other events. Maybe someday
+ we'll use HTTP proxies or synchronous events (which also have other nice use
+ cases), but for now we still use the handler code.
+
+Events have this format:
+
+ EVENT [uzbl_instance_name] EVENT_NAME event_details
+
+#### Reported events
+
+* `EVENT [uzbl_instance_name] INSTANCE_START process_id`: `uzbl` startup.
+* `EVENT [uzbl_instance_name] INSTANCE_EXIT process_id`: `uzbl` shutdown
+* `EVENT [uzbl_instance_name] VARIABLE_SET variable_name str|int|float
+ variable_value`: Note: `str|int|float` denote the type of `variable_value`.
+* `EVENT [uzbl_instance_name] COMMAND_EXECUTED command_name optional_arguments`:
+ A command is executed.
+* `EVENT [uzbl_instance_name] COMMAND_ERROR command_name`: Tried to execute the
+ command `command_name`, but it does not exist.
+* `EVENT [uzbl_instance_name] GEOMETRY_CHANGED
+ WIDTHxHEIGHT+X_POSITION+Y_POSITION`: When the size or position of the `uzbl`
+ window changes.
+* `EVENT [uzbl_instance_name] FIFO_SET path_to_fifo`: The path to the FIFO is
+ set.
+* `EVENT [uzbl_instance_name] SOCKET_SET path_to_socket`: The path to the socket
+ is set.
+* `EVENT [uzbl_instance_name] LOAD_COMMIT uri`: The first data of a page has
+ loaded. `uri` is the URI of the page being loaded.
+* `EVENT [uzbl_instance_name] LOAD_START uri`: A change of the page has been
+ requested. `uri` is the current URI; the one being departed.
+* `EVENT [uzbl_instance_name] LOAD_FINISHED uri`: Loading has finished for the
+ page at `uri`.
+* `EVENT [uzbl_instance_name] LOAD_ERROR uri reason_of_error`: The URI `uri`
+ could not be loaded for the reason described in `reason_of_error`.
+* `EVENT [uzbl_instance_name] LOAD_PROGRESS percentage` : While the page is
+ loading, gives the `percentage` of the page that has finished loading.
+* `EVENT [uzbl_instance_name] REQUEST_STARTING uri`: http resource gets
+ requested
+* `EVENT [uzbl_instance_name] TITLE_CHANGED title_name`: When the title of the
+ page (and hence maybe, the window title) changed. `title_name` is the new
+ title.
+* `EVENT [uzbl_instance_name] DOWNLOAD_REQUEST download_uri`: When content needs
+ to be downloaded, `download_uri` is the URI to get.
+* `EVENT [uzbl_instance_name] LINK_HOVER uri`: The mouse hovers over the link
+ `uri`.
+* `EVENT [uzbl_instance_name] LINK_UNHOVER uri`: The mouse leaves the link
+ `uri`.
+* `EVENT [uzbl_instance_name] KEY_PRESS key_name`: The key (or mouse button)
+ `key_name` is pressed.
+* `EVENT [uzbl_instance_name] KEY_RELEASE key_name`: The key (or mouse button)
+ `key_name` is released.
+* `EVENT [uzbl_instance_name] SELECTION_CHANGED selected_text`: When text is
+ selected in the `uzbl` window.
+* `EVENT [uzbl_instance_name] NEW_WINDOW uri`: Creation of new `uzbl` window,
+ with URI `uri`.
+* `EVENT [uzbl_instance_name] WEBINSPECTOR open`: Upon opening webinspector
+ window.
+* `EVENT [uzbl_instance_name] WEBINSPECTOR close`: Upon closing webinspector
+ window.
+* `EVENT [uzbl_instance_name] FOCUS_GAINED`: When `uzbl` window gains keyboard
+ focus.
+* `EVENT [uzbl_instance_name] FOCUS_LOST`: When `uzbl` window loses keyboard
+ focus.
+* `EVENT [uzbl_instance_name] FORM_ACTIVE`: When an editable HTML is clicked.
+* `EVENT [uzbl_instance_name] ROOT_ACTIVE`: When the document body or any
+ non-editable element is clicked.
+* `EVENT [uzbl_instance_name] FILE_INCLUDED filename`: When the `include`
+ commands successfully loads a file, given by `filename`.
+* `EVENT [uzbl_instance_name] PLUG_CREATED plug_id`: When `uzbl-core` is in
+ Xembed mode, `plug_id` is the Xembed ID used.
+* `EVENT [uzbl_instance_name] BUILTINS command_list`: Shows a list of all `uzbl`
+ commands, whitespace separated, on startup.
+
+Events/requests which the EM and its plugins listens for
+
+* `BIND` and `MODE_BIND`: Define global and per-mode key/button binds.
+ - `request BIND <keycmd> = <command>` Set global binding (this is a shortcut
+ for `request MODE_BIND global <keycmd> = <command>`).
+ - `request MODE_BIND <modespec> <keycmd> = <command>` Set a local binding for
+ `<modespec>`. The `<modespec>` can be anything like `command`,
+ `insert,command`, `global`, `global,-insert`.
+
+ The `<keycmd>` has a special syntax:
+ - `<keycmd>` ends with a `_`: the command will only be invoked after pressing
+ return/enter. If the user enters text where `<string>` has the underscore,
+ `%s` in the `<command>` string will be replaced by this text (optional).
+ - `<keycmd>` ends with a `*`: similar behavior as with an underscore, but also
+ makes the binding incremental (i.e. the command will be invoked on every
+ keystroke).
+ - `<keycmd>` ends with a `!`: the command will only be invoked after pressing
+ return/enter, no replacement happens. this is useful for preventing `x` to
+ match when you want to bind `xx` also.
+ - `<keycmd>` ends on a different character: you need to type the full string,
+ which will trigger the command immediately, without pressing enter/return.
+ - TODO explain stacked bindings and multi-stage (is that the same?) and what
+ else am i missing? modkeys, showing a prompt mid-bind.
+
+ The `<keycmd>` can be any representation of a key on your keyboard or a
+ mousebutton. (note: not all mousebuttons work correctly yet). Examples:
+
+ - `event BIND o _ = uri %s`
+ - `uzbl` will load the url when you type: `o <url><enter>`
+ - `event BIND /* = search %s`
+ - A `search` command which is called on every character typed after the slash,
+ letting you see the search narrow down while typing.
+ - Hitting return, enter or esc will terminate the search.
+ - `event BIND ZZ = exit`
+ - When you type `ZZ` and nothing else, the `exit` command will be triggered
+ immediately.
+* `MODE_CONFIG`: Set mode specific configs. If the mode being modified is the
+ current mode then apply the changes immediately.
+ - `request MODE_CONFIG <mode> <key> = <value>`
+* `ON_EVENT`: Execute a command when a given event is fired.
+ - `request ON_EVENT <EVENT_NAME> <command>`
+* `PROGRESS_CONFIG`: Set a configuration option for `LOAD_PROGRESS` updates.
+ - `request PROGRESS_CONFIG <key> = <value>`: Set progress config variable
+ `key` to `value`.
+* `MODMAP`: Set an alternate name for a key or button.
+ - `request MODMAP <from> <to>`: Create an alias `<to>` for key command
+ `<from>`. This allows `<to>` to be bound to a command, which will be invoked
+ when the `<from>` key or button is pressed.
+* `IGNORE_KEY`: Ignore a key pattern, specified by `<glob>`.
+ - `request IGNORE_KEY <glob>`
+* `MODKEY_ADDITION`: Create a compound modkey from multiple individual keys.
+ - `request MODKEY_ADDITION <key1> <key2> <keyn> <result>`: The modkey
+ `<result>` is considered pressed when all of `<key1>`, `<key2>`, and
+ `<keyn>` are pressed.
+* `TOGGLE_MODES`
+ - `request TOGGLE_MODES <mode1> <mode2> ... <moden>`
+* `APPEND_KEYCMD`: Append a string to the current keycmd.
+ - `request APPEND_KEYCMD <string>`: Append `<string>` to the current keycmd.
+* `INJECT_KEYCMD`: Injecting a string into the keycmd at the cursor position.
+ - `request INJECT_KEYCMD <string>`: Inject `<string>` into the keycmd at the
+ current cursor position.
+* `KEYCMD_DELETE`: Removes the character after the cursor position in the
+ keycmd.
+* `KEYCMD_STRIP_WORD`: Removes the last word from the keycmd, similar to
+ readline `^W`.
+* `KEYCMD_EXEC_CURRENT`: (tries to) execute whatever is in the keycmd.
+* `SET_KEYCMD`: Allow setting of the keycmd externally.
+ - `request SET_KEYCMD <string>`: Set the keycmd to `<string>`.
+* `SET_CURSOR_POS`: Allow setting of the cursor position externally.
+ - `request SET_CURSOR_POS <index>`: Set the keycmd cursor to `<index>`. If
+ `<index>` is `+`, advance the cursor by one character, and if it is `-`,
+ move the cursor back by one character.
+* `START_COMPLETION`: TODO explain completion
+
+### COMMAND LINE ARGUMENTS
+
+`uzbl` is invoked as
+
+ uzbl-(core|browser|tabbed) [ arguments ] [ uri ]
+
+where `arguments` and `uri` are both optional. `arguments` can be:
+
+* `-u`, `--uri=URI`: URI to load at startup. Equivalent to `uzbl <uri>` or `set
+ uri = URI` after `uzbl` has launched.
+* `-v`, `--verbose`: Whether to print all messages or just errors.
+* `-n`, `--name=NAME`: Name of the current instance (defaults to Xorg window
+ id or random for GtkSocket mode).
+* `-c`, `--config=FILE`: Path to config file or `-` for stdin.
+* `-s`, `--socket=SOCKET`: Xembed socket ID.
+* `--connect-socket=SOCKET`: Connect to server socket for event managing.
+* `-p`, `--print-events`: Whether to print events to stdout
+* `-g`, `--geometry=GEOMETRY`: Set window geometry (format: `WIDTHxHEIGHT+-X+-Y`
+ or `maximized`).
+* `-V`, `--version`: Print the version and exit.
+* `--display=DISPLAY`: X display to use.
+* `--help`: Display help.
+
+`uzbl-core scheme://address` will work as you expect. If you don't provide the
+`scheme://` part, it will check if the argument is an existing file in the
+filesystem, if it is, it will prepend `file://`, if not, it will prepend
+`http://`.
+
+### BUGS
+
+Please report new issues to the [Uzbl bugtracker](http://uzbl.org/bugs).
diff --git a/docs/COMMUNITY b/docs/COMMUNITY
new file mode 100644
index 0000000..2817ee9
--- /dev/null
+++ b/docs/COMMUNITY
@@ -0,0 +1,33 @@
+COMMUNITY
+------
+
+### Mailing list
+
+* Address: [uzbl-dev@lists.uzbl.org](mailto:uzbl-dev@lists.uzbl.org)
+* [Page](http://lists.uzbl.org/listinfo.cgi/uzbl-dev-uzbl.org)
+* [Archives](http://lists.uzbl.org/pipermail/uzbl-dev-uzbl.org/)
+
+### IRC
+
+* `#uzbl` on irc.freenode.net
+* [Archive](http://www.uzbl.org/irc-log)
+
+### Website
+
+* [www.uzbl.org](http://www.uzbl.org/)
+
+### Bugtracker
+
+* [www.uzbl.org/bugs/](http://www.uzbl.org/bugs/)
+
+### Code repositories
+
+* [github.com/Dieterbe/uzbl](http://github.com/Dieterbe/uzbl/)
+* [github.com/anydot/uzbl](http://github.com/anydot/uzbl/)
+* [github.com/Barrucadu/uzbl](http://github.com/Barrucadu/uzbl/)
+* [github.com/dusanx/uzbl](http://github.com/dusanx/uzbl/)
+* [github.com/robm/uzbl](http://github.com/robm/uzbl/)
+
+There are more contributors who have forks. See:
+
+[Uzbl Network graph](http://github.com/Dieterbe/uzbl/network)
diff --git a/docs/CONTRIBUTING b/docs/CONTRIBUTING
new file mode 100644
index 0000000..bcfd108
--- /dev/null
+++ b/docs/CONTRIBUTING
@@ -0,0 +1,108 @@
+### Users
+
+Just use Uzbl, hang around in our IRC channel, try out different things and report bugs.
+If you're feeling more adventurous, you can use one of the development branches and give bug
+reports and suggestions straight to the developer in charge of that, so the
+same problems don't occur when they get merged into the master branch.
+Play around with the configs and scripts and see if you can improve things.
+The wiki can be a good source of inspiration.
+You may also find mistakes in our documentation.
+
+### Developers
+
+If you don't feel like just sending bug reports, you can dive into the
+code and start hacking.
+Even if you're not good at C, thanks to uzbl's 'ecosystem' of scripts there
+is a lot that can be done.
+However, it's usually a good idea to tell us first
+what you want to do, to avoid unneeded or duplicate work.
+
+Read on for more info...
+
+### Clone/patch/merge workflow
+
+1. clone the code with git.
+ Either you host your git repo yourself, or use something userfriendly
+ like [Github](http://www.github.com)
+ If you want to use github, you basically just need to register and click
+ the fork button on [Dieterbe/uzbl](http://github.com/Dieterbe/uzbl)
+ If you're new to Git/github, have no fear:
+
+ * [Github guides (highly recommended)](http://github.com/guides/home)
+ * [Guides: Fork a project and submit your modifications](http://github.com/guides/fork-a-project-and-submit-your-modifications)
+
+2. Do your work, test it and push to your repo. It may be interesting to ask
+ others for input on your work. Develop against master for small changes/fixes,
+ experimental or a new topic branch for experimental/intrusive work.
+
+3. If you think your code should be in the main uzbl code and meets all
+ requirements (see below), then ask Dieter to merge it.
+
+ * send a mail to the mailing list with subject "`Pull request: <title>`"
+ with a short description of your stuff and link to your git clone and
+ which branch you're talking about.
+ We prefer you send us links to git clones, and not just patches. They
+ seem to be easier to merge, especially if the 'base code' has changed in the
+ meanwhile.
+ * If you really don't want to subscribe to the ML and only if it's a
+ small change, you can email Dieter personally.
+ * Please avoid sending private messages on github or asking Dieter to merge on IRC.
+ * If your patch is big, if it will help a lot if you can mention
+ that your code has been tested and is supported by other people.
+
+This is a relatively easy, solid and transparent way to handle all requests in order.
+
+### Patch/branch requirements before merging:
+
+* patches/merges must be about one thing. If you want to work on multiple things, create new branches.
+ I allow exceptions for trivial typo fixes and such, but that's it.
+ This also implies that you also need to update your tree reguraly. Don't fall behind too much. (ie merge from Dieter)
+* any change in functionality that you want merged in must also be documented.
+ There is a readme and some files in the 'docs' directory who should correspond to the code base at all times.
+ Update them, not only for end users but also for your fellow hackers.
+* We recommend you finish your stuff first and then let Dieter know you want your stuff to be merged in, but
+ we know for bigger changes this is not always feasible. Just try to keep the merges about bigger "clean changesets".
+* Your code should not introduce any compile warnings or errors. And also,
+ no regressions but that's harder to check. Always run `make tests` to run
+ the test suite.
+* Please try to keep the code clean - we don't like extraneous whitespace.
+ The sample pre-commit hook can check for this - so go ahead and
+ $ cp .git/hooks/pre-commit.sample .git/hooks/pre-commit
+That said, you can always ask us to check on your stuff or ask for advice.
+
+
+### Bugreporting
+
+Bug reports are also welcome, especially the ones that come with a patch ;-)
+Before making a new ticket, check whether the bug is reported already.
+If you want to report a bug and you don't know where the problem in the code
+is, please supply:
+
+* version (commit hash) (see `uzbl --version`)
+* operating system
+* versions of libsoup, webkit, gtk.
+* output of uzbl --verbose (with http_debug set, if relevant)
+
+### Valgrind profiling
+ $ add this to Makefile header: CFLAGS=-g
+ $ recompile
+ $ valgrind --tool=callgrind ./uzbl ....
+ $ kcachegrind callgrind.out.foo
+
+### Memory leak checking
+ valgrind --tool=memcheck --leak-check=full ./uzbl
+
+### Writing unit tests
+
+If you can, write a unit test for a bugfix or new functionality. Add relevant unit
+tests to existing .c files in tests/. Others should be made in new source files with
+corresponding changes to the tests/Makefile.
+Run all tests with `make tests`
+
+### Debugging / backtraces
+
+* compile with -ggdb (enabled by default on experimental tree)
+* run: `gdb ./uzbl`
+* `(gdb) run -c /path/to/config`
+* `bt` if it segfaults to see a backtrace
+* you'll find more info on the interwebs
diff --git a/docs/FAQ b/docs/FAQ
new file mode 100644
index 0000000..7587d70
--- /dev/null
+++ b/docs/FAQ
@@ -0,0 +1,146 @@
+FAQ
+---
+
+### Uzbl crashes immediately (segfaults). WTF ?
+You are using a libwebkit version (usually 1.1.15.*) that uses enchant which
+is compiled with zemberek support built-in. Compile enchant with
+--disable-zemberek or ask your package maintainer.
+See also:
+
+ * https://bugs.webkit.org/show_bug.cgi?id=30860
+ * http://bugzilla.abisource.com/show_bug.cgi?id=12413
+ * http://bugzilla.abisource.com/show_bug.cgi?id=12529
+ * http://bugs.archlinux.org/task/17401
+ * http://www.uzbl.org/news.php?id=17
+
+
+### I just installed uzbl but it doesn't do much. What now?
+"Uzbl" is the name for the umbrella project that has several subprojects.
+You probably want `uzbl-browser` or any of the other projects.
+The main program (uzbl-core) is a program meant for integration with other
+tools and scripts, by itself doesn't do many usefull things. See README.
+
+### Where are the widgets (forward, back,.. button etc)
+The layout of uzbl (and derivatives) only contains what you really need to see. we only have a statusbar, which even can also be disabled. There are no buttons, but we do
+have lots of keybinding possibilities.
+
+### Why can uzbl-core/uzbl-browser only show one page?
+It is nearly unanimously agreed that one page per uzbl-core is best.
+It allows a simple implementation of both uzbl-core and
+uzbl-browser, and it makes things more robust.
+But read the next entry...
+
+### How to have multiple pages in one window?
+So, given that uzbl-core and uzbl-browser only deal with one page at a time (see
+above), how can you have a window with multiple pages?
+
+Basically this is involves concerns on two sides:
+
+* window management
+ - can I keep all pages together in 1 X window so that I can move all at once to a different workspace
+ - can I "split off" pages into separate windows (i.e. move only one specific page/window to a different desktop)
+ or merge windows together?
+ - can I integrate uzbl pages/windows into WM features? (alt-tab, tiling layouts, taskbar, ...)
+ - ...
+* application-level
+ - realtime overview of all page titles of all uzbl instances
+ - representation styles which are tightly coupled to the application such as treeviews that show from which you page you opened
+ others, or page state (loading etc)
+ - ...
+
+Uzbl itself can hardly be a limiting factor, as it supports/has:
+
+* Xembed (GtkPlug mode) so you can embed a uzbl-browser or uzbl-core into another window
+* an events system you can have realtime updates of window title, pageload state, etc.
+* command interface to programmatically change it's behavior.
+
+And then there is the style of representation (tabs, tree overviews, visual
+thumbnails etc) which can be handled from the WM side or the application
+side.
+
+There are multiple approaches, each with pros and cons.
+
+* Tabbing in the WM: Xmonads tabbed layout, Wmii's stacked layout, fluxbox or kwin tabs and so on.
+* Visual overview in the WM: commonly used with dwm or Awesome's tiling layouts with master/slave areas.
+ The [dynamic zoom script](http://www.uzbl.org/wiki/dynamic_zooming) is useful here.
+* A container application whih embeds multiple uzbl-browsers and provide tablists, tree views, and more.
+ Examples:
+ - [uzbl-tabbed](http://www.uzbl.org/wiki/uzbl_tabbed) (officially supported)
+ - [uzbltreetab](http://www.uzbl.org/wiki/uzbltreetab)
+ - [uzbltab](http://www.uzbl.org/wiki/uzbltab)
+ - [suckless tabbed](http://tools.suckless.org/tabbed)
+* An application to mimic tabbing independently of WM support.
+ The only thing you need to do is focus/maximize the instance you want,
+ keep the others out of sight and use tools like dmenu/xbindkeys and wmctrl to switch instances.
+ This allows you to use application-specific properties (such as uzbl tag, name etc).
+ For more ideas on such an approach, see docs/multiple-instances-management.
+ Examples:
+ - [wmctrl-based](http://www.uzbl.org/wiki/metacity-tabs) (works on at least Metacity)
+ - [wmii](http://www.uzbl.org/wiki/wmii)
+
+There are really a lot of options. You need to think about what you need,
+what you want and what you don't care about.
+On the wiki you'll find a lot of related scripts, some of them providing new
+workflows (do you really need open windows for all pages you intend to read, or is a list enough?
+[articlecue](http://www.uzbl.org/wiki/article_queue.py)), some providing integration with WM's such as
+[awesome](http://www.uzbl.org/wiki/awesome), and more.
+
+### Okay, what can I actually do? What commands are there? How do I get more information?
+ * Commands and other features are documented in README. Read it.
+ * You should also peek into the sampleconfigs to see how commands are used in practice.
+ * The wiki is also a great resource.
+
+### Why can't I type anything in forms? How does the keybinding work?
+You are in command mode, not in insert mode.
+
+* command mode: you can trigger actions inside uzbl with minimum amount of keypresses (eg 'b' to go back, 'ZZ' to quit etc) (see config examples), but not to type actual text into forms, because all your keypresses are interpreted.
+* insert mode: after going into insert mode (by default this is the 'i' binding from inside command mode), your keypresses are not interpreted but passed on, so you can enter text into forms. Press Esc to go out of insert mode.
+
+The above method is called "modal" as inspired on VI. If you don't like this you can easily change this.
+
+This method is how many applications work.
+TODO: you can call things from inside insert mode by using modkeys, right?
+
+
+### Why do you depend on gtk?
+Uzbl itself doesn't use much gtk stuff (only the statusbar) so we could do without gtk. But Webkit needs a widget toolkit to create widgets (think javascript popups, html forms etc).
+Officially, webkit also supportss Qt and wxwigdets. There are also some unofficial patchsets floating on the interwebs for the EFL and FLTK toolkits. One could argue we don't need no popups or fancy form widgets and you could have a point, but
+we prefer being reasonably assured that things work as they are supposed to rather then using some obscure patchset which may be incomplete, broken and/or badly designed, or wasting time ourselves in what is not our core objective.
+That's why we picked the Gtk variant of Webkit.
+Note that we do *not* depend on any Gnome libraries such as gconf. _That_ would be something worth complaining about :)
+
+### Do you support flash? javascript? Ajax? Recent html/css/.. standards? Java/media plugins?
+Yes, Webkit takes care of all of that. Not that we like all of these, but you can use them if you want.
+We use the NPAPI plugin architecture (just like mozilla, opera, etc) so just
+install the plugins normally, and things should work.
+
+### What's the difference between the socket file and the fifo?
+They both have advantages and disadvantages:
+
+ * fifo's are _very_ easy to work with. You can write just plaintext commands into them, but they are unidirectional (you can only communicate in one direction)
+ * Sockets are bidirectional but more complex. You cannot just write a plaintext string into them. In shellscripts you can use socat to work with sockets, when programming you need to use library functions.
+
+So, when writing scripts, using fifo's is usually the fastest method (because you do not need to fork another process), so fifo is preferred unless you need a response.
+
+### Uzbl uses too much memory! Especially when having multiple windows (i.e. with uzbl-tabbed)
+Don't be fooled with how memory usage is measured and reported on Linux. (or other systems)
+You need to be aware of the difference between RSS and VSS.
+And dynamic libraries (libwebkit, libgtk, etc) that are used by multiple processes are only stored in RAM once.
+See [this page](http://virtualthreads.blogspot.com/2006/02/understanding-memory-usage-on-linux.html) for a good explanation.
+
+### What the hell is this 'XDG' stuff??
+You'll notice our example/default scripts and configs use variables such as `$XDG_CONFIG_HOME` and `$XDG_DATA_HOME`.
+Most of us really like the [xdg basedir spec](http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html).
+Basically it helps you keeping a clean `$HOME` and it separates config, data and cache.
+If these variables are not defined on your system, it could be that you need to install an xdg package.
+If you don't like this, no one is stopping you from changing the scripts and configs to point to a single `$HOME/.uzbl` directory or whatever you want.
+
+
+### Does the world really need another browser?
+We did try a lot of browsers, and we do not suffer [NIH](http://en.wikipedia.org/wiki/Not_Invented_Here).
+We believe that the approach taken by way too many browsers is wrong. We do not want browsers that try to do everything,
+instead we prefer a system where different applications work together, which gives plenty of advantages.
+We also like open source. We take a lot of things from other projects and we also try to contribute to other projects.
+
+### What? You call all of this user-friendly?
+Yes. If you don't agree, don't use it :)
diff --git a/docs/INSTALL b/docs/INSTALL
new file mode 100644
index 0000000..c795502
--- /dev/null
+++ b/docs/INSTALL
@@ -0,0 +1,62 @@
+Packages
+--------
+Uzbl is available through the package managers of Arch, Gentoo, Debian,
+Ubuntu and more. If you cannot find a package for your system you may find
+a tutorial on [uzbl.org/wiki/howtos](http://www.uzbl.org/wiki/howtos)
+
+From source
+-----------
+You can pull the code from git or get a tagged tarball.
+
+ $ git clone git://github.com/Dieterbe/uzbl.git
+ $ cd uzbl
+ [ $ git checkout origin/experimental ] # optional. see below
+ $ make
+ $ sudo make install
+If you want to remove uzbl again, you can issue:
+
+ $ make uninstall
+
+Tarballs can be pulled from [github.com/Dieterbe/uzbl/downloads](http://github.com/Dieterbe/uzbl/downloads)
+
+Though you can only get tagged versions from the master branch, which may be older then what you'll have through git.
+
+If you want the specific subprojects, you can issue:
+
+ $ sudo make install-uzbl-core
+ $ sudo make install-uzbl-browser
+ $ sudo make install-uzbl-tabbed
+
+Dependencies
+------------
+Dependencies which are optional for uzbl-core are marked with an asterisk. (i.e. these are needed for extra scripts)
+* libwebkit 1.1.15 or higher
+* libsoup 2.24 or higher (dep for webkit/gtk+)
+* gtk 2.14 or higher
+* socat (for socket communication) [*]
+* dmenu (vertical patch recommended) [*]
+* zenity [*]
+* bash [*]
+* python [*]
+* perl (formfiller.pl) [*]
+* xclip [*]
+* pango [*]
+* pygtk [*]
+* pygobject [*]
+* python-simplejson [*]
+
+Make dependencies
+-----------------
+* git (for downloading)
+* pkg-config (for Make/gcc)
+
+
+Git Repo's & branches
+--------------------
+* Main official repo:
+ http://github.com/Dieterbe/uzbl
+- master -> main development branch
+- experimental -> intrusive/experimental stuff
+- <..> -> specific topic branches that come and go. they are a place to work on specific features
+* Most contributors & developers also have their clones on github (http://github.com/dusanx, http://github.com/Barrucadu/uzbl, ...).
+ They may be developing specific features, which get merged into Dieters experimental branch
diff --git a/docs/TODO b/docs/TODO
new file mode 100644
index 0000000..552467f
--- /dev/null
+++ b/docs/TODO
@@ -0,0 +1,81 @@
+THIS FILE IS SORT OF OUTDATED
+
+what if fifo/socket doesn't exist, or exists but nothing working behind it (anymore)?
+-> useful for both cookie handler and event manager
+-> implementing in uzbl-core needs refactoring of handler args, or some edge-case workarounds
+ (mason-l's proposal was not too bad: basically make cookie stuff more cookie specific.
+ we don't need a generic talk_to_socket. 3 vars: cookie_handler, cookie_handler_socket and
+ cookie_handler_launcher. act depending on which vars are set)
+-> for now, assume that we don't need to check for crashes, just make sure there is the initial socket/fifo -> can be done in uzbl-browser script
+
+
+== event-messages specific ==
+* document the event handling mechanism, all events, how to get started with sample event handler
+* VARIABLE_SET for all types (but probably not useful for custom vars)
+* remove chain command
+* scalability -> 1 event manager per n uzbl-browser instances -> mkfifo and redirect uzbl-core stdout to fifo in uzbl-browser
+* event manager -> everything based on per instance basis (associative array with uzbl instance name as key)
+* migrate all *_handler to EM plugins
+* cookies over EM (but we can delay that. it works okay for now)
+
+
+= keybinding features =
+* bring somehow the old 'modkey' possibility back. (have all bindings available in insert mode by holding an extra modkey).. enum plugin?
+
+= key handling (example event_handler.py) examples to implement =
+* demonstrate separate key_press and key_release (eg press 'z' to zoom in, on release reset zoom to what it was before. use "locking boolean" as discussed on irc)
+
+More or less in order of importance/urgency
+
+* a variable that holds the page state: loading, pending, seen. this can be shown in titlebar/statusbar and used for multiple instances management
+* allow to tag , to group instances together
+* store uri/tag/name/state in xorg window properties
+* scripts that we use in uzbl-browser -> move from examples dir to $PREFIX/share/uzbl/ (update scripts_dir in sampleconfig accordingly)
+* shortcuts to focus other instances (see docs/multiple-instances-management)
+* cookie daemon (and maybe other daemons) are not scoped to the testing session when running some test-*-browser targets
+* recognize -h with GOption?
+* scrolling: make page up and page down configurable.
+* conditionals in format strings: eg if(SELECTED_URI) { "-> SELECTED_URI" } or another smart way to achieve the same.
+* on uzbl.org commits overview: add date+time and repository
+* how to handle different content types? (text-plain, image/png, application/pdf,... maybe a map of content-type to uzbl/command
+ xdg already has a spec for this i think
+ different "opening" modes (open as configured vs ask before opening)
+ integration with download and new window thingies?
+* blinking cursor when not in insert mode is confusing. i suggest dimming it's color if possible
+* tab key to jump between input fields should probably work in both insert and command mode
+* optional logging of http requests&responses with ip/hostname and port. -> how to implement? handler? stdout? (through a socket so you know what corresponds to what?)
+* bench/optimize fifo vs socket performance. measure delays. minimize forks. does glib use a shell? how does it detect the shebang line?
+* "remember account settings" support. but how? configure post data per site? regex match eg '^bbs.archlinux.org' ?
+* http_proxy env var not recognized. libproxy (used by libsoup) should handle this http://mail.gnome.org/archives/libsoup-list/2009-February/msg00018.html
+* support ssl. do ssl certificate & exception management similar to how we do cookies
+* improve DCOMMIT macro. what if WC is dirty?
+* DARCH is not correct (should be at runtime)
+* keybinds to open "next" or "previous" by looking for next/prev links and/or looking for numbers in the uri we can inc/decrement
+* settings iterating "state generator" so we can "open in new window" again.
+* handler for (broken) ssl certs.
+* proxy_url is not a good var name. it's not a url.
+* regex style page searching? so you can do 'or' and 'and' things. flags like case sensitive etc.
+* check for real command name, not just the first letter.
+* let users attach handlers to the most common events/signals in uzbl.
+ great use case: automatically calling formfiller for certain sites, doing stuff at uzbl startup, etc
+* document:
+ stylesheet overridding
+ formfiller
+ full duplex socket
+ ^X and such binds
+ link following
+ scrolling in %
+ webkit inspector usage
+ scroll commands can take %s, eg scroll 100% for pages
+ chaining of actions, print (and other actions that aren't documented yet)
+ overriding variables (such as -u)
+ variable expansion (@var, @{var}, where do they get expanded? can users have their own vars?, should we merge this with the replacement we do for useragent/window title etc?)
+ how %s works for the js command
+
+
+SOMEDAY:
+figure out caching with webkit and in general how we can speed up everything
+figure out how webkit intercepts key input
+make "disable insert mode" (esc key) configurable
+keywords don't work for external commands. is this a problem?
+* pass a bit less arguments by default, use the socket to query for them instead, or export the stuff through environment variables, or export them as xorg window properties
diff --git a/docs/config-syntax b/docs/config-syntax
new file mode 100644
index 0000000..9894401
--- /dev/null
+++ b/docs/config-syntax
@@ -0,0 +1,21 @@
+here: design notes
+readme: end-user instructions
+
+
+Rationale. AKA "why not config files?"
+-> unify code to configure uzbl and to accept incoming commands
+-> no more code needed for config file parsing/handling
+-> runtime changing of configuration
+
+issues
+-> new instances (open link etc) need same config/state
+ solutions?
+ - serialize all state structs -> some libs available, could work. but:
+ - binary format not very user friendly
+ - not that easy (C has no introspection etc)
+ - iterate over state structs and generate appropriate commands to bring an instance in this state.
+ - plaintext :)
+ - user editable
+ - also useful for saving state if we need to update/shutdown/..
+ -> all of this is overkill. for now we just decided to live with the fact we pass on '--config' flags,
+ it's up to the user to tweak his setup so it works.
diff --git a/docs/multiple-instances-management b/docs/multiple-instances-management
new file mode 100644
index 0000000..da55002
--- /dev/null
+++ b/docs/multiple-instances-management
@@ -0,0 +1,41 @@
+"multiple instance management" or: how to manage multiple open pages/instances of uzbl.
+
+The way we do MIM in uzbl will be better then what you can do with tabs in other programs.
+Tabs are just one specific gui implementation which aids in some aspects of "multiple instances management" but not all.
+We can get the same and even better features functionality wise, without limiting ourselves to the particular implementation that tabs are.
+We use a "use-case"/tasks driven approach, unlike tabs which is a "lets do this, and then see how we can use it" approach.
+
+The approach we are implementing in uzbl is like this:
+- 1 page per instance.
+- ability to spawn new windows ("open in new window"), keybinds for having the new window focused or not.
+- each instance keeps track of it's page state (loading, loaded but not seen, loaded and seen)
+- ability to "tag" instances (inherited when forking new instances) (eg tag "work" or tag "personal") so you can keep related instances together.
+ The tag can be made visible in the title / statusbar.
+- allow user to make keybinds to focus the next/previous:
+ - window
+ - unseen/seen window
+ - unseen/seen window from the same tag
+ - unseen/seen window from tag <foo> (dmenu)
+- have a program that lists all instances (optionally filtered, or categorized by tag, state, ...) and allow user to select one from it using smart matching techniques which require a minimal amount of keystrokes. (dmenu with vertical patch is great for this)
+
+Here's an overview what do tabs really do, whether we really need it, and if can improve it?
+
+
+* tabs keep multiple open pages together when moving the window other tag/workspace
+ -> in an ideal workspace, you assign windows to a tag/workspace and keep them there.
+ -> in practice, when I was still using Firefox, I noticed I moved my "group of bundled pages" (eg a FF window) sometimes, to aid in copy pasting between browser and another app, or doing something while watching/reading a webpage.
+ This means however I only really need that specific page, not the others. -> With the suggested approach, you can easily temporarily move one window.
+* tabs keep an oversight of which pages you have open
+ -> you should "know" more or less know which pages you have open. The gui is clutter. It should however be possible to show the list "on demand".
+ -> a horizontal list of tabs is also not very readable and lacks useful categorisation/grouping.
+ With the suggested approach we will have each url below each other, which is far more readable, and we can use additional text or color themes to denote the state/tag/...
+* tabs aid opening new pages in the background which you will open later and provide shortcuts to go to the previous/next tab.
+ -> our approach is more powerful.
+ -> With tab based browsers you can usually configure if you want new tabs to open "immediately after the current tab", or "at the end of the list". This is much less flexible/usable then what we are building.
+
+Conclusion
+---------
+I think our method is better then tabs. Bonus advantages:
+* single uzbl instances are simple to implement, no added clutter.
+* one crashing instance does not affect the rest
+* all this stuff is implemented outside of uzbl. You can even decide to not use it and you won't even know the feature was ever there. (though i guess most people will want this).
diff --git a/docs/performance b/docs/performance
new file mode 100644
index 0000000..0f9763f
--- /dev/null
+++ b/docs/performance
@@ -0,0 +1,9 @@
+The usage of external scripts causes some slowdowns. (forking new processes, running interpreted scripts,...)
+It can be interesting to keep an eye on this, though I don't expect major problems.
+
+** History file size/performance
+each new pageload -> fopen(history_file, "a"), fwrite one line, fclose.
+I use utf8, so unless you use characters that are "special" (chinese etc) each character takes 1 byte.
+So, assume each entry is about 80 chars, you visit 100 pages per day (?), and you wonder when your history file will be 50MB big:
+(50 * 1000 * 1000 ) / ( 80 * 100 ) = 6250 days or 17 years.
+There is code to run a benchmark in the 'extra' dir. For results & interpretation, see http://dieter.plaetinck.be/poor_mans_dmenu_benchmark
diff --git a/docs/url-editing b/docs/url-editing
new file mode 100644
index 0000000..d8943fb
--- /dev/null
+++ b/docs/url-editing
@@ -0,0 +1,9 @@
+* All url changing/editing happens outside of uzbl
+
+Use cases:
+* load url from history/bookmarks (dmenu-vertical is great for this)
+* edit current url or one from history/bookmarks: dmenu-vertical is relatively good for this, though it would be better if we had:
+- minimal editing helper key shortcuts (eg to go to beginning/end of input buffer, erase search string, undo)
+- copy/paste support
+
+* Though you can obviously type new urls and edit the current ones with the appropriate statusbar stuff/binds/xclip commands, this is sometimes enough. \ No newline at end of file
diff --git a/examples/config/config b/examples/config/config
new file mode 100644
index 0000000..4c63fe7
--- /dev/null
+++ b/examples/config/config
@@ -0,0 +1,389 @@
+# example uzbl config.
+# all settings are optional. you can use uzbl without any config at all (but it won't do much)
+
+set prefix = /usr/local
+
+# === Shortcuts / Aliases ===================================================
+
+# Config related events (use the request function):
+# request BIND <bind cmd> = <command>
+set bind = request BIND
+# request MODE_BIND <mode> <bind cmd> = <command>
+set mode_bind = request MODE_BIND
+# request MODE_CONFIG <mode> <key> = <value>
+set mode_config = request MODE_CONFIG
+# request ON_EVENT <EVENT_NAME> <command>
+set on_event = request ON_EVENT
+# request PROGRESS_CONFIG <key> = <value>
+set progress = request PROGRESS_CONFIG
+# request MODMAP <From> <To>
+set modmap = request MODMAP
+# request IGNORE_KEY <glob>
+set ignore_key = request IGNORE_KEY
+# request MODKEY_ADDITION <key1> <key2> <keyn> <result>
+set modkey_addition = request MODKEY_ADDITION
+
+# Action related events (use the event function):
+# event TOGGLE_MODES <mode1> <mode2> ... <moden>
+set toggle_modes = event TOGGLE_MODES
+
+set set_mode = set mode =
+set set_status = set status_message =
+set shell_cmd = sh -c
+
+# Spawn path shortcuts. In spawn the first dir+path match is used in "dir1:dir2:dir3:executable"
+set scripts_dir = $XDG_DATA_HOME/uzbl:@prefix/share/uzbl/examples/data:scripts
+
+
+# === Handlers ===============================================================
+
+# --- Hardcoded event handlers -----------------------------------------------
+
+# These handlers can't be moved to the new event system yet as we don't
+# support events that can wait for a response from a script.
+set cookie_handler = talk_to_socket $XDG_CACHE_HOME/uzbl/cookie_daemon_socket
+set scheme_handler = sync_spawn @scripts_dir/scheme.py
+set authentication_handler = sync_spawn @scripts_dir/auth.py
+
+# Open in the same window.
+#set new_window = sh 'echo uri "$8" > $4'
+# Open a link in a new window. equivalent to default behavior
+set new_window = sh 'uzbl-browser -u $8'
+
+# --- Optional dynamic event handlers ----------------------------------------
+
+# Download handler
+@on_event DOWNLOAD_REQUEST spawn @scripts_dir/download.sh %s \@proxy_url
+
+# Load start handler
+@on_event LOAD_START @set_status <span foreground="khaki">wait</span>
+
+# Load commit handlers
+@on_event LOAD_COMMIT @set_status <span foreground="green">recv</span>
+@on_event LOAD_COMMIT script @scripts_dir/scroll-percentage.js
+# Reset the keycmd on navigation
+@on_event LOAD_COMMIT @set_mode
+
+# Load finish handlers
+@on_event LOAD_FINISH @set_status <span foreground="gold">done</span>
+@on_event LOAD_FINISH spawn @scripts_dir/history.sh
+
+# Switch to insert mode if a (editable) html form is clicked
+@on_event FORM_ACTIVE @set_mode insert
+# Switch to command mode if anything else is clicked
+@on_event ROOT_ACTIVE @set_mode command
+
+# Example CONFIG_CHANGED event handler
+#@on_event CONFIG_CHANGED print Config changed: %1 = %2
+
+
+# === Behaviour and appearance ===============================================
+
+set show_status = 1
+set status_top = 0
+set status_background = #303030
+
+set modcmd_style = weight="bold" foreground="red"
+set keycmd_style = weight="light" foreground="red"
+set prompt_style = foreground="grey"
+set cursor_style = underline="single"
+set completion_style = foreground="green"
+set hint_style = weight="bold"
+
+set mode_section = <span background="khaki" foreground="black">[\@[\@mode_indicator]\@]</span>
+set keycmd_section = [<span \@prompt_style>\@[\@keycmd_prompt]\@</span><span \@modcmd_style>\@modcmd</span><span \@keycmd_style>\@keycmd</span><span \@completion_style>\@completion_list</span>]
+set progress_section = <span foreground="#606060">\@[\@progress_format]\@</span>
+set scroll_section = <span foreground="#606060">\@[\@scroll_message]\@</span>
+set uri_section = <span foreground="#99FF66">\@[\@uri]\@</span>
+set name_section = <span foreground="khaki">\@[\@NAME]\@</span>
+set status_section = <span foreground="orange">\@status_message</span>
+set selected_section = <span foreground="#606060">\@[\@SELECTED_URI]\@</span>
+
+set status_format = <span font_family="monospace">@mode_section @keycmd_section @progress_section @uri_section @name_section @status_section @scroll_section @selected_section</span>
+
+set title_format_long = \@keycmd_prompt \@raw_modcmd \@raw_keycmd \@TITLE - Uzbl browser <\@NAME> \@SELECTED_URI
+
+# Progress bar config
+@progress width = 8
+# %d = done, %p = pending %c = percent done, %i = int done, %s = spinner,
+# %t = percent pending, %o = int pending, %r = sprite scroll
+@progress format = [%d>%p]%c
+@progress done = =
+@progress pending =
+
+# Or ride those spinnas'
+#@progress format = [%d%s%p]
+#@progress spinner = -\\|/
+#@progress done = -
+#@progress pending =
+
+
+# === Core settings ==========================================================
+
+set useragent = Uzbl (Webkit @WEBKIT_MAJOR.@WEBKIT_MINOR.@WEBKIT_MICRO) (@(+uname -o)@ @(+uname -m)@ [@ARCH_UZBL]) (Commit @COMMIT)
+set fifo_dir = /tmp
+set socket_dir = /tmp
+
+
+# === Key modmapping and ignoring ============================================
+
+#modmap <From> <To>
+@modmap <Control> <Ctrl>
+@modmap <ISO_Left_Tab> <Shift-Tab>
+@modmap <space> <Space>
+
+#modkey_addition <Key1> <Key2> <Result>
+@modkey_addition <Shift> <Ctrl> <Meta>
+@modkey_addition <Shift> <Tab> <Shift-Tab>
+
+#ignore_key <glob>
+@ignore_key <ISO_*>
+@ignore_key <Shift>
+
+
+# === Mode bind aliases ======================================================
+
+# Global binding alias (this is done automatically inside the bind plugin).
+#set bind = @mode_bind global
+
+# Insert mode binding alias
+set ibind = @mode_bind insert
+
+# Command mode binding alias
+set cbind = @mode_bind command
+
+# Non-insert mode bindings alias (ebind for edit-bind).
+set ebind = @mode_bind global,-insert
+
+
+# === Global & keycmd editing binds ==========================================
+
+# Resets keycmd and returns to default mode.
+@bind <Escape> = @set_mode
+
+# Commands for editing and traversing the keycmd.
+@ebind <Return> = event KEYCMD_EXEC_CURRENT
+@ebind <Home> = event SET_CURSOR_POS
+@ebind <End> = event SET_CURSOR_POS -1
+@ebind <Left> = event SET_CURSOR_POS -
+@ebind <Right> = event SET_CURSOR_POS +
+@ebind <BackSpace> = event KEYCMD_BACKSPACE
+@ebind <Delete> = event KEYCMD_DELETE
+@ebind <Tab> = event START_COMPLETION
+# Readline-ish bindings.
+@ebind <Ctrl>w = event KEYCMD_STRIP_WORD
+@ebind <Ctrl>u = event SET_KEYCMD
+@ebind <Ctrl>a = event SET_CURSOR_POS 0
+@ebind <Ctrl>e = event SET_CURSOR_POS -1
+
+# Keycmd injection/append examples.
+#@ebind <Ctrl>su = event INJECT_KEYCMD \@uri
+#@ebind <Ctrl>st = event INJECT_KEYCMD \@title
+#@ebind <Ctrl>du = event APPEND_KEYCMD \@uri
+#@ebind <Ctrl>dt = event APPEND_KEYCMD \@title
+
+
+# === Mouse bindings =========================================================
+
+# Middle click open in new window
+@bind <Button2> = sh 'if [ "\@SELECTED_URI" ]; then uzbl-browser -u "\@SELECTED_URI"; else echo "uri $(xclip -o)" > $4; fi'
+
+
+# === Keyboard bindings ======================================================
+
+# With this command you can enter in any command at runtime when prefixed with
+# a colon.
+@cbind :_ = %s
+
+# --- Page movement binds ---
+@cbind j = scroll vertical 20
+@cbind k = scroll vertical -20
+@cbind h = scroll horizontal -20
+@cbind l = scroll horizontal 20
+@cbind <Page_Up> = scroll vertical -100%
+@cbind <Page_Down> = scroll vertical 100%
+@cbind << = scroll vertical begin
+@cbind >> = scroll vertical end
+@cbind ^ = scroll horizontal begin
+@cbind $ = scroll horizontal end
+@cbind <Space> = scroll vertical end
+
+# --- Navigation binds ---
+@cbind b = back
+@cbind m = forward
+@cbind S = stop
+@cbind r = reload
+@cbind R = reload_ign_cache
+
+# --- Zoom binds ---
+@cbind + = zoom_in
+@cbind - = zoom_out
+@cbind T = toggle_zoom_type
+@cbind 1 = set zoom_level 1.0
+@cbind 2 = set zoom_level 2.0
+
+# --- Appearance binds ---
+@cbind t = toggle_status
+
+# --- Page searching binds ---
+@cbind /* = search %s
+@cbind ?* = search_reverse %s
+# Jump to next and previous items
+@cbind n = search
+@cbind N = search_reverse
+
+# --- Uzbl tabbed binds ---
+# Tab opening
+@cbind gn = event NEW_TAB
+@cbind go<uri:>_ = event NEW_TAB %s
+@cbind gY = sh 'echo "event NEW_TAB `xclip -selection primary -o`" > $4'
+# Closing / resting
+@cbind gC = exit
+@cbind gQ = event CLEAN_TABS
+# Tab navigating
+@cbind g< = event FIRST_TAB
+@cbind g> = event LAST_TAB
+@cbind gt = event NEXT_TAB
+@cbind gT = event PREV_TAB
+@cbind gi<index:>_ = event GOTO_TAB %s
+# Preset loading
+set preset = event PRESET_TABS
+@cbind gs<preset save:>_ = @preset save %s
+@cbind glo<preset load:>_ = @preset load %s
+@cbind gd<preset del:>_ = @preset del %s
+@cbind gli = @preset list
+
+# --- Web searching binds ---
+@cbind gg<Google:>_ = uri http://www.google.com/search?q=\@<encodeURIComponent(%r)>\@
+@cbind \\awiki<Archwiki:>_ = uri http://wiki.archlinux.org/index.php/Special:Search?search=\@<encodeURIComponent(%r)>\@&go=Go
+@cbind \\wiki<Wikipedia:>_ = uri http://en.wikipedia.org/w/index.php?title=Special:Search&search=\@<encodeURIComponent(%r)>\@&go=Go
+
+# --- Handy binds ---
+# Set function shortcut
+@cbind s<var:>_<value:>_ = set %1 = %2
+# Exit binding
+@cbind ZZ = exit
+# Dump config to stdout
+@cbind !dump = sh "echo dump_config > $4"
+# Reload config
+@cbind !reload = sh "sed '/^# === Post-load misc commands/,$d' $1 > $4"
+# Uzbl Terminal. TODO explain why this is useful
+@cbind <Ctrl><Alt>t = sh 'xterm -e "socat unix-connect:$5 -"'
+#@cbind <Ctrl><Alt>t = sh 'urxvt -e socat unix-connect:$5 -'
+
+# --- Uri opening prompts ---
+@cbind o<uri:>_ = uri %s
+# Or have it load the current uri into the keycmd for editing
+@cbind O<uri:\@uri>_ = uri %s
+
+# --- Mode setting binds ---
+# Changing mode via set.
+@cbind I = @set_mode insert
+# Or toggle between modes by raising the toggle event.
+set toggle_cmd_ins = @toggle_modes command insert
+@cbind i = @toggle_cmd_ins
+# And the global toggle bind.
+@bind <Ctrl>i = @toggle_cmd_ins
+
+# --- Hard-bound bookmarks ---
+@cbind gh = uri http://www.uzbl.org
+
+# --- Yanking & pasting binds ---
+@cbind yu = sh 'echo -n $6 | xclip'
+@cbind yy = sh 'echo -n $7 | xclip'
+
+# Go the page from primary selection
+@cbind p = sh 'echo "uri `xclip -selection primary -o`" > $4'
+# Go to the page in clipboard
+@cbind P = sh 'echo "uri `xclip -selection clipboard -o`" > $4'
+# Start a new uzbl instance from the page in primary selection
+@cbind 'p = sh 'exec uzbl-browser --uri $(xclip -o)'
+
+# --- Bookmark inserting binds ---
+@cbind <Ctrl>b<tags:>_ = sh 'echo -e "$6 %s" >> $XDG_DATA_HOME/uzbl/bookmarks'
+# Or use a script to insert a bookmark.
+@cbind B = spawn @scripts_dir/insert_bookmark.sh
+
+# --- Bookmark/history loading ---
+@cbind U = spawn @scripts_dir/load_url_from_history.sh
+@cbind u = spawn @scripts_dir/load_url_from_bookmarks.sh
+
+# --- Link following (similar to vimperator and konqueror) ---
+# Set custom keys you wish to use for navigation. Some common examples:
+set follow_hint_keys = 0123456789
+#set follow_hint_keys = qwerty
+#set follow_hint_keys = asdfghjkl;
+#set follow_hint_keys = thsnd-rcgmvwb/;789aefijkopquxyz234
+@cbind fl* = script @scripts_dir/follow.js '@follow_hint_keys %s'
+
+# --- Form filler binds ---
+# this script allows you to configure (per domain) values to fill in form
+# fields (eg login information) and to fill in these values automatically
+set formfiller = spawn @scripts_dir/formfiller
+@cbind za = @{formfiller}.sh
+@cbind ze = @{formfiller}.sh edit
+@cbind zn = @{formfiller}.sh new
+@cbind zl = @{formfiller}.sh load
+# Or the more advanced implementation using perl: (could not get this to run - Dieter)
+@cbind LL = @{formfiller}.pl load
+@cbind LN = @{formfiller}.pl new
+@cbind LE = @{formfiller}.pl edit
+
+# --- External edit script configuration & binds ---
+# Edit form input fields in an external editor (gvim, emacs, urxvt -e vim, ..)
+set external_editor = gvim
+#set external_editor = xterm -e vim
+@cbind E = script @scripts_dir/extedit.js
+# And add menu option.
+menu_editable_add Open in @external_editor = script @scripts_dir/extedit.js
+
+# --- Examples ---
+# Example showing how to use uzbl's fifo to execute a command.
+#@bind X1 = sh 'echo "set zoom_level = 1.0" > "$4"'
+#@bind X2 = sh 'echo "js alert (\\"This is sent by the shell via a fifo\\")" > "$4"'
+
+
+# === Context menu items =====================================================
+
+# Default context menu
+menu_add Google = set uri = http://google.com
+menu_add Go Home = set uri = http://uzbl.org
+menu_separator separator_1
+menu_add Quit uzbl = exit
+
+# Link context menu
+menu_link_add Print Link = print \@SELECTED_URI
+
+
+# === Mode configuration =====================================================
+
+# Define some mode specific uzbl configurations.
+set command = @mode_config command
+set insert = @mode_config insert
+set stack = @mode_config stack
+
+# Command mode config.
+@command keycmd_style = foreground="red"
+@command status_background = #202020
+@command mode_indicator = Cmd
+
+# Insert mode config.
+@insert status_background = #303030
+@insert mode_indicator = Ins
+
+# Multi-stage-binding mode config.
+@stack keycmd_events = 1
+@stack modcmd_updates = 1
+@stack forward_keys = 0
+@stack keycmd_style = foreground="red"
+@stack prompt_style = foreground="#888" weight="light"
+@stack status_background = #202020
+@stack mode_indicator = Bnd
+
+set default_mode = command
+
+
+# === Post-load misc commands ===============================================
+
+# Set the "home" page.
+set uri = uzbl.org/doesitwork/@COMMIT
diff --git a/examples/config/cookies b/examples/config/cookies
new file mode 100644
index 0000000..9b7374a
--- /dev/null
+++ b/examples/config/cookies
@@ -0,0 +1,22 @@
+# This file demonstrates how one *could* manage his cookies. this file is used by the example cookie handler script.
+# stick to this format.
+# trusted -> always store what we get, send what we have (TODO: by default, or when requested?)
+# deny -> deny storing + sending
+
+# if you don't like to edit this file manually, you could even write a script that adds/removes entries using sed, and call the script from uzbl with a keybind...
+
+
+TRUSTED
+bbs.archlinux.org
+archlinux.org
+linux.com
+
+
+
+
+DENY
+www.icanhascheezburger.com
+
+
+
+# rest -> ask \ No newline at end of file
diff --git a/examples/data/bookmarks b/examples/data/bookmarks
new file mode 100644
index 0000000..13fcd48
--- /dev/null
+++ b/examples/data/bookmarks
@@ -0,0 +1,4 @@
+http://www.archlinux.org linux arch
+http://www.uzbl.org uzbl browser
+http://dieter.plaetinck.be uzbl
+http://www.icanhascheezburger.com lolcats fun
diff --git a/examples/data/forms/bbs.archlinux.org b/examples/data/forms/bbs.archlinux.org
new file mode 100644
index 0000000..73c1539
--- /dev/null
+++ b/examples/data/forms/bbs.archlinux.org
@@ -0,0 +1,5 @@
+form_sent:
+redirect_url:
+req_username: <your username>
+req_password: <password>
+login:
diff --git a/examples/data/plugins/bind.py b/examples/data/plugins/bind.py
new file mode 100644
index 0000000..a1a5d89
--- /dev/null
+++ b/examples/data/plugins/bind.py
@@ -0,0 +1,492 @@
+'''Plugin provides support for binds in uzbl.
+
+For example:
+ event BIND ZZ = exit -> bind('ZZ', 'exit')
+ event BIND o _ = uri %s -> bind('o _', 'uri %s')
+ event BIND fl* = sh 'echo %s' -> bind('fl*', "sh 'echo %s'")
+
+And it is also possible to execute a function on activation:
+ bind('DD', myhandler)
+'''
+
+import sys
+import re
+import pprint
+
+# Hold the bind dicts for each uzbl instance.
+UZBLS = {}
+
+# Commonly used regular expressions.
+MOD_START = re.compile('^<([A-Z][A-Za-z0-9-_]*)>').match
+# Matches <x:y>, <'x':y>, <:'y'>, <x!y>, <'x'!y>, ...
+PROMPTS = '<(\"[^\"]*\"|\'[^\']*\'|[^:!>]*)(:|!)(\"[^\"]*\"|\'[^\']*\'|[^>]*)>'
+FIND_PROMPTS = re.compile(PROMPTS).split
+VALID_MODE = re.compile('^(-|)[A-Za-z0-9][A-Za-z0-9_]*$').match
+
+# For accessing a bind glob stack.
+ON_EXEC, HAS_ARGS, MOD_CMD, GLOB, MORE = range(5)
+
+
+# Custom errors.
+class ArgumentError(Exception): pass
+
+
+class Bindlet(object):
+ '''Per-instance bind status/state tracker.'''
+
+ def __init__(self, uzbl):
+ self.binds = {'global': {}}
+ self.uzbl = uzbl
+ self.depth = 0
+ self.args = []
+ self.last_mode = None
+ self.after_cmds = None
+ self.stack_binds = []
+
+ # A subset of the global mode binds containing non-stack and modkey
+ # activiated binds for use in the stack mode.
+ self.globals = []
+
+
+ def __getitem__(self, key):
+ return self.get_binds(key)
+
+
+ def reset(self):
+ '''Reset the tracker state and return to last mode.'''
+
+ self.depth = 0
+ self.args = []
+ self.after_cmds = None
+ self.stack_binds = []
+
+ if self.last_mode:
+ mode, self.last_mode = self.last_mode, None
+ self.uzbl.set_mode(mode)
+
+ self.uzbl.set('keycmd_prompt')
+
+
+ def stack(self, bind, args, depth):
+ '''Enter or add new bind in the next stack level.'''
+
+ if self.depth != depth:
+ if bind not in self.stack_binds:
+ self.stack_binds.append(bind)
+
+ return
+
+ current_mode = self.uzbl.get_mode()
+ if current_mode != 'stack':
+ self.last_mode = current_mode
+ self.uzbl.set_mode('stack')
+
+ self.stack_binds = [bind,]
+ self.args += args
+ self.depth += 1
+ self.after_cmds = bind.prompts[depth]
+
+
+ def after(self):
+ '''If a stack was triggered then set the prompt and default value.'''
+
+ if self.after_cmds is None:
+ return
+
+ (prompt, is_cmd, set), self.after_cmds = self.after_cmds, None
+
+ self.uzbl.clear_keycmd()
+ if prompt:
+ self.uzbl.set('keycmd_prompt', prompt)
+
+ if set and is_cmd:
+ self.uzbl.send(set)
+
+ elif set and not is_cmd:
+ self.uzbl.send('event SET_KEYCMD %s' % set)
+
+
+ def get_binds(self, mode=None):
+ '''Return the mode binds + globals. If we are stacked then return
+ the filtered stack list and modkey & non-stack globals.'''
+
+ if mode is None:
+ mode = self.uzbl.get_mode()
+
+ if not mode:
+ mode = 'global'
+
+ if self.depth:
+ return self.stack_binds + self.globals
+
+ globals = self.binds['global']
+ if mode not in self.binds or mode == 'global':
+ return filter(None, globals.values())
+
+ binds = dict(globals.items() + self.binds[mode].items())
+ return filter(None, binds.values())
+
+
+ def add_bind(self, mode, glob, bind=None):
+ '''Insert (or override) a bind into the mode bind dict.'''
+
+ if mode not in self.binds:
+ self.binds[mode] = {glob: bind}
+ return
+
+ binds = self.binds[mode]
+ binds[glob] = bind
+
+ if mode == 'global':
+ # Regen the global-globals list.
+ self.globals = []
+ for bind in binds.values():
+ if bind is not None and bind.is_global:
+ self.globals.append(bind)
+
+
+def add_instance(uzbl, *args):
+ UZBLS[uzbl] = Bindlet(uzbl)
+
+
+def del_instance(uzbl, *args):
+ if uzbl in UZBLS:
+ del UZBLS[uzbl]
+
+
+def get_bindlet(uzbl):
+ '''Return the bind tracklet for the given uzbl instance.'''
+
+ if uzbl not in UZBLS:
+ add_instance(uzbl)
+
+ return UZBLS[uzbl]
+
+
+def ismodbind(glob):
+ '''Return True if the glob specifies a modbind.'''
+
+ return bool(MOD_START(glob))
+
+
+def split_glob(glob):
+ '''Take a string of the form "<Mod1><Mod2>cmd _" and return a list of the
+ modkeys in the glob and the command.'''
+
+ mods = set()
+ while True:
+ match = MOD_START(glob)
+ if not match:
+ break
+
+ end = match.span()[1]
+ mods.add(glob[:end])
+ glob = glob[end:]
+
+ return (mods, glob)
+
+
+def unquote(str):
+ '''Remove quotation marks around string.'''
+
+ if str and str[0] == str[-1] and str[0] in ['"', "'"]:
+ str = str[1:-1]
+
+ return str
+
+
+class Bind(object):
+
+ # Class attribute to hold the number of Bind classes created.
+ counter = [0,]
+
+ def __init__(self, glob, handler, *args, **kargs):
+ self.is_callable = callable(handler)
+ self._repr_cache = None
+
+ if not glob:
+ raise ArgumentError('glob cannot be blank')
+
+ if self.is_callable:
+ self.function = handler
+ self.args = args
+ self.kargs = kargs
+
+ elif kargs:
+ raise ArgumentError('cannot supply kargs for uzbl commands')
+
+ elif hasattr(handler, '__iter__'):
+ self.commands = handler
+
+ else:
+ self.commands = [handler,] + list(args)
+
+ self.glob = glob
+
+ # Assign unique id.
+ self.counter[0] += 1
+ self.bid = self.counter[0]
+
+ self.split = split = FIND_PROMPTS(glob)
+ self.prompts = []
+ for (prompt, cmd, set) in zip(split[1::4], split[2::4], split[3::4]):
+ prompt, set = map(unquote, [prompt, set])
+ cmd = True if cmd == '!' else False
+ if prompt and prompt[-1] != ":":
+ prompt = "%s:" % prompt
+
+ self.prompts.append((prompt, cmd, set))
+
+ # Check that there is nothing like: fl*<int:>*
+ for glob in split[:-1:4]:
+ if glob.endswith('*'):
+ msg = "token '*' not at the end of a prompt bind: %r" % split
+ raise SyntaxError(msg)
+
+ # Check that there is nothing like: fl<prompt1:><prompt2:>_
+ for glob in split[4::4]:
+ if not glob:
+ msg = 'found null segment after first prompt: %r' % split
+ raise SyntaxError(msg)
+
+ stack = []
+ for (index, glob) in enumerate(reversed(split[::4])):
+ # Is the binding a MODCMD or KEYCMD:
+ mod_cmd = ismodbind(glob)
+
+ # Do we execute on UPDATES or EXEC events?
+ on_exec = True if glob[-1] in ['!', '_'] else False
+
+ # Does the command take arguments?
+ has_args = True if glob[-1] in ['*', '_'] else False
+
+ glob = glob[:-1] if has_args or on_exec else glob
+ mods, glob = split_glob(glob)
+ stack.append((on_exec, has_args, mods, glob, index))
+
+ self.stack = list(reversed(stack))
+ self.is_global = (len(self.stack) == 1 and self.stack[0][MOD_CMD])
+
+
+ def __getitem__(self, depth):
+ '''Get bind info at a depth.'''
+
+ if self.is_global:
+ return self.stack[0]
+
+ return self.stack[depth]
+
+
+ def __repr__(self):
+ if self._repr_cache:
+ return self._repr_cache
+
+ args = ['glob=%r' % self.glob, 'bid=%d' % self.bid]
+
+ if self.is_callable:
+ args.append('function=%r' % self.function)
+ if self.args:
+ args.append('args=%r' % self.args)
+
+ if self.kargs:
+ args.append('kargs=%r' % self.kargs)
+
+ else:
+ cmdlen = len(self.commands)
+ cmds = self.commands[0] if cmdlen == 1 else self.commands
+ args.append('command%s=%r' % ('s' if cmdlen-1 else '', cmds))
+
+ self._repr_cache = '<Bind(%s)>' % ', '.join(args)
+ return self._repr_cache
+
+
+def exec_bind(uzbl, bind, *args, **kargs):
+ '''Execute bind objects.'''
+
+ uzbl.event("EXEC_BIND", bind, args, kargs)
+
+ if bind.is_callable:
+ args += bind.args
+ kargs = dict(bind.kargs.items()+kargs.items())
+ bind.function(uzbl, *args, **kargs)
+ return
+
+ if kargs:
+ raise ArgumentError('cannot supply kargs for uzbl commands')
+
+ commands = []
+ cmd_expand = uzbl.cmd_expand
+ for cmd in bind.commands:
+ cmd = cmd_expand(cmd, args)
+ uzbl.send(cmd)
+
+
+def mode_bind(uzbl, modes, glob, handler=None, *args, **kargs):
+ '''Add a mode bind.'''
+
+ bindlet = get_bindlet(uzbl)
+
+ if not hasattr(modes, '__iter__'):
+ modes = unicode(modes).split(',')
+
+ # Sort and filter binds.
+ modes = filter(None, map(unicode.strip, modes))
+
+ if callable(handler) or (handler is not None and handler.strip()):
+ bind = Bind(glob, handler, *args, **kargs)
+
+ else:
+ bind = None
+
+ for mode in modes:
+ if not VALID_MODE(mode):
+ raise NameError('invalid mode name: %r' % mode)
+
+ for mode in modes:
+ if mode[0] == '-':
+ mode, bind = mode[1:], None
+
+ bindlet.add_bind(mode, glob, bind)
+ uzbl.event('ADDED_MODE_BIND', mode, glob, bind)
+
+
+def bind(uzbl, glob, handler, *args, **kargs):
+ '''Legacy bind function.'''
+
+ mode_bind(uzbl, 'global', glob, handler, *args, **kargs)
+
+
+def parse_mode_bind(uzbl, args):
+ '''Parser for the MODE_BIND event.
+
+ Example events:
+ MODE_BIND <mode> <bind> = <command>
+ MODE_BIND command o<location:>_ = uri %s
+ MODE_BIND insert,command <BackSpace> = ...
+ MODE_BIND global ... = ...
+ MODE_BIND global,-insert ... = ...
+ '''
+
+ if not args:
+ raise ArgumentError('missing bind arguments')
+
+ split = map(unicode.strip, args.split(' ', 1))
+ if len(split) != 2:
+ raise ArgumentError('missing mode or bind section: %r' % args)
+
+ modes, args = split[0].split(','), split[1]
+ split = map(unicode.strip, args.split('=', 1))
+ if len(split) != 2:
+ raise ArgumentError('missing delimiter in bind section: %r' % args)
+
+ glob, command = split
+ mode_bind(uzbl, modes, glob, command)
+
+
+def parse_bind(uzbl, args):
+ '''Legacy parsing of the BIND event and conversion to the new format.
+
+ Example events:
+ request BIND <bind> = <command>
+ request BIND o<location:>_ = uri %s
+ request BIND <BackSpace> = ...
+ request BIND ... = ...
+ '''
+
+ parse_mode_bind(uzbl, "global %s" % args)
+
+
+def mode_changed(uzbl, mode):
+ '''Clear the stack on all non-stack mode changes.'''
+
+ if mode != 'stack':
+ get_bindlet(uzbl).reset()
+
+
+def match_and_exec(uzbl, bind, depth, keylet, bindlet):
+ (on_exec, has_args, mod_cmd, glob, more) = bind[depth]
+ cmd = keylet.modcmd if mod_cmd else keylet.keycmd
+
+ if mod_cmd and keylet.held != mod_cmd:
+ return False
+
+ if has_args:
+ if not cmd.startswith(glob):
+ return False
+
+ args = [cmd[len(glob):],]
+
+ elif cmd != glob:
+ return False
+
+ else:
+ args = []
+
+ if bind.is_global or (not more and depth == 0):
+ exec_bind(uzbl, bind, *args)
+ if not has_args:
+ uzbl.clear_current()
+
+ return True
+
+ elif more:
+ bindlet.stack(bind, args, depth)
+ (on_exec, has_args, mod_cmd, glob, more) = bind[depth+1]
+ if not on_exec and has_args and not glob and not more:
+ exec_bind(uzbl, bind, *(args+['',]))
+
+ return False
+
+ args = bindlet.args + args
+ exec_bind(uzbl, bind, *args)
+ if not has_args or on_exec:
+ uzbl.set_mode()
+ bindlet.reset()
+ uzbl.clear_current()
+
+ return True
+
+
+def key_event(uzbl, keylet, mod_cmd=False, on_exec=False):
+ bindlet = get_bindlet(uzbl)
+ depth = bindlet.depth
+ for bind in bindlet.get_binds():
+ t = bind[depth]
+ if (bool(t[MOD_CMD]) != mod_cmd) or (t[ON_EXEC] != on_exec):
+ continue
+
+ if match_and_exec(uzbl, bind, depth, keylet, bindlet):
+ return
+
+ bindlet.after()
+
+ # Return to the previous mode if the KEYCMD_EXEC keycmd doesn't match any
+ # binds in the stack mode.
+ if on_exec and not mod_cmd and depth and depth == bindlet.depth:
+ uzbl.set_mode()
+
+
+def init(uzbl):
+ # Event handling hooks.
+ uzbl.connect_dict({
+ 'BIND': parse_bind,
+ 'MODE_BIND': parse_mode_bind,
+ 'MODE_CHANGED': mode_changed,
+ })
+
+ # Connect key related events to the key_event function.
+ events = [['KEYCMD_UPDATE', 'KEYCMD_EXEC'],
+ ['MODCMD_UPDATE', 'MODCMD_EXEC']]
+
+ for mod_cmd in range(2):
+ for on_exec in range(2):
+ event = events[mod_cmd][on_exec]
+ uzbl.connect(event, key_event, bool(mod_cmd), bool(on_exec))
+
+ # Function exports to the uzbl object, `function(uzbl, *args, ..)`
+ # becomes `uzbl.function(*args, ..)`.
+ uzbl.export_dict({
+ 'bind': bind,
+ 'mode_bind': mode_bind,
+ 'get_bindlet': get_bindlet,
+ })
diff --git a/examples/data/plugins/cmd_expand.py b/examples/data/plugins/cmd_expand.py
new file mode 100644
index 0000000..3f6ae2b
--- /dev/null
+++ b/examples/data/plugins/cmd_expand.py
@@ -0,0 +1,42 @@
+def escape(str):
+ for (level, char) in [(3, '\\'), (2, "'"), (2, '"'), (1, '@')]:
+ str = str.replace(char, (level * '\\') + char)
+
+ return str
+
+
+def cmd_expand(uzbl, cmd, args):
+ '''Exports a function that provides the following
+ expansions in any uzbl command string:
+
+ %s = replace('%s', ' '.join(args))
+ %r = replace('%r', "'%s'" % escaped(' '.join(args)))
+ %1 = replace('%1', arg[0])
+ %2 = replace('%2', arg[1])
+ %n = replace('%n', arg[n-1])
+ '''
+
+ # Ensure (1) all string representable and (2) correct string encoding.
+ args = map(unicode, args)
+
+ # Direct string replace.
+ if '%s' in cmd:
+ cmd = cmd.replace('%s', ' '.join(args))
+
+ # Escaped and quoted string replace.
+ if '%r' in cmd:
+ cmd = cmd.replace('%r', "'%s'" % escape(' '.join(args)))
+
+ # Arg index string replace.
+ for (index, arg) in enumerate(args):
+ index += 1
+ if '%%%d' % index in cmd:
+ cmd = cmd.replace('%%%d' % index, unicode(arg))
+
+ return cmd
+
+
+def init(uzbl):
+ # Function exports to the uzbl object, `function(uzbl, *args, ..)`
+ # becomes `uzbl.function(*args, ..)`.
+ uzbl.export('cmd_expand', cmd_expand)
diff --git a/examples/data/plugins/completion.py b/examples/data/plugins/completion.py
new file mode 100644
index 0000000..8cea203
--- /dev/null
+++ b/examples/data/plugins/completion.py
@@ -0,0 +1,206 @@
+'''Keycmd completion.'''
+
+# A list of functions this plugin exports to be used via uzbl object.
+__export__ = ['start_completion', 'get_completion_dict']
+
+import re
+
+# Holds the per-instance completion dicts.
+UZBLS = {}
+
+# Completion level
+NONE, ONCE, LIST, COMPLETE = range(4)
+
+# Default instance dict.
+DEFAULTS = {'completions': [], 'level': NONE, 'lock': False}
+
+# The reverse keyword finding re.
+FIND_SEGMENT = re.compile("(\@[\w_]+|set[\s]+[\w_]+|[\w_]+)$").findall
+
+# Formats
+LIST_FORMAT = "<span> %s </span>"
+ITEM_FORMAT = "<span @hint_style>%s</span>%s"
+
+
+def escape(str):
+ return str.replace("@", "\@")
+
+
+def add_instance(uzbl, *args):
+ UZBLS[uzbl] = dict(DEFAULTS)
+
+ # Make sure the config keys for all possible completions are known.
+ uzbl.send('dump_config_as_events')
+
+
+def del_instance(uzbl, *args):
+ if uzbl in UZBLS:
+ del UZBLS[uzbl]
+
+
+def get_completion_dict(uzbl):
+ '''Get data stored for an instance.'''
+
+ if uzbl not in UZBLS:
+ add_instance(uzbl)
+
+ return UZBLS[uzbl]
+
+
+def get_incomplete_keyword(uzbl):
+ '''Gets the segment of the keycmd leading up to the cursor position and
+ uses a regular expression to search backwards finding parially completed
+ keywords or @variables. Returns a null string if the correct completion
+ conditions aren't met.'''
+
+ keylet = uzbl.get_keylet()
+ left_segment = keylet.keycmd[:keylet.cursor]
+ partial = (FIND_SEGMENT(left_segment) + ['',])[0].lstrip()
+ if partial.startswith('set '):
+ return ('@%s' % partial[4:].lstrip(), True)
+
+ return (partial, False)
+
+
+def stop_completion(uzbl, *args):
+ '''Stop command completion and return the level to NONE.'''
+
+ d = get_completion_dict(uzbl)
+ d['level'] = NONE
+ uzbl.set('completion_list')
+
+
+def complete_completion(uzbl, partial, hint, set_completion=False):
+ '''Inject the remaining porition of the keyword into the keycmd then stop
+ the completioning.'''
+
+ if set_completion:
+ remainder = "%s = " % hint[len(partial):]
+
+ else:
+ remainder = "%s " % hint[len(partial):]
+
+ uzbl.inject_keycmd(remainder)
+ stop_completion(uzbl)
+
+
+def partial_completion(uzbl, partial, hint):
+ '''Inject a common portion of the hints into the keycmd.'''
+
+ remainder = hint[len(partial):]
+ uzbl.inject_keycmd(remainder)
+
+
+def update_completion_list(uzbl, *args):
+ '''Checks if the user still has a partially completed keyword under his
+ cursor then update the completion hints list.'''
+
+ partial = get_incomplete_keyword(uzbl)[0]
+ if not partial:
+ return stop_completion(uzbl)
+
+ d = get_completion_dict(uzbl)
+ if d['level'] < LIST:
+ return
+
+ hints = [h for h in d['completions'] if h.startswith(partial)]
+ if not hints:
+ return uzbl.set('completion_list')
+
+ j = len(partial)
+ l = [ITEM_FORMAT % (escape(h[:j]), h[j:]) for h in sorted(hints)]
+ uzbl.set('completion_list', LIST_FORMAT % ' '.join(l))
+
+
+def start_completion(uzbl, *args):
+
+ d = get_completion_dict(uzbl)
+ if d['lock']:
+ return
+
+ (partial, set_completion) = get_incomplete_keyword(uzbl)
+ if not partial:
+ return stop_completion(uzbl)
+
+ if d['level'] < COMPLETE:
+ d['level'] += 1
+
+ hints = [h for h in d['completions'] if h.startswith(partial)]
+ if not hints:
+ return
+
+ elif len(hints) == 1:
+ d['lock'] = True
+ complete_completion(uzbl, partial, hints[0], set_completion)
+ d['lock'] = False
+ return
+
+ elif partial in hints and d['level'] == COMPLETE:
+ d['lock'] = True
+ complete_completion(uzbl, partial, partial, set_completion)
+ d['lock'] = False
+ return
+
+ smalllen, smallest = sorted([(len(h), h) for h in hints])[0]
+ common = ''
+ for i in range(len(partial), smalllen):
+ char, same = smallest[i], True
+ for hint in hints:
+ if hint[i] != char:
+ same = False
+ break
+
+ if not same:
+ break
+
+ common += char
+
+ if common:
+ d['lock'] = True
+ partial_completion(uzbl, partial, partial+common)
+ d['lock'] = False
+
+ update_completion_list(uzbl)
+
+
+def add_builtins(uzbl, args):
+ '''Pump the space delimited list of builtin commands into the
+ builtin list.'''
+
+ completions = get_completion_dict(uzbl)['completions']
+ builtins = filter(None, map(unicode.strip, args.split(" ")))
+ for builtin in builtins:
+ if builtin not in completions:
+ completions.append(builtin)
+
+
+def add_config_key(uzbl, key, value):
+ '''Listen on the CONFIG_CHANGED event and add config keys to the variable
+ list for @var<Tab> like expansion support.'''
+
+ completions = get_completion_dict(uzbl)['completions']
+ key = "@%s" % key
+ if key not in completions:
+ completions.append(key)
+
+
+def init(uzbl):
+ # Event handling hooks.
+ uzbl.connect_dict({
+ 'BUILTINS': add_builtins,
+ 'CONFIG_CHANGED': add_config_key,
+ 'INSTANCE_EXIT': del_instance,
+ 'INSTANCE_START': add_instance,
+ 'KEYCMD_CLEARED': stop_completion,
+ 'KEYCMD_EXEC': stop_completion,
+ 'KEYCMD_UPDATE': update_completion_list,
+ 'START_COMPLETION': start_completion,
+ 'STOP_COMPLETION': stop_completion,
+ })
+
+ # Function exports to the uzbl object, `function(uzbl, *args, ..)`
+ # becomes `uzbl.function(*args, ..)`.
+ uzbl.export_dict({
+ 'get_completion_dict': get_completion_dict,
+ 'start_completion': start_completion,
+ })
diff --git a/examples/data/plugins/config.py b/examples/data/plugins/config.py
new file mode 100644
index 0000000..4a848a3
--- /dev/null
+++ b/examples/data/plugins/config.py
@@ -0,0 +1,97 @@
+import re
+import types
+
+__export__ = ['set', 'get_config']
+
+VALIDKEY = re.compile("^[a-zA-Z][a-zA-Z0-9_]*$").match
+TYPECONVERT = {'int': int, 'float': float, 'str': unicode}
+
+UZBLS = {}
+
+
+def escape(value):
+ '''A real escaping function may be required.'''
+
+ return unicode(value)
+
+
+def set(uzbl, key, value='', config=None, force=False):
+ '''Sends a: "set key = value" command to the uzbl instance. If force is
+ False then only send a set command if the values aren't equal.'''
+
+ if type(value) == types.BooleanType:
+ value = int(value)
+
+ else:
+ value = unicode(value)
+
+ if not VALIDKEY(key):
+ raise KeyError("%r" % key)
+
+ value = escape(value)
+ if '\n' in value:
+ value = value.replace("\n", "\\n")
+
+ if not force:
+ if config is None:
+ config = get_config(uzbl)
+
+ if key in config and config[key] == value:
+ return
+
+ uzbl.send('set %s = %s' % (key, value))
+
+
+class ConfigDict(dict):
+ def __init__(self, uzbl):
+ self._uzbl = uzbl
+
+ def __setitem__(self, key, value):
+ '''Makes "config[key] = value" a wrapper for the set function.'''
+
+ set(self._uzbl, key, value, config=self)
+
+
+def add_instance(uzbl, *args):
+ UZBLS[uzbl] = ConfigDict(uzbl)
+
+
+def del_instance(uzbl, *args):
+ if uzbl in UZBLS:
+ del uzbl
+
+
+def get_config(uzbl):
+ if uzbl not in UZBLS:
+ add_instance(uzbl)
+
+ return UZBLS[uzbl]
+
+
+def variable_set(uzbl, args):
+ config = get_config(uzbl)
+
+ key, type, value = list(args.split(' ', 2) + ['',])[:3]
+ old = config[key] if key in config else None
+ value = TYPECONVERT[type](value)
+
+ dict.__setitem__(config, key, value)
+
+ if old != value:
+ uzbl.event("CONFIG_CHANGED", key, value)
+
+
+def init(uzbl):
+ # Event handling hooks.
+ uzbl.connect_dict({
+ 'INSTANCE_EXIT': del_instance,
+ 'INSTANCE_START': add_instance,
+ 'VARIABLE_SET': variable_set,
+ })
+
+ # Function exports to the uzbl object, `function(uzbl, *args, ..)`
+ # becomes `uzbl.function(*args, ..)`.
+ uzbl.export_dict({
+ 'get_config': get_config,
+ 'set': set,
+ })
diff --git a/examples/data/plugins/keycmd.py b/examples/data/plugins/keycmd.py
new file mode 100644
index 0000000..c119077
--- /dev/null
+++ b/examples/data/plugins/keycmd.py
@@ -0,0 +1,571 @@
+import re
+
+# Hold the keylets.
+UZBLS = {}
+
+# Keycmd format which includes the markup for the cursor.
+KEYCMD_FORMAT = "%s<span @cursor_style>%s</span>%s"
+MODCMD_FORMAT = "<span> %s </span>"
+
+
+def escape(str):
+ for char in ['\\', '@']:
+ str = str.replace(char, '\\'+char)
+
+ return str
+
+
+def uzbl_escape(str):
+ return "@[%s]@" % escape(str) if str else ''
+
+
+class Keylet(object):
+ '''Small per-instance object that tracks all the keys held and characters
+ typed.'''
+
+ def __init__(self):
+ # Modcmd tracking
+ self.held = set()
+ self.ignored = set()
+ self.modcmd = ''
+ self.is_modcmd = False
+
+ # Keycmd tracking
+ self.keycmd = ''
+ self.cursor = 0
+
+ self.modmaps = {}
+ self.ignores = {}
+ self.additions = {}
+
+ # Keylet string repr cache.
+ self._repr_cache = None
+
+
+ def get_keycmd(self):
+ '''Get the keycmd-part of the keylet.'''
+
+ return self.keycmd
+
+
+ def get_modcmd(self):
+ '''Get the modcmd-part of the keylet.'''
+
+ if not self.is_modcmd:
+ return ''
+
+ return ''.join(self.held) + self.modcmd
+
+
+ def modmap_key(self, key):
+ '''Make some obscure names for some keys friendlier.'''
+
+ if key in self.modmaps:
+ return self.modmaps[key]
+
+ elif key.endswith('_L') or key.endswith('_R'):
+ # Remove left-right discrimination and try again.
+ return self.modmap_key(key[:-2])
+
+ else:
+ return key
+
+
+ def find_addition(self, modkey):
+ '''Key has just been pressed, check if this key + the held list
+ results in a modkey addition. Return that addition and remove all
+ modkeys that created it.'''
+
+ # Intersection of (held list + modkey) and additions.
+ added = (self.held | set([modkey])) & set(self.additions.keys())
+ for key in added:
+ if key == modkey or modkey in self.additions[key]:
+ self.held -= self.additions[key]
+ return key
+
+ # Held list + ignored list + modkey.
+ modkeys = self.held | self.ignored | set([modkey])
+ for (key, value) in self.additions.items():
+ if modkeys.issuperset(value):
+ self.held -= value
+ return key
+
+ return modkey
+
+
+ def key_ignored(self, key):
+ '''Check if the given key is ignored by any ignore rules.'''
+
+ for (glob, match) in self.ignores.items():
+ if match(key):
+ return True
+
+ return False
+
+
+ def __repr__(self):
+ '''Return a string representation of the keylet.'''
+
+ if self._repr_cache:
+ return self._repr_cache
+
+ l = []
+ if self.is_modcmd:
+ l.append('modcmd=%r' % self.get_modcmd())
+
+ elif self.held:
+ l.append('held=%r' % ''.join(sorted(self.held)))
+
+ if self.keycmd:
+ l.append('keycmd=%r' % self.get_keycmd())
+
+ self._repr_cache = '<Keylet(%s)>' % ', '.join(l)
+ return self._repr_cache
+
+
+def add_modmap(uzbl, key, map):
+ '''Add modmaps.
+
+ Examples:
+ set modmap = request MODMAP
+ @modmap <Control> <Ctrl>
+ @modmap <ISO_Left_Tab> <Shift-Tab>
+ ...
+
+ Then:
+ @bind <Shift-Tab> = <command1>
+ @bind <Ctrl>x = <command2>
+ ...
+
+ '''
+
+ assert len(key)
+ modmaps = get_keylet(uzbl).modmaps
+
+ if key[0] == "<" and key[-1] == ">":
+ key = key[1:-1]
+
+ modmaps[key] = map
+ uzbl.event("NEW_MODMAP", key, map)
+
+
+def modmap_parse(uzbl, map):
+ '''Parse a modmap definiton.'''
+
+ split = [s.strip() for s in map.split(' ') if s.split()]
+
+ if not split or len(split) > 2:
+ raise Exception('Invalid modmap arugments: %r' % map)
+
+ add_modmap(uzbl, *split)
+
+
+def add_key_ignore(uzbl, glob):
+ '''Add an ignore definition.
+
+ Examples:
+ set ignore_key = request IGNORE_KEY
+ @ignore_key <Shift>
+ @ignore_key <ISO_*>
+ ...
+ '''
+
+ assert len(glob) > 1
+ ignores = get_keylet(uzbl).ignores
+
+ glob = "<%s>" % glob.strip("<> ")
+ restr = glob.replace('*', '[^\s]*')
+ match = re.compile(restr).match
+
+ ignores[glob] = match
+ uzbl.event('NEW_KEY_IGNORE', glob)
+
+
+def add_modkey_addition(uzbl, modkeys, result):
+ '''Add a modkey addition definition.
+
+ Examples:
+ set mod_addition = request MODKEY_ADDITION
+ @mod_addition <Shift> <Control> <Meta>
+ @mod_addition <Left> <Up> <Left-Up>
+ @mod_addition <Right> <Up> <Right-Up>
+ ...
+
+ Then:
+ @bind <Right-Up> = <command1>
+ @bind <Meta>o = <command2>
+ ...
+ '''
+
+ additions = get_keylet(uzbl).additions
+ modkeys = set(modkeys)
+
+ assert len(modkeys) and result and result not in modkeys
+
+ for (existing_result, existing_modkeys) in additions.items():
+ if existing_result != result:
+ assert modkeys != existing_modkeys
+
+ additions[result] = modkeys
+ uzbl.event('NEW_MODKEY_ADDITION', modkeys, result)
+
+
+def modkey_addition_parse(uzbl, modkeys):
+ '''Parse modkey addition definition.'''
+
+ keys = filter(None, map(unicode.strip, modkeys.split(" ")))
+ keys = ['<%s>' % key.strip("<>") for key in keys if key.strip("<>")]
+
+ assert len(keys) > 1
+ add_modkey_addition(uzbl, keys[:-1], keys[-1])
+
+
+def add_instance(uzbl, *args):
+ '''Create the Keylet object for this uzbl instance.'''
+
+ UZBLS[uzbl] = Keylet()
+
+
+def del_instance(uzbl, *args):
+ '''Delete the Keylet object for this uzbl instance.'''
+
+ if uzbl in UZBLS:
+ del UZBLS[uzbl]
+
+
+def get_keylet(uzbl):
+ '''Return the corresponding keylet for this uzbl instance.'''
+
+ # Startup events are not correctly captured and sent over the uzbl socket
+ # yet so this line is needed because the INSTANCE_START event is lost.
+ if uzbl not in UZBLS:
+ add_instance(uzbl)
+
+ keylet = UZBLS[uzbl]
+ keylet._repr_cache = False
+ return keylet
+
+
+def clear_keycmd(uzbl):
+ '''Clear the keycmd for this uzbl instance.'''
+
+ k = get_keylet(uzbl)
+ k.keycmd = ''
+ k.cursor = 0
+ k._repr_cache = False
+ uzbl.set('keycmd')
+ uzbl.set('raw_keycmd')
+ uzbl.event('KEYCMD_CLEARED')
+
+
+def clear_modcmd(uzbl, clear_held=False):
+ '''Clear the modcmd for this uzbl instance.'''
+
+ k = get_keylet(uzbl)
+ k.modcmd = ''
+ k.is_modcmd = False
+ k._repr_cache = False
+ if clear_held:
+ k.ignored = set()
+ k.held = set()
+
+ uzbl.set('modcmd')
+ uzbl.set('raw_modcmd')
+ uzbl.event('MODCMD_CLEARED')
+
+
+def clear_current(uzbl):
+ '''Clear the modcmd if is_modcmd else clear keycmd.'''
+
+ k = get_keylet(uzbl)
+ if k.is_modcmd:
+ clear_modcmd(uzbl)
+
+ else:
+ clear_keycmd(uzbl)
+
+
+def focus_changed(uzbl, *args):
+ '''Focus to the uzbl instance has now been lost which means all currently
+ held keys in the held list will not get a KEY_RELEASE event so clear the
+ entire held list.'''
+
+ clear_modcmd(uzbl, clear_held=True)
+
+
+def update_event(uzbl, k, execute=True):
+ '''Raise keycmd & modcmd update events.'''
+
+ config = uzbl.get_config()
+ keycmd, modcmd = k.get_keycmd(), k.get_modcmd()
+
+ if k.is_modcmd:
+ uzbl.event('MODCMD_UPDATE', k)
+
+ else:
+ uzbl.event('KEYCMD_UPDATE', k)
+
+ if 'modcmd_updates' not in config or config['modcmd_updates'] == '1':
+ new_modcmd = k.get_modcmd()
+ if not new_modcmd:
+ uzbl.set('modcmd', config=config)
+ uzbl.set('raw_modcmd', config=config)
+
+ elif new_modcmd == modcmd:
+ uzbl.set('raw_modcmd', escape(modcmd), config=config)
+ uzbl.set('modcmd', MODCMD_FORMAT % uzbl_escape(modcmd),
+ config=config)
+
+ if 'keycmd_events' in config and config['keycmd_events'] != '1':
+ return
+
+ new_keycmd = k.get_keycmd()
+ if not new_keycmd:
+ uzbl.set('keycmd', config=config)
+ uzbl.set('raw_keycmd', config=config)
+
+ elif new_keycmd == keycmd:
+ # Generate the pango markup for the cursor in the keycmd.
+ curchar = keycmd[k.cursor] if k.cursor < len(keycmd) else ' '
+ chunks = [keycmd[:k.cursor], curchar, keycmd[k.cursor+1:]]
+ value = KEYCMD_FORMAT % tuple(map(uzbl_escape, chunks))
+ uzbl.set('keycmd', value, config=config)
+ uzbl.set('raw_keycmd', escape(keycmd), config=config)
+
+
+def inject_str(str, index, inj):
+ '''Inject a string into string at at given index.'''
+
+ return "%s%s%s" % (str[:index], inj, str[index:])
+
+
+def get_keylet_and_key(uzbl, key, add=True):
+ '''Return the keylet and apply any transformations to the key as defined
+ by the modmapping or modkey addition rules. Return None if the key is
+ ignored.'''
+
+ keylet = get_keylet(uzbl)
+ key = keylet.modmap_key(key)
+ if len(key) == 1:
+ return (keylet, key)
+
+ modkey = "<%s>" % key.strip("<>")
+
+ if keylet.key_ignored(modkey):
+ if add:
+ keylet.ignored.add(modkey)
+
+ elif modkey in keylet.ignored:
+ keylet.ignored.remove(modkey)
+
+ modkey = keylet.find_addition(modkey)
+
+ if keylet.key_ignored(modkey):
+ return (keylet, None)
+
+ return (keylet, modkey)
+
+
+def key_press(uzbl, key):
+ '''Handle KEY_PRESS events. Things done by this function include:
+
+ 1. Ignore all shift key presses (shift can be detected by capital chars)
+ 3. In non-modcmd mode:
+ a. append char to keycmd
+ 4. If not in modcmd mode and a modkey was pressed set modcmd mode.
+ 5. If in modcmd mode the pressed key is added to the held keys list.
+ 6. Keycmd is updated and events raised if anything is changed.'''
+
+ (k, key) = get_keylet_and_key(uzbl, key.strip())
+ if not key:
+ return
+
+ if key.lower() == '<space>' and not k.held and k.keycmd:
+ k.keycmd = inject_str(k.keycmd, k.cursor, ' ')
+ k.cursor += 1
+
+ elif not k.held and len(key) == 1:
+ config = uzbl.get_config()
+ if 'keycmd_events' in config and config['keycmd_events'] != '1':
+ k.keycmd = ''
+ k.cursor = 0
+ uzbl.set('keycmd', config=config)
+ uzbl.set('raw_keycmd', config=config)
+ return
+
+ k.keycmd = inject_str(k.keycmd, k.cursor, key)
+ k.cursor += 1
+
+ elif len(key) > 1:
+ k.is_modcmd = True
+ if key not in k.held:
+ k.held.add(key)
+
+ else:
+ k.is_modcmd = True
+ k.modcmd += key
+
+ update_event(uzbl, k)
+
+
+def key_release(uzbl, key):
+ '''Respond to KEY_RELEASE event. Things done by this function include:
+
+ 1. Remove the key from the keylet held list.
+ 2. If in a mod-command then raise a MODCMD_EXEC.
+ 3. Check if any modkey is held, if so set modcmd mode.
+ 4. Update the keycmd uzbl variable if anything changed.'''
+
+ (k, key) = get_keylet_and_key(uzbl, key.strip(), add=False)
+
+ if key in k.held:
+ if k.is_modcmd:
+ uzbl.event('MODCMD_EXEC', k)
+
+ k.held.remove(key)
+ clear_modcmd(uzbl)
+
+
+def set_keycmd(uzbl, keycmd):
+ '''Allow setting of the keycmd externally.'''
+
+ k = get_keylet(uzbl)
+ k.keycmd = keycmd
+ k._repr_cache = None
+ k.cursor = len(keycmd)
+ update_event(uzbl, k, False)
+
+
+def inject_keycmd(uzbl, keycmd):
+ '''Allow injecting of a string into the keycmd at the cursor position.'''
+
+ k = get_keylet(uzbl)
+ k.keycmd = inject_str(k.keycmd, k.cursor, keycmd)
+ k._repr_cache = None
+ k.cursor += len(keycmd)
+ update_event(uzbl, k, False)
+
+
+def append_keycmd(uzbl, keycmd):
+ '''Allow appening of a string to the keycmd.'''
+
+ k = get_keylet(uzbl)
+ k.keycmd += keycmd
+ k._repr_cache = None
+ k.cursor = len(k.keycmd)
+ update_event(uzbl, k, False)
+
+
+def keycmd_strip_word(uzbl, sep):
+ ''' Removes the last word from the keycmd, similar to readline ^W '''
+
+ sep = sep or ' '
+ k = get_keylet(uzbl)
+ if not k.keycmd:
+ return
+
+ head, tail = k.keycmd[:k.cursor].rstrip(sep), k.keycmd[k.cursor:]
+ rfind = head.rfind(sep)
+ head = head[:rfind] if rfind + 1 else ''
+ k.keycmd = head + tail
+ k.cursor = len(head)
+ update_event(uzbl, k, False)
+
+
+def keycmd_backspace(uzbl, *args):
+ '''Removes the character at the cursor position in the keycmd.'''
+
+ k = get_keylet(uzbl)
+ if not k.keycmd:
+ return
+
+ k.keycmd = k.keycmd[:k.cursor-1] + k.keycmd[k.cursor:]
+ k.cursor -= 1
+ update_event(uzbl, k, False)
+
+
+def keycmd_delete(uzbl, *args):
+ '''Removes the character after the cursor position in the keycmd.'''
+
+ k = get_keylet(uzbl)
+ if not k.keycmd:
+ return
+
+ k.keycmd = k.keycmd[:k.cursor] + k.keycmd[k.cursor+1:]
+ update_event(uzbl, k, False)
+
+
+def keycmd_exec_current(uzbl, *args):
+ '''Raise a KEYCMD_EXEC with the current keylet and then clear the
+ keycmd.'''
+
+ k = get_keylet(uzbl)
+ uzbl.event('KEYCMD_EXEC', k)
+ clear_keycmd(uzbl)
+
+
+def set_cursor_pos(uzbl, index):
+ '''Allow setting of the cursor position externally. Supports negative
+ indexing and relative stepping with '+' and '-'.'''
+
+ k = get_keylet(uzbl)
+ if index == '-':
+ cursor = k.cursor - 1
+
+ elif index == '+':
+ cursor = k.cursor + 1
+
+ else:
+ cursor = int(index.strip())
+ if cursor < 0:
+ cursor = len(k.keycmd) + cursor + 1
+
+ if cursor < 0:
+ cursor = 0
+
+ if cursor > len(k.keycmd):
+ cursor = len(k.keycmd)
+
+ k.cursor = cursor
+ update_event(uzbl, k, False)
+
+
+def init(uzbl):
+ '''Connect handlers to uzbl events.'''
+
+ # Event handling hooks.
+ uzbl.connect_dict({
+ 'APPEND_KEYCMD': append_keycmd,
+ 'FOCUS_GAINED': focus_changed,
+ 'FOCUS_LOST': focus_changed,
+ 'IGNORE_KEY': add_key_ignore,
+ 'INJECT_KEYCMD': inject_keycmd,
+ 'INSTANCE_EXIT': del_instance,
+ 'INSTANCE_START': add_instance,
+ 'KEYCMD_BACKSPACE': keycmd_backspace,
+ 'KEYCMD_DELETE': keycmd_delete,
+ 'KEYCMD_EXEC_CURRENT': keycmd_exec_current,
+ 'KEYCMD_STRIP_WORD': keycmd_strip_word,
+ 'KEY_PRESS': key_press,
+ 'KEY_RELEASE': key_release,
+ 'MODKEY_ADDITION': modkey_addition_parse,
+ 'MODMAP': modmap_parse,
+ 'SET_CURSOR_POS': set_cursor_pos,
+ 'SET_KEYCMD': set_keycmd,
+ })
+
+ # Function exports to the uzbl object, `function(uzbl, *args, ..)`
+ # becomes `uzbl.function(*args, ..)`.
+ uzbl.export_dict({
+ 'add_key_ignore': add_key_ignore,
+ 'add_modkey_addition': add_modkey_addition,
+ 'add_modmap': add_modmap,
+ 'append_keycmd': append_keycmd,
+ 'clear_current': clear_current,
+ 'clear_keycmd': clear_keycmd,
+ 'clear_modcmd': clear_modcmd,
+ 'get_keylet': get_keylet,
+ 'inject_keycmd': inject_keycmd,
+ 'set_cursor_pos': set_cursor_pos,
+ 'set_keycmd': set_keycmd,
+ })
diff --git a/examples/data/plugins/mode.py b/examples/data/plugins/mode.py
new file mode 100644
index 0000000..54d865a
--- /dev/null
+++ b/examples/data/plugins/mode.py
@@ -0,0 +1,176 @@
+import sys
+import re
+
+__export__ = ['set_mode', 'get_mode', 'set_mode_config', 'get_mode_config']
+
+UZBLS = {}
+
+DEFAULTS = {
+ 'mode': '',
+ 'modes': {
+ 'insert': {
+ 'forward_keys': True,
+ 'keycmd_events': False,
+ 'modcmd_updates': False,
+ 'mode_indicator': 'I'},
+ 'command': {
+ 'forward_keys': False,
+ 'keycmd_events': True,
+ 'modcmd_updates': True,
+ 'mode_indicator': 'C'}}}
+
+FINDSPACES = re.compile("\s+")
+VALID_KEY = re.compile("^[\w_]+$").match
+
+
+def add_instance(uzbl, *args):
+ UZBLS[uzbl] = dict(DEFAULTS)
+
+
+def del_instance(uzbl, *args):
+ if uzbl in UZBLS:
+ del UZBLS[uzbl]
+
+
+def get_mode_dict(uzbl):
+ '''Return the mode dict for an instance.'''
+
+ if uzbl not in UZBLS:
+ add_instance(uzbl)
+
+ return UZBLS[uzbl]
+
+
+def get_mode_config(uzbl, mode):
+ '''Return the mode config for a given mode.'''
+
+ modes = get_mode_dict(uzbl)['modes']
+ if mode not in modes:
+ modes[mode] = {}
+
+ return modes[mode]
+
+
+def get_mode(uzbl):
+ return get_mode_dict(uzbl)['mode']
+
+
+def mode_changed(uzbl, mode):
+ '''The mode has just been changed, now set the per-mode config.'''
+
+ if get_mode(uzbl) != mode:
+ return
+
+ config = uzbl.get_config()
+ mode_config = get_mode_config(uzbl, mode)
+ for (key, value) in mode_config.items():
+ uzbl.set(key, value, config=config)
+
+ if 'mode_indicator' not in mode_config:
+ config['mode_indicator'] = mode
+
+ uzbl.clear_keycmd()
+ uzbl.clear_modcmd()
+
+
+def set_mode(uzbl, mode=None):
+ '''Set the mode and raise the MODE_CHANGED event if the mode has changed.
+ Fallback on the default mode if no mode argument was given and the default
+ mode is not null.'''
+
+ config = uzbl.get_config()
+ mode_dict = get_mode_dict(uzbl)
+ if mode is None:
+ mode_dict['mode'] = ''
+ if 'default_mode' in config:
+ mode = config['default_mode']
+
+ else:
+ mode = 'command'
+
+ if not VALID_KEY(mode):
+ raise KeyError("invalid mode name: %r" % mode)
+
+ if 'mode' not in config or config['mode'] != mode:
+ config['mode'] = mode
+
+ elif mode_dict['mode'] != mode:
+ mode_dict['mode'] = mode
+ uzbl.event("MODE_CHANGED", mode)
+
+
+def config_changed(uzbl, key, value):
+ '''Check for mode related config changes.'''
+
+ value = None if not value else value
+ if key == 'default_mode':
+ if not get_mode(uzbl):
+ set_mode(uzbl, value)
+
+ elif key == 'mode':
+ set_mode(uzbl, value)
+
+
+def set_mode_config(uzbl, mode, key, value):
+ '''Set mode specific configs. If the mode being modified is the current
+ mode then apply the changes on the go.'''
+
+ assert VALID_KEY(mode) and VALID_KEY(key)
+
+ mode_config = get_mode_config(uzbl, mode)
+ mode_config[key] = value
+
+ if get_mode(uzbl) == mode:
+ uzbl.set(key, value)
+
+
+def mode_config(uzbl, args):
+ '''Parse mode config events.'''
+
+ split = map(unicode.strip, FINDSPACES.split(args.lstrip(), 1))
+ if len(split) != 2:
+ raise SyntaxError('invalid mode config syntax: %r' % args)
+
+ mode, set = split
+ split = map(unicode.strip, set.split('=', 1))
+ if len(split) != 2:
+ raise SyntaxError('invalid set syntax: %r' % args)
+
+ key, value = split
+ set_mode_config(uzbl, mode, key, value)
+
+
+def toggle_modes(uzbl, modes):
+ '''Toggle or cycle between or through a list of modes.'''
+
+ assert len(modes.strip())
+
+ modelist = filter(None, map(unicode.strip, modes.split(' ')))
+ mode = get_mode(uzbl)
+
+ index = 0
+ if mode in modelist:
+ index = (modelist.index(mode)+1) % len(modelist)
+
+ set_mode(uzbl, modelist[index])
+
+
+def init(uzbl):
+ # Event handling hooks.
+ uzbl.connect_dict({
+ 'CONFIG_CHANGED': config_changed,
+ 'INSTANCE_EXIT': del_instance,
+ 'INSTANCE_START': add_instance,
+ 'MODE_CHANGED': mode_changed,
+ 'MODE_CONFIG': mode_config,
+ 'TOGGLE_MODES': toggle_modes,
+ })
+
+ # Function exports to the uzbl object, `function(uzbl, *args, ..)`
+ # becomes `uzbl.function(*args, ..)`.
+ uzbl.export_dict({
+ 'get_mode': get_mode,
+ 'get_mode_config': get_mode_config,
+ 'set_mode': set_mode,
+ 'set_mode_config': set_mode_config,
+ })
diff --git a/examples/data/plugins/on_event.py b/examples/data/plugins/on_event.py
new file mode 100644
index 0000000..b9c504a
--- /dev/null
+++ b/examples/data/plugins/on_event.py
@@ -0,0 +1,107 @@
+'''Plugin provides arbitrary binding of uzbl events to uzbl commands.
+
+Formatting options:
+ %s = space separated string of the arguments
+ %r = escaped and quoted version of %s
+ %1 = argument 1
+ %2 = argument 2
+ %n = argument n
+
+Usage:
+ request ON_EVENT LINK_HOVER set selected_uri = $1
+ --> LINK_HOVER http://uzbl.org/
+ <-- set selected_uri = http://uzbl.org/
+
+ request ON_EVENT CONFIG_CHANGED print Config changed: %1 = %2
+ --> CONFIG_CHANGED selected_uri http://uzbl.org/
+ <-- print Config changed: selected_uri = http://uzbl.org/
+'''
+
+import sys
+import re
+
+__export__ = ['get_on_events', 'on_event']
+
+UZBLS = {}
+
+
+def error(msg):
+ sys.stderr.write('on_event plugin: error: %s\n' % msg)
+
+
+def add_instance(uzbl, *args):
+ UZBLS[uzbl] = {}
+
+
+def del_instance(uzbl, *args):
+ if uzbl in UZBLS:
+ del UZBLS[uzbl]
+
+
+def get_on_events(uzbl):
+ if uzbl not in UZBLS:
+ add_instance(uzbl)
+
+ return UZBLS[uzbl]
+
+
+def event_handler(uzbl, *args, **kargs):
+ '''This function handles all the events being watched by various
+ on_event definitions and responds accordingly.'''
+
+ events = get_on_events(uzbl)
+ event = kargs['on_event']
+ if event not in events:
+ return
+
+ commands = events[event]
+ cmd_expand = uzbl.cmd_expand
+ for cmd in commands:
+ cmd = cmd_expand(cmd, args)
+ uzbl.send(cmd)
+
+
+def on_event(uzbl, event, cmd):
+ '''Add a new event to watch and respond to.'''
+
+ event = event.upper()
+ events = get_on_events(uzbl)
+ if event not in events:
+ uzbl.connect(event, event_handler, on_event=event)
+ events[event] = []
+
+ cmds = events[event]
+ if cmd not in cmds:
+ cmds.append(cmd)
+
+
+def parse_on_event(uzbl, args):
+ '''Parse ON_EVENT events and pass them to the on_event function.
+
+ Syntax: "event ON_EVENT <EVENT_NAME> commands".'''
+
+ if not args:
+ return error("missing on_event arguments")
+
+ split = args.split(' ', 1)
+ if len(split) != 2:
+ return error("invalid ON_EVENT syntax: %r" % args)
+
+ event, cmd = split
+ on_event(uzbl, event, cmd)
+
+
+def init(uzbl):
+ # Event handling hooks.
+ uzbl.connect_dict({
+ 'INSTANCE_EXIT': del_instance,
+ 'INSTANCE_START': add_instance,
+ 'ON_EVENT': parse_on_event,
+ })
+
+ # Function exports to the uzbl object, `function(uzbl, *args, ..)`
+ # becomes `uzbl.function(*args, ..)`.
+ uzbl.export_dict({
+ 'get_on_events': get_on_events,
+ 'on_event': on_event,
+ })
diff --git a/examples/data/plugins/plugin_template.py b/examples/data/plugins/plugin_template.py
new file mode 100644
index 0000000..565a999
--- /dev/null
+++ b/examples/data/plugins/plugin_template.py
@@ -0,0 +1,76 @@
+'''Plugin template.'''
+
+# Holds the per-instance data dict.
+UZBLS = {}
+
+# The default instance dict.
+DEFAULTS = {}
+
+
+def add_instance(uzbl, *args):
+ '''Add a new instance with default config options.'''
+
+ UZBLS[uzbl] = dict(DEFAULTS)
+
+
+def del_instance(uzbl, *args):
+ '''Delete data stored for an instance.'''
+
+ if uzbl in UZBLS:
+ del UZBLS[uzbl]
+
+
+def get_myplugin_dict(uzbl):
+ '''Get data stored for an instance.'''
+
+ if uzbl not in UZBLS:
+ add_instance(uzbl)
+
+ return UZBLS[uzbl]
+
+
+def myplugin_function(uzbl, *args, **kargs):
+ '''Custom plugin function which is exported by the __export__ list at the
+ top of the file for use by other functions/callbacks.'''
+
+ print "My plugin function arguments:", args, kargs
+
+ # Get the per-instance data object.
+ data = get_myplugin_dict(uzbl)
+
+ # Function logic goes here.
+
+
+def myplugin_event_parser(uzbl, args):
+ '''Parses MYPLUGIN_EVENT raised by uzbl or another plugin.'''
+
+ print "Got MYPLUGIN_EVENT with arguments: %r" % args
+
+ # Parsing logic goes here.
+
+
+def init(uzbl):
+ '''The main function of the plugin which is used to attach all the event
+ hooks that are going to be used throughout the plugins life. This function
+ is called each time a UzblInstance() object is created in the event
+ manager.'''
+
+ # Make a dictionary comprising of {"EVENT_NAME": handler, ..} to the event
+ # handler stack:
+ uzbl.connect_dict({
+ # event name function
+ 'INSTANCE_START': add_instance,
+ 'INSTANCE_EXIT': del_instance,
+ 'MYPLUGIN_EVENT': myplugin_event_parser,
+ })
+
+ # Or connect a handler to an event manually and supply additional optional
+ # arguments:
+ #uzbl.connect("MYOTHER_EVENT", myother_event_parser, True, limit=20)
+
+ # Function exports to the uzbl object, `function(uzbl, *args, ..)`
+ # becomes `uzbl.function(*args, ..)`.
+ uzbl.connect_dict({
+ # external name function
+ 'myplugin_function': myplugin_function,
+ })
diff --git a/examples/data/plugins/progress_bar.py b/examples/data/plugins/progress_bar.py
new file mode 100644
index 0000000..89ba175
--- /dev/null
+++ b/examples/data/plugins/progress_bar.py
@@ -0,0 +1,159 @@
+import sys
+
+UZBLS = {}
+
+DEFAULTS = {'width': 8,
+ 'done': '=',
+ 'pending': '.',
+ 'format': '[%d%a%p]%c',
+ 'spinner': '-\\|/',
+ 'sprites': 'loading',
+ 'updates': 0,
+ 'progress': 100}
+
+
+def error(msg):
+ sys.stderr.write("progress_bar plugin: error: %s\n" % msg)
+
+
+def add_instance(uzbl, *args):
+ UZBLS[uzbl] = dict(DEFAULTS)
+
+
+def del_instance(uzbl, *args):
+ if uzbl in UZBLS:
+ del UZBLS[uzbl]
+
+
+def get_progress_config(uzbl):
+ if uzbl not in UZBLS:
+ add_instance(uzbl)
+
+ return UZBLS[uzbl]
+
+
+def update_progress(uzbl, prog=None):
+ '''Updates the progress_format variable on LOAD_PROGRESS update.
+
+ The current substitution options are:
+ %d = done char * done
+ %p = pending char * remaining
+ %c = percent done
+ %i = int done
+ %s = -\|/ spinner
+ %t = percent pending
+ %o = int pending
+ %r = sprites
+ '''
+
+ prog_config = get_progress_config(uzbl)
+ config = uzbl.get_config()
+
+ if prog is None:
+ prog = prog_config['progress']
+
+ else:
+ prog = int(prog)
+ prog_config['progress'] = prog
+
+ prog_config['updates'] += 1
+ format = prog_config['format']
+ width = prog_config['width']
+
+ # Inflate the done and pending bars to stop the progress bar
+ # jumping around.
+ if '%c' in format or '%i' in format:
+ count = format.count('%c') + format.count('%i')
+ width += (3-len(str(prog))) * count
+
+ if '%t' in format or '%o' in format:
+ count = format.count('%t') + format.count('%o')
+ width += (3-len(str(100-prog))) * count
+
+ done = int(((prog/100.0)*width)+0.5)
+ pending = width - done
+
+ if '%d' in format:
+ format = format.replace('%d', prog_config['done']*done)
+
+ if '%p' in format:
+ format = format.replace('%p', prog_config['pending']*pending)
+
+ if '%c' in format:
+ format = format.replace('%c', '%d%%' % prog)
+
+ if '%i' in format:
+ format = format.replace('%i', '%d' % prog)
+
+ if '%t' in format:
+ format = format.replace('%t', '%d%%' % (100-prog))
+
+ if '%o' in format:
+ format = format.replace('%o', '%d' % (100-prog))
+
+ if '%s' in format:
+ spinner = prog_config['spinner']
+ spin = '-' if not spinner else spinner
+ index = 0 if prog == 100 else prog_config['updates'] % len(spin)
+ char = '\\\\' if spin[index] == '\\' else spin[index]
+ format = format.replace('%s', char)
+
+ if '%r' in format:
+ sprites = prog_config['sprites']
+ sprites = '-' if not sprites else sprites
+ index = int(((prog/100.0)*len(sprites))+0.5)-1
+ sprite = '\\\\' if sprites[index] == '\\' else sprites[index]
+ format = format.replace('%r', sprite)
+
+ if 'progress_format' not in config or config['progress_format'] != format:
+ config['progress_format'] = format
+
+
+def progress_config(uzbl, args):
+ '''Parse PROGRESS_CONFIG events from the uzbl instance.
+
+ Syntax: event PROGRESS_CONFIG <key> = <value>
+ '''
+
+ split = args.split('=', 1)
+ if len(split) != 2:
+ return error("invalid syntax: %r" % args)
+
+ key, value = map(unicode.strip, split)
+ prog_config = get_progress_config(uzbl)
+
+ if key not in prog_config:
+ return error("key error: %r" % args)
+
+ if type(prog_config[key]) == type(1):
+ try:
+ value = int(value)
+
+ except:
+ return error("invalid type: %r" % args)
+
+ elif not value:
+ value = ' '
+
+ prog_config[key] = value
+ update_progress(uzbl)
+
+
+def reset_progress(uzbl, args):
+ '''Reset the spinner counter, reset the progress int and re-draw the
+ progress bar on LOAD_COMMIT.'''
+
+ prog_dict = get_progress_config(uzbl)
+ prog_dict['updates'] = prog_dict['progress'] = 0
+ update_progress(uzbl)
+
+
+def init(uzbl):
+ # Event handling hooks.
+ uzbl.connect_dict({
+ 'INSTANCE_EXIT': del_instance,
+ 'INSTANCE_START': add_instance,
+ 'LOAD_COMMIT': reset_progress,
+ 'LOAD_PROGRESS': update_progress,
+ 'PROGRESS_CONFIG': progress_config,
+ })
diff --git a/examples/data/scripts/auth.py b/examples/data/scripts/auth.py
new file mode 100755
index 0000000..4feb90b
--- /dev/null
+++ b/examples/data/scripts/auth.py
@@ -0,0 +1,53 @@
+#!/usr/bin/python
+
+import gtk
+import sys
+
+def responseToDialog(entry, dialog, response):
+ dialog.response(response)
+
+def getText(authInfo, authHost, authRealm):
+ dialog = gtk.MessageDialog(
+ None,
+ gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
+ gtk.MESSAGE_QUESTION,
+ gtk.BUTTONS_OK_CANCEL,
+ None)
+ dialog.set_markup('%s at %s' % (authRealm, authHost))
+
+ login = gtk.Entry()
+ password = gtk.Entry()
+ password.set_visibility(False)
+
+ login.connect("activate", responseToDialog, dialog, gtk.RESPONSE_OK)
+ password.connect("activate", responseToDialog, dialog, gtk.RESPONSE_OK)
+
+ hbox = gtk.HBox();
+
+ vbox_entries = gtk.VBox();
+ vbox_labels = gtk.VBox();
+
+ vbox_labels.pack_start(gtk.Label("Login:"), False, 5, 5)
+ vbox_labels.pack_end(gtk.Label("Password:"), False, 5, 5)
+
+ vbox_entries.pack_start(login)
+ vbox_entries.pack_end(password)
+
+ dialog.format_secondary_markup("Please enter username and password:")
+ hbox.pack_start(vbox_labels, True, True, 0)
+ hbox.pack_end(vbox_entries, True, True, 0)
+
+ dialog.vbox.pack_start(hbox)
+ dialog.show_all()
+ rv = dialog.run()
+
+ output = login.get_text() + "\n" + password.get_text()
+ dialog.destroy()
+ return rv, output
+
+if __name__ == '__main__':
+ rv, output = getText(sys.argv[8], sys.argv[9], sys.argv[10])
+ if (rv == gtk.RESPONSE_OK):
+ print output;
+ else:
+ exit(1)
diff --git a/examples/data/scripts/cookies.sh b/examples/data/scripts/cookies.sh
new file mode 100755
index 0000000..ee2ce51
--- /dev/null
+++ b/examples/data/scripts/cookies.sh
@@ -0,0 +1,154 @@
+#!/bin/sh
+
+set -n;
+
+# THIS IS EXPERIMENTAL AND COULD BE INSECURE !!!!!!
+
+# this is an example bash script of how you could manage your cookies. it is very raw and basic and not as good as uzbl-cookie-daemon
+# we use the cookies.txt format (See http://kb.mozillazine.org/Cookies.txt)
+# This is one textfile with entries like this:
+# kb.mozillazine.org FALSE / FALSE 1146030396 wikiUserID 16993
+# domain alow-read-other-subdomains path http-required expiration name value
+# you probably want your cookies config file in your $XDG_CONFIG_HOME ( eg $HOME/.config/uzbl/cookies)
+# Note. in uzbl there is no strict definition on what a session is. it's YOUR job to clear cookies marked as end_session if you want to keep cookies only valid during a "session"
+# MAYBE TODO: allow user to edit cookie before saving. this cannot be done with zenity :(
+# TODO: different cookie paths per config (eg per group of uzbl instances)
+
+# TODO: correct implementation.
+# see http://curl.haxx.se/rfc/cookie_spec.html
+# http://en.wikipedia.org/wiki/HTTP_cookie
+
+# TODO : check expires= before sending.
+# write sample script that cleans up cookies dir based on expires attribute.
+# TODO: check uri against domain attribute. and path also.
+# implement secure attribute.
+# support blocking or not for 3rd parties
+# http://kb.mozillazine.org/Cookies.txt
+# don't always append cookies, sometimes we need to overwrite
+
+cookie_config=${XDG_CONFIG_HOME:-${HOME}/.config}/uzbl/cookies
+[ "x$cookie_config" = x ] && exit 1
+[ -d "${XDG_DATA_HOME:-${HOME}/.local/share}/uzbl/" ] &&\
+cookie_data=${XDG_DATA_HOME:-${HOME}/.local/share}/uzbl/cookies.txt || exit 1
+
+notifier=
+#notifier=notify-send
+#notify_wrapper () {
+# echo "$@" >> $HOME/cookielog
+#}
+#notifier=notifier_wrapper
+
+# if this variable is set, we will use it to inform you when and which cookies we store, and when/which we send.
+# it's primarily used for debugging
+notifier=
+which zenity &>/dev/null || exit 2
+
+# Example cookie:
+# test_cookie=CheckForPermission; expires=Thu, 07-May-2009 19:17:55 GMT; path=/; domain=.doubleclick.net
+
+# uri=$6
+# uri=${uri/http:\/\/} # strip 'http://' part
+# host=${uri/\/*/}
+action=$8 # GET/PUT
+shift
+host=$9
+shift
+path=$9
+shift
+cookie=$9
+
+field_domain=$host
+field_path=$path
+field_name=
+field_value=
+field_exp='end_session'
+
+notify() {
+ [ -n "$notifier" ] && $notifier "$@"
+}
+
+
+# FOR NOW LETS KEEP IT SIMPLE AND JUST ALWAYS PUT AND ALWAYS GET
+parse_cookie() {
+ IFS=$';'
+ first_pair=1
+ for pair in $cookie
+ do
+ if [ "x$first_pair" = x1 ]
+ then
+ field_name=${pair%%=*}
+ field_value=${pair#*=}
+ first_pair=0
+ else
+ echo "$pair" | read -r pair #strip leading/trailing wite space
+ key=${pair%%=*}
+ val=${pair#*=}
+ [ "$key" == expires ] && field_exp=`date -u -d "$val" +'%s'`
+ # TODO: domain
+ [ "$key" == path ] && field_path=$val
+ fi
+ done
+ unset IFS
+}
+
+# match cookies in cookies.txt against hostname and path
+get_cookie() {
+ path_esc=${path//\//\\/}
+ search="^[^\t]*$host\t[^\t]*\t$path_esc"
+ cookie=`awk "/$search/" $cookie_data 2>/dev/null | tail -n 1`
+ if [ -z "$cookie" ]
+ then
+ notify "Get_cookie: search: $search in $cookie_data -> no result"
+ false
+ else
+ notify "Get_cookie: search: $search in $cookie_data -> result: $cookie"
+ echo "$cookie" | \
+ read domain alow_read_other_subdomains path http_required expiration name \
+ value;
+ cookie="$name=$value"
+ true
+ fi
+}
+
+save_cookie() {
+ if parse_cookie
+ then
+ data="$field_domain\tFALSE\t$field_path\tFALSE\t$field_exp\t$field_name\t$field_value"
+ notify "save_cookie: adding $data to $cookie_data"
+ echo -e "$data" >> $cookie_data
+ else
+ notify "not saving a cookie. since we don't have policies yet, parse_cookie must have returned false. this is a bug"
+ fi
+}
+
+[ "x$action" = xPUT ] && save_cookie
+[ "x$action" = xGET ] && get_cookie && echo "$cookie"
+
+exit
+
+
+# TODO: implement this later.
+# $1 = section (TRUSTED or DENY)
+# $2 =url
+match() {
+ sed -n "/$1/,/^\$/p" $cookie_config 2>/dev/null | grep -q "^$host"
+}
+
+fetch_cookie() {
+ cookie=`cat $cookie_data`
+}
+
+store_cookie() {
+ echo $cookie > $cookie_data
+}
+
+if match TRUSTED $host
+then
+ [ "x$action" = xPUT ] && store_cookie $host
+ [ "x$action" = xGET ] && fetch_cookie && echo "$cookie"
+elif ! match DENY $host
+then
+ [ "x$action" = xPUT ] && cookie=`zenity --entry --title 'Uzbl Cookie handler' --text "Accept this cookie from $host ?" --entry-text="$cookie"` && store_cookie $host
+ [ "x$action" = xGET ] && fetch_cookie && cookie=`zenity --entry --title 'Uzbl Cookie handler' --text "Submit this cookie to $host ?" --entry-text="$cookie"` && echo $cookie
+fi
+exit 0
diff --git a/examples/data/scripts/download.sh b/examples/data/scripts/download.sh
new file mode 100755
index 0000000..1c7d039
--- /dev/null
+++ b/examples/data/scripts/download.sh
@@ -0,0 +1,22 @@
+#!/bin/sh
+# just an example of how you could handle your downloads
+# try some pattern matching on the uri to determine what we should do
+
+# Some sites block the default wget --user-agent..
+GET="wget --user-agent=Firefox"
+
+dest="$HOME"
+url="$8"
+
+http_proxy="$9"
+export http_proxy
+
+test "x$url" = "x" && { echo "you must supply a url! ($url)"; exit 1; }
+
+# only changes the dir for the $get sub process
+if echo "$url" | grep -E '.*\.torrent' >/dev/null;
+then
+ ( cd "$dest"; $GET "$url")
+else
+ ( cd "$dest"; $GET "$url")
+fi
diff --git a/examples/data/scripts/extedit.js b/examples/data/scripts/extedit.js
new file mode 100644
index 0000000..8ed346d
--- /dev/null
+++ b/examples/data/scripts/extedit.js
@@ -0,0 +1,102 @@
+/*
+ * Edit forms in external editor
+ *
+ * (c) 2009, Robert Manea
+ * utf8 functions are (c) by Webtoolkit.info (http://www.webtoolkit.info/)
+ *
+ *
+ * Installation:
+ * - Copy this script to $HOME/.local/share/uzbl/scripts
+ * - Add the following to $HOME/.config/uzbl/config:
+ * @bind E = script @scripts_dir/extedit.js
+ * - Set your preferred editor
+ * set editor = gvim
+ * - non-GUI editors
+ * set editor = xterm -e vim
+ *
+ * Usage:
+ * Select (click) an editable form, go to command mode and hit E
+ *
+*/
+
+
+function utf8_decode ( str_data ) {
+ var tmp_arr = [], i = 0, ac = 0, c1 = 0, c2 = 0, c3 = 0;
+
+ str_data += '';
+
+ while ( i < str_data.length ) {
+ c1 = str_data.charCodeAt(i);
+ if (c1 < 128) {
+ tmp_arr[ac++] = String.fromCharCode(c1);
+ i++;
+ } else if ((c1 > 191) && (c1 < 224)) {
+ c2 = str_data.charCodeAt(i+1);
+ tmp_arr[ac++] = String.fromCharCode(((c1 & 31) << 6) | (c2 & 63));
+ i += 2;
+ } else {
+ c2 = str_data.charCodeAt(i+1);
+ c3 = str_data.charCodeAt(i+2);
+ tmp_arr[ac++] = String.fromCharCode(((c1 & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
+ i += 3;
+ }
+ }
+
+ return tmp_arr.join('');
+}
+
+
+function utf8_encode ( argString ) {
+ var string = (argString+''); // .replace(/\r\n/g, "\n").replace(/\r/g, "\n");
+
+ var utftext = "";
+ var start, end;
+ var stringl = 0;
+
+ start = end = 0;
+ stringl = string.length;
+ for (var n = 0; n < stringl; n++) {
+ var c1 = string.charCodeAt(n);
+ var enc = null;
+
+ if (c1 < 128) {
+ end++;
+ } else if (c1 > 127 && c1 < 2048) {
+ enc = String.fromCharCode((c1 >> 6) | 192) + String.fromCharCode((c1 & 63) | 128);
+ } else {
+ enc = String.fromCharCode((c1 >> 12) | 224) + String.fromCharCode(((c1 >> 6) & 63) | 128) + String.fromCharCode((c1 & 63) | 128);
+ }
+ if (enc !== null) {
+ if (end > start) {
+ utftext += string.substring(start, end);
+ }
+ utftext += enc;
+ start = end = n+1;
+ }
+ }
+
+ if (end > start) {
+ utftext += string.substring(start, string.length);
+ }
+
+ return utftext;
+}
+
+
+(function() {
+ var actelem = document.activeElement;
+
+ if(actelem.type == 'text' || actelem.type == 'textarea') {
+ var editor = Uzbl.run("print @external_editor") || "gvim";
+ var filename = Uzbl.run("print @(mktemp /tmp/uzbl_edit.XXXXXX)@");
+
+ if(actelem.value)
+ Uzbl.run("sh 'echo " + window.btoa(utf8_encode(actelem.value)) + " | base64 -d > " + filename + "'");
+
+ Uzbl.run("sync_sh '" + editor + " " + filename + "'");
+ actelem.value = utf8_decode(window.atob(Uzbl.run("print @(base64 -w 0 " + filename + ")@")));
+
+ Uzbl.run("sh 'rm -f " + filename + "'");
+ }
+
+ })();
diff --git a/examples/data/scripts/follow.js b/examples/data/scripts/follow.js
new file mode 100644
index 0000000..a42447c
--- /dev/null
+++ b/examples/data/scripts/follow.js
@@ -0,0 +1,269 @@
+/* This is the basic linkfollowing script.
+ * Its pretty stable, and you configure which keys to use for hinting
+ *
+ * TODO: Some pages mess around a lot with the zIndex which
+ * lets some hints in the background.
+ * TODO: Some positions are not calculated correctly (mostly
+ * because of uber-fancy-designed-webpages. Basic HTML and CSS
+ * works good
+ * TODO: Still some links can't be followed/unexpected things
+ * happen. Blame some freaky webdesigners ;)
+ */
+
+//Just some shortcuts and globals
+var uzblid = 'uzbl_link_hint';
+var uzbldivid = uzblid + '_div_container';
+var doc = document;
+var win = window;
+var links = document.links;
+var forms = document.forms;
+//Make onlick-links "clickable"
+try {
+ HTMLElement.prototype.click = function() {
+ if (typeof this.onclick == 'function') {
+ this.onclick({
+ type: 'click'
+ });
+ }
+ };
+} catch(e) {}
+//Catch the ESC keypress to stop linkfollowing
+function keyPressHandler(e) {
+ var kC = window.event ? event.keyCode: e.keyCode;
+ var Esc = window.event ? 27 : e.DOM_VK_ESCAPE;
+ if (kC == Esc) {
+ removeAllHints();
+ }
+}
+//Calculate element position to draw the hint
+//Pretty accurate but on fails in some very fancy cases
+function elementPosition(el) {
+ var up = el.offsetTop;
+ var left = el.offsetLeft;
+ var width = el.offsetWidth;
+ var height = el.offsetHeight;
+ while (el.offsetParent) {
+ el = el.offsetParent;
+ up += el.offsetTop;
+ left += el.offsetLeft;
+ }
+ return [up, left, width, height];
+}
+//Calculate if an element is visible
+function isVisible(el) {
+ if (el == doc) {
+ return true;
+ }
+ if (!el) {
+ return false;
+ }
+ if (!el.parentNode) {
+ return false;
+ }
+ if (el.style) {
+ if (el.style.display == 'none') {
+ return false;
+ }
+ if (el.style.visibility == 'hidden') {
+ return false;
+ }
+ }
+ return isVisible(el.parentNode);
+}
+//Calculate if an element is on the viewport.
+function elementInViewport(el) {
+ offset = elementPosition(el);
+ var up = offset[0];
+ var left = offset[1];
+ var width = offset[2];
+ var height = offset[3];
+ return up < window.pageYOffset + window.innerHeight && left < window.pageXOffset + window.innerWidth && (up + height) > window.pageYOffset && (left + width) > window.pageXOffset;
+}
+//Removes all hints/leftovers that might be generated
+//by this script.
+function removeAllHints() {
+ var elements = doc.getElementById(uzbldivid);
+ if (elements) {
+ elements.parentNode.removeChild(elements);
+ }
+}
+//Generate a hint for an element with the given label
+//Here you can play around with the style of the hints!
+function generateHint(el, label) {
+ var pos = elementPosition(el);
+ var hint = doc.createElement('div');
+ hint.setAttribute('name', uzblid);
+ hint.innerText = label;
+ hint.style.display = 'inline';
+ hint.style.backgroundColor = '#B9FF00';
+ hint.style.border = '2px solid #4A6600';
+ hint.style.color = 'black';
+ hint.style.fontSize = '9px';
+ hint.style.fontWeight = 'bold';
+ hint.style.lineHeight = '9px';
+ hint.style.margin = '0px';
+ hint.style.width = 'auto'; // fix broken rendering on w3schools.com
+ hint.style.padding = '1px';
+ hint.style.position = 'absolute';
+ hint.style.zIndex = '1000';
+ // hint.style.textTransform = 'uppercase';
+ hint.style.left = pos[1] + 'px';
+ hint.style.top = pos[0] + 'px';
+ // var img = el.getElementsByTagName('img');
+ // if (img.length > 0) {
+ // hint.style.top = pos[1] + img[0].height / 2 - 6 + 'px';
+ // }
+ hint.style.textDecoration = 'none';
+ // hint.style.webkitBorderRadius = '6px'; // slow
+ // Play around with this, pretty funny things to do :)
+ // hint.style.webkitTransform = 'scale(1) rotate(0deg) translate(-6px,-5px)';
+ return hint;
+}
+//Here we choose what to do with an element if we
+//want to "follow" it. On form elements we "select"
+//or pass the focus, on links we try to perform a click,
+//but at least set the href of the link. (needs some improvements)
+function clickElem(item) {
+ removeAllHints();
+ if (item) {
+ var name = item.tagName;
+ if (name == 'A') {
+ item.click();
+ window.location = item.href;
+ } else if (name == 'INPUT') {
+ var type = item.getAttribute('type').toUpperCase();
+ if (type == 'TEXT' || type == 'FILE' || type == 'PASSWORD') {
+ item.focus();
+ item.select();
+ } else {
+ item.click();
+ }
+ } else if (name == 'TEXTAREA' || name == 'SELECT') {
+ item.focus();
+ item.select();
+ } else {
+ item.click();
+ window.location = item.href;
+ }
+ }
+}
+//Returns a list of all links (in this version
+//just the elements itself, but in other versions, we
+//add the label here.
+function addLinks() {
+ res = [[], []];
+ for (var l = 0; l < links.length; l++) {
+ var li = links[l];
+ if (isVisible(li) && elementInViewport(li)) {
+ res[0].push(li);
+ }
+ }
+ return res;
+}
+//Same as above, just for the form elements
+function addFormElems() {
+ res = [[], []];
+ for (var f = 0; f < forms.length; f++) {
+ for (var e = 0; e < forms[f].elements.length; e++) {
+ var el = forms[f].elements[e];
+ if (el && ['INPUT', 'TEXTAREA', 'SELECT'].indexOf(el.tagName) + 1 && isVisible(el) && elementInViewport(el)) {
+ res[0].push(el);
+ }
+ }
+ }
+ return res;
+}
+//Draw all hints for all elements passed. "len" is for
+//the number of chars we should use to avoid collisions
+function reDrawHints(elems, chars) {
+ removeAllHints();
+ var hintdiv = doc.createElement('div');
+ hintdiv.setAttribute('id', uzbldivid);
+ for (var i = 0; i < elems[0].length; i++) {
+ if (elems[0][i]) {
+ var label = elems[1][i].substring(chars);
+ var h = generateHint(elems[0][i], label);
+ hintdiv.appendChild(h);
+ }
+ }
+ if (document.body) {
+ document.body.appendChild(hintdiv);
+ }
+}
+// pass: number of keys
+// returns: key length
+function labelLength(n) {
+ var oldn = n;
+ var keylen = 0;
+ if(n < 2) {
+ return 1;
+ }
+ n -= 1; // our highest key will be n-1
+ while(n) {
+ keylen += 1;
+ n = Math.floor(n / charset.length);
+ }
+ return keylen;
+}
+// pass: number
+// returns: label
+function intToLabel(n) {
+ var label = '';
+ do {
+ label = charset.charAt(n % charset.length) + label;
+ n = Math.floor(n / charset.length);
+ } while(n);
+ return label;
+}
+// pass: label
+// returns: number
+function labelToInt(label) {
+ var n = 0;
+ var i;
+ for(i = 0; i < label.length; ++i) {
+ n *= charset.length;
+ n += charset.indexOf(label[i]);
+ }
+ return n;
+}
+//Put it all together
+function followLinks(follow) {
+ // if(follow.charAt(0) == 'l') {
+ // follow = follow.substr(1);
+ // charset = 'thsnlrcgfdbmwvz-/';
+ // }
+ var s = follow.split('');
+ var linknr = labelToInt(follow);
+ if (document.body) document.body.setAttribute('onkeyup', 'keyPressHandler(event)');
+ var linkelems = addLinks();
+ var formelems = addFormElems();
+ var elems = [linkelems[0].concat(formelems[0]), linkelems[1].concat(formelems[1])];
+ var len = labelLength(elems[0].length);
+ var oldDiv = doc.getElementById(uzbldivid);
+ var leftover = [[], []];
+ if (s.length == len && linknr < elems[0].length && linknr >= 0) {
+ clickElem(elems[0][linknr]);
+ } else {
+ for (var j = 0; j < elems[0].length; j++) {
+ var b = true;
+ var label = intToLabel(j);
+ var n = label.length;
+ for (n; n < len; n++) {
+ label = charset.charAt(0) + label;
+ }
+ for (var k = 0; k < s.length; k++) {
+ b = b && label.charAt(k) == s[k];
+ }
+ if (b) {
+ leftover[0].push(elems[0][j]);
+ leftover[1].push(label);
+ }
+ }
+ reDrawHints(leftover, s.length);
+ }
+}
+
+//Parse input: first argument is follow keys, second is user input.
+var args = '%s'.split(' ');
+var charset = args[0];
+followLinks(args[1]);
diff --git a/examples/data/scripts/follower.js b/examples/data/scripts/follower.js
new file mode 100644
index 0000000..604b779
--- /dev/null
+++ b/examples/data/scripts/follower.js
@@ -0,0 +1,420 @@
+// A Link Follower for Uzbl.
+// P.C. Shyamshankar <sykora@lucentbeing.com>
+//
+// WARNING: this script depends on the Uzbl object which is now disabled for
+// WARNING security reasons. So the script currently doesn't work but it's
+// WARNING interesting nonetheless
+//
+// Based extensively (like copy-paste) on the follow_numbers.js and
+// linkfollow.js included with uzbl, but modified to be more customizable and
+// extensible.
+//
+// Usage
+// -----
+//
+// First, you'll need to make sure the script is loaded on each page. This can
+// be done with:
+//
+// @on_event LOAD_COMMIT script /path/to/follower.js
+//
+// Then you can bind it to a key:
+//
+// @bind f* = js follower.follow('%s', matchSpec, handler, hintStyler)
+//
+// where matchSpec, handler and hintStyler are parameters which control the
+// operation of follower. If you don't want to customize any further, you can
+// set these to follower.genericMatchSpec, follower.genericHandler and
+// follower.genericHintStyler respectively.
+//
+// For example,
+//
+// @bind f* = js follower.follow('%s', follower.genericMatchSpec, follower.genericHandler, follower.genericHintStyler)
+// @bind F* = js follower.follow('%s', follower.onlyLinksMatchSpec, follower.newPageHandler, follower.newPageHintStyler)
+//
+// In order to make hints disappear when pressing a key (the Escape key, for
+// example), you can do this:
+//
+// @bind <Escape> = js follower.clearHints()
+//
+// If your Escape is already bound to something like command mode, chain it.
+//
+// Alternatively, you can tell your <Escape> key to emit an event, and handle
+// that instead.
+//
+// @bind <Escape> = event ESCAPE
+// @on_event ESCAPE js follower.clearHints()
+//
+// Customization
+// -------------
+//
+// If however you do want to customize, 3 Aspects of the link follower can be
+// customized with minimal pain or alteration to the existing code base:
+//
+// * What elements are hinted.
+// * The style of the hints displayed.
+// * How the hints are handled.
+//
+// In order to customize behavior, write an alternative, and pass that in to
+// follower.follow invocation. You _will_ have to modify this script, but only
+// locally, it beats having to copy the entire script under a new name and
+// modify.
+//
+// TODO:
+// * Whatever all the other TODOs in the file say.
+// * Find out how to do default arguments in Javascript.
+// * Abstract out the hints into a Hint object, make hintables a list of hint
+// objects instead of two lists.
+
+// Helpers
+String.prototype.lpad = function(padding, length) {
+ var padded = this;
+ while (padded.length < length) {
+ padded = padding + padded;
+ }
+
+ return padded;
+}
+
+function Follower() {
+
+ // Globals
+ var uzblID = 'uzbl-follow'; // ID to apply to each hint.
+ var uzblContainerID = 'uzbl-follow-container'; // ID to apply to the div containing hints.
+
+ // Translation table, used to display something other than numbers as hint
+ // labels. Typically set to the ten keys of the home row.
+ //
+ // Must have exactly 10 elements.
+ //
+ // I haven't parameterized this, to make it customizable. Should I? Do
+ // people really use more than one set of keys at a time?
+ var translation = ["a", "r", "s", "t", "d", "h", "n", "e", "i", "o"];
+
+ // MatchSpecs
+ // These are XPath expressions which indicate which elements will be hinted.
+ // Use multiple expressions for different situations, like hinting only form
+ // elements, or only links, etc.
+ //
+ // TODO: Check that these XPath expressions are correct, and optimize/make
+ // them more elegant. Preferably by someone who actually knows XPath, unlike
+ // me.
+
+ // Vimperator default (copy-pasted, I never used vimperator).
+ this.genericMatchSpec = " //*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or @class='lk' or @role='link' or @href] | //input[not(@type='hidden')] | //a | //area | //iframe | //textarea | //button | //select";
+
+ // Matches only links, suitable for opening in a new instance (I think).
+ this.onlyLinksMatchSpec = " //*[@href] | //a | //area";
+
+ // Follow Handlers
+ // These decide how an element should be 'followed'. The handler is passed
+ // the element in question.
+
+ // Generic Handler, opens links in the same instance, emits the FORM_ACTIVE
+ // event if a form element was chosen. Also clears the keycmd.
+ this.genericHandler = function(node) {
+ if (node) {
+ if (window.itemClicker != undefined) {
+ window.itemClicker(node);
+ } else {
+ var tag = node.tagName.toLowerCase();
+ if (tag == 'a') {
+ node.click();
+ window.location = node.href;
+ } else if (tag == 'input') {
+ var inputType = node.getAttribute('type');
+ if (inputType == undefined)
+ inputType = 'text';
+
+ inputType = inputType.toLowerCase();
+
+ if (inputType == 'text' || inputType == 'file' || inputType == 'password') {
+ node.focus();
+ node.select();
+ } else {
+ node.click();
+ }
+ Uzbl.run("event FORM_ACTIVE");
+ } else if (tag == 'textarea'|| tag == 'select') {
+ node.focus();
+ node.select();
+ Uzbl.run("event FORM_ACTIVE");
+ } else {
+ node.click();
+ if ((node.href != undefined) && node.href)
+ window.location = node.href;
+ }
+ }
+ }
+ Uzbl.run("event SET_KEYCMD");
+ }
+
+ // Handler to open links in a new page. The rest is the same as before.
+ this.newPageHandler = function(node) {
+ if (node) {
+ if (window.itemClicker != undefined) {
+ window.itemClicker(node);
+ } else {
+ var tag = node.tagName.toLowerCase();
+ if (tag == 'a') {
+ node.click();
+ Uzbl.run("@new_window " + node.href);
+ } else if (tag == 'input') {
+ var inputType = node.getAttribute('type');
+ if (inputType == undefined)
+ inputType = 'text';
+
+ inputType = inputType.toLowerCase();
+
+ if (inputType == 'text' || inputType == 'file' || inputType == 'password') {
+ node.focus();
+ node.select();
+ } else {
+ node.click();
+ }
+ Uzbl.run("event FORM_ACTIVE");
+ } else if (tag == 'textarea'|| tag == 'select') {
+ node.focus();
+ node.select();
+ Uzbl.run("event FORM_ACTIVE");
+ } else {
+ node.click();
+ if ((node.href != undefined) && node.href)
+ window.location = node.href;
+ }
+ }
+ }
+ Uzbl.run("event SET_KEYCMD");
+ };
+
+ // Hint styling.
+ // Pretty much any attribute of the hint object can be modified here, but it
+ // was meant to change the styling. Useful to differentiate between hints
+ // with different handlers.
+ //
+ // Hint stylers are applied at the end of hint creation, so that they
+ // override the defaults.
+
+ this.genericHintStyler = function(hint) {
+ hint.style.backgroundColor = '#AAAAAA';
+ hint.style.border = '2px solid #4A6600';
+ hint.style.color = 'black';
+ hint.style.fontSize = '10px';
+ hint.style.fontWeight = 'bold';
+ hint.style.lineHeight = '12px';
+ return hint;
+ };
+
+ this.newPageHintStyler = function(hint) {
+ hint.style.backgroundColor = '#FFCC00';
+ hint.style.border = '2px solid #4A6600';
+ hint.style.color = 'black';
+ hint.style.fontSize = '10px';
+ hint.style.fontWeight = 'bold';
+ hint.style.lineHeight = '12px';
+ return hint;
+ };
+
+ // Beyond lies a jungle of pasta and verbosity.
+
+ // Translate a numeric label using the translation table.
+ function translate(digitLabel, translationTable) {
+ translatedLabel = '';
+ for (var i = 0; i < digitLabel.length; i++) {
+ translatedLabel += translationTable[digitLabel.charAt(i)];
+ }
+
+ return translatedLabel;
+ }
+
+ function computeElementPosition(element) {
+ var up = element.offsetTop;
+ var left = element.offsetLeft;
+ var width = element.offsetWidth;
+ var height = element.offsetHeight;
+
+ while (element.offsetParent) {
+ element = element.offsetParent;
+ up += element.offsetTop;
+ left += element.offsetLeft;
+ }
+
+ return {up: up, left: left, width: width, height: height};
+ }
+
+ // Pretty much copy-pasted from every other link following script.
+ function isInViewport(element) {
+ offset = computeElementPosition(element);
+
+ var up = offset.up;
+ var left = offset.left;
+ var width = offset.width;
+ var height = offset.height;
+
+ return up < window.pageYOffset + window.innerHeight &&
+ left < window.pageXOffset + window.innerWidth &&
+ (up + height) > window.pageYOffset &&
+ (left + width) > window.pageXOffset;
+ }
+
+ function isVisible(element) {
+ if (element == document) {
+ return true;
+ }
+
+ if (!element){
+ return false;
+ }
+
+ if (element.style) {
+ if (element.style.display == 'none' || element.style.visibiilty == 'hidden') {
+ return false;
+ }
+ }
+
+ return isVisible(element.parentNode);
+ }
+
+ function generateHintContainer() {
+ var container = document.getElementById(uzblContainerID);
+ if (container) {
+ container.parentNode.removeChild(container);
+ }
+
+ container = document.createElement('div');
+ container.id = uzblContainerID;
+
+ if (document.body) {
+ document.body.appendChild(container);
+ }
+ return container;
+ }
+
+ // Generate everything that is to be hinted, as per the given matchSpec.
+ // hintables[0] refers to the items, hintables[1] to their labels.
+ function generateHintables(matchSpec) {
+ var hintables = [[], []];
+
+ var itemsFromXPath = document.evaluate(matchSpec, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
+
+ for (var i = 0; i < itemsFromXPath.snapshotLength; ++i) {
+ var element = itemsFromXPath.snapshotItem(i);
+ if (element && isVisible(element) && isInViewport(element)) {
+ hintables[0].push(element);
+ }
+ }
+
+ // Assign labels to each hintable. Can't be combined with the previous
+ // step, because we didn't know how many there were at that time.
+ var hintLength = hintables.length;
+ for (var i = 0; i < hintables[0].length; ++i) {
+ var code = translate(i.toString(), translation);
+ hintables[1].push(code.lpad(translation[0], hintLength));
+ }
+
+ return hintables;
+ }
+
+ // Filter the hintables based on input from the user. Makes the screen less
+ // cluttered after the user has typed some prefix of hint labels.
+ function filterHintables(hintables, target) {
+ var filtered = [[], []];
+
+ var targetPattern = new RegExp("^" + target);
+
+ for (var i = 0; i < hintables[0].length; i++) {
+ if (hintables[1][i].match(targetPattern)) {
+ filtered[0].push(hintables[0][i]);
+ filtered[1].push(hintables[1][i].substring(target.length));
+ }
+ }
+
+ return filtered;
+ }
+
+ // TODO make this use the container variable from main, instead of searching
+ // for it?
+ function clearHints() {
+ var container = document.getElementById(uzblContainerID);
+ if (container) {
+ container.parentNode.removeChild(container);
+ }
+ }
+
+ // So that we can offer this as a separate function.
+ this.clearHints = clearHints;
+
+ function makeHint(node, code, styler) {
+ var position = computeElementPosition(node);
+ var hint = document.createElement('div');
+
+ hint.name = uzblID;
+ hint.innerText = code;
+ hint.style.display = 'inline';
+
+ hint.style.margin = '0px';
+ hint.style.padding = '1px';
+ hint.style.position = 'absolute';
+ hint.style.zIndex = '10000';
+
+ hint.style.left = position.left + 'px';
+ hint.style.top = position.up + 'px';
+
+ var img = node.getElementsByTagName('img');
+ if (img.length > 0) {
+ hint.style.left = position.left + img[0].width / 2 + 'px';
+ }
+
+ hint.style.textDecoration = 'none';
+ hint.style.webkitBorderRadius = '6px';
+ hint.style.webkitTransform = 'scale(1) rotate(0deg) translate(-6px, -5px)';
+
+ hint = styler(hint); // So that custom hint stylers can override the above.
+ return hint;
+ }
+
+
+ function drawHints(container, hintables, styler) {
+ for (var i = 0; i < hintables[0].length; i++) {
+ hint = makeHint(hintables[0][i], hintables[1][i], styler);
+ container.appendChild(hint);
+ }
+
+ if (document.body) {
+ document.body.appendChild(container);
+ }
+ }
+
+ // The main hinting function. I don't know how to do default values to
+ // functions, so all arguments must be specified. Use generics if you must.
+ this.follow = function(target, matchSpec, handler, hintStyler) {
+ var container = generateHintContainer(); // Get a container to hold all hints.
+ var allHintables = generateHintables(matchSpec); // Get all items that can be hinted.
+ hintables = filterHintables(allHintables, target); // Filter them based on current input.
+
+ clearHints(); // Clear existing hints, if any.
+
+ if (hintables[0].length == 0) {
+ // Nothing was hinted, user pressed an unknown key, maybe?
+ // Do nothing.
+ } else if (hintables[0].length == 1) {
+ handler(hintables[0][0]); // Only one hint remains, handle it.
+ } else {
+ drawHints(container, hintables, hintStyler); // Draw whatever hints remain.
+ }
+
+ return;
+ };
+}
+
+// Make on-click links clickable.
+try {
+ HTMLElement.prototype.click = function() {
+ if (typeof this.onclick == 'function') {
+ this.onclick({
+ type: 'click'
+ });
+ }
+ };
+} catch(e) {}
+
+follower = new Follower();
diff --git a/examples/data/scripts/formfiller.pl b/examples/data/scripts/formfiller.pl
new file mode 100755
index 0000000..74dcc80
--- /dev/null
+++ b/examples/data/scripts/formfiller.pl
@@ -0,0 +1,99 @@
+#!/usr/bin/perl
+
+# a slightly more advanced form filler
+#
+# uses settings file like: $keydir/<domain>
+#TODO: fallback to $HOME/.local/share
+# user arg 1:
+# edit: force editing of the file (fetches if file is missing)
+# load: fill forms from file (fetches if file is missing)
+# new: fetch new file
+
+# usage example:
+# bind LL = spawn /usr/share/uzbl/examples/data/scripts/formfiller.pl load
+# bind LN = spawn /usr/share/uzbl/examples/data/scripts/formfiller.pl new
+# bind LE = spawn /usr/share/uzbl/examples/data/scripts/formfiller.pl edit
+
+use strict;
+use warnings;
+
+my $keydir = $ENV{XDG_CONFIG_HOME} . "/uzbl/forms";
+my ($config,$pid,$xid,$fifoname,$socket,$url,$title,$cmd) = @ARGV;
+if (!defined $fifoname || $fifoname eq "") { die "No fifo"; }
+
+sub domain {
+ my ($url) = @_;
+ $url =~ s#http(s)?://([A-Za-z0-9\.-]+)(/.*)?#$2#;
+ return $url;
+};
+
+my $editor = "xterm -e vim";
+#my $editor = "gvim";
+
+# ideally, there would be some way to ask uzbl for the html content instead of having to redownload it with
+# Also, you may need to fake the user-agent on some sites (like facebook)
+ my $downloader = "curl -A 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.10) Gecko/2009042810 GranParadiso/3.0.10' ";
+#my $downloader = "curl -s";
+
+my @fields = ("type","name","value");
+
+my %command;
+
+$command{load} = sub {
+ my ($domain) = @_;
+ my $filename = "$keydir/$domain";
+ if (-e $filename){
+ open(my $file, $filename) or die "Failed to open $filename: $!";
+ my (@lines) = <$file>;
+ close($file);
+ $|++;
+ open(my $fifo, ">>", $fifoname) or die "Failed to open $fifoname: $!";
+ foreach my $line (@lines) {
+ next if ($line =~ m/^#/);
+ my ($type,$name,$value) = ($line =~ /^\s*(\w+)\s*\|\s*(.*?)\s*\|\s*(.*?)\s*$/);
+ if ($type eq "checkbox")
+ {
+ printf $fifo 'js document.getElementsByName("%s")[0].checked = %s;', $name, $value;
+ } elsif ($type eq "submit")
+ {
+ printf $fifo 'js function fs (n) {try{n.submit()} catch (e){fs(n.parentNode)}}; fs(document.getElementsByName("%s")[0]);', $name;
+ } elsif ($type ne "")
+ {
+ printf $fifo 'js document.getElementsByName("%s")[0].value = "%s";', $name, $value;
+ }
+ print $fifo "\n";
+ }
+ $|--;
+ } else {
+ $command{new}->($domain);
+ $command{edit}->($domain);
+ }
+};
+$command{edit} = sub {
+ my ($domain) = @_;
+ my $file = "$keydir/$domain";
+ if(-e $file){
+ system ($editor, $file);
+ } else {
+ $command{new}->($domain);
+ }
+};
+$command{new} = sub {
+ my ($domain) = @_;
+ my $filename = "$keydir/$domain";
+ open (my $file,">>", $filename) or die "Failed to open $filename: $!";
+ $|++;
+ print $file "# Make sure that there are no extra submits, since it may trigger the wrong one.\n";
+ printf $file "#%-10s | %-10s | %s\n", @fields;
+ print $file "#------------------------------\n";
+ my @data = `$downloader $url`;
+ foreach my $line (@data){
+ if($line =~ m/<input ([^>].*?)>/i){
+ $line =~ s/.*(<input ([^>].*?)>).*/$1/;
+ printf $file " %-10s | %-10s | %s\n", map { my ($r) = $line =~ /.*$_=["'](.*?)["']/;$r } @fields;
+ };
+ };
+ $|--;
+};
+
+$command{$cmd}->(domain($url));
diff --git a/examples/data/scripts/formfiller.sh b/examples/data/scripts/formfiller.sh
new file mode 100755
index 0000000..10afaba
--- /dev/null
+++ b/examples/data/scripts/formfiller.sh
@@ -0,0 +1,62 @@
+#!/bin/bash
+
+# simple html form (eg for logins) filler (and manager) for uzbl.
+# uses settings files like: $keydir/<domain>
+# files contain lines like: <fieldname>: <value>
+
+
+# user arg 1:
+# edit: force editing the file (falls back to new if not found)
+# new: start with a new file.
+# load: try to load from file into form
+
+# something else (or empty): if file not available: new, otherwise load.
+
+keydir=${XDG_DATA_HOME:-$HOME/.local/share}/uzbl/forms
+[ -d "`dirname $keydir`" ] || exit 1
+[ -d "$keydir" ] || mkdir "$keydir"
+
+editor=${VISUAL}
+if [[ -z ${editor} ]]; then
+ #editor='gvim'
+ editor='urxvt -e vim'
+fi
+
+config=$1; shift
+pid=$1; shift
+xid=$1; shift
+fifo=$1; shift
+socket=$1; shift
+url=$1; shift
+title=$1; shift
+action=$1
+
+[ -d $keydir ] || mkdir $keydir || exit 1
+
+if [ "$action" != 'edit' -a "$action" != 'new' -a "$action" != 'load' ]
+then
+ action=new
+ [[ -e $keydir/$domain ]] && action=load
+elif [ "$action" == 'edit' ] && [[ ! -e $keydir/$domain ]]
+then
+ action=new
+fi
+domain=$(echo $url | sed -re 's|(http\|https)+://([A-Za-z0-9\.]+)/.*|\2|')
+
+
+#regex='s|.*<input.*?name="([[:graph:]]+)".*?/>.*|\1: |p' # sscj's first version, does not work on http://wiki.archlinux.org/index.php?title=Special:UserLogin&returnto=Main_Page
+ regex='s|.*<input.*?name="([^"]*)".*|\1: |p' #works on arch wiki, but not on http://lists.uzbl.org/listinfo.cgi/uzbl-dev-uzbl.org TODO: improve
+
+
+if [ "$action" = 'load' ]
+then
+ [[ -e $keydir/$domain ]] || exit 2
+ gawk -F': ' '{ print "js document.getElementsByName(\"" $1 "\")[0].value = \"" $2 "\";"}' $keydir/$domain >> $fifo
+else
+ if [ "$action" == 'new' ]
+ then
+ curl "$url" | grep '<input' | sed -nre "$regex" > $keydir/$domain
+ fi
+ [[ -e $keydir/$domain ]] || exit 3 #this should never happen, but you never know.
+ $editor $keydir/$domain #TODO: if user aborts save in editor, the file is already overwritten
+fi
diff --git a/examples/data/scripts/hint.js b/examples/data/scripts/hint.js
new file mode 100644
index 0000000..ec7f1e2
--- /dev/null
+++ b/examples/data/scripts/hint.js
@@ -0,0 +1,26 @@
+for (var i=0; i < document.links.length; i++) {
+ var uzblid = 'uzbl_link_hint_';
+ var li = document.links[i];
+ var pre = document.getElementById(uzblid+i);
+
+ if (pre) {
+ li.removeChild(pre);
+ } else {
+ var hint = document.createElement('div');
+ hint.setAttribute('id',uzblid+i);
+ hint.innerHTML = i;
+ hint.style.display='inline';
+ hint.style.lineHeight='90%';
+ hint.style.backgroundColor='red';
+ hint.style.color='white';
+ hint.style.fontSize='small-xx';
+ hint.style.fontWeight='light';
+ hint.style.margin='0px';
+ hint.style.padding='2px';
+ hint.style.position='absolute';
+ hint.style.textDecoration='none';
+ hint.style.left=li.style.left;
+ hint.style.top=li.style.top;
+ li.insertAdjacentElement('afterBegin',hint);
+ }
+}
diff --git a/examples/data/scripts/history.sh b/examples/data/scripts/history.sh
new file mode 100755
index 0000000..7c83aa6
--- /dev/null
+++ b/examples/data/scripts/history.sh
@@ -0,0 +1,5 @@
+#!/bin/sh
+
+file=${XDG_DATA_HOME:-$HOME/.local/share}/uzbl/history
+[ -d `dirname $file` ] || exit 1
+echo `date +'%Y-%m-%d %H:%M:%S'`" $6 $7" >> $file
diff --git a/examples/data/scripts/insert_bookmark.sh b/examples/data/scripts/insert_bookmark.sh
new file mode 100755
index 0000000..c34e7db
--- /dev/null
+++ b/examples/data/scripts/insert_bookmark.sh
@@ -0,0 +1,16 @@
+#!/bin/sh
+
+[ -d "${XDG_DATA_HOME:-$HOME/.local/share}/uzbl" ] || exit 1
+file=${XDG_DATA_HOME:-$HOME/.local/share}/uzbl/bookmarks
+
+which zenity &>/dev/null || exit 2
+
+entry=`zenity --entry --text="Add bookmark. add tags after the '\t', separated by spaces" --entry-text="$6 $7\t"`
+exitstatus=$?
+if [ $exitstatus -ne 0 ]; then exit $exitstatus; fi
+url=`echo $entry | awk '{print $1}'`
+
+# TODO: check if already exists, if so, and tags are different: ask if you want to replace tags
+echo "$entry" >/dev/null #for some reason we need this.. don't ask me why
+echo -e "$entry" >> $file
+true
diff --git a/examples/data/scripts/instance-select-wmii.sh b/examples/data/scripts/instance-select-wmii.sh
new file mode 100755
index 0000000..2bf13ba
--- /dev/null
+++ b/examples/data/scripts/instance-select-wmii.sh
@@ -0,0 +1,54 @@
+#!/bin/sh
+
+
+# This script allows you to focus another uzbl window
+# It considers all uzbl windows in the current tag
+# you can select one from a list, or go to the next/previous one
+# It does not change the layout (stacked/tiled/floating) nor does it
+# changes the size or viewing mode of a uzbl window
+# When your current uzbl window is maximized, the one you change to
+# will be maximized as well.
+# See http://www.uzbl.org/wiki/wmii for more info
+# $1 must be one of 'list', 'next', 'prev'
+
+COLORS=" -nb #303030 -nf khaki -sb #CCFFAA -sf #303030"
+
+if dmenu --help 2>&1 | grep -q '\[-rs\] \[-ni\] \[-nl\] \[-xs\]'
+then
+ DMENU="dmenu -i -xs -rs -l 10" # vertical patch
+else
+ DMENU="dmenu -i"
+fi
+
+if [ "$1" == 'list' ]
+then
+ list=
+ # get window id's of uzbl clients. we could also get the label in one shot but it's pretty tricky
+ for i in $(wmiir read /tag/sel/index | grep uzbl |cut -d ' ' -f2)
+ do
+ label=$(wmiir read /client/$i/label)
+ list="$list$i : $label\n"
+ done
+ window=$(echo -e "$list" | $DMENU $COLORS | cut -d ' ' -f1)
+ wmiir xwrite /tag/sel/ctl "select client $window"
+elif [ "$1" == 'next' ]
+then
+ current=$(wmiir read /client/sel/ctl | head -n 1)
+ # find the next uzbl window and focus it
+ next=$(wmiir read /tag/sel/index | grep -A 10000 " $current " | grep -m 1 uzbl | cut -d ' ' -f2)
+ if [ x"$next" != "x" ]
+ then
+ wmiir xwrite /tag/sel/ctl "select client $next"
+ fi
+elif [ "$1" == 'prev' ]
+then
+ current=$(wmiir read /client/sel/ctl | head -n 1)
+ prev=$(wmiir read /tag/sel/index | grep -B 10000 " $current " | tac | grep -m 1 uzbl | cut -d ' ' -f2)
+ if [ x"$prev" != "x" ]
+ then
+ wmiir xwrite /tag/sel/ctl "select client $prev"
+ fi
+else
+ echo "\$1 not valid" >&2
+ exit 2
+fi
diff --git a/examples/data/scripts/linkfollow.js b/examples/data/scripts/linkfollow.js
new file mode 100644
index 0000000..3109cda
--- /dev/null
+++ b/examples/data/scripts/linkfollow.js
@@ -0,0 +1,269 @@
+// link follower for uzbl
+// requires http://github.com/DuClare/uzbl/commit/6c11777067bdb8aac09bba78d54caea04f85e059
+//
+// first, it needs to be loaded before every time it is used.
+// One way would be to use the load_commit_handler:
+// set load_commit_handler = sh 'echo "script /usr/share/uzbl/examples/data/scripts/linkfollow.js" > "$4"'
+//
+// when script is loaded, it can be invoked with
+// bind f* = js hints.set("%s", hints.open)
+// bind f_ = js hints.follow("%s",hints.open)
+//
+// At the moment, it may be useful to have way of forcing uzbl to load the script
+// bind :lf = script /usr/share/uzbl/examples/data/scripts/linkfollow.js
+//
+// The default style for the hints are pretty ugly, so it is recommended to add the following
+// to config file
+// set stylesheet_uri = /usr/share/uzbl/examples/data/style.css
+//
+// based on follow_Numbers.js
+//
+// TODO: fix styling for the first element
+// TODO: emulate mouseover events when visiting some elements
+// TODO: rewrite the element->action handling
+
+
+function Hints(){
+
+ // Settings
+ ////////////////////////////////////////////////////////////////////////////
+
+ // if set to true, you must explicitly call hints.follow(), otherwise it will
+ // follow the link if there is only one matching result
+ var requireReturn = true;
+
+ // Case sensitivity flag
+ var matchCase = "i";
+
+ // For case sensitive matching, uncomment:
+ // var matchCase = "";
+
+
+ var uzblid = 'uzbl_hint';
+ var uzblclass = 'uzbl_highlight';
+ var uzblclassfirst = 'uzbl_h_first';
+ var doc = document;
+ var visible = [];
+ var hintdiv;
+
+ this.set = hint;
+ this.follow = follow;
+ this.keyPressHandler = keyPressHandler;
+
+ function elementPosition(el) {
+ var up = el.offsetTop;
+ var left = el.offsetLeft; var width = el.offsetWidth;
+ var height = el.offsetHeight;
+
+ while (el.offsetParent) {
+ el = el.offsetParent;
+ up += el.offsetTop;
+ left += el.offsetLeft;
+ }
+ return {up: up, left: left, width: width, height: height};
+ }
+
+ function elementInViewport(p) {
+ return (p.up < window.pageYOffset + window.innerHeight &&
+ p.left < window.pageXOffset + window.innerWidth &&
+ (p.up + p.height) > window.pageYOffset &&
+ (p.left + p.width) > window.pageXOffset);
+ }
+
+ function isVisible(el) {
+ if (el == doc) { return true; }
+ if (!el) { return false; }
+ if (!el.parentNode) { return false; }
+ if (el.style) {
+ if (el.style.display == 'none') {
+ return false;
+ }
+ if (el.style.visibility == 'hidden') {
+ return false;
+ }
+ }
+ return isVisible(el.parentNode);
+ }
+
+ // the vimperator defaults minus the xhtml elements, since it gave DOM errors
+ var hintable = " //*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or @class='lk' or @role='link' or @href] | //input[not(@type='hidden')] | //a | //area | //iframe | //textarea | //button | //select";
+
+ function Matcher(str){
+ var numbers = str.replace(/[^\d]/g,"");
+ var words = str.replace(/\d/g,"").split(/\s+/).map(function (n) { return new RegExp(n,matchCase)});
+ this.test = test;
+ this.toString = toString;
+ this.numbers = numbers;
+ function matchAgainst(element){
+ if(element.node.nodeName == "INPUT"){
+ return element.node.value;
+ } else {
+ return element.node.textContent;
+ }
+ }
+ function test(element) {
+ // test all the regexp
+ var item = matchAgainst(element);
+ return words.every(function (regex) { return item.match(regex)});
+ }
+ }
+
+ function HintElement(node,pos){
+
+ this.node = node;
+ this.isHinted = false;
+ this.position = pos;
+ this.num = 0;
+
+ this.addHint = function (labelNum) {
+ // TODO: fix uzblclassfirst
+ if(!this.isHinted){
+ this.node.className += " " + uzblclass;
+ }
+ this.isHinted = true;
+
+ // create hint
+ var hintNode = doc.createElement('div');
+ hintNode.name = uzblid;
+ hintNode.innerText = labelNum;
+ hintNode.style.left = this.position.left + 'px';
+ hintNode.style.top = this.position.up + 'px';
+ hintNode.style.position = "absolute";
+ doc.body.firstChild.appendChild(hintNode);
+
+ }
+ this.removeHint = function(){
+ if(this.isHinted){
+ var s = (this.num)?uzblclassfirst:uzblclass;
+ this.node.className = this.node.className.replace(new RegExp(" "+s,"g"),"");
+ this.isHinted = false;
+ }
+ }
+ }
+
+ function createHintDiv(){
+ var hintdiv = doc.getElementById(uzblid);
+ if(hintdiv){
+ hintdiv.parentNode.removeChild(hintdiv);
+ }
+ hintdiv = doc.createElement("div");
+ hintdiv.setAttribute('id',uzblid);
+ doc.body.insertBefore(hintdiv,doc.body.firstChild);
+ return hintdiv;
+ }
+
+ function init(){
+ // WHAT?
+ doc.body.setAttribute("onkeyup","hints.keyPressHandler(event)");
+ hintdiv = createHintDiv();
+ visible = [];
+
+ var items = doc.evaluate(hintable,doc,null,XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,null);
+ for (var i = 0;i<items.snapshotLength;i++){
+ var item = items.snapshotItem(i);
+ var pos = elementPosition(item);
+ if(isVisible && elementInViewport(elementPosition(item))){
+ visible.push(new HintElement(item,pos));
+ }
+ }
+ }
+
+ function clear(){
+
+ visible.forEach(function (n) { n.removeHint(); } );
+ hintdiv = doc.getElementById(uzblid);
+ while(hintdiv){
+ hintdiv.parentNode.removeChild(hintdiv);
+ hintdiv = doc.getElementById(uzblid);
+ }
+ }
+
+ function update(str,openFun) {
+ var match = new Matcher(str);
+ hintdiv = createHintDiv();
+ var i = 1;
+ visible.forEach(function (n) {
+ if(match.test(n)) {
+ n.addHint(i);
+ i++;
+ } else {
+ n.removeHint();
+ }});
+ if(!requireReturn){
+ if(i==2){ //only been incremented once
+ follow(str,openFun);
+ }
+ }
+ }
+
+ function hint(str,openFun){
+ if(str.length == 0) init();
+ update(str,openFun);
+ }
+
+ function keyPressHandler(e) {
+ var kC = window.event ? event.keyCode: e.keyCode;
+ var Esc = window.event ? 27 : e.DOM_VK_ESCAPE;
+ if (kC == Esc) {
+ clear();
+ doc.body.removeAttribute("onkeyup");
+ }
+ }
+
+ this.openNewWindow = function(item){
+ // TODO: this doesn't work yet
+ item.className += " uzbl_follow";
+ window.open(item.href,"uzblnew","");
+ }
+ this.open = function(item){
+ simulateMouseOver(item);
+ item.className += " uzbl_follow";
+ window.location = item.href;
+ }
+
+ function simulateMouseOver(item){
+ var evt = doc.createEvent("MouseEvents");
+ evt.initMouseEvent("MouseOver",true,true,
+ doc.defaultView,1,0,0,0,0,
+ false,false,false,false,0,null);
+ return item.dispatchEvent(evt);
+ }
+
+
+ function follow(str,openFunction){
+ var m = new Matcher(str);
+ var items = visible.filter(function (n) { return n.isHinted });
+ clear();
+ var num = parseInt(m.numbers,10);
+ if(num){
+ var item = items[num-1].node;
+ } else {
+ var item = items[0].node;
+ }
+ if (item) {
+ var name = item.tagName;
+ if (name == 'A') {
+ if(item.click) {item.click()};
+ openFunction(item);
+ } else if (name == 'INPUT') {
+ var type = item.getAttribute('type').toUpperCase();
+ if (type == 'TEXT' || type == 'FILE' || type == 'PASSWORD') {
+ item.focus();
+ item.select();
+ } else {
+ item.click();
+ }
+ } else if (name == 'TEXTAREA' || name == 'SELECT') {
+ item.focus();
+ item.select();
+ } else {
+ item.click();
+ openFunction(item);
+ }
+ }
+ }
+}
+
+var hints = new Hints();
+
+// vim:set et sw=2:
diff --git a/examples/data/scripts/load_url_from_bookmarks.sh b/examples/data/scripts/load_url_from_bookmarks.sh
new file mode 100755
index 0000000..1e9f9e7
--- /dev/null
+++ b/examples/data/scripts/load_url_from_bookmarks.sh
@@ -0,0 +1,20 @@
+#!/bin/bash
+
+#NOTE: it's the job of the script that inserts bookmarks to make sure there are no dupes.
+
+file=${XDG_DATA_HOME:-$HOME/.local/share}/uzbl/bookmarks
+[ -r "$file" ] || exit
+COLORS=" -nb #303030 -nf khaki -sb #CCFFAA -sf #303030"
+if dmenu --help 2>&1 | grep -q '\[-rs\] \[-ni\] \[-nl\] \[-xs\]'
+then
+ DMENU="dmenu -i -xs -rs -l 10" # vertical patch
+ # show tags as well
+ goto=`$DMENU $COLORS < $file | awk '{print $1}'`
+else
+ DMENU="dmenu -i"
+ # because they are all after each other, just show the url, not their tags.
+ goto=`awk '{print $1}' $file | $DMENU $COLORS`
+fi
+
+#[ -n "$goto" ] && echo "uri $goto" > $4
+[ -n "$goto" ] && echo "uri $goto" | socat - unix-connect:$5
diff --git a/examples/data/scripts/load_url_from_history.sh b/examples/data/scripts/load_url_from_history.sh
new file mode 100755
index 0000000..62e02ac
--- /dev/null
+++ b/examples/data/scripts/load_url_from_history.sh
@@ -0,0 +1,24 @@
+#!/bin/sh
+
+history_file=${XDG_DATA_HOME:-$HOME/.local/share}/uzbl/history
+[ -r "$history_file" ] || exit 1
+
+# choose from all entries, sorted and uniqued
+# goto=`awk '{print $3}' $history_file | sort -u | dmenu -i`
+COLORS=" -nb #303030 -nf khaki -sb #CCFFAA -sf #303030"
+if dmenu --help 2>&1 | grep -q '\[-rs\] \[-ni\] \[-nl\] \[-xs\]';
+then
+ DMENU="dmenu -i -xs -rs -l 10" # vertical patch
+ # choose an item in reverse order, showing also the date and page titles
+ # pick the last field from the first 3 fields. this way you can pick a url (prefixed with date & time) or type just a new url.
+ goto=`tac $history_file | $DMENU $COLORS | cut -d ' ' -f -3 | awk '{print $NF}'`
+else
+ DMENU="dmenu -i"
+ # choose from all entries (no date or title), the first one being current url, and after that all others, sorted and uniqued, in ascending order
+ current=`tail -n 1 $history_file | awk '{print $3}'`;
+ goto=`(echo $current; awk '{print $3}' $history_file | grep -v "^$current\$" \
+ | sort -u) | $DMENU $COLORS`
+fi
+
+[ -n "$goto" ] && echo "uri $goto" > $4
+#[ -n "$goto" ] && echo "uri $goto" | socat - unix-connect:$5
diff --git a/examples/data/scripts/scheme.py b/examples/data/scripts/scheme.py
new file mode 100755
index 0000000..0916466
--- /dev/null
+++ b/examples/data/scripts/scheme.py
@@ -0,0 +1,24 @@
+#!/usr/bin/env python
+
+import os, subprocess, sys, urlparse
+
+def detach_open(cmd):
+ # Thanks to the vast knowledge of Laurence Withers (lwithers) and this message:
+ # http://mail.python.org/pipermail/python-list/2006-November/587523.html
+ if not os.fork():
+ null = os.open(os.devnull,os.O_WRONLY)
+ for i in range(3): os.dup2(null,i)
+ os.close(null)
+ subprocess.Popen(cmd)
+ print 'USED'
+
+if __name__ == '__main__':
+ uri = sys.argv[8]
+ u = urlparse.urlparse(uri)
+ if u.scheme == 'mailto':
+ detach_open(['xterm', '-e', 'mail', u.path])
+ elif u.scheme == 'xmpp':
+ # Someone check for safe arguments to gajim-remote
+ detach_open(['gajim-remote', 'open_chat', uri])
+ elif u.scheme == 'git':
+ detach_open(['git', 'clone', '--', uri], cwd=os.path.expanduser('~/src'))
diff --git a/examples/data/scripts/scroll-percentage.js b/examples/data/scripts/scroll-percentage.js
new file mode 100644
index 0000000..c9a51aa
--- /dev/null
+++ b/examples/data/scripts/scroll-percentage.js
@@ -0,0 +1,68 @@
+// VIM ruler style scroll message
+(function() {
+ var run = Uzbl.run;
+ var update_message = function() {
+ var innerHeight = window.innerHeight;
+ var scrollY = window.scrollY;
+ var height = document.height;
+ var message;
+
+ if (UzblZoom.type === "full") {
+ var zoom_level = UzblZoom.level;
+ innerHeight = Math.ceil(innerHeight * zoom_level);
+ scrollY = Math.ceil(scrollY * zoom_level);
+ height -= 1;
+ }
+
+ if (! height) {
+ message = "";
+ }
+ else if (height <= innerHeight) {
+ message = run("print @scroll_all_indicator") || "All";
+ }
+ else if (scrollY === 0) {
+ message = run("print @scroll_top_indicator") || "Top";
+ }
+ else if (scrollY + innerHeight >= height) {
+ message = run("print @scroll_bottom_indicator") || "Bot";
+ }
+ else {
+ var percentage = Math.round(scrollY / (height - innerHeight) * 100);
+ message = percentage + "%";
+ }
+ run("set scroll_message=" + message);
+ };
+
+ self.UzblZoom = {
+ get level() {
+ return Number(run("print @zoom_level")) || 1;
+ },
+ set level(level) {
+ if (typeof level === "number" && level > 0) {
+ run("set zoom_level = " + level);
+ update_message();
+ }
+ },
+ get type() {
+ return run("print @zoom_type") || "text";
+ },
+ set type(type) {
+ if ((type === "text" || type === "full") && this.type != type) {
+ run("toggle_zoom_type");
+ run("set zoom_type = " + type);
+ update_message();
+ }
+ },
+ toggle_type: function() {
+ this.type = (this.type === "text" ? "full" : "text");
+ }
+ };
+
+ window.addEventListener("DOMContentLoaded", update_message, false);
+ window.addEventListener("load", update_message, false);
+ window.addEventListener("resize", update_message, false);
+ window.addEventListener("scroll", update_message, false);
+ update_message();
+})();
+
+// vim: set noet ff=unix
diff --git a/examples/data/scripts/session.sh b/examples/data/scripts/session.sh
new file mode 100755
index 0000000..1059b5e
--- /dev/null
+++ b/examples/data/scripts/session.sh
@@ -0,0 +1,62 @@
+#!/bin/sh
+
+# Very simple session manager for uzbl-browser. When called with "endsession" as the
+# argument, it'll backup $sessionfile, look for fifos in $fifodir and
+# instruct each of them to store their current url in $sessionfile and
+# terminate themselves. Run with "launch" as the argument and an instance of
+# uzbl-browser will be launched for each stored url. "endinstance" is used internally
+# and doesn't need to be called manually at any point.
+# Add a line like 'bind quit = /path/to/session.sh endsession' to your config
+
+[ -d ${XDG_DATA_HOME:-$HOME/.local/share}/uzbl ] || exit 1
+scriptfile=$0 # this script
+sessionfile=${XDG_DATA_HOME:-$HOME/.local/share}/uzbl/browser-session # the file in which the "session" (i.e. urls) are stored
+configfile=${XDG_DATA_HOME:-$HOME/.local/share}/uzbl/config # uzbl configuration file
+UZBL="uzbl-browser -c $configfile" # add custom flags and whatever here.
+
+fifodir=/tmp # remember to change this if you instructed uzbl to put its fifos elsewhere
+thisfifo="$4"
+act="$8"
+url="$6"
+
+if [ "$act." = "." ]; then
+ act="$1"
+fi
+
+
+case $act in
+ "launch" )
+ urls=`cat $sessionfile`
+ if [ "$urls." = "." ]; then
+ $UZBL
+ else
+ for url in $urls; do
+ $UZBL --uri "$url" &
+ done
+ fi
+ exit 0
+ ;;
+
+ "endinstance" )
+ if [ "$url" != "(null)" ]; then
+ echo "$url" >> $sessionfile;
+ fi
+ echo "exit" > "$thisfifo"
+ ;;
+
+ "endsession" )
+ mv "$sessionfile" "$sessionfile~"
+ for fifo in $fifodir/uzbl_fifo_*; do
+ if [ "$fifo" != "$thisfifo" ]; then
+ echo "spawn $scriptfile endinstance" > "$fifo"
+ fi
+ done
+ echo "spawn $scriptfile endinstance" > "$thisfifo"
+ ;;
+
+ * ) echo "session manager: bad action"
+ echo "Usage: $scriptfile [COMMAND] where commands are:"
+ echo " launch - Restore a saved session or start a new one"
+ echo " endsession - Quit the running session. Must be called from uzbl"
+ ;;
+esac
diff --git a/examples/data/scripts/uzbl-cookie-daemon b/examples/data/scripts/uzbl-cookie-daemon
new file mode 100755
index 0000000..fde8b8e
--- /dev/null
+++ b/examples/data/scripts/uzbl-cookie-daemon
@@ -0,0 +1,664 @@
+#!/usr/bin/env python
+
+# The Python Cookie Daemon for Uzbl.
+# Copyright (c) 2009, Tom Adams <tom@holizz.com>
+# Copyright (c) 2009, Dieter Plaetinck <dieter@plaetinck.be>
+# Copyright (c) 2009, Mason Larobina <mason.larobina@gmail.com>
+# Copyright (c) 2009, Michael Fiano <axionix@gmail.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+'''
+The Python Cookie Daemon
+========================
+
+This daemon is a re-write of the original cookies.py script found in uzbl's
+master branch. This script provides more functionality than the original
+cookies.py by adding numerous command line options to specify different cookie
+jar locations, socket locations, verbose output, etc. This functionality is
+very useful as it allows you to run multiple daemons at once serving cookies
+to different groups of uzbl instances as required.
+
+Keeping up to date
+==================
+
+Check the cookie daemon uzbl-wiki page for more information on where to
+find the latest version of the cookie_daemon.py
+
+ http://www.uzbl.org/wiki/cookie_daemon.py
+
+Command line options
+====================
+
+Use the following command to get a full list of the cookie_daemon.py command
+line options:
+
+ ./cookie_daemon.py --help
+
+Talking with uzbl
+=================
+
+In order to get uzbl to talk to a running cookie daemon you add the following
+to your uzbl config:
+
+ set cookie_handler = talk_to_socket $XDG_CACHE_HOME/uzbl/cookie_daemon_socket
+
+Or if you prefer using the $HOME variable:
+
+ set cookie_handler = talk_to_socket $HOME/.cache/uzbl/cookie_daemon_socket
+
+Todo list
+=========
+
+ - Use a pid file to make force killing a running daemon possible.
+
+Reporting bugs / getting help
+=============================
+
+The best way to report bugs and or get help with the cookie daemon is to
+contact the maintainers it the #uzbl irc channel found on the Freenode IRC
+network (irc.freenode.org).
+'''
+
+import cookielib
+import os
+import sys
+import urllib2
+import select
+import socket
+import time
+import atexit
+from traceback import print_exc
+from signal import signal, SIGTERM
+from optparse import OptionParser
+from os.path import join
+
+try:
+ import cStringIO as StringIO
+
+except ImportError:
+ import StringIO
+
+
+# ============================================================================
+# ::: Default configuration section ::::::::::::::::::::::::::::::::::::::::::
+# ============================================================================
+
+def xdghome(key, default):
+ '''Attempts to use the environ XDG_*_HOME paths if they exist otherwise
+ use $HOME and the default path.'''
+
+ xdgkey = "XDG_%s_HOME" % key
+ if xdgkey in os.environ.keys() and os.environ[xdgkey]:
+ return os.environ[xdgkey]
+
+ return join(os.environ['HOME'], default)
+
+# Setup xdg paths.
+CACHE_DIR = join(xdghome('CACHE', '.cache/'), 'uzbl/')
+DATA_DIR = join(xdghome('DATA', '.local/share/'), 'uzbl/')
+CONFIG_DIR = join(xdghome('CONFIG', '.config/'), 'uzbl/')
+
+# Ensure data paths exist.
+for path in [CACHE_DIR, DATA_DIR, CONFIG_DIR]:
+ if not os.path.exists(path):
+ os.makedirs(path)
+
+# Default config
+config = {
+
+ # Default cookie jar, whitelist, and daemon socket locations.
+ 'cookie_jar': join(DATA_DIR, 'cookies.txt'),
+ 'cookie_whitelist': join(CONFIG_DIR, 'cookie_whitelist'),
+ 'cookie_socket': join(CACHE_DIR, 'cookie_daemon_socket'),
+
+ # Don't use a cookie whitelist policy by default.
+ 'use_whitelist': False,
+
+ # Time out after x seconds of inactivity (set to 0 for never time out).
+ # WARNING: Do not use this option if you are manually launching the daemon.
+ 'daemon_timeout': 0,
+
+ # Daemonise by default.
+ 'daemon_mode': True,
+
+ # Optionally print helpful debugging messages to the terminal.
+ 'verbose': False,
+
+} # End of config dictionary.
+
+
+# ============================================================================
+# ::: End of configuration section :::::::::::::::::::::::::::::::::::::::::::
+# ============================================================================
+
+
+_SCRIPTNAME = os.path.basename(sys.argv[0])
+def echo(msg):
+ '''Prints only if the verbose flag has been set.'''
+
+ if config['verbose']:
+ sys.stderr.write("%s: %s\n" % (_SCRIPTNAME, msg))
+
+
+def error(msg):
+ '''Prints error message and exits.'''
+
+ sys.stderr.write("%s: error: %s\n" % (_SCRIPTNAME, msg))
+ sys.exit(1)
+
+
+def mkbasedir(filepath):
+ '''Create the base directories of the file in the file-path if the dirs
+ don't exist.'''
+
+ dirname = os.path.dirname(filepath)
+ if not os.path.exists(dirname):
+ echo("creating dirs: %r" % dirname)
+ os.makedirs(dirname)
+
+
+def daemon_running(cookie_socket):
+ '''Check if another process (hopefully a cookie_daemon.py) is listening
+ on the cookie daemon socket. If another process is found to be
+ listening on the socket exit the daemon immediately and leave the
+ socket alone. If the connect fails assume the socket has been abandoned
+ and delete it (to be re-created in the create socket function).'''
+
+ if not os.path.exists(cookie_socket):
+ return False
+
+ if os.path.isfile(cookie_socket):
+ raise Exception("regular file at %r is not a socket" % cookie_socket)
+
+
+ if os.path.isdir(cookie_socket):
+ raise Exception("directory at %r is not a socket" % cookie_socket)
+
+ try:
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_SEQPACKET)
+ sock.connect(cookie_socket)
+ sock.close()
+ echo("detected daemon listening on %r" % cookie_socket)
+ return True
+
+ except socket.error:
+ # Failed to connect to cookie_socket so assume it has been
+ # abandoned by another cookie daemon process.
+ if os.path.exists(cookie_socket):
+ echo("deleting abandoned socket at %r" % cookie_socket)
+ os.remove(cookie_socket)
+
+ return False
+
+
+def send_command(cookie_socket, cmd):
+ '''Send a command to a running cookie daemon.'''
+
+ if not daemon_running(cookie_socket):
+ return False
+
+ try:
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_SEQPACKET)
+ sock.connect(cookie_socket)
+ sock.send(cmd)
+ sock.close()
+ echo("sent command %r to %r" % (cmd, cookie_socket))
+ return True
+
+ except socket.error:
+ print_exc()
+ error("failed to send message %r to %r" % (cmd, cookie_socket))
+ return False
+
+
+def kill_daemon(cookie_socket):
+ '''Send the "EXIT" command to running cookie_daemon.'''
+
+ if send_command(cookie_socket, "EXIT"):
+ # Now ensure the cookie_socket is cleaned up.
+ start = time.time()
+ while os.path.exists(cookie_socket):
+ time.sleep(0.1)
+ if (time.time() - start) > 5:
+ error("force deleting socket %r" % cookie_socket)
+ os.remove(cookie_socket)
+ return
+
+ echo("stopped daemon listening on %r"% cookie_socket)
+
+ else:
+ if os.path.exists(cookie_socket):
+ os.remove(cookie_socket)
+ echo("removed abandoned/broken socket %r" % cookie_socket)
+
+
+def daemonize():
+ '''Daemonize the process using the Stevens' double-fork magic.'''
+
+ try:
+ if os.fork():
+ os._exit(0)
+
+ except OSError:
+ print_exc()
+ sys.stderr.write("fork #1 failed")
+ sys.exit(1)
+
+ os.chdir('/')
+ os.setsid()
+ os.umask(0)
+
+ try:
+ if os.fork():
+ os._exit(0)
+
+ except OSError:
+ print_exc()
+ sys.stderr.write("fork #2 failed")
+ sys.exit(1)
+
+ sys.stdout.flush()
+ sys.stderr.flush()
+
+ devnull = '/dev/null'
+ stdin = file(devnull, 'r')
+ stdout = file(devnull, 'a+')
+ stderr = file(devnull, 'a+', 0)
+
+ os.dup2(stdin.fileno(), sys.stdin.fileno())
+ os.dup2(stdout.fileno(), sys.stdout.fileno())
+ os.dup2(stderr.fileno(), sys.stderr.fileno())
+
+
+class CookieMonster:
+ '''The uzbl cookie daemon class.'''
+
+ def __init__(self):
+ '''Initialise class variables.'''
+
+ self.server_socket = None
+ self.jar = None
+ self.last_request = time.time()
+ self._running = False
+
+
+ def run(self):
+ '''Start the daemon.'''
+
+ # The check healthy function will exit if another daemon is detected
+ # listening on the cookie socket and remove the abandoned socket if
+ # there isnt.
+ if os.path.exists(config['cookie_socket']):
+ if daemon_running(config['cookie_socket']):
+ sys.exit(1)
+
+ # Create cookie daemon socket.
+ self.create_socket()
+
+ # Daemonize process.
+ if config['daemon_mode']:
+ echo("entering daemon mode")
+ daemonize()
+
+ # Register a function to cleanup on exit.
+ atexit.register(self.quit)
+
+ # Make SIGTERM act orderly.
+ signal(SIGTERM, lambda signum, stack_frame: sys.exit(1))
+
+ # Create cookie jar object from file.
+ self.open_cookie_jar()
+
+ # Create a way to exit nested loops by setting a running flag.
+ self._running = True
+
+ while self._running:
+ try:
+ # Enter main listen loop.
+ self.listen()
+
+ except KeyboardInterrupt:
+ self._running = False
+ print
+
+ except socket.error:
+ print_exc()
+
+ except:
+ # Clean up
+ self.del_socket()
+
+ # Raise exception
+ raise
+
+ # Always delete the socket before calling create again.
+ self.del_socket()
+ # Create cookie daemon socket.
+ self.create_socket()
+
+
+ def load_whitelist(self):
+ '''Load the cookie jar whitelist policy.'''
+
+ cookie_whitelist = config['cookie_whitelist']
+
+ if cookie_whitelist:
+ mkbasedir(cookie_whitelist)
+
+ # Create cookie whitelist file if it does not exist.
+ if not os.path.exists(cookie_whitelist):
+ open(cookie_whitelist, 'w').close()
+
+ # Read cookie whitelist file into list.
+ file = open(cookie_whitelist,'r')
+ domain_list = [line.rstrip('\n') for line in file]
+ file.close()
+
+ # Define policy of allowed domains
+ policy = cookielib.DefaultCookiePolicy(allowed_domains=domain_list)
+ self.jar.set_policy(policy)
+
+ # Save the last modified time of the whitelist.
+ self._whitelistmtime = os.stat(cookie_whitelist).st_mtime
+
+
+ def open_cookie_jar(self):
+ '''Open the cookie jar.'''
+
+ cookie_jar = config['cookie_jar']
+ cookie_whitelist = config['cookie_whitelist']
+
+ if cookie_jar:
+ mkbasedir(cookie_jar)
+
+ # Create cookie jar object from file.
+ self.jar = cookielib.MozillaCookieJar(cookie_jar)
+
+ # Load cookie whitelist policy.
+ if config['use_whitelist']:
+ self.load_whitelist()
+
+ if cookie_jar:
+ try:
+ # Attempt to load cookies from the cookie jar.
+ self.jar.load(ignore_discard=True)
+
+ # Ensure restrictive permissions are set on the cookie jar
+ # to prevent other users on the system from hi-jacking your
+ # authenticated sessions simply by copying your cookie jar.
+ os.chmod(cookie_jar, 0600)
+
+ except:
+ pass
+
+
+ def reload_whitelist(self):
+ '''Reload the cookie whitelist.'''
+
+ cookie_whitelist = config['cookie_whitelist']
+ if os.path.exists(cookie_whitelist):
+ echo("reloading whitelist %r" % cookie_whitelist)
+ self.open_cookie_jar()
+
+
+ def create_socket(self):
+ '''Create AF_UNIX socket for communication with uzbl instances.'''
+
+ cookie_socket = config['cookie_socket']
+ mkbasedir(cookie_socket)
+
+ self.server_socket = socket.socket(socket.AF_UNIX,
+ socket.SOCK_SEQPACKET)
+
+ self.server_socket.bind(cookie_socket)
+
+ # Set restrictive permissions on the cookie socket to prevent other
+ # users on the system from data-mining your cookies.
+ os.chmod(cookie_socket, 0600)
+
+
+ def listen(self):
+ '''Listen for incoming cookie PUT and GET requests.'''
+
+ daemon_timeout = config['daemon_timeout']
+ echo("listening on %r" % config['cookie_socket'])
+
+ while self._running:
+ # This line tells the socket how many pending incoming connections
+ # to enqueue at once. Raising this number may or may not increase
+ # performance.
+ self.server_socket.listen(1)
+
+ if bool(select.select([self.server_socket], [], [], 1)[0]):
+ client_socket, _ = self.server_socket.accept()
+ self.handle_request(client_socket)
+ self.last_request = time.time()
+ client_socket.close()
+ continue
+
+ if daemon_timeout:
+ # Checks if the daemon has been idling for too long.
+ idle = time.time() - self.last_request
+ if idle > daemon_timeout:
+ self._running = False
+
+
+ def handle_request(self, client_socket):
+ '''Connection made, now to serve a cookie PUT or GET request.'''
+
+ # Receive cookie request from client.
+ data = client_socket.recv(8192)
+ if not data:
+ return
+
+ # Cookie argument list in packet is null separated.
+ argv = data.split("\0")
+ action = argv[0].upper().strip()
+
+ # Catch the EXIT command sent to kill running daemons.
+ if action == "EXIT":
+ self._running = False
+ return
+
+ # Catch whitelist RELOAD command.
+ elif action == "RELOAD":
+ self.reload_whitelist()
+ return
+
+ # Return if command unknown.
+ elif action not in ['GET', 'PUT']:
+ error("unknown command %r." % argv)
+ return
+
+ # Determine whether or not to print cookie data to terminal.
+ print_cookie = (config['verbose'] and not config['daemon_mode'])
+ if print_cookie:
+ print ' '.join(argv[:4])
+
+ uri = urllib2.urlparse.ParseResult(
+ scheme=argv[1],
+ netloc=argv[2],
+ path=argv[3],
+ params='',
+ query='',
+ fragment='').geturl()
+
+ req = urllib2.Request(uri)
+
+ if action == "GET":
+ self.jar.add_cookie_header(req)
+ if req.has_header('Cookie'):
+ cookie = req.get_header('Cookie')
+ client_socket.send(cookie)
+ if print_cookie:
+ print cookie
+
+ else:
+ client_socket.send("\0")
+
+ elif action == "PUT":
+ cookie = argv[4] if len(argv) > 3 else None
+ if print_cookie:
+ print cookie
+
+ self.put_cookie(req, cookie)
+
+ if print_cookie:
+ print
+
+
+ def put_cookie(self, req, cookie=None):
+ '''Put a cookie in the cookie jar.'''
+
+ hdr = urllib2.httplib.HTTPMessage(\
+ StringIO.StringIO('Set-Cookie: %s' % cookie))
+ res = urllib2.addinfourl(StringIO.StringIO(), hdr,
+ req.get_full_url())
+ self.jar.extract_cookies(res, req)
+ if config['cookie_jar']:
+ self.jar.save(ignore_discard=True)
+
+
+ def del_socket(self):
+ '''Remove the cookie_socket file on exit. In a way the cookie_socket
+ is the daemons pid file equivalent.'''
+
+ if self.server_socket:
+ try:
+ self.server_socket.close()
+
+ except:
+ pass
+
+ self.server_socket = None
+
+ cookie_socket = config['cookie_socket']
+ if os.path.exists(cookie_socket):
+ echo("deleting socket %r" % cookie_socket)
+ os.remove(cookie_socket)
+
+
+ def quit(self):
+ '''Called on exit to make sure all loose ends are tied up.'''
+
+ self.del_socket()
+ sys.exit(0)
+
+
+def main():
+ '''Main function.'''
+
+ # Define command line parameters.
+ usage = "usage: %prog [options] {start|stop|restart|reload}"
+ parser = OptionParser(usage=usage)
+ parser.add_option('-n', '--no-daemon', dest='no_daemon',
+ action='store_true', help="don't daemonise the process.")
+
+ parser.add_option('-v', '--verbose', dest="verbose",
+ action='store_true', help="print verbose output.")
+
+ parser.add_option('-t', '--daemon-timeout', dest='daemon_timeout',
+ action="store", metavar="SECONDS", help="shutdown the daemon after x "\
+ "seconds inactivity. WARNING: Do not use this when launching the "\
+ "cookie daemon manually.")
+
+ parser.add_option('-s', '--cookie-socket', dest="cookie_socket",
+ metavar="SOCKET", help="manually specify the socket location.")
+
+ parser.add_option('-j', '--cookie-jar', dest='cookie_jar',
+ metavar="FILE", help="manually specify the cookie jar location.")
+
+ parser.add_option('-m', '--memory', dest='memory', action='store_true',
+ help="store cookies in memory only - do not write to disk")
+
+ parser.add_option('-u', '--use-whitelist', dest='usewhitelist',
+ action='store_true', help="use cookie whitelist policy")
+
+ parser.add_option('-w', '--cookie-whitelist', dest='whitelist',
+ action='store', help="manually specify whitelist location",
+ metavar='FILE')
+
+ # Parse the command line arguments.
+ (options, args) = parser.parse_args()
+
+ expand = lambda p: os.path.realpath(os.path.expandvars(p))
+
+ initcommands = ['start', 'stop', 'restart', 'reload']
+ for arg in args:
+ if arg not in initcommands:
+ error("unknown argument %r" % args[0])
+ sys.exit(1)
+
+ if len(args) > 1:
+ error("the daemon only accepts one {%s} action at a time."
+ % '|'.join(initcommands))
+ sys.exit(1)
+
+ if len(args):
+ action = args[0]
+
+ else:
+ action = "start"
+
+ if options.no_daemon:
+ config['daemon_mode'] = False
+
+ if options.cookie_socket:
+ config['cookie_socket'] = expand(options.cookie_socket)
+
+ if options.cookie_jar:
+ config['cookie_jar'] = expand(options.cookie_jar)
+
+ if options.memory:
+ config['cookie_jar'] = None
+
+ if options.whitelist:
+ config['cookie_whitelist'] = expand(options.whitelist)
+
+ if options.whitelist or options.usewhitelist:
+ config['use_whitelist'] = True
+
+ if options.daemon_timeout:
+ try:
+ config['daemon_timeout'] = int(options.daemon_timeout)
+
+ except ValueError:
+ error("expected int argument for -t, --daemon-timeout")
+
+ # Expand $VAR's in config keys that relate to paths.
+ for key in ['cookie_socket', 'cookie_jar', 'cookie_whitelist']:
+ if config[key]:
+ config[key] = os.path.expandvars(config[key])
+
+ if options.verbose:
+ config['verbose'] = True
+ import pprint
+ sys.stderr.write("%s\n" % pprint.pformat(config))
+
+ # It would be better if we didn't need to start this python process just
+ # to send a command to the socket, but unfortunately socat doesn't seem
+ # to support SEQPACKET.
+ if action == "reload":
+ send_command(config['cookie_socket'], "RELOAD")
+
+ if action in ['stop', 'restart']:
+ kill_daemon(config['cookie_socket'])
+
+ if action in ['start', 'restart']:
+ CookieMonster().run()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/data/scripts/uzbl-event-manager b/examples/data/scripts/uzbl-event-manager
new file mode 100755
index 0000000..9624b14
--- /dev/null
+++ b/examples/data/scripts/uzbl-event-manager
@@ -0,0 +1,837 @@
+#!/usr/bin/env python
+
+# Event Manager for Uzbl
+# Copyright (c) 2009, Mason Larobina <mason.larobina@gmail.com>
+# Copyright (c) 2009, Dieter Plaetinck <dieter@plaetinck.be>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+'''
+
+E V E N T _ M A N A G E R . P Y
+===============================
+
+Event manager for uzbl written in python.
+
+'''
+
+import imp
+import os
+import sys
+import re
+import socket
+import pprint
+import time
+import atexit
+from select import select
+from signal import signal, SIGTERM
+from optparse import OptionParser
+from traceback import print_exc
+from functools import partial
+
+
+def xdghome(key, default):
+ '''Attempts to use the environ XDG_*_HOME paths if they exist otherwise
+ use $HOME and the default path.'''
+
+ xdgkey = "XDG_%s_HOME" % key
+ if xdgkey in os.environ.keys() and os.environ[xdgkey]:
+ return os.environ[xdgkey]
+
+ return os.path.join(os.environ['HOME'], default)
+
+
+# ============================================================================
+# ::: Default configuration section ::::::::::::::::::::::::::::::::::::::::::
+# ============================================================================
+
+# `make install` will put the correct value here for your system
+PREFIX = '/usr/local/'
+
+# Setup xdg paths.
+DATA_DIR = os.path.join(xdghome('DATA', '.local/share/'), 'uzbl/')
+CACHE_DIR = os.path.join(xdghome('CACHE', '.cache/'), 'uzbl/')
+
+# Event manager config dictionary. This is not to be confused with the config
+# dict that tracks variables in the uzbl instance.
+CONFIG = {
+ 'verbose': False,
+ 'daemon_mode': True,
+ 'auto_close': False,
+
+ 'plugins_load': [],
+ 'plugins_ignore': [],
+
+ 'plugin_dirs': [os.path.join(DATA_DIR, 'plugins/'),
+ os.path.join(PREFIX, 'share/uzbl/examples/data/plugins/')],
+
+ 'server_socket': os.path.join(CACHE_DIR, 'event_daemon'),
+ 'pid_file': os.path.join(CACHE_DIR, 'event_daemon.pid'),
+}
+
+# ============================================================================
+# ::: End of configuration section :::::::::::::::::::::::::::::::::::::::::::
+# ============================================================================
+
+
+# Define some globals.
+SCRIPTNAME = os.path.basename(sys.argv[0])
+FINDSPACES = re.compile("\s+")
+
+
+class ArgumentError(Exception):
+ pass
+
+
+def echo(msg):
+ '''Prints only if the verbose flag has been set.'''
+
+ if CONFIG['verbose']:
+ sys.stdout.write("%s: %s\n" % (SCRIPTNAME, msg))
+
+
+def error(msg):
+ '''Prints error messages to stderr.'''
+
+ sys.stderr.write("%s: error: %s\n" % (SCRIPTNAME, msg))
+
+
+def counter():
+ '''Generate unique object id's.'''
+
+ i = 0
+ while True:
+ i += 1
+ yield i
+
+
+def find_plugins(plugin_dirs):
+ '''Find all event manager plugins in the plugin dirs and return a
+ dictionary of {'plugin-name.py': '/full/path/to/plugin-name.py', ...}'''
+
+ plugins = {}
+
+ for plugin_dir in plugin_dirs:
+ plugin_dir = os.path.realpath(os.path.expandvars(plugin_dir))
+ if not os.path.isdir(plugin_dir):
+ continue
+
+ for filename in os.listdir(plugin_dir):
+ if not filename.lower().endswith('.py'):
+ continue
+
+ path = os.path.join(plugin_dir, filename)
+ if not os.path.isfile(path):
+ continue
+
+ if filename not in plugins:
+ plugins[filename] = plugin_dir
+
+ return plugins
+
+
+def load_plugins(plugin_dirs, load=None, ignore=None):
+ '''Load event manager plugins found in the plugin_dirs.'''
+
+ load = [] if load is None else load
+ ignore = [] if ignore is None else ignore
+
+ # Find the plugins in the plugin_dirs.
+ found = find_plugins(plugin_dirs)
+
+ if load:
+ # Ignore anything not in the load list.
+ for plugin in found.keys():
+ if plugin not in load:
+ del found[plugin]
+
+ if ignore:
+ # Ignore anything in the ignore list.
+ for plugin in found.keys():
+ if plugin in ignore:
+ del found[plugin]
+
+ # Print plugin list to be loaded.
+ pprint.pprint(found)
+
+ loaded = {}
+ # Load all found plugins into the loaded dict.
+ for (filename, plugin_dir) in found.items():
+ name = filename[:-3]
+ info = imp.find_module(name, [plugin_dir])
+ plugin = imp.load_module(name, *info)
+ loaded[(plugin_dir, filename)] = plugin
+
+ return loaded
+
+
+def daemonize():
+ '''Daemonize the process using the Stevens' double-fork magic.'''
+
+ try:
+ if os.fork():
+ os._exit(0)
+
+ except OSError:
+ print_exc()
+ sys.stderr.write("fork #1 failed")
+ sys.exit(1)
+
+ os.chdir('/')
+ os.setsid()
+ os.umask(0)
+
+ try:
+ if os.fork():
+ os._exit(0)
+
+ except OSError:
+ print_exc()
+ sys.stderr.write("fork #2 failed")
+ sys.exit(1)
+
+ sys.stdout.flush()
+ sys.stderr.flush()
+
+ devnull = '/dev/null'
+ stdin = file(devnull, 'r')
+ stdout = file(devnull, 'a+')
+ stderr = file(devnull, 'a+', 0)
+
+ os.dup2(stdin.fileno(), sys.stdin.fileno())
+ os.dup2(stdout.fileno(), sys.stdout.fileno())
+ os.dup2(stderr.fileno(), sys.stderr.fileno())
+
+
+def make_dirs(path):
+ '''Make all basedirs recursively as required.'''
+
+ try:
+ dirname = os.path.dirname(path)
+ if not os.path.isdir(dirname):
+ os.makedirs(dirname)
+
+ except OSError:
+ print_exc()
+
+
+def make_pid_file(pid_file):
+ '''Make pid file at given pid_file location.'''
+
+ make_dirs(pid_file)
+ fileobj = open(pid_file, 'w')
+ fileobj.write('%d' % os.getpid())
+ fileobj.close()
+
+
+def del_pid_file(pid_file):
+ '''Delete pid file at given pid_file location.'''
+
+ if os.path.isfile(pid_file):
+ os.remove(pid_file)
+
+
+def get_pid(pid_file):
+ '''Read pid from pid_file.'''
+
+ try:
+ fileobj = open(pid_file, 'r')
+ pid = int(fileobj.read())
+ fileobj.close()
+ return pid
+
+ except IOError, ValueError:
+ print_exc()
+ return None
+
+
+def pid_running(pid):
+ '''Returns True if a process with the given pid is running.'''
+
+ try:
+ os.kill(pid, 0)
+
+ except OSError:
+ return False
+
+ else:
+ return True
+
+
+def term_process(pid):
+ '''Send a SIGTERM signal to the process with the given pid.'''
+
+ if not pid_running(pid):
+ return False
+
+ os.kill(pid, SIGTERM)
+
+ start = time.time()
+ while True:
+ if not pid_running(pid):
+ return True
+
+ if time.time() - start > 5:
+ raise OSError('failed to stop process with pid: %d' % pid)
+
+ time.sleep(0.25)
+
+
+def parse_msg(uzbl, msg):
+ '''Parse an incoming msg from a uzbl instance. All non-event messages
+ will be printed here and not be passed to the uzbl instance event
+ handler function.'''
+
+ if not msg:
+ return
+
+ cmd = FINDSPACES.split(msg, 3)
+ if not cmd or cmd[0] != 'EVENT':
+ # Not an event message.
+ print '---', msg.encode('utf-8')
+ return
+
+ while len(cmd) < 4:
+ cmd.append('')
+
+ event, args = cmd[2], cmd[3]
+ if not event:
+ return
+
+ try:
+ uzbl.event(event, args)
+
+ except:
+ print_exc()
+
+
+class EventHandler(object):
+
+ nexthid = counter().next
+
+ def __init__(self, event, handler, *args, **kargs):
+ if not callable(handler):
+ raise ArgumentError("EventHandler object requires a callable "
+ "object function for the handler argument not: %r" % handler)
+
+ self.function = handler
+ self.args = args
+ self.kargs = kargs
+ self.event = event
+ self.hid = self.nexthid()
+
+
+ def __repr__(self):
+ args = ["event=%s" % self.event, "hid=%d" % self.hid,
+ "function=%r" % self.function]
+
+ if self.args:
+ args.append(u"args=%r" % unicode(self.args))
+
+ if self.kargs:
+ args.append(u"kargs=%r" % unicode(self.kargs))
+
+ return u"<EventHandler(%s)>" % ', '.join(args)
+
+
+class UzblInstance(object):
+
+ # Give all plugins access to the main config dict.
+ config = CONFIG
+
+ def __init__(self, parent, client_socket):
+
+ # Internal variables.
+ self.exports = {}
+ self.handlers = {}
+ self.parent = parent
+ self.client_socket = client_socket
+
+ self.depth = 0
+ self.buffer = ''
+ self.pid = None
+
+ # Call the init function in every plugin. The init function in each
+ # plugin is where that plugin connects functions to events and exports
+ # functions to the uzbl object.
+ for plugin in self.parent['plugins'].values():
+ try:
+ plugin.init(self)
+
+ except:
+ raise
+
+
+ def send(self, msg):
+ '''Send a command to the uzbl instance via the socket file.'''
+
+ msg = msg.strip()
+ if self.client_socket:
+ print (u'%s<-- %s' % (' ' * self.depth, msg)).encode('utf-8')
+ self.client_socket.send(("%s\n" % msg).encode('utf-8'))
+
+ else:
+ print (u'%s!-- %s' % (' ' * self.depth, msg)).encode('utf-8')
+
+
+ def export(self, name, function):
+ '''Export `function(uzbl, *args, ..)` inside a plugin to the uzbl
+ object like so `uzbl.function(*args, ..)`. This will allow other
+ plugins to call functions inside the current plugin (which is currently
+ calling this function) via the uzbl object.'''
+
+ self.__dict__.__setitem__(name, partial(function, self))
+
+
+ def export_dict(self, export_dict):
+ '''Export multiple (name, function)'s at once inside a dict of the
+ form `{name1: function1, name2: function2, ...}`.'''
+
+ for (name, function) in export_dict.items():
+ self.export(name, function)
+
+
+ def connect(self, event, handler, *args, **kargs):
+ '''Connect a uzbl event with a handler. Handlers can either be a
+ function or a uzbl command string.'''
+
+ event = event.upper().strip()
+ assert event and ' ' not in event
+
+ if event not in self.handlers.keys():
+ self.handlers[event] = []
+
+ handlerobj = EventHandler(event, handler, *args, **kargs)
+ self.handlers[event].append(handlerobj)
+ print handlerobj
+
+
+ def connect_dict(self, connect_dict):
+ '''Connect a dictionary comprising of {"EVENT_NAME": handler, ..} to
+ the event handler stack.
+
+ If you need to supply args or kargs to an event use the normal connect
+ function.'''
+
+ for (event, handler) in connect_dict.items():
+ self.connect(event, handler)
+
+
+ def remove_by_id(self, hid):
+ '''Remove connected event handler by unique handler id.'''
+
+ for (event, handlers) in self.handlers.items():
+ for handler in list(handlers):
+ if hid != handler.hid:
+ continue
+
+ echo("removed %r" % handler)
+ handlers.remove(handler)
+ return
+
+ echo('unable to find & remove handler with id: %d' % hid)
+
+
+ def remove(self, handler):
+ '''Remove connected event handler.'''
+
+ for (event, handlers) in self.handlers.items():
+ if handler in handlers:
+ echo("removed %r" % handler)
+ handlers.remove(handler)
+ return
+
+ echo('unable to find & remove handler: %r' % handler)
+
+
+ def exec_handler(self, handler, *args, **kargs):
+ '''Execute event handler function.'''
+
+ args += handler.args
+ kargs = dict(handler.kargs.items()+kargs.items())
+ handler.function(self, *args, **kargs)
+
+
+ def event(self, event, *args, **kargs):
+ '''Raise an event.'''
+
+ event = event.upper()
+ elems = [event,]
+ if args: elems.append(unicode(args))
+ if kargs: elems.append(unicode(kargs))
+ print (u'%s--> %s' % (' ' * self.depth, ' '.join(elems))).encode('utf-8')
+
+ if event == "INSTANCE_START" and args:
+ self.pid = int(args[0])
+
+ if event not in self.handlers:
+ return
+
+ for handler in self.handlers[event]:
+ self.depth += 1
+ try:
+ self.exec_handler(handler, *args, **kargs)
+
+ except:
+ print_exc()
+
+ self.depth -= 1
+
+
+ def close(self):
+ '''Close the client socket and clean up.'''
+
+ try:
+ self.client_socket.close()
+
+ except:
+ pass
+
+ for (name, plugin) in self.parent['plugins'].items():
+ if hasattr(plugin, 'cleanup'):
+ plugin.cleanup(self)
+
+
+class UzblEventDaemon(dict):
+ def __init__(self):
+
+ # Init variables and dict keys.
+ dict.__init__(self, {'uzbls': {}})
+ self.running = None
+ self.server_socket = None
+ self.socket_location = None
+
+ # Register that the event daemon server has started by creating the
+ # pid file.
+ make_pid_file(CONFIG['pid_file'])
+
+ # Register a function to clean up the socket and pid file on exit.
+ atexit.register(self.quit)
+
+ # Make SIGTERM act orderly.
+ signal(SIGTERM, lambda signum, stack_frame: sys.exit(1))
+
+ # Load plugins, first-build of the plugins may be a costly operation.
+ self['plugins'] = load_plugins(CONFIG['plugin_dirs'],
+ CONFIG['plugins_load'], CONFIG['plugins_ignore'])
+
+
+ def _create_server_socket(self):
+ '''Create the event manager daemon socket for uzbl instance duplex
+ communication.'''
+
+ server_socket = CONFIG['server_socket']
+ server_socket = os.path.realpath(os.path.expandvars(server_socket))
+ self.socket_location = server_socket
+
+ # Delete socket if it exists.
+ if os.path.exists(server_socket):
+ os.remove(server_socket)
+
+ self.server_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ self.server_socket.bind(server_socket)
+ self.server_socket.listen(5)
+
+
+ def _close_server_socket(self):
+ '''Close and delete the server socket.'''
+
+ try:
+ self.server_socket.close()
+ self.server_socket = None
+
+ if os.path.exists(self.socket_location):
+ os.remove(self.socket_location)
+
+ except:
+ pass
+
+
+ def run(self):
+ '''Main event daemon loop.'''
+
+ # Create event daemon socket.
+ self._create_server_socket()
+ echo('listening on: %s' % self.socket_location)
+
+ if CONFIG['daemon_mode']:
+ echo('entering daemon mode.')
+ daemonize()
+ # The pid has changed so update the pid file.
+ make_pid_file(CONFIG['pid_file'])
+
+ # Now listen for incoming connections and or data.
+ self.listen()
+
+ # Clean up.
+ self.quit()
+
+
+ def listen(self):
+ '''Accept incoming connections and constantly poll instance sockets
+ for incoming data.'''
+
+ self.running = True
+ while self.running:
+
+ sockets = [self.server_socket] + self['uzbls'].keys()
+
+ reads, _, errors = select(sockets, [], sockets, 1)
+
+ if self.server_socket in reads:
+ self.accept_connection()
+ reads.remove(self.server_socket)
+
+ for client in reads:
+ self.read_socket(client)
+
+ for client in errors:
+ error('Unknown error on socket: %r' % client)
+ self.close_connection(client)
+
+
+ def read_socket(self, client):
+ '''Read data from an instance socket and pass to the uzbl objects
+ event handler function.'''
+
+ uzbl = self['uzbls'][client]
+ try:
+ raw = unicode(client.recv(8192), 'utf-8', 'ignore')
+
+ except:
+ print_exc()
+ raw = None
+
+ if not raw:
+ # Read null byte, close socket.
+ return self.close_connection(client)
+
+ uzbl.buffer += raw
+ msgs = uzbl.buffer.split('\n')
+ uzbl.buffer = msgs.pop()
+
+ for msg in msgs:
+ try:
+ parse_msg(uzbl, msg.strip())
+
+ except:
+ print_exc()
+
+
+ def accept_connection(self):
+ '''Accept incoming connection to the server socket.'''
+
+ client_socket = self.server_socket.accept()[0]
+
+ uzbl = UzblInstance(self, client_socket)
+ self['uzbls'][client_socket] = uzbl
+
+
+ def close_connection(self, client):
+ '''Clean up after instance close.'''
+
+ try:
+ if client in self['uzbls']:
+ uzbl = self['uzbls'][client]
+ uzbl.close()
+ del self['uzbls'][client]
+
+ except:
+ print_exc()
+
+ if not len(self['uzbls']) and CONFIG['auto_close']:
+ echo('auto closing event manager.')
+ self.running = False
+
+
+ def quit(self):
+ '''Close all instance socket objects, server socket and delete the
+ pid file.'''
+
+ echo('shutting down event manager.')
+
+ for client in self['uzbls'].keys():
+ self.close_connection(client)
+
+ echo('unlinking: %r' % self.socket_location)
+ self._close_server_socket()
+
+ echo('deleting pid file: %r' % CONFIG['pid_file'])
+ del_pid_file(CONFIG['pid_file'])
+
+
+def stop_action():
+ '''Stop the event manager daemon.'''
+
+ pid_file = CONFIG['pid_file']
+ if not os.path.isfile(pid_file):
+ return echo('no running daemon found.')
+
+ echo('found pid file: %r' % pid_file)
+ pid = get_pid(pid_file)
+ if not pid_running(pid):
+ echo('no process with pid: %d' % pid)
+ return os.remove(pid_file)
+
+ echo("terminating process with pid: %d" % pid)
+ term_process(pid)
+ if os.path.isfile(pid_file):
+ os.remove(pid_file)
+
+ echo('stopped event daemon.')
+
+
+def start_action():
+ '''Start the event manager daemon.'''
+
+ pid_file = CONFIG['pid_file']
+ if os.path.isfile(pid_file):
+ echo('found pid file: %r' % pid_file)
+ pid = get_pid(pid_file)
+ if pid_running(pid):
+ return echo('event daemon already started with pid: %d' % pid)
+
+ echo('no process with pid: %d' % pid)
+ os.remove(pid_file)
+
+ echo('starting event manager.')
+ UzblEventDaemon().run()
+
+
+def restart_action():
+ '''Restart the event manager daemon.'''
+
+ echo('restarting event manager daemon.')
+ stop_action()
+ start_action()
+
+
+def list_action():
+ '''List all the plugins being loaded by the event daemon.'''
+
+ plugins = find_plugins(CONFIG['plugin_dirs'])
+ dirs = {}
+
+ for (plugin, plugin_dir) in plugins.items():
+ if plugin_dir not in dirs:
+ dirs[plugin_dir] = []
+
+ dirs[plugin_dir].append(plugin)
+
+ for (index, (plugin_dir, plugin_list)) in enumerate(sorted(dirs.items())):
+ if index:
+ print
+
+ print "%s:" % plugin_dir
+ for plugin in sorted(plugin_list):
+ print " %s" % plugin
+
+
+if __name__ == "__main__":
+ USAGE = "usage: %prog [options] {start|stop|restart|list}"
+ PARSER = OptionParser(usage=USAGE)
+ PARSER.add_option('-v', '--verbose', dest='verbose', action="store_true",
+ help="print verbose output.")
+
+ PARSER.add_option('-d', '--plugin-dirs', dest='plugin_dirs', action="store",
+ metavar="DIRS", help="Specify plugin directories in the form of "\
+ "'dir1:dir2:dir3'.")
+
+ PARSER.add_option('-l', '--load-plugins', dest="load", action="store",
+ metavar="PLUGINS", help="comma separated list of plugins to load")
+
+ PARSER.add_option('-i', '--ignore-plugins', dest="ignore", action="store",
+ metavar="PLUGINS", help="comma separated list of plugins to ignore")
+
+ PARSER.add_option('-p', '--pid-file', dest='pid', action='store',
+ metavar='FILE', help="specify pid file location")
+
+ PARSER.add_option('-s', '--server-socket', dest='socket', action='store',
+ metavar='SOCKET', help="specify the daemon socket location")
+
+ PARSER.add_option('-n', '--no-daemon', dest="daemon",
+ action="store_true", help="don't enter daemon mode.")
+
+ PARSER.add_option('-a', '--auto-close', dest='autoclose',
+ action='store_true', help='auto close after all instances disconnect.')
+
+ (OPTIONS, ARGS) = PARSER.parse_args()
+
+ # init like {start|stop|..} daemon actions dict.
+ DAEMON_ACTIONS = {'start': start_action, 'stop': stop_action,
+ 'restart': restart_action, 'list': list_action}
+
+ if not ARGS:
+ ACTION = 'start'
+
+ elif len(ARGS) == 1:
+ ACTION = ARGS[0]
+ if ACTION not in DAEMON_ACTIONS:
+ raise ArgumentError("unknown argument: %r" % ACTION)
+
+ else:
+ raise ArgumentError("too many arguments: %r" % ARGS)
+
+ # parse other flags & options.
+ if OPTIONS.verbose:
+ CONFIG['verbose'] = True
+
+ if OPTIONS.plugin_dirs:
+ PLUGIN_DIRS = []
+ for DIR in OPTIONS.plugin_dirs.split(':'):
+ if not DIR:
+ continue
+
+ PLUGIN_DIRS.append(os.path.realpath(DIR))
+
+ CONFIG['plugin_dirs'] = PLUGIN_DIRS
+ echo("plugin search dirs: %r" % PLUGIN_DIRS)
+
+ if OPTIONS.load and OPTIONS.ignore:
+ error("you can't load and ignore at the same time.")
+ sys.exit(1)
+
+ elif OPTIONS.load:
+ LOAD = CONFIG['plugins_load']
+ for PLUGIN in OPTIONS.load.split(','):
+ if PLUGIN.strip():
+ LOAD.append(PLUGIN.strip())
+
+ echo('only loading plugin(s): %s' % ', '.join(LOAD))
+
+ elif OPTIONS.ignore:
+ IGNORE = CONFIG['plugins_ignore']
+ for PLUGIN in OPTIONS.ignore.split(','):
+ if PLUGIN.strip():
+ IGNORE.append(PLUGIN.strip())
+
+ echo('ignoring plugin(s): %s' % ', '.join(IGNORE))
+
+ if OPTIONS.autoclose:
+ CONFIG['auto_close'] = True
+ echo('will auto close.')
+
+ if OPTIONS.pid:
+ CONFIG['pid_file'] = os.path.realpath(OPTIONS.pid)
+ echo("pid file location: %r" % CONFIG['pid_file'])
+
+ if OPTIONS.socket:
+ CONFIG['server_socket'] = os.path.realpath(OPTIONS.socket)
+ echo("daemon socket location: %s" % CONFIG['server_socket'])
+
+ if OPTIONS.daemon:
+ CONFIG['daemon_mode'] = False
+
+ # Now {start|stop|...}
+ DAEMON_ACTIONS[ACTION]()
diff --git a/examples/data/scripts/uzbl-tabbed b/examples/data/scripts/uzbl-tabbed
new file mode 100755
index 0000000..7bd90d5
--- /dev/null
+++ b/examples/data/scripts/uzbl-tabbed
@@ -0,0 +1,1417 @@
+#!/usr/bin/env python
+
+# Uzbl tabbing wrapper using a fifo socket interface
+# Copyright (c) 2009, Tom Adams <tom@holizz.com>
+# Copyright (c) 2009, Chris van Dijk <cn.vandijk@hotmail.com>
+# Copyright (c) 2009, Mason Larobina <mason.larobina@gmail.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+# Author(s):
+# Tom Adams <tom@holizz.com>
+# Wrote the original uzbl_tabbed.py as a proof of concept.
+#
+# Chris van Dijk (quigybo) <cn.vandijk@hotmail.com>
+# Made signifigant headway on the old uzbl_tabbing.py script on the
+# uzbl wiki <http://www.uzbl.org/wiki/uzbl_tabbed>
+#
+# Mason Larobina <mason.larobina@gmail.com>
+# Rewrite of the uzbl_tabbing.py script to use a fifo socket interface
+# and inherit configuration options from the user's uzbl config.
+#
+# Contributor(s):
+# mxey <mxey@ghosthacking.net>
+# uzbl_config path now honors XDG_CONFIG_HOME if it exists.
+#
+# Romain Bignon <romain@peerfuse.org>
+# Fix for session restoration code.
+#
+# Jake Probst <jake.probst@gmail.com>
+# Wrote a patch that overflows tabs in the tablist on to new lines when
+# running of room.
+#
+# Devon Jones <devon.jones@gmail.com>
+# Fifo command bring_to_front which brings the gtk window to focus.
+#
+# Simon Lipp (sloonz)
+# Various
+
+
+# Dependencies:
+# pygtk - python bindings for gtk.
+# pango - python bindings needed for text rendering & layout in gtk widgets.
+# pygobject - GLib's GObject bindings for python.
+#
+# Optional dependencies:
+# simplejson - save uzbl_tabbed.py sessions & presets in json.
+#
+# Note: I haven't included version numbers with this dependency list because
+# I've only ever tested uzbl_tabbed.py on the latest stable versions of these
+# packages in Gentoo's portage. Package names may vary on different systems.
+
+
+# Configuration:
+# Because this version of uzbl_tabbed is able to inherit options from your main
+# uzbl configuration file you may wish to configure uzbl tabbed from there.
+# Here is a list of configuration options that can be customised and some
+# example values for each:
+#
+# General tabbing options:
+# show_tablist = 1
+# show_gtk_tabs = 0
+# tablist_top = 1
+# gtk_tab_pos = (top|left|bottom|right)
+# gtk_refresh = 1000
+# switch_to_new_tabs = 1
+# capture_new_windows = 1
+# multiline_tabs = 1
+#
+# Tab title options:
+# tab_titles = 1
+# tab_indexes = 1
+# new_tab_title = Loading
+# max_title_len = 50
+# show_ellipsis = 1
+#
+# Session options:
+# save_session = 1
+# json_session = 0
+# session_file = $HOME/.local/share/uzbl/session
+#
+# Inherited uzbl options:
+# fifo_dir = /tmp
+# socket_dir = /tmp
+# icon_path = $HOME/.local/share/uzbl/uzbl.png
+# status_background = #303030
+#
+# Misc options:
+# window_size = 800,800
+# verbose = 0
+#
+# And uzbl_tabbed.py takes care of the actual binding of the commands via each
+# instances fifo socket.
+#
+# Custom tab styling:
+# tab_colours = foreground = "#888" background = "#303030"
+# tab_text_colours = foreground = "#bbb"
+# selected_tab = foreground = "#fff"
+# selected_tab_text = foreground = "green"
+# tab_indicate_https = 1
+# https_colours = foreground = "#888"
+# https_text_colours = foreground = "#9c8e2d"
+# selected_https = foreground = "#fff"
+# selected_https_text = foreground = "gold"
+#
+# How these styling values are used are soley defined by the syling policy
+# handler below (the function in the config section). So you can for example
+# turn the tab text colour Firetruck-Red in the event "error" appears in the
+# tab title or some other arbitrary event. You may wish to make a trusted
+# hosts file and turn tab titles of tabs visiting trusted hosts purple.
+
+
+# Issues:
+# - new windows are not caught and opened in a new tab.
+# - when uzbl_tabbed.py crashes it takes all the children with it.
+# - when a new tab is opened when using gtk tabs the tab button itself
+# grabs focus from its child for a few seconds.
+# - when switch_to_new_tabs is not selected the notebook page is
+# maintained but the new window grabs focus (try as I might to stop it).
+
+
+# Todo:
+# - add command line options to use a different session file, not use a
+# session file and or open a uri on starup.
+# - ellipsize individual tab titles when the tab-list becomes over-crowded
+# - add "<" & ">" arrows to tablist to indicate that only a subset of the
+# currently open tabs are being displayed on the tablist.
+# - add the small tab-list display when both gtk tabs and text vim-like
+# tablist are hidden (I.e. [ 1 2 3 4 5 ])
+# - check spelling.
+# - pass a uzbl socketid to uzbl_tabbed.py and have it assimilated into
+# the collective. Resistance is futile!
+
+
+import pygtk
+import gtk
+import subprocess
+import os
+import re
+import time
+import getopt
+import pango
+import select
+import sys
+import gobject
+import socket
+import random
+import hashlib
+import atexit
+import types
+
+from gobject import io_add_watch, source_remove, timeout_add, IO_IN, IO_HUP
+from signal import signal, SIGTERM, SIGINT
+from optparse import OptionParser, OptionGroup
+
+
+pygtk.require('2.0')
+
+_SCRIPTNAME = os.path.basename(sys.argv[0])
+def error(msg):
+ sys.stderr.write("%s: error: %s\n" % (_SCRIPTNAME, msg))
+
+# ============================================================================
+# ::: Default configuration section ::::::::::::::::::::::::::::::::::::::::::
+# ============================================================================
+
+def xdghome(key, default):
+ '''Attempts to use the environ XDG_*_HOME paths if they exist otherwise
+ use $HOME and the default path.'''
+
+ xdgkey = "XDG_%s_HOME" % key
+ if xdgkey in os.environ.keys() and os.environ[xdgkey]:
+ return os.environ[xdgkey]
+
+ return os.path.join(os.environ['HOME'], default)
+
+# Setup xdg paths.
+DATA_DIR = os.path.join(xdghome('DATA', '.local/share/'), 'uzbl/')
+
+# Ensure uzbl xdg paths exist
+if not os.path.exists(DATA_DIR):
+ os.makedirs(DATA_DIR)
+
+# All of these settings can be inherited from your uzbl config file.
+config = {
+ # Tab options
+ 'show_tablist': True, # Show text uzbl like statusbar tab-list
+ 'show_gtk_tabs': False, # Show gtk notebook tabs
+ 'tablist_top': True, # Display tab-list at top of window
+ 'gtk_tab_pos': 'top', # Gtk tab position (top|left|bottom|right)
+ 'gtk_refresh': 1000, # Tablist refresh millisecond interval
+ 'switch_to_new_tabs': True, # Upon opening a new tab switch to it
+ 'capture_new_windows': True, # Use uzbl_tabbed to catch new windows
+ 'multiline_tabs': True, # Tabs overflow onto new tablist lines.
+
+ # Tab title options
+ 'tab_titles': True, # Display tab titles (else only tab-nums)
+ 'tab_indexes': True, # Display tab nums (else only tab titles)
+ 'new_tab_title': 'Loading', # New tab title
+ 'max_title_len': 50, # Truncate title at n characters
+ 'show_ellipsis': True, # Show ellipsis when truncating titles
+
+ # Session options
+ 'save_session': True, # Save session in file when quit
+ 'json_session': False, # Use json to save session.
+ 'saved_sessions_dir': os.path.join(DATA_DIR, 'sessions/'),
+ 'session_file': os.path.join(DATA_DIR, 'session'),
+
+ # Inherited uzbl options
+ 'fifo_dir': '/tmp', # Path to look for uzbl fifo.
+ 'socket_dir': '/tmp', # Path to look for uzbl socket.
+ 'icon_path': os.path.join(DATA_DIR, 'uzbl.png'),
+ 'status_background': "#303030", # Default background for all panels.
+
+ # Misc options
+ 'window_size': "800,800", # width,height in pixels.
+ 'verbose': False, # Print verbose output.
+
+ # Add custom tab style definitions to be used by the tab colour policy
+ # handler here. Because these are added to the config dictionary like
+ # any other uzbl_tabbed configuration option remember that they can
+ # be superseeded from your main uzbl config file.
+ 'tab_colours': 'foreground = "#888" background = "#303030"',
+ 'tab_text_colours': 'foreground = "#bbb"',
+ 'selected_tab': 'foreground = "#fff"',
+ 'selected_tab_text': 'foreground = "green"',
+ 'tab_indicate_https': True,
+ 'https_colours': 'foreground = "#888"',
+ 'https_text_colours': 'foreground = "#9c8e2d"',
+ 'selected_https': 'foreground = "#fff"',
+ 'selected_https_text': 'foreground = "gold"',
+
+} # End of config dict.
+
+UZBL_TABBED_VARS = config.keys()
+
+# This is the tab style policy handler. Every time the tablist is updated
+# this function is called to determine how to colourise that specific tab
+# according the simple/complex rules as defined here. You may even wish to
+# move this function into another python script and import it using:
+# from mycustomtabbingconfig import colour_selector
+# Remember to rename, delete or comment out this function if you do that.
+
+def colour_selector(tabindex, currentpage, uzbl):
+ '''Tablist styling policy handler. This function must return a tuple of
+ the form (tab style, text style).'''
+
+ # Just as an example:
+ # if 'error' in uzbl.title:
+ # if tabindex == currentpage:
+ # return ('foreground="#fff"', 'foreground="red"')
+ # return ('foreground="#888"', 'foreground="red"')
+
+ # Style tabs to indicate connected via https.
+ if config['tab_indicate_https'] and uzbl.uri.startswith("https://"):
+ if tabindex == currentpage:
+ return (config['selected_https'], config['selected_https_text'])
+ return (config['https_colours'], config['https_text_colours'])
+
+ # Style to indicate selected.
+ if tabindex == currentpage:
+ return (config['selected_tab'], config['selected_tab_text'])
+
+ # Default tab style.
+ return (config['tab_colours'], config['tab_text_colours'])
+
+# ============================================================================
+# ::: End of configuration section :::::::::::::::::::::::::::::::::::::::::::
+# ============================================================================
+
+def echo(msg):
+ if config['verbose']:
+ sys.stderr.write("%s: %s\n" % (_SCRIPTNAME, msg))
+
+
+def counter():
+ '''To infinity and beyond!'''
+
+ i = 0
+ while True:
+ i += 1
+ yield i
+
+
+def escape(s):
+ '''Replaces html markup in tab titles that screw around with pango.'''
+
+ for (split, glue) in [('&','&amp;'), ('<', '&lt;'), ('>', '&gt;')]:
+ s = s.replace(split, glue)
+ return s
+
+
+class SocketClient:
+ '''Represents a Uzbl instance, which is not necessarly linked with a UzblInstance'''
+
+ # List of UzblInstance objects not already linked with a SocketClient
+ instances_queue = {}
+
+ def __init__(self, socket):
+ self._buffer = ""
+ self._socket = socket
+ self._watchers = [io_add_watch(socket, IO_IN, self._socket_recv),\
+ io_add_watch(socket, IO_HUP, self._socket_closed)]
+ self.uzbl = None
+
+
+ def _socket_recv(self, fd, condition):
+ '''Data available on socket, process it'''
+
+ self._feed(self._socket.recv(1024)) #TODO: is io_add_watch edge or level-triggered ?
+ return True
+
+
+ def _socket_closed(self, fd, condition):
+ '''Remote client exited'''
+ self.uzbl.close()
+ return False
+
+
+ def _feed(self, data):
+ '''An Uzbl instance sent some data, parse it'''
+
+ self._buffer += data
+ if self.uzbl:
+ if "\n" in self._buffer:
+ cmds = self._buffer.split("\n")
+
+ if cmds[-1]: # Last command has been received incomplete, don't process it
+ self._buffer, cmds = cmds[-1], cmds[:-1]
+ else:
+ self._buffer = ""
+
+ for cmd in cmds:
+ if cmd:
+ self.uzbl.parse_command(cmd)
+ else:
+ name = re.findall('^EVENT \[(\d+-\d+)\] INSTANCE_START \d+$', self._buffer, re.M)
+ uzbl = self.instances_queue.get(name[0])
+ if uzbl:
+ del self.instances_queue[name[0]]
+ self.uzbl = uzbl
+ self.uzbl.got_socket(self)
+ self._feed("")
+
+ def send(self, data):
+ '''Child socket send function.'''
+
+ self._socket.send(data + "\n")
+
+ def close(self):
+ '''Close the connection'''
+
+ if self._socket:
+ self._socket.close()
+ self._socket = None
+ map(source_remove, self._watchers)
+ self._watchers = []
+
+
+class UzblInstance:
+ '''Uzbl instance meta-data/meta-action object.'''
+
+ def __init__(self, parent, tab, name, uri, title, switch):
+
+ self.parent = parent
+ self.tab = tab
+ self.name = name
+ self.title = title
+ self.tabtitle = ""
+ self.uri = uri
+ self._client = None
+ self._switch = switch # Switch to tab after loading ?
+ self.title_changed()
+
+
+ def got_socket(self, client):
+ '''Uzbl instance is now connected'''
+
+ self._client = client
+ self.parent.config_uzbl(self)
+ if self._switch:
+ tabid = self.parent.notebook.page_num(self.tab)
+ self.parent.goto_tab(tabid)
+
+
+ def title_changed(self, gtk_only = True): # GTK-only is for indexes
+ '''self.title has changed, update the tabs list'''
+
+ tab_titles = config['tab_titles']
+ tab_indexes = config['tab_indexes']
+ show_ellipsis = config['show_ellipsis']
+ max_title_len = config['max_title_len']
+
+ # Unicode heavy strings do not like being truncated/sliced so by
+ # re-encoding the string sliced of limbs are removed.
+ self.tabtitle = self.title[:max_title_len + int(show_ellipsis)]
+ if type(self.tabtitle) != types.UnicodeType:
+ self.tabtitle = unicode(self.tabtitle, 'utf-8', 'ignore')
+
+ self.tabtitle = self.tabtitle.encode('utf-8', 'ignore').strip()
+
+ if show_ellipsis and len(self.tabtitle) != len(self.title):
+ self.tabtitle += "\xe2\x80\xa6"
+
+ gtk_tab_format = "%d %s"
+ index = self.parent.notebook.page_num(self.tab)
+ if tab_titles and tab_indexes:
+ self.parent.notebook.set_tab_label_text(self.tab,
+ gtk_tab_format % (index, self.tabtitle))
+ elif tab_titles:
+ self.parent.notebook.set_tab_label_text(self.tab, self.tabtitle)
+ else:
+ self.parent.notebook.set_tab_label_text(self.tab, str(index))
+
+ # If instance is current tab, update window title
+ if index == self.parent.notebook.get_current_page():
+ title_format = "%s - Uzbl Browser"
+ self.parent.window.set_title(title_format % self.title)
+
+ # Non-GTK tabs
+ if not gtk_only:
+ self.parent.update_tablist()
+
+
+ def set(self, key, val):
+ ''' Send the SET command to Uzbl '''
+
+ if self._client:
+ self._client.send('set %s = %s') #TODO: escape chars ?
+
+
+ def exit(self):
+ ''' Ask the Uzbl instance to close '''
+
+ if self._client:
+ self._client.send('exit')
+
+
+ def parse_command(self, cmd):
+ ''' Parse event givent by the Uzbl instance '''
+
+ type, _, args = cmd.split(" ", 2)
+ if type == "EVENT":
+ type, args = args.split(" ", 1)
+ if type == "TITLE_CHANGED":
+ self.title = args
+ self.title_changed()
+ elif type == "VARIABLE_SET":
+ var, _, val = args.split(" ", 2)
+ try:
+ val = int(val)
+ except:
+ pass
+
+ if var in UZBL_TABBED_VARS:
+ if config[var] != val:
+ config[var] = val
+ if var == "show_gtk_tabs":
+ self.parent.notebook.set_show_tabs(bool(val))
+ elif var == "show_tablist" or var == "tablist_top":
+ self.parent.update_tablist_display()
+ elif var == "gtk_tab_pos":
+ self.parent.update_gtk_tab_pos()
+ elif var == "status_background":
+ col = gtk.gdk.color_parse(config['status_background'])
+ self.parent.ebox.modify_bg(gtk.STATE_NORMAL, col)
+ elif var == "tab_titles" or var == "tab_indexes":
+ for tab in self.parent.notebook:
+ self.parent.tabs[tab].title_changed(True)
+
+ self.parent.update_tablist()
+ else:
+ config[var] = val
+
+ if var == "uri":
+ self.uri = var
+ self.parent.update_tablist()
+ elif type == "NEW_TAB":
+ self.parent.new_tab(args)
+ elif type == "NEXT_TAB":
+ if args:
+ self.parent.next_tab(int(args))
+ else:
+ self.parent.next_tab()
+ elif type == "PREV_TAB":
+ if args:
+ self.parent.prev_tab(int(args))
+ else:
+ self.parent.prev_tab()
+ elif type == "GOTO_TAB":
+ self.parent.goto_tab(int(args))
+ elif type == "FIRST_TAB":
+ self.parent.goto_tab(0)
+ elif type == "LAST_TAB":
+ self.parent.goto_tab(-1)
+ elif type == "PRESET_TABS":
+ self.parent.parse_command(["preset"] + args.split())
+ elif type == "BRING_TO_FRONT":
+ self.parent.window.present()
+ elif type == "CLEAN_TABS":
+ self.parent.clean_slate()
+ elif type == "EXIT_ALL_TABS":
+ self.parent.quitrequest()
+
+
+ def close(self):
+ '''The remote instance exited'''
+
+ if self._client:
+ self._client.close()
+ self._client = None
+
+
+class UzblTabbed:
+ '''A tabbed version of uzbl using gtk.Notebook'''
+
+ def __init__(self):
+ '''Create tablist, window and notebook.'''
+
+ self._timers = {}
+ self._buffer = ""
+ self._killed = False
+
+ # A list of the recently closed tabs
+ self._closed = []
+
+ # Holds metadata on the uzbl childen open.
+ self.tabs = {}
+
+ # Uzbl sockets (socket => SocketClient)
+ self.clients = {}
+
+ # Generates a unique id for uzbl socket filenames.
+ self.next_pid = counter().next
+
+ # Create main window
+ self.window = gtk.Window()
+ try:
+ window_size = map(int, config['window_size'].split(','))
+ self.window.set_default_size(*window_size)
+
+ except:
+ error("Invalid value for default_size in config file.")
+
+ self.window.set_title("Uzbl Browser")
+ self.window.set_border_width(0)
+
+ # Set main window icon
+ icon_path = config['icon_path']
+ if os.path.exists(icon_path):
+ self.window.set_icon(gtk.gdk.pixbuf_new_from_file(icon_path))
+
+ else:
+ icon_path = '/usr/share/uzbl/examples/data/uzbl.png'
+ if os.path.exists(icon_path):
+ self.window.set_icon(gtk.gdk.pixbuf_new_from_file(icon_path))
+
+ # Attach main window event handlers
+ self.window.connect("delete-event", self.quitrequest)
+
+ # Create tab list
+ vbox = gtk.VBox()
+ self.vbox = vbox
+ self.window.add(vbox)
+ ebox = gtk.EventBox()
+ self.ebox = ebox
+ self.tablist = gtk.Label()
+
+ self.tablist.set_use_markup(True)
+ self.tablist.set_justify(gtk.JUSTIFY_LEFT)
+ self.tablist.set_line_wrap(False)
+ self.tablist.set_selectable(False)
+ self.tablist.set_padding(2,2)
+ self.tablist.set_alignment(0,0)
+ self.tablist.set_ellipsize(pango.ELLIPSIZE_END)
+ self.tablist.set_text(" ")
+ self.tablist.show()
+ ebox.add(self.tablist)
+ ebox.show()
+ bgcolor = gtk.gdk.color_parse(config['status_background'])
+ ebox.modify_bg(gtk.STATE_NORMAL, bgcolor)
+
+ # Create notebook
+ self.notebook = gtk.Notebook()
+ self.notebook.set_show_tabs(config['show_gtk_tabs'])
+
+ # Set tab position
+ self.update_gtk_tab_pos()
+
+ self.notebook.set_show_border(False)
+ self.notebook.set_scrollable(True)
+ self.notebook.set_border_width(0)
+
+ self.notebook.connect("page-removed", self.tab_closed)
+ self.notebook.connect("switch-page", self.tab_changed)
+ self.notebook.connect("page-added", self.tab_opened)
+
+ self.notebook.show()
+ vbox.pack_start(self.notebook, True, True, 0)
+ vbox.reorder_child(self.notebook, 1)
+ self.update_tablist_display()
+
+ self.vbox.show()
+ self.window.show()
+ self.wid = self.notebook.window.xid
+
+ # Store information about the applications fifo and socket.
+ fifo_filename = 'uzbltabbed_%d.fifo' % os.getpid()
+ socket_filename = 'uzbltabbed_%d.socket' % os.getpid()
+ self._fifo = None
+ self._socket = None
+ self.fifo_path = os.path.join(config['fifo_dir'], fifo_filename)
+ self.socket_path = os.path.join(config['socket_dir'], socket_filename)
+
+ # Now initialise the fifo and the socket
+ self.init_fifo()
+ self.init_socket()
+
+ # If we are using sessions then load the last one if it exists.
+ if config['save_session']:
+ self.load_session()
+
+
+ def run(self):
+ '''UzblTabbed main function that calls the gtk loop.'''
+
+ if not self.clients and not SocketClient.instances_queue and not self.tabs:
+ self.new_tab()
+
+ gtk_refresh = int(config['gtk_refresh'])
+ if gtk_refresh < 100:
+ gtk_refresh = 100
+
+ # Make SIGTERM act orderly.
+ signal(SIGTERM, lambda signum, stack_frame: self.terminate(SIGTERM))
+
+ # Catch keyboard interrupts
+ signal(SIGINT, lambda signum, stack_frame: self.terminate(SIGINT))
+
+ try:
+ gtk.main()
+
+ except:
+ error("encounted error %r" % sys.exc_info()[1])
+
+ # Unlink fifo socket
+ self.unlink_fifo()
+ self.close_socket()
+
+ # Attempt to close all uzbl instances nicely.
+ self.quitrequest()
+
+ # Allow time for all the uzbl instances to quit.
+ time.sleep(1)
+
+ raise
+
+
+ def terminate(self, termsig=None):
+ '''Handle termination signals and exit safely and cleanly.'''
+
+ # Not required but at least it lets the user know what killed his
+ # browsing session.
+ if termsig == SIGTERM:
+ error("caught SIGTERM signal")
+
+ elif termsig == SIGINT:
+ error("caught keyboard interrupt")
+
+ else:
+ error("caught unknown signal")
+
+ error("commencing infanticide!")
+
+ # Sends the exit signal to all uzbl instances.
+ self.quitrequest()
+
+
+ def init_socket(self):
+ '''Create interprocess communication socket.'''
+
+ def accept(sock, condition):
+ '''A new uzbl instance was created'''
+
+ client, _ = sock.accept()
+ self.clients[client] = SocketClient(client)
+
+ return True
+
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ sock.bind(self.socket_path)
+ sock.listen(1)
+
+ # Add event handler for IO_IN event.
+ self._socket = (sock, io_add_watch(sock, IO_IN, accept))
+
+ echo("[socket] listening at %r" % self.socket_path)
+
+ # Add atexit register to destroy the socket on program termination.
+ atexit.register(self.close_socket)
+
+
+ def close_socket(self):
+ '''Close the socket when closing the application'''
+
+ if self._socket:
+ (fd, watcher) = self._socket
+ source_remove(watcher)
+ fd.close()
+ os.unlink(self.socket_path)
+ self._socket = None
+
+
+ def init_fifo(self):
+ '''Create interprocess communication fifo.'''
+
+ if os.path.exists(self.fifo_path):
+ if not os.access(self.fifo_path, os.F_OK | os.R_OK | os.W_OK):
+ os.mkfifo(self.fifo_path)
+
+ else:
+ basedir = os.path.dirname(self.fifo_path)
+ if not os.path.exists(basedir):
+ os.makedirs(basedir)
+
+ os.mkfifo(self.fifo_path)
+
+ # Add event handlers for IO_IN & IO_HUP events.
+ self.setup_fifo_watchers()
+
+ echo("[fifo] listening at %r" % self.fifo_path)
+
+ # Add atexit register to destroy the fifo on program termination.
+ atexit.register(self.unlink_fifo)
+
+
+ def unlink_fifo(self):
+ '''Unlink the fifo socket. Note: This function is called automatically
+ on exit by an atexit register.'''
+
+ # Make sure the fifo fd is closed.
+ self.close_fifo()
+
+ # And unlink if the real fifo exists.
+ if os.path.exists(self.fifo_path):
+ os.unlink(self.fifo_path)
+ echo("unlinked %r" % self.fifo_path)
+
+
+ def close_fifo(self):
+ '''Remove all event handlers watching the fifo and close the fd.'''
+
+ # Already closed
+ if self._fifo is None: return
+
+ (fd, watchers) = self._fifo
+ os.close(fd)
+
+ # Stop all gobject io watchers watching the fifo.
+ for gid in watchers:
+ source_remove(gid)
+
+ self._fifo = None
+
+
+ def setup_fifo_watchers(self):
+ '''Open fifo socket fd and setup gobject IO_IN & IO_HUP event
+ handlers.'''
+
+ # Close currently open fifo fd and kill all watchers
+ self.close_fifo()
+
+ fd = os.open(self.fifo_path, os.O_RDONLY | os.O_NONBLOCK)
+
+ # Add gobject io event handlers to the fifo socket.
+ watchers = [io_add_watch(fd, IO_IN, self.main_fifo_read),\
+ io_add_watch(fd, IO_HUP, self.main_fifo_hangup)]
+
+ self._fifo = (fd, watchers)
+
+
+ def main_fifo_hangup(self, fd, cb_condition):
+ '''Handle main fifo socket hangups.'''
+
+ # Close old fd, open new fifo socket and add io event handlers.
+ self.setup_fifo_watchers()
+
+ # Kill the gobject event handler calling this handler function.
+ return False
+
+
+ def main_fifo_read(self, fd, cb_condition):
+ '''Read from main fifo socket.'''
+
+ self._buffer = os.read(fd, 1024)
+ temp = self._buffer.split("\n")
+ self._buffer = temp.pop()
+ cmds = [s.strip().split() for s in temp if len(s.strip())]
+
+ for cmd in cmds:
+ try:
+ #print cmd
+ self.parse_command(cmd)
+
+ except:
+ error("parse_command: invalid command %s" % ' '.join(cmd))
+ raise
+
+ return True
+
+
+ def parse_command(self, cmd):
+ '''Parse instructions from uzbl child processes.'''
+
+ # Commands ( [] = optional, {} = required )
+ # new [uri]
+ # open new tab and head to optional uri.
+ # close [tab-num]
+ # close current tab or close via tab id.
+ # next [n-tabs]
+ # open next tab or n tabs down. Supports negative indexing.
+ # prev [n-tabs]
+ # open prev tab or n tabs down. Supports negative indexing.
+ # goto {tab-n}
+ # goto tab n.
+ # first
+ # goto first tab.
+ # last
+ # goto last tab.
+ # title {pid} {document-title}
+ # updates tablist title.
+ # uri {pid} {document-location}
+ # updates tablist uri
+ # bring_to_front
+ # brings the gtk window to focus.
+ # exit
+ # exits uzbl_tabbed.py
+
+ if cmd[0] == "new":
+ if len(cmd) == 2:
+ self.new_tab(cmd[1])
+
+ else:
+ self.new_tab()
+
+ elif cmd[0] == "newfromclip":
+ uri = subprocess.Popen(['xclip','-selection','clipboard','-o'],\
+ stdout=subprocess.PIPE).communicate()[0]
+ if uri:
+ self.new_tab(uri)
+
+ elif cmd[0] == "close":
+ if len(cmd) == 2:
+ self.close_tab(int(cmd[1]))
+
+ else:
+ self.close_tab()
+
+ elif cmd[0] == "next":
+ if len(cmd) == 2:
+ self.next_tab(int(cmd[1]))
+
+ else:
+ self.next_tab()
+
+ elif cmd[0] == "prev":
+ if len(cmd) == 2:
+ self.prev_tab(int(cmd[1]))
+
+ else:
+ self.prev_tab()
+
+ elif cmd[0] == "goto":
+ self.goto_tab(int(cmd[1]))
+
+ elif cmd[0] == "first":
+ self.goto_tab(0)
+
+ elif cmd[0] == "last":
+ self.goto_tab(-1)
+
+ elif cmd[0] in ["title", "uri"]:
+ if len(cmd) > 2:
+ uzbl = self.get_tab_by_name(int(cmd[1]))
+ if uzbl:
+ old = getattr(uzbl, cmd[0])
+ new = ' '.join(cmd[2:])
+ setattr(uzbl, cmd[0], new)
+ if old != new:
+ self.update_tablist()
+
+ else:
+ error("parse_command: no uzbl with name %r" % int(cmd[1]))
+
+ elif cmd[0] == "preset":
+ if len(cmd) < 3:
+ error("parse_command: invalid preset command")
+
+ elif cmd[1] == "save":
+ path = os.path.join(config['saved_sessions_dir'], cmd[2])
+ self.save_session(path)
+
+ elif cmd[1] == "load":
+ path = os.path.join(config['saved_sessions_dir'], cmd[2])
+ self.load_session(path)
+
+ elif cmd[1] == "del":
+ path = os.path.join(config['saved_sessions_dir'], cmd[2])
+ if os.path.isfile(path):
+ os.remove(path)
+
+ else:
+ error("parse_command: preset %r does not exist." % path)
+
+ elif cmd[1] == "list":
+ uzbl = self.get_tab_by_name(int(cmd[2]))
+ if uzbl:
+ if not os.path.isdir(config['saved_sessions_dir']):
+ js = "js alert('No saved presets.');"
+ uzbl._client.send(js)
+
+ else:
+ listdir = os.listdir(config['saved_sessions_dir'])
+ listdir = "\\n".join(listdir)
+ js = "js alert('Session presets:\\n\\n%s');" % listdir
+ uzbl._client.send(js)
+
+ else:
+ error("parse_command: unknown tab name.")
+
+ else:
+ error("parse_command: unknown parse command %r"\
+ % ' '.join(cmd))
+
+ elif cmd[0] == "bring_to_front":
+ self.window.present()
+
+ elif cmd[0] == "clean":
+ self.clean_slate()
+
+ elif cmd[0] == "exit":
+ self.quitrequest()
+
+ else:
+ error("parse_command: unknown command %r" % ' '.join(cmd))
+
+
+ def get_tab_by_name(self, name):
+ '''Return uzbl instance by name.'''
+
+ for (tab, uzbl) in self.tabs.items():
+ if uzbl.name == name:
+ return uzbl
+
+ return False
+
+
+ def new_tab(self, uri='', title='', switch=None):
+ '''Add a new tab to the notebook and start a new instance of uzbl.
+ Use the switch option to negate config['switch_to_new_tabs'] option
+ when you need to load multiple tabs at a time (I.e. like when
+ restoring a session from a file).'''
+
+ tab = gtk.Socket()
+ tab.show()
+ self.notebook.append_page(tab)
+ sid = tab.get_id()
+ uri = uri.strip()
+ name = "%d-%d" % (os.getpid(), self.next_pid())
+
+ if switch is None:
+ switch = config['switch_to_new_tabs']
+
+ if not title:
+ title = config['new_tab_title']
+
+ cmd = ['uzbl-browser', '-n', name, '-s', str(sid),
+ '--connect-socket', self.socket_path, '--uri', uri]
+ subprocess.Popen(cmd) # TODO: do i need close_fds=True ?
+
+ uzbl = UzblInstance(self, tab, name, uri, title, switch)
+ SocketClient.instances_queue[name] = uzbl
+ self.tabs[tab] = uzbl
+
+
+ def clean_slate(self):
+ '''Close all open tabs and open a fresh brand new one.'''
+
+ self.new_tab()
+ tabs = self.tabs.keys()
+ for tab in list(self.notebook)[:-1]:
+ if tab not in tabs: continue
+ uzbl = self.tabs[tab]
+ uzbl.exit()
+
+
+ def config_uzbl(self, uzbl):
+ '''Send bind commands for tab new/close/next/prev to a uzbl
+ instance.'''
+
+ # Set definitions here
+ # set(key, command back to fifo)
+ if config['capture_new_windows']:
+ uzbl.set("new_window", r'new $8')
+
+
+ def goto_tab(self, index):
+ '''Goto tab n (supports negative indexing).'''
+
+ title_format = "%s - Uzbl Browser"
+
+ tabs = list(self.notebook)
+ if 0 <= index < len(tabs):
+ self.notebook.set_current_page(index)
+ uzbl = self.tabs[self.notebook.get_nth_page(index)]
+ self.window.set_title(title_format % uzbl.title)
+ self.update_tablist()
+ return None
+
+ try:
+ tab = tabs[index]
+ # Update index because index might have previously been a
+ # negative index.
+ index = tabs.index(tab)
+ self.notebook.set_current_page(index)
+ uzbl = self.tabs[self.notebook.get_nth_page(index)]
+ self.window.set_title(title_format % uzbl.title)
+ self.update_tablist()
+
+ except IndexError:
+ pass
+
+
+ def next_tab(self, step=1):
+ '''Switch to next tab or n tabs right.'''
+
+ if step < 1:
+ error("next_tab: invalid step %r" % step)
+ return None
+
+ ntabs = self.notebook.get_n_pages()
+ tabn = (self.notebook.get_current_page() + step) % ntabs
+ self.goto_tab(tabn)
+
+
+ def prev_tab(self, step=1):
+ '''Switch to prev tab or n tabs left.'''
+
+ if step < 1:
+ error("prev_tab: invalid step %r" % step)
+ return None
+
+ ntabs = self.notebook.get_n_pages()
+ tabn = self.notebook.get_current_page() - step
+ while tabn < 0: tabn += ntabs
+ self.goto_tab(tabn)
+
+
+ def close_tab(self, tabn=None):
+ '''Closes current tab. Supports negative indexing.'''
+
+ if tabn is None:
+ tabn = self.notebook.get_current_page()
+
+ else:
+ try:
+ tab = list(self.notebook)[tabn]
+
+ except IndexError:
+ error("close_tab: invalid index %r" % tabn)
+ return None
+
+ self.notebook.remove_page(tabn)
+
+
+ def tab_opened(self, notebook, tab, index):
+ '''Called upon tab creation. Called by page-added signal.'''
+
+ if config['switch_to_new_tabs']:
+ self.notebook.set_focus_child(tab)
+
+ else:
+ oldindex = self.notebook.get_current_page()
+ oldtab = self.notebook.get_nth_page(oldindex)
+ self.notebook.set_focus_child(oldtab)
+
+
+ def tab_closed(self, notebook, tab, index):
+ '''Close the window if no tabs are left. Called by page-removed
+ signal.'''
+
+ if tab in self.tabs.keys():
+ uzbl = self.tabs[tab]
+ uzbl.close()
+
+ self._closed.append((uzbl.uri, uzbl.title))
+ self._closed = self._closed[-10:]
+ del self.tabs[tab]
+
+ if self.notebook.get_n_pages() == 0:
+ if not self._killed and config['save_session']:
+ if os.path.exists(config['session_file']):
+ os.remove(config['session_file'])
+
+ self.quit()
+
+ for tab in self.notebook:
+ self.tabs[tab].title_changed(True)
+ self.update_tablist()
+
+ return True
+
+
+ def tab_changed(self, notebook, page, index):
+ '''Refresh tab list. Called by switch-page signal.'''
+
+ tab = self.notebook.get_nth_page(index)
+ self.notebook.set_focus_child(tab)
+ self.update_tablist(index)
+ return True
+
+
+ def update_tablist_display(self):
+ '''Called when show_tablist or tablist_top has changed'''
+
+ if self.ebox in self.vbox.get_children():
+ self.vbox.remove(self.ebox)
+
+ if config['show_tablist']:
+ self.vbox.pack_start(self.ebox, False, False, 0)
+ if config['tablist_top']:
+ self.vbox.reorder_child(self.ebox, 0)
+ else:
+ self.vbox.reorder_child(self.ebox, 2)
+
+ def update_gtk_tab_pos(self):
+ ''' Called when gtk_tab_pos has changed '''
+
+ allposes = {'left': gtk.POS_LEFT, 'right':gtk.POS_RIGHT,
+ 'top':gtk.POS_TOP, 'bottom':gtk.POS_BOTTOM}
+ if config['gtk_tab_pos'] in allposes.keys():
+ self.notebook.set_tab_pos(allposes[config['gtk_tab_pos']])
+
+
+ def update_tablist(self, curpage=None):
+ '''Upate tablist status bar.'''
+
+ if not config['show_tablist']:
+ return True
+
+ tab_titles = config['tab_titles']
+ tab_indexes = config['tab_indexes']
+ multiline_tabs = config['multiline_tabs']
+
+ if multiline_tabs:
+ multiline = []
+
+ tabs = self.tabs.keys()
+ if curpage is None:
+ curpage = self.notebook.get_current_page()
+
+ pango = ""
+ normal = (config['tab_colours'], config['tab_text_colours'])
+ selected = (config['selected_tab'], config['selected_tab_text'])
+
+ if tab_titles and tab_indexes:
+ tab_format = "<span %(tabc)s> [ %(index)d <span %(textc)s> %(title)s</span> ] </span>"
+ elif tab_titles:
+ tab_format = "<span %(tabc)s> [ <span %(textc)s>%(title)s</span> ] </span>"
+ else:
+ tab_format = "<span %(tabc)s> [ <span %(textc)s>%(index)d</span> ] </span>"
+
+ for index, tab in enumerate(self.notebook):
+ if tab not in tabs: continue
+ uzbl = self.tabs[tab]
+ title = escape(uzbl.tabtitle)
+
+ style = colour_selector(index, curpage, uzbl)
+ (tabc, textc) = style
+
+ if multiline_tabs:
+ opango = pango
+
+ pango += tab_format % locals()
+
+ self.tablist.set_markup(pango)
+ listwidth = self.tablist.get_layout().get_pixel_size()[0]
+ winwidth = self.window.get_size()[0]
+
+ if listwidth > (winwidth - 20):
+ multiline.append(opango)
+ pango = tab_format % locals()
+ else:
+ pango += tab_format % locals()
+
+ if multiline_tabs:
+ multiline.append(pango)
+ self.tablist.set_markup('&#10;'.join(multiline))
+
+ else:
+ self.tablist.set_markup(pango)
+
+ return True
+
+
+ def save_session(self, session_file=None):
+ '''Save the current session to file for restoration on next load.'''
+
+ strip = str.strip
+
+ if session_file is None:
+ session_file = config['session_file']
+
+ tabs = self.tabs.keys()
+ state = []
+ for tab in list(self.notebook):
+ if tab not in tabs: continue
+ uzbl = self.tabs[tab]
+ if not uzbl.uri: continue
+ state += [(uzbl.uri, uzbl.title),]
+
+ session = {'curtab': self.notebook.get_current_page(),
+ 'tabs': state}
+
+ if config['json_session']:
+ raw = json.dumps(session)
+
+ else:
+ lines = ["curtab = %d" % session['curtab'],]
+ for (uri, title) in session['tabs']:
+ lines += ["%s\t%s" % (strip(uri), strip(title)),]
+
+ raw = "\n".join(lines)
+
+ if not os.path.isfile(session_file):
+ dirname = os.path.dirname(session_file)
+ if not os.path.isdir(dirname):
+ os.makedirs(dirname)
+
+ h = open(session_file, 'w')
+ h.write(raw)
+ h.close()
+
+
+ def load_session(self, session_file=None):
+ '''Load a saved session from file.'''
+
+ default_path = False
+ strip = str.strip
+ json_session = config['json_session']
+ delete_loaded = False
+
+ if session_file is None:
+ default_path = True
+ delete_loaded = True
+ session_file = config['session_file']
+
+ if not os.path.isfile(session_file):
+ return False
+
+ h = open(session_file, 'r')
+ raw = h.read()
+ h.close()
+ if json_session:
+ if sum([1 for s in raw.split("\n") if strip(s)]) != 1:
+ error("Warning: The session file %r does not look json. "\
+ "Trying to load it as a non-json session file."\
+ % session_file)
+ json_session = False
+
+ if json_session:
+ try:
+ session = json.loads(raw)
+ curtab, tabs = session['curtab'], session['tabs']
+
+ except:
+ error("Failed to load jsonifed session from %r"\
+ % session_file)
+ return None
+
+ else:
+ tabs = []
+ strip = str.strip
+ curtab, tabs = 0, []
+ lines = [s for s in raw.split("\n") if strip(s)]
+ if len(lines) < 2:
+ error("Warning: The non-json session file %r looks invalid."\
+ % session_file)
+ return None
+
+ try:
+ for line in lines:
+ if line.startswith("curtab"):
+ curtab = int(line.split()[-1])
+
+ else:
+ uri, title = line.split("\t",1)
+ tabs += [(strip(uri), strip(title)),]
+
+ except:
+ error("Warning: failed to load session file %r" % session_file)
+ return None
+
+ session = {'curtab': curtab, 'tabs': tabs}
+
+ # Now populate notebook with the loaded session.
+ for (index, (uri, title)) in enumerate(tabs):
+ self.new_tab(uri=uri, title=title, switch=(curtab==index))
+
+ # A saved session has been loaded now delete it.
+ if delete_loaded and os.path.exists(session_file):
+ os.remove(session_file)
+
+ # There may be other state information in the session dict of use to
+ # other functions. Of course however the non-json session object is
+ # just a dummy object of no use to no one.
+ return session
+
+
+ def quitrequest(self, *args):
+ '''Attempt to close all uzbl instances nicely and exit.'''
+
+ self._killed = True
+
+ if config['save_session']:
+ if len(list(self.notebook)) > 1:
+ self.save_session()
+
+ else:
+ # Notebook has one page open so delete the session file.
+ if os.path.isfile(config['session_file']):
+ os.remove(config['session_file'])
+
+ for (tab, uzbl) in self.tabs.items():
+ uzbl.exit()
+
+ # Add a gobject timer to make sure the application force-quits after a
+ # reasonable period. Calling quit when all the tabs haven't had time to
+ # close should be a last resort.
+ timer = "force-quit"
+ timerid = timeout_add(5000, self.quit, timer)
+ self._timers[timer] = timerid
+
+
+ def quit(self, *args):
+ '''Cleanup and quit. Called by delete-event signal.'''
+
+ # Close the fifo socket, remove any gobject io event handlers and
+ # delete socket.
+ self.unlink_fifo()
+ self.close_socket()
+
+ # Remove all gobject timers that are still ticking.
+ for (timerid, gid) in self._timers.items():
+ source_remove(gid)
+ del self._timers[timerid]
+
+ try:
+ gtk.main_quit()
+
+ except:
+ pass
+
+
+if __name__ == "__main__":
+
+ # Build command line parser
+ usage = "usage: %prog [OPTIONS] {URIS}..."
+ parser = OptionParser(usage=usage)
+ parser.add_option('-n', '--no-session', dest='nosession',
+ action='store_true', help="ignore session saving a loading.")
+ parser.add_option('-v', '--verbose', dest='verbose',
+ action='store_true', help='print verbose output.')
+
+ # Parse command line options
+ (options, uris) = parser.parse_args()
+
+ if options.nosession:
+ config['save_session'] = False
+
+ if options.verbose:
+ config['verbose'] = True
+
+ if config['json_session']:
+ try:
+ import simplejson as json
+
+ except:
+ error("Warning: json_session set but cannot import the python "\
+ "module simplejson. Fix: \"set json_session = 0\" or "\
+ "install the simplejson python module to remove this warning.")
+ config['json_session'] = False
+
+ if config['verbose']:
+ import pprint
+ sys.stderr.write("%s\n" % pprint.pformat(config))
+
+ uzbl = UzblTabbed()
+
+ # All extra arguments given to uzbl_tabbed.py are interpreted as
+ # web-locations to opened in new tabs.
+ lasturi = len(uris)-1
+ for (index,uri) in enumerate(uris):
+ uzbl.new_tab(uri, switch=(index==lasturi))
+
+ uzbl.run()
diff --git a/examples/data/scripts/uzblcat b/examples/data/scripts/uzblcat
new file mode 100755
index 0000000..e955608
--- /dev/null
+++ b/examples/data/scripts/uzblcat
@@ -0,0 +1,12 @@
+#!/usr/bin/env python
+# uzblcat - safely push html to uzbl
+# See http://www.uzbl.org/wiki/html-mode
+
+from sys import stdin, stdout
+
+stdout.write("uri data:text/html,")
+for line in stdin:
+ stdout.write(line[0:-1])
+
+# vim: set noet ff=unix
+
diff --git a/examples/data/style.css b/examples/data/style.css
new file mode 100644
index 0000000..f9b111e
--- /dev/null
+++ b/examples/data/style.css
@@ -0,0 +1,25 @@
+.uzbl_highlight { background-color: yellow;}
+.uzbl_h_first { background-color: lightgreen;}
+
+.uzbl_follow { border-style: dotted;
+ border-width: thin;
+}
+
+#uzbl_hint > div {
+ display: inline;
+ border: 2px solid #4a6600;
+ background-color: #b9ff00;
+ color: black;
+ font-size: 9px;
+ font-weight: bold;
+ line-height: 9px;
+ margin: 0px;
+ padding: 0px;
+ position: absolute;
+ z-index: 1000;
+ -webkit-border-radius: 6px;
+ text-decoration: none;
+ -wekit-transform: scale(1) rotate(0deg) translate(-6px,-5px);
+}
+
+/* vim:set et ts=4: */
diff --git a/examples/data/uzbl.png b/examples/data/uzbl.png
new file mode 100644
index 0000000..773ea84
--- /dev/null
+++ b/examples/data/uzbl.png
Binary files differ
diff --git a/misc/cleanprocs-files.sh b/misc/cleanprocs-files.sh
new file mode 100755
index 0000000..463d1fb
--- /dev/null
+++ b/misc/cleanprocs-files.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+killall uzbl; killall strace;
+killall -9 uzbl; killall -9 strace
+
+rm -rf /tmp/uzbl_*
+
+echo "Uzbl processes:"
+ps aux | grep uzbl | grep -v grep
+echo "Uzbl /tmp entries:"
+ls -alh /tmp/uzbl* 2>/dev/null
diff --git a/misc/dmenu-performancetest-bench.sh b/misc/dmenu-performancetest-bench.sh
new file mode 100755
index 0000000..c06df46
--- /dev/null
+++ b/misc/dmenu-performancetest-bench.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+echo "Run this test more then once. the first read on the file may be uncached. after that, the file is in Linux' block cache"
+
+echo "Plain awk '{print \$3}':"
+time awk '{print $3}' dummy_history_file >/dev/null
+
+echo "awk + sort"
+time awk '{print $3}' dummy_history_file | sort >/dev/null
+echo "awk + sort + uniq"
+time awk '{print $3}' dummy_history_file | sort | uniq >/dev/null
+
+echo "Plain dmenu:"
+dmenu < dummy_history_file
+echo "awked into dmenu:"
+awk '{print $3}' dummy_history_file | dmenu
+echo "awk + sort + uniq into dmenu:"
+awk '{print $3}' dummy_history_file | sort | uniq | dmenu
diff --git a/misc/dmenu-performancetest-generate-dummy-history-file.sh b/misc/dmenu-performancetest-generate-dummy-history-file.sh
new file mode 100755
index 0000000..17566bd
--- /dev/null
+++ b/misc/dmenu-performancetest-generate-dummy-history-file.sh
@@ -0,0 +1,12 @@
+#!/bin/bash
+echo "Creating dummy file of 50MB in size (625000 entries of 80chars)"
+echo "Note: this takes about an hour and a half"
+entries_per_iteration=1000
+for i in `seq 1 625`
+do
+ echo "Iteration $i of 625 ( $entries_per_iteration each )"
+ for j in `seq 1 $entries_per_iteration`
+ do
+ echo "`date +'%Y-%m-%d %H:%M:%S'` `date +%s`abcdefhijklmno`date +%s | md5sum`" >> ./dummy_history_file
+ done
+done
diff --git a/misc/env.sh b/misc/env.sh
new file mode 100755
index 0000000..0dfedfa
--- /dev/null
+++ b/misc/env.sh
@@ -0,0 +1,23 @@
+#!/bin/sh
+# This script is copied in to the sandbox/ directory during any sandbox make
+# target and is used to create an isolated environment for uzbl-core,
+# uzbl-browser and uzbl-tabbed to be run and tested.
+
+# It would be better to use something more flexible like $(dirname $0) but I
+# couldn't get to work nicely from the Makefile:
+# - Sourcing file gives $0 == /bin/sh
+# - Executing limits scope of variables too much (even with exporting)
+# Maybe we should spawn processes from here with an 'exec' at the end?
+
+# Re-define our home location inside the sandbox dir.
+export HOME=./sandbox/home
+
+# Export default XDG_{DATA,CACHE,..}_HOME locations inside the sandbox
+# directory according to defaults in the xdg specification.
+# <http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html>
+export XDG_DATA_HOME=$HOME/.local/share
+export XDG_CACHE_HOME=$HOME/.cache
+export XDG_CONFIG_HOME=$HOME/.config
+
+# Needed to run uzbl-browser etc from here.
+export PATH="./sandbox/usr/local/bin:$PATH"
diff --git a/misc/fifotest.sh b/misc/fifotest.sh
new file mode 100755
index 0000000..8ad0593
--- /dev/null
+++ b/misc/fifotest.sh
@@ -0,0 +1,20 @@
+#!/bin/bash
+if [ -z "$1" ]
+then
+ echo "Need fifo filename!" >&2
+ exit 2
+fi
+while :
+do
+ echo 'uri dns.be'
+ echo 'uri dns.be' > $1
+ sleep 2
+ echo 'uri www.archlinux.org'
+ echo 'uri www.archlinux.org' > $1
+ sleep 2
+ echo 'uri icanhascheezburger.com'
+ echo 'uri icanhascheezburger.com' > $1
+ sleep 2
+ echo 'back'
+ echo 'back' > $1
+done
diff --git a/misc/hash.sh b/misc/hash.sh
new file mode 100755
index 0000000..0c97722
--- /dev/null
+++ b/misc/hash.sh
@@ -0,0 +1,25 @@
+#!/bin/sh
+# script to determine git hash of current source tree
+
+# set a variable when running `git --archive <hash/tag>` (this is what github does)
+# alternatively, you could also git get-tar-commit-id < tarball (but that's a bit dirtier)
+FROM_ARCHIVE=$Format:%H$
+
+# ... but try to use whatever git tells us if there is a .git folder
+if [ -d .git -a -r .git ]
+then
+ hash=$(git log 2>/dev/null | head -n1 2>/dev/null | sed "s/.* //" 2>/dev/null)
+fi
+
+if [ x"$hash" != x ]
+then
+ echo $hash
+elif [ "$FROM_ARCHIVE" != ':%H$' ]
+then
+ echo $FROM_ARCHIVE
+else
+ echo "commit hash detection fail. Dear packager, please figure out what goes wrong or get in touch with us" >&2
+ echo UNKNOWN
+ exit 2
+fi
+exit 0
diff --git a/src/callbacks.c b/src/callbacks.c
new file mode 100644
index 0000000..9b90d3c
--- /dev/null
+++ b/src/callbacks.c
@@ -0,0 +1,781 @@
+/*
+ ** Callbacks
+ ** (c) 2009 by Robert Manea et al.
+*/
+
+#include "uzbl-core.h"
+#include "callbacks.h"
+#include "events.h"
+
+
+void
+set_proxy_url() {
+ SoupURI *suri;
+
+ if(uzbl.net.proxy_url == NULL || *uzbl.net.proxy_url == ' ') {
+ soup_session_remove_feature_by_type(uzbl.net.soup_session,
+ (GType) SOUP_SESSION_PROXY_URI);
+ }
+ else {
+ suri = soup_uri_new(uzbl.net.proxy_url);
+ g_object_set(G_OBJECT(uzbl.net.soup_session),
+ SOUP_SESSION_PROXY_URI,
+ suri, NULL);
+ soup_uri_free(suri);
+ }
+ return;
+}
+
+void
+set_authentication_handler() {
+ /* Check if WEBKIT_TYPE_SOUP_AUTH_DIALOG feature is set */
+ GSList *flist = soup_session_get_features (uzbl.net.soup_session, (GType) WEBKIT_TYPE_SOUP_AUTH_DIALOG);
+ guint feature_is_set = g_slist_length(flist);
+ g_slist_free(flist);
+
+ if (uzbl.behave.authentication_handler == NULL || *uzbl.behave.authentication_handler == NULL) {
+ if (!feature_is_set)
+ soup_session_add_feature_by_type
+ (uzbl.net.soup_session, (GType) WEBKIT_TYPE_SOUP_AUTH_DIALOG);
+ } else {
+ if (feature_is_set)
+ soup_session_remove_feature_by_type
+ (uzbl.net.soup_session, (GType) WEBKIT_TYPE_SOUP_AUTH_DIALOG);
+ }
+ return;
+}
+
+void
+set_icon() {
+ if(file_exists(uzbl.gui.icon)) {
+ if (uzbl.gui.main_window)
+ gtk_window_set_icon_from_file (GTK_WINDOW (uzbl.gui.main_window), uzbl.gui.icon, NULL);
+ } else {
+ g_printerr ("Icon \"%s\" not found. ignoring.\n", uzbl.gui.icon);
+ }
+}
+
+void
+cmd_set_geometry() {
+ int ret=0, x=0, y=0;
+ unsigned int w=0, h=0;
+ if(uzbl.gui.geometry) {
+ if(uzbl.gui.geometry[0] == 'm') { /* m/maximize/maximized */
+ gtk_window_maximize((GtkWindow *)(uzbl.gui.main_window));
+ } else {
+ /* we used to use gtk_window_parse_geometry() but that didn't work how it was supposed to */
+ ret = XParseGeometry(uzbl.gui.geometry, &x, &y, &w, &h);
+ if(ret & XValue)
+ gtk_window_move((GtkWindow *)uzbl.gui.main_window, x, y);
+ if(ret & WidthValue)
+ gtk_window_resize((GtkWindow *)uzbl.gui.main_window, w, h);
+ }
+ }
+
+ /* update geometry var with the actual geometry
+ this is necessary as some WMs don't seem to honour
+ the above setting and we don't want to end up with
+ wrong geometry information
+ */
+ retrieve_geometry();
+}
+
+void
+cmd_set_status() {
+ if (!uzbl.behave.show_status) {
+ gtk_widget_hide(uzbl.gui.mainbar);
+ } else {
+ gtk_widget_show(uzbl.gui.mainbar);
+ }
+ update_title();
+}
+
+void
+cmd_load_uri() {
+ load_uri_imp (uzbl.state.uri);
+}
+
+void
+cmd_max_conns() {
+ g_object_set(G_OBJECT(uzbl.net.soup_session),
+ SOUP_SESSION_MAX_CONNS, uzbl.net.max_conns, NULL);
+}
+
+void
+cmd_max_conns_host() {
+ g_object_set(G_OBJECT(uzbl.net.soup_session),
+ SOUP_SESSION_MAX_CONNS_PER_HOST, uzbl.net.max_conns_host, NULL);
+}
+
+void
+cmd_http_debug() {
+ soup_session_remove_feature
+ (uzbl.net.soup_session, SOUP_SESSION_FEATURE(uzbl.net.soup_logger));
+ /* do we leak if this doesn't get freed? why does it occasionally crash if freed? */
+ /*g_free(uzbl.net.soup_logger);*/
+
+ uzbl.net.soup_logger = soup_logger_new(uzbl.behave.http_debug, -1);
+ soup_session_add_feature(uzbl.net.soup_session,
+ SOUP_SESSION_FEATURE(uzbl.net.soup_logger));
+}
+
+WebKitWebSettings*
+view_settings() {
+ return webkit_web_view_get_settings(uzbl.gui.web_view);
+}
+
+void
+cmd_font_size() {
+ WebKitWebSettings *ws = view_settings();
+ if (uzbl.behave.font_size > 0) {
+ g_object_set (G_OBJECT(ws), "default-font-size", uzbl.behave.font_size, NULL);
+ }
+
+ if (uzbl.behave.monospace_size > 0) {
+ g_object_set (G_OBJECT(ws), "default-monospace-font-size",
+ uzbl.behave.monospace_size, NULL);
+ } else {
+ g_object_set (G_OBJECT(ws), "default-monospace-font-size",
+ uzbl.behave.font_size, NULL);
+ }
+}
+
+void
+cmd_default_font_family() {
+ g_object_set (G_OBJECT(view_settings()), "default-font-family",
+ uzbl.behave.default_font_family, NULL);
+}
+
+void
+cmd_monospace_font_family() {
+ g_object_set (G_OBJECT(view_settings()), "monospace-font-family",
+ uzbl.behave.monospace_font_family, NULL);
+}
+
+void
+cmd_sans_serif_font_family() {
+ g_object_set (G_OBJECT(view_settings()), "sans_serif-font-family",
+ uzbl.behave.sans_serif_font_family, NULL);
+}
+
+void
+cmd_serif_font_family() {
+ g_object_set (G_OBJECT(view_settings()), "serif-font-family",
+ uzbl.behave.serif_font_family, NULL);
+}
+
+void
+cmd_cursive_font_family() {
+ g_object_set (G_OBJECT(view_settings()), "cursive-font-family",
+ uzbl.behave.cursive_font_family, NULL);
+}
+
+void
+cmd_fantasy_font_family() {
+ g_object_set (G_OBJECT(view_settings()), "fantasy-font-family",
+ uzbl.behave.fantasy_font_family, NULL);
+}
+
+void
+cmd_zoom_level() {
+ webkit_web_view_set_zoom_level (uzbl.gui.web_view, uzbl.behave.zoom_level);
+}
+
+void
+cmd_disable_plugins() {
+ g_object_set (G_OBJECT(view_settings()), "enable-plugins",
+ !uzbl.behave.disable_plugins, NULL);
+}
+
+void
+cmd_disable_scripts() {
+ g_object_set (G_OBJECT(view_settings()), "enable-scripts",
+ !uzbl.behave.disable_scripts, NULL);
+}
+
+void
+cmd_minimum_font_size() {
+ g_object_set (G_OBJECT(view_settings()), "minimum-font-size",
+ uzbl.behave.minimum_font_size, NULL);
+}
+void
+cmd_autoload_img() {
+ g_object_set (G_OBJECT(view_settings()), "auto-load-images",
+ uzbl.behave.autoload_img, NULL);
+}
+
+
+void
+cmd_autoshrink_img() {
+ g_object_set (G_OBJECT(view_settings()), "auto-shrink-images",
+ uzbl.behave.autoshrink_img, NULL);
+}
+
+
+void
+cmd_enable_spellcheck() {
+ g_object_set (G_OBJECT(view_settings()), "enable-spell-checking",
+ uzbl.behave.enable_spellcheck, NULL);
+}
+
+void
+cmd_enable_private() {
+ g_object_set (G_OBJECT(view_settings()), "enable-private-browsing",
+ uzbl.behave.enable_private, NULL);
+}
+
+void
+cmd_print_bg() {
+ g_object_set (G_OBJECT(view_settings()), "print-backgrounds",
+ uzbl.behave.print_bg, NULL);
+}
+
+void
+cmd_style_uri() {
+ g_object_set (G_OBJECT(view_settings()), "user-stylesheet-uri",
+ uzbl.behave.style_uri, NULL);
+}
+
+void
+cmd_resizable_txt() {
+ g_object_set (G_OBJECT(view_settings()), "resizable-text-areas",
+ uzbl.behave.resizable_txt, NULL);
+}
+
+void
+cmd_default_encoding() {
+ g_object_set (G_OBJECT(view_settings()), "default-encoding",
+ uzbl.behave.default_encoding, NULL);
+}
+
+void
+cmd_enforce_96dpi() {
+ g_object_set (G_OBJECT(view_settings()), "enforce-96-dpi",
+ uzbl.behave.enforce_96dpi, NULL);
+}
+
+void
+cmd_caret_browsing() {
+ g_object_set (G_OBJECT(view_settings()), "enable-caret-browsing",
+ uzbl.behave.caret_browsing, NULL);
+}
+
+void
+cmd_fifo_dir() {
+ uzbl.behave.fifo_dir = init_fifo(uzbl.behave.fifo_dir);
+}
+
+void
+cmd_socket_dir() {
+ uzbl.behave.socket_dir = init_socket(uzbl.behave.socket_dir);
+}
+
+void
+cmd_inject_html() {
+ if(uzbl.behave.inject_html) {
+ webkit_web_view_load_html_string (uzbl.gui.web_view,
+ uzbl.behave.inject_html, NULL);
+ }
+}
+
+void
+cmd_useragent() {
+ if (*uzbl.net.useragent == ' ') {
+ g_free (uzbl.net.useragent);
+ uzbl.net.useragent = NULL;
+ } else {
+ g_object_set(G_OBJECT(uzbl.net.soup_session), SOUP_SESSION_USER_AGENT,
+ uzbl.net.useragent, NULL);
+ }
+}
+
+void
+cmd_scrollbars_visibility() {
+ if(uzbl.gui.scrollbars_visible) {
+ uzbl.gui.bar_h = gtk_scrolled_window_get_hadjustment (GTK_SCROLLED_WINDOW (uzbl.gui.scrolled_win));
+ uzbl.gui.bar_v = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (uzbl.gui.scrolled_win));
+ }
+ else {
+ uzbl.gui.bar_v = gtk_range_get_adjustment (GTK_RANGE (uzbl.gui.scbar_v));
+ uzbl.gui.bar_h = gtk_range_get_adjustment (GTK_RANGE (uzbl.gui.scbar_h));
+ }
+ gtk_widget_set_scroll_adjustments (GTK_WIDGET (uzbl.gui.web_view), uzbl.gui.bar_h, uzbl.gui.bar_v);
+}
+
+/* requires webkit >=1.1.14 */
+void
+cmd_view_source() {
+ webkit_web_view_set_view_source_mode(uzbl.gui.web_view,
+ (gboolean) uzbl.behave.view_source);
+}
+
+void
+cmd_set_zoom_type () {
+ if(uzbl.behave.zoom_type)
+ webkit_web_view_set_full_content_zoom (uzbl.gui.web_view, TRUE);
+ else
+ webkit_web_view_set_full_content_zoom (uzbl.gui.web_view, FALSE);
+}
+
+void
+toggle_zoom_type (WebKitWebView* page, GArray *argv, GString *result) {
+ (void)argv;
+ (void)result;
+
+ webkit_web_view_set_full_content_zoom (page, !webkit_web_view_get_full_content_zoom (page));
+}
+
+void
+toggle_status_cb (WebKitWebView* page, GArray *argv, GString *result) {
+ (void)page;
+ (void)argv;
+ (void)result;
+
+ if (uzbl.behave.show_status) {
+ gtk_widget_hide(uzbl.gui.mainbar);
+ } else {
+ gtk_widget_show(uzbl.gui.mainbar);
+ }
+ uzbl.behave.show_status = !uzbl.behave.show_status;
+ update_title();
+}
+
+void
+link_hover_cb (WebKitWebView* page, const gchar* title, const gchar* link, gpointer data) {
+ (void) page;
+ (void) title;
+ (void) data;
+ State *s = &uzbl.state;
+
+ if(s->selected_url) {
+ if(s->last_selected_url)
+ g_free(s->last_selected_url);
+ s->last_selected_url = g_strdup(s->selected_url);
+ }
+ else {
+ if(s->last_selected_url) g_free(s->last_selected_url);
+ s->last_selected_url = NULL;
+ }
+
+ g_free(s->selected_url);
+ s->selected_url = NULL;
+
+ if (link) {
+ s->selected_url = g_strdup(link);
+
+ if(s->last_selected_url &&
+ g_strcmp0(s->selected_url, s->last_selected_url))
+ send_event(LINK_UNHOVER, s->last_selected_url, NULL);
+
+ send_event(LINK_HOVER, s->selected_url, NULL);
+ }
+ else if(s->last_selected_url) {
+ send_event(LINK_UNHOVER, s->last_selected_url, NULL);
+ }
+
+ update_title();
+}
+
+void
+title_change_cb (WebKitWebView* web_view, GParamSpec param_spec) {
+ (void) web_view;
+ (void) param_spec;
+ const gchar *title = webkit_web_view_get_title(web_view);
+ if (uzbl.gui.main_title)
+ g_free (uzbl.gui.main_title);
+ uzbl.gui.main_title = title ? g_strdup (title) : g_strdup ("(no title)");
+ update_title();
+ send_event(TITLE_CHANGED, uzbl.gui.main_title, NULL);
+}
+
+void
+progress_change_cb (WebKitWebView* page, gint progress, gpointer data) {
+ (void) page;
+ (void) data;
+ gchar *prg_str;
+
+ prg_str = itos(progress);
+ send_event(LOAD_PROGRESS, prg_str, NULL);
+ g_free(prg_str);
+}
+
+void
+selection_changed_cb(WebKitWebView *webkitwebview, gpointer ud) {
+ (void)ud;
+ gchar *tmp;
+
+ webkit_web_view_copy_clipboard(webkitwebview);
+ tmp = gtk_clipboard_wait_for_text(gtk_clipboard_get (GDK_SELECTION_CLIPBOARD));
+ send_event(SELECTION_CHANGED, tmp, NULL);
+ g_free(tmp);
+}
+
+void
+load_finish_cb (WebKitWebView* page, WebKitWebFrame* frame, gpointer data) {
+ (void) page;
+ (void) data;
+
+ send_event(LOAD_FINISH, webkit_web_frame_get_uri(frame), NULL);
+}
+
+void
+load_start_cb (WebKitWebView* page, WebKitWebFrame* frame, gpointer data) {
+ (void) page;
+ (void) frame;
+ (void) data;
+
+ send_event(LOAD_START, uzbl.state.uri, NULL);
+}
+
+void
+load_error_cb (WebKitWebView* page, WebKitWebFrame* frame, gchar *uri, gpointer web_err, gpointer ud) {
+ (void) page;
+ (void) frame;
+ (void) ud;
+ GError *err = web_err;
+ gchar *details;
+
+ details = g_strdup_printf("%s %d:%s", uri, err->code, err->message);
+ send_event(LOAD_ERROR, details, NULL);
+ g_free(details);
+}
+
+void
+load_commit_cb (WebKitWebView* page, WebKitWebFrame* frame, gpointer data) {
+ (void) page;
+ (void) data;
+ g_free (uzbl.state.uri);
+ GString* newuri = g_string_new (webkit_web_frame_get_uri (frame));
+ uzbl.state.uri = g_string_free (newuri, FALSE);
+
+ send_event(LOAD_COMMIT, webkit_web_frame_get_uri (frame), NULL);
+}
+
+void
+destroy_cb (GtkWidget* widget, gpointer data) {
+ (void) widget;
+ (void) data;
+ gtk_main_quit ();
+}
+
+gboolean
+configure_event_cb(GtkWidget* window, GdkEventConfigure* event) {
+ (void) window;
+ (void) event;
+ gchar *lastgeo = NULL;
+
+ lastgeo = g_strdup(uzbl.gui.geometry);
+ retrieve_geometry();
+
+ if(strcmp(lastgeo, uzbl.gui.geometry))
+ send_event(GEOMETRY_CHANGED, uzbl.gui.geometry, NULL);
+ g_free(lastgeo);
+
+ return FALSE;
+}
+
+gboolean
+focus_cb(GtkWidget* window, GdkEventFocus* event, void *ud) {
+ (void) window;
+ (void) event;
+ (void) ud;
+
+ if(event->in)
+ send_event(FOCUS_GAINED, "", NULL);
+ else
+ send_event(FOCUS_LOST, "", NULL);
+
+ return FALSE;
+}
+
+gboolean
+key_press_cb (GtkWidget* window, GdkEventKey* event) {
+ (void) window;
+
+ if(event->type == GDK_KEY_PRESS)
+ key_to_event(event->keyval, GDK_KEY_PRESS);
+
+ return uzbl.behave.forward_keys ? FALSE : TRUE;
+}
+
+gboolean
+key_release_cb (GtkWidget* window, GdkEventKey* event) {
+ (void) window;
+
+ if(event->type == GDK_KEY_RELEASE)
+ key_to_event(event->keyval, GDK_KEY_RELEASE);
+
+ return uzbl.behave.forward_keys ? FALSE : TRUE;
+}
+
+gboolean
+button_press_cb (GtkWidget* window, GdkEventButton* event) {
+ (void) window;
+ gint context;
+ gchar *details;
+ gboolean propagate = FALSE,
+ sendev = FALSE;
+
+ if(event->type == GDK_BUTTON_PRESS) {
+ if(uzbl.state.last_button)
+ gdk_event_free((GdkEvent *)uzbl.state.last_button);
+ uzbl.state.last_button = (GdkEventButton *)gdk_event_copy((GdkEvent *)event);
+
+ context = get_click_context(NULL);
+ /* left click */
+ if(event->button == 1) {
+ if((context & WEBKIT_HIT_TEST_RESULT_CONTEXT_EDITABLE))
+ send_event(FORM_ACTIVE, "button1", NULL);
+ else if((context & WEBKIT_HIT_TEST_RESULT_CONTEXT_DOCUMENT))
+ send_event(ROOT_ACTIVE, "button1", NULL);
+ }
+ else if(event->button == 2 && !(context & WEBKIT_HIT_TEST_RESULT_CONTEXT_EDITABLE)) {
+ sendev = TRUE;
+ propagate = TRUE;
+ }
+ else if(event->button > 3) {
+ sendev = TRUE;
+ propagate = TRUE;
+ }
+
+ if(sendev) {
+ details = g_strdup_printf("Button%d", event->button);
+ send_event(KEY_PRESS, details, NULL);
+ g_free(details);
+ }
+ }
+
+ return propagate;
+}
+
+gboolean
+button_release_cb (GtkWidget* window, GdkEventButton* event) {
+ (void) window;
+ gint context;
+ gchar *details;
+ gboolean propagate = FALSE,
+ sendev = FALSE;
+
+ context = get_click_context(NULL);
+ if(event->type == GDK_BUTTON_RELEASE) {
+ if(event->button == 2 && !(context & WEBKIT_HIT_TEST_RESULT_CONTEXT_EDITABLE)) {
+ sendev = TRUE;
+ propagate = TRUE;
+ }
+ else if(event->button > 3) {
+ sendev = TRUE;
+ propagate = TRUE;
+ }
+
+ if(sendev) {
+ details = g_strdup_printf("Button%d", event->button);
+ send_event(KEY_RELEASE, details, NULL);
+ g_free(details);
+ }
+ }
+
+ return propagate;
+}
+
+gboolean
+motion_notify_cb(GtkWidget* window, GdkEventMotion* event, gpointer user_data) {
+ (void) window;
+ (void) event;
+ (void) user_data;
+
+ gchar *details;
+ details = g_strdup_printf("%.0lf %.0lf %u", event->x, event->y, event->state);
+ send_event(PTR_MOVE, details, NULL);
+
+ return FALSE;
+}
+
+gboolean
+navigation_decision_cb (WebKitWebView *web_view, WebKitWebFrame *frame, WebKitNetworkRequest *request, WebKitWebNavigationAction *navigation_action, WebKitWebPolicyDecision *policy_decision, gpointer user_data) {
+ (void) web_view;
+ (void) frame;
+ (void) navigation_action;
+ (void) user_data;
+
+ const gchar* uri = webkit_network_request_get_uri (request);
+ gboolean decision_made = FALSE;
+
+ if (uzbl.state.verbose)
+ printf("Navigation requested -> %s\n", uri);
+
+ if (uzbl.behave.scheme_handler) {
+ GString *s = g_string_new ("");
+ g_string_printf(s, "'%s'", uri);
+
+ run_handler(uzbl.behave.scheme_handler, s->str);
+
+ if(uzbl.comm.sync_stdout && strcmp (uzbl.comm.sync_stdout, "") != 0) {
+ char *p = strchr(uzbl.comm.sync_stdout, '\n' );
+ if ( p != NULL ) *p = '\0';
+ if (!strcmp(uzbl.comm.sync_stdout, "USED")) {
+ webkit_web_policy_decision_ignore(policy_decision);
+ decision_made = TRUE;
+ }
+ }
+ if (uzbl.comm.sync_stdout)
+ uzbl.comm.sync_stdout = strfree(uzbl.comm.sync_stdout);
+
+ g_string_free(s, TRUE);
+ }
+ if (!decision_made)
+ webkit_web_policy_decision_use(policy_decision);
+
+ return TRUE;
+}
+
+gboolean
+new_window_cb (WebKitWebView *web_view, WebKitWebFrame *frame, WebKitNetworkRequest *request, WebKitWebNavigationAction *navigation_action, WebKitWebPolicyDecision *policy_decision, gpointer user_data) {
+ (void) web_view;
+ (void) frame;
+ (void) navigation_action;
+ (void) policy_decision;
+ (void) user_data;
+ const gchar* uri = webkit_network_request_get_uri (request);
+ if (uzbl.state.verbose)
+ printf("New window requested -> %s \n", uri);
+ webkit_web_policy_decision_use(policy_decision);
+ send_event(NEW_WINDOW, uri, NULL);
+ return TRUE;
+}
+
+gboolean
+mime_policy_cb(WebKitWebView *web_view, WebKitWebFrame *frame, WebKitNetworkRequest *request, gchar *mime_type, WebKitWebPolicyDecision *policy_decision, gpointer user_data) {
+ (void) frame;
+ (void) request;
+ (void) user_data;
+
+ /* If we can display it, let's display it... */
+ if (webkit_web_view_can_show_mime_type (web_view, mime_type)) {
+ webkit_web_policy_decision_use (policy_decision);
+ return TRUE;
+ }
+
+ /* ...everything we can't display is downloaded */
+ webkit_web_policy_decision_download (policy_decision);
+ return TRUE;
+}
+
+void
+request_starting_cb(WebKitWebView *web_view, WebKitWebFrame *frame, WebKitWebResource *resource,
+ WebKitNetworkRequest *request, WebKitNetworkResponse *response, gpointer user_data) {
+ (void) web_view;
+ (void) frame;
+ (void) resource;
+ (void) response;
+ (void) user_data;
+
+ send_event(REQUEST_STARTING, webkit_network_request_get_uri(request), NULL);
+}
+
+/*@null@*/ WebKitWebView*
+create_web_view_cb (WebKitWebView *web_view, WebKitWebFrame *frame, gpointer user_data) {
+ (void) web_view;
+ (void) frame;
+ (void) user_data;
+ if (uzbl.state.selected_url != NULL) {
+ if (uzbl.state.verbose)
+ printf("\nNew web view -> %s\n",uzbl.state.selected_url);
+ new_window_load_uri(uzbl.state.selected_url);
+ } else {
+ if (uzbl.state.verbose)
+ printf("New web view -> %s\n","Nothing to open, exiting");
+ }
+ return (NULL);
+}
+
+gboolean
+download_cb (WebKitWebView *web_view, GObject *download, gpointer user_data) {
+ (void) web_view;
+ (void) user_data;
+ if (uzbl.behave.download_handler) {
+ const gchar* uri = webkit_download_get_uri ((WebKitDownload*)download);
+ if (uzbl.state.verbose)
+ printf("Download -> %s\n",uri);
+
+ /* if urls not escaped, we may have to escape and quote uri before this call */
+ GString *args = g_string_new(uri);
+
+ if (uzbl.net.proxy_url) {
+ g_string_append_c(args, ' ');
+ g_string_append(args, uzbl.net.proxy_url);
+ }
+
+ run_handler(uzbl.behave.download_handler, args->str);
+
+ g_string_free(args, TRUE);
+ }
+ send_event(DOWNLOAD_REQ, webkit_download_get_uri ((WebKitDownload*)download), NULL);
+ return (FALSE);
+}
+
+void
+run_menu_command(GtkWidget *menu, const char *line) {
+ (void) menu;
+
+ parse_cmd_line(line, NULL);
+}
+
+
+void
+populate_popup_cb(WebKitWebView *v, GtkMenu *m, void *c) {
+ (void) v;
+ (void) c;
+ GUI *g = &uzbl.gui;
+ GtkWidget *item;
+ MenuItem *mi;
+ guint i=0;
+ gint context, hit=0;
+
+ if(!g->menu_items)
+ return;
+
+ /* check context */
+ if((context = get_click_context(NULL)) == -1)
+ return;
+
+
+ for(i=0; i < uzbl.gui.menu_items->len; i++) {
+ hit = 0;
+ mi = g_ptr_array_index(uzbl.gui.menu_items, i);
+
+ if((mi->context > WEBKIT_HIT_TEST_RESULT_CONTEXT_DOCUMENT) &&
+ (context & mi->context)) {
+ if(mi->issep) {
+ item = gtk_separator_menu_item_new();
+ gtk_menu_append(GTK_MENU(m), item);
+ gtk_widget_show(item);
+ }
+ else {
+ item = gtk_menu_item_new_with_label(mi->name);
+ g_signal_connect(item, "activate",
+ G_CALLBACK(run_menu_command), mi->cmd);
+ gtk_menu_append(GTK_MENU(m), item);
+ gtk_widget_show(item);
+ }
+ hit++;
+ }
+
+ if((mi->context == WEBKIT_HIT_TEST_RESULT_CONTEXT_DOCUMENT) &&
+ (context <= WEBKIT_HIT_TEST_RESULT_CONTEXT_DOCUMENT) &&
+ !hit) {
+ if(mi->issep) {
+ item = gtk_separator_menu_item_new();
+ gtk_menu_append(GTK_MENU(m), item);
+ gtk_widget_show(item);
+ }
+ else {
+ item = gtk_menu_item_new_with_label(mi->name);
+ g_signal_connect(item, "activate",
+ G_CALLBACK(run_menu_command), mi->cmd);
+ gtk_menu_append(GTK_MENU(m), item);
+ gtk_widget_show(item);
+ }
+ }
+ }
+}
+
diff --git a/src/callbacks.h b/src/callbacks.h
new file mode 100644
index 0000000..6ed8986
--- /dev/null
+++ b/src/callbacks.h
@@ -0,0 +1,210 @@
+/*
+ ** Callbacks
+ ** (c) 2009 by Robert Manea et al.
+*/
+
+void
+cmd_load_uri();
+
+void
+cmd_set_status();
+
+void
+set_proxy_url();
+
+void
+set_authentication_handler();
+
+void
+set_icon();
+
+void
+move_statusbar();
+
+void
+cmd_http_debug();
+
+void
+cmd_max_conns();
+
+void
+cmd_max_conns_host();
+
+/* exported WebKitWebSettings properties */
+void
+cmd_font_size();
+
+void
+cmd_default_font_family();
+
+void
+cmd_monospace_font_family();
+
+void
+cmd_sans_serif_font_family();
+
+void
+cmd_serif_font_family();
+
+void
+cmd_cursive_font_family();
+
+void
+cmd_fantasy_font_family();
+
+void
+cmd_zoom_level();
+
+void
+cmd_set_zoom_type();
+
+void
+cmd_disable_plugins();
+
+void
+cmd_disable_scripts();
+
+void
+cmd_minimum_font_size();
+
+void
+cmd_fifo_dir();
+
+void
+cmd_socket_dir();
+
+void
+cmd_useragent() ;
+
+void
+cmd_autoload_img();
+
+void
+cmd_autoshrink_img();
+
+void
+cmd_enable_spellcheck();
+
+void
+cmd_enable_private();
+
+void
+cmd_print_bg();
+
+void
+cmd_style_uri();
+
+void
+cmd_resizable_txt();
+
+void
+cmd_default_encoding();
+
+void
+cmd_enforce_96dpi();
+
+void
+cmd_inject_html();
+
+void
+cmd_caret_browsing();
+
+void
+cmd_set_geometry();
+
+void
+cmd_view_source();
+
+void
+cmd_scrollbars_visibility();
+
+void
+cmd_load_start();
+
+WebKitWebSettings*
+view_settings();
+
+gboolean
+download_cb (WebKitWebView *web_view, GObject *download, gpointer user_data);
+
+void
+toggle_zoom_type (WebKitWebView* page, GArray *argv, GString *result);
+
+void
+toggle_status_cb (WebKitWebView* page, GArray *argv, GString *result);
+
+void
+link_hover_cb (WebKitWebView* page, const gchar* title, const gchar* link, gpointer data);
+
+void
+title_change_cb (WebKitWebView* web_view, GParamSpec param_spec);
+
+void
+progress_change_cb (WebKitWebView* page, gint progress, gpointer data);
+
+void
+load_commit_cb (WebKitWebView* page, WebKitWebFrame* frame, gpointer data);
+
+void
+load_start_cb (WebKitWebView* page, WebKitWebFrame* frame, gpointer data);
+
+void
+load_finish_cb (WebKitWebView* page, WebKitWebFrame* frame, gpointer data);
+
+void
+load_error_cb (WebKitWebView* page, WebKitWebFrame* frame, gchar *uri, gpointer web_err, gpointer ud);
+
+void
+selection_changed_cb(WebKitWebView *webkitwebview, gpointer ud);
+
+void
+destroy_cb (GtkWidget* widget, gpointer data);
+
+gboolean
+configure_event_cb(GtkWidget* window, GdkEventConfigure* event);
+
+gboolean
+key_press_cb (GtkWidget* window, GdkEventKey* event);
+
+gboolean
+key_release_cb (GtkWidget* window, GdkEventKey* event);
+
+gboolean
+motion_notify_cb(GtkWidget* window, GdkEventMotion* event, gpointer user_data);
+
+gboolean
+navigation_decision_cb (WebKitWebView *web_view, WebKitWebFrame *frame,
+ WebKitNetworkRequest *request, WebKitWebNavigationAction *navigation_action,
+ WebKitWebPolicyDecision *policy_decision, gpointer user_data);
+
+gboolean
+new_window_cb (WebKitWebView *web_view, WebKitWebFrame *frame,
+ WebKitNetworkRequest *request, WebKitWebNavigationAction *navigation_action,
+ WebKitWebPolicyDecision *policy_decision, gpointer user_data);
+
+gboolean
+mime_policy_cb(WebKitWebView *web_view, WebKitWebFrame *frame, WebKitNetworkRequest *request,
+ gchar *mime_type, WebKitWebPolicyDecision *policy_decision, gpointer user_data);
+
+void
+request_starting_cb(WebKitWebView *web_view, WebKitWebFrame *frame, WebKitWebResource *resource,
+ WebKitNetworkRequest *request, WebKitNetworkResponse *response, gpointer user_data);
+
+/*@null@*/ WebKitWebView*
+create_web_view_cb (WebKitWebView *web_view, WebKitWebFrame *frame, gpointer user_data);
+
+gboolean
+download_cb (WebKitWebView *web_view, GObject *download, gpointer user_data);
+
+void
+populate_popup_cb(WebKitWebView *v, GtkMenu *m, void *c);
+
+gboolean
+button_press_cb (GtkWidget* window, GdkEventButton* event);
+
+gboolean
+button_release_cb (GtkWidget* window, GdkEventButton* event);
+
+gboolean
+focus_cb(GtkWidget* window, GdkEventFocus* event, void *ud);
+
diff --git a/src/config.h b/src/config.h
new file mode 100644
index 0000000..7c4bae5
--- /dev/null
+++ b/src/config.h
@@ -0,0 +1,11 @@
+const struct {
+ /*@null@*/ char *command;
+} default_config[] = {
+{ "set status_format = <b>\\@[\\@TITLE]\\@</b> - \\@[\\@uri]\\@ - <span foreground=\"#bbb\">\\@NAME</span>" },
+{ "set show_status = 1" },
+{ "set title_format_long = \\@keycmd \\@TITLE - Uzbl browser <\\@NAME> > \\@SELECTED_URI" },
+{ "set title_format_short = \\@TITLE - Uzbl browser <\\@NAME>" },
+{ "set max_conns = 100" }, /* WebkitGTK default: 10 */
+{ "set max_conns_host = 6" }, /* WebkitGTK default: 2 */
+{ NULL }
+};
diff --git a/src/events.c b/src/events.c
new file mode 100644
index 0000000..1028c30
--- /dev/null
+++ b/src/events.c
@@ -0,0 +1,208 @@
+/*
+ ** Uzbl event routines
+ ** (c) 2009 by Robert Manea
+*/
+
+#include "uzbl-core.h"
+#include "events.h"
+
+UzblCore uzbl;
+
+/* Event id to name mapping
+ * Event names must be in the same
+ * order as in 'enum event_type'
+ *
+ * TODO: Add more useful events
+*/
+const char *event_table[LAST_EVENT] = {
+ "LOAD_START" ,
+ "LOAD_COMMIT" ,
+ "LOAD_FINISH" ,
+ "LOAD_ERROR" ,
+ "REQUEST_STARTING" ,
+ "KEY_PRESS" ,
+ "KEY_RELEASE" ,
+ "DOWNLOAD_REQUEST" ,
+ "COMMAND_EXECUTED" ,
+ "LINK_HOVER" ,
+ "TITLE_CHANGED" ,
+ "GEOMETRY_CHANGED" ,
+ "WEBINSPECTOR" ,
+ "NEW_WINDOW" ,
+ "SELECTION_CHANGED",
+ "VARIABLE_SET" ,
+ "FIFO_SET" ,
+ "SOCKET_SET" ,
+ "INSTANCE_START" ,
+ "INSTANCE_EXIT" ,
+ "LOAD_PROGRESS" ,
+ "LINK_UNHOVER" ,
+ "FORM_ACTIVE" ,
+ "ROOT_ACTIVE" ,
+ "FOCUS_LOST" ,
+ "FOCUS_GAINED" ,
+ "FILE_INCLUDED" ,
+ "PLUG_CREATED" ,
+ "COMMAND_ERROR" ,
+ "BUILTINS" ,
+ "PTR_MOVE"
+};
+
+void
+event_buffer_timeout(guint sec) {
+ struct itimerval t;
+ memset(&t, 0, sizeof t);
+ t.it_value.tv_sec = sec;
+ t.it_value.tv_usec = 0;
+ setitimer(ITIMER_REAL, &t, NULL);
+}
+
+
+void
+send_event_socket(GString *msg) {
+ GError *error = NULL;
+ GString *tmp;
+ GIOChannel *gio = NULL;
+ GIOStatus ret;
+ gsize len;
+ guint i=0, j=0;
+
+ /* write to all --connect-socket sockets */
+ if(uzbl.comm.connect_chan) {
+ while(i < uzbl.comm.connect_chan->len) {
+ gio = g_ptr_array_index(uzbl.comm.connect_chan, i++);
+ j=0;
+
+ if(gio && gio->is_writeable) {
+ if(uzbl.state.event_buffer) {
+ event_buffer_timeout(0);
+
+ /* replay buffered events */
+ while(j < uzbl.state.event_buffer->len) {
+ tmp = g_ptr_array_index(uzbl.state.event_buffer, j++);
+ ret = g_io_channel_write_chars (gio,
+ tmp->str, tmp->len,
+ &len, &error);
+
+ if (ret == G_IO_STATUS_ERROR)
+ g_warning ("Error sending event to socket: %s", error->message);
+ else
+ g_io_channel_flush(gio, &error);
+ }
+ }
+
+ if(msg) {
+ ret = g_io_channel_write_chars (gio,
+ msg->str, msg->len,
+ &len, &error);
+
+ if (ret == G_IO_STATUS_ERROR)
+ g_warning ("Error sending event to socket: %s", error->message);
+ else
+ g_io_channel_flush(gio, &error);
+ }
+ }
+ }
+ if(uzbl.state.event_buffer) {
+ g_ptr_array_free(uzbl.state.event_buffer, TRUE);
+ uzbl.state.event_buffer = NULL;
+ }
+ }
+ /* buffer events until a socket is set and connected
+ * or a timeout is encountered
+ */
+ else {
+ if(!uzbl.state.event_buffer)
+ uzbl.state.event_buffer = g_ptr_array_new();
+ g_ptr_array_add(uzbl.state.event_buffer, (gpointer)g_string_new(msg->str));
+ }
+
+ /* write to all client sockets */
+ i=0;
+ if(msg && uzbl.comm.client_chan) {
+ while(i < uzbl.comm.client_chan->len) {
+ gio = g_ptr_array_index(uzbl.comm.client_chan, i++);
+
+ if(gio && gio->is_writeable && msg) {
+ ret = g_io_channel_write_chars (gio,
+ msg->str, msg->len,
+ &len, &error);
+
+ if (ret == G_IO_STATUS_ERROR)
+ g_warning ("Error sending event to socket: %s", error->message);
+ else
+ g_io_channel_flush(gio, &error);
+ }
+ }
+ }
+}
+
+void
+send_event_stdout(GString *msg) {
+ printf("%s", msg->str);
+ fflush(stdout);
+}
+
+/*
+ * build event string and send over the supported interfaces
+ * custom_event == NULL indicates an internal event
+*/
+void
+send_event(int type, const gchar *details, const gchar *custom_event) {
+ GString *event_message = g_string_new("");
+ gchar *buf, *p_val = NULL;
+
+ /* expand shell vars */
+ if(details) {
+ buf = g_strdup(details);
+ p_val = parseenv(buf ? g_strchug(buf) : " ");
+ g_free(buf);
+ }
+
+ /* check for custom events */
+ if(custom_event) {
+ g_string_printf(event_message, "EVENT [%s] %s %s\n",
+ uzbl.state.instance_name, custom_event, p_val);
+ }
+ /* check wether we support the internal event */
+ else if(type < LAST_EVENT) {
+ g_string_printf(event_message, "EVENT [%s] %s %s\n",
+ uzbl.state.instance_name, event_table[type], p_val);
+ }
+
+ if(event_message->str) {
+ if(uzbl.state.events_stdout)
+ send_event_stdout(event_message);
+ send_event_socket(event_message);
+
+ g_string_free(event_message, TRUE);
+ }
+ g_free(p_val);
+}
+
+/* Transform gdk key events to our own events */
+void
+key_to_event(guint keyval, gint mode) {
+ gchar ucs[7];
+ gint ulen;
+ guint32 ukval = gdk_keyval_to_unicode(keyval);
+
+ /* check for printable unicode char */
+ /* TODO: Pass the keyvals through a GtkIMContext so that
+ * we also get combining chars right
+ */
+ if(g_unichar_isgraph(ukval)) {
+ ulen = g_unichar_to_utf8(ukval, ucs);
+ ucs[ulen] = 0;
+
+ send_event(mode == GDK_KEY_PRESS ? KEY_PRESS : KEY_RELEASE,
+ ucs, NULL);
+ }
+ /* send keysym for non-printable chars */
+ else {
+ send_event(mode == GDK_KEY_PRESS ? KEY_PRESS : KEY_RELEASE,
+ gdk_keyval_name(keyval), NULL);
+ }
+
+}
+
diff --git a/src/events.h b/src/events.h
new file mode 100644
index 0000000..fe3ff3b
--- /dev/null
+++ b/src/events.h
@@ -0,0 +1,37 @@
+/*
+ ** Uzbl event routines
+ ** (c) 2009 by Robert Manea
+*/
+
+/* Event system */
+enum event_type {
+ LOAD_START, LOAD_COMMIT, LOAD_FINISH, LOAD_ERROR,
+ REQUEST_STARTING,
+ KEY_PRESS, KEY_RELEASE, DOWNLOAD_REQ, COMMAND_EXECUTED,
+ LINK_HOVER, TITLE_CHANGED, GEOMETRY_CHANGED,
+ WEBINSPECTOR, NEW_WINDOW, SELECTION_CHANGED,
+ VARIABLE_SET, FIFO_SET, SOCKET_SET,
+ INSTANCE_START, INSTANCE_EXIT, LOAD_PROGRESS,
+ LINK_UNHOVER, FORM_ACTIVE, ROOT_ACTIVE,
+ FOCUS_LOST, FOCUS_GAINED, FILE_INCLUDED,
+ PLUG_CREATED, COMMAND_ERROR, BUILTINS,
+ PTR_MOVE,
+
+ /* must be last entry */
+ LAST_EVENT
+};
+
+void
+event_buffer_timeout(guint sec);
+
+void
+send_event_socket(GString *msg);
+
+void
+send_event_stdout(GString *msg);
+
+void
+send_event(int type, const gchar *details, const gchar *custom_event);
+
+void
+key_to_event(guint keyval, gint mode);
diff --git a/src/inspector.c b/src/inspector.c
new file mode 100644
index 0000000..de3dbcd
--- /dev/null
+++ b/src/inspector.c
@@ -0,0 +1,103 @@
+/*
+ ** WebInspector
+ ** (c) 2009 by Robert Manea
+*/
+
+#include "uzbl-core.h"
+#include "events.h"
+#include "callbacks.h"
+
+
+void
+hide_window_cb(GtkWidget *widget, gpointer data) {
+ (void) data;
+
+ gtk_widget_hide(widget);
+}
+
+WebKitWebView*
+create_inspector_cb (WebKitWebInspector* web_inspector, WebKitWebView* page, gpointer data){
+ (void) data;
+ (void) page;
+ (void) web_inspector;
+ GtkWidget* scrolled_window;
+ GtkWidget* new_web_view;
+ GUI *g = &uzbl.gui;
+
+ g->inspector_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
+ g_signal_connect(G_OBJECT(g->inspector_window), "delete-event",
+ G_CALLBACK(hide_window_cb), NULL);
+
+ gtk_window_set_title(GTK_WINDOW(g->inspector_window), "Uzbl WebInspector");
+ gtk_window_set_default_size(GTK_WINDOW(g->inspector_window), 400, 300);
+ gtk_widget_show(g->inspector_window);
+
+ scrolled_window = gtk_scrolled_window_new(NULL, NULL);
+ gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled_window),
+ GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
+ gtk_container_add(GTK_CONTAINER(g->inspector_window), scrolled_window);
+ gtk_widget_show(scrolled_window);
+
+ new_web_view = webkit_web_view_new();
+ gtk_container_add(GTK_CONTAINER(scrolled_window), new_web_view);
+
+ return WEBKIT_WEB_VIEW(new_web_view);
+}
+
+gboolean
+inspector_show_window_cb (WebKitWebInspector* inspector){
+ (void) inspector;
+ gtk_widget_show(uzbl.gui.inspector_window);
+
+ send_event(WEBINSPECTOR, "open", NULL);
+ return TRUE;
+}
+
+/* TODO: Add variables and code to make use of these functions */
+gboolean
+inspector_close_window_cb (WebKitWebInspector* inspector){
+ (void) inspector;
+ send_event(WEBINSPECTOR, "close", NULL);
+ return TRUE;
+}
+
+gboolean
+inspector_attach_window_cb (WebKitWebInspector* inspector){
+ (void) inspector;
+ return FALSE;
+}
+
+gboolean
+inspector_detach_window_cb (WebKitWebInspector* inspector){
+ (void) inspector;
+ return FALSE;
+}
+
+gboolean
+inspector_uri_changed_cb (WebKitWebInspector* inspector){
+ (void) inspector;
+ return FALSE;
+}
+
+gboolean
+inspector_inspector_destroyed_cb (WebKitWebInspector* inspector){
+ (void) inspector;
+ return FALSE;
+}
+
+void
+set_up_inspector() {
+ GUI *g = &uzbl.gui;
+ WebKitWebSettings *settings = view_settings();
+ g_object_set(G_OBJECT(settings), "enable-developer-extras", TRUE, NULL);
+
+ uzbl.gui.inspector = webkit_web_view_get_inspector(uzbl.gui.web_view);
+ g_signal_connect (G_OBJECT (g->inspector), "inspect-web-view", G_CALLBACK (create_inspector_cb), NULL);
+ g_signal_connect (G_OBJECT (g->inspector), "show-window", G_CALLBACK (inspector_show_window_cb), NULL);
+ g_signal_connect (G_OBJECT (g->inspector), "close-window", G_CALLBACK (inspector_close_window_cb), NULL);
+ g_signal_connect (G_OBJECT (g->inspector), "attach-window", G_CALLBACK (inspector_attach_window_cb), NULL);
+ g_signal_connect (G_OBJECT (g->inspector), "detach-window", G_CALLBACK (inspector_detach_window_cb), NULL);
+ g_signal_connect (G_OBJECT (g->inspector), "finished", G_CALLBACK (inspector_inspector_destroyed_cb), NULL);
+
+ g_signal_connect (G_OBJECT (g->inspector), "notify::inspected-uri", G_CALLBACK (inspector_uri_changed_cb), NULL);
+}
diff --git a/src/inspector.h b/src/inspector.h
new file mode 100644
index 0000000..57d0ca9
--- /dev/null
+++ b/src/inspector.h
@@ -0,0 +1,7 @@
+/*
+ ** WebInspector
+ ** (c) 2009 by Robert Manea
+*/
+
+void
+set_up_inspector();
diff --git a/src/uzbl-browser b/src/uzbl-browser
new file mode 100755
index 0000000..de4f7af
--- /dev/null
+++ b/src/uzbl-browser
@@ -0,0 +1,66 @@
+#!/bin/sh
+# this script implements a more useful out-of-the-box "browsing experience".
+# it does so by combining uzbl-core with a set of "recommended" tools and practices.
+# see docs for more info
+# If you want to customize the behavior of the cookie-daemon or similar helper tools,
+# copy them to your $XDG_DATA_HOME/uzbl/scripts/, edit them and update $PATH
+
+# Also, we assume existence of fifo/socket == correctly functioning cookie_daemon/event_manager.
+# Checking correct functioning of the daemons here would be too complex here, and it's not implemented in uzbl-core either.
+# But this shouldn't cause much problems..
+
+PREFIX=/usr/local
+if [ -z "$XDG_DATA_HOME" ]
+then
+ export XDG_DATA_HOME=$HOME/.local/share
+fi
+
+if [ -z "$XDG_CACHE_HOME" ]
+then
+ export XDG_CACHE_HOME=$HOME/.cache
+fi
+
+if [ -z "$XDG_CONFIG_HOME" ]
+then
+ export XDG_CONFIG_HOME=$HOME/.config
+fi
+
+# assure the relevant directories exist.
+for dir in $XDG_CACHE_HOME/uzbl $XDG_DATA_HOME/uzbl $XDG_CONFIG_HOME/uzbl
+do
+ if [ ! -d $dir ]
+ then
+ if ! mkdir -p $dir
+ then
+ echo "could not create $dir" >&2
+ exit 2
+ fi
+ fi
+done
+# if no config exists yet in the recommended location, put the default (recommended) config there
+if [ ! -f $XDG_CONFIG_HOME/uzbl/config ]
+then
+ if ! cp $PREFIX/share/uzbl/examples/config/config $XDG_CONFIG_HOME/uzbl/config
+ then
+ echo "Could not copy default config to $XDG_CONFIG_HOME/uzbl/config" >&2
+ exit 3
+ fi
+fi
+
+# Uncomment this for a slight speedup at the expense of not having
+# stale cookie daemon sockets cleaned up.
+#if [ ! -S $XDG_CACHE_HOME/uzbl/cookie_daemon_socket ]
+#then
+ # if you want to customize it, copy to your $XDG_DATA_HOME/uzbl/scripts/ and update $PATH
+ uzbl-cookie-daemon -v start
+#fi
+
+DAEMON_SOCKET=$XDG_CACHE_HOME/uzbl/event_daemon
+DAEMON_PID=${DAEMON_SOCKET}.pid
+
+#if [ -f "$DAEMON_PID" ]
+#then
+ uzbl-event-manager -va start
+#fi
+
+exec uzbl-core "$@" --connect-socket $DAEMON_SOCKET
diff --git a/src/uzbl-core.c b/src/uzbl-core.c
new file mode 100644
index 0000000..ebd8ef8
--- /dev/null
+++ b/src/uzbl-core.c
@@ -0,0 +1,2709 @@
+/* -*- c-basic-offset: 4; -*- */
+// Original code taken from the example webkit-gtk+ application. see notice below.
+// Modified code is licensed under the GPL 3. See LICENSE file.
+
+
+/*
+ * Copyright (C) 2006, 2007 Apple Inc.
+ * Copyright (C) 2007 Alp Toker <alp@atoker.com>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "uzbl-core.h"
+#include "callbacks.h"
+#include "events.h"
+#include "inspector.h"
+#include "config.h"
+
+UzblCore uzbl;
+
+/* commandline arguments (set initial values for the state variables) */
+const
+GOptionEntry entries[] =
+{
+ { "uri", 'u', 0, G_OPTION_ARG_STRING, &uzbl.state.uri,
+ "Uri to load at startup (equivalent to 'uzbl <uri>' or 'set uri = URI' after uzbl has launched)", "URI" },
+ { "verbose", 'v', 0, G_OPTION_ARG_NONE, &uzbl.state.verbose,
+ "Whether to print all messages or just errors.", NULL },
+ { "name", 'n', 0, G_OPTION_ARG_STRING, &uzbl.state.instance_name,
+ "Name of the current instance (defaults to Xorg window id or random for GtkSocket mode)", "NAME" },
+ { "config", 'c', 0, G_OPTION_ARG_STRING, &uzbl.state.config_file,
+ "Path to config file or '-' for stdin", "FILE" },
+ { "socket", 's', 0, G_OPTION_ARG_INT, &uzbl.state.socket_id,
+ "Xembed Socket ID", "SOCKET" },
+ { "connect-socket", 0, 0, G_OPTION_ARG_STRING_ARRAY, &uzbl.state.connect_socket_names,
+ "Connect to server socket for event managing", "CSOCKET" },
+ { "print-events", 'p', 0, G_OPTION_ARG_NONE, &uzbl.state.events_stdout,
+ "Whether to print events to stdout.", NULL },
+ { "geometry", 'g', 0, G_OPTION_ARG_STRING, &uzbl.gui.geometry,
+ "Set window geometry (format: 'WIDTHxHEIGHT+-X+-Y' or 'maximized')", "GEOMETRY" },
+ { "version", 'V', 0, G_OPTION_ARG_NONE, &uzbl.behave.print_version,
+ "Print the version and exit", NULL },
+ { NULL, 0, 0, 0, NULL, NULL, NULL }
+};
+
+XDG_Var XDG[] =
+{
+ { "XDG_CONFIG_HOME", "~/.config" },
+ { "XDG_DATA_HOME", "~/.local/share" },
+ { "XDG_CACHE_HOME", "~/.cache" },
+ { "XDG_CONFIG_DIRS", "/etc/xdg" },
+ { "XDG_DATA_DIRS", "/usr/local/share/:/usr/share/" },
+};
+
+/* abbreviations to help keep the table's width humane */
+#define PTR_V_STR(var, d, fun) { .ptr.s = &(var), .type = TYPE_STR, .dump = d, .writeable = 1, .func = fun }
+#define PTR_V_INT(var, d, fun) { .ptr.i = (int*)&(var), .type = TYPE_INT, .dump = d, .writeable = 1, .func = fun }
+#define PTR_V_FLOAT(var, d, fun) { .ptr.f = &(var), .type = TYPE_FLOAT, .dump = d, .writeable = 1, .func = fun }
+#define PTR_C_STR(var, fun) { .ptr.s = &(var), .type = TYPE_STR, .dump = 0, .writeable = 0, .func = fun }
+#define PTR_C_INT(var, fun) { .ptr.i = (int*)&(var), .type = TYPE_INT, .dump = 0, .writeable = 0, .func = fun }
+#define PTR_C_FLOAT(var, fun) { .ptr.f = &(var), .type = TYPE_FLOAT, .dump = 0, .writeable = 0, .func = fun }
+
+const struct var_name_to_ptr_t {
+ const char *name;
+ uzbl_cmdprop cp;
+} var_name_to_ptr[] = {
+/* variable name pointer to variable in code dump callback function */
+/* ---------------------------------------------------------------------------------------------- */
+ { "uri", PTR_V_STR(uzbl.state.uri, 1, cmd_load_uri)},
+ { "verbose", PTR_V_INT(uzbl.state.verbose, 1, NULL)},
+ { "print_events", PTR_V_INT(uzbl.state.events_stdout, 1, NULL)},
+ { "inject_html", PTR_V_STR(uzbl.behave.inject_html, 0, cmd_inject_html)},
+ { "geometry", PTR_V_STR(uzbl.gui.geometry, 1, cmd_set_geometry)},
+ { "keycmd", PTR_V_STR(uzbl.state.keycmd, 1, NULL)},
+ { "show_status", PTR_V_INT(uzbl.behave.show_status, 1, cmd_set_status)},
+ { "status_top", PTR_V_INT(uzbl.behave.status_top, 1, move_statusbar)},
+ { "status_format", PTR_V_STR(uzbl.behave.status_format, 1, NULL)},
+ { "status_background", PTR_V_STR(uzbl.behave.status_background, 1, NULL)},
+ { "title_format_long", PTR_V_STR(uzbl.behave.title_format_long, 1, NULL)},
+ { "title_format_short", PTR_V_STR(uzbl.behave.title_format_short, 1, NULL)},
+ { "icon", PTR_V_STR(uzbl.gui.icon, 1, set_icon)},
+ { "forward_keys", PTR_V_INT(uzbl.behave.forward_keys, 1, NULL)},
+ { "download_handler", PTR_V_STR(uzbl.behave.download_handler, 1, NULL)},
+ { "cookie_handler", PTR_V_STR(uzbl.behave.cookie_handler, 1, NULL)},
+ { "authentication_handler", PTR_V_STR(uzbl.behave.authentication_handler, 1, set_authentication_handler)},
+ { "new_window", PTR_V_STR(uzbl.behave.new_window, 1, NULL)},
+ { "scheme_handler", PTR_V_STR(uzbl.behave.scheme_handler, 1, NULL)},
+ { "fifo_dir", PTR_V_STR(uzbl.behave.fifo_dir, 1, cmd_fifo_dir)},
+ { "socket_dir", PTR_V_STR(uzbl.behave.socket_dir, 1, cmd_socket_dir)},
+ { "http_debug", PTR_V_INT(uzbl.behave.http_debug, 1, cmd_http_debug)},
+ { "shell_cmd", PTR_V_STR(uzbl.behave.shell_cmd, 1, NULL)},
+ { "proxy_url", PTR_V_STR(uzbl.net.proxy_url, 1, set_proxy_url)},
+ { "max_conns", PTR_V_INT(uzbl.net.max_conns, 1, cmd_max_conns)},
+ { "max_conns_host", PTR_V_INT(uzbl.net.max_conns_host, 1, cmd_max_conns_host)},
+ { "useragent", PTR_V_STR(uzbl.net.useragent, 1, cmd_useragent)},
+ /* requires webkit >=1.1.14 */
+ { "view_source", PTR_V_INT(uzbl.behave.view_source, 0, cmd_view_source)},
+
+ /* exported WebKitWebSettings properties */
+ { "zoom_level", PTR_V_FLOAT(uzbl.behave.zoom_level, 1, cmd_zoom_level)},
+ { "zoom_type", PTR_V_INT(uzbl.behave.zoom_type, 1, cmd_set_zoom_type)},
+ { "font_size", PTR_V_INT(uzbl.behave.font_size, 1, cmd_font_size)},
+ { "default_font_family", PTR_V_STR(uzbl.behave.default_font_family, 1, cmd_default_font_family)},
+ { "monospace_font_family", PTR_V_STR(uzbl.behave.monospace_font_family, 1, cmd_monospace_font_family)},
+ { "cursive_font_family", PTR_V_STR(uzbl.behave.cursive_font_family, 1, cmd_cursive_font_family)},
+ { "sans_serif_font_family", PTR_V_STR(uzbl.behave.sans_serif_font_family, 1, cmd_sans_serif_font_family)},
+ { "serif_font_family", PTR_V_STR(uzbl.behave.serif_font_family, 1, cmd_serif_font_family)},
+ { "fantasy_font_family", PTR_V_STR(uzbl.behave.fantasy_font_family, 1, cmd_fantasy_font_family)},
+ { "monospace_size", PTR_V_INT(uzbl.behave.monospace_size, 1, cmd_font_size)},
+ { "minimum_font_size", PTR_V_INT(uzbl.behave.minimum_font_size, 1, cmd_minimum_font_size)},
+ { "disable_plugins", PTR_V_INT(uzbl.behave.disable_plugins, 1, cmd_disable_plugins)},
+ { "disable_scripts", PTR_V_INT(uzbl.behave.disable_scripts, 1, cmd_disable_scripts)},
+ { "autoload_images", PTR_V_INT(uzbl.behave.autoload_img, 1, cmd_autoload_img)},
+ { "autoshrink_images", PTR_V_INT(uzbl.behave.autoshrink_img, 1, cmd_autoshrink_img)},
+ { "enable_spellcheck", PTR_V_INT(uzbl.behave.enable_spellcheck, 1, cmd_enable_spellcheck)},
+ { "enable_private", PTR_V_INT(uzbl.behave.enable_private, 1, cmd_enable_private)},
+ { "print_backgrounds", PTR_V_INT(uzbl.behave.print_bg, 1, cmd_print_bg)},
+ { "stylesheet_uri", PTR_V_STR(uzbl.behave.style_uri, 1, cmd_style_uri)},
+ { "resizable_text_areas", PTR_V_INT(uzbl.behave.resizable_txt, 1, cmd_resizable_txt)},
+ { "default_encoding", PTR_V_STR(uzbl.behave.default_encoding, 1, cmd_default_encoding)},
+ { "enforce_96_dpi", PTR_V_INT(uzbl.behave.enforce_96dpi, 1, cmd_enforce_96dpi)},
+ { "caret_browsing", PTR_V_INT(uzbl.behave.caret_browsing, 1, cmd_caret_browsing)},
+ { "scrollbars_visible", PTR_V_INT(uzbl.gui.scrollbars_visible, 1, cmd_scrollbars_visibility)},
+
+ /* constants (not dumpable or writeable) */
+ { "WEBKIT_MAJOR", PTR_C_INT(uzbl.info.webkit_major, NULL)},
+ { "WEBKIT_MINOR", PTR_C_INT(uzbl.info.webkit_minor, NULL)},
+ { "WEBKIT_MICRO", PTR_C_INT(uzbl.info.webkit_micro, NULL)},
+ { "ARCH_UZBL", PTR_C_STR(uzbl.info.arch, NULL)},
+ { "COMMIT", PTR_C_STR(uzbl.info.commit, NULL)},
+ { "TITLE", PTR_C_STR(uzbl.gui.main_title, NULL)},
+ { "SELECTED_URI", PTR_C_STR(uzbl.state.selected_url, NULL)},
+ { "NAME", PTR_C_STR(uzbl.state.instance_name, NULL)},
+ { "PID", PTR_C_STR(uzbl.info.pid_str, NULL)},
+
+ { NULL, {.ptr.s = NULL, .type = TYPE_INT, .dump = 0, .writeable = 0, .func = NULL}}
+};
+
+/* construct a hash from the var_name_to_ptr array for quick access */
+void
+create_var_to_name_hash() {
+ const struct var_name_to_ptr_t *n2v_p = var_name_to_ptr;
+ uzbl.comm.proto_var = g_hash_table_new(g_str_hash, g_str_equal);
+ while(n2v_p->name) {
+ g_hash_table_insert(uzbl.comm.proto_var,
+ (gpointer) n2v_p->name,
+ (gpointer) &n2v_p->cp);
+ n2v_p++;
+ }
+}
+
+
+/* --- UTILITY FUNCTIONS --- */
+enum exp_type {EXP_ERR, EXP_SIMPLE_VAR, EXP_BRACED_VAR, EXP_EXPR, EXP_JS, EXP_ESCAPE};
+enum exp_type
+get_exp_type(const gchar *s) {
+ /* variables */
+ if(*(s+1) == '(')
+ return EXP_EXPR;
+ else if(*(s+1) == '{')
+ return EXP_BRACED_VAR;
+ else if(*(s+1) == '<')
+ return EXP_JS;
+ else if(*(s+1) == '[')
+ return EXP_ESCAPE;
+ else
+ return EXP_SIMPLE_VAR;
+
+ /*@notreached@*/
+return EXP_ERR;
+}
+
+/*
+ * recurse == 1: don't expand '@(command)@'
+ * recurse == 2: don't expand '@<java script>@'
+*/
+gchar *
+expand(const char *s, guint recurse) {
+ uzbl_cmdprop *c;
+ enum exp_type etype;
+ char *end_simple_var = "^°!\"§$%&/()=?'`'+~*'#-.:,;@<>| \\{}[]¹²³¼½";
+ char *ret = NULL;
+ char *vend = NULL;
+ GError *err = NULL;
+ gchar *cmd_stdout = NULL;
+ gchar *mycmd = NULL;
+ GString *buf = g_string_new("");
+ GString *js_ret = g_string_new("");
+
+ while(s && *s) {
+ switch(*s) {
+ case '\\':
+ g_string_append_c(buf, *++s);
+ s++;
+ break;
+
+ case '@':
+ etype = get_exp_type(s);
+ s++;
+
+ switch(etype) {
+ case EXP_SIMPLE_VAR:
+ vend = strpbrk(s, end_simple_var);
+ if(!vend) vend = strchr(s, '\0');
+ break;
+ case EXP_BRACED_VAR:
+ s++;
+ vend = strchr(s, '}');
+ if(!vend) vend = strchr(s, '\0');
+ break;
+ case EXP_EXPR:
+ s++;
+ vend = strstr(s, ")@");
+ if(!vend) vend = strchr(s, '\0');
+ break;
+ case EXP_JS:
+ s++;
+ vend = strstr(s, ">@");
+ if(!vend) vend = strchr(s, '\0');
+ break;
+ case EXP_ESCAPE:
+ s++;
+ vend = strstr(s, "]@");
+ if(!vend) vend = strchr(s, '\0');
+ break;
+ /*@notreached@*/
+ case EXP_ERR:
+ break;
+ }
+ assert(vend);
+
+ ret = g_strndup(s, vend-s);
+
+ if(etype == EXP_SIMPLE_VAR ||
+ etype == EXP_BRACED_VAR) {
+ if( (c = g_hash_table_lookup(uzbl.comm.proto_var, ret)) ) {
+ if(c->type == TYPE_STR && *c->ptr.s != NULL) {
+ g_string_append(buf, (gchar *)*c->ptr.s);
+ }
+ else if(c->type == TYPE_INT) {
+ g_string_append_printf(buf, "%d", *c->ptr.i);
+ }
+ else if(c->type == TYPE_FLOAT) {
+ g_string_append_printf(buf, "%f", *c->ptr.f);
+ }
+ }
+
+ if(etype == EXP_SIMPLE_VAR)
+ s = vend;
+ else
+ s = vend+1;
+ }
+ else if(recurse != 1 &&
+ etype == EXP_EXPR) {
+
+ /* execute program directly */
+ if(ret[0] == '+') {
+ mycmd = expand(ret+1, 1);
+ g_spawn_command_line_sync(mycmd, &cmd_stdout, NULL, NULL, &err);
+ g_free(mycmd);
+ }
+ /* execute program through shell, quote it first */
+ else {
+ mycmd = expand(ret, 1);
+ gchar *quoted = g_shell_quote(mycmd);
+ gchar *tmp = g_strdup_printf("%s %s",
+ uzbl.behave.shell_cmd?uzbl.behave.shell_cmd:"/bin/sh -c",
+ quoted);
+ g_spawn_command_line_sync(tmp, &cmd_stdout, NULL, NULL, &err);
+ g_free(mycmd);
+ g_free(quoted);
+ g_free(tmp);
+ }
+
+ if (err) {
+ g_printerr("error on running command: %s\n", err->message);
+ g_error_free (err);
+ }
+ else if (*cmd_stdout) {
+ size_t len = strlen(cmd_stdout);
+
+ if(len > 0 && cmd_stdout[len-1] == '\n')
+ cmd_stdout[--len] = '\0'; /* strip trailing newline */
+
+ g_string_append(buf, cmd_stdout);
+ g_free(cmd_stdout);
+ }
+ s = vend+2;
+ }
+ else if(recurse != 2 &&
+ etype == EXP_JS) {
+
+ /* read JS from file */
+ if(ret[0] == '+') {
+ GArray *tmp = g_array_new(TRUE, FALSE, sizeof(gchar *));
+ mycmd = expand(ret+1, 2);
+ g_array_append_val(tmp, mycmd);
+
+ run_external_js(uzbl.gui.web_view, tmp, js_ret);
+ g_array_free(tmp, TRUE);
+ }
+ /* JS from string */
+ else {
+ mycmd = expand(ret, 2);
+ eval_js(uzbl.gui.web_view, mycmd, js_ret, "(command)");
+ g_free(mycmd);
+ }
+
+ if(js_ret->str) {
+ g_string_append(buf, js_ret->str);
+ g_string_free(js_ret, TRUE);
+ js_ret = g_string_new("");
+ }
+ s = vend+2;
+ }
+ else if(etype == EXP_ESCAPE) {
+ mycmd = expand(ret, 0);
+ char *escaped = g_markup_escape_text(mycmd, strlen(mycmd));
+
+ g_string_append(buf, escaped);
+
+ g_free(escaped);
+ g_free(mycmd);
+ s = vend+2;
+ }
+
+ g_free(ret);
+ ret = NULL;
+ break;
+
+ default:
+ g_string_append_c(buf, *s);
+ s++;
+ break;
+ }
+ }
+ g_string_free(js_ret, TRUE);
+ return g_string_free(buf, FALSE);
+}
+
+char *
+itos(int val) {
+ char tmp[20];
+
+ snprintf(tmp, sizeof(tmp), "%i", val);
+ return g_strdup(tmp);
+}
+
+gchar*
+strfree(gchar *str) {
+ g_free(str);
+ return NULL;
+}
+
+gchar*
+argv_idx(const GArray *a, const guint idx) { return g_array_index(a, gchar*, idx); }
+
+char *
+str_replace (const char* search, const char* replace, const char* string) {
+ gchar **buf;
+ char *ret;
+
+ if(!string)
+ return NULL;
+
+ buf = g_strsplit (string, search, -1);
+ ret = g_strjoinv (replace, buf);
+ g_strfreev(buf);
+
+ return ret;
+}
+
+GArray*
+read_file_by_line (const gchar *path) {
+ GIOChannel *chan = NULL;
+ gchar *readbuf = NULL;
+ gsize len;
+ GArray *lines = g_array_new(TRUE, FALSE, sizeof(gchar*));
+ int i = 0;
+
+ chan = g_io_channel_new_file(path, "r", NULL);
+ if (chan) {
+ while (g_io_channel_read_line(chan, &readbuf, &len, NULL, NULL) == G_IO_STATUS_NORMAL) {
+ const gchar* val = g_strdup (readbuf);
+ g_array_append_val (lines, val);
+ g_free (readbuf);
+ i ++;
+ }
+
+ g_io_channel_unref (chan);
+ } else {
+ gchar *tmp = g_strdup_printf("File %s can not be read.", path);
+ send_event(COMMAND_ERROR, tmp, NULL);
+ g_free(tmp);
+ }
+
+ return lines;
+}
+
+/* search a PATH style string for an existing file+path combination */
+gchar*
+find_existing_file(gchar* path_list) {
+ int i=0;
+ int cnt;
+ gchar **split;
+ gchar *tmp = NULL;
+ gchar *executable;
+
+ if(!path_list)
+ return NULL;
+
+ split = g_strsplit(path_list, ":", 0);
+ while(split[i])
+ i++;
+
+ if(i<=1) {
+ tmp = g_strdup(split[0]);
+ g_strfreev(split);
+ return tmp;
+ }
+ else
+ cnt = i-1;
+
+ i=0;
+ tmp = g_strdup(split[cnt]);
+ g_strstrip(tmp);
+ if(tmp[0] == '/')
+ executable = g_strdup_printf("%s", tmp+1);
+ else
+ executable = g_strdup(tmp);
+ g_free(tmp);
+
+ while(i<cnt) {
+ tmp = g_strconcat(g_strstrip(split[i]), "/", executable, NULL);
+ if(g_file_test(tmp, G_FILE_TEST_EXISTS)) {
+ g_strfreev(split);
+ return tmp;
+ }
+ else
+ g_free(tmp);
+ i++;
+ }
+
+ g_free(executable);
+ g_strfreev(split);
+ return NULL;
+}
+
+
+/* Returns a new string with environment $variables expanded */
+gchar*
+parseenv (gchar* string) {
+ extern char** environ;
+ gchar* tmpstr = NULL, * out;
+ int i = 0;
+
+ if(!string)
+ return NULL;
+
+ out = g_strdup(string);
+ while (environ[i] != NULL) {
+ gchar** env = g_strsplit (environ[i], "=", 2);
+ gchar* envname = g_strconcat ("$", env[0], NULL);
+
+ if (g_strrstr (string, envname) != NULL) {
+ tmpstr = out;
+ out = str_replace(envname, env[1], out);
+ g_free (tmpstr);
+ }
+
+ g_free (envname);
+ g_strfreev (env); // somebody said this breaks uzbl
+ i++;
+ }
+
+ return out;
+}
+
+
+void
+clean_up(void) {
+ send_event(INSTANCE_EXIT, uzbl.info.pid_str, NULL);
+ g_free(uzbl.info.pid_str);
+
+ g_free(uzbl.state.executable_path);
+ g_hash_table_destroy(uzbl.behave.commands);
+
+ if(uzbl.state.event_buffer)
+ g_ptr_array_free(uzbl.state.event_buffer, TRUE);
+
+ if (uzbl.behave.fifo_dir)
+ unlink (uzbl.comm.fifo_path);
+ if (uzbl.behave.socket_dir)
+ unlink (uzbl.comm.socket_path);
+}
+
+gint
+get_click_context() {
+ GUI *g = &uzbl.gui;
+ WebKitHitTestResult *ht;
+ guint context;
+
+ if(!uzbl.state.last_button)
+ return -1;
+
+ ht = webkit_web_view_get_hit_test_result(g->web_view, uzbl.state.last_button);
+ g_object_get(ht, "context", &context, NULL);
+
+ return (gint)context;
+}
+
+/* --- SIGNALS --- */
+int sigs[] = {SIGTERM, SIGINT, SIGSEGV, SIGILL, SIGFPE, SIGQUIT, SIGALRM, 0};
+
+sigfunc*
+setup_signal(int signr, sigfunc *shandler) {
+ struct sigaction nh, oh;
+
+ nh.sa_handler = shandler;
+ sigemptyset(&nh.sa_mask);
+ nh.sa_flags = 0;
+
+ if(sigaction(signr, &nh, &oh) < 0)
+ return SIG_ERR;
+
+ return NULL;
+}
+
+void
+catch_signal(int s) {
+ if(s == SIGTERM ||
+ s == SIGINT ||
+ s == SIGILL ||
+ s == SIGFPE ||
+ s == SIGQUIT) {
+ clean_up();
+ exit(EXIT_SUCCESS);
+ }
+ else if(s == SIGSEGV) {
+ clean_up();
+ fprintf(stderr, "Program aborted, segmentation fault!\nAttempting to clean up...\n");
+ exit(EXIT_FAILURE);
+ }
+ else if(s == SIGALRM && uzbl.state.event_buffer) {
+ g_ptr_array_free(uzbl.state.event_buffer, TRUE);
+ uzbl.state.event_buffer = NULL;
+ }
+}
+
+/* scroll a bar in a given direction */
+void
+scroll (GtkAdjustment* bar, gchar *amount_str) {
+ gchar *end;
+ gdouble max_value;
+
+ gdouble page_size = gtk_adjustment_get_page_size(bar);
+ gdouble value = gtk_adjustment_get_value(bar);
+ gdouble amount = g_ascii_strtod(amount_str, &end);
+
+ if (*end == '%')
+ value += page_size * amount * 0.01;
+ else
+ value += amount;
+
+ max_value = gtk_adjustment_get_upper(bar) - page_size;
+
+ if (value > max_value)
+ value = max_value; /* don't scroll past the end of the page */
+
+ gtk_adjustment_set_value (bar, value);
+}
+
+/*
+ * scroll vertical 20
+ * scroll vertical 20%
+ * scroll vertical -40
+ * scroll vertical begin
+ * scroll vertical end
+ * scroll horizontal 10
+ * scroll horizontal -500
+ * scroll horizontal begin
+ * scroll horizontal end
+ */
+void
+scroll_cmd(WebKitWebView* page, GArray *argv, GString *result) {
+ (void) page; (void) result;
+ gchar *direction = g_array_index(argv, gchar*, 0);
+ gchar *argv1 = g_array_index(argv, gchar*, 1);
+
+ if (g_strcmp0(direction, "horizontal") == 0)
+ {
+ if (g_strcmp0(argv1, "begin") == 0)
+ gtk_adjustment_set_value(uzbl.gui.bar_h, gtk_adjustment_get_lower(uzbl.gui.bar_h));
+ else if (g_strcmp0(argv1, "end") == 0)
+ gtk_adjustment_set_value (uzbl.gui.bar_h, gtk_adjustment_get_upper(uzbl.gui.bar_h) -
+ gtk_adjustment_get_page_size(uzbl.gui.bar_h));
+ else
+ scroll(uzbl.gui.bar_h, argv1);
+ }
+ else if (g_strcmp0(direction, "vertical") == 0)
+ {
+ if (g_strcmp0(argv1, "begin") == 0)
+ gtk_adjustment_set_value(uzbl.gui.bar_v, gtk_adjustment_get_lower(uzbl.gui.bar_v));
+ else if (g_strcmp0(argv1, "end") == 0)
+ gtk_adjustment_set_value (uzbl.gui.bar_v, gtk_adjustment_get_upper(uzbl.gui.bar_v) -
+ gtk_adjustment_get_page_size(uzbl.gui.bar_v));
+ else
+ scroll(uzbl.gui.bar_v, argv1);
+ }
+ else
+ if(uzbl.state.verbose)
+ puts("Unrecognized scroll format");
+}
+
+
+/* VIEW funcs (little webkit wrappers) */
+#define VIEWFUNC(name) void view_##name(WebKitWebView *page, GArray *argv, GString *result){(void)argv; (void)result; webkit_web_view_##name(page);}
+VIEWFUNC(reload)
+VIEWFUNC(reload_bypass_cache)
+VIEWFUNC(stop_loading)
+VIEWFUNC(zoom_in)
+VIEWFUNC(zoom_out)
+VIEWFUNC(go_back)
+VIEWFUNC(go_forward)
+#undef VIEWFUNC
+
+/* -- command to callback/function map for things we cannot attach to any signals */
+struct {const char *key; CommandInfo value;} cmdlist[] =
+{ /* key function no_split */
+ { "back", {view_go_back, 0} },
+ { "forward", {view_go_forward, 0} },
+ { "scroll", {scroll_cmd, 0} },
+ { "reload", {view_reload, 0}, },
+ { "reload_ign_cache", {view_reload_bypass_cache, 0} },
+ { "stop", {view_stop_loading, 0}, },
+ { "zoom_in", {view_zoom_in, 0}, }, //Can crash (when max zoom reached?).
+ { "zoom_out", {view_zoom_out, 0}, },
+ { "toggle_zoom_type", {toggle_zoom_type, 0}, },
+ { "uri", {load_uri, TRUE} },
+ { "js", {run_js, TRUE} },
+ { "script", {run_external_js, 0} },
+ { "toggle_status", {toggle_status_cb, 0} },
+ { "spawn", {spawn, 0} },
+ { "sync_spawn", {spawn_sync, 0} }, // needed for cookie handler
+ { "sh", {spawn_sh, 0} },
+ { "sync_sh", {spawn_sh_sync, 0} }, // needed for cookie handler
+ { "talk_to_socket", {talk_to_socket, 0} },
+ { "exit", {close_uzbl, 0} },
+ { "search", {search_forward_text, TRUE} },
+ { "search_reverse", {search_reverse_text, TRUE} },
+ { "search_clear", {search_clear, TRUE} },
+ { "dehilight", {dehilight, 0} },
+ { "set", {set_var, TRUE} },
+ { "dump_config", {act_dump_config, 0} },
+ { "dump_config_as_events", {act_dump_config_as_events, 0} },
+ { "chain", {chain, 0} },
+ { "print", {print, TRUE} },
+ { "event", {event, TRUE} },
+ { "request", {event, TRUE} },
+ { "menu_add", {menu_add, TRUE} },
+ { "menu_link_add", {menu_add_link, TRUE} },
+ { "menu_image_add", {menu_add_image, TRUE} },
+ { "menu_editable_add", {menu_add_edit, TRUE} },
+ { "menu_separator", {menu_add_separator, TRUE} },
+ { "menu_link_separator", {menu_add_separator_link, TRUE} },
+ { "menu_image_separator", {menu_add_separator_image, TRUE}},
+ { "menu_editable_separator", {menu_add_separator_edit, TRUE} },
+ { "menu_remove", {menu_remove, TRUE} },
+ { "menu_link_remove", {menu_remove_link, TRUE} },
+ { "menu_image_remove", {menu_remove_image, TRUE} },
+ { "menu_editable_remove", {menu_remove_edit, TRUE} },
+ { "hardcopy", {hardcopy, TRUE} },
+ { "include", {include, TRUE} }
+};
+
+void
+commands_hash(void)
+{
+ unsigned int i;
+ uzbl.behave.commands = g_hash_table_new(g_str_hash, g_str_equal);
+
+ for (i = 0; i < LENGTH(cmdlist); i++)
+ g_hash_table_insert(uzbl.behave.commands, (gpointer) cmdlist[i].key, &cmdlist[i].value);
+}
+
+void
+builtins() {
+ unsigned int i,
+ len = LENGTH(cmdlist);
+ GString *command_list = g_string_new("");
+
+ for (i = 0; i < len; i++) {
+ g_string_append(command_list, cmdlist[i].key);
+ g_string_append_c(command_list, ' ');
+ }
+
+ send_event(BUILTINS, command_list->str, NULL);
+ g_string_free(command_list, TRUE);
+}
+
+/* -- CORE FUNCTIONS -- */
+
+bool
+file_exists (const char * filename) {
+ return (access(filename, F_OK) == 0);
+}
+
+void
+set_var(WebKitWebView *page, GArray *argv, GString *result) {
+ (void) page; (void) result;
+
+ if(!argv_idx(argv, 0))
+ return;
+
+ gchar **split = g_strsplit(argv_idx(argv, 0), "=", 2);
+ if (split[0] != NULL) {
+ gchar *value = parseenv(split[1] ? g_strchug(split[1]) : " ");
+ set_var_value(g_strstrip(split[0]), value);
+ g_free(value);
+ }
+ g_strfreev(split);
+}
+
+void
+add_to_menu(GArray *argv, guint context) {
+ GUI *g = &uzbl.gui;
+ MenuItem *m;
+ gchar *item_cmd = NULL;
+
+ if(!argv_idx(argv, 0))
+ return;
+
+ gchar **split = g_strsplit(argv_idx(argv, 0), "=", 2);
+ if(!g->menu_items)
+ g->menu_items = g_ptr_array_new();
+
+ if(split[1])
+ item_cmd = g_strdup(split[1]);
+
+ if(split[0]) {
+ m = malloc(sizeof(MenuItem));
+ m->name = g_strdup(split[0]);
+ m->cmd = g_strdup(item_cmd?item_cmd:"");
+ m->context = context;
+ m->issep = FALSE;
+ g_ptr_array_add(g->menu_items, m);
+ }
+ else
+ g_free(item_cmd);
+
+ g_strfreev(split);
+}
+
+void
+menu_add(WebKitWebView *page, GArray *argv, GString *result) {
+ (void) page;
+ (void) result;
+
+ add_to_menu(argv, WEBKIT_HIT_TEST_RESULT_CONTEXT_DOCUMENT);
+
+}
+
+void
+menu_add_link(WebKitWebView *page, GArray *argv, GString *result) {
+ (void) page;
+ (void) result;
+
+ add_to_menu(argv, WEBKIT_HIT_TEST_RESULT_CONTEXT_LINK);
+}
+
+void
+menu_add_image(WebKitWebView *page, GArray *argv, GString *result) {
+ (void) page;
+ (void) result;
+
+ add_to_menu(argv, WEBKIT_HIT_TEST_RESULT_CONTEXT_IMAGE);
+}
+
+void
+menu_add_edit(WebKitWebView *page, GArray *argv, GString *result) {
+ (void) page;
+ (void) result;
+
+ add_to_menu(argv, WEBKIT_HIT_TEST_RESULT_CONTEXT_EDITABLE);
+}
+
+void
+add_separator_to_menu(GArray *argv, guint context) {
+ GUI *g = &uzbl.gui;
+ MenuItem *m;
+ gchar *sep_name;
+
+ if(!g->menu_items)
+ g->menu_items = g_ptr_array_new();
+
+ if(!argv_idx(argv, 0))
+ return;
+ else
+ sep_name = argv_idx(argv, 0);
+
+ m = malloc(sizeof(MenuItem));
+ m->name = g_strdup(sep_name);
+ m->cmd = NULL;
+ m->context = context;
+ m->issep = TRUE;
+ g_ptr_array_add(g->menu_items, m);
+}
+
+void
+menu_add_separator(WebKitWebView *page, GArray *argv, GString *result) {
+ (void) page;
+ (void) result;
+
+ add_separator_to_menu(argv, WEBKIT_HIT_TEST_RESULT_CONTEXT_DOCUMENT);
+}
+
+void
+menu_add_separator_link(WebKitWebView *page, GArray *argv, GString *result) {
+ (void) page;
+ (void) result;
+
+ add_separator_to_menu(argv, WEBKIT_HIT_TEST_RESULT_CONTEXT_LINK);
+}
+void
+menu_add_separator_image(WebKitWebView *page, GArray *argv, GString *result) {
+ (void) page;
+ (void) result;
+
+ add_separator_to_menu(argv, WEBKIT_HIT_TEST_RESULT_CONTEXT_IMAGE);
+}
+
+void
+menu_add_separator_edit(WebKitWebView *page, GArray *argv, GString *result) {
+ (void) page;
+ (void) result;
+
+ add_separator_to_menu(argv, WEBKIT_HIT_TEST_RESULT_CONTEXT_EDITABLE);
+}
+
+void
+remove_from_menu(GArray *argv, guint context) {
+ GUI *g = &uzbl.gui;
+ MenuItem *mi;
+ gchar *name = NULL;
+ guint i=0;
+
+ if(!g->menu_items)
+ return;
+
+ if(!argv_idx(argv, 0))
+ return;
+ else
+ name = argv_idx(argv, 0);
+
+ for(i=0; i < g->menu_items->len; i++) {
+ mi = g_ptr_array_index(g->menu_items, i);
+
+ if((context == mi->context) && !strcmp(name, mi->name)) {
+ g_free(mi->name);
+ g_free(mi->cmd);
+ g_free(mi);
+ g_ptr_array_remove_index(g->menu_items, i);
+ }
+ }
+}
+
+void
+menu_remove(WebKitWebView *page, GArray *argv, GString *result) {
+ (void) page;
+ (void) result;
+
+ remove_from_menu(argv, WEBKIT_HIT_TEST_RESULT_CONTEXT_DOCUMENT);
+}
+
+void
+menu_remove_link(WebKitWebView *page, GArray *argv, GString *result) {
+ (void) page;
+ (void) result;
+
+ remove_from_menu(argv, WEBKIT_HIT_TEST_RESULT_CONTEXT_LINK);
+}
+
+void
+menu_remove_image(WebKitWebView *page, GArray *argv, GString *result) {
+ (void) page;
+ (void) result;
+
+ remove_from_menu(argv, WEBKIT_HIT_TEST_RESULT_CONTEXT_IMAGE);
+}
+
+void
+menu_remove_edit(WebKitWebView *page, GArray *argv, GString *result) {
+ (void) page;
+ (void) result;
+
+ remove_from_menu(argv, WEBKIT_HIT_TEST_RESULT_CONTEXT_EDITABLE);
+}
+
+void
+event(WebKitWebView *page, GArray *argv, GString *result) {
+ (void) page; (void) result;
+ GString *event_name;
+ gchar **split = NULL;
+
+ if(!argv_idx(argv, 0))
+ return;
+
+ split = g_strsplit(argv_idx(argv, 0), " ", 2);
+ if(split[0])
+ event_name = g_string_ascii_up(g_string_new(split[0]));
+ else
+ return;
+
+ send_event(0, split[1]?split[1]:"", event_name->str);
+
+ g_string_free(event_name, TRUE);
+ g_strfreev(split);
+}
+
+void
+print(WebKitWebView *page, GArray *argv, GString *result) {
+ (void) page; (void) result;
+ gchar* buf;
+
+ buf = expand(argv_idx(argv, 0), 0);
+ g_string_assign(result, buf);
+ g_free(buf);
+}
+
+void
+hardcopy(WebKitWebView *page, GArray *argv, GString *result) {
+ (void) argv;
+ (void) result;
+
+ webkit_web_frame_print(webkit_web_view_get_main_frame(page));
+}
+
+void
+include(WebKitWebView *page, GArray *argv, GString *result) {
+ (void) page;
+ (void) result;
+ gchar *pe = NULL,
+ *path = NULL,
+ *line;
+ int i=0;
+
+ if(!argv_idx(argv, 0))
+ return;
+
+ pe = parseenv(argv_idx(argv, 0));
+ if((path = find_existing_file(pe))) {
+ GArray* lines = read_file_by_line(path);
+
+ while ((line = g_array_index(lines, gchar*, i))) {
+ parse_cmd_line (line, NULL);
+ i++;
+ g_free (line);
+ }
+ g_array_free (lines, TRUE);
+
+ send_event(FILE_INCLUDED, path, NULL);
+ g_free(path);
+ }
+ g_free(pe);
+}
+
+void
+act_dump_config() {
+ dump_config();
+}
+
+void
+act_dump_config_as_events() {
+ dump_config_as_events();
+}
+
+void
+load_uri (WebKitWebView *web_view, GArray *argv, GString *result) {
+ (void) web_view; (void) result;
+ load_uri_imp (argv_idx (argv, 0));
+}
+
+/* Javascript*/
+
+JSValueRef
+js_run_command (JSContextRef ctx, JSObjectRef function, JSObjectRef thisObject,
+ size_t argumentCount, const JSValueRef arguments[],
+ JSValueRef* exception) {
+ (void) function;
+ (void) thisObject;
+ (void) exception;
+
+ JSStringRef js_result_string;
+ GString *result = g_string_new("");
+
+ if (argumentCount >= 1) {
+ JSStringRef arg = JSValueToStringCopy(ctx, arguments[0], NULL);
+ size_t arg_size = JSStringGetMaximumUTF8CStringSize(arg);
+ char ctl_line[arg_size];
+ JSStringGetUTF8CString(arg, ctl_line, arg_size);
+
+ parse_cmd_line(ctl_line, result);
+
+ JSStringRelease(arg);
+ }
+ js_result_string = JSStringCreateWithUTF8CString(result->str);
+
+ g_string_free(result, TRUE);
+
+ return JSValueMakeString(ctx, js_result_string);
+}
+
+JSStaticFunction js_static_functions[] = {
+ {"run", js_run_command, kJSPropertyAttributeNone},
+};
+
+void
+js_init() {
+ /* This function creates the class and its definition, only once */
+ if (!uzbl.js.initialized) {
+ /* it would be pretty cool to make this dynamic */
+ uzbl.js.classdef = kJSClassDefinitionEmpty;
+ uzbl.js.classdef.staticFunctions = js_static_functions;
+
+ uzbl.js.classref = JSClassCreate(&uzbl.js.classdef);
+ }
+}
+
+
+void
+eval_js(WebKitWebView * web_view, gchar *script, GString *result, const char *file) {
+ WebKitWebFrame *frame;
+ JSGlobalContextRef context;
+ JSObjectRef globalobject;
+ JSStringRef js_file;
+ JSStringRef js_script;
+ JSValueRef js_result;
+ JSValueRef js_exc = NULL;
+ JSStringRef js_result_string;
+ size_t js_result_size;
+
+ js_init();
+
+ frame = webkit_web_view_get_main_frame(WEBKIT_WEB_VIEW(web_view));
+ context = webkit_web_frame_get_global_context(frame);
+ globalobject = JSContextGetGlobalObject(context);
+
+ /* evaluate the script and get return value*/
+ js_script = JSStringCreateWithUTF8CString(script);
+ js_file = JSStringCreateWithUTF8CString(file);
+ js_result = JSEvaluateScript(context, js_script, globalobject, js_file, 0, &js_exc);
+ if (js_result && !JSValueIsUndefined(context, js_result)) {
+ js_result_string = JSValueToStringCopy(context, js_result, NULL);
+ js_result_size = JSStringGetMaximumUTF8CStringSize(js_result_string);
+
+ if (js_result_size) {
+ char js_result_utf8[js_result_size];
+ JSStringGetUTF8CString(js_result_string, js_result_utf8, js_result_size);
+ g_string_assign(result, js_result_utf8);
+ }
+
+ JSStringRelease(js_result_string);
+ }
+ else if (js_exc) {
+ size_t size;
+ JSStringRef prop, val;
+ JSObjectRef exc = JSValueToObject(context, js_exc, NULL);
+
+ printf("Exception occured while executing script:\n");
+
+ /* Print file */
+ prop = JSStringCreateWithUTF8CString("sourceURL");
+ val = JSValueToStringCopy(context, JSObjectGetProperty(context, exc, prop, NULL), NULL);
+ size = JSStringGetMaximumUTF8CStringSize(val);
+ if(size) {
+ char cstr[size];
+ JSStringGetUTF8CString(val, cstr, size);
+ printf("At %s", cstr);
+ }
+ JSStringRelease(prop);
+ JSStringRelease(val);
+
+ /* Print line */
+ prop = JSStringCreateWithUTF8CString("line");
+ val = JSValueToStringCopy(context, JSObjectGetProperty(context, exc, prop, NULL), NULL);
+ size = JSStringGetMaximumUTF8CStringSize(val);
+ if(size) {
+ char cstr[size];
+ JSStringGetUTF8CString(val, cstr, size);
+ printf(":%s: ", cstr);
+ }
+ JSStringRelease(prop);
+ JSStringRelease(val);
+
+ /* Print message */
+ val = JSValueToStringCopy(context, exc, NULL);
+ size = JSStringGetMaximumUTF8CStringSize(val);
+ if(size) {
+ char cstr[size];
+ JSStringGetUTF8CString(val, cstr, size);
+ printf("%s\n", cstr);
+ }
+ JSStringRelease(val);
+ }
+
+ /* cleanup */
+ JSStringRelease(js_script);
+ JSStringRelease(js_file);
+}
+
+void
+run_js (WebKitWebView * web_view, GArray *argv, GString *result) {
+ if (argv_idx(argv, 0))
+ eval_js(web_view, argv_idx(argv, 0), result, "(command)");
+}
+
+void
+run_external_js (WebKitWebView * web_view, GArray *argv, GString *result) {
+ (void) result;
+ gchar *path = NULL;
+
+ if (argv_idx(argv, 0) &&
+ ((path = find_existing_file(argv_idx(argv, 0)))) ) {
+ GArray* lines = read_file_by_line (path);
+ gchar* js = NULL;
+ int i = 0;
+ gchar* line;
+
+ while ((line = g_array_index(lines, gchar*, i))) {
+ if (js == NULL) {
+ js = g_strdup (line);
+ } else {
+ gchar* newjs = g_strconcat (js, line, NULL);
+ js = newjs;
+ }
+ i ++;
+ g_free (line);
+ }
+
+ if (uzbl.state.verbose)
+ printf ("External JavaScript file %s loaded\n", argv_idx(argv, 0));
+
+ gchar* newjs = str_replace("%s", argv_idx (argv, 1)?argv_idx (argv, 1):"", js);
+ g_free (js);
+ js = newjs;
+
+ eval_js (web_view, js, result, path);
+ g_free (js);
+ g_array_free (lines, TRUE);
+ g_free(path);
+ }
+}
+
+void
+search_text (WebKitWebView *page, GArray *argv, const gboolean forward) {
+ if (argv_idx(argv, 0) && (*argv_idx(argv, 0) != '\0')) {
+ if (g_strcmp0 (uzbl.state.searchtx, argv_idx(argv, 0)) != 0) {
+ webkit_web_view_unmark_text_matches (page);
+ webkit_web_view_mark_text_matches (page, argv_idx(argv, 0), FALSE, 0);
+ uzbl.state.searchtx = g_strdup(argv_idx(argv, 0));
+ }
+ }
+
+
+ if (uzbl.state.searchtx) {
+ if (uzbl.state.verbose)
+ printf ("Searching: %s\n", uzbl.state.searchtx);
+ webkit_web_view_set_highlight_text_matches (page, TRUE);
+ webkit_web_view_search_text (page, uzbl.state.searchtx, FALSE, forward, TRUE);
+ }
+}
+
+void
+search_clear(WebKitWebView *page, GArray *argv, GString *result) {
+ (void) argv;
+ (void) result;
+
+ webkit_web_view_unmark_text_matches (page);
+ if(uzbl.state.searchtx) {
+ g_free(uzbl.state.searchtx);
+ uzbl.state.searchtx = NULL;
+ }
+}
+
+void
+search_forward_text (WebKitWebView *page, GArray *argv, GString *result) {
+ (void) result;
+ search_text(page, argv, TRUE);
+}
+
+void
+search_reverse_text (WebKitWebView *page, GArray *argv, GString *result) {
+ (void) result;
+ search_text(page, argv, FALSE);
+}
+
+void
+dehilight (WebKitWebView *page, GArray *argv, GString *result) {
+ (void) argv; (void) result;
+ webkit_web_view_set_highlight_text_matches (page, FALSE);
+}
+
+
+void
+new_window_load_uri (const gchar * uri) {
+ if (uzbl.behave.new_window) {
+ GString *s = g_string_new ("");
+ g_string_printf(s, "'%s'", uri);
+ run_handler(uzbl.behave.new_window, s->str);
+ send_event(NEW_WINDOW, s->str, NULL);
+ return;
+ }
+ GString* to_execute = g_string_new ("");
+ g_string_append_printf (to_execute, "%s --uri '%s'", uzbl.state.executable_path, uri);
+ int i;
+ for (i = 0; entries[i].long_name != NULL; i++) {
+ if ((entries[i].arg == G_OPTION_ARG_STRING) &&
+ !strcmp(entries[i].long_name,"uri") &&
+ !strcmp(entries[i].long_name,"name")) {
+ gchar** str = (gchar**)entries[i].arg_data;
+ if (*str!=NULL)
+ g_string_append_printf (to_execute, " --%s '%s'", entries[i].long_name, *str);
+ }
+ else if(entries[i].arg == G_OPTION_ARG_STRING_ARRAY) {
+ int j;
+ gchar **str = *((gchar ***)entries[i].arg_data);
+ for(j=0; str[j]; j++)
+ g_string_append_printf(to_execute, " --%s '%s'", entries[i].long_name, str[j]);
+ }
+ }
+ if (uzbl.state.verbose)
+ printf("\n%s\n", to_execute->str);
+ g_spawn_command_line_async (to_execute->str, NULL);
+ /* TODO: should we just report the uri as event detail? */
+ send_event(NEW_WINDOW, to_execute->str, NULL);
+ g_string_free (to_execute, TRUE);
+}
+
+void
+chain (WebKitWebView *page, GArray *argv, GString *result) {
+ (void) page; (void) result;
+ gchar *a = NULL;
+ gchar **parts = NULL;
+ guint i = 0;
+ while ((a = argv_idx(argv, i++))) {
+ parts = g_strsplit (a, " ", 2);
+ if (parts[0])
+ parse_command(parts[0], parts[1], result);
+ g_strfreev (parts);
+ }
+}
+
+void
+close_uzbl (WebKitWebView *page, GArray *argv, GString *result) {
+ (void)page;
+ (void)argv;
+ (void)result;
+ gtk_main_quit ();
+}
+
+void
+sharg_append(GArray *a, const gchar *str) {
+ const gchar *s = (str ? str : "");
+ g_array_append_val(a, s);
+}
+
+// make sure that the args string you pass can properly be interpreted (eg properly escaped against whitespace, quotes etc)
+gboolean
+run_command (const gchar *command, const guint npre, const gchar **args,
+ const gboolean sync, char **output_stdout) {
+ //command <uzbl conf> <uzbl pid> <uzbl win id> <uzbl fifo file> <uzbl socket file> [args]
+ GError *err = NULL;
+
+ GArray *a = g_array_new (TRUE, FALSE, sizeof(gchar*));
+ gchar *pid = itos(getpid());
+ gchar *xwin = itos(uzbl.xwin);
+ guint i;
+ sharg_append(a, command);
+ for (i = 0; i < npre; i++) /* add n args before the default vars */
+ sharg_append(a, args[i]);
+ sharg_append(a, uzbl.state.config_file);
+ sharg_append(a, pid);
+ sharg_append(a, xwin);
+ sharg_append(a, uzbl.comm.fifo_path);
+ sharg_append(a, uzbl.comm.socket_path);
+ sharg_append(a, uzbl.state.uri);
+ sharg_append(a, uzbl.gui.main_title);
+
+ for (i = npre; i < g_strv_length((gchar**)args); i++)
+ sharg_append(a, args[i]);
+
+ gboolean result;
+ if (sync) {
+ if (*output_stdout) *output_stdout = strfree(*output_stdout);
+
+ result = g_spawn_sync(NULL, (gchar **)a->data, NULL, G_SPAWN_SEARCH_PATH,
+ NULL, NULL, output_stdout, NULL, NULL, &err);
+ } else
+ result = g_spawn_async(NULL, (gchar **)a->data, NULL, G_SPAWN_SEARCH_PATH,
+ NULL, NULL, NULL, &err);
+
+ if (uzbl.state.verbose) {
+ GString *s = g_string_new("spawned:");
+ for (i = 0; i < (a->len); i++) {
+ gchar *qarg = g_shell_quote(g_array_index(a, gchar*, i));
+ g_string_append_printf(s, " %s", qarg);
+ g_free (qarg);
+ }
+ g_string_append_printf(s, " -- result: %s", (result ? "true" : "false"));
+ printf("%s\n", s->str);
+ g_string_free(s, TRUE);
+ if(output_stdout) {
+ printf("Stdout: %s\n", *output_stdout);
+ }
+ }
+ if (err) {
+ g_printerr("error on run_command: %s\n", err->message);
+ g_error_free (err);
+ }
+ g_free (pid);
+ g_free (xwin);
+ g_array_free (a, TRUE);
+ return result;
+}
+
+/*@null@*/ gchar**
+split_quoted(const gchar* src, const gboolean unquote) {
+ /* split on unquoted space, return array of strings;
+ remove a layer of quotes and backslashes if unquote */
+ if (!src) return NULL;
+
+ gboolean dq = FALSE;
+ gboolean sq = FALSE;
+ GArray *a = g_array_new (TRUE, FALSE, sizeof(gchar*));
+ GString *s = g_string_new ("");
+ const gchar *p;
+ gchar **ret;
+ gchar *dup;
+ for (p = src; *p != '\0'; p++) {
+ if ((*p == '\\') && unquote) g_string_append_c(s, *++p);
+ else if (*p == '\\') { g_string_append_c(s, *p++);
+ g_string_append_c(s, *p); }
+ else if ((*p == '"') && unquote && !sq) dq = !dq;
+ else if (*p == '"' && !sq) { g_string_append_c(s, *p);
+ dq = !dq; }
+ else if ((*p == '\'') && unquote && !dq) sq = !sq;
+ else if (*p == '\'' && !dq) { g_string_append_c(s, *p);
+ sq = ! sq; }
+ else if ((*p == ' ') && !dq && !sq) {
+ dup = g_strdup(s->str);
+ g_array_append_val(a, dup);
+ g_string_truncate(s, 0);
+ } else g_string_append_c(s, *p);
+ }
+ dup = g_strdup(s->str);
+ g_array_append_val(a, dup);
+ ret = (gchar**)a->data;
+ g_array_free (a, FALSE);
+ g_string_free (s, TRUE);
+ return ret;
+}
+
+void
+spawn(WebKitWebView *web_view, GArray *argv, GString *result) {
+ (void)web_view; (void)result;
+ gchar *path = NULL;
+
+ //TODO: allow more control over argument order so that users can have some arguments before the default ones from run_command, and some after
+ if (argv_idx(argv, 0) &&
+ ((path = find_existing_file(argv_idx(argv, 0)))) ) {
+ run_command(path, 0,
+ ((const gchar **) (argv->data + sizeof(gchar*))),
+ FALSE, NULL);
+ g_free(path);
+ }
+}
+
+void
+spawn_sync(WebKitWebView *web_view, GArray *argv, GString *result) {
+ (void)web_view; (void)result;
+ gchar *path = NULL;
+
+ if (argv_idx(argv, 0) &&
+ ((path = find_existing_file(argv_idx(argv, 0)))) ) {
+ run_command(path, 0,
+ ((const gchar **) (argv->data + sizeof(gchar*))),
+ TRUE, &uzbl.comm.sync_stdout);
+ g_free(path);
+ }
+}
+
+void
+spawn_sh(WebKitWebView *web_view, GArray *argv, GString *result) {
+ (void)web_view; (void)result;
+ if (!uzbl.behave.shell_cmd) {
+ g_printerr ("spawn_sh: shell_cmd is not set!\n");
+ return;
+ }
+
+ guint i;
+ gchar *spacer = g_strdup("");
+ g_array_insert_val(argv, 1, spacer);
+ gchar **cmd = split_quoted(uzbl.behave.shell_cmd, TRUE);
+
+ for (i = 1; i < g_strv_length(cmd); i++)
+ g_array_prepend_val(argv, cmd[i]);
+
+ if (cmd) run_command(cmd[0], g_strv_length(cmd) + 1, (const gchar **) argv->data, FALSE, NULL);
+ g_free (spacer);
+ g_strfreev (cmd);
+}
+
+void
+spawn_sh_sync(WebKitWebView *web_view, GArray *argv, GString *result) {
+ (void)web_view; (void)result;
+ if (!uzbl.behave.shell_cmd) {
+ g_printerr ("spawn_sh_sync: shell_cmd is not set!\n");
+ return;
+ }
+
+ guint i;
+ gchar *spacer = g_strdup("");
+ g_array_insert_val(argv, 1, spacer);
+ gchar **cmd = split_quoted(uzbl.behave.shell_cmd, TRUE);
+
+ for (i = 1; i < g_strv_length(cmd); i++)
+ g_array_prepend_val(argv, cmd[i]);
+
+ if (cmd) run_command(cmd[0], g_strv_length(cmd) + 1, (const gchar **) argv->data,
+ TRUE, &uzbl.comm.sync_stdout);
+ g_free (spacer);
+ g_strfreev (cmd);
+}
+
+void
+talk_to_socket(WebKitWebView *web_view, GArray *argv, GString *result) {
+ (void)web_view; (void)result;
+
+ int fd, len;
+ struct sockaddr_un sa;
+ char* sockpath;
+ ssize_t ret;
+ struct pollfd pfd;
+ struct iovec* iov;
+ guint i;
+
+ if(uzbl.comm.sync_stdout) uzbl.comm.sync_stdout = strfree(uzbl.comm.sync_stdout);
+
+ /* This function could be optimised by storing a hash table of socket paths
+ and associated connected file descriptors rather than closing and
+ re-opening for every call. Also we could launch a script if socket connect
+ fails. */
+
+ /* First element argv[0] is path to socket. Following elements are tokens to
+ write to the socket. We write them as a single packet with each token
+ separated by an ASCII nul (\0). */
+ if(argv->len < 2) {
+ g_printerr("talk_to_socket called with only %d args (need at least two).\n",
+ (int)argv->len);
+ return;
+ }
+
+ /* copy socket path, null terminate result */
+ sockpath = g_array_index(argv, char*, 0);
+ g_strlcpy(sa.sun_path, sockpath, sizeof(sa.sun_path));
+ sa.sun_family = AF_UNIX;
+
+ /* create socket file descriptor and connect it to path */
+ fd = socket(AF_UNIX, SOCK_SEQPACKET, 0);
+ if(fd == -1) {
+ g_printerr("talk_to_socket: creating socket failed (%s)\n", strerror(errno));
+ return;
+ }
+ if(connect(fd, (struct sockaddr*)&sa, sizeof(sa))) {
+ g_printerr("talk_to_socket: connect failed (%s)\n", strerror(errno));
+ close(fd);
+ return;
+ }
+
+ /* build request vector */
+ iov = g_malloc(sizeof(struct iovec) * (argv->len - 1));
+ if(!iov) {
+ g_printerr("talk_to_socket: unable to allocated memory for token vector\n");
+ close(fd);
+ return;
+ }
+ for(i = 1; i < argv->len; ++i) {
+ iov[i - 1].iov_base = g_array_index(argv, char*, i);
+ iov[i - 1].iov_len = strlen(iov[i - 1].iov_base) + 1; /* string plus \0 */
+ }
+
+ /* write request */
+ ret = writev(fd, iov, argv->len - 1);
+ g_free(iov);
+ if(ret == -1) {
+ g_printerr("talk_to_socket: write failed (%s)\n", strerror(errno));
+ close(fd);
+ return;
+ }
+
+ /* wait for a response, with a 500ms timeout */
+ pfd.fd = fd;
+ pfd.events = POLLIN;
+ while(1) {
+ ret = poll(&pfd, 1, 500);
+ if(ret == 1) break;
+ if(ret == 0) errno = ETIMEDOUT;
+ if(errno == EINTR) continue;
+ g_printerr("talk_to_socket: poll failed while waiting for input (%s)\n",
+ strerror(errno));
+ close(fd);
+ return;
+ }
+
+ /* get length of response */
+ if(ioctl(fd, FIONREAD, &len) == -1) {
+ g_printerr("talk_to_socket: cannot find daemon response length, "
+ "ioctl failed (%s)\n", strerror(errno));
+ close(fd);
+ return;
+ }
+
+ /* if there is a response, read it */
+ if(len) {
+ uzbl.comm.sync_stdout = g_malloc(len + 1);
+ if(!uzbl.comm.sync_stdout) {
+ g_printerr("talk_to_socket: failed to allocate %d bytes\n", len);
+ close(fd);
+ return;
+ }
+ uzbl.comm.sync_stdout[len] = 0; /* ensure result is null terminated */
+
+ ret = read(fd, uzbl.comm.sync_stdout, len);
+ if(ret == -1) {
+ g_printerr("talk_to_socket: failed to read from socket (%s)\n",
+ strerror(errno));
+ close(fd);
+ return;
+ }
+ }
+
+ /* clean up */
+ close(fd);
+ return;
+}
+
+void
+parse_command(const char *cmd, const char *param, GString *result) {
+ CommandInfo *c;
+ GString *tmp = g_string_new("");
+
+ if ((c = g_hash_table_lookup(uzbl.behave.commands, cmd))) {
+ guint i;
+ gchar **par = split_quoted(param, TRUE);
+ GArray *a = g_array_new (TRUE, FALSE, sizeof(gchar*));
+
+ if (c->no_split) { /* don't split */
+ sharg_append(a, param);
+ } else if (par) {
+ for (i = 0; i < g_strv_length(par); i++)
+ sharg_append(a, par[i]);
+ }
+
+ if (result == NULL) {
+ GString *result_print = g_string_new("");
+
+ c->function(uzbl.gui.web_view, a, result_print);
+ if (result_print->len)
+ printf("%*s\n", (int)result_print->len, result_print->str);
+
+ g_string_free(result_print, TRUE);
+ } else {
+ c->function(uzbl.gui.web_view, a, result);
+ }
+ g_strfreev (par);
+ g_array_free (a, TRUE);
+
+ if(strcmp("set", cmd) &&
+ strcmp("event", cmd) &&
+ strcmp("request", cmd)) {
+ g_string_printf(tmp, "%s %s", cmd, param?param:"");
+ send_event(COMMAND_EXECUTED, tmp->str, NULL);
+ g_string_free(tmp, TRUE);
+ }
+ }
+ else {
+ gchar *tmp = g_strdup_printf("%s %s", cmd, param?param:"");
+ send_event(COMMAND_ERROR, tmp, NULL);
+ g_free(tmp);
+ }
+}
+
+
+void
+move_statusbar() {
+ if (!uzbl.gui.scrolled_win &&
+ !uzbl.gui.mainbar)
+ return;
+
+ g_object_ref(uzbl.gui.scrolled_win);
+ g_object_ref(uzbl.gui.mainbar);
+ gtk_container_remove(GTK_CONTAINER(uzbl.gui.vbox), uzbl.gui.scrolled_win);
+ gtk_container_remove(GTK_CONTAINER(uzbl.gui.vbox), uzbl.gui.mainbar);
+
+ if(uzbl.behave.status_top) {
+ gtk_box_pack_start (GTK_BOX (uzbl.gui.vbox), uzbl.gui.mainbar, FALSE, TRUE, 0);
+ gtk_box_pack_start (GTK_BOX (uzbl.gui.vbox), uzbl.gui.scrolled_win, TRUE, TRUE, 0);
+ }
+ else {
+ gtk_box_pack_start (GTK_BOX (uzbl.gui.vbox), uzbl.gui.scrolled_win, TRUE, TRUE, 0);
+ gtk_box_pack_start (GTK_BOX (uzbl.gui.vbox), uzbl.gui.mainbar, FALSE, TRUE, 0);
+ }
+ g_object_unref(uzbl.gui.scrolled_win);
+ g_object_unref(uzbl.gui.mainbar);
+ gtk_widget_grab_focus (GTK_WIDGET (uzbl.gui.web_view));
+ return;
+}
+
+gboolean
+set_var_value(const gchar *name, gchar *val) {
+ uzbl_cmdprop *c = NULL;
+ char *endp = NULL;
+ char *buf = NULL;
+ char *invalid_chars = "^°!\"§$%&/()=?'`'+~*'#-.:,;@<>| \\{}[]¹²³¼½";
+ GString *msg;
+
+ if( (c = g_hash_table_lookup(uzbl.comm.proto_var, name)) ) {
+ if(!c->writeable) return FALSE;
+
+ msg = g_string_new(name);
+
+ /* check for the variable type */
+ if (c->type == TYPE_STR) {
+ buf = g_strdup(val);
+ g_free(*c->ptr.s);
+ *c->ptr.s = buf;
+ g_string_append_printf(msg, " str %s", buf);
+
+ } else if(c->type == TYPE_INT) {
+ *c->ptr.i = (int)strtoul(val, &endp, 10);
+ g_string_append_printf(msg, " int %d", *c->ptr.i);
+
+ } else if (c->type == TYPE_FLOAT) {
+ *c->ptr.f = strtod(val, &endp);
+ g_string_append_printf(msg, " float %f", *c->ptr.f);
+ }
+
+ send_event(VARIABLE_SET, msg->str, NULL);
+ g_string_free(msg,TRUE);
+
+ /* invoke a command specific function */
+ if(c->func) c->func();
+ } else {
+ /* check wether name violates our naming scheme */
+ if(strpbrk(name, invalid_chars)) {
+ if (uzbl.state.verbose)
+ printf("Invalid variable name\n");
+ return FALSE;
+ }
+
+ /* custom vars */
+ c = g_malloc(sizeof(uzbl_cmdprop));
+ c->type = TYPE_STR;
+ c->dump = 0;
+ c->func = NULL;
+ c->writeable = 1;
+ buf = g_strdup(val);
+ c->ptr.s = g_malloc(sizeof(char *));
+ *c->ptr.s = buf;
+ g_hash_table_insert(uzbl.comm.proto_var,
+ g_strdup(name), (gpointer) c);
+
+ msg = g_string_new(name);
+ g_string_append_printf(msg, " str %s", buf);
+ send_event(VARIABLE_SET, msg->str, NULL);
+ g_string_free(msg,TRUE);
+ }
+ update_title();
+ return TRUE;
+}
+
+void
+parse_cmd_line(const char *ctl_line, GString *result) {
+ size_t len=0;
+ gchar *ctlstrip = NULL;
+ gchar *exp_line = NULL;
+ gchar *work_string = NULL;
+
+ work_string = g_strdup(ctl_line);
+
+ /* strip trailing newline */
+ len = strlen(ctl_line);
+ if (work_string[len - 1] == '\n')
+ ctlstrip = g_strndup(work_string, len - 1);
+ else
+ ctlstrip = g_strdup(work_string);
+ g_free(work_string);
+
+ if( strcmp(g_strchug(ctlstrip), "") &&
+ strcmp(exp_line = expand(ctlstrip, 0), "")
+ ) {
+ /* ignore comments */
+ if((exp_line[0] == '#'))
+ ;
+
+ /* parse a command */
+ else {
+ gchar **tokens = NULL;
+
+ tokens = g_strsplit(exp_line, " ", 2);
+ parse_command(tokens[0], tokens[1], result);
+ g_strfreev(tokens);
+ }
+ g_free(exp_line);
+ }
+
+ g_free(ctlstrip);
+}
+
+
+/*@null@*/ gchar*
+build_stream_name(int type, const gchar* dir) {
+ State *s = &uzbl.state;
+ gchar *str = NULL;
+
+ if (type == FIFO) {
+ str = g_strdup_printf
+ ("%s/uzbl_fifo_%s", dir, s->instance_name);
+ } else if (type == SOCKET) {
+ str = g_strdup_printf
+ ("%s/uzbl_socket_%s", dir, s->instance_name);
+ }
+ return str;
+}
+
+gboolean
+control_fifo(GIOChannel *gio, GIOCondition condition) {
+ if (uzbl.state.verbose)
+ printf("triggered\n");
+ gchar *ctl_line;
+ GIOStatus ret;
+ GError *err = NULL;
+
+ if (condition & G_IO_HUP)
+ g_error ("Fifo: Read end of pipe died!\n");
+
+ if(!gio)
+ g_error ("Fifo: GIOChannel broke\n");
+
+ ret = g_io_channel_read_line(gio, &ctl_line, NULL, NULL, &err);
+ if (ret == G_IO_STATUS_ERROR) {
+ g_error ("Fifo: Error reading: %s\n", err->message);
+ g_error_free (err);
+ }
+
+ parse_cmd_line(ctl_line, NULL);
+ g_free(ctl_line);
+
+ return TRUE;
+}
+
+/*@null@*/ gchar*
+init_fifo(gchar *dir) { /* return dir or, on error, free dir and return NULL */
+ if (uzbl.comm.fifo_path) { /* get rid of the old fifo if one exists */
+ if (unlink(uzbl.comm.fifo_path) == -1)
+ g_warning ("Fifo: Can't unlink old fifo at %s\n", uzbl.comm.fifo_path);
+ g_free(uzbl.comm.fifo_path);
+ uzbl.comm.fifo_path = NULL;
+ }
+
+ GIOChannel *chan = NULL;
+ GError *error = NULL;
+ gchar *path = build_stream_name(FIFO, dir);
+
+ if (!file_exists(path)) {
+ if (mkfifo (path, 0666) == 0) {
+ // we don't really need to write to the file, but if we open the file as 'r' we will block here, waiting for a writer to open the file.
+ chan = g_io_channel_new_file(path, "r+", &error);
+ if (chan) {
+ if (g_io_add_watch(chan, G_IO_IN|G_IO_HUP, (GIOFunc) control_fifo, NULL)) {
+ if (uzbl.state.verbose)
+ printf ("init_fifo: created successfully as %s\n", path);
+ send_event(FIFO_SET, path, NULL);
+ uzbl.comm.fifo_path = path;
+ return dir;
+ } else g_warning ("init_fifo: could not add watch on %s\n", path);
+ } else g_warning ("init_fifo: can't open: %s\n", error->message);
+ } else g_warning ("init_fifo: can't create %s: %s\n", path, strerror(errno));
+ } else g_warning ("init_fifo: can't create %s: file exists\n", path);
+
+ /* if we got this far, there was an error; cleanup */
+ if (error) g_error_free (error);
+ g_free(dir);
+ g_free(path);
+ return NULL;
+}
+
+gboolean
+control_stdin(GIOChannel *gio, GIOCondition condition) {
+ (void) condition;
+ gchar *ctl_line = NULL;
+ GIOStatus ret;
+
+ ret = g_io_channel_read_line(gio, &ctl_line, NULL, NULL, NULL);
+ if ( (ret == G_IO_STATUS_ERROR) || (ret == G_IO_STATUS_EOF) )
+ return FALSE;
+
+ parse_cmd_line(ctl_line, NULL);
+ g_free(ctl_line);
+
+ return TRUE;
+}
+
+void
+create_stdin () {
+ GIOChannel *chan = NULL;
+ GError *error = NULL;
+
+ chan = g_io_channel_unix_new(fileno(stdin));
+ if (chan) {
+ if (!g_io_add_watch(chan, G_IO_IN|G_IO_HUP, (GIOFunc) control_stdin, NULL)) {
+ g_error ("Stdin: could not add watch\n");
+ } else {
+ if (uzbl.state.verbose)
+ printf ("Stdin: watch added successfully\n");
+ }
+ } else {
+ g_error ("Stdin: Error while opening: %s\n", error->message);
+ }
+ if (error) g_error_free (error);
+}
+
+gboolean
+remove_socket_from_array(GIOChannel *chan) {
+ gboolean ret = 0;
+
+ /* TODO: Do wee need to manually free the IO channel or is this
+ * happening implicitly on unref?
+ */
+ ret = g_ptr_array_remove_fast(uzbl.comm.connect_chan, chan);
+ if(!ret)
+ ret = g_ptr_array_remove_fast(uzbl.comm.client_chan, chan);
+
+ return ret;
+}
+
+gboolean
+control_socket(GIOChannel *chan) {
+ struct sockaddr_un remote;
+ unsigned int t = sizeof(remote);
+ GIOChannel *iochan;
+ int clientsock;
+
+ clientsock = accept (g_io_channel_unix_get_fd(chan),
+ (struct sockaddr *) &remote, &t);
+
+ if(!uzbl.comm.client_chan)
+ uzbl.comm.client_chan = g_ptr_array_new();
+
+ if ((iochan = g_io_channel_unix_new(clientsock))) {
+ g_io_channel_set_encoding(iochan, NULL, NULL);
+ g_io_add_watch(iochan, G_IO_IN|G_IO_HUP,
+ (GIOFunc) control_client_socket, iochan);
+ g_ptr_array_add(uzbl.comm.client_chan, (gpointer)iochan);
+ }
+ return TRUE;
+}
+
+void
+init_connect_socket() {
+ int sockfd, replay = 0;
+ struct sockaddr_un local;
+ GIOChannel *chan;
+ gchar **name = NULL;
+
+ if(!uzbl.comm.connect_chan)
+ uzbl.comm.connect_chan = g_ptr_array_new();
+
+ name = uzbl.state.connect_socket_names;
+
+ while(name && *name) {
+ sockfd = socket (AF_UNIX, SOCK_STREAM, 0);
+ local.sun_family = AF_UNIX;
+ strcpy (local.sun_path, *name);
+
+ if(!connect(sockfd, (struct sockaddr *) &local, sizeof(local))) {
+ if ((chan = g_io_channel_unix_new(sockfd))) {
+ g_io_channel_set_encoding(chan, NULL, NULL);
+ g_io_add_watch(chan, G_IO_IN|G_IO_HUP,
+ (GIOFunc) control_client_socket, chan);
+ g_ptr_array_add(uzbl.comm.connect_chan, (gpointer)chan);
+ replay++;
+ }
+ }
+ else
+ g_warning("Error connecting to socket: %s\n", *name);
+ name++;
+ }
+
+ /* replay buffered events */
+ if(replay)
+ send_event_socket(NULL);
+}
+
+gboolean
+control_client_socket(GIOChannel *clientchan) {
+ char *ctl_line;
+ GString *result = g_string_new("");
+ GError *error = NULL;
+ GIOStatus ret;
+ gsize len;
+
+ ret = g_io_channel_read_line(clientchan, &ctl_line, &len, NULL, &error);
+ if (ret == G_IO_STATUS_ERROR) {
+ g_warning ("Error reading: %s\n", error->message);
+ remove_socket_from_array(clientchan);
+ g_io_channel_shutdown(clientchan, TRUE, &error);
+ return FALSE;
+ } else if (ret == G_IO_STATUS_EOF) {
+ remove_socket_from_array(clientchan);
+ /* shutdown and remove channel watch from main loop */
+ g_io_channel_shutdown(clientchan, TRUE, &error);
+ return FALSE;
+ }
+
+ if (ctl_line) {
+ parse_cmd_line (ctl_line, result);
+ g_string_append_c(result, '\n');
+ ret = g_io_channel_write_chars (clientchan, result->str, result->len,
+ &len, &error);
+ if (ret == G_IO_STATUS_ERROR) {
+ g_warning ("Error writing: %s", error->message);
+ }
+ g_io_channel_flush(clientchan, &error);
+ }
+
+ if (error) g_error_free (error);
+ g_string_free(result, TRUE);
+ g_free(ctl_line);
+ return TRUE;
+}
+
+/*@null@*/ gchar*
+init_socket(gchar *dir) { /* return dir or, on error, free dir and return NULL */
+ if (uzbl.comm.socket_path) { /* remove an existing socket should one exist */
+ if (unlink(uzbl.comm.socket_path) == -1)
+ g_warning ("init_socket: couldn't unlink socket at %s\n", uzbl.comm.socket_path);
+ g_free(uzbl.comm.socket_path);
+ uzbl.comm.socket_path = NULL;
+ }
+
+ if (*dir == ' ') {
+ g_free(dir);
+ return NULL;
+ }
+
+ GIOChannel *chan = NULL;
+ int sock, len;
+ struct sockaddr_un local;
+ gchar *path = build_stream_name(SOCKET, dir);
+
+ sock = socket (AF_UNIX, SOCK_STREAM, 0);
+
+ local.sun_family = AF_UNIX;
+ strcpy (local.sun_path, path);
+ unlink (local.sun_path);
+
+ len = strlen (local.sun_path) + sizeof (local.sun_family);
+ if (bind (sock, (struct sockaddr *) &local, len) != -1) {
+ if (uzbl.state.verbose)
+ printf ("init_socket: opened in %s\n", path);
+ listen (sock, 5);
+
+ if( (chan = g_io_channel_unix_new(sock)) ) {
+ g_io_add_watch(chan, G_IO_IN|G_IO_HUP, (GIOFunc) control_socket, chan);
+ uzbl.comm.socket_path = path;
+ send_event(SOCKET_SET, path, NULL);
+ return dir;
+ }
+ } else g_warning ("init_socket: could not open in %s: %s\n", path, strerror(errno));
+
+ /* if we got this far, there was an error; cleanup */
+ g_free(path);
+ g_free(dir);
+ return NULL;
+}
+
+/*
+ NOTE: we want to keep variables like b->title_format_long in their "unprocessed" state
+ it will probably improve performance if we would "cache" the processed variant, but for now it works well enough...
+*/
+// this function may be called very early when the templates are not set (yet), hence the checks
+void
+update_title (void) {
+ Behaviour *b = &uzbl.behave;
+ gchar *parsed;
+
+ if (b->show_status) {
+ if (b->title_format_short) {
+ parsed = expand(b->title_format_short, 0);
+ if (uzbl.gui.main_window)
+ gtk_window_set_title (GTK_WINDOW(uzbl.gui.main_window), parsed);
+ g_free(parsed);
+ }
+ if (b->status_format) {
+ parsed = expand(b->status_format, 0);
+ gtk_label_set_markup(GTK_LABEL(uzbl.gui.mainbar_label), parsed);
+ g_free(parsed);
+ }
+ if (b->status_background) {
+ GdkColor color;
+ gdk_color_parse (b->status_background, &color);
+ //labels and hboxes do not draw their own background. applying this on the vbox/main_window is ok as the statusbar is the only affected widget. (if not, we could also use GtkEventBox)
+ if (uzbl.gui.main_window)
+ gtk_widget_modify_bg (uzbl.gui.main_window, GTK_STATE_NORMAL, &color);
+ else if (uzbl.gui.plug)
+ gtk_widget_modify_bg (GTK_WIDGET(uzbl.gui.plug), GTK_STATE_NORMAL, &color);
+ }
+ } else {
+ if (b->title_format_long) {
+ parsed = expand(b->title_format_long, 0);
+ if (uzbl.gui.main_window)
+ gtk_window_set_title (GTK_WINDOW(uzbl.gui.main_window), parsed);
+ g_free(parsed);
+ }
+ }
+}
+
+void
+create_browser () {
+ GUI *g = &uzbl.gui;
+
+ g->web_view = WEBKIT_WEB_VIEW (webkit_web_view_new ());
+
+ g_object_connect((GObject*)g->web_view,
+ "signal::key-press-event", (GCallback)key_press_cb, NULL,
+ "signal::key-release-event", (GCallback)key_release_cb, NULL,
+ "signal::button-press-event", (GCallback)button_press_cb, NULL,
+ "signal::button-release-event", (GCallback)button_release_cb, NULL,
+ "signal::motion-notify-event", (GCallback)motion_notify_cb, NULL,
+ "signal::title-changed", (GCallback)title_change_cb, NULL,
+ "signal::selection-changed", (GCallback)selection_changed_cb, NULL,
+ "signal::load-progress-changed", (GCallback)progress_change_cb, NULL,
+ "signal::load-committed", (GCallback)load_commit_cb, NULL,
+ "signal::load-started", (GCallback)load_start_cb, NULL,
+ "signal::load-finished", (GCallback)load_finish_cb, NULL,
+ "signal::load-error", (GCallback)load_error_cb, NULL,
+ "signal::hovering-over-link", (GCallback)link_hover_cb, NULL,
+ "signal::navigation-policy-decision-requested", (GCallback)navigation_decision_cb, NULL,
+ "signal::new-window-policy-decision-requested", (GCallback)new_window_cb, NULL,
+ "signal::download-requested", (GCallback)download_cb, NULL,
+ "signal::create-web-view", (GCallback)create_web_view_cb, NULL,
+ "signal::mime-type-policy-decision-requested", (GCallback)mime_policy_cb, NULL,
+ "signal::resource-request-starting", (GCallback)request_starting_cb, NULL,
+ "signal::populate-popup", (GCallback)populate_popup_cb, NULL,
+ "signal::focus-in-event", (GCallback)focus_cb, NULL,
+ "signal::focus-out-event", (GCallback)focus_cb, NULL,
+ NULL);
+}
+
+GtkWidget*
+create_mainbar () {
+ GUI *g = &uzbl.gui;
+
+ g->mainbar = gtk_hbox_new (FALSE, 0);
+ g->mainbar_label = gtk_label_new ("");
+ gtk_label_set_selectable((GtkLabel *)g->mainbar_label, TRUE);
+ gtk_label_set_ellipsize(GTK_LABEL(g->mainbar_label), PANGO_ELLIPSIZE_END);
+ gtk_misc_set_alignment (GTK_MISC(g->mainbar_label), 0, 0);
+ gtk_misc_set_padding (GTK_MISC(g->mainbar_label), 2, 2);
+ gtk_box_pack_start (GTK_BOX (g->mainbar), g->mainbar_label, TRUE, TRUE, 0);
+
+ g_object_connect((GObject*)g->mainbar,
+ "signal::key-press-event", (GCallback)key_press_cb, NULL,
+ "signal::key-release-event", (GCallback)key_release_cb, NULL,
+ NULL);
+
+ return g->mainbar;
+}
+
+
+GtkWidget*
+create_window () {
+ GtkWidget* window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
+
+ gtk_window_set_default_size (GTK_WINDOW (window), 800, 600);
+ gtk_widget_set_name (window, "Uzbl browser");
+
+ g_signal_connect (G_OBJECT (window), "destroy", G_CALLBACK (destroy_cb), NULL);
+ g_signal_connect (G_OBJECT (window), "configure-event", G_CALLBACK (configure_event_cb), NULL);
+
+ return window;
+}
+
+GtkPlug*
+create_plug () {
+ GtkPlug* plug = GTK_PLUG (gtk_plug_new (uzbl.state.socket_id));
+ g_signal_connect (G_OBJECT (plug), "destroy", G_CALLBACK (destroy_cb), NULL);
+ g_signal_connect (G_OBJECT (plug), "key-press-event", G_CALLBACK (key_press_cb), NULL);
+ g_signal_connect (G_OBJECT (plug), "key-release-event", G_CALLBACK (key_release_cb), NULL);
+
+ return plug;
+}
+
+
+gchar**
+inject_handler_args(const gchar *actname, const gchar *origargs, const gchar *newargs) {
+ /*
+ If actname is one that calls an external command, this function will inject
+ newargs in front of the user-provided args in that command line. They will
+ come become after the body of the script (in sh) or after the name of
+ the command to execute (in spawn).
+ i.e. sh <body> <userargs> becomes sh <body> <ARGS> <userargs> and
+ spawn <command> <userargs> becomes spawn <command> <ARGS> <userargs>.
+
+ The return value consist of two strings: the action (sh, ...) and its args.
+
+ If act is not one that calls an external command, then the given action merely
+ gets duplicated.
+ */
+ GArray *rets = g_array_new(TRUE, FALSE, sizeof(gchar*));
+ /* Arrr! Here be memory leaks */
+ gchar *actdup = g_strdup(actname);
+ g_array_append_val(rets, actdup);
+
+ if ((g_strcmp0(actname, "spawn") == 0) ||
+ (g_strcmp0(actname, "sh") == 0) ||
+ (g_strcmp0(actname, "sync_spawn") == 0) ||
+ (g_strcmp0(actname, "sync_sh") == 0) ||
+ (g_strcmp0(actname, "talk_to_socket") == 0)) {
+ guint i;
+ GString *a = g_string_new("");
+ gchar **spawnparts = split_quoted(origargs, FALSE);
+ g_string_append_printf(a, "%s", spawnparts[0]); /* sh body or script name */
+ if (newargs) g_string_append_printf(a, " %s", newargs); /* handler args */
+
+ for (i = 1; i < g_strv_length(spawnparts); i++) /* user args */
+ if (spawnparts[i]) g_string_append_printf(a, " %s", spawnparts[i]);
+
+ g_array_append_val(rets, a->str);
+ g_string_free(a, FALSE);
+ g_strfreev(spawnparts);
+ } else {
+ gchar *origdup = g_strdup(origargs);
+ g_array_append_val(rets, origdup);
+ }
+ return (gchar**)g_array_free(rets, FALSE);
+}
+
+void
+run_handler (const gchar *act, const gchar *args) {
+ /* Consider this code a temporary hack to make the handlers usable.
+ In practice, all this splicing, injection, and reconstruction is
+ inefficient, annoying and hard to manage. Potential pitfalls arise
+ when the handler specific args 1) are not quoted (the handler
+ callbacks should take care of this) 2) are quoted but interfere
+ with the users' own quotation. A more ideal solution is
+ to refactor parse_command so that it doesn't just take a string
+ and execute it; rather than that, we should have a function which
+ returns the argument vector parsed from the string. This vector
+ could be modified (e.g. insert additional args into it) before
+ passing it to the next function that actually executes it. Though
+ it still isn't perfect for chain actions.. will reconsider & re-
+ factor when I have the time. -duc */
+
+ if (!act) return;
+ char **parts = g_strsplit(act, " ", 2);
+ if (!parts || !parts[0]) return;
+ if (g_strcmp0(parts[0], "chain") == 0) {
+ GString *newargs = g_string_new("");
+ gchar **chainparts = split_quoted(parts[1], FALSE);
+
+ /* for every argument in the chain, inject the handler args
+ and make sure the new parts are wrapped in quotes */
+ gchar **cp = chainparts;
+ gchar quot = '\'';
+ gchar *quotless = NULL;
+ gchar **spliced_quotless = NULL; // sigh -_-;
+ gchar **inpart = NULL;
+
+ while (*cp) {
+ if ((**cp == '\'') || (**cp == '\"')) { /* strip old quotes */
+ quot = **cp;
+ quotless = g_strndup(&(*cp)[1], strlen(*cp) - 2);
+ } else quotless = g_strdup(*cp);
+
+ spliced_quotless = g_strsplit(quotless, " ", 2);
+ inpart = inject_handler_args(spliced_quotless[0], spliced_quotless[1], args);
+ g_strfreev(spliced_quotless);
+
+ g_string_append_printf(newargs, " %c%s %s%c", quot, inpart[0], inpart[1], quot);
+ g_free(quotless);
+ g_strfreev(inpart);
+ cp++;
+ }
+
+ parse_command(parts[0], &(newargs->str[1]), NULL);
+ g_string_free(newargs, TRUE);
+ g_strfreev(chainparts);
+
+ } else {
+ gchar **inparts;
+ gchar *inparts_[2];
+ if (parts[1]) {
+ /* expand the user-specified arguments */
+ gchar* expanded = expand(parts[1], 0);
+ inparts = inject_handler_args(parts[0], expanded, args);
+ g_free(expanded);
+ } else {
+ inparts_[0] = parts[0];
+ inparts_[1] = g_strdup(args);
+ inparts = inparts_;
+ }
+
+ parse_command(inparts[0], inparts[1], NULL);
+
+ if (inparts != inparts_) {
+ g_free(inparts[0]);
+ g_free(inparts[1]);
+ } else
+ g_free(inparts[1]);
+ }
+ g_strfreev(parts);
+}
+
+/*@null@*/ gchar*
+get_xdg_var (XDG_Var xdg) {
+ const gchar* actual_value = getenv (xdg.environmental);
+ const gchar* home = getenv ("HOME");
+ gchar* return_value;
+
+ if (! actual_value || strcmp (actual_value, "") == 0) {
+ if (xdg.default_value) {
+ return_value = str_replace ("~", home, xdg.default_value);
+ } else {
+ return_value = NULL;
+ }
+ } else {
+ return_value = str_replace("~", home, actual_value);
+ }
+
+ return return_value;
+}
+
+/*@null@*/ gchar*
+find_xdg_file (int xdg_type, const char* filename) {
+ /* xdg_type = 0 => config
+ xdg_type = 1 => data
+ xdg_type = 2 => cache*/
+
+ gchar* xdgv = get_xdg_var (XDG[xdg_type]);
+ gchar* temporary_file = g_strconcat (xdgv, filename, NULL);
+ g_free (xdgv);
+
+ gchar* temporary_string;
+ char* saveptr;
+ char* buf;
+
+ if (! file_exists (temporary_file) && xdg_type != 2) {
+ buf = get_xdg_var (XDG[3 + xdg_type]);
+ temporary_string = (char *) strtok_r (buf, ":", &saveptr);
+ g_free(buf);
+
+ while ((temporary_string = (char * ) strtok_r (NULL, ":", &saveptr)) && ! file_exists (temporary_file)) {
+ g_free (temporary_file);
+ temporary_file = g_strconcat (temporary_string, filename, NULL);
+ }
+ }
+
+ //g_free (temporary_string); - segfaults.
+
+ if (file_exists (temporary_file)) {
+ return temporary_file;
+ } else {
+ g_free(temporary_file);
+ return NULL;
+ }
+}
+void
+settings_init () {
+ State *s = &uzbl.state;
+ Network *n = &uzbl.net;
+
+ int i;
+ for (i = 0; default_config[i].command != NULL; i++) {
+ parse_cmd_line(default_config[i].command, NULL);
+ }
+
+ if (g_strcmp0(s->config_file, "-") == 0) {
+ s->config_file = NULL;
+ create_stdin();
+ }
+
+ else if (!s->config_file) {
+ s->config_file = find_xdg_file (0, "/uzbl/config");
+ }
+
+ if (s->config_file) {
+ GArray* lines = read_file_by_line (s->config_file);
+ int i = 0;
+ gchar* line;
+
+ while ((line = g_array_index(lines, gchar*, i))) {
+ parse_cmd_line (line, NULL);
+ i ++;
+ g_free (line);
+ }
+ g_array_free (lines, TRUE);
+ } else {
+ if (uzbl.state.verbose)
+ printf ("No configuration file loaded.\n");
+ }
+
+ if(s->connect_socket_names)
+ init_connect_socket();
+
+ g_signal_connect_after(n->soup_session, "request-started", G_CALLBACK(handle_cookies), NULL);
+ g_signal_connect(n->soup_session, "authenticate", G_CALLBACK(handle_authentication), NULL);
+}
+
+void handle_authentication (SoupSession *session, SoupMessage *msg, SoupAuth *auth, gboolean retrying, gpointer user_data) {
+
+ (void) user_data;
+
+ if(uzbl.behave.authentication_handler && *uzbl.behave.authentication_handler != NULL) {
+ gchar *info, *host, *realm;
+ gchar *p;
+
+ soup_session_pause_message(session, msg);
+
+ /* Sanitize strings */
+ info = g_strdup(soup_auth_get_info(auth));
+ host = g_strdup(soup_auth_get_host(auth));
+ realm = g_strdup(soup_auth_get_realm(auth));
+ for (p = info; *p; p++) if (*p == '\'') *p = '\"';
+ for (p = host; *p; p++) if (*p == '\'') *p = '\"';
+ for (p = realm; *p; p++) if (*p == '\'') *p = '\"';
+
+ GString *s = g_string_new ("");
+ g_string_printf(s, "'%s' '%s' '%s' '%s'",
+ info, host, realm, retrying?"TRUE":"FALSE");
+
+ run_handler(uzbl.behave.authentication_handler, s->str);
+
+ if (uzbl.comm.sync_stdout && strcmp (uzbl.comm.sync_stdout, "") != 0) {
+ char *username, *password;
+ int number_of_endls=0;
+
+ username = uzbl.comm.sync_stdout;
+
+ for (p = uzbl.comm.sync_stdout; *p; p++) {
+ if (*p == '\n') {
+ *p = '\0';
+ if (++number_of_endls == 1)
+ password = p + 1;
+ }
+ }
+
+ /* If stdout was correct (contains exactly two lines of text) do
+ * authenticate. */
+ if (number_of_endls == 2)
+ soup_auth_authenticate(auth, username, password);
+ }
+
+ if (uzbl.comm.sync_stdout)
+ uzbl.comm.sync_stdout = strfree(uzbl.comm.sync_stdout);
+
+ soup_session_unpause_message(session, msg);
+
+ g_string_free(s, TRUE);
+ g_free(info);
+ g_free(host);
+ g_free(realm);
+ }
+}
+
+void handle_cookies (SoupSession *session, SoupMessage *msg, gpointer user_data){
+ (void) session;
+ (void) user_data;
+ //if (!uzbl.behave.cookie_handler)
+ // return;
+
+ soup_message_add_header_handler(msg, "got-headers", "Set-Cookie", G_CALLBACK(save_cookies), NULL);
+ GString *s = g_string_new ("");
+ SoupURI * soup_uri = soup_message_get_uri(msg);
+ g_string_printf(s, "GET '%s' '%s' '%s'", soup_uri->scheme, soup_uri->host, soup_uri->path);
+ if(uzbl.behave.cookie_handler)
+ run_handler(uzbl.behave.cookie_handler, s->str);
+
+ if(uzbl.behave.cookie_handler &&
+ uzbl.comm.sync_stdout && strcmp (uzbl.comm.sync_stdout, "") != 0) {
+ char *p = strchr(uzbl.comm.sync_stdout, '\n' );
+ if ( p != NULL ) *p = '\0';
+ soup_message_headers_replace (msg->request_headers, "Cookie", (const char *) uzbl.comm.sync_stdout);
+
+ }
+ if (uzbl.comm.sync_stdout)
+ uzbl.comm.sync_stdout = strfree(uzbl.comm.sync_stdout);
+
+ g_string_free(s, TRUE);
+}
+
+void
+save_cookies (SoupMessage *msg, gpointer user_data){
+ (void) user_data;
+ GSList *ck;
+ char *cookie;
+ for (ck = soup_cookies_from_response(msg); ck; ck = ck->next){
+ cookie = soup_cookie_to_set_cookie_header(ck->data);
+ SoupURI * soup_uri = soup_message_get_uri(msg);
+ GString *s = g_string_new ("");
+ g_string_printf(s, "PUT '%s' '%s' '%s' '%s'", soup_uri->scheme, soup_uri->host, soup_uri->path, cookie);
+ run_handler(uzbl.behave.cookie_handler, s->str);
+ g_free (cookie);
+ g_string_free(s, TRUE);
+ }
+ g_slist_free(ck);
+}
+
+void
+dump_var_hash(gpointer k, gpointer v, gpointer ud) {
+ (void) ud;
+ uzbl_cmdprop *c = v;
+
+ if(!c->dump)
+ return;
+
+ if(c->type == TYPE_STR)
+ printf("set %s = %s\n", (char *)k, *c->ptr.s ? *c->ptr.s : " ");
+ else if(c->type == TYPE_INT)
+ printf("set %s = %d\n", (char *)k, *c->ptr.i);
+ else if(c->type == TYPE_FLOAT)
+ printf("set %s = %f\n", (char *)k, *c->ptr.f);
+}
+
+void
+dump_config() {
+ g_hash_table_foreach(uzbl.comm.proto_var, dump_var_hash, NULL);
+}
+
+void
+dump_var_hash_as_event(gpointer k, gpointer v, gpointer ud) {
+ (void) ud;
+ uzbl_cmdprop *c = v;
+ GString *msg;
+
+ if(!c->dump)
+ return;
+
+ /* check for the variable type */
+ msg = g_string_new((char *)k);
+ if (c->type == TYPE_STR) {
+ g_string_append_printf(msg, " str %s", *c->ptr.s ? *c->ptr.s : " ");
+ } else if(c->type == TYPE_INT) {
+ g_string_append_printf(msg, " int %d", *c->ptr.i);
+ } else if (c->type == TYPE_FLOAT) {
+ g_string_append_printf(msg, " float %f", *c->ptr.f);
+ }
+
+ send_event(VARIABLE_SET, msg->str, NULL);
+ g_string_free(msg, TRUE);
+}
+
+void
+dump_config_as_events() {
+ g_hash_table_foreach(uzbl.comm.proto_var, dump_var_hash_as_event, NULL);
+}
+
+void
+retrieve_geometry() {
+ int w, h, x, y;
+ GString *buf = g_string_new("");
+
+ gtk_window_get_size(GTK_WINDOW(uzbl.gui.main_window), &w, &h);
+ gtk_window_get_position(GTK_WINDOW(uzbl.gui.main_window), &x, &y);
+
+ g_string_printf(buf, "%dx%d+%d+%d", w, h, x, y);
+
+ if(uzbl.gui.geometry)
+ g_free(uzbl.gui.geometry);
+ uzbl.gui.geometry = g_string_free(buf, FALSE);
+}
+
+/* set up gtk, gobject, variable defaults and other things that tests and other
+ * external applications need to do anyhow */
+void
+initialize(int argc, char *argv[]) {
+ int i;
+
+ for(i=0; i<argc; ++i) {
+ if(!strcmp(argv[i], "-s") || !strcmp(argv[i], "--socket")) {
+ uzbl.state.plug_mode = TRUE;
+ break;
+ }
+ }
+
+ if (!g_thread_supported ())
+ g_thread_init (NULL);
+ gtk_init (&argc, &argv);
+
+ uzbl.state.executable_path = g_strdup(argv[0]);
+ uzbl.state.selected_url = NULL;
+ uzbl.state.searchtx = NULL;
+
+ GOptionContext* context = g_option_context_new ("[ uri ] - load a uri by default");
+ g_option_context_add_main_entries (context, entries, NULL);
+ g_option_context_add_group (context, gtk_get_option_group (TRUE));
+ g_option_context_parse (context, &argc, &argv, NULL);
+ g_option_context_free(context);
+
+ if (uzbl.behave.print_version) {
+ printf("Commit: %s\n", COMMIT);
+ exit(EXIT_SUCCESS);
+ }
+
+ uzbl.net.soup_session = webkit_get_default_session();
+ uzbl.state.keycmd = g_strdup("");
+
+ for(i=0; sigs[i]; i++) {
+ if(setup_signal(sigs[i], catch_signal) == SIG_ERR)
+ fprintf(stderr, "uzbl: error hooking %d: %s\n", sigs[i], strerror(errno));
+ }
+ event_buffer_timeout(10);
+
+ uzbl.info.webkit_major = webkit_major_version();
+ uzbl.info.webkit_minor = webkit_minor_version();
+ uzbl.info.webkit_micro = webkit_micro_version();
+ uzbl.info.arch = ARCH;
+ uzbl.info.commit = COMMIT;
+
+ commands_hash ();
+ create_var_to_name_hash();
+
+ create_mainbar();
+ create_browser();
+}
+
+void
+load_uri_imp(gchar *uri) {
+ GString* newuri;
+
+ /* Strip leading whitespaces */
+ while (*uri) {
+ if (!isspace(*uri)) break;
+ uri++;
+ }
+
+ if (g_strstr_len (uri, 11, "javascript:") != NULL) {
+ eval_js(uzbl.gui.web_view, uri, NULL, "javascript:");
+ return;
+ }
+ newuri = g_string_new (uri);
+ if (!soup_uri_new(uri)) {
+ GString* fullpath = g_string_new ("");
+ if (g_path_is_absolute (newuri->str))
+ g_string_assign (fullpath, newuri->str);
+ else {
+ gchar* wd;
+ wd = g_get_current_dir ();
+ g_string_assign (fullpath, g_build_filename (wd, newuri->str, NULL));
+ free(wd);
+ }
+ struct stat stat_result;
+ if (! g_stat(fullpath->str, &stat_result)) {
+ g_string_prepend (fullpath, "file://");
+ g_string_assign (newuri, fullpath->str);
+ }
+ else
+ g_string_prepend (newuri, "http://");
+ g_string_free (fullpath, TRUE);
+ }
+ /* if we do handle cookies, ask our handler for them */
+ webkit_web_view_load_uri (uzbl.gui.web_view, newuri->str);
+ g_string_free (newuri, TRUE);
+}
+
+
+#ifndef UZBL_LIBRARY
+/** -- MAIN -- **/
+int
+main (int argc, char* argv[]) {
+ initialize(argc, argv);
+
+ uzbl.gui.scrolled_win = gtk_scrolled_window_new (NULL, NULL);
+ gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (uzbl.gui.scrolled_win),
+ GTK_POLICY_NEVER, GTK_POLICY_NEVER);
+
+ gtk_container_add (GTK_CONTAINER (uzbl.gui.scrolled_win),
+ GTK_WIDGET (uzbl.gui.web_view));
+
+ uzbl.gui.vbox = gtk_vbox_new (FALSE, 0);
+
+ /* initial packing */
+ gtk_box_pack_start (GTK_BOX (uzbl.gui.vbox), uzbl.gui.scrolled_win, TRUE, TRUE, 0);
+ gtk_box_pack_start (GTK_BOX (uzbl.gui.vbox), uzbl.gui.mainbar, FALSE, TRUE, 0);
+
+ if (uzbl.state.plug_mode) {
+ uzbl.gui.plug = create_plug ();
+ gtk_container_add (GTK_CONTAINER (uzbl.gui.plug), uzbl.gui.vbox);
+ gtk_widget_show_all (GTK_WIDGET (uzbl.gui.plug));
+ /* in xembed mode the window has no unique id and thus
+ * socket/fifo names aren't unique either.
+ * we use a custom randomizer to create a random id
+ */
+ struct timeval tv;
+ gettimeofday(&tv, NULL);
+ srand((unsigned int)tv.tv_sec*tv.tv_usec);
+ uzbl.xwin = rand();
+ } else {
+ uzbl.gui.main_window = create_window ();
+ gtk_container_add (GTK_CONTAINER (uzbl.gui.main_window), uzbl.gui.vbox);
+ gtk_widget_show_all (uzbl.gui.main_window);
+ uzbl.xwin = GDK_WINDOW_XID (GTK_WIDGET (uzbl.gui.main_window)->window);
+ }
+
+ uzbl.gui.scbar_v = (GtkScrollbar*) gtk_vscrollbar_new (NULL);
+ uzbl.gui.bar_v = gtk_range_get_adjustment((GtkRange*) uzbl.gui.scbar_v);
+ uzbl.gui.scbar_h = (GtkScrollbar*) gtk_hscrollbar_new (NULL);
+ uzbl.gui.bar_h = gtk_range_get_adjustment((GtkRange*) uzbl.gui.scbar_h);
+ gtk_widget_set_scroll_adjustments ((GtkWidget*) uzbl.gui.web_view, uzbl.gui.bar_h, uzbl.gui.bar_v);
+
+ if(!uzbl.state.instance_name)
+ uzbl.state.instance_name = itos((int)uzbl.xwin);
+
+ GString *tmp = g_string_new("");
+ g_string_printf(tmp, "%d", getpid());
+ uzbl.info.pid_str = g_string_free(tmp, FALSE);
+ send_event(INSTANCE_START, uzbl.info.pid_str, NULL);
+
+ if(uzbl.state.plug_mode) {
+ char *t = itos(gtk_plug_get_id(uzbl.gui.plug));
+ send_event(PLUG_CREATED, t, NULL);
+ g_free(t);
+ }
+
+ /* generate an event with a list of built in commands */
+ builtins();
+
+ gtk_widget_grab_focus (GTK_WIDGET (uzbl.gui.web_view));
+
+ if (uzbl.state.verbose) {
+ printf("Uzbl start location: %s\n", argv[0]);
+ if (uzbl.state.socket_id)
+ printf("plug_id %i\n", gtk_plug_get_id(uzbl.gui.plug));
+ else
+ printf("window_id %i\n",(int) uzbl.xwin);
+ printf("pid %i\n", getpid ());
+ printf("name: %s\n", uzbl.state.instance_name);
+ printf("commit: %s\n", uzbl.info.commit);
+ }
+
+ /* Check uzbl is in window mode before getting/setting geometry */
+ if (uzbl.gui.main_window) {
+ if(uzbl.gui.geometry)
+ cmd_set_geometry();
+ else
+ retrieve_geometry();
+ }
+
+ gchar *uri_override = (uzbl.state.uri ? g_strdup(uzbl.state.uri) : NULL);
+ if (argc > 1 && !uzbl.state.uri)
+ uri_override = g_strdup(argv[1]);
+ gboolean verbose_override = uzbl.state.verbose;
+
+ settings_init ();
+
+ if (!uzbl.behave.show_status)
+ gtk_widget_hide(uzbl.gui.mainbar);
+ else
+ update_title();
+
+ /* WebInspector */
+ set_up_inspector();
+
+ if (verbose_override > uzbl.state.verbose)
+ uzbl.state.verbose = verbose_override;
+
+ if (uri_override) {
+ set_var_value("uri", uri_override);
+ g_free(uri_override);
+ }
+
+ gtk_main ();
+ clean_up();
+
+ return EXIT_SUCCESS;
+}
+#endif
+
+/* vi: set et ts=4: */
diff --git a/src/uzbl-core.h b/src/uzbl-core.h
new file mode 100644
index 0000000..23072e4
--- /dev/null
+++ b/src/uzbl-core.h
@@ -0,0 +1,504 @@
+/* -*- c-basic-offset: 4; -*-
+
+ * See LICENSE for license details
+ *
+ * Changelog:
+ * ---------
+ *
+ * (c) 2009 by Robert Manea
+ * - introduced struct concept
+ *
+ */
+
+#define _POSIX_SOURCE
+
+#include <glib/gstdio.h>
+#include <gtk/gtk.h>
+#include <gdk/gdkx.h>
+#include <gdk/gdkkeysyms.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/un.h>
+#include <sys/utsname.h>
+#include <sys/time.h>
+#include <webkit/webkit.h>
+#include <libsoup/soup.h>
+#include <JavaScriptCore/JavaScript.h>
+
+#include <stdio.h>
+#include <string.h>
+#include <ctype.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <assert.h>
+#include <poll.h>
+#include <sys/uio.h>
+#include <sys/ioctl.h>
+#include <assert.h>
+
+#define LENGTH(x) (sizeof x / sizeof x[0])
+
+/* gui elements */
+typedef struct {
+ GtkWidget* main_window;
+ gchar* geometry;
+ GtkPlug* plug;
+ GtkWidget* scrolled_win;
+ GtkWidget* vbox;
+ GtkWidget* mainbar;
+ GtkWidget* mainbar_label;
+ GtkScrollbar* scbar_v; // Horizontal and Vertical Scrollbar
+ GtkScrollbar* scbar_h; // (These are still hidden)
+ GtkAdjustment* bar_v; // Information about document length
+ GtkAdjustment* bar_h; // and scrolling position
+ int scrollbars_visible;
+ WebKitWebView* web_view;
+ gchar* main_title;
+ gchar* icon;
+
+ /* WebInspector */
+ GtkWidget *inspector_window;
+ WebKitWebInspector *inspector;
+
+ /* custom context menu item */
+ GPtrArray *menu_items;
+} GUI;
+
+
+/* external communication*/
+enum { FIFO, SOCKET};
+typedef struct {
+ gchar *fifo_path;
+ gchar *socket_path;
+ /* stores (key)"variable name" -> (value)"pointer to var*/
+ GHashTable *proto_var;
+
+ gchar *sync_stdout;
+ GPtrArray *connect_chan;
+ GPtrArray *client_chan;
+} Communication;
+
+
+/* internal state */
+typedef struct {
+ gchar *uri;
+ gchar *config_file;
+ int socket_id;
+ char *instance_name;
+ gchar *selected_url;
+ gchar *last_selected_url;
+ gchar *executable_path;
+ gchar* keycmd;
+ gchar* searchtx;
+ gboolean verbose;
+ gboolean events_stdout;
+ GPtrArray *event_buffer;
+ gchar** connect_socket_names;
+ GdkEventButton *last_button;
+ gboolean plug_mode;
+} State;
+
+
+/* networking */
+typedef struct {
+ SoupSession *soup_session;
+ SoupLogger *soup_logger;
+ char *proxy_url;
+ char *useragent;
+ gint max_conns;
+ gint max_conns_host;
+} Network;
+
+
+/* behaviour */
+typedef struct {
+ gchar* status_format;
+ gchar* title_format_short;
+ gchar* title_format_long;
+ gchar* status_background;
+ gchar* fifo_dir;
+ gchar* socket_dir;
+ gchar* download_handler;
+ gchar* cookie_handler;
+ gchar* authentication_handler;
+ gchar* new_window;
+ gchar* default_font_family;
+ gchar* monospace_font_family;
+ gchar* sans_serif_font_family;
+ gchar* serif_font_family;
+ gchar* fantasy_font_family;
+ gchar* cursive_font_family;
+ gchar* scheme_handler;
+ gboolean show_status;
+ gboolean forward_keys;
+ gboolean status_top;
+ guint modmask;
+ guint http_debug;
+ gchar* shell_cmd;
+ guint view_source;
+ /* WebKitWebSettings exports */
+ guint font_size;
+ guint monospace_size;
+ guint minimum_font_size;
+ gfloat zoom_level;
+ gboolean zoom_type;
+ guint disable_plugins;
+ guint disable_scripts;
+ guint autoload_img;
+ guint autoshrink_img;
+ guint enable_spellcheck;
+ guint enable_private;
+ guint print_bg;
+ gchar* style_uri;
+ guint resizable_txt;
+ gchar* default_encoding;
+ guint enforce_96dpi;
+ gchar *inject_html;
+ guint caret_browsing;
+ guint mode;
+ gchar* base_url;
+ gboolean print_version;
+
+ /* command list: (key)name -> (value)Command */
+ /* command list: (key)name -> (value)Command */
+ GHashTable* commands;
+ /* event lookup: (key)event_id -> (value)event_name */
+ GHashTable *event_lookup;
+} Behaviour;
+
+/* javascript */
+typedef struct {
+ gboolean initialized;
+ JSClassDefinition classdef;
+ JSClassRef classref;
+} Javascript;
+
+/* static information */
+typedef struct {
+ int webkit_major;
+ int webkit_minor;
+ int webkit_micro;
+ gchar *arch;
+ gchar *commit;
+ gchar *pid_str;
+} Info;
+
+/* main uzbl data structure */
+typedef struct {
+ GUI gui;
+ State state;
+ Network net;
+ Behaviour behave;
+ Communication comm;
+ Javascript js;
+ Info info;
+
+ Window xwin;
+} UzblCore;
+
+/* Main Uzbl object */
+extern UzblCore uzbl;
+
+typedef void sigfunc(int);
+
+/* XDG Stuff */
+typedef struct {
+ gchar* environmental;
+ gchar* default_value;
+} XDG_Var;
+
+/* uzbl variables */
+enum ptr_type {TYPE_INT, TYPE_STR, TYPE_FLOAT};
+typedef struct {
+ enum ptr_type type;
+ union {
+ int *i;
+ float *f;
+ gchar **s;
+ } ptr;
+ int dump;
+ int writeable;
+ /*@null@*/ void (*func)(void);
+} uzbl_cmdprop;
+
+/* Functions */
+char *
+itos(int val);
+
+char *
+str_replace (const char* search, const char* replace, const char* string);
+
+gchar*
+strfree(gchar *str);
+
+GArray*
+read_file_by_line (const gchar *path);
+
+gchar*
+parseenv (gchar* string);
+
+void
+clean_up(void);
+
+void
+catch_sigterm(int s);
+
+sigfunc *
+setup_signal(int signe, sigfunc *shandler);
+
+gboolean
+set_var_value(const gchar *name, gchar *val);
+
+void
+load_uri_imp(gchar *uri);
+
+void
+print(WebKitWebView *page, GArray *argv, GString *result);
+
+void
+commands_hash(void);
+
+bool
+file_exists (const char * filename);
+
+void
+set_keycmd();
+
+void
+load_uri (WebKitWebView * web_view, GArray *argv, GString *result);
+
+void
+new_window_load_uri (const gchar * uri);
+
+void
+chain (WebKitWebView *page, GArray *argv, GString *result);
+
+void
+close_uzbl (WebKitWebView *page, GArray *argv, GString *result);
+
+gboolean
+run_command(const gchar *command, const guint npre,
+ const gchar **args, const gboolean sync, char **output_stdout);
+
+void
+talk_to_socket(WebKitWebView *web_view, GArray *argv, GString *result);
+
+void
+spawn(WebKitWebView *web_view, GArray *argv, GString *result);
+
+void
+spawn_sh(WebKitWebView *web_view, GArray *argv, GString *result);
+
+void
+spawn_sync(WebKitWebView *web_view, GArray *argv, GString *result);
+
+void
+spawn_sh_sync(WebKitWebView *web_view, GArray *argv, GString *result);
+
+void
+parse_command(const char *cmd, const char *param, GString *result);
+
+void
+parse_cmd_line(const char *ctl_line, GString *result);
+
+/*@null@*/ gchar*
+build_stream_name(int type, const gchar *dir);
+
+gboolean
+control_fifo(GIOChannel *gio, GIOCondition condition);
+
+/*@null@*/ gchar*
+init_fifo(gchar *dir);
+
+gboolean
+control_stdin(GIOChannel *gio, GIOCondition condition);
+
+void
+create_stdin();
+
+/*@null@*/ gchar*
+init_socket(gchar *dir);
+
+gboolean
+control_socket(GIOChannel *chan);
+
+gboolean
+control_client_socket(GIOChannel *chan);
+
+void
+update_title (void);
+
+gboolean
+key_press_cb (GtkWidget* window, GdkEventKey* event);
+
+gboolean
+key_release_cb (GtkWidget* window, GdkEventKey* event);
+
+void
+run_keycmd(const gboolean key_ret);
+
+void
+initialize (int argc, char *argv[]);
+
+void
+create_browser ();
+
+GtkWidget*
+create_mainbar ();
+
+GtkWidget*
+create_window ();
+
+GtkPlug*
+create_plug ();
+
+void
+run_handler (const gchar *act, const gchar *args);
+
+/*@null@*/ gchar*
+get_xdg_var (XDG_Var xdg);
+
+/*@null@*/ gchar*
+find_xdg_file (int xdg_type, const char* filename);
+
+void
+settings_init ();
+
+void
+search_text (WebKitWebView *page, GArray *argv, const gboolean forward);
+
+void
+search_forward_text (WebKitWebView *page, GArray *argv, GString *result);
+
+void
+search_reverse_text (WebKitWebView *page, GArray *argv, GString *result);
+
+void
+search_clear(WebKitWebView *page, GArray *argv, GString *result);
+
+void
+dehilight (WebKitWebView *page, GArray *argv, GString *result);
+
+void
+run_js (WebKitWebView * web_view, GArray *argv, GString *result);
+
+void
+run_external_js (WebKitWebView * web_view, GArray *argv, GString *result);
+
+void
+eval_js(WebKitWebView * web_view, gchar *script, GString *result, const gchar *script_file);
+
+void
+handle_authentication (SoupSession *session,
+ SoupMessage *msg,
+ SoupAuth *auth,
+ gboolean retrying,
+ gpointer user_data);
+
+void handle_cookies (SoupSession *session,
+ SoupMessage *msg,
+ gpointer user_data);
+void
+save_cookies (SoupMessage *msg, gpointer user_data);
+
+void
+set_var(WebKitWebView *page, GArray *argv, GString *result);
+
+void
+act_dump_config();
+
+void
+act_dump_config_as_events();
+
+void
+dump_var_hash(gpointer k, gpointer v, gpointer ud);
+
+void
+dump_key_hash(gpointer k, gpointer v, gpointer ud);
+
+void
+dump_config();
+
+void
+dump_config_as_events();
+
+void
+retrieve_geometry();
+
+void
+event(WebKitWebView *page, GArray *argv, GString *result);
+
+void
+init_connect_socket();
+
+gboolean
+remove_socket_from_array(GIOChannel *chan);
+
+void
+menu_add(WebKitWebView *page, GArray *argv, GString *result);
+
+void
+menu_add_link(WebKitWebView *page, GArray *argv, GString *result);
+
+void
+menu_add_image(WebKitWebView *page, GArray *argv, GString *result);
+
+void
+menu_add_edit(WebKitWebView *page, GArray *argv, GString *result);
+
+void
+menu_add_separator(WebKitWebView *page, GArray *argv, GString *result);
+
+void
+menu_add_separator_link(WebKitWebView *page, GArray *argv, GString *result);
+
+void
+menu_add_separator_image(WebKitWebView *page, GArray *argv, GString *result);
+
+void
+menu_add_separator_edit(WebKitWebView *page, GArray *argv, GString *result);
+
+void
+menu_remove(WebKitWebView *page, GArray *argv, GString *result);
+
+void
+menu_remove_link(WebKitWebView *page, GArray *argv, GString *result);
+
+void
+menu_remove_image(WebKitWebView *page, GArray *argv, GString *result);
+
+void
+menu_remove_edit(WebKitWebView *page, GArray *argv, GString *result);
+
+gint
+get_click_context();
+
+void
+hardcopy(WebKitWebView *page, GArray *argv, GString *result);
+
+void
+include(WebKitWebView *page, GArray *argv, GString *result);
+
+void
+builtins();
+
+typedef void (*Command)(WebKitWebView*, GArray *argv, GString *result);
+
+typedef struct {
+ Command function;
+ gboolean no_split;
+} CommandInfo;
+
+typedef struct {
+ gchar *name;
+ gchar *cmd;
+ gboolean issep;
+ guint context;
+} MenuItem;
+
+
+/* vi: set et ts=4: */
diff --git a/tests/Makefile b/tests/Makefile
new file mode 100644
index 0000000..3f63adf
--- /dev/null
+++ b/tests/Makefile
@@ -0,0 +1,17 @@
+CFLAGS:=-std=c99 -I$(shell pwd)/../ -L$(shell pwd) -luzbl-core $(shell pkg-config --cflags gtk+-2.0 webkit-1.0 libsoup-2.4 gthread-2.0) -ggdb -Wall -W -DARCH="\"$(shell uname -m)\"" -lgthread-2.0 -DG_ERRORCHECK_MUTEXES -DCOMMIT="\"$(shell git log | head -n1 | sed "s/.* //")\"" $(CPPFLAGS)
+CFLAGS!=echo -std=c99 `pkg-config --cflags gtk+-2.0 webkit-1.0 libsoup-2.4 gthread-2.0` -ggdb -Wall -W -DARCH='"\""'`uname -m`'"\""' -lgthread-2.0 -DG_ERRORCHECK_MUTEXES -DCOMMIT='"\""'`git log | head -n1 | sed "s/.* //"`'"\""' $(CPPFLAGS)
+
+LDFLAGS:=$(shell pkg-config --libs gtk+-2.0 webkit-1.0 libsoup-2.4 gthread-2.0) -pthread $(LDFLAGS)
+LDFLAGS!=echo `pkg-config --libs gtk+-2.0 webkit-1.0 libsoup-2.4 gthread-2.0` -pthread $(LDFLAGS)
+
+GTESTER:=gtester
+GTESTER_REPORT:=gtester-report
+
+TEST_PROGS:=test-expand test-command
+
+all: $(TEST_PROGS)
+ LD_LIBRARY_PATH="$(LD_LIBRARY_PATH):." $(GTESTER) --verbose $(TEST_PROGS)
+
+clean:
+ rm -f $(TEST_PROGS)
+ rm -f libuzbl-core.so
diff --git a/tests/test-command.c b/tests/test-command.c
new file mode 100644
index 0000000..6194081
--- /dev/null
+++ b/tests/test-command.c
@@ -0,0 +1,377 @@
+/* -*- c-basic-offset: 4; -*- */
+#define _POSIX_SOURCE
+
+#include <gtk/gtk.h>
+#include <gdk/gdkx.h>
+#include <gdk/gdkkeysyms.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/un.h>
+#include <sys/utsname.h>
+#include <sys/time.h>
+#include <webkit/webkit.h>
+#include <libsoup/soup.h>
+#include <JavaScriptCore/JavaScript.h>
+
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <signal.h>
+
+#include <src/uzbl-core.h>
+#include <src/config.h>
+
+extern UzblCore uzbl;
+
+#define INSTANCE_NAME "testing"
+
+gchar*
+assert_str_beginswith(GString *expected, gchar *actual) {
+ gchar *actual_beginning = g_strndup(actual, expected->len);
+ g_assert_cmpstr(expected->str, ==, actual_beginning);
+ g_free(actual_beginning);
+
+ /* return the part of the actual string that hasn't been compared yet */
+ return &actual[expected->len];
+}
+
+/* compare the contents of uzbl.comm.sync_stdout to the standard arguments that
+ * should have been passed. This is meant to be called after something like "sync echo". */
+gchar*
+assert_sync_beginswith_stdarg() {
+ GString *stdargs = g_string_new("");
+
+ g_string_append_printf(stdargs, "%s %d %d ", uzbl.state.config_file, getpid(), (int)uzbl.xwin);
+ g_string_append_printf(stdargs, "%s %s ", uzbl.comm.fifo_path, uzbl.comm.socket_path);
+ g_string_append_printf(stdargs, "%s %s ", uzbl.state.uri, uzbl.gui.main_title);
+
+ gchar *rest = assert_str_beginswith(stdargs, uzbl.comm.sync_stdout);
+
+ g_string_free(stdargs, TRUE);
+
+ return rest;
+}
+
+#define ASSERT_EVENT(EF, STR) { read_event(ef); \
+ g_assert_cmpstr("EVENT [" INSTANCE_NAME "] " STR "\n", ==, ef->event_buffer); }
+
+struct EventFixture
+{
+ /* uzbl's end of the socketpair */
+ int uzbl_sock;
+
+ /* the test framework's end of the socketpair */
+ int test_sock;
+ char event_buffer[1024];
+};
+
+void
+read_event (struct EventFixture *ef) {
+ int r = read(ef->test_sock, ef->event_buffer, 1023); \
+ ef->event_buffer[r] = 0;
+}
+
+void
+assert_no_event (struct EventFixture *ef) {
+ fd_set rfds;
+
+ FD_ZERO(&rfds);
+ FD_SET(ef->test_sock, &rfds);
+
+ struct timeval timeout;
+ timeout.tv_sec = 0;
+ timeout.tv_usec = 0;
+
+ /* check if there's any data waiting */
+ int res = select(ef->test_sock + 1, &rfds, NULL, NULL, &timeout);
+
+ if(res == 0) {
+ /* timeout expired, there was no event */
+
+ /* success */
+ return;
+ } else if(res == -1) {
+ /* mechanical failure */
+ perror("select():");
+ assert(0);
+ } else {
+ /* there was an event. display it. */
+ read_event(ef);
+ g_assert_cmpstr("", ==, ef->event_buffer);
+ }
+}
+
+void
+event_fixture_setup(struct EventFixture *ef, const void* data)
+{
+ (void) data;
+
+ int socks[2];
+
+ /* make some sockets, fresh for every test */
+ if(socketpair(AF_UNIX, SOCK_STREAM, 0, socks) == -1)
+ {
+ perror("socketpair() failed");
+ g_assert(0);
+ }
+
+ ef->uzbl_sock = socks[0];
+ ef->test_sock = socks[1];
+
+ /* attach uzbl_sock to uzbl's event dispatcher. */
+ GIOChannel *iochan = g_io_channel_unix_new(ef->uzbl_sock);
+ g_io_channel_set_encoding(iochan, NULL, NULL);
+
+ if(!uzbl.comm.connect_chan)
+ uzbl.comm.connect_chan = g_ptr_array_new();
+ if(!uzbl.comm.client_chan)
+ uzbl.comm.client_chan = g_ptr_array_new();
+ g_ptr_array_add(uzbl.comm.client_chan, (gpointer)iochan);
+}
+
+void
+event_fixture_teardown(struct EventFixture *ef, const void *data)
+{
+ (void) data;
+
+ /* there should be no events left waiting */
+ assert_no_event(ef);
+
+ /* clean up the io channel we opened for uzbl */
+ GIOChannel *iochan = g_ptr_array_index(uzbl.comm.client_chan, 0);
+ remove_socket_from_array(iochan);
+
+ /* close the sockets so that nothing sticks around between tests */
+ close(ef->uzbl_sock);
+ close(ef->test_sock);
+}
+
+/* actual tests begin here */
+
+void
+test_event (struct EventFixture *ef, const void *data) {
+ (void) data;
+
+ parse_cmd_line("event", NULL);
+ assert_no_event(ef);
+
+ /* a simple event can be sent */
+ parse_cmd_line("event event_type arg u ments", NULL);
+ ASSERT_EVENT(ef, "EVENT_TYPE arg u ments");
+
+ /* arguments to event should be expanded */
+ parse_cmd_line("event event_type @(echo expansion)@ test", NULL);
+ ASSERT_EVENT(ef, "EVENT_TYPE expansion test");
+
+ /* "request" is just an alias for "event" */
+ parse_cmd_line("request event_type arg u ments", NULL);
+ ASSERT_EVENT(ef, "EVENT_TYPE arg u ments");
+}
+
+
+void
+test_set_variable (struct EventFixture *ef, const void *data) {
+ (void) data;
+
+ /* set a string */
+ parse_cmd_line("set useragent = Uzbl browser kthxbye!", NULL);
+ ASSERT_EVENT(ef, "VARIABLE_SET useragent str Uzbl browser kthxbye!");
+ g_assert_cmpstr("Uzbl browser kthxbye!", ==, uzbl.net.useragent);
+
+ /* set an int */
+ parse_cmd_line("set forward_keys = 0", NULL);
+ ASSERT_EVENT(ef, "VARIABLE_SET forward_keys int 0");
+ g_assert_cmpint(0, ==, uzbl.behave.forward_keys);
+
+ /* set a float */
+ /* we have to be careful about locales here */
+ GString *cmd, *ev;
+ cmd = g_string_new("set zoom_level = ");
+ g_string_append_printf(cmd, "%f", 0.25);
+ parse_cmd_line(g_string_free(cmd, FALSE), NULL);
+
+ ev = g_string_new("EVENT [" INSTANCE_NAME "] VARIABLE_SET zoom_level float ");
+ g_string_append_printf(ev, "%f\n", 0.25);
+ read_event(ef);
+ g_assert_cmpstr(g_string_free(ev, FALSE), ==, ef->event_buffer);
+
+ g_assert_cmpfloat(0.25, ==, uzbl.behave.zoom_level);
+
+ /* set a constant int (nothing should happen) */
+ int old_major = uzbl.info.webkit_major;
+ parse_cmd_line("set WEBKIT_MAJOR = 100", NULL);
+ assert_no_event(ef);
+ g_assert_cmpint(old_major, ==, uzbl.info.webkit_major);
+
+ /* set a constant str (nothing should happen) */
+ GString *old_arch = g_string_new(uzbl.info.arch);
+ parse_cmd_line("set ARCH_UZBL = A Lisp Machine", NULL);
+ assert_no_event(ef);
+ g_assert_cmpstr(g_string_free(old_arch, FALSE), ==, uzbl.info.arch);
+
+ /* set a custom variable */
+ parse_cmd_line("set nonexistant_variable = Some Value", NULL);
+ ASSERT_EVENT(ef, "VARIABLE_SET nonexistant_variable str Some Value");
+ uzbl_cmdprop *c = g_hash_table_lookup(uzbl.comm.proto_var, "nonexistant_variable");
+ g_assert_cmpstr("Some Value", ==, *c->ptr.s);
+
+ /* set a custom variable with expansion */
+ parse_cmd_line("set an_expanded_variable = Test @(echo expansion)@", NULL);
+ ASSERT_EVENT(ef, "VARIABLE_SET an_expanded_variable str Test expansion");
+ c = g_hash_table_lookup(uzbl.comm.proto_var, "an_expanded_variable");
+ g_assert_cmpstr("Test expansion", ==, *c->ptr.s);
+}
+
+void
+test_print (void) {
+ GString *result = g_string_new("");
+
+ /* a simple message can be returned as a result */
+ parse_cmd_line("print A simple test", result);
+ g_assert_cmpstr("A simple test", ==, result->str);
+
+ /* arguments to print should be expanded */
+ parse_cmd_line("print A simple @(echo expansion)@ test", result);
+ g_assert_cmpstr("A simple expansion test", ==, result->str);
+
+ g_string_free(result, TRUE);
+}
+
+void
+test_scroll (void) {
+ uzbl.gui.scbar_v = (GtkScrollbar*) gtk_vscrollbar_new (NULL);
+ uzbl.gui.bar_v = gtk_range_get_adjustment((GtkRange*) uzbl.gui.scbar_v);
+
+ gtk_adjustment_set_lower(uzbl.gui.bar_v, 0);
+ gtk_adjustment_set_upper(uzbl.gui.bar_v, 100);
+ gtk_adjustment_set_page_size(uzbl.gui.bar_v, 5);
+
+ /* scroll vertical end should scroll it to upper - page_size */
+ parse_cmd_line("scroll vertical end", NULL);
+ g_assert_cmpfloat(gtk_adjustment_get_value(uzbl.gui.bar_v), ==, 95);
+
+ /* scroll vertical begin should scroll it to lower */
+ parse_cmd_line("scroll vertical begin", NULL);
+ g_assert_cmpfloat(gtk_adjustment_get_value(uzbl.gui.bar_v), ==, 0);
+
+ /* scroll vertical can scroll by pixels */
+ parse_cmd_line("scroll vertical 15", NULL);
+ g_assert_cmpfloat(gtk_adjustment_get_value(uzbl.gui.bar_v), ==, 15);
+
+ parse_cmd_line("scroll vertical -10", NULL);
+ g_assert_cmpfloat(gtk_adjustment_get_value(uzbl.gui.bar_v), ==, 5);
+
+ /* scroll vertical can scroll by a percentage of the page size */
+ parse_cmd_line("scroll vertical 100%", NULL);
+ g_assert_cmpfloat(gtk_adjustment_get_value(uzbl.gui.bar_v), ==, 10);
+
+ parse_cmd_line("scroll vertical 150%", NULL);
+ g_assert_cmpfloat(gtk_adjustment_get_value(uzbl.gui.bar_v), ==, 17.5);
+
+ /* scroll_horz behaves basically the same way. */
+}
+
+void
+test_toggle_status (void) {
+ g_assert(!uzbl.behave.show_status);
+
+ /* status bar can be toggled on */
+ parse_cmd_line("toggle_status", NULL);
+ g_assert(uzbl.behave.show_status);
+
+ /* status bar can be toggled back off */
+ parse_cmd_line("toggle_status", NULL);
+ g_assert(!uzbl.behave.show_status);
+}
+
+void
+test_sync_sh (void) {
+ parse_cmd_line("sync_sh 'echo Test echo.'", NULL);
+ g_assert_cmpstr("Test echo.\n", ==, uzbl.comm.sync_stdout);
+
+ /* clean up after ourselves */
+ uzbl.comm.sync_stdout = strfree(uzbl.comm.sync_stdout);
+}
+
+void
+test_js (void) {
+ GString *result = g_string_new("");
+
+ /* simple javascript can be evaluated and returned */
+ parse_cmd_line("js ('x' + 345).toUpperCase()", result);
+ g_assert_cmpstr("X345", ==, result->str);
+
+ g_string_free(result, TRUE);
+}
+
+void
+test_run_handler_arg_order (void) {
+ run_handler("sync_spawn echo uvw xyz", "abc def");
+
+ assert(uzbl.comm.sync_stdout);
+
+ /* the result should begin with the standard handler arguments */
+ gchar *rest = assert_sync_beginswith_stdarg();
+
+ /* the rest of the result should be the arguments passed to run_handler. */
+ /* the arguments in the second argument to run_handler should be placed before any
+ * included in the first argument to run handler. */
+ g_assert_cmpstr("abc def uvw xyz\n", ==, rest);
+}
+
+void
+test_run_handler_expand (void) {
+ uzbl.net.useragent = "Test uzbl uzr agent";
+ run_handler("sync_spawn echo @useragent", "result:");
+
+ assert(uzbl.comm.sync_stdout);
+
+ /* the result should begin with the standard handler arguments */
+ gchar *rest = assert_sync_beginswith_stdarg();
+
+ /* the rest of the result should be the arguments passed to run_handler. */
+ /* the user-specified arguments to the handler should have been expanded */
+ g_assert_cmpstr("result: Test uzbl uzr agent\n", ==, rest);
+}
+
+int
+main (int argc, char *argv[]) {
+ /* set up tests */
+ g_type_init();
+ g_test_init(&argc, &argv, NULL);
+
+ g_test_add("/test-command/set-variable", struct EventFixture, NULL, event_fixture_setup, test_set_variable, event_fixture_teardown);
+ g_test_add("/test-command/event", struct EventFixture, NULL, event_fixture_setup, test_event, event_fixture_teardown);
+
+ g_test_add_func("/test-command/print", test_print);
+ g_test_add_func("/test-command/scroll", test_scroll);
+ g_test_add_func("/test-command/toggle-status", test_toggle_status);
+ g_test_add_func("/test-command/sync-sh", test_sync_sh);
+
+ g_test_add_func("/test-command/js", test_js);
+
+ /* the following aren't really "command" tests, but they're not worth
+ * splitting into a separate file yet */
+ g_test_add_func("/test-command/run_handler/arg-order", test_run_handler_arg_order);
+ g_test_add_func("/test-command/run_handler/expand", test_run_handler_expand);
+
+ /* set up uzbl */
+ initialize(argc, argv);
+
+ uzbl.state.config_file = "/tmp/uzbl-config";
+ uzbl.comm.fifo_path = "/tmp/some-nonexistant-fifo";
+ uzbl.comm.socket_path = "/tmp/some-nonexistant-socket";
+ uzbl.state.uri = "http://example.org/";
+ uzbl.gui.main_title = "Example.Org";
+
+ uzbl.state.instance_name = INSTANCE_NAME;
+ uzbl.behave.shell_cmd = "sh -c";
+
+ return g_test_run();
+}
+
+/* vi: set et ts=4: */
diff --git a/tests/test-expand.c b/tests/test-expand.c
new file mode 100644
index 0000000..7ea3d61
--- /dev/null
+++ b/tests/test-expand.c
@@ -0,0 +1,209 @@
+/* -*- c-basic-offset: 4; -*- */
+#define _POSIX_SOURCE
+
+#include <gtk/gtk.h>
+#include <gdk/gdkx.h>
+#include <gdk/gdkkeysyms.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/un.h>
+#include <sys/utsname.h>
+#include <sys/time.h>
+#include <webkit/webkit.h>
+#include <libsoup/soup.h>
+#include <JavaScriptCore/JavaScript.h>
+
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <signal.h>
+
+#include <src/uzbl-core.h>
+#include <src/config.h>
+
+extern UzblCore uzbl;
+
+extern gchar* expand(char*, guint);
+extern void make_var_to_name_hash(void);
+
+void
+test_keycmd (void) {
+ uzbl.state.keycmd = "gg winslow";
+ g_assert_cmpstr(expand("@keycmd", 0), ==, "gg winslow");
+}
+
+void
+test_uri (void) {
+ g_assert_cmpstr(expand("@uri", 0), ==, "");
+
+ uzbl.state.uri = g_strdup("http://www.uzbl.org/");
+ g_assert_cmpstr(expand("@uri", 0), ==, uzbl.state.uri);
+ g_free(uzbl.state.uri);
+}
+
+void
+test_TITLE (void) {
+ uzbl.gui.main_title = "Lorem Ipsum";
+ g_assert_cmpstr(expand("@TITLE", 0), ==, "Lorem Ipsum");
+}
+
+void
+test_SELECTED_URI (void) {
+ uzbl.state.selected_url = "http://example.org/";
+ g_assert_cmpstr(expand("@SELECTED_URI", 0), ==, "http://example.org/");
+}
+
+void
+test_NAME (void) {
+ uzbl.state.instance_name = "testing";
+ g_assert_cmpstr(expand("@NAME", 0), ==, "testing");
+}
+
+void
+test_useragent (void) {
+ uzbl.net.useragent = "This is the uzbl browser (sort of). and btw: Hello from frosty Edmonton!";
+ g_assert_cmpstr(expand("@useragent", 0), ==, "This is the uzbl browser (sort of). and btw: Hello from frosty Edmonton!");
+}
+
+void
+test_WEBKIT_VERSION (void) {
+ GString* expected = g_string_new("");
+ g_string_append(expected, itos(webkit_major_version()));
+ g_string_append(expected, " ");
+ g_string_append(expected, itos(webkit_minor_version()));
+ g_string_append(expected, " ");
+ g_string_append(expected, itos(webkit_micro_version()));
+
+ g_assert_cmpstr(expand("@WEBKIT_MAJOR @WEBKIT_MINOR @WEBKIT_MICRO", 0), ==, g_string_free(expected, FALSE));
+}
+
+void
+test_ARCH_UZBL (void) {
+ g_assert_cmpstr(expand("@ARCH_UZBL", 0), ==, ARCH);
+}
+
+void
+test_COMMIT (void) {
+ g_assert_cmpstr(expand("@COMMIT", 0), ==, uzbl.info.commit);
+}
+
+void
+test_cmd_useragent_simple (void) {
+ GString* expected = g_string_new("Uzbl (Webkit ");
+ g_string_append(expected, itos(WEBKIT_MAJOR_VERSION));
+ g_string_append(expected, ".");
+ g_string_append(expected, itos(WEBKIT_MINOR_VERSION));
+ g_string_append(expected, ".");
+ g_string_append(expected, itos(WEBKIT_MICRO_VERSION));
+ g_string_append(expected, ")");
+
+ g_assert_cmpstr(expand("Uzbl (Webkit @WEBKIT_MAJOR.@WEBKIT_MINOR.@WEBKIT_MICRO)", 0), ==, g_string_free(expected, FALSE));
+}
+
+void
+test_cmd_useragent_full (void) {
+ GString* expected = g_string_new("Uzbl (Webkit ");
+ g_string_append(expected, itos(WEBKIT_MAJOR_VERSION));
+ g_string_append(expected, ".");
+ g_string_append(expected, itos(WEBKIT_MINOR_VERSION));
+ g_string_append(expected, ".");
+ g_string_append(expected, itos(WEBKIT_MICRO_VERSION));
+ g_string_append(expected, ") (");
+
+ struct utsname unameinfo;
+ if(uname(&unameinfo) == -1)
+ g_printerr("Can't retrieve unameinfo. This test might fail.\n");
+
+ g_string_append(expected, unameinfo.sysname);
+ g_string_append(expected, " ");
+ g_string_append(expected, unameinfo.nodename);
+ g_string_append(expected, " ");
+ g_string_append(expected, unameinfo.release);
+ g_string_append(expected, " ");
+ g_string_append(expected, unameinfo.version);
+ g_string_append(expected, " ");
+ g_string_append(expected, unameinfo.machine);
+ g_string_append(expected, " [");
+ g_string_append(expected, ARCH);
+ g_string_append(expected, "]) (Commit ");
+ g_string_append(expected, uzbl.info.commit);
+ g_string_append(expected, ")");
+
+ g_assert_cmpstr(expand("Uzbl (Webkit @WEBKIT_MAJOR.@WEBKIT_MINOR.@WEBKIT_MICRO) (@(uname -s)@ @(uname -n)@ @(uname -r)@ @(uname -v)@ @(uname -m)@ [@ARCH_UZBL]) (Commit @COMMIT)", 0), ==, g_string_free(expected, FALSE));
+}
+
+void
+test_escape_markup (void) {
+ /* simple expansion */
+ uzbl.state.uri = g_strdup("<&>");
+ g_assert_cmpstr(expand("@uri", 0), ==, uzbl.state.uri);
+ g_assert_cmpstr(expand("@[@uri]@", 0), ==, "&lt;&amp;&gt;");
+
+ /* shell expansion */
+ g_assert_cmpstr(expand("@(echo -n '<&>')@", 0), ==, "<&>");
+ g_assert_cmpstr(expand("@[@(echo -n '<&>')@]@", 0), ==, "&lt;&amp;&gt;");
+
+ /* javascript expansion */
+ g_assert_cmpstr(expand("@<'<&>'>@", 0), ==, "<&>");
+ g_assert_cmpstr(expand("@[@<'<&>'>@]@", 0), ==, "&lt;&amp;&gt;");
+
+ g_free(uzbl.state.uri);
+}
+
+void
+test_escape_expansion (void) {
+ /* \@ -> @ */
+ g_assert_cmpstr(expand("\\@uri", 0), ==, "@uri");
+
+ /* \\\@ -> \@ */
+ g_assert_cmpstr(expand("\\\\\\@uri", 0), ==, "\\@uri");
+
+ /* \@(...)\@ -> @(...)@ */
+ g_assert_cmpstr(expand("\\@(echo hi)\\@", 0), ==, "@(echo hi)@");
+
+ /* \@<...>\@ -> @<...>@ */
+ g_assert_cmpstr(expand("\\@<\"hi\">\\@", 0), ==, "@<\"hi\">@");
+}
+
+void
+test_nested (void) {
+ uzbl.net.useragent = "xxx";
+ g_assert_cmpstr(expand("@<\"..@useragent..\">@", 0), ==, "..xxx..");
+ g_assert_cmpstr(expand("@<\"..\\@useragent..\">@", 0), ==, "..@useragent..");
+
+ g_assert_cmpstr(expand("@(echo ..@useragent..)@", 0), ==, "..xxx..");
+ g_assert_cmpstr(expand("@(echo ..\\@useragent..)@", 0), ==, "..@useragent..");
+}
+
+int
+main (int argc, char *argv[]) {
+ g_type_init();
+ g_test_init(&argc, &argv, NULL);
+
+ g_test_add_func("/test-expand/@keycmd", test_keycmd);
+ g_test_add_func("/test-expand/@useragent", test_useragent);
+ g_test_add_func("/test-expand/@uri", test_uri);
+ g_test_add_func("/test-expand/@TITLE", test_TITLE);
+ g_test_add_func("/test-expand/@SELECTED_URI", test_SELECTED_URI);
+ g_test_add_func("/test-expand/@NAME", test_NAME);
+ g_test_add_func("/test-expand/@WEBKIT_*", test_WEBKIT_VERSION);
+ g_test_add_func("/test-expand/@ARCH_UZBL", test_ARCH_UZBL);
+ g_test_add_func("/test-expand/@COMMIT", test_COMMIT);
+
+ g_test_add_func("/test-expand/cmd_useragent_simple", test_cmd_useragent_simple);
+ g_test_add_func("/test-expand/cmd_useragent_full", test_cmd_useragent_full);
+
+ g_test_add_func("/test-expand/escape_markup", test_escape_markup);
+ g_test_add_func("/test-expand/escape_expansion", test_escape_expansion);
+ g_test_add_func("/test-expand/nested", test_nested);
+
+ initialize(argc, argv);
+
+ return g_test_run();
+}
+
+/* vi: set et ts=4: */