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