Merge branch 'experimental' of git://github.com/Dieterbe/uzbl into experimental
[uzbl-mobile] / examples / data / uzbl / scripts / uzbl_tabbed.py
index 485adcc..d384fc6 100755 (executable)
@@ -2,7 +2,7 @@
 
 # Uzbl tabbing wrapper using a fifo socket interface
 # Copyright (c) 2009, Tom Adams <tom@holizz.com>
-# Copyright (c) 2009, quigybo <?>
+# Copyright (c) 2009, Chris van Dijk <cn.vandijk@hotmail.com>
 # Copyright (c) 2009, Mason Larobina <mason.larobina@gmail.com>
 #
 # This program is free software: you can redistribute it and/or modify
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
-# Author(s): 
+# Author(s):
 #   Tom Adams <tom@holizz.com>
 #       Wrote the original uzbl_tabbed.py as a proof of concept.
 #
-#   quigybo <?>
-#       Made signifigant headway on the uzbl_tabbing.py script on the 
-#       uzbl wiki <http://www.uzbl.org/wiki/uzbl_tabbed> 
+#  Chris van Dijk (quigybo) <cn.vandijk@hotmail.com>
+#       Made signifigant headway on the old uzbl_tabbing.py script on the
+#       uzbl wiki <http://www.uzbl.org/wiki/uzbl_tabbed>
 #
 #   Mason Larobina <mason.larobina@gmail.com>
 #       Rewrite of the uzbl_tabbing.py script to use a fifo socket interface
 # Contributor(s):
 #   mxey <mxey@ghosthacking.net>
 #       uzbl_config path now honors XDG_CONFIG_HOME if it exists.
+#
+#   Romain Bignon <romain@peerfuse.org>
+#       Fix for session restoration code.
+
+
+# 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)
+#   switch_to_new_tabs      = 1
+#
+# Tab title options:
+#   tab_titles              = 1
+#   new_tab_title           = Loading
+#   max_title_len           = 50
+#   show_ellipsis           = 1
+#
+# Core options:
+#   save_session            = 1
+#   fifo_dir                = /tmp
+#   socket_dir              = /tmp
+#   icon_path               = $HOME/.local/share/uzbl/uzbl.png
+#   session_file            = $HOME/.local/share/uzbl/session
+#
+# Window options:
+#   status_background       = #303030
+#   window_size             = 800,800
+#
+# 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>
+#
+# 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: 
-#   - status_background colour is not honoured (reverts to gtk default).
+# Issues:
 #   - new windows are not caught and opened in a new tab.
-#   - need an easier way to read a uzbl instances window title instead of 
-#     spawning a shell to spawn uzblctrl to communicate to the uzbl 
-#     instance via socket to dump the window title to then pipe it to 
-#     the tabbing managers fifo socket.
-#   - probably missing some os.path.expandvars somewhere. 
+#   - 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: 
+# Todo:
 #   - add command line options to use a different session file, not use a
-#     session file and or open a uri on starup. 
+#     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 
+#   - add "<" & ">" arrows to tablist to indicate that only a subset of the
 #     currently open tabs are being displayed on the tablist.
-#   - probably missing some os.path.expandvars somewhere and other 
-#     user-friendly.. things, this is still a very early version. 
-#   - fix status_background issues & style 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!
+#   - on demand store the session to file (need binding & command for that)
 
 
 import pygtk
@@ -71,50 +135,124 @@ import pango
 import select
 import sys
 import gobject
+import socket
+import random
+import hashlib
 
 pygtk.require('2.0')
 
 def error(msg):
     sys.stderr.write("%s\n"%msg)
 
+
+# ============================================================================
+# ::: Default configuration section ::::::::::::::::::::::::::::::::::::::::::
+# ============================================================================
+
+# Location of your uzbl data directory.
 if 'XDG_DATA_HOME' in os.environ.keys() and os.environ['XDG_DATA_HOME']:
     data_dir = os.path.join(os.environ['XDG_DATA_HOME'], 'uzbl/')
