indentation fix
[uzbl-mobile] / examples / data / uzbl / scripts / uzbl_tabbed.py
1 #!/usr/bin/env 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 #   Romain Bignon <romain@peerfuse.org>
39 #       Fix for session restoration code.
40
41
42 # Dependencies:
43 #   pygtk - python bindings for gtk.
44 #   pango - python bindings needed for text rendering & layout in gtk widgets.
45 #   pygobject - GLib's GObject bindings for python.
46 #
47 # Optional dependencies:
48 #   simplejson - save uzbl_tabbed.py sessions & presets in json.
49 #
50 # Note: I haven't included version numbers with this dependency list because 
51 # I've only ever tested uzbl_tabbed.py on the latest stable versions of these
52 # packages in Gentoo's portage. Package names may vary on different systems.
53
54
55 # Configuration:
56 # Because this version of uzbl_tabbed is able to inherit options from your main
57 # uzbl configuration file you may wish to configure uzbl tabbed from there.
58 # Here is a list of configuration options that can be customised and some
59 # example values for each:
60 #
61 # General tabbing options:
62 #   show_tablist            = 1
63 #   show_gtk_tabs           = 0
64 #   tablist_top             = 1
65 #   gtk_tab_pos             = (top|left|bottom|right)
66 #   switch_to_new_tabs      = 1
67 #   capture_new_windows     = 1
68 #
69 # Tab title options:
70 #   tab_titles              = 1
71 #   new_tab_title           = Loading
72 #   max_title_len           = 50
73 #   show_ellipsis           = 1
74 #
75 # Session options:
76 #   save_session            = 1
77 #   json_session            = 0
78 #   session_file            = $HOME/.local/share/uzbl/session
79 #
80 # Inherited uzbl options:
81 #   fifo_dir                = /tmp
82 #   socket_dir              = /tmp
83 #   icon_path               = $HOME/.local/share/uzbl/uzbl.png
84 #   status_background       = #303030
85 #
86 # Window options:
87 #   window_size             = 800,800
88 #
89 # And the key bindings:
90 #   bind_new_tab            = gn
91 #   bind_tab_from_clip      = gY
92 #   bind_tab_from_uri       = go _
93 #   bind_close_tab          = gC
94 #   bind_next_tab           = gt
95 #   bind_prev_tab           = gT
96 #   bind_goto_tab           = gi_
97 #   bind_goto_first         = g<
98 #   bind_goto_last          = g>
99 #   bind_clean_slate        = gQ
100 #
101 # Session preset key bindings:
102 #   bind_save_preset       = gsave _
103 #   bind_load_preset       = gload _
104 #   bind_del_preset        = gdel _
105 #   bind_list_presets      = glist
106 #
107 # And uzbl_tabbed.py takes care of the actual binding of the commands via each
108 # instances fifo socket.
109 #
110 # Custom tab styling:
111 #   tab_colours             = foreground = "#888" background = "#303030"
112 #   tab_text_colours        = foreground = "#bbb"
113 #   selected_tab            = foreground = "#fff"
114 #   selected_tab_text       = foreground = "green"
115 #   tab_indicate_https      = 1
116 #   https_colours           = foreground = "#888"
117 #   https_text_colours      = foreground = "#9c8e2d"
118 #   selected_https          = foreground = "#fff"
119 #   selected_https_text     = foreground = "gold"
120 #
121 # How these styling values are used are soley defined by the syling policy
122 # handler below (the function in the config section). So you can for example
123 # turn the tab text colour Firetruck-Red in the event "error" appears in the
124 # tab title or some other arbitrary event. You may wish to make a trusted
125 # hosts file and turn tab titles of tabs visiting trusted hosts purple.
126
127
128 # Issues:
129 #   - new windows are not caught and opened in a new tab.
130 #   - when uzbl_tabbed.py crashes it takes all the children with it.
131 #   - when a new tab is opened when using gtk tabs the tab button itself
132 #     grabs focus from its child for a few seconds.
133 #   - when switch_to_new_tabs is not selected the notebook page is
134 #     maintained but the new window grabs focus (try as I might to stop it).
135
136
137 # Todo:
138 #   - add command line options to use a different session file, not use a
139 #     session file and or open a uri on starup.
140 #   - ellipsize individual tab titles when the tab-list becomes over-crowded
141 #   - add "<" & ">" arrows to tablist to indicate that only a subset of the
142 #     currently open tabs are being displayed on the tablist.
143 #   - add the small tab-list display when both gtk tabs and text vim-like
144 #     tablist are hidden (I.e. [ 1 2 3 4 5 ])
145 #   - check spelling.
146 #   - pass a uzbl socketid to uzbl_tabbed.py and have it assimilated into
147 #     the collective. Resistance is futile!
148
149
150 import pygtk
151 import gtk
152 import subprocess
153 import os
154 import re
155 import time
156 import getopt
157 import pango
158 import select
159 import sys
160 import gobject
161 import socket
162 import random
163 import hashlib
164
165 pygtk.require('2.0')
166
167 def error(msg):
168     sys.stderr.write("%s\n"%msg)
169
170
171 # ============================================================================
172 # ::: Default configuration section ::::::::::::::::::::::::::::::::::::::::::
173 # ============================================================================
174
175 # Location of your uzbl data directory.
176 if 'XDG_DATA_HOME' in os.environ.keys() and os.environ['XDG_DATA_HOME']:
177     data_dir = os.path.join(os.environ['XDG_DATA_HOME'], 'uzbl/')
178 else:
179     data_dir = os.path.join(os.environ['HOME'], '.local/share/uzbl/')
180 if not os.path.exists(data_dir):
181     error("Warning: uzbl data_dir does not exist: %r" % data_dir)
182
183 # Location of your uzbl configuration file.
184 if 'XDG_CONFIG_HOME' in os.environ.keys() and os.environ['XDG_CONFIG_HOME']:
185     uzbl_config = os.path.join(os.environ['XDG_CONFIG_HOME'], 'uzbl/config')
186 else:
187     uzbl_config = os.path.join(os.environ['HOME'],'.config/uzbl/config')
188 if not os.path.exists(uzbl_config):
189     error("Warning: Cannot locate your uzbl_config file %r" % uzbl_config)
190
191 # All of these settings can be inherited from your uzbl config file.
192 config = {
193   # Tab options
194   'show_tablist':           True,   # Show text uzbl like statusbar tab-list
195   'show_gtk_tabs':          False,  # Show gtk notebook tabs
196   'tablist_top':            True,   # Display tab-list at top of window
197   'gtk_tab_pos':            'top',  # Gtk tab position (top|left|bottom|right)
198   'switch_to_new_tabs':     True,   # Upon opening a new tab switch to it
199   'capture_new_windows':    True,   # Use uzbl_tabbed to catch new windows
200
201   # Tab title options
202   'tab_titles':             True,   # Display tab titles (else only tab-nums)
203   'new_tab_title':          'Loading', # New tab title
204   'max_title_len':          50,     # Truncate title at n characters
205   'show_ellipsis':          True,   # Show ellipsis when truncating titles
206
207   # Session options
208   'save_session':           True,   # Save session in file when quit
209   'json_session':           False,   # Use json to save session.
210   'saved_sessions_dir':     os.path.join(data_dir, 'sessions/'),
211   'session_file':           os.path.join(data_dir, 'session'),
212
213   # Inherited uzbl options
214   'fifo_dir':               '/tmp', # Path to look for uzbl fifo.
215   'socket_dir':             '/tmp', # Path to look for uzbl socket.
216   'icon_path':              os.path.join(data_dir, 'uzbl.png'),
217   'status_background':      "#303030", # Default background for all panels.
218
219   # Window options
220   'window_size':            "800,800", # width,height in pixels.
221
222   # Key bindings
223   'bind_new_tab':           'gn',   # Open new tab.
224   'bind_tab_from_clip':     'gY',   # Open tab from clipboard.
225   'bind_tab_from_uri':      'go _', # Open new tab and goto entered uri.
226   'bind_close_tab':         'gC',   # Close tab.
227   'bind_next_tab':          'gt',   # Next tab.
228   'bind_prev_tab':          'gT',   # Prev tab.
229   'bind_goto_tab':          'gi_',  # Goto tab by tab-number (in title).
230   'bind_goto_first':        'g<',   # Goto first tab.
231   'bind_goto_last':         'g>',   # Goto last tab.
232   'bind_clean_slate':       'gQ',   # Close all tabs and open new tab.
233
234   # Session preset key bindings
235   'bind_save_preset':       'gsave _', # Save session to file %s.
236   'bind_load_preset':       'gload _', # Load preset session from file %s.
237   'bind_del_preset':        'gdel _',  # Delete preset session %s.
238   'bind_list_presets':      'glist',   # List all session presets.
239
240   # Add custom tab style definitions to be used by the tab colour policy
241   # handler here. Because these are added to the config dictionary like
242   # any other uzbl_tabbed configuration option remember that they can
243   # be superseeded from your main uzbl config file.
244   'tab_colours':            'foreground = "#888" background = "#303030"',
245   'tab_text_colours':       'foreground = "#bbb"',
246   'selected_tab':           'foreground = "#fff"',
247   'selected_tab_text':      'foreground = "green"',
248   'tab_indicate_https':     True,
249   'https_colours':          'foreground = "#888"',
250   'https_text_colours':     'foreground = "#9c8e2d"',
251   'selected_https':         'foreground = "#fff"',
252   'selected_https_text':    'foreground = "gold"',
253
254   } # End of config dict.
255
256 # This is the tab style policy handler. Every time the tablist is updated
257 # this function is called to determine how to colourise that specific tab
258 # according the simple/complex rules as defined here. You may even wish to
259 # move this function into another python script and import it using:
260 #   from mycustomtabbingconfig import colour_selector
261 # Remember to rename, delete or comment out this function if you do that.
262
263 def colour_selector(tabindex, currentpage, uzbl):
264     '''Tablist styling policy handler. This function must return a tuple of
265     the form (tab style, text style).'''
266
267     # Just as an example:
268     # if 'error' in uzbl.title:
269     #     if tabindex == currentpage:
270     #         return ('foreground="#fff"', 'foreground="red"')
271     #     return ('foreground="#888"', 'foreground="red"')
272
273     # Style tabs to indicate connected via https.
274     if config['tab_indicate_https'] and uzbl.uri.startswith("https://"):
275         if tabindex == currentpage:
276             return (config['selected_https'], config['selected_https_text'])
277         return (config['https_colours'], config['https_text_colours'])
278
279     # Style to indicate selected.
280     if tabindex == currentpage:
281         return (config['selected_tab'], config['selected_tab_text'])
282
283     # Default tab style.
284     return (config['tab_colours'], config['tab_text_colours'])
285
286
287 # ============================================================================
288 # ::: End of configuration section :::::::::::::::::::::::::::::::::::::::::::
289 # ============================================================================
290
291
292 def readconfig(uzbl_config, config):
293     '''Loads relevant config from the users uzbl config file into the global
294     config dictionary.'''
295
296     if not os.path.exists(uzbl_config):
297         error("Unable to load config %r" % uzbl_config)
298         return None
299
300     # Define parsing regular expressions
301     isint = re.compile("^(\-|)[0-9]+$").match
302     findsets = re.compile("^set\s+([^\=]+)\s*\=\s*(.+)$",\
303       re.MULTILINE).findall
304
305     h = open(os.path.expandvars(uzbl_config), 'r')
306     rawconfig = h.read()
307     h.close()
308
309     configkeys, strip = config.keys(), str.strip
310     for (key, value) in findsets(rawconfig):
311         key, value = strip(key), strip(value)
312         if key not in configkeys: continue
313         if isint(value): value = int(value)
314         config[key] = value
315
316     # Ensure that config keys that relate to paths are expanded.
317     expand = ['fifo_dir', 'socket_dir', 'session_file', 'icon_path']
318     for key in expand:
319         config[key] = os.path.expandvars(config[key])
320
321
322 def counter():
323     '''To infinity and beyond!'''
324
325     i = 0
326     while True:
327         i += 1
328         yield i
329
330
331 def escape(s):
332     '''Replaces html markup in tab titles that screw around with pango.'''
333
334     for (split, glue) in [('&','&amp;'), ('<', '&lt;'), ('>', '&gt;')]:
335         s = s.replace(split, glue)
336     return s
337
338
339 def gen_endmarker():
340     '''Generates a random md5 for socket message-termination endmarkers.'''
341
342     return hashlib.md5(str(random.random()*time.time())).hexdigest()
343
344
345 class UzblTabbed:
346     '''A tabbed version of uzbl using gtk.Notebook'''
347
348     class UzblInstance:
349         '''Uzbl instance meta-data/meta-action object.'''
350
351         def __init__(self, parent, tab, fifo_socket, socket_file, pid,\
352           uri, title, switch):
353
354             self.parent = parent
355             self.tab = tab
356             self.fifo_socket = fifo_socket
357             self.socket_file = socket_file
358             self.pid = pid
359             self.title = title
360             self.uri = uri
361             self.timers = {}
362             self._lastprobe = 0
363             self._fifoout = []
364             self._socketout = []
365             self._socket = None
366             self._buffer = ""
367             # Switch to tab after loading
368             self._switch = switch
369             # fifo/socket files exists and socket connected.
370             self._connected = False
371             # The kill switch
372             self._kill = False
373
374             # Message termination endmarker.
375             self._marker = gen_endmarker()
376
377             # Gen probe commands string
378             probes = []
379             probe = probes.append
380             probe('print uri %d @uri %s' % (self.pid, self._marker))
381             probe('print title %d @<document.title>@ %s' % (self.pid,\
382               self._marker))
383             self._probecmds = '\n'.join(probes)
384
385             # Enqueue keybinding config for child uzbl instance
386             self.parent.config_uzbl(self)
387
388
389         def flush(self, timer_call=False):
390             '''Flush messages from the socket-out and fifo-out queues.'''
391
392             if self._kill:
393                 if self._socket:
394                     self._socket.close()
395                     self._socket = None
396
397                 error("Flush called on dead tab.")
398                 return False
399
400             if len(self._fifoout):
401                 if os.path.exists(self.fifo_socket):
402                     h = open(self.fifo_socket, 'w')
403                     while len(self._fifoout):
404                         msg = self._fifoout.pop(0)
405                         h.write("%s\n"%msg)
406                     h.close()
407
408             if len(self._socketout):
409                 if not self._socket and os.path.exists(self.socket_file):
410                     sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
411                     sock.connect(self.socket_file)
412                     self._socket = sock
413
414                 if self._socket:
415                     while len(self._socketout):
416                         msg = self._socketout.pop(0)
417                         self._socket.send("%s\n"%msg)
418
419             if not self._connected and timer_call:
420                 if not len(self._fifoout + self._socketout):
421                     self._connected = True
422
423                     if timer_call in self.timers.keys():
424                         gobject.source_remove(self.timers[timer_call])
425                         del self.timers[timer_call]
426
427                     if self._switch:
428                         self.grabfocus()
429
430             return len(self._fifoout + self._socketout)
431
432
433         def grabfocus(self):
434             '''Steal parent focus and switch the notebook to my own tab.'''
435
436             tabs = list(self.parent.notebook)
437             tabid = tabs.index(self.tab)
438             self.parent.goto_tab(tabid)
439
440
441         def probe(self):
442             '''Probes the client for information about its self.'''
443
444             if self._connected:
445                 self.send(self._probecmds)
446                 self._lastprobe = time.time()
447
448
449         def write(self, msg):
450             '''Child fifo write function.'''
451
452             self._fifoout.append(msg)
453             # Flush messages from the queue if able.
454             return self.flush()
455
456
457         def send(self, msg):
458             '''Child socket send function.'''
459
460             self._socketout.append(msg)
461             # Flush messages from queue if able.
462             return self.flush()
463
464
465     def __init__(self):
466         '''Create tablist, window and notebook.'''
467
468         self._fifos = {}
469         self._timers = {}
470         self._buffer = ""
471         self._killed = False
472         
473         # A list of the recently closed tabs
474         self._closed = []
475
476         # Holds metadata on the uzbl childen open.
477         self.tabs = {}
478
479         # Generates a unique id for uzbl socket filenames.
480         self.next_pid = counter().next
481
482         # Create main window
483         self.window = gtk.Window()
484         try:
485             window_size = map(int, config['window_size'].split(','))
486             self.window.set_default_size(*window_size)
487
488         except:
489             error("Invalid value for default_size in config file.")
490
491         self.window.set_title("Uzbl Browser")
492         self.window.set_border_width(0)
493
494         # Set main window icon
495         icon_path = config['icon_path']
496         if os.path.exists(icon_path):
497             self.window.set_icon(gtk.gdk.pixbuf_new_from_file(icon_path))
498
499         else:
500             icon_path = '/usr/share/uzbl/examples/data/uzbl/uzbl.png'
501             if os.path.exists(icon_path):
502                 self.window.set_icon(gtk.gdk.pixbuf_new_from_file(icon_path))
503
504         # Attach main window event handlers
505         self.window.connect("delete-event", self.quitrequest)
506
507         # Create tab list
508         if config['show_tablist']:
509             vbox = gtk.VBox()
510             self.window.add(vbox)
511             ebox = gtk.EventBox()
512             self.tablist = gtk.Label()
513             self.tablist.set_use_markup(True)
514             self.tablist.set_justify(gtk.JUSTIFY_LEFT)
515             self.tablist.set_line_wrap(False)
516             self.tablist.set_selectable(False)
517             self.tablist.set_padding(2,2)
518             self.tablist.set_alignment(0,0)
519             self.tablist.set_ellipsize(pango.ELLIPSIZE_END)
520             self.tablist.set_text(" ")
521             self.tablist.show()
522             ebox.add(self.tablist)
523             ebox.show()
524             bgcolor = gtk.gdk.color_parse(config['status_background'])
525             ebox.modify_bg(gtk.STATE_NORMAL, bgcolor)
526
527         # Create notebook
528         self.notebook = gtk.Notebook()
529         self.notebook.set_show_tabs(config['show_gtk_tabs'])
530
531         # Set tab position
532         allposes = {'left': gtk.POS_LEFT, 'right':gtk.POS_RIGHT,
533           'top':gtk.POS_TOP, 'bottom':gtk.POS_BOTTOM}
534         if config['gtk_tab_pos'] in allposes.keys():
535             self.notebook.set_tab_pos(allposes[config['gtk_tab_pos']])
536
537         self.notebook.set_show_border(False)
538         self.notebook.set_scrollable(True)
539         self.notebook.set_border_width(0)
540
541         self.notebook.connect("page-removed", self.tab_closed)
542         self.notebook.connect("switch-page", self.tab_changed)
543         self.notebook.connect("page-added", self.tab_opened)
544
545         self.notebook.show()
546         if config['show_tablist']:
547             if config['tablist_top']:
548                 vbox.pack_start(ebox, False, False, 0)
549                 vbox.pack_end(self.notebook, True, True, 0)
550
551             else:
552                 vbox.pack_start(self.notebook, True, True, 0)
553                 vbox.pack_end(ebox, False, False, 0)
554
555             vbox.show()
556
557         else:
558             self.window.add(self.notebook)
559
560         self.window.show()
561         self.wid = self.notebook.window.xid
562
563         # Create the uzbl_tabbed fifo
564         fifo_filename = 'uzbltabbed_%d' % os.getpid()
565         self.fifo_socket = os.path.join(config['fifo_dir'], fifo_filename)
566         self._create_fifo_socket(self.fifo_socket)
567         self._setup_fifo_watcher(self.fifo_socket)
568         
569
570     def _create_fifo_socket(self, fifo_socket):
571         '''Create interprocess communication fifo socket.'''
572
573         if os.path.exists(fifo_socket):
574             if not os.access(fifo_socket, os.F_OK | os.R_OK | os.W_OK):
575                 os.mkfifo(fifo_socket)
576
577         else:
578             basedir = os.path.dirname(self.fifo_socket)
579             if not os.path.exists(basedir):
580                 os.makedirs(basedir)
581
582             os.mkfifo(self.fifo_socket)
583
584         print "Listening on %s" % self.fifo_socket
585
586
587     def _setup_fifo_watcher(self, fifo_socket):
588         '''Open fifo socket fd and setup gobject IO_IN & IO_HUP watchers.
589         Also log the creation of a fd and store the the internal
590         self._watchers dictionary along with the filename of the fd.'''
591
592         if fifo_socket in self._fifos.keys():
593             fd, watchers = self._fifos[fifo_socket]
594             os.close(fd)
595             for (watcherid, gid) in watchers.items():
596                 gobject.source_remove(gid)
597                 del watchers[watcherid]
598
599             del self._fifos[fifo_socket]
600
601         # Re-open fifo and add listeners.
602         fd = os.open(fifo_socket, os.O_RDONLY | os.O_NONBLOCK)
603         watchers = {}
604         self._fifos[fifo_socket] = (fd, watchers)
605         watcher = lambda key, id: watchers.__setitem__(key, id)
606
607         # Watch for incoming data.
608         gid = gobject.io_add_watch(fd, gobject.IO_IN, self.main_fifo_read)
609         watcher('main-fifo-read', gid)
610
611         # Watch for fifo hangups.
612         gid = gobject.io_add_watch(fd, gobject.IO_HUP, self.main_fifo_hangup)
613         watcher('main-fifo-hangup', gid)
614
615
616     def run(self):
617         '''UzblTabbed main function that calls the gtk loop.'''
618         
619         if config['save_session']:
620             self.load_session()
621         
622         if not len(self.tabs):
623             self.new_tab()
624         
625         # Update tablist timer
626         #timer = "update-tablist"
627         #timerid = gobject.timeout_add(500, self.update_tablist,timer)
628         #self._timers[timer] = timerid
629
630         # Probe clients every second for window titles and location
631         timer = "probe-clients"
632         timerid = gobject.timeout_add(1000, self.probe_clients, timer)
633         self._timers[timer] = timerid
634
635         gtk.main()
636
637
638     def probe_clients(self, timer_call):
639         '''Probe all uzbl clients for up-to-date window titles and uri's.'''
640     
641         save_session = config['save_session']
642
643         sockd = {}
644         tabskeys = self.tabs.keys()
645         notebooklist = list(self.notebook)
646
647         for tab in notebooklist:
648             if tab not in tabskeys: continue
649             uzbl = self.tabs[tab]
650             uzbl.probe()
651             if uzbl._socket:
652                 sockd[uzbl._socket] = uzbl          
653
654         sockets = sockd.keys()
655         (reading, _, errors) = select.select(sockets, [], sockets, 0)
656
657         for sock in reading:
658             uzbl = sockd[sock]
659             uzbl._buffer = sock.recv(1024).replace('\n',' ')
660             temp = uzbl._buffer.split(uzbl._marker)
661             self._buffer = temp.pop()
662             cmds = [s.strip().split() for s in temp if len(s.strip())]
663             for cmd in cmds:
664                 try:
665                     #print cmd
666                     self.parse_command(cmd)
667
668                 except:
669                     error("parse_command: invalid command %s" % ' '.join(cmd))
670                     raise
671
672         return True
673
674
675     def main_fifo_hangup(self, fd, cb_condition):
676         '''Handle main fifo socket hangups.'''
677
678         # Close fd, re-open fifo_socket and watch.
679         self._setup_fifo_watcher(self.fifo_socket)
680
681         # And to kill any gobject event handlers calling this function:
682         return False
683
684
685     def main_fifo_read(self, fd, cb_condition):
686         '''Read from main fifo socket.'''
687
688         self._buffer = os.read(fd, 1024)
689         temp = self._buffer.split("\n")
690         self._buffer = temp.pop()
691         cmds = [s.strip().split() for s in temp if len(s.strip())]
692
693         for cmd in cmds:
694             try:
695                 #print cmd
696                 self.parse_command(cmd)
697
698             except:
699                 error("parse_command: invalid command %s" % ' '.join(cmd))
700                 raise
701
702         return True
703
704
705     def parse_command(self, cmd):
706         '''Parse instructions from uzbl child processes.'''
707
708         # Commands ( [] = optional, {} = required )
709         # new [uri]
710         #   open new tab and head to optional uri.
711         # close [tab-num]
712         #   close current tab or close via tab id.
713         # next [n-tabs]
714         #   open next tab or n tabs down. Supports negative indexing.
715         # prev [n-tabs]
716         #   open prev tab or n tabs down. Supports negative indexing.
717         # goto {tab-n}
718         #   goto tab n.
719         # first
720         #   goto first tab.
721         # last
722         #   goto last tab.
723         # title {pid} {document-title}
724         #   updates tablist title.
725         # uri {pid} {document-location}
726
727         if cmd[0] == "new":
728             if len(cmd) == 2:
729                 self.new_tab(cmd[1])
730
731             else:
732                 self.new_tab()
733
734         elif cmd[0] == "newfromclip":
735             uri = subprocess.Popen(['xclip','-selection','clipboard','-o'],\
736               stdout=subprocess.PIPE).communicate()[0]
737             if uri:
738                 self.new_tab(uri)
739
740         elif cmd[0] == "close":
741             if len(cmd) == 2:
742                 self.close_tab(int(cmd[1]))
743
744             else:
745                 self.close_tab()
746
747         elif cmd[0] == "next":
748             if len(cmd) == 2:
749                 self.next_tab(int(cmd[1]))
750
751             else:
752                 self.next_tab()
753
754         elif cmd[0] == "prev":
755             if len(cmd) == 2:
756                 self.prev_tab(int(cmd[1]))
757
758             else:
759                 self.prev_tab()
760
761         elif cmd[0] == "goto":
762             self.goto_tab(int(cmd[1]))
763
764         elif cmd[0] == "first":
765             self.goto_tab(0)
766
767         elif cmd[0] == "last":
768             self.goto_tab(-1)
769
770         elif cmd[0] in ["title", "uri"]:
771             if len(cmd) > 2:
772                 uzbl = self.get_tab_by_pid(int(cmd[1]))
773                 if uzbl:
774                     old = getattr(uzbl, cmd[0])
775                     new = ' '.join(cmd[2:])
776                     setattr(uzbl, cmd[0], new)
777                     if old != new:
778                        self.update_tablist()
779                 else:
780                     error("parse_command: no uzbl with pid %r" % int(cmd[1]))
781
782         elif cmd[0] == "preset":
783             if len(cmd) < 3:
784                 error("parse_command: invalid preset command")
785             
786             elif cmd[1] == "save":
787                 path = os.path.join(config['saved_sessions_dir'], cmd[2])
788                 self.save_session(path)
789
790             elif cmd[1] == "load":
791                 path = os.path.join(config['saved_sessions_dir'], cmd[2])
792                 self.load_session(path)
793
794             elif cmd[1] == "del":
795                 path = os.path.join(config['saved_sessions_dir'], cmd[2])
796                 if os.path.isfile(path):
797                     os.remove(path)
798
799                 else:
800                     error("parse_command: preset %r does not exist." % path)
801             
802             elif cmd[1] == "list":
803                 uzbl = self.get_tab_by_pid(int(cmd[2]))
804                 if uzbl:
805                     if not os.path.isdir(config['saved_sessions_dir']):
806                         js = "js alert('No saved presets.');"
807                         uzbl.send(js)
808
809                     else:
810                         listdir = os.listdir(config['saved_sessions_dir'])
811                         listdir = "\\n".join(listdir)
812                         js = "js alert('Session presets:\\n\\n%s');" % listdir
813                         uzbl.send(js)
814
815                 else:
816                     error("parse_command: unknown tab pid.")
817
818             else:
819                 error("parse_command: unknown parse command %r"\
820                   % ' '.join(cmd))
821
822         elif cmd[0] == "clean":
823             self.clean_slate()
824             
825         else:
826             error("parse_command: unknown command %r" % ' '.join(cmd))
827
828
829     def get_tab_by_pid(self, pid):
830         '''Return uzbl instance by pid.'''
831
832         for (tab, uzbl) in self.tabs.items():
833             if uzbl.pid == pid:
834                 return uzbl
835
836         return False
837
838
839     def new_tab(self, uri='', title='', switch=None):
840         '''Add a new tab to the notebook and start a new instance of uzbl.
841         Use the switch option to negate config['switch_to_new_tabs'] option
842         when you need to load multiple tabs at a time (I.e. like when
843         restoring a session from a file).'''
844
845         pid = self.next_pid()
846         tab = gtk.Socket()
847         tab.show()
848         self.notebook.append_page(tab)
849         sid = tab.get_id()
850         uri = uri.strip()
851
852         fifo_filename = 'uzbl_fifo_%s_%0.2d' % (self.wid, pid)
853         fifo_socket = os.path.join(config['fifo_dir'], fifo_filename)
854         socket_filename = 'uzbl_socket_%s_%0.2d' % (self.wid, pid)
855         socket_file = os.path.join(config['socket_dir'], socket_filename)
856
857         if switch is None:
858             switch = config['switch_to_new_tabs']
859         
860         if not title:
861             title = config['new_tab_title']
862
863         uzbl = self.UzblInstance(self, tab, fifo_socket, socket_file, pid,\
864           uri, title, switch)
865
866         if len(uri):
867             uri = "--uri %r" % uri
868
869         self.tabs[tab] = uzbl
870         cmd = 'uzbl -s %s -n %s_%0.2d %s &' % (sid, self.wid, pid, uri)
871         subprocess.Popen([cmd], shell=True) # TODO: do i need close_fds=True ?
872
873         # Add gobject timer to make sure the config is pushed when fifo socket
874         # has been created.
875         timerid = gobject.timeout_add(100, uzbl.flush, "flush-initial-config")
876         uzbl.timers['flush-initial-config'] = timerid
877
878         self.update_tablist()
879
880
881     def clean_slate(self):
882         '''Close all open tabs and open a fresh brand new one.'''
883
884         self.new_tab()
885         tabs = self.tabs.keys()
886         for tab in list(self.notebook)[:-1]:
887             if tab not in tabs: continue
888             uzbl = self.tabs[tab]
889             uzbl.send("exit")
890     
891
892     def config_uzbl(self, uzbl):
893         '''Send bind commands for tab new/close/next/prev to a uzbl
894         instance.'''
895
896         binds = []
897         bind_format = r'bind %s = sh "echo \"%s\" > \"%s\""'
898         bind = lambda key, action: binds.append(bind_format % (key, action,\
899           self.fifo_socket))
900
901         sets = []
902         set_format = r'set %s = sh \"echo \\"%s\\" > \\"%s\\""'
903         set = lambda key, action: binds.append(set_format % (key, action,\
904           self.fifo_socket))
905
906         # Bind definitions here
907         # bind(key, command back to fifo)
908         bind(config['bind_new_tab'], 'new')
909         bind(config['bind_tab_from_clip'], 'newfromclip')
910         bind(config['bind_tab_from_uri'], 'new %s')
911         bind(config['bind_close_tab'], 'close')
912         bind(config['bind_next_tab'], 'next')
913         bind(config['bind_prev_tab'], 'prev')
914         bind(config['bind_goto_tab'], 'goto %s')
915         bind(config['bind_goto_first'], 'goto 0')
916         bind(config['bind_goto_last'], 'goto -1')
917         bind(config['bind_clean_slate'], 'clean')
918
919         # session preset binds
920         bind(config['bind_save_preset'], 'preset save %s')
921         bind(config['bind_load_preset'], 'preset load %s')
922         bind(config['bind_del_preset'], 'preset del %s')
923         bind(config['bind_list_presets'], 'preset list %d' % uzbl.pid)
924
925         # Set definitions here
926         # set(key, command back to fifo)
927         if config['capture_new_windows']:
928             set("new_window", r'new $8')
929         
930         # Send config to uzbl instance via its socket file.
931         uzbl.send("\n".join(binds+sets))
932
933
934     def goto_tab(self, index):
935         '''Goto tab n (supports negative indexing).'''
936
937         tabs = list(self.notebook)
938         if 0 <= index < len(tabs):
939             self.notebook.set_current_page(index)
940             self.update_tablist()
941             return None
942
943         try:
944             tab = tabs[index]
945             # Update index because index might have previously been a
946             # negative index.
947             index = tabs.index(tab)
948             self.notebook.set_current_page(index)
949             self.update_tablist()
950
951         except IndexError:
952             pass
953
954
955     def next_tab(self, step=1):
956         '''Switch to next tab or n tabs right.'''
957
958         if step < 1:
959             error("next_tab: invalid step %r" % step)
960             return None
961
962         ntabs = self.notebook.get_n_pages()
963         tabn = (self.notebook.get_current_page() + step) % ntabs
964         self.notebook.set_current_page(tabn)
965         self.update_tablist()
966
967
968     def prev_tab(self, step=1):
969         '''Switch to prev tab or n tabs left.'''
970
971         if step < 1:
972             error("prev_tab: invalid step %r" % step)
973             return None
974
975         ntabs = self.notebook.get_n_pages()
976         tabn = self.notebook.get_current_page() - step
977         while tabn < 0: tabn += ntabs
978         self.notebook.set_current_page(tabn)
979         self.update_tablist()
980
981
982     def close_tab(self, tabn=None):
983         '''Closes current tab. Supports negative indexing.'''
984
985         if tabn is None:
986             tabn = self.notebook.get_current_page()
987
988         else:
989             try:
990                 tab = list(self.notebook)[tabn]
991
992             except IndexError:
993                 error("close_tab: invalid index %r" % tabn)
994                 return None
995
996         self.notebook.remove_page(tabn)
997
998
999     def tab_opened(self, notebook, tab, index):
1000         '''Called upon tab creation. Called by page-added signal.'''
1001
1002         if config['switch_to_new_tabs']:
1003             self.notebook.set_focus_child(tab)
1004
1005         else:
1006             oldindex = self.notebook.get_current_page()
1007             oldtab = self.notebook.get_nth_page(oldindex)
1008             self.notebook.set_focus_child(oldtab)
1009
1010
1011     def tab_closed(self, notebook, tab, index):
1012         '''Close the window if no tabs are left. Called by page-removed
1013         signal.'''
1014
1015         if tab in self.tabs.keys():
1016             uzbl = self.tabs[tab]
1017             for (timer, gid) in uzbl.timers.items():
1018                 error("tab_closed: removing timer %r" % timer)
1019                 gobject.source_remove(gid)
1020                 del uzbl.timers[timer]
1021
1022             if uzbl._socket:
1023                 uzbl._socket.close()
1024                 uzbl._socket = None
1025
1026             uzbl._fifoout = []
1027             uzbl._socketout = []
1028             uzbl._kill = True
1029             self._closed.append((uzbl.uri, uzbl.title))
1030             self._closed = self._closed[-10:]
1031             del self.tabs[tab]
1032
1033         if self.notebook.get_n_pages() == 0:
1034             if not self._killed and config['save_session']:
1035                 if len(self._closed):
1036                     d = {'curtab': 0, 'tabs': [self._closed[-1],]}
1037                     self.save_session(session=d)
1038
1039             self.quit()
1040
1041         self.update_tablist()
1042
1043         return True
1044
1045
1046     def tab_changed(self, notebook, page, index):
1047         '''Refresh tab list. Called by switch-page signal.'''
1048
1049         tab = self.notebook.get_nth_page(index)
1050         self.notebook.set_focus_child(tab)
1051         self.update_tablist(index)
1052         return True
1053
1054
1055     def update_tablist(self, curpage=None):
1056         '''Upate tablist status bar.'''
1057
1058         show_tablist = config['show_tablist']
1059         show_gtk_tabs = config['show_gtk_tabs']
1060         tab_titles = config['tab_titles']
1061         show_ellipsis = config['show_ellipsis']
1062         if not show_tablist and not show_gtk_tabs:
1063             return True
1064
1065         tabs = self.tabs.keys()
1066         if curpage is None:
1067             curpage = self.notebook.get_current_page()
1068
1069         title_format = "%s - Uzbl Browser"
1070         max_title_len = config['max_title_len']
1071
1072         if show_tablist:
1073             pango = ""
1074             normal = (config['tab_colours'], config['tab_text_colours'])
1075             selected = (config['selected_tab'], config['selected_tab_text'])
1076             if tab_titles:
1077                 tab_format = "<span %s> [ %d <span %s> %s</span> ] </span>"
1078             else:
1079                 tab_format = "<span %s> [ <span %s>%d</span> ] </span>"
1080
1081         if show_gtk_tabs:
1082             gtk_tab_format = "%d %s"
1083
1084         for index, tab in enumerate(self.notebook):
1085             if tab not in tabs: continue
1086             uzbl = self.tabs[tab]
1087
1088             if index == curpage:
1089                 self.window.set_title(title_format % uzbl.title)
1090
1091             tabtitle = uzbl.title[:max_title_len]
1092             if show_ellipsis and len(tabtitle) != len(uzbl.title):
1093                 tabtitle = "%s\xe2\x80\xa6" % tabtitle[:-1] # Show Ellipsis
1094
1095             if show_gtk_tabs:
1096                 if tab_titles:
1097                     self.notebook.set_tab_label_text(tab,\
1098                       gtk_tab_format % (index, tabtitle))
1099                 else:
1100                     self.notebook.set_tab_label_text(tab, str(index))
1101
1102             if show_tablist:
1103                 style = colour_selector(index, curpage, uzbl)
1104                 (tabc, textc) = style
1105
1106                 if tab_titles:
1107                     pango += tab_format % (tabc, index, textc,\
1108                       escape(tabtitle))
1109                 else:
1110                     pango += tab_format % (tabc, textc, index)
1111
1112         if show_tablist:
1113             self.tablist.set_markup(pango)
1114
1115         return True
1116
1117
1118     def save_session(self, session_file=None, session=None):
1119         '''Save the current session to file for restoration on next load.'''
1120
1121         strip = str.strip
1122
1123         if session_file is None:
1124             session_file = config['session_file']
1125         
1126         if session is None:
1127             tabs = self.tabs.keys()
1128             state = []
1129             for tab in list(self.notebook):
1130                 if tab not in tabs: continue
1131                 uzbl = self.tabs[tab]
1132                 if not uzbl.uri: continue
1133                 state += [(uzbl.uri, uzbl.title),]
1134
1135             session = {'curtab': self.notebook.get_current_page(),
1136               'tabs': state}
1137
1138         if config['json_session']:
1139             raw = json.dumps(session)
1140
1141         else:
1142             lines = ["curtab = %d" % session['curtab'],]
1143             for (uri, title) in session['tabs']:
1144                 lines += ["%s\t%s" % (strip(uri), strip(title)),]
1145
1146             raw = "\n".join(lines)
1147         
1148         if not os.path.isfile(session_file):
1149             dirname = os.path.dirname(session_file)
1150             if not os.path.isdir(dirname):
1151                 os.makedirs(dirname)
1152
1153         h = open(session_file, 'w')
1154         h.write(raw)
1155         h.close()
1156         
1157
1158     def load_session(self, session_file=None):
1159         '''Load a saved session from file.'''
1160         
1161         default_path = False
1162         strip = str.strip
1163         json_session = config['json_session']
1164
1165         if session_file is None:
1166             default_path = True
1167             session_file = config['session_file']
1168
1169         if not os.path.isfile(session_file):
1170             return False
1171
1172         h = open(session_file, 'r')
1173         raw = h.read()
1174         h.close()
1175         if json_session:
1176             if sum([1 for s in raw.split("\n") if strip(s)]) != 1:
1177                 error("Warning: The session file %r does not look json. "\
1178                   "Trying to load it as a non-json session file."\
1179                   % session_file)
1180                 json_session = False
1181         
1182         if json_session: 
1183             try:
1184                 session = json.loads(raw)
1185                 curtab, tabs = session['curtab'], session['tabs']
1186
1187             except:
1188                 error("Failed to load jsonifed session from %r"\
1189                   % session_file)
1190                 return None
1191
1192         else:
1193             tabs = []
1194             strip = str.strip
1195             curtab, tabs = 0, []
1196             lines = [s for s in raw.split("\n") if strip(s)]
1197             if len(lines) < 2:
1198                 error("Warning: The non-json session file %r looks invalid."\
1199                   % session_file)
1200                 return None
1201             
1202             try:
1203                 for line in lines:
1204                     if line.startswith("curtab"):
1205                         curtab = int(line.split()[-1])
1206
1207                     else:
1208                         uri, title = line.split("\t",1)
1209                         tabs += [(strip(uri), strip(title)),]
1210
1211             except:
1212                 error("Warning: failed to load session file %r" % session_file)
1213                 return None
1214
1215             session = {'curtab': curtab, 'tabs': tabs}
1216         
1217         # Now populate notebook with the loaded session.
1218         for (index, (uri, title)) in enumerate(tabs):
1219             self.new_tab(uri=uri, title=title, switch=(curtab==index))
1220
1221         # There may be other state information in the session dict of use to 
1222         # other functions. Of course however the non-json session object is 
1223         # just a dummy object of no use to no one.
1224         return session
1225
1226
1227     def quitrequest(self, *args):
1228         '''Called by delete-event signal to kill all uzbl instances.'''
1229         
1230         #TODO: Even though I send the kill request to all uzbl instances 
1231         # i should add a gobject timeout to check they all die.
1232
1233         self._killed = True
1234
1235         if config['save_session']:
1236             if len(list(self.notebook)):
1237                 self.save_session()
1238
1239             else:
1240                 # Notebook has no pages so delete session file if it exists.
1241                 if os.path.isfile(session_file):
1242                     os.remove(session_file)
1243         
1244         for (tab, uzbl) in self.tabs.items():
1245             uzbl.send("exit")
1246     
1247         
1248     def quit(self, *args):
1249         '''Cleanup the application and quit. Called by delete-event signal.'''
1250         
1251         for (fifo_socket, (fd, watchers)) in self._fifos.items():
1252             os.close(fd)
1253             for (watcherid, gid) in watchers.items():
1254                 gobject.source_remove(gid)
1255                 del watchers[watcherid]
1256
1257             del self._fifos[fifo_socket]
1258
1259         for (timerid, gid) in self._timers.items():
1260             gobject.source_remove(gid)
1261             del self._timers[timerid]
1262
1263         if os.path.exists(self.fifo_socket):
1264             os.unlink(self.fifo_socket)
1265             print "Unlinked %s" % self.fifo_socket
1266
1267         gtk.main_quit()
1268
1269
1270 if __name__ == "__main__":
1271
1272     # Read from the uzbl config into the global config dictionary.
1273     readconfig(uzbl_config, config)
1274
1275     if config['json_session']:
1276         try:
1277             import simplejson as json
1278
1279         except:
1280             error("Warning: json_session set but cannot import the python "\
1281               "module simplejson. Fix: \"set json_session = 0\" or "\
1282               "install the simplejson python module to remove this warning.")
1283             config['json_session'] = False
1284
1285     uzbl = UzblTabbed()
1286     uzbl.run()
1287
1288