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>
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.
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>
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.
38 # Romain Bignon <romain@peerfuse.org>
39 # Fix for session restoration code.
43 # pygtk - python bindings for gtk.
44 # pango - python bindings needed for text rendering & layout in gtk widgets.
45 # pygobject - GLib's GObject bindings for python.
47 # Optional dependencies:
48 # simplejson - save uzbl_tabbed.py sessions & presets in json.
50 # Note: I haven't included version numbers with this dependency list because
51 # I've only ever tested uzbl_tabbed.py on the latest stable versions of these
52 # packages in Gentoo's portage. Package names may vary on different systems.
56 # Because this version of uzbl_tabbed is able to inherit options from your main
57 # uzbl configuration file you may wish to configure uzbl tabbed from there.
58 # Here is a list of configuration options that can be customised and some
59 # example values for each:
61 # General tabbing options:
65 # gtk_tab_pos = (top|left|bottom|right)
66 # switch_to_new_tabs = 1
67 # capture_new_windows = 1
71 # new_tab_title = Loading
78 # session_file = $HOME/.local/share/uzbl/session
80 # Inherited uzbl options:
83 # icon_path = $HOME/.local/share/uzbl/uzbl.png
84 # status_background = #303030
87 # window_size = 800,800
89 # And the key bindings:
91 # bind_tab_from_clip = gY
92 # bind_tab_from_uri = go _
97 # bind_goto_first = g<
99 # bind_clean_slate = gQ
101 # Session preset key bindings:
102 # bind_save_preset = gsave _
103 # bind_load_preset = gload _
104 # bind_del_preset = gdel _
105 # bind_list_presets = glist
107 # And uzbl_tabbed.py takes care of the actual binding of the commands via each
108 # instances fifo socket.
110 # Custom tab styling:
111 # tab_colours = foreground = "#888" background = "#303030"
112 # tab_text_colours = foreground = "#bbb"
113 # selected_tab = foreground = "#fff"
114 # selected_tab_text = foreground = "green"
115 # tab_indicate_https = 1
116 # https_colours = foreground = "#888"
117 # https_text_colours = foreground = "#9c8e2d"
118 # selected_https = foreground = "#fff"
119 # selected_https_text = foreground = "gold"
121 # How these styling values are used are soley defined by the syling policy
122 # handler below (the function in the config section). So you can for example
123 # turn the tab text colour Firetruck-Red in the event "error" appears in the
124 # tab title or some other arbitrary event. You may wish to make a trusted
125 # hosts file and turn tab titles of tabs visiting trusted hosts purple.
129 # - new windows are not caught and opened in a new tab.
130 # - when uzbl_tabbed.py crashes it takes all the children with it.
131 # - when a new tab is opened when using gtk tabs the tab button itself
132 # grabs focus from its child for a few seconds.
133 # - when switch_to_new_tabs is not selected the notebook page is
134 # maintained but the new window grabs focus (try as I might to stop it).
138 # - add command line options to use a different session file, not use a
139 # session file and or open a uri on starup.
140 # - ellipsize individual tab titles when the tab-list becomes over-crowded
141 # - add "<" & ">" arrows to tablist to indicate that only a subset of the
142 # currently open tabs are being displayed on the tablist.
143 # - add the small tab-list display when both gtk tabs and text vim-like
144 # tablist are hidden (I.e. [ 1 2 3 4 5 ])
146 # - pass a uzbl socketid to uzbl_tabbed.py and have it assimilated into
147 # the collective. Resistance is futile!
166 from gobject import io_add_watch, source_remove, timeout_add, IO_IN, IO_HUP
167 from signal import signal, SIGTERM, SIGINT
168 from optparse import OptionParser, OptionGroup
172 _scriptname = os.path.basename(sys.argv[0])
174 sys.stderr.write("%s: %s\n" % (_scriptname, msg))
177 print "%s: %s" % (_scriptname, msg)
180 # ============================================================================
181 # ::: Default configuration section ::::::::::::::::::::::::::::::::::::::::::
182 # ============================================================================
185 # Location of your uzbl data directory.
186 if 'XDG_DATA_HOME' in os.environ.keys() and os.environ['XDG_DATA_HOME']:
187 data_dir = os.path.join(os.environ['XDG_DATA_HOME'], 'uzbl/')
189 data_dir = os.path.join(os.environ['HOME'], '.local/share/uzbl/')
190 if not os.path.exists(data_dir):
191 error("Warning: uzbl data_dir does not exist: %r" % data_dir)
193 # Location of your uzbl configuration file.
194 if 'XDG_CONFIG_HOME' in os.environ.keys() and os.environ['XDG_CONFIG_HOME']:
195 uzbl_config = os.path.join(os.environ['XDG_CONFIG_HOME'], 'uzbl/config')
197 uzbl_config = os.path.join(os.environ['HOME'],'.config/uzbl/config')
198 if not os.path.exists(uzbl_config):
199 error("Warning: Cannot locate your uzbl_config file %r" % uzbl_config)
201 # All of these settings can be inherited from your uzbl config file.
204 'show_tablist': True, # Show text uzbl like statusbar tab-list
205 'show_gtk_tabs': False, # Show gtk notebook tabs
206 'tablist_top': True, # Display tab-list at top of window
207 'gtk_tab_pos': 'top', # Gtk tab position (top|left|bottom|right)
208 'switch_to_new_tabs': True, # Upon opening a new tab switch to it
209 'capture_new_windows': True, # Use uzbl_tabbed to catch new windows
212 'tab_titles': True, # Display tab titles (else only tab-nums)
213 'new_tab_title': 'Loading', # New tab title
214 'max_title_len': 50, # Truncate title at n characters
215 'show_ellipsis': True, # Show ellipsis when truncating titles
218 'save_session': True, # Save session in file when quit
219 'json_session': False, # Use json to save session.
220 'saved_sessions_dir': os.path.join(data_dir, 'sessions/'),
221 'session_file': os.path.join(data_dir, 'session'),
223 # Inherited uzbl options
224 'fifo_dir': '/tmp', # Path to look for uzbl fifo.
225 'socket_dir': '/tmp', # Path to look for uzbl socket.
226 'icon_path': os.path.join(data_dir, 'uzbl.png'),
227 'status_background': "#303030", # Default background for all panels.
230 'window_size': "800,800", # width,height in pixels.
233 'bind_new_tab': 'gn', # Open new tab.
234 'bind_tab_from_clip': 'gY', # Open tab from clipboard.
235 'bind_tab_from_uri': 'go _', # Open new tab and goto entered uri.
236 'bind_close_tab': 'gC', # Close tab.
237 'bind_next_tab': 'gt', # Next tab.
238 'bind_prev_tab': 'gT', # Prev tab.
239 'bind_goto_tab': 'gi_', # Goto tab by tab-number (in title).
240 'bind_goto_first': 'g<', # Goto first tab.
241 'bind_goto_last': 'g>', # Goto last tab.
242 'bind_clean_slate': 'gQ', # Close all tabs and open new tab.
244 # Session preset key bindings
245 'bind_save_preset': 'gsave _', # Save session to file %s.
246 'bind_load_preset': 'gload _', # Load preset session from file %s.
247 'bind_del_preset': 'gdel _', # Delete preset session %s.
248 'bind_list_presets': 'glist', # List all session presets.
250 # Add custom tab style definitions to be used by the tab colour policy
251 # handler here. Because these are added to the config dictionary like
252 # any other uzbl_tabbed configuration option remember that they can
253 # be superseeded from your main uzbl config file.
254 'tab_colours': 'foreground = "#888" background = "#303030"',
255 'tab_text_colours': 'foreground = "#bbb"',
256 'selected_tab': 'foreground = "#fff"',
257 'selected_tab_text': 'foreground = "green"',
258 'tab_indicate_https': True,
259 'https_colours': 'foreground = "#888"',
260 'https_text_colours': 'foreground = "#9c8e2d"',
261 'selected_https': 'foreground = "#fff"',
262 'selected_https_text': 'foreground = "gold"',
264 } # End of config dict.
266 # This is the tab style policy handler. Every time the tablist is updated
267 # this function is called to determine how to colourise that specific tab
268 # according the simple/complex rules as defined here. You may even wish to
269 # move this function into another python script and import it using:
270 # from mycustomtabbingconfig import colour_selector
271 # Remember to rename, delete or comment out this function if you do that.
273 def colour_selector(tabindex, currentpage, uzbl):
274 '''Tablist styling policy handler. This function must return a tuple of
275 the form (tab style, text style).'''
277 # Just as an example:
278 # if 'error' in uzbl.title:
279 # if tabindex == currentpage:
280 # return ('foreground="#fff"', 'foreground="red"')
281 # return ('foreground="#888"', 'foreground="red"')
283 # Style tabs to indicate connected via https.
284 if config['tab_indicate_https'] and uzbl.uri.startswith("https://"):
285 if tabindex == currentpage:
286 return (config['selected_https'], config['selected_https_text'])
287 return (config['https_colours'], config['https_text_colours'])
289 # Style to indicate selected.
290 if tabindex == currentpage:
291 return (config['selected_tab'], config['selected_tab_text'])
294 return (config['tab_colours'], config['tab_text_colours'])
297 # ============================================================================
298 # ::: End of configuration section :::::::::::::::::::::::::::::::::::::::::::
299 # ============================================================================
302 def readconfig(uzbl_config, config):
303 '''Loads relevant config from the users uzbl config file into the global
304 config dictionary.'''
306 if not os.path.exists(uzbl_config):
307 error("Unable to load config %r" % uzbl_config)
310 # Define parsing regular expressions
311 isint = re.compile("^(\-|)[0-9]+$").match
312 findsets = re.compile("^set\s+([^\=]+)\s*\=\s*(.+)$",\
313 re.MULTILINE).findall
315 h = open(os.path.expandvars(uzbl_config), 'r')
319 configkeys, strip = config.keys(), str.strip
320 for (key, value) in findsets(rawconfig):
321 key, value = strip(key), strip(value)
322 if key not in configkeys: continue
323 if isint(value): value = int(value)
326 # Ensure that config keys that relate to paths are expanded.
327 expand = ['fifo_dir', 'socket_dir', 'session_file', 'icon_path']
329 config[key] = os.path.expandvars(config[key])
333 '''To infinity and beyond!'''
342 '''Replaces html markup in tab titles that screw around with pango.'''
344 for (split, glue) in [('&','&'), ('<', '<'), ('>', '>')]:
345 s = s.replace(split, glue)
350 '''Generates a random md5 for socket message-termination endmarkers.'''
352 return hashlib.md5(str(random.random()*time.time())).hexdigest()
356 '''A tabbed version of uzbl using gtk.Notebook'''
359 '''Uzbl instance meta-data/meta-action object.'''
361 def __init__(self, parent, tab, fifo_socket, socket_file, pid,\
366 self.fifo_socket = fifo_socket
367 self.socket_file = socket_file
377 # Switch to tab after loading
378 self._switch = switch
379 # fifo/socket files exists and socket connected.
380 self._connected = False
384 # Message termination endmarker.
385 self._marker = gen_endmarker()
387 # Gen probe commands string
389 probe = probes.append
390 probe('print uri %d @uri %s' % (self.pid, self._marker))
391 probe('print title %d @<document.title>@ %s' % (self.pid,\
393 self._probecmds = '\n'.join(probes)
395 # Enqueue keybinding config for child uzbl instance
396 self.parent.config_uzbl(self)
399 def flush(self, timer_call=False):
400 '''Flush messages from the socket-out and fifo-out queues.'''
407 error("Flush called on dead tab.")
410 if len(self._fifoout):
411 if os.path.exists(self.fifo_socket):
412 h = open(self.fifo_socket, 'w')
413 while len(self._fifoout):
414 msg = self._fifoout.pop(0)
418 if len(self._socketout):
419 if not self._socket and os.path.exists(self.socket_file):
420 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
421 sock.connect(self.socket_file)
425 while len(self._socketout):
426 msg = self._socketout.pop(0)
427 self._socket.send("%s\n"%msg)
429 if not self._connected and timer_call:
430 if not len(self._fifoout + self._socketout):
431 self._connected = True
433 if timer_call in self.timers.keys():
434 source_remove(self.timers[timer_call])
435 del self.timers[timer_call]
440 return len(self._fifoout + self._socketout)
444 '''Steal parent focus and switch the notebook to my own tab.'''
446 tabs = list(self.parent.notebook)
447 tabid = tabs.index(self.tab)
448 self.parent.goto_tab(tabid)
452 '''Probes the client for information about its self.'''
455 self.send(self._probecmds)
456 self._lastprobe = time.time()
459 def write(self, msg):
460 '''Child fifo write function.'''
462 self._fifoout.append(msg)
463 # Flush messages from the queue if able.
468 '''Child socket send function.'''
470 self._socketout.append(msg)
471 # Flush messages from queue if able.
476 '''Create tablist, window and notebook.'''
478 # Store information about the applications fifo_socket.
485 # A list of the recently closed tabs
488 # Holds metadata on the uzbl childen open.
491 # Generates a unique id for uzbl socket filenames.
492 self.next_pid = counter().next
495 self.window = gtk.Window()
497 window_size = map(int, config['window_size'].split(','))
498 self.window.set_default_size(*window_size)
501 error("Invalid value for default_size in config file.")
503 self.window.set_title("Uzbl Browser")
504 self.window.set_border_width(0)
506 # Set main window icon
507 icon_path = config['icon_path']
508 if os.path.exists(icon_path):
509 self.window.set_icon(gtk.gdk.pixbuf_new_from_file(icon_path))
512 icon_path = '/usr/share/uzbl/examples/data/uzbl/uzbl.png'
513 if os.path.exists(icon_path):
514 self.window.set_icon(gtk.gdk.pixbuf_new_from_file(icon_path))
516 # Attach main window event handlers
517 self.window.connect("delete-event", self.quitrequest)
520 if config['show_tablist']:
522 self.window.add(vbox)
523 ebox = gtk.EventBox()
524 self.tablist = gtk.Label()
525 self.tablist.set_use_markup(True)
526 self.tablist.set_justify(gtk.JUSTIFY_LEFT)
527 self.tablist.set_line_wrap(False)
528 self.tablist.set_selectable(False)
529 self.tablist.set_padding(2,2)
530 self.tablist.set_alignment(0,0)
531 self.tablist.set_ellipsize(pango.ELLIPSIZE_END)
532 self.tablist.set_text(" ")
534 ebox.add(self.tablist)
536 bgcolor = gtk.gdk.color_parse(config['status_background'])
537 ebox.modify_bg(gtk.STATE_NORMAL, bgcolor)
540 self.notebook = gtk.Notebook()
541 self.notebook.set_show_tabs(config['show_gtk_tabs'])
544 allposes = {'left': gtk.POS_LEFT, 'right':gtk.POS_RIGHT,
545 'top':gtk.POS_TOP, 'bottom':gtk.POS_BOTTOM}
546 if config['gtk_tab_pos'] in allposes.keys():
547 self.notebook.set_tab_pos(allposes[config['gtk_tab_pos']])
549 self.notebook.set_show_border(False)
550 self.notebook.set_scrollable(True)
551 self.notebook.set_border_width(0)
553 self.notebook.connect("page-removed", self.tab_closed)
554 self.notebook.connect("switch-page", self.tab_changed)
555 self.notebook.connect("page-added", self.tab_opened)
558 if config['show_tablist']:
559 if config['tablist_top']:
560 vbox.pack_start(ebox, False, False, 0)
561 vbox.pack_end(self.notebook, True, True, 0)
564 vbox.pack_start(self.notebook, True, True, 0)
565 vbox.pack_end(ebox, False, False, 0)
570 self.window.add(self.notebook)
573 self.wid = self.notebook.window.xid
575 # Generate the fifo socket filename.
576 fifo_filename = 'uzbltabbed_%d' % os.getpid()
577 self.fifo_socket = os.path.join(config['fifo_dir'], fifo_filename)
579 # Now initialise the fifo socket at self.fifo_socket
580 self.init_fifo_socket()
582 # If we are using sessions then load the last one if it exists.
583 if config['save_session']:
588 '''UzblTabbed main function that calls the gtk loop.'''
590 if not len(self.tabs):
593 # Update tablist timer
594 #timer = "update-tablist"
595 #timerid = timeout_add(500, self.update_tablist,timer)
596 #self._timers[timer] = timerid
598 # Probe clients every second for window titles and location
599 timer = "probe-clients"
600 timerid = timeout_add(1000, self.probe_clients, timer)
601 self._timers[timer] = timerid
603 # Make SIGTERM act orderly.
604 signal(SIGTERM, lambda signum, stack_frame: self.terminate(SIGTERM))
606 # Catch keyboard interrupts
607 signal(SIGINT, lambda signum, stack_frame: self.terminate(SIGINT))
613 error("encounted error %r" % sys.exc_info()[1])
616 self.unlink_fifo_socket()
618 # Attempt to close all uzbl instances nicely.
621 # Allow time for all the uzbl instances to quit.
627 def terminate(self, termsig=None):
628 '''Handle termination signals and exit safely and cleanly.'''
630 # Not required but at least it lets the user know what killed his
632 if termsig == SIGTERM:
633 error("caught SIGTERM signal")
635 elif termsig == SIGINT:
636 error("caught keyboard interrupt")
639 error("caught unknown signal")
641 error("commencing infanticide!")
643 # Sends the exit signal to all uzbl instances.
647 def init_fifo_socket(self):
648 '''Create interprocess communication fifo socket.'''
650 if os.path.exists(self.fifo_socket):
651 if not os.access(self.fifo_socket, os.F_OK | os.R_OK | os.W_OK):
652 os.mkfifo(self.fifo_socket)
655 basedir = os.path.dirname(self.fifo_socket)
656 if not os.path.exists(basedir):
659 os.mkfifo(self.fifo_socket)
661 # Add event handlers for IO_IN & IO_HUP events.
662 self.setup_fifo_watchers()
664 echo("listening at %r" % self.fifo_socket)
666 # Add atexit register to destroy the socket on program termination.
667 atexit.register(self.unlink_fifo_socket)
670 def unlink_fifo_socket(self):
671 '''Unlink the fifo socket. Note: This function is called automatically
672 on exit by an atexit register.'''
674 # Make sure the fifo_socket fd is closed.
677 # And unlink if the real fifo_socket exists.
678 if os.path.exists(self.fifo_socket):
679 os.unlink(self.fifo_socket)
680 echo("unlinked %r" % self.fifo_socket)
683 def close_fifo(self):
684 '''Remove all event handlers watching the fifo and close the fd.'''
687 if self._fifo is None: return
689 (fd, watchers) = self._fifo
692 # Stop all gobject io watchers watching the fifo.
699 def setup_fifo_watchers(self):
700 '''Open fifo socket fd and setup gobject IO_IN & IO_HUP event
703 # Close currently open fifo_socket fd and kill all watchers
706 fd = os.open(self.fifo_socket, os.O_RDONLY | os.O_NONBLOCK)
708 # Add gobject io event handlers to the fifo socket.
709 watchers = [io_add_watch(fd, IO_IN, self.main_fifo_read),\
710 io_add_watch(fd, IO_HUP, self.main_fifo_hangup)]
712 self._fifo = (fd, watchers)
715 def main_fifo_hangup(self, fd, cb_condition):
716 '''Handle main fifo socket hangups.'''
718 # Close old fd, open new fifo socket and add io event handlers.
719 self.setup_fifo_watchers()
721 # Kill the gobject event handler calling this handler function.
725 def main_fifo_read(self, fd, cb_condition):
726 '''Read from main fifo socket.'''
728 self._buffer = os.read(fd, 1024)
729 temp = self._buffer.split("\n")
730 self._buffer = temp.pop()
731 cmds = [s.strip().split() for s in temp if len(s.strip())]
736 self.parse_command(cmd)
739 error("parse_command: invalid command %s" % ' '.join(cmd))
745 def probe_clients(self, timer_call):
746 '''Probe all uzbl clients for up-to-date window titles and uri's.'''
748 save_session = config['save_session']
751 tabskeys = self.tabs.keys()
752 notebooklist = list(self.notebook)
754 for tab in notebooklist:
755 if tab not in tabskeys: continue
756 uzbl = self.tabs[tab]
759 sockd[uzbl._socket] = uzbl
761 sockets = sockd.keys()
762 (reading, _, errors) = select.select(sockets, [], sockets, 0)
766 uzbl._buffer = sock.recv(1024).replace('\n',' ')
767 temp = uzbl._buffer.split(uzbl._marker)
768 self._buffer = temp.pop()
769 cmds = [s.strip().split() for s in temp if len(s.strip())]
773 self.parse_command(cmd)
776 error("parse_command: invalid command %s" % ' '.join(cmd))
782 def parse_command(self, cmd):
783 '''Parse instructions from uzbl child processes.'''
785 # Commands ( [] = optional, {} = required )
787 # open new tab and head to optional uri.
789 # close current tab or close via tab id.
791 # open next tab or n tabs down. Supports negative indexing.
793 # open prev tab or n tabs down. Supports negative indexing.
800 # title {pid} {document-title}
801 # updates tablist title.
802 # uri {pid} {document-location}
811 elif cmd[0] == "newfromclip":
812 uri = subprocess.Popen(['xclip','-selection','clipboard','-o'],\
813 stdout=subprocess.PIPE).communicate()[0]
817 elif cmd[0] == "close":
819 self.close_tab(int(cmd[1]))
824 elif cmd[0] == "next":
826 self.next_tab(int(cmd[1]))
831 elif cmd[0] == "prev":
833 self.prev_tab(int(cmd[1]))
838 elif cmd[0] == "goto":
839 self.goto_tab(int(cmd[1]))
841 elif cmd[0] == "first":
844 elif cmd[0] == "last":
847 elif cmd[0] in ["title", "uri"]:
849 uzbl = self.get_tab_by_pid(int(cmd[1]))
851 old = getattr(uzbl, cmd[0])
852 new = ' '.join(cmd[2:])
853 setattr(uzbl, cmd[0], new)
855 self.update_tablist()
858 error("parse_command: no uzbl with pid %r" % int(cmd[1]))
860 elif cmd[0] == "preset":
862 error("parse_command: invalid preset command")
864 elif cmd[1] == "save":
865 path = os.path.join(config['saved_sessions_dir'], cmd[2])
866 self.save_session(path)
868 elif cmd[1] == "load":
869 path = os.path.join(config['saved_sessions_dir'], cmd[2])
870 self.load_session(path)
872 elif cmd[1] == "del":
873 path = os.path.join(config['saved_sessions_dir'], cmd[2])
874 if os.path.isfile(path):
878 error("parse_command: preset %r does not exist." % path)
880 elif cmd[1] == "list":
881 uzbl = self.get_tab_by_pid(int(cmd[2]))
883 if not os.path.isdir(config['saved_sessions_dir']):
884 js = "js alert('No saved presets.');"
888 listdir = os.listdir(config['saved_sessions_dir'])
889 listdir = "\\n".join(listdir)
890 js = "js alert('Session presets:\\n\\n%s');" % listdir
894 error("parse_command: unknown tab pid.")
897 error("parse_command: unknown parse command %r"\
900 elif cmd[0] == "clean":
904 error("parse_command: unknown command %r" % ' '.join(cmd))
907 def get_tab_by_pid(self, pid):
908 '''Return uzbl instance by pid.'''
910 for (tab, uzbl) in self.tabs.items():
917 def new_tab(self, uri='', title='', switch=None):
918 '''Add a new tab to the notebook and start a new instance of uzbl.
919 Use the switch option to negate config['switch_to_new_tabs'] option
920 when you need to load multiple tabs at a time (I.e. like when
921 restoring a session from a file).'''
923 pid = self.next_pid()
926 self.notebook.append_page(tab)
930 fifo_filename = 'uzbl_fifo_%s_%0.2d' % (self.wid, pid)
931 fifo_socket = os.path.join(config['fifo_dir'], fifo_filename)
932 socket_filename = 'uzbl_socket_%s_%0.2d' % (self.wid, pid)
933 socket_file = os.path.join(config['socket_dir'], socket_filename)
936 switch = config['switch_to_new_tabs']
939 title = config['new_tab_title']
941 uzbl = self.UzblInstance(self, tab, fifo_socket, socket_file, pid,\
945 uri = "--uri %r" % uri
947 self.tabs[tab] = uzbl
948 cmd = 'uzbl -s %s -n %s_%0.2d %s &' % (sid, self.wid, pid, uri)
949 subprocess.Popen([cmd], shell=True) # TODO: do i need close_fds=True ?
951 # Add gobject timer to make sure the config is pushed when fifo socket
953 timerid = timeout_add(100, uzbl.flush, "flush-initial-config")
954 uzbl.timers['flush-initial-config'] = timerid
956 self.update_tablist()
959 def clean_slate(self):
960 '''Close all open tabs and open a fresh brand new one.'''
963 tabs = self.tabs.keys()
964 for tab in list(self.notebook)[:-1]:
965 if tab not in tabs: continue
966 uzbl = self.tabs[tab]
970 def config_uzbl(self, uzbl):
971 '''Send bind commands for tab new/close/next/prev to a uzbl
975 bind_format = r'bind %s = sh "echo \"%s\" > \"%s\""'
976 bind = lambda key, action: binds.append(bind_format % (key, action,\
980 set_format = r'set %s = sh \"echo \\"%s\\" > \\"%s\\""'
981 set = lambda key, action: binds.append(set_format % (key, action,\
984 # Bind definitions here
985 # bind(key, command back to fifo)
986 bind(config['bind_new_tab'], 'new')
987 bind(config['bind_tab_from_clip'], 'newfromclip')
988 bind(config['bind_tab_from_uri'], 'new %s')
989 bind(config['bind_close_tab'], 'close')
990 bind(config['bind_next_tab'], 'next')
991 bind(config['bind_prev_tab'], 'prev')
992 bind(config['bind_goto_tab'], 'goto %s')
993 bind(config['bind_goto_first'], 'goto 0')
994 bind(config['bind_goto_last'], 'goto -1')
995 bind(config['bind_clean_slate'], 'clean')
996 bind(config['bind_save_preset'], 'preset save %s')
997 bind(config['bind_load_preset'], 'preset load %s')
998 bind(config['bind_del_preset'], 'preset del %s')
999 bind(config['bind_list_presets'], 'preset list %d' % uzbl.pid)
1001 # Set definitions here
1002 # set(key, command back to fifo)
1003 if config['capture_new_windows']:
1004 set("new_window", r'new $8')
1006 # Send config to uzbl instance via its socket file.
1007 uzbl.send("\n".join(binds+sets))
1010 def goto_tab(self, index):
1011 '''Goto tab n (supports negative indexing).'''
1013 tabs = list(self.notebook)
1014 if 0 <= index < len(tabs):
1015 self.notebook.set_current_page(index)
1016 self.update_tablist()
1021 # Update index because index might have previously been a
1023 index = tabs.index(tab)
1024 self.notebook.set_current_page(index)
1025 self.update_tablist()
1031 def next_tab(self, step=1):
1032 '''Switch to next tab or n tabs right.'''
1035 error("next_tab: invalid step %r" % step)
1038 ntabs = self.notebook.get_n_pages()
1039 tabn = (self.notebook.get_current_page() + step) % ntabs
1040 self.notebook.set_current_page(tabn)
1041 self.update_tablist()
1044 def prev_tab(self, step=1):
1045 '''Switch to prev tab or n tabs left.'''
1048 error("prev_tab: invalid step %r" % step)
1051 ntabs = self.notebook.get_n_pages()
1052 tabn = self.notebook.get_current_page() - step
1053 while tabn < 0: tabn += ntabs
1054 self.notebook.set_current_page(tabn)
1055 self.update_tablist()
1058 def close_tab(self, tabn=None):
1059 '''Closes current tab. Supports negative indexing.'''
1062 tabn = self.notebook.get_current_page()
1066 tab = list(self.notebook)[tabn]
1069 error("close_tab: invalid index %r" % tabn)
1072 self.notebook.remove_page(tabn)
1075 def tab_opened(self, notebook, tab, index):
1076 '''Called upon tab creation. Called by page-added signal.'''
1078 if config['switch_to_new_tabs']:
1079 self.notebook.set_focus_child(tab)
1082 oldindex = self.notebook.get_current_page()
1083 oldtab = self.notebook.get_nth_page(oldindex)
1084 self.notebook.set_focus_child(oldtab)
1087 def tab_closed(self, notebook, tab, index):
1088 '''Close the window if no tabs are left. Called by page-removed
1091 if tab in self.tabs.keys():
1092 uzbl = self.tabs[tab]
1093 for (timer, gid) in uzbl.timers.items():
1094 error("tab_closed: removing timer %r" % timer)
1096 del uzbl.timers[timer]
1099 uzbl._socket.close()
1103 uzbl._socketout = []
1105 self._closed.append((uzbl.uri, uzbl.title))
1106 self._closed = self._closed[-10:]
1109 if self.notebook.get_n_pages() == 0:
1110 if not self._killed and config['save_session']:
1111 if len(self._closed):
1112 d = {'curtab': 0, 'tabs': [self._closed[-1],]}
1113 self.save_session(session=d)
1117 self.update_tablist()
1122 def tab_changed(self, notebook, page, index):
1123 '''Refresh tab list. Called by switch-page signal.'''
1125 tab = self.notebook.get_nth_page(index)
1126 self.notebook.set_focus_child(tab)
1127 self.update_tablist(index)
1131 def update_tablist(self, curpage=None):
1132 '''Upate tablist status bar.'''
1134 show_tablist = config['show_tablist']
1135 show_gtk_tabs = config['show_gtk_tabs']
1136 tab_titles = config['tab_titles']
1137 show_ellipsis = config['show_ellipsis']
1138 if not show_tablist and not show_gtk_tabs:
1141 tabs = self.tabs.keys()
1143 curpage = self.notebook.get_current_page()
1145 title_format = "%s - Uzbl Browser"
1146 max_title_len = config['max_title_len']
1150 normal = (config['tab_colours'], config['tab_text_colours'])
1151 selected = (config['selected_tab'], config['selected_tab_text'])
1153 tab_format = "<span %s> [ %d <span %s> %s</span> ] </span>"
1155 tab_format = "<span %s> [ <span %s>%d</span> ] </span>"
1158 gtk_tab_format = "%d %s"
1160 for index, tab in enumerate(self.notebook):
1161 if tab not in tabs: continue
1162 uzbl = self.tabs[tab]
1164 if index == curpage:
1165 self.window.set_title(title_format % uzbl.title)
1167 tabtitle = uzbl.title[:max_title_len]
1168 if show_ellipsis and len(tabtitle) != len(uzbl.title):
1169 tabtitle = "%s\xe2\x80\xa6" % tabtitle[:-1] # Show Ellipsis
1173 self.notebook.set_tab_label_text(tab,\
1174 gtk_tab_format % (index, tabtitle))
1176 self.notebook.set_tab_label_text(tab, str(index))
1179 style = colour_selector(index, curpage, uzbl)
1180 (tabc, textc) = style
1183 pango += tab_format % (tabc, index, textc,\
1186 pango += tab_format % (tabc, textc, index)
1189 self.tablist.set_markup(pango)
1194 def save_session(self, session_file=None, session=None):
1195 '''Save the current session to file for restoration on next load.'''
1199 if session_file is None:
1200 session_file = config['session_file']
1203 tabs = self.tabs.keys()
1205 for tab in list(self.notebook):
1206 if tab not in tabs: continue
1207 uzbl = self.tabs[tab]
1208 if not uzbl.uri: continue
1209 state += [(uzbl.uri, uzbl.title),]
1211 session = {'curtab': self.notebook.get_current_page(),
1214 if config['json_session']:
1215 raw = json.dumps(session)
1218 lines = ["curtab = %d" % session['curtab'],]
1219 for (uri, title) in session['tabs']:
1220 lines += ["%s\t%s" % (strip(uri), strip(title)),]
1222 raw = "\n".join(lines)
1224 if not os.path.isfile(session_file):
1225 dirname = os.path.dirname(session_file)
1226 if not os.path.isdir(dirname):
1227 os.makedirs(dirname)
1229 h = open(session_file, 'w')
1234 def load_session(self, session_file=None):
1235 '''Load a saved session from file.'''
1237 default_path = False
1239 json_session = config['json_session']
1241 if session_file is None:
1243 session_file = config['session_file']
1245 if not os.path.isfile(session_file):
1248 h = open(session_file, 'r')
1252 if sum([1 for s in raw.split("\n") if strip(s)]) != 1:
1253 error("Warning: The session file %r does not look json. "\
1254 "Trying to load it as a non-json session file."\
1256 json_session = False
1260 session = json.loads(raw)
1261 curtab, tabs = session['curtab'], session['tabs']
1264 error("Failed to load jsonifed session from %r"\
1271 curtab, tabs = 0, []
1272 lines = [s for s in raw.split("\n") if strip(s)]
1274 error("Warning: The non-json session file %r looks invalid."\
1280 if line.startswith("curtab"):
1281 curtab = int(line.split()[-1])
1284 uri, title = line.split("\t",1)
1285 tabs += [(strip(uri), strip(title)),]
1288 error("Warning: failed to load session file %r" % session_file)
1291 session = {'curtab': curtab, 'tabs': tabs}
1293 # Now populate notebook with the loaded session.
1294 for (index, (uri, title)) in enumerate(tabs):
1295 self.new_tab(uri=uri, title=title, switch=(curtab==index))
1297 # There may be other state information in the session dict of use to
1298 # other functions. Of course however the non-json session object is
1299 # just a dummy object of no use to no one.
1303 def quitrequest(self, *args):
1304 '''Called by delete-event signal to kill all uzbl instances.'''
1308 if config['save_session']:
1309 if len(list(self.notebook)):
1313 # Notebook has no pages so delete session file if it exists.
1314 if os.path.isfile(config['session_file']):
1315 os.remove(config['session_file'])
1317 for (tab, uzbl) in self.tabs.items():
1320 # Add a gobject timer to make sure the application force-quits after a period.
1321 timer = "force-quit"
1322 timerid = timeout_add(5000, self.quit, timer)
1323 self._timers[timer] = timerid
1326 def quit(self, *args):
1327 '''Cleanup and quit. Called by delete-event signal.'''
1329 # Close the fifo socket, remove any gobject io event handlers and
1331 self.unlink_fifo_socket()
1333 # Remove all gobject timers that are still ticking.
1334 for (timerid, gid) in self._timers.items():
1336 del self._timers[timerid]
1345 if __name__ == "__main__":
1347 # Read from the uzbl config into the global config dictionary.
1348 readconfig(uzbl_config, config)
1350 # Build command line parser
1351 parser = OptionParser()
1352 parser.add_option('-n', '--no-session', dest='nosession',\
1353 action='store_true', help="ignore session saving a loading.")
1354 group = OptionGroup(parser, "Note", "All other command line arguments are "\
1355 "interpreted as uris and loaded in new tabs.")
1356 parser.add_option_group(group)
1358 # Parse command line options
1359 (options, uris) = parser.parse_args()
1361 if options.nosession:
1362 config['save_session'] = False
1364 if config['json_session']:
1366 import simplejson as json
1369 error("Warning: json_session set but cannot import the python "\
1370 "module simplejson. Fix: \"set json_session = 0\" or "\
1371 "install the simplejson python module to remove this warning.")
1372 config['json_session'] = False
1376 # All extra arguments given to uzbl_tabbed.py are interpreted as
1377 # web-locations to opened in new tabs.
1378 lasturi = len(uris)-1
1379 for (index,uri) in enumerate(uris):
1380 uzbl.new_tab(uri, switch=(index==lasturi))