-
 else:
     data_dir = os.path.join(os.environ['HOME'], '.local/share/uzbl/')
-
-# === Default Configuration ====================================================
+if not os.path.exists(data_dir):
+    error("Warning: uzbl data_dir does not exist: %r" % data_dir)
 
 # Location of your uzbl configuration file.
 if 'XDG_CONFIG_HOME' in os.environ.keys() and os.environ['XDG_CONFIG_HOME']:
     uzbl_config = os.path.join(os.environ['XDG_CONFIG_HOME'], 'uzbl/config')
 else:
     uzbl_config = os.path.join(os.environ['HOME'],'.config/uzbl/config')
+if not os.path.exists(uzbl_config):
+    error("Warning: Cannot locate your uzbl_config file %r" % uzbl_config)
 
 # All of these settings can be inherited from your uzbl config file.
-config = {'show_tabs': True,
-  'show_gtk_tabs': False,
-  'switch_to_new_tabs': True,
-  'save_session': True,
-  'fifo_dir': '/tmp',
-  'icon_path': os.path.join(data_dir, 'uzbl.png'),
-  'session_file': os.path.join(data_dir, 'session'),
-  'tab_colours': 'foreground = "#999"',
-  'tab_text_colours': 'foreground = "#444"',
-  'selected_tab': 'foreground = "#aaa" background="#303030"',
-  'selected_tab_text': 'foreground = "green"',
-  'window_size': "800,800",
-  'monospace_size': 10, 
-  'bind_new_tab': 'gn',
-  'bind_tab_from_clipboard': 'gY', 
-  'bind_close_tab': 'gC',
-  'bind_next_tab': 'gt',
-  'bind_prev_tab': 'gT',
-  'bind_goto_tab': 'gi_',
-  'bind_goto_first': 'g<',
-  'bind_goto_last':'g>'}
-
-# === End Configuration =======================================================
+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)
+  'switch_to_new_tabs':     True,   # Upon opening a new tab switch to it
+
+  # 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
+
+  # Core 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'),
+  'session_file':           os.path.join(data_dir, 'session'),
+
+  # Window options
+  'status_background':      "#303030", # Default background for all panels
+  'window_size':            "800,800", # width,height in pixels
+
+  # 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
+
+  # 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 readconfig(uzbl_config, config):
     '''Loads relevant config from the users uzbl config file into the global
@@ -123,22 +261,27 @@ def readconfig(uzbl_config, config):
     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
+    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()
-    
+
     for (key, value) in findsets(rawconfig):
-        key = key.strip()
+        key, value = key.strip(), value.strip()
         if key not in config.keys(): continue
         if isint(value): value = int(value)
         config[key] = value
 
+    # Ensure that config keys that relate to paths are expanded.
+    expand = ['fifo_dir', 'socket_dir', 'session_file', 'icon_path']
+    for key in expand:
+        config[key] = os.path.expandvars(config[key])
+
 
 def rmkdir(path):
     '''Recursively make directories.
@@ -161,121 +304,162 @@ def counter():
         yield i
 
 
+def escape(s):
+    '''Replaces html markup in tab titles that screw around with pango.'''
+
+    for (split, glue) in [('&','&amp;'), ('<', '&lt;'), ('>', '&gt;')]:
+        s = s.replace(split, glue)
+    return s
+
+
+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, socket, fifo, pid, url='', switch=True):
+        def __init__(self, parent, tab, fifo_socket, socket_file, pid,\
+          uri, switch):
+
             self.parent = parent
-            self.socket = socket # the gtk socket
-            self.fifo = fifo
+            self.tab = tab
+            self.fifo_socket = fifo_socket
+            self.socket_file = socket_file
             self.pid = pid
-            self.title = "New tab"
-            self.url = url
+            self.title = config['new_tab_title']
+            self.uri = uri
             self.timers = {}
             self._lastprobe = 0
