From fff9c1bbfa6b120c6fd349f95b0e74c618ebb6ea Mon Sep 17 00:00:00 2001 From: ben Date: Mon, 4 Jan 2010 21:04:41 +0100 Subject: add uzbl configuration --- .config/uzbl/scripts/uzbl_tabbed.py | 1474 +++++++++++++++++++++++++++++++++++ 1 file changed, 1474 insertions(+) create mode 100755 .config/uzbl/scripts/uzbl_tabbed.py (limited to '.config/uzbl/scripts/uzbl_tabbed.py') diff --git a/.config/uzbl/scripts/uzbl_tabbed.py b/.config/uzbl/scripts/uzbl_tabbed.py new file mode 100755 index 0000000..cd5ef4f --- /dev/null +++ b/.config/uzbl/scripts/uzbl_tabbed.py @@ -0,0 +1,1474 @@ +#!/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. + + +# 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 +# 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 the key bindings: +# bind_new_tab = gn +# bind_tab_from_clip = gY +# bind_tab_from_uri = go _ +# bind_close_tab = gC +# bind_next_tab = gt +# bind_prev_tab = gT +# bind_goto_tab = gi_ +# bind_goto_first = g< +# bind_goto_last = g> +# bind_clean_slate = gQ +# bind_exit = gZ +# +# Session preset key bindings: +# bind_save_preset = gsave _ +# bind_load_preset = gload _ +# bind_del_preset = gdel _ +# bind_list_presets = glist +# +# 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/') +CONFIG_DIR = os.path.join(xdghome('CONFIG', '.config/'), 'uzbl/') + +# Ensure uzbl xdg paths exist +for path in [DATA_DIR, CONFIG_DIR]: + if not os.path.exists(path): + os.makedirs(path) + +# Path to uzbl config +UZBL_CONFIG = os.path.join(CONFIG_DIR, 'config') +if not os.path.exists(UZBL_CONFIG): + error("cannot find uzbl config file at %r" % UZBL_CONFIG) + sys.exit(1) + +# 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) + '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. + + # Key bindings + 'bind_new_tab': 'gn', # Open new tab. + 'bind_tab_from_clip': 'gY', # Open tab from clipboard. + 'bind_tab_from_uri': 'go _', # Open new tab and goto entered uri. + 'bind_close_tab': 'gC', # Close tab. + 'bind_next_tab': 'gt', # Next tab. + 'bind_prev_tab': 'gT', # Prev tab. + 'bind_goto_tab': 'gi_', # Goto tab by tab-number (in title). + 'bind_goto_first': 'g<', # Goto first tab. + 'bind_goto_last': 'g>', # Goto last tab. + 'bind_clean_slate': 'gQ', # Close all tabs and open new tab. + 'bind_exit': 'gZ', # Exit nicely. + + # Session preset key bindings + 'bind_save_preset': 'gsave _', # Save session to file %s. + 'bind_load_preset': 'gload _', # Load preset session from file %s. + 'bind_del_preset': 'gdel _', # Delete preset session %s. + 'bind_list_presets': 'glist', # List all session presets. + + # 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. + +# 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 readconfig(uzbl_config, config): + '''Loads relevant config from the users uzbl config file into the global + config dictionary.''' + + if not os.path.exists(uzbl_config): + error("Unable to load config %r" % uzbl_config) + return None + + # Define parsing regular expressions + isint = re.compile("^(\-|)[0-9]+$").match + findsets = re.compile("^set\s+([^\=]+)\s*\=\s*(.+)$",\ + re.MULTILINE).findall + + h = open(os.path.expandvars(uzbl_config), 'r') + rawconfig = h.read() + h.close() + + configkeys, strip = config.keys(), str.strip + for (key, value) in findsets(rawconfig): + key, value = strip(key), strip(value) + if key not in configkeys: continue + if isint(value): value = int(value) + config[key] = value + + # Ensure that config keys that relate to paths are expanded. + pathkeys = ['fifo_dir', 'socket_dir', 'session_file', 'icon_path', + 'saved_sessions_dir'] + for key in pathkeys: + config[key] = os.path.expandvars(config[key]) + + +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 + + +def gen_endmarker(): + '''Generates a random md5 for socket message-termination endmarkers.''' + + return hashlib.md5(str(random.random()*time.time())).hexdigest() + + +class UzblTabbed: + '''A tabbed version of uzbl using gtk.Notebook''' + + class UzblInstance: + '''Uzbl instance meta-data/meta-action object.''' + + def __init__(self, parent, tab, fifo_socket, socket_file, pid,\ + uri, title, switch): + + self.parent = parent + self.tab = tab + self.fifo_socket = fifo_socket + self.socket_file = socket_file + self.pid = pid + self.title = title + self.uri = uri + self.timers = {} + self._lastprobe = 0 + self._fifoout = [] + self._socketout = [] + self._socket = None + self._buffer = "" + # Switch to tab after loading + self._switch = switch + # fifo/socket files exists and socket connected. + self._connected = False + # The kill switch + self._kill = False + + # Message termination endmarker. + self._marker = gen_endmarker() + + # Gen probe commands string + probes = [] + probe = probes.append + probe('print uri %d @uri %s' % (self.pid, self._marker)) + probe('print title %d @@ %s' % (self.pid,\ + self._marker)) + self._probecmds = '\n'.join(probes) + + # Enqueue keybinding config for child uzbl instance + self.parent.config_uzbl(self) + + + def flush(self, timer_call=False): + '''Flush messages from the socket-out and fifo-out queues.''' + + if self._kill: + if self._socket: + self._socket.close() + self._socket = None + + error("Flush called on dead tab.") + return False + + if len(self._fifoout): + if os.path.exists(self.fifo_socket): + h = open(self.fifo_socket, 'w') + while len(self._fifoout): + msg = self._fifoout.pop(0) + h.write("%s\n"%msg) + h.close() + + if len(self._socketout): + if not self._socket and os.path.exists(self.socket_file): + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.connect(self.socket_file) + self._socket = sock + + if self._socket: + while len(self._socketout): + msg = self._socketout.pop(0) + self._socket.send("%s\n"%msg) + + if not self._connected and timer_call: + if not len(self._fifoout + self._socketout): + self._connected = True + + if timer_call in self.timers.keys(): + source_remove(self.timers[timer_call]) + del self.timers[timer_call] + + if self._switch: + self.grabfocus() + + return len(self._fifoout + self._socketout) + + + def grabfocus(self): + '''Steal parent focus and switch the notebook to my own tab.''' + + tabs = list(self.parent.notebook) + tabid = tabs.index(self.tab) + self.parent.goto_tab(tabid) + + + def probe(self): + '''Probes the client for information about its self.''' + + if self._connected: + self.send(self._probecmds) + self._lastprobe = time.time() + + + def write(self, msg): + '''Child fifo write function.''' + + self._fifoout.append(msg) + # Flush messages from the queue if able. + return self.flush() + + + def send(self, msg): + '''Child socket send function.''' + + self._socketout.append(msg) + # Flush messages from queue if able. + return self.flush() + + + def __init__(self): + '''Create tablist, window and notebook.''' + + # Store information about the applications fifo_socket. + self._fifo = None + + 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 = {} + + # 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/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 + if config['show_tablist']: + vbox = gtk.VBox() + self.window.add(vbox) + ebox = gtk.EventBox() + 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 + 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']]) + + 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() + if config['show_tablist']: + if config['tablist_top']: + vbox.pack_start(ebox, False, False, 0) + vbox.pack_end(self.notebook, True, True, 0) + + else: + vbox.pack_start(self.notebook, True, True, 0) + vbox.pack_end(ebox, False, False, 0) + + vbox.show() + + else: + self.window.add(self.notebook) + + self.window.show() + self.wid = self.notebook.window.xid + # Generate the fifo socket filename. + fifo_filename = 'uzbltabbed_%d' % os.getpid() + self.fifo_socket = os.path.join(config['fifo_dir'], fifo_filename) + + # Now initialise the fifo socket at self.fifo_socket + self.init_fifo_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 len(self.tabs): + self.new_tab() + + gtk_refresh = int(config['gtk_refresh']) + if gtk_refresh < 100: + gtk_refresh = 100 + + # Update tablist timer + timerid = timeout_add(gtk_refresh, self.update_tablist) + self._timers["update-tablist"] = timerid + + # Probe clients every second for window titles and location + timerid = timeout_add(gtk_refresh, self.probe_clients) + self._timers["probe-clients"] = timerid + + # 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_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_fifo_socket(self): + '''Create interprocess communication fifo socket.''' + + if os.path.exists(self.fifo_socket): + if not os.access(self.fifo_socket, os.F_OK | os.R_OK | os.W_OK): + os.mkfifo(self.fifo_socket) + + else: + basedir = os.path.dirname(self.fifo_socket) + if not os.path.exists(basedir): + os.makedirs(basedir) + + os.mkfifo(self.fifo_socket) + + # Add event handlers for IO_IN & IO_HUP events. + self.setup_fifo_watchers() + + echo("listening at %r" % self.fifo_socket) + + # Add atexit register to destroy the socket on program termination. + atexit.register(self.unlink_fifo_socket) + + + def unlink_fifo_socket(self): + '''Unlink the fifo socket. Note: This function is called automatically + on exit by an atexit register.''' + + # Make sure the fifo_socket fd is closed. + self.close_fifo() + + # And unlink if the real fifo_socket exists. + if os.path.exists(self.fifo_socket): + os.unlink(self.fifo_socket) + echo("unlinked %r" % self.fifo_socket) + + + 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_socket fd and kill all watchers + self.close_fifo() + + fd = os.open(self.fifo_socket, 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 probe_clients(self): + '''Probe all uzbl clients for up-to-date window titles and uri's.''' + + save_session = config['save_session'] + + sockd = {} + tabskeys = self.tabs.keys() + notebooklist = list(self.notebook) + + for tab in notebooklist: + if tab not in tabskeys: continue + uzbl = self.tabs[tab] + uzbl.probe() + if uzbl._socket: + sockd[uzbl._socket] = uzbl + + sockets = sockd.keys() + (reading, _, errors) = select.select(sockets, [], sockets, 0) + + for sock in reading: + uzbl = sockd[sock] + uzbl._buffer = sock.recv(1024).replace('\n',' ') + temp = uzbl._buffer.split(uzbl._marker) + 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_pid(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 pid %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_pid(int(cmd[2])) + if uzbl: + if not os.path.isdir(config['saved_sessions_dir']): + js = "js alert('No saved presets.');" + uzbl.send(js) + + else: + listdir = os.listdir(config['saved_sessions_dir']) + listdir = "\\n".join(listdir) + js = "js alert('Session presets:\\n\\n%s');" % listdir + uzbl.send(js) + + else: + error("parse_command: unknown tab pid.") + + 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_pid(self, pid): + '''Return uzbl instance by pid.''' + + for (tab, uzbl) in self.tabs.items(): + if uzbl.pid == pid: + 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).''' + + pid = self.next_pid() + tab = gtk.Socket() + tab.show() + self.notebook.append_page(tab) + sid = tab.get_id() + uri = uri.strip() + + fifo_filename = 'uzbl_fifo_%s_%0.2d' % (self.wid, pid) + fifo_socket = os.path.join(config['fifo_dir'], fifo_filename) + socket_filename = 'uzbl_socket_%s_%0.2d' % (self.wid, pid) + socket_file = os.path.join(config['socket_dir'], socket_filename) + + if switch is None: + switch = config['switch_to_new_tabs'] + + if not title: + title = config['new_tab_title'] + + uzbl = self.UzblInstance(self, tab, fifo_socket, socket_file, pid,\ + uri, title, switch) + + if len(uri): + uri = "--uri %r" % uri + + self.tabs[tab] = uzbl + cmd = 'uzbl -s %s -n %s_%0.2d %s &' % (sid, self.wid, pid, uri) + subprocess.Popen([cmd], shell=True) # TODO: do i need close_fds=True ? + + # Add gobject timer to make sure the config is pushed when fifo socket + # has been created. + timerid = timeout_add(100, uzbl.flush, "flush-initial-config") + uzbl.timers['flush-initial-config'] = timerid + + self.update_tablist() + + + 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.send("exit") + + + def config_uzbl(self, uzbl): + '''Send bind commands for tab new/close/next/prev to a uzbl + instance.''' + + binds = [] + bind_format = r'bind %s = sh "echo \"%s\" > \"%s\""' + bind = lambda key, action: binds.append(bind_format % (key, action,\ + self.fifo_socket)) + + sets = [] + set_format = r'set %s = sh \"echo \\"%s\\" > \\"%s\\""' + set = lambda key, action: binds.append(set_format % (key, action,\ + self.fifo_socket)) + + # Bind definitions here + # bind(key, command back to fifo) + bind(config['bind_new_tab'], 'new') + bind(config['bind_tab_from_clip'], 'newfromclip') + bind(config['bind_tab_from_uri'], 'new %s') + bind(config['bind_close_tab'], 'close') + bind(config['bind_next_tab'], 'next') + bind(config['bind_prev_tab'], 'prev') + bind(config['bind_goto_tab'], 'goto %s') + bind(config['bind_goto_first'], 'goto 0') + bind(config['bind_goto_last'], 'goto -1') + bind(config['bind_clean_slate'], 'clean') + bind(config['bind_save_preset'], 'preset save %s') + bind(config['bind_load_preset'], 'preset load %s') + bind(config['bind_del_preset'], 'preset del %s') + bind(config['bind_list_presets'], 'preset list %d' % uzbl.pid) + bind(config['bind_exit'], 'exit') + + # Set definitions here + # set(key, command back to fifo) + if config['capture_new_windows']: + set("new_window", r'new $8') + + # Send config to uzbl instance via its socket file. + uzbl.send("\n".join(binds+sets)) + + + def goto_tab(self, index): + '''Goto tab n (supports negative indexing).''' + + tabs = list(self.notebook) + if 0 <= index < len(tabs): + self.notebook.set_current_page(index) + 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) + 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.notebook.set_current_page(tabn) + self.update_tablist() + + + 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.notebook.set_current_page(tabn) + self.update_tablist() + + + 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] + for (timer, gid) in uzbl.timers.items(): + error("tab_closed: removing timer %r" % timer) + source_remove(gid) + del uzbl.timers[timer] + + if uzbl._socket: + uzbl._socket.close() + uzbl._socket = None + + uzbl._fifoout = [] + uzbl._socketout = [] + uzbl._kill = True + 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() + + 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(self, curpage=None): + '''Upate tablist status bar.''' + + show_tablist = config['show_tablist'] + show_gtk_tabs = config['show_gtk_tabs'] + tab_titles = config['tab_titles'] + show_ellipsis = config['show_ellipsis'] + multiline_tabs = config['multiline_tabs'] + + if multiline_tabs: + multiline = [] + + if not show_tablist and not show_gtk_tabs: + return True + + tabs = self.tabs.keys() + if curpage is None: + curpage = self.notebook.get_current_page() + + title_format = "%s - Uzbl Browser" + max_title_len = config['max_title_len'] + + if show_tablist: + pango = "" + normal = (config['tab_colours'], config['tab_text_colours']) + selected = (config['selected_tab'], config['selected_tab_text']) + + if tab_titles: + tab_format = " [ %d %s ] " + + else: + tab_format = " [ %d ] " + + if show_gtk_tabs: + gtk_tab_format = "%d %s" + + for index, tab in enumerate(self.notebook): + if tab not in tabs: continue + uzbl = self.tabs[tab] + + if index == curpage: + self.window.set_title(title_format % uzbl.title) + + # Unicode heavy strings do not like being truncated/sliced so by + # re-encoding the string sliced of limbs are removed. + tabtitle = uzbl.title[:max_title_len + int(show_ellipsis)] + if type(tabtitle) != types.UnicodeType: + tabtitle = unicode(tabtitle, 'utf-8', 'ignore') + + tabtitle = tabtitle.encode('utf-8', 'ignore').strip() + + if show_ellipsis and len(tabtitle) != len(uzbl.title): + tabtitle += "\xe2\x80\xa6" + + if show_gtk_tabs: + if tab_titles: + self.notebook.set_tab_label_text(tab, + gtk_tab_format % (index, tabtitle)) + + else: + self.notebook.set_tab_label_text(tab, str(index)) + + if show_tablist: + style = colour_selector(index, curpage, uzbl) + (tabc, textc) = style + + if multiline_tabs: + opango = pango + + if tab_titles: + pango += tab_format % (tabc, index, textc, + escape(tabtitle)) + + else: + pango += tab_format % (tabc, textc, index) + + 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 % (tabc, index, textc, + escape(tabtitle)) + + elif tab_titles: + pango += tab_format % (tabc, index, textc, + escape(tabtitle)) + + else: + pango += tab_format % (tabc, textc, index) + + if show_tablist: + 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.send("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_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__": + + # Read from the uzbl config into the global config dictionary. + readconfig(UZBL_CONFIG, config) + + # 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() -- cgit