Merge branch 'experimental' of git://github.com/Dieterbe/uzbl into experimental
authorMason Larobina <mason.larobina@gmail.com>
Sat, 25 Jul 2009 21:20:01 +0000 (05:20 +0800)
committerMason Larobina <mason.larobina@gmail.com>
Sat, 25 Jul 2009 21:20:01 +0000 (05:20 +0800)
examples/data/uzbl/scripts/uzbl_tabbed.py

index 9de6fba..7a9abf5 100755 (executable)
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python
 
 # Uzbl tabbing wrapper using a fifo socket interface
 # Copyright (c) 2009, Tom Adams <tom@holizz.com>
 #       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.
@@ -51,6 +64,7 @@
 #   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
@@ -173,6 +196,7 @@ config = {
   '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)
@@ -180,27 +204,38 @@ config = {
   '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
@@ -271,9 +306,10 @@ def readconfig(uzbl_config, config):
     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
 
@@ -313,14 +349,14 @@ class UzblTabbed:
         '''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
@@ -432,12 +468,10 @@ class UzblTabbed:
         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 = {}
@@ -468,7 +502,7 @@ class UzblTabbed:
                 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']:
@@ -531,7 +565,7 @@ class UzblTabbed:
         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)
-
+        
 
     def _create_fifo_socket(self, fifo_socket):
         '''Create interprocess communication fifo socket.'''
@@ -558,8 +592,8 @@ class UzblTabbed:
         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]
@@ -581,7 +615,13 @@ class UzblTabbed:
 
     def run(self):
         '''UzblTabbed main function that calls the gtk loop.'''
-
+        
+        if config['save_session']:
+            self.load_session()
+        
+        if not len(self.tabs):
+            self.new_tab()
+        
         # Update tablist timer
         #timer = "update-tablist"
         #timerid = gobject.timeout_add(500, self.update_tablist,timer)
@@ -597,22 +637,19 @@ class UzblTabbed:
 
     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)
@@ -741,6 +778,50 @@ class UzblTabbed:
                        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))
 
@@ -748,14 +829,14 @@ class UzblTabbed:
     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
@@ -766,6 +847,7 @@ class UzblTabbed:
         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)
@@ -774,14 +856,16 @@ class UzblTabbed:
 
         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 ?
@@ -794,17 +878,33 @@ class UzblTabbed:
         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))
 
-        # Keys are defined in the config section
-        # bind ( key , command back to fifo )
+        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')
@@ -814,9 +914,21 @@ class UzblTabbed:
         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):
@@ -902,9 +1014,10 @@ class UzblTabbed:
 
         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()
@@ -913,9 +1026,16 @@ class UzblTabbed:
             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()
@@ -995,43 +1115,154 @@ class UzblTabbed:
         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()
 
@@ -1041,32 +1272,17 @@ if __name__ == "__main__":
     # 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')
-        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])
-
-            else:
-                urls.append(line.strip())
-
-        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()
+    uzbl.run()
 
-    else:
-        uzbl.new_tab()
 
-    uzbl.run()