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 # Because this version of uzbl_tabbed is able to inherit options from your main
44 # uzbl configuration file you may wish to configure uzbl tabbed from there.
45 # Here is a list of configuration options that can be customised and some
46 # example values for each:
48 # General tabbing options:
52 # gtk_tab_pos = (top|left|bottom|right)
53 # switch_to_new_tabs = 1
54 # capture_new_windows = 1
58 # new_tab_title = Loading
65 # session_file = $HOME/.local/share/uzbl/session
67 # Inherited uzbl options:
70 # icon_path = $HOME/.local/share/uzbl/uzbl.png
71 # status_background = #303030
74 # window_size = 800,800
76 # And the key bindings:
78 # bind_tab_from_clip = gY
79 # bind_tab_from_uri = go _
84 # bind_goto_first = g<
86 # bind_clean_slate = gQ
88 # Session preset key bindings:
89 # bind_save_preset = gsave _
90 # bind_load_preset = gload _
91 # bind_del_preset = gdel _
92 # bind_list_presets = glist
94 # And uzbl_tabbed.py takes care of the actual binding of the commands via each
95 # instances fifo socket.
98 # tab_colours = foreground = "#888" background = "#303030"
99 # tab_text_colours = foreground = "#bbb"
100 # selected_tab = foreground = "#fff"
101 # selected_tab_text = foreground = "green"
102 # tab_indicate_https = 1
103 # https_colours = foreground = "#888"
104 # https_text_colours = foreground = "#9c8e2d"
105 # selected_https = foreground = "#fff"
106 # selected_https_text = foreground = "gold"
108 # How these styling values are used are soley defined by the syling policy
109 # handler below (the function in the config section). So you can for example
110 # turn the tab text colour Firetruck-Red in the event "error" appears in the
111 # tab title or some other arbitrary event. You may wish to make a trusted
112 # hosts file and turn tab titles of tabs visiting trusted hosts purple.
116 # - new windows are not caught and opened in a new tab.
117 # - when uzbl_tabbed.py crashes it takes all the children with it.
118 # - when a new tab is opened when using gtk tabs the tab button itself
119 # grabs focus from its child for a few seconds.
120 # - when switch_to_new_tabs is not selected the notebook page is
121 # maintained but the new window grabs focus (try as I might to stop it).
125 # - add command line options to use a different session file, not use a
126 # session file and or open a uri on starup.
127 # - ellipsize individual tab titles when the tab-list becomes over-crowded
128 # - add "<" & ">" arrows to tablist to indicate that only a subset of the
129 # currently open tabs are being displayed on the tablist.
130 # - add the small tab-list display when both gtk tabs and text vim-like
131 # tablist are hidden (I.e. [ 1 2 3 4 5 ])
133 # - pass a uzbl socketid to uzbl_tabbed.py and have it assimilated into
134 # the collective. Resistance is futile!
155 sys.stderr.write("%s\n"%msg)
158 # ============================================================================
159 # ::: Default configuration section ::::::::::::::::::::::::::::::::::::::::::
160 # ============================================================================
162 # Location of your uzbl data directory.
163 if 'XDG_DATA_HOME' in os.environ.keys() and os.environ['XDG_DATA_HOME']:
164 data_dir = os.path.join(os.environ['XDG_DATA_HOME'], 'uzbl/')
166 data_dir = os.path.join(os.environ['HOME'], '.local/share/uzbl/')
167 if not os.path.exists(data_dir):
168 error("Warning: uzbl data_dir does not exist: %r" % data_dir)
170 # Location of your uzbl configuration file.
171 if 'XDG_CONFIG_HOME' in os.environ.keys() and os.environ['XDG_CONFIG_HOME']:
172 uzbl_config = os.path.join(os.environ['XDG_CONFIG_HOME'], 'uzbl/config')
174 uzbl_config = os.path.join(os.environ['HOME'],'.config/uzbl/config')
175 if not os.path.exists(uzbl_config):
176 error("Warning: Cannot locate your uzbl_config file %r" % uzbl_config)
178 # All of these settings can be inherited from your uzbl config file.
181 'show_tablist': True, # Show text uzbl like statusbar tab-list
182 'show_gtk_tabs': False, # Show gtk notebook tabs
183 'tablist_top': True, # Display tab-list at top of window
184 'gtk_tab_pos': 'top', # Gtk tab position (top|left|bottom|right)
185 'switch_to_new_tabs': True, # Upon opening a new tab switch to it
186 'capture_new_windows': True, # Use uzbl_tabbed to catch new windows
189 'tab_titles': True, # Display tab titles (else only tab-nums)
190 'new_tab_title': 'Loading', # New tab title
191 'max_title_len': 50, # Truncate title at n characters
192 'show_ellipsis': True, # Show ellipsis when truncating titles
195 'save_session': True, # Save session in file when quit
196 'json_session': True, # Use json to save session.
197 'saved_sessions_dir': os.path.join(data_dir, 'sessions/'),
198 'session_file': os.path.join(data_dir, 'session'),
200 # Inherited uzbl options
201 'fifo_dir': '/tmp', # Path to look for uzbl fifo.
202 'socket_dir': '/tmp', # Path to look for uzbl socket.
203 'icon_path': os.path.join(data_dir, 'uzbl.png'),
204 'status_background': "#303030", # Default background for all panels.
207 'window_size': "800,800", # width,height in pixels.
210 'bind_new_tab': 'gn', # Open new tab.
211 'bind_tab_from_clip': 'gY', # Open tab from clipboard.
212 'bind_tab_from_uri': 'go _', # Open new tab and goto entered uri.
213 'bind_close_tab': 'gC', # Close tab.
214 'bind_next_tab': 'gt', # Next tab.
215 'bind_prev_tab': 'gT', # Prev tab.
216 'bind_goto_tab': 'gi_', # Goto tab by tab-number (in title).
217 'bind_goto_first': 'g<', # Goto first tab.
218 'bind_goto_last': 'g>', # Goto last tab.
219 'bind_clean_slate': 'gQ', # Close all tabs and open new tab.
221 # Session preset key bindings
222 'bind_save_preset': 'gsave _', # Save session to file %s.
223 'bind_load_preset': 'gload _', # Load preset session from file %s.
224 'bind_del_preset': 'gdel _', # Delete preset session %s.
225 'bind_list_presets': 'glist', # List all session presets.
227 # Add custom tab style definitions to be used by the tab colour policy
228 # handler here. Because these are added to the config dictionary like
229 # any other uzbl_tabbed configuration option remember that they can
230 # be superseeded from your main uzbl config file.
231 'tab_colours': 'foreground = "#888" background = "#303030"',
232 'tab_text_colours': 'foreground = "#bbb"',
233 'selected_tab': 'foreground = "#fff"',
234 'selected_tab_text': 'foreground = "green"',
235 'tab_indicate_https': True,
236 'https_colours': 'foreground = "#888"',
237 'https_text_colours': 'foreground = "#9c8e2d"',
238 'selected_https': 'foreground = "#fff"',
239 'selected_https_text': 'foreground = "gold"',
241 } # End of config dict.
243 # This is the tab style policy handler. Every time the tablist is updated
244 # this function is called to determine how to colourise that specific tab
245 # according the simple/complex rules as defined here. You may even wish to
246 # move this function into another python script and import it using:
247 # from mycustomtabbingconfig import colour_selector
248 # Remember to rename, delete or comment out this function if you do that.
250 def colour_selector(tabindex, currentpage, uzbl):
251 '''Tablist styling policy handler. This function must return a tuple of
252 the form (tab style, text style).'''
254 # Just as an example:
255 # if 'error' in uzbl.title:
256 # if tabindex == currentpage:
257 # return ('foreground="#fff"', 'foreground="red"')
258 # return ('foreground="#888"', 'foreground="red"')
260 # Style tabs to indicate connected via https.
261 if config['tab_indicate_https'] and uzbl.uri.startswith("https://"):
262 if tabindex == currentpage:
263 return (config['selected_https'], config['selected_https_text'])
264 return (config['https_colours'], config['https_text_colours'])
266 # Style to indicate selected.
267 if tabindex == currentpage:
268 return (config['selected_tab'], config['selected_tab_text'])
271 return (config['tab_colours'], config['tab_text_colours'])
274 # ============================================================================
275 # ::: End of configuration section :::::::::::::::::::::::::::::::::::::::::::
276 # ============================================================================
279 def readconfig(uzbl_config, config):
280 '''Loads relevant config from the users uzbl config file into the global
281 config dictionary.'''
283 if not os.path.exists(uzbl_config):
284 error("Unable to load config %r" % uzbl_config)
287 # Define parsing regular expressions
288 isint = re.compile("^(\-|)[0-9]+$").match
289 findsets = re.compile("^set\s+([^\=]+)\s*\=\s*(.+)$",\
290 re.MULTILINE).findall
292 h = open(os.path.expandvars(uzbl_config), 'r')
296 configkeys, strip = config.keys(), str.strip
297 for (key, value) in findsets(rawconfig):
298 key, value = strip(key), strip(value)
299 if key not in configkeys: continue
300 if isint(value): value = int(value)
303 # Ensure that config keys that relate to paths are expanded.
304 expand = ['fifo_dir', 'socket_dir', 'session_file', 'icon_path']
306 config[key] = os.path.expandvars(config[key])
310 '''To infinity and beyond!'''
319 '''Replaces html markup in tab titles that screw around with pango.'''
321 for (split, glue) in [('&','&'), ('<', '<'), ('>', '>')]:
322 s = s.replace(split, glue)
327 '''Generates a random md5 for socket message-termination endmarkers.'''
329 return hashlib.md5(str(random.random()*time.time())).hexdigest()
333 '''A tabbed version of uzbl using gtk.Notebook'''
336 '''Uzbl instance meta-data/meta-action object.'''
338 def __init__(self, parent, tab, fifo_socket, socket_file, pid,\
343 self.fifo_socket = fifo_socket
344 self.socket_file = socket_file
354 # Switch to tab after loading
355 self._switch = switch
356 # fifo/socket files exists and socket connected.
357 self._connected = False
361 # Message termination endmarker.
362 self._marker = gen_endmarker()
364 # Gen probe commands string
366 probe = probes.append
367 probe('print uri %d @uri %s' % (self.pid, self._marker))
368 probe('print title %d @<document.title>@ %s' % (self.pid,\
370 self._probecmds = '\n'.join(probes)
372 # Enqueue keybinding config for child uzbl instance
373 self.parent.config_uzbl(self)
376 def flush(self, timer_call=False):
377 '''Flush messages from the socket-out and fifo-out queues.'''
384 error("Flush called on dead tab.")
387 if len(self._fifoout):
388 if os.path.exists(self.fifo_socket):
389 h = open(self.fifo_socket, 'w')
390 while len(self._fifoout):
391 msg = self._fifoout.pop(0)
395 if len(self._socketout):
396 if not self._socket and os.path.exists(self.socket_file):
397 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
398 sock.connect(self.socket_file)
402 while len(self._socketout):
403 msg = self._socketout.pop(0)
404 self._socket.send("%s\n"%msg)
406 if not self._connected and timer_call:
407 if not len(self._fifoout + self._socketout):
408 self._connected = True
410 if timer_call in self.timers.keys():
411 gobject.source_remove(self.timers[timer_call])
412 del self.timers[timer_call]
417 return len(self._fifoout + self._socketout)
421 '''Steal parent focus and switch the notebook to my own tab.'''
423 tabs = list(self.parent.notebook)
424 tabid = tabs.index(self.tab)
425 self.parent.goto_tab(tabid)
429 '''Probes the client for information about its self.'''
432 self.send(self._probecmds)
433 self._lastprobe = time.time()
436 def write(self, msg):
437 '''Child fifo write function.'''
439 self._fifoout.append(msg)
440 # Flush messages from the queue if able.
445 '''Child socket send function.'''
447 self._socketout.append(msg)
448 # Flush messages from queue if able.
453 '''Create tablist, window and notebook.'''
460 # A list of the recently closed tabs
463 # Holds metadata on the uzbl childen open.
466 # Generates a unique id for uzbl socket filenames.
467 self.next_pid = counter().next
470 self.window = gtk.Window()
472 window_size = map(int, config['window_size'].split(','))
473 self.window.set_default_size(*window_size)
476 error("Invalid value for default_size in config file.")
478 self.window.set_title("Uzbl Browser")
479 self.window.set_border_width(0)
481 # Set main window icon
482 icon_path = config['icon_path']
483 if os.path.exists(icon_path):
484 self.window.set_icon(gtk.gdk.pixbuf_new_from_file(icon_path))
487 icon_path = '/usr/share/uzbl/examples/data/uzbl/uzbl.png'
488 if os.path.exists(icon_path):
489 self.window.set_icon(gtk.gdk.pixbuf_new_from_file(icon_path))
491 # Attach main window event handlers
492 self.window.connect("delete-event", self.quitrequest)
495 if config['show_tablist']:
497 self.window.add(vbox)
498 ebox = gtk.EventBox()
499 self.tablist = gtk.Label()
500 self.tablist.set_use_markup(True)
501 self.tablist.set_justify(gtk.JUSTIFY_LEFT)
502 self.tablist.set_line_wrap(False)
503 self.tablist.set_selectable(False)
504 self.tablist.set_padding(2,2)
505 self.tablist.set_alignment(0,0)
506 self.tablist.set_ellipsize(pango.ELLIPSIZE_END)
507 self.tablist.set_text(" ")
509 ebox.add(self.tablist)
511 bgcolor = gtk.gdk.color_parse(config['status_background'])
512 ebox.modify_bg(gtk.STATE_NORMAL, bgcolor)
515 self.notebook = gtk.Notebook()
516 self.notebook.set_show_tabs(config['show_gtk_tabs'])
519 allposes = {'left': gtk.POS_LEFT, 'right':gtk.POS_RIGHT,
520 'top':gtk.POS_TOP, 'bottom':gtk.POS_BOTTOM}
521 if config['gtk_tab_pos'] in allposes.keys():
522 self.notebook.set_tab_pos(allposes[config['gtk_tab_pos']])
524 self.notebook.set_show_border(False)
525 self.notebook.set_scrollable(True)
526 self.notebook.set_border_width(0)
528 self.notebook.connect("page-removed", self.tab_closed)
529 self.notebook.connect("switch-page", self.tab_changed)
530 self.notebook.connect("page-added", self.tab_opened)
533 if config['show_tablist']:
534 if config['tablist_top']:
535 vbox.pack_start(ebox, False, False, 0)
536 vbox.pack_end(self.notebook, True, True, 0)
539 vbox.pack_start(self.notebook, True, True, 0)
540 vbox.pack_end(ebox, False, False, 0)
545 self.window.add(self.notebook)
548 self.wid = self.notebook.window.xid
550 # Create the uzbl_tabbed fifo
551 fifo_filename = 'uzbltabbed_%d' % os.getpid()
552 self.fifo_socket = os.path.join(config['fifo_dir'], fifo_filename)
553 self._create_fifo_socket(self.fifo_socket)
554 self._setup_fifo_watcher(self.fifo_socket)
557 def _create_fifo_socket(self, fifo_socket):
558 '''Create interprocess communication fifo socket.'''
560 if os.path.exists(fifo_socket):
561 if not os.access(fifo_socket, os.F_OK | os.R_OK | os.W_OK):
562 os.mkfifo(fifo_socket)
565 basedir = os.path.dirname(self.fifo_socket)
566 if not os.path.exists(basedir):
569 os.mkfifo(self.fifo_socket)
571 print "Listening on %s" % self.fifo_socket
574 def _setup_fifo_watcher(self, fifo_socket):
575 '''Open fifo socket fd and setup gobject IO_IN & IO_HUP watchers.
576 Also log the creation of a fd and store the the internal
577 self._watchers dictionary along with the filename of the fd.'''
579 if fifo_socket in self._fifos.keys():
580 fd, watchers = self._fifos[fifo_socket]
582 for (watcherid, gid) in watchers.items():
583 gobject.source_remove(gid)
584 del watchers[watcherid]
586 del self._fifos[fifo_socket]
588 # Re-open fifo and add listeners.
589 fd = os.open(fifo_socket, os.O_RDONLY | os.O_NONBLOCK)
591 self._fifos[fifo_socket] = (fd, watchers)
592 watcher = lambda key, id: watchers.__setitem__(key, id)
594 # Watch for incoming data.
595 gid = gobject.io_add_watch(fd, gobject.IO_IN, self.main_fifo_read)
596 watcher('main-fifo-read', gid)
598 # Watch for fifo hangups.
599 gid = gobject.io_add_watch(fd, gobject.IO_HUP, self.main_fifo_hangup)
600 watcher('main-fifo-hangup', gid)
604 '''UzblTabbed main function that calls the gtk loop.'''
606 if config['save_session']:
609 if not len(self.tabs):
612 # Update tablist timer
613 #timer = "update-tablist"
614 #timerid = gobject.timeout_add(500, self.update_tablist,timer)
615 #self._timers[timer] = timerid
617 # Probe clients every second for window titles and location
618 timer = "probe-clients"
619 timerid = gobject.timeout_add(1000, self.probe_clients, timer)
620 self._timers[timer] = timerid
625 def probe_clients(self, timer_call):
626 '''Probe all uzbl clients for up-to-date window titles and uri's.'''
628 save_session = config['save_session']
631 tabskeys = self.tabs.keys()
632 notebooklist = list(self.notebook)
634 for tab in notebooklist:
635 if tab not in tabskeys: continue
636 uzbl = self.tabs[tab]
639 sockd[uzbl._socket] = uzbl
641 sockets = sockd.keys()
642 (reading, _, errors) = select.select(sockets, [], sockets, 0)
646 uzbl._buffer = sock.recv(1024).replace('\n',' ')
647 temp = uzbl._buffer.split(uzbl._marker)
648 self._buffer = temp.pop()
649 cmds = [s.strip().split() for s in temp if len(s.strip())]
653 self.parse_command(cmd)
656 error("parse_command: invalid command %s" % ' '.join(cmd))
662 def main_fifo_hangup(self, fd, cb_condition):
663 '''Handle main fifo socket hangups.'''
665 # Close fd, re-open fifo_socket and watch.
666 self._setup_fifo_watcher(self.fifo_socket)
668 # And to kill any gobject event handlers calling this function:
672 def main_fifo_read(self, fd, cb_condition):
673 '''Read from main fifo socket.'''
675 self._buffer = os.read(fd, 1024)
676 temp = self._buffer.split("\n")
677 self._buffer = temp.pop()
678 cmds = [s.strip().split() for s in temp if len(s.strip())]
683 self.parse_command(cmd)
686 error("parse_command: invalid command %s" % ' '.join(cmd))
692 def parse_command(self, cmd):
693 '''Parse instructions from uzbl child processes.'''
695 # Commands ( [] = optional, {} = required )
697 # open new tab and head to optional uri.
699 # close current tab or close via tab id.
701 # open next tab or n tabs down. Supports negative indexing.
703 # open prev tab or n tabs down. Supports negative indexing.
710 # title {pid} {document-title}
711 # updates tablist title.
712 # uri {pid} {document-location}
721 elif cmd[0] == "newfromclip":
722 uri = subprocess.Popen(['xclip','-selection','clipboard','-o'],\
723 stdout=subprocess.PIPE).communicate()[0]
727 elif cmd[0] == "close":
729 self.close_tab(int(cmd[1]))
734 elif cmd[0] == "next":
736 self.next_tab(int(cmd[1]))
741 elif cmd[0] == "prev":
743 self.prev_tab(int(cmd[1]))
748 elif cmd[0] == "goto":
749 self.goto_tab(int(cmd[1]))
751 elif cmd[0] == "first":
754 elif cmd[0] == "last":
757 elif cmd[0] in ["title", "uri"]:
759 uzbl = self.get_tab_by_pid(int(cmd[1]))
761 old = getattr(uzbl, cmd[0])
762 new = ' '.join(cmd[2:])
763 setattr(uzbl, cmd[0], new)
765 self.update_tablist()
767 error("parse_command: no uzbl with pid %r" % int(cmd[1]))
769 elif cmd[0] == "preset":
771 error("parse_command: invalid preset command")
773 elif cmd[1] == "save":
774 path = os.path.join(config['saved_sessions_dir'], cmd[2])
775 self.save_session(path)
777 elif cmd[1] == "load":
778 path = os.path.join(config['saved_sessions_dir'], cmd[2])
779 self.load_session(path)
781 elif cmd[1] == "del":
782 path = os.path.join(config['saved_sessions_dir'], cmd[2])
783 if os.path.isfile(path):
787 error("parse_command: preset %r does not exist." % path)
789 elif cmd[1] == "list":
790 uzbl = self.get_tab_by_pid(int(cmd[2]))
792 if not os.path.isdir(config['saved_sessions_dir']):
793 js = "js alert('No saved presets.');"
797 listdir = os.listdir(config['saved_sessions_dir'])
798 listdir = "\\n".join(listdir)
799 js = "js alert('Session presets:\\n\\n%s');" % listdir
803 error("parse_command: unknown tab pid.")
806 error("parse_command: unknown parse command %r"\
809 elif cmd[0] == "clean":
813 error("parse_command: unknown command %r" % ' '.join(cmd))
816 def get_tab_by_pid(self, pid):
817 '''Return uzbl instance by pid.'''
819 for (tab, uzbl) in self.tabs.items():
826 def new_tab(self, uri='', title='', switch=None):
827 '''Add a new tab to the notebook and start a new instance of uzbl.
828 Use the switch option to negate config['switch_to_new_tabs'] option
829 when you need to load multiple tabs at a time (I.e. like when
830 restoring a session from a file).'''
832 pid = self.next_pid()
835 self.notebook.append_page(tab)
839 fifo_filename = 'uzbl_fifo_%s_%0.2d' % (self.wid, pid)
840 fifo_socket = os.path.join(config['fifo_dir'], fifo_filename)
841 socket_filename = 'uzbl_socket_%s_%0.2d' % (self.wid, pid)
842 socket_file = os.path.join(config['socket_dir'], socket_filename)
845 switch = config['switch_to_new_tabs']
848 title = config['new_tab_title']
850 uzbl = self.UzblInstance(self, tab, fifo_socket, socket_file, pid,\
854 uri = "--uri %r" % uri
856 self.tabs[tab] = uzbl
857 cmd = 'uzbl -s %s -n %s_%0.2d %s &' % (sid, self.wid, pid, uri)
858 subprocess.Popen([cmd], shell=True) # TODO: do i need close_fds=True ?
860 # Add gobject timer to make sure the config is pushed when fifo socket
862 timerid = gobject.timeout_add(100, uzbl.flush, "flush-initial-config")
863 uzbl.timers['flush-initial-config'] = timerid
865 self.update_tablist()
868 def clean_slate(self):
869 '''Close all open tabs and open a fresh brand new one.'''
872 tabs = self.tabs.keys()
873 for tab in list(self.notebook)[:-1]:
874 if tab not in tabs: continue
875 uzbl = self.tabs[tab]
879 def config_uzbl(self, uzbl):
880 '''Send bind commands for tab new/close/next/prev to a uzbl
884 bind_format = r'bind %s = sh "echo \"%s\" > \"%s\""'
885 bind = lambda key, action: binds.append(bind_format % (key, action,\
889 set_format = r'set %s = sh \"echo \\"%s\\" > \\"%s\\""'
890 set = lambda key, action: binds.append(set_format % (key, action,\
893 # Bind definitions here
894 # bind(key, command back to fifo)
895 bind(config['bind_new_tab'], 'new')
896 bind(config['bind_tab_from_clip'], 'newfromclip')
897 bind(config['bind_tab_from_uri'], 'new %s')
898 bind(config['bind_close_tab'], 'close')
899 bind(config['bind_next_tab'], 'next')
900 bind(config['bind_prev_tab'], 'prev')
901 bind(config['bind_goto_tab'], 'goto %s')
902 bind(config['bind_goto_first'], 'goto 0')
903 bind(config['bind_goto_last'], 'goto -1')
904 bind(config['bind_clean_slate'], 'clean')
906 # session preset binds
907 bind(config['bind_save_preset'], 'preset save %s')
908 bind(config['bind_load_preset'], 'preset load %s')
909 bind(config['bind_del_preset'], 'preset del %s')
910 bind(config['bind_list_presets'], 'preset list %d' % uzbl.pid)
912 # Set definitions here
913 # set(key, command back to fifo)
914 if config['capture_new_windows']:
915 set("new_window", r'new $8')
917 # Send config to uzbl instance via its socket file.
918 uzbl.send("\n".join(binds+sets))
921 def goto_tab(self, index):
922 '''Goto tab n (supports negative indexing).'''
924 tabs = list(self.notebook)
925 if 0 <= index < len(tabs):
926 self.notebook.set_current_page(index)
927 self.update_tablist()
932 # Update index because index might have previously been a
934 index = tabs.index(tab)
935 self.notebook.set_current_page(index)
936 self.update_tablist()
942 def next_tab(self, step=1):
943 '''Switch to next tab or n tabs right.'''
946 error("next_tab: invalid step %r" % step)
949 ntabs = self.notebook.get_n_pages()
950 tabn = (self.notebook.get_current_page() + step) % ntabs
951 self.notebook.set_current_page(tabn)
952 self.update_tablist()
955 def prev_tab(self, step=1):
956 '''Switch to prev tab or n tabs left.'''
959 error("prev_tab: invalid step %r" % step)
962 ntabs = self.notebook.get_n_pages()
963 tabn = self.notebook.get_current_page() - step
964 while tabn < 0: tabn += ntabs
965 self.notebook.set_current_page(tabn)
966 self.update_tablist()
969 def close_tab(self, tabn=None):
970 '''Closes current tab. Supports negative indexing.'''
973 tabn = self.notebook.get_current_page()
977 tab = list(self.notebook)[tabn]
980 error("close_tab: invalid index %r" % tabn)
983 self.notebook.remove_page(tabn)
986 def tab_opened(self, notebook, tab, index):
987 '''Called upon tab creation. Called by page-added signal.'''
989 if config['switch_to_new_tabs']:
990 self.notebook.set_focus_child(tab)
993 oldindex = self.notebook.get_current_page()
994 oldtab = self.notebook.get_nth_page(oldindex)
995 self.notebook.set_focus_child(oldtab)
998 def tab_closed(self, notebook, tab, index):
999 '''Close the window if no tabs are left. Called by page-removed
1002 if tab in self.tabs.keys():
1003 uzbl = self.tabs[tab]
1004 for (timer, gid) in uzbl.timers.items():
1005 error("tab_closed: removing timer %r" % timer)
1006 gobject.source_remove(gid)
1007 del uzbl.timers[timer]
1010 uzbl._socket.close()
1014 uzbl._socketout = []
1016 self._closed.append((uzbl.uri, uzbl.title))
1017 self._closed = self._closed[-10:]
1020 if self.notebook.get_n_pages() == 0:
1021 if not self._killed and config['save_session']:
1022 if len(self._closed):
1023 d = {'curtab': 0, 'tabs': [self._closed[-1],]}
1024 self.save_session(session=d)
1028 self.update_tablist()
1033 def tab_changed(self, notebook, page, index):
1034 '''Refresh tab list. Called by switch-page signal.'''
1036 tab = self.notebook.get_nth_page(index)
1037 self.notebook.set_focus_child(tab)
1038 self.update_tablist(index)
1042 def update_tablist(self, curpage=None):
1043 '''Upate tablist status bar.'''
1045 show_tablist = config['show_tablist']
1046 show_gtk_tabs = config['show_gtk_tabs']
1047 tab_titles = config['tab_titles']
1048 show_ellipsis = config['show_ellipsis']
1049 if not show_tablist and not show_gtk_tabs:
1052 tabs = self.tabs.keys()
1054 curpage = self.notebook.get_current_page()
1056 title_format = "%s - Uzbl Browser"
1057 max_title_len = config['max_title_len']
1061 normal = (config['tab_colours'], config['tab_text_colours'])
1062 selected = (config['selected_tab'], config['selected_tab_text'])
1064 tab_format = "<span %s> [ %d <span %s> %s</span> ] </span>"
1066 tab_format = "<span %s> [ <span %s>%d</span> ] </span>"
1069 gtk_tab_format = "%d %s"
1071 for index, tab in enumerate(self.notebook):
1072 if tab not in tabs: continue
1073 uzbl = self.tabs[tab]
1075 if index == curpage:
1076 self.window.set_title(title_format % uzbl.title)
1078 tabtitle = uzbl.title[:max_title_len]
1079 if show_ellipsis and len(tabtitle) != len(uzbl.title):
1080 tabtitle = "%s\xe2\x80\xa6" % tabtitle[:-1] # Show Ellipsis
1084 self.notebook.set_tab_label_text(tab,\
1085 gtk_tab_format % (index, tabtitle))
1087 self.notebook.set_tab_label_text(tab, str(index))
1090 style = colour_selector(index, curpage, uzbl)
1091 (tabc, textc) = style
1094 pango += tab_format % (tabc, index, textc,\
1097 pango += tab_format % (tabc, textc, index)
1100 self.tablist.set_markup(pango)
1105 def save_session(self, session_file=None, session=None):
1106 '''Save the current session to file for restoration on next load.'''
1110 if session_file is None:
1111 session_file = config['session_file']
1114 tabs = self.tabs.keys()
1116 for tab in list(self.notebook):
1117 if tab not in tabs: continue
1118 uzbl = self.tabs[tab]
1119 if not uzbl.uri: continue
1120 state += [(uzbl.uri, uzbl.title),]
1122 session = {'curtab': self.notebook.get_current_page(),
1125 if config['json_session']:
1126 raw = json.dumps(session)
1129 lines = ["curtab = %d" % session['curtab'],]
1130 for (uri, title) in session['tabs']:
1131 lines += ["%s\t%s" % (strip(uri), strip(title)),]
1133 raw = "\n".join(lines)
1135 if not os.path.isfile(session_file):
1136 dirname = os.path.dirname(session_file)
1137 if not os.path.isdir(dirname):
1138 os.makedirs(dirname)
1140 h = open(session_file, 'w')
1145 def load_session(self, session_file=None):
1146 '''Load a saved session from file.'''
1148 default_path = False
1150 json_session = config['json_session']
1152 if session_file is None:
1154 session_file = config['session_file']
1156 if not os.path.isfile(session_file):
1159 h = open(session_file, 'r')
1163 if sum([1 for s in raw.split("\n") if strip(s)]) != 1:
1164 error("Warning: The session file %r does not look json. "\
1165 "Trying to load it as a non-json session file."\
1167 json_session = False
1171 session = json.loads(raw)
1172 curtab, tabs = session['curtab'], session['tabs']
1175 error("Failed to load jsonifed session from %r"\
1182 curtab, tabs = 0, []
1183 lines = [s for s in raw.split("\n") if strip(s)]
1185 error("Warning: The non-json session file %r looks invalid."\
1191 if line.startswith("curtab"):
1192 curtab = int(line.split()[-1])
1195 uri, title = line.split("\t",1)
1196 tabs += [(strip(uri), strip(title)),]
1199 error("Warning: failed to load session file %r" % session_file)
1202 session = {'curtab': curtab, 'tabs': tabs}
1204 # Now populate notebook with the loaded session.
1205 for (index, (uri, title)) in enumerate(tabs):
1206 self.new_tab(uri=uri, title=title, switch=(curtab==index))
1208 # There may be other state information in the session dict of use to
1209 # other functions. Of course however the non-json session object is
1210 # just a dummy object of no use to no one.
1214 def quitrequest(self, *args):
1215 '''Called by delete-event signal to kill all uzbl instances.'''
1217 #TODO: Even though I send the kill request to all uzbl instances
1218 # i should add a gobject timeout to check they all die.
1222 if config['save_session']:
1223 if len(list(self.notebook)):
1227 # Notebook has no pages so delete session file if it exists.
1228 if os.path.isfile(session_file):
1229 os.remove(session_file)
1231 for (tab, uzbl) in self.tabs.items():
1235 def quit(self, *args):
1236 '''Cleanup the application and quit. Called by delete-event signal.'''
1238 for (fifo_socket, (fd, watchers)) in self._fifos.items():
1240 for (watcherid, gid) in watchers.items():
1241 gobject.source_remove(gid)
1242 del watchers[watcherid]
1244 del self._fifos[fifo_socket]
1246 for (timerid, gid) in self._timers.items():
1247 gobject.source_remove(gid)
1248 del self._timers[timerid]
1250 if os.path.exists(self.fifo_socket):
1251 os.unlink(self.fifo_socket)
1252 print "Unlinked %s" % self.fifo_socket
1257 if __name__ == "__main__":
1259 # Read from the uzbl config into the global config dictionary.
1260 readconfig(uzbl_config, config)
1262 if config['json_session']:
1264 import simplejson as json
1267 error("Warning: json_session set but cannot import the python "\
1268 "module simplejson. Fix: \"set json_session = 0\" or "\
1269 "install the simplejson python module to remove this warning.")
1270 config['json_session'] = False