-            self._switch_on_config = switch
-            self._outgoing = []
-            self._configured = False
-
-            # Probe commands
-            self._probeurl = 'sh \'echo "url %s $6" > "%s"\'' % (self.pid,\
-              self.parent.fifo_socket)
-            
-            # As soon as the variable expansion bug is fixed in uzbl
-            # I can start using this command to fetch the winow title
-            self._probetitle = 'sh \'echo "title %s @window_title" > "%s"\'' \
-              % (self.pid, self.parent.fifo_socket)
-
-            # When notebook tab deleted the kill switch is raised.
+            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
-             
-            # Queue binds for uzbl child
+
+            # 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 @<document.title>@ %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 queue.'''
-            
+            '''Flush messages from the socket-out and fifo-out queues.'''
+
             if self._kill:
-                error("Flush called on dead page.")
+                if self._socket:
+                    self._socket.close()
+                    self._socket = None
+
+                error("Flush called on dead tab.")
                 return False
 
-            if os.path.exists(self.fifo):
-                h = open(self.fifo, 'w')
-                while len(self._outgoing):
-                    msg = self._outgoing.pop(0)
-                    h.write("%s\n" % msg)
-                h.close()
+            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():
+                        gobject.source_remove(self.timers[timer_call])
+                        del self.timers[timer_call]
 
-            elif not timer_call and self._configured:
-                # TODO: I dont know what to do here. A previously thought
-                # alright uzbl client fifo socket has now gone missing.
-                # I think this should be fatal (at least for the page in
-                # question). I'll wait until this error appears in the wild. 
-                error("Error: fifo %r lost in action." % self.fifo)
-            
-            if not len(self._outgoing) and timer_call:
-                self._configured = True
+                    if self._switch:
+                        self.grabfocus()
 
-                if timer_call in self.timers.keys():
-                    gobject.source_remove(self.timers[timer_call])
-                    del self.timers[timer_call]
+            return len(self._fifoout + self._socketout)
 
-                if self._switch_on_config:
-                    notebook = list(self.parent.notebook)
-                    try:
-                        tabid = notebook.index(self.socket)
-                        self.parent.goto_tab(tabid)
 
-                    except ValueError:
-                        pass
-                
-            return len(self._outgoing)
+        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.'''
-            
-            # Ugly way of getting the socket path. Screwed if fifo is in any
-            # other part of the fifo socket path.
-            socket = 'socket'.join(self.fifo.split('fifo'))
-            # Hackish & wasteful way of getting the window title. 
-            subcmd = 'print title %s @<document.title>@' % self.pid
-            cmd = 'uzblctrl -s "%s" -c "%s" > "%s" &' % (socket, subcmd, \
-              self.parent.fifo_socket)
-            subprocess.Popen([cmd], shell=True)
-            self.send(self._probeurl)
-            
-            # Wont work yet.
-            #self.send(self._probetitle)
-
-            self._lastprobe = time.time()
 
+            if self._connected:
+                self.send(self._probecmds)
+                self._lastprobe = time.time()
 
-        def send(self, msg):
+
+        def write(self, msg):
             '''Child fifo write function.'''
 
-            self._outgoing.append(msg)
+            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.'''
-        
-        self.pages = {}
-        self._pidcounter = counter()
-        self.next_pid = self._pidcounter.next
-        self._watchers = {}
+
+        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
+
+        # 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: 
+        try:
             window_size = map(int, config['window_size'].split(','))
             self.window.set_default_size(*window_size)
 
@@ -284,7 +468,7 @@ class UzblTabbed:
 
         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):
@@ -294,79 +478,75 @@ class UzblTabbed:
             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.quit)
