diff --git a/.gitignore b/.gitignore index 8c08dc0..092909f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ uzbl-core +local.mk *.o *.lo *.pyc @@ -6,3 +7,4 @@ uzbl-core *~ tags sandbox +/build diff --git a/AUTHORS b/AUTHORS index b3ce2e2..22d3c9b 100644 --- a/AUTHORS +++ b/AUTHORS @@ -46,6 +46,7 @@ In alphabetical order: Gregor Uhlenheuer (kongo2002) - uzbl vim syntax & related files Helmut Grohne (helmut) - move void **ptr to union, various fixes Henri Kemppainen (DuClare) - many contributions, mostly old handler code + HÃ¥kan Jerning - uzbl-tabbed: autosave_session patch Igor Bogomazov - mouse ptr events Jake Probst - uzbl_tabbed: multiline tablist, new window opening patches James Campos (aeosynth) - Re-orderable gtk notebook tabs in uzbl-tabbed diff --git a/Makefile b/Makefile index a11fc8d..ba74e57 100644 --- a/Makefile +++ b/Makefile @@ -1,25 +1,53 @@ +# Create a local.mk file to store default local settings to override the +# defaults below. +include $(wildcard local.mk) + # 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) -DOCDIR?=$(INSTALLDIR)/share/uzbl/docs -RUN_PREFIX?=$(PREFIX) +PREFIX ?= /usr/local +INSTALLDIR ?= $(DESTDIR)$(PREFIX) +DOCDIR ?= $(INSTALLDIR)/share/uzbl/docs +RUN_PREFIX ?= $(PREFIX) + +ENABLE_WEBKIT2 ?= no +ENABLE_GTK3 ?= auto + +PYTHON=python3 +PYTHONV=$(shell $(PYTHON) --version | sed -n /[0-9].[0-9]/p) +COVERAGE=$(shell which coverage) + +# --- configuration ends here --- + +ifeq ($(ENABLE_WEBKIT2),auto) +ENABLE_WEBKIT2 := $(shell pkg-config --exists webkit2gtk-3.0 && echo yes) +endif -# use GTK3-based webkit when it is available -USE_GTK3 = $(shell pkg-config --exists gtk+-3.0 webkitgtk-3.0 && echo 1) +ifeq ($(ENABLE_GTK3),auto) +ENABLE_GTK3 := $(shell pkg-config --exists gtk+-3.0 && echo yes) +endif -ifeq ($(USE_GTK3),1) - REQ_PKGS += gtk+-3.0 webkitgtk-3.0 javascriptcoregtk-3.0 - CPPFLAGS = -DG_DISABLE_DEPRECATED -DGTK_DISABLE_DEPRECATED +ifeq ($(ENABLE_WEBKIT2),yes) +REQ_PKGS += 'webkit2gtk-3.0 >= 1.2.4' javascriptcoregtk-3.0 +CPPFLAGS += -DUSE_WEBKIT2 +# WebKit2 requires GTK3 +ENABLE_GTK3 := yes +else +ifeq ($(ENABLE_GTK3),yes) +REQ_PKGS += 'webkitgtk-3.0 >= 1.2.4' javascriptcoregtk-3.0 else - REQ_PKGS += gtk+-2.0 webkit-1.0 javascriptcoregtk-1.0 - CPPFLAGS = +REQ_PKGS += 'webkit-1.0 >= 1.2.4' javascriptcoregtk-1.0 +endif endif -# --- configuration ends here --- +ifeq ($(ENABLE_GTK3),yes) +REQ_PKGS += gtk+-3.0 +CPPFLAGS += -DG_DISABLE_DEPRECATED -DGTK_DISABLE_DEPRECATED +else +REQ_PKGS += gtk+-2.0 +endif -REQ_PKGS += libsoup-2.4 gthread-2.0 glib-2.0 +REQ_PKGS += 'libsoup-2.4 >= 2.30' gthread-2.0 glib-2.0 ARCH:=$(shell uname -m) @@ -33,10 +61,11 @@ LDLIBS:=$(shell pkg-config --libs $(REQ_PKGS) x11) CFLAGS += -std=c99 $(PKG_CFLAGS) -ggdb -W -Wall -Wextra -pedantic -pthread -SRC = $(wildcard src/*.c) +SRC = $(wildcard src/*.c) HEAD = $(wildcard src/*.h) OBJ = $(foreach obj, $(SRC:.c=.o), $(notdir $(obj))) LOBJ = $(foreach obj, $(SRC:.c=.lo), $(notdir $(obj))) +PY = $(wildcard uzbl/*.py uzbl/plugins/*.py) all: uzbl-browser @@ -46,7 +75,13 @@ ${OBJ}: ${HEAD} uzbl-core: ${OBJ} -uzbl-browser: uzbl-core +uzbl-browser: uzbl-core uzbl-event-manager + +build: ${PY} + $(PYTHON) setup.py build + +.PHONY: uzbl-event-manager +uzbl-event-manager: build # the 'tests' target can never be up to date .PHONY: tests @@ -61,38 +96,43 @@ tests: ${LOBJ} force $(CC) -shared -Wl ${LOBJ} -o ./tests/libuzbl-core.so cd ./tests/; $(MAKE) +test-event-manager: force + ${PYTHON} -m unittest discover tests/event-manager -v + +coverage-event-manager: force + ${PYTHON} ${COVERAGE} erase + ${PYTHON} ${COVERAGE} run -m unittest discover tests/event-manager + ${PYTHON} ${COVERAGE} html ${PY} + # Hmm, I wonder what a good default browser would be + uzbl-browser htmlcov/index.html + test-uzbl-core: uzbl-core ./uzbl-core --uri http://www.uzbl.org --verbose test-uzbl-browser: uzbl-browser ./bin/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 +test-uzbl-core-sandbox: sandbox uzbl-core sandbox-install-uzbl-core sandbox-install-example-data . ./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-browser - make DESTDIR=./sandbox RUN_PREFIX=`pwd`/sandbox/usr/local install-example-data - cp -np ./misc/env.sh ./sandbox/env.sh - -. ./sandbox/env.sh && uzbl-event-manager restart -avv +test-uzbl-browser-sandbox: sandbox uzbl-browser sandbox-install-uzbl-browser sandbox-install-example-data + . ./sandbox/env.sh && ${PYTHON} -S `which uzbl-event-manager` restart -navv & . ./sandbox/env.sh && uzbl-browser --uri http://www.uzbl.org --verbose - . ./sandbox/env.sh && uzbl-event-manager stop -ivv + . ./sandbox/env.sh && ${PYTHON} -S `which uzbl-event-manager` stop -vv -o /dev/null make DESTDIR=./sandbox uninstall rm -rf ./sandbox/usr -test-uzbl-tabbed-sandbox: uzbl-browser - make DESTDIR=./sandbox RUN_PREFIX=`pwd`/sandbox/usr/local install-uzbl-browser - make DESTDIR=./sandbox RUN_PREFIX=`pwd`/sandbox/usr/local install-uzbl-tabbed - make DESTDIR=./sandbox RUN_PREFIX=`pwd`/sandbox/usr/local install-example-data - cp -np ./misc/env.sh ./sandbox/env.sh - -. ./sandbox/env.sh && uzbl-event-manager restart -avv +test-uzbl-tabbed-sandbox: sandbox uzbl-browser sandbox-install-uzbl-browser sandbox-install-uzbl-tabbed sandbox-install-example-data + . ./sandbox/env.sh && ${PYTHON} -S `which uzbl-event-manager` restart -avv . ./sandbox/env.sh && uzbl-tabbed - . ./sandbox/env.sh && uzbl-event-manager stop -ivv + . ./sandbox/env.sh && ${PYTHON} -S `which uzbl-event-manager` stop -avv + make DESTDIR=./sandbox uninstall + rm -rf ./sandbox/usr + +test-uzbl-event-manager-sandbox: sandbox uzbl-browser sandbox-install-uzbl-browser sandbox-install-example-data + . ./sandbox/env.sh && ${PYTHON} -S `which uzbl-event-manager` restart -navv make DESTDIR=./sandbox uninstall rm -rf ./sandbox/usr @@ -102,18 +142,44 @@ clean: find ./examples/ -name "*.pyc" -delete cd ./tests/; $(MAKE) clean rm -rf ./sandbox/ + $(PYTHON) setup.py clean strip: @echo Stripping binary @strip uzbl-core @echo ... done. +SANDBOXOPTS=\ + DESTDIR=./sandbox\ + RUN_PREFIX=`pwd`/sandbox/usr/local\ + PYINSTALL_EXTRA='--prefix=./sandbox/usr/local --install-scripts=./sandbox/usr/local/bin' + +sandbox: misc/env.sh + mkdir -p sandbox/${PREFIX}/lib64 + cp -p misc/env.sh sandbox/env.sh + test -e sandbox/${PREFIX}/lib || ln -s lib64 sandbox/${PREFIX}/lib + +sandbox-install-uzbl-browser: + make ${SANDBOXOPTS} install-uzbl-browser + +sandbox-install-uzbl-tabbed: + make ${SANDBOXOPTS} install-uzbl-tabbed + +sandbox-install-uzbl-core: + make ${SANDBOXOPTS} install-uzbl-core + +sandbox-install-event-manager: + make ${SANDBOXOPTS} install-event-manager + +sandbox-install-example-data: + make ${SANDBOXOPTS} install-example-data + install: install-uzbl-core install-uzbl-browser install-uzbl-tabbed install-dirs: [ -d "$(INSTALLDIR)/bin" ] || install -d -m755 $(INSTALLDIR)/bin -install-uzbl-core: all install-dirs +install-uzbl-core: uzbl-core install-dirs install -d $(INSTALLDIR)/share/uzbl/ install -d $(DOCDIR) install -m644 docs/* $(DOCDIR)/ @@ -123,8 +189,7 @@ install-uzbl-core: all install-dirs install -m755 uzbl-core $(INSTALLDIR)/bin/uzbl-core install-event-manager: install-dirs - sed "s#^PREFIX = .*#PREFIX = '$(RUN_PREFIX)'#" < bin/uzbl-event-manager > $(INSTALLDIR)/bin/uzbl-event-manager - chmod 755 $(INSTALLDIR)/bin/uzbl-event-manager + $(PYTHON) setup.py install --prefix=$(PREFIX) --install-scripts=$(INSTALLDIR)/bin $(PYINSTALL_EXTRA) install-uzbl-browser: install-dirs install-uzbl-core install-event-manager sed 's#^PREFIX=.*#PREFIX=$(RUN_PREFIX)#' < bin/uzbl-browser > $(INSTALLDIR)/bin/uzbl-browser diff --git a/README b/README index b124fb4..5241aba 100644 --- a/README +++ b/README @@ -252,18 +252,42 @@ The following commands are recognized: - Open the print dialog. * `include ` - Read contents of `` and interpret as a set of `uzbl` commands. -* `show_inspector` - - Show the WebInspector +* `inspector | node >` + - Control the inspector. The `coord` command coordinates are relative to the + viewport, not the page. The `node` subcommand requires webkitgtk >= + 1.3.17. +* `spell_checker ... | learn ... | autocorrect | guesses >` + - Control the spell checker. Requires webkitgtk >= 1.5.1. * `add_cookie ` - Adds a new cookie to the cookie jar * `delete_cookie [ ]` - Deletes a matching cookie from the cookie jar. scheme and expire time - is currently not considered when matching. + is currently not considered when matching. * `clear_cookies` - Clears all cookies from the cookie jar * `download []` - Starts a download using the given uri. A destination file path can be given to specify where the download should be written to. +* `auth ` + - Authenticate as `` with `` for the previously issued + challenge with the id ``. authenticating for a invalid id or one + expired one has no effect. +* `snapshot ` + - Saves an image of the visible page as a PNG to the given path. Only available + with webkitgtk >= 1.9.6. This is not in webkit2gtk. +* `load []` + - Load a string as text/html with the given uri. If given, all links will be + assumed relative to baseuri. Requires webkit2gtk >= 1.9.90. +* `save [ []]` + - Saves the current page to a file in a given format (currently only "mhtml" + is supported). Requires webkit2gtk >= 1.9.90. +* `remove_all_db` + - Removes all of the web databases from the current database directory path. +* `plugin_refresh` + - Refreshes the plugin database. Requires webkitgtk >= 1.3.8. +* `plugin_toggle [ [...]]` + - Toggles whether the plugins named as arguments are enabled. No arguments is + interpreted as "all plugins". Requires webkitgtk >= 1.3.8. ### VARIABLES AND CONSTANTS @@ -283,8 +307,10 @@ file). * `uri`: The URI of the current page. (callback: load the uri) * `verbose`: Controls the verbosity printed to `stdout`. +* `inject_text`: Inject an text string, navigating to the URI "about:blank" and + rendering the text string given. Only available in webkit2gtk. * `inject_html`: Inject an HTML string, navigating to the URI "about:blank" and - rendering the HTML sting given. + rendering the HTML string given. * `geometry`: Geometry and position of the Uzbl window. Format is "x++". * `keycmd`: Holds the input buffer (callback: update input buffer). @@ -333,11 +359,16 @@ file). * `useragent`: The User-Agent to send to the browser, expands variables in its definition. * `accept_languages`: The Accept-Language header to send with HTTP requests. +* `transparent`: If set to 1, the background of the view will be transparent + (default 0). +* `view_mode`: The view mode for webkit. One of: "windowed", "floating", + "fullscreen", "maximized", or "minimized". Requires webkitgtk >= 1.3.4. * `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. + full-content zoom is off, only the text of a page is zoomed. This is + unavailable with webkit2gtk. Use `zoom_text_only` instead. * `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 @@ -349,7 +380,6 @@ file). * `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). -* `enable_pagecache`: Enable the webkit pagecache (it caches rendered pages for a speedup when you go back or forward in history) (default 0). * `enable_plugins`: Disable embedded plugin objects (default 0). * `enable_scripts`: Disable embedded scripting languages (default 0). * `autoload_images`: Automatically load images (default 1). @@ -360,25 +390,109 @@ file). `en_CA` or `pt_BR`) to be used for spell checking, separated by commas. Defaults to the value returned by `gtk_get_default_language`. * `enable_private`: Whether to enable private browsing mode (default 0). +* `cookie_policy`: If set to 0, all cookies are accepted, if set to 1, all + cookies are rejected, and 2 rejects third party cookies (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"). -* `current_encoding`: This can be set to force a text encoding. +* `custom_encoding`: This can be set to force a text encoding. (Used to be + `current_encoding` which is now read-only). * `enforce_96_dpi`: Enforce a resolution of 96 DPI (default 1). +* `editable`: Whether the page can be edited or not (default 0). * `caret_browsing`: Whether the caret is enabled in the text portion of pages (default 0). * `enable_cross_file_access`: Whether a page loaded from a `file://` URI can access the contents of other `file://` URIs. (default 0). * `follow_hint_keys`: keys for keyboard-based navigation and link - highlighting + highlighting * `handle_multi_click`: If set to 1, event handlers attached to `2Button*` - and `3Button*` bindings will only be used instead of the default actions in - WebKit (default 0). + and `3Button*` bindings will only be used instead of the default actions in + WebKit (default 0). * `ssl_ca_file`: File that contains CA certificates. * `ssl_verify`: If set to 1, uzbl won't connect to "https" url unless it can validate certificate presented by remote server against `ssl_ca_file`. +* `enable_builtin_auth`: Enable WebKits builtin authentication handler +* `enable_java_applet`: If set to 1, support for Java tags will be + enabled (default 1). +* `enable_database`: If set to 1, support for HTML5 client-side SQL databases + will be enabled (default 1). +* `enable_local_storage`: If set to 1, websites will be able to store data + locally (default 1). +* `enable_pagecache`: If set to 1, uzbl will store previously visited pages for + faster access (the cache is local to each uzbl instance) (default 0). +* `enable_offline_app_cache`: If set to 1, web applications may be cached locally + for offline use (default 1). +* `enable_universal_file_access`: If set to 1, allow `file://` URIs to access + all pages (default 0). +* `enable_hyperlink_auditing`: If set to 1, the `ping` attribute on anchors will + be supported (default 0). +* `zoom_step`: The change in the zoon level when zooming (default 0.1). +* `auto_resize_window`: If set to 1, allow web pages to change window dimensions + (default 0). +* `enable_spatial_navigation`: If set to 1, the arrow keys in `Ins` mode will + navigate between form elements (default 0). +* `editing_behavior`: When set to 0, emulate Mac behavior in text fields, 1 + for Windows behavior, and 2 for *nix behavior (the default). +* `enable_tab_cycle`: If set to 1, the `Tab` key cycles between elements on + the page (default 1). +* `default_context_menu`: If set to 0, do not cause context menus to appear when + right clicking (default 1). +* `enable_site_workarounds`: If set to 1, enable filters to help unbreak + certain websites (default 0). +* `javascript_clipboard`: If set to 1, JavaScript may access the clipboard + (default 0). Requires webkitgtk >= 1.3.0. +* `javascript_dom_paste`: If set to 1, JavaScript will able to paste from the + clipboard (default 0). +* `enable_frame_flattening`: If set to 1, flatten all frames into a single + page to become one scrollable page (default 0). Requires webkitgtk >= 1.3.5. +* `enable_fullscreen`: If set to 1, Mozilla-style fullscreening will be + available (default 0). Requires webkitgtk >= 1.3.8 +* `enable_dns_prefetch`: If set to 1, domain names will be prefetched + (default 1). Private browsing does *not* affect this value. Requires + webkitgtk >= 1.3.13. +* `display_insecure_content`: If set to 1, non-HTTPS content will be displayed + on HTTPS pages (default 1). Requires webkitgtk >= 1.11.13. +* `run_insecure_content`: If set to 1, non-HTTPS content will be allowed to run + on HTTPS pages (default 1). Requires webkitgtk >= 1.11.13. +* `maintain_history`: If set to 1, the back/forward list will be kept. (default + 1). +* `enable_webgl`: If set to 1, WebGL support will be enabled (default 0). + Requires webkitgtk >= 1.3.14. +* `local_storage_path`: Where to store local databases (default + $XDG_DATA_HOME/webkit/databases/). Requires webkit >= 1.5.2. +* `enable_webaudio`: If set to 1, allows JavaScript to generate audio + directly (default 0). Requires webkit >= 1.7.5. +* `enable_3d_acceleration`: If set to 1, the GPU will be used to render + animations and 3D CSS transformations. Requires webkitgtk >= 1.7.90. +* `zoom_text_only`: If set to 1, only text will be zoomed (default 0). Requires + webkit2gtk >= 1.7.91. +* `enable_smooth_scrolling`: If set to 1, scrolling the page will be smoothed + (default 0). Requires webkitgtk >= 1.9.0. +* `enable_inline_media`: If set to 1, inline playback of media is allowed, + otherwise, only full-screen playback is allowed (default 1). Requires + webkitgtk >= 1.9.3. +* `require_click_to_play`: If set to 1, playback of media requires user + interaction before playing, otherwise, media will be allowed to autoplay + (default 0). Requires webkitgtk >= 1.9.3. +* `enable_css_shaders`: If set to 1, CSS shaders will be enabled (default 0). + Requires webkitgtk >= 1.11.1. +* `enable_media_stream`: If set to 1, web pages will be able to access the + local video and audio input devices (default 0). Requires webkitgtk >= 1.11.1. +* `cache_model`: The cache model of webkit. Valid values: + "document_viewer" (no caching; low memory; usage: single local file), + "web_browser" (heavy caching; faster; usage: general browsing), + "document_browser" (moderate caching; usage: series of local files) + (default "web_browser"). +* `app_cache_size`: The maximum size of the application cache (in bytes) + (default UINT_MAX (no quota)). Changing the variable clears the cache. + Requires webkitgtk >= 1.3.13. +* `web_database_directory`: The directory where web databases are stored. + (default is under $XDG_DATA_HOME). +* `web_database_quota`: The default quota for web databases. (default 5MB). +* `profile_js`: Sets whether to profile JavaScript code. +* `profile_timeline`: Sets whether to profile the timeline. #### Constants (not dumpable or writeable) @@ -396,6 +510,13 @@ file). - overridable with cmdline arg - in GtkSocket mode, this is a random number to prevent name clashes * `PID`: The process ID of this Uzbl instance. +* `current_encoding`: The current encoding of the web page. +* `inspected_uri`: The URI that is being inspected. Requires webkitgtk >= + 1.3.17. +* `app_cache_directory`: The directory webkit uses to store its cache. + Requires webkitgtk >= 1.3.13. +* `plugin_list`: A JSON list of objects describing the available plugins. + Requires webkitgtk >= 1.3.8. ### VARIABLE EXPANSION AND COMMAND / JAVASCRIPT SUBSTITUTION @@ -514,10 +635,10 @@ access to the following environment variables: * `$UZBL_SOCKET`: The filename of the Unix socket being used, if any. * `$UZBL_URI`: The URI of the current page. * `$UZBL_TITLE`: The current page title. +* `$UZBL_PRIVATE`: Set if uzbl is in "private browsing mode", unset otherwise. -Handler scripts (`download_handler`, `cookie_handler`, `scheme_handler`, -`request_handler`, and `authentication_handler`) are called with special -arguments: +Handler scripts (`download_handler`, `scheme_handler`, and `request_handler`) +are called with special arguments: * download handler @@ -532,16 +653,6 @@ arguments: that the file should be saved to. A download handler using WebKit's internal downloader can just echo this path and exit when this argument is present. -* cookie handler - - - `$1 GET/PUT`: Whether a cookie should be sent to the server (`GET`) or - stored by the browser (`PUT`). - - `$2 scheme`: Either `http` or `https`. - - `$3 host`: If current page URL is `www.example.com/somepage`, this could be - something else than `example.com`, eg advertising from another host. - - `$4 path`: The request address path. - - `$5 data`: The cookie data. Only included for `PUT` requests. - * scheme handler - `$1 URI` of the page to be navigated to @@ -550,13 +661,6 @@ arguments: - `$1 URI` of the resource which is being requested -* authentication handler: - - - `$1`: authentication zone unique identifier - - `$2`: domain part of URL that requests authentication - - `$3`: authentication realm - - `$4`: FALSE if this is the first attempt to authenticate, TRUE otherwise - ### Formfiller.sh Example config entries for formfiller script @@ -584,47 +688,33 @@ after closing the editor, it will load the data into the formfields. The temp file is removed ### HTTP/BASIC AUTHENTICATION +HTTP auth can be handled in two different ways. Using the builtin auth dialog +in WebKit or by dispatching the work to a external script. -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: +To use the builtin auth dialog set `enable_builtin_auth` to 1. With this set +you'll get a basic GTK+ prompt for username/password when trying to access a +protected site. - set authentication_handler = sync_spawn /patch/to/your/script +Whenever authentication is needed the `AUTHENTICATE` event will be sent, this +is what you would use to hook up a custom authentication system. This event +will be sent even when using the bultin dialog so remember to disable that if +adding a dialog of your own. -Script will be executed on each authentication request. -It will receive four auth-related parameters: +The `AUTHENTICATE` event has four arguments + * a unique identifier to be used in the exchange + * domain part of URL that requests authentication + * authentication realm + * the empty string for the first attempt and "retrying" for further attempts - $1 authentication zone unique identifier (may be used as 'key') - $2 domain part of URL that requests authentication - $3 authentication realm - $4 FALSE if this is the first attempt to authenticate, TRUE otherwise +After this event has been sent the request is paused until credentials are +provided. This is done using the `auth` command e.g: -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 $4 = 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. + `auth "uniqueid" "alice" "wonderland"` -The simplest example of authentication handler script is: +A super simple setup that will always try to authenticate with the same password +could look like this. (assuming aliases from the example configuration) -#!/bin/sh -[ "$4" == "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. +@on_event AUTHENTICATE auth "%1" "alice" "wonderland" ### WINDOW MANAGER INTEGRATION @@ -657,11 +747,6 @@ The EM allows: * Many fine-grained events (`hover_over_link`, `key_press`, `key_release`,..) * See example `uzbl-event-manager`. -**Note**: Cookie events are sent in addition to (optionally) being handled by - the cookie handler (set by the cookie_handler var). If using a handler it will - take precedence before the internal state configured by (add|delete)_cookie - commands. - Events have this format: EVENT [uzbl_instance_name] EVENT_NAME event_details @@ -687,14 +772,18 @@ Events have this format: 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 +* `EVENT [uzbl_instance_name] LOAD_FINISH 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_QUEUED uri`: http resource gets + enqueued * `EVENT [uzbl_instance_name] REQUEST_STARTING uri`: http resource gets requested +* `EVENT [uzbl_instance_name] REQUEST_FINISHED uri`: http resource has finished + loading * `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. @@ -743,6 +832,8 @@ Events have this format: be a unix-timestamp or empty * `EVENT [uzbl_instance_name] DELETE_COOKIE domain path name value scheme expire`: When a cookie was deleted. arguments as ADD_COOKIE +* `EVENT [uzbl_instance_name] AUTHENTICATE uniqueid host realm retry`: When a + request requires authentication. authentication is done by calling `auth` Events/requests which the EM and its plugins listens for diff --git a/bin/uzbl-browser b/bin/uzbl-browser index fb9a368..4381050 100755 --- a/bin/uzbl-browser +++ b/bin/uzbl-browser @@ -67,9 +67,9 @@ fi # uzbl-event-manager will exit if one is already running. # we could also check if its pid file exists to avoid having to spawn it. DAEMON_SOCKET="$XDG_CACHE_HOME"/uzbl/event_daemon -#if [ ! -f "$DAEMON_SOCKET".pid ] -#then +if [ ! -f "$DAEMON_SOCKET".pid ] +then ${UZBL_EVENT_MANAGER:-uzbl-event-manager -va start} -#fi +fi exec uzbl-core "$@" ${config_file:+--config "$config_file"} --connect-socket $DAEMON_SOCKET diff --git a/bin/uzbl-event-manager b/bin/uzbl-event-manager index 56253ef..221fa73 100755 --- a/bin/uzbl-event-manager +++ b/bin/uzbl-event-manager @@ -1,1011 +1,3 @@ -#!/usr/bin/env python2 - -# Event Manager for Uzbl -# Copyright (c) 2009-2010, Mason Larobina -# Copyright (c) 2009, Dieter Plaetinck -# -# 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 . - -''' - -E V E N T _ M A N A G E R . P Y -=============================== - -Event manager for uzbl written in python. - -''' - -import atexit -import imp -import logging -import os -import sys -import time -import weakref -import re -import errno -from collections import defaultdict -from functools import partial -from glob import glob -from itertools import count -from optparse import OptionParser -from select import select -from signal import signal, SIGTERM, SIGINT, SIGKILL -from socket import socket, AF_UNIX, SOCK_STREAM, error as socket_error -from traceback import format_exc - - -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) - -# `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/') - -# Define some globals. -SCRIPTNAME = os.path.basename(sys.argv[0]) - -logger = logging.getLogger(SCRIPTNAME) - - -def get_exc(): - '''Format `format_exc` for logging.''' - return "\n%s" % format_exc().rstrip() - - -def expandpath(path): - '''Expand and realpath paths.''' - return os.path.realpath(os.path.expandvars(path)) - - -def ascii(u): - '''Convert unicode strings into ascii for transmission over - ascii-only streams/sockets/devices.''' - return u.encode('utf-8') - - -def daemonize(): - '''Daemonize the process using the Stevens' double-fork magic.''' - - logger.info('entering daemon mode') - - try: - if os.fork(): - os._exit(0) - - except OSError: - logger.critical('failed to daemonize', exc_info=True) - sys.exit(1) - - os.chdir('/') - os.setsid() - os.umask(0) - - try: - if os.fork(): - os._exit(0) - - except OSError: - logger.critical('failed to daemonize', exc_info=True) - sys.exit(1) - - if sys.stdout.isatty(): - 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()) - - logger.info('entered daemon mode') - - -def make_dirs(path): - '''Make all basedirs recursively as required.''' - - try: - dirname = os.path.dirname(path) - if not os.path.isdir(dirname): - logger.debug('creating directories %r', dirname) - os.makedirs(dirname) - - except OSError: - logger.error('failed to create directories', exc_info=True) - - -class EventHandler(object): - '''Event handler class. Used to store args and kwargs which are merged - come time to call the callback with the event args and kwargs.''' - - nextid = count().next - - def __init__(self, plugin, event, callback, args, kwargs): - self.id = self.nextid() - self.plugin = plugin - self.event = event - self.callback = callback - self.args = args - self.kwargs = kwargs - - def __repr__(self): - elems = ['id=%d' % self.id, 'event=%s' % self.event, - 'callback=%r' % self.callback] - - if self.args: - elems.append('args=%s' % repr(self.args)) - - if self.kwargs: - elems.append('kwargs=%s' % repr(self.kwargs)) - - elems.append('plugin=%s' % self.plugin.name) - return u'' % ', '.join(elems) - - def call(self, uzbl, *args, **kwargs): - '''Execute the handler function and merge argument lists.''' - - args = args + self.args - kwargs = dict(self.kwargs.items() + kwargs.items()) - self.callback(uzbl, *args, **kwargs) - - -class Plugin(object): - '''Plugin module wrapper object.''' - - # Special functions exported from the Plugin instance to the - # plugin namespace. - special_functions = ['require', 'export', 'export_dict', 'connect', - 'connect_dict', 'logger', 'unquote', 'splitquoted'] - - def __init__(self, parent, name, path, plugin): - self.parent = parent - self.name = name - self.path = path - self.plugin = plugin - self.logger = logging.getLogger('plugin.%s' % name) - - # Weakrefs to all handlers created by this plugin - self.handlers = set([]) - - # Plugins init hook - init = getattr(plugin, 'init', None) - self.init = init if callable(init) else None - - # Plugins optional after hook - after = getattr(plugin, 'after', None) - self.after = after if callable(after) else None - - # Plugins optional cleanup hook - cleanup = getattr(plugin, 'cleanup', None) - self.cleanup = cleanup if callable(cleanup) else None - - assert init or after or cleanup, "missing hooks in plugin" - - # Export plugin's instance methods to plugin namespace - for attr in self.special_functions: - plugin.__dict__[attr] = getattr(self, attr) - - def __repr__(self): - return u'' % self.plugin - - def export(self, uzbl, attr, obj, prepend=True): - '''Attach `obj` to `uzbl` instance. This is the preferred method - of sharing functionality, functions, data and objects between - plugins. - - If the object is callable you may wish to turn the callable object - in to a meta-instance-method by prepending `uzbl` to the call stack. - You can change this behaviour with the `prepend` argument. - ''' - - assert attr not in uzbl.exports, "attr %r already exported by %r" %\ - (attr, uzbl.exports[attr][0]) - - prepend = True if prepend and callable(obj) else False - uzbl.__dict__[attr] = partial(obj, uzbl) if prepend else obj - uzbl.exports[attr] = (self, obj, prepend) - uzbl.logger.info('exported %r to %r by plugin %r, prepended %r', - obj, 'uzbl.%s' % attr, self.name, prepend) - - def export_dict(self, uzbl, exports): - for (attr, object) in exports.items(): - self.export(uzbl, attr, object) - - def find_handler(self, event, callback, args, kwargs): - '''Check if a handler with the identical callback and arguments - exists and return it.''' - - # Remove dead refs - self.handlers -= set(filter(lambda ref: not ref(), self.handlers)) - - # Find existing identical handler - for handler in [ref() for ref in self.handlers]: - if handler.event == event and handler.callback == callback \ - and handler.args == args and handler.kwargs == kwargs: - return handler - - def connect(self, uzbl, event, callback, *args, **kwargs): - '''Create an event handler object which handles `event` events. - - Arguments passed to the connect function (`args` and `kwargs`) are - stored in the handler object and merged with the event arguments - come handler execution. - - All handler functions must behave like a `uzbl` instance-method (that - means `uzbl` is prepended to the callback call arguments).''' - - # Sanitise and check event name - event = event.upper().strip() - assert event and ' ' not in event - - assert callable(callback), 'callback must be callable' - - # Check if an identical handler already exists - handler = self.find_handler(event, callback, args, kwargs) - if not handler: - # Create a new handler - handler = EventHandler(self, event, callback, args, kwargs) - self.handlers.add(weakref.ref(handler)) - self.logger.info('new %r', handler) - - uzbl.handlers[event].append(handler) - uzbl.logger.info('connected %r', handler) - return handler - - def connect_dict(self, uzbl, connects): - for (event, callback) in connects.items(): - self.connect(uzbl, event, callback) - - def require(self, plugin): - '''Check that plugin with name `plugin` has been loaded. Use this to - ensure that your plugins dependencies have been met.''' - - assert plugin in self.parent.plugins, self.logger.critical( - 'plugin %r required by plugin %r', plugin, self.name) - - @classmethod - def unquote(cls, s): - '''Removes quotation marks around strings if any and interprets - \\-escape sequences using `string_escape`''' - if s and s[0] == s[-1] and s[0] in ['"', "'"]: - s = s[1:-1] - return s.encode('utf-8').decode('string_escape').decode('utf-8') - - _splitquoted = re.compile("( |\"(?:\\\\.|[^\"])*?\"|'(?:\\\\.|[^'])*?')") - - @classmethod - def splitquoted(cls, text): - '''Splits string on whitespace while respecting quotations''' - parts = cls._splitquoted.split(text) - return [cls.unquote(p) for p in parts if p.strip()] - - -class Uzbl(object): - def __init__(self, parent, child_socket): - self.opts = opts - self.parent = parent - self.child_socket = child_socket - self.child_buffer = [] - self.time = time.time() - self.pid = None - self.name = None - - # Flag if the instance has raised the INSTANCE_START event. - self.instance_start = False - - # Use name "unknown" until name is discovered. - self.logger = logging.getLogger('uzbl-instance[]') - - # Track plugin event handlers and exported functions. - self.exports = {} - self.handlers = defaultdict(list) - - # Internal vars - self._depth = 0 - self._buffer = '' - - def __repr__(self): - return '' % ', '.join([ - 'pid=%s' % (self.pid if self.pid else "Unknown"), - 'name=%s' % ('%r' % self.name if self.name else "Unknown"), - 'uptime=%f' % (time.time() - self.time), - '%d exports' % len(self.exports.keys()), - '%d handlers' % sum([len(l) for l in self.handlers.values()])]) - - def init_plugins(self): - '''Call the init and after hooks in all loaded plugins for this - instance.''' - - # Initialise each plugin with the current uzbl instance. - for plugin in self.parent.plugins.values(): - if plugin.init: - self.logger.debug('calling %r plugin init hook', plugin.name) - plugin.init(self) - - # Allow plugins to use exported features of other plugins by calling an - # optional `after` function in the plugins namespace. - for plugin in self.parent.plugins.values(): - if plugin.after: - self.logger.debug('calling %r plugin after hook', plugin.name) - plugin.after(self) - - def send(self, msg): - '''Send a command to the uzbl instance via the child socket - instance.''' - - msg = msg.strip() - assert self.child_socket, "socket inactive" - - if opts.print_events: - print ascii(u'%s<-- %s' % (' ' * self._depth, msg)) - - self.child_buffer.append(ascii("%s\n" % msg)) - - def do_send(self): - data = ''.join(self.child_buffer) - try: - bsent = self.child_socket.send(data) - except socket_error as e: - if e.errno in (errno.EAGAIN, errno.EINTR): - self.child_buffer = [data] - return - else: - self.logger.error('failed to send', exc_info=True) - return self.close() - else: - if bsent == 0: - self.logger.debug('write end of connection closed') - self.close() - elif bsent < len(data): - self.child_buffer = [data[bsent:]] - else: - del self.child_buffer[:] - - def read(self): - '''Read data from the child socket and pass lines to the parse_msg - function.''' - - try: - raw = unicode(self.child_socket.recv(8192), 'utf-8', 'ignore') - if not raw: - self.logger.debug('read null byte') - return self.close() - - except: - self.logger.error('failed to read', exc_info=True) - return self.close() - - lines = (self._buffer + raw).split('\n') - self._buffer = lines.pop() - - for line in filter(None, map(unicode.strip, lines)): - try: - self.parse_msg(line.strip()) - - except: - self.logger.error(get_exc()) - self.logger.error('erroneous event: %r' % line) - - def parse_msg(self, line): - '''Parse an incoming message from a uzbl instance. Event strings - will be parsed into `self.event(event, args)`.''' - - # Split by spaces (and fill missing with nulls) - elems = (line.split(' ', 3) + [''] * 3)[:4] - - # Ignore non-event messages. - if elems[0] != 'EVENT': - logger.info('non-event message: %r', line) - if opts.print_events: - print '--- %s' % ascii(line) - return - - # Check event string elements - (name, event, args) = elems[1:] - assert name and event, 'event string missing elements' - if not self.name: - self.name = name - self.logger = logging.getLogger('uzbl-instance%s' % name) - self.logger.info('found instance name %r', name) - - assert self.name == name, 'instance name mismatch' - - # Handle the event with the event handlers through the event method - self.event(event, args) - - def event(self, event, *args, **kargs): - '''Raise an event.''' - - event = event.upper() - - if not opts.daemon_mode and opts.print_events: - elems = [event] - if args: - elems.append(unicode(args)) - if kargs: - elems.append(unicode(kargs)) - print ascii(u'%s--> %s' % (' ' * self._depth, ' '.join(elems))) - - if event == "INSTANCE_START" and args: - assert not self.instance_start, 'instance already started' - - self.pid = int(args[0]) - self.logger.info('found instance pid %r', self.pid) - - self.init_plugins() - - elif event == "INSTANCE_EXIT": - self.logger.info('uzbl instance exit') - self.close() - - if event not in self.handlers: - return - - for handler in self.handlers[event]: - self._depth += 1 - try: - handler.call(self, *args, **kargs) - - except: - self.logger.error('error in handler', exc_info=True) - - self._depth -= 1 - - def close_connection(self, child_socket): - '''Close child socket and delete the uzbl instance created for that - child socket connection.''' - - def close(self): - '''Close the client socket and call the plugin cleanup hooks.''' - - self.logger.debug('called close method') - - # Remove self from parent uzbls dict. - if self.child_socket in self.parent.uzbls: - self.logger.debug('removing self from uzbls list') - del self.parent.uzbls[self.child_socket] - - try: - if self.child_socket: - self.logger.debug('closing child socket') - self.child_socket.close() - - except: - self.logger.error('failed to close socket', exc_info=True) - - finally: - self.child_socket = None - - # Call plugins cleanup hooks. - for plugin in self.parent.plugins.values(): - if plugin.cleanup: - self.logger.debug('calling %r plugin cleanup hook', - plugin.name) - plugin.cleanup(self) - - logger.info('removed %r', self) - - -class UzblEventDaemon(object): - def __init__(self): - self.opts = opts - self.server_socket = None - self._quit = False - - # Hold uzbl instances - # {child socket: Uzbl instance, ..} - self.uzbls = {} - - # Hold plugins - # {plugin name: Plugin instance, ..} - self.plugins = {} - - # Register that the event daemon server has started by creating the - # pid file. - make_pid_file(opts.pid_file) - - # Register a function to clean up the socket and pid file on exit. - atexit.register(self.quit) - - # Add signal handlers. - for sigint in [SIGTERM, SIGINT]: - signal(sigint, self.quit) - - # Load plugins into self.plugins - self.load_plugins(opts.plugins) - - def load_plugins(self, plugins): - '''Load event manager plugins.''' - - for path in plugins: - logger.debug('loading plugin %r', path) - (dir, file) = os.path.split(path) - name = file[:-3] if file.lower().endswith('.py') else file - - info = imp.find_module(name, [dir]) - module = imp.load_module(name, *info) - - # Check if the plugin has a callable hook. - hooks = filter(callable, [getattr(module, attr, None) \ - for attr in ['init', 'after', 'cleanup']]) - assert hooks, "no hooks in plugin %r" % module - - logger.debug('creating plugin instance for %r plugin', name) - plugin = Plugin(self, name, path, module) - self.plugins[name] = plugin - logger.info('new %r', plugin) - - def create_server_socket(self): - '''Create the event manager daemon socket for uzbl instance duplex - communication.''' - - # Close old socket. - self.close_server_socket() - - sock = socket(AF_UNIX, SOCK_STREAM) - sock.bind(opts.server_socket) - sock.listen(5) - - self.server_socket = sock - logger.debug('bound server socket to %r', opts.server_socket) - - def run(self): - '''Main event daemon loop.''' - - logger.debug('entering main loop') - - # Create and listen on the server socket - self.create_server_socket() - - if opts.daemon_mode: - # Daemonize the process - daemonize() - - # Update the pid file - make_pid_file(opts.pid_file) - - try: - # Accept incoming connections and listen for incoming data - self.listen() - - except: - if not self._quit: - logger.critical('failed to listen', exc_info=True) - - # Clean up and exit - self.quit() - - logger.debug('exiting main loop') - - def listen(self): - '''Accept incoming connections and constantly poll instance sockets - for incoming data.''' - - logger.info('listening on %r', opts.server_socket) - - # Count accepted connections - connections = 0 - - while (self.uzbls or not connections) or (not opts.auto_close): - socks = [self.server_socket] + self.uzbls.keys() - wsocks = [k for k, v in self.uzbls.items() if v.child_buffer] - reads, writes, errors = select(socks, wsocks, socks, 1) - - if self.server_socket in reads: - reads.remove(self.server_socket) - - # Accept connection and create uzbl instance. - child_socket = self.server_socket.accept()[0] - child_socket.setblocking(False) - self.uzbls[child_socket] = Uzbl(self, child_socket) - connections += 1 - - for uzbl in [self.uzbls[s] for s in writes if s in self.uzbls ]: - uzbl.do_send() - - for uzbl in [self.uzbls[s] for s in reads if s in self.uzbls]: - uzbl.read() - - for uzbl in [self.uzbls[s] for s in errors if s in self.uzbls]: - uzbl.logger.error('socket read error') - uzbl.close() - - logger.info('auto closing') - - def close_server_socket(self): - '''Close and delete the server socket.''' - - try: - if self.server_socket: - logger.debug('closing server socket') - self.server_socket.close() - self.server_socket = None - - if os.path.exists(opts.server_socket): - logger.info('unlinking %r', opts.server_socket) - os.unlink(opts.server_socket) - - except: - logger.error('failed to close server socket', exc_info=True) - - def quit(self, sigint=None, *args): - '''Close all instance socket objects, server socket and delete the - pid file.''' - - if sigint == SIGTERM: - logger.critical('caught SIGTERM, exiting') - - elif sigint == SIGINT: - logger.critical('caught SIGINT, exiting') - - elif not self._quit: - logger.debug('shutting down event manager') - - self.close_server_socket() - - for uzbl in self.uzbls.values(): - uzbl.close() - - del_pid_file(opts.pid_file) - - if not self._quit: - logger.info('event manager shut down') - self._quit = True - - -def make_pid_file(pid_file): - '''Creates a pid file at `pid_file`, fails silently.''' - - try: - logger.debug('creating pid file %r', pid_file) - make_dirs(pid_file) - pid = os.getpid() - fileobj = open(pid_file, 'w') - fileobj.write('%d' % pid) - fileobj.close() - logger.info('created pid file %r with pid %d', pid_file, pid) - - except: - logger.error('failed to create pid file', exc_info=True) - - -def del_pid_file(pid_file): - '''Deletes a pid file at `pid_file`, fails silently.''' - - if os.path.isfile(pid_file): - try: - logger.debug('deleting pid file %r', pid_file) - os.remove(pid_file) - logger.info('deleted pid file %r', pid_file) - - except: - logger.error('failed to delete pid file', exc_info=True) - - -def get_pid(pid_file): - '''Reads a pid from pid file `pid_file`, fails None.''' - - try: - logger.debug('reading pid file %r', pid_file) - fileobj = open(pid_file, 'r') - pid = int(fileobj.read()) - fileobj.close() - logger.info('read pid %d from pid file %r', pid, pid_file) - return pid - - except (IOError, ValueError): - logger.error('failed to read pid', exc_info=True) - return None - - -def pid_running(pid): - '''Checks if a process with a pid `pid` is running.''' - - try: - os.kill(pid, 0) - except OSError: - return False - else: - return True - - -def term_process(pid): - '''Asks nicely then forces process with pid `pid` to exit.''' - - try: - logger.info('sending SIGTERM to process with pid %r', pid) - os.kill(pid, SIGTERM) - - except OSError: - logger.error(get_exc()) - - logger.debug('waiting for process with pid %r to exit', pid) - start = time.time() - while True: - if not pid_running(pid): - logger.debug('process with pid %d exit', pid) - return True - - if (time.time() - start) > 5: - logger.warning('process with pid %d failed to exit', pid) - logger.info('sending SIGKILL to process with pid %d', pid) - try: - os.kill(pid, SIGKILL) - except: - logger.critical('failed to kill %d', pid, exc_info=True) - raise - - if (time.time() - start) > 10: - logger.critical('unable to kill process with pid %d', pid) - raise OSError - - time.sleep(0.25) - - -def stop_action(): - '''Stop the event manager daemon.''' - - pid_file = opts.pid_file - if not os.path.isfile(pid_file): - logger.error('could not find running event manager with pid file %r', - pid_file) - return - - pid = get_pid(pid_file) - if not pid_running(pid): - logger.debug('no process with pid %r', pid) - del_pid_file(pid_file) - return - - logger.debug('terminating process with pid %r', pid) - term_process(pid) - del_pid_file(pid_file) - logger.info('stopped event manager process with pid %d', pid) - - -def start_action(): - '''Start the event manager daemon.''' - - pid_file = opts.pid_file - if os.path.isfile(pid_file): - pid = get_pid(pid_file) - if pid_running(pid): - logger.error('event manager already started with pid %d', pid) - return - - logger.info('no process with pid %d', pid) - del_pid_file(pid_file) - - UzblEventDaemon().run() - - -def restart_action(): - '''Restart the event manager daemon.''' - - stop_action() - start_action() - - -def list_action(): - '''List all the plugins that would be loaded in the current search - dirs.''' - - names = {} - for plugin in opts.plugins: - (head, tail) = os.path.split(plugin) - if tail not in names: - names[tail] = plugin - - for plugin in sorted(names.values()): - print plugin - - -def make_parser(): - parser = OptionParser('usage: %prog [options] {start|stop|restart|list}') - add = parser.add_option - - add('-v', '--verbose', - dest='verbose', default=2, action='count', - help='increase verbosity') - - add('-d', '--plugin-dir', - dest='plugin_dirs', action='append', metavar="DIR", default=[], - help='add extra plugin search dir, same as `-l "DIR/*.py"`') - - add('-l', '--load-plugin', - dest='load_plugins', action='append', metavar="PLUGIN", default=[], - help='load plugin, loads before plugins in search dirs') - - socket_location = os.path.join(CACHE_DIR, 'event_daemon') - - add('-s', '--server-socket', - dest='server_socket', metavar="SOCKET", default=socket_location, - help='server AF_UNIX socket location') - - add('-p', '--pid-file', - metavar="FILE", dest='pid_file', - help='pid file location, defaults to server socket + .pid') - - add('-n', '--no-daemon', - dest='daemon_mode', action='store_false', default=True, - help='do not daemonize the process') - - add('-a', '--auto-close', - dest='auto_close', action='store_true', default=False, - help='auto close after all instances disconnect') - - add('-i', '--no-default-dirs', - dest='default_dirs', action='store_false', default=True, - help='ignore the default plugin search dirs') - - add('-o', '--log-file', - dest='log_file', metavar='FILE', - help='write logging output to a file, defaults to server socket +' - ' .log') - - add('-q', '--quiet-events', - dest='print_events', action="store_false", default=True, - help="silence the printing of events to stdout") - - return parser - - -def init_logger(): - log_level = logging.CRITICAL - opts.verbose * 10 - logger = logging.getLogger() - logger.setLevel(max(log_level, 10)) - - # Console - handler = logging.StreamHandler() - handler.setLevel(max(log_level + 10, 10)) - handler.setFormatter(logging.Formatter( - '%(name)s: %(levelname)s: %(message)s')) - logger.addHandler(handler) - - # Logfile - handler = logging.FileHandler(opts.log_file, 'w', 'utf-8', 1) - handler.setLevel(max(log_level, 10)) - handler.setFormatter(logging.Formatter( - '[%(created)f] %(name)s: %(levelname)s: %(message)s')) - logger.addHandler(handler) - - -def main(): - global opts - - parser = make_parser() - - (opts, args) = parser.parse_args() - - opts.server_socket = expandpath(opts.server_socket) - - # Set default pid file location - if not opts.pid_file: - opts.pid_file = "%s.pid" % opts.server_socket - - else: - opts.pid_file = expandpath(opts.pid_file) - - # Set default log file location - if not opts.log_file: - opts.log_file = "%s.log" % opts.server_socket - - else: - opts.log_file = expandpath(opts.log_file) - - # Logging setup - init_logger() - logger.info('logging to %r', opts.log_file) - - plugins = {} - - # Load all `opts.load_plugins` into the plugins list - for path in opts.load_plugins: - path = expandpath(path) - matches = glob(path) - if not matches: - parser.error('cannot find plugin(s): %r' % path) - - for plugin in matches: - (head, tail) = os.path.split(plugin) - if tail not in plugins: - logger.debug('found plugin: %r', plugin) - plugins[tail] = plugin - - else: - logger.debug('ignoring plugin: %r', plugin) - - # Add default plugin locations - if opts.default_dirs: - logger.debug('adding default plugin dirs to plugin dirs list') - opts.plugin_dirs += [os.path.join(DATA_DIR, 'plugins/'), - os.path.join(PREFIX, 'share/uzbl/examples/data/plugins/')] - - else: - logger.debug('ignoring default plugin dirs') - - # Load all plugins in `opts.plugin_dirs` into the plugins list - for dir in opts.plugin_dirs: - dir = expandpath(dir) - logger.debug('searching plugin dir: %r', dir) - for plugin in glob(os.path.join(dir, '*.py')): - (head, tail) = os.path.split(plugin) - if tail not in plugins: - logger.debug('found plugin: %r', plugin) - plugins[tail] = plugin - - else: - logger.debug('ignoring plugin: %r', plugin) - - plugins = plugins.values() - - # Check all the paths in the plugins list are files - for plugin in plugins: - if not os.path.isfile(plugin): - parser.error('plugin not a file: %r' % plugin) - - if opts.auto_close: - logger.debug('will auto close') - else: - logger.debug('will not auto close') - - if opts.daemon_mode: - logger.debug('will daemonize') - else: - logger.debug('will not daemonize') - - opts.plugins = plugins - - # init like {start|stop|..} daemon actions - daemon_actions = {'start': start_action, 'stop': stop_action, - 'restart': restart_action, 'list': list_action} - - if len(args) == 1: - action = args[0] - if action not in daemon_actions: - parser.error('invalid action: %r' % action) - - elif not args: - action = 'start' - logger.warning('no daemon action given, assuming %r', action) - - else: - parser.error('invalid action argument: %r' % args) - - logger.info('daemon action %r', action) - # Do action - daemon_actions[action]() - - logger.debug('process CPU time: %f', time.clock()) - - -if __name__ == "__main__": - main() - - -# vi: set et ts=4: +#!/usr/bin/python3 +from uzbl import event_manager +event_manager.main() diff --git a/bin/uzbl-tabbed b/bin/uzbl-tabbed index b78a54a..12fa249 100755 --- a/bin/uzbl-tabbed +++ b/bin/uzbl-tabbed @@ -47,6 +47,10 @@ # # Simon Lipp (sloonz) # Various +# +# Hakan Jerning +# Wrote autosave_session patch to have a saved session even if +# uzbl-tabbed is closed unexpectedly. # Dependencies: @@ -85,6 +89,7 @@ # save_session = 1 # json_session = 0 # session_file = $HOME/.local/share/uzbl/session +# autosave_session = 0 # # Inherited uzbl options: # icon_path = $HOME/.local/share/uzbl/uzbl.png @@ -209,6 +214,7 @@ config = { 'save_session': True, # Save session in file when quit 'saved_sessions_dir': os.path.join(DATA_DIR, 'sessions/'), 'session_file': os.path.join(DATA_DIR, 'session'), + 'autosave_session': False, # Save session for every tab change # Inherited uzbl options 'icon_path': os.path.join(DATA_DIR, 'uzbl.png'), @@ -232,6 +238,11 @@ config = { 'selected_https': 'foreground = "#fff"', 'selected_https_text': 'foreground = "gold"', + #Explicit config file. Unlike the other configs, this one cannot be inherited + #from the uzbl config file, as stated above. I've only put it here because + #load_session() is already called in UzblTabbed.__init__. + 'explicit_config_file': None, + } # End of config dict. UZBL_TABBED_VARS = config.keys() @@ -410,7 +421,7 @@ class GlobalEventDispatcher(EventDispatcher): def new_tab(self, uri = ''): self.uzbl_tabbed.new_tab(uri) - def new_tab_bg(self, uri = ''): + def new_bg_tab(self, uri = ''): self.uzbl_tabbed.new_tab(uri, switch = False) def new_tab_next(self, uri = ''): @@ -434,6 +445,15 @@ class GlobalEventDispatcher(EventDispatcher): def last_tab(self): self.uzbl_tabbed.goto_tab(-1) + def move_current_tab(self, index=0): + self.uzbl_tabbed.move_current_tab(absolute=int(index)) + + def move_current_tab_left(self): + self.uzbl_tabbed.move_current_tab(relative=-1) + + def move_current_tab_right(self): + self.uzbl_tabbed.move_current_tab(relative=1) + def preset_tabs(self, *args): self.uzbl_tabbed.run_preset_command(*args) @@ -889,6 +909,9 @@ class UzblTabbed: if(uri): cmd = cmd + ['--uri', str(uri)] + if config['explicit_config_file'] is not None: + cmd = cmd + ['-c', config['explicit_config_file']] + gobject.spawn_async(cmd, flags=gobject.SPAWN_SEARCH_PATH) uzbl = UzblInstance(self, name, uri, title, switch) @@ -968,6 +991,18 @@ class UzblTabbed: while tabn < 0: tabn += ntabs self.goto_tab(tabn) + def move_tab(self, tab, index): + '''Move tab to position.''' + self.notebook.reorder_child(tab, index) + self.update_tablist() + + def move_current_tab(self, absolute=None, relative=None): + '''Move current tab to position.''' + current = self.notebook.get_current_page() + index = absolute if absolute is not None else current + relative \ + if current + relative < len(self.notebook) else 0 + tab = self.notebook.get_nth_page(current) + self.move_tab(tab, index) def close_tab(self, tabn=None): '''Closes current tab. Supports negative indexing.''' @@ -1030,6 +1065,11 @@ class UzblTabbed: tab = self.notebook.get_nth_page(index) self.notebook.set_focus_child(tab) self.update_tablist(index) + + if config['save_session'] and config['autosave_session']: + if len(list(self.notebook)) > 1: + self.save_session() + return True @@ -1038,6 +1078,7 @@ class UzblTabbed: for tab in self.notebook: self.tabs[tab].title_changed(True) + self.update_tablist() return True @@ -1261,6 +1302,8 @@ if __name__ == "__main__": help="directory to create socket") parser.add_option('-f', '--fifodir', dest='fifodir', help="directory to create fifo") + parser.add_option('--config-file', dest='config_file', + help="configuration file for all uzbl-browser instances") # Parse command line options (options, uris) = parser.parse_args() @@ -1275,6 +1318,15 @@ if __name__ == "__main__": import pprint sys.stderr.write("%s\n" % pprint.pformat(config)) + if options.config_file is not None: + if not os.path.exists(options.config_file): + errorstr = "Explicit config file {} does not exist" % ( + options.config_file) + error(errorstr) + sys.exit(-1) + + config['explicit_config_file'] = options.config_file + uzbl = UzblTabbed() if options.socketdir: diff --git a/examples/config/config b/examples/config/config index 11f1d82..d607cb4 100644 --- a/examples/config/config +++ b/examples/config/config @@ -8,6 +8,7 @@ set prefix = @(echo $PREFIX)@ set data_home = @(echo $XDG_DATA_HOME)@ set cache_home = @(echo $XDG_CACHE_HOME)@ set config_home = @(echo $XDG_CONFIG_HOME)@ +set local_storage_path = @data_home/uzbl/databases/ # Interface paths. set fifo_dir = /tmp @@ -70,7 +71,6 @@ set download_handler = sync_spawn @scripts_dir/download.sh @on_event LOAD_COMMIT @set_status recv # add some javascript to the page for other 'js' and 'script' commands to access later. -@on_event LOAD_COMMIT js uzbl = {}; @on_event LOAD_COMMIT script @scripts_dir/formfiller.js @on_event LOAD_COMMIT script @scripts_dir/follow.js @@ -86,6 +86,8 @@ set download_handler = sync_spawn @scripts_dir/download.sh # Switch to command mode if anything else is clicked @on_event ROOT_ACTIVE @set_mode command +@on_event AUTHENTICATE spawn @scripts_dir/auth.py "%1" "%2" "%3" + # Example CONFIG_CHANGED event handler #@on_event CONFIG_CHANGED print Config changed: %1 = %2 @@ -97,6 +99,10 @@ set download_handler = sync_spawn @scripts_dir/download.sh # Custom CSS can be defined here, including link follower hint styles set stylesheet_uri = file://@config_home/uzbl/style.css +# If WebKits builtin authentication dialog should be used, if enabling remember +# to disable external authentication handlers +set enable_builtin_auth = 0 + set show_status = 1 set status_top = 0 set status_background = #303030 @@ -138,6 +144,8 @@ set useragent = Uzbl (Webkit @{WEBKIT_MAJOR}.@{WEBKIT_MINOR}) (@(+uname # === Configure cookie blacklist ======================================================== +set cookie_policy = 0 + # Accept 'session cookies' from uzbl.org (when you have a whitelist all other cookies are dropped) #request WHITELIST_COOKIE domain 'uzbl.org$' expires '^$' @@ -404,6 +412,9 @@ set formfiller = spawn @scripts_dir/formfiller.sh @cbind gt = event NEXT_TAB @cbind gT = event PREV_TAB @cbind gi_ = event GOTO_TAB %s +@cbind = event MOVE_CURRENT_TAB_LEFT +@cbind = event MOVE_CURRENT_TAB_RIGHT +@cbind gm_ = event MOVE_CURRENT_TAB %s # Preset loading set preset = event PRESET_TABS diff --git a/examples/data/plugins/bind.py b/examples/data/plugins/bind.py deleted file mode 100644 index fc8b392..0000000 --- a/examples/data/plugins/bind.py +++ /dev/null @@ -1,462 +0,0 @@ -'''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 - -# Commonly used regular expressions. -MOD_START = re.compile('^<([A-Z][A-Za-z0-9-_]*)>').match -# Matches , <'x':y>, <:'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.config['mode'] = mode - - del self.uzbl.config['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 - - mode = self.uzbl.config.get('mode', None) - if mode != 'stack': - self.last_mode = mode - self.uzbl.config['mode'] = 'stack' - - self.stack_binds = [bind,] - self.args += args - 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.config['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.config.get('mode', None) - - 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 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 "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) - - -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** - 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_ - 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 = '' % ', '.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 = uzbl.bindlet - - 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 o_ = uri %s - MODE_BIND insert,command = ... - 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 = - request BIND o_ = uri %s - request BIND = ... - 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': - uzbl.bindlet.reset() - - -def match_and_exec(uzbl, bind, depth, modstate, 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 modstate != 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: - del uzbl.config['mode'] - bindlet.reset() - - return True - - -def key_event(uzbl, modstate, keylet, mod_cmd=False, on_exec=False): - bindlet = uzbl.bindlet - 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, modstate, 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: - del uzbl.config['mode'] - - -# plugin init hook -def init(uzbl): - '''Export functions and connect handlers to events.''' - - connect_dict(uzbl, { - '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] - connect(uzbl, event, key_event, bool(mod_cmd), bool(on_exec)) - - export_dict(uzbl, { - 'bind': bind, - 'mode_bind': mode_bind, - 'bindlet': Bindlet(uzbl), - }) - -# vi: set et ts=4: diff --git a/examples/data/plugins/cmd_expand.py b/examples/data/plugins/cmd_expand.py deleted file mode 100644 index b007975..0000000 --- a/examples/data/plugins/cmd_expand.py +++ /dev/null @@ -1,40 +0,0 @@ -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 - -# plugin init hook -def init(uzbl): - export(uzbl, 'cmd_expand', cmd_expand) diff --git a/examples/data/plugins/completion.py b/examples/data/plugins/completion.py deleted file mode 100644 index e8c7f34..0000000 --- a/examples/data/plugins/completion.py +++ /dev/null @@ -1,179 +0,0 @@ -'''Keycmd completion.''' - -import re - -# Completion level -NONE, ONCE, LIST, COMPLETE = range(4) - -# The reverse keyword finding re. -FIND_SEGMENT = re.compile("(\@[\w_]+|set[\s]+[\w_]+|[\w_]+)$").findall - -# Formats -LIST_FORMAT = " %s " -ITEM_FORMAT = "%s%s" - -def escape(str): - return str.replace("@", "\@") - - -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.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.''' - - uzbl.completion.level = NONE - del uzbl.config['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) - - if uzbl.completion.level < LIST: - return - - hints = filter(lambda h: h.startswith(partial), uzbl.completion) - if not hints: - del uzbl.config['completion_list'] - return - - j = len(partial) - l = [ITEM_FORMAT % (escape(h[:j]), h[j:]) for h in sorted(hints)] - uzbl.config['completion_list'] = LIST_FORMAT % ' '.join(l) - - -def start_completion(uzbl, *args): - - comp = uzbl.completion - if comp.locked: - return - - (partial, set_completion) = get_incomplete_keyword(uzbl) - if not partial: - return stop_completion(uzbl) - - if comp.level < COMPLETE: - comp.level += 1 - - hints = filter(lambda h: h.startswith(partial), comp) - if not hints: - return - - elif len(hints) == 1: - comp.lock() - complete_completion(uzbl, partial, hints[0], set_completion) - comp.unlock() - return - - elif partial in hints and comp.level == COMPLETE: - comp.lock() - complete_completion(uzbl, partial, partial, set_completion) - comp.unlock() - 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: - comp.lock() - partial_completion(uzbl, partial, partial+common) - comp.unlock() - - update_completion_list(uzbl) - - -def add_builtins(uzbl, builtins): - '''Pump the space delimited list of builtin commands into the - builtin list.''' - - uzbl.completion.update(builtins.split()) - - -def add_config_key(uzbl, key, value): - '''Listen on the CONFIG_CHANGED event and add config keys to the variable - list for @var like expansion support.''' - - uzbl.completion.add("@%s" % key) - - -class Completions(set): - def __init__(self): - set.__init__(self) - self.locked = False - self.level = NONE - - def lock(self): - self.locked = True - - def unlock(self): - self.locked = False - - -def init(uzbl): - '''Export functions and connect handlers to events.''' - - export_dict(uzbl, { - 'completion': Completions(), - 'start_completion': start_completion, - }) - - connect_dict(uzbl, { - 'BUILTINS': add_builtins, - 'CONFIG_CHANGED': add_config_key, - 'KEYCMD_CLEARED': stop_completion, - 'KEYCMD_EXEC': stop_completion, - 'KEYCMD_UPDATE': update_completion_list, - 'START_COMPLETION': start_completion, - 'STOP_COMPLETION': stop_completion, - }) - - uzbl.send('dump_config_as_events') diff --git a/examples/data/plugins/config.py b/examples/data/plugins/config.py deleted file mode 100644 index c9bdf67..0000000 --- a/examples/data/plugins/config.py +++ /dev/null @@ -1,91 +0,0 @@ -from re import compile -from types import BooleanType -from UserDict import DictMixin - -valid_key = compile('^[A-Za-z0-9_\.]+$').match - -class Config(DictMixin): - def __init__(self, uzbl): - self.uzbl = uzbl - - # Create the base dict and map allowed methods to `self`. - self.data = data = {} - - methods = ['__contains__', '__getitem__', '__iter__', - '__len__', 'get', 'has_key', 'items', 'iteritems', - 'iterkeys', 'itervalues', 'values'] - - for method in methods: - setattr(self, method, getattr(data, method)) - - - def __setitem__(self, key, value): - self.set(key, value) - - def __delitem__(self, key): - self.set(key) - - def update(self, other=None, **kwargs): - if other is None: - other = {} - - for (key, value) in dict(other).items() + kwargs.items(): - self[key] = value - - - def set(self, key, value='', force=False): - '''Generates a `set = ` command string to send to the - current uzbl instance. - - Note that the config dict isn't updated by this function. The config - dict is only updated after a successful `VARIABLE_SET ..` event - returns from the uzbl instance.''' - - assert valid_key(key) - - if type(value) == BooleanType: - value = int(value) - - else: - value = unicode(value) - assert '\n' not in value - - if not force and key in self and self[key] == value: - return - - self.uzbl.send(u'set %s = %s' % (key, value)) - - -def parse_set_event(uzbl, args): - '''Parse `VARIABLE_SET ` event and load the - (key, value) pair into the `uzbl.config` dict.''' - - (key, type, raw_value) = (args.split(' ', 2) + ['',])[:3] - - assert valid_key(key) - assert type in types - - new_value = types[type](raw_value) - old_value = uzbl.config.get(key, None) - - # Update new value. - uzbl.config.data[key] = new_value - - if old_value != new_value: - uzbl.event('CONFIG_CHANGED', key, new_value) - - # Cleanup null config values. - if type == 'str' and not new_value: - del uzbl.config.data[key] - - -# plugin init hook -def init(uzbl): - global types - types = {'int': int, 'float': float, 'str': unquote} - export(uzbl, 'config', Config(uzbl)) - connect(uzbl, 'VARIABLE_SET', parse_set_event) - -# plugin cleanup hook -def cleanup(uzbl): - uzbl.config.data.clear() diff --git a/examples/data/plugins/cookies.py b/examples/data/plugins/cookies.py deleted file mode 100644 index bf59e96..0000000 --- a/examples/data/plugins/cookies.py +++ /dev/null @@ -1,222 +0,0 @@ -""" Basic cookie manager - forwards cookies to all other instances connected to the event manager""" - -from collections import defaultdict -import os, re, stat - -# these are symbolic names for the components of the cookie tuple -symbolic = {'domain': 0, 'path':1, 'name':2, 'value':3, 'scheme':4, 'expires':5} - -# allows for partial cookies -# ? allow wildcard in key -def match(key, cookie): - for k,c in zip(key,cookie): - if k != c: - return False - return True - -class NullStore(object): - def add_cookie(self, rawcookie, cookie): - pass - - def delete_cookie(self, rkey, key): - pass - -class ListStore(list): - def add_cookie(self, rawcookie, cookie): - self.append(rawcookie) - - def delete_cookie(self, rkey, key): - self[:] = [x for x in self if not match(key, splitquoted(x))] - -class TextStore(object): - def __init__(self, filename): - self.filename = filename - try: - # make sure existing cookie jar is not world-open - perm_mode = os.stat(self.filename).st_mode - if (perm_mode & (stat.S_IRWXO | stat.S_IRWXG)) > 0: - safe_perm = stat.S_IMODE(perm_mode) & ~(stat.S_IRWXO | stat.S_IRWXG) - os.chmod(self.filename, safe_perm) - except OSError: - pass - - def as_event(self, cookie): - """Convert cookie.txt row to uzbls cookie event format""" - scheme = { - 'TRUE' : 'https', - 'FALSE' : 'http' - } - extra = '' - if cookie[0].startswith("#HttpOnly_"): - extra = 'Only' - domain = cookie[0][len("#HttpOnly_"):] - elif cookie[0].startswith('#'): - return None - else: - domain = cookie[0] - try: - return (domain, - cookie[2], - cookie[5], - cookie[6], - scheme[cookie[3]] + extra, - cookie[4]) - except (KeyError,IndexError): - # Let malformed rows pass through like comments - return None - - def as_file(self, cookie): - """Convert cookie event to cookie.txt row""" - secure = { - 'https' : 'TRUE', - 'http' : 'FALSE', - 'httpsOnly' : 'TRUE', - 'httpOnly' : 'FALSE' - } - http_only = { - 'https' : '', - 'http' : '', - 'httpsOnly' : '#HttpOnly_', - 'httpOnly' : '#HttpOnly_' - } - return (http_only[cookie[4]] + cookie[0], - 'TRUE' if cookie[0].startswith('.') else 'FALSE', - cookie[1], - secure[cookie[4]], - cookie[5], - cookie[2], - cookie[3]) - - def add_cookie(self, rawcookie, cookie): - assert len(cookie) == 6 - - # delete equal cookies (ignoring expire time, value and secure flag) - self.delete_cookie(None, cookie[:-3]) - - # restrict umask before creating the cookie jar - curmask=os.umask(0) - os.umask(curmask| stat.S_IRWXO | stat.S_IRWXG) - - first = not os.path.exists(self.filename) - with open(self.filename, 'a') as f: - if first: - print >> f, "# HTTP Cookie File" - print >> f, '\t'.join(self.as_file(cookie)) - os.umask(curmask) - - def delete_cookie(self, rkey, key): - if not os.path.exists(self.filename): - return - - # restrict umask before creating the cookie jar - curmask=os.umask(0) - os.umask(curmask | stat.S_IRWXO | stat.S_IRWXG) - - # read all cookies - with open(self.filename, 'r') as f: - cookies = f.readlines() - - # write those that don't match the cookie to delete - with open(self.filename, 'w') as f: - for l in cookies: - c = self.as_event(l.split('\t')) - if c is None or not match(key, c): - print >> f, l, - os.umask(curmask) - -xdg_data_home = os.environ.get('XDG_DATA_HOME', os.path.join(os.environ['HOME'], '.local/share')) -DefaultStore = TextStore(os.path.join(xdg_data_home, 'uzbl/cookies.txt')) -SessionStore = TextStore(os.path.join(xdg_data_home, 'uzbl/session-cookies.txt')) - -def match_list(_list, cookie): - for matcher in _list: - for component, match in matcher: - if match(cookie[component]) is None: - break - else: - return True - return False - -# accept a cookie only when: -# a. there is no whitelist and the cookie is in the blacklist -# b. the cookie is in the whitelist and not in the blacklist -def accept_cookie(uzbl, cookie): - if uzbl.cookie_whitelist: - if match_list(uzbl.cookie_whitelist, cookie): - return not match_list(uzbl.cookie_blacklist, cookie) - return False - - return not match_list(uzbl.cookie_blacklist, cookie) - -def expires_with_session(uzbl, cookie): - return cookie[5] == '' - -def get_recipents(uzbl): - """ get a list of Uzbl instances to send the cookie too. """ - # This could be a lot more interesting - return [u for u in uzbl.parent.uzbls.values() if u is not uzbl] - -def get_store(uzbl, session=False): - if session: - return SessionStore - return DefaultStore - -def add_cookie(uzbl, cookie): - splitted = splitquoted(cookie) - if accept_cookie(uzbl, splitted): - for u in get_recipents(uzbl): - u.send('add_cookie %s' % cookie) - - get_store(uzbl, expires_with_session(uzbl, splitted)).add_cookie(cookie, splitted) - else: - logger.debug('cookie %r is blacklisted' % splitted) - uzbl.send('delete_cookie %s' % cookie) - -def delete_cookie(uzbl, cookie): - for u in get_recipents(uzbl): - u.send('delete_cookie %s' % cookie) - - splitted = splitquoted(cookie) - if len(splitted) == 6: - get_store(uzbl, expires_with_session(uzbl, splitted)).delete_cookie(cookie, splitted) - else: - for store in set([get_store(uzbl, session) for session in (True, False)]): - store.delete_cookie(cookie, splitted) - -# add a cookie matcher to a whitelist or a blacklist. -# a matcher is a list of (component, re) tuples that matches a cookie when the -# "component" part of the cookie matches the regular expression "re". -# "component" is one of the keys defined in the variable "symbolic" above, -# or the index of a component of a cookie tuple. -def add_cookie_matcher(_list, arg): - args = splitquoted(arg) - mlist = [] - for (component, regexp) in zip(args[0::2], args[1::2]): - try: - component = symbolic[component] - except KeyError: - component = int(component) - assert component <= 5 - mlist.append((component, re.compile(regexp).search)) - _list.append(mlist) - -def blacklist(uzbl, arg): - add_cookie_matcher(uzbl.cookie_blacklist, arg) - -def whitelist(uzbl, arg): - add_cookie_matcher(uzbl.cookie_whitelist, arg) - -def init(uzbl): - connect_dict(uzbl, { - 'ADD_COOKIE': add_cookie, - 'DELETE_COOKIE': delete_cookie, - 'BLACKLIST_COOKIE': blacklist, - 'WHITELIST_COOKIE': whitelist - }) - export_dict(uzbl, { - 'cookie_blacklist' : [], - 'cookie_whitelist' : [] - }) - -# vi: set et ts=4: diff --git a/examples/data/plugins/downloads.py b/examples/data/plugins/downloads.py deleted file mode 100644 index 8d796ce..0000000 --- a/examples/data/plugins/downloads.py +++ /dev/null @@ -1,77 +0,0 @@ -# this plugin does a very simple display of download progress. to use it, add -# @downloads to your status_format. - -import os -ACTIVE_DOWNLOADS = {} - -# after a download's status has changed this is called to update the status bar -def update_download_section(uzbl): - global ACTIVE_DOWNLOADS - - if len(ACTIVE_DOWNLOADS): - # add a newline before we list downloads - result = ' downloads:' - for path in ACTIVE_DOWNLOADS: - # add each download - fn = os.path.basename(path) - progress, = ACTIVE_DOWNLOADS[path] - - dl = " %s (%d%%)" % (fn, progress * 100) - - # replace entities to make sure we don't break our markup - # (this could be done with an @[]@ expansion in uzbl, but then we - # can't use the above to make a new line) - dl = dl.replace("&", "&").replace("<", "<") - result += dl - else: - result = '' - - # and the result gets saved to an uzbl variable that can be used in - # status_format - if uzbl.config.get('downloads', '') != result: - uzbl.config['downloads'] = result - -def download_started(uzbl, args): - # parse the arguments - args = splitquoted(args) - destination_path = args[0] - - # add to the list of active downloads - global ACTIVE_DOWNLOADS - ACTIVE_DOWNLOADS[destination_path] = (0.0,) - - # update the progress - update_download_section(uzbl) - -def download_progress(uzbl, args): - # parse the arguments - args = splitquoted(args) - destination_path = args[0] - progress = float(args[1]) - - # update the progress - global ACTIVE_DOWNLOADS - ACTIVE_DOWNLOADS[destination_path] = (progress,) - - # update the status bar variable - update_download_section(uzbl) - -def download_complete(uzbl, args): - # parse the arguments - args = splitquoted(args) - destination_path = args[0] - - # remove from the list of active downloads - global ACTIVE_DOWNLOADS - del ACTIVE_DOWNLOADS[destination_path] - - # update the status bar variable - update_download_section(uzbl) - -# plugin init hook -def init(uzbl): - connect_dict(uzbl, { - 'DOWNLOAD_STARTED': download_started, - 'DOWNLOAD_PROGRESS': download_progress, - 'DOWNLOAD_COMPLETE': download_complete, - }) diff --git a/examples/data/plugins/history.py b/examples/data/plugins/history.py deleted file mode 100644 index f42f86f..0000000 --- a/examples/data/plugins/history.py +++ /dev/null @@ -1,129 +0,0 @@ -import random - -shared_history = {'':[]} - -class History(object): - def __init__(self, uzbl): - self.uzbl = uzbl - self._temporary = [] - self.prompt = '' - self.cursor = None - self.__temp_tail = False - self.search_key = None - - def prev(self): - if self.cursor is None: - self.cursor = len(self) - 1 - else: - self.cursor -= 1 - - if self.search_key: - while self.cursor >= 0 and self.search_key not in self[self.cursor]: - self.cursor -= 1 - - if self.cursor < 0 or len(self) == 0: - self.cursor = -1 - return random.choice(end_messages) - - return self[self.cursor] - - def next(self): - if self.cursor is None: - return '' - - self.cursor += 1 - - if self.search_key: - while self.cursor < len(self) and self.search_key not in self[self.cursor]: - self.cursor += 1 - - if self.cursor >= len(shared_history[self.prompt]): - self.cursor = None - self.search_key = None - - if self._temporary: - return self._temporary.pop() - return '' - - return self[self.cursor] - - def change_prompt(self, prompt): - self.prompt = prompt - self._temporary = [] - self.__temp_tail = False - if prompt not in shared_history: - shared_history[prompt] = [] - - def search(self, key): - self.search_key = key - self.cursor = None - - def add(self, cmd): - if self._temporary: - self._temporary.pop() - - shared_history[self.prompt].append(cmd) - self.cursor = None - self.search_key = None - - def add_temporary(self, cmd): - assert not self._temporary - - self._temporary.append(cmd) - self.cursor = len(self) - 1 - - def __getitem__(self, i): - if i < len(shared_history[self.prompt]): - return shared_history[self.prompt][i] - return self._temporary[i-len(shared_history)+1] - - def __len__(self): - return len(shared_history[self.prompt]) + len(self._temporary) - - def __str__(self): - return "(History %s, %s)" % (self.cursor, self.prompt) - -def keycmd_exec(uzbl, modstate, keylet): - cmd = keylet.get_keycmd() - if cmd: - uzbl.history.add(cmd) - -def history_prev(uzbl, _x): - cmd = uzbl.keylet.get_keycmd() - if uzbl.history.cursor is None and cmd: - uzbl.history.add_temporary(cmd) - - uzbl.set_keycmd(uzbl.history.prev()) - uzbl.logger.debug('PREV %s' % uzbl.history) - -def history_next(uzbl, _x): - cmd = uzbl.keylet.get_keycmd() - - uzbl.set_keycmd(uzbl.history.next()) - uzbl.logger.debug('NEXT %s' % uzbl.history) - -def history_search(uzbl, key): - uzbl.history.search(key) - uzbl.send('event HISTORY_PREV') - uzbl.logger.debug('SEARCH %s %s' % (key, uzbl.history)) - -end_messages = ('Look behind you, A three-headed monkey!', 'error #4: static from nylon underwear.', 'error #5: static from plastic slide rules.', 'error #6: global warming.', 'error #9: doppler effect.', 'error #16: somebody was calculating pi on the server.', 'error #19: floating point processor overflow.', 'error #21: POSIX compliance problem.', 'error #25: Decreasing electron flux.', 'error #26: first Saturday after first full moon in Winter.', 'error #64: CPU needs recalibration.', 'error #116: the real ttys became pseudo ttys and vice-versa.', 'error #229: wrong polarity of neutron flow.', 'error #330: quantum decoherence.', 'error #388: Bad user karma.', 'error #407: Route flapping at the NAP.', 'error #435: Internet shut down due to maintenance.') - -# plugin init hook -def init(uzbl): - connect_dict(uzbl, { - 'KEYCMD_EXEC': keycmd_exec, - 'HISTORY_PREV': history_prev, - 'HISTORY_NEXT': history_next, - 'HISTORY_SEARCH': history_search - }) - - export_dict(uzbl, { - 'history' : History(uzbl) - }) - -# plugin after hook -def after(uzbl): - uzbl.on_set('keycmd_prompt', lambda uzbl, k, v: uzbl.history.change_prompt(v)) - -# vi: set et ts=4: diff --git a/examples/data/plugins/keycmd.py b/examples/data/plugins/keycmd.py deleted file mode 100644 index 1bb70e3..0000000 --- a/examples/data/plugins/keycmd.py +++ /dev/null @@ -1,423 +0,0 @@ -import re - -# Keycmd format which includes the markup for the cursor. -KEYCMD_FORMAT = "%s%s%s" -MODCMD_FORMAT = " %s " - - -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 characters typed.''' - - def __init__(self): - # Modcmd tracking - self.modcmd = '' - self.is_modcmd = False - - # Keycmd tracking - self.keycmd = '' - self.cursor = 0 - - self.modmaps = {} - self.ignores = {} - - - 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 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 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.''' - - l = [] - if self.is_modcmd: - l.append('modcmd=%r' % self.get_modcmd()) - - if self.keycmd: - l.append('keycmd=%r' % self.get_keycmd()) - - return '' % ', '.join(l) - - -def add_modmap(uzbl, key, map): - '''Add modmaps. - - Examples: - set modmap = request MODMAP - @modmap - @modmap - ... - - Then: - @bind = - @bind x = - ... - - ''' - - assert len(key) - modmaps = uzbl.keylet.modmaps - - modmaps[key.strip('<>')] = map.strip('<>') - 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 - @ignore_key - ... - ''' - - assert len(glob) > 1 - ignores = uzbl.keylet.ignores - - glob = "<%s>" % glob.strip("<> ") - restr = glob.replace('*', '[^\s]*') - match = re.compile(restr).match - - ignores[glob] = match - uzbl.event('NEW_KEY_IGNORE', glob) - - -def clear_keycmd(uzbl, *args): - '''Clear the keycmd for this uzbl instance.''' - - k = uzbl.keylet - k.keycmd = '' - k.cursor = 0 - del uzbl.config['keycmd'] - uzbl.event('KEYCMD_CLEARED') - - -def clear_modcmd(uzbl): - '''Clear the modcmd for this uzbl instance.''' - - k = uzbl.keylet - k.modcmd = '' - k.is_modcmd = False - - del uzbl.config['modcmd'] - uzbl.event('MODCMD_CLEARED') - - -def clear_current(uzbl): - '''Clear the modcmd if is_modcmd else clear keycmd.''' - - if uzbl.keylet.is_modcmd: - clear_modcmd(uzbl) - - else: - clear_keycmd(uzbl) - - -def update_event(uzbl, modstate, k, execute=True): - '''Raise keycmd & modcmd update events.''' - - keycmd, modcmd = k.get_keycmd(), ''.join(modstate) + k.get_modcmd() - - if k.is_modcmd: - logger.debug('modcmd_update, %s' % modcmd) - uzbl.event('MODCMD_UPDATE', modstate, k) - - else: - logger.debug('keycmd_update, %s' % keycmd) - uzbl.event('KEYCMD_UPDATE', modstate, k) - - if uzbl.config.get('modcmd_updates', '1') == '1': - new_modcmd = ''.join(modstate) + k.get_modcmd() - if not new_modcmd or not k.is_modcmd: - del uzbl.config['modcmd'] - - elif new_modcmd == modcmd: - uzbl.config['modcmd'] = MODCMD_FORMAT % uzbl_escape(modcmd) - - if uzbl.config.get('keycmd_events', '1') != '1': - return - - new_keycmd = k.get_keycmd() - if not new_keycmd: - del uzbl.config['keycmd'] - - elif new_keycmd == keycmd: - # Generate the pango markup for the cursor in the keycmd. - curchar = keycmd[k.cursor] if k.cursor < len(keycmd) else ' ' - chunks = [keycmd[:k.cursor], curchar, keycmd[k.cursor+1:]] - value = KEYCMD_FORMAT % tuple(map(uzbl_escape, chunks)) - - uzbl.config['keycmd'] = value - - -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 parse_key_event(uzbl, key): - ''' Build a set from the modstate part of the event, and pass all keys through modmap ''' - keylet = uzbl.keylet - - modstate, key = splitquoted(key) - modstate = set(['<%s>' % keylet.modmap_key(k) for k in modstate.split('|') if k]) - - key = keylet.modmap_key(key) - return modstate, key - - -def key_press(uzbl, key): - '''Handle KEY_PRESS events. Things done by this function include: - - 1. Ignore all shift key presses (shift can be detected by capital chars) - 2. In non-modcmd mode: - a. append char to keycmd - 3. If not in modcmd mode and a modkey was pressed set modcmd mode. - 4. Keycmd is updated and events raised if anything is changed.''' - - k = uzbl.keylet - modstate, key = parse_key_event(uzbl, key) - k.is_modcmd = any(not k.key_ignored(m) for m in modstate) - - logger.debug('key press modstate=%s' % str(modstate)) - if key.lower() == 'space' and not k.is_modcmd and k.keycmd: - k.keycmd = inject_str(k.keycmd, k.cursor, ' ') - k.cursor += 1 - - elif not k.is_modcmd and len(key) == 1: - if uzbl.config.get('keycmd_events', '1') != '1': - # TODO, make a note on what's going on here - k.keycmd = '' - k.cursor = 0 - del uzbl.config['keycmd'] - return - - k.keycmd = inject_str(k.keycmd, k.cursor, key) - k.cursor += 1 - - elif len(key) == 1: - k.modcmd += key - - else: - if not k.key_ignored('<%s>' % key): - modstate.add('<%s>' % key) - k.is_modcmd = True - - update_event(uzbl, modstate, k) - - -def key_release(uzbl, key): - '''Respond to KEY_RELEASE event. Things done by this function include: - - 1. If in a mod-command then raise a MODCMD_EXEC. - 2. Update the keycmd uzbl variable if anything changed.''' - k = uzbl.keylet - modstate, key = parse_key_event(uzbl, key) - - if len(key) > 1: - if k.is_modcmd: - uzbl.event('MODCMD_EXEC', modstate, k) - - clear_modcmd(uzbl) - - -def set_keycmd(uzbl, keycmd): - '''Allow setting of the keycmd externally.''' - - k = uzbl.keylet - k.keycmd = keycmd - k.cursor = len(keycmd) - update_event(uzbl, set(), k, False) - - -def inject_keycmd(uzbl, keycmd): - '''Allow injecting of a string into the keycmd at the cursor position.''' - - k = uzbl.keylet - k.keycmd = inject_str(k.keycmd, k.cursor, keycmd) - k.cursor += len(keycmd) - update_event(uzbl, set(), k, False) - - -def append_keycmd(uzbl, keycmd): - '''Allow appening of a string to the keycmd.''' - - k = uzbl.keylet - k.keycmd += keycmd - k.cursor = len(k.keycmd) - update_event(uzbl, set(), k, False) - - -def keycmd_strip_word(uzbl, seps): - ''' Removes the last word from the keycmd, similar to readline ^W ''' - - seps = seps or ' ' - k = uzbl.keylet - if not k.keycmd: - return - - head, tail = k.keycmd[:k.cursor].rstrip(seps), k.keycmd[k.cursor:] - rfind = -1 - for sep in seps: - p = head.rfind(sep) - if p >= 0 and rfind < p + 1: - rfind = p + 1 - if rfind == len(head) and head[-1] in seps: - rfind -= 1 - head = head[:rfind] if rfind + 1 else '' - k.keycmd = head + tail - k.cursor = len(head) - update_event(uzbl, set(), k, False) - - -def keycmd_backspace(uzbl, *args): - '''Removes the character at the cursor position in the keycmd.''' - - k = uzbl.keylet - if not k.keycmd or not k.cursor: - return - - k.keycmd = k.keycmd[:k.cursor-1] + k.keycmd[k.cursor:] - k.cursor -= 1 - update_event(uzbl, set(), k, False) - - -def keycmd_delete(uzbl, *args): - '''Removes the character after the cursor position in the keycmd.''' - - k = uzbl.keylet - if not k.keycmd: - return - - k.keycmd = k.keycmd[:k.cursor] + k.keycmd[k.cursor+1:] - update_event(uzbl, set(), k, False) - - -def keycmd_exec_current(uzbl, *args): - '''Raise a KEYCMD_EXEC with the current keylet and then clear the - keycmd.''' - - uzbl.event('KEYCMD_EXEC', set(), uzbl.keylet) - 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 = uzbl.keylet - 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, set(), k, False) - - -# plugin init hook -def init(uzbl): - '''Export functions and connect handlers to events.''' - - connect_dict(uzbl, { - 'APPEND_KEYCMD': append_keycmd, - 'IGNORE_KEY': add_key_ignore, - 'INJECT_KEYCMD': inject_keycmd, - 'KEYCMD_BACKSPACE': keycmd_backspace, - 'KEYCMD_DELETE': keycmd_delete, - 'KEYCMD_EXEC_CURRENT': keycmd_exec_current, - 'KEYCMD_STRIP_WORD': keycmd_strip_word, - 'KEYCMD_CLEAR': clear_keycmd, - 'KEY_PRESS': key_press, - 'KEY_RELEASE': key_release, - 'MOD_PRESS': key_press, - 'MOD_RELEASE': key_release, - 'MODMAP': modmap_parse, - 'SET_CURSOR_POS': set_cursor_pos, - 'SET_KEYCMD': set_keycmd, - }) - - export_dict(uzbl, { - 'add_key_ignore': add_key_ignore, - 'add_modmap': add_modmap, - 'append_keycmd': append_keycmd, - 'clear_current': clear_current, - 'clear_keycmd': clear_keycmd, - 'clear_modcmd': clear_modcmd, - 'inject_keycmd': inject_keycmd, - 'keylet': Keylet(), - 'set_cursor_pos': set_cursor_pos, - 'set_keycmd': set_keycmd, - }) - -# vi: set et ts=4: diff --git a/examples/data/plugins/mode.py b/examples/data/plugins/mode.py deleted file mode 100644 index e0de706..0000000 --- a/examples/data/plugins/mode.py +++ /dev/null @@ -1,68 +0,0 @@ -from collections import defaultdict - -def parse_mode_config(uzbl, args): - '''Parse `MODE_CONFIG = ` event and update config if - the `` is the current mode.''' - - ustrip = unicode.strip - args = unicode(args) - - assert args.strip(), "missing mode config args" - (mode, args) = map(ustrip, (args.strip().split(' ', 1) + ['',])[:2]) - - assert args.strip(), "missing mode config set arg" - (key, value) = map(ustrip, (args.strip().split('=', 1) + [None,])[:2]) - assert key and value is not None, "invalid mode config set syntax" - - uzbl.mode_config[mode][key] = value - if uzbl.config.get('mode', None) == mode: - uzbl.config[key] = value - - -def default_mode_updated(uzbl, var, mode): - if mode and not uzbl.config.get('mode', None): - logger.debug('setting mode to default %r' % mode) - uzbl.config['mode'] = mode - - -def mode_updated(uzbl, var, mode): - if not mode: - mode = uzbl.config.get('default_mode', 'command') - logger.debug('setting mode to default %r' % mode) - uzbl.config['mode'] = mode - return - - # Load mode config - mode_config = uzbl.mode_config.get(mode, None) - if mode_config: - uzbl.config.update(mode_config) - - uzbl.send('event MODE_CONFIRM %s' % mode) - - -def confirm_change(uzbl, mode): - if mode and uzbl.config.get('mode', None) == mode: - uzbl.event('MODE_CHANGED', mode) - - -# plugin init hook -def init(uzbl): - require('config') - require('on_set') - - # Usage `uzbl.mode_config[mode][key] = value` - export(uzbl, 'mode_config', defaultdict(dict)) - - connect_dict(uzbl, { - 'MODE_CONFIG': parse_mode_config, - 'MODE_CONFIRM': confirm_change, - }) - -# plugin after hook -def after(uzbl): - uzbl.on_set('mode', mode_updated) - uzbl.on_set('default_mode', default_mode_updated) - -# plugin cleanup hook -def cleanup(uzbl): - uzbl.mode_config.clear() diff --git a/examples/data/plugins/on_event.py b/examples/data/plugins/on_event.py deleted file mode 100644 index 32f09e2..0000000 --- a/examples/data/plugins/on_event.py +++ /dev/null @@ -1,88 +0,0 @@ -'''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 - -def event_handler(uzbl, *args, **kargs): - '''This function handles all the events being watched by various - on_event definitions and responds accordingly.''' - - # Could be connected to a EM internal event that can use anything as args - if len(args) == 1 and isinstance(args[0], basestring): - args = splitquoted(args[0]) - - events = uzbl.on_events - 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 = uzbl.on_events - if event not in events: - connect(uzbl, 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 commands".''' - - args = args.strip() - assert args, 'missing on event arguments' - - (event, command) = (args.split(' ', 1) + ['',])[:2] - assert event and command, 'missing on event command' - on_event(uzbl, event, command) - - -# plugin init hook -def init(uzbl): - '''Export functions and connect handlers to events.''' - - connect(uzbl, 'ON_EVENT', parse_on_event) - - export_dict(uzbl, { - 'on_event': on_event, - 'on_events': {}, - }) - -# plugin cleanup hook -def cleanup(uzbl): - for handlers in uzbl.on_events.values(): - del handlers[:] - - uzbl.on_events.clear() - -# vi: set et ts=4: diff --git a/examples/data/plugins/on_set.py b/examples/data/plugins/on_set.py deleted file mode 100644 index 130b816..0000000 --- a/examples/data/plugins/on_set.py +++ /dev/null @@ -1,92 +0,0 @@ -from re import compile -from functools import partial - -valid_glob = compile('^[A-Za-z0-9_\*\.]+$').match - -def make_matcher(glob): - '''Make matcher function from simple glob.''' - - pattern = "^%s$" % glob.replace('*', '[^\s]*') - return compile(pattern).match - - -def exec_handlers(uzbl, handlers, key, arg): - '''Execute the on_set handlers that matched the key.''' - - for handler in handlers: - if callable(handler): - handler(key, arg) - - else: - uzbl.send(uzbl.cmd_expand(handler, [key, arg])) - - -def check_for_handlers(uzbl, key, arg): - '''Check for handlers for the current key.''' - - for (matcher, handlers) in uzbl.on_sets.values(): - if matcher(key): - exec_handlers(uzbl, handlers, key, arg) - - -def on_set(uzbl, glob, handler, prepend=True): - '''Add a new handler for a config key change. - - Structure of the `uzbl.on_sets` dict: - { glob : ( glob matcher function, handlers list ), .. } - ''' - - assert valid_glob(glob) - - while '**' in glob: - glob = glob.replace('**', '*') - - if callable(handler): - orig_handler = handler - if prepend: - handler = partial(handler, uzbl) - - else: - orig_handler = handler = unicode(handler) - - if glob in uzbl.on_sets: - (matcher, handlers) = uzbl.on_sets[glob] - handlers.append(handler) - - else: - matcher = make_matcher(glob) - uzbl.on_sets[glob] = (matcher, [handler,]) - - uzbl.logger.info('on set %r call %r' % (glob, orig_handler)) - - -def parse_on_set(uzbl, args): - '''Parse `ON_SET ` event then pass arguments to the - `on_set(..)` function.''' - - (glob, command) = (args.split(' ', 1) + [None,])[:2] - assert glob and command and valid_glob(glob) - on_set(uzbl, glob, command) - - -# plugins init hook -def init(uzbl): - require('config') - require('cmd_expand') - - export_dict(uzbl, { - 'on_sets': {}, - 'on_set': on_set, - }) - - connect_dict(uzbl, { - 'ON_SET': parse_on_set, - 'CONFIG_CHANGED': check_for_handlers, - }) - -# plugins cleanup hook -def cleanup(uzbl): - for (matcher, handlers) in uzbl.on_sets.values(): - del handlers[:] - - uzbl.on_sets.clear() diff --git a/examples/data/plugins/progress_bar.py b/examples/data/plugins/progress_bar.py deleted file mode 100644 index b2edffc..0000000 --- a/examples/data/plugins/progress_bar.py +++ /dev/null @@ -1,93 +0,0 @@ -UPDATES = 0 - -def update_progress(uzbl, progress=None): - '''Updates the progress.output 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 - - Default configuration options: - progress.format = [%d>%p]%c - progress.width = 8 - progress.done = = - progress.pending = - progress.spinner = -\|/ - progress.sprites = loading - ''' - - global UPDATES - - if progress is None: - UPDATES = 0 - progress = 100 - - else: - UPDATES += 1 - progress = int(progress) - - # Get progress config vars. - format = uzbl.config.get('progress.format', '[%d>%p]%c') - width = int(uzbl.config.get('progress.width', 8)) - done_symbol = uzbl.config.get('progress.done', '=') - pend = uzbl.config.get('progress.pending', None) - pending_symbol = pend if pend else ' ' - - # Inflate the done and pending bars to stop the progress bar - # jumping around. - if '%c' in format or '%i' in format: - count = format.count('%c') + format.count('%i') - width += (3-len(str(progress))) * count - - if '%t' in format or '%o' in format: - count = format.count('%t') + format.count('%o') - width += (3-len(str(100-progress))) * count - - done = int(((progress/100.0)*width)+0.5) - pending = width - done - - if '%d' in format: - format = format.replace('%d', done_symbol * done) - - if '%p' in format: - format = format.replace('%p', pending_symbol * pending) - - if '%c' in format: - format = format.replace('%c', '%d%%' % progress) - - if '%i' in format: - format = format.replace('%i', '%d' % progress) - - if '%t' in format: - format = format.replace('%t', '%d%%' % (100-progress)) - - if '%o' in format: - format = format.replace('%o', '%d' % (100-progress)) - - if '%s' in format: - spinner = uzbl.config.get('progress.spinner', '-\\|/') - index = 0 if progress == 100 else UPDATES % len(spinner) - spin = '\\\\' if spinner[index] == '\\' else spinner[index] - format = format.replace('%s', spin) - - if '%r' in format: - sprites = uzbl.config.get('progress.sprites', 'loading') - index = int(((progress/100.0)*len(sprites))+0.5)-1 - sprite = '\\\\' if sprites[index] == '\\' else sprites[index] - format = format.replace('%r', sprite) - - if uzbl.config.get('progress.output', None) != format: - uzbl.config['progress.output'] = format - -# plugin init hook -def init(uzbl): - connect_dict(uzbl, { - 'LOAD_COMMIT': lambda uzbl, uri: update_progress(uzbl), - 'LOAD_PROGRESS': update_progress, - }) diff --git a/examples/data/scripts/auth.py b/examples/data/scripts/auth.py index 49fa41e..dbf58dc 100755 --- a/examples/data/scripts/auth.py +++ b/examples/data/scripts/auth.py @@ -1,5 +1,6 @@ #!/usr/bin/env python +import os import gtk import sys @@ -41,13 +42,20 @@ def getText(authInfo, authHost, authRealm): dialog.show_all() rv = dialog.run() - output = login.get_text() + "\n" + password.get_text() + output = { + 'username': login.get_text(), + 'password': password.get_text() + } dialog.destroy() return rv, output if __name__ == '__main__': - rv, output = getText(sys.argv[1], sys.argv[2], sys.argv[3]) + fifo = open(os.environ.get('UZBL_FIFO'), 'w') + me, info, host, realm = sys.argv + rv, output = getText(info, host, realm) if (rv == gtk.RESPONSE_OK): - print output; + print >> fifo, 'auth "%s" "%s" "%s"' % ( + info, output['username'], output['password'] + ) else: exit(1) diff --git a/examples/data/scripts/follow.js b/examples/data/scripts/follow.js index 83bbf55..0afeec3 100644 --- a/examples/data/scripts/follow.js +++ b/examples/data/scripts/follow.js @@ -12,6 +12,7 @@ // Globals uzbldivid = 'uzbl_link_hints'; +var uzbl = uzbl || {}; uzbl.follow = function() { // Export diff --git a/examples/data/scripts/formfiller.js b/examples/data/scripts/formfiller.js index 06db648..9c56f7b 100644 --- a/examples/data/scripts/formfiller.js +++ b/examples/data/scripts/formfiller.js @@ -1,3 +1,5 @@ +var uzbl = uzbl || {}; + uzbl.formfiller = { // this is pointlessly duplicated in uzbl.follow diff --git a/examples/data/scripts/history.sh b/examples/data/scripts/history.sh index 0709b5e..2d96a07 100755 --- a/examples/data/scripts/history.sh +++ b/examples/data/scripts/history.sh @@ -1,5 +1,7 @@ #!/bin/sh +[ -n "$UZBL_PRIVATE" ] && exit 0 + . "$UZBL_UTIL_DIR/uzbl-dir.sh" >> "$UZBL_HISTORY_FILE" || exit 1 diff --git a/examples/data/scripts/scheme.py b/examples/data/scripts/scheme.py index 4b0b7ca..c652478 100755 --- a/examples/data/scripts/scheme.py +++ b/examples/data/scripts/scheme.py @@ -1,6 +1,11 @@ #!/usr/bin/env python -import os, subprocess, sys, urlparse +import os, subprocess, sys + +try: + import urllib.parse as urlparse +except ImportError: + import urlparse def detach_open(cmd): # Thanks to the vast knowledge of Laurence Withers (lwithers) and this message: @@ -10,7 +15,7 @@ def detach_open(cmd): for i in range(3): os.dup2(null,i) os.close(null) subprocess.Popen(cmd) - print 'USED' + print('USED') if __name__ == '__main__': uri = sys.argv[1] diff --git a/misc/env.sh b/misc/env.sh index f815c44..d5f0db8 100755 --- a/misc/env.sh +++ b/misc/env.sh @@ -28,3 +28,11 @@ export XDG_CONFIG_HOME # Needed to run uzbl-browser etc from here. PATH="$(pwd)/sandbox/usr/local/bin:$PATH" export PATH + +PYTHONLIB=$(python3 -c 'from distutils.sysconfig import get_python_lib; print(get_python_lib(prefix="/usr/local"))') + +UZBL_PLUGIN_PATH="$(pwd)/sandbox/$PYTHONLIB/uzbl/plugins" +export UZBL_PLUGIN_PATH + +PYTHONPATH="$(pwd)/sandbox/$PYTHONLIB/" +export PYTHONPATH diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..5dd6b59 --- /dev/null +++ b/setup.py @@ -0,0 +1,11 @@ +from distutils.core import setup + +setup(name='uzbl', + version='201100808', + description='Uzbl event daemon', + url='http://uzbl.org', + packages=['uzbl', 'uzbl.plugins'], + scripts=[ + 'bin/uzbl-event-manager', + ], + ) diff --git a/src/callbacks.c b/src/callbacks.c index eee9f69..370f679 100644 --- a/src/callbacks.c +++ b/src/callbacks.c @@ -10,6 +10,8 @@ #include "type.h" #include "variables.h" +#include + void link_hover_cb (WebKitWebView *page, const gchar *title, const gchar *link, gpointer data) { (void) page; (void) title; (void) data; @@ -147,6 +149,21 @@ key_release_cb (GtkWidget* window, GdkEventKey* event) { return uzbl.behave.forward_keys ? FALSE : TRUE; } +gint +get_click_context() { + WebKitHitTestResult *ht; + guint context; + + if(!uzbl.state.last_button) + return -1; + + ht = webkit_web_view_get_hit_test_result (uzbl.gui.web_view, uzbl.state.last_button); + g_object_get (ht, "context", &context, NULL); + g_object_unref (ht); + + return (gint)context; +} + gboolean button_press_cb (GtkWidget* window, GdkEventButton* event) { (void) window; @@ -233,22 +250,9 @@ button_release_cb (GtkWidget* window, GdkEventButton* event) { } gboolean -motion_notify_cb(GtkWidget* window, GdkEventMotion* event, gpointer user_data) { - (void) window; - (void) event; - (void) user_data; - - send_event (PTR_MOVE, NULL, - TYPE_FLOAT, event->x, - TYPE_FLOAT, event->y, - TYPE_INT, event->state, - NULL); - - return FALSE; -} - -gboolean -navigation_decision_cb (WebKitWebView *web_view, WebKitWebFrame *frame, WebKitNetworkRequest *request, WebKitWebNavigationAction *navigation_action, WebKitWebPolicyDecision *policy_decision, gpointer user_data) { +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; @@ -288,39 +292,16 @@ navigation_decision_cb (WebKitWebView *web_view, WebKitWebFrame *frame, WebKitNe 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; - - if (uzbl.state.verbose) - printf ("New window requested -> %s \n", webkit_network_request_get_uri (request)); - - /* This event function causes troubles with `target="_blank"` anchors. - * Either we: - * 1. Comment it out and target blank links are ignored. - * 2. Uncomment it and two windows are opened when you click on target - * blank links. - * - * This problem is caused by create_web_view_cb also being called whenever - * this callback is triggered thus resulting in the doubled events. - * - * We are leaving this uncommented as we would rather links open twice - * than not at all. - */ - send_event (NEW_WINDOW, NULL, TYPE_STR, webkit_network_request_get_uri (request), NULL); - - webkit_web_policy_decision_ignore (policy_decision); - return TRUE; +void +close_web_view_cb(WebKitWebView *webview, gpointer user_data) { + (void) webview; (void) user_data; + send_event (CLOSE_WINDOW, NULL, NULL); } gboolean -mime_policy_cb(WebKitWebView *web_view, WebKitWebFrame *frame, WebKitNetworkRequest *request, gchar *mime_type, WebKitWebPolicyDecision *policy_decision, gpointer user_data) { +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; @@ -345,11 +326,17 @@ request_starting_cb(WebKitWebView *web_view, WebKitWebFrame *frame, WebKitWebRes (void) response; (void) user_data; - const gchar* uri = webkit_network_request_get_uri (request); + const gchar *uri = webkit_network_request_get_uri (request); + SoupMessage *message = webkit_network_request_get_message (request); + + if (message) { + SoupURI *soup_uri = soup_uri_new (uri); + soup_message_set_first_party (message, soup_uri); + } if (uzbl.state.verbose) printf("Request starting -> %s\n", uri); - send_event (REQUEST_STARTING, NULL, TYPE_STR, webkit_network_request_get_uri(request), NULL); + send_event (REQUEST_STARTING, NULL, TYPE_STR, uri, NULL); if (uzbl.behave.request_handler) { GString *result = g_string_new (""); @@ -373,17 +360,16 @@ request_starting_cb(WebKitWebView *web_view, WebKitWebFrame *frame, WebKitWebRes } void -create_web_view_js_cb (WebKitWebView* web_view, GParamSpec param_spec) { +create_web_view_got_uri_cb (WebKitWebView* web_view, GParamSpec param_spec) { (void) web_view; (void) param_spec; webkit_web_view_stop_loading(web_view); + const gchar* uri = webkit_web_view_get_uri(web_view); - if (strncmp(uri, "javascript:", strlen("javascript:")) == 0) { + if (strncmp(uri, "javascript:", strlen("javascript:")) == 0) eval_js(uzbl.gui.web_view, (gchar*) uri + strlen("javascript:"), NULL, "javascript:"); - gtk_widget_destroy(GTK_WIDGET(web_view)); - } else send_event(NEW_WINDOW, NULL, TYPE_STR, uri, NULL); } @@ -394,14 +380,29 @@ create_web_view_cb (WebKitWebView *web_view, WebKitWebFrame *frame, gpointer us (void) frame; (void) user_data; - if (uzbl.state.verbose) - printf("New web view -> javascript link...\n"); - - WebKitWebView* new_view = WEBKIT_WEB_VIEW(webkit_web_view_new()); + // unfortunately we don't know the URL at this point; all webkit-gtk will + // tell us is that we're opening a new window. + // + // (we can't use the new-window-policy-decision-requested event; it doesn't + // fire when Javascript requests a new window with window.open().) + // + // so, we have to create a temporary web view and allow it to load. webkit + // segfaults if we try to destroy it or mark it for garbage collection in + // create_web_view_got_uri_cb, so we might as well keep it around and reuse + // it. + + if(!uzbl.state._tmp_web_view) { + uzbl.state._tmp_web_view = WEBKIT_WEB_VIEW(webkit_web_view_new()); + + g_object_connect (uzbl.state._tmp_web_view, "signal::notify::uri", + G_CALLBACK(create_web_view_got_uri_cb), NULL, NULL); + + // we're taking ownership of this WebView (sinking its floating reference + // since it will never be added to a display widget). + g_object_ref_sink(uzbl.state._tmp_web_view); + } - g_object_connect (new_view, "signal::notify::uri", - G_CALLBACK(create_web_view_js_cb), NULL, NULL); - return new_view; + return uzbl.state._tmp_web_view; } void @@ -464,7 +465,21 @@ download_cb(WebKitWebView *web_view, WebKitDownload *download, gpointer user_dat /* get a reasonable suggestion for a filename */ const gchar *suggested_filename; +#ifdef USE_WEBKIT2 + WebKitURIResponse *response; + g_object_get(download, "network-response", &response, NULL); +#if WEBKIT_CHECK_VERSION (1, 9, 90) + g_object_get(response, "suggested-filename", &suggested_filename, NULL); +#else + suggested_filename = webkit_uri_response_get_suggested_filename(respose); +#endif +#elif WEBKIT_CHECK_VERSION (1, 9, 6) + WebKitNetworkResponse *response; + g_object_get(download, "network-response", &response, NULL); + g_object_get(response, "suggested-filename", &suggested_filename, NULL); +#else g_object_get(download, "suggested-filename", &suggested_filename, NULL); +#endif /* get the mimetype of the download */ const gchar *content_type = NULL; @@ -582,90 +597,107 @@ run_menu_command(GtkWidget *menu, MenuItem *mi) { (void) menu; if (mi->context & WEBKIT_HIT_TEST_RESULT_CONTEXT_IMAGE) { - gchar* uri; - g_object_get(mi->hittest, "image-uri", &uri, NULL); - gchar* cmd = g_strdup_printf("%s %s", mi->cmd, uri); + gchar* cmd = g_strdup_printf("%s %s", mi->cmd, mi->argument); parse_cmd_line(cmd, NULL); g_free(cmd); - g_free(uri); - g_object_unref(mi->hittest); + g_free(mi->argument); } else { parse_cmd_line(mi->cmd, NULL); } } +gboolean +populate_context_menu (GtkWidget *default_menu, WebKitHitTestResult *hit_test_result, gint context) { + guint i; + + /* find the user-defined menu items that are approprate for whatever was + * clicked and append them to the default context menu. */ + for(i = 0; i < uzbl.gui.menu_items->len; i++) { + MenuItem *mi = g_ptr_array_index(uzbl.gui.menu_items, i); + GtkWidget *item; + + gboolean contexts_match = (context & mi->context); + + if(!contexts_match) { + continue; + } + + if (mi->context & WEBKIT_HIT_TEST_RESULT_CONTEXT_IMAGE) { + g_object_get(hit_test_result, "image-uri", &(mi->argument), NULL); + } + + if(mi->issep) { + item = gtk_separator_menu_item_new(); + } else { + item = gtk_menu_item_new_with_label(mi->name); + g_signal_connect(item, "activate", + G_CALLBACK(run_menu_command), mi); + } + + gtk_menu_shell_append(GTK_MENU_SHELL(default_menu), item); + gtk_widget_show(item); + } + + return FALSE; +} + +#if WEBKIT_CHECK_VERSION (1, 9, 0) +gboolean +context_menu_cb (WebKitWebView *v, GtkWidget *default_menu, WebKitHitTestResult *hit_test_result, gboolean triggered_with_keyboard, gpointer user_data) { + (void) v; (void) triggered_with_keyboard; (void) user_data; + gint context; + + if(!uzbl.gui.menu_items) + return FALSE; + + /* check context */ + if((context = get_click_context()) == -1) + return FALSE; + /* display the default menu with our modifications. */ + return populate_context_menu(default_menu, hit_test_result, context); +} +#else void populate_popup_cb(WebKitWebView *v, GtkMenu *m, void *c) { (void) c; - GUI *g = &uzbl.gui; - GtkWidget *item; - MenuItem *mi; - guint i=0; - gint context, hit=0; + gint context; - if(!g->menu_items) + if(!uzbl.gui.menu_items) return; /* check context */ if((context = get_click_context()) == -1) return; - for(i=0; i < uzbl.gui.menu_items->len; i++) { - hit = 0; - mi = g_ptr_array_index(uzbl.gui.menu_items, i); + WebKitHitTestResult *hit_test_result; + GdkEventButton ev; + gint x, y; +#if GTK_CHECK_VERSION (3, 0, 0) + gdk_window_get_device_position (gtk_widget_get_window(GTK_WIDGET(v)), + gdk_device_manager_get_client_pointer ( + gdk_display_get_device_manager ( + gtk_widget_get_display (GTK_WIDGET (v)))), + &x, &y, NULL); +#else + gdk_window_get_pointer(gtk_widget_get_window(GTK_WIDGET(v)), &x, &y, NULL); +#endif + ev.x = x; + ev.y = y; + hit_test_result = webkit_web_view_get_hit_test_result(v, &ev); - if (mi->context & WEBKIT_HIT_TEST_RESULT_CONTEXT_IMAGE) { - GdkEventButton ev; - gint x, y; - gdk_window_get_pointer(gtk_widget_get_window(GTK_WIDGET(v)), &x, &y, NULL); - ev.x = x; - ev.y = y; - mi->hittest = webkit_web_view_get_hit_test_result(v, &ev); - } + populate_context_menu(m, hit_test_result, context); - if((mi->context > WEBKIT_HIT_TEST_RESULT_CONTEXT_DOCUMENT) && - (context & mi->context)) { - if(mi->issep) { - item = gtk_separator_menu_item_new(); - gtk_menu_shell_append(GTK_MENU_SHELL(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); - gtk_menu_shell_append(GTK_MENU_SHELL(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_shell_append(GTK_MENU_SHELL(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); - gtk_menu_shell_append(GTK_MENU_SHELL(m), item); - gtk_widget_show(item); - } - } - } + g_object_unref(hit_test_result); } +#endif void window_object_cleared_cb(WebKitWebView *webview, WebKitWebFrame *frame, - JSGlobalContextRef *context, JSObjectRef *object) { + JSGlobalContextRef *context, JSObjectRef *object) { (void) frame; (void) context; (void) object; #if WEBKIT_CHECK_VERSION (1, 3, 13) // Take this opportunity to set some callbacks on the DOM diff --git a/src/callbacks.h b/src/callbacks.h index 6a10205..56bf612 100644 --- a/src/callbacks.h +++ b/src/callbacks.h @@ -31,19 +31,11 @@ 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); @@ -57,8 +49,13 @@ create_web_view_cb (WebKitWebView *web_view, WebKitWebFrame *frame, gpointer us gboolean download_cb (WebKitWebView *web_view, WebKitDownload *download, gpointer user_data); +#if WEBKIT_CHECK_VERSION (1, 9, 0) +gboolean +context_menu_cb (WebKitWebView *web_view, GtkWidget *default_menu, WebKitHitTestResult *hit_test_result, gboolean triggered_with_keyboard, gpointer user_data); +#else void populate_popup_cb(WebKitWebView *v, GtkMenu *m, void *c); +#endif gboolean button_press_cb (GtkWidget* window, GdkEventButton* event); @@ -76,8 +73,11 @@ gboolean scroll_horiz_cb(GtkAdjustment *adjust, void *w); void +close_web_view_cb(WebKitWebView *webview, gpointer user_data); + +void window_object_cleared_cb(WebKitWebView *webview, WebKitWebFrame *frame, - JSGlobalContextRef *context, JSObjectRef *object); + JSGlobalContextRef *context, JSObjectRef *object); #if WEBKIT_CHECK_VERSION (1, 3, 13) void diff --git a/src/commands.c b/src/commands.c index b3546a9..6769189 100644 --- a/src/commands.c +++ b/src/commands.c @@ -6,6 +6,7 @@ #include "callbacks.h" #include "variables.h" #include "type.h" +#include "soup.h" /* -- command to callback/function map for things we cannot attach to any signals */ CommandInfo cmdlist[] = @@ -54,12 +55,34 @@ CommandInfo cmdlist[] = { "menu_image_remove", menu_remove_image, TRUE }, { "menu_editable_remove", menu_remove_edit, TRUE }, { "hardcopy", hardcopy, TRUE }, +#ifndef USE_WEBKIT2 +#if WEBKIT_CHECK_VERSION (1, 9, 6) + { "snapshot", snapshot, TRUE }, +#endif +#endif +#ifdef USE_WEBKIT2 +#if WEBKIT_CHECK_VERSION (1, 9, 90) + { "load", load, TRUE }, + { "save", save, TRUE }, +#endif +#endif + { "remove_all_db", remove_all_db, 0 }, +#if WEBKIT_CHECK_VERSION (1, 3, 8) + { "plugin_refresh", plugin_refresh, TRUE }, + { "plugin_toggle", plugin_toggle, TRUE }, +#endif { "include", include, TRUE }, + /* Deprecated */ { "show_inspector", show_inspector, 0 }, + { "inspector", inspector, TRUE }, +#if WEBKIT_CHECK_VERSION (1, 5, 1) + { "spell_checker", spell_checker, TRUE }, +#endif { "add_cookie", add_cookie, 0 }, { "delete_cookie", delete_cookie, 0 }, { "clear_cookies", clear_cookies, 0 }, - { "download", download, 0 } + { "download", download, 0 }, + { "auth", auth, 0 } }; void @@ -190,7 +213,11 @@ toggle_var(WebKitWebView *page, GArray *argv, GString *result) { uzbl_cmdprop *c = get_var_c(var_name); if(!c) { - set_var_value(var_name, argv_idx(argv, 1)); + if (argv->len > 1) { + set_var_value(var_name, argv_idx(argv, 1)); + } else { + set_var_value(var_name, "1"); + } return; } @@ -230,18 +257,18 @@ toggle_var(WebKitWebView *page, GArray *argv, GString *result) { if(argv->len >= 3) { guint i = 2; - int first = strtoul(argv_idx(argv, 1), NULL, 10); + int first = strtol(argv_idx(argv, 1), NULL, 10); int this = first; const gchar *next_s = argv_idx(argv, 2); while(next_s && this != current) { - this = strtoul(next_s, NULL, 10); + this = strtol(next_s, NULL, 10); next_s = argv_idx(argv, ++i); } if(next_s) - next = strtoul(next_s, NULL, 10); + next = strtol(next_s, NULL, 10); else next = first; } else @@ -250,6 +277,34 @@ toggle_var(WebKitWebView *page, GArray *argv, GString *result) { set_var_value_int_c(c, next); break; } + case TYPE_ULL: + { + unsigned long long current = get_var_value_int_c(c); + unsigned long long next; + + if(argv->len >= 3) { + guint i = 2; + + unsigned long long first = strtoull(argv_idx(argv, 1), NULL, 10); + unsigned long long this = first; + + const gchar *next_s = argv_idx(argv, 2); + + while(next_s && this != current) { + this = strtoull(next_s, NULL, 10); + next_s = argv_idx(argv, ++i); + } + + if(next_s) + next = strtoull(next_s, NULL, 10); + else + next = first; + } else + next = !current; + + set_var_value_ull_c(c, next); + break; + } case TYPE_FLOAT: { float current = get_var_value_float_c(c); @@ -325,6 +380,117 @@ hardcopy(WebKitWebView *page, GArray *argv, GString *result) { webkit_web_frame_print(webkit_web_view_get_main_frame(page)); } +#ifndef USE_WEBKIT2 +#if WEBKIT_CHECK_VERSION (1, 9, 6) +void +snapshot(WebKitWebView *page, GArray *argv, GString *result) { + (void) result; + cairo_surface_t* surface; + + surface = webkit_web_view_get_snapshot(page); + + cairo_surface_write_to_png(surface, argv_idx(argv, 0)); + + cairo_surface_destroy(surface); +} +#endif +#endif + +#ifdef USE_WEBKIT2 +#if WEBKIT_CHECK_VERSION (1, 9, 90) +void +load(WebKitWebView *page, GArray *argv, GString *result) { + (void) result; + + guint sz = argv->len; + + const gchar *content = sz > 0 ? argv_idx(argv, 0) : NULL; + const gchar *content_uri = sz > 2 ? argv_idx(argv, 1) : NULL; + const gchar *base_uri = sz > 2 ? argv_idx(argv, 2) : NULL; + + webkit_web_view_load_alternate_html(page, content, content_uri, base_uri); +} + +void +save(WebKitWebView *page, GArray *argv, GString *result) { + (void) result; + guint sz = argv->len; + + const gchar *mode_str = sz > 0 ? argv_idx(argv, 0) : NULL; + + WebKitSaveMode mode = WEBKIT_SAVE_MODE_MHTML; + + if (!mode) { + mode = WEBKIT_SAVE_MODE_MHTML; + } else if (!strcmp("mhtml", mode_str)) { + mode = WEBKIT_SAVE_MODE_MHTML; + } + + if (sz > 1) { + const gchar *path = argv_idx(argv, 1); + GFile *gfile = g_file_new_for_path(path); + + webkit_web_view_save_to_file(page, gfile, mode, NULL, NULL, NULL); + /* TODO: Don't ignore the error */ + webkit_web_view_save_to_file_finish(page, NULL, NULL); + } else { + webkit_web_view_save(page, mode, NULL, NULL, NULL); + /* TODO: Don't ignore the error */ + webkit_web_view_save_finish(page, NULL, NULL); + } +} +#endif +#endif + +void +remove_all_db(WebKitWebView *page, GArray *argv, GString *result) { + (void) page; (void) argv; (void) result; + + webkit_remove_all_web_databases (); +} + +#if WEBKIT_CHECK_VERSION (1, 3, 8) +void +plugin_refresh(WebKitWebView *page, GArray *argv, GString *result) { + (void) page; (void) argv; (void) result; + + WebKitWebPluginDatabase *db = webkit_get_web_plugin_database (); + webkit_web_plugin_database_refresh (db); +} + +static void +plugin_toggle_one(WebKitWebPlugin *plugin, const gchar *name) { + const gchar *plugin_name = webkit_web_plugin_get_name (plugin); + + if (!name || !g_strcmp0 (name, plugin_name)) { + gboolean enabled = webkit_web_plugin_get_enabled (plugin); + + webkit_web_plugin_set_enabled (plugin, !enabled); + } +} + +void +plugin_toggle(WebKitWebView *page, GArray *argv, GString *result) { + (void) page; (void) result; + + WebKitWebPluginDatabase *db = webkit_get_web_plugin_database (); + GSList *plugins = webkit_web_plugin_database_get_plugins (db); + + if (argv->len == 0) { + g_slist_foreach (plugins, (GFunc)plugin_toggle_one, NULL); + } else { + guint i; + for (i = 0; i < argv->len; ++i) { + const gchar *plugin_name = argv_idx (argv, i); + + g_slist_foreach (plugins, (GFunc)plugin_toggle_one, &plugin_name); + } + } + + webkit_web_plugin_database_plugins_list_free (plugins); +} +#endif + void include(WebKitWebView *page, GArray *argv, GString *result) { (void) page; (void) result; @@ -348,6 +514,103 @@ show_inspector(WebKitWebView *page, GArray *argv, GString *result) { } void +inspector(WebKitWebView *page, GArray *argv, GString *result) { + (void) page; (void) result; + + if (argv->len < 1) { + return; + } + + const gchar* command = argv_idx (argv, 0); + + if (!g_strcmp0 (command, "show")) { + webkit_web_inspector_show (uzbl.gui.inspector); + } else if (!g_strcmp0 (command, "close")) { + webkit_web_inspector_close (uzbl.gui.inspector); + } else if (!g_strcmp0 (command, "coord")) { + if (argv->len < 3) { + return; + } + + gdouble x = strtod (argv_idx (argv, 1), NULL); + gdouble y = strtod (argv_idx (argv, 2), NULL); + + /* Let's not tempt the dragons. */ + if (errno == ERANGE) { + return; + } + + webkit_web_inspector_inspect_coordinates (uzbl.gui.inspector, x, y); +#if WEBKIT_CHECK_VERSION (1, 3, 17) + } else if (!g_strcmp0 (command, "node")) { + /* TODO: Implement */ +#endif + } +} + +#if WEBKIT_CHECK_VERSION (1, 5, 1) +void +spell_checker(WebKitWebView *page, GArray *argv, GString *result) { + (void) page; + + if (argv->len < 1) { + return; + } + + GObject *obj = webkit_get_text_checker (); + + if (!obj) { + return; + } + if (!WEBKIT_IS_SPELL_CHECKER (obj)) { + return; + } + + WebKitSpellChecker *checker = WEBKIT_SPELL_CHECKER (obj); + + const gchar* command = argv_idx (argv, 0); + + if (!g_strcmp0 (command, "ignore")) { + if (argv->len < 2) { + return; + } + + guint i; + for (i = 1; i < argv->len; ++i) { + const gchar *word = argv_idx (argv, i); + + webkit_spell_checker_ignore_word (checker, word); + } + } else if (!g_strcmp0 (command, "learn")) { + if (argv->len < 2) { + return; + } + + guint i; + for (i = 1; i < argv->len; ++i) { + const gchar *word = argv_idx (argv, i); + + webkit_spell_checker_learn_word (checker, word); + } + } else if (!g_strcmp0 (command, "autocorrect")) { + if (argv->len != 2) { + return; + } + + gchar *word = argv_idx (argv, 1); + + char *new_word = webkit_spell_checker_get_autocorrect_suggestions_for_misspelled_word (checker, word); + + g_string_assign (result, new_word); + + free (new_word); + } else if (!g_strcmp0 (command, "guesses")) { + /* TODO Implement */ + } +} +#endif + +void add_cookie(WebKitWebView *page, GArray *argv, GString *result) { (void) page; (void) result; gchar *host, *path, *name, *value, *scheme; @@ -583,3 +846,18 @@ act_dump_config_as_events(WebKitWebView *web_view, GArray *argv, GString *result (void)web_view; (void) argv; (void)result; dump_config_as_events(); } + +void +auth(WebKitWebView *page, GArray *argv, GString *result) { + (void) page; (void) result; + gchar *info, *username, *password; + + if(argv->len != 3) + return; + + info = argv_idx (argv, 0); + username = argv_idx (argv, 1); + password = argv_idx (argv, 2); + + authenticate (info, username, password); +} diff --git a/src/commands.h b/src/commands.h index 38bd5f2..6a14b9b 100644 --- a/src/commands.h +++ b/src/commands.h @@ -4,7 +4,11 @@ #ifndef __COMMANDS__ #define __COMMANDS__ +#ifdef USE_WEBKIT2 +#include +#else #include +#endif typedef void (*Command)(WebKitWebView*, GArray *argv, GString *result); @@ -51,8 +55,29 @@ void search_reverse_text (WebKitWebView *page, GArray *argv, GString *res void search_clear(WebKitWebView *page, GArray *argv, GString *result); void dehilight (WebKitWebView *page, GArray *argv, GString *result); void hardcopy(WebKitWebView *page, GArray *argv, GString *result); +#ifndef USE_WEBKIT2 +#if WEBKIT_CHECK_VERSION (1, 9, 6) +void snapshot(WebKitWebView *page, GArray *argv, GString *result); +#endif +#endif +#ifdef USE_WEBKIT2 +#if WEBKIT_CHECK_VERSION (1, 9, 90) +void load(WebKitWebView *page, GArray *argv, GString *result); +void save(WebKitWebView *page, GArray *argv, GString *result); +#endif +#endif +void remove_all_db(WebKitWebView *page, GArray *argv, GString *result); +#if WEBKIT_CHECK_VERSION (1, 3, 8) +void plugin_refresh(WebKitWebView *page, GArray *argv, GString *result); +void plugin_toggle(WebKitWebView *page, GArray *argv, GString *result); +#endif void include(WebKitWebView *page, GArray *argv, GString *result); +/* Deprecated (use inspector instead) */ void show_inspector(WebKitWebView *page, GArray *argv, GString *result); +void inspector(WebKitWebView *page, GArray *argv, GString *result); +#if WEBKIT_CHECK_VERSION (1, 5, 1) +void spell_checker(WebKitWebView *page, GArray *argv, GString *result); +#endif void add_cookie(WebKitWebView *page, GArray *argv, GString *result); void delete_cookie(WebKitWebView *page, GArray *argv, GString *result); void clear_cookies(WebKitWebView *pag, GArray *argv, GString *result); @@ -65,5 +90,6 @@ void toggle_zoom_type (WebKitWebView* page, GArray *argv, GString *result void toggle_status (WebKitWebView* page, GArray *argv, GString *result); void act_dump_config(WebKitWebView* page, GArray *argv, GString *result); void act_dump_config_as_events(WebKitWebView* page, GArray *argv, GString *result); +void auth(WebKitWebView* page, GArray *argv, GString *result); #endif diff --git a/src/cookie-jar.c b/src/cookie-jar.c index 2f6be83..83facd5 100644 --- a/src/cookie-jar.c +++ b/src/cookie-jar.c @@ -41,21 +41,21 @@ changed(SoupCookieJar *jar, SoupCookie *old_cookie, SoupCookie *new_cookie) { * propagated to other uzbl instances using add/delete_cookie. */ if(!uzbl_jar->in_manual_add) { gchar *scheme = cookie->secure - ? cookie->http_only ? "httpsOnly" : "https" - : cookie->http_only ? "httpOnly" : "http"; + ? cookie->http_only ? "httpsOnly" : "https" + : cookie->http_only ? "httpOnly" : "http"; gchar *expires = NULL; if(cookie->expires) expires = g_strdup_printf ("%ld", (long)soup_date_to_time_t (cookie->expires)); - send_event (new_cookie ? ADD_COOKIE : DELETE_COOKIE, NULL, - TYPE_STR, cookie->domain, - TYPE_STR, cookie->path, - TYPE_STR, cookie->name, - TYPE_STR, cookie->value, - TYPE_STR, scheme, - TYPE_STR, expires ? expires : "", - NULL); + send_event (new_cookie ? ADD_COOKIE : DELETE_COOKIE, NULL, + TYPE_STR, cookie->domain, + TYPE_STR, cookie->path, + TYPE_STR, cookie->name, + TYPE_STR, cookie->value, + TYPE_STR, scheme, + TYPE_STR, expires ? expires : "", + NULL); if(expires) g_free(expires); diff --git a/src/events.c b/src/events.c index 081a942..8fc8573 100644 --- a/src/events.c +++ b/src/events.c @@ -21,7 +21,9 @@ const char *event_table[LAST_EVENT] = { "LOAD_COMMIT" , "LOAD_FINISH" , "LOAD_ERROR" , + "REQUEST_QUEUED" , "REQUEST_STARTING" , + "REQUEST_FINISHED" , "KEY_PRESS" , "KEY_RELEASE" , "MOD_PRESS" , @@ -32,6 +34,7 @@ const char *event_table[LAST_EVENT] = { "GEOMETRY_CHANGED" , "WEBINSPECTOR" , "NEW_WINDOW" , + "CLOSE_WINDOW" , "SELECTION_CHANGED", "VARIABLE_SET" , "FIFO_SET" , @@ -48,7 +51,6 @@ const char *event_table[LAST_EVENT] = { "PLUG_CREATED" , "COMMAND_ERROR" , "BUILTINS" , - "PTR_MOVE" , "SCROLL_VERT" , "SCROLL_HORIZ" , "DOWNLOAD_STARTED" , @@ -57,7 +59,8 @@ const char *event_table[LAST_EVENT] = { "ADD_COOKIE" , "DELETE_COOKIE" , "FOCUS_ELEMENT" , - "BLUR_ELEMENT" + "BLUR_ELEMENT" , + "AUTHENTICATE" }; /* for now this is just a alias for GString */ @@ -168,6 +171,10 @@ vformat_event(int type, const gchar *custom_event, va_list vargs) { g_string_append_printf (event_message, "%d", va_arg (vargs, int)); break; + case TYPE_ULL: + g_string_append_printf (event_message, "%llu", va_arg (vargs, unsigned long long)); + break; + case TYPE_STR: /* a string that needs to be escaped */ g_string_append_c (event_message, '\''); @@ -284,10 +291,10 @@ get_modifier_mask(guint state) { g_string_append(modifiers, "Ctrl|"); if(state & GDK_MOD1_MASK) g_string_append(modifiers,"Mod1|"); - /* Mod2 is usually Num_Luck. Ignore it as it messes up keybindings. + /* Mod2 is usually Num_Luck. Ignore it as it messes up keybindings. if(state & GDK_MOD2_MASK) g_string_append(modifiers,"Mod2|"); - */ + */ if(state & GDK_MOD3_MASK) g_string_append(modifiers,"Mod3|"); if(state & GDK_MOD4_MASK) @@ -349,10 +356,10 @@ guint key_to_modifier(guint keyval) { } } -guint button_to_modifier(guint buttonval) { - if(buttonval <= 5) - return 1 << (7 + buttonval); - return 0; +guint button_to_modifier (guint buttonval) { + if(buttonval <= 5) + return 1 << (7 + buttonval); + return 0; } /* Transform gdk key events to our own events */ diff --git a/src/events.h b/src/events.h index 73d0712..bd9df33 100644 --- a/src/events.h +++ b/src/events.h @@ -12,20 +12,21 @@ /* Event system */ enum event_type { LOAD_START, LOAD_COMMIT, LOAD_FINISH, LOAD_ERROR, - REQUEST_STARTING, + REQUEST_QUEUED, REQUEST_STARTING, REQUEST_FINISHED, KEY_PRESS, KEY_RELEASE, MOD_PRESS, MOD_RELEASE, COMMAND_EXECUTED, LINK_HOVER, TITLE_CHANGED, GEOMETRY_CHANGED, - WEBINSPECTOR, NEW_WINDOW, SELECTION_CHANGED, + WEBINSPECTOR, NEW_WINDOW, CLOSE_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, SCROLL_VERT, SCROLL_HORIZ, + SCROLL_VERT, SCROLL_HORIZ, DOWNLOAD_STARTED, DOWNLOAD_PROGRESS, DOWNLOAD_COMPLETE, ADD_COOKIE, DELETE_COOKIE, FOCUS_ELEMENT, BLUR_ELEMENT, + AUTHENTICATE, /* must be last entry */ LAST_EVENT diff --git a/src/inspector.c b/src/inspector.c index d0d86b9..d584c77 100644 --- a/src/inspector.c +++ b/src/inspector.c @@ -8,14 +8,14 @@ #include "callbacks.h" #include "type.h" -void +static void hide_window_cb(GtkWidget *widget, gpointer data) { (void) data; gtk_widget_hide(widget); } -WebKitWebView* +static WebKitWebView * create_inspector_cb (WebKitWebInspector* web_inspector, WebKitWebView* page, gpointer data){ (void) data; (void) page; @@ -44,7 +44,7 @@ create_inspector_cb (WebKitWebInspector* web_inspector, WebKitWebView* page, gpo return WEBKIT_WEB_VIEW(new_web_view); } -gboolean +static gboolean inspector_show_window_cb (WebKitWebInspector* inspector){ (void) inspector; gtk_widget_show(uzbl.gui.inspector_window); @@ -54,32 +54,32 @@ inspector_show_window_cb (WebKitWebInspector* inspector){ } /* TODO: Add variables and code to make use of these functions */ -gboolean +static gboolean inspector_close_window_cb (WebKitWebInspector* inspector){ (void) inspector; send_event(WEBINSPECTOR, NULL, TYPE_NAME, "close", NULL); return TRUE; } -gboolean +static gboolean inspector_attach_window_cb (WebKitWebInspector* inspector){ (void) inspector; return FALSE; } -gboolean +static gboolean inspector_detach_window_cb (WebKitWebInspector* inspector){ (void) inspector; return FALSE; } -gboolean +static gboolean inspector_uri_changed_cb (WebKitWebInspector* inspector){ (void) inspector; return FALSE; } -gboolean +static gboolean inspector_inspector_destroyed_cb (WebKitWebInspector* inspector){ (void) inspector; return FALSE; diff --git a/src/io.c b/src/io.c index ff418ef..ca3fcf6 100644 --- a/src/io.c +++ b/src/io.c @@ -47,7 +47,7 @@ control_fifo(GIOChannel *gio, GIOCondition condition) { } -gboolean +static gboolean attach_fifo(gchar *path) { GError *error = NULL; /* we don't really need to write to the file, but if we open the @@ -259,7 +259,7 @@ control_client_socket(GIOChannel *clientchan) { } -gboolean +static gboolean attach_socket(gchar *path, struct sockaddr_un *local) { GIOChannel *chan = NULL; int sock = socket (AF_UNIX, SOCK_STREAM, 0); diff --git a/src/menu.c b/src/menu.c index 451b35a..1d9c5b4 100644 --- a/src/menu.c +++ b/src/menu.c @@ -2,7 +2,7 @@ #include "util.h" #include "uzbl-core.h" -void +static void add_to_menu(GArray *argv, guint context) { GUI *g = &uzbl.gui; MenuItem *m; @@ -68,7 +68,7 @@ menu_add_edit(WebKitWebView *page, GArray *argv, GString *result) { } -void +static void add_separator_to_menu(GArray *argv, guint context) { GUI *g = &uzbl.gui; MenuItem *m; @@ -127,7 +127,7 @@ menu_add_separator_edit(WebKitWebView *page, GArray *argv, GString *result) { } -void +static void remove_from_menu(GArray *argv, guint context) { GUI *g = &uzbl.gui; MenuItem *mi; diff --git a/src/menu.h b/src/menu.h index 03055e5..c4ca047 100644 --- a/src/menu.h +++ b/src/menu.h @@ -1,14 +1,18 @@ #ifndef __MENU__ #define __MENU__ +#ifdef USE_WEBKIT2 +#include +#else #include +#endif typedef struct { gchar* name; gchar* cmd; gboolean issep; guint context; - WebKitHitTestResult* hittest; + gchar* argument; } MenuItem; void menu_add(WebKitWebView *page, GArray *argv, GString *result); diff --git a/src/soup.c b/src/soup.c new file mode 100644 index 0000000..8430018 --- /dev/null +++ b/src/soup.c @@ -0,0 +1,183 @@ +#include "uzbl-core.h" +#include "util.h" +#include "events.h" +#include "type.h" + +static void handle_authentication (SoupSession *session, + SoupMessage *msg, + SoupAuth *auth, + gboolean retrying, + gpointer user_data); + +static void handle_request_queued (SoupSession *session, + SoupMessage *msg, + gpointer user_data); + +static void handle_request_started (SoupSession *session, + SoupMessage *msg, + gpointer user_data); + +static void handle_request_finished (SoupMessage *msg, + gpointer user_data); + +struct _PendingAuth +{ + SoupAuth *auth; + GList *messages; +}; +typedef struct _PendingAuth PendingAuth; + +static PendingAuth *pending_auth_new (SoupAuth *auth); +static void pending_auth_free (PendingAuth *self); +static void pending_auth_add_message (PendingAuth *self, + SoupMessage *message); + +void +uzbl_soup_init (SoupSession *session) +{ + uzbl.net.soup_cookie_jar = uzbl_cookie_jar_new (); + uzbl.net.pending_auths = g_hash_table_new_full ( + g_str_hash, g_str_equal, + g_free, pending_auth_free + ); + + soup_session_add_feature ( + session, + SOUP_SESSION_FEATURE (uzbl.net.soup_cookie_jar) + ); + + g_signal_connect ( + session, "request-queued", + G_CALLBACK (handle_request_queued), NULL + ); + + g_signal_connect ( + session, "request-started", + G_CALLBACK (handle_request_started), NULL + ); + + g_signal_connect ( + session, "authenticate", + G_CALLBACK (handle_authentication), NULL + ); +} + +void authenticate (const char *authinfo, + const char *username, + const char *password) +{ + PendingAuth *pending = g_hash_table_lookup ( + uzbl.net.pending_auths, + authinfo + ); + + if (pending == NULL) { + return; + } + + soup_auth_authenticate (pending->auth, username, password); + for(GList *l = pending->messages; l != NULL; l = l->next) { + soup_session_unpause_message ( + uzbl.net.soup_session, + SOUP_MESSAGE (l->data) + ); + } + + g_hash_table_remove (uzbl.net.pending_auths, authinfo); +} + +static void +handle_request_queued (SoupSession *session, + SoupMessage *msg, + gpointer user_data) +{ + (void) session; (void) user_data; + + send_event ( + REQUEST_QUEUED, NULL, + TYPE_STR, soup_uri_to_string (soup_message_get_uri (msg), FALSE), + NULL + ); +} + +static void +handle_request_started (SoupSession *session, + SoupMessage *msg, + gpointer user_data) +{ + (void) session; (void) user_data; + + send_event ( + REQUEST_STARTING, NULL, + TYPE_STR, soup_uri_to_string (soup_message_get_uri (msg), FALSE), + NULL + ); + + g_signal_connect ( + G_OBJECT (msg), "finished", + G_CALLBACK (handle_request_finished), NULL + ); +} + +static void +handle_request_finished (SoupMessage *msg, gpointer user_data) +{ + (void) user_data; + + send_event ( + REQUEST_FINISHED, NULL, + TYPE_STR, soup_uri_to_string (soup_message_get_uri (msg), FALSE), + NULL + ); +} + +static void +handle_authentication (SoupSession *session, + SoupMessage *msg, + SoupAuth *auth, + gboolean retrying, + gpointer user_data) +{ + (void) user_data; + PendingAuth *pending; + char *authinfo = soup_auth_get_info (auth); + + pending = g_hash_table_lookup (uzbl.net.pending_auths, authinfo); + if (pending == NULL) { + pending = pending_auth_new (auth); + g_hash_table_insert (uzbl.net.pending_auths, authinfo, pending); + } + + pending_auth_add_message (pending, msg); + soup_session_pause_message (session, msg); + + send_event ( + AUTHENTICATE, NULL, + TYPE_STR, authinfo, + TYPE_STR, soup_auth_get_host (auth), + TYPE_STR, soup_auth_get_realm (auth), + TYPE_STR, (retrying ? "retrying" : ""), + NULL + ); +} + +static PendingAuth *pending_auth_new (SoupAuth *auth) +{ + PendingAuth *self = g_new (PendingAuth, 1); + self->auth = auth; + self->messages = NULL; + g_object_ref (auth); + return self; +} + +static void pending_auth_free (PendingAuth *self) +{ + g_object_unref (self->auth); + g_list_free_full (self->messages, g_object_unref); + g_free (self); +} + +static void pending_auth_add_message (PendingAuth *self, SoupMessage *message) +{ + self->messages = g_list_append (self->messages, g_object_ref (message)); +} diff --git a/src/soup.h b/src/soup.h new file mode 100644 index 0000000..ce7d605 --- /dev/null +++ b/src/soup.h @@ -0,0 +1,18 @@ +/** + * Uzbl tweaks and extension for soup + */ + +#ifndef __UZBL_SOUP__ +#define __UZBL_SOUP__ + +#include + +/** + * Attach uzbl specific behaviour to the given SoupSession + */ +void uzbl_soup_init (SoupSession *session); + +void authenticate (const char *authinfo, + const char *username, + const char *password); +#endif diff --git a/src/status-bar.h b/src/status-bar.h index cae8905..1fd4ef2 100644 --- a/src/status-bar.h +++ b/src/status-bar.h @@ -10,7 +10,7 @@ #define UZBL_IS_STATUS_BAR_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), UZBL_TYPE_STATUS_BAR)) #define UZBL_STATUS_BAR_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), UZBL_TYPE_STATUS_BAR, UzblStatusBarClass)) -typedef struct _UzblStatusBar UzblStatusBar; +typedef struct _UzblStatusBar UzblStatusBar; typedef struct _UzblStatusBarClass UzblStatusBarClass; struct _UzblStatusBar { diff --git a/src/type.h b/src/type.h index eda02c1..826ca3b 100644 --- a/src/type.h +++ b/src/type.h @@ -9,6 +9,7 @@ enum ptr_type { TYPE_INT = 1, TYPE_STR, TYPE_FLOAT, + TYPE_ULL, TYPE_NAME, // used by send_event TYPE_FORMATTEDSTR, @@ -24,6 +25,7 @@ enum ptr_type { typedef union uzbl_value_ptr_t { int *i; float *f; + unsigned long long *ull; gchar **s; } uzbl_value_ptr; diff --git a/src/util.c b/src/util.c index 1468d23..3f7f093 100644 --- a/src/util.c +++ b/src/util.c @@ -195,3 +195,9 @@ append_escaped (GString *dest, const gchar *src) { } return dest; } + +void +sharg_append (GArray *a, const gchar *str) { + const gchar *s = (str ? str : ""); + g_array_append_val(a, s); +} diff --git a/src/util.h b/src/util.h index cc29247..e0a8bbd 100644 --- a/src/util.h +++ b/src/util.h @@ -12,7 +12,11 @@ char* str_replace(const char* search, const char* replace, const char* str gboolean for_each_line_in_file(const gchar *path, void (*callback)(const gchar *l, void *c), void *user_data); gchar* find_existing_file(const gchar*); gchar* argv_idx(const GArray*, const guint); + /** * appends `src' to `dest' with backslash, single-quotes and newlines in - * `src' escaped */ + * `src' escaped + */ GString * append_escaped (GString *dest, const gchar *src); + +void sharg_append (GArray *array, const gchar *str); diff --git a/src/uzbl-core.c b/src/uzbl-core.c index 1288f22..748202f 100644 --- a/src/uzbl-core.c +++ b/src/uzbl-core.c @@ -39,6 +39,7 @@ #include "io.h" #include "variables.h" #include "type.h" +#include "soup.h" UzblCore uzbl; @@ -279,22 +280,6 @@ clean_up(void) { } } -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); - g_object_unref (ht); - - return (gint)context; -} - /* --- SIGNALS --- */ sigfunc* setup_signal(int signr, sigfunc *shandler) { @@ -310,7 +295,7 @@ setup_signal(int signr, sigfunc *shandler) { return NULL; } -void +static void empty_event_buffer(int s) { (void) s; if(uzbl.state.event_buffer) { @@ -356,12 +341,12 @@ parse_cmd_line_cb(const char *line, void *user_data) { } void -run_command_file(const gchar *path) { - if(!for_each_line_in_file(path, parse_cmd_line_cb, NULL)) { - gchar *tmp = g_strdup_printf("File %s can not be read.", path); - send_event(COMMAND_ERROR, NULL, TYPE_STR, tmp, NULL); - g_free(tmp); - } +run_command_file (const gchar *path) { + if(!for_each_line_in_file(path, parse_cmd_line_cb, NULL)) { + gchar *tmp = g_strdup_printf("File %s can not be read.", path); + send_event(COMMAND_ERROR, NULL, TYPE_STR, tmp, NULL); + g_free(tmp); + } } /* Javascript*/ @@ -463,12 +448,6 @@ search_text (WebKitWebView *page, const gchar *key, const gboolean forward) { } } -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 @@ -517,7 +496,7 @@ run_command (const gchar *command, const gchar **args, const gboolean sync, return result; } -/*@null@*/ gchar** +/*@null@*/ static gchar** split_quoted(const gchar* src, const gboolean unquote) { /* split on unquoted space or tab, return array of strings; remove a layer of quotes and backslashes if unquote */ @@ -769,7 +748,6 @@ create_scrolled_win() { "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::notify::title", (GCallback)title_change_cb, NULL, "signal::notify::progress", (GCallback)progress_change_cb, NULL, "signal::notify::load-status", (GCallback)load_status_change_cb, NULL, @@ -777,12 +755,16 @@ create_scrolled_win() { "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::close-web-view", (GCallback)close_web_view_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, +#if WEBKIT_CHECK_VERSION (1, 9, 0) + "signal::context-menu", (GCallback)context_menu_cb, NULL, +#else "signal::populate-popup", (GCallback)populate_popup_cb, NULL, +#endif "signal::focus-in-event", (GCallback)focus_cb, NULL, "signal::focus-out-event", (GCallback)focus_cb, NULL, "signal::window-object-cleared", (GCallback)window_object_cleared_cb,NULL, @@ -822,7 +804,6 @@ create_plug() { void settings_init () { State* s = &uzbl.state; - Network* n = &uzbl.net; int i; /* Load default config */ @@ -841,70 +822,13 @@ settings_init () { /* Load config file, if any */ if (s->config_file) { - run_command_file(s->config_file); + run_command_file(s->config_file); g_setenv("UZBL_CONFIG", s->config_file, TRUE); } else if (uzbl.state.verbose) printf ("No configuration file loaded.\n"); if (s->connect_socket_names) init_connect_socket(); - - 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 != 0) { - soup_session_pause_message(session, msg); - - GString *result = g_string_new (""); - - gchar *info = g_strdup(soup_auth_get_info(auth)); - gchar *host = g_strdup(soup_auth_get_host(auth)); - gchar *realm = g_strdup(soup_auth_get_realm(auth)); - - GArray *a = g_array_new (TRUE, FALSE, sizeof(gchar*)); - const CommandInfo *c = parse_command_parts(uzbl.behave.authentication_handler, a); - if(c) { - sharg_append(a, info); - sharg_append(a, host); - sharg_append(a, realm); - sharg_append(a, retrying ? "TRUE" : "FALSE"); - - run_parsed_command(c, a, result); - } - g_array_free(a, TRUE); - - if (result->len > 0) { - char *username, *password; - int number_of_endls=0; - - username = result->str; - - gchar *p; - for (p = result->str; *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); - } - - soup_session_unpause_message(session, msg); - - g_string_free(result, TRUE); - g_free(info); - g_free(host); - g_free(realm); - } } /* Set up gtk, gobject, variable defaults and other things that tests and other @@ -922,11 +846,19 @@ initialize(int argc, char** argv) { uzbl.info.webkit_major = webkit_major_version(); uzbl.info.webkit_minor = webkit_minor_version(); uzbl.info.webkit_micro = webkit_micro_version(); +#ifdef USE_WEBKIT2 + uzbl.info.webkit2 = 1; +#else + uzbl.info.webkit2 = 0; +#endif uzbl.info.arch = ARCH; uzbl.info.commit = COMMIT; uzbl.state.last_result = NULL; + /* BUG There isn't a getter for this; need to maintain separately. */ + uzbl.behave.maintain_history = TRUE; + /* Parse commandline arguments */ GOptionContext* context = g_option_context_new ("[ uri ] - load a uri by default"); g_option_context_add_main_entries(context, entries, NULL); @@ -956,10 +888,8 @@ initialize(int argc, char** argv) { event_buffer_timeout(10); /* HTTP client */ - uzbl.net.soup_session = webkit_get_default_session(); - uzbl.net.soup_cookie_jar = uzbl_cookie_jar_new(); - - soup_session_add_feature(uzbl.net.soup_session, SOUP_SESSION_FEATURE(uzbl.net.soup_cookie_jar)); + uzbl.net.soup_session = webkit_get_default_session(); + uzbl_soup_init (uzbl.net.soup_session); commands_hash(); variables_hash(); @@ -1081,6 +1011,13 @@ main (int argc, char* argv[]) { gboolean verbose_override = uzbl.state.verbose; + /* Finally show the window */ + if (uzbl.gui.main_window) { + gtk_widget_show_all (uzbl.gui.main_window); + } else { + gtk_widget_show_all (GTK_WIDGET (uzbl.gui.plug)); + } + /* Read configuration file */ settings_init(); @@ -1099,13 +1036,6 @@ main (int argc, char* argv[]) { g_free(uri_override); } - /* Finally show the window */ - if (uzbl.gui.main_window) { - gtk_widget_show_all (uzbl.gui.main_window); - } else { - gtk_widget_show_all (GTK_WIDGET (uzbl.gui.plug)); - } - /* Verbose feedback */ if (uzbl.state.verbose) { printf("Uzbl start location: %s\n", argv[0]); diff --git a/src/uzbl-core.h b/src/uzbl-core.h index 1f9613e..6f27818 100644 --- a/src/uzbl-core.h +++ b/src/uzbl-core.h @@ -23,7 +23,11 @@ #include #include #include +#ifdef USE_WEBKIT2 +#include +#else #include +#endif #include #include @@ -78,6 +82,7 @@ typedef struct { WebKitWebInspector* inspector; /* Custom context menu item */ + gboolean custom_context_menu; GPtrArray* menu_items; } GUI; @@ -114,6 +119,9 @@ typedef struct { gboolean handle_multi_button; GPtrArray* event_buffer; gchar** connect_socket_names; + + /* Temporary web view used when a new window is opened */ + WebKitWebView* _tmp_web_view; } State; @@ -121,6 +129,7 @@ typedef struct { typedef struct { SoupSession* soup_session; UzblCookieJar* soup_cookie_jar; + GHashTable* pending_auths; SoupLogger* soup_logger; char* proxy_url; char* useragent; @@ -129,12 +138,6 @@ typedef struct { gint max_conns_host; } Network; -/* ssl */ -typedef struct { - gchar *ca_file; - gchar *verify_cert; -} Ssl; - /* Behaviour */ typedef struct { /* Status bar */ @@ -161,6 +164,7 @@ typedef struct { guint http_debug; gchar* shell_cmd; guint view_source; + gboolean maintain_history; gboolean print_version; @@ -176,6 +180,7 @@ typedef struct { int webkit_major; int webkit_minor; int webkit_micro; + int webkit2; gchar* arch; gchar* commit; @@ -189,7 +194,6 @@ typedef struct { GUI gui; State state; Network net; - Ssl ssl; Behaviour behave; Communication comm; Info info; @@ -245,19 +249,12 @@ void search_text (WebKitWebView *page, const gchar *key, const gboolean f void eval_js(WebKitWebView *web_view, const gchar *script, GString *result, const gchar *script_file); /* Network functions */ -void handle_authentication (SoupSession *session, - SoupMessage *msg, - SoupAuth *auth, - gboolean retrying, - gpointer user_data); - void init_connect_socket(); gboolean remove_socket_from_array(GIOChannel *chan); /* Window */ void retrieve_geometry(); void scroll(GtkAdjustment* bar, gchar *amount_str); -gint get_click_context(); #endif diff --git a/src/variables.c b/src/variables.c index e4763bc..749a176 100644 --- a/src/variables.c +++ b/src/variables.c @@ -5,6 +5,8 @@ #include "io.h" #include "util.h" +#include + uzbl_cmdprop * get_var_c(const gchar *name) { return g_hash_table_lookup(uzbl.behave.proto_var, name); @@ -32,6 +34,13 @@ send_set_var_event(const char *name, const uzbl_cmdprop *c) { TYPE_INT, get_var_value_int_c(c), NULL); break; + case TYPE_ULL: + send_event (VARIABLE_SET, NULL, + TYPE_NAME, name, + TYPE_NAME, "ull", + TYPE_ULL, get_var_value_ull_c(c), + NULL); + break; case TYPE_FLOAT: send_event (VARIABLE_SET, NULL, TYPE_NAME, name, @@ -78,6 +87,14 @@ set_var_value_int_c(uzbl_cmdprop *c, int i) { } void +set_var_value_ull_c(uzbl_cmdprop *c, unsigned long long ull) { + if(c->setter) + ((void (*)(unsigned long long))c->setter)(ull); + else + *(c->ptr.ull) = ull; +} + +void set_var_value_float_c(uzbl_cmdprop *c, float f) { if(c->setter) ((void (*)(float))c->setter)(f); @@ -100,10 +117,16 @@ set_var_value(const gchar *name, gchar *val) { break; case TYPE_INT: { - int i = (int)strtoul(val, NULL, 10); + int i = (int)strtol(val, NULL, 10); set_var_value_int_c(c, i); break; } + case TYPE_ULL: + { + unsigned long long ull = strtoull(val, NULL, 10); + set_var_value_ull_c(c, ull); + break; + } case TYPE_FLOAT: { float f = strtod(val, NULL); @@ -184,6 +207,24 @@ get_var_value_int(const gchar *name) { return get_var_value_int_c(c); } +unsigned long long +get_var_value_ull_c(const uzbl_cmdprop *c) { + if(!c) return 0; + + if(c->getter) { + return ((unsigned long long (*)())c->getter)(); + } else if(c->ptr.ull) + return *(c->ptr.ull); + + return 0; +} + +unsigned long long +get_var_value_ull(const gchar *name) { + uzbl_cmdprop *c = get_var_c(name); + return get_var_value_ull_c(c); +} + float get_var_value_float_c(const uzbl_cmdprop *c) { if(!c) return 0; @@ -202,7 +243,7 @@ get_var_value_float(const gchar *name) { return get_var_value_float_c(c); } -void +static void dump_var_hash(gpointer k, gpointer v, gpointer ud) { (void) ud; uzbl_cmdprop *c = v; @@ -214,10 +255,13 @@ dump_var_hash(gpointer k, gpointer v, gpointer ud) { gchar *v = get_var_value_string_c(c); printf("set %s = %s\n", (char *)k, v); g_free(v); - } else if(c->type == TYPE_INT) + } else if(c->type == TYPE_INT) { printf("set %s = %d\n", (char *)k, get_var_value_int_c(c)); - else if(c->type == TYPE_FLOAT) + } else if(c->type == TYPE_ULL) { + printf("set %s = %llu\n", (char *)k, get_var_value_ull_c(c)); + } else if(c->type == TYPE_FLOAT) { printf("set %s = %f\n", (char *)k, get_var_value_float_c(c)); + } } void @@ -225,7 +269,7 @@ dump_config() { g_hash_table_foreach(uzbl.behave.proto_var, dump_var_hash, NULL); } -void +static void dump_var_hash_as_event(gpointer k, gpointer v, gpointer ud) { (void) ud; uzbl_cmdprop *c = v; @@ -240,18 +284,23 @@ dump_config_as_events() { } /* is the given string made up entirely of decimal digits? */ -gboolean +static gboolean string_is_integer(const char *s) { return (strspn(s, "0123456789") == strlen(s)); } -GObject* +static GObject * +cookie_jar() { + return G_OBJECT(uzbl.net.soup_cookie_jar); +} + +static GObject * view_settings() { return G_OBJECT(webkit_web_view_get_settings(uzbl.gui.web_view)); } -void +static void set_window_property(const gchar* prop, const gchar* value) { if(GTK_IS_WIDGET(uzbl.gui.main_window)) { gdk_property_change( @@ -276,7 +325,7 @@ uri_change_cb (WebKitWebView *web_view, GParamSpec param_spec) { set_window_property("UZBL_URI", uzbl.state.uri); } -gchar * +static gchar * make_uri_from_user_input(const gchar *uri) { gchar *result = NULL; @@ -312,7 +361,7 @@ make_uri_from_user_input(const gchar *uri) { return g_strconcat("http://", uri, NULL); } -void +static void set_uri(const gchar *uri) { /* Strip leading whitespace */ while (*uri && isspace(*uri)) @@ -340,7 +389,7 @@ set_uri(const gchar *uri) { g_free (newuri); } -void +static void set_max_conns(int max_conns) { uzbl.net.max_conns = max_conns; @@ -348,7 +397,7 @@ set_max_conns(int max_conns) { SOUP_SESSION_MAX_CONNS, uzbl.net.max_conns, NULL); } -void +static void set_max_conns_host(int max_conns_host) { uzbl.net.max_conns_host = max_conns_host; @@ -356,7 +405,7 @@ set_max_conns_host(int max_conns_host) { SOUP_SESSION_MAX_CONNS_PER_HOST, uzbl.net.max_conns_host, NULL); } -void +static void set_http_debug(int debug) { uzbl.behave.http_debug = debug; @@ -371,77 +420,258 @@ set_http_debug(int debug) { SOUP_SESSION_FEATURE(uzbl.net.soup_logger)); } -void +static void set_ca_file(gchar *path) { g_object_set (uzbl.net.soup_session, "ssl-ca-file", path, NULL); } -gchar * +static gchar * get_ca_file() { gchar *path; g_object_get (uzbl.net.soup_session, "ssl-ca-file", &path, NULL); return path; } -void -set_verify_cert(int strict) { - g_object_set (uzbl.net.soup_session, "ssl-strict", strict, NULL); +#define EXPOSE_WEB_INSPECTOR_SETTINGS(SYM, PROPERTY, TYPE) \ +static void set_##SYM(TYPE val) { \ + g_object_set(uzbl.gui.inspector, (PROPERTY), val, NULL); \ +} \ +static TYPE get_##SYM() { \ + TYPE val; \ + g_object_get(uzbl.gui.inspector, (PROPERTY), &val, NULL); \ + return val; \ } -int -get_verify_cert() { - int strict; - g_object_get (uzbl.net.soup_session, "ssl-strict", &strict, NULL); - return strict; +EXPOSE_WEB_INSPECTOR_SETTINGS(profile_js, "javascript-profiling-enabled", int) +EXPOSE_WEB_INSPECTOR_SETTINGS(profile_timeline, "timeline-profiling-enabled", gchar *) + +#undef EXPOSE_WEB_INSPECTOR_SETTINGS + +#define EXPOSE_SOUP_SESSION_SETTINGS(SYM, PROPERTY, TYPE) \ +static void set_##SYM(TYPE val) { \ + g_object_set(uzbl.net.soup_session, (PROPERTY), val, NULL); \ +} \ +static TYPE get_##SYM() { \ + TYPE val; \ + g_object_get(uzbl.net.soup_session, (PROPERTY), &val, NULL); \ + return val; \ } +EXPOSE_SOUP_SESSION_SETTINGS(verify_cert, "ssl-strict", int) + +#undef EXPOSE_SOUP_SESSION_SETTINGS + +#define EXPOSE_SOUP_COOKIE_JAR_SETTINGS(SYM, PROPERTY, TYPE) \ +static void set_##SYM(TYPE val) { \ + g_object_set(cookie_jar(), (PROPERTY), val, NULL); \ +} \ +static TYPE get_##SYM() { \ + TYPE val; \ + g_object_get(cookie_jar(), (PROPERTY), &val, NULL); \ + return val; \ +} + +EXPOSE_SOUP_COOKIE_JAR_SETTINGS(cookie_policy, "accept-policy", int) + +#undef EXPOSE_SOUP_COOKIE_JAR_SETTINGS + +#define EXPOSE_WEBKIT_VIEW_VIEW_SETTINGS(SYM, PROPERTY, TYPE) \ +static void set_##SYM(TYPE val) { \ + g_object_set(uzbl.gui.web_view, (PROPERTY), val, NULL); \ +} \ +static TYPE get_##SYM() { \ + TYPE val; \ + g_object_get(uzbl.gui.web_view, (PROPERTY), &val, NULL); \ + return val; \ +} + +EXPOSE_WEBKIT_VIEW_VIEW_SETTINGS(editable, "editable", int) +EXPOSE_WEBKIT_VIEW_VIEW_SETTINGS(transparent, "transparent", int) + +#undef EXPOSE_WEBKIT_VIEW_SETTINGS + #define EXPOSE_WEBKIT_VIEW_SETTINGS(SYM, PROPERTY, TYPE) \ -void set_##SYM(TYPE val) { \ +static void set_##SYM(TYPE val) { \ g_object_set(view_settings(), (PROPERTY), val, NULL); \ } \ -TYPE get_##SYM() { \ +static TYPE get_##SYM() { \ TYPE val; \ g_object_get(view_settings(), (PROPERTY), &val, NULL); \ return val; \ } -EXPOSE_WEBKIT_VIEW_SETTINGS(default_font_family, "default-font-family", gchar *) -EXPOSE_WEBKIT_VIEW_SETTINGS(monospace_font_family, "monospace-font-family", gchar *) -EXPOSE_WEBKIT_VIEW_SETTINGS(sans_serif_font_family, "sans_serif-font-family", gchar *) -EXPOSE_WEBKIT_VIEW_SETTINGS(serif_font_family, "serif-font-family", gchar *) -EXPOSE_WEBKIT_VIEW_SETTINGS(cursive_font_family, "cursive-font-family", gchar *) -EXPOSE_WEBKIT_VIEW_SETTINGS(fantasy_font_family, "fantasy-font-family", gchar *) +/* Font settings */ +EXPOSE_WEBKIT_VIEW_SETTINGS(default_font_family, "default-font-family", gchar *) +EXPOSE_WEBKIT_VIEW_SETTINGS(monospace_font_family, "monospace-font-family", gchar *) +EXPOSE_WEBKIT_VIEW_SETTINGS(sans_serif_font_family, "sans_serif-font-family", gchar *) +EXPOSE_WEBKIT_VIEW_SETTINGS(serif_font_family, "serif-font-family", gchar *) +EXPOSE_WEBKIT_VIEW_SETTINGS(cursive_font_family, "cursive-font-family", gchar *) +EXPOSE_WEBKIT_VIEW_SETTINGS(fantasy_font_family, "fantasy-font-family", gchar *) + +/* Font size settings */ +EXPOSE_WEBKIT_VIEW_SETTINGS(minimum_font_size, "minimum-font-size", int) +EXPOSE_WEBKIT_VIEW_SETTINGS(font_size, "default-font-size", int) +EXPOSE_WEBKIT_VIEW_SETTINGS(monospace_size, "default-monospace-font-size", int) + +/* Text settings */ +EXPOSE_WEBKIT_VIEW_SETTINGS(default_encoding, "default-encoding", gchar *) +EXPOSE_WEBKIT_VIEW_SETTINGS(enforce_96_dpi, "enforce-96-dpi", int) + +/* Feature settings */ +EXPOSE_WEBKIT_VIEW_SETTINGS(enable_plugins, "enable-plugins", int) +EXPOSE_WEBKIT_VIEW_SETTINGS(enable_java_applet, "enable-java-applet", int) +#if WEBKIT_CHECK_VERSION (1, 3, 14) +EXPOSE_WEBKIT_VIEW_SETTINGS(enable_webgl, "enable-webgl", int) +#endif +#if WEBKIT_CHECK_VERSION (1, 7, 5) +EXPOSE_WEBKIT_VIEW_SETTINGS(enable_webaudio, "enable-webaudio", int) +#endif +#if WEBKIT_CHECK_VERSION (1, 7, 90) // Documentation says 1.7.5, but it's not there. +EXPOSE_WEBKIT_VIEW_SETTINGS(enable_3d_acceleration, "enable-accelerated-compositing", int) +#endif +#if WEBKIT_CHECK_VERSION (1, 11, 1) +EXPOSE_WEBKIT_VIEW_SETTINGS(enable_css_shaders, "enable-css-shaders", int) +#endif -EXPOSE_WEBKIT_VIEW_SETTINGS(minimum_font_size, "minimum-font-size", int) -EXPOSE_WEBKIT_VIEW_SETTINGS(font_size, "default-font-size", int) -EXPOSE_WEBKIT_VIEW_SETTINGS(monospace_size, "default-monospace-font-size", int) +/* HTML5 Database settings */ +EXPOSE_WEBKIT_VIEW_SETTINGS(enable_database, "enable-html5-database", int) +EXPOSE_WEBKIT_VIEW_SETTINGS(enable_local_storage, "enable-html5-local-storage", int) +EXPOSE_WEBKIT_VIEW_SETTINGS(enable_pagecache, "enable-page-cache", int) +EXPOSE_WEBKIT_VIEW_SETTINGS(enable_offline_app_cache, "enable-offline-web-application-cache", int) +#if WEBKIT_CHECK_VERSION (1, 5, 2) +EXPOSE_WEBKIT_VIEW_SETTINGS(local_storage_path, "html5-local-storage-database-path", gchar *) +#endif -EXPOSE_WEBKIT_VIEW_SETTINGS(enable_plugins, "enable-plugins", int) -EXPOSE_WEBKIT_VIEW_SETTINGS(enable_scripts, "enable-scripts", int) +/* Security settings */ +EXPOSE_WEBKIT_VIEW_SETTINGS(enable_private_webkit, "enable-private-browsing", int) +EXPOSE_WEBKIT_VIEW_SETTINGS(enable_universal_file_access, "enable-universal-access-from-file-uris", int) +EXPOSE_WEBKIT_VIEW_SETTINGS(enable_cross_file_access, "enable-file-access-from-file-uris", int) +EXPOSE_WEBKIT_VIEW_SETTINGS(enable_hyperlink_auditing, "enable-hyperlink-auditing", int) +#if WEBKIT_CHECK_VERSION (1, 3, 13) +EXPOSE_WEBKIT_VIEW_SETTINGS(enable_dns_prefetch, "enable-dns-prefetching", int) +#endif +#if WEBKIT_CHECK_VERSION (1, 11, 2) +EXPOSE_WEBKIT_VIEW_SETTINGS(display_insecure_content, "enable-display-of-insecure-content", int) +EXPOSE_WEBKIT_VIEW_SETTINGS(run_insecure_content, "enable-running-of-insecure-content", int) +#endif -EXPOSE_WEBKIT_VIEW_SETTINGS(javascript_windows, "javascript-can-open-windows-automatically", int) +/* Display settings */ +EXPOSE_WEBKIT_VIEW_SETTINGS(zoom_step, "zoom-step", float) +EXPOSE_WEBKIT_VIEW_SETTINGS(caret_browsing, "enable-caret-browsing", int) +EXPOSE_WEBKIT_VIEW_SETTINGS(auto_resize_window, "auto-resize-window", int) +#if WEBKIT_CHECK_VERSION (1, 3, 5) +EXPOSE_WEBKIT_VIEW_SETTINGS(enable_frame_flattening, "enable-frame-flattening", int) +#endif +#if WEBKIT_CHECK_VERSION (1, 3, 8) +EXPOSE_WEBKIT_VIEW_SETTINGS(enable_fullscreen, "enable-fullscreen", int) +#endif +#ifdef USE_WEBKIT2 +#if WEBKIT_CHECK_VERSION (1, 7, 91) +EXPOSE_WEBKIT_VIEW_SETTINGS(zoom_text_only, "zoom-text-only", int) +#endif +#endif +#if WEBKIT_CHECK_VERSION (1, 9, 0) +EXPOSE_WEBKIT_VIEW_SETTINGS(enable_smooth_scrolling, "enable-smooth-scrolling", int) +#endif -EXPOSE_WEBKIT_VIEW_SETTINGS(autoload_images, "auto-load-images", int) -EXPOSE_WEBKIT_VIEW_SETTINGS(autoshrink_images, "auto-shrink-images", int) +/* Javascript settings */ +EXPOSE_WEBKIT_VIEW_SETTINGS(enable_scripts, "enable-scripts", int) +EXPOSE_WEBKIT_VIEW_SETTINGS(javascript_windows, "javascript-can-open-windows-automatically", int) +EXPOSE_WEBKIT_VIEW_SETTINGS(javascript_dom_paste, "enable-dom-paste", int) +#if WEBKIT_CHECK_VERSION (1, 3, 0) +EXPOSE_WEBKIT_VIEW_SETTINGS(javascript_clipboard, "javascript-can-access-clipboard", int) +#endif -EXPOSE_WEBKIT_VIEW_SETTINGS(enable_pagecache, "enable-page-cache", int) -EXPOSE_WEBKIT_VIEW_SETTINGS(enable_private, "enable-private-browsing", int) +/* Media settings */ +#if WEBKIT_CHECK_VERSION (1, 9, 3) +EXPOSE_WEBKIT_VIEW_SETTINGS(enable_inline_media, "media-playback-allows-inline", int) +EXPOSE_WEBKIT_VIEW_SETTINGS(require_click_to_play, "media-playback-requires-user-gesture", int) +#endif +#if WEBKIT_CHECK_VERSION (1, 11, 1) +EXPOSE_WEBKIT_VIEW_SETTINGS(enable_media_stream, "enable-media-stream", int) +#endif -EXPOSE_WEBKIT_VIEW_SETTINGS(enable_spellcheck, "enable-spell-checking", int) -EXPOSE_WEBKIT_VIEW_SETTINGS(spellcheck_languages, "spell-checking-languages", gchar *) -EXPOSE_WEBKIT_VIEW_SETTINGS(resizable_text_areas, "resizable-text-areas", int) +/* Image settings */ +EXPOSE_WEBKIT_VIEW_SETTINGS(autoload_images, "auto-load-images", int) +EXPOSE_WEBKIT_VIEW_SETTINGS(autoshrink_images, "auto-shrink-images", int) -EXPOSE_WEBKIT_VIEW_SETTINGS(stylesheet_uri, "user-stylesheet-uri", gchar *) -EXPOSE_WEBKIT_VIEW_SETTINGS(print_bg, "print-backgrounds", int) -EXPOSE_WEBKIT_VIEW_SETTINGS(enforce_96_dpi, "enforce-96-dpi", int) +/* Spell checking settings */ +EXPOSE_WEBKIT_VIEW_SETTINGS(enable_spellcheck, "enable-spell-checking", int) -EXPOSE_WEBKIT_VIEW_SETTINGS(caret_browsing, "enable-caret-browsing", int) +/* Form settings */ +EXPOSE_WEBKIT_VIEW_SETTINGS(resizable_text_areas, "resizable-text-areas", int) +EXPOSE_WEBKIT_VIEW_SETTINGS(enable_spatial_navigation, "enable-spatial-navigation", int) +EXPOSE_WEBKIT_VIEW_SETTINGS(editing_behavior, "editing-behavior", int) +EXPOSE_WEBKIT_VIEW_SETTINGS(enable_tab_cycle, "tab-key-cycles-through-elements", int) -EXPOSE_WEBKIT_VIEW_SETTINGS(enable_cross_file_access, "enable-file-access-from-file-uris", int) +/* Customization */ +EXPOSE_WEBKIT_VIEW_SETTINGS(stylesheet_uri, "user-stylesheet-uri", gchar *) +#if !WEBKIT_CHECK_VERSION (1, 9, 0) +EXPOSE_WEBKIT_VIEW_SETTINGS(default_context_menu, "enable-default-context-menu", int) +#endif -EXPOSE_WEBKIT_VIEW_SETTINGS(default_encoding, "default-encoding", gchar *) +/* Hacks */ +EXPOSE_WEBKIT_VIEW_SETTINGS(enable_site_workarounds, "enable-site-specific-quirks", int) -void +/* Printing settings */ +EXPOSE_WEBKIT_VIEW_SETTINGS(print_bg, "print-backgrounds", int) + +#undef EXPOSE_WEBKIT_VIEW_SETTINGS + +static void +set_maintain_history (int maintain) { + uzbl.behave.maintain_history = maintain; + + webkit_web_view_set_maintains_back_forward_list (uzbl.gui.web_view, maintain); +} + +static int +get_maintain_history () { + return uzbl.behave.maintain_history; +} + +static void +set_spellcheck_languages(const gchar *languages) { + GObject *obj = webkit_get_text_checker (); + + if (!obj) { + return; + } + if (!WEBKIT_IS_SPELL_CHECKER (obj)) { + return; + } + + WebKitSpellChecker *checker = WEBKIT_SPELL_CHECKER (obj); + + webkit_spell_checker_update_spell_checking_languages (checker, languages); + g_object_set(view_settings(), "spell-checking-languages", languages, NULL); +} + +static gchar * +get_spellcheck_languages() { + gchar *val; + g_object_get(view_settings(), "spell-checking-languages", &val, NULL); + return val; +} + +static void +set_enable_private (int private) { + const char *priv_envvar = "UZBL_PRIVATE"; + + if (private) + setenv (priv_envvar, "true", 1); + else + unsetenv (priv_envvar); + + set_enable_private_webkit (private); +} + +static int +get_enable_private () { + return get_enable_private_webkit (); +} + +static void set_proxy_url(const gchar *proxy_url) { g_free(uzbl.net.proxy_url); uzbl.net.proxy_url = g_strdup(proxy_url); @@ -459,28 +689,45 @@ set_proxy_url(const gchar *proxy_url) { soup_uri_free(soup_uri); } -void -set_authentication_handler(const gchar *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); - - g_free(uzbl.behave.authentication_handler); - uzbl.behave.authentication_handler = g_strdup(handler); - - if (uzbl.behave.authentication_handler == NULL || *uzbl.behave.authentication_handler == 0) { - if (!feature_is_set) - soup_session_add_feature_by_type - (uzbl.net.soup_session, (GType) WEBKIT_TYPE_SOUP_AUTH_DIALOG); +/** + * Check if the webkit auth dialog is enabled for the soup session + */ +int +get_enable_builtin_auth () { + SoupSessionFeature *auth = soup_session_get_feature ( + uzbl.net.soup_session, + (GType) WEBKIT_TYPE_SOUP_AUTH_DIALOG + ); + + return auth != NULL; +} + +/** + * Enable/Disable the webkit auth dialog for the soup session + */ +static void +set_enable_builtin_auth (int enabled) { + SoupSessionFeature *auth = soup_session_get_feature ( + uzbl.net.soup_session, + (GType) WEBKIT_TYPE_SOUP_AUTH_DIALOG + ); + + if (enabled > 0) { + if (auth == NULL) { + 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); + if (auth != NULL) { + soup_session_remove_feature (uzbl.net.soup_session, auth); + } } + } -void +static void set_status_background(const gchar *background) { /* 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 @@ -501,7 +748,7 @@ set_status_background(const gchar *background) { #endif } -void +static void set_icon(const gchar *icon) { if(file_exists(icon) && uzbl.gui.main_window) { g_free(uzbl.gui.icon); @@ -513,7 +760,7 @@ set_icon(const gchar *icon) { } } -void +static void set_window_role(const gchar *role) { if (!uzbl.gui.main_window) return; @@ -521,7 +768,7 @@ set_window_role(const gchar *role) { gtk_window_set_role(GTK_WINDOW (uzbl.gui.main_window), role); } -gchar * +static gchar * get_window_role() { if (!uzbl.gui.main_window) return NULL; @@ -585,7 +832,7 @@ get_show_status() { return gtk_widget_get_visible(uzbl.gui.status_bar); } -void +static void set_status_top(int status_top) { if (!uzbl.gui.scrolled_win && !uzbl.gui.status_bar) return; @@ -612,21 +859,27 @@ set_status_top(int status_top) { gtk_widget_grab_focus (GTK_WIDGET (uzbl.gui.web_view)); } -void -set_current_encoding(const gchar *encoding) { +static void +set_custom_encoding(const gchar *encoding) { if(strlen(encoding) == 0) encoding = NULL; webkit_web_view_set_custom_encoding(uzbl.gui.web_view, encoding); } -gchar * -get_current_encoding() { +static gchar * +get_custom_encoding() { const gchar *encoding = webkit_web_view_get_custom_encoding(uzbl.gui.web_view); return g_strdup(encoding); } -void +static gchar * +get_current_encoding() { + const gchar *encoding = webkit_web_view_get_encoding (uzbl.gui.web_view); + return g_strdup(encoding); +} + +static void set_fifo_dir(const gchar *fifo_dir) { g_free(uzbl.behave.fifo_dir); @@ -636,7 +889,7 @@ set_fifo_dir(const gchar *fifo_dir) { uzbl.behave.fifo_dir = NULL; } -void +static void set_socket_dir(const gchar *socket_dir) { g_free(uzbl.behave.socket_dir); @@ -646,26 +899,38 @@ set_socket_dir(const gchar *socket_dir) { uzbl.behave.socket_dir = NULL; } -void +#ifdef USE_WEBKIT2 +static void +set_inject_text(const gchar *text) { + webkit_web_view_load_plain_text (uzbl.gui.web_view, html, NULL); +} +#endif + +static void set_inject_html(const gchar *html) { +#ifdef USE_WEBKIT2 + webkit_web_view_load_html (uzbl.gui.web_view, html, NULL); +#else webkit_web_view_load_html_string (uzbl.gui.web_view, html, NULL); +#endif } -void +static void set_useragent(const gchar *useragent) { g_free(uzbl.net.useragent); - if (*useragent == ' ') { + if (!useragent || !*useragent) { uzbl.net.useragent = NULL; } else { uzbl.net.useragent = g_strdup(useragent); g_object_set(G_OBJECT(uzbl.net.soup_session), SOUP_SESSION_USER_AGENT, uzbl.net.useragent, NULL); + g_object_set(view_settings(), "user-agent", uzbl.net.useragent, NULL); } } -void +static void set_accept_languages(const gchar *accept_languages) { g_free(uzbl.net.accept_languages); @@ -679,8 +944,7 @@ set_accept_languages(const gchar *accept_languages) { } } -/* requires webkit >=1.1.14 */ -void +static void set_view_source(int view_source) { uzbl.behave.view_source = view_source; @@ -688,6 +952,7 @@ set_view_source(int view_source) { (gboolean) uzbl.behave.view_source); } +#ifndef USE_WEBKIT2 void set_zoom_type (int type) { webkit_web_view_set_full_content_zoom (uzbl.gui.web_view, type); @@ -697,17 +962,207 @@ int get_zoom_type () { return webkit_web_view_get_full_content_zoom (uzbl.gui.web_view); } +#endif -void +static void set_zoom_level(float zoom_level) { webkit_web_view_set_zoom_level (uzbl.gui.web_view, zoom_level); } -float +static float get_zoom_level() { return webkit_web_view_get_zoom_level (uzbl.gui.web_view); } +static gchar * +get_cache_model() { + WebKitCacheModel model = webkit_get_cache_model (); + + switch (model) { + case WEBKIT_CACHE_MODEL_DOCUMENT_VIEWER: + return g_strdup("document_viewer"); + case WEBKIT_CACHE_MODEL_WEB_BROWSER: + return g_strdup("web_browser"); + case WEBKIT_CACHE_MODEL_DOCUMENT_BROWSER: + return g_strdup("document_browser"); + default: + return g_strdup("unknown"); + } +} + +static void +set_cache_model(const gchar *model) { + if (!g_strcmp0 (model, "default")) { + webkit_set_cache_model (WEBKIT_CACHE_MODEL_DEFAULT); + } else if (!g_strcmp0 (model, "document_viewer")) { + webkit_set_cache_model (WEBKIT_CACHE_MODEL_DOCUMENT_VIEWER); + } else if (!g_strcmp0 (model, "web_browser")) { + webkit_set_cache_model (WEBKIT_CACHE_MODEL_WEB_BROWSER); + } else if (!g_strcmp0 (model, "document_browser")) { + webkit_set_cache_model (WEBKIT_CACHE_MODEL_DOCUMENT_BROWSER); + } +} + +static gchar * +get_web_database_directory() { + return g_strdup (webkit_get_web_database_directory_path ()); +} + +static unsigned long long +get_web_database_quota () { + return webkit_get_default_web_database_quota (); +} + +static void +set_web_database_quota (unsigned long long quota) { + webkit_set_default_web_database_quota (quota); +} + +static void +set_web_database_directory(const gchar *path) { + webkit_set_web_database_directory_path (path); +} + +#if WEBKIT_CHECK_VERSION (1, 3, 4) +static gchar * +get_view_mode() { + WebKitWebViewViewMode mode = webkit_web_view_get_view_mode (uzbl.gui.web_view); + + switch (mode) { + case WEBKIT_WEB_VIEW_VIEW_MODE_WINDOWED: + return g_strdup("windowed"); + case WEBKIT_WEB_VIEW_VIEW_MODE_FLOATING: + return g_strdup("floating"); + case WEBKIT_WEB_VIEW_VIEW_MODE_FULLSCREEN: + return g_strdup("fullscreen"); + case WEBKIT_WEB_VIEW_VIEW_MODE_MAXIMIZED: + return g_strdup("maximized"); + case WEBKIT_WEB_VIEW_VIEW_MODE_MINIMIZED: + return g_strdup("minimized"); + default: + return g_strdup("unknown"); + } +} + +static void +set_view_mode(const gchar *mode) { + if (!g_strcmp0 (mode, "windowed")) { + webkit_web_view_set_view_mode (uzbl.gui.web_view, WEBKIT_WEB_VIEW_VIEW_MODE_WINDOWED); + } else if (!g_strcmp0 (mode, "floating")) { + webkit_web_view_set_view_mode (uzbl.gui.web_view, WEBKIT_WEB_VIEW_VIEW_MODE_FLOATING); + } else if (!g_strcmp0 (mode, "fullscreen")) { + webkit_web_view_set_view_mode (uzbl.gui.web_view, WEBKIT_WEB_VIEW_VIEW_MODE_FULLSCREEN); + } else if (!g_strcmp0 (mode, "maximized")) { + webkit_web_view_set_view_mode (uzbl.gui.web_view, WEBKIT_WEB_VIEW_VIEW_MODE_MAXIMIZED); + } else if (!g_strcmp0 (mode, "minimized")) { + webkit_web_view_set_view_mode (uzbl.gui.web_view, WEBKIT_WEB_VIEW_VIEW_MODE_MINIMIZED); + } +} +#endif + +#if WEBKIT_CHECK_VERSION (1, 3, 17) +static gchar * +get_inspected_uri() { + return g_strdup (webkit_web_inspector_get_inspected_uri (uzbl.gui.inspector)); +} +#endif + +#if WEBKIT_CHECK_VERSION (1, 3, 13) +static gchar * +get_app_cache_directory() { + return g_strdup (webkit_application_cache_get_database_directory_path ()); +} + +static unsigned long long +get_app_cache_size() { + return webkit_application_cache_get_maximum_size (); +} + +static void +set_app_cache_size(unsigned long long size) { + webkit_application_cache_set_maximum_size (size); +} +#endif + +#if WEBKIT_CHECK_VERSION (1, 3, 8) +static void +mimetype_list_append(WebKitWebPluginMIMEType *mimetype, GString *list) { + if (*list->str != '[') { + g_string_append_c (list, ','); + } + + /* Write out a JSON representation of the information */ + g_string_append_printf (list, + "{\"name\": \"%s\"," + "\"description\": \"%s\"," + "\"extensions\": [", /* Open array for the extensions */ + mimetype->name, + mimetype->description); + + char **extension = mimetype->extensions; + gboolean first = TRUE; + + while (extension) { + if (first) { + first = FALSE; + } else { + g_string_append_c (list, ','); + } + g_string_append (list, *extension); + + ++extension; + } + + g_string_append_c (list, '}'); +} + +static void +plugin_list_append(WebKitWebPlugin *plugin, GString *list) { + if (*list->str != '[') { + g_string_append_c (list, ','); + } + + const gchar *desc = webkit_web_plugin_get_description (plugin); + gboolean enabled = webkit_web_plugin_get_enabled (plugin); + GSList *mimetypes = webkit_web_plugin_get_mimetypes (plugin); + const gchar *name = webkit_web_plugin_get_name (plugin); + const gchar *path = webkit_web_plugin_get_path (plugin); + + /* Write out a JSON representation of the information */ + g_string_append_printf (list, + "{\"name\": \"%s\"," + "\"description\": \"%s\"," + "\"enabled\": %s," + "\"path\": \"%s\"," + "\"mimetypes\": [", /* Open array for the mimetypes */ + name, + desc, + enabled ? "true" : "false", + path); + + g_slist_foreach (mimetypes, (GFunc)mimetype_list_append, list); + + /* Close the array and the object */ + g_string_append (list, "]}"); +} + +static gchar * +get_plugin_list() { + WebKitWebPluginDatabase *db = webkit_get_web_plugin_database (); + GSList *plugins = webkit_web_plugin_database_get_plugins (db); + + GString *list = g_string_new ("["); + + g_slist_foreach (plugins, (GFunc)plugin_list_append, list); + + g_string_append_c (list, ']'); + + webkit_web_plugin_database_plugins_list_free (plugins); + + return g_string_free (list, FALSE); +} +#endif + /* abbreviations to help keep the table's width humane */ /* variables */ @@ -717,6 +1172,7 @@ get_zoom_level() { #define PTR_V_STR_GETSET(var) { .type = TYPE_STR, .dump = 1, .writeable = 1, .getter = (uzbl_fp) get_##var, .setter = (uzbl_fp)set_##var } #define PTR_V_INT_GETSET(var) { .type = TYPE_INT, .dump = 1, .writeable = 1, .getter = (uzbl_fp) get_##var, .setter = (uzbl_fp)set_##var } +#define PTR_V_ULL_GETSET(var) { .type = TYPE_ULL, .dump = 1, .writeable = 1, .getter = (uzbl_fp) get_##var, .setter = (uzbl_fp)set_##var } #define PTR_V_FLOAT_GETSET(var) { .type = TYPE_FLOAT, .dump = 1, .writeable = 1, .getter = (uzbl_fp) get_##var, .setter = (uzbl_fp)set_##var } /* constants */ @@ -724,6 +1180,11 @@ get_zoom_level() { #define PTR_C_INT(var) { .ptr = { .i = (int*)&(var) }, .type = TYPE_INT, .dump = 0, .writeable = 0, .getter = NULL, .setter = NULL } #define PTR_C_FLOAT(var) { .ptr = { .f = &(var) }, .type = TYPE_FLOAT, .dump = 0, .writeable = 0, .getter = NULL, .setter = NULL } +/* programmatic constants */ +#define PTR_C_STR_F(get) { .type = TYPE_STR, .dump = 0, .writeable = 0, .getter = (uzbl_fp)get, .setter = NULL } +#define PTR_C_INT_F(get) { .type = TYPE_INT, .dump = 0, .writeable = 0, .getter = (uzbl_fp)get, .setter = NULL } +#define PTR_C_FLOAT_F(get) { .type = TYPE_FLOAT, .dump = 0, .writeable = 0, .getter = (uzbl_fp)get, .setter = NULL } + const struct var_name_to_ptr_t { const char *name; uzbl_cmdprop cp; @@ -751,7 +1212,6 @@ const struct var_name_to_ptr_t { { "forward_keys", PTR_V_INT(uzbl.behave.forward_keys, 1, NULL)}, - { "authentication_handler", PTR_V_STR(uzbl.behave.authentication_handler, 1, set_authentication_handler)}, { "scheme_handler", PTR_V_STR(uzbl.behave.scheme_handler, 1, NULL)}, { "request_handler", PTR_V_STR(uzbl.behave.request_handler, 1, NULL)}, { "download_handler", PTR_V_STR(uzbl.behave.download_handler, 1, NULL)}, @@ -773,45 +1233,145 @@ const struct var_name_to_ptr_t { { "ssl_ca_file", PTR_V_STR_GETSET(ca_file)}, { "ssl_verify", PTR_V_INT_GETSET(verify_cert)}, - /* exported WebKitWebSettings properties */ - { "javascript_windows", PTR_V_INT_GETSET(javascript_windows)}, - { "zoom_level", PTR_V_FLOAT_GETSET(zoom_level)}, - { "zoom_type", PTR_V_INT_GETSET(zoom_type)}, + { "enable_builtin_auth", PTR_V_INT_GETSET(enable_builtin_auth)}, + { "cache_model", PTR_V_STR_GETSET(cache_model)}, +#if WEBKIT_CHECK_VERSION (1, 3, 13) + { "app_cache_size", PTR_V_ULL_GETSET(app_cache_size)}, +#endif + { "web_database_directory", PTR_V_STR_GETSET(web_database_directory)}, + { "web_database_quota", PTR_V_ULL_GETSET(web_database_quota)}, + /* exported WebKitWebSettings properties */ + /* Font settings */ { "default_font_family", PTR_V_STR_GETSET(default_font_family)}, { "monospace_font_family", PTR_V_STR_GETSET(monospace_font_family)}, - { "cursive_font_family", PTR_V_STR_GETSET(cursive_font_family)}, { "sans_serif_font_family", PTR_V_STR_GETSET(sans_serif_font_family)}, { "serif_font_family", PTR_V_STR_GETSET(serif_font_family)}, + { "cursive_font_family", PTR_V_STR_GETSET(cursive_font_family)}, { "fantasy_font_family", PTR_V_STR_GETSET(fantasy_font_family)}, - - { "monospace_size", PTR_V_INT_GETSET(monospace_size)}, - { "font_size", PTR_V_INT_GETSET(font_size)}, + /* Font size settings */ { "minimum_font_size", PTR_V_INT_GETSET(minimum_font_size)}, - - { "enable_pagecache", PTR_V_INT_GETSET(enable_pagecache)}, + { "font_size", PTR_V_INT_GETSET(font_size)}, + { "monospace_size", PTR_V_INT_GETSET(monospace_size)}, + /* Text settings */ + { "default_encoding", PTR_V_STR_GETSET(default_encoding)}, + { "custom_encoding", PTR_V_STR_GETSET(custom_encoding)}, + { "enforce_96_dpi", PTR_V_INT_GETSET(enforce_96_dpi)}, + { "editable", PTR_V_INT_GETSET(editable)}, + /* Feature settings */ { "enable_plugins", PTR_V_INT_GETSET(enable_plugins)}, + { "enable_java_applet", PTR_V_INT_GETSET(enable_java_applet)}, +#if WEBKIT_CHECK_VERSION (1, 3, 14) + { "enable_webgl", PTR_V_INT_GETSET(enable_webgl)}, +#endif +#if WEBKIT_CHECK_VERSION (1, 7, 5) + { "enable_webaudio", PTR_V_INT_GETSET(enable_webaudio)}, +#endif +#if WEBKIT_CHECK_VERSION (1, 7, 90) /* Documentation says 1.7.5, but it's not there. */ + { "enable_3d_acceleration", PTR_V_INT_GETSET(enable_3d_acceleration)}, +#endif +#if WEBKIT_CHECK_VERSION (1, 11, 1) + { "enable_css_shaders", PTR_V_INT_GETSET(enable_css_shaders)}, +#endif + /* HTML5 Database settings */ + { "enable_database", PTR_V_INT_GETSET(enable_database)}, + { "enable_local_storage", PTR_V_INT_GETSET(enable_local_storage)}, + { "enable_pagecache", PTR_V_INT_GETSET(enable_pagecache)}, + { "enable_offline_app_cache", PTR_V_INT_GETSET(enable_offline_app_cache)}, +#if WEBKIT_CHECK_VERSION (1, 5, 2) + { "local_storage_path", PTR_V_STR_GETSET(local_storage_path)}, +#endif + /* Security settings */ + { "enable_private", PTR_V_INT_GETSET(enable_private)}, + { "enable_universal_file_access", PTR_V_INT_GETSET(enable_universal_file_access)}, + { "enable_cross_file_access", PTR_V_INT_GETSET(enable_cross_file_access)}, + { "enable_hyperlink_auditing", PTR_V_INT_GETSET(enable_hyperlink_auditing)}, + { "cookie_policy", PTR_V_INT_GETSET(cookie_policy)}, +#if WEBKIT_CHECK_VERSION (1, 3, 13) + { "enable_dns_prefetch", PTR_V_INT_GETSET(enable_dns_prefetch)}, +#endif +#if WEBKIT_CHECK_VERSION (1, 11, 2) + { "display_insecure_content",PTR_V_INT_GETSET(display_insecure_content)}, + { "run_insecure_content", PTR_V_INT_GETSET(run_insecure_content)}, +#endif + { "maintain_history", PTR_V_INT_GETSET(maintain_history)}, + /* Display settings */ + { "transparent", PTR_V_STR_GETSET(transparent)}, +#if WEBKIT_CHECK_VERSION (1, 3, 4) + { "view_mode", PTR_V_STR_GETSET(view_mode)}, +#endif + { "zoom_level", PTR_V_FLOAT_GETSET(zoom_level)}, + { "zoom_step", PTR_V_FLOAT_GETSET(zoom_step)}, +#ifndef USE_WEBKIT2 + { "zoom_type", PTR_V_INT_GETSET(zoom_type)}, +#endif + { "caret_browsing", PTR_V_INT_GETSET(caret_browsing)}, + { "auto_resize_window", PTR_V_INT_GETSET(auto_resize_window)}, +#if WEBKIT_CHECK_VERSION (1, 3, 5) + { "enable_frame_flattening", PTR_V_INT_GETSET(enable_frame_flattening)}, +#endif +#if WEBKIT_CHECK_VERSION (1, 3, 8) + { "enable_fullscreen", PTR_V_INT_GETSET(enable_fullscreen)}, +#endif +#ifdef USE_WEBKIT2 +#if WEBKIT_CHECK_VERSION (1, 7, 91) + { "zoom_text_only", PTR_V_INT_GETSET(zoom_text_only)}, +#endif +#endif +#if WEBKIT_CHECK_VERSION (1, 9, 0) + { "enable_smooth_scrolling",PTR_V_INT_GETSET(enable_smooth_scrolling)}, +#endif + /* Javascript settings */ { "enable_scripts", PTR_V_INT_GETSET(enable_scripts)}, + { "javascript_windows", PTR_V_INT_GETSET(javascript_windows)}, + { "javascript_dom_paste", PTR_V_INT_GETSET(javascript_dom_paste)}, +#if WEBKIT_CHECK_VERSION (1, 3, 0) + { "javascript_clipboard", PTR_V_INT_GETSET(javascript_clipboard)}, +#endif + /* Media settings */ +#if WEBKIT_CHECK_VERSION (1, 9, 3) + { "enable_inline_media", PTR_V_INT_GETSET(enable_inline_media)}, + { "require_click_to_play", PTR_V_INT_GETSET(require_click_to_play)}, +#endif +#if WEBKIT_CHECK_VERSION (1, 11, 1) + { "enable_media_stream", PTR_V_INT_GETSET(enable_media_stream)}, +#endif + /* Image settings */ { "autoload_images", PTR_V_INT_GETSET(autoload_images)}, { "autoshrink_images", PTR_V_INT_GETSET(autoshrink_images)}, + /* Spell checking settings */ { "enable_spellcheck", PTR_V_INT_GETSET(enable_spellcheck)}, { "spellcheck_languages", PTR_V_STR_GETSET(spellcheck_languages)}, - { "enable_private", PTR_V_INT_GETSET(enable_private)}, - { "print_backgrounds", PTR_V_INT_GETSET(print_bg)}, - { "stylesheet_uri", PTR_V_STR_GETSET(stylesheet_uri)}, + /* Form settings */ { "resizable_text_areas", PTR_V_INT_GETSET(resizable_text_areas)}, - { "default_encoding", PTR_V_STR_GETSET(default_encoding)}, - { "current_encoding", PTR_V_STR_GETSET(current_encoding)}, - { "enforce_96_dpi", PTR_V_INT_GETSET(enforce_96_dpi)}, - { "caret_browsing", PTR_V_INT_GETSET(caret_browsing)}, - { "enable_cross_file_access", PTR_V_INT_GETSET(enable_cross_file_access)}, + { "enable_spatial_navigation", PTR_V_INT_GETSET(enable_spatial_navigation)}, + { "editing_behavior", PTR_V_INT_GETSET(editing_behavior)}, + { "enable_tab_cycle", PTR_V_INT_GETSET(enable_tab_cycle)}, + /* Customization */ + { "stylesheet_uri", PTR_V_STR_GETSET(stylesheet_uri)}, +#if WEBKIT_CHECK_VERSION (1, 9, 0) + { "default_context_menu", PTR_V_INT(uzbl.gui.custom_context_menu, 1, NULL)}, +#else + { "default_context_menu", PTR_V_INT_GETSET(default_context_menu)}, +#endif + /* Hacks */ + { "enable_site_workarounds", PTR_V_INT_GETSET(enable_site_workarounds)}, + /* Printing settings */ + { "print_backgrounds", PTR_V_INT_GETSET(print_bg)}, + /* Inspector settings */ + { "profile_js", PTR_V_INT_GETSET(profile_js)}, + { "profile_timeline", PTR_V_INT_GETSET(profile_timeline)}, +#ifdef USE_WEBKIT2 + { "inject_text", { .type = TYPE_STR, .dump = 0, .writeable = 1, .getter = NULL, .setter = (uzbl_fp) set_inject_text }}, +#endif { "inject_html", { .type = TYPE_STR, .dump = 0, .writeable = 1, .getter = NULL, .setter = (uzbl_fp) set_inject_html }}, /* constants (not dumpable or writeable) */ { "WEBKIT_MAJOR", PTR_C_INT(uzbl.info.webkit_major)}, { "WEBKIT_MINOR", PTR_C_INT(uzbl.info.webkit_minor)}, { "WEBKIT_MICRO", PTR_C_INT(uzbl.info.webkit_micro)}, + { "HAS_WEBKIT2", PTR_C_INT(uzbl.info.webkit2)}, { "ARCH_UZBL", PTR_C_STR(uzbl.info.arch)}, { "COMMIT", PTR_C_STR(uzbl.info.commit)}, { "TITLE", PTR_C_STR(uzbl.gui.main_title)}, @@ -820,6 +1380,18 @@ const struct var_name_to_ptr_t { { "PID", PTR_C_STR(uzbl.info.pid_str)}, { "_", PTR_C_STR(uzbl.state.last_result)}, + /* runtime settings */ + { "current_encoding", PTR_C_STR_F(get_current_encoding)}, +#if WEBKIT_CHECK_VERSION (1, 3, 17) + { "inspected_uri", PTR_C_STR_F(get_inspected_uri)}, +#endif +#if WEBKIT_CHECK_VERSION (1, 3, 13) + { "app_cache_directory", PTR_C_STR_F(get_app_cache_directory)}, +#endif +#if WEBKIT_CHECK_VERSION (1, 3, 8) + { "plugin_list", PTR_C_STR_F(get_plugin_list)}, +#endif + /* and we terminate the whole thing with the closest thing we have to NULL. * it's important that dump = 0. */ { NULL, {.ptr = { .i = NULL }, .type = TYPE_INT, .dump = 0, .writeable = 0}} diff --git a/src/variables.h b/src/variables.h index dade652..0b935eb 100644 --- a/src/variables.h +++ b/src/variables.h @@ -6,7 +6,11 @@ #define __VARIABLES__ #include +#ifdef USE_WEBKIT2 +#include +#else #include +#endif #include "type.h" @@ -20,11 +24,14 @@ gchar *get_var_value_string_c(const uzbl_cmdprop *c); gchar *get_var_value_string(const char *name); int get_var_value_int_c(const uzbl_cmdprop *c); int get_var_value_int(const char *name); +unsigned long long get_var_value_ull_c(const uzbl_cmdprop *c); +unsigned long long get_var_value_ull(const char *name); float get_var_value_float_c(const uzbl_cmdprop *c); float get_var_value_float(const char *name); void set_var_value_string_c(uzbl_cmdprop *c, const gchar *val); -void set_var_value_int_c(uzbl_cmdprop *c, int f); +void set_var_value_int_c(uzbl_cmdprop *c, int i); +void set_var_value_ull_c(uzbl_cmdprop *c, unsigned long long ull); void set_var_value_float_c(uzbl_cmdprop *c, float f); void send_set_var_event(const char *name, const uzbl_cmdprop *c); @@ -34,8 +41,6 @@ void dump_config_as_events(); void uri_change_cb (WebKitWebView *web_view, GParamSpec param_spec); -void set_show_status(int); - void set_zoom_type(int); int get_zoom_type(); diff --git a/tests/event-manager/emtest.py b/tests/event-manager/emtest.py new file mode 100644 index 0000000..27ce21b --- /dev/null +++ b/tests/event-manager/emtest.py @@ -0,0 +1,30 @@ +from mock import Mock +import logging +from uzbl.event_manager import Uzbl + + +class EventManagerMock(object): + def __init__(self, + global_plugins=(), instance_plugins=(), + global_mock_plugins=(), instance_mock_plugins=() + ): + self.uzbls = {} + self.plugins = {} + self.instance_plugins = instance_plugins + self.instance_mock_plugins = instance_mock_plugins + for plugin in global_plugins: + self.plugins[plugin] = plugin(self) + for (plugin, mock) in global_mock_plugins: + self.plugins[plugin] = mock() if mock else Mock(plugin) + + def add(self): + u = Mock(spec=Uzbl) + u.parent = self + u.logger = logging.getLogger('debug') + u.plugins = {} + for plugin in self.instance_plugins: + u.plugins[plugin] = plugin(u) + for (plugin, mock) in self.instance_mock_plugins: + u.plugins[plugin] = mock() if mock else Mock(plugin) + self.uzbls[Mock()] = u + return u diff --git a/tests/event-manager/testarguments.py b/tests/event-manager/testarguments.py new file mode 100755 index 0000000..edb102d --- /dev/null +++ b/tests/event-manager/testarguments.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python + +import unittest +from uzbl.arguments import Arguments + + +class ArgumentsTest(unittest.TestCase): + def test_empty(self): + a = Arguments('') + self.assertEqual(len(a), 0) + self.assertEqual(a.raw(), '') + + def test_space(self): + a = Arguments(' foo bar') + self.assertEqual(len(a), 2) + self.assertEqual(a.raw(), 'foo bar') + self.assertEqual(a.raw(0, 0), 'foo') + self.assertEqual(a.raw(1, 1), 'bar') + + def test_tab(self): + a = Arguments('\tfoo\t\tbar') + self.assertEqual(len(a), 2) + self.assertEqual(a.raw(), 'foo\t\tbar') + self.assertEqual(a.raw(0, 0), 'foo') + self.assertEqual(a.raw(1, 1), 'bar') diff --git a/tests/event-manager/testbind.py b/tests/event-manager/testbind.py new file mode 100644 index 0000000..a2e8d70 --- /dev/null +++ b/tests/event-manager/testbind.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python + + +import sys +if '' not in sys.path: + sys.path.insert(0, '') + +import mock +import unittest +from emtest import EventManagerMock +from uzbl.plugins.bind import Bind, BindPlugin +from uzbl.plugins.config import Config + + +def justafunction(): + pass + + +class BindTest(unittest.TestCase): + def test_unique_id(self): + a = Bind('spam', 'spam') + b = Bind('spam', 'spam') + self.assertNotEqual(a.bid, b.bid) + + +class BindPluginTest(unittest.TestCase): + def setUp(self): + self.event_manager = EventManagerMock((), (Config, BindPlugin)) + self.uzbl = self.event_manager.add() + + def test_add_bind(self): + b = BindPlugin[self.uzbl] + modes = 'global' + glob = 'test' + handler = justafunction + b.mode_bind(modes, glob, handler) + + binds = b.bindlet.get_binds() + self.assertEqual(len(binds), 1) + self.assertIs(binds[0].function, justafunction) + + def test_parse_bind(self): + b = BindPlugin[self.uzbl] + modes = 'global' + glob = 'test' + handler = 'handler' + + b.parse_mode_bind('%s %s = %s' % (modes, glob, handler)) + binds = b.bindlet.get_binds() + self.assertEqual(len(binds), 1) + self.assertEqual(binds[0].commands, [handler]) diff --git a/tests/event-manager/testcompletion.py b/tests/event-manager/testcompletion.py new file mode 100644 index 0000000..8ae32a2 --- /dev/null +++ b/tests/event-manager/testcompletion.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python +# vi: set et ts=4: + + + +import unittest +from emtest import EventManagerMock +from uzbl.plugins.config import Config +from uzbl.plugins.keycmd import KeyCmd +from uzbl.plugins.completion import CompletionPlugin + + +class DummyFormatter(object): + def format(self, partial, completions): + return '[%s] %s' % (partial, ', '.join(completions)) + +class TestAdd(unittest.TestCase): + def setUp(self): + self.event_manager = EventManagerMock( + (), (CompletionPlugin,), + (), ((Config, dict), (KeyCmd, None)) + ) + self.uzbl = self.event_manager.add() + + def test_builtins(self): + c = CompletionPlugin[self.uzbl] + c.add_builtins(('spam', 'egg')) + self.assertIn('spam', c.completion) + self.assertIn('egg', c.completion) + + def test_config(self): + c = CompletionPlugin[self.uzbl] + c.add_config_key('spam', 'SPAM') + self.assertIn('@spam', c.completion) + + +class TestCompletion(unittest.TestCase): + def setUp(self): + self.event_manager = EventManagerMock( + (), (KeyCmd, CompletionPlugin), + (), ((Config, dict),) + ) + self.uzbl = self.event_manager.add() + + c = CompletionPlugin[self.uzbl] + c.listformatter = DummyFormatter() + c.add_builtins(('spam', 'egg', 'bar', 'baz')) + c.add_config_key('spam', 'SPAM') + c.add_config_key('Something', 'Else') + + def test_incomplete_keyword(self): + k, c = KeyCmd[self.uzbl], CompletionPlugin[self.uzbl] + k.keylet.keycmd = 'sp' + k.keylet.cursor = len(k.keylet.keycmd) + + r = c.get_incomplete_keyword() + self.assertEqual(r, ('sp', False)) + + def test_incomplete_keyword_var(self): + k, c = KeyCmd[self.uzbl], CompletionPlugin[self.uzbl] + k.keylet.keycmd = 'set @sp' + k.keylet.cursor = len(k.keylet.keycmd) + + r = c.get_incomplete_keyword() + self.assertEqual(r, ('@sp', False)) + + def test_incomplete_keyword_var_noat(self): + k, c = KeyCmd[self.uzbl], CompletionPlugin[self.uzbl] + k.keylet.keycmd = 'set Some' + k.keylet.cursor = len(k.keylet.keycmd) + + r = c.get_incomplete_keyword() + self.assertEqual(r, ('@Some', True)) + + def test_stop_completion(self): + config, c = Config[self.uzbl], CompletionPlugin[self.uzbl] + c.completion.level = 99 + config['completion_list'] = 'test' + + c.stop_completion() + self.assertNotIn('completion_list', config) + self.assertEqual(c.completion.level, 0) + + def test_completion(self): + k, c = KeyCmd[self.uzbl], CompletionPlugin[self.uzbl] + config = Config[self.uzbl] + + comp = ( + ('sp', 'spam '), + ('e', 'egg '), + ('set @sp', 'set @spam '), + ) + + for i, o in comp: + k.keylet.keycmd = i + k.keylet.cursor = len(k.keylet.keycmd) + + c.start_completion() + self.assertEqual(k.keylet.keycmd, o) + c.start_completion() + self.assertNotIn('completion_list', config) + + def test_completion_list(self): + k, c = KeyCmd[self.uzbl], CompletionPlugin[self.uzbl] + config = Config[self.uzbl] + + comp = ( + ('b', 'ba', '[ba] bar, baz'), + ) + + for i, o, l in comp: + k.keylet.keycmd = i + k.keylet.cursor = len(k.keylet.keycmd) + + c.start_completion() + self.assertEqual(k.keylet.keycmd, o) + c.start_completion() + self.assertEqual(config['completion_list'], l) diff --git a/tests/event-manager/testconfig.py b/tests/event-manager/testconfig.py new file mode 100644 index 0000000..d2a891e --- /dev/null +++ b/tests/event-manager/testconfig.py @@ -0,0 +1,77 @@ + + +import unittest +from emtest import EventManagerMock + +from uzbl.plugins.config import Config + + +class ConfigTest(unittest.TestCase): + def setUp(self): + self.event_manager = EventManagerMock((), (Config,)) + self.uzbl = self.event_manager.add() + + def test_set(self): + cases = ( + (True, '1'), + (False, '0'), + ("test", "test"), + (5, '5') + ) + c = Config[self.uzbl] + for input, expected in cases: + c.set('foo', input) + self.uzbl.send.assert_called_once_with( + 'set foo = ' + expected) + self.uzbl.send.reset_mock() + + def test_set_invalid(self): + cases = ( + ("foo\nbar", AssertionError), # Better Exception type + ("bad'key", AssertionError) + ) + c = Config[self.uzbl] + for input, exception in cases: + self.assertRaises(exception, c.set, input) + + def test_parse(self): + cases = ( + ('foo str value', 'foo', 'value'), + ('foo str "ba ba"', 'foo', 'ba ba'), + ('foo float 5', 'foo', 5.0) + ) + c = Config[self.uzbl] + for input, ekey, evalue in cases: + c.parse_set_event(input) + self.assertIn(ekey, c) + self.assertEqual(c[ekey], evalue) + self.uzbl.event.assert_called_once_with( + 'CONFIG_CHANGED', ekey, evalue) + self.uzbl.event.reset_mock() + + def test_parse_null(self): + cases = ( + ('foo str', 'foo'), + ('foo str ""', 'foo'), + #('foo int', 'foo') # Not sure if this input is valid + ) + c = Config[self.uzbl] + for input, ekey in cases: + c.update({'foo': '-'}) + c.parse_set_event(input) + self.assertNotIn(ekey, c) + self.uzbl.event.assert_called_once_with( + 'CONFIG_CHANGED', ekey, '') + self.uzbl.event.reset_mock() + + def test_parse_invalid(self): + cases = ( + ('foo bar', AssertionError), # TypeError? + ('foo bad^key', AssertionError), + ('', Exception), + ('foo int z', ValueError) + ) + c = Config[self.uzbl] + for input, exception in cases: + self.assertRaises(exception, c.parse_set_event, input) + self.assertEqual(len(list(c.keys())), 0) diff --git a/tests/event-manager/testcookie.py b/tests/event-manager/testcookie.py new file mode 100755 index 0000000..c33ba3b --- /dev/null +++ b/tests/event-manager/testcookie.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python + + +import sys +if '' not in sys.path: + sys.path.insert(0, '') + +import unittest +from emtest import EventManagerMock + +from uzbl.plugins.cookies import Cookies + +cookies = ( + r'".nyan.cat" "/" "__utmb" "183192761.1.10.1313990640" "http" "1313992440"', + r'".twitter.com" "/" "guest_id" "v1%3A131399064036991891" "http" "1377104460"' +) + + +class CookieFilterTest(unittest.TestCase): + def setUp(self): + self.event_manager = EventManagerMock((), (Cookies,)) + self.uzbl = self.event_manager.add() + self.other = self.event_manager.add() + + def test_add_cookie(self): + c = Cookies[self.uzbl] + c.add_cookie(cookies[0]) + self.other.send.assert_called_once_with( + 'add_cookie ' + cookies[0]) + + def test_whitelist_block(self): + c = Cookies[self.uzbl] + c.whitelist_cookie(r'domain "nyan\.cat$"') + c.add_cookie(cookies[1]) + self.uzbl.send.assert_called_once_with( + 'delete_cookie ' + cookies[1]) + + def test_whitelist_accept(self): + c = Cookies[self.uzbl] + c.whitelist_cookie(r'domain "nyan\.cat$"') + c.add_cookie(cookies[0]) + self.other.send.assert_called_once_with( + 'add_cookie ' + cookies[0]) + + def test_blacklist_block(self): + c = Cookies[self.uzbl] + c.blacklist_cookie(r'domain "twitter\.com$"') + c.add_cookie(cookies[1]) + self.uzbl.send.assert_called_once_with( + 'delete_cookie ' + cookies[1]) + + def test_blacklist_accept(self): + c = Cookies[self.uzbl] + c.blacklist_cookie(r'domain "twitter\.com$"') + c.add_cookie(cookies[0]) + self.other.send.assert_called_once_with( + 'add_cookie ' + cookies[0]) + + def test_filter_numeric(self): + c = Cookies[self.uzbl] + c.blacklist_cookie(r'0 "twitter\.com$"') + c.add_cookie(cookies[1]) + self.uzbl.send.assert_called_once_with( + 'delete_cookie ' + cookies[1]) + +if __name__ == '__main__': + unittest.main() diff --git a/tests/event-manager/testcore.py b/tests/event-manager/testcore.py new file mode 100644 index 0000000..1a8b4fa --- /dev/null +++ b/tests/event-manager/testcore.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python +# vi: set et ts=4: + + + +import unittest +from mock import Mock +from uzbl.core import Uzbl + + +class TestUzbl(unittest.TestCase): + def setUp(self): + options = Mock() + options.print_events = False + self.em = Mock() + self.proto = Mock() + self.uzbl = Uzbl(self.em, self.proto, options) + + def test_repr(self): + r = '%r' % self.uzbl + self.assertRegex(r, r'') + + def test_event_handler(self): + handler = Mock() + event, arg = 'FOO', 'test' + self.uzbl.connect(event, handler) + self.uzbl.event(event, arg) + handler.assert_called_once_with(arg) + + def test_parse_sets_name(self): + name = 'spam' + self.uzbl.parse_msg(' '.join(['EVENT', name, 'FOO', 'BAR'])) + self.assertEqual(self.uzbl.name, name) + + def test_parse_sends_event(self): + handler = Mock() + event, arg = 'FOO', 'test' + self.uzbl.connect(event, handler) + self.uzbl.parse_msg(' '.join(['EVENT', 'instance-name', event, arg])) + handler.assert_called_once_with(arg) + + def test_malformed_message(self): + # Should not crash + self.uzbl.parse_msg('asdaf') + self.assertTrue(True) + + def test_send(self): + self.uzbl.send('hello ') + self.proto.push.assert_called_once_with('hello\n'.encode('utf-8')) + + def test_close_calls_remove_instance(self): + self.uzbl.close() + self.em.remove_instance.assert_called_once_with(self.proto.socket) + + def test_close_cleans_plugins(self): + p1, p2 = Mock(), Mock() + self.uzbl._plugin_instances = (p1, p2) + self.uzbl.plugins = {} + self.uzbl.close() + p1.cleanup.assert_called_once_with() + p2.cleanup.assert_called_once_with() + + def test_close_connection_closes_protocol(self): + self.uzbl.close_connection(Mock()) + self.proto.close.assert_called_once_with() + + def test_exit_triggers_close(self): + self.uzbl.parse_msg(' '.join(['EVENT', 'spam', 'INSTANCE_EXIT'])) + self.em.remove_instance.assert_called_once_with(self.proto.socket) + + def test_instance_start(self): + pid = 1234 + self.em.plugind.per_instance_plugins = [] + self.uzbl.parse_msg( + ' '.join(['EVENT', 'spam', 'INSTANCE_START', str(pid)]) + ) + self.assertEqual(self.uzbl.pid, pid) + + def test_init_plugins(self): + u = self.uzbl + class FooPlugin(object): + def __init__(self, uzbl): pass + class BarPlugin(object): + def __init__(self, uzbl): pass + self.em.plugind.per_instance_plugins = [FooPlugin, BarPlugin] + u.init_plugins() + self.assertEqual(len(u.plugins), 2) + for t in (FooPlugin, BarPlugin): + self.assertIn(t, u.plugins) + self.assertTrue(isinstance(u.plugins[t], t)) diff --git a/tests/event-manager/testdoc.py b/tests/event-manager/testdoc.py new file mode 100755 index 0000000..0f3b8eb --- /dev/null +++ b/tests/event-manager/testdoc.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +# vi: set et ts=4: + +import sys +if '' not in sys.path: + sys.path.insert(0, '') + +import unittest +from doctest import DocTestSuite +keycmd_tests = DocTestSuite('uzbl.plugins.keycmd') +arguments_tests = DocTestSuite('uzbl.arguments') + + +def load_tests(loader, standard, pattern): + tests = unittest.TestSuite() + tests.addTest(keycmd_tests) + tests.addTest(arguments_tests) + return tests + +if __name__ == '__main__': + runner = unittest.TextTestRunner() + runner.run() diff --git a/tests/event-manager/testdownload.py b/tests/event-manager/testdownload.py new file mode 100644 index 0000000..49ed726 --- /dev/null +++ b/tests/event-manager/testdownload.py @@ -0,0 +1,29 @@ +# vi: set et ts=4: + + + +import unittest +from emtest import EventManagerMock + +from uzbl.plugins.config import Config +from uzbl.plugins.downloads import Downloads + + +class DownloadsTest(unittest.TestCase): + def setUp(self): + self.event_manager = EventManagerMock((), (Downloads, Config)) + self.uzbl = self.event_manager.add() + + def test_start(self): + cases = ( + ('foo', 'foo', 'foo (0%)'), + ('"b@r"', 'b@r', 'b@r (0%'), + ) + d = Downloads[self.uzbl] + for input, key, section in cases: + d.download_started(input) + self.assertIn(key, d.active_downloads) + self.assertEqual(d.active_downloads[key], 0) + self.uzbl.send.assert_called_once() + self.assertIn(section, self.uzbl.send.call_args[0][0]) + self.uzbl.reset_mock() diff --git a/tests/event-manager/testhistory.py b/tests/event-manager/testhistory.py new file mode 100644 index 0000000..0817d9f --- /dev/null +++ b/tests/event-manager/testhistory.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python +# vi: set et ts=4: + + + +import sys +if '' not in sys.path: + sys.path.insert(0, '') + +import unittest +from emtest import EventManagerMock + +from uzbl.plugins.history import History, SharedHistory +from uzbl.plugins.keycmd import Keylet, KeyCmd +from uzbl.plugins.on_set import OnSetPlugin +from uzbl.plugins.config import Config + + +class SharedHistoryTest(unittest.TestCase): + def setUp(self): + self.event_manager = EventManagerMock((SharedHistory,), ()) + self.uzbl = self.event_manager.add() + self.other = self.event_manager.add() + + def test_instance(self): + a = SharedHistory[self.uzbl] + b = SharedHistory[self.other] + self.assertIs(a, b) + + def test_add_and_get(self): + s = SharedHistory[self.uzbl] + s.addline('foo', 'bar') + s.addline('foo', 'baz') + s.addline('foo', 'bap') + self.assertEqual(s.get_line_number('foo'), 3) + self.assertEqual(s.get_line_number('other'), 0) + self.assertEqual(s.getline('foo', 0), 'bar') + self.assertEqual(s.getline('foo', 1), 'baz') + self.assertEqual(s.getline('foo', 2), 'bap') + self.assertEqual(s.getline('foo', -1), 'bap') + + def test_empty_line_number(self): + s = SharedHistory[self.uzbl] + s.addline('foo', 'bar') + self.assertEqual(s.get_line_number(''), 0) + self.assertEqual(s.get_line_number('other'), 0) + + def test_get_missing_prompt(self): + s = SharedHistory[self.uzbl] + s.addline('foo', 'bar') + self.assertRaises(IndexError, s.getline, 'bar', 0) + + +class HistoryTest(unittest.TestCase): + def setUp(self): + self.event_manager = EventManagerMock( + (SharedHistory,), + (OnSetPlugin, KeyCmd, Config, History) + ) + self.uzbl = self.event_manager.add() + self.other = self.event_manager.add() + s = SharedHistory[self.uzbl] + data = ( + ('', 'woop'), + ('', 'doop'), + ('', 'bar'), + ('', 'foo'), + ('git', 'spam'), + ('git', 'egg'), + ('foo', 'foo') + ) + for prompt, input in data: + s.addline(prompt, input) + + def test_step(self): + h = History[self.uzbl] + self.assertEqual('', next(h)) + self.assertEqual('', next(h)) + self.assertEqual('foo', h.prev()) + self.assertEqual('bar', h.prev()) + self.assertEqual('foo', next(h)) + self.assertEqual('bar', h.prev()) + self.assertEqual('doop', h.prev()) + self.assertEqual('woop', h.prev()) + self.assertTrue(len(h.prev()) > 0) + self.assertTrue(len(h.prev()) > 0) + self.assertEqual('woop', next(h)) + + def test_step_prompt(self): + h = History[self.uzbl] + h.change_prompt('git') + self.assertEqual('', next(h)) + self.assertEqual('', next(h)) + self.assertEqual('egg', h.prev()) + self.assertEqual('spam', h.prev()) + self.assertTrue(len(h.prev()) > 0) + self.assertTrue(len(h.prev()) > 0) + self.assertEqual('spam', next(h)) + + def test_change_prompt(self): + h = History[self.uzbl] + self.assertEqual('foo', h.prev()) + self.assertEqual('bar', h.prev()) + h.change_prompt('git') + self.assertEqual('egg', h.prev()) + self.assertEqual('spam', h.prev()) + + def test_exec(self): + modstate = set() + keylet = Keylet() + keylet.set_keycmd('foo') + History[self.uzbl].keycmd_exec(modstate, keylet) + s = SharedHistory[self.uzbl] + self.assertEqual(s.getline('', -1), 'foo') + + def test_exec_from_history(self): + h = History[self.uzbl] + self.assertEqual('foo', h.prev()) + self.assertEqual('bar', h.prev()) + self.assertEqual('doop', h.prev()) + modstate = set() + keylet = Keylet() + keylet.set_keycmd('doop') + h.keycmd_exec(modstate, keylet) + self.assertEqual('doop', h.prev()) + self.assertEqual('foo', h.prev()) + self.assertEqual('bar', h.prev()) + # do we really want this one here ? + self.assertEqual('doop', h.prev()) + self.assertEqual('woop', h.prev()) + + def test_search(self): + h = History[self.uzbl] + h.search('oop') + self.assertEqual('doop', h.prev()) + self.assertEqual('woop', h.prev()) + self.assertTrue(len(h.prev()) > 0) + self.assertEqual('woop', next(h)) + self.assertEqual('doop', next(h)) + # this reset the search + self.assertEqual('', next(h)) + self.assertEqual('foo', h.prev()) + + def test_temp(self): + kl = KeyCmd[self.uzbl].keylet + kl.set_keycmd('uzbl') + h = History[self.uzbl] + h.change_prompt('foo') + # Why is the preserve current logic in this method? + h.history_prev(None) + self.assertTrue(len(h.prev()) > 0) + self.assertEqual('foo', next(h)) + self.assertEqual('uzbl', next(h)) + self.assertEqual('', next(h)) # this clears the keycmd diff --git a/tests/event-manager/testkeycmd.py b/tests/event-manager/testkeycmd.py new file mode 100644 index 0000000..bc82c6d --- /dev/null +++ b/tests/event-manager/testkeycmd.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python + +import re +import mock +import unittest +from emtest import EventManagerMock +from uzbl.plugins.keycmd import KeyCmd +from uzbl.plugins.config import Config + + +def getkeycmd(s): + return re.match(r'@\[([^\]]*)\]@', s).group(1) + +class KeyCmdTest(unittest.TestCase): + def setUp(self): + self.event_manager = EventManagerMock( + (), (KeyCmd,), + (), ((Config, dict),) + ) + self.uzbl = self.event_manager.add() + + def test_press_key(self): + c, k = Config[self.uzbl], KeyCmd[self.uzbl] + k.key_press(('', 'a')) + self.assertEqual(c.get('modcmd', ''), '') + keycmd = getkeycmd(c['keycmd']) + self.assertEqual(keycmd, 'a') + + def test_press_keys(self): + c, k = Config[self.uzbl], KeyCmd[self.uzbl] + string = 'uzbl' + for char in string: + k.key_press(('', char)) + self.assertEqual(c.get('modcmd', ''), '') + keycmd = getkeycmd(c['keycmd']) + self.assertEqual(keycmd, string) + + def test_press_unicode_keys(self): + c, k = Config[self.uzbl], KeyCmd[self.uzbl] + string = '\u5927\u962a\u5e02' + for char in string: + k.key_press(('', char)) + self.assertEqual(c.get('modcmd', ''), '') + keycmd = getkeycmd(c['keycmd']) + self.assertEqual(keycmd, string) diff --git a/tests/event-manager/testmode.py b/tests/event-manager/testmode.py new file mode 100644 index 0000000..d198c32 --- /dev/null +++ b/tests/event-manager/testmode.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python +# vi: set et ts=4: + + + +import unittest +from emtest import EventManagerMock +from uzbl.plugins.config import Config +from uzbl.plugins.mode import ModePlugin +from uzbl.plugins.on_set import OnSetPlugin + +class ModeParseTest(unittest.TestCase): + def setUp(self): + self.event_manager = EventManagerMock( + (), (OnSetPlugin, ModePlugin), + (), ((Config, dict),) + ) + self.uzbl = self.event_manager.add() + + def test_parse_config(self): + uzbl = self.uzbl + m = ModePlugin[uzbl] + + mode, key, value = 'foo', 'x', 'y' + m.parse_mode_config((mode, key, '=', value)) + self.assertIn(mode, m.mode_config) + self.assertIn(key, m.mode_config[mode]) + self.assertEqual(m.mode_config[mode][key], value) + + +class ModeTest(unittest.TestCase): + def setUp(self): + self.event_manager = EventManagerMock( + (), (OnSetPlugin, ModePlugin), + (), ((Config, dict),) + ) + self.uzbl = self.event_manager.add() + + mode = ModePlugin[self.uzbl] + config = Config[self.uzbl] + + mode.parse_mode_config(('mode0', 'foo', '=', 'default')) + + mode.parse_mode_config(('mode1', 'foo', '=', 'xxx')) + mode.parse_mode_config('mode1 bar = "spam spam"') + mode.parse_mode_config('mode1 baz = foo="baz"') + + mode.parse_mode_config(('mode2', 'foo', '=', 'XXX')) + mode.parse_mode_config(('mode2', 'spam', '=', 'spam')) + + config['default_mode'] = 'mode0' + mode.default_mode_updated(None, 'mode0') + + def test_mode_sets_vars(self): + mode, config = ModePlugin[self.uzbl], Config[self.uzbl] + mode.mode_updated(None, 'mode1') + + self.assertIn('foo', config) + self.assertIn('bar', config) + self.assertIn('baz', config) + self.assertEqual(config['foo'], 'xxx') + self.assertEqual(config['bar'], 'spam spam') + self.assertEqual(config['baz'], 'foo="baz"') + + def test_mode_overwrite_vars(self): + mode, config = ModePlugin[self.uzbl], Config[self.uzbl] + config['mode'] = 'mode1' + mode.mode_updated(None, 'mode1') + config['mode'] = 'mode2' + mode.mode_updated(None, 'mode2') + + self.assertIn('foo', config) + self.assertIn('bar', config) + self.assertIn('baz', config) + self.assertIn('spam', config) + self.assertEqual(config['foo'], 'XXX') + self.assertEqual(config['bar'], 'spam spam') + self.assertEqual(config['baz'], 'foo="baz"') + self.assertEqual(config['spam'], 'spam') + + def test_default_mode(self): + ''' Setting to mode to nothing should enter the default mode''' + mode, config = ModePlugin[self.uzbl], Config[self.uzbl] + + config['foo'] = 'nthth' + config['mode'] = '' + mode.mode_updated(None, '') + self.assertEqual(config['mode'], 'mode0') + mode.mode_updated(None, config['mode']) + self.assertEqual(config['mode'], 'mode0') + self.assertEqual(config['foo'], 'default') diff --git a/tests/event-manager/testonevent.py b/tests/event-manager/testonevent.py new file mode 100644 index 0000000..7d80dbd --- /dev/null +++ b/tests/event-manager/testonevent.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python +# vi: set et ts=4: + + + +import unittest +from emtest import EventManagerMock +from uzbl.plugins.config import Config +from uzbl.plugins.mode import ModePlugin +from uzbl.plugins.on_event import OnEventPlugin + + +class OnEventTest(unittest.TestCase): + def setUp(self): + self.event_manager = EventManagerMock( + (), (OnEventPlugin,), + ) + self.uzbl = self.event_manager.add() + + def test_command(self): + oe = OnEventPlugin[self.uzbl] + event, command = 'FOO', 'test test' + + oe.on_event(event, [], command) + oe.event_handler('', on_event=event) + self.uzbl.send.assert_called_once_with(command) + + def test_matching_pattern(self): + oe = OnEventPlugin[self.uzbl] + event, pattern, command = 'FOO', ['BAR'], 'test test' + + oe.on_event(event, pattern, command) + oe.event_handler('BAR else', on_event=event) + self.uzbl.send.assert_called_once_with(command) + + def test_non_matching_pattern(self): + oe = OnEventPlugin[self.uzbl] + event, pattern, command = 'FOO', ['BAR'], 'test test' + + oe.on_event(event, pattern, command) + oe.event_handler('FOO else', on_event=event) + self.assertFalse(self.uzbl.send.called) + + def test_parse(self): + oe = OnEventPlugin[self.uzbl] + event, command = 'FOO', 'test test' + + oe.parse_on_event((event, command)) + self.assertIn(event, oe.events) + + def test_parse_pattern(self): + oe = OnEventPlugin[self.uzbl] + event, pattern, command = 'FOO', 'BAR', 'test test' + + oe.parse_on_event((event, '[', pattern, ']', command)) + self.assertIn(event, oe.events) + commands = oe.events[event] + self.assertIn(command, commands) + self.assertEqual(commands[command], [pattern]) diff --git a/tests/event-manager/testprogressbar.py b/tests/event-manager/testprogressbar.py new file mode 100644 index 0000000..93ebaa8 --- /dev/null +++ b/tests/event-manager/testprogressbar.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python +# vi: set et ts=4: + + + +import sys +if '' not in sys.path: + sys.path.insert(0, '') + +import unittest +from emtest import EventManagerMock +from uzbl.plugins.config import Config +from uzbl.plugins.progress_bar import ProgressBar + + +class ProgressBarTest(unittest.TestCase): + def setUp(self): + self.event_manager = EventManagerMock( + (), (ProgressBar,), + (), ((Config, dict),) + ) + self.uzbl = self.event_manager.add() + + def test_percent_done(self): + uzbl = self.uzbl + p, c = ProgressBar[uzbl], Config[uzbl] + c['progress.format'] = '%c' + + p.update_progress() + inout = ( + (9, '9%'), + (99, '99%'), + (100, '100%'), + #(101, '100%') # TODO + ) + + for i, o in inout: + p.update_progress(i) + self.assertEqual(c['progress.output'], o) + + def test_done_char(self): + uzbl = self.uzbl + p, c = ProgressBar[uzbl], Config[uzbl] + c['progress.format'] = '%d' + + p.update_progress() + inout = ( + (9, '='), + (50, '===='), + (99, '========'), + (100, '========'), + (101, '========') + ) + + for i, o in inout: + p.update_progress(i) + self.assertEqual(c['progress.output'], o) + + def test_pending_char(self): + uzbl = self.uzbl + p, c = ProgressBar[uzbl], Config[uzbl] + c['progress.format'] = '%p' + c['progress.pending'] = '-' + + p.update_progress() + inout = ( + (9, '-------'), + (50, '----'), + (99, ''), + (100, ''), + (101, '') + ) + + for i, o in inout: + p.update_progress(i) + self.assertEqual(c['progress.output'], o) + + def test_percent_pending(self): + uzbl = self.uzbl + p, c = ProgressBar[uzbl], Config[uzbl] + c['progress.format'] = '%t' + + p.update_progress() + inout = ( + (9, '91%'), + (50, '50%'), + (99, '1%'), + (100, '0%'), + #(101, '0%') # TODO + ) + + for i, o in inout: + p.update_progress(i) + self.assertEqual(c['progress.output'], o) diff --git a/uzbl/__init__.py b/uzbl/__init__.py new file mode 100644 index 0000000..58f0d85 --- /dev/null +++ b/uzbl/__init__.py @@ -0,0 +1,6 @@ +''' +Event manager for uzbl + +A event manager for uzbl that supports plugins and multiple simultaneus +connections from uzbl-core(s) +''' diff --git a/uzbl/arguments.py b/uzbl/arguments.py new file mode 100644 index 0000000..7f71b74 --- /dev/null +++ b/uzbl/arguments.py @@ -0,0 +1,103 @@ +''' +Arguments parser + +provides argument parsing for event handlers +''' + +import re +import ast + + +class Arguments(tuple): + ''' + Given a argument line gives access to the split parts + honoring common quotation and escaping rules + + >>> Arguments(r"simple 'quoted string'") + ('simple', 'quoted string') + ''' + + _splitquoted = re.compile("(\s+|\"(?:\\\\.|[^\"])*?\"|'(?:\\\\.|[^'])*?')") + + def __new__(cls, s): + ''' + >>> Arguments(r"one two three") + ('one', 'two', 'three') + >>> Arguments(r"spam 'escaping \\'works\\''") + ('spam', "escaping 'works'") + >>> # For testing purposes we can pass a preparsed tuple + >>> Arguments(('foo', 'bar', 'baz az')) + ('foo', 'bar', 'baz az') + ''' + if isinstance(s, tuple): + self = tuple.__new__(cls, s) + self._raw, self._ref = s, list(range(len(s))) + return self + raw = cls._splitquoted.split(s) + ref = [] + self = tuple.__new__(cls, cls.parse(raw, ref)) + self._raw, self._ref = raw, ref + return self + + @classmethod + def parse(cls, raw, ref): + ''' + Generator used to initialise the arguments tuple + + Indexes to where in source list the arguments start will be put in 'ref' + ''' + c = None + for i, part in enumerate(raw): + if re.match('\s+', part): + # Whitespace ends the current argument, leading ws is ignored + if c is not None: + yield c + c = None + else: + f = unquote(part) + if c is None: + # Mark the start of the argument in the raw input + if part != '': + ref.append(i) + c = f + else: + c += f + if c is not None: + yield c + + def raw(self, frm=0, to=None): + ''' + Returs the portion of the raw input that yielded arguments + from 'frm' to 'to' + + >>> args = Arguments(r"'spam, spam' egg sausage and 'spam'") + >>> args + ('spam, spam', 'egg', 'sausage', 'and', 'spam') + >>> args.raw(1) + "egg sausage and 'spam'" + ''' + if len(self._ref) < 1: + return '' + rfrm = self._ref[frm] + if to is None or len(self._ref) <= to + 1: + rto = len(self._raw) + else: + rto = self._ref[to + 1] - 1 + return ''.join(self._raw[rfrm:rto]) + +splitquoted = Arguments # or define a function? + + +def is_quoted(s): + return s and s[0] == s[-1] and s[0] in "'\"" + + +def unquote(s): + ''' + Returns the input string without quotations and with + escape sequences interpreted + ''' + + if is_quoted(s): + return ast.literal_eval(s) + return ast.literal_eval('"' + s + '"') diff --git a/uzbl/core.py b/uzbl/core.py new file mode 100644 index 0000000..d0f9010 --- /dev/null +++ b/uzbl/core.py @@ -0,0 +1,153 @@ +import time +import logging +from collections import defaultdict + + +class Uzbl(object): + + def __init__(self, parent, proto, options): + proto.target = self + self.opts = options + self.parent = parent + self.proto = proto + self.time = time.time() + self.pid = None + self.name = None + + # Flag if the instance has raised the INSTANCE_START event. + self.instance_start = False + + # Use name "unknown" until name is discovered. + self.logger = logging.getLogger('uzbl-instance[]') + + # Plugin instances + self._plugin_instances = [] + self.plugins = {} + + # Track plugin event handlers + self.handlers = defaultdict(list) + + # Internal vars + self._depth = 0 + self._buffer = '' + + def __repr__(self): + return '' % ', '.join([ + 'pid=%s' % (self.pid if self.pid else "Unknown"), + 'name=%s' % ('%r' % self.name if self.name else "Unknown"), + 'uptime=%f' % (time.time() - self.time), + '%d handlers' % sum([len(l) for l in list(self.handlers.values())])]) + + def init_plugins(self): + '''Creates instances of per-instance plugins''' + + for plugin in self.parent.plugind.per_instance_plugins: + pinst = plugin(self) + self._plugin_instances.append(pinst) + self.plugins[plugin] = pinst + + def send(self, msg): + '''Send a command to the uzbl instance via the child socket + instance.''' + + msg = msg.strip() + + if self.opts.print_events: + print(('%s<-- %s' % (' ' * self._depth, msg))) + + self.proto.push((msg+'\n').encode('utf-8')) + + def parse_msg(self, line): + '''Parse an incoming message from a uzbl instance. Event strings + will be parsed into `self.event(event, args)`.''' + + # Split by spaces (and fill missing with nulls) + elems = (line.split(' ', 3) + [''] * 3)[:4] + + # Ignore non-event messages. + if elems[0] != 'EVENT': + self.logger.info('non-event message: %r', line) + if self.opts.print_events: + print(('--- %s' % line)) + return + + # Check event string elements + (name, event, args) = elems[1:] + assert name and event, 'event string missing elements' + if not self.name: + self.name = name + self.logger = logging.getLogger('uzbl-instance%s' % name) + self.logger.info('found instance name %r', name) + + assert self.name == name, 'instance name mismatch' + + # Handle the event with the event handlers through the event method + self.event(event, args) + + def event(self, event, *args, **kargs): + '''Raise an event.''' + + event = event.upper() + + if not self.opts.daemon_mode and self.opts.print_events: + elems = [event] + if args: + elems.append(str(args)) + if kargs: + elems.append(str(kargs)) + print(('%s--> %s' % (' ' * self._depth, ' '.join(elems)))) + + if event == "INSTANCE_START" and args: + assert not self.instance_start, 'instance already started' + + self.pid = int(args[0]) + self.logger.info('found instance pid %r', self.pid) + + self.init_plugins() + + elif event == "INSTANCE_EXIT": + self.logger.info('uzbl instance exit') + self.close() + + if event not in self.handlers: + return + + for handler in self.handlers[event]: + self._depth += 1 + try: + handler(*args, **kargs) + + except Exception: + self.logger.error('error in handler', exc_info=True) + + self._depth -= 1 + + def close_connection(self, child_socket): + '''Close child socket and delete the uzbl instance created for that + child socket connection.''' + self.proto.close() + + def close(self): + '''Close the client socket and call the plugin cleanup hooks.''' + + self.logger.debug('called close method') + + # Remove self from parent uzbls dict. + self.logger.debug('removing self from uzbls list') + self.parent.remove_instance(self.proto.socket) + + for plugin in self._plugin_instances: + plugin.cleanup() + del self.plugins # to avoid cyclic links + del self._plugin_instances + + self.logger.info('removed %r', self) + + def connect(self, name, handler): + """Attach event handler + + No extra arguments added. Use bound methods and partials to have + extra arguments. + """ + self.handlers[name].append(handler) + diff --git a/uzbl/event_manager.py b/uzbl/event_manager.py new file mode 100755 index 0000000..fcf9a47 --- /dev/null +++ b/uzbl/event_manager.py @@ -0,0 +1,540 @@ +#!/usr/bin/env python3 + + +# Event Manager for Uzbl +# Copyright (c) 2009-2010, Mason Larobina +# Copyright (c) 2009, Dieter Plaetinck +# +# 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 . + +''' + +E V E N T _ M A N A G E R . P Y +=============================== + +Event manager for uzbl written in python. + +''' + +import atexit +import imp +import logging +import os +import sys +import time +import weakref +import re +import errno +import asyncore +from collections import defaultdict +from functools import partial +from glob import glob +from itertools import count +from optparse import OptionParser +from select import select +from signal import signal, SIGTERM, SIGINT, SIGKILL +from traceback import format_exc + +from uzbl.net import Listener, Protocol +from uzbl.core import Uzbl + +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 list(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/') +CACHE_DIR = os.path.join(xdghome('CACHE', '.cache/'), 'uzbl/') + +# Define some globals. +SCRIPTNAME = os.path.basename(sys.argv[0]) + +logger = logging.getLogger(SCRIPTNAME) + + +def get_exc(): + '''Format `format_exc` for logging.''' + return "\n%s" % format_exc().rstrip() + + +def expandpath(path): + '''Expand and realpath paths.''' + return os.path.realpath(os.path.expandvars(path)) + + + +def daemonize(): + '''Daemonize the process using the Stevens' double-fork magic.''' + + logger.info('entering daemon mode') + + try: + if os.fork(): + os._exit(0) + + except OSError: + logger.critical('failed to daemonize', exc_info=True) + sys.exit(1) + + os.chdir('/') + os.setsid() + os.umask(0) + + try: + if os.fork(): + os._exit(0) + + except OSError: + logger.critical('failed to daemonize', exc_info=True) + sys.exit(1) + + if sys.stdout.isatty(): + sys.stdout.flush() + sys.stderr.flush() + + devnull = '/dev/null' + stdin = open(devnull, 'r') + stdout = open(devnull, 'a+') + stderr = open(devnull, 'a+') + + os.dup2(stdin.fileno(), sys.stdin.fileno()) + os.dup2(stdout.fileno(), sys.stdout.fileno()) + os.dup2(stderr.fileno(), sys.stderr.fileno()) + + logger.info('entered daemon mode') + + +def make_dirs(path): + '''Make all basedirs recursively as required.''' + + try: + dirname = os.path.dirname(path) + if not os.path.isdir(dirname): + logger.debug('creating directories %r', dirname) + os.makedirs(dirname) + + except OSError: + logger.error('failed to create directories', exc_info=True) + + +class PluginDirectory(object): + def __init__(self): + self.global_plugins = [] + self.per_instance_plugins = [] + + def load(self): + ''' Import plugin files ''' + + import uzbl.plugins + import pkgutil + + path = uzbl.plugins.__path__ + for impr, name, ispkg in pkgutil.iter_modules(path, 'uzbl.plugins.'): + __import__(name, globals(), locals()) + + from uzbl.ext import global_registry, per_instance_registry + self.global_plugins.extend(global_registry) + self.per_instance_plugins.extend(per_instance_registry) + + +class UzblEventDaemon(object): + def __init__(self, listener, plugind): + listener.target = self + self.opts = opts + self.listener = listener + self.plugind = plugind + self._quit = False + + # Hold uzbl instances + # {child socket: Uzbl instance, ..} + self.uzbls = {} + + self.plugins = {} + + # Register that the event daemon server has started by creating the + # pid file. + make_pid_file(opts.pid_file) + + # Register a function to clean up the socket and pid file on exit. + atexit.register(self.quit) + + # Add signal handlers. + for sigint in [SIGTERM, SIGINT]: + signal(sigint, self.quit) + + # Scan plugin directory for plugins + self.plugind.load() + + # Initialise global plugins with instances in self.plugins + self.init_plugins() + + def init_plugins(self): + '''Initialise event manager plugins.''' + self._plugin_instances = [] + self.plugins = {} + + for plugin in self.plugind.global_plugins: + pinst = plugin(self) + self._plugin_instances.append(pinst) + self.plugins[plugin] = pinst + + def run(self): + '''Main event daemon loop.''' + + logger.debug('entering main loop') + + if opts.daemon_mode: + # Daemonize the process + daemonize() + + # Update the pid file + make_pid_file(opts.pid_file) + + asyncore.loop() + + # Clean up and exit + self.quit() + + logger.debug('exiting main loop') + + def add_instance(self, sock): + proto = Protocol(sock) + uzbl = Uzbl(self, proto, opts) + self.uzbls[sock] = uzbl + + def remove_instance(self, sock): + if sock in self.uzbls: + del self.uzbls[sock] + if not self.uzbls and opts.auto_close: + self.quit() + + def close_server_socket(self): + '''Close and delete the server socket.''' + + try: + self.listener.close() + + except: + logger.error('failed to close server socket', exc_info=True) + + def quit(self, sigint=None, *args): + '''Close all instance socket objects, server socket and delete the + pid file.''' + + if sigint == SIGTERM: + logger.critical('caught SIGTERM, exiting') + + elif sigint == SIGINT: + logger.critical('caught SIGINT, exiting') + + elif not self._quit: + logger.debug('shutting down event manager') + + self.close_server_socket() + + for uzbl in list(self.uzbls.values()): + uzbl.close() + + if not self._quit: + for plugin in self._plugin_instances: + plugin.cleanup() + del self.plugins # to avoid cyclic links + del self._plugin_instances + + del_pid_file(opts.pid_file) + + if not self._quit: + logger.info('event manager shut down') + self._quit = True + raise SystemExit() + + +def make_pid_file(pid_file): + '''Creates a pid file at `pid_file`, fails silently.''' + + try: + logger.debug('creating pid file %r', pid_file) + make_dirs(pid_file) + pid = os.getpid() + fileobj = open(pid_file, 'w') + fileobj.write('%d' % pid) + fileobj.close() + logger.info('created pid file %r with pid %d', pid_file, pid) + + except: + logger.error('failed to create pid file', exc_info=True) + + +def del_pid_file(pid_file): + '''Deletes a pid file at `pid_file`, fails silently.''' + + if os.path.isfile(pid_file): + try: + logger.debug('deleting pid file %r', pid_file) + os.remove(pid_file) + logger.info('deleted pid file %r', pid_file) + + except: + logger.error('failed to delete pid file', exc_info=True) + + +def get_pid(pid_file): + '''Reads a pid from pid file `pid_file`, fails None.''' + + try: + logger.debug('reading pid file %r', pid_file) + fileobj = open(pid_file, 'r') + pid = int(fileobj.read()) + fileobj.close() + logger.info('read pid %d from pid file %r', pid, pid_file) + return pid + + except (IOError, ValueError): + logger.error('failed to read pid', exc_info=True) + return None + + +def pid_running(pid): + '''Checks if a process with a pid `pid` is running.''' + + try: + os.kill(pid, 0) + except OSError: + return False + else: + return True + + +def term_process(pid): + '''Asks nicely then forces process with pid `pid` to exit.''' + + try: + logger.info('sending SIGTERM to process with pid %r', pid) + os.kill(pid, SIGTERM) + + except OSError: + logger.error(get_exc()) + + logger.debug('waiting for process with pid %r to exit', pid) + start = time.time() + while True: + if not pid_running(pid): + logger.debug('process with pid %d exit', pid) + return True + + if (time.time() - start) > 5: + logger.warning('process with pid %d failed to exit', pid) + logger.info('sending SIGKILL to process with pid %d', pid) + try: + os.kill(pid, SIGKILL) + except: + logger.critical('failed to kill %d', pid, exc_info=True) + raise + + if (time.time() - start) > 10: + logger.critical('unable to kill process with pid %d', pid) + raise OSError + + time.sleep(0.25) + + +def stop_action(): + '''Stop the event manager daemon.''' + + pid_file = opts.pid_file + if not os.path.isfile(pid_file): + logger.error('could not find running event manager with pid file %r', + pid_file) + return + + pid = get_pid(pid_file) + if not pid_running(pid): + logger.debug('no process with pid %r', pid) + del_pid_file(pid_file) + return + + logger.debug('terminating process with pid %r', pid) + term_process(pid) + del_pid_file(pid_file) + logger.info('stopped event manager process with pid %d', pid) + + +def start_action(): + '''Start the event manager daemon.''' + + pid_file = opts.pid_file + if os.path.isfile(pid_file): + pid = get_pid(pid_file) + if pid_running(pid): + logger.error('event manager already started with pid %d', pid) + return + + logger.info('no process with pid %d', pid) + del_pid_file(pid_file) + + listener = Listener(opts.server_socket) + listener.start() + plugind = PluginDirectory() + daemon = UzblEventDaemon(listener, plugind) + daemon.run() + + +def restart_action(): + '''Restart the event manager daemon.''' + + stop_action() + start_action() + + +def list_action(): + '''List all the plugins that would be loaded in the current search + dirs.''' + + from types import ModuleType + import uzbl.plugins + import pkgutil + for line in pkgutil.iter_modules(uzbl.plugins.__path__, 'uzbl.plugins.'): + imp, name, ispkg = line + print(name) + + +def make_parser(): + parser = OptionParser('usage: %prog [options] {start|stop|restart|list}') + add = parser.add_option + + add('-v', '--verbose', + dest='verbose', default=2, action='count', + help='increase verbosity') + + socket_location = os.path.join(CACHE_DIR, 'event_daemon') + + add('-s', '--server-socket', + dest='server_socket', metavar="SOCKET", default=socket_location, + help='server AF_UNIX socket location') + + add('-p', '--pid-file', + metavar="FILE", dest='pid_file', + help='pid file location, defaults to server socket + .pid') + + add('-n', '--no-daemon', + dest='daemon_mode', action='store_false', default=True, + help='do not daemonize the process') + + add('-a', '--auto-close', + dest='auto_close', action='store_true', default=False, + help='auto close after all instances disconnect') + + add('-o', '--log-file', + dest='log_file', metavar='FILE', + help='write logging output to a file, defaults to server socket +' + ' .log') + + add('-q', '--quiet-events', + dest='print_events', action="store_false", default=True, + help="silence the printing of events to stdout") + + return parser + + +def init_logger(): + log_level = logging.CRITICAL - opts.verbose * 10 + logger = logging.getLogger() + logger.setLevel(max(log_level, 10)) + + # Console + handler = logging.StreamHandler() + handler.setLevel(max(log_level + 10, 10)) + handler.setFormatter(logging.Formatter( + '%(name)s: %(levelname)s: %(message)s')) + logger.addHandler(handler) + + # Logfile + handler = logging.FileHandler(opts.log_file, 'w', 'utf-8', 1) + handler.setLevel(max(log_level, 10)) + handler.setFormatter(logging.Formatter( + '[%(created)f] %(name)s: %(levelname)s: %(message)s')) + logger.addHandler(handler) + + +def main(): + global opts + + parser = make_parser() + + (opts, args) = parser.parse_args() + + opts.server_socket = expandpath(opts.server_socket) + + # Set default pid file location + if not opts.pid_file: + opts.pid_file = "%s.pid" % opts.server_socket + + else: + opts.pid_file = expandpath(opts.pid_file) + + # Set default log file location + if not opts.log_file: + opts.log_file = "%s.log" % opts.server_socket + + else: + opts.log_file = expandpath(opts.log_file) + + # Logging setup + init_logger() + logger.info('logging to %r', opts.log_file) + + if opts.auto_close: + logger.debug('will auto close') + else: + logger.debug('will not auto close') + + if opts.daemon_mode: + logger.debug('will daemonize') + else: + logger.debug('will not daemonize') + + # init like {start|stop|..} daemon actions + daemon_actions = {'start': start_action, 'stop': stop_action, + 'restart': restart_action, 'list': list_action} + + if len(args) == 1: + action = args[0] + if action not in daemon_actions: + parser.error('invalid action: %r' % action) + + elif not args: + action = 'start' + logger.warning('no daemon action given, assuming %r', action) + + else: + parser.error('invalid action argument: %r' % args) + + logger.info('daemon action %r', action) + # Do action + daemon_actions[action]() + + logger.debug('process CPU time: %f', time.clock()) + + +if __name__ == "__main__": + main() + + +# vi: set et ts=4: diff --git a/uzbl/ext.py b/uzbl/ext.py new file mode 100644 index 0000000..b2795ed --- /dev/null +++ b/uzbl/ext.py @@ -0,0 +1,92 @@ +from .event_manager import Uzbl +import logging + + +per_instance_registry = [] +global_registry = [] + + +class PluginMeta(type): + """Registers plugin in registry so that it instantiates when needed""" + + def __init__(self, name, bases, dic): + super(PluginMeta, self).__init__(name, bases, dic) + # Sorry, a bit of black magick + if bases == (object,) or bases == (BasePlugin,): + # base classes for the plugins + return + if issubclass(self, PerInstancePlugin): + per_instance_registry.append(self) + elif issubclass(self, GlobalPlugin): + global_registry.append(self) + + def __getitem__(self, owner): + """This method returns instance of plugin corresponding to owner + + :param owner: can be uzbl or event manager + + If you will try to get instance of :class:`GlobalPlugin` on uzbl + instance it will find instance on it's parent. If you will try to + find instance of a :class:`PerInstancePlugin` it will raise + :class:`ValueError` + """ + return self._get_instance(owner) + + +class BasePlugin(object, metaclass=PluginMeta): + """Base class for all uzbl plugins""" + + +class PerInstancePlugin(BasePlugin): + """Base class for plugins which instantiate once per uzbl instance""" + + def __init__(self, uzbl): + self.uzbl = uzbl + self.logger = uzbl.logger # we can also append plugin name to logger + + def cleanup(self): + """Cleanup state after instance is gone + + Default function avoids cyclic refrences, so don't forget to call + super() if overriding + """ + del self.uzbl + + @classmethod + def _get_instance(cls, owner): + """Returns instance of the plugin + + This method should be private to not violate TOOWTDI + """ + if not isinstance(owner, Uzbl): + raise ValueError("Can only get {0} instance for uzbl, not {1}" + .format(cls.__name__, type(owner).__name__)) + # TODO(tailhook) probably subclasses can be returned as well + return owner.plugins[cls] + + +class GlobalPlugin(BasePlugin): + """Base class for plugins which instantiate once per daemon""" + + def __init__(self, event_manager): + self.event_manager = event_manager + self.logger = logging.getLogger(self.__module__) + + @classmethod + def _get_instance(cls, owner): + """Returns instance of the plugin + + This method should be private to not violate TOOWTDI + """ + if isinstance(owner, Uzbl): + owner = owner.parent + # TODO(tailhook) probably subclasses can be returned as well + return owner.plugins[cls] + + def cleanup(self): + """Cleanup state after instance is gone + + Default function avoids cyclic refrences, so don't forget to call + super() if overriding + """ + del self.event_manager diff --git a/uzbl/net.py b/uzbl/net.py new file mode 100644 index 0000000..2c34e7b --- /dev/null +++ b/uzbl/net.py @@ -0,0 +1,109 @@ +# Network communication classes +# vi: set et ts=4: +import asyncore +import asynchat +import socket +import os +import logging + +logger = logging.getLogger('uzbl.net') + + +class NoTargetSet(Exception): + pass + + +class TargetAlreadySet(Exception): + pass + + +class WithTarget(object): + ''' + Mixin that adds a property 'target' than can only be set once and + raises an exception if not set when accesed + ''' + + @property + def target(self): + try: + return self._target + except AttributeError as e: + raise NoTargetSet("No target for %r" % self, e) + + @target.setter + def target(self, value): + if hasattr(self, '_target') and self._target is not None: + raise TargetAlreadySet( + "target of listener already set (%r)" % self._target + ) + self._target = value + + +class Listener(asyncore.dispatcher, WithTarget): + ''' Waits for new connections and accept()s them ''' + + def __init__(self, addr, target=None): + asyncore.dispatcher.__init__(self) + self.addr = addr + self.target = target + + def start(self): + self.create_socket(socket.AF_UNIX, socket.SOCK_STREAM) + self.knock() + self.set_reuse_addr() + self.bind(self.addr) + self.listen(5) + + def knock(self): + '''Unlink existing socket if it's stale''' + + if os.path.exists(self.addr): + logger.info('socket already exists, checking if active') + s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + try: + s.connect(self.addr) + except socket.error as e: + logger.info('unlinking %r', self.addr) + os.unlink(self.addr) + + def writable(self): + return False + + def handle_accept(self): + try: + sock, addr = self.accept() + except socket.error: + return + else: + self.target.add_instance(sock) + + def close(self): + super(Listener, self).close() + if os.path.exists(self.addr): + logger.info('unlinking %r', self.addr) + os.unlink(self.addr) + + def handle_error(self): + raise + + +class Protocol(asynchat.async_chat): + ''' A connection with a single client ''' + + def __init__(self, socket, target=None): + asynchat.async_chat.__init__(self, socket) + self.socket = socket + self.target = target + self.buffer = bytearray() + self.set_terminator(b'\n') + + def collect_incoming_data(self, data): + self.buffer += data + + def found_terminator(self): + val = self.buffer.decode('utf-8') + del self.buffer[:] + self.target.parse_msg(val) + + def handle_error(self): + raise diff --git a/uzbl/plugins/__init__.py b/uzbl/plugins/__init__.py new file mode 100644 index 0000000..84cf2ec --- /dev/null +++ b/uzbl/plugins/__init__.py @@ -0,0 +1,14 @@ +''' +Plugins collection + +plugins for use with uzbl-event-manager +''' + +import os.path + +plugin_path = os.environ.get("UZBL_PLUGIN_PATH", + "~/.local/share/uzbl/plugins:/usr/share/uzbl/site-plugins", + ).split(":") +if plugin_path: + __path__ = list(map(os.path.expanduser, plugin_path)) + __path__ + diff --git a/uzbl/plugins/bind.py b/uzbl/plugins/bind.py new file mode 100644 index 0000000..f40da36 --- /dev/null +++ b/uzbl/plugins/bind.py @@ -0,0 +1,463 @@ +'''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 +from functools import partial +from itertools import count + +from uzbl.arguments import unquote, splitquoted +from uzbl.ext import PerInstancePlugin +from .cmd_expand import cmd_expand +from .config import Config +from .keycmd import KeyCmd +import collections + +# Commonly used regular expressions. +MOD_START = re.compile('^<([A-Z][A-Za-z0-9-_]*)>').match +# Matches , <'x':y>, <:'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 = list(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.uzbl_config = Config[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_config['mode'] = mode + + del self.uzbl_config['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 + + mode = self.uzbl_config.get('mode', None) + if mode != 'stack': + self.last_mode = mode + self.uzbl_config['mode'] = 'stack' + + self.stack_binds = [bind,] + self.args += args + 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 + + KeyCmd[self.uzbl].clear_keycmd() + if prompt: + self.uzbl_config['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_config.get('mode', None) + + 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 [_f for _f in list(globals.values()) if _f] + + binds = dict(list(globals.items()) + list(self.binds[mode].items())) + return [_f for _f in list(binds.values()) if _f] + + + 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 list(binds.values()): + if bind is not None and bind.is_global: + self.globals.append(bind) + + +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 "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) + + +class Bind(object): + + # unique id generator + nextid = count().__next__ + + def __init__(self, glob, handler, *args, **kargs): + self.is_callable = isinstance(handler, collections.Callable) + 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 not isinstance(handler, str): + self.commands = handler + + else: + self.commands = [handler,] + list(args) + + self.glob = glob + + # Assign unique id. + self.bid = self.nextid() + + 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 = list(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** + 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_ + 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 = '' % ', '.join(args) + return self._repr_cache + + +class BindPlugin(PerInstancePlugin): + def __init__(self, uzbl): + '''Export functions and connect handlers to events.''' + super(BindPlugin, self).__init__(uzbl) + + self.bindlet = Bindlet(uzbl) + + uzbl.connect('BIND', self.parse_bind) + uzbl.connect('MODE_BIND', self.parse_mode_bind) + uzbl.connect('MODE_CHANGED', self.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] + handler = partial(self.key_event, + mod_cmd=bool(mod_cmd), + on_exec=bool(on_exec)) + uzbl.connect(event, handler) + + def exec_bind(self, bind, *args, **kargs): + '''Execute bind objects.''' + + self.uzbl.event("EXEC_BIND", bind, args, kargs) + + if bind.is_callable: + args += bind.args + kargs = dict(list(bind.kargs.items())+list(kargs.items())) + bind.function(self.uzbl, *args, **kargs) + return + + if kargs: + raise ArgumentError('cannot supply kargs for uzbl commands') + + commands = [] + for cmd in bind.commands: + cmd = cmd_expand(cmd, args) + self.uzbl.send(cmd) + + def mode_bind(self, modes, glob, handler=None, *args, **kargs): + '''Add a mode bind.''' + + bindlet = self.bindlet + + if isinstance(modes, str): + modes = modes.split(',') + + # Sort and filter binds. + modes = [_f for _f in map(str.strip, modes) if _f] + + if isinstance(handler, collections.Callable) 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) + self.uzbl.event('ADDED_MODE_BIND', mode, glob, bind) + self.logger.info('added bind %s %s %s', mode, glob, bind) + + def bind(self, glob, handler, *args, **kargs): + '''Legacy bind function.''' + + self.mode_bind('global', glob, handler, *args, **kargs) + + def parse_mode_bind(self, args): + '''Parser for the MODE_BIND event. + + Example events: + MODE_BIND = + MODE_BIND command o_ = uri %s + MODE_BIND insert,command = ... + MODE_BIND global ... = ... + MODE_BIND global,-insert ... = ... + ''' + + args = splitquoted(args) + if len(args) < 2: + raise ArgumentError('missing mode or bind section: %r' % args.raw()) + + modes = args[0].split(',') + for i, g in enumerate(args[1:]): + if g == '=': + glob = args.raw(1, i) + command = args.raw(i+2) + break + else: + raise ArgumentError('missing delimiter in bind section: %r' % args.raw()) + + self.mode_bind(modes, glob, command) + + def parse_bind(self, args): + '''Legacy parsing of the BIND event and conversion to the new format. + + Example events: + request BIND = + request BIND o_ = uri %s + request BIND = ... + request BIND ... = ... + ''' + + self.parse_mode_bind("global %s" % args) + + def mode_changed(self, mode): + '''Clear the stack on all non-stack mode changes.''' + + if mode != 'stack': + self.bindlet.reset() + + def match_and_exec(self, bind, depth, modstate, 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 modstate != 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): + self.exec_bind(bind, *args) + if not has_args: + KeyCmd[self.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: + self.exec_bind(uzbl, bind, *(args+['',])) + + return False + + args = bindlet.args + args + self.exec_bind(bind, *args) + if not has_args or on_exec: + config = Config[self.uzbl] + del config['mode'] + bindlet.reset() + + return True + + def key_event(self, modstate, keylet, mod_cmd=False, on_exec=False): + bindlet = self.bindlet + 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 self.match_and_exec(bind, depth, modstate, 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: + config = Config[uzbl] + del config['mode'] + +# vi: set et ts=4: diff --git a/uzbl/plugins/cmd_expand.py b/uzbl/plugins/cmd_expand.py new file mode 100644 index 0000000..46e1e67 --- /dev/null +++ b/uzbl/plugins/cmd_expand.py @@ -0,0 +1,36 @@ +def escape(str): + for (level, char) in [(3, '\\'), (2, "'"), (2, '"'), (1, '@')]: + str = str.replace(char, (level * '\\') + char) + + return str + + +def cmd_expand(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 = list(map(str, 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, str(arg)) + + return cmd diff --git a/uzbl/plugins/completion.py b/uzbl/plugins/completion.py new file mode 100644 index 0000000..ef2f277 --- /dev/null +++ b/uzbl/plugins/completion.py @@ -0,0 +1,186 @@ +'''Keycmd completion.''' + +import re + +from uzbl.arguments import splitquoted +from uzbl.ext import PerInstancePlugin +from .config import Config +from .keycmd import KeyCmd + +# Completion level +NONE, ONCE, LIST, COMPLETE = list(range(4)) + +# The reverse keyword finding re. +FIND_SEGMENT = re.compile("(\@[\w_]+|set[\s]+[\w_]+|[\w_]+)$").findall + + +def escape(str): + return str.replace("@", "\@") + + +class Completions(set): + def __init__(self): + set.__init__(self) + self.locked = False + self.level = NONE + + def lock(self): + self.locked = True + + def unlock(self): + self.locked = False + + def add_var(self, var): + self.add('@' + var) + + +class CompletionListFormatter(object): + LIST_FORMAT = " %s " + ITEM_FORMAT = "%s%s" + + def format(self, partial, completions): + p = len(partial) + completions.sort() + return self.LIST_FORMAT % ' '.join( + [self.ITEM_FORMAT % (escape(h[:p]), h[p:]) for h in completions] + ) + + +class CompletionPlugin(PerInstancePlugin): + def __init__(self, uzbl): + '''Export functions and connect handlers to events.''' + super(CompletionPlugin, self).__init__(uzbl) + + self.completion = Completions() + self.listformatter = CompletionListFormatter() + + uzbl.connect('BUILTINS', self.add_builtins) + uzbl.connect('CONFIG_CHANGED', self.add_config_key) + uzbl.connect('KEYCMD_CLEARED', self.stop_completion) + uzbl.connect('KEYCMD_EXEC', self.stop_completion) + uzbl.connect('KEYCMD_UPDATE', self.update_completion_list) + uzbl.connect('START_COMPLETION', self.start_completion) + uzbl.connect('STOP_COMPLETION', self.stop_completion) + + uzbl.send('dump_config_as_events') + + def get_incomplete_keyword(self): + '''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 = KeyCmd[self.uzbl].keylet + left_segment = keylet.keycmd[:keylet.cursor] + partial = (FIND_SEGMENT(left_segment) + ['', ])[0].lstrip() + if partial.startswith('set '): + return ('@' + partial[4:].lstrip(), True) + + return (partial, False) + + def stop_completion(self, *args): + '''Stop command completion and return the level to NONE.''' + + self.completion.level = NONE + if 'completion_list' in Config[self.uzbl]: + del Config[self.uzbl]['completion_list'] + + def complete_completion(self, 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):] + + KeyCmd[self.uzbl].inject_keycmd(remainder) + self.stop_completion() + + def partial_completion(self, partial, hint): + '''Inject a common portion of the hints into the keycmd.''' + + remainder = hint[len(partial):] + KeyCmd[self.uzbl].inject_keycmd(remainder) + + def update_completion_list(self, *args): + '''Checks if the user still has a partially completed keyword under his + cursor then update the completion hints list.''' + + partial = self.get_incomplete_keyword()[0] + if not partial: + return self.stop_completion() + + if self.completion.level < LIST: + return + + config = Config[self.uzbl] + + hints = [h for h in self.completion if h.startswith(partial)] + if not hints: + del config['completion_list'] + return + + config['completion_list'] = self.listformatter.format(partial, hints) + + def start_completion(self, *args): + if self.completion.locked: + return + + (partial, set_completion) = self.get_incomplete_keyword() + if not partial: + return self.stop_completion() + + if self.completion.level < COMPLETE: + self.completion.level += 1 + + hints = [h for h in self.completion if h.startswith(partial)] + if not hints: + return + + elif len(hints) == 1: + self.completion.lock() + self.complete_completion(partial, hints[0], set_completion) + self.completion.unlock() + return + + elif partial in hints and completion.level == COMPLETE: + self.completion.lock() + self.complete_completion(partial, partial, set_completion) + self.completion.unlock() + 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: + self.completion.lock() + self.partial_completion(partial, partial + common) + self.completion.unlock() + + self.update_completion_list() + + def add_builtins(self, builtins): + '''Pump the space delimited list of builtin commands into the + builtin list.''' + + builtins = splitquoted(builtins) + self.completion.update(builtins) + + def add_config_key(self, key, value): + '''Listen on the CONFIG_CHANGED event and add config keys to the variable + list for @var like expansion support.''' + + self.completion.add_var(key) diff --git a/uzbl/plugins/config.py b/uzbl/plugins/config.py new file mode 100644 index 0000000..4f00315 --- /dev/null +++ b/uzbl/plugins/config.py @@ -0,0 +1,109 @@ +from re import compile + +from uzbl.arguments import splitquoted +from uzbl.ext import PerInstancePlugin + +types = {'int': int, 'float': float, 'str': str} + +valid_key = compile('^[A-Za-z0-9_\.]+$').match + +class Config(PerInstancePlugin): + """Configuration plugin, has dictionary interface for config access + + This class is currenty not inherited from either UserDict or abc.Mapping + because not sure what version of python we want to support. It's not + hard to implement all needed methods either. + """ + + def __init__(self, uzbl): + super(Config, self).__init__(uzbl) + + self.data = {} + uzbl.connect('VARIABLE_SET', self.parse_set_event) + assert not 'a' in self.data + + def __getitem__(self, key): + return self.data[key] + + def __setitem__(self, key, value): + self.set(key, value) + + def __delitem__(self, key): + self.set(key) + + def get(self, key, default=None): + return self.data.get(key, default) + + def __contains__(self, key): + return key in self.data + + def keys(self): + return iter(self.data.keys()) + + def items(self): + return iter(self.data.items()) + + def update(self, other=None, **kwargs): + if other is None: + other = {} + + for (key, value) in list(dict(other).items()) + list(kwargs.items()): + self[key] = value + + + def set(self, key, value='', force=False): + '''Generates a `set = ` command string to send to the + current uzbl instance. + + Note that the config dict isn't updated by this function. The config + dict is only updated after a successful `VARIABLE_SET ..` event + returns from the uzbl instance.''' + + assert valid_key(key) + + if isinstance(value, bool): + value = int(value) + + else: + value = str(value) + assert '\n' not in value + + if not force and key in self and self[key] == value: + return + + self.uzbl.send('set %s = %s' % (key, value)) + + + def parse_set_event(self, args): + '''Parse `VARIABLE_SET ` event and load the + (key, value) pair into the `uzbl.config` dict.''' + + args = splitquoted(args) + if len(args) == 2: + key, type, raw_value = args[0], args[1], '' + elif len(args) == 3: + key, type, raw_value = args + else: + raise Exception('Invalid number of arguments') + + assert valid_key(key) + assert type in types + + new_value = types[type](raw_value) + old_value = self.data.get(key, None) + + # Update new value. + self.data[key] = new_value + + if old_value != new_value: + self.uzbl.event('CONFIG_CHANGED', key, new_value) + + # Cleanup null config values. + if type == 'str' and not new_value: + del self.data[key] + + def cleanup(self): + # not sure it's needed, but safer for cyclic links + self.data.clear() + super(Config, self).cleanup() + diff --git a/uzbl/plugins/cookies.py b/uzbl/plugins/cookies.py new file mode 100644 index 0000000..8875e99 --- /dev/null +++ b/uzbl/plugins/cookies.py @@ -0,0 +1,226 @@ +""" Basic cookie manager + forwards cookies to all other instances connected to the event manager""" + +from collections import defaultdict +import os, re, stat + +from uzbl.arguments import splitquoted +from uzbl.ext import GlobalPlugin, PerInstancePlugin + +# these are symbolic names for the components of the cookie tuple +symbolic = {'domain': 0, 'path':1, 'name':2, 'value':3, 'scheme':4, 'expires':5} + +# allows for partial cookies +# ? allow wildcard in key +def match(key, cookie): + for k,c in zip(key,cookie): + if k != c: + return False + return True + +def match_list(_list, cookie): + for matcher in _list: + for component, match in matcher: + if match(cookie[component]) is None: + break + else: + return True + return False + +def add_cookie_matcher(_list, arg): + ''' add a cookie matcher to a whitelist or a blacklist. + a matcher is a list of (component, re) tuples that matches a cookie when the + "component" part of the cookie matches the regular expression "re". + "component" is one of the keys defined in the variable "symbolic" above, + or the index of a component of a cookie tuple. + ''' + + args = splitquoted(arg) + mlist = [] + for (component, regexp) in zip(args[0::2], args[1::2]): + try: + component = symbolic[component] + except KeyError: + component = int(component) + assert component <= 5 + mlist.append((component, re.compile(regexp).search)) + _list.append(mlist) + +class NullStore(object): + def add_cookie(self, rawcookie, cookie): + pass + + def delete_cookie(self, rkey, key): + pass + +class ListStore(list): + def add_cookie(self, rawcookie, cookie): + self.append(rawcookie) + + def delete_cookie(self, rkey, key): + self[:] = [x for x in self if not match(key, splitquoted(x))] + +class TextStore(object): + def __init__(self, filename): + self.filename = filename + try: + # make sure existing cookie jar is not world-open + perm_mode = os.stat(self.filename).st_mode + if (perm_mode & (stat.S_IRWXO | stat.S_IRWXG)) > 0: + safe_perm = stat.S_IMODE(perm_mode) & ~(stat.S_IRWXO | stat.S_IRWXG) + os.chmod(self.filename, safe_perm) + except OSError: + pass + + def as_event(self, cookie): + """Convert cookie.txt row to uzbls cookie event format""" + scheme = { + 'TRUE' : 'https', + 'FALSE' : 'http' + } + extra = '' + if cookie[0].startswith("#HttpOnly_"): + extra = 'Only' + domain = cookie[0][len("#HttpOnly_"):] + elif cookie[0].startswith('#'): + return None + else: + domain = cookie[0] + try: + return (domain, + cookie[2], + cookie[5], + cookie[6], + scheme[cookie[3]] + extra, + cookie[4]) + except (KeyError,IndexError): + # Let malformed rows pass through like comments + return None + + def as_file(self, cookie): + """Convert cookie event to cookie.txt row""" + secure = { + 'https' : 'TRUE', + 'http' : 'FALSE', + 'httpsOnly' : 'TRUE', + 'httpOnly' : 'FALSE' + } + http_only = { + 'https' : '', + 'http' : '', + 'httpsOnly' : '#HttpOnly_', + 'httpOnly' : '#HttpOnly_' + } + return (http_only[cookie[4]] + cookie[0], + 'TRUE' if cookie[0].startswith('.') else 'FALSE', + cookie[1], + secure[cookie[4]], + cookie[5], + cookie[2], + cookie[3]) + + def add_cookie(self, rawcookie, cookie): + assert len(cookie) == 6 + + # delete equal cookies (ignoring expire time, value and secure flag) + self.delete_cookie(None, cookie[:-3]) + + # restrict umask before creating the cookie jar + curmask=os.umask(0) + os.umask(curmask| stat.S_IRWXO | stat.S_IRWXG) + + first = not os.path.exists(self.filename) + with open(self.filename, 'a') as f: + if first: + print("# HTTP Cookie File", file=f) + print('\t'.join(self.as_file(cookie)), file=f) + + def delete_cookie(self, rkey, key): + if not os.path.exists(self.filename): + return + + # restrict umask before creating the cookie jar + curmask=os.umask(0) + os.umask(curmask | stat.S_IRWXO | stat.S_IRWXG) + + # read all cookies + with open(self.filename, 'r') as f: + cookies = f.readlines() + + # write those that don't match the cookie to delete + with open(self.filename, 'w') as f: + for l in cookies: + c = self.as_event(l.split('\t')) + if c is None or not match(key, c): + print(l, end='', file=f) + os.umask(curmask) + +xdg_data_home = os.environ.get('XDG_DATA_HOME', os.path.join(os.environ['HOME'], '.local/share')) +DefaultStore = TextStore(os.path.join(xdg_data_home, 'uzbl/cookies.txt')) +SessionStore = TextStore(os.path.join(xdg_data_home, 'uzbl/session-cookies.txt')) + +class Cookies(PerInstancePlugin): + def __init__(self, uzbl): + super(Cookies, self).__init__(uzbl) + + self.whitelist = [] + self.blacklist = [] + + uzbl.connect('ADD_COOKIE', self.add_cookie) + uzbl.connect('DELETE_COOKIE', self.delete_cookie) + uzbl.connect('BLACKLIST_COOKIE', self.blacklist_cookie) + uzbl.connect('WHITELIST_COOKIE', self.whitelist_cookie) + + # accept a cookie only when: + # a. there is no whitelist and the cookie is in the blacklist + # b. the cookie is in the whitelist and not in the blacklist + def accept_cookie(self, cookie): + if self.whitelist: + if match_list(self.whitelist, cookie): + return not match_list(self.blacklist, cookie) + return False + + return not match_list(self.blacklist, cookie) + + def expires_with_session(self, cookie): + return cookie[5] == '' + + def get_recipents(self): + """ get a list of Uzbl instances to send the cookie too. """ + # This could be a lot more interesting + return [u for u in list(self.uzbl.parent.uzbls.values()) if u is not self.uzbl] + + def get_store(self, session=False): + if session: + return SessionStore + return DefaultStore + + def add_cookie(self, cookie): + cookie = splitquoted(cookie) + if self.accept_cookie(cookie): + for u in self.get_recipents(): + u.send('add_cookie %s' % cookie.raw()) + + self.get_store(self.expires_with_session(cookie)).add_cookie(cookie.raw(), cookie) + else: + self.logger.debug('cookie %r is blacklisted', cookie) + self.uzbl.send('delete_cookie %s' % cookie.raw()) + + def delete_cookie(self, cookie): + cookie = splitquoted(cookie) + for u in self.get_recipents(): + u.send('delete_cookie %s' % cookie.raw()) + + if len(cookie) == 6: + self.get_store(self.expires_with_session(cookie)).delete_cookie(cookie.raw(), cookie) + else: + for store in set([self.get_store(session) for session in (True, False)]): + store.delete_cookie(cookie.raw(), cookie) + + def blacklist_cookie(self, arg): + add_cookie_matcher(self.blacklist, arg) + + def whitelist_cookie(self, arg): + add_cookie_matcher(self.whitelist, arg) + +# vi: set et ts=4: diff --git a/uzbl/plugins/downloads.py b/uzbl/plugins/downloads.py new file mode 100644 index 0000000..208dbda --- /dev/null +++ b/uzbl/plugins/downloads.py @@ -0,0 +1,83 @@ +# this plugin does a very simple display of download progress. to use it, add +# @downloads to your status_format. + +import os +import html + +from uzbl.arguments import splitquoted +from .config import Config +from uzbl.ext import PerInstancePlugin + +class Downloads(PerInstancePlugin): + + def __init__(self, uzbl): + super(Downloads, self).__init__(uzbl) + uzbl.connect('DOWNLOAD_STARTED', self.download_started) + uzbl.connect('DOWNLOAD_PROGRESS', self.download_progress) + uzbl.connect('DOWNLOAD_COMPLETE', self.download_complete) + self.active_downloads = {} + + def update_download_section(self): + """after a download's status has changed this + is called to update the status bar + """ + + if self.active_downloads: + # add a newline before we list downloads + result = ' downloads:' + for path, progress in list(self.active_downloads.items()): + # add each download + fn = os.path.basename(path) + + dl = " %s (%d%%)" % (fn, progress * 100) + + # replace entities to make sure we don't break our markup + # (this could be done with an @[]@ expansion in uzbl, but then we + # can't use the above to make a new line) + dl = html.escape(dl) + result += dl + else: + result = '' + + # and the result gets saved to an uzbl variable that can be used in + # status_format + config = Config[self.uzbl] + if config.get('downloads', '') != result: + config['downloads'] = result + + def download_started(self, args): + # parse the arguments + args = splitquoted(args) + destination_path = args[0] + + # add to the list of active downloads + self.active_downloads[destination_path] = 0.0 + + # update the progress + self.update_download_section() + + def download_progress(self, args): + # parse the arguments + args = splitquoted(args) + destination_path = args[0] + progress = float(args[1]) + + # update the progress + self.active_downloads[destination_path] = progress + + # update the status bar variable + self.update_download_section() + + def download_complete(self, args): + # TODO(tailhook) be more userfriendly: show download for some time! + + # parse the arguments + args = splitquoted(args) + destination_path = args[0] + + # remove from the list of active downloads + del self.active_downloads[destination_path] + + # update the status bar variable + self.update_download_section() + diff --git a/uzbl/plugins/history.py b/uzbl/plugins/history.py new file mode 100644 index 0000000..1a0e0fb --- /dev/null +++ b/uzbl/plugins/history.py @@ -0,0 +1,148 @@ +import random + +from .on_set import OnSetPlugin +from .keycmd import KeyCmd +from uzbl.ext import GlobalPlugin, PerInstancePlugin + +class SharedHistory(GlobalPlugin): + + def __init__(self, event_manager): + super(SharedHistory, self).__init__(event_manager) + self.history = {} #TODO(tailhook) save and load from file + + def get_line_number(self, prompt): + try: + return len(self.history[prompt]) + except KeyError: + return 0 + + def addline(self, prompt, entry): + lst = self.history.get(prompt) + if lst is None: + self.history[prompt] = [entry] + else: + lst.append(entry) + + def getline(self, prompt, index): + try: + return self.history[prompt][index] + except KeyError: + # not existent list is same as empty one + raise IndexError() + + +class History(PerInstancePlugin): + + def __init__(self, uzbl): + super(History, self).__init__(uzbl) + self._tail = '' + self.prompt = '' + self.cursor = None + self.search_key = None + uzbl.connect('KEYCMD_EXEC', self.keycmd_exec) + uzbl.connect('HISTORY_PREV', self.history_prev) + uzbl.connect('HISTORY_NEXT', self.history_next) + uzbl.connect('HISTORY_SEARCH', self.history_search) + OnSetPlugin[uzbl].on_set('keycmd_prompt', + lambda uzbl, k, v: self.change_prompt(v)) + + def prev(self): + shared = SharedHistory[self.uzbl] + if self.cursor is None: + self.cursor = shared.get_line_number(self.prompt) - 1 + else: + self.cursor -= 1 + + if self.search_key: + while self.cursor >= 0: + line = shared.getline(self.prompt, self.cursor) + if self.search_key in line: + return line + self.cursor -= 1 + + if self.cursor >= 0: + return shared.getline(self.prompt, self.cursor) + + self.cursor = -1 + return random.choice(end_messages) + + def __next__(self): + if self.cursor is None: + return '' + shared = SharedHistory[self.uzbl] + + self.cursor += 1 + + num = shared.get_line_number(self.prompt) + if self.search_key: + while self.cursor < num: + line = shared.getline(self.prompt, self.cursor) + if self.search_key in line: + return line + self.cursor += 1 + + if self.cursor >= num: + self.cursor = None + self.search_key = None + if self._tail: + value = self._tail + self._tail = None + return value + return '' + return shared.getline(self.prompt, self.cursor) + + def change_prompt(self, prompt): + self.prompt = prompt + self._tail = None + + def search(self, key): + self.search_key = key + self.cursor = None + + def __str__(self): + return "(History %s, %s)" % (self.cursor, self.prompt) + + def keycmd_exec(self, modstate, keylet): + cmd = keylet.get_keycmd() + if cmd: + SharedHistory[self.uzbl].addline(self.prompt, cmd) + self._tail = None + self.cursor = None + self.search_key = None + + def history_prev(self, _x): + cmd = KeyCmd[self.uzbl].keylet.get_keycmd() + if self.cursor is None and cmd: + self._tail = cmd + val = self.prev() + KeyCmd[self.uzbl].set_keycmd(val) + + def history_next(self, _x): + KeyCmd[self.uzbl].set_keycmd(next(self)) + + def history_search(self, key): + self.search(key) + self.uzbl.send('event HISTORY_PREV') + +end_messages = ( + 'Look behind you, A three-headed monkey!', + 'error #4: static from nylon underwear.', + 'error #5: static from plastic slide rules.', + 'error #6: global warming.', + 'error #9: doppler effect.', + 'error #16: somebody was calculating pi on the server.', + 'error #19: floating point processor overflow.', + 'error #21: POSIX compliance problem.', + 'error #25: Decreasing electron flux.', + 'error #26: first Saturday after first full moon in Winter.', + 'error #64: CPU needs recalibration.', + 'error #116: the real ttys became pseudo ttys and vice-versa.', + 'error #229: wrong polarity of neutron flow.', + 'error #330: quantum decoherence.', + 'error #388: Bad user karma.', + 'error #407: Route flapping at the NAP.', + 'error #435: Internet shut down due to maintenance.', + ) + + +# vi: set et ts=4: diff --git a/uzbl/plugins/keycmd.py b/uzbl/plugins/keycmd.py new file mode 100644 index 0000000..2020fda --- /dev/null +++ b/uzbl/plugins/keycmd.py @@ -0,0 +1,503 @@ +import re + +from uzbl.arguments import splitquoted +from uzbl.ext import PerInstancePlugin +from .config import Config + +# Keycmd format which includes the markup for the cursor. +KEYCMD_FORMAT = "%s%s%s" +MODCMD_FORMAT = " %s " + + +# FIXME, add utility functions to shared module +def escape(str): + for char in ['\\', '@']: + str = str.replace(char, '\\'+char) + + return str + + +def uzbl_escape(str): + return "@[%s]@" % escape(str) if str else '' + + +def inject_str(str, index, inj): + '''Inject a string into string at at given index.''' + + return "%s%s%s" % (str[:index], inj, str[index:]) + + +class Keylet(object): + '''Small per-instance object that tracks characters typed. + + >>> k = Keylet() + >>> k.set_keycmd('spam') + >>> print(k) + + >>> k.append_keycmd(' and egg') + >>> print(k) + + >>> print(k.cursor) + 12 + ''' + + def __init__(self): + # Modcmd tracking + self.modcmd = '' + self.is_modcmd = False + + # Keycmd tracking + self.keycmd = '' + self.cursor = 0 + + def get_keycmd(self): + ''' Get the keycmd-part of the keylet. ''' + + return self.keycmd + + def clear_keycmd(self): + ''' Clears the keycmd part of the keylet ''' + + self.keycmd = '' + self.cursor = 0 + + def get_modcmd(self): + ''' Get the modcmd-part of the keylet. ''' + + if not self.is_modcmd: + return '' + + return self.modcmd + + def clear_modcmd(self): + self.modcmd = '' + self.is_modcmd = False + + def set_keycmd(self, keycmd): + self.keycmd = keycmd + self.cursor = len(keycmd) + + def insert_keycmd(self, s): + ''' Inserts string at the current position + + >>> k = Keylet() + >>> k.set_keycmd('spam') + >>> k.cursor = 1 + >>> k.insert_keycmd('egg') + >>> print(k) + + >>> print(k.cursor) + 4 + ''' + + self.keycmd = inject_str(self.keycmd, self.cursor, s) + self.cursor += len(s) + + def append_keycmd(self, s): + ''' Appends string to to end of keycmd and moves the cursor + + >>> k = Keylet() + >>> k.set_keycmd('spam') + >>> k.cursor = 1 + >>> k.append_keycmd('egg') + >>> print(k) + + >>> print(k.cursor) + 7 + ''' + + self.keycmd += s + self.cursor = len(self.keycmd) + + def backspace(self): + ''' Removes the character at the cursor position. ''' + if not self.keycmd or not self.cursor: + return False + + self.keycmd = self.keycmd[:self.cursor-1] + self.keycmd[self.cursor:] + self.cursor -= 1 + return True + + def delete(self): + ''' Removes the character after the cursor position. ''' + if not self.keycmd: + return False + + self.keycmd = self.keycmd[:self.cursor] + self.keycmd[self.cursor+1:] + return True + + def strip_word(self, seps=' '): + ''' Removes the last word from the keycmd, similar to readline ^W + returns the part removed or None + + >>> k = Keylet() + >>> k.set_keycmd('spam and egg') + >>> k.strip_word() + 'egg' + >>> print(k) + + >>> k.strip_word() + 'and' + >>> print(k) + + ''' + if not self.keycmd: + return None + + head, tail = self.keycmd[:self.cursor].rstrip(seps), self.keycmd[self.cursor:] + rfind = -1 + for sep in seps: + p = head.rfind(sep) + if p >= 0 and rfind < p + 1: + rfind = p + 1 + if rfind == len(head) and head[-1] in seps: + rfind -= 1 + self.keycmd = head[:rfind] if rfind + 1 else '' + tail + self.cursor = len(head) + return head[rfind:] + + def set_cursor_pos(self, index): + ''' Sets the cursor position, Supports negative indexing and relative + stepping with '+' and '-'. + Returns the new cursor position + + >>> k = Keylet() + >>> k.set_keycmd('spam and egg') + >>> k.set_cursor_pos(2) + 2 + >>> k.set_cursor_pos(-3) + 10 + >>> k.set_cursor_pos('+') + 11 + ''' + + if index == '-': + cursor = self.cursor - 1 + + elif index == '+': + cursor = self.cursor + 1 + + else: + cursor = int(index) + if cursor < 0: + cursor = len(self.keycmd) + cursor + 1 + + if cursor < 0: + cursor = 0 + + if cursor > len(self.keycmd): + cursor = len(self.keycmd) + + self.cursor = cursor + return self.cursor + + def markup(self): + ''' Returns the keycmd with the cursor in pango markup spliced in + + >>> k = Keylet() + >>> k.set_keycmd('spam and egg') + >>> k.set_cursor_pos(4) + 4 + >>> k.markup() + '@[spam]@@[ ]@@[and egg]@' + ''' + + if self.cursor < len(self.keycmd): + curchar = self.keycmd[self.cursor] + else: + curchar = ' ' + chunks = [self.keycmd[:self.cursor], curchar, self.keycmd[self.cursor+1:]] + return KEYCMD_FORMAT % tuple(map(uzbl_escape, chunks)) + + def __repr__(self): + ''' Return a string representation of the keylet. ''' + + l = [] + if self.is_modcmd: + l.append('modcmd=%r' % self.get_modcmd()) + + if self.keycmd: + l.append('keycmd=%r' % self.get_keycmd()) + + return '' % ', '.join(l) + + +class KeyCmd(PerInstancePlugin): + def __init__(self, uzbl): + '''Export functions and connect handlers to events.''' + super(KeyCmd, self).__init__(uzbl) + + self.keylet = Keylet() + self.modmaps = {} + self.ignores = {} + + uzbl.connect('APPEND_KEYCMD', self.append_keycmd) + uzbl.connect('IGNORE_KEY', self.add_key_ignore) + uzbl.connect('INJECT_KEYCMD', self.inject_keycmd) + uzbl.connect('KEYCMD_BACKSPACE', self.keycmd_backspace) + uzbl.connect('KEYCMD_DELETE', self.keycmd_delete) + uzbl.connect('KEYCMD_EXEC_CURRENT', self.keycmd_exec_current) + uzbl.connect('KEYCMD_STRIP_WORD', self.keycmd_strip_word) + uzbl.connect('KEYCMD_CLEAR', self.clear_keycmd) + uzbl.connect('KEY_PRESS', self.key_press) + uzbl.connect('KEY_RELEASE', self.key_release) + uzbl.connect('MOD_PRESS', self.key_press) + uzbl.connect('MOD_RELEASE', self.key_release) + uzbl.connect('MODMAP', self.modmap_parse) + uzbl.connect('SET_CURSOR_POS', self.set_cursor_pos) + uzbl.connect('SET_KEYCMD', self.set_keycmd) + + 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 key_ignored(self, key): + '''Check if the given key is ignored by any ignore rules.''' + + for (glob, match) in list(self.ignores.items()): + if match(key): + return True + + return False + + def add_modmap(self, key, map): + '''Add modmaps. + + Examples: + set modmap = request MODMAP + @modmap + @modmap + ... + + Then: + @bind = + @bind x = + ... + + ''' + + assert len(key) + modmaps = self.modmaps + + modmaps[key.strip('<>')] = map.strip('<>') + self.uzbl.event("NEW_MODMAP", key, map) + + def modmap_parse(self, map): + '''Parse a modmap definiton.''' + + split = splitquoted(map) + + if not split or len(split) > 2: + raise Exception('Invalid modmap arugments: %r' % map) + + self.add_modmap(*split) + + def add_key_ignore(self, glob): + '''Add an ignore definition. + + Examples: + set ignore_key = request IGNORE_KEY + @ignore_key + @ignore_key + ... + ''' + + assert len(glob) > 1 + ignores = self.ignores + + glob = "<%s>" % glob.strip("<> ") + restr = glob.replace('*', '[^\s]*') + match = re.compile(restr).match + + ignores[glob] = match + self.uzbl.event('NEW_KEY_IGNORE', glob) + + def clear_keycmd(self, *args): + '''Clear the keycmd for this uzbl instance.''' + + self.keylet.clear_keycmd() + config = Config[self.uzbl] + del config['keycmd'] + self.uzbl.event('KEYCMD_CLEARED') + + def clear_modcmd(self): + '''Clear the modcmd for this uzbl instance.''' + + self.keylet.clear_modcmd() + + config = Config[self.uzbl] + del config['modcmd'] + self.uzbl.event('MODCMD_CLEARED') + + def clear_current(self): + '''Clear the modcmd if is_modcmd else clear keycmd.''' + + if self.keylet.is_modcmd: + self.clear_modcmd() + + else: + self.clear_keycmd() + + def update_event(self, modstate, k, execute=True): + '''Raise keycmd & modcmd update events.''' + + keycmd, modcmd = k.get_keycmd(), ''.join(modstate) + k.get_modcmd() + + if k.is_modcmd: + self.logger.debug('modcmd_update, %s', modcmd) + self.uzbl.event('MODCMD_UPDATE', modstate, k) + + else: + self.logger.debug('keycmd_update, %s', keycmd) + self.uzbl.event('KEYCMD_UPDATE', modstate, k) + + config = Config[self.uzbl] + if config.get('modcmd_updates', '1') == '1': + new_modcmd = ''.join(modstate) + k.get_modcmd() + if not new_modcmd or not k.is_modcmd: + if 'modcmd' in config: + del config['modcmd'] + + elif new_modcmd == modcmd: + config['modcmd'] = MODCMD_FORMAT % uzbl_escape(modcmd) + + if config.get('keycmd_events', '1') != '1': + return + + new_keycmd = k.get_keycmd() + if not new_keycmd: + del config['keycmd'] + + elif new_keycmd == keycmd: + # Generate the pango markup for the cursor in the keycmd. + config['keycmd'] = str(k.markup()) + + def parse_key_event(self, key): + ''' Build a set from the modstate part of the event, and pass all keys through modmap ''' + + modstate, key = splitquoted(key) + modstate = set(['<%s>' % self.modmap_key(k) for k in modstate.split('|') if k]) + + key = self.modmap_key(key) + return modstate, key + + def key_press(self, key): + '''Handle KEY_PRESS events. Things done by this function include: + + 1. Ignore all shift key presses (shift can be detected by capital chars) + 2. In non-modcmd mode: + a. append char to keycmd + 3. If not in modcmd mode and a modkey was pressed set modcmd mode. + 4. Keycmd is updated and events raised if anything is changed.''' + + k = self.keylet + config = Config[self.uzbl] + modstate, key = self.parse_key_event(key) + k.is_modcmd = any(not self.key_ignored(m) for m in modstate) + + self.logger.debug('key press modstate=%s', modstate) + if key.lower() == 'space' and not k.is_modcmd and k.keycmd: + k.insert_keycmd(' ') + + elif not k.is_modcmd and len(key) == 1: + if config.get('keycmd_events', '1') != '1': + # TODO, make a note on what's going on here + k.keycmd = '' + k.cursor = 0 + del config['keycmd'] + return + + k.insert_keycmd(key) + + elif len(key) == 1: + k.modcmd += key + + else: + if not self.key_ignored('<%s>' % key): + modstate.add('<%s>' % key) + k.is_modcmd = True + + self.update_event(modstate, k) + + def key_release(self, key): + '''Respond to KEY_RELEASE event. Things done by this function include: + + 1. If in a mod-command then raise a MODCMD_EXEC. + 2. Update the keycmd uzbl variable if anything changed.''' + k = self.keylet + modstate, key = self.parse_key_event(key) + + if len(key) > 1: + if k.is_modcmd: + self.uzbl.event('MODCMD_EXEC', modstate, k) + + self.clear_modcmd() + + def set_keycmd(self, keycmd): + '''Allow setting of the keycmd externally.''' + + self.keylet.set_keycmd(keycmd) + self.update_event(set(), self.keylet, False) + + def inject_keycmd(self, keycmd): + '''Allow injecting of a string into the keycmd at the cursor position.''' + + self.keylet.insert_keycmd(keycmd) + self.update_event(set(), self.keylet, False) + + def append_keycmd(self, keycmd): + '''Allow appening of a string to the keycmd.''' + + self.keylet.append_keycmd(keycmd) + self.update_event(set(), self.keylet, False) + + def keycmd_strip_word(self, args): + ''' Removes the last word from the keycmd, similar to readline ^W ''' + + args = splitquoted(args) + assert len(args) <= 1 + self.logger.debug('STRIPWORD %r %r', args, self.keylet) + if self.keylet.strip_word(*args): + self.update_event(set(), self.keylet, False) + + def keycmd_backspace(self, *args): + '''Removes the character at the cursor position in the keycmd.''' + + if self.keylet.backspace(): + self.update_event(set(), self.keylet, False) + + def keycmd_delete(self, *args): + '''Removes the character after the cursor position in the keycmd.''' + + if self.keylet.delete(): + self.update_event(set(), self.keylet, False) + + def keycmd_exec_current(self, *args): + '''Raise a KEYCMD_EXEC with the current keylet and then clear the + keycmd.''' + + self.uzbl.event('KEYCMD_EXEC', set(), self.keylet) + self.clear_keycmd() + + def set_cursor_pos(self, args): + '''Allow setting of the cursor position externally. Supports negative + indexing and relative stepping with '+' and '-'.''' + + args = splitquoted(args) + assert len(args) == 1 + + self.keylet.set_cursor_pos(args[0]) + self.update_event(set(), self.keylet, False) + +# vi: set et ts=4: + diff --git a/uzbl/plugins/mode.py b/uzbl/plugins/mode.py new file mode 100644 index 0000000..6eaf009 --- /dev/null +++ b/uzbl/plugins/mode.py @@ -0,0 +1,69 @@ +from collections import defaultdict + +from .on_set import OnSetPlugin +from .config import Config +from uzbl.arguments import splitquoted, is_quoted +from uzbl.ext import PerInstancePlugin + + +class ModePlugin(PerInstancePlugin): + def __init__(self, uzbl): + super(ModePlugin, self).__init__(uzbl) + self.mode_config = defaultdict(dict) + uzbl.connect('MODE_CONFIG', self.parse_mode_config) + uzbl.connect('MODE_CONFIRM', self.confirm_change) + OnSetPlugin[uzbl].on_set('mode', self.mode_updated, False) + OnSetPlugin[uzbl].on_set('default_mode', self.default_mode_updated, False) + + def cleanup(self): + self.mode_config.clear() + + def parse_mode_config(self, args): + '''Parse `MODE_CONFIG = ` event and update config + if the `` is the current mode.''' + + args = splitquoted(args) + assert len(args) >= 3, 'missing mode config args %r' % args + mode = args[0] + key = args[1] + assert args[2] == '=', 'invalid mode config set syntax' + + # Use the rest of the line verbatim as the value unless it's a + # single properly quoted string + if len(args) == 4 and is_quoted(args.raw(3)): + value = args[3] + else: + value = args.raw(3).strip() + + self.logger.debug('value %r', value) + + self.mode_config[mode][key] = value + config = Config[self.uzbl] + if config.get('mode', None) == mode: + config[key] = value + + def default_mode_updated(self, var, mode): + config = Config[self.uzbl] + if mode and not config.get('mode', None): + self.logger.debug('setting mode to default %r' % mode) + config['mode'] = mode + + def mode_updated(self, var, mode): + config = Config[self.uzbl] + if not mode: + mode = config.get('default_mode', 'command') + self.logger.debug('setting mode to default %r' % mode) + config['mode'] = mode + return + + # Load mode config + mode_config = self.mode_config.get(mode, None) + if mode_config: + config.update(mode_config) + + self.uzbl.send('event MODE_CONFIRM %s' % mode) + + def confirm_change(self, mode): + config = Config[self.uzbl] + if mode and config.get('mode', None) == mode: + self.uzbl.event('MODE_CHANGED', mode) diff --git a/uzbl/plugins/on_event.py b/uzbl/plugins/on_event.py new file mode 100644 index 0000000..cf33799 --- /dev/null +++ b/uzbl/plugins/on_event.py @@ -0,0 +1,106 @@ +'''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 re +import fnmatch +from functools import partial + +from uzbl.arguments import splitquoted +from .cmd_expand import cmd_expand +from uzbl.ext import PerInstancePlugin + +def match_args(pattern, args): + if len(pattern) > len(args): + return False + for p, a in zip(pattern, args): + if not fnmatch.fnmatch(a, p): + return False + return True + + +class OnEventPlugin(PerInstancePlugin): + + def __init__(self, uzbl): + '''Export functions and connect handlers to events.''' + super(OnEventPlugin, self).__init__(uzbl) + + self.events = {} + + uzbl.connect('ON_EVENT', self.parse_on_event) + + def event_handler(self, *args, **kargs): + '''This function handles all the events being watched by various + on_event definitions and responds accordingly.''' + + # Could be connected to a EM internal event that can use anything as args + if len(args) == 1 and isinstance(args[0], str): + args = splitquoted(args[0]) + + event = kargs['on_event'] + if event not in self.events: + return + + commands = self.events[event] + for cmd, pattern in list(commands.items()): + if not pattern or match_args(pattern, args): + cmd = cmd_expand(cmd, args) + self.uzbl.send(cmd) + + def on_event(self, event, pattern, cmd): + '''Add a new event to watch and respond to.''' + + event = event.upper() + self.logger.debug('new event handler %r %r %r', event, pattern, cmd) + if event not in self.events: + self.uzbl.connect(event, + partial(self.event_handler, on_event=event)) + self.events[event] = {} + + cmds = self.events[event] + if cmd not in cmds: + cmds[cmd] = pattern + + def parse_on_event(self, args): + '''Parse ON_EVENT events and pass them to the on_event function. + + Syntax: "event ON_EVENT commands".''' + + args = splitquoted(args) + assert args, 'missing on event arguments' + + # split arguments into event name, optional argument pattern and command + event = args[0] + pattern = [] + if args[1] == '[': + for i, arg in enumerate(args[2:]): + if arg == ']': + break + pattern.append(arg) + command = args.raw(3+i) + else: + command = args.raw(1) + + assert event and command, 'missing on event command' + self.on_event(event, pattern, command) + + def cleanup(self): + self.events.clear() + super(OnEventPlugin, self).cleanup() + +# vi: set et ts=4: diff --git a/uzbl/plugins/on_set.py b/uzbl/plugins/on_set.py new file mode 100644 index 0000000..f6b9229 --- /dev/null +++ b/uzbl/plugins/on_set.py @@ -0,0 +1,85 @@ +from re import compile +from functools import partial + +import uzbl.plugins.config +from .cmd_expand import cmd_expand +from uzbl.arguments import splitquoted +from uzbl.ext import PerInstancePlugin +import collections + +valid_glob = compile('^[A-Za-z0-9_\*\.]+$').match + +def make_matcher(glob): + '''Make matcher function from simple glob.''' + + pattern = "^%s$" % glob.replace('*', '[^\s]*') + return compile(pattern).match + + +class OnSetPlugin(PerInstancePlugin): + + def __init__(self, uzbl): + super(OnSetPlugin, self).__init__(uzbl) + self.on_sets = {} + uzbl.connect('ON_SET', self.parse_on_set) + uzbl.connect('CONFIG_CHANGED', self.check_for_handlers) + + def _exec_handlers(self, handlers, key, arg): + '''Execute the on_set handlers that matched the key.''' + + for handler in handlers: + if isinstance(handler, collections.Callable): + handler(key, arg) + else: + self.uzbl.send(cmd_expand(handler, [key, arg])) + + def check_for_handlers(self, key, arg): + '''Check for handlers for the current key.''' + + for (matcher, handlers) in list(self.on_sets.values()): + if matcher(key): + self._exec_handlers(handlers, key, arg) + + def on_set(self, glob, handler, prepend=True): + '''Add a new handler for a config key change. + + Structure of the `self.on_sets` dict: + { glob : ( glob matcher function, handlers list ), .. } + ''' + + assert valid_glob(glob) + + while '**' in glob: + glob = glob.replace('**', '*') + + if isinstance(handler, collections.Callable): + orig_handler = handler + if prepend: + handler = partial(handler, self.uzbl) + + else: + orig_handler = handler = str(handler) + + if glob in self.on_sets: + (matcher, handlers) = self.on_sets[glob] + handlers.append(handler) + + else: + matcher = make_matcher(glob) + self.on_sets[glob] = (matcher, [handler,]) + + self.logger.info('on set %r call %r' % (glob, orig_handler)) + + + def parse_on_set(self, args): + '''Parse `ON_SET ` event then pass arguments to the + `on_set(..)` function.''' + + args = splitquoted(args) + assert len(args) >= 2 + glob = args[0] + command = args.raw(1) + + assert glob and command and valid_glob(glob) + self.on_set(glob, command) + diff --git a/uzbl/plugins/progress_bar.py b/uzbl/plugins/progress_bar.py new file mode 100644 index 0000000..5eb10d3 --- /dev/null +++ b/uzbl/plugins/progress_bar.py @@ -0,0 +1,92 @@ +import re +from .config import Config + +from uzbl.ext import PerInstancePlugin + +class ProgressBar(PerInstancePlugin): + splitfrmt = re.compile(r'(%[A-Z][^%]|%[^%])').split + + def __init__(self, uzbl): + super(ProgressBar, self).__init__(uzbl) + uzbl.connect('LOAD_COMMIT', lambda uri: self.update_progress()) + uzbl.connect('LOAD_PROGRESS', self.update_progress) + self.updates = 0 + + def update_progress(self, progress=None): + '''Updates the progress.output 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 + + Default configuration options: + progress.format = [%d>%p]%c + progress.width = 8 + progress.done = = + progress.pending = + progress.spinner = -\|/ + progress.sprites = loading + ''' + + if progress is None: + self.updates = 0 + progress = 100 + + else: + self.updates += 1 + progress = int(progress) + + # Get progress config vars. + config = Config[self.uzbl] + frmt = config.get('progress.format', '[%d>%p]%c') + width = int(config.get('progress.width', 8)) + done_symbol = config.get('progress.done', '=') + pend = config.get('progress.pending', None) + pending_symbol = pend if pend else ' ' + + # Get spinner character + spinner = config.get('progress.spinner', '-\\|/') + index = 0 if progress == 100 else self.updates % len(spinner) + spinner = '\\\\' if spinner[index] == '\\' else spinner[index] + + # get sprite character + sprites = config.get('progress.sprites', 'loading') + index = int(((progress/100.0)*len(sprites))+0.5)-1 + sprite = '%r' % ('\\\\' if sprites[index] == '\\' else sprites[index]) + + # Inflate the done and pending bars to stop the progress bar + # jumping around. + if '%c' in frmt or '%i' in frmt: + count = frmt.count('%c') + frmt.count('%i') + width += (3-len(str(progress))) * count + + if '%t' in frmt or '%o' in frmt: + count = frmt.count('%t') + frmt.count('%o') + width += (3-len(str(100-progress))) * count + + done = int(((progress/100.0)*width)+0.5) + pending = width - done + + # values to replace with + values = { + 'd': done_symbol * done, + 'p': pending_symbol * pending, + 'c': '%d%%' % progress, + 'i': '%d' % progress, + 't': '%d%%' % (100 - progress), + 'o': '%d' % (100 - progress), + 's': spinner, + 'r': sprite + } + + frmt = ''.join([str(values[k[1:]]) if k.startswith('%') else k for + k in self.splitfrmt(frmt)]) + + if config.get('progress.output', None) != frmt: + config['progress.output'] = frmt --- uzbl-2012.05.14/Makefile~ 2013-12-08 16:55:13.000000000 +0100 +++ uzbl-2012.05.14/Makefile 2013-12-08 16:56:12.412142265 +0100 @@ -189,7 +189,7 @@ install -m755 uzbl-core $(INSTALLDIR)/bin/uzbl-core install-event-manager: install-dirs - $(PYTHON) setup.py install --prefix=$(PREFIX) --install-scripts=$(INSTALLDIR)/bin $(PYINSTALL_EXTRA) + $(PYTHON) setup.py install --prefix=$(PREFIX) --root=$(DESTDIR) --install-scripts=$(PREFIX)/bin $(PYINSTALL_EXTRA) install-uzbl-browser: install-dirs install-uzbl-core install-event-manager sed 's#^PREFIX=.*#PREFIX=$(RUN_PREFIX)#' < bin/uzbl-browser > $(INSTALLDIR)/bin/uzbl-browser