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