-        
-        # Create tab list 
-        if config['show_tabs']:
+
+        # 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(0,2)
+            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()
-            vbox.pack_start(self.tablist, False, False, 0)
-        
+            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_tabs']:
-            vbox.pack_end(self.notebook, True, True, 0)
+        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
-        # Fifo socket definition
-        self._refindfifos = re.compile('^uzbl_fifo_%s_[0-9]+$' % self.wid)
+
+        # Create the uzbl_tabbed fifo
         fifo_filename = 'uzbltabbed_%d' % os.getpid()
         self.fifo_socket = os.path.join(config['fifo_dir'], fifo_filename)
-
-        self._watchers = {}
-        self._buffer = ""
         self._create_fifo_socket(self.fifo_socket)
         self._setup_fifo_watcher(self.fifo_socket)
 
 
-    def run(self):
-        
-        # Update tablist timer
-        timer = "update-tablist"
-        timerid = gobject.timeout_add(500, self.update_tablist,timer)
-        self._timers[timer] = timerid
-
-        # Due to the hackish way in which the window titles are read 
-        # too many window will cause the application to slow down insanely
-        timer = "probe-clients"
-        timerid = gobject.timeout_add(1000, self.probe_clients, timer)
-        self._timers[timer] = timerid
-
-        gtk.main()
-
-
-    def _find_fifos(self, fifo_dir):
-        '''Find all child fifo sockets in fifo_dir.'''
-        
-        dirlist = '\n'.join(os.listdir(fifo_dir))
-        allfifos = self._refindfifos.findall(dirlist)
-        return sorted(allfifos)
-
-
     def _create_fifo_socket(self, fifo_socket):
-        '''Create interprocess communication fifo socket.''' 
+        '''Create interprocess communication fifo socket.'''
 
         if os.path.exists(fifo_socket):
             if not os.access(fifo_socket, os.F_OK | os.R_OK | os.W_OK):
@@ -377,103 +557,146 @@ class UzblTabbed:
             if not os.path.exists(basedir):
                 rmkdir(basedir)
             os.mkfifo(self.fifo_socket)
-        
+
         print "Listening on %s" % self.fifo_socket
 
 
-    def _setup_fifo_watcher(self, fifo_socket, fd=None):
+    def _setup_fifo_watcher(self, fifo_socket):
         '''Open fifo socket fd and setup gobject IO_IN & IO_HUP watchers.
         Also log the creation of a fd and store the the internal
         self._watchers dictionary along with the filename of the fd.'''
-        
-        #TODO: Convert current self._watcher dict manipulation to the better 
-        # IMHO self._timers handling by using "timer-keys" as the keys instead
-        # of the fifo fd's as keys.
 
-        if fd:
+        if fifo_socket in self._fifos.keys():
+            fd, watchers = self._fifos[fifo_socket]
             os.close(fd)
-            if fd in self._watchers.keys():
-                d = self._watchers[fd]
-                watchers = d['watchers']
-                for watcher in list(watchers):
-                    gobject.source_remove(watcher)
-                    watchers.remove(watcher)
-                del self._watchers[fd]         
-        
+            for watcherid in watchers.keys():
+                gobject.source_remove(watchers[watcherid])
+                del watchers[watcherid]
+
+            del self._fifos[fifo_socket]
+
+        # Re-open fifo and add listeners.
         fd = os.open(fifo_socket, os.O_RDONLY | os.O_NONBLOCK)
-        self._watchers[fd] = {'watchers': [], 'filename': fifo_socket}
-            
-        watcher = self._watchers[fd]['watchers'].append
-        watcher(gobject.io_add_watch(fd, gobject.IO_IN, self.read_fifo))
-        watcher(gobject.io_add_watch(fd, gobject.IO_HUP, self.fifo_hangup))
-        
+        watchers = {}
+        self._fifos[fifo_socket] = (fd, watchers)
+        watcher = lambda key, id: watchers.__setitem__(key, id)
+
+        # Watch for incoming data.
+        gid = gobject.io_add_watch(fd, gobject.IO_IN, self.main_fifo_read)
+        watcher('main-fifo-read', gid)
+
+        # Watch for fifo hangups.
+        gid = gobject.io_add_watch(fd, gobject.IO_HUP, self.main_fifo_hangup)
+        watcher('main-fifo-hangup', gid)
 
