3 # Uzbl tabbing wrapper using a fifo socket interface
4 # Copyright (c) 2009, Tom Adams <tom@holizz.com>
5 # Copyright (c) 2009, quigybo <?>
6 # Copyright (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 # 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:
46 # set show_gtk_tabs = 0
47 # set switch_to_new_tabs = 1
48 # set save_session = 1
49 # set new_tab_title = New tab
50 # set status_background = #303030
51 # set session_file = $HOME/.local/share/session
52 # set tab_colours = foreground = "#999"
53 # set tab_text_colours = foreground = "#444"
54 # set selected_tab = foreground = "#aaa" background="#303030"
55 # set selected_tab_text = foreground = "green"
56 # set window_size = 800,800
58 # And the keybindings:
60 # set bind_new_tab = gn
61 # set bind_tab_from_clip = gY
62 # set bind_close_tab = gC
63 # set bind_next_tab = gt
64 # set bind_prev_tab = gT
65 # set bind_goto_tab = gi_
66 # set bind_goto_first = g<
67 # set bind_goto_last = g>
69 # And uzbl_tabbed.py takes care of the actual binding of the commands via each
70 # instances fifo socket.
74 # - new windows are not caught and opened in a new tab.
75 # - probably missing some os.path.expandvars somewhere.
79 # - add command line options to use a different session file, not use a
80 # session file and or open a uri on starup.
81 # - ellipsize individual tab titles when the tab-list becomes over-crowded
82 # - add "<" & ">" arrows to tablist to indicate that only a subset of the
83 # currently open tabs are being displayed on the tablist.
84 # - probably missing some os.path.expandvars somewhere and other
85 # user-friendly.. things, this is still a very early version.
86 # - fix status_background issues & style tablist.
87 # - add the small tab-list display when both gtk tabs and text vim-like
88 # tablist are hidden (I.e. [ 1 2 3 4 5 ])
108 sys.stderr.write("%s\n"%msg)
110 if 'XDG_DATA_HOME' in os.environ.keys() and os.environ['XDG_DATA_HOME']:
111 data_dir = os.path.join(os.environ['XDG_DATA_HOME'], 'uzbl/')
114 data_dir = os.path.join(os.environ['HOME'], '.local/share/uzbl/')
116 # === Default Configuration ====================================================
118 # Location of your uzbl configuration file.
119 if 'XDG_CONFIG_HOME' in os.environ.keys() and os.environ['XDG_CONFIG_HOME']:
120 uzbl_config = os.path.join(os.environ['XDG_CONFIG_HOME'], 'uzbl/config')
122 uzbl_config = os.path.join(os.environ['HOME'],'.config/uzbl/config')
124 # All of these settings can be inherited from your uzbl config file.
125 config = {'show_tabs': True,
126 'show_gtk_tabs': False,
127 'switch_to_new_tabs': True,
128 'save_session': True,
130 'socket_dir': '/tmp',
131 'new_tab_title': 'New tab',
132 'icon_path': os.path.join(data_dir, 'uzbl.png'),
133 'session_file': os.path.join(data_dir, 'session'),
134 'status_background': "#303030",
135 'tab_colours': 'foreground = "#888"',
136 'tab_text_colours': 'foreground = "#bbb"',
137 'selected_tab': 'foreground = "#fff" background = "#303030"',
138 'selected_tab_text': 'foreground = "#99FF66"',
139 'window_size': "800,800",
140 'monospace_size': 10,
141 'bind_new_tab': 'gn',
142 'bind_tab_from_clip': 'gY',
143 'bind_close_tab': 'gC',
144 'bind_next_tab': 'gt',
145 'bind_prev_tab': 'gT',
146 'bind_goto_tab': 'gi_',
147 'bind_goto_first': 'g<',
148 'bind_goto_last':'g>'}
150 # === End Configuration =======================================================
152 def readconfig(uzbl_config, config):
153 '''Loads relevant config from the users uzbl config file into the global
154 config dictionary.'''
156 if not os.path.exists(uzbl_config):
157 error("Unable to load config %r" % uzbl_config)
160 # Define parsing regular expressions
161 isint = re.compile("^[0-9]+$").match
162 findsets = re.compile("^set\s+([^\=]+)\s*\=\s*(.+)$",\
163 re.MULTILINE).findall
165 h = open(os.path.expandvars(uzbl_config), 'r')
169 for (key, value) in findsets(rawconfig):
171 if key not in config.keys(): continue
172 if isint(value): value = int(value)
177 '''Recursively make directories.
178 I.e. `mkdir -p /some/nonexistant/path/`'''
180 path, sep = os.path.realpath(path), os.path.sep
181 dirs = path.split(sep)
182 for i in range(2,len(dirs)+1):
183 dir = os.path.join(sep,sep.join(dirs[:i]))
184 if not os.path.exists(dir):
189 '''To infinity and beyond!'''
198 '''A tabbed version of uzbl using gtk.Notebook'''
201 '''Uzbl instance meta-data/meta-action object.'''
203 def __init__(self, parent, tab, fifo_socket, socket_file, pid,\
208 self.fifo_socket = fifo_socket
209 self.socket_file = socket_file
211 self.title = config['new_tab_title']
219 # Switch to tab after connection
220 self._switch = switch
221 # fifo/socket files exists and socket connected.
222 self._connected = False
226 # Gen probe commands string
228 probe = probes.append
229 probe('print uri %d @uri' % self.pid)
230 probe('print title %d @<document.title>@' % self.pid)
231 self._probecmds = '\n'.join(probes)
233 # Enqueue keybinding config for child uzbl instance
234 self.parent.config_uzbl(self)
237 def flush(self, timer_call=False):
238 '''Flush messages from the socket-out and fifo-out queues.'''
245 error("Flush called on dead tab.")
248 if len(self._fifoout):
249 if os.path.exists(self.fifo_socket):
250 h = open(self.fifo_socket, 'w')
251 while len(self._fifoout):
252 msg = self._fifoout.pop(0)
256 if len(self._socketout):
257 if not self._socket and os.path.exists(self.socket_file):
258 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
259 sock.connect(self.socket_file)
263 while len(self._socketout):
264 msg = self._socketout.pop(0)
265 self._socket.send("%s\n"%msg)
267 if not self._connected and timer_call:
268 if not len(self._fifoout + self._socketout):
269 self._connected = True
271 if timer_call in self.timers.keys():
272 gobject.source_remove(self.timers[timer_call])
273 del self.timers[timer_call]
276 tabs = list(self.parent.notebook)
277 tabid = tabs.index(self.tab)
278 self.parent.goto_tab(tabid)
280 return len(self._fifoout + self._socketout)
284 '''Probes the client for information about its self.'''
287 self.send(self._probecmds)
288 self._lastprobe = time.time()
291 def write(self, msg):
292 '''Child fifo write function.'''
294 self._fifoout.append(msg)
295 # Flush messages from the queue if able.
300 '''Child socket send function.'''
302 self._socketout.append(msg)
303 # Flush messages from queue if able.
308 '''Create tablist, window and notebook.'''
314 # Holds metadata on the uzbl childen open.
317 # Generates a unique id for uzbl socket filenames.
318 self.next_pid = counter().next
321 self.window = gtk.Window()
323 window_size = map(int, config['window_size'].split(','))
324 self.window.set_default_size(*window_size)
327 error("Invalid value for default_size in config file.")
329 self.window.set_title("Uzbl Browser")
330 self.window.set_border_width(0)
332 # Set main window icon
333 icon_path = config['icon_path']
334 if os.path.exists(icon_path):
335 self.window.set_icon(gtk.gdk.pixbuf_new_from_file(icon_path))
338 icon_path = '/usr/share/uzbl/examples/data/uzbl/uzbl.png'
339 if os.path.exists(icon_path):
340 self.window.set_icon(gtk.gdk.pixbuf_new_from_file(icon_path))
342 # Attach main window event handlers
343 self.window.connect("delete-event", self.quit)
346 if config['show_tabs']:
348 self.window.add(vbox)
349 ebox = gtk.EventBox()
350 self.tablist = gtk.Label()
351 self.tablist.set_use_markup(True)
352 self.tablist.set_justify(gtk.JUSTIFY_LEFT)
353 self.tablist.set_line_wrap(False)
354 self.tablist.set_selectable(False)
355 self.tablist.set_padding(2,2)
356 self.tablist.set_alignment(0,0)
357 self.tablist.set_ellipsize(pango.ELLIPSIZE_END)
358 self.tablist.set_text(" ")
360 ebox.add(self.tablist)
362 bgcolor = gtk.gdk.color_parse(config['status_background'])
363 ebox.modify_bg(gtk.STATE_NORMAL, bgcolor)
364 vbox.pack_start(ebox, False, False, 0)
367 self.notebook = gtk.Notebook()
368 self.notebook.set_show_tabs(config['show_gtk_tabs'])
369 self.notebook.set_show_border(False)
370 self.notebook.connect("page-removed", self.tab_closed)
371 self.notebook.connect("switch-page", self.tab_changed)
373 if config['show_tabs']:
374 vbox.pack_end(self.notebook, True, True, 0)
377 self.window.add(self.notebook)
380 self.wid = self.notebook.window.xid
382 # Create the uzbl_tabbed fifo
383 fifo_filename = 'uzbltabbed_%d' % os.getpid()
384 self.fifo_socket = os.path.join(config['fifo_dir'], fifo_filename)
385 self._create_fifo_socket(self.fifo_socket)
386 self._setup_fifo_watcher(self.fifo_socket)
389 def _create_fifo_socket(self, fifo_socket):
390 '''Create interprocess communication fifo socket.'''
392 if os.path.exists(fifo_socket):
393 if not os.access(fifo_socket, os.F_OK | os.R_OK | os.W_OK):
394 os.mkfifo(fifo_socket)
397 basedir = os.path.dirname(self.fifo_socket)
398 if not os.path.exists(basedir):
400 os.mkfifo(self.fifo_socket)
402 print "Listening on %s" % self.fifo_socket
405 def _setup_fifo_watcher(self, fifo_socket):
406 '''Open fifo socket fd and setup gobject IO_IN & IO_HUP watchers.
407 Also log the creation of a fd and store the the internal
408 self._watchers dictionary along with the filename of the fd.'''
410 if fifo_socket in self._fifos.keys():
411 fd, watchers = self._fifos[fifo_socket]
413 for watcherid in watchers.keys():
414 gobject.source_remove(watchers[watcherid])
415 del watchers[watcherid]
417 del self._fifos[fifo_socket]
419 # Re-open fifo and add listeners.
420 fd = os.open(fifo_socket, os.O_RDONLY | os.O_NONBLOCK)
422 self._fifos[fifo_socket] = (fd, watchers)
423 watcher = lambda key, id: watchers.__setitem__(key, id)
425 # Watch for incoming data.
426 gid = gobject.io_add_watch(fd, gobject.IO_IN, self.main_fifo_read)
427 watcher('main-fifo-read', gid)
429 # Watch for fifo hangups.
430 gid = gobject.io_add_watch(fd, gobject.IO_HUP, self.main_fifo_hangup)
431 watcher('main-fifo-hangup', gid)
435 '''UzblTabbed main function that calls the gtk loop.'''
437 # Update tablist timer
438 #timer = "update-tablist"
439 #timerid = gobject.timeout_add(500, self.update_tablist,timer)
440 #self._timers[timer] = timerid
442 # Probe clients every second for window titles and location
443 timer = "probe-clients"
444 timerid = gobject.timeout_add(1000, self.probe_clients, timer)
445 self._timers[timer] = timerid
450 def probe_clients(self, timer_call):
451 '''Probe all uzbl clients for up-to-date window titles and uri's.'''
455 for tab in self.tabs.keys():
456 uzbl = self.tabs[tab]
459 sockd[uzbl._socket] = uzbl
461 sockets = sockd.keys()
462 (reading, _, errors) = select.select(sockets, [], sockets, 0)
466 uzbl._buffer = sock.recv(1024)
467 temp = uzbl._buffer.split("\n")
468 self._buffer = temp.pop()
469 cmds = [s.strip().split() for s in temp if len(s.strip())]
473 self.parse_command(cmd)
476 error("parse_command: invalid command %s" % ' '.join(cmd))
482 def main_fifo_hangup(self, fd, cb_condition):
483 '''Handle main fifo socket hangups.'''
485 # Close fd, re-open fifo_socket and watch.
486 self._setup_fifo_watcher(self.fifo_socket)
488 # And to kill any gobject event handlers calling this function:
492 def main_fifo_read(self, fd, cb_condition):
493 '''Read from main fifo socket.'''
495 self._buffer = os.read(fd, 1024)
496 temp = self._buffer.split("\n")
497 self._buffer = temp.pop()
498 cmds = [s.strip().split() for s in temp if len(s.strip())]
503 self.parse_command(cmd)
506 error("parse_command: invalid command %s" % ' '.join(cmd))
512 def parse_command(self, cmd):
513 '''Parse instructions from uzbl child processes.'''
515 # Commands ( [] = optional, {} = required )
517 # open new tab and head to optional uri.
519 # close current tab or close via tab id.
521 # open next tab or n tabs down. Supports negative indexing.
523 # open prev tab or n tabs down. Supports negative indexing.
530 # title {pid} {document-title}
531 # updates tablist title.
532 # uri {pid} {document-location}
541 elif cmd[0] == "newfromclip":
542 uri = subprocess.Popen(['xclip','-selection','clipboard','-o'],\
543 stdout=subprocess.PIPE).communicate()[0]
547 elif cmd[0] == "close":
549 self.close_tab(int(cmd[1]))
554 elif cmd[0] == "next":
556 self.next_tab(int(cmd[1]))
561 elif cmd[0] == "prev":
563 self.prev_tab(int(cmd[1]))
568 elif cmd[0] == "goto":
569 self.goto_tab(int(cmd[1]))
571 elif cmd[0] == "first":
574 elif cmd[0] == "last":
577 elif cmd[0] in ["title", "uri"]:
579 uzbl = self.get_tab_by_pid(int(cmd[1]))
581 old = getattr(uzbl, cmd[0])
582 new = ' '.join(cmd[2:])
583 setattr(uzbl, cmd[0], new)
585 self.update_tablist()
587 error("parse_command: no uzbl with pid %r" % int(cmd[1]))
589 error("parse_command: unknown command %r" % ' '.join(cmd))
592 def get_tab_by_pid(self, pid):
593 '''Return uzbl instance by pid.'''
595 for tab in self.tabs.keys():
596 if self.tabs[tab].pid == pid:
597 return self.tabs[tab]
602 def new_tab(self, uri='', switch=True):
603 '''Add a new tab to the notebook and start a new instance of uzbl.
604 Use the switch option to negate config['switch_to_new_tabs'] option
605 when you need to load multiple tabs at a time (I.e. like when
606 restoring a session from a file).'''
608 pid = self.next_pid()
611 self.notebook.append_page(tab)
614 fifo_filename = 'uzbl_fifo_%s_%0.2d' % (self.wid, pid)
615 fifo_socket = os.path.join(config['fifo_dir'], fifo_filename)
616 socket_filename = 'uzbl_socket_%s_%0.2d' % (self.wid, pid)
617 socket_file = os.path.join(config['socket_dir'], socket_filename)
619 # Create meta-instance and spawn child
620 if uri: uri = '--uri %s' % uri
621 uzbl = self.UzblInstance(self, tab, fifo_socket, socket_file, pid,\
623 self.tabs[tab] = uzbl
624 cmd = 'uzbl -s %s -n %s_%0.2d %s &' % (sid, self.wid, pid, uri)
625 subprocess.Popen([cmd], shell=True) # TODO: do i need close_fds=True ?
627 # Add gobject timer to make sure the config is pushed when fifo socket
629 timerid = gobject.timeout_add(100, uzbl.flush, "flush-initial-config")
630 uzbl.timers['flush-initial-config'] = timerid
632 self.update_tablist()
635 def config_uzbl(self, uzbl):
636 '''Send bind commands for tab new/close/next/prev to a uzbl
640 bind_format = 'bind %s = sh "echo \\\"%s\\\" > \\\"%s\\\""'
641 bind = lambda key, action: binds.append(bind_format % (key, action, \
644 # Keys are defined in the config section
645 # bind ( key , command back to fifo )
646 bind(config['bind_new_tab'], 'new')
647 bind(config['bind_tab_from_clip'], 'newfromclip')
648 bind(config['bind_close_tab'], 'close')
649 bind(config['bind_next_tab'], 'next')
650 bind(config['bind_prev_tab'], 'prev')
651 bind(config['bind_goto_tab'], 'goto %s')
652 bind(config['bind_goto_first'], 'goto 0')
653 bind(config['bind_goto_last'], 'goto -1')
655 # uzbl.send via socket or uzbl.write via fifo, I'll try send.
656 uzbl.send("\n".join(binds))
659 def goto_tab(self, n):
660 '''Goto tab n (supports negative indexing).'''
662 if 0 <= n < self.notebook.get_n_pages():
663 self.notebook.set_current_page(n)
664 self.update_tablist()
668 tabs = list(self.notebook)
671 self.notebook.set_current_page(i)
672 self.update_tablist()
678 def next_tab(self, step=1):
679 '''Switch to next tab or n tabs right.'''
682 error("next_tab: invalid step %r" % step)
685 ntabs = self.notebook.get_n_pages()
686 tabn = self.notebook.get_current_page() + step
687 self.notebook.set_current_page(tabn % ntabs)
688 self.update_tablist()
691 def prev_tab(self, step=1):
692 '''Switch to prev tab or n tabs left.'''
695 error("prev_tab: invalid step %r" % step)
698 ntabs = self.notebook.get_n_pages()
699 tabn = self.notebook.get_current_page() - step
700 while tabn < 0: tabn += ntabs
701 self.notebook.set_current_page(tabn)
702 self.update_tablist()
705 def close_tab(self, tabn=None):
706 '''Closes current tab. Supports negative indexing.'''
709 tabn = self.notebook.get_current_page()
712 tab = list(self.notebook)[tabn]
716 error("close_tab: invalid index %r" % tabn)
719 self.notebook.remove_page(tabn)
722 def tab_closed(self, notebook, tab, page_num):
723 '''Close the window if no tabs are left. Called by page-removed
726 if tab in self.tabs.keys():
727 uzbl = self.tabs[tab]
728 for timer in uzbl.timers.keys():
729 error("tab_closed: removing timer %r" % timer)
730 gobject.source_remove(uzbl.timers[timer])
741 if self.notebook.get_n_pages() == 0:
744 self.update_tablist()
747 def tab_changed(self, notebook, page, page_num):
748 '''Refresh tab list. Called by switch-page signal.'''
750 self.update_tablist()
753 def update_tablist(self):
754 '''Upate tablist status bar.'''
758 normal = (config['tab_colours'], config['tab_text_colours'])
759 selected = (config['selected_tab'], config['selected_tab_text'])
761 tab_format = "<span %s> [ %d <span %s> %s</span> ] </span>"
762 title_format = "%s - Uzbl Browser"
764 tabs = self.tabs.keys()
765 curpage = self.notebook.get_current_page()
767 for index, tab in enumerate(self.notebook):
768 if tab not in tabs: continue
769 uzbl = self.tabs[tab]
773 self.window.set_title(title_format % uzbl.title)
778 pango += tab_format % (colours[0], index, colours[1], uzbl.title)
780 self.tablist.set_markup(pango)
785 def quit(self, *args):
786 '''Cleanup the application and quit. Called by delete-event signal.'''
788 for fifo_socket in self._fifos.keys():
789 fd, watchers = self._fifos[fifo_socket]
791 for watcherid in watchers.keys():
792 gobject.source_remove(watchers[watcherid])
793 del watchers[watcherid]
795 del self._fifos[fifo_socket]
797 for timerid in self._timers.keys():
798 gobject.source_remove(self._timers[timerid])
799 del self._timers[timerid]
801 if os.path.exists(self.fifo_socket):
802 os.unlink(self.fifo_socket)
803 print "Unlinked %s" % self.fifo_socket
805 if config['save_session']:
806 session_file = os.path.expandvars(config['session_file'])
807 if self.notebook.get_n_pages():
808 if not os.path.isfile(session_file):
809 dirname = os.path.dirname(session_file)
810 if not os.path.isdir(dirname):
813 h = open(session_file, 'w')
814 h.write('current = %s\n' % self.notebook.get_current_page())
815 tabs = self.tabs.keys()
816 for tab in list(self.notebook):
817 if tab not in tabs: continue
818 uzbl = self.tabs[tab]
819 h.write("%s\n" % uzbl.uri)
823 # Notebook has no pages so delete session file if it exists.
824 if os.path.isfile(session_file):
825 os.remove(session_file)
830 if __name__ == "__main__":
832 # Read from the uzbl config into the global config dictionary.
833 readconfig(uzbl_config, config)
837 if os.path.isfile(os.path.expandvars(config['session_file'])):
838 h = open(os.path.expandvars(config['session_file']),'r')
839 lines = [line.strip() for line in h.readlines()]
843 if line.startswith("current"):
844 current = int(line.split()[-1])
847 uzbl.new_tab(line, False)