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