3 # Uzbl tabbing wrapper using a fifo socket interface
4 # Copyrite (c) 2009, Tom Adams <tom@holizz.com>
5 # Copyrite (c) 2009, quigybo <?>
6 # Copyrite (c) 2009, Mason Larobina <mason.larobina@gmail.com>
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.
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.
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/>.
23 # Tom Adams <tom@holizz.com>
24 # Wrote the original uzbl_tabbed.py as a proof of concept.
27 # Made signifigant headway on the uzbl_tabbing.py script on the
28 # uzbl wiki <http://www.uzbl.org/wiki/uzbl_tabbed>
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.
35 # mxey <mxey@ghosthacking.net>
36 # uzbl_config path now honors XDG_CONFIG_HOME if it exists.
40 # - status_background colour is not honoured (reverts to gtk default).
41 # - new windows are not caught and opened in a new tab.
42 # - need an easier way to read a uzbl instances window title instead of
43 # spawning a shell to spawn uzblctrl to communicate to the uzbl
44 # instance via socket to dump the window title to then pipe it to
45 # the tabbing managers fifo socket.
46 # - probably missing some os.path.expandvars somewhere.
50 # - add command line options to use a different session file, not use a
51 # session file and or open a uri on starup.
52 # - ellipsize individual tab titles when the tab-list becomes over-crowded
53 # - add "<" & ">" arrows to tablist to indicate that only a subset of the
54 # currently open tabs are being displayed on the tablist.
55 # - probably missing some os.path.expandvars somewhere and other
56 # user-friendly.. things, this is still a very early version.
57 # - fix status_background issues & style tablist.
58 # - add the small tab-list display when both gtk tabs and text vim-like
59 # tablist are hidden (I.e. [ 1 2 3 4 5 ])
78 sys.stderr.write("%s\n"%msg)
80 if 'XDG_DATA_HOME' in os.environ.keys() and os.environ['XDG_DATA_HOME']:
81 data_dir = os.path.join(os.environ['XDG_DATA_HOME'], 'uzbl/')
84 data_dir = os.path.join(os.environ['HOME'], '.local/share/uzbl/')
86 # === Default Configuration ====================================================
88 # Location of your uzbl configuration file.
89 if 'XDG_CONFIG_HOME' in os.environ.keys() and os.environ['XDG_CONFIG_HOME']:
90 uzbl_config = os.path.join(os.environ['XDG_CONFIG_HOME'], 'uzbl/config')
92 uzbl_config = os.path.join(os.environ['HOME'],'.config/uzbl/config')
94 # All of these settings can be inherited from your uzbl config file.
95 config = {'show_tabs': True,
96 'show_gtk_tabs': False,
97 'switch_to_new_tabs': True,
100 'icon_path': os.path.join(data_dir, 'uzbl.png'),
101 'session_file': os.path.join(data_dir, 'session'),
102 'tab_colours': 'foreground = "#999"',
103 'tab_text_colours': 'foreground = "#444"',
104 'selected_tab': 'foreground = "#aaa" background="#303030"',
105 'selected_tab_text': 'foreground = "green"',
106 'window_size': "800,800",
107 'monospace_size': 10,
108 'bind_new_tab': 'gn',
109 'bind_tab_from_clipboard': 'gY',
110 'bind_close_tab': 'gC',
111 'bind_next_tab': 'gt',
112 'bind_prev_tab': 'gT',
113 'bind_goto_tab': 'gi_',
114 'bind_goto_first': 'g<',
115 'bind_goto_last':'g>'}
117 # === End Configuration =======================================================
119 def readconfig(uzbl_config, config):
120 '''Loads relevant config from the users uzbl config file into the global
121 config dictionary.'''
123 if not os.path.exists(uzbl_config):
124 error("Unable to load config %r" % uzbl_config)
127 # Define parsing regular expressions
128 isint = re.compile("^[0-9]+$").match
129 findsets = re.compile("^set\s+([^\=]+)\s*\=\s*(.+)$",\
130 re.MULTILINE).findall
132 h = open(os.path.expandvars(uzbl_config), 'r')
136 for (key, value) in findsets(rawconfig):
138 if key not in config.keys(): continue
139 if isint(value): value = int(value)
144 '''Recursively make directories.
145 I.e. `mkdir -p /some/nonexistant/path/`'''
147 path, sep = os.path.realpath(path), os.path.sep
148 dirs = path.split(sep)
149 for i in range(2,len(dirs)+1):
150 dir = os.path.join(sep,sep.join(dirs[:i]))
151 if not os.path.exists(dir):
156 '''To infinity and beyond!'''
165 '''A tabbed version of uzbl using gtk.Notebook'''
168 '''Uzbl instance meta-data/meta-action object.'''
170 def __init__(self, parent, socket, fifo, pid, url='', switch=True):
172 self.socket = socket # the gtk socket
175 self.title = "New tab"
179 self._switch_on_config = switch
181 self._configured = False
184 self._probeurl = 'sh \'echo "url %s $6" > "%s"\'' % (self.pid,\
185 self.parent.fifo_socket)
187 # As soon as the variable expansion bug is fixed in uzbl
188 # I can start using this command to fetch the winow title
189 self._probetitle = 'sh \'echo "title %s @window_title" > "%s"\'' \
190 % (self.pid, self.parent.fifo_socket)
192 # When notebook tab deleted the kill switch is raised.
195 # Queue binds for uzbl child
196 self.parent.config_uzbl(self)
199 def flush(self, timer_call=False):
200 '''Flush messages from the queue.'''
203 error("Flush called on dead page.")
206 if os.path.exists(self.fifo):
207 h = open(self.fifo, 'w')
208 while len(self._outgoing):
209 msg = self._outgoing.pop(0)
210 h.write("%s\n" % msg)
213 elif not timer_call and self._configured:
214 # TODO: I dont know what to do here. A previously thought
215 # alright uzbl client fifo socket has now gone missing.
216 # I think this should be fatal (at least for the page in
217 # question). I'll wait until this error appears in the wild.
218 error("Error: fifo %r lost in action." % self.fifo)
220 if not len(self._outgoing) and timer_call:
221 self._configured = True
223 if timer_call in self.timers.keys():
224 gobject.source_remove(self.timers[timer_call])
225 del self.timers[timer_call]
227 if self._switch_on_config:
228 notebook = list(self.parent.notebook)
230 tabid = notebook.index(self.socket)
231 self.parent.goto_tab(tabid)
236 return len(self._outgoing)
240 '''Probes the client for information about its self.'''
242 # Ugly way of getting the socket path. Screwed if fifo is in any
243 # other part of the fifo socket path.
244 socket = 'socket'.join(self.fifo.split('fifo'))
245 # Hackish & wasteful way of getting the window title.
246 subcmd = 'print title %s @<document.title>@' % self.pid
247 cmd = 'uzblctrl -s "%s" -c "%s" > "%s" &' % (socket, subcmd, \
248 self.parent.fifo_socket)
249 subprocess.Popen([cmd], shell=True)
250 self.send(self._probeurl)
253 #self.send(self._probetitle)
255 self._lastprobe = time.time()
259 '''Child fifo write function.'''
261 self._outgoing.append(msg)
262 # Flush messages from the queue if able.
267 '''Create tablist, window and notebook.'''
270 self._pidcounter = counter()
271 self.next_pid = self._pidcounter.next
277 self.window = gtk.Window()
279 window_size = map(int, config['window_size'].split(','))
280 self.window.set_default_size(*window_size)
283 error("Invalid value for default_size in config file.")
285 self.window.set_title("Uzbl Browser")
286 self.window.set_border_width(0)
288 # Set main window icon
289 icon_path = config['icon_path']
290 if os.path.exists(icon_path):
291 self.window.set_icon(gtk.gdk.pixbuf_new_from_file(icon_path))
294 icon_path = '/usr/share/uzbl/examples/data/uzbl/uzbl.png'
295 if os.path.exists(icon_path):
296 self.window.set_icon(gtk.gdk.pixbuf_new_from_file(icon_path))
298 # Attach main window event handlers
299 self.window.connect("delete-event", self.quit)
302 if config['show_tabs']:
304 self.window.add(vbox)
306 self.tablist = gtk.Label()
307 self.tablist.set_use_markup(True)
308 self.tablist.set_justify(gtk.JUSTIFY_LEFT)
309 self.tablist.set_line_wrap(False)
310 self.tablist.set_selectable(False)
311 self.tablist.set_padding(0,2)
312 self.tablist.set_alignment(0,0)
313 self.tablist.set_ellipsize(pango.ELLIPSIZE_END)
314 self.tablist.set_text(" ")
316 vbox.pack_start(self.tablist, False, False, 0)
319 self.notebook = gtk.Notebook()
320 self.notebook.set_show_tabs(config['show_gtk_tabs'])
321 self.notebook.set_show_border(False)
322 self.notebook.connect("page-removed", self.tab_closed)
323 self.notebook.connect("switch-page", self.tab_changed)
325 if config['show_tabs']:
326 vbox.pack_end(self.notebook, True, True, 0)
329 self.window.add(self.notebook)
332 self.wid = self.notebook.window.xid
333 # Fifo socket definition
334 self._refindfifos = re.compile('^uzbl_fifo_%s_[0-9]+$' % self.wid)
335 fifo_filename = 'uzbltabbed_%d' % os.getpid()
336 self.fifo_socket = os.path.join(config['fifo_dir'], fifo_filename)
340 self._create_fifo_socket(self.fifo_socket)
341 self._setup_fifo_watcher(self.fifo_socket)
346 # Update tablist timer
347 timer = "update-tablist"
348 timerid = gobject.timeout_add(500, self.update_tablist,timer)
349 self._timers[timer] = timerid
351 # Due to the hackish way in which the window titles are read
352 # too many window will cause the application to slow down insanely
353 timer = "probe-clients"
354 timerid = gobject.timeout_add(1000, self.probe_clients, timer)
355 self._timers[timer] = timerid
360 def _find_fifos(self, fifo_dir):
361 '''Find all child fifo sockets in fifo_dir.'''
363 dirlist = '\n'.join(os.listdir(fifo_dir))
364 allfifos = self._refindfifos.findall(dirlist)
365 return sorted(allfifos)
368 def _create_fifo_socket(self, fifo_socket):
369 '''Create interprocess communication fifo socket.'''
371 if os.path.exists(fifo_socket):
372 if not os.access(fifo_socket, os.F_OK | os.R_OK | os.W_OK):
373 os.mkfifo(fifo_socket)
376 basedir = os.path.dirname(self.fifo_socket)
377 if not os.path.exists(basedir):
379 os.mkfifo(self.fifo_socket)
381 print "Listening on %s" % self.fifo_socket
384 def _setup_fifo_watcher(self, fifo_socket, fd=None):
385 '''Open fifo socket fd and setup gobject IO_IN & IO_HUP watchers.
386 Also log the creation of a fd and store the the internal
387 self._watchers dictionary along with the filename of the fd.'''
389 #TODO: Convert current self._watcher dict manipulation to the better
390 # IMHO self._timers handling by using "timer-keys" as the keys instead
391 # of the fifo fd's as keys.
395 if fd in self._watchers.keys():
396 d = self._watchers[fd]
397 watchers = d['watchers']
398 for watcher in list(watchers):
399 gobject.source_remove(watcher)
400 watchers.remove(watcher)
401 del self._watchers[fd]
403 fd = os.open(fifo_socket, os.O_RDONLY | os.O_NONBLOCK)
404 self._watchers[fd] = {'watchers': [], 'filename': fifo_socket}
406 watcher = self._watchers[fd]['watchers'].append
407 watcher(gobject.io_add_watch(fd, gobject.IO_IN, self.read_fifo))
408 watcher(gobject.io_add_watch(fd, gobject.IO_HUP, self.fifo_hangup))
411 def probe_clients(self, timer_call):
412 '''Load balance probe all uzbl clients for up-to-date window titles
416 probetimes = [(s, p[s]._lastprobe) for s in p.keys()]
417 socket, lasttime = sorted(probetimes, key=lambda t: t[1])[0]
419 if (time.time()-lasttime) > 5:
420 # Probe a uzbl instance at most once every 10 seconds
421 self.pages[socket].probe()
426 def fifo_hangup(self, fd, cb_condition):
427 '''Handle fifo socket hangups.'''
429 # Close fd, re-open fifo_socket and watch.
430 self._setup_fifo_watcher(self.fifo_socket, fd)
432 # And to kill any gobject event handlers calling this function:
436 def read_fifo(self, fd, cb_condition):
437 '''Read from fifo socket and handle fifo socket hangups.'''
439 self._buffer = os.read(fd, 1024)
440 temp = self._buffer.split("\n")
441 self._buffer = temp.pop()
443 for cmd in [s.strip().split() for s in temp if len(s.strip())]:
446 self.parse_command(cmd)
450 error("Invalid command: %s" % ' '.join(cmd))
454 def parse_command(self, cmd):
455 '''Parse instructions from uzbl child processes.'''
457 # Commands ( [] = optional, {} = required )
459 # open new tab and head to optional uri.
461 # close current tab or close via tab id.
463 # open next tab or n tabs down. Supports negative indexing.
465 # open prev tab or n tabs down. Supports negative indexing.
472 # title {pid} {document-title}
473 # updates tablist title.
474 # url {pid} {document-location}
476 # WARNING SOME OF THESE COMMANDS MIGHT NOT BE WORKING YET OR FAIL.
485 elif cmd[0] == "newfromclip":
486 url = subprocess.Popen(['xclip','-selection','clipboard','-o'],\
487 stdout=subprocess.PIPE).communicate()[0]
491 elif cmd[0] == "close":
493 self.close_tab(int(cmd[1]))
498 elif cmd[0] == "next":
500 self.next_tab(int(cmd[1]))
505 elif cmd[0] == "prev":
507 self.prev_tab(int(cmd[1]))
512 elif cmd[0] == "goto":
513 self.goto_tab(int(cmd[1]))
515 elif cmd[0] == "first":
518 elif cmd[0] == "last":
521 elif cmd[0] in ["title", "url"]:
523 uzbl = self.get_uzbl_by_pid(int(cmd[1]))
525 old = getattr(uzbl, cmd[0])
526 new = ' '.join(cmd[2:])
527 setattr(uzbl, cmd[0], new)
529 self.update_tablist()
531 error("Cannot find uzbl instance with pid %r" % int(cmd[1]))
533 error("Unknown command: %s" % ' '.join(cmd))
536 def get_uzbl_by_pid(self, pid):
537 '''Return uzbl instance by pid.'''
539 for socket in self.pages.keys():
540 if self.pages[socket].pid == pid:
541 return self.pages[socket]
545 def new_tab(self,url='', switch=True):
546 '''Add a new tab to the notebook and start a new instance of uzbl.
547 Use the switch option to negate config['switch_to_new_tabs'] option
548 when you need to load multiple tabs at a time (I.e. like when
549 restoring a session from a file).'''
551 pid = self.next_pid()
552 socket = gtk.Socket()
554 self.notebook.append_page(socket)
555 sid = socket.get_id()
558 url = '--uri %s' % url
560 fifo_filename = 'uzbl_fifo_%s_%0.2d' % (self.wid, pid)
561 fifo_socket = os.path.join(config['fifo_dir'], fifo_filename)
562 uzbl = self.UzblInstance(self, socket, fifo_socket, pid,\
563 url=url, switch=switch)
564 self.pages[socket] = uzbl
565 cmd = 'uzbl -s %s -n %s_%0.2d %s &' % (sid, self.wid, pid, url)
566 subprocess.Popen([cmd], shell=True)
568 # Add gobject timer to make sure the config is pushed when fifo socket
570 timerid = gobject.timeout_add(100, uzbl.flush, "flush-initial-config")
571 uzbl.timers['flush-initial-config'] = timerid
573 self.update_tablist()
576 def config_uzbl(self, uzbl):
577 '''Send bind commands for tab new/close/next/prev to a uzbl
581 bind_format = 'bind %s = sh "echo \\\"%s\\\" > \\\"%s\\\""'
582 bind = lambda key, action: binds.append(bind_format % (key, action, \
585 # Keys are defined in the config section
586 # bind ( key , command back to fifo )
587 bind(config['bind_new_tab'], 'new')
588 bind(config['bind_tab_from_clipboard'], 'newfromclip')
589 bind(config['bind_close_tab'], 'close')
590 bind(config['bind_next_tab'], 'next')
591 bind(config['bind_prev_tab'], 'prev')
592 bind(config['bind_goto_tab'], 'goto %s')
593 bind(config['bind_goto_first'], 'goto 0')
594 bind(config['bind_goto_last'], 'goto -1')
596 uzbl.send("\n".join(binds))
599 def goto_tab(self, n):
600 '''Goto tab n (supports negative indexing).'''
602 notebook = list(self.notebook)
606 i = notebook.index(page)
607 self.notebook.set_current_page(i)
612 self.update_tablist()
615 def next_tab(self, n=1):
616 '''Switch to next tab or n tabs right.'''
619 numofpages = self.notebook.get_n_pages()
620 pagen = self.notebook.get_current_page() + n
621 self.notebook.set_current_page( pagen % numofpages )
623 self.update_tablist()
626 def prev_tab(self, n=1):
627 '''Switch to prev tab or n tabs left.'''
630 numofpages = self.notebook.get_n_pages()
631 pagen = self.notebook.get_current_page() - n
634 self.notebook.set_current_page(pagen)
636 self.update_tablist()
639 def close_tab(self, tabid=None):
640 '''Closes current tab. Supports negative indexing.'''
643 tabid = self.notebook.get_current_page()
646 socket = list(self.notebook)[tabid]
649 error("Invalid index. Cannot close tab.")
652 uzbl = self.pages[socket]
654 for timer in uzbl.timers.keys():
655 error("Removing timer %r %r" % (timer, uzbl.timers[timer]))
656 gobject.source_remove(uzbl.timers[timer])
660 del self.pages[socket]
661 self.notebook.remove_page(tabid)
663 self.update_tablist()
666 def tab_closed(self, notebook, socket, page_num):
667 '''Close the window if no tabs are left. Called by page-removed
670 if socket in self.pages.keys():
671 uzbl = self.pages[socket]
672 for timer in uzbl.timers.keys():
673 error("Removing timer %r %r" % (timer, uzbl.timers[timer]))
674 gobject.source_remove(uzbl.timers[timer])
678 del self.pages[socket]
680 if self.notebook.get_n_pages() == 0:
683 self.update_tablist()
686 def tab_changed(self, notebook, page, page_num):
687 '''Refresh tab list. Called by switch-page signal.'''
689 self.update_tablist()
692 def update_tablist(self, timer_call=None):
693 '''Upate tablist status bar.'''
697 normal = (config['tab_colours'], config['tab_text_colours'])
698 selected = (config['selected_tab'], config['selected_tab_text'])
700 tab_format = "<span %s> [ %d <span %s> %s</span> ] </span>"
702 title_format = "%s - Uzbl Browser"
704 uzblkeys = self.pages.keys()
705 curpage = self.notebook.get_current_page()
707 for index, socket in enumerate(self.notebook):
708 if socket not in uzblkeys:
709 #error("Theres a socket in the notebook that I have no uzbl "\
712 uzbl = self.pages[socket]
716 self.window.set_title(title_format % uzbl.title)
721 pango += tab_format % (colours[0], index, colours[1], uzbl.title)
723 self.tablist.set_markup(pango)
728 #def quit(self, window, event):
729 def quit(self, *args):
730 '''Cleanup the application and quit. Called by delete-event signal.'''
732 for fd in self._watchers.keys():
733 d = self._watchers[fd]
734 watchers = d['watchers']
735 for watcher in list(watchers):
736 gobject.source_remove(watcher)
738 for timer in self._timers.keys():
739 gobject.source_remove(self._timers[timer])
741 if os.path.exists(self.fifo_socket):
742 os.unlink(self.fifo_socket)
743 print "Unlinked %s" % self.fifo_socket
745 if config['save_session']:
746 session_file = os.path.expandvars(config['session_file'])
747 if self.notebook.get_n_pages():
748 if not os.path.isfile(session_file):
749 dirname = os.path.dirname(session_file)
750 if not os.path.isdir(dirname):
753 h = open(session_file, 'w')
754 h.write('current = %s\n' % self.notebook.get_current_page())
756 for socket in list(self.notebook):
757 if socket not in self.pages.keys(): continue
758 uzbl = self.pages[socket]
759 uzbl.send('sh "echo $6 >> %s"' % session_file)
763 # Notebook has no pages so delete session file if it exists.
764 # Its better to not exist than be blank IMO.
765 if os.path.isfile(session_file):
766 os.remove(session_file)
771 if __name__ == "__main__":
773 # Read from the uzbl config into the global config dictionary.
774 readconfig(uzbl_config, config)
778 if os.path.isfile(os.path.expandvars(config['session_file'])):
779 h = open(os.path.expandvars(config['session_file']),'r')
780 urls = [s.strip() for s in h.readlines()]
784 if url.startswith("current"):
785 current = int(url.split()[-1])
787 uzbl.new_tab(url, False)