-    def probe_clients(self, timer_call):
-        '''Load balance probe all uzbl clients for up-to-date window titles 
-        and uri's.'''
-        
-        p = self.pages 
-        probetimes = [(s, p[s]._lastprobe) for s in p.keys()]
-        socket, lasttime = sorted(probetimes, key=lambda t: t[1])[0]
 
-        if (time.time()-lasttime) > 5:
-            # Probe a uzbl instance at most once every 10 seconds
-            self.pages[socket].probe()
+    def run(self):
+        '''UzblTabbed main function that calls the gtk loop.'''
+
+        # Update tablist timer
+        #timer = "update-tablist"
+        #timerid = gobject.timeout_add(500, self.update_tablist,timer)
+        #self._timers[timer] = timerid
+
+        # Probe clients every second for window titles and location
+        timer = "probe-clients"
+        timerid = gobject.timeout_add(1000, self.probe_clients, timer)
+        self._timers[timer] = timerid
+
+        gtk.main()
+
+
+    def probe_clients(self, timer_call):
+        '''Probe all uzbl clients for up-to-date window titles and uri's.'''
+
+        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()
+
+        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 fifo_hangup(self, fd, cb_condition):
-        '''Handle fifo socket hangups.'''
-        
+    def main_fifo_hangup(self, fd, cb_condition):
+        '''Handle main fifo socket hangups.'''
+
         # Close fd, re-open fifo_socket and watch.
-        self._setup_fifo_watcher(self.fifo_socket, fd)
+        self._setup_fifo_watcher(self.fifo_socket)
 
         # And to kill any gobject event handlers calling this function:
         return False
 
 
-    def read_fifo(self, fd, cb_condition):
-        '''Read from fifo socket and handle fifo socket hangups.'''
+    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 [s.strip().split() for s in temp if len(s.strip())]:
+        for cmd in cmds:
             try:
                 #print cmd
                 self.parse_command(cmd)
 
             except:
-                #raise
-                error("Invalid command: %s" % ' '.join(cmd))
-        
+                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 ) 
+
+        # Commands ( [] = optional, {} = required )
         # new [uri]
-        #   open new tab and head to optional uri. 
-        # close [tab-num] 
+        #   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.  
+        #   goto tab n.
         # first
         #   goto first tab.
         # last
-        #   goto last tab. 
+        #   goto last tab.
         # title {pid} {document-title}
         #   updates tablist title.
-        # url {pid} {document-location}
-         
-        # WARNING SOME OF THESE COMMANDS MIGHT NOT BE WORKING YET OR FAIL.
+        # uri {pid} {document-location}
 
         if cmd[0] == "new":
             if len(cmd) == 2:
@@ -483,10 +706,10 @@ class UzblTabbed:
                 self.new_tab()
 
         elif cmd[0] == "newfromclip":
