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