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