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