aboutsummaryrefslogtreecommitdiffhomepage
path: root/examples/data/scripts
diff options
context:
space:
mode:
Diffstat (limited to 'examples/data/scripts')
-rwxr-xr-xexamples/data/scripts/cookies.sh154
-rwxr-xr-xexamples/data/scripts/download.sh22
-rw-r--r--examples/data/scripts/extedit.js102
-rw-r--r--examples/data/scripts/follow_Numbers.js228
-rw-r--r--examples/data/scripts/follow_Numbers_Strings.js212
-rwxr-xr-xexamples/data/scripts/formfiller.pl99
-rwxr-xr-xexamples/data/scripts/formfiller.sh62
-rw-r--r--examples/data/scripts/hint.js26
-rwxr-xr-xexamples/data/scripts/history.sh5
-rwxr-xr-xexamples/data/scripts/insert_bookmark.sh16
-rwxr-xr-xexamples/data/scripts/instance-select-wmii.sh54
-rw-r--r--examples/data/scripts/linkfollow.js269
-rwxr-xr-xexamples/data/scripts/load_url_from_bookmarks.sh20
-rwxr-xr-xexamples/data/scripts/load_url_from_history.sh24
-rwxr-xr-xexamples/data/scripts/scheme.py24
-rw-r--r--examples/data/scripts/scroll-percentage.js68
-rwxr-xr-xexamples/data/scripts/session.sh62
-rwxr-xr-xexamples/data/scripts/uzbl-cookie-daemon664
-rwxr-xr-xexamples/data/scripts/uzbl-event-manager833
-rwxr-xr-xexamples/data/scripts/uzbl-tabbed1417
-rwxr-xr-xexamples/data/scripts/uzblcat12
21 files changed, 4373 insertions, 0 deletions
diff --git a/examples/data/scripts/cookies.sh b/examples/data/scripts/cookies.sh
new file mode 100755
index 0000000..ee2ce51
--- /dev/null
+++ b/examples/data/scripts/cookies.sh
@@ -0,0 +1,154 @@
+#!/bin/sh
+
+set -n;
+
+# THIS IS EXPERIMENTAL AND COULD BE INSECURE !!!!!!
+
+# this is an example bash script of how you could manage your cookies. it is very raw and basic and not as good as uzbl-cookie-daemon
+# we use the cookies.txt format (See http://kb.mozillazine.org/Cookies.txt)
+# This is one textfile with entries like this:
+# kb.mozillazine.org FALSE / FALSE 1146030396 wikiUserID 16993
+# domain alow-read-other-subdomains path http-required expiration name value
+# you probably want your cookies config file in your $XDG_CONFIG_HOME ( eg $HOME/.config/uzbl/cookies)
+# Note. in uzbl there is no strict definition on what a session is. it's YOUR job to clear cookies marked as end_session if you want to keep cookies only valid during a "session"
+# MAYBE TODO: allow user to edit cookie before saving. this cannot be done with zenity :(
+# TODO: different cookie paths per config (eg per group of uzbl instances)
+
+# TODO: correct implementation.
+# see http://curl.haxx.se/rfc/cookie_spec.html
+# http://en.wikipedia.org/wiki/HTTP_cookie
+
+# TODO : check expires= before sending.
+# write sample script that cleans up cookies dir based on expires attribute.
+# TODO: check uri against domain attribute. and path also.
+# implement secure attribute.
+# support blocking or not for 3rd parties
+# http://kb.mozillazine.org/Cookies.txt
+# don't always append cookies, sometimes we need to overwrite
+
+cookie_config=${XDG_CONFIG_HOME:-${HOME}/.config}/uzbl/cookies
+[ "x$cookie_config" = x ] && exit 1
+[ -d "${XDG_DATA_HOME:-${HOME}/.local/share}/uzbl/" ] &&\
+cookie_data=${XDG_DATA_HOME:-${HOME}/.local/share}/uzbl/cookies.txt || exit 1
+
+notifier=
+#notifier=notify-send
+#notify_wrapper () {
+# echo "$@" >> $HOME/cookielog
+#}
+#notifier=notifier_wrapper
+
+# if this variable is set, we will use it to inform you when and which cookies we store, and when/which we send.
+# it's primarily used for debugging
+notifier=
+which zenity &>/dev/null || exit 2
+
+# Example cookie:
+# test_cookie=CheckForPermission; expires=Thu, 07-May-2009 19:17:55 GMT; path=/; domain=.doubleclick.net
+
+# uri=$6
+# uri=${uri/http:\/\/} # strip 'http://' part
+# host=${uri/\/*/}
+action=$8 # GET/PUT
+shift
+host=$9
+shift
+path=$9
+shift
+cookie=$9
+
+field_domain=$host
+field_path=$path
+field_name=
+field_value=
+field_exp='end_session'
+
+notify() {
+ [ -n "$notifier" ] && $notifier "$@"
+}
+
+
+# FOR NOW LETS KEEP IT SIMPLE AND JUST ALWAYS PUT AND ALWAYS GET
+parse_cookie() {
+ IFS=$';'
+ first_pair=1
+ for pair in $cookie
+ do
+ if [ "x$first_pair" = x1 ]
+ then
+ field_name=${pair%%=*}
+ field_value=${pair#*=}
+ first_pair=0
+ else
+ echo "$pair" | read -r pair #strip leading/trailing wite space
+ key=${pair%%=*}
+ val=${pair#*=}
+ [ "$key" == expires ] && field_exp=`date -u -d "$val" +'%s'`
+ # TODO: domain
+ [ "$key" == path ] && field_path=$val
+ fi
+ done
+ unset IFS
+}
+
+# match cookies in cookies.txt against hostname and path
+get_cookie() {
+ path_esc=${path//\//\\/}
+ search="^[^\t]*$host\t[^\t]*\t$path_esc"
+ cookie=`awk "/$search/" $cookie_data 2>/dev/null | tail -n 1`
+ if [ -z "$cookie" ]
+ then
+ notify "Get_cookie: search: $search in $cookie_data -> no result"
+ false
+ else
+ notify "Get_cookie: search: $search in $cookie_data -> result: $cookie"
+ echo "$cookie" | \
+ read domain alow_read_other_subdomains path http_required expiration name \
+ value;
+ cookie="$name=$value"
+ true
+ fi
+}
+
+save_cookie() {
+ if parse_cookie
+ then
+ data="$field_domain\tFALSE\t$field_path\tFALSE\t$field_exp\t$field_name\t$field_value"
+ notify "save_cookie: adding $data to $cookie_data"
+ echo -e "$data" >> $cookie_data
+ else
+ notify "not saving a cookie. since we don't have policies yet, parse_cookie must have returned false. this is a bug"
+ fi
+}
+
+[ "x$action" = xPUT ] && save_cookie
+[ "x$action" = xGET ] && get_cookie && echo "$cookie"
+
+exit
+
+
+# TODO: implement this later.
+# $1 = section (TRUSTED or DENY)
+# $2 =url
+match() {
+ sed -n "/$1/,/^\$/p" $cookie_config 2>/dev/null | grep -q "^$host"
+}
+
+fetch_cookie() {
+ cookie=`cat $cookie_data`
+}
+
+store_cookie() {
+ echo $cookie > $cookie_data
+}
+
+if match TRUSTED $host
+then
+ [ "x$action" = xPUT ] && store_cookie $host
+ [ "x$action" = xGET ] && fetch_cookie && echo "$cookie"
+elif ! match DENY $host
+then
+ [ "x$action" = xPUT ] && cookie=`zenity --entry --title 'Uzbl Cookie handler' --text "Accept this cookie from $host ?" --entry-text="$cookie"` && store_cookie $host
+ [ "x$action" = xGET ] && fetch_cookie && cookie=`zenity --entry --title 'Uzbl Cookie handler' --text "Submit this cookie to $host ?" --entry-text="$cookie"` && echo $cookie
+fi
+exit 0
diff --git a/examples/data/scripts/download.sh b/examples/data/scripts/download.sh
new file mode 100755
index 0000000..1c7d039
--- /dev/null
+++ b/examples/data/scripts/download.sh
@@ -0,0 +1,22 @@
+#!/bin/sh
+# just an example of how you could handle your downloads
+# try some pattern matching on the uri to determine what we should do
+
+# Some sites block the default wget --user-agent..
+GET="wget --user-agent=Firefox"
+
+dest="$HOME"
+url="$8"
+
+http_proxy="$9"
+export http_proxy
+
+test "x$url" = "x" && { echo "you must supply a url! ($url)"; exit 1; }
+
+# only changes the dir for the $get sub process
+if echo "$url" | grep -E '.*\.torrent' >/dev/null;
+then
+ ( cd "$dest"; $GET "$url")
+else
+ ( cd "$dest"; $GET "$url")
+fi
diff --git a/examples/data/scripts/extedit.js b/examples/data/scripts/extedit.js
new file mode 100644
index 0000000..8ed346d
--- /dev/null
+++ b/examples/data/scripts/extedit.js
@@ -0,0 +1,102 @@
+/*
+ * Edit forms in external editor
+ *
+ * (c) 2009, Robert Manea
+ * utf8 functions are (c) by Webtoolkit.info (http://www.webtoolkit.info/)
+ *
+ *
+ * Installation:
+ * - Copy this script to $HOME/.local/share/uzbl/scripts
+ * - Add the following to $HOME/.config/uzbl/config:
+ * @bind E = script @scripts_dir/extedit.js
+ * - Set your preferred editor
+ * set editor = gvim
+ * - non-GUI editors
+ * set editor = xterm -e vim
+ *
+ * Usage:
+ * Select (click) an editable form, go to command mode and hit E
+ *
+*/
+
+
+function utf8_decode ( str_data ) {
+ var tmp_arr = [], i = 0, ac = 0, c1 = 0, c2 = 0, c3 = 0;
+
+ str_data += '';
+
+ while ( i < str_data.length ) {
+ c1 = str_data.charCodeAt(i);
+ if (c1 < 128) {
+ tmp_arr[ac++] = String.fromCharCode(c1);
+ i++;
+ } else if ((c1 > 191) && (c1 < 224)) {
+ c2 = str_data.charCodeAt(i+1);
+ tmp_arr[ac++] = String.fromCharCode(((c1 & 31) << 6) | (c2 & 63));
+ i += 2;
+ } else {
+ c2 = str_data.charCodeAt(i+1);
+ c3 = str_data.charCodeAt(i+2);
+ tmp_arr[ac++] = String.fromCharCode(((c1 & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
+ i += 3;
+ }
+ }
+
+ return tmp_arr.join('');
+}
+
+
+function utf8_encode ( argString ) {
+ var string = (argString+''); // .replace(/\r\n/g, "\n").replace(/\r/g, "\n");
+
+ var utftext = "";
+ var start, end;
+ var stringl = 0;
+
+ start = end = 0;
+ stringl = string.length;
+ for (var n = 0; n < stringl; n++) {
+ var c1 = string.charCodeAt(n);
+ var enc = null;
+
+ if (c1 < 128) {
+ end++;
+ } else if (c1 > 127 && c1 < 2048) {
+ enc = String.fromCharCode((c1 >> 6) | 192) + String.fromCharCode((c1 & 63) | 128);
+ } else {
+ enc = String.fromCharCode((c1 >> 12) | 224) + String.fromCharCode(((c1 >> 6) & 63) | 128) + String.fromCharCode((c1 & 63) | 128);
+ }
+ if (enc !== null) {
+ if (end > start) {
+ utftext += string.substring(start, end);
+ }
+ utftext += enc;
+ start = end = n+1;
+ }
+ }
+
+ if (end > start) {
+ utftext += string.substring(start, string.length);
+ }
+
+ return utftext;
+}
+
+
+(function() {
+ var actelem = document.activeElement;
+
+ if(actelem.type == 'text' || actelem.type == 'textarea') {
+ var editor = Uzbl.run("print @external_editor") || "gvim";
+ var filename = Uzbl.run("print @(mktemp /tmp/uzbl_edit.XXXXXX)@");
+
+ if(actelem.value)
+ Uzbl.run("sh 'echo " + window.btoa(utf8_encode(actelem.value)) + " | base64 -d > " + filename + "'");
+
+ Uzbl.run("sync_sh '" + editor + " " + filename + "'");
+ actelem.value = utf8_decode(window.atob(Uzbl.run("print @(base64 -w 0 " + filename + ")@")));
+
+ Uzbl.run("sh 'rm -f " + filename + "'");
+ }
+
+ })();
diff --git a/examples/data/scripts/follow_Numbers.js b/examples/data/scripts/follow_Numbers.js
new file mode 100644
index 0000000..00b279e
--- /dev/null
+++ b/examples/data/scripts/follow_Numbers.js
@@ -0,0 +1,228 @@
+/* This is the basic linkfollowing script.
+ * Its pretty stable, only using numbers to navigate.
+ *
+ * TODO: Some pages mess around a lot with the zIndex which
+ * lets some hints in the background.
+ * TODO: Some positions are not calculated correctly (mostly
+ * because of uber-fancy-designed-webpages. Basic HTML and CSS
+ * works good
+ * TODO: Still some links can't be followed/unexpected things
+ * happen. Blame some freaky webdesigners ;)
+ */
+
+//Just some shortcuts and globals
+var uzblid = 'uzbl_link_hint';
+var uzbldivid = uzblid + '_div_container';
+var doc = document;
+var win = window;
+var links = document.links;
+var forms = document.forms;
+
+//Reset keycmd, modcmd and return to default mode.
+function clearKeycmd() { Uzbl.run('set mode ='); }
+
+//Make onlick-links "clickable"
+try {
+ HTMLElement.prototype.click = function() {
+ if (typeof this.onclick == 'function') {
+ this.onclick({
+ type: 'click'
+ });
+ }
+ };
+} catch(e) {}
+//Catch the ESC keypress to stop linkfollowing
+function keyPressHandler(e) {
+ var kC = window.event ? event.keyCode: e.keyCode;
+ var Esc = window.event ? 27 : e.DOM_VK_ESCAPE;
+ if (kC == Esc) {
+ removeAllHints();
+ }
+}
+//Calculate element position to draw the hint
+//Pretty accurate but on fails in some very fancy cases
+function elementPosition(el) {
+ var up = el.offsetTop;
+ var left = el.offsetLeft;
+ var width = el.offsetWidth;
+ var height = el.offsetHeight;
+ while (el.offsetParent) {
+ el = el.offsetParent;
+ up += el.offsetTop;
+ left += el.offsetLeft;
+ }
+ return [up, left, width, height];
+}
+//Calculate if an element is visible
+function isVisible(el) {
+ if (el == doc) {
+ return true;
+ }
+ if (!el) {
+ return false;
+ }
+ if (!el.parentNode) {
+ return false;
+ }
+ if (el.style) {
+ if (el.style.display == 'none') {
+ return false;
+ }
+ if (el.style.visibility == 'hidden') {
+ return false;
+ }
+ }
+ return isVisible(el.parentNode);
+}
+//Calculate if an element is on the viewport.
+function elementInViewport(el) {
+ offset = elementPosition(el);
+ var up = offset[0];
+ var left = offset[1];
+ var width = offset[2];
+ var height = offset[3];
+ return up < window.pageYOffset + window.innerHeight && left < window.pageXOffset + window.innerWidth && (up + height) > window.pageYOffset && (left + width) > window.pageXOffset;
+}
+//Removes all hints/leftovers that might be generated
+//by this script.
+function removeAllHints() {
+ var elements = doc.getElementById(uzbldivid);
+ if (elements) {
+ elements.parentNode.removeChild(elements);
+ }
+}
+//Generate a hint for an element with the given label
+//Here you can play around with the style of the hints!
+function generateHint(el, label) {
+ var pos = elementPosition(el);
+ var hint = doc.createElement('div');
+ hint.setAttribute('name', uzblid);
+ hint.innerText = label;
+ hint.style.display = 'inline';
+ hint.style.backgroundColor = '#B9FF00';
+ hint.style.border = '2px solid #4A6600';
+ hint.style.color = 'black';
+ hint.style.fontSize = '9px';
+ hint.style.fontWeight = 'bold';
+ hint.style.lineHeight = '9px';
+ hint.style.margin = '0px';
+ hint.style.padding = '1px';
+ hint.style.position = 'absolute';
+ hint.style.zIndex = '1000';
+ hint.style.left = pos[1] + 'px';
+ hint.style.top = pos[0] + 'px';
+ var img = el.getElementsByTagName('img');
+ if (img.length > 0) {
+ hint.style.left = pos[1] + img[0].width / 2 + 'px';
+ }
+ hint.style.textDecoration = 'none';
+ hint.style.webkitBorderRadius = '6px';
+ // Play around with this, pretty funny things to do :)
+ hint.style.webkitTransform = 'scale(1) rotate(0deg) translate(-6px,-5px)';
+ return hint;
+}
+//Here we choose what to do with an element if we
+//want to "follow" it. On form elements we "select"
+//or pass the focus, on links we try to perform a click,
+//but at least set the href of the link. (needs some improvements)
+function clickElem(item) {
+ removeAllHints();
+ clearKeycmd();
+ if (item) {
+ var name = item.tagName;
+ if (name == 'A') {
+ item.click();
+ window.location = item.href;
+ } else if (name == 'INPUT') {
+ var type = item.getAttribute('type').toUpperCase();
+ if (type == 'TEXT' || type == 'FILE' || type == 'PASSWORD') {
+ item.focus();
+ item.select();
+ } else {
+ item.click();
+ }
+ } else if (name == 'TEXTAREA' || name == 'SELECT') {
+ item.focus();
+ item.select();
+ } else {
+ item.click();
+ window.location = item.href;
+ }
+ }
+}
+//Returns a list of all links (in this version
+//just the elements itself, but in other versions, we
+//add the label here.
+function addLinks() {
+ res = [[], []];
+ for (var l = 0; l < links.length; l++) {
+ var li = links[l];
+ if (isVisible(li) && elementInViewport(li)) {
+ res[0].push(li);
+ }
+ }
+ return res;
+}
+//Same as above, just for the form elements
+function addFormElems() {
+ res = [[], []];
+ for (var f = 0; f < forms.length; f++) {
+ for (var e = 0; e < forms[f].elements.length; e++) {
+ var el = forms[f].elements[e];
+ if (el && ['INPUT', 'TEXTAREA', 'SELECT'].indexOf(el.tagName) + 1 && isVisible(el) && elementInViewport(el)) {
+ res[0].push(el);
+ }
+ }
+ }
+ return res;
+}
+//Draw all hints for all elements passed. "len" is for
+//the number of chars we should use to avoid collisions
+function reDrawHints(elems, chars) {
+ removeAllHints();
+ var hintdiv = doc.createElement('div');
+ hintdiv.setAttribute('id', uzbldivid);
+ for (var i = 0; i < elems[0].length; i++) {
+ if (elems[0][i]) {
+ var label = elems[1][i].substring(chars);
+ var h = generateHint(elems[0][i], label);
+ hintdiv.appendChild(h);
+ }
+ }
+ if (document.body) {
+ document.body.appendChild(hintdiv);
+ }
+}
+//Put it all together
+function followLinks(follow) {
+ var s = follow.split('');
+ var linknr = parseInt(follow, 10);
+ if (document.body) document.body.setAttribute('onkeyup', 'keyPressHandler(event)');
+ var linkelems = addLinks();
+ var formelems = addFormElems();
+ var elems = [linkelems[0].concat(formelems[0]), linkelems[1].concat(formelems[1])];
+ var len = (elems[0].length + '').length;
+ var oldDiv = doc.getElementById(uzbldivid);
+ var leftover = [[], []];
+ if (linknr + 1 && s.length == len && linknr < elems[0].length && linknr >= 0) {
+ clickElem(elems[0][linknr]);
+ } else {
+ for (var j = 0; j < elems[0].length; j++) {
+ var b = true;
+ var label = j + '';
+ var n = label.length;
+ for (n; n < len; n++) {
+ label = '0' + label;
+ }
+ for (var k = 0; k < s.length; k++) {
+ b = b && label.charAt(k) == s[k];
+ }
+ if (b) {
+ leftover[0].push(elems[0][j]);
+ leftover[1].push(label);
+ }
+ }
+ reDrawHints(leftover, s.length);
+ }
+}
+followLinks('%s');
diff --git a/examples/data/scripts/follow_Numbers_Strings.js b/examples/data/scripts/follow_Numbers_Strings.js
new file mode 100644
index 0000000..e50da5d
--- /dev/null
+++ b/examples/data/scripts/follow_Numbers_Strings.js
@@ -0,0 +1,212 @@
+var uzblid = 'uzbl_link_hint';
+var uzbldivid = uzblid + '_div_container';
+var doc = document;
+var win = window;
+var links = document.links;
+var forms = document.forms;
+
+//Reset keycmd, modcmd and return to default mode.
+function clearKeycmd() { Uzbl.run('set mode ='); }
+
+try {
+ HTMLElement.prototype.click = function() {
+ if (typeof this.onclick == 'function') {
+ this.onclick({
+ type: 'click'
+ });
+ }
+ };
+} catch(e) {}
+function keyPressHandler(e) {
+ var kC = window.event ? event.keyCode: e.keyCode;
+ var Esc = window.event ? 27 : e.DOM_VK_ESCAPE;
+ if (kC == Esc) {
+ removeAllHints();
+ }
+}
+function elementPosition(el) {
+ var up = el.offsetTop;
+ var left = el.offsetLeft;
+ var width = el.offsetWidth;
+ var height = el.offsetHeight;
+ while (el.offsetParent) {
+ el = el.offsetParent;
+ up += el.offsetTop;
+ left += el.offsetLeft;
+ }
+ return [up, left, width, height];
+}
+function isVisible(el) {
+ if (el == doc) {
+ return true;
+ }
+ if (!el) {
+ return false;
+ }
+ if (!el.parentNode) {
+ return false;
+ }
+ if (el.style) {
+ if (el.style.display == 'none') {
+ return false;
+ }
+ if (el.style.visibility == 'hidden') {
+ return false;
+ }
+ }
+ return isVisible(el.parentNode);
+}
+function elementInViewport(el) {
+ offset = elementPosition(el);
+ var up = offset[0];
+ var left = offset[1];
+ var width = offset[2];
+ var height = offset[3];
+ return up < window.pageYOffset + window.innerHeight && left < window.pageXOffset + window.innerWidth && (up + height) > window.pageYOffset && (left + width) > window.pageXOffset;
+}
+function removeAllHints() {
+ var elements = doc.getElementById(uzbldivid);
+ if (elements) {
+ elements.parentNode.removeChild(elements);
+ }
+}
+function generateHint(el, label) {
+ var pos = elementPosition(el);
+ var hint = doc.createElement('div');
+ hint.setAttribute('name', uzblid);
+ hint.innerText = label;
+ hint.style.display = 'inline';
+ hint.style.backgroundColor = '#B9FF00';
+ hint.style.border = '2px solid #4A6600';
+ hint.style.color = 'black';
+ hint.style.zIndex = '1000';
+ hint.style.fontSize = '9px';
+ hint.style.fontWeight = 'bold';
+ hint.style.lineHeight = '9px';
+ hint.style.margin = '0px';
+ hint.style.padding = '1px';
+ hint.style.position = 'absolute';
+ hint.style.left = pos[1] + 'px';
+ hint.style.top = pos[0] + 'px';
+ var img = el.getElementsByTagName('img');
+ if (img.length > 0) {
+ hint.style.left = pos[1] + img[0].width / 2 + 'px';
+ }
+ hint.style.textDecoration = 'none';
+ hint.style.webkitBorderRadius = '6px';
+ hint.style.webkitTransform = 'scale(1) rotate(0deg) translate(-6px,-5px)';
+ return hint;
+}
+
+function clickElem(item) {
+ removeAllHints();
+ clearKeycmd();
+ if (item) {
+ var name = item.tagName;
+ if (name == 'A') {
+ item.click();
+ window.location = item.href;
+ } else if (name == 'INPUT') {
+ var type = item.getAttribute('type').toUpperCase();
+ if (type == 'TEXT' || type == 'FILE' || type == 'PASSWORD') {
+ item.focus();
+ item.select();
+ } else {
+ item.click();
+ }
+ } else if (name == 'TEXTAREA' || name == 'SELECT') {
+ item.focus();
+ item.select();
+ } else {
+ item.click();
+ window.location = item.href;
+ }
+ }
+}
+
+function addLinks() {
+ res = [[], []];
+ for (var l = 0; l < links.length; l++) {
+ var li = links[l];
+ if (isVisible(li) && elementInViewport(li)) {
+ res[0].push(li);
+ res[1].push(li.innerText.toLowerCase());
+ }
+ }
+ return res;
+}
+function addFormElems() {
+ res = [[], []];
+ for (var f = 0; f < forms.length; f++) {
+ for (var e = 0; e < forms[f].elements.length; e++) {
+ var el = forms[f].elements[e];
+ if (el && ['INPUT', 'TEXTAREA', 'SELECT'].indexOf(el.tagName) + 1 && isVisible(el) && elementInViewport(el)) {
+ res[0].push(el);
+ if (el.getAttribute('value')) {
+ res[1].push(el.getAttribute('value').toLowerCase());
+ } else {
+ res[1].push(el.getAttribute('name').toLowerCase());
+ }
+ }
+ }
+ }
+ return res;
+}
+function reDrawHints(elems, len) {
+ var hintdiv = doc.createElement('div');
+ hintdiv.setAttribute('id', uzbldivid);
+ hintdiv.style.opacity = '0.0';
+ for (var i = 0; i < elems[0].length; i++) {
+ var label = i + '';
+ var n = label.length;
+ for (n; n < len; n++) {
+ label = '0' + label;
+ }
+ if (elems[0][i]) {
+ var h = generateHint(elems[0][i], label);
+ hintdiv.appendChild(h);
+ }
+ }
+ if (document.body) {
+ document.body.appendChild(hintdiv);
+ hintdiv.style.opacity = '0.7'
+ }
+}
+function followLinks(follow) {
+ var s = follow.split('');
+ var linknr = parseInt(follow, 10);
+ if (document.body) document.body.setAttribute('onkeyup', 'keyPressHandler(event)');
+ var linkelems = addLinks();
+ var formelems = addFormElems();
+ var elems = [linkelems[0].concat(formelems[0]), linkelems[1].concat(formelems[1])];
+ var len = (elems[0].length + '').length;
+ var oldDiv = doc.getElementById(uzbldivid);
+ var leftover = [[], []];
+ if (linknr + 1 && s.length == len && linknr < elems[0].length && linknr >= 0) {
+ clickElem(elems[0][linknr]);
+ } else {
+ for (var j = 0; j < elems[0].length; j++) {
+ var b = true;
+ for (var k = 0; k < s.length; k++) {
+ b = b && elems[1][j].charAt(k) == s[k];
+ }
+ if (!b) {
+ elems[0][j] = null;
+ elems[1][j] = null;
+ } else {
+ leftover[0].push(elems[0][j]);
+ leftover[1].push(elems[1][j]);
+ }
+ }
+ if (leftover[0].length == 1) {
+ clickElem(leftover[0][0]);
+ } else if (!oldDiv) {
+ if (linknr + 1 || s.length == 0) {
+ reDrawHints(elems, len);
+ } else {
+ reDrawHints(leftover, len);
+ }
+ }
+ }
+}
+followLinks('%s');
diff --git a/examples/data/scripts/formfiller.pl b/examples/data/scripts/formfiller.pl
new file mode 100755
index 0000000..74dcc80
--- /dev/null
+++ b/examples/data/scripts/formfiller.pl
@@ -0,0 +1,99 @@
+#!/usr/bin/perl
+
+# a slightly more advanced form filler
+#
+# uses settings file like: $keydir/<domain>
+#TODO: fallback to $HOME/.local/share
+# user arg 1:
+# edit: force editing of the file (fetches if file is missing)
+# load: fill forms from file (fetches if file is missing)
+# new: fetch new file
+
+# usage example:
+# bind LL = spawn /usr/share/uzbl/examples/data/scripts/formfiller.pl load
+# bind LN = spawn /usr/share/uzbl/examples/data/scripts/formfiller.pl new
+# bind LE = spawn /usr/share/uzbl/examples/data/scripts/formfiller.pl edit
+
+use strict;
+use warnings;
+
+my $keydir = $ENV{XDG_CONFIG_HOME} . "/uzbl/forms";
+my ($config,$pid,$xid,$fifoname,$socket,$url,$title,$cmd) = @ARGV;
+if (!defined $fifoname || $fifoname eq "") { die "No fifo"; }
+
+sub domain {
+ my ($url) = @_;
+ $url =~ s#http(s)?://([A-Za-z0-9\.-]+)(/.*)?#$2#;
+ return $url;
+};
+
+my $editor = "xterm -e vim";
+#my $editor = "gvim";
+
+# ideally, there would be some way to ask uzbl for the html content instead of having to redownload it with
+# Also, you may need to fake the user-agent on some sites (like facebook)
+ my $downloader = "curl -A 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.10) Gecko/2009042810 GranParadiso/3.0.10' ";
+#my $downloader = "curl -s";
+
+my @fields = ("type","name","value");
+
+my %command;
+
+$command{load} = sub {
+ my ($domain) = @_;
+ my $filename = "$keydir/$domain";
+ if (-e $filename){
+ open(my $file, $filename) or die "Failed to open $filename: $!";
+ my (@lines) = <$file>;
+ close($file);
+ $|++;
+ open(my $fifo, ">>", $fifoname) or die "Failed to open $fifoname: $!";
+ foreach my $line (@lines) {
+ next if ($line =~ m/^#/);
+ my ($type,$name,$value) = ($line =~ /^\s*(\w+)\s*\|\s*(.*?)\s*\|\s*(.*?)\s*$/);
+ if ($type eq "checkbox")
+ {
+ printf $fifo 'js document.getElementsByName("%s")[0].checked = %s;', $name, $value;
+ } elsif ($type eq "submit")
+ {
+ printf $fifo 'js function fs (n) {try{n.submit()} catch (e){fs(n.parentNode)}}; fs(document.getElementsByName("%s")[0]);', $name;
+ } elsif ($type ne "")
+ {
+ printf $fifo 'js document.getElementsByName("%s")[0].value = "%s";', $name, $value;
+ }
+ print $fifo "\n";
+ }
+ $|--;
+ } else {
+ $command{new}->($domain);
+ $command{edit}->($domain);
+ }
+};
+$command{edit} = sub {
+ my ($domain) = @_;
+ my $file = "$keydir/$domain";
+ if(-e $file){
+ system ($editor, $file);
+ } else {
+ $command{new}->($domain);
+ }
+};
+$command{new} = sub {
+ my ($domain) = @_;
+ my $filename = "$keydir/$domain";
+ open (my $file,">>", $filename) or die "Failed to open $filename: $!";
+ $|++;
+ print $file "# Make sure that there are no extra submits, since it may trigger the wrong one.\n";
+ printf $file "#%-10s | %-10s | %s\n", @fields;
+ print $file "#------------------------------\n";
+ my @data = `$downloader $url`;
+ foreach my $line (@data){
+ if($line =~ m/<input ([^>].*?)>/i){
+ $line =~ s/.*(<input ([^>].*?)>).*/$1/;
+ printf $file " %-10s | %-10s | %s\n", map { my ($r) = $line =~ /.*$_=["'](.*?)["']/;$r } @fields;
+ };
+ };
+ $|--;
+};
+
+$command{$cmd}->(domain($url));
diff --git a/examples/data/scripts/formfiller.sh b/examples/data/scripts/formfiller.sh
new file mode 100755
index 0000000..10afaba
--- /dev/null
+++ b/examples/data/scripts/formfiller.sh
@@ -0,0 +1,62 @@
+#!/bin/bash
+
+# simple html form (eg for logins) filler (and manager) for uzbl.
+# uses settings files like: $keydir/<domain>
+# files contain lines like: <fieldname>: <value>
+
+
+# user arg 1:
+# edit: force editing the file (falls back to new if not found)
+# new: start with a new file.
+# load: try to load from file into form
+
+# something else (or empty): if file not available: new, otherwise load.
+
+keydir=${XDG_DATA_HOME:-$HOME/.local/share}/uzbl/forms
+[ -d "`dirname $keydir`" ] || exit 1
+[ -d "$keydir" ] || mkdir "$keydir"
+
+editor=${VISUAL}
+if [[ -z ${editor} ]]; then
+ #editor='gvim'
+ editor='urxvt -e vim'
+fi
+
+config=$1; shift
+pid=$1; shift
+xid=$1; shift
+fifo=$1; shift
+socket=$1; shift
+url=$1; shift
+title=$1; shift
+action=$1
+
+[ -d $keydir ] || mkdir $keydir || exit 1
+
+if [ "$action" != 'edit' -a "$action" != 'new' -a "$action" != 'load' ]
+then
+ action=new
+ [[ -e $keydir/$domain ]] && action=load
+elif [ "$action" == 'edit' ] && [[ ! -e $keydir/$domain ]]
+then
+ action=new
+fi
+domain=$(echo $url | sed -re 's|(http\|https)+://([A-Za-z0-9\.]+)/.*|\2|')
+
+
+#regex='s|.*<input.*?name="([[:graph:]]+)".*?/>.*|\1: |p' # sscj's first version, does not work on http://wiki.archlinux.org/index.php?title=Special:UserLogin&returnto=Main_Page
+ regex='s|.*<input.*?name="([^"]*)".*|\1: |p' #works on arch wiki, but not on http://lists.uzbl.org/listinfo.cgi/uzbl-dev-uzbl.org TODO: improve
+
+
+if [ "$action" = 'load' ]
+then
+ [[ -e $keydir/$domain ]] || exit 2
+ gawk -F': ' '{ print "js document.getElementsByName(\"" $1 "\")[0].value = \"" $2 "\";"}' $keydir/$domain >> $fifo
+else
+ if [ "$action" == 'new' ]
+ then
+ curl "$url" | grep '<input' | sed -nre "$regex" > $keydir/$domain
+ fi
+ [[ -e $keydir/$domain ]] || exit 3 #this should never happen, but you never know.
+ $editor $keydir/$domain #TODO: if user aborts save in editor, the file is already overwritten
+fi
diff --git a/examples/data/scripts/hint.js b/examples/data/scripts/hint.js
new file mode 100644
index 0000000..ec7f1e2
--- /dev/null
+++ b/examples/data/scripts/hint.js
@@ -0,0 +1,26 @@
+for (var i=0; i < document.links.length; i++) {
+ var uzblid = 'uzbl_link_hint_';
+ var li = document.links[i];
+ var pre = document.getElementById(uzblid+i);
+
+ if (pre) {
+ li.removeChild(pre);
+ } else {
+ var hint = document.createElement('div');
+ hint.setAttribute('id',uzblid+i);
+ hint.innerHTML = i;
+ hint.style.display='inline';
+ hint.style.lineHeight='90%';
+ hint.style.backgroundColor='red';
+ hint.style.color='white';
+ hint.style.fontSize='small-xx';
+ hint.style.fontWeight='light';
+ hint.style.margin='0px';
+ hint.style.padding='2px';
+ hint.style.position='absolute';
+ hint.style.textDecoration='none';
+ hint.style.left=li.style.left;
+ hint.style.top=li.style.top;
+ li.insertAdjacentElement('afterBegin',hint);
+ }
+}
diff --git a/examples/data/scripts/history.sh b/examples/data/scripts/history.sh
new file mode 100755
index 0000000..7c83aa6
--- /dev/null
+++ b/examples/data/scripts/history.sh
@@ -0,0 +1,5 @@
+#!/bin/sh
+
+file=${XDG_DATA_HOME:-$HOME/.local/share}/uzbl/history
+[ -d `dirname $file` ] || exit 1
+echo `date +'%Y-%m-%d %H:%M:%S'`" $6 $7" >> $file
diff --git a/examples/data/scripts/insert_bookmark.sh b/examples/data/scripts/insert_bookmark.sh
new file mode 100755
index 0000000..c34e7db
--- /dev/null
+++ b/examples/data/scripts/insert_bookmark.sh
@@ -0,0 +1,16 @@
+#!/bin/sh
+
+[ -d "${XDG_DATA_HOME:-$HOME/.local/share}/uzbl" ] || exit 1
+file=${XDG_DATA_HOME:-$HOME/.local/share}/uzbl/bookmarks
+
+which zenity &>/dev/null || exit 2
+
+entry=`zenity --entry --text="Add bookmark. add tags after the '\t', separated by spaces" --entry-text="$6 $7\t"`
+exitstatus=$?
+if [ $exitstatus -ne 0 ]; then exit $exitstatus; fi
+url=`echo $entry | awk '{print $1}'`
+
+# TODO: check if already exists, if so, and tags are different: ask if you want to replace tags
+echo "$entry" >/dev/null #for some reason we need this.. don't ask me why
+echo -e "$entry" >> $file
+true
diff --git a/examples/data/scripts/instance-select-wmii.sh b/examples/data/scripts/instance-select-wmii.sh
new file mode 100755
index 0000000..2bf13ba
--- /dev/null
+++ b/examples/data/scripts/instance-select-wmii.sh
@@ -0,0 +1,54 @@
+#!/bin/sh
+
+
+# This script allows you to focus another uzbl window
+# It considers all uzbl windows in the current tag
+# you can select one from a list, or go to the next/previous one
+# It does not change the layout (stacked/tiled/floating) nor does it
+# changes the size or viewing mode of a uzbl window
+# When your current uzbl window is maximized, the one you change to
+# will be maximized as well.
+# See http://www.uzbl.org/wiki/wmii for more info
+# $1 must be one of 'list', 'next', 'prev'
+
+COLORS=" -nb #303030 -nf khaki -sb #CCFFAA -sf #303030"
+
+if dmenu --help 2>&1 | grep -q '\[-rs\] \[-ni\] \[-nl\] \[-xs\]'
+then
+ DMENU="dmenu -i -xs -rs -l 10" # vertical patch
+else
+ DMENU="dmenu -i"
+fi
+
+if [ "$1" == 'list' ]
+then
+ list=
+ # get window id's of uzbl clients. we could also get the label in one shot but it's pretty tricky
+ for i in $(wmiir read /tag/sel/index | grep uzbl |cut -d ' ' -f2)
+ do
+ label=$(wmiir read /client/$i/label)
+ list="$list$i : $label\n"
+ done
+ window=$(echo -e "$list" | $DMENU $COLORS | cut -d ' ' -f1)
+ wmiir xwrite /tag/sel/ctl "select client $window"
+elif [ "$1" == 'next' ]
+then
+ current=$(wmiir read /client/sel/ctl | head -n 1)
+ # find the next uzbl window and focus it
+ next=$(wmiir read /tag/sel/index | grep -A 10000 " $current " | grep -m 1 uzbl | cut -d ' ' -f2)
+ if [ x"$next" != "x" ]
+ then
+ wmiir xwrite /tag/sel/ctl "select client $next"
+ fi
+elif [ "$1" == 'prev' ]
+then
+ current=$(wmiir read /client/sel/ctl | head -n 1)
+ prev=$(wmiir read /tag/sel/index | grep -B 10000 " $current " | tac | grep -m 1 uzbl | cut -d ' ' -f2)
+ if [ x"$prev" != "x" ]
+ then
+ wmiir xwrite /tag/sel/ctl "select client $prev"
+ fi
+else
+ echo "\$1 not valid" >&2
+ exit 2
+fi
diff --git a/examples/data/scripts/linkfollow.js b/examples/data/scripts/linkfollow.js
new file mode 100644
index 0000000..3109cda
--- /dev/null
+++ b/examples/data/scripts/linkfollow.js
@@ -0,0 +1,269 @@
+// link follower for uzbl
+// requires http://github.com/DuClare/uzbl/commit/6c11777067bdb8aac09bba78d54caea04f85e059
+//
+// first, it needs to be loaded before every time it is used.
+// One way would be to use the load_commit_handler:
+// set load_commit_handler = sh 'echo "script /usr/share/uzbl/examples/data/scripts/linkfollow.js" > "$4"'
+//
+// when script is loaded, it can be invoked with
+// bind f* = js hints.set("%s", hints.open)
+// bind f_ = js hints.follow("%s",hints.open)
+//
+// At the moment, it may be useful to have way of forcing uzbl to load the script
+// bind :lf = script /usr/share/uzbl/examples/data/scripts/linkfollow.js
+//
+// The default style for the hints are pretty ugly, so it is recommended to add the following
+// to config file
+// set stylesheet_uri = /usr/share/uzbl/examples/data/style.css
+//
+// based on follow_Numbers.js
+//
+// TODO: fix styling for the first element
+// TODO: emulate mouseover events when visiting some elements
+// TODO: rewrite the element->action handling
+
+
+function Hints(){
+
+ // Settings
+ ////////////////////////////////////////////////////////////////////////////
+
+ // if set to true, you must explicitly call hints.follow(), otherwise it will
+ // follow the link if there is only one matching result
+ var requireReturn = true;
+
+ // Case sensitivity flag
+ var matchCase = "i";
+
+ // For case sensitive matching, uncomment:
+ // var matchCase = "";
+
+
+ var uzblid = 'uzbl_hint';
+ var uzblclass = 'uzbl_highlight';
+ var uzblclassfirst = 'uzbl_h_first';
+ var doc = document;
+ var visible = [];
+ var hintdiv;
+
+ this.set = hint;
+ this.follow = follow;
+ this.keyPressHandler = keyPressHandler;
+
+ function elementPosition(el) {
+ var up = el.offsetTop;
+ var left = el.offsetLeft; var width = el.offsetWidth;
+ var height = el.offsetHeight;
+
+ while (el.offsetParent) {
+ el = el.offsetParent;
+ up += el.offsetTop;
+ left += el.offsetLeft;
+ }
+ return {up: up, left: left, width: width, height: height};
+ }
+
+ function elementInViewport(p) {
+ return (p.up < window.pageYOffset + window.innerHeight &&
+ p.left < window.pageXOffset + window.innerWidth &&
+ (p.up + p.height) > window.pageYOffset &&
+ (p.left + p.width) > window.pageXOffset);
+ }
+
+ function isVisible(el) {
+ if (el == doc) { return true; }
+ if (!el) { return false; }
+ if (!el.parentNode) { return false; }
+ if (el.style) {
+ if (el.style.display == 'none') {
+ return false;
+ }
+ if (el.style.visibility == 'hidden') {
+ return false;
+ }
+ }
+ return isVisible(el.parentNode);
+ }
+
+ // the vimperator defaults minus the xhtml elements, since it gave DOM errors
+ var hintable = " //*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or @class='lk' or @role='link' or @href] | //input[not(@type='hidden')] | //a | //area | //iframe | //textarea | //button | //select";
+
+ function Matcher(str){
+ var numbers = str.replace(/[^\d]/g,"");
+ var words = str.replace(/\d/g,"").split(/\s+/).map(function (n) { return new RegExp(n,matchCase)});
+ this.test = test;
+ this.toString = toString;
+ this.numbers = numbers;
+ function matchAgainst(element){
+ if(element.node.nodeName == "INPUT"){
+ return element.node.value;
+ } else {
+ return element.node.textContent;
+ }
+ }
+ function test(element) {
+ // test all the regexp
+ var item = matchAgainst(element);
+ return words.every(function (regex) { return item.match(regex)});
+ }
+ }
+
+ function HintElement(node,pos){
+
+ this.node = node;
+ this.isHinted = false;
+ this.position = pos;
+ this.num = 0;
+
+ this.addHint = function (labelNum) {
+ // TODO: fix uzblclassfirst
+ if(!this.isHinted){
+ this.node.className += " " + uzblclass;
+ }
+ this.isHinted = true;
+
+ // create hint
+ var hintNode = doc.createElement('div');
+ hintNode.name = uzblid;
+ hintNode.innerText = labelNum;
+ hintNode.style.left = this.position.left + 'px';
+ hintNode.style.top = this.position.up + 'px';
+ hintNode.style.position = "absolute";
+ doc.body.firstChild.appendChild(hintNode);
+
+ }
+ this.removeHint = function(){
+ if(this.isHinted){
+ var s = (this.num)?uzblclassfirst:uzblclass;
+ this.node.className = this.node.className.replace(new RegExp(" "+s,"g"),"");
+ this.isHinted = false;
+ }
+ }
+ }
+
+ function createHintDiv(){
+ var hintdiv = doc.getElementById(uzblid);
+ if(hintdiv){
+ hintdiv.parentNode.removeChild(hintdiv);
+ }
+ hintdiv = doc.createElement("div");
+ hintdiv.setAttribute('id',uzblid);
+ doc.body.insertBefore(hintdiv,doc.body.firstChild);
+ return hintdiv;
+ }
+
+ function init(){
+ // WHAT?
+ doc.body.setAttribute("onkeyup","hints.keyPressHandler(event)");
+ hintdiv = createHintDiv();
+ visible = [];
+
+ var items = doc.evaluate(hintable,doc,null,XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,null);
+ for (var i = 0;i<items.snapshotLength;i++){
+ var item = items.snapshotItem(i);
+ var pos = elementPosition(item);
+ if(isVisible && elementInViewport(elementPosition(item))){
+ visible.push(new HintElement(item,pos));
+ }
+ }
+ }
+
+ function clear(){
+
+ visible.forEach(function (n) { n.removeHint(); } );
+ hintdiv = doc.getElementById(uzblid);
+ while(hintdiv){
+ hintdiv.parentNode.removeChild(hintdiv);
+ hintdiv = doc.getElementById(uzblid);
+ }
+ }
+
+ function update(str,openFun) {
+ var match = new Matcher(str);
+ hintdiv = createHintDiv();
+ var i = 1;
+ visible.forEach(function (n) {
+ if(match.test(n)) {
+ n.addHint(i);
+ i++;
+ } else {
+ n.removeHint();
+ }});
+ if(!requireReturn){
+ if(i==2){ //only been incremented once
+ follow(str,openFun);
+ }
+ }
+ }
+
+ function hint(str,openFun){
+ if(str.length == 0) init();
+ update(str,openFun);
+ }
+
+ function keyPressHandler(e) {
+ var kC = window.event ? event.keyCode: e.keyCode;
+ var Esc = window.event ? 27 : e.DOM_VK_ESCAPE;
+ if (kC == Esc) {
+ clear();
+ doc.body.removeAttribute("onkeyup");
+ }
+ }
+
+ this.openNewWindow = function(item){
+ // TODO: this doesn't work yet
+ item.className += " uzbl_follow";
+ window.open(item.href,"uzblnew","");
+ }
+ this.open = function(item){
+ simulateMouseOver(item);
+ item.className += " uzbl_follow";
+ window.location = item.href;
+ }
+
+ function simulateMouseOver(item){
+ var evt = doc.createEvent("MouseEvents");
+ evt.initMouseEvent("MouseOver",true,true,
+ doc.defaultView,1,0,0,0,0,
+ false,false,false,false,0,null);
+ return item.dispatchEvent(evt);
+ }
+
+
+ function follow(str,openFunction){
+ var m = new Matcher(str);
+ var items = visible.filter(function (n) { return n.isHinted });
+ clear();
+ var num = parseInt(m.numbers,10);
+ if(num){
+ var item = items[num-1].node;
+ } else {
+ var item = items[0].node;
+ }
+ if (item) {
+ var name = item.tagName;
+ if (name == 'A') {
+ if(item.click) {item.click()};
+ openFunction(item);
+ } else if (name == 'INPUT') {
+ var type = item.getAttribute('type').toUpperCase();
+ if (type == 'TEXT' || type == 'FILE' || type == 'PASSWORD') {
+ item.focus();
+ item.select();
+ } else {
+ item.click();
+ }
+ } else if (name == 'TEXTAREA' || name == 'SELECT') {
+ item.focus();
+ item.select();
+ } else {
+ item.click();
+ openFunction(item);
+ }
+ }
+ }
+}
+
+var hints = new Hints();
+
+// vim:set et sw=2:
diff --git a/examples/data/scripts/load_url_from_bookmarks.sh b/examples/data/scripts/load_url_from_bookmarks.sh
new file mode 100755
index 0000000..1e9f9e7
--- /dev/null
+++ b/examples/data/scripts/load_url_from_bookmarks.sh
@@ -0,0 +1,20 @@
+#!/bin/bash
+
+#NOTE: it's the job of the script that inserts bookmarks to make sure there are no dupes.
+
+file=${XDG_DATA_HOME:-$HOME/.local/share}/uzbl/bookmarks
+[ -r "$file" ] || exit
+COLORS=" -nb #303030 -nf khaki -sb #CCFFAA -sf #303030"
+if dmenu --help 2>&1 | grep -q '\[-rs\] \[-ni\] \[-nl\] \[-xs\]'
+then
+ DMENU="dmenu -i -xs -rs -l 10" # vertical patch
+ # show tags as well
+ goto=`$DMENU $COLORS < $file | awk '{print $1}'`
+else
+ DMENU="dmenu -i"
+ # because they are all after each other, just show the url, not their tags.
+ goto=`awk '{print $1}' $file | $DMENU $COLORS`
+fi
+
+#[ -n "$goto" ] && echo "uri $goto" > $4
+[ -n "$goto" ] && echo "uri $goto" | socat - unix-connect:$5
diff --git a/examples/data/scripts/load_url_from_history.sh b/examples/data/scripts/load_url_from_history.sh
new file mode 100755
index 0000000..62e02ac
--- /dev/null
+++ b/examples/data/scripts/load_url_from_history.sh
@@ -0,0 +1,24 @@
+#!/bin/sh
+
+history_file=${XDG_DATA_HOME:-$HOME/.local/share}/uzbl/history
+[ -r "$history_file" ] || exit 1
+
+# choose from all entries, sorted and uniqued
+# goto=`awk '{print $3}' $history_file | sort -u | dmenu -i`
+COLORS=" -nb #303030 -nf khaki -sb #CCFFAA -sf #303030"
+if dmenu --help 2>&1 | grep -q '\[-rs\] \[-ni\] \[-nl\] \[-xs\]';
+then
+ DMENU="dmenu -i -xs -rs -l 10" # vertical patch
+ # choose an item in reverse order, showing also the date and page titles
+ # pick the last field from the first 3 fields. this way you can pick a url (prefixed with date & time) or type just a new url.
+ goto=`tac $history_file | $DMENU $COLORS | cut -d ' ' -f -3 | awk '{print $NF}'`
+else
+ DMENU="dmenu -i"
+ # choose from all entries (no date or title), the first one being current url, and after that all others, sorted and uniqued, in ascending order
+ current=`tail -n 1 $history_file | awk '{print $3}'`;
+ goto=`(echo $current; awk '{print $3}' $history_file | grep -v "^$current\$" \
+ | sort -u) | $DMENU $COLORS`
+fi
+
+[ -n "$goto" ] && echo "uri $goto" > $4
+#[ -n "$goto" ] && echo "uri $goto" | socat - unix-connect:$5
diff --git a/examples/data/scripts/scheme.py b/examples/data/scripts/scheme.py
new file mode 100755
index 0000000..0916466
--- /dev/null
+++ b/examples/data/scripts/scheme.py
@@ -0,0 +1,24 @@
+#!/usr/bin/env python
+
+import os, subprocess, sys, urlparse
+
+def detach_open(cmd):
+ # Thanks to the vast knowledge of Laurence Withers (lwithers) and this message:
+ # http://mail.python.org/pipermail/python-list/2006-November/587523.html
+ if not os.fork():
+ null = os.open(os.devnull,os.O_WRONLY)
+ for i in range(3): os.dup2(null,i)
+ os.close(null)
+ subprocess.Popen(cmd)
+ print 'USED'
+
+if __name__ == '__main__':
+ uri = sys.argv[8]
+ u = urlparse.urlparse(uri)
+ if u.scheme == 'mailto':
+ detach_open(['xterm', '-e', 'mail', u.path])
+ elif u.scheme == 'xmpp':
+ # Someone check for safe arguments to gajim-remote
+ detach_open(['gajim-remote', 'open_chat', uri])
+ elif u.scheme == 'git':
+ detach_open(['git', 'clone', '--', uri], cwd=os.path.expanduser('~/src'))
diff --git a/examples/data/scripts/scroll-percentage.js b/examples/data/scripts/scroll-percentage.js
new file mode 100644
index 0000000..c9a51aa
--- /dev/null
+++ b/examples/data/scripts/scroll-percentage.js
@@ -0,0 +1,68 @@
+// VIM ruler style scroll message
+(function() {
+ var run = Uzbl.run;
+ var update_message = function() {
+ var innerHeight = window.innerHeight;
+ var scrollY = window.scrollY;
+ var height = document.height;
+ var message;
+
+ if (UzblZoom.type === "full") {
+ var zoom_level = UzblZoom.level;
+ innerHeight = Math.ceil(innerHeight * zoom_level);
+ scrollY = Math.ceil(scrollY * zoom_level);
+ height -= 1;
+ }
+
+ if (! height) {
+ message = "";
+ }
+ else if (height <= innerHeight) {
+ message = run("print @scroll_all_indicator") || "All";
+ }
+ else if (scrollY === 0) {
+ message = run("print @scroll_top_indicator") || "Top";
+ }
+ else if (scrollY + innerHeight >= height) {
+ message = run("print @scroll_bottom_indicator") || "Bot";
+ }
+ else {
+ var percentage = Math.round(scrollY / (height - innerHeight) * 100);
+ message = percentage + "%";
+ }
+ run("set scroll_message=" + message);
+ };
+
+ self.UzblZoom = {
+ get level() {
+ return Number(run("print @zoom_level")) || 1;
+ },
+ set level(level) {
+ if (typeof level === "number" && level > 0) {
+ run("set zoom_level = " + level);
+ update_message();
+ }
+ },
+ get type() {
+ return run("print @zoom_type") || "text";
+ },
+ set type(type) {
+ if ((type === "text" || type === "full") && this.type != type) {
+ run("toggle_zoom_type");
+ run("set zoom_type = " + type);
+ update_message();
+ }
+ },
+ toggle_type: function() {
+ this.type = (this.type === "text" ? "full" : "text");
+ }
+ };
+
+ window.addEventListener("DOMContentLoaded", update_message, false);
+ window.addEventListener("load", update_message, false);
+ window.addEventListener("resize", update_message, false);
+ window.addEventListener("scroll", update_message, false);
+ update_message();
+})();
+
+// vim: set noet ff=unix
diff --git a/examples/data/scripts/session.sh b/examples/data/scripts/session.sh
new file mode 100755
index 0000000..1059b5e
--- /dev/null
+++ b/examples/data/scripts/session.sh
@@ -0,0 +1,62 @@
+#!/bin/sh
+
+# Very simple session manager for uzbl-browser. When called with "endsession" as the
+# argument, it'll backup $sessionfile, look for fifos in $fifodir and
+# instruct each of them to store their current url in $sessionfile and
+# terminate themselves. Run with "launch" as the argument and an instance of
+# uzbl-browser will be launched for each stored url. "endinstance" is used internally
+# and doesn't need to be called manually at any point.
+# Add a line like 'bind quit = /path/to/session.sh endsession' to your config
+
+[ -d ${XDG_DATA_HOME:-$HOME/.local/share}/uzbl ] || exit 1
+scriptfile=$0 # this script
+sessionfile=${XDG_DATA_HOME:-$HOME/.local/share}/uzbl/browser-session # the file in which the "session" (i.e. urls) are stored
+configfile=${XDG_DATA_HOME:-$HOME/.local/share}/uzbl/config # uzbl configuration file
+UZBL="uzbl-browser -c $configfile" # add custom flags and whatever here.
+
+fifodir=/tmp # remember to change this if you instructed uzbl to put its fifos elsewhere
+thisfifo="$4"
+act="$8"
+url="$6"
+
+if [ "$act." = "." ]; then
+ act="$1"
+fi
+
+
+case $act in
+ "launch" )
+ urls=`cat $sessionfile`
+ if [ "$urls." = "." ]; then
+ $UZBL
+ else
+ for url in $urls; do
+ $UZBL --uri "$url" &
+ done
+ fi
+ exit 0
+ ;;
+
+ "endinstance" )
+ if [ "$url" != "(null)" ]; then
+ echo "$url" >> $sessionfile;
+ fi
+ echo "exit" > "$thisfifo"
+ ;;
+
+ "endsession" )
+ mv "$sessionfile" "$sessionfile~"
+ for fifo in $fifodir/uzbl_fifo_*; do
+ if [ "$fifo" != "$thisfifo" ]; then
+ echo "spawn $scriptfile endinstance" > "$fifo"
+ fi
+ done
+ echo "spawn $scriptfile endinstance" > "$thisfifo"
+ ;;
+
+ * ) echo "session manager: bad action"
+ echo "Usage: $scriptfile [COMMAND] where commands are:"
+ echo " launch - Restore a saved session or start a new one"
+ echo " endsession - Quit the running session. Must be called from uzbl"
+ ;;
+esac
diff --git a/examples/data/scripts/uzbl-cookie-daemon b/examples/data/scripts/uzbl-cookie-daemon
new file mode 100755
index 0000000..fde8b8e
--- /dev/null
+++ b/examples/data/scripts/uzbl-cookie-daemon
@@ -0,0 +1,664 @@
+#!/usr/bin/env python
+
+# The Python Cookie Daemon for Uzbl.
+# Copyright (c) 2009, Tom Adams <tom@holizz.com>
+# Copyright (c) 2009, Dieter Plaetinck <dieter@plaetinck.be>
+# Copyright (c) 2009, Mason Larobina <mason.larobina@gmail.com>
+# Copyright (c) 2009, Michael Fiano <axionix@gmail.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+'''
+The Python Cookie Daemon
+========================
+
+This daemon is a re-write of the original cookies.py script found in uzbl's
+master branch. This script provides more functionality than the original
+cookies.py by adding numerous command line options to specify different cookie
+jar locations, socket locations, verbose output, etc. This functionality is
+very useful as it allows you to run multiple daemons at once serving cookies
+to different groups of uzbl instances as required.
+
+Keeping up to date
+==================
+
+Check the cookie daemon uzbl-wiki page for more information on where to
+find the latest version of the cookie_daemon.py
+
+ http://www.uzbl.org/wiki/cookie_daemon.py
+
+Command line options
+====================
+
+Use the following command to get a full list of the cookie_daemon.py command
+line options:
+
+ ./cookie_daemon.py --help
+
+Talking with uzbl
+=================
+
+In order to get uzbl to talk to a running cookie daemon you add the following
+to your uzbl config:
+
+ set cookie_handler = talk_to_socket $XDG_CACHE_HOME/uzbl/cookie_daemon_socket
+
+Or if you prefer using the $HOME variable:
+
+ set cookie_handler = talk_to_socket $HOME/.cache/uzbl/cookie_daemon_socket
+
+Todo list
+=========
+
+ - Use a pid file to make force killing a running daemon possible.
+
+Reporting bugs / getting help
+=============================
+
+The best way to report bugs and or get help with the cookie daemon is to
+contact the maintainers it the #uzbl irc channel found on the Freenode IRC
+network (irc.freenode.org).
+'''
+
+import cookielib
+import os
+import sys
+import urllib2
+import select
+import socket
+import time
+import atexit
+from traceback import print_exc
+from signal import signal, SIGTERM
+from optparse import OptionParser
+from os.path import join
+
+try:
+ import cStringIO as StringIO
+
+except ImportError:
+ import StringIO
+
+
+# ============================================================================
+# ::: Default configuration section ::::::::::::::::::::::::::::::::::::::::::
+# ============================================================================
+
+def xdghome(key, default):
+ '''Attempts to use the environ XDG_*_HOME paths if they exist otherwise
+ use $HOME and the default path.'''
+
+ xdgkey = "XDG_%s_HOME" % key
+ if xdgkey in os.environ.keys() and os.environ[xdgkey]:
+ return os.environ[xdgkey]
+
+ return join(os.environ['HOME'], default)
+
+# Setup xdg paths.
+CACHE_DIR = join(xdghome('CACHE', '.cache/'), 'uzbl/')
+DATA_DIR = join(xdghome('DATA', '.local/share/'), 'uzbl/')
+CONFIG_DIR = join(xdghome('CONFIG', '.config/'), 'uzbl/')
+
+# Ensure data paths exist.
+for path in [CACHE_DIR, DATA_DIR, CONFIG_DIR]:
+ if not os.path.exists(path):
+ os.makedirs(path)
+
+# Default config
+config = {
+
+ # Default cookie jar, whitelist, and daemon socket locations.
+ 'cookie_jar': join(DATA_DIR, 'cookies.txt'),
+ 'cookie_whitelist': join(CONFIG_DIR, 'cookie_whitelist'),
+ 'cookie_socket': join(CACHE_DIR, 'cookie_daemon_socket'),
+
+ # Don't use a cookie whitelist policy by default.
+ 'use_whitelist': False,
+
+ # Time out after x seconds of inactivity (set to 0 for never time out).
+ # WARNING: Do not use this option if you are manually launching the daemon.
+ 'daemon_timeout': 0,
+
+ # Daemonise by default.
+ 'daemon_mode': True,
+
+ # Optionally print helpful debugging messages to the terminal.
+ 'verbose': False,
+
+} # End of config dictionary.
+
+
+# ============================================================================
+# ::: End of configuration section :::::::::::::::::::::::::::::::::::::::::::
+# ============================================================================
+
+
+_SCRIPTNAME = os.path.basename(sys.argv[0])
+def echo(msg):
+ '''Prints only if the verbose flag has been set.'''
+
+ if config['verbose']:
+ sys.stderr.write("%s: %s\n" % (_SCRIPTNAME, msg))
+
+
+def error(msg):
+ '''Prints error message and exits.'''
+
+ sys.stderr.write("%s: error: %s\n" % (_SCRIPTNAME, msg))
+ sys.exit(1)
+
+
+def mkbasedir(filepath):
+ '''Create the base directories of the file in the file-path if the dirs
+ don't exist.'''
+
+ dirname = os.path.dirname(filepath)
+ if not os.path.exists(dirname):
+ echo("creating dirs: %r" % dirname)
+ os.makedirs(dirname)
+
+
+def daemon_running(cookie_socket):
+ '''Check if another process (hopefully a cookie_daemon.py) is listening
+ on the cookie daemon socket. If another process is found to be
+ listening on the socket exit the daemon immediately and leave the
+ socket alone. If the connect fails assume the socket has been abandoned
+ and delete it (to be re-created in the create socket function).'''
+
+ if not os.path.exists(cookie_socket):
+ return False
+
+ if os.path.isfile(cookie_socket):
+ raise Exception("regular file at %r is not a socket" % cookie_socket)
+
+
+ if os.path.isdir(cookie_socket):
+ raise Exception("directory at %r is not a socket" % cookie_socket)
+
+ try:
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_SEQPACKET)
+ sock.connect(cookie_socket)
+ sock.close()
+ echo("detected daemon listening on %r" % cookie_socket)
+ return True
+
+ except socket.error:
+ # Failed to connect to cookie_socket so assume it has been
+ # abandoned by another cookie daemon process.
+ if os.path.exists(cookie_socket):
+ echo("deleting abandoned socket at %r" % cookie_socket)
+ os.remove(cookie_socket)
+
+ return False
+
+
+def send_command(cookie_socket, cmd):
+ '''Send a command to a running cookie daemon.'''
+
+ if not daemon_running(cookie_socket):
+ return False
+
+ try:
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_SEQPACKET)
+ sock.connect(cookie_socket)
+ sock.send(cmd)
+ sock.close()
+ echo("sent command %r to %r" % (cmd, cookie_socket))
+ return True
+
+ except socket.error:
+ print_exc()
+ error("failed to send message %r to %r" % (cmd, cookie_socket))
+ return False
+
+
+def kill_daemon(cookie_socket):
+ '''Send the "EXIT" command to running cookie_daemon.'''
+
+ if send_command(cookie_socket, "EXIT"):
+ # Now ensure the cookie_socket is cleaned up.
+ start = time.time()
+ while os.path.exists(cookie_socket):
+ time.sleep(0.1)
+ if (time.time() - start) > 5:
+ error("force deleting socket %r" % cookie_socket)
+ os.remove(cookie_socket)
+ return
+
+ echo("stopped daemon listening on %r"% cookie_socket)
+
+ else:
+ if os.path.exists(cookie_socket):
+ os.remove(cookie_socket)
+ echo("removed abandoned/broken socket %r" % cookie_socket)
+
+
+def daemonize():
+ '''Daemonize the process using the Stevens' double-fork magic.'''
+
+ try:
+ if os.fork():
+ os._exit(0)
+
+ except OSError:
+ print_exc()
+ sys.stderr.write("fork #1 failed")
+ sys.exit(1)
+
+ os.chdir('/')
+ os.setsid()
+ os.umask(0)
+
+ try:
+ if os.fork():
+ os._exit(0)
+
+ except OSError:
+ print_exc()
+ sys.stderr.write("fork #2 failed")
+ sys.exit(1)
+
+ sys.stdout.flush()
+ sys.stderr.flush()
+
+ devnull = '/dev/null'
+ stdin = file(devnull, 'r')
+ stdout = file(devnull, 'a+')
+ stderr = file(devnull, 'a+', 0)
+
+ os.dup2(stdin.fileno(), sys.stdin.fileno())
+ os.dup2(stdout.fileno(), sys.stdout.fileno())
+ os.dup2(stderr.fileno(), sys.stderr.fileno())
+
+
+class CookieMonster:
+ '''The uzbl cookie daemon class.'''
+
+ def __init__(self):
+ '''Initialise class variables.'''
+
+ self.server_socket = None
+ self.jar = None
+ self.last_request = time.time()
+ self._running = False
+
+
+ def run(self):
+ '''Start the daemon.'''
+
+ # The check healthy function will exit if another daemon is detected
+ # listening on the cookie socket and remove the abandoned socket if
+ # there isnt.
+ if os.path.exists(config['cookie_socket']):
+ if daemon_running(config['cookie_socket']):
+ sys.exit(1)
+
+ # Create cookie daemon socket.
+ self.create_socket()
+
+ # Daemonize process.
+ if config['daemon_mode']:
+ echo("entering daemon mode")
+ daemonize()
+
+ # Register a function to cleanup on exit.
+ atexit.register(self.quit)
+
+ # Make SIGTERM act orderly.
+ signal(SIGTERM, lambda signum, stack_frame: sys.exit(1))
+
+ # Create cookie jar object from file.
+ self.open_cookie_jar()
+
+ # Create a way to exit nested loops by setting a running flag.
+ self._running = True
+
+ while self._running:
+ try:
+ # Enter main listen loop.
+ self.listen()
+
+ except KeyboardInterrupt:
+ self._running = False
+ print
+
+ except socket.error:
+ print_exc()
+
+ except:
+ # Clean up
+ self.del_socket()
+
+ # Raise exception
+ raise
+
+ # Always delete the socket before calling create again.
+ self.del_socket()
+ # Create cookie daemon socket.
+ self.create_socket()
+
+
+ def load_whitelist(self):
+ '''Load the cookie jar whitelist policy.'''
+
+ cookie_whitelist = config['cookie_whitelist']
+
+ if cookie_whitelist:
+ mkbasedir(cookie_whitelist)
+
+ # Create cookie whitelist file if it does not exist.
+ if not os.path.exists(cookie_whitelist):
+ open(cookie_whitelist, 'w').close()
+
+ # Read cookie whitelist file into list.
+ file = open(cookie_whitelist,'r')
+ domain_list = [line.rstrip('\n') for line in file]
+ file.close()
+
+ # Define policy of allowed domains
+ policy = cookielib.DefaultCookiePolicy(allowed_domains=domain_list)
+ self.jar.set_policy(policy)
+
+ # Save the last modified time of the whitelist.
+ self._whitelistmtime = os.stat(cookie_whitelist).st_mtime
+
+
+ def open_cookie_jar(self):
+ '''Open the cookie jar.'''
+
+ cookie_jar = config['cookie_jar']
+ cookie_whitelist = config['cookie_whitelist']
+
+ if cookie_jar:
+ mkbasedir(cookie_jar)
+
+ # Create cookie jar object from file.
+ self.jar = cookielib.MozillaCookieJar(cookie_jar)
+
+ # Load cookie whitelist policy.
+ if config['use_whitelist']:
+ self.load_whitelist()
+
+ if cookie_jar:
+ try:
+ # Attempt to load cookies from the cookie jar.
+ self.jar.load(ignore_discard=True)
+
+ # Ensure restrictive permissions are set on the cookie jar
+ # to prevent other users on the system from hi-jacking your
+ # authenticated sessions simply by copying your cookie jar.
+ os.chmod(cookie_jar, 0600)
+
+ except:
+ pass
+
+
+ def reload_whitelist(self):
+ '''Reload the cookie whitelist.'''
+
+ cookie_whitelist = config['cookie_whitelist']
+ if os.path.exists(cookie_whitelist):
+ echo("reloading whitelist %r" % cookie_whitelist)
+ self.open_cookie_jar()
+
+
+ def create_socket(self):
+ '''Create AF_UNIX socket for communication with uzbl instances.'''
+
+ cookie_socket = config['cookie_socket']
+ mkbasedir(cookie_socket)
+
+ self.server_socket = socket.socket(socket.AF_UNIX,
+ socket.SOCK_SEQPACKET)
+
+ self.server_socket.bind(cookie_socket)
+
+ # Set restrictive permissions on the cookie socket to prevent other
+ # users on the system from data-mining your cookies.
+ os.chmod(cookie_socket, 0600)
+
+
+ def listen(self):
+ '''Listen for incoming cookie PUT and GET requests.'''
+
+ daemon_timeout = config['daemon_timeout']
+ echo("listening on %r" % config['cookie_socket'])
+
+ while self._running:
+ # This line tells the socket how many pending incoming connections
+ # to enqueue at once. Raising this number may or may not increase
+ # performance.
+ self.server_socket.listen(1)
+
+ if bool(select.select([self.server_socket], [], [], 1)[0]):
+ client_socket, _ = self.server_socket.accept()
+ self.handle_request(client_socket)
+ self.last_request = time.time()
+ client_socket.close()
+ continue
+
+ if daemon_timeout:
+ # Checks if the daemon has been idling for too long.
+ idle = time.time() - self.last_request
+ if idle > daemon_timeout:
+ self._running = False
+
+
+ def handle_request(self, client_socket):
+ '''Connection made, now to serve a cookie PUT or GET request.'''
+
+ # Receive cookie request from client.
+ data = client_socket.recv(8192)
+ if not data:
+ return
+
+ # Cookie argument list in packet is null separated.
+ argv = data.split("\0")
+ action = argv[0].upper().strip()
+
+ # Catch the EXIT command sent to kill running daemons.
+ if action == "EXIT":
+ self._running = False
+ return
+
+ # Catch whitelist RELOAD command.
+ elif action == "RELOAD":
+ self.reload_whitelist()
+ return
+
+ # Return if command unknown.
+ elif action not in ['GET', 'PUT']:
+ error("unknown command %r." % argv)
+ return
+
+ # Determine whether or not to print cookie data to terminal.
+ print_cookie = (config['verbose'] and not config['daemon_mode'])
+ if print_cookie:
+ print ' '.join(argv[:4])
+
+ uri = urllib2.urlparse.ParseResult(
+ scheme=argv[1],
+ netloc=argv[2],
+ path=argv[3],
+ params='',
+ query='',
+ fragment='').geturl()
+
+ req = urllib2.Request(uri)
+
+ if action == "GET":
+ self.jar.add_cookie_header(req)
+ if req.has_header('Cookie'):
+ cookie = req.get_header('Cookie')
+ client_socket.send(cookie)
+ if print_cookie:
+ print cookie
+
+ else:
+ client_socket.send("\0")
+
+ elif action == "PUT":
+ cookie = argv[4] if len(argv) > 3 else None
+ if print_cookie:
+ print cookie
+
+ self.put_cookie(req, cookie)
+
+ if print_cookie:
+ print
+
+
+ def put_cookie(self, req, cookie=None):
+ '''Put a cookie in the cookie jar.'''
+
+ hdr = urllib2.httplib.HTTPMessage(\
+ StringIO.StringIO('Set-Cookie: %s' % cookie))
+ res = urllib2.addinfourl(StringIO.StringIO(), hdr,
+ req.get_full_url())
+ self.jar.extract_cookies(res, req)
+ if config['cookie_jar']:
+ self.jar.save(ignore_discard=True)
+
+
+ def del_socket(self):
+ '''Remove the cookie_socket file on exit. In a way the cookie_socket
+ is the daemons pid file equivalent.'''
+
+ if self.server_socket:
+ try:
+ self.server_socket.close()
+
+ except:
+ pass
+
+ self.server_socket = None
+
+ cookie_socket = config['cookie_socket']
+ if os.path.exists(cookie_socket):
+ echo("deleting socket %r" % cookie_socket)
+ os.remove(cookie_socket)
+
+
+ def quit(self):
+ '''Called on exit to make sure all loose ends are tied up.'''
+
+ self.del_socket()
+ sys.exit(0)
+
+
+def main():
+ '''Main function.'''
+
+ # Define command line parameters.
+ usage = "usage: %prog [options] {start|stop|restart|reload}"
+ parser = OptionParser(usage=usage)
+ parser.add_option('-n', '--no-daemon', dest='no_daemon',
+ action='store_true', help="don't daemonise the process.")
+
+ parser.add_option('-v', '--verbose', dest="verbose",
+ action='store_true', help="print verbose output.")
+
+ parser.add_option('-t', '--daemon-timeout', dest='daemon_timeout',
+ action="store", metavar="SECONDS", help="shutdown the daemon after x "\
+ "seconds inactivity. WARNING: Do not use this when launching the "\
+ "cookie daemon manually.")
+
+ parser.add_option('-s', '--cookie-socket', dest="cookie_socket",
+ metavar="SOCKET", help="manually specify the socket location.")
+
+ parser.add_option('-j', '--cookie-jar', dest='cookie_jar',
+ metavar="FILE", help="manually specify the cookie jar location.")
+
+ parser.add_option('-m', '--memory', dest='memory', action='store_true',
+ help="store cookies in memory only - do not write to disk")
+
+ parser.add_option('-u', '--use-whitelist', dest='usewhitelist',
+ action='store_true', help="use cookie whitelist policy")
+
+ parser.add_option('-w', '--cookie-whitelist', dest='whitelist',
+ action='store', help="manually specify whitelist location",
+ metavar='FILE')
+
+ # Parse the command line arguments.
+ (options, args) = parser.parse_args()
+
+ expand = lambda p: os.path.realpath(os.path.expandvars(p))
+
+ initcommands = ['start', 'stop', 'restart', 'reload']
+ for arg in args:
+ if arg not in initcommands:
+ error("unknown argument %r" % args[0])
+ sys.exit(1)
+
+ if len(args) > 1:
+ error("the daemon only accepts one {%s} action at a time."
+ % '|'.join(initcommands))
+ sys.exit(1)
+
+ if len(args):
+ action = args[0]
+
+ else:
+ action = "start"
+
+ if options.no_daemon:
+ config['daemon_mode'] = False
+
+ if options.cookie_socket:
+ config['cookie_socket'] = expand(options.cookie_socket)
+
+ if options.cookie_jar:
+ config['cookie_jar'] = expand(options.cookie_jar)
+
+ if options.memory:
+ config['cookie_jar'] = None
+
+ if options.whitelist:
+ config['cookie_whitelist'] = expand(options.whitelist)
+
+ if options.whitelist or options.usewhitelist:
+ config['use_whitelist'] = True
+
+ if options.daemon_timeout:
+ try:
+ config['daemon_timeout'] = int(options.daemon_timeout)
+
+ except ValueError:
+ error("expected int argument for -t, --daemon-timeout")
+
+ # Expand $VAR's in config keys that relate to paths.
+ for key in ['cookie_socket', 'cookie_jar', 'cookie_whitelist']:
+ if config[key]:
+ config[key] = os.path.expandvars(config[key])
+
+ if options.verbose:
+ config['verbose'] = True
+ import pprint
+ sys.stderr.write("%s\n" % pprint.pformat(config))
+
+ # It would be better if we didn't need to start this python process just
+ # to send a command to the socket, but unfortunately socat doesn't seem
+ # to support SEQPACKET.
+ if action == "reload":
+ send_command(config['cookie_socket'], "RELOAD")
+
+ if action in ['stop', 'restart']:
+ kill_daemon(config['cookie_socket'])
+
+ if action in ['start', 'restart']:
+ CookieMonster().run()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/data/scripts/uzbl-event-manager b/examples/data/scripts/uzbl-event-manager
new file mode 100755
index 0000000..99b215a
--- /dev/null
+++ b/examples/data/scripts/uzbl-event-manager
@@ -0,0 +1,833 @@
+#!/usr/bin/env python
+
+# Event Manager for Uzbl
+# Copyright (c) 2009, Mason Larobina <mason.larobina@gmail.com>
+# Copyright (c) 2009, Dieter Plaetinck <dieter@plaetinck.be>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+'''
+
+E V E N T _ M A N A G E R . P Y
+===============================
+
+Event manager for uzbl written in python.
+
+'''
+
+import imp
+import os
+import sys
+import re
+import socket
+import pprint
+import time
+import atexit
+from select import select
+from signal import signal, SIGTERM
+from optparse import OptionParser
+from traceback import print_exc
+from functools import partial
+
+
+def xdghome(key, default):
+ '''Attempts to use the environ XDG_*_HOME paths if they exist otherwise
+ use $HOME and the default path.'''
+
+ xdgkey = "XDG_%s_HOME" % key
+ if xdgkey in os.environ.keys() and os.environ[xdgkey]:
+ return os.environ[xdgkey]
+
+ return os.path.join(os.environ['HOME'], default)
+
+
+# ============================================================================
+# ::: Default configuration section ::::::::::::::::::::::::::::::::::::::::::
+# ============================================================================
+
+# `make install` will put the correct value here for your system
+PREFIX = '/usr/local/'
+
+# Setup xdg paths.
+DATA_DIR = os.path.join(xdghome('DATA', '.local/share/'), 'uzbl/')
+CACHE_DIR = os.path.join(xdghome('CACHE', '.cache/'), 'uzbl/')
+
+# Event manager config dictionary. This is not to be confused with the config
+# dict that tracks variables in the uzbl instance.
+CONFIG = {
+ 'verbose': False,
+ 'daemon_mode': True,
+ 'auto_close': False,
+
+ 'plugins_load': [],
+ 'plugins_ignore': [],
+
+ 'plugin_dirs': [os.path.join(DATA_DIR, 'plugins/'),
+ os.path.join(PREFIX, 'share/uzbl/examples/data/plugins/')],
+
+ 'server_socket': os.path.join(CACHE_DIR, 'event_daemon'),
+ 'pid_file': os.path.join(CACHE_DIR, 'event_daemon.pid'),
+}
+
+# ============================================================================
+# ::: End of configuration section :::::::::::::::::::::::::::::::::::::::::::
+# ============================================================================
+
+
+# Define some globals.
+SCRIPTNAME = os.path.basename(sys.argv[0])
+FINDSPACES = re.compile("\s+")
+
+
+class ArgumentError(Exception):
+ pass
+
+
+def echo(msg):
+ '''Prints only if the verbose flag has been set.'''
+
+ if CONFIG['verbose']:
+ sys.stdout.write("%s: %s\n" % (SCRIPTNAME, msg))
+
+
+def error(msg):
+ '''Prints error messages to stderr.'''
+
+ sys.stderr.write("%s: error: %s\n" % (SCRIPTNAME, msg))
+
+
+def counter():
+ '''Generate unique object id's.'''
+
+ i = 0
+ while True:
+ i += 1
+ yield i
+
+
+def find_plugins(plugin_dirs):
+ '''Find all event manager plugins in the plugin dirs and return a
+ dictionary of {'plugin-name.py': '/full/path/to/plugin-name.py', ...}'''
+
+ plugins = {}
+
+ for plugin_dir in plugin_dirs:
+ plugin_dir = os.path.realpath(os.path.expandvars(plugin_dir))
+ if not os.path.isdir(plugin_dir):
+ continue
+
+ for filename in os.listdir(plugin_dir):
+ if not filename.lower().endswith('.py'):
+ continue
+
+ path = os.path.join(plugin_dir, filename)
+ if not os.path.isfile(path):
+ continue
+
+ if filename not in plugins:
+ plugins[filename] = plugin_dir
+
+ return plugins
+
+
+def load_plugins(plugin_dirs, load=None, ignore=None):
+ '''Load event manager plugins found in the plugin_dirs.'''
+
+ load = [] if load is None else load
+ ignore = [] if ignore is None else ignore
+
+ # Find the plugins in the plugin_dirs.
+ found = find_plugins(plugin_dirs)
+
+ if load:
+ # Ignore anything not in the load list.
+ for plugin in found.keys():
+ if plugin not in load:
+ del found[plugin]
+
+ if ignore:
+ # Ignore anything in the ignore list.
+ for plugin in found.keys():
+ if plugin in ignore:
+ del found[plugin]
+
+ # Print plugin list to be loaded.
+ pprint.pprint(found)
+
+ loaded = {}
+ # Load all found plugins into the loaded dict.
+ for (filename, plugin_dir) in found.items():
+ name = filename[:-3]
+ info = imp.find_module(name, [plugin_dir])
+ plugin = imp.load_module(name, *info)
+ loaded[(plugin_dir, filename)] = plugin
+
+ return loaded
+
+
+def daemonize():
+ '''Daemonize the process using the Stevens' double-fork magic.'''
+
+ try:
+ if os.fork():
+ os._exit(0)
+
+ except OSError:
+ print_exc()
+ sys.stderr.write("fork #1 failed")
+ sys.exit(1)
+
+ os.chdir('/')
+ os.setsid()
+ os.umask(0)
+
+ try:
+ if os.fork():
+ os._exit(0)
+
+ except OSError:
+ print_exc()
+ sys.stderr.write("fork #2 failed")
+ sys.exit(1)
+
+ sys.stdout.flush()
+ sys.stderr.flush()
+
+ devnull = '/dev/null'
+ stdin = file(devnull, 'r')
+ stdout = file(devnull, 'a+')
+ stderr = file(devnull, 'a+', 0)
+
+ os.dup2(stdin.fileno(), sys.stdin.fileno())
+ os.dup2(stdout.fileno(), sys.stdout.fileno())
+ os.dup2(stderr.fileno(), sys.stderr.fileno())
+
+
+def make_dirs(path):
+ '''Make all basedirs recursively as required.'''
+
+ dirname = os.path.dirname(path)
+ if not os.path.isdir(dirname):
+ os.makedirs(dirname)
+
+
+def make_pid_file(pid_file):
+ '''Make pid file at given pid_file location.'''
+
+ make_dirs(pid_file)
+ fileobj = open(pid_file, 'w')
+ fileobj.write('%d' % os.getpid())
+ fileobj.close()
+
+
+def del_pid_file(pid_file):
+ '''Delete pid file at given pid_file location.'''
+
+ if os.path.isfile(pid_file):
+ os.remove(pid_file)
+
+
+def get_pid(pid_file):
+ '''Read pid from pid_file.'''
+
+ try:
+ fileobj = open(pid_file, 'r')
+ pid = int(fileobj.read())
+ fileobj.close()
+ return pid
+
+ except IOError, ValueError:
+ print_exc()
+ return None
+
+
+def pid_running(pid):
+ '''Returns True if a process with the given pid is running.'''
+
+ try:
+ os.kill(pid, 0)
+
+ except OSError:
+ return False
+
+ else:
+ return True
+
+
+def term_process(pid):
+ '''Send a SIGTERM signal to the process with the given pid.'''
+
+ if not pid_running(pid):
+ return False
+
+ os.kill(pid, SIGTERM)
+
+ start = time.time()
+ while True:
+ if not pid_running(pid):
+ return True
+
+ if time.time() - start > 5:
+ raise OSError('failed to stop process with pid: %d' % pid)
+
+ time.sleep(0.25)
+
+
+def parse_msg(uzbl, msg):
+ '''Parse an incoming msg from a uzbl instance. All non-event messages
+ will be printed here and not be passed to the uzbl instance event
+ handler function.'''
+
+ if not msg:
+ return
+
+ cmd = FINDSPACES.split(msg, 3)
+ if not cmd or cmd[0] != 'EVENT':
+ # Not an event message.
+ print '---', msg
+ return
+
+ while len(cmd) < 4:
+ cmd.append('')
+
+ event, args = cmd[2], cmd[3]
+ if not event:
+ return
+
+ try:
+ uzbl.event(event, args)
+
+ except:
+ print_exc()
+
+
+class EventHandler(object):
+
+ nexthid = counter().next
+
+ def __init__(self, event, handler, *args, **kargs):
+ if not callable(handler):
+ raise ArgumentError("EventHandler object requires a callable "
+ "object function for the handler argument not: %r" % handler)
+
+ self.function = handler
+ self.args = args
+ self.kargs = kargs
+ self.event = event
+ self.hid = self.nexthid()
+
+
+ def __repr__(self):
+ args = ["event=%s" % self.event, "hid=%d" % self.hid,
+ "function=%r" % self.function]
+
+ if self.args:
+ args.append("args=%r" % self.args)
+
+ if self.kargs:
+ args.append("kargs=%r" % self.kargs)
+
+ return "<EventHandler(%s)>" % ', '.join(args)
+
+
+class UzblInstance(object):
+
+ # Give all plugins access to the main config dict.
+ config = CONFIG
+
+ def __init__(self, parent, client_socket):
+
+ # Internal variables.
+ self.exports = {}
+ self.handlers = {}
+ self.parent = parent
+ self.client_socket = client_socket
+
+ self.depth = 0
+ self.buffer = ''
+ self.pid = None
+
+ # Call the init function in every plugin. The init function in each
+ # plugin is where that plugin connects functions to events and exports
+ # functions to the uzbl object.
+ for plugin in self.parent['plugins'].values():
+ try:
+ plugin.init(self)
+
+ except:
+ raise
+
+
+ def send(self, msg):
+ '''Send a command to the uzbl instance via the socket file.'''
+
+ msg = msg.strip()
+ if self.client_socket:
+ print '%s<-- %s' % (' ' * self.depth, msg)
+ self.client_socket.send(("%s\n" % msg).encode('utf-8'))
+
+ else:
+ print '%s!-- %s' % (' ' * self.depth, msg)
+
+
+ def export(self, name, function):
+ '''Export `function(uzbl, *args, ..)` inside a plugin to the uzbl
+ object like so `uzbl.function(*args, ..)`. This will allow other
+ plugins to call functions inside the current plugin (which is currently
+ calling this function) via the uzbl object.'''
+
+ self.__dict__.__setitem__(name, partial(function, self))
+
+
+ def export_dict(self, export_dict):
+ '''Export multiple (name, function)'s at once inside a dict of the
+ form `{name1: function1, name2: function2, ...}`.'''
+
+ for (name, function) in export_dict.items():
+ self.export(name, function)
+
+
+ def connect(self, event, handler, *args, **kargs):
+ '''Connect a uzbl event with a handler. Handlers can either be a
+ function or a uzbl command string.'''
+
+ event = event.upper().strip()
+ assert event and ' ' not in event
+
+ if event not in self.handlers.keys():
+ self.handlers[event] = []
+
+ handlerobj = EventHandler(event, handler, *args, **kargs)
+ self.handlers[event].append(handlerobj)
+ print handlerobj
+
+
+ def connect_dict(self, connect_dict):
+ '''Connect a dictionary comprising of {"EVENT_NAME": handler, ..} to
+ the event handler stack.
+
+ If you need to supply args or kargs to an event use the normal connect
+ function.'''
+
+ for (event, handler) in connect_dict.items():
+ self.connect(event, handler)
+
+
+ def remove_by_id(self, hid):
+ '''Remove connected event handler by unique handler id.'''
+
+ for (event, handlers) in self.handlers.items():
+ for handler in list(handlers):
+ if hid != handler.hid:
+ continue
+
+ echo("removed %r" % handler)
+ handlers.remove(handler)
+ return
+
+ echo('unable to find & remove handler with id: %d' % hid)
+
+
+ def remove(self, handler):
+ '''Remove connected event handler.'''
+
+ for (event, handlers) in self.handlers.items():
+ if handler in handlers:
+ echo("removed %r" % handler)
+ handlers.remove(handler)
+ return
+
+ echo('unable to find & remove handler: %r' % handler)
+
+
+ def exec_handler(self, handler, *args, **kargs):
+ '''Execute event handler function.'''
+
+ args += handler.args
+ kargs = dict(handler.kargs.items()+kargs.items())
+ handler.function(self, *args, **kargs)
+
+
+ def event(self, event, *args, **kargs):
+ '''Raise an event.'''
+
+ event = event.upper()
+ elems = [event,]
+ if args: elems.append(unicode(args))
+ if kargs: elems.append(unicode(kargs))
+ print "%s--> %s" % (' ' * self.depth, ' '.join(elems))
+
+ if event == "INSTANCE_START" and args:
+ self.pid = int(args[0])
+
+ if event not in self.handlers:
+ return
+
+ for handler in self.handlers[event]:
+ self.depth += 1
+ try:
+ self.exec_handler(handler, *args, **kargs)
+
+ except:
+ print_exc()
+
+ self.depth -= 1
+
+
+ def close(self):
+ '''Close the client socket and clean up.'''
+
+ try:
+ self.client_socket.close()
+
+ except:
+ pass
+
+ for (name, plugin) in self.parent['plugins'].items():
+ if hasattr(plugin, 'cleanup'):
+ plugin.cleanup(self)
+
+
+class UzblEventDaemon(dict):
+ def __init__(self):
+
+ # Init variables and dict keys.
+ dict.__init__(self, {'uzbls': {}})
+ self.running = None
+ self.server_socket = None
+ self.socket_location = None
+
+ # Register that the event daemon server has started by creating the
+ # pid file.
+ make_pid_file(CONFIG['pid_file'])
+
+ # Register a function to clean up the socket and pid file on exit.
+ atexit.register(self.quit)
+
+ # Make SIGTERM act orderly.
+ signal(SIGTERM, lambda signum, stack_frame: sys.exit(1))
+
+ # Load plugins, first-build of the plugins may be a costly operation.
+ self['plugins'] = load_plugins(CONFIG['plugin_dirs'],
+ CONFIG['plugins_load'], CONFIG['plugins_ignore'])
+
+
+ def _create_server_socket(self):
+ '''Create the event manager daemon socket for uzbl instance duplex
+ communication.'''
+
+ server_socket = CONFIG['server_socket']
+ server_socket = os.path.realpath(os.path.expandvars(server_socket))
+ self.socket_location = server_socket
+
+ # Delete socket if it exists.
+ if os.path.exists(server_socket):
+ os.remove(server_socket)
+
+ self.server_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ self.server_socket.bind(server_socket)
+ self.server_socket.listen(5)
+
+
+ def _close_server_socket(self):
+ '''Close and delete the server socket.'''
+
+ try:
+ self.server_socket.close()
+ self.server_socket = None
+
+ if os.path.exists(self.socket_location):
+ os.remove(self.socket_location)
+
+ except:
+ pass
+
+
+ def run(self):
+ '''Main event daemon loop.'''
+
+ # Create event daemon socket.
+ self._create_server_socket()
+ echo('listening on: %s' % self.socket_location)
+
+ if CONFIG['daemon_mode']:
+ echo('entering daemon mode.')
+ daemonize()
+ # The pid has changed so update the pid file.
+ make_pid_file(CONFIG['pid_file'])
+
+ # Now listen for incoming connections and or data.
+ self.listen()
+
+ # Clean up.
+ self.quit()
+
+
+ def listen(self):
+ '''Accept incoming connections and constantly poll instance sockets
+ for incoming data.'''
+
+ self.running = True
+ while self.running:
+
+ sockets = [self.server_socket] + self['uzbls'].keys()
+
+ reads, _, errors = select(sockets, [], sockets, 1)
+
+ if self.server_socket in reads:
+ self.accept_connection()
+ reads.remove(self.server_socket)
+
+ for client in reads:
+ self.read_socket(client)
+
+ for client in errors:
+ error('Unknown error on socket: %r' % client)
+ self.close_connection(client)
+
+
+ def read_socket(self, client):
+ '''Read data from an instance socket and pass to the uzbl objects
+ event handler function.'''
+
+ uzbl = self['uzbls'][client]
+ try:
+ raw = unicode(client.recv(8192), 'utf-8', 'ignore')
+
+ except:
+ print_exc()
+ raw = None
+
+ if not raw:
+ # Read null byte, close socket.
+ return self.close_connection(client)
+
+ uzbl.buffer += raw
+ msgs = uzbl.buffer.split('\n')
+ uzbl.buffer = msgs.pop()
+
+ for msg in msgs:
+ try:
+ parse_msg(uzbl, msg.strip())
+
+ except:
+ print_exc()
+
+
+ def accept_connection(self):
+ '''Accept incoming connection to the server socket.'''
+
+ client_socket = self.server_socket.accept()[0]
+
+ uzbl = UzblInstance(self, client_socket)
+ self['uzbls'][client_socket] = uzbl
+
+
+ def close_connection(self, client):
+ '''Clean up after instance close.'''
+
+ try:
+ if client in self['uzbls']:
+ uzbl = self['uzbls'][client]
+ uzbl.close()
+ del self['uzbls'][client]
+
+ except:
+ print_exc()
+
+ if not len(self['uzbls']) and CONFIG['auto_close']:
+ echo('auto closing event manager.')
+ self.running = False
+
+
+ def quit(self):
+ '''Close all instance socket objects, server socket and delete the
+ pid file.'''
+
+ echo('shutting down event manager.')
+
+ for client in self['uzbls'].keys():
+ self.close_connection(client)
+
+ echo('unlinking: %r' % self.socket_location)
+ self._close_server_socket()
+
+ echo('deleting pid file: %r' % CONFIG['pid_file'])
+ del_pid_file(CONFIG['pid_file'])
+
+
+def stop_action():
+ '''Stop the event manager daemon.'''
+
+ pid_file = CONFIG['pid_file']
+ if not os.path.isfile(pid_file):
+ return echo('no running daemon found.')
+
+ echo('found pid file: %r' % pid_file)
+ pid = get_pid(pid_file)
+ if not pid_running(pid):
+ echo('no process with pid: %d' % pid)
+ return os.remove(pid_file)
+
+ echo("terminating process with pid: %d" % pid)
+ term_process(pid)
+ if os.path.isfile(pid_file):
+ os.remove(pid_file)
+
+ echo('stopped event daemon.')
+
+
+def start_action():
+ '''Start the event manager daemon.'''
+
+ pid_file = CONFIG['pid_file']
+ if os.path.isfile(pid_file):
+ echo('found pid file: %r' % pid_file)
+ pid = get_pid(pid_file)
+ if pid_running(pid):
+ return echo('event daemon already started with pid: %d' % pid)
+
+ echo('no process with pid: %d' % pid)
+ os.remove(pid_file)
+
+ echo('starting event manager.')
+ UzblEventDaemon().run()
+
+
+def restart_action():
+ '''Restart the event manager daemon.'''
+
+ echo('restarting event manager daemon.')
+ stop_action()
+ start_action()
+
+
+def list_action():
+ '''List all the plugins being loaded by the event daemon.'''
+
+ plugins = find_plugins(CONFIG['plugin_dirs'])
+ dirs = {}
+
+ for (plugin, plugin_dir) in plugins.items():
+ if plugin_dir not in dirs:
+ dirs[plugin_dir] = []
+
+ dirs[plugin_dir].append(plugin)
+
+ for (index, (plugin_dir, plugin_list)) in enumerate(sorted(dirs.items())):
+ if index:
+ print
+
+ print "%s:" % plugin_dir
+ for plugin in sorted(plugin_list):
+ print " %s" % plugin
+
+
+if __name__ == "__main__":
+ USAGE = "usage: %prog [options] {start|stop|restart|list}"
+ PARSER = OptionParser(usage=USAGE)
+ PARSER.add_option('-v', '--verbose', dest='verbose', action="store_true",
+ help="print verbose output.")
+
+ PARSER.add_option('-d', '--plugin-dirs', dest='plugin_dirs', action="store",
+ metavar="DIRS", help="Specify plugin directories in the form of "\
+ "'dir1:dir2:dir3'.")
+
+ PARSER.add_option('-l', '--load-plugins', dest="load", action="store",
+ metavar="PLUGINS", help="comma separated list of plugins to load")
+
+ PARSER.add_option('-i', '--ignore-plugins', dest="ignore", action="store",
+ metavar="PLUGINS", help="comma separated list of plugins to ignore")
+
+ PARSER.add_option('-p', '--pid-file', dest='pid', action='store',
+ metavar='FILE', help="specify pid file location")
+
+ PARSER.add_option('-s', '--server-socket', dest='socket', action='store',
+ metavar='SOCKET', help="specify the daemon socket location")
+
+ PARSER.add_option('-n', '--no-daemon', dest="daemon",
+ action="store_true", help="don't enter daemon mode.")
+
+ PARSER.add_option('-a', '--auto-close', dest='autoclose',
+ action='store_true', help='auto close after all instances disconnect.')
+
+ (OPTIONS, ARGS) = PARSER.parse_args()
+
+ # init like {start|stop|..} daemon actions dict.
+ DAEMON_ACTIONS = {'start': start_action, 'stop': stop_action,
+ 'restart': restart_action, 'list': list_action}
+
+ if not ARGS:
+ ACTION = 'start'
+
+ elif len(ARGS) == 1:
+ ACTION = ARGS[0]
+ if ACTION not in DAEMON_ACTIONS:
+ raise ArgumentError("unknown argument: %r" % ACTION)
+
+ else:
+ raise ArgumentError("too many arguments: %r" % ARGS)
+
+ # parse other flags & options.
+ if OPTIONS.verbose:
+ CONFIG['verbose'] = True
+
+ if OPTIONS.plugin_dirs:
+ PLUGIN_DIRS = []
+ for DIR in OPTIONS.plugin_dirs.split(':'):
+ if not DIR:
+ continue
+
+ PLUGIN_DIRS.append(os.path.realpath(DIR))
+
+ CONFIG['plugin_dirs'] = PLUGIN_DIRS
+ echo("plugin search dirs: %r" % PLUGIN_DIRS)
+
+ if OPTIONS.load and OPTIONS.ignore:
+ error("you can't load and ignore at the same time.")
+ sys.exit(1)
+
+ elif OPTIONS.load:
+ LOAD = CONFIG['plugins_load']
+ for PLUGIN in OPTIONS.load.split(','):
+ if PLUGIN.strip():
+ LOAD.append(PLUGIN.strip())
+
+ echo('only loading plugin(s): %s' % ', '.join(LOAD))
+
+ elif OPTIONS.ignore:
+ IGNORE = CONFIG['plugins_ignore']
+ for PLUGIN in OPTIONS.ignore.split(','):
+ if PLUGIN.strip():
+ IGNORE.append(PLUGIN.strip())
+
+ echo('ignoring plugin(s): %s' % ', '.join(IGNORE))
+
+ if OPTIONS.autoclose:
+ CONFIG['auto_close'] = True
+ echo('will auto close.')
+
+ if OPTIONS.pid:
+ CONFIG['pid_file'] = os.path.realpath(OPTIONS.pid)
+ echo("pid file location: %r" % CONFIG['pid_file'])
+
+ if OPTIONS.socket:
+ CONFIG['server_socket'] = os.path.realpath(OPTIONS.socket)
+ echo("daemon socket location: %s" % CONFIG['server_socket'])
+
+ if OPTIONS.daemon:
+ CONFIG['daemon_mode'] = False
+
+ # Now {start|stop|...}
+ DAEMON_ACTIONS[ACTION]()
diff --git a/examples/data/scripts/uzbl-tabbed b/examples/data/scripts/uzbl-tabbed
new file mode 100755
index 0000000..7bd90d5
--- /dev/null
+++ b/examples/data/scripts/uzbl-tabbed
@@ -0,0 +1,1417 @@
+#!/usr/bin/env python
+
+# Uzbl tabbing wrapper using a fifo socket interface
+# Copyright (c) 2009, Tom Adams <tom@holizz.com>
+# Copyright (c) 2009, Chris van Dijk <cn.vandijk@hotmail.com>
+# Copyright (c) 2009, Mason Larobina <mason.larobina@gmail.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+# Author(s):
+# Tom Adams <tom@holizz.com>
+# Wrote the original uzbl_tabbed.py as a proof of concept.
+#
+# Chris van Dijk (quigybo) <cn.vandijk@hotmail.com>
+# Made signifigant headway on the old uzbl_tabbing.py script on the
+# uzbl wiki <http://www.uzbl.org/wiki/uzbl_tabbed>
+#
+# Mason Larobina <mason.larobina@gmail.com>
+# Rewrite of the uzbl_tabbing.py script to use a fifo socket interface
+# and inherit configuration options from the user's uzbl config.
+#
+# Contributor(s):
+# mxey <mxey@ghosthacking.net>
+# uzbl_config path now honors XDG_CONFIG_HOME if it exists.
+#
+# Romain Bignon <romain@peerfuse.org>
+# Fix for session restoration code.
+#
+# Jake Probst <jake.probst@gmail.com>
+# Wrote a patch that overflows tabs in the tablist on to new lines when
+# running of room.
+#
+# Devon Jones <devon.jones@gmail.com>
+# Fifo command bring_to_front which brings the gtk window to focus.
+#
+# Simon Lipp (sloonz)
+# Various
+
+
+# Dependencies:
+# pygtk - python bindings for gtk.
+# pango - python bindings needed for text rendering & layout in gtk widgets.
+# pygobject - GLib's GObject bindings for python.
+#
+# Optional dependencies:
+# simplejson - save uzbl_tabbed.py sessions & presets in json.
+#
+# Note: I haven't included version numbers with this dependency list because
+# I've only ever tested uzbl_tabbed.py on the latest stable versions of these
+# packages in Gentoo's portage. Package names may vary on different systems.
+
+
+# Configuration:
+# Because this version of uzbl_tabbed is able to inherit options from your main
+# uzbl configuration file you may wish to configure uzbl tabbed from there.
+# Here is a list of configuration options that can be customised and some
+# example values for each:
+#
+# General tabbing options:
+# show_tablist = 1
+# show_gtk_tabs = 0
+# tablist_top = 1
+# gtk_tab_pos = (top|left|bottom|right)
+# gtk_refresh = 1000
+# switch_to_new_tabs = 1
+# capture_new_windows = 1
+# multiline_tabs = 1
+#
+# Tab title options:
+# tab_titles = 1
+# tab_indexes = 1
+# new_tab_title = Loading
+# max_title_len = 50
+# show_ellipsis = 1
+#
+# Session options:
+# save_session = 1
+# json_session = 0
+# session_file = $HOME/.local/share/uzbl/session
+#
+# Inherited uzbl options:
+# fifo_dir = /tmp
+# socket_dir = /tmp
+# icon_path = $HOME/.local/share/uzbl/uzbl.png
+# status_background = #303030
+#
+# Misc options:
+# window_size = 800,800
+# verbose = 0
+#
+# And uzbl_tabbed.py takes care of the actual binding of the commands via each
+# instances fifo socket.
+#
+# Custom tab styling:
+# tab_colours = foreground = "#888" background = "#303030"
+# tab_text_colours = foreground = "#bbb"
+# selected_tab = foreground = "#fff"
+# selected_tab_text = foreground = "green"
+# tab_indicate_https = 1
+# https_colours = foreground = "#888"
+# https_text_colours = foreground = "#9c8e2d"
+# selected_https = foreground = "#fff"
+# selected_https_text = foreground = "gold"
+#
+# How these styling values are used are soley defined by the syling policy
+# handler below (the function in the config section). So you can for example
+# turn the tab text colour Firetruck-Red in the event "error" appears in the
+# tab title or some other arbitrary event. You may wish to make a trusted
+# hosts file and turn tab titles of tabs visiting trusted hosts purple.
+
+
+# Issues:
+# - new windows are not caught and opened in a new tab.
+# - when uzbl_tabbed.py crashes it takes all the children with it.
+# - when a new tab is opened when using gtk tabs the tab button itself
+# grabs focus from its child for a few seconds.
+# - when switch_to_new_tabs is not selected the notebook page is
+# maintained but the new window grabs focus (try as I might to stop it).
+
+
+# Todo:
+# - add command line options to use a different session file, not use a
+# session file and or open a uri on starup.
+# - ellipsize individual tab titles when the tab-list becomes over-crowded
+# - add "<" & ">" arrows to tablist to indicate that only a subset of the
+# currently open tabs are being displayed on the tablist.
+# - add the small tab-list display when both gtk tabs and text vim-like
+# tablist are hidden (I.e. [ 1 2 3 4 5 ])
+# - check spelling.
+# - pass a uzbl socketid to uzbl_tabbed.py and have it assimilated into
+# the collective. Resistance is futile!
+
+
+import pygtk
+import gtk
+import subprocess
+import os
+import re
+import time
+import getopt
+import pango
+import select
+import sys
+import gobject
+import socket
+import random
+import hashlib
+import atexit
+import types
+
+from gobject import io_add_watch, source_remove, timeout_add, IO_IN, IO_HUP
+from signal import signal, SIGTERM, SIGINT
+from optparse import OptionParser, OptionGroup
+
+
+pygtk.require('2.0')
+
+_SCRIPTNAME = os.path.basename(sys.argv[0])
+def error(msg):
+ sys.stderr.write("%s: error: %s\n" % (_SCRIPTNAME, msg))
+
+# ============================================================================
+# ::: Default configuration section ::::::::::::::::::::::::::::::::::::::::::
+# ============================================================================
+
+def xdghome(key, default):
+ '''Attempts to use the environ XDG_*_HOME paths if they exist otherwise
+ use $HOME and the default path.'''
+
+ xdgkey = "XDG_%s_HOME" % key
+ if xdgkey in os.environ.keys() and os.environ[xdgkey]:
+ return os.environ[xdgkey]
+
+ return os.path.join(os.environ['HOME'], default)
+
+# Setup xdg paths.
+DATA_DIR = os.path.join(xdghome('DATA', '.local/share/'), 'uzbl/')
+
+# Ensure uzbl xdg paths exist
+if not os.path.exists(DATA_DIR):
+ os.makedirs(DATA_DIR)
+
+# All of these settings can be inherited from your uzbl config file.
+config = {
+ # Tab options
+ 'show_tablist': True, # Show text uzbl like statusbar tab-list
+ 'show_gtk_tabs': False, # Show gtk notebook tabs
+ 'tablist_top': True, # Display tab-list at top of window
+ 'gtk_tab_pos': 'top', # Gtk tab position (top|left|bottom|right)
+ 'gtk_refresh': 1000, # Tablist refresh millisecond interval
+ 'switch_to_new_tabs': True, # Upon opening a new tab switch to it
+ 'capture_new_windows': True, # Use uzbl_tabbed to catch new windows
+ 'multiline_tabs': True, # Tabs overflow onto new tablist lines.
+
+ # Tab title options
+ 'tab_titles': True, # Display tab titles (else only tab-nums)
+ 'tab_indexes': True, # Display tab nums (else only tab titles)
+ 'new_tab_title': 'Loading', # New tab title
+ 'max_title_len': 50, # Truncate title at n characters
+ 'show_ellipsis': True, # Show ellipsis when truncating titles
+
+ # Session options
+ 'save_session': True, # Save session in file when quit
+ 'json_session': False, # Use json to save session.
+ 'saved_sessions_dir': os.path.join(DATA_DIR, 'sessions/'),
+ 'session_file': os.path.join(DATA_DIR, 'session'),
+
+ # Inherited uzbl options
+ 'fifo_dir': '/tmp', # Path to look for uzbl fifo.
+ 'socket_dir': '/tmp', # Path to look for uzbl socket.
+ 'icon_path': os.path.join(DATA_DIR, 'uzbl.png'),
+ 'status_background': "#303030", # Default background for all panels.
+
+ # Misc options
+ 'window_size': "800,800", # width,height in pixels.
+ 'verbose': False, # Print verbose output.
+
+ # Add custom tab style definitions to be used by the tab colour policy
+ # handler here. Because these are added to the config dictionary like
+ # any other uzbl_tabbed configuration option remember that they can
+ # be superseeded from your main uzbl config file.
+ 'tab_colours': 'foreground = "#888" background = "#303030"',
+ 'tab_text_colours': 'foreground = "#bbb"',
+ 'selected_tab': 'foreground = "#fff"',
+ 'selected_tab_text': 'foreground = "green"',
+ 'tab_indicate_https': True,
+ 'https_colours': 'foreground = "#888"',
+ 'https_text_colours': 'foreground = "#9c8e2d"',
+ 'selected_https': 'foreground = "#fff"',
+ 'selected_https_text': 'foreground = "gold"',
+
+} # End of config dict.
+
+UZBL_TABBED_VARS = config.keys()
+
+# This is the tab style policy handler. Every time the tablist is updated
+# this function is called to determine how to colourise that specific tab
+# according the simple/complex rules as defined here. You may even wish to
+# move this function into another python script and import it using:
+# from mycustomtabbingconfig import colour_selector
+# Remember to rename, delete or comment out this function if you do that.
+
+def colour_selector(tabindex, currentpage, uzbl):
+ '''Tablist styling policy handler. This function must return a tuple of
+ the form (tab style, text style).'''
+
+ # Just as an example:
+ # if 'error' in uzbl.title:
+ # if tabindex == currentpage:
+ # return ('foreground="#fff"', 'foreground="red"')
+ # return ('foreground="#888"', 'foreground="red"')
+
+ # Style tabs to indicate connected via https.
+ if config['tab_indicate_https'] and uzbl.uri.startswith("https://"):
+ if tabindex == currentpage:
+ return (config['selected_https'], config['selected_https_text'])
+ return (config['https_colours'], config['https_text_colours'])
+
+ # Style to indicate selected.
+ if tabindex == currentpage:
+ return (config['selected_tab'], config['selected_tab_text'])
+
+ # Default tab style.
+ return (config['tab_colours'], config['tab_text_colours'])
+
+# ============================================================================
+# ::: End of configuration section :::::::::::::::::::::::::::::::::::::::::::
+# ============================================================================
+
+def echo(msg):
+ if config['verbose']:
+ sys.stderr.write("%s: %s\n" % (_SCRIPTNAME, msg))
+
+
+def counter():
+ '''To infinity and beyond!'''
+
+ i = 0
+ while True:
+ i += 1
+ yield i
+
+
+def escape(s):
+ '''Replaces html markup in tab titles that screw around with pango.'''
+
+ for (split, glue) in [('&','&amp;'), ('<', '&lt;'), ('>', '&gt;')]:
+ s = s.replace(split, glue)
+ return s
+
+
+class SocketClient:
+ '''Represents a Uzbl instance, which is not necessarly linked with a UzblInstance'''
+
+ # List of UzblInstance objects not already linked with a SocketClient
+ instances_queue = {}
+
+ def __init__(self, socket):
+ self._buffer = ""
+ self._socket = socket
+ self._watchers = [io_add_watch(socket, IO_IN, self._socket_recv),\
+ io_add_watch(socket, IO_HUP, self._socket_closed)]
+ self.uzbl = None
+
+
+ def _socket_recv(self, fd, condition):
+ '''Data available on socket, process it'''
+
+ self._feed(self._socket.recv(1024)) #TODO: is io_add_watch edge or level-triggered ?
+ return True
+
+
+ def _socket_closed(self, fd, condition):
+ '''Remote client exited'''
+ self.uzbl.close()
+ return False
+
+
+ def _feed(self, data):
+ '''An Uzbl instance sent some data, parse it'''
+
+ self._buffer += data
+ if self.uzbl:
+ if "\n" in self._buffer:
+ cmds = self._buffer.split("\n")
+
+ if cmds[-1]: # Last command has been received incomplete, don't process it
+ self._buffer, cmds = cmds[-1], cmds[:-1]
+ else:
+ self._buffer = ""
+
+ for cmd in cmds:
+ if cmd:
+ self.uzbl.parse_command(cmd)
+ else:
+ name = re.findall('^EVENT \[(\d+-\d+)\] INSTANCE_START \d+$', self._buffer, re.M)
+ uzbl = self.instances_queue.get(name[0])
+ if uzbl:
+ del self.instances_queue[name[0]]
+ self.uzbl = uzbl
+ self.uzbl.got_socket(self)
+ self._feed("")
+
+ def send(self, data):
+ '''Child socket send function.'''
+
+ self._socket.send(data + "\n")
+
+ def close(self):
+ '''Close the connection'''
+
+ if self._socket:
+ self._socket.close()
+ self._socket = None
+ map(source_remove, self._watchers)
+ self._watchers = []
+
+
+class UzblInstance:
+ '''Uzbl instance meta-data/meta-action object.'''
+
+ def __init__(self, parent, tab, name, uri, title, switch):
+
+ self.parent = parent
+ self.tab = tab
+ self.name = name
+ self.title = title
+ self.tabtitle = ""
+ self.uri = uri
+ self._client = None
+ self._switch = switch # Switch to tab after loading ?
+ self.title_changed()
+
+
+ def got_socket(self, client):
+ '''Uzbl instance is now connected'''
+
+ self._client = client
+ self.parent.config_uzbl(self)
+ if self._switch:
+ tabid = self.parent.notebook.page_num(self.tab)
+ self.parent.goto_tab(tabid)
+
+
+ def title_changed(self, gtk_only = True): # GTK-only is for indexes
+ '''self.title has changed, update the tabs list'''
+
+ tab_titles = config['tab_titles']
+ tab_indexes = config['tab_indexes']
+ show_ellipsis = config['show_ellipsis']
+ max_title_len = config['max_title_len']
+
+ # Unicode heavy strings do not like being truncated/sliced so by
+ # re-encoding the string sliced of limbs are removed.
+ self.tabtitle = self.title[:max_title_len + int(show_ellipsis)]
+ if type(self.tabtitle) != types.UnicodeType:
+ self.tabtitle = unicode(self.tabtitle, 'utf-8', 'ignore')
+
+ self.tabtitle = self.tabtitle.encode('utf-8', 'ignore').strip()
+
+ if show_ellipsis and len(self.tabtitle) != len(self.title):
+ self.tabtitle += "\xe2\x80\xa6"
+
+ gtk_tab_format = "%d %s"
+ index = self.parent.notebook.page_num(self.tab)
+ if tab_titles and tab_indexes:
+ self.parent.notebook.set_tab_label_text(self.tab,
+ gtk_tab_format % (index, self.tabtitle))
+ elif tab_titles:
+ self.parent.notebook.set_tab_label_text(self.tab, self.tabtitle)
+ else:
+ self.parent.notebook.set_tab_label_text(self.tab, str(index))
+
+ # If instance is current tab, update window title
+ if index == self.parent.notebook.get_current_page():
+ title_format = "%s - Uzbl Browser"
+ self.parent.window.set_title(title_format % self.title)
+
+ # Non-GTK tabs
+ if not gtk_only:
+ self.parent.update_tablist()
+
+
+ def set(self, key, val):
+ ''' Send the SET command to Uzbl '''
+
+ if self._client:
+ self._client.send('set %s = %s') #TODO: escape chars ?
+
+
+ def exit(self):
+ ''' Ask the Uzbl instance to close '''
+
+ if self._client:
+ self._client.send('exit')
+
+
+ def parse_command(self, cmd):
+ ''' Parse event givent by the Uzbl instance '''
+
+ type, _, args = cmd.split(" ", 2)
+ if type == "EVENT":
+ type, args = args.split(" ", 1)
+ if type == "TITLE_CHANGED":
+ self.title = args
+ self.title_changed()
+ elif type == "VARIABLE_SET":
+ var, _, val = args.split(" ", 2)
+ try:
+ val = int(val)
+ except:
+ pass
+
+ if var in UZBL_TABBED_VARS:
+ if config[var] != val:
+ config[var] = val
+ if var == "show_gtk_tabs":
+ self.parent.notebook.set_show_tabs(bool(val))
+ elif var == "show_tablist" or var == "tablist_top":
+ self.parent.update_tablist_display()
+ elif var == "gtk_tab_pos":
+ self.parent.update_gtk_tab_pos()
+ elif var == "status_background":
+ col = gtk.gdk.color_parse(config['status_background'])
+ self.parent.ebox.modify_bg(gtk.STATE_NORMAL, col)
+ elif var == "tab_titles" or var == "tab_indexes":
+ for tab in self.parent.notebook:
+ self.parent.tabs[tab].title_changed(True)
+
+ self.parent.update_tablist()
+ else:
+ config[var] = val
+
+ if var == "uri":
+ self.uri = var
+ self.parent.update_tablist()
+ elif type == "NEW_TAB":
+ self.parent.new_tab(args)
+ elif type == "NEXT_TAB":
+ if args:
+ self.parent.next_tab(int(args))
+ else:
+ self.parent.next_tab()
+ elif type == "PREV_TAB":
+ if args:
+ self.parent.prev_tab(int(args))
+ else:
+ self.parent.prev_tab()
+ elif type == "GOTO_TAB":
+ self.parent.goto_tab(int(args))
+ elif type == "FIRST_TAB":
+ self.parent.goto_tab(0)
+ elif type == "LAST_TAB":
+ self.parent.goto_tab(-1)
+ elif type == "PRESET_TABS":
+ self.parent.parse_command(["preset"] + args.split())
+ elif type == "BRING_TO_FRONT":
+ self.parent.window.present()
+ elif type == "CLEAN_TABS":
+ self.parent.clean_slate()
+ elif type == "EXIT_ALL_TABS":
+ self.parent.quitrequest()
+
+
+ def close(self):
+ '''The remote instance exited'''
+
+ if self._client:
+ self._client.close()
+ self._client = None
+
+
+class UzblTabbed:
+ '''A tabbed version of uzbl using gtk.Notebook'''
+
+ def __init__(self):
+ '''Create tablist, window and notebook.'''
+
+ self._timers = {}
+ self._buffer = ""
+ self._killed = False
+
+ # A list of the recently closed tabs
+ self._closed = []
+
+ # Holds metadata on the uzbl childen open.
+ self.tabs = {}
+
+ # Uzbl sockets (socket => SocketClient)
+ self.clients = {}
+
+ # Generates a unique id for uzbl socket filenames.
+ self.next_pid = counter().next
+
+ # Create main window
+ self.window = gtk.Window()
+ try:
+ window_size = map(int, config['window_size'].split(','))
+ self.window.set_default_size(*window_size)
+
+ except:
+ error("Invalid value for default_size in config file.")
+
+ self.window.set_title("Uzbl Browser")
+ self.window.set_border_width(0)
+
+ # Set main window icon
+ icon_path = config['icon_path']
+ if os.path.exists(icon_path):
+ self.window.set_icon(gtk.gdk.pixbuf_new_from_file(icon_path))
+
+ else:
+ icon_path = '/usr/share/uzbl/examples/data/uzbl.png'
+ if os.path.exists(icon_path):
+ self.window.set_icon(gtk.gdk.pixbuf_new_from_file(icon_path))
+
+ # Attach main window event handlers
+ self.window.connect("delete-event", self.quitrequest)
+
+ # Create tab list
+ vbox = gtk.VBox()
+ self.vbox = vbox
+ self.window.add(vbox)
+ ebox = gtk.EventBox()
+ self.ebox = ebox
+ self.tablist = gtk.Label()
+
+ self.tablist.set_use_markup(True)
+ self.tablist.set_justify(gtk.JUSTIFY_LEFT)
+ self.tablist.set_line_wrap(False)
+ self.tablist.set_selectable(False)
+ self.tablist.set_padding(2,2)
+ self.tablist.set_alignment(0,0)
+ self.tablist.set_ellipsize(pango.ELLIPSIZE_END)
+ self.tablist.set_text(" ")
+ self.tablist.show()
+ ebox.add(self.tablist)
+ ebox.show()
+ bgcolor = gtk.gdk.color_parse(config['status_background'])
+ ebox.modify_bg(gtk.STATE_NORMAL, bgcolor)
+
+ # Create notebook
+ self.notebook = gtk.Notebook()
+ self.notebook.set_show_tabs(config['show_gtk_tabs'])
+
+ # Set tab position
+ self.update_gtk_tab_pos()
+
+ self.notebook.set_show_border(False)
+ self.notebook.set_scrollable(True)
+ self.notebook.set_border_width(0)
+
+ self.notebook.connect("page-removed", self.tab_closed)
+ self.notebook.connect("switch-page", self.tab_changed)
+ self.notebook.connect("page-added", self.tab_opened)
+
+ self.notebook.show()
+ vbox.pack_start(self.notebook, True, True, 0)
+ vbox.reorder_child(self.notebook, 1)
+ self.update_tablist_display()
+
+ self.vbox.show()
+ self.window.show()
+ self.wid = self.notebook.window.xid
+
+ # Store information about the applications fifo and socket.
+ fifo_filename = 'uzbltabbed_%d.fifo' % os.getpid()
+ socket_filename = 'uzbltabbed_%d.socket' % os.getpid()
+ self._fifo = None
+ self._socket = None
+ self.fifo_path = os.path.join(config['fifo_dir'], fifo_filename)
+ self.socket_path = os.path.join(config['socket_dir'], socket_filename)
+
+ # Now initialise the fifo and the socket
+ self.init_fifo()
+ self.init_socket()
+
+ # If we are using sessions then load the last one if it exists.
+ if config['save_session']:
+ self.load_session()
+
+
+ def run(self):
+ '''UzblTabbed main function that calls the gtk loop.'''
+
+ if not self.clients and not SocketClient.instances_queue and not self.tabs:
+ self.new_tab()
+
+ gtk_refresh = int(config['gtk_refresh'])
+ if gtk_refresh < 100:
+ gtk_refresh = 100
+
+ # Make SIGTERM act orderly.
+ signal(SIGTERM, lambda signum, stack_frame: self.terminate(SIGTERM))
+
+ # Catch keyboard interrupts
+ signal(SIGINT, lambda signum, stack_frame: self.terminate(SIGINT))
+
+ try:
+ gtk.main()
+
+ except:
+ error("encounted error %r" % sys.exc_info()[1])
+
+ # Unlink fifo socket
+ self.unlink_fifo()
+ self.close_socket()
+
+ # Attempt to close all uzbl instances nicely.
+ self.quitrequest()
+
+ # Allow time for all the uzbl instances to quit.
+ time.sleep(1)
+
+ raise
+
+
+ def terminate(self, termsig=None):
+ '''Handle termination signals and exit safely and cleanly.'''
+
+ # Not required but at least it lets the user know what killed his
+ # browsing session.
+ if termsig == SIGTERM:
+ error("caught SIGTERM signal")
+
+ elif termsig == SIGINT:
+ error("caught keyboard interrupt")
+
+ else:
+ error("caught unknown signal")
+
+ error("commencing infanticide!")
+
+ # Sends the exit signal to all uzbl instances.
+ self.quitrequest()
+
+
+ def init_socket(self):
+ '''Create interprocess communication socket.'''
+
+ def accept(sock, condition):
+ '''A new uzbl instance was created'''
+
+ client, _ = sock.accept()
+ self.clients[client] = SocketClient(client)
+
+ return True
+
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ sock.bind(self.socket_path)
+ sock.listen(1)
+
+ # Add event handler for IO_IN event.
+ self._socket = (sock, io_add_watch(sock, IO_IN, accept))
+
+ echo("[socket] listening at %r" % self.socket_path)
+
+ # Add atexit register to destroy the socket on program termination.
+ atexit.register(self.close_socket)
+
+
+ def close_socket(self):
+ '''Close the socket when closing the application'''
+
+ if self._socket:
+ (fd, watcher) = self._socket
+ source_remove(watcher)
+ fd.close()
+ os.unlink(self.socket_path)
+ self._socket = None
+
+
+ def init_fifo(self):
+ '''Create interprocess communication fifo.'''
+
+ if os.path.exists(self.fifo_path):
+ if not os.access(self.fifo_path, os.F_OK | os.R_OK | os.W_OK):
+ os.mkfifo(self.fifo_path)
+
+ else:
+ basedir = os.path.dirname(self.fifo_path)
+ if not os.path.exists(basedir):
+ os.makedirs(basedir)
+
+ os.mkfifo(self.fifo_path)
+
+ # Add event handlers for IO_IN & IO_HUP events.
+ self.setup_fifo_watchers()
+
+ echo("[fifo] listening at %r" % self.fifo_path)
+
+ # Add atexit register to destroy the fifo on program termination.
+ atexit.register(self.unlink_fifo)
+
+
+ def unlink_fifo(self):
+ '''Unlink the fifo socket. Note: This function is called automatically
+ on exit by an atexit register.'''
+
+ # Make sure the fifo fd is closed.
+ self.close_fifo()
+
+ # And unlink if the real fifo exists.
+ if os.path.exists(self.fifo_path):
+ os.unlink(self.fifo_path)
+ echo("unlinked %r" % self.fifo_path)
+
+
+ def close_fifo(self):
+ '''Remove all event handlers watching the fifo and close the fd.'''
+
+ # Already closed
+ if self._fifo is None: return
+
+ (fd, watchers) = self._fifo
+ os.close(fd)
+
+ # Stop all gobject io watchers watching the fifo.
+ for gid in watchers:
+ source_remove(gid)
+
+ self._fifo = None
+
+
+ def setup_fifo_watchers(self):
+ '''Open fifo socket fd and setup gobject IO_IN & IO_HUP event
+ handlers.'''
+
+ # Close currently open fifo fd and kill all watchers
+ self.close_fifo()
+
+ fd = os.open(self.fifo_path, os.O_RDONLY | os.O_NONBLOCK)
+
+ # Add gobject io event handlers to the fifo socket.
+ watchers = [io_add_watch(fd, IO_IN, self.main_fifo_read),\
+ io_add_watch(fd, IO_HUP, self.main_fifo_hangup)]
+
+ self._fifo = (fd, watchers)
+
+
+ def main_fifo_hangup(self, fd, cb_condition):
+ '''Handle main fifo socket hangups.'''
+
+ # Close old fd, open new fifo socket and add io event handlers.
+ self.setup_fifo_watchers()
+
+ # Kill the gobject event handler calling this handler function.
+ return False
+
+
+ def main_fifo_read(self, fd, cb_condition):
+ '''Read from main fifo socket.'''
+
+ self._buffer = os.read(fd, 1024)
+ temp = self._buffer.split("\n")
+ self._buffer = temp.pop()
+ cmds = [s.strip().split() for s in temp if len(s.strip())]
+
+ for cmd in cmds:
+ try:
+ #print cmd
+ self.parse_command(cmd)
+
+ except:
+ error("parse_command: invalid command %s" % ' '.join(cmd))
+ raise
+
+ return True
+
+
+ def parse_command(self, cmd):
+ '''Parse instructions from uzbl child processes.'''
+
+ # Commands ( [] = optional, {} = required )
+ # new [uri]
+ # open new tab and head to optional uri.
+ # close [tab-num]
+ # close current tab or close via tab id.
+ # next [n-tabs]
+ # open next tab or n tabs down. Supports negative indexing.
+ # prev [n-tabs]
+ # open prev tab or n tabs down. Supports negative indexing.
+ # goto {tab-n}
+ # goto tab n.
+ # first
+ # goto first tab.
+ # last
+ # goto last tab.
+ # title {pid} {document-title}
+ # updates tablist title.
+ # uri {pid} {document-location}
+ # updates tablist uri
+ # bring_to_front
+ # brings the gtk window to focus.
+ # exit
+ # exits uzbl_tabbed.py
+
+ if cmd[0] == "new":
+ if len(cmd) == 2:
+ self.new_tab(cmd[1])
+
+ else:
+ self.new_tab()
+
+ elif cmd[0] == "newfromclip":
+ uri = subprocess.Popen(['xclip','-selection','clipboard','-o'],\
+ stdout=subprocess.PIPE).communicate()[0]
+ if uri:
+ self.new_tab(uri)
+
+ elif cmd[0] == "close":
+ if len(cmd) == 2:
+ self.close_tab(int(cmd[1]))
+
+ else:
+ self.close_tab()
+
+ elif cmd[0] == "next":
+ if len(cmd) == 2:
+ self.next_tab(int(cmd[1]))
+
+ else:
+ self.next_tab()
+
+ elif cmd[0] == "prev":
+ if len(cmd) == 2:
+ self.prev_tab(int(cmd[1]))
+
+ else:
+ self.prev_tab()
+
+ elif cmd[0] == "goto":
+ self.goto_tab(int(cmd[1]))
+
+ elif cmd[0] == "first":
+ self.goto_tab(0)
+
+ elif cmd[0] == "last":
+ self.goto_tab(-1)
+
+ elif cmd[0] in ["title", "uri"]:
+ if len(cmd) > 2:
+ uzbl = self.get_tab_by_name(int(cmd[1]))
+ if uzbl:
+ old = getattr(uzbl, cmd[0])
+ new = ' '.join(cmd[2:])
+ setattr(uzbl, cmd[0], new)
+ if old != new:
+ self.update_tablist()
+
+ else:
+ error("parse_command: no uzbl with name %r" % int(cmd[1]))
+
+ elif cmd[0] == "preset":
+ if len(cmd) < 3:
+ error("parse_command: invalid preset command")
+
+ elif cmd[1] == "save":
+ path = os.path.join(config['saved_sessions_dir'], cmd[2])
+ self.save_session(path)
+
+ elif cmd[1] == "load":
+ path = os.path.join(config['saved_sessions_dir'], cmd[2])
+ self.load_session(path)
+
+ elif cmd[1] == "del":
+ path = os.path.join(config['saved_sessions_dir'], cmd[2])
+ if os.path.isfile(path):
+ os.remove(path)
+
+ else:
+ error("parse_command: preset %r does not exist." % path)
+
+ elif cmd[1] == "list":
+ uzbl = self.get_tab_by_name(int(cmd[2]))
+ if uzbl:
+ if not os.path.isdir(config['saved_sessions_dir']):
+ js = "js alert('No saved presets.');"
+ uzbl._client.send(js)
+
+ else:
+ listdir = os.listdir(config['saved_sessions_dir'])
+ listdir = "\\n".join(listdir)
+ js = "js alert('Session presets:\\n\\n%s');" % listdir
+ uzbl._client.send(js)
+
+ else:
+ error("parse_command: unknown tab name.")
+
+ else:
+ error("parse_command: unknown parse command %r"\
+ % ' '.join(cmd))
+
+ elif cmd[0] == "bring_to_front":
+ self.window.present()
+
+ elif cmd[0] == "clean":
+ self.clean_slate()
+
+ elif cmd[0] == "exit":
+ self.quitrequest()
+
+ else:
+ error("parse_command: unknown command %r" % ' '.join(cmd))
+
+
+ def get_tab_by_name(self, name):
+ '''Return uzbl instance by name.'''
+
+ for (tab, uzbl) in self.tabs.items():
+ if uzbl.name == name:
+ return uzbl
+
+ return False
+
+
+ def new_tab(self, uri='', title='', switch=None):
+ '''Add a new tab to the notebook and start a new instance of uzbl.
+ Use the switch option to negate config['switch_to_new_tabs'] option
+ when you need to load multiple tabs at a time (I.e. like when
+ restoring a session from a file).'''
+
+ tab = gtk.Socket()
+ tab.show()
+ self.notebook.append_page(tab)
+ sid = tab.get_id()
+ uri = uri.strip()
+ name = "%d-%d" % (os.getpid(), self.next_pid())
+
+ if switch is None:
+ switch = config['switch_to_new_tabs']
+
+ if not title:
+ title = config['new_tab_title']
+
+ cmd = ['uzbl-browser', '-n', name, '-s', str(sid),
+ '--connect-socket', self.socket_path, '--uri', uri]
+ subprocess.Popen(cmd) # TODO: do i need close_fds=True ?
+
+ uzbl = UzblInstance(self, tab, name, uri, title, switch)
+ SocketClient.instances_queue[name] = uzbl
+ self.tabs[tab] = uzbl
+
+
+ def clean_slate(self):
+ '''Close all open tabs and open a fresh brand new one.'''
+
+ self.new_tab()
+ tabs = self.tabs.keys()
+ for tab in list(self.notebook)[:-1]:
+ if tab not in tabs: continue
+ uzbl = self.tabs[tab]
+ uzbl.exit()
+
+
+ def config_uzbl(self, uzbl):
+ '''Send bind commands for tab new/close/next/prev to a uzbl
+ instance.'''
+
+ # Set definitions here
+ # set(key, command back to fifo)
+ if config['capture_new_windows']:
+ uzbl.set("new_window", r'new $8')
+
+
+ def goto_tab(self, index):
+ '''Goto tab n (supports negative indexing).'''
+
+ title_format = "%s - Uzbl Browser"
+
+ tabs = list(self.notebook)
+ if 0 <= index < len(tabs):
+ self.notebook.set_current_page(index)
+ uzbl = self.tabs[self.notebook.get_nth_page(index)]
+ self.window.set_title(title_format % uzbl.title)
+ self.update_tablist()
+ return None
+
+ try:
+ tab = tabs[index]
+ # Update index because index might have previously been a
+ # negative index.
+ index = tabs.index(tab)
+ self.notebook.set_current_page(index)
+ uzbl = self.tabs[self.notebook.get_nth_page(index)]
+ self.window.set_title(title_format % uzbl.title)
+ self.update_tablist()
+
+ except IndexError:
+ pass
+
+
+ def next_tab(self, step=1):
+ '''Switch to next tab or n tabs right.'''
+
+ if step < 1:
+ error("next_tab: invalid step %r" % step)
+ return None
+
+ ntabs = self.notebook.get_n_pages()
+ tabn = (self.notebook.get_current_page() + step) % ntabs
+ self.goto_tab(tabn)
+
+
+ def prev_tab(self, step=1):
+ '''Switch to prev tab or n tabs left.'''
+
+ if step < 1:
+ error("prev_tab: invalid step %r" % step)
+ return None
+
+ ntabs = self.notebook.get_n_pages()
+ tabn = self.notebook.get_current_page() - step
+ while tabn < 0: tabn += ntabs
+ self.goto_tab(tabn)
+
+
+ def close_tab(self, tabn=None):
+ '''Closes current tab. Supports negative indexing.'''
+
+ if tabn is None:
+ tabn = self.notebook.get_current_page()
+
+ else:
+ try:
+ tab = list(self.notebook)[tabn]
+
+ except IndexError:
+ error("close_tab: invalid index %r" % tabn)
+ return None
+
+ self.notebook.remove_page(tabn)
+
+
+ def tab_opened(self, notebook, tab, index):
+ '''Called upon tab creation. Called by page-added signal.'''
+
+ if config['switch_to_new_tabs']:
+ self.notebook.set_focus_child(tab)
+
+ else:
+ oldindex = self.notebook.get_current_page()
+ oldtab = self.notebook.get_nth_page(oldindex)
+ self.notebook.set_focus_child(oldtab)
+
+
+ def tab_closed(self, notebook, tab, index):
+ '''Close the window if no tabs are left. Called by page-removed
+ signal.'''
+
+ if tab in self.tabs.keys():
+ uzbl = self.tabs[tab]
+ uzbl.close()
+
+ self._closed.append((uzbl.uri, uzbl.title))
+ self._closed = self._closed[-10:]
+ del self.tabs[tab]
+
+ if self.notebook.get_n_pages() == 0:
+ if not self._killed and config['save_session']:
+ if os.path.exists(config['session_file']):
+ os.remove(config['session_file'])
+
+ self.quit()
+
+ for tab in self.notebook:
+ self.tabs[tab].title_changed(True)
+ self.update_tablist()
+
+ return True
+
+
+ def tab_changed(self, notebook, page, index):
+ '''Refresh tab list. Called by switch-page signal.'''
+
+ tab = self.notebook.get_nth_page(index)
+ self.notebook.set_focus_child(tab)
+ self.update_tablist(index)
+ return True
+
+
+ def update_tablist_display(self):
+ '''Called when show_tablist or tablist_top has changed'''
+
+ if self.ebox in self.vbox.get_children():
+ self.vbox.remove(self.ebox)
+
+ if config['show_tablist']:
+ self.vbox.pack_start(self.ebox, False, False, 0)
+ if config['tablist_top']:
+ self.vbox.reorder_child(self.ebox, 0)
+ else:
+ self.vbox.reorder_child(self.ebox, 2)
+
+ def update_gtk_tab_pos(self):
+ ''' Called when gtk_tab_pos has changed '''
+
+ allposes = {'left': gtk.POS_LEFT, 'right':gtk.POS_RIGHT,
+ 'top':gtk.POS_TOP, 'bottom':gtk.POS_BOTTOM}
+ if config['gtk_tab_pos'] in allposes.keys():
+ self.notebook.set_tab_pos(allposes[config['gtk_tab_pos']])
+
+
+ def update_tablist(self, curpage=None):
+ '''Upate tablist status bar.'''
+
+ if not config['show_tablist']:
+ return True
+
+ tab_titles = config['tab_titles']
+ tab_indexes = config['tab_indexes']
+ multiline_tabs = config['multiline_tabs']
+
+ if multiline_tabs:
+ multiline = []
+
+ tabs = self.tabs.keys()
+ if curpage is None:
+ curpage = self.notebook.get_current_page()
+
+ pango = ""
+ normal = (config['tab_colours'], config['tab_text_colours'])
+ selected = (config['selected_tab'], config['selected_tab_text'])
+
+ if tab_titles and tab_indexes:
+ tab_format = "<span %(tabc)s> [ %(index)d <span %(textc)s> %(title)s</span> ] </span>"
+ elif tab_titles:
+ tab_format = "<span %(tabc)s> [ <span %(textc)s>%(title)s</span> ] </span>"
+ else:
+ tab_format = "<span %(tabc)s> [ <span %(textc)s>%(index)d</span> ] </span>"
+
+ for index, tab in enumerate(self.notebook):
+ if tab not in tabs: continue
+ uzbl = self.tabs[tab]
+ title = escape(uzbl.tabtitle)
+
+ style = colour_selector(index, curpage, uzbl)
+ (tabc, textc) = style
+
+ if multiline_tabs:
+ opango = pango
+
+ pango += tab_format % locals()
+
+ self.tablist.set_markup(pango)
+ listwidth = self.tablist.get_layout().get_pixel_size()[0]
+ winwidth = self.window.get_size()[0]
+
+ if listwidth > (winwidth - 20):
+ multiline.append(opango)
+ pango = tab_format % locals()
+ else:
+ pango += tab_format % locals()
+
+ if multiline_tabs:
+ multiline.append(pango)
+ self.tablist.set_markup('&#10;'.join(multiline))
+
+ else:
+ self.tablist.set_markup(pango)
+
+ return True
+
+
+ def save_session(self, session_file=None):
+ '''Save the current session to file for restoration on next load.'''
+
+ strip = str.strip
+
+ if session_file is None:
+ session_file = config['session_file']
+
+ tabs = self.tabs.keys()
+ state = []
+ for tab in list(self.notebook):
+ if tab not in tabs: continue
+ uzbl = self.tabs[tab]
+ if not uzbl.uri: continue
+ state += [(uzbl.uri, uzbl.title),]
+
+ session = {'curtab': self.notebook.get_current_page(),
+ 'tabs': state}
+
+ if config['json_session']:
+ raw = json.dumps(session)
+
+ else:
+ lines = ["curtab = %d" % session['curtab'],]
+ for (uri, title) in session['tabs']:
+ lines += ["%s\t%s" % (strip(uri), strip(title)),]
+
+ raw = "\n".join(lines)
+
+ if not os.path.isfile(session_file):
+ dirname = os.path.dirname(session_file)
+ if not os.path.isdir(dirname):
+ os.makedirs(dirname)
+
+ h = open(session_file, 'w')
+ h.write(raw)
+ h.close()
+
+
+ def load_session(self, session_file=None):
+ '''Load a saved session from file.'''
+
+ default_path = False
+ strip = str.strip
+ json_session = config['json_session']
+ delete_loaded = False
+
+ if session_file is None:
+ default_path = True
+ delete_loaded = True
+ session_file = config['session_file']
+
+ if not os.path.isfile(session_file):
+ return False
+
+ h = open(session_file, 'r')
+ raw = h.read()
+ h.close()
+ if json_session:
+ if sum([1 for s in raw.split("\n") if strip(s)]) != 1:
+ error("Warning: The session file %r does not look json. "\
+ "Trying to load it as a non-json session file."\
+ % session_file)
+ json_session = False
+
+ if json_session:
+ try:
+ session = json.loads(raw)
+ curtab, tabs = session['curtab'], session['tabs']
+
+ except:
+ error("Failed to load jsonifed session from %r"\
+ % session_file)
+ return None
+
+ else:
+ tabs = []
+ strip = str.strip
+ curtab, tabs = 0, []
+ lines = [s for s in raw.split("\n") if strip(s)]
+ if len(lines) < 2:
+ error("Warning: The non-json session file %r looks invalid."\
+ % session_file)
+ return None
+
+ try:
+ for line in lines:
+ if line.startswith("curtab"):
+ curtab = int(line.split()[-1])
+
+ else:
+ uri, title = line.split("\t",1)
+ tabs += [(strip(uri), strip(title)),]
+
+ except:
+ error("Warning: failed to load session file %r" % session_file)
+ return None
+
+ session = {'curtab': curtab, 'tabs': tabs}
+
+ # Now populate notebook with the loaded session.
+ for (index, (uri, title)) in enumerate(tabs):
+ self.new_tab(uri=uri, title=title, switch=(curtab==index))
+
+ # A saved session has been loaded now delete it.
+ if delete_loaded and os.path.exists(session_file):
+ os.remove(session_file)
+
+ # There may be other state information in the session dict of use to
+ # other functions. Of course however the non-json session object is
+ # just a dummy object of no use to no one.
+ return session
+
+
+ def quitrequest(self, *args):
+ '''Attempt to close all uzbl instances nicely and exit.'''
+
+ self._killed = True
+
+ if config['save_session']:
+ if len(list(self.notebook)) > 1:
+ self.save_session()
+
+ else:
+ # Notebook has one page open so delete the session file.
+ if os.path.isfile(config['session_file']):
+ os.remove(config['session_file'])
+
+ for (tab, uzbl) in self.tabs.items():
+ uzbl.exit()
+
+ # Add a gobject timer to make sure the application force-quits after a
+ # reasonable period. Calling quit when all the tabs haven't had time to
+ # close should be a last resort.
+ timer = "force-quit"
+ timerid = timeout_add(5000, self.quit, timer)
+ self._timers[timer] = timerid
+
+
+ def quit(self, *args):
+ '''Cleanup and quit. Called by delete-event signal.'''
+
+ # Close the fifo socket, remove any gobject io event handlers and
+ # delete socket.
+ self.unlink_fifo()
+ self.close_socket()
+
+ # Remove all gobject timers that are still ticking.
+ for (timerid, gid) in self._timers.items():
+ source_remove(gid)
+ del self._timers[timerid]
+
+ try:
+ gtk.main_quit()
+
+ except:
+ pass
+
+
+if __name__ == "__main__":
+
+ # Build command line parser
+ usage = "usage: %prog [OPTIONS] {URIS}..."
+ parser = OptionParser(usage=usage)
+ parser.add_option('-n', '--no-session', dest='nosession',
+ action='store_true', help="ignore session saving a loading.")
+ parser.add_option('-v', '--verbose', dest='verbose',
+ action='store_true', help='print verbose output.')
+
+ # Parse command line options
+ (options, uris) = parser.parse_args()
+
+ if options.nosession:
+ config['save_session'] = False
+
+ if options.verbose:
+ config['verbose'] = True
+
+ if config['json_session']:
+ try:
+ import simplejson as json
+
+ except:
+ error("Warning: json_session set but cannot import the python "\
+ "module simplejson. Fix: \"set json_session = 0\" or "\
+ "install the simplejson python module to remove this warning.")
+ config['json_session'] = False
+
+ if config['verbose']:
+ import pprint
+ sys.stderr.write("%s\n" % pprint.pformat(config))
+
+ uzbl = UzblTabbed()
+
+ # All extra arguments given to uzbl_tabbed.py are interpreted as
+ # web-locations to opened in new tabs.
+ lasturi = len(uris)-1
+ for (index,uri) in enumerate(uris):
+ uzbl.new_tab(uri, switch=(index==lasturi))
+
+ uzbl.run()
diff --git a/examples/data/scripts/uzblcat b/examples/data/scripts/uzblcat
new file mode 100755
index 0000000..e955608
--- /dev/null
+++ b/examples/data/scripts/uzblcat
@@ -0,0 +1,12 @@
+#!/usr/bin/env python
+# uzblcat - safely push html to uzbl
+# See http://www.uzbl.org/wiki/html-mode
+
+from sys import stdin, stdout
+
+stdout.write("uri data:text/html,")
+for line in stdin:
+ stdout.write(line[0:-1])
+
+# vim: set noet ff=unix
+