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