93592da678b710f63f3223a85abb10ae9b745d53
[uzbl-mobile] / examples / data / uzbl / scripts / uzbl_tabbed.py
1 #!/usr/bin/python
2
3 # Uzbl tabbing wrapper using a fifo socket interface
4 # Copyright (c) 2009, Tom Adams <tom@holizz.com>
5 # Copyright (c) 2009, Chris van Dijk <cn.vandijk@hotmail.com>
6 # Copyright (c) 2009, Mason Larobina <mason.larobina@gmail.com>
7 #
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
20
21
22 # Author(s): 
23 #   Tom Adams <tom@holizz.com>
24 #       Wrote the original uzbl_tabbed.py as a proof of concept.
25 #
26 #  Chris van Dijk (quigybo) <cn.vandijk@hotmail.com>
27 #       Made signifigant headway on the old uzbl_tabbing.py script on the 
28 #       uzbl wiki <http://www.uzbl.org/wiki/uzbl_tabbed> 
29 #
30 #   Mason Larobina <mason.larobina@gmail.com>
31 #       Rewrite of the uzbl_tabbing.py script to use a fifo socket interface
32 #       and inherit configuration options from the user's uzbl config.
33 #
34 # Contributor(s):
35 #   mxey <mxey@ghosthacking.net>
36 #       uzbl_config path now honors XDG_CONFIG_HOME if it exists.
37
38
39 # Configuration:
40 # Because this version of uzbl_tabbed is able to inherit options from your main
41 # uzbl configuration file you may wish to configure uzbl tabbed from there.
42 # Here is a list of configuration options that can be customised and some
43 # example values for each:
44 #
45 #   set show_tablist        = 1
46 #   set show_gtk_tabs       = 0
47 #   set switch_to_new_tabs  = 1
48 #   set save_session        = 1
49 #   set gtk_tab_pos         = (left|bottom|top|right)
50 #   set max_title_len       = 50
51 #   set new_tab_title       = New tab
52 #   set status_background   = #303030
53 #   set session_file        = $HOME/.local/share/session
54 #   set tab_colours         = foreground = "#999"
55 #   set tab_text_colours    = foreground = "#444"
56 #   set selected_tab        = foreground = "#aaa" background="#303030"
57 #   set selected_tab_text   = foreground = "green" 
58 #   set window_size         = 800,800
59 #
60 # And the keybindings:
61 #
62 #   set bind_new_tab        = gn
63 #   set bind_tab_from_clip  = gY
64 #   set bind_close_tab      = gC
65 #   set bind_next_tab       = gt
66 #   set bind_prev_tab       = gT
67 #   set bind_goto_tab       = gi_
68 #   set bind_goto_first     = g<
69 #   set bind_goto_last      = g>
70 #
71 # And uzbl_tabbed.py takes care of the actual binding of the commands via each
72 # instances fifo socket. 
73
74
75 # Issues: 
76 #   - new windows are not caught and opened in a new tab.
77 #   - when uzbl_tabbed.py crashes it takes all the children with it.
78 #   - when a new tab is opened when using gtk tabs the tab button itself 
79 #     grabs focus from its child for a few seconds. 
80 #   - when switch_to_new_tabs is not selected the notebook page is 
81 #     maintained but the new window grabs focus (try as I might to stop it).
82
83
84 # Todo: 
85 #   - add command line options to use a different session file, not use a
86 #     session file and or open a uri on starup. 
87 #   - ellipsize individual tab titles when the tab-list becomes over-crowded
88 #   - add "<" & ">" arrows to tablist to indicate that only a subset of the 
89 #     currently open tabs are being displayed on the tablist.
90 #   - add the small tab-list display when both gtk tabs and text vim-like
91 #     tablist are hidden (I.e. [ 1 2 3 4 5 ])
92 #   - check spelling.
93 #   - pass a uzbl socketid to uzbl_tabbed.py and have it assimilated into 
94 #     the collective. Resistance is futile!
95
96
97 import pygtk
98 import gtk
99 import subprocess
100 import os
101 import re
102 import time
103 import getopt
104 import pango
105 import select
106 import sys
107 import gobject
108 import socket
109 import random
110 import hashlib
111
112 pygtk.require('2.0')
113
114 def error(msg):
115     sys.stderr.write("%s\n"%msg)
116
117
118 # ============================================================================
119 # ::: Default configuration section ::::::::::::::::::::::::::::::::::::::::::
120 # ============================================================================
121
122 # Location of your uzbl data directory. 
123 if 'XDG_DATA_HOME' in os.environ.keys() and os.environ['XDG_DATA_HOME']:
124     data_dir = os.path.join(os.environ['XDG_DATA_HOME'], 'uzbl/')
125 else:
126     data_dir = os.path.join(os.environ['HOME'], '.local/share/uzbl/')
127 if not os.path.exists(data_dir):
128     error("Warning: uzbl data_dir does not exist: %r" % data_dir)
129
130 # Location of your uzbl configuration file.
131 if 'XDG_CONFIG_HOME' in os.environ.keys() and os.environ['XDG_CONFIG_HOME']:
132     uzbl_config = os.path.join(os.environ['XDG_CONFIG_HOME'], 'uzbl/config')
133 else:
134     uzbl_config = os.path.join(os.environ['HOME'],'.config/uzbl/config')
135 if not os.path.exists(uzbl_config):
136     error("Warning: Cannot locate your uzbl_config file %r" % uzbl_config)
137
138 # All of these settings can be inherited from your uzbl config file.
139 config = { 
140   # Tab options
141   'show_tablist':           True,   # Show text uzbl like statusbar tab-list
142   'show_gtk_tabs':          False,  # Show gtk notebook tabs
143   'tablist_top':            True,   # Display tab-list at top of window
144   'gtk_tab_pos':            'top',  # Gtk tab position (top|left|bottom|right)
145   'switch_to_new_tabs':     True,   # Upon opening a new tab switch to it
146   
147   # Tab title options
148   'tab_titles':             True,   # Display tab titles (else only tab-nums)
149   'new_tab_title':          'Loading', # New tab title
150   'max_title_len':          50,     # Truncate title at n characters
151   'show_ellipsis':          True,   # Show ellipsis when truncating titles
152
153   # Core options
154   'save_session':           True,   # Save session in file when quit
155   'fifo_dir':               '/tmp', # Path to look for uzbl fifo
156   'socket_dir':             '/tmp', # Path to look for uzbl socket 
157   'icon_path':              os.path.join(data_dir, 'uzbl.png'),
158   'session_file':           os.path.join(data_dir, 'session'),
159
160   # Window options
161   'status_background':      "#303030", # Default background for all panels
162   'window_size':            "800,800", # width,height in pixels
163   
164   # Key bindings.
165   'bind_new_tab':           'gn',   # Open new tab. 
166   'bind_tab_from_clip':     'gY',   # Open tab from clipboard.
167   'bind_tab_from_uri':      'go _', # Open new tab and goto entered uri.
168   'bind_close_tab':         'gC',   # Close tab.  
169   'bind_next_tab':          'gt',   # Next tab.
170   'bind_prev_tab':          'gT',   # Prev tab.
171   'bind_goto_tab':          'gi_',  # Goto tab by tab-number (in title)
172   'bind_goto_first':        'g<',   # Goto first tab
173   'bind_goto_last':         'g>',   # Goto last tab
174   
175   # Add custom tab style definitions to be used by the tab colour policy 
176   # handler here. Because these are added to the config dictionary like
177   # any other uzbl_tabbed configuration option remember that they can 
178   # be superseeded from your main uzbl config file. 
179   'tab_colours':            'foreground = "#888" background = "#303030"',
180   'tab_text_colours':       'foreground = "#bbb"',  
181   'selected_tab':           'foreground = "#fff"',
182   'selected_tab_text':      'foreground = "green"',
183   'tab_indicate_https':     True,
184   'https_colours':          'foreground = "#888"',
185   'https_text_colours':     'foreground = "#9c8e2d"', 
186   'selected_https':         'foreground = "#fff"',
187   'selected_https_text':    'foreground = "gold"',
188   
189   } # End of config dict.
190
191 # This is the tab style policy handler. Every time the tablist is updated 
192 # this function is called to determine how to colourise that specific tab
193 # according the simple/complex rules as defined here. You may even wish to 
194 # move this function into another python script and import it using:
195 #   from mycustomtabbingconfig import colour_selector
196 # Remember to rename, delete or comment out this function if you do that.
197
198 def colour_selector(tabindex, currentpage, uzbl):
199     '''Tablist styling policy handler. This function must return a tuple of 
200     the form (tab style, text style).'''
201     
202     # Just as an example: 
203     # if 'error' in uzbl.title:
204     #     if tabindex == currentpage:
205     #         return ('foreground="#fff"', 'foreground="red"')
206     #     return ('foreground="#888"', 'foreground="red"')
207
208     # Style tabs to indicate connected via https.
209     if config['tab_indicate_https'] and uzbl.uri.startswith("https://"):
210         if tabindex == currentpage:
211             return (config['selected_https'], config['selected_https_text'])
212         return (config['https_colours'], config['https_text_colours'])
213
214     # Style to indicate selected.
215     if tabindex == currentpage: 
216         return (config['selected_tab'], config['selected_tab_text'])
217
218     # Default tab style.
219     return (config['tab_colours'], config['tab_text_colours'])    
220
221
222 # ============================================================================
223 # ::: End of configuration section :::::::::::::::::::::::::::::::::::::::::::
224 # ============================================================================
225
226
227 def readconfig(uzbl_config, config):
228     '''Loads relevant config from the users uzbl config file into the global
229     config dictionary.'''
230
231     if not os.path.exists(uzbl_config):
232         error("Unable to load config %r" % uzbl_config)
233         return None
234     
235     # Define parsing regular expressions
236     isint = re.compile("^(\-|)[0-9]+$").match
237     findsets = re.compile("^set\s+([^\=]+)\s*\=\s*(.+)$",\
238       re.MULTILINE).findall
239     
240     h = open(os.path.expandvars(uzbl_config), 'r')
241     rawconfig = h.read()
242     h.close()
243     
244     for (key, value) in findsets(rawconfig):
245         key, value = key.strip(), value.strip()
246         if key not in config.keys(): continue
247         if isint(value): value = int(value)
248         config[key] = value
249     
250     # Ensure that config keys that relate to paths are expanded.
251     expand = ['fifo_dir', 'socket_dir', 'session_file', 'icon_path']
252     for key in expand:
253         config[key] = os.path.expandvars(config[key]) 
254
255
256 def rmkdir(path):
257     '''Recursively make directories.
258     I.e. `mkdir -p /some/nonexistant/path/`'''
259
260     path, sep = os.path.realpath(path), os.path.sep
261     dirs = path.split(sep)
262     for i in range(2,len(dirs)+1):
263         dir = os.path.join(sep,sep.join(dirs[:i]))
264         if not os.path.exists(dir):
265             os.mkdir(dir)
266
267
268 def counter():
269     '''To infinity and beyond!'''
270
271     i = 0
272     while True:
273         i += 1
274         yield i
275
276
277 def gen_endmarker():
278     '''Generates a random md5 for socket message-termination endmarkers.'''
279
280     return hashlib.md5(str(random.random()*time.time())).hexdigest()
281
282
283 class UzblTabbed:
284     '''A tabbed version of uzbl using gtk.Notebook'''
285
286     class UzblInstance: 
287         '''Uzbl instance meta-data/meta-action object.'''
288
289         def __init__(self, parent, tab, fifo_socket, socket_file, pid,\
290           uri, switch):
291
292             self.parent = parent
293             self.tab = tab 
294             self.fifo_socket = fifo_socket
295             self.socket_file = socket_file
296             self.pid = pid
297             self.title = config['new_tab_title']
298             self.uri = uri
299             self.timers = {}
300             self._lastprobe = 0
301             self._fifoout = []
302             self._socketout = []
303             self._socket = None
304             self._buffer = ""
305             # Switch to tab after loading
306             self._switch = switch
307             # fifo/socket files exists and socket connected. 
308             self._connected = False
309             # The kill switch
310             self._kill = False
311
312             # Message termination endmarker. 
313             self._marker = gen_endmarker()
314
315             # Gen probe commands string
316             probes = []
317             probe = probes.append
318             probe('print uri %d @uri %s' % (self.pid, self._marker))
319             probe('print title %d @<document.title>@ %s' % (self.pid,\
320               self._marker))
321             self._probecmds = '\n'.join(probes)
322              
323             # Enqueue keybinding config for child uzbl instance
324             self.parent.config_uzbl(self)
325
326
327         def flush(self, timer_call=False):
328             '''Flush messages from the socket-out and fifo-out queues.'''
329             
330             if self._kill:
331                 if self._socket: 
332                     self._socket.close()
333                     self._socket = None
334
335                 error("Flush called on dead tab.")
336                 return False
337
338             if len(self._fifoout):
339                 if os.path.exists(self.fifo_socket):
340                     h = open(self.fifo_socket, 'w')
341                     while len(self._fifoout):
342                         msg = self._fifoout.pop(0)
343                         h.write("%s\n"%msg)
344                     h.close()
345             
346             if len(self._socketout):
347                 if not self._socket and os.path.exists(self.socket_file):
348                     sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
349                     sock.connect(self.socket_file)
350                     self._socket = sock
351
352                 if self._socket:
353                     while len(self._socketout):
354                         msg = self._socketout.pop(0)
355                         self._socket.send("%s\n"%msg)
356             
357             if not self._connected and timer_call:
358                 if not len(self._fifoout + self._socketout):
359                     self._connected = True
360                     
361                     if timer_call in self.timers.keys():
362                         gobject.source_remove(self.timers[timer_call])
363                         del self.timers[timer_call]
364
365                     if self._switch:
366                         self.grabfocus()
367
368             return len(self._fifoout + self._socketout)
369
370         
371         def grabfocus(self):
372             '''Steal parent focus and switch the notebook to my own tab.'''
373             
374             tabs = list(self.parent.notebook)
375             tabid = tabs.index(self.tab)
376             self.parent.goto_tab(tabid)
377
378
379         def probe(self):
380             '''Probes the client for information about its self.'''
381             
382             if self._connected:
383                 self.send(self._probecmds)
384                 self._lastprobe = time.time()
385
386
387         def write(self, msg):
388             '''Child fifo write function.'''
389
390             self._fifoout.append(msg)
391             # Flush messages from the queue if able.
392             return self.flush()
393
394
395         def send(self, msg):
396             '''Child socket send function.'''
397
398             self._socketout.append(msg)
399             # Flush messages from queue if able.
400             return self.flush()
401               
402
403     def __init__(self):
404         '''Create tablist, window and notebook.'''
405         
406         self._fifos = {}
407         self._timers = {}
408         self._buffer = ""
409         
410         # Once a second is updated with the latest tabs' uris so that when the 
411         # window is killed the session is saved.
412         self._tabsuris = []
413         # And index of current page in self._tabsuris
414         self._curpage = 0
415
416         # Holds metadata on the uzbl childen open.
417         self.tabs = {}
418         
419         # Generates a unique id for uzbl socket filenames.
420         self.next_pid = counter().next
421         
422         # Create main window
423         self.window = gtk.Window()
424         try: 
425             window_size = map(int, config['window_size'].split(','))
426             self.window.set_default_size(*window_size)
427
428         except:
429             error("Invalid value for default_size in config file.")
430
431         self.window.set_title("Uzbl Browser")
432         self.window.set_border_width(0)
433         
434         # Set main window icon
435         icon_path = config['icon_path']
436         if os.path.exists(icon_path):
437             self.window.set_icon(gtk.gdk.pixbuf_new_from_file(icon_path))
438
439         else:
440             icon_path = '/usr/share/uzbl/examples/data/uzbl/uzbl.png'
441             if os.path.exists(icon_path):
442                 self.window.set_icon(gtk.gdk.pixbuf_new_from_file(icon_path))
443         
444         # Attach main window event handlers
445         self.window.connect("delete-event", self.quit)
446         
447         # Create tab list 
448         if config['show_tablist']:
449             vbox = gtk.VBox()
450             self.window.add(vbox)
451             ebox = gtk.EventBox()
452             self.tablist = gtk.Label()
453             self.tablist.set_use_markup(True)
454             self.tablist.set_justify(gtk.JUSTIFY_LEFT)
455             self.tablist.set_line_wrap(False)
456             self.tablist.set_selectable(False)
457             self.tablist.set_padding(2,2)
458             self.tablist.set_alignment(0,0)
459             self.tablist.set_ellipsize(pango.ELLIPSIZE_END)
460             self.tablist.set_text(" ")
461             self.tablist.show()
462             ebox.add(self.tablist)
463             ebox.show()
464             bgcolor = gtk.gdk.color_parse(config['status_background'])
465             ebox.modify_bg(gtk.STATE_NORMAL, bgcolor)
466         
467         # Create notebook
468         self.notebook = gtk.Notebook()
469         self.notebook.set_show_tabs(config['show_gtk_tabs'])
470
471         # Set tab position
472         allposes = {'left': gtk.POS_LEFT, 'right':gtk.POS_RIGHT, 
473           'top':gtk.POS_TOP, 'bottom':gtk.POS_BOTTOM}
474         if config['gtk_tab_pos'] in allposes.keys():
475             self.notebook.set_tab_pos(allposes[config['gtk_tab_pos']])
476
477         self.notebook.set_show_border(False)
478         self.notebook.set_scrollable(True)
479         self.notebook.set_border_width(0)
480
481         self.notebook.connect("page-removed", self.tab_closed)
482         self.notebook.connect("switch-page", self.tab_changed)
483         self.notebook.connect("page-added", self.tab_opened)
484
485         self.notebook.show()
486         if config['show_tablist']:
487             if config['tablist_top']:
488                 vbox.pack_start(ebox, False, False, 0)
489                 vbox.pack_end(self.notebook, True, True, 0)
490
491             else:
492                 vbox.pack_start(self.notebook, True, True, 0)
493                 vbox.pack_end(ebox, False, False, 0)
494
495             vbox.show()
496
497         else:
498             self.window.add(self.notebook)
499         
500         self.window.show()
501         self.wid = self.notebook.window.xid
502         
503         # Create the uzbl_tabbed fifo
504         fifo_filename = 'uzbltabbed_%d' % os.getpid()
505         self.fifo_socket = os.path.join(config['fifo_dir'], fifo_filename)
506         self._create_fifo_socket(self.fifo_socket)
507         self._setup_fifo_watcher(self.fifo_socket)
508
509
510     def _create_fifo_socket(self, fifo_socket):
511         '''Create interprocess communication fifo socket.''' 
512
513         if os.path.exists(fifo_socket):
514             if not os.access(fifo_socket, os.F_OK | os.R_OK | os.W_OK):
515                 os.mkfifo(fifo_socket)
516
517         else:
518             basedir = os.path.dirname(self.fifo_socket)
519             if not os.path.exists(basedir):
520                 rmkdir(basedir)
521             os.mkfifo(self.fifo_socket)
522         
523         print "Listening on %s" % self.fifo_socket
524
525
526     def _setup_fifo_watcher(self, fifo_socket):
527         '''Open fifo socket fd and setup gobject IO_IN & IO_HUP watchers.
528         Also log the creation of a fd and store the the internal
529         self._watchers dictionary along with the filename of the fd.'''
530
531         if fifo_socket in self._fifos.keys():
532             fd, watchers = self._fifos[fifo_socket]
533             os.close(fd)
534             for watcherid in watchers.keys():
535                 gobject.source_remove(watchers[watcherid])
536                 del watchers[watcherid]
537
538             del self._fifos[fifo_socket]
539         
540         # Re-open fifo and add listeners.
541         fd = os.open(fifo_socket, os.O_RDONLY | os.O_NONBLOCK)
542         watchers = {}
543         self._fifos[fifo_socket] = (fd, watchers)
544         watcher = lambda key, id: watchers.__setitem__(key, id)
545         
546         # Watch for incoming data.
547         gid = gobject.io_add_watch(fd, gobject.IO_IN, self.main_fifo_read)
548         watcher('main-fifo-read', gid)
549         
550         # Watch for fifo hangups.
551         gid = gobject.io_add_watch(fd, gobject.IO_HUP, self.main_fifo_hangup)
552         watcher('main-fifo-hangup', gid)
553         
554     
555     def run(self):
556         '''UzblTabbed main function that calls the gtk loop.'''
557
558         # Update tablist timer
559         #timer = "update-tablist"
560         #timerid = gobject.timeout_add(500, self.update_tablist,timer)
561         #self._timers[timer] = timerid
562         
563         # Probe clients every second for window titles and location
564         timer = "probe-clients"
565         timerid = gobject.timeout_add(1000, self.probe_clients, timer)
566         self._timers[timer] = timerid
567
568         gtk.main()
569
570
571     def probe_clients(self, timer_call):
572         '''Probe all uzbl clients for up-to-date window titles and uri's.'''
573         
574         sockd = {}
575         uriinventory = []
576         tabskeys = self.tabs.keys()
577         notebooklist = list(self.notebook)
578
579         for tab in notebooklist:
580             if tab not in tabskeys: continue
581             uzbl = self.tabs[tab]
582             uriinventory.append(uzbl.uri)
583             uzbl.probe()
584             if uzbl._socket:
585                 sockd[uzbl._socket] = uzbl
586
587         self._tabsuris = uriinventory
588         self._curpage = self.notebook.get_current_page()
589
590         sockets = sockd.keys()
591         (reading, _, errors) = select.select(sockets, [], sockets, 0)
592         
593         for sock in reading:
594             uzbl = sockd[sock]
595             uzbl._buffer = sock.recv(1024).replace('\n',' ')
596             temp = uzbl._buffer.split(uzbl._marker)
597             self._buffer = temp.pop()
598             cmds = [s.strip().split() for s in temp if len(s.strip())]
599             for cmd in cmds:
600                 try:
601                     #print cmd
602                     self.parse_command(cmd)
603
604                 except:
605                     error("parse_command: invalid command %s" % ' '.join(cmd))
606                     raise
607
608         return True
609
610
611     def main_fifo_hangup(self, fd, cb_condition):
612         '''Handle main fifo socket hangups.'''
613         
614         # Close fd, re-open fifo_socket and watch.
615         self._setup_fifo_watcher(self.fifo_socket)
616
617         # And to kill any gobject event handlers calling this function:
618         return False
619
620
621     def main_fifo_read(self, fd, cb_condition):
622         '''Read from main fifo socket.'''
623
624         self._buffer = os.read(fd, 1024)
625         temp = self._buffer.split("\n")
626         self._buffer = temp.pop()
627         cmds = [s.strip().split() for s in temp if len(s.strip())]
628
629         for cmd in cmds:
630             try:
631                 #print cmd
632                 self.parse_command(cmd)
633
634             except:
635                 error("parse_command: invalid command %s" % ' '.join(cmd))
636                 raise
637         
638         return True
639
640
641     def parse_command(self, cmd):
642         '''Parse instructions from uzbl child processes.'''
643         
644         # Commands ( [] = optional, {} = required ) 
645         # new [uri]
646         #   open new tab and head to optional uri. 
647         # close [tab-num] 
648         #   close current tab or close via tab id.
649         # next [n-tabs]
650         #   open next tab or n tabs down. Supports negative indexing.
651         # prev [n-tabs]
652         #   open prev tab or n tabs down. Supports negative indexing.
653         # goto {tab-n}
654         #   goto tab n.  
655         # first
656         #   goto first tab.
657         # last
658         #   goto last tab. 
659         # title {pid} {document-title}
660         #   updates tablist title.
661         # uri {pid} {document-location}
662
663         if cmd[0] == "new":
664             if len(cmd) == 2:
665                 self.new_tab(cmd[1])
666
667             else:
668                 self.new_tab()
669
670         elif cmd[0] == "newfromclip":
671             uri = subprocess.Popen(['xclip','-selection','clipboard','-o'],\
672               stdout=subprocess.PIPE).communicate()[0]
673             if uri:
674                 self.new_tab(uri)
675
676         elif cmd[0] == "close":
677             if len(cmd) == 2:
678                 self.close_tab(int(cmd[1]))
679
680             else:
681                 self.close_tab()
682
683         elif cmd[0] == "next":
684             if len(cmd) == 2:
685                 self.next_tab(int(cmd[1]))
686                    
687             else:
688                 self.next_tab()
689
690         elif cmd[0] == "prev":
691             if len(cmd) == 2:
692                 self.prev_tab(int(cmd[1]))
693
694             else:
695                 self.prev_tab()
696         
697         elif cmd[0] == "goto":
698             self.goto_tab(int(cmd[1]))
699
700         elif cmd[0] == "first":
701             self.goto_tab(0)
702
703         elif cmd[0] == "last":
704             self.goto_tab(-1)
705
706         elif cmd[0] in ["title", "uri"]:
707             if len(cmd) > 2:
708                 uzbl = self.get_tab_by_pid(int(cmd[1]))
709                 if uzbl:
710                     old = getattr(uzbl, cmd[0])
711                     new = ' '.join(cmd[2:])
712                     setattr(uzbl, cmd[0], new)
713                     if old != new:
714                        self.update_tablist()
715                 else:
716                     error("parse_command: no uzbl with pid %r" % int(cmd[1]))
717         else:
718             error("parse_command: unknown command %r" % ' '.join(cmd))
719
720     
721     def get_tab_by_pid(self, pid):
722         '''Return uzbl instance by pid.'''
723
724         for tab in self.tabs.keys():
725             if self.tabs[tab].pid == pid:
726                 return self.tabs[tab]
727
728         return False
729    
730
731     def new_tab(self, uri='', switch=None):
732         '''Add a new tab to the notebook and start a new instance of uzbl.
733         Use the switch option to negate config['switch_to_new_tabs'] option 
734         when you need to load multiple tabs at a time (I.e. like when 
735         restoring a session from a file).'''
736        
737         pid = self.next_pid()
738         tab = gtk.Socket()
739         tab.show()
740         self.notebook.append_page(tab)
741         sid = tab.get_id()
742         
743         fifo_filename = 'uzbl_fifo_%s_%0.2d' % (self.wid, pid)
744         fifo_socket = os.path.join(config['fifo_dir'], fifo_filename)
745         socket_filename = 'uzbl_socket_%s_%0.2d' % (self.wid, pid)
746         socket_file = os.path.join(config['socket_dir'], socket_filename)
747         
748         if switch is None:
749             switch = config['switch_to_new_tabs']
750
751
752         # Create meta-instance and spawn child
753         if len(uri.strip()):
754             uri = '--uri %s' % uri
755
756         uzbl = self.UzblInstance(self, tab, fifo_socket, socket_file, pid,\
757           uri, switch)
758         self.tabs[tab] = uzbl
759         cmd = 'uzbl -s %s -n %s_%0.2d %s &' % (sid, self.wid, pid, uri)
760         subprocess.Popen([cmd], shell=True) # TODO: do i need close_fds=True ?
761         
762         # Add gobject timer to make sure the config is pushed when fifo socket
763         # has been created. 
764         timerid = gobject.timeout_add(100, uzbl.flush, "flush-initial-config")
765         uzbl.timers['flush-initial-config'] = timerid
766     
767         self.update_tablist()
768
769
770     def config_uzbl(self, uzbl):
771         '''Send bind commands for tab new/close/next/prev to a uzbl 
772         instance.'''
773
774         binds = []
775         bind_format = 'bind %s = sh "echo \\\"%s\\\" > \\\"%s\\\""'
776         bind = lambda key, action: binds.append(bind_format % (key, action, \
777           self.fifo_socket))
778         
779         # Keys are defined in the config section
780         # bind ( key , command back to fifo ) 
781         bind(config['bind_new_tab'], 'new')
782         bind(config['bind_tab_from_clip'], 'newfromclip')
783         bind(config['bind_tab_from_uri'], 'new %s')
784         bind(config['bind_close_tab'], 'close')
785         bind(config['bind_next_tab'], 'next')
786         bind(config['bind_prev_tab'], 'prev')
787         bind(config['bind_goto_tab'], 'goto %s')
788         bind(config['bind_goto_first'], 'goto 0')
789         bind(config['bind_goto_last'], 'goto -1')
790
791         # uzbl.send via socket or uzbl.write via fifo, I'll try send. 
792         uzbl.send("\n".join(binds))
793
794
795     def goto_tab(self, index):
796         '''Goto tab n (supports negative indexing).'''
797         
798         tabs = list(self.notebook)
799         if 0 <= index < len(tabs):
800             self.notebook.set_current_page(index)
801             self.update_tablist()
802             return None
803
804         try: 
805             tab = tabs[index]
806             # Update index because index might have previously been a
807             # negative index. 
808             index = tabs.index(tab)
809             self.notebook.set_current_page(index)
810             self.update_tablist()
811         
812         except IndexError:
813             pass
814
815
816     def next_tab(self, step=1):
817         '''Switch to next tab or n tabs right.'''
818         
819         if step < 1:
820             error("next_tab: invalid step %r" % step)
821             return None
822                 
823         ntabs = self.notebook.get_n_pages()
824         tabn = (self.notebook.get_current_page() + step) % ntabs
825         self.notebook.set_current_page(tabn)
826         self.update_tablist()
827
828
829     def prev_tab(self, step=1):
830         '''Switch to prev tab or n tabs left.'''
831         
832         if step < 1:
833             error("prev_tab: invalid step %r" % step)
834             return None
835
836         ntabs = self.notebook.get_n_pages()
837         tabn = self.notebook.get_current_page() - step
838         while tabn < 0: tabn += ntabs
839         self.notebook.set_current_page(tabn)
840         self.update_tablist()
841
842
843     def close_tab(self, tabn=None):
844         '''Closes current tab. Supports negative indexing.'''
845         
846         if tabn is None: 
847             tabn = self.notebook.get_current_page()
848         
849         else:
850             try: 
851                 tab = list(self.notebook)[tabn]
852         
853             except IndexError:
854                 error("close_tab: invalid index %r" % tabn)
855                 return None
856
857         self.notebook.remove_page(tabn)
858
859     
860     def tab_opened(self, notebook, tab, index):
861         '''Called upon tab creation. Called by page-added signal.'''
862         
863         if config['switch_to_new_tabs']:
864             self.notebook.set_focus_child(tab)
865
866         else:
867             oldindex = self.notebook.get_current_page()
868             oldtab = self.notebook.get_nth_page(oldindex)
869             self.notebook.set_focus_child(oldtab)
870
871
872     def tab_closed(self, notebook, tab, index):
873         '''Close the window if no tabs are left. Called by page-removed 
874         signal.'''
875         
876         if tab in self.tabs.keys():
877             uzbl = self.tabs[tab]
878             for timer in uzbl.timers.keys():
879                 error("tab_closed: removing timer %r" % timer)
880                 gobject.source_remove(uzbl.timers[timer])
881             
882             if uzbl._socket:
883                 uzbl._socket.close()
884                 uzbl._socket = None
885
886             uzbl._fifoout = []
887             uzbl._socketout = []
888             uzbl._kill = True
889             del self.tabs[tab]
890         
891         if self.notebook.get_n_pages() == 0:
892             self.quit()
893
894         self.update_tablist()
895
896         return True
897
898
899     def tab_changed(self, notebook, page, index):
900         '''Refresh tab list. Called by switch-page signal.'''
901         
902         tab = self.notebook.get_nth_page(index)
903         self.notebook.set_focus_child(tab)
904         self.update_tablist(index)
905         return True
906
907
908     def update_tablist(self, curpage=None):
909         '''Upate tablist status bar.'''
910     
911         show_tablist = config['show_tablist']
912         show_gtk_tabs = config['show_gtk_tabs']
913         tab_titles = config['tab_titles']
914         show_ellipsis = config['show_ellipsis']
915         if not show_tablist and not show_gtk_tabs:
916             return True
917
918         tabs = self.tabs.keys()
919         if curpage is None:
920             curpage = self.notebook.get_current_page()
921
922         title_format = "%s - Uzbl Browser"
923         max_title_len = config['max_title_len']
924
925         if show_tablist:
926             pango = ""
927             normal = (config['tab_colours'], config['tab_text_colours'])
928             selected = (config['selected_tab'], config['selected_tab_text'])
929             if tab_titles:
930                 tab_format = "<span %s> [ %d <span %s> %s</span> ] </span>"
931             else:
932                 tab_format = "<span %s> [ <span %s>%d</span> ] </span>"
933        
934         if show_gtk_tabs:
935             gtk_tab_format = "%d %s"
936
937         for index, tab in enumerate(self.notebook):
938             if tab not in tabs: continue
939             uzbl = self.tabs[tab]
940             
941             if index == curpage:
942                 self.window.set_title(title_format % uzbl.title)
943             
944             tabtitle = uzbl.title[:max_title_len]
945             if show_ellipsis and len(tabtitle) != len(uzbl.title):
946                 tabtitle = "%s\xe2\x80\xa6" % tabtitle[:-1] # Show Ellipsis
947
948             if show_gtk_tabs:
949                 if tab_titles:
950                     self.notebook.set_tab_label_text(tab,\
951                       gtk_tab_format % (index, tabtitle))
952                 else:
953                     self.notebook.set_tab_label_text(tab, str(index))
954
955             if show_tablist:
956                 style = colour_selector(index, curpage, uzbl)
957                 (tabc, textc) = style
958
959                 if tab_titles:
960                     pango += tab_format % (tabc, index, textc, tabtitle)
961                 else:
962                     pango += tab_format % (tabc, textc, index)
963         
964         if show_tablist:
965             self.tablist.set_markup(pango)
966
967         return True
968
969
970     def quit(self, *args):
971         '''Cleanup the application and quit. Called by delete-event signal.'''
972         
973         for fifo_socket in self._fifos.keys():
974             fd, watchers = self._fifos[fifo_socket]
975             os.close(fd)
976             for watcherid in watchers.keys():
977                 gobject.source_remove(watchers[watcherid])
978                 del watchers[watcherid]
979
980             del self._fifos[fifo_socket]
981         
982         for timerid in self._timers.keys():
983             gobject.source_remove(self._timers[timerid])
984             del self._timers[timerid]
985
986         if os.path.exists(self.fifo_socket):
987             os.unlink(self.fifo_socket)
988             print "Unlinked %s" % self.fifo_socket
989
990         if config['save_session']:
991             session_file = config['session_file']
992             if len(self._tabsuris):
993                 if not os.path.isfile(session_file):
994                     dirname = os.path.dirname(session_file)
995                     if not os.path.isdir(dirname):
996                         # Recursive mkdir not rmdir. 
997                         rmkdir(dirname)
998                 
999                 sessionstr = '\n'.join(self._tabsuris)
1000                 h = open(session_file, 'w')
1001                 h.write('current = %s\n%s' % (self._curpage, sessionstr))
1002                 h.close()
1003                 
1004             else:
1005                 # Notebook has no pages so delete session file if it exists.
1006                 if os.path.isfile(session_file):
1007                     os.remove(session_file)
1008
1009         gtk.main_quit() 
1010
1011
1012 if __name__ == "__main__":
1013     
1014     # Read from the uzbl config into the global config dictionary. 
1015     readconfig(uzbl_config, config)
1016      
1017     uzbl = UzblTabbed()
1018     
1019     if os.path.isfile(os.path.expandvars(config['session_file'])):
1020         h = open(os.path.expandvars(config['session_file']),'r')
1021         lines = [line.strip() for line in h.readlines()]
1022         h.close()
1023         current = 0
1024         urls = []
1025         for line in lines:
1026             if line.startswith("current"):
1027                 current = int(line.split()[-1])
1028
1029             else:
1030                 urls.append(line.strip())
1031
1032         for (index, url) in enumerate(urls):
1033             if current == index:
1034                 uzbl.new_tab(line, True)
1035             
1036             else:        
1037                 uzbl.new_tab(line, False)
1038
1039         if not len(urls):
1040             self.new_tab()
1041
1042     else:
1043         uzbl.new_tab()
1044
1045     uzbl.run()
1046
1047