summaryrefslogtreecommitdiff
path: root/.emacs.d/ezbl.el
diff options
context:
space:
mode:
Diffstat (limited to '.emacs.d/ezbl.el')
-rw-r--r--.emacs.d/ezbl.el1246
1 files changed, 1246 insertions, 0 deletions
diff --git a/.emacs.d/ezbl.el b/.emacs.d/ezbl.el
new file mode 100644
index 0000000..c8ebf71
--- /dev/null
+++ b/.emacs.d/ezbl.el
@@ -0,0 +1,1246 @@
+;;; ezbl.el --- Emacs interface for Uzbl (uzbl.org)
+;;
+;; Version: 0.4
+;; URL: http://github.com/dhax/ezbl
+
+;; This file 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, or (at your option) any later version.
+
+;; This file 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
+;; GNU Emacs; see the file COPYING. If not, write to the Free Software
+;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+;; USA.
+
+;;; Commentary:
+;;
+;; Uzbl version 2009.11.30 or greater is required. Ezbl will not work with
+;; older versions.
+;;
+;; Run `ezbl-open' to start a Uzbl instance and browse to a URL. Also, check
+;; `ezbl-mode' for a listing of the key bindings of Ezbl.
+
+(eval-when-compile
+ (require 'cl))
+(require 'url-cookie)
+
+(defgroup ezbl nil "Settings for Ezbl, the Emacs frontend for Uzbl.")
+
+(defcustom ezbl-exec-path (or (executable-find "uzbl-core") "/usr/bin/uzbl-core")
+ "The location of the Uzbl executable."
+ :group 'ezbl
+ :type 'file)
+
+(defcustom ezbl-cookie-socket "/tmp/ezbl-cookies"
+ "The location of the socket through which to handle cookies."
+ :group 'ezbl
+ :type 'file)
+
+(defvar ezbl-inst-list nil
+ "An alist of Uzbl instances and their pids.
+
+Has the format:
+
+ ((pid . instance)
+ (pid2 . instance2))
+
+The values are instances of the `ezbl-inst' struct.")
+
+(defvar ezbl-inst nil
+ "A buffer-local variable storing the current Ezbl instance.
+
+It is an instance of the `ezbl-inst' struct.")
+
+(defvar ezbl-cookie-process nil
+ "The process which is listening for cookie requests from Uzbl
+processes.")
+
+(defvar ezbl-initialized nil
+ "Keeps track of whether or not Ezbl has been initialized. This
+should only be set by `ezbl-init'.")
+
+(defconst ezbl-inst-slots
+ '(args
+ process
+ pid
+ output-buffer
+ display-buffer
+ vars)
+ "A list of the slot names in the `ezbl-inst' structure.
+
+`defstruct' does not keep a list of the fields of the struct for
+later use, so do this manually.")
+
+(eval `(defstruct ezbl-inst
+ "A structure containing the properties of an Ezbl instance."
+ ,@ezbl-inst-slots))
+
+(defconst ezbl-inst-get-first
+ '(ezbl-inst-get-first
+ nil ;; protected
+ t ;; enabled
+ (advice . (lambda ()
+ (ad-set-arg 0 (ezbl-inst-get (ad-get-arg 0))))))
+ "Computed advice to apply to each of the `ezbl-inst' slot
+accessors.")
+
+(defconst ezbl-output-buffer-format " *ezbl-output-%s*"
+ "The format used for transforming ezbl instance names into
+buffer names.")
+
+(defconst ezbl-display-buffer-format "*ezbl-display-%s*"
+ "The format used for transforming ezbl instance names into
+buffer names.")
+
+(defvar ezbl-commands
+ '(((name . "back")
+ (format . "back")
+ (interactive . "U")
+ (key-binding . "C-c C-b")
+ (doc . "Move backwards in the browser history."))
+
+ ((name . "forward")
+ (format . "forward")
+ (interactive . "U")
+ (key-binding . "C-c C-f")
+ (doc . "Move forwards in the browser history."))
+
+ ((name . "scroll")
+ (format . "scroll <direction> <argument>")
+ (interactive . "U\nnScroll amount: ")
+ (key-binding . "C-c C-v")
+ (doc . "Scroll in DIRECTION by ARGUMENT.
+
+* argument can be `begin`, `end`, or an amount given in pixels or
+ as a percentage of the size of the view.
+
+* set the amount to 100% to scroll a whole page"))
+
+ ((name . "reload")
+ (format . "reload")
+ (interactive . "U")
+ (key-binding . "C-c C-r")
+ (doc . "Reload the current page."))
+
+ ((name . "reload_ign_cache")
+ (format . "reload_ign_cache")
+ (interactive . "U")
+ (doc . "Reload the current page, clearing the cache."))
+
+ ((name . "stop")
+ (format . "stop")
+ (interactive . "U")
+ (key-binding . "C-c C-g")
+ (doc . "Stop the currently loading page."))
+
+ ((name . "zoom_in")
+ (format . "zoom_in")
+ (interactive . "U")
+ (key-binding . "C-c z i")
+ (doc . "Increase the zoom level."))
+
+ ((name . "zoom_out")
+ (format . "zoom_out")
+ (interactive . "U")
+ (key-binding . "C-c z o")
+ (doc . "Decrease the zoom level."))
+
+ ((name . "toggle_zoom_type")
+ (format . "toggle_zoom_type")
+ (interactive)
+ (doc . "Toggles the variable 'zoom_type' between
+'full-content' and 'text-only' zoom. In 'text-only' zoom, only
+the text of the page is zoomed, while in 'full-content' zoom,
+images and other page elements are zoomed along with the text."))
+
+ ((name . "uri")
+ (format . "uri <address>")
+ (interactive . "U\nsAddress: ")
+ (key-binding . "C-c C-o")
+ (doc . "Visit the Uri ADDRESS"))
+
+ ((name . "js")
+ (format . "js <body>")
+ (interactive . "U\nsJavascript to execute: ")
+ (doc . "Execute JavaScript within the browser.
+
+* execute the javascript in BODY.
+* remember that the commands must not contain line breaks."))
+
+ ((name . "script")
+ (format . "script <file>")
+ (interactive . "U\nfJavascript file to execute: ")
+ (key-binding . "C-c C-j")
+ (doc . "execute the JavaScript in FILE."))
+
+ ((name . "toggle_status")
+ (format . "toggle_status")
+ (interactive . "U")
+ (doc . "Toggle the display of the status bar."))
+
+ ((name . "spawn")
+ (format . "spawn <executable> <additonal_args>")
+ (interactive . "U\nFFile to spawn\nsAdditional arguments:")
+ (doc . "Runs a command.
+
+* See the \"external scripts\" section of the Uzbl readme for
+ details.
+
+* PATH is searched so giving the full path to commands is not
+ necessary.
+
+* note that the arguments as specified in \"external scripts\"
+ are appended at the end, so the argument numbers will be
+ higher."))
+
+ ((name . "sync_spawn")
+ (format . "sync_spawn <executable> <additional_args>")
+ (interactive . "U\nFFile to spawn\nsAdditional arguments:")
+ (doc . "Tell Uzbl to synchronously spawn a command.
+
+See `ezbl-command-spawn' for details.
+
+* these are synchronous variants of spawn and sh, which means
+ uzbl will wait for them to return.
+
+* you should only need to use these manually if you want to use a
+ chain command in a handler that wants output from the command
+ it runs"))
+
+ ((name . "sh")
+ (format . "sh <command>")
+ (interactive . "U\nsCommand to execute: ")
+ (doc . "Run a shell command.
+
+* runs a shell command by expanding %s in the shell_cmd variable
+ with the specified command; primarily useful as a shortcut for
+ \"spawn sh -c BODY\"
+
+* note that the arguments as specified in \"external scripts\"
+ are appended at the end, so the argument numbers will be
+ higher."))
+
+ ((name . "sync_sh")
+ (format . "sync_sh <command>")
+ (interactive . "U\nsCommand to run: ")
+ (doc . "Tell Uzbl to synchronously run a shell command.
+
+See `ezbl-command-sh' for details.
+
+* these are synchronous variants of spawn and sh, which means
+ uzbl will wait for them to return.
+
+* you should only need to use these manually if you want to use a
+ chain command in a handler that wants output from the command
+ it runs"))
+
+ ((name . "talk_to_socket")
+ (format . "talk_to_socket <socketfile> <args>")
+ (doc . "Lets uzbl talk to a socketfile."))
+
+ ((name . "exit")
+ (format . "exit")
+ (interactive . "U")
+ (key-binding . "C-c C-q")
+ (doc . "Close this instance of Uzbl."))
+
+ ((name . "search")
+ (format . "search <string>")
+ (interactive . "U\nsSearch: ")
+ (key-binding . "C-s")
+ (doc . "Search for STRING within the content of the current
+Uzbl page.
+
+* search with no string will search for the next/previous
+ occurrence of the string previously searched for."))
+
+ ((name . "search_reverse")
+ (format . "search_reverse <string>")
+ (interactive . "U\nsSearch backward: ")
+ (key-binding . "C-r")
+ (doc . "Search backwards for STRING in the current page.
+
+* search with no string will search for the next/previous
+ occurrence of the string previously searched for."))
+
+ ((name . "search_clear")
+ (format . "search_clear")
+ (interactive . "U")
+ (doc . "Unmark and clear the search string"))
+
+ ((name . "dehilight")
+ (format . "dehilight")
+ (interactive)
+ (doc . "Remove highlighting of search matches."))
+
+ ((name . "set")
+ (format . "set <key> = <value>")
+ (interactive . (let* ((var-name
+ (completing-read "Variable to set: "
+ (mapcar '(lambda (item)
+ (cdr-safe (assq 'name item))) ezbl-variables)
+ nil ;; predicate
+ t ;; require-match
+ nil ;; initial-input
+ 'ezbl-command-set-history)) ;; hist
+ (default (ezbl-variable-get nil var-name))
+ (new-val (read-string (format "New value (%s): " default) nil nil default)))
+ (list nil var-name new-val)))
+ (key-binding . "C-c C-s")
+ (doc . "Used for changing variables on the fly. Sets KEY
+ equal to VALUE.
+
+* the changes are effective immediately; for example, setting the
+ variable uri will make uzbl start loading, and changing
+ status_format will make the status bar react immediately
+
+* if you want to unset a string, use `set' with one space as the
+ value."))
+
+ ((name . "dump_config")
+ (format . "dump_config")
+ (doc . "Dump the current Uzbl configuration.
+
+* dumps your current config (which may have been changed at
+ runtime) to stdout, in a format you can use to pipe into uzbl
+ again (or use as config file)"))
+
+ ((name . "dump_config_as_events")
+ (format . "dump_config_as_events")
+ (doc . "Dump the current config as a series of
+'VARIABLE_SET' events, which can be handled by an event manager."))
+
+ ((name . "chain")
+ (format . "chain <command> <command2>")
+ (interactive . "U\nsCommand 1: \nsCommand 2: ")
+ (doc . "Use for chaining multiple commands.
+
+* remember to quote the commands; one command must come as one
+ parameter.
+
+* If you use chain with a handler script which must return some
+ output (such as a cookie handler -- uzbl will wait for and use
+ its output), use 'sync_spawn' or 'sync_sh' instead of 'spawn'
+ or 'sh' in the command that should give the output."))
+
+ ((name . "print")
+ (format . "print <key>")
+ (doc . "Print the value of KEY.
+
+If KEY contains a string of the form '@var', the value of the Uzl
+variable 'var' is printed.
+
+* use this to print the value of a variable."))
+
+ ((name . "event")
+ (format . "event <name> <details>")
+ (interactive . "U")
+ (doc . "Send custom event.
+NAME is the event name and DETAILS is additional information to include."))
+
+ ((name . "request")
+ (format . "request <name> <details>")
+ (interactive . "U")
+ (doc . "Send custom request.
+Same idea as events, but to be processed by EM, not uzbl-core."))
+
+ ((name . "menu_add")
+ (format . "menu_add <label> = <command>"))
+
+ ((name . "menu_link_add")
+ (format . "menu_link_add <label> = <command>"))
+
+ ((name . "menu_image_add")
+ (format . "menu_image_add <label> = <command>"))
+
+ ((name . "menu_editable_add")
+ (format . "menu_editable_add <label> = <command>")
+ (doc . "add a new entry LABEL that will execute COMMAND to one
+of the right click context menus."))
+
+ ((name . "menu_separator")
+ (format . "menu_separator <label>"))
+
+ ((name . "menu_link_separator")
+ (format . "menu_link_separator <label>"))
+
+ ((name . "menu_image_separator")
+ (format . "menu_image_separator <label>"))
+
+ ((name . "menu_editable_separator")
+ (format . "menu_editable_separator <label>")
+ (doc . "Adds a separator line to one of the right click context menus."))
+
+ ((name . "menu_remove")
+ (format . "menu_remove <label>"))
+
+ ((name . "menu_link_remove")
+ (format . "menu_link_remove <label>"))
+
+ ((name . "menu_image_remove")
+ (format . "menu_image_remove <label>"))
+
+ ((name . "menu_editable_remove")
+ (format . "menu_editable_remove <label>")
+ (doc . "Removes the entry LABEL from one of the right click context menus."))
+
+ ((name . "hardcopy")
+ (format . "hardcopy")
+ (doc . "open print dialog"))
+
+ ((name . "include")
+ (format . "include <file>")
+ (doc . "read contents of file and interpret commands")))
+ "A list of commands which Uzbl accepts. These are used to
+generate the functions to call each command.
+
+The following attributes can be used in each alist:
+
+- name (mandatory)
+
+ The name of the command. This is the string that is passed to
+ Uzbl when invoking the command.
+
+- format (mandatory)
+
+ The format of the command, for example
+
+ scroll <direction> <amount>
+
+- interactive (optional)
+
+ The interactive spec for the function. If this attribute is not
+ present, then the function will not be interactive.
+
+- doc (optional)
+
+ The documentation string to provide for the function.
+
+- key-binding (optional)
+
+ The key binding in `ezbl-mode-map' of the function. If it is
+ not included, no keybinding is set for that command. The value
+ must be a string to be supplied to the `kbd' macro.
+
+All variables must be enclosed in angle brackets.")
+
+(defvar ezbl-variables
+ '(((name . "uri")
+ (doc . "(callback: load the uri)"))
+
+ ((name . "verbose")
+ (doc . "affects output on stdout"))
+
+ ((name . "inject_html")
+ (doc . "base_url: used when passing html through stdin"))
+
+ ((name . "geometry")
+ (doc . "Geometry and position of the Uzbl window. Format is \"<width>x<height>+<x-offset>+<y-offset>\"."))
+
+ ((name . "keycmd")
+ (doc . "Holds the input buffer (callback: update input buffer)"))
+
+ ((name . "show_status")
+ (doc . "show statusbar or not"))
+
+ ((name . "status_top")
+ (doc . "statusbar on top?"))
+
+ ((name . "status_format")
+ (doc . "marked up, to be expanded string for statusbar (callback: update statusbar)"))
+
+ ((name . "status_background")
+ (doc . "color which can be used to override Gtk theme."))
+
+ ((name . "title_format_long")
+ (doc . "titlebar string when no statusbar shown (will be expanded"))
+
+ ((name . "title_format_short")
+ (doc . "titlebar string when statusbar shown (will be expanded)"))
+
+ ((name . "icon")
+ (doc . "path to icon for Gtk"))
+
+ ((name . "forward_keys")
+ (doc . "whether uzbl-core should send key events to the webkit view")
+ (default . "1"))
+
+ ((name . "download_handler")
+ (doc . "The command to call (usually a shell script) when the page requests a download"))
+
+ ((name . "cookie_handler")
+ (doc . "The command to call to handle cookies.")
+ (default . (eval (format "talk_to_socket %s" ezbl-cookie-socket))))
+
+ ((name . "new_window")
+ (doc . "handler to execute to invoke new uzbl window (TODO better name)"))
+
+ ((name . "scheme_handler")
+ (doc . "handler to execute for each URI navigated
+to - the navigation request will be ignored if handler prints USED."))
+
+ ((name . "fifo_dir")
+ (doc . "location to store fifo's"))
+
+ ((name . "socket_dir")
+ (doc . "location to store sockets"))
+
+ ((name . "http_debug")
+ (doc . "http debug mode (value 0-3)"))
+
+ ((name . "shell_cmd")
+ (doc . "alias which will be expanded to use shell commands (eg sh -c)"))
+
+ ((name . "proxy_url")
+ (doc . "http traffic socks proxy (eg: http://<host>:<port>)"))
+
+ ((name . "max_conns")
+ (doc . "max simultaneous connections (default: 100)"))
+
+ ((name . "max_conns_host")
+ (doc . "max simultaneous connections per hostname (default: 6)"))
+
+ ((name . "view_source")
+ (doc . "View the page source."))
+
+ ((name . "useragent")
+ (doc . "to be expanded string"))
+
+ ((name . "zoom_level")
+ (doc . "The level of zoom of the page."))
+
+ ((name . "zoom_type")
+ (doc . "The type of zoom."))
+
+ ((name . "font_size")
+ (doc . "The current font size."))
+
+ ((name . "default_font_family")
+ (doc . "sans-serif"))
+
+ ((name . "monospace_font_family")
+ (doc . "monospace (example Aerial Mono)"))
+
+ ((name . "cursive_font_family")
+ (doc . "sans"))
+
+ ((name . "sans_serif_font_family")
+ (doc . "sans (example DejaVu Sans)"))
+
+ ((name . "serif_font_family")
+ (doc . "serif (example DejaVu Serif)"))
+
+ ((name . "fantasy_font_family")
+ (doc . "Pterra"))
+
+ ((name . "monospace_size")
+ (doc . "Default monospace font size."))
+
+ ((name . "minimum_font_size")
+ (doc . "The minimum font size."))
+
+ ((name . "disable_plugins")
+ (doc . "(TODO rename to enable)"))
+
+ ((name . "disable_scripts")
+ (doc . "(TODO rename to enable)"))
+
+ ((name . "autoload_images")
+ (doc . "Images are automatically loaded."))
+
+ ((name . "autoshrink_images")
+ (doc . "shrink images to window size (default 0)"))
+
+ ((name . "enable_spellcheck")
+ (doc . "Spellcheck is enabled."))
+
+ ((name . "enable_private")
+ (doc . "Private mode is enabled."))
+
+ ((name . "print_backgrounds")
+ (doc . "print background images? (default 0)"))
+
+ ((name . "stylesheet_uri")
+ (doc . "use this to override the pagelayout with a custom stylesheet"))
+
+ ((name . "resizable_text_areas")
+ (doc . "Are text areas resizable?"))
+
+ ((name . "default_encoding")
+ (doc . "iso-8859-1 by default"))
+
+ ((name . "enforce_96_dpi")
+ (doc . "1 by default"))
+
+ ((name . "caret_browsing")
+ (doc . "Whether to use caret browsing.")))
+ "The variables available from Uzbl.
+
+This is a list of alists, where each alist has the attributes:
+
+- name (mandatory)
+
+ The name of the variable. Must exactly match the name used by
+ Uzbl.
+
+- doc (mandatory)
+
+ A description of the variable. Usually taken straight from the
+ Uzbl README.
+
+- default (optional)
+
+ An optional default value for the variable. Note that this is
+ not the same as Uzbl's default values; values here will
+ overwrite Uzbl defaults.
+
+ Either a string to be sent to Uzbl or a list of the form (eval
+ FORM), where FORM is a Lisp expression, evaluated after each
+ Uzbl instance is started, which returns a string to use as the
+ default value.")
+
+(defvar ezbl-xwidget-id-counter 0
+ "Keeps track of the current value to assign to newly-created xwidgets.
+
+IDs must be unique, or Emacs will crash.")
+
+(defconst ezbl-xwidget-type 3
+ "The type attribute for xwidget embedded widgets.
+
+For whatever reason, the Xwidget patch specifies that embedded X windows have
+this type.")
+
+(defvar ezbl-xembed-ready-hook nil
+ "Commands to run when an ezbl instance receives the
+
+`xembed-ready' signal.")
+
+(defun ezbl-command-get-args (command)
+ "Extracts the arguments (as symbols) from a Uzbl command specification.
+
+For example, the spec
+
+ scroll_vert <amount>
+
+Would return (amount)."
+ (let ((start 0)
+ (args nil))
+ (while (string-match "<\\([[:alnum:]_-]+\\)>" command start)
+ (setq args (add-to-list 'args (intern (match-string 1 command)) t))
+ (setq start (match-end 1)))
+ args))
+
+(defun ezbl-command-make-func (spec)
+ "Creates a function which produces the Uzbl command string described by SPEC.
+
+The function created takes a number of arguments specified by the
+`format' attribute of SPEC and returns a string suitable for
+`ezbl-command-exec'.
+
+Sets the `interactive' declaration to the `interactive' attribute
+of SPEC.
+
+It also defines a variable `ezbl-command-NAME-history' which can
+be used by the function to record the input history of the
+function when called interactively.
+
+See `ezbl-commands' for a description of the format of SPEC."
+ (let* ((name (cdr (assq 'name spec)))
+ (format (cdr (assq 'format spec)))
+ (args (ezbl-command-get-args format))
+ (doc (cdr (assq 'doc spec)))
+ (output-format (replace-regexp-in-string "<[[:alnum:]_-]+>" "%s" format))
+ (interactive-spec (cdr (assq 'interactive spec)))
+ (command-name (intern (concat "ezbl-command-" name)))
+ (history-list (intern (format "%s-history" command-name))))
+ ;; Make the symbol contained in `history-list' bound.
+ (set history-list nil)
+ (fset command-name
+ `(lambda (inst ,@args)
+ ,doc
+ ,(when interactive-spec `(interactive ,interactive-spec))
+ (ezbl-command-exec inst (format ,output-format ,@args))))))
+
+(defun ezbl-command-init ()
+ "Create Emacs functions from `ezbl-commands' and `ezbl-instance-spec'.
+
+Read through `ezbl-commands' and call `ezbl-command-make-func' on
+each one. Also, run through `ezbl-instance-spec' and call
+`ezbl-make-instance-accessor-func' on each one."
+ (interactive)
+ (append (mapcar 'ezbl-command-make-func ezbl-commands)))
+
+(defun ezbl-variable-set-defaults (&optional inst)
+ "Set the variable defaults according to `ezbl-variables'."
+ (mapc '(lambda (spec)
+ (let ((name (cdr (assq 'name spec)))
+ (default (cdr-safe (assq 'default spec))))
+ (when default
+ (if (and (consp default) (eq (car default) 'eval))
+ (setq default (eval (cadr default))))
+ (ezbl-command-set inst name default))))
+ ezbl-variables))
+
+(defun ezbl-init ()
+ "Starts up the services needed for Ezbl to run."
+ (unless ezbl-initialized
+ (when (or (not (boundp 'server-name)) (null server-name))
+ (error "Emacs server is required for Ezbl, but `server-name' is nil."))
+ (when (null server-process)
+ (error "Emacs server is required for Ezbl, but the server is not started."))
+ (unless (featurep 'xwidget)
+ (error "This version of Emacs does not support embedding windows. Please get a patched version from http://github.com/jave/emacs"))
+ (unless (featurep 'make-network-process '(:type seqpacket))
+ (error "This version of Emacs does not support SEQPACKET sockets"))
+ (ezbl-cookie-socket-listen nil t)
+ (ezbl-inst-define-advice)
+ (setq ezbl-initialized t)))
+
+(defun ezbl-inst-start (&rest args)
+ "Start an instance of Uzbl. ARGS is a keyword list of
+options and values to pass to the Uzbl instance.
+
+The following keywords are used:
+
+:class VALUE
+ Program class as used by the window manager
+:gtk-name NAME
+ Program name as used by the window manager
+:screen SCREEN
+ X screen to use
+:sync nil
+ Make X calls synchronous. Must include `nil' as the argument.
+:gtk-module MODULES
+ Load additional GTK+ modules. MODULES should be a list of
+ string names of modules.
+:g-fatal-warnings nil
+ Make all warnings fatal. Must include `nil' as the argument.
+:uri URI
+ Uri to load at startup (equivalent to 'set uri = URI')
+:verbose nil
+ Whether to print all messages or just errors. Must
+ include `nil' as the argument.
+:name NAME
+ Name of the current instance (defaults to Xorg window id)
+:config FILE
+ Config file (this is pretty much equivalent to 'uzbl < FILE' )
+:socket SOCKET
+ Socket ID for GtkSocket.
+:display DISPLAY
+ X display to use
+
+Returns an `ezbl-inst' struct."
+ (let (program-args)
+ ;; Process keywords
+ (while args
+ (let ((arg (car args)))
+ (setq args (cdr args))
+ (unless (symbolp arg)
+ (error "Junk in args %S" args))
+ (let ((keyword arg)
+ (value (car args)))
+ (unless args
+ (error "Keyword %s is missing an argument" keyword))
+ (setq args (cdr args))
+ (cond
+ ((eq keyword :class)
+ (setq program-args (append program-args (list "--class") (list value))))
+ ((eq keyword :gtk-name)
+ (setq program-args (append program-args (list "--gtk-name") (list value))))
+ ((eq keyword :screen)
+ (setq program-args (append program-args (list "--screen") (list value))))
+ ((eq keyword :sync)
+ (setq program-args (append program-args (list "--sync"))))
+ ((eq keyword :gtk-module)
+ (setq program-args (append program-args (list "--gtk-module")
+ (list (mapconcat 'identity value ",")))))
+ ((eq keyword :g-fatal-warnings)
+ (setq program-args (append program-args (list "--g-fatal-warnings "))))
+ ((eq keyword :uri)
+ (setq program-args (append program-args (list "--uri") (list value))))
+ ((eq keyword :verbose)
+ (setq program-args (append program-args (list "--verbose"))))
+ ((eq keyword :name)
+ (setq program-args (append program-args (list "--name") (list value))))
+ ((eq keyword :config)
+ (setq program-args (append program-args (list "--config") (list value))))
+ ((eq keyword :socket)
+ (setq program-args (append program-args (list "--socket") (list value))))
+ ((eq keyword :display)
+ (setq program-args (append program-args (list "--display") (list value))))))))
+
+ ;; Start process
+ (let* (inst
+ (proc-name "ezbl-process")
+ (output-buffer (generate-new-buffer "*ezbl-output*"))
+ (proc (apply 'start-process
+ (append (list proc-name
+ output-buffer
+ ezbl-exec-path)
+ program-args)))
+ (pid (process-id proc)))
+
+ (setq inst (make-ezbl-inst
+ :args program-args
+ :process proc
+ :pid pid
+ :output-buffer output-buffer
+ :display-buffer (current-buffer)
+ :vars (make-hash-table)))
+ (set-process-filter proc 'ezbl-event-listener)
+
+ (with-current-buffer output-buffer
+ (rename-buffer (format ezbl-output-buffer-format (int-to-string pid)))
+ (set (make-local-variable 'ezbl-inst) inst))
+ (rename-buffer (format ezbl-display-buffer-format pid))
+ (set (make-local-variable 'ezbl-inst) inst)
+
+ ;; Make `ezbl-inst' survive `kill-all-local-variables'
+ (put 'ezbl-inst 'permanent-local t)
+
+ (add-to-list 'ezbl-inst-list (cons pid inst))
+ (ezbl-mode)
+ inst)))
+
+(defun ezbl-inst-get (&optional inst strict)
+ "Returns the ezbl instance from INST.
+
+If INST is an ezbl instance, then it is returned unchanged. If it
+is a buffer, then the local variable of `ezbl-inst' is
+returned. If it is an integer, then `ezbl-inst-list' is searched
+for an instance with a matching pid. If it is nil or not
+supplied, then the value of `ezbl-inst' in the current buffer
+is returned.
+
+If STRICT is non-nil, raise an error if INST is not resolvable to
+an instance.
+
+Returns an `ezbl-inst'."
+ (when (null inst)
+ (set 'inst ezbl-inst))
+
+ (let ((instance
+ (cond
+ ((ezbl-inst-p inst)
+ inst)
+ ((bufferp inst)
+ (with-current-buffer inst
+ ezbl-inst))
+ ((integerp inst)
+ (cdr-safe (assq inst
+ ezbl-inst-list)))
+ ((processp inst)
+ (cdr-safe (assq (process-id inst)
+ ezbl-inst-list)))
+ ((stringp inst)
+ (if (and (bufferp (get-buffer inst))
+ (not (null (with-current-buffer inst
+ ezbl-inst))))
+ (with-current-buffer inst
+ ezbl-inst)
+ ;; Is the name of an instance, so open the output buffer which
+ ;; corresponds to this name.
+ (when (get-buffer (format ezbl-output-buffer-format inst))
+ (with-current-buffer (format ezbl-output-buffer-format inst)
+ ezbl-inst)))))))
+ (if (ezbl-inst-p instance)
+ instance
+ (when strict
+ (error (format "`%s' is not an Ezbl instance or resolvable to an Ezbl instance" inst))))))
+
+(defun ezbl-inst-define-advice ()
+ "Define and activate the advice for each slot in `ezbl-inst'.
+
+Makes the accessors call `ezbl-inst-get' before operating, so
+that the accessors work on things which are resolvable to an
+`ezbl-inst', rather than only allowing the insts themselves.
+
+Also redefines the `setf-method' for each slot, since `setf'
+doesn't actually call the slot accessor, so it wouldn't resolve
+its argument to an `ezbl-inst'."
+ (mapc '(lambda (item)
+ (let ((func (intern (concat "ezbl-inst-" (symbol-name item)))))
+ (ad-add-advice func
+ ezbl-inst-get-first 'before 'first)
+ (ad-activate func)
+
+ ;; This is tricky. Redefines the `setf-method' to be the same as
+ ;; the old one, except that it is passed the result of
+ ;; `ezbl-inst-get' on the `eval'ed argument.
+ (eval (list 'define-setf-method
+ func
+ '(cl-x)
+ `(apply ,(get func 'setf-method)
+ (list (ezbl-inst-get (eval cl-x) t)))))))
+ ezbl-inst-slots))
+
+(defun ezbl-command-exec (inst command)
+ "Sends the string COMMAND to the Uzbl instance INST.
+
+If INST is a buffer, use the value of `ezbl-inst' in that
+buffer. If COMMAND is a Uzbl command as described by the Uzbl
+Readme (http://www.uzbl.org/readme.php).
+
+See `ezbl-inst-start' for a description of the format of INST."
+ ;; Append a newline (\n) to the end of COMMAND if one is not already there.
+ (when (not (string= "\n" (substring command -1)))
+ (setq command (concat command "\n")))
+ (process-send-string (ezbl-inst-process inst) command))
+
+(defun ezbl-sync-request (inst req)
+ "Request Uzl to evaluate a request string REQ and wait for the result.
+
+Uses the Uzbl \"print\" command to make a request to the Uzbl
+process specified by INST. Waits until Uzbl replies and returns
+the response that Uzbl produces.
+
+The following substitutions are supported (see the Uzbl readme
+for more info):
+
+ @var: Returns the value of the variable \"var\".
+
+ @{var}: Returns the value of the variable \"var\". Used to
+ denote the beginning and end of a variable name.
+
+ @(command)@: Executes the shell command \"command\" and returns
+ its result.
+
+ @<code>@: Executes the string \"code\" as javascript in the
+ current page and returns the result.
+
+ @[xml]@: Escapes any XML in the brackets."
+ (let ((tag (sha1 (int-to-string (random)))))
+ (with-current-buffer (ezbl-inst-output-buffer inst)
+ (ezbl-command-print inst
+ (format "%s{%s}%s" tag req tag))
+ (goto-char (point-max))
+ ;; Keep trying until tag is found. TODO: avoid searching backwards through
+ ;; the whole buffer.
+ (while (not (re-search-backward (format "%s{\\(.*\\)}%s" tag tag) (point-min) t))
+ (goto-char (point-max))
+ ;; Sleep for 1 millisecond
+ (sleep-for 0 1))
+ (match-string 1))))
+
+(defun ezbl-variable-get (inst var)
+ "Return the value of VAR from the ezbl instance INST."
+ (gethash (intern-soft var) (ezbl-inst-vars inst)))
+
+(defun ezbl-run-js (inst js)
+ "Execute the Javascript in JS on the Uzbl instance INST and
+return the result."
+ (ezbl-sync-request inst (concat "@<" js ">@")))
+
+(defun ezbl-xwidget-insert (where id type title width height)
+ "Insert an embedded widget.into the current buffer.
+
+WHERE - The point at which to insert the widget.
+ID - The id of the widget. MUST be unique and < 100!
+TYPE - 1=button, 2=toggle btn, 3=xembed socket(id will be printed to stdout)
+TITLE - The title of the embedded widget
+WIDTH - The width of the widget
+HEIGHT - The height of the widget"
+ (save-excursion
+ (goto-char where)
+ (put-text-property
+ (point)
+ (1+ (point))
+ 'display
+ (list 'xwidget ':xwidget-id id ':type type ':title title ':width width ':height height))))
+
+(defun ezbl-embed ()
+ "Insert an xwidget into the current buffer.
+
+Also, set a callback (using the `xwidget-event' \"keybinding\")
+to launch Uzbl once the widget is fully initialized."
+ (save-excursion
+ (use-local-map (make-sparse-keymap))
+ (define-key (current-local-map) [(xwidget-event)] 'ezbl-xwidget-handler)
+ (ezbl-xwidget-insert
+ (point) ;; Where
+ (ezbl-xwidget-next-id) ;; ID
+ ezbl-xwidget-type ;; Type
+ "ezbl" ;; Name
+ 600 ;; Width
+ 600))) ;; Height
+
+(defun ezbl-xwidget-next-id ()
+ "Returns the next xwidget id based on the value of `ezbl-xwidget-id-counter'."
+ (incf ezbl-xwidget-id-counter))
+
+(defun ezbl-xwidget-handler ()
+ "Respond to the creation of an xwidget.
+
+Once the xwidget is set up, start a Uzbl process and give it the
+xwidget's socket id."
+ (interactive)
+ (let* ((xwidget-event-type (nth 2 last-input-event))
+ (xwidget-id (nth 1 last-input-event)))
+ (cond
+ ((eq xwidget-event-type 'xembed-ready)
+ (let* ((xembed-id (nth 3 last-input-event)))
+ (ezbl-inst-start :socket (number-to-string xembed-id)
+ :config "-") ;; Use stdin for config
+ (run-hooks 'ezbl-xembed-ready-hook))))))
+
+(defun ezbl-open (uri)
+ "Create a new Uzbl instance in a new buffer and browse to URI."
+ (interactive "sUri: ")
+
+ (ezbl-init)
+ (switch-to-buffer (generate-new-buffer uri))
+ ;; Currently has problems embedding into an empty buffer, so insert a space.
+ (insert " ")
+ (backward-char)
+ (ezbl-embed)
+
+ (add-hook 'ezbl-xembed-ready-hook
+ `(lambda () (ezbl-command-uri ezbl-inst ,uri))
+ nil t)
+ (put 'ezbl-xembed-ready-hook 'permanent-local t)
+ (current-buffer))
+
+(defun ezbl-cookie-listener (proc answer)
+ "Handle a cookie request over a socket.
+
+This function is intended to be set as the filter of the
+server-process listening on the cookie socket. It receives the
+process PROC with which it is communicating and ANSWER, the text
+sent by Uzbl."
+ (let* ((args (split-string answer "\0"))
+ (result (apply 'ezbl-cookie-handler args)))
+ (when (and result
+ (> 0 (length (split-string result))))
+ (process-send-string proc result))
+ (process-send-eof proc)))
+
+(defun ezbl-cookie-handler (op scheme host path &optional data &rest ignored)
+ "Process a single cookie.
+
+OP - either \"PUT\" or \"GET\", indicating whether a cookie
+ should be stored or retrieved, respectively.
+
+SCHEME - The URL scheme of the request, usually \"http\".
+
+HOST - The host requesting the cookie.
+
+PATH - The path of the cookie.
+
+DATA - In a \"PUT\" request, this is the data to store."
+ (let ((secure (if (equal scheme "https")
+ t
+ nil))
+ (url-current-object (url-parse-make-urlobj scheme nil nil host nil path)))
+ (when (equal op "PUT")
+ (url-cookie-handle-set-cookie data))
+ (when (equal op "GET")
+ (url-cookie-generate-header-lines host path secure))))
+
+(defun ezbl-cookie-socket-listen (&optional path force)
+ "Begin listening for Uzbl cookie requests.
+
+Creates a server process on a local socket at PATH, or
+`ezbl-cookie-socket' if PATH is nil.
+
+Starts a process and stores it in `ezbl-cookie-process' if it is
+nil. If `ezbl-cookie-process' is non-nil, then don't create a
+process unless FORCE is non-nil, in which case kill the existing
+process and start a new one."
+ (when (processp ezbl-cookie-process)
+ (unless force
+ (error "A cookie process already exists")))
+
+ (let* ((sock-path (or path ezbl-cookie-socket)))
+ (when (file-exists-p sock-path)
+ (if force
+ (delete-file sock-path)
+ (error (format "Cannot listen on `%s', file exists" sock-path))))
+
+ (setq ezbl-cookie-process
+ (make-network-process :name "ezbl-cookie"
+ :type 'seqpacket
+ :server t
+ :service sock-path
+ :family 'local
+ :filter 'ezbl-cookie-listener))))
+
+(defun ezbl-update-mode-line-format ()
+ "Updates the mode-line format in each ezbl display-window,
+
+according to `ezbl-mode-line-format'."
+ (mapc '(lambda (inst)
+ (with-current-buffer (ezbl-inst-display-buffer (car inst))
+ (setq mode-line-format ezbl-mode-line-format)))
+ ezbl-inst-list))
+
+(defun ezbl-set-mode-line-format (symbol value)
+ "Used for setting `ezbl-mode-line-format'; sets SYMBOL's value
+to VALUE and runs `ezbl-update-mode-line-format'."
+ (set-default symbol value)
+ (ezbl-update-mode-line-format))
+
+(defcustom ezbl-mode-line-format
+ '("-"
+ mode-line-mule-info
+ mode-line-modified
+ mode-line-frame-identification
+ (:propertize (:eval (ezbl-variable-get ezbl-inst 'title))
+ face bold)
+ " -- "
+ (:eval (ezbl-variable-get ezbl-inst 'uri))
+ " "
+ mode-line-modes
+ (which-func-mode ("" which-func-format "--"))
+ "-%-")
+ "The format of the mode line in an Ezbl display buffer."
+ :group 'ezbl
+ :type 'sexp
+ :set 'ezbl-set-mode-line-format)
+
+(defvar ezbl-mode-map
+ (let ((map (make-sparse-keymap)))
+ (mapc '(lambda (item)
+ (let* ((binding (cdr (assq 'key-binding item)))
+ (name (cdr (assq 'name item)))
+ (func (intern (concat "ezbl-command-" name))))
+ (when binding
+ (define-key map (read-kbd-macro binding) func))))
+ ezbl-commands)
+ map)
+ "Keymap for `ezbl-mode'.")
+
+(define-derived-mode ezbl-mode nil "Ezbl"
+ "Mode for interacting with Ezbl processes.
+
+\\{ezbl-mode-map}"
+ :group 'ezbl
+ (toggle-read-only t)
+ (set-buffer-modified-p nil)
+ (add-hook 'window-configuration-change-hook 'ezbl-fill-window nil t))
+
+(add-hook 'ezbl-mode-hook 'ezbl-variable-set-defaults)
+(add-hook 'ezbl-mode-hook 'ezbl-fill-window)
+(add-hook 'ezbl-mode-hook 'ezbl-update-mode-line-format)
+
+(defun ezbl-xwidget-resize-at (pos width height)
+ "Resize xwidget at postion POS to WIDTH and HEIGHT.
+
+There is no corresponding resize-id fn yet, because of display
+property/xwidget id impedance mismatch."
+ (let* ((xwidget-prop (cdr (get-text-property pos 'display)))
+ (id (plist-get xwidget-prop ':xwidget-id)))
+
+ (setq xwidget-prop (plist-put xwidget-prop ':width width))
+ (setq xwidget-prop (plist-put xwidget-prop ':height height))
+
+ (put-text-property pos (+ 1 pos) 'display (cons 'xwidget xwidget-prop))
+ (xwidget-resize id width height)))
+
+(defun ezbl-fill-window (&optional inst)
+ "Re-sizes the xwidget in the display-buffer of INST to fill its
+entire window."
+ (let ((buffer (ezbl-inst-display-buffer inst)))
+ (with-current-buffer buffer
+ (let* ((edges-list (window-inside-pixel-edges (get-buffer-window buffer)))
+ (left (nth 0 edges-list))
+ (top (nth 1 edges-list))
+ (right (nth 2 edges-list))
+ (bottom (nth 3 edges-list))
+ (height (- bottom top))
+ (width (- right left)))
+ ;; Turn read-only off while modifying the size of the xwidget, then
+ ;; reactivate it.
+ (toggle-read-only -1)
+ (ezbl-xwidget-resize-at 1 width height)
+ (toggle-read-only t)
+ (set-buffer-modified-p nil)))))
+
+(defun ezbl-event-listener (inst answer)
+ "Filter for ezbl processes.
+
+INST should be the `ezbl-inst' of the associated Uzbl process
+and ANSWER is the string returned by the process."
+ (let ((answers (split-string answer "\n" t)))
+ (dolist (ans answers)
+ (if (string-match "^EVENT \\[\\([[:alnum:]]+\\)\\] \\([A-Z_]+\\) ?\\(.*\\)$" ans)
+ (let ((app-name (match-string-no-properties 1 ans))
+ (event (intern (match-string-no-properties 2 ans)))
+ (detail (match-string-no-properties 3 ans)))
+ (ezbl-event-handler inst event detail app-name))
+ (ezbl-process-append inst (concat ans "\n"))))))
+
+(defun ezbl-event-handler (inst event detail &optional app-name)
+ "Respond to a Uzbl-generated event.
+
+EVENT is the interned symbol of the event Uzbl returned, while
+DETAIL is a (possibly-empty) string containing any additional
+information included with the event.
+
+INST is resolvable to an ezbl instance."
+ (cond
+ ((eq event 'INSTANCE_START))
+ ((eq event 'INSTANCE_EXIT))
+ ((eq event 'VARIABLE_SET)
+ (if (string-match "\\([a-z0-9_]+\\) \\(str\\|int\\|float\\) \\(.*\\)" detail)
+ (let ((var-name (intern (match-string-no-properties 1 detail)))
+ (type (match-string-no-properties 2 detail))
+ (value (match-string-no-properties 3 detail)))
+ (puthash var-name value (ezbl-inst-vars inst)))
+ (error "VARIABLE_SET event had invalid details: `%s'" detail)))
+ ((eq event 'COMMAND_EXECUTED))
+ ;; ((eq event 'COMMAND_ERROR))
+ ;; ((eq event 'GEOMETRY_CHANGED))
+ ;; ((eq event 'FIFO_SET))
+ ;; ((eq event 'SOCKET_SET))
+ ((eq event 'LOAD_COMMIT)
+ (puthash 'uri detail (ezbl-inst-vars inst)))
+ ((eq event 'LOAD_START))
+ ((eq event 'LOAD_FINISH))
+ ;; ((eq event 'LOAD_ERROR))
+ ((eq event 'LOAD_PROGRESS))
+ ((eq event 'TITLE_CHANGED)
+ (puthash 'title detail (ezbl-inst-vars inst)))
+ ;; ((eq event 'DOWNLOAD_REQUEST))
+ ((eq event 'LINK_HOVER))
+ ((eq event 'LINK_UNHOVER))
+ ;; ((eq event 'KEY_PRESS))
+ ;; ((eq event 'KEY_RELEASE))
+ ;; ((eq event 'SELECTION_CHANGED))
+ ;; ((eq event 'NEW_WINDOW))
+ ;; ((eq event 'WEBINSPECTOR))
+ ;; ((eq event 'WEBINSPECTOR))
+ ;; ((eq event 'FOCUS_GAINED))
+ ;; ((eq event 'FOCUS_LOST))
+ ;; ((eq event 'FORM_ACTIVE))
+ ;; ((eq event 'ROOT_ACTIVE))
+ ;; ((eq event 'FILE_INCLUDED))
+ ((eq event 'PLUG_CREATED))
+ ((eq event 'BUILTINS))
+ (t
+ (ezbl-process-append inst (format "EVENT [%s] %s %s\n" app-name event detail)))))
+
+(defun ezbl-process-append (inst string)
+ "Insert STRING at the end of the process buffer of INST.
+
+Takes into account the current position of process-mark for the
+process owning buffer."
+ (with-current-buffer (ezbl-inst-output-buffer inst)
+ (let* ((proc (ezbl-inst-process inst))
+ (moving (= (point) (process-mark proc))))
+ (save-excursion
+ ;; Insert the text, advancing the process marker.
+ (goto-char (process-mark proc))
+ (insert string)
+ (set-marker (process-mark proc) (point)))
+ (if moving (goto-char (process-mark proc))))))
+
+;; Should always remain at the end, just before "(provide 'ezbl)".
+(ezbl-command-init)
+
+(provide 'ezbl)
+
+;;; ezbl.el ends here