-#!/usr/bin/python
+#!/usr/bin/env python
# Uzbl tabbing wrapper using a fifo socket interface
# Copyright (c) 2009, Tom Adams <tom@holizz.com>
# Tom Adams <tom@holizz.com>
# Wrote the original uzbl_tabbed.py as a proof of concept.
#
-# Chris van Dijk (quigybo) <cn.vandijk@hotmail.com>
+# Chris van Dijk (quigybo) <cn.vandijk@hotmail.com>
# Made signifigant headway on the old uzbl_tabbing.py script on the
# uzbl wiki <http://www.uzbl.org/wiki/uzbl_tabbed>
#
# Fix for session restoration code.
+# 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.
# tablist_top = 1
# gtk_tab_pos = (top|left|bottom|right)
# switch_to_new_tabs = 1
+# capture_new_windows = 1
#
# Tab title options:
# tab_titles = 1
# max_title_len = 50
# show_ellipsis = 1
#
-# Core options:
+# 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
-# session_file = $HOME/.local/share/uzbl/session
+# status_background = #303030
#
# Window options:
-# status_background = #303030
# window_size = 800,800
#
# And the key bindings:
# bind_goto_tab = gi_
# bind_goto_first = g<
# bind_goto_last = g>
+# bind_clean_slate = gQ
+#
+# 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.
# - check spelling.
# - pass a uzbl socketid to uzbl_tabbed.py and have it assimilated into
# the collective. Resistance is futile!
-# - on demand store the session to file (need binding & command for that)
import pygtk
import random
import hashlib
+from optparse import OptionParser, OptionGroup
+
pygtk.require('2.0')
def error(msg):
'tablist_top': True, # Display tab-list at top of window
'gtk_tab_pos': 'top', # Gtk tab position (top|left|bottom|right)
'switch_to_new_tabs': True, # Upon opening a new tab switch to it
+ 'capture_new_windows': True, # Use uzbl_tabbed to catch new windows
# Tab title options
'tab_titles': True, # Display tab titles (else only tab-nums)
'max_title_len': 50, # Truncate title at n characters
'show_ellipsis': True, # Show ellipsis when truncating titles
- # Core options
+ # Session options
'save_session': True, # Save session in file when quit
- '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'),
+ '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.
+
# Window options
- 'status_background': "#303030", # Default background for all panels
- 'window_size': "800,800", # width,height in pixels
+ 'window_size': "800,800", # width,height in pixels.
- # Key bindings.
+ # 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_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.
+
+ # 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
rawconfig = h.read()
h.close()
+ configkeys, strip = config.keys(), str.strip
for (key, value) in findsets(rawconfig):
- key, value = key.strip(), value.strip()
- if key not in config.keys(): continue
+ key, value = strip(key), strip(value)
+ if key not in configkeys: continue
if isint(value): value = int(value)
config[key] = value
'''Uzbl instance meta-data/meta-action object.'''
def __init__(self, parent, tab, fifo_socket, socket_file, pid,\
- uri, switch):
+ uri, title, switch):
self.parent = parent
self.tab = tab
self.fifo_socket = fifo_socket
self.socket_file = socket_file
self.pid = pid
- self.title = config['new_tab_title']
+ self.title = title
self.uri = uri
self.timers = {}
self._lastprobe = 0
self._fifos = {}
self._timers = {}
self._buffer = ""
-
- # Once a second is updated with the latest tabs' uris so that when the
- # window is killed the session is saved.
- self._tabsuris = []
- # And index of current page in self._tabsuris
- self._curpage = 0
+ self._killed = False
+
+ # A list of the recently closed tabs
+ self._closed = []
# Holds metadata on the uzbl childen open.
self.tabs = {}
self.window.set_icon(gtk.gdk.pixbuf_new_from_file(icon_path))
# Attach main window event handlers
- self.window.connect("delete-event", self.quit)
+ self.window.connect("delete-event", self.quitrequest)
# Create tab list
if config['show_tablist']:
self.fifo_socket = os.path.join(config['fifo_dir'], fifo_filename)
self._create_fifo_socket(self.fifo_socket)
self._setup_fifo_watcher(self.fifo_socket)
+
+ # If we are using sessions then load the last one if it exists.
+ if config['save_session']:
+ self.load_session()
def _create_fifo_socket(self, fifo_socket):
if fifo_socket in self._fifos.keys():
fd, watchers = self._fifos[fifo_socket]
os.close(fd)
- for watcherid in watchers.keys():
- gobject.source_remove(watchers[watcherid])
+ for (watcherid, gid) in watchers.items():
+ gobject.source_remove(gid)
del watchers[watcherid]
del self._fifos[fifo_socket]
def run(self):
'''UzblTabbed main function that calls the gtk loop.'''
-
+
+ if not len(self.tabs):
+ self.new_tab()
+
# Update tablist timer
#timer = "update-tablist"
#timerid = gobject.timeout_add(500, self.update_tablist,timer)
def probe_clients(self, timer_call):
'''Probe all uzbl clients for up-to-date window titles and uri's.'''
+
+ save_session = config['save_session']
sockd = {}
- uriinventory = []
tabskeys = self.tabs.keys()
notebooklist = list(self.notebook)
for tab in notebooklist:
if tab not in tabskeys: continue
uzbl = self.tabs[tab]
- uriinventory.append(uzbl.uri)
uzbl.probe()
if uzbl._socket:
- sockd[uzbl._socket] = uzbl
-
- self._tabsuris = uriinventory
- self._curpage = self.notebook.get_current_page()
+ sockd[uzbl._socket] = uzbl
sockets = sockd.keys()
(reading, _, errors) = select.select(sockets, [], sockets, 0)
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] == "clean":
+ self.clean_slate()
+
else:
error("parse_command: unknown command %r" % ' '.join(cmd))
def get_tab_by_pid(self, pid):
'''Return uzbl instance by pid.'''
- for tab in self.tabs.keys():
- if self.tabs[tab].pid == pid:
- return self.tabs[tab]
+ for (tab, uzbl) in self.tabs.items():
+ if uzbl.pid == pid:
+ return uzbl
return False
- def new_tab(self, uri='', switch=None):
+ 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
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)
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)
- # Create meta-instance and spawn child
- if len(uri.strip()):
- uri = '--uri %s' % uri
+ if len(uri):
+ uri = "--uri %r" % uri
- uzbl = self.UzblInstance(self, tab, fifo_socket, socket_file, pid,\
- uri, switch)
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 ?
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 = 'bind %s = sh "echo \\\"%s\\\" > \\\"%s\\\""'
- bind = lambda key, action: binds.append(bind_format % (key, action, \
+ 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))
- # Keys are defined in the config section
- # bind ( key , command back to fifo )
+ # 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_goto_tab'], 'goto %s')
bind(config['bind_goto_first'], 'goto 0')
bind(config['bind_goto_last'], 'goto -1')
+ bind(config['bind_clean_slate'], 'clean')
+
+ # session preset binds
+ 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)
- # uzbl.send via socket or uzbl.write via fifo, I'll try send.
- uzbl.send("\n".join(binds))
+ # 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):
if tab in self.tabs.keys():
uzbl = self.tabs[tab]
- for timer in uzbl.timers.keys():
+ for (timer, gid) in uzbl.timers.items():
error("tab_closed: removing timer %r" % timer)
- gobject.source_remove(uzbl.timers[timer])
+ gobject.source_remove(gid)
+ del uzbl.timers[timer]
if uzbl._socket:
uzbl._socket.close()
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 len(self._closed):
+ d = {'curtab': 0, 'tabs': [self._closed[-1],]}
+ self.save_session(session=d)
+
self.quit()
self.update_tablist()
return True
- def quit(self, *args):
- '''Cleanup the application and quit. Called by delete-event signal.'''
+ def save_session(self, session_file=None, session=None):
+ '''Save the current session to file for restoration on next load.'''
- for fifo_socket in self._fifos.keys():
- fd, watchers = self._fifos[fifo_socket]
- os.close(fd)
- for watcherid in watchers.keys():
- gobject.source_remove(watchers[watcherid])
- del watchers[watcherid]
+ strip = str.strip
- del self._fifos[fifo_socket]
+ if session_file is None:
+ session_file = config['session_file']
+
+ if session is None:
+ 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)
- for timerid in self._timers.keys():
- gobject.source_remove(self._timers[timerid])
- del self._timers[timerid]
+ 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()
+
- if os.path.exists(self.fifo_socket):
- os.unlink(self.fifo_socket)
- print "Unlinked %s" % self.fifo_socket
+ def load_session(self, session_file=None):
+ '''Load a saved session from file.'''
+
+ default_path = False
+ strip = str.strip
+ json_session = config['json_session']
- if config['save_session']:
+ if session_file is None:
+ default_path = True
session_file = config['session_file']
- if len(self._tabsuris):
- if not os.path.isfile(session_file):
- dirname = os.path.dirname(session_file)
- if not os.path.isdir(dirname):
- os.makedirs(dirname)
- sessionstr = '\n'.join(self._tabsuris)
- h = open(session_file, 'w')
- h.write('current = %s\n%s' % (self._curpage, sessionstr))
- h.close()
+ 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))
+
+ # 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):
+ '''Called by delete-event signal to kill all uzbl instances.'''
+
+ #TODO: Even though I send the kill request to all uzbl instances
+ # i should add a gobject timeout to check they all die.
+
+ self._killed = True
+
+ if config['save_session']:
+ if len(list(self.notebook)):
+ self.save_session()
else:
# Notebook has no pages so delete session file if it exists.
if os.path.isfile(session_file):
os.remove(session_file)
+
+ for (tab, uzbl) in self.tabs.items():
+ uzbl.send("exit")
+
+
+ def quit(self, *args):
+ '''Cleanup the application and quit. Called by delete-event signal.'''
+
+ for (fifo_socket, (fd, watchers)) in self._fifos.items():
+ os.close(fd)
+ for (watcherid, gid) in watchers.items():
+ gobject.source_remove(gid)
+ del watchers[watcherid]
+
+ del self._fifos[fifo_socket]
+
+ for (timerid, gid) in self._timers.items():
+ gobject.source_remove(gid)
+ del self._timers[timerid]
+
+ if os.path.exists(self.fifo_socket):
+ os.unlink(self.fifo_socket)
+ print "Unlinked %s" % self.fifo_socket
gtk.main_quit()
# Read from the uzbl config into the global config dictionary.
readconfig(uzbl_config, config)
- uzbl = UzblTabbed()
+ # Build command line parser
+ parser = OptionParser()
+ parser.add_option('-n', '--no-session', dest='nosession',\
+ action='store_true', help="ignore session saving a loading.")
+ group = OptionGroup(parser, "Note", "All other command line arguments are "\
+ "interpreted as uris and loaded in new tabs.")
+ parser.add_option_group(group)
- if os.path.isfile(os.path.expandvars(config['session_file'])):
- h = open(os.path.expandvars(config['session_file']),'r')
- lines = [line.strip() for line in h.readlines()]
- h.close()
- current = 0
- urls = []
- for line in lines:
- if line.startswith("current"):
- current = int(line.split()[-1])
+ # Parse command line options
+ (options, uris) = parser.parse_args()
- else:
- urls.append(line.strip())
+ if options.nosession:
+ config['save_session'] = False
- for (index, url) in enumerate(urls):
- if current == index:
- uzbl.new_tab(line, True)
+ if config['json_session']:
+ try:
+ import simplejson as json
- else:
- uzbl.new_tab(line, False)
+ 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 not len(urls):
- uzbl.new_tab()
+ uzbl = UzblTabbed()
- else:
- uzbl.new_tab()
+ # 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()