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