-            url = subprocess.Popen(['xclip','-selection','clipboard','-o'],\
+            uri = subprocess.Popen(['xclip','-selection','clipboard','-o'],\
               stdout=subprocess.PIPE).communicate()[0]
-            if url:
-                self.new_tab(url)
+            if uri:
+                self.new_tab(uri)
 
         elif cmd[0] == "close":
             if len(cmd) == 2:
@@ -498,7 +721,7 @@ class UzblTabbed:
         elif cmd[0] == "next":
             if len(cmd) == 2:
                 self.next_tab(int(cmd[1]))
-                   
+
             else:
                 self.next_tab()
 
@@ -508,7 +731,7 @@ class UzblTabbed:
 
             else:
                 self.prev_tab()
-        
+
         elif cmd[0] == "goto":
             self.goto_tab(int(cmd[1]))
 
@@ -518,9 +741,9 @@ class UzblTabbed:
         elif cmd[0] == "last":
             self.goto_tab(-1)
 
-        elif cmd[0] in ["title", "url"]:
+        elif cmd[0] in ["title", "uri"]:
             if len(cmd) > 2:
-                uzbl = self.get_uzbl_by_pid(int(cmd[1]))
+                uzbl = self.get_tab_by_pid(int(cmd[1]))
                 if uzbl:
                     old = getattr(uzbl, cmd[0])
                     new = ' '.join(cmd[2:])
@@ -528,64 +751,74 @@ class UzblTabbed:
                     if old != new:
                        self.update_tablist()
                 else:
-                    error("Cannot find uzbl instance with pid %r" % int(cmd[1]))
+                    error("parse_command: no uzbl with pid %r" % int(cmd[1]))
         else:
-            error("Unknown command: %s" % ' '.join(cmd))
+            error("parse_command: unknown command %r" % ' '.join(cmd))
+
 
-    
-    def get_uzbl_by_pid(self, pid):
+    def get_tab_by_pid(self, pid):
         '''Return uzbl instance by pid.'''
 
-        for socket in self.pages.keys():
-            if self.pages[socket].pid == pid:
-                return self.pages[socket]
+        for tab in self.tabs.keys():
+            if self.tabs[tab].pid == pid:
+                return self.tabs[tab]
+
         return False
-   
 
-    def new_tab(self,url='', switch=True):
+
+    def new_tab(self, uri='', 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 
+        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()
-        socket = gtk.Socket()
-        socket.show()
-        self.notebook.append_page(socket)
-        sid = socket.get_id()
-        
-        if url:
-            url = '--uri %s' % url
-        
+        tab = gtk.Socket()
+        tab.show()
+        self.notebook.append_page(tab)
+        sid = tab.get_id()
+
         fifo_filename = 'uzbl_fifo_%s_%0.2d' % (self.wid, pid)
         fifo_socket = os.path.join(config['fifo_dir'], fifo_filename)
-        uzbl = self.UzblInstance(self, socket, fifo_socket, pid,\
-          url=url, switch=switch)
-        self.pages[socket] = uzbl
-        cmd = 'uzbl -s %s -n %s_%0.2d %s &' % (sid, self.wid, pid, url)
-        subprocess.Popen([cmd], shell=True)        
-        
+        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']
+
+
+        # Create meta-instance and spawn child
+        if len(uri.strip()):
+            uri = '--uri %s' % 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 ?
+
         # Add gobject timer to make sure the config is pushed when fifo socket
-        # has been created. 
+        # has been created.
         timerid = gobject.timeout_add(100, uzbl.flush, "flush-initial-config")
         uzbl.timers['flush-initial-config'] = timerid
-    
+
         self.update_tablist()
 
 
     def config_uzbl(self, uzbl):
-        '''Send bind commands for tab new/close/next/prev to a 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, \
           self.fifo_socket))
-        
+
         # Keys are defined in the config section
-        # bind ( key , command back to fifo ) 
+        # bind ( key , command back to fifo )
         bind(config['bind_new_tab'], 'new')
-        bind(config['bind_tab_from_clipboard'], 'newfromclip')
+        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')
@@ -593,201 +826,259 @@ class UzblTabbed:
         bind(config['bind_goto_first'], 'goto 0')
         bind(config['bind_goto_last'], 'goto -1')
 
+        # uzbl.send via socket or uzbl.write via fifo, I'll try send.
         uzbl.send("\n".join(binds))
 
 
-    def goto_tab(self, n):
+    def goto_tab(self, index):
         '''Goto tab n (supports negative indexing).'''
-        
-        notebook = list(self.notebook)
-        
-        try: 
-            page = notebook[n]
-            i = notebook.index(page)
-            self.notebook.set_current_page(i)
-        
+
+        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
 
-        self.update_tablist()
-
 
-    def next_tab(self, n=1):
+    def next_tab(self, step=1):
         '''Switch to next tab or n tabs right.'''
-        
-        if n >= 1:
-            numofpages = self.notebook.get_n_pages()
-            pagen = self.notebook.get_current_page() + n
-            self.notebook.set_current_page( pagen % numofpages ) 
 
+        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, n=1):
+    def prev_tab(self, step=1):
         '''Switch to prev tab or n tabs left.'''
-        
-        if n >= 1:
-            numofpages = self.notebook.get_n_pages()
-            pagen = self.notebook.get_current_page() - n
-            while pagen < 0: 
-                pagen += numofpages
-            self.notebook.set_current_page(pagen)
 
+        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, tabid=None):
+    def close_tab(self, tabn=None):
         '''Closes current tab. Supports negative indexing.'''
-        
-        if not tabid: 
-            tabid = self.notebook.get_current_page()
-        
-        try: 
-            socket = list(self.notebook)[tabid]
 
-        except IndexError:
-            error("Invalid index. Cannot close tab.")
-            return False
+        if tabn is None:
+            tabn = self.notebook.get_current_page()
 
-        uzbl = self.pages[socket]
-        # Kill timers:
-        for timer in uzbl.timers.keys():
-            error("Removing timer %r %r" % (timer, uzbl.timers[timer]))
-            gobject.source_remove(uzbl.timers[timer])
+        else:
+            try:
+                tab = list(self.notebook)[tabn]
 
-        uzbl._outgoing = []
-        uzbl._kill = True
-        del self.pages[socket]
-        self.notebook.remove_page(tabid)
+            except IndexError:
+                error("close_tab: invalid index %r" % tabn)
+                return None
+
+        self.notebook.remove_page(tabn)
 
-        self.update_tablist()
 
+    def tab_opened(self, notebook, tab, index):
+        '''Called upon tab creation. Called by page-added signal.'''
 
-    def tab_closed(self, notebook, socket, page_num):
-        '''Close the window if no tabs are left. Called by page-removed 
+        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 socket in self.pages.keys():
-            uzbl = self.pages[socket]
+
+        if tab in self.tabs.keys():
+            uzbl = self.tabs[tab]
             for timer in uzbl.timers.keys():
-                error("Removing timer %r %r" % (timer, uzbl.timers[timer]))
+                error("tab_closed: removing timer %r" % timer)
                 gobject.source_remove(uzbl.timers[timer])
 
-            uzbl._outgoing = []
+            if uzbl._socket:
+                uzbl._socket.close()
+                uzbl._socket = None
+
+            uzbl._fifoout = []
+            uzbl._socketout = []
             uzbl._kill = True
-            del self.pages[socket]
-        
+            del self.tabs[tab]
+
         if self.notebook.get_n_pages() == 0:
             self.quit()
 
         self.update_tablist()
 
+        return True
 
-    def tab_changed(self, notebook, page, page_num):
+
+    def tab_changed(self, notebook, page, index):
         '''Refresh tab list. Called by switch-page signal.'''
 
-        self.update_tablist()
+        tab = self.notebook.get_nth_page(index)
+        self.notebook.set_focus_child(tab)
+        self.update_tablist(index)
+        return True
 
 
-    def update_tablist(self, timer_call=None):
+    def update_tablist(self, curpage=None):
         '''Upate tablist status bar.'''
 
-        pango = ""
+        show_tablist = config['show_tablist']
+        show_gtk_tabs = config['show_gtk_tabs']
+        tab_titles = config['tab_titles']
+        show_ellipsis = config['show_ellipsis']
+        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()
 
-        normal = (config['tab_colours'], config['tab_text_colours'])
-        selected = (config['selected_tab'], config['selected_tab_text'])
-        
-        tab_format = "<span %s> [ %d <span %s> %s</span> ] </span>"
-        
         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 = "<span %s> [ %d <span %s> %s</span> ] </span>"
+            else:
+                tab_format = "<span %s> [ <span %s>%d</span> ] </span>"
 
-        uzblkeys = self.pages.keys()
-        curpage = self.notebook.get_current_page()
+        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]
 
-        for index, socket in enumerate(self.notebook):
-            if socket not in uzblkeys:
-                #error("Theres a socket in the notebook that I have no uzbl "\
-                #  "record of.")
-                continue
-            uzbl = self.pages[socket]
-            
             if index == curpage:
-                colours = selected
                 self.window.set_title(title_format % uzbl.title)
 
-            else:
-                colours = normal
-            
-            pango += tab_format % (colours[0], index, colours[1], uzbl.title)
+            tabtitle = uzbl.title[:max_title_len]
+            if show_ellipsis and len(tabtitle) != len(uzbl.title):
+                tabtitle = "%s\xe2\x80\xa6" % tabtitle[:-1] # Show Ellipsis
 
-        self.tablist.set_markup(pango)
+            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 tab_titles:
+                    pango += tab_format % (tabc, index, textc,\
+                      escape(tabtitle))
+                else:
+                    pango += tab_format % (tabc, textc, index)
+
+        if show_tablist:
+            self.tablist.set_markup(pango)
 
         return True
 
 
-    #def quit(self, window, event):
     def quit(self, *args):
         '''Cleanup the application and quit. Called by delete-event signal.'''
 
-        for fd in self._watchers.keys():
-            d = self._watchers[fd]
-            watchers = d['watchers']
-            for watcher in list(watchers):
-                gobject.source_remove(watcher)
-        
-        for timer in self._timers.keys():
-            gobject.source_remove(self._timers[timer])
+        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]
+
+            del self._fifos[fifo_socket]
+
+        for timerid in self._timers.keys():
+            gobject.source_remove(self._timers[timerid])
+            del self._timers[timerid]
 
         if os.path.exists(self.fifo_socket):
             os.unlink(self.fifo_socket)
             print "Unlinked %s" % self.fifo_socket
