From 9dd1370d0b7cd876f004f7a822b0357039252184 Mon Sep 17 00:00:00 2001 From: Dieter Plaetinck Date: Sat, 2 Jan 2010 20:11:15 +0100 Subject: remove the 'uzbl' subdirectory in examples/*/, since the sandbox they are no longer needed + update paths everywhere + remove examples/config/enchant (also not needed since sandbox) + bugfix: set /home/dieter in sandbox --- examples/data/scripts/cookies.sh | 154 +++ examples/data/scripts/download.sh | 22 + examples/data/scripts/extedit.js | 102 ++ examples/data/scripts/follow_Numbers.js | 228 ++++ examples/data/scripts/follow_Numbers_Strings.js | 212 ++++ examples/data/scripts/formfiller.pl | 99 ++ examples/data/scripts/formfiller.sh | 62 + examples/data/scripts/hint.js | 26 + examples/data/scripts/history.sh | 5 + examples/data/scripts/insert_bookmark.sh | 16 + examples/data/scripts/instance-select-wmii.sh | 54 + examples/data/scripts/linkfollow.js | 269 ++++ examples/data/scripts/load_url_from_bookmarks.sh | 20 + examples/data/scripts/load_url_from_history.sh | 24 + examples/data/scripts/scheme.py | 24 + examples/data/scripts/scroll-percentage.js | 68 ++ examples/data/scripts/session.sh | 62 + examples/data/scripts/uzbl-cookie-daemon | 664 ++++++++++ examples/data/scripts/uzbl-event-manager | 833 +++++++++++++ examples/data/scripts/uzbl-tabbed | 1417 ++++++++++++++++++++++ examples/data/scripts/uzblcat | 12 + 21 files changed, 4373 insertions(+) create mode 100755 examples/data/scripts/cookies.sh create mode 100755 examples/data/scripts/download.sh create mode 100644 examples/data/scripts/extedit.js create mode 100644 examples/data/scripts/follow_Numbers.js create mode 100644 examples/data/scripts/follow_Numbers_Strings.js create mode 100755 examples/data/scripts/formfiller.pl create mode 100755 examples/data/scripts/formfiller.sh create mode 100644 examples/data/scripts/hint.js create mode 100755 examples/data/scripts/history.sh create mode 100755 examples/data/scripts/insert_bookmark.sh create mode 100755 examples/data/scripts/instance-select-wmii.sh create mode 100644 examples/data/scripts/linkfollow.js create mode 100755 examples/data/scripts/load_url_from_bookmarks.sh create mode 100755 examples/data/scripts/load_url_from_history.sh create mode 100755 examples/data/scripts/scheme.py create mode 100644 examples/data/scripts/scroll-percentage.js create mode 100755 examples/data/scripts/session.sh create mode 100755 examples/data/scripts/uzbl-cookie-daemon create mode 100755 examples/data/scripts/uzbl-event-manager create mode 100755 examples/data/scripts/uzbl-tabbed create mode 100755 examples/data/scripts/uzblcat (limited to 'examples/data/scripts') 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/ +#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/].*?)>/i){ + $line =~ s/.*(].*?)>).*/$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/ +# files contain lines like: : + + +# 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|.*.*|\1: |p' # sscj's first version, does not work on http://wiki.archlinux.org/index.php?title=Special:UserLogin&returnto=Main_Page + regex='s|.*> $fifo +else + if [ "$action" == 'new' ] + then + curl "$url" | grep ' $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&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 +# Copyright (c) 2009, Dieter Plaetinck +# Copyright (c) 2009, Mason Larobina +# Copyright (c) 2009, Michael Fiano +# +# 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 . + +''' +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 +# Copyright (c) 2009, Dieter Plaetinck +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +''' + +E V E N T _ M A N A G E R . P Y +=============================== + +Event manager for uzbl written in python. + +''' + +import 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 "" % ', '.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 +# Copyright (c) 2009, Chris van Dijk +# Copyright (c) 2009, Mason Larobina +# +# 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 . + + +# Author(s): +# Tom Adams +# Wrote the original uzbl_tabbed.py as a proof of concept. +# +# Chris van Dijk (quigybo) +# Made signifigant headway on the old uzbl_tabbing.py script on the +# uzbl wiki +# +# Mason Larobina +# 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 +# uzbl_config path now honors XDG_CONFIG_HOME if it exists. +# +# Romain Bignon +# Fix for session restoration code. +# +# Jake Probst +# Wrote a patch that overflows tabs in the tablist on to new lines when +# running of room. +# +# Devon Jones +# Fifo command bring_to_front which brings the gtk window to focus. +# +# Simon Lipp (sloonz) +# Various + + +# Dependencies: +# pygtk - python bindings for gtk. +# pango - python bindings needed for text rendering & layout in gtk widgets. +# pygobject - GLib's GObject bindings for python. +# +# Optional dependencies: +# simplejson - save uzbl_tabbed.py sessions & presets in json. +# +# Note: I haven't included version numbers with this dependency list because +# I've only ever tested uzbl_tabbed.py on the latest stable versions of these +# packages in Gentoo's portage. Package names may vary on different systems. + + +# Configuration: +# Because this version of uzbl_tabbed is able to inherit options from your main +# uzbl configuration file you may wish to configure uzbl tabbed from there. +# Here is a list of configuration options that can be customised and some +# example values for each: +# +# General tabbing options: +# show_tablist = 1 +# show_gtk_tabs = 0 +# tablist_top = 1 +# gtk_tab_pos = (top|left|bottom|right) +# gtk_refresh = 1000 +# switch_to_new_tabs = 1 +# capture_new_windows = 1 +# multiline_tabs = 1 +# +# Tab title options: +# tab_titles = 1 +# tab_indexes = 1 +# new_tab_title = Loading +# max_title_len = 50 +# show_ellipsis = 1 +# +# Session options: +# save_session = 1 +# json_session = 0 +# session_file = $HOME/.local/share/uzbl/session +# +# Inherited uzbl options: +# fifo_dir = /tmp +# socket_dir = /tmp +# icon_path = $HOME/.local/share/uzbl/uzbl.png +# status_background = #303030 +# +# Misc options: +# window_size = 800,800 +# verbose = 0 +# +# And uzbl_tabbed.py takes care of the actual binding of the commands via each +# instances fifo socket. +# +# Custom tab styling: +# tab_colours = foreground = "#888" background = "#303030" +# tab_text_colours = foreground = "#bbb" +# selected_tab = foreground = "#fff" +# selected_tab_text = foreground = "green" +# tab_indicate_https = 1 +# https_colours = foreground = "#888" +# https_text_colours = foreground = "#9c8e2d" +# selected_https = foreground = "#fff" +# selected_https_text = foreground = "gold" +# +# How these styling values are used are soley defined by the syling policy +# handler below (the function in the config section). So you can for example +# turn the tab text colour Firetruck-Red in the event "error" appears in the +# tab title or some other arbitrary event. You may wish to make a trusted +# hosts file and turn tab titles of tabs visiting trusted hosts purple. + + +# Issues: +# - new windows are not caught and opened in a new tab. +# - when uzbl_tabbed.py crashes it takes all the children with it. +# - when a new tab is opened when using gtk tabs the tab button itself +# grabs focus from its child for a few seconds. +# - when switch_to_new_tabs is not selected the notebook page is +# maintained but the new window grabs focus (try as I might to stop it). + + +# Todo: +# - add command line options to use a different session file, not use a +# session file and or open a uri on starup. +# - ellipsize individual tab titles when the tab-list becomes over-crowded +# - add "<" & ">" arrows to tablist to indicate that only a subset of the +# currently open tabs are being displayed on the tablist. +# - add the small tab-list display when both gtk tabs and text vim-like +# tablist are hidden (I.e. [ 1 2 3 4 5 ]) +# - check spelling. +# - pass a uzbl socketid to uzbl_tabbed.py and have it assimilated into +# the collective. Resistance is futile! + + +import pygtk +import gtk +import subprocess +import os +import re +import time +import getopt +import pango +import select +import sys +import gobject +import socket +import random +import hashlib +import atexit +import types + +from gobject import io_add_watch, source_remove, timeout_add, IO_IN, IO_HUP +from signal import signal, SIGTERM, SIGINT +from optparse import OptionParser, OptionGroup + + +pygtk.require('2.0') + +_SCRIPTNAME = os.path.basename(sys.argv[0]) +def error(msg): + sys.stderr.write("%s: error: %s\n" % (_SCRIPTNAME, msg)) + +# ============================================================================ +# ::: Default configuration section :::::::::::::::::::::::::::::::::::::::::: +# ============================================================================ + +def xdghome(key, default): + '''Attempts to use the environ XDG_*_HOME paths if they exist otherwise + use $HOME and the default path.''' + + xdgkey = "XDG_%s_HOME" % key + if xdgkey in os.environ.keys() and os.environ[xdgkey]: + return os.environ[xdgkey] + + return os.path.join(os.environ['HOME'], default) + +# Setup xdg paths. +DATA_DIR = os.path.join(xdghome('DATA', '.local/share/'), 'uzbl/') + +# Ensure uzbl xdg paths exist +if not os.path.exists(DATA_DIR): + os.makedirs(DATA_DIR) + +# All of these settings can be inherited from your uzbl config file. +config = { + # Tab options + 'show_tablist': True, # Show text uzbl like statusbar tab-list + 'show_gtk_tabs': False, # Show gtk notebook tabs + 'tablist_top': True, # Display tab-list at top of window + 'gtk_tab_pos': 'top', # Gtk tab position (top|left|bottom|right) + 'gtk_refresh': 1000, # Tablist refresh millisecond interval + 'switch_to_new_tabs': True, # Upon opening a new tab switch to it + 'capture_new_windows': True, # Use uzbl_tabbed to catch new windows + 'multiline_tabs': True, # Tabs overflow onto new tablist lines. + + # Tab title options + 'tab_titles': True, # Display tab titles (else only tab-nums) + 'tab_indexes': True, # Display tab nums (else only tab titles) + 'new_tab_title': 'Loading', # New tab title + 'max_title_len': 50, # Truncate title at n characters + 'show_ellipsis': True, # Show ellipsis when truncating titles + + # Session options + 'save_session': True, # Save session in file when quit + 'json_session': False, # Use json to save session. + 'saved_sessions_dir': os.path.join(DATA_DIR, 'sessions/'), + 'session_file': os.path.join(DATA_DIR, 'session'), + + # Inherited uzbl options + 'fifo_dir': '/tmp', # Path to look for uzbl fifo. + 'socket_dir': '/tmp', # Path to look for uzbl socket. + 'icon_path': os.path.join(DATA_DIR, 'uzbl.png'), + 'status_background': "#303030", # Default background for all panels. + + # Misc options + 'window_size': "800,800", # width,height in pixels. + 'verbose': False, # Print verbose output. + + # Add custom tab style definitions to be used by the tab colour policy + # handler here. Because these are added to the config dictionary like + # any other uzbl_tabbed configuration option remember that they can + # be superseeded from your main uzbl config file. + 'tab_colours': 'foreground = "#888" background = "#303030"', + 'tab_text_colours': 'foreground = "#bbb"', + 'selected_tab': 'foreground = "#fff"', + 'selected_tab_text': 'foreground = "green"', + 'tab_indicate_https': True, + 'https_colours': 'foreground = "#888"', + 'https_text_colours': 'foreground = "#9c8e2d"', + 'selected_https': 'foreground = "#fff"', + 'selected_https_text': 'foreground = "gold"', + +} # End of config dict. + +UZBL_TABBED_VARS = config.keys() + +# This is the tab style policy handler. Every time the tablist is updated +# this function is called to determine how to colourise that specific tab +# according the simple/complex rules as defined here. You may even wish to +# move this function into another python script and import it using: +# from mycustomtabbingconfig import colour_selector +# Remember to rename, delete or comment out this function if you do that. + +def colour_selector(tabindex, currentpage, uzbl): + '''Tablist styling policy handler. This function must return a tuple of + the form (tab style, text style).''' + + # Just as an example: + # if 'error' in uzbl.title: + # if tabindex == currentpage: + # return ('foreground="#fff"', 'foreground="red"') + # return ('foreground="#888"', 'foreground="red"') + + # Style tabs to indicate connected via https. + if config['tab_indicate_https'] and uzbl.uri.startswith("https://"): + if tabindex == currentpage: + return (config['selected_https'], config['selected_https_text']) + return (config['https_colours'], config['https_text_colours']) + + # Style to indicate selected. + if tabindex == currentpage: + return (config['selected_tab'], config['selected_tab_text']) + + # Default tab style. + return (config['tab_colours'], config['tab_text_colours']) + +# ============================================================================ +# ::: End of configuration section ::::::::::::::::::::::::::::::::::::::::::: +# ============================================================================ + +def echo(msg): + if config['verbose']: + sys.stderr.write("%s: %s\n" % (_SCRIPTNAME, msg)) + + +def counter(): + '''To infinity and beyond!''' + + i = 0 + while True: + i += 1 + yield i + + +def escape(s): + '''Replaces html markup in tab titles that screw around with pango.''' + + for (split, glue) in [('&','&'), ('<', '<'), ('>', '>')]: + s = s.replace(split, glue) + return s + + +class SocketClient: + '''Represents a Uzbl instance, which is not necessarly linked with a UzblInstance''' + + # List of UzblInstance objects not already linked with a SocketClient + instances_queue = {} + + def __init__(self, socket): + self._buffer = "" + self._socket = socket + self._watchers = [io_add_watch(socket, IO_IN, self._socket_recv),\ + io_add_watch(socket, IO_HUP, self._socket_closed)] + self.uzbl = None + + + def _socket_recv(self, fd, condition): + '''Data available on socket, process it''' + + self._feed(self._socket.recv(1024)) #TODO: is io_add_watch edge or level-triggered ? + return True + + + def _socket_closed(self, fd, condition): + '''Remote client exited''' + self.uzbl.close() + return False + + + def _feed(self, data): + '''An Uzbl instance sent some data, parse it''' + + self._buffer += data + if self.uzbl: + if "\n" in self._buffer: + cmds = self._buffer.split("\n") + + if cmds[-1]: # Last command has been received incomplete, don't process it + self._buffer, cmds = cmds[-1], cmds[:-1] + else: + self._buffer = "" + + for cmd in cmds: + if cmd: + self.uzbl.parse_command(cmd) + else: + name = re.findall('^EVENT \[(\d+-\d+)\] INSTANCE_START \d+$', self._buffer, re.M) + uzbl = self.instances_queue.get(name[0]) + if uzbl: + del self.instances_queue[name[0]] + self.uzbl = uzbl + self.uzbl.got_socket(self) + self._feed("") + + def send(self, data): + '''Child socket send function.''' + + self._socket.send(data + "\n") + + def close(self): + '''Close the connection''' + + if self._socket: + self._socket.close() + self._socket = None + map(source_remove, self._watchers) + self._watchers = [] + + +class UzblInstance: + '''Uzbl instance meta-data/meta-action object.''' + + def __init__(self, parent, tab, name, uri, title, switch): + + self.parent = parent + self.tab = tab + self.name = name + self.title = title + self.tabtitle = "" + self.uri = uri + self._client = None + self._switch = switch # Switch to tab after loading ? + self.title_changed() + + + def got_socket(self, client): + '''Uzbl instance is now connected''' + + self._client = client + self.parent.config_uzbl(self) + if self._switch: + tabid = self.parent.notebook.page_num(self.tab) + self.parent.goto_tab(tabid) + + + def title_changed(self, gtk_only = True): # GTK-only is for indexes + '''self.title has changed, update the tabs list''' + + tab_titles = config['tab_titles'] + tab_indexes = config['tab_indexes'] + show_ellipsis = config['show_ellipsis'] + max_title_len = config['max_title_len'] + + # Unicode heavy strings do not like being truncated/sliced so by + # re-encoding the string sliced of limbs are removed. + self.tabtitle = self.title[:max_title_len + int(show_ellipsis)] + if type(self.tabtitle) != types.UnicodeType: + self.tabtitle = unicode(self.tabtitle, 'utf-8', 'ignore') + + self.tabtitle = self.tabtitle.encode('utf-8', 'ignore').strip() + + if show_ellipsis and len(self.tabtitle) != len(self.title): + self.tabtitle += "\xe2\x80\xa6" + + gtk_tab_format = "%d %s" + index = self.parent.notebook.page_num(self.tab) + if tab_titles and tab_indexes: + self.parent.notebook.set_tab_label_text(self.tab, + gtk_tab_format % (index, self.tabtitle)) + elif tab_titles: + self.parent.notebook.set_tab_label_text(self.tab, self.tabtitle) + else: + self.parent.notebook.set_tab_label_text(self.tab, str(index)) + + # If instance is current tab, update window title + if index == self.parent.notebook.get_current_page(): + title_format = "%s - Uzbl Browser" + self.parent.window.set_title(title_format % self.title) + + # Non-GTK tabs + if not gtk_only: + self.parent.update_tablist() + + + def set(self, key, val): + ''' Send the SET command to Uzbl ''' + + if self._client: + self._client.send('set %s = %s') #TODO: escape chars ? + + + def exit(self): + ''' Ask the Uzbl instance to close ''' + + if self._client: + self._client.send('exit') + + + def parse_command(self, cmd): + ''' Parse event givent by the Uzbl instance ''' + + type, _, args = cmd.split(" ", 2) + if type == "EVENT": + type, args = args.split(" ", 1) + if type == "TITLE_CHANGED": + self.title = args + self.title_changed() + elif type == "VARIABLE_SET": + var, _, val = args.split(" ", 2) + try: + val = int(val) + except: + pass + + if var in UZBL_TABBED_VARS: + if config[var] != val: + config[var] = val + if var == "show_gtk_tabs": + self.parent.notebook.set_show_tabs(bool(val)) + elif var == "show_tablist" or var == "tablist_top": + self.parent.update_tablist_display() + elif var == "gtk_tab_pos": + self.parent.update_gtk_tab_pos() + elif var == "status_background": + col = gtk.gdk.color_parse(config['status_background']) + self.parent.ebox.modify_bg(gtk.STATE_NORMAL, col) + elif var == "tab_titles" or var == "tab_indexes": + for tab in self.parent.notebook: + self.parent.tabs[tab].title_changed(True) + + self.parent.update_tablist() + else: + config[var] = val + + if var == "uri": + self.uri = var + self.parent.update_tablist() + elif type == "NEW_TAB": + self.parent.new_tab(args) + elif type == "NEXT_TAB": + if args: + self.parent.next_tab(int(args)) + else: + self.parent.next_tab() + elif type == "PREV_TAB": + if args: + self.parent.prev_tab(int(args)) + else: + self.parent.prev_tab() + elif type == "GOTO_TAB": + self.parent.goto_tab(int(args)) + elif type == "FIRST_TAB": + self.parent.goto_tab(0) + elif type == "LAST_TAB": + self.parent.goto_tab(-1) + elif type == "PRESET_TABS": + self.parent.parse_command(["preset"] + args.split()) + elif type == "BRING_TO_FRONT": + self.parent.window.present() + elif type == "CLEAN_TABS": + self.parent.clean_slate() + elif type == "EXIT_ALL_TABS": + self.parent.quitrequest() + + + def close(self): + '''The remote instance exited''' + + if self._client: + self._client.close() + self._client = None + + +class UzblTabbed: + '''A tabbed version of uzbl using gtk.Notebook''' + + def __init__(self): + '''Create tablist, window and notebook.''' + + self._timers = {} + self._buffer = "" + self._killed = False + + # A list of the recently closed tabs + self._closed = [] + + # Holds metadata on the uzbl childen open. + self.tabs = {} + + # Uzbl sockets (socket => SocketClient) + self.clients = {} + + # Generates a unique id for uzbl socket filenames. + self.next_pid = counter().next + + # Create main window + self.window = gtk.Window() + try: + window_size = map(int, config['window_size'].split(',')) + self.window.set_default_size(*window_size) + + except: + error("Invalid value for default_size in config file.") + + self.window.set_title("Uzbl Browser") + self.window.set_border_width(0) + + # Set main window icon + icon_path = config['icon_path'] + if os.path.exists(icon_path): + self.window.set_icon(gtk.gdk.pixbuf_new_from_file(icon_path)) + + else: + icon_path = '/usr/share/uzbl/examples/data/uzbl.png' + if os.path.exists(icon_path): + self.window.set_icon(gtk.gdk.pixbuf_new_from_file(icon_path)) + + # Attach main window event handlers + self.window.connect("delete-event", self.quitrequest) + + # Create tab list + vbox = gtk.VBox() + self.vbox = vbox + self.window.add(vbox) + ebox = gtk.EventBox() + self.ebox = ebox + self.tablist = gtk.Label() + + self.tablist.set_use_markup(True) + self.tablist.set_justify(gtk.JUSTIFY_LEFT) + self.tablist.set_line_wrap(False) + self.tablist.set_selectable(False) + self.tablist.set_padding(2,2) + self.tablist.set_alignment(0,0) + self.tablist.set_ellipsize(pango.ELLIPSIZE_END) + self.tablist.set_text(" ") + self.tablist.show() + ebox.add(self.tablist) + ebox.show() + bgcolor = gtk.gdk.color_parse(config['status_background']) + ebox.modify_bg(gtk.STATE_NORMAL, bgcolor) + + # Create notebook + self.notebook = gtk.Notebook() + self.notebook.set_show_tabs(config['show_gtk_tabs']) + + # Set tab position + self.update_gtk_tab_pos() + + self.notebook.set_show_border(False) + self.notebook.set_scrollable(True) + self.notebook.set_border_width(0) + + self.notebook.connect("page-removed", self.tab_closed) + self.notebook.connect("switch-page", self.tab_changed) + self.notebook.connect("page-added", self.tab_opened) + + self.notebook.show() + vbox.pack_start(self.notebook, True, True, 0) + vbox.reorder_child(self.notebook, 1) + self.update_tablist_display() + + self.vbox.show() + self.window.show() + self.wid = self.notebook.window.xid + + # Store information about the applications fifo and socket. + fifo_filename = 'uzbltabbed_%d.fifo' % os.getpid() + socket_filename = 'uzbltabbed_%d.socket' % os.getpid() + self._fifo = None + self._socket = None + self.fifo_path = os.path.join(config['fifo_dir'], fifo_filename) + self.socket_path = os.path.join(config['socket_dir'], socket_filename) + + # Now initialise the fifo and the socket + self.init_fifo() + self.init_socket() + + # If we are using sessions then load the last one if it exists. + if config['save_session']: + self.load_session() + + + def run(self): + '''UzblTabbed main function that calls the gtk loop.''' + + if not self.clients and not SocketClient.instances_queue and not self.tabs: + self.new_tab() + + gtk_refresh = int(config['gtk_refresh']) + if gtk_refresh < 100: + gtk_refresh = 100 + + # Make SIGTERM act orderly. + signal(SIGTERM, lambda signum, stack_frame: self.terminate(SIGTERM)) + + # Catch keyboard interrupts + signal(SIGINT, lambda signum, stack_frame: self.terminate(SIGINT)) + + try: + gtk.main() + + except: + error("encounted error %r" % sys.exc_info()[1]) + + # Unlink fifo socket + self.unlink_fifo() + self.close_socket() + + # Attempt to close all uzbl instances nicely. + self.quitrequest() + + # Allow time for all the uzbl instances to quit. + time.sleep(1) + + raise + + + def terminate(self, termsig=None): + '''Handle termination signals and exit safely and cleanly.''' + + # Not required but at least it lets the user know what killed his + # browsing session. + if termsig == SIGTERM: + error("caught SIGTERM signal") + + elif termsig == SIGINT: + error("caught keyboard interrupt") + + else: + error("caught unknown signal") + + error("commencing infanticide!") + + # Sends the exit signal to all uzbl instances. + self.quitrequest() + + + def init_socket(self): + '''Create interprocess communication socket.''' + + def accept(sock, condition): + '''A new uzbl instance was created''' + + client, _ = sock.accept() + self.clients[client] = SocketClient(client) + + return True + + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.bind(self.socket_path) + sock.listen(1) + + # Add event handler for IO_IN event. + self._socket = (sock, io_add_watch(sock, IO_IN, accept)) + + echo("[socket] listening at %r" % self.socket_path) + + # Add atexit register to destroy the socket on program termination. + atexit.register(self.close_socket) + + + def close_socket(self): + '''Close the socket when closing the application''' + + if self._socket: + (fd, watcher) = self._socket + source_remove(watcher) + fd.close() + os.unlink(self.socket_path) + self._socket = None + + + def init_fifo(self): + '''Create interprocess communication fifo.''' + + if os.path.exists(self.fifo_path): + if not os.access(self.fifo_path, os.F_OK | os.R_OK | os.W_OK): + os.mkfifo(self.fifo_path) + + else: + basedir = os.path.dirname(self.fifo_path) + if not os.path.exists(basedir): + os.makedirs(basedir) + + os.mkfifo(self.fifo_path) + + # Add event handlers for IO_IN & IO_HUP events. + self.setup_fifo_watchers() + + echo("[fifo] listening at %r" % self.fifo_path) + + # Add atexit register to destroy the fifo on program termination. + atexit.register(self.unlink_fifo) + + + def unlink_fifo(self): + '''Unlink the fifo socket. Note: This function is called automatically + on exit by an atexit register.''' + + # Make sure the fifo fd is closed. + self.close_fifo() + + # And unlink if the real fifo exists. + if os.path.exists(self.fifo_path): + os.unlink(self.fifo_path) + echo("unlinked %r" % self.fifo_path) + + + def close_fifo(self): + '''Remove all event handlers watching the fifo and close the fd.''' + + # Already closed + if self._fifo is None: return + + (fd, watchers) = self._fifo + os.close(fd) + + # Stop all gobject io watchers watching the fifo. + for gid in watchers: + source_remove(gid) + + self._fifo = None + + + def setup_fifo_watchers(self): + '''Open fifo socket fd and setup gobject IO_IN & IO_HUP event + handlers.''' + + # Close currently open fifo fd and kill all watchers + self.close_fifo() + + fd = os.open(self.fifo_path, os.O_RDONLY | os.O_NONBLOCK) + + # Add gobject io event handlers to the fifo socket. + watchers = [io_add_watch(fd, IO_IN, self.main_fifo_read),\ + io_add_watch(fd, IO_HUP, self.main_fifo_hangup)] + + self._fifo = (fd, watchers) + + + def main_fifo_hangup(self, fd, cb_condition): + '''Handle main fifo socket hangups.''' + + # Close old fd, open new fifo socket and add io event handlers. + self.setup_fifo_watchers() + + # Kill the gobject event handler calling this handler function. + return False + + + def main_fifo_read(self, fd, cb_condition): + '''Read from main fifo socket.''' + + self._buffer = os.read(fd, 1024) + temp = self._buffer.split("\n") + self._buffer = temp.pop() + cmds = [s.strip().split() for s in temp if len(s.strip())] + + for cmd in cmds: + try: + #print cmd + self.parse_command(cmd) + + except: + error("parse_command: invalid command %s" % ' '.join(cmd)) + raise + + return True + + + def parse_command(self, cmd): + '''Parse instructions from uzbl child processes.''' + + # Commands ( [] = optional, {} = required ) + # new [uri] + # open new tab and head to optional uri. + # close [tab-num] + # close current tab or close via tab id. + # next [n-tabs] + # open next tab or n tabs down. Supports negative indexing. + # prev [n-tabs] + # open prev tab or n tabs down. Supports negative indexing. + # goto {tab-n} + # goto tab n. + # first + # goto first tab. + # last + # goto last tab. + # title {pid} {document-title} + # updates tablist title. + # uri {pid} {document-location} + # updates tablist uri + # bring_to_front + # brings the gtk window to focus. + # exit + # exits uzbl_tabbed.py + + if cmd[0] == "new": + if len(cmd) == 2: + self.new_tab(cmd[1]) + + else: + self.new_tab() + + elif cmd[0] == "newfromclip": + uri = subprocess.Popen(['xclip','-selection','clipboard','-o'],\ + stdout=subprocess.PIPE).communicate()[0] + if uri: + self.new_tab(uri) + + elif cmd[0] == "close": + if len(cmd) == 2: + self.close_tab(int(cmd[1])) + + else: + self.close_tab() + + elif cmd[0] == "next": + if len(cmd) == 2: + self.next_tab(int(cmd[1])) + + else: + self.next_tab() + + elif cmd[0] == "prev": + if len(cmd) == 2: + self.prev_tab(int(cmd[1])) + + else: + self.prev_tab() + + elif cmd[0] == "goto": + self.goto_tab(int(cmd[1])) + + elif cmd[0] == "first": + self.goto_tab(0) + + elif cmd[0] == "last": + self.goto_tab(-1) + + elif cmd[0] in ["title", "uri"]: + if len(cmd) > 2: + uzbl = self.get_tab_by_name(int(cmd[1])) + if uzbl: + old = getattr(uzbl, cmd[0]) + new = ' '.join(cmd[2:]) + setattr(uzbl, cmd[0], new) + if old != new: + self.update_tablist() + + else: + error("parse_command: no uzbl with name %r" % int(cmd[1])) + + elif cmd[0] == "preset": + if len(cmd) < 3: + error("parse_command: invalid preset command") + + elif cmd[1] == "save": + path = os.path.join(config['saved_sessions_dir'], cmd[2]) + self.save_session(path) + + elif cmd[1] == "load": + path = os.path.join(config['saved_sessions_dir'], cmd[2]) + self.load_session(path) + + elif cmd[1] == "del": + path = os.path.join(config['saved_sessions_dir'], cmd[2]) + if os.path.isfile(path): + os.remove(path) + + else: + error("parse_command: preset %r does not exist." % path) + + elif cmd[1] == "list": + uzbl = self.get_tab_by_name(int(cmd[2])) + if uzbl: + if not os.path.isdir(config['saved_sessions_dir']): + js = "js alert('No saved presets.');" + uzbl._client.send(js) + + else: + listdir = os.listdir(config['saved_sessions_dir']) + listdir = "\\n".join(listdir) + js = "js alert('Session presets:\\n\\n%s');" % listdir + uzbl._client.send(js) + + else: + error("parse_command: unknown tab name.") + + else: + error("parse_command: unknown parse command %r"\ + % ' '.join(cmd)) + + elif cmd[0] == "bring_to_front": + self.window.present() + + elif cmd[0] == "clean": + self.clean_slate() + + elif cmd[0] == "exit": + self.quitrequest() + + else: + error("parse_command: unknown command %r" % ' '.join(cmd)) + + + def get_tab_by_name(self, name): + '''Return uzbl instance by name.''' + + for (tab, uzbl) in self.tabs.items(): + if uzbl.name == name: + return uzbl + + return False + + + def new_tab(self, uri='', title='', switch=None): + '''Add a new tab to the notebook and start a new instance of uzbl. + Use the switch option to negate config['switch_to_new_tabs'] option + when you need to load multiple tabs at a time (I.e. like when + restoring a session from a file).''' + + tab = gtk.Socket() + tab.show() + self.notebook.append_page(tab) + sid = tab.get_id() + uri = uri.strip() + name = "%d-%d" % (os.getpid(), self.next_pid()) + + if switch is None: + switch = config['switch_to_new_tabs'] + + if not title: + title = config['new_tab_title'] + + cmd = ['uzbl-browser', '-n', name, '-s', str(sid), + '--connect-socket', self.socket_path, '--uri', uri] + subprocess.Popen(cmd) # TODO: do i need close_fds=True ? + + uzbl = UzblInstance(self, tab, name, uri, title, switch) + SocketClient.instances_queue[name] = uzbl + self.tabs[tab] = uzbl + + + def clean_slate(self): + '''Close all open tabs and open a fresh brand new one.''' + + self.new_tab() + tabs = self.tabs.keys() + for tab in list(self.notebook)[:-1]: + if tab not in tabs: continue + uzbl = self.tabs[tab] + uzbl.exit() + + + def config_uzbl(self, uzbl): + '''Send bind commands for tab new/close/next/prev to a uzbl + instance.''' + + # Set definitions here + # set(key, command back to fifo) + if config['capture_new_windows']: + uzbl.set("new_window", r'new $8') + + + def goto_tab(self, index): + '''Goto tab n (supports negative indexing).''' + + title_format = "%s - Uzbl Browser" + + tabs = list(self.notebook) + if 0 <= index < len(tabs): + self.notebook.set_current_page(index) + uzbl = self.tabs[self.notebook.get_nth_page(index)] + self.window.set_title(title_format % uzbl.title) + self.update_tablist() + return None + + try: + tab = tabs[index] + # Update index because index might have previously been a + # negative index. + index = tabs.index(tab) + self.notebook.set_current_page(index) + uzbl = self.tabs[self.notebook.get_nth_page(index)] + self.window.set_title(title_format % uzbl.title) + self.update_tablist() + + except IndexError: + pass + + + def next_tab(self, step=1): + '''Switch to next tab or n tabs right.''' + + if step < 1: + error("next_tab: invalid step %r" % step) + return None + + ntabs = self.notebook.get_n_pages() + tabn = (self.notebook.get_current_page() + step) % ntabs + self.goto_tab(tabn) + + + def prev_tab(self, step=1): + '''Switch to prev tab or n tabs left.''' + + if step < 1: + error("prev_tab: invalid step %r" % step) + return None + + ntabs = self.notebook.get_n_pages() + tabn = self.notebook.get_current_page() - step + while tabn < 0: tabn += ntabs + self.goto_tab(tabn) + + + def close_tab(self, tabn=None): + '''Closes current tab. Supports negative indexing.''' + + if tabn is None: + tabn = self.notebook.get_current_page() + + else: + try: + tab = list(self.notebook)[tabn] + + except IndexError: + error("close_tab: invalid index %r" % tabn) + return None + + self.notebook.remove_page(tabn) + + + def tab_opened(self, notebook, tab, index): + '''Called upon tab creation. Called by page-added signal.''' + + if config['switch_to_new_tabs']: + self.notebook.set_focus_child(tab) + + else: + oldindex = self.notebook.get_current_page() + oldtab = self.notebook.get_nth_page(oldindex) + self.notebook.set_focus_child(oldtab) + + + def tab_closed(self, notebook, tab, index): + '''Close the window if no tabs are left. Called by page-removed + signal.''' + + if tab in self.tabs.keys(): + uzbl = self.tabs[tab] + uzbl.close() + + self._closed.append((uzbl.uri, uzbl.title)) + self._closed = self._closed[-10:] + del self.tabs[tab] + + if self.notebook.get_n_pages() == 0: + if not self._killed and config['save_session']: + if os.path.exists(config['session_file']): + os.remove(config['session_file']) + + self.quit() + + for tab in self.notebook: + self.tabs[tab].title_changed(True) + self.update_tablist() + + return True + + + def tab_changed(self, notebook, page, index): + '''Refresh tab list. Called by switch-page signal.''' + + tab = self.notebook.get_nth_page(index) + self.notebook.set_focus_child(tab) + self.update_tablist(index) + return True + + + def update_tablist_display(self): + '''Called when show_tablist or tablist_top has changed''' + + if self.ebox in self.vbox.get_children(): + self.vbox.remove(self.ebox) + + if config['show_tablist']: + self.vbox.pack_start(self.ebox, False, False, 0) + if config['tablist_top']: + self.vbox.reorder_child(self.ebox, 0) + else: + self.vbox.reorder_child(self.ebox, 2) + + def update_gtk_tab_pos(self): + ''' Called when gtk_tab_pos has changed ''' + + allposes = {'left': gtk.POS_LEFT, 'right':gtk.POS_RIGHT, + 'top':gtk.POS_TOP, 'bottom':gtk.POS_BOTTOM} + if config['gtk_tab_pos'] in allposes.keys(): + self.notebook.set_tab_pos(allposes[config['gtk_tab_pos']]) + + + def update_tablist(self, curpage=None): + '''Upate tablist status bar.''' + + if not config['show_tablist']: + return True + + tab_titles = config['tab_titles'] + tab_indexes = config['tab_indexes'] + multiline_tabs = config['multiline_tabs'] + + if multiline_tabs: + multiline = [] + + tabs = self.tabs.keys() + if curpage is None: + curpage = self.notebook.get_current_page() + + pango = "" + normal = (config['tab_colours'], config['tab_text_colours']) + selected = (config['selected_tab'], config['selected_tab_text']) + + if tab_titles and tab_indexes: + tab_format = " [ %(index)d %(title)s ] " + elif tab_titles: + tab_format = " [ %(title)s ] " + else: + tab_format = " [ %(index)d ] " + + for index, tab in enumerate(self.notebook): + if tab not in tabs: continue + uzbl = self.tabs[tab] + title = escape(uzbl.tabtitle) + + style = colour_selector(index, curpage, uzbl) + (tabc, textc) = style + + if multiline_tabs: + opango = pango + + pango += tab_format % locals() + + self.tablist.set_markup(pango) + listwidth = self.tablist.get_layout().get_pixel_size()[0] + winwidth = self.window.get_size()[0] + + if listwidth > (winwidth - 20): + multiline.append(opango) + pango = tab_format % locals() + else: + pango += tab_format % locals() + + if multiline_tabs: + multiline.append(pango) + self.tablist.set_markup(' '.join(multiline)) + + else: + self.tablist.set_markup(pango) + + return True + + + def save_session(self, session_file=None): + '''Save the current session to file for restoration on next load.''' + + strip = str.strip + + if session_file is None: + session_file = config['session_file'] + + tabs = self.tabs.keys() + state = [] + for tab in list(self.notebook): + if tab not in tabs: continue + uzbl = self.tabs[tab] + if not uzbl.uri: continue + state += [(uzbl.uri, uzbl.title),] + + session = {'curtab': self.notebook.get_current_page(), + 'tabs': state} + + if config['json_session']: + raw = json.dumps(session) + + else: + lines = ["curtab = %d" % session['curtab'],] + for (uri, title) in session['tabs']: + lines += ["%s\t%s" % (strip(uri), strip(title)),] + + raw = "\n".join(lines) + + if not os.path.isfile(session_file): + dirname = os.path.dirname(session_file) + if not os.path.isdir(dirname): + os.makedirs(dirname) + + h = open(session_file, 'w') + h.write(raw) + h.close() + + + def load_session(self, session_file=None): + '''Load a saved session from file.''' + + default_path = False + strip = str.strip + json_session = config['json_session'] + delete_loaded = False + + if session_file is None: + default_path = True + delete_loaded = True + session_file = config['session_file'] + + if not os.path.isfile(session_file): + return False + + h = open(session_file, 'r') + raw = h.read() + h.close() + if json_session: + if sum([1 for s in raw.split("\n") if strip(s)]) != 1: + error("Warning: The session file %r does not look json. "\ + "Trying to load it as a non-json session file."\ + % session_file) + json_session = False + + if json_session: + try: + session = json.loads(raw) + curtab, tabs = session['curtab'], session['tabs'] + + except: + error("Failed to load jsonifed session from %r"\ + % session_file) + return None + + else: + tabs = [] + strip = str.strip + curtab, tabs = 0, [] + lines = [s for s in raw.split("\n") if strip(s)] + if len(lines) < 2: + error("Warning: The non-json session file %r looks invalid."\ + % session_file) + return None + + try: + for line in lines: + if line.startswith("curtab"): + curtab = int(line.split()[-1]) + + else: + uri, title = line.split("\t",1) + tabs += [(strip(uri), strip(title)),] + + except: + error("Warning: failed to load session file %r" % session_file) + return None + + session = {'curtab': curtab, 'tabs': tabs} + + # Now populate notebook with the loaded session. + for (index, (uri, title)) in enumerate(tabs): + self.new_tab(uri=uri, title=title, switch=(curtab==index)) + + # A saved session has been loaded now delete it. + if delete_loaded and os.path.exists(session_file): + os.remove(session_file) + + # There may be other state information in the session dict of use to + # other functions. Of course however the non-json session object is + # just a dummy object of no use to no one. + return session + + + def quitrequest(self, *args): + '''Attempt to close all uzbl instances nicely and exit.''' + + self._killed = True + + if config['save_session']: + if len(list(self.notebook)) > 1: + self.save_session() + + else: + # Notebook has one page open so delete the session file. + if os.path.isfile(config['session_file']): + os.remove(config['session_file']) + + for (tab, uzbl) in self.tabs.items(): + uzbl.exit() + + # Add a gobject timer to make sure the application force-quits after a + # reasonable period. Calling quit when all the tabs haven't had time to + # close should be a last resort. + timer = "force-quit" + timerid = timeout_add(5000, self.quit, timer) + self._timers[timer] = timerid + + + def quit(self, *args): + '''Cleanup and quit. Called by delete-event signal.''' + + # Close the fifo socket, remove any gobject io event handlers and + # delete socket. + self.unlink_fifo() + self.close_socket() + + # Remove all gobject timers that are still ticking. + for (timerid, gid) in self._timers.items(): + source_remove(gid) + del self._timers[timerid] + + try: + gtk.main_quit() + + except: + pass + + +if __name__ == "__main__": + + # Build command line parser + usage = "usage: %prog [OPTIONS] {URIS}..." + parser = OptionParser(usage=usage) + parser.add_option('-n', '--no-session', dest='nosession', + action='store_true', help="ignore session saving a loading.") + parser.add_option('-v', '--verbose', dest='verbose', + action='store_true', help='print verbose output.') + + # Parse command line options + (options, uris) = parser.parse_args() + + if options.nosession: + config['save_session'] = False + + if options.verbose: + config['verbose'] = True + + if config['json_session']: + try: + import simplejson as json + + except: + error("Warning: json_session set but cannot import the python "\ + "module simplejson. Fix: \"set json_session = 0\" or "\ + "install the simplejson python module to remove this warning.") + config['json_session'] = False + + if config['verbose']: + import pprint + sys.stderr.write("%s\n" % pprint.pformat(config)) + + uzbl = UzblTabbed() + + # All extra arguments given to uzbl_tabbed.py are interpreted as + # web-locations to opened in new tabs. + lasturi = len(uris)-1 + for (index,uri) in enumerate(uris): + uzbl.new_tab(uri, switch=(index==lasturi)) + + uzbl.run() diff --git a/examples/data/scripts/uzblcat b/examples/data/scripts/uzblcat new file mode 100755 index 0000000..e955608 --- /dev/null +++ b/examples/data/scripts/uzblcat @@ -0,0 +1,12 @@ +#!/usr/bin/env python +# uzblcat - safely push html to uzbl +# See http://www.uzbl.org/wiki/html-mode + +from sys import stdin, stdout + +stdout.write("uri data:text/html,") +for line in stdin: + stdout.write(line[0:-1]) + +# vim: set noet ff=unix + -- cgit v1.2.3