-        
+
         if config['save_session']:
-            session_file = os.path.expandvars(config['session_file'])
-            if self.notebook.get_n_pages():
+            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):
+                        # Recursive mkdir not rmdir.
                         rmkdir(dirname)
 
+                sessionstr = '\n'.join(self._tabsuris)
                 h = open(session_file, 'w')
-                h.write('current = %s\n' % self.notebook.get_current_page())
+                h.write('current = %s\n%s' % (self._curpage, sessionstr))
                 h.close()
-                for socket in list(self.notebook):
-                    if socket not in self.pages.keys(): continue
-                    uzbl = self.pages[socket]
-                    uzbl.send('sh "echo $6 >> %s"' % session_file)
-                    time.sleep(0.05)
 
             else:
                 # Notebook has no pages so delete session file if it exists.
-                # Its better to not exist than be blank IMO. 
                 if os.path.isfile(session_file):
                     os.remove(session_file)
 
-        gtk.main_quit() 
+        gtk.main_quit()
 
 
 if __name__ == "__main__":
-    
-    # Read from the uzbl config into the global config dictionary. 
+
+    # Read from the uzbl config into the global config dictionary.
     readconfig(uzbl_config, config)
-     
+
     uzbl = UzblTabbed()
-    
+
     if os.path.isfile(os.path.expandvars(config['session_file'])):
         h = open(os.path.expandvars(config['session_file']),'r')
-        urls = [s.strip() for s in h.readlines()]
+        lines = [line.strip() for line in h.readlines()]
         h.close()
         current = 0
-        for url in urls:
-            if url.startswith("current"):
-                current = int(url.split()[-1])
+        urls = []
+        for line in lines:
+            if line.startswith("current"):
+                current = int(line.split()[-1])
+
+            else:
+                urls.append(line.strip())
+
+        for (index, url) in enumerate(urls):
+            if current == index:
+                uzbl.new_tab(line, True)
+
             else:
-                uzbl.new_tab(url, False)
+                uzbl.new_tab(line, False)
+
+        if not len(urls):
+            uzbl.new_tab()
+
     else:
         uzbl.new_tab()
 
     uzbl.run()
-
-