#!/usr/bin/python
########################################################################
#
# ffselect.py
#
# This is a GNOME Panel applet for managing multiple instances of
# the Firefox web browser, where each instance may be using a different
# profile with distinct browser settings.
#
# When you click on the applet's icon, a menu is presented which lists
# the running browser windows and their associated profiles, as well as
# profiles which are not currently running. If you click on an item
# representing a running browser window, the window will be raised to
# the foreground. If you click on an item representing a non-running
# profile, a new Firefox instance will be launched with the selected
# profile.
#
# This program is in the public domain - do whatever you want with it.
#
# David Simmons
# October 24, 2009
#
# 2010-01-17:
# This script stopped working after a recent Ubuntu update. Some
# change exposed a minor bug in the code. This is now corrected.
#
########################################################################
#
# INSTALLATION INSTRUCTIONS:
#
# Using this script as a GNOME Panel applet requires registering
# it with GNOME's Bonobo component system. If you run the script
# with the "install" argument as root, it will try to perform
# this registration.
#
# 1. Place the script in its final location:
# cp ffselect.py /usr/local/bin/
# 2. Run the script with the "install" argument with root privileges:
# sudo /usr/local/bin/ffselect.py install
# 3. Refresh the GNOME Panel:
# killall gnome-panel
# 4. Right-click the GNOME Panel, select "Add to Panel", and pick
# the Firefox Selection Applet.
#
# NOTES:
#
# This script requires the python-xlib library, available in Ubuntu
# with "sudo apt-get install python-xlib" or from this web site:
# http://python-xlib.sourceforge.net/
#
# This script also requires pygtk, wnck, and other libraries which
# seemed to already be installed on my Ubuntu 8.04 system.
#
# CAVEATS:
#
# I've only tested this with Firefox 3.5.3.
#
########################################################################
########################################################################
# configuration
########################################################################
# set this to the full path to firefox, so the script can invoke
# firefox directly when it can't find a suitable existing window
PATH_TO_FIREFOX = "/usr/bin/firefox"
# set this to a firefox icon. this will be the icon which will
# appear in your panel. if this file is not available, the
# applet will be represented by an ugly "FF" text button.
FIREFOX_ICON = "/usr/share/pixmaps/firefox-3.0.png"
# the remaining settings are not used in this script. they
# are leftover from my ffremote.py script from which I
# borrowed code.
# if another application is controlling firefox, retry every
# LOCK_RETRY_INTERVAL seconds, until LOCK_OVERRIDE_TIME seconds
# have passed. you probably don't need to change this.
LOCK_RETRY_INTERVAL = 0.5
LOCK_OVERRIDE_TIME = 5
########################################################################
import sys
import os
import time
from Xlib import X, display, Xatom
from socket import gethostname
import pygtk
import sys
pygtk.require('2.0')
import gobject
import gnomeapplet
import gtk
import re
import wnck
import pango
######################################################################
# bonobo-activation component installation
######################################################################
# if the program is run with the "install" argument, install the
# bonobo server file into a suitable path so the applet can be
# available for use in the panel.
if len(sys.argv) == 2 and sys.argv[1] == "install":
# path discovery
script_path = os.path.abspath(sys.argv[0])
# bonobo server directory discovery
bonobo_dir = None;
for dir in ['/usr','/usr/local']:
test_dir = dir+'/lib/bonobo/servers'
if os.path.exists(test_dir):
bonobo_dir = test_dir
break
if not bonobo_dir:
print 'error: cannot find a suitable bonobo server path in which to'
print ' install our ffselect.server component descriptor.'
os._exit(1)
# final .server path
server_path = bonobo_dir + '/ffselect.server'
print
print 'Attempting to register this applet script with bonobo'
print 'using the following parameters:'
print
print 'Script path: '+script_path
print 'Bonobo server path: '+bonobo_dir
print 'Component descriptor: '+server_path
print
component_descriptor = '''
'''
outfile = open(server_path, 'w')
outfile.write(component_descriptor)
outfile.close()
print 'Success!'
print 'You may need to reload your GNOME Panel with the following command:'
print "\tkillall gnome-panel"
print 'Then, right-click the panel, select "Add to Panel...", and'
print 'select the Firefox Selection Applet.'
print
os._exit(0)
######################################################################
#
# class Firefox
#
# This class represents a single Firefox instance. It knows how to
# retrieve information about the instance, and issue commands.
#
class Firefox:
class State:
def __init__(self, properties):
self.properties = properties
self.reset()
def reset(self):
for i in self.properties:
self.__dict__[i] = None
def __init__(self, properties, atoms, xdisplay, window, pid):
self.properties = properties
self.atoms = atoms
self.xdisplay = xdisplay
self.window = window
self.state = Firefox.State(properties)
self.update_state()
self.pid = pid
self.browsers = []
def set_windows(self,windows):
self.browsers = [];
for window in windows:
name = window.get_wm_name()
if (not name) or len(name) == 0:
p = window.get_property(self.atoms["wm_name"], self.atoms["compound_text"], 0, 1000)
if p:
name = p.value
if name.endswith(" - Mozilla Firefox"):
name = name[0:len(name)-len(" - Mozilla Firefox")]
browser = {};
browser["window"] = window
browser["name"] = name
self.browsers.append(browser)
def update_state(self):
self.state.reset()
for i in self.properties:
if i in self.atoms:
property = self.window.get_property(
self.atoms[i],
Xatom.STRING,
0,
1024
)
if property:
self.state.__dict__[i] = property.value
def show(self):
self.update_state()
self.show_current_state()
def show_current_state(self):
print '%-6s 0x%-10x %-20s %-15s %-10s %-10s %-10s %-20s %s' % (
self.pid,
self.window.id,
self.state.program+" "+self.state.version,
self.state.profile,
self.state.user,
self.state.command,
self.state.response,
self.state.lock,
self.state.commandline
)
def lock(self):
lock_value = "pid%d@%s" % (os.getpid(), gethostname())
# check to see if another application has locked this instance
lock_property = self.window.get_property(
self.atoms["lock"],
Xatom.STRING,
0,
128
)
lock_retry_elapsed = 0
while lock_property:
if lock_retry_elapsed == 0:
print "firefox locked by %s -- waiting..." % (
lock_property.value
)
time.sleep(LOCK_RETRY_INTERVAL)
lock_retry_elapsed += LOCK_RETRY_INTERVAL
if lock_retry_elapsed >= LOCK_OVERRIDE_TIME:
print "overriding lock after %d seconds." % LOCK_OVERRIDE_TIME
break
lock_property = self.window.get_property(
self.atoms["lock"],
Xatom.STRING,
0,
128
)
# mark our territory
self.window.change_property(
self.atoms["lock"],
Xatom.STRING,
8,
lock_value
)
self.xdisplay.sync()
def unlock(self):
# clear the lock property
property = self.window.get_property(
self.atoms["lock"],
Xatom.STRING,
0, 128,
True # delete after getting
);
self.xdisplay.sync()
def send_command(self, command):
self.lock()
self.window.change_property(
self.atoms["command"],
Xatom.STRING,
8,
command
)
self.unlock()
#
# class FirefoxScanner
#
# This class scans the toplevel windows of the X11 display,
# looking for Firefox instances.
#
class FirefoxScanner:
properties = {
"version": "_MOZILLA_VERSION",
"lock": "_MOZILLA_LOCK",
"command": "_MOZILLA_COMMAND",
"response": "_MOZILLA_RESPONSE",
"user": "_MOZILLA_USER",
"profile": "_MOZILLA_PROFILE",
"program": "_MOZILLA_PROGRAM",
"commandline": "_MOZILLA_COMMANDLINE",
"wm_pid": "_NET_WM_PID",
"window_type": "_NET_WM_WINDOW_TYPE",
"normal": "_NET_WM_WINDOW_TYPE_NORMAL",
"window_role": "WM_WINDOW_ROLE",
"wm_name": "WM_NAME",
"compound_text":"COMPOUND_TEXT",
}
def __init__(self, xdisplay):
self.xdisplay = xdisplay
self.root = xdisplay.screen().root
self.initialize_atoms()
def initialize_atoms(self):
self.atoms = {};
for i in self.properties:
atom = self.xdisplay.get_atom(self.properties[i], 1)
if atom:
self.atoms[i] = atom
def is_firefox(self, window):
# query the _MOZILLA_PROGRAM property on this window
p = window.get_property(self.atoms["program"], Xatom.STRING, 0, 1000)
# Other Mozilla programs (like Thunderbird) will also have these
# properties, so we make sure to only regard "firefox".
if p and (p.value == "firefox"):
return 1
else:
return 0
def get_window_pid(self,window):
# query the _NET_WM_PID property on this window
p = window.get_property(self.atoms["wm_pid"], Xatom.CARDINAL, 0, 1000)
if p and len(p.value) > 0:
return str(p.value[0])
else:
return None
def is_browser(self,window):
# query the WM_WINDOW_ROLE property on this window
p = window.get_property(self.atoms["window_role"], Xatom.STRING, 0, 1000)
if p and p.value == "browser":
return True
else:
return False
def add_window_pid(self,window,pid):
if pid in self.window_pids:
self.window_pids[pid].append(window)
else:
self.window_pids[pid] = [ window ]
def scan(self):
self.instances = []
self.window_pids = {}
bypid = {}
# if the _MOZILLA_PROGRAM atom isn't available, then there's
# no use in scanning. (i.e., firefox hasn't been run yet
# in this X session.)
if not "program" in self.atoms:
return
# iterate over toplevel windows
for window in self.root.query_tree().children:
pid = self.get_window_pid(window)
# look for the unmapped firefox control window
if self.is_firefox(window):
firefox = Firefox(self.properties,self.atoms,self.xdisplay,window,pid)
self.instances.append(firefox)
bypid[pid] = firefox
# add this window/pid to our dictionary
if pid and self.is_browser(window):
self.add_window_pid(window,pid)
# iterate over child windows and add those to the dictionary
for child in window.query_tree().children:
pid = self.get_window_pid(child)
if pid and self.is_browser(child):
self.add_window_pid(child,pid)
# associate browser windows with firefox instances
for pid in self.window_pids:
if pid in bypid:
bypid[pid].set_windows(self.window_pids[pid])
def show(self):
self.xdisplay.sync()
print '%-6s %-12s %-20s %-15s %-10s %-10s %-10s %-20s %s' % (
'pid', 'window id', 'program/ver', 'profile', 'user',
'command', 'response', 'lock', 'commandline'
);
print '%-6s %-12s %-20s %-15s %-10s %-10s %-10s %-20s %s' % (
'------',
'------------', '--------------------',
'---------------',
'----------',
'----------',
'----------',
'--------------------',
'---------------',
);
for i in self.instances:
i.show_current_state()
def get_instance(self, profile):
for i in self.instances:
if i.state.profile == profile:
return i
return None
def profile_compare(x,y):
if x.lower() == "default":
return -1
elif y.lower() == "default":
return +1
else:
return cmp(x,y)
#
# class BrowserMenuItem
#
# This class renders the multi-line menu item used to represent
# a browser window/profile.
#
class BrowserMenuItem(gtk.MenuItem):
def __init__(self,profile,instance,browser):
self.profile = profile
self.instance = instance
self.browser = browser
name = None
if self.browser:
name = self.browser['name']
else:
name = "(not running)"
profile_label = gtk.Label('Profile: '+profile)
profile_label.set_alignment(0,0)
profile_label.modify_font(pango.FontDescription("sans bold 12"))
profile_label.show()
name_label = gtk.Label(name)
name_label.set_alignment(0,0)
name_label.modify_font(pango.FontDescription("normal italic"))
name_label.show()
vbox = gtk.VBox(True,0)
vbox.add(profile_label);
vbox.add(name_label);
vbox.show()
gtk.MenuItem.__init__(self)
self.add(vbox)
def do_expose_event(self, event):
retval = gtk.MenuItem.do_expose_event(self, event)
return retval
gobject.type_register(BrowserMenuItem)
#
# class FirefoxSelect
#
# This is the primary applet class.
#
class FirefoxSelect:
def profile_scan(self):
filename = os.environ['HOME']+'/.mozilla/firefox/profiles.ini'
infile = open(filename,'r')
in_profile_section = False
profiles = []
for line in infile:
m = re.search('^\s*\[(.*)\]\s*$', line)
if m:
if re.search('^profile\d*$', m.group(1), re.IGNORECASE):
in_profile_section = True
else:
in_profile_section = False
if in_profile_section:
m = re.search('^\s*Name\s*=\s*(\S+)\s*$', line)
if m:
profiles.append(m.group(1))
profiles.sort(profile_compare);
return profiles
def update_menu(self):
# always re-init the atoms, since some may not have been available
# the last time initialize_atoms was executed. for instance, if
# firefox hadn't yet run in this X session, the _MOZILLA_* atoms
# might not have been available.
self.scanner.initialize_atoms()
# perform the window scanning
self.scanner.scan()
self.menu = gtk.Menu()
self.browserItems = []
for profile in self.profile_scan():
instance = self.scanner.get_instance(profile)
if instance:
for browser in instance.browsers:
self.browserItems.append(BrowserMenuItem(profile, instance, browser))
else:
self.browserItems.append(BrowserMenuItem(profile, None, None))
for item in self.browserItems:
self.menu.append(item)
item.connect("activate", self.select)
for item in self.browserItems:
item.show()
def select(self, item):
if not item.instance:
# run a new instance of firefox, using the desired profile
args = [
PATH_TO_FIREFOX,
'-no-remote',
'-P',
item.profile
]
os.spawnv(os.P_NOWAIT, PATH_TO_FIREFOX, args)
else:
# use wnck to raise the window.
# we can't use python-xlib, since it uses a
# different connection to the X server that the
# window manager wouldn't trust.
window = item.browser['window']
screen = wnck.screen_get_default()
while gtk.events_pending():
gtk.main_iteration()
for w in screen.get_windows():
if w.get_xid() == window.id:
w.activate(gtk.get_current_event_time())
def delete_event(self, widget, event, data=None):
return False
def destroy(self, widget, data=None):
gtk.main_quit()
def show(self, button, event):
self.update_menu()
self.menu.popup(None,None,None,event.button,event.time)
def __init__(self):
self.xdisplay = display.Display()
self.scanner = FirefoxScanner(self.xdisplay);
######################################################################
# create an instance of the applet class
firefoxSelect = FirefoxSelect();
def factory(applet, iid):
icon = gtk.Image()
try:
pixbuf = gtk.gdk.pixbuf_new_from_file(FIREFOX_ICON)
except gobject.GError:
icon = None
if icon:
scaled_buf = pixbuf.scale_simple(24,24,gtk.gdk.INTERP_BILINEAR)
icon.set_from_pixbuf(scaled_buf)
icon.show()
button = gtk.Button()
button.set_relief(gtk.RELIEF_NONE)
if icon:
button.add(icon)
else:
button.set_label("FF")
button.connect("button_press_event", showMenu, applet)
applet.add(button)
applet.show_all()
return True
def showMenu(widget, event, applet):
if event.type == gtk.gdk.BUTTON_PRESS:
if event.button == 3:
widget.emit_stop_by_name("button_press_event")
create_menu(applet)
if event.button == 1:
widget.emit_stop_by_name("button_press_event")
firefoxSelect.show(widget, event)
def create_menu(applet):
propxml="""
"""
verbs = [("About", showAboutDialog)]
# propxml="""
#
#
#
#
# """
# verbs = [("About", showAboutDialog), ("Quit", quitApplet), ("Invoke", invokeApplet)]
applet.setup_menu(propxml, verbs, None)
def showAboutDialog(*arguments, **keywords):
dialog = gtk.AboutDialog()
dialog.set_name("Firefox Selection Applet")
dialog.set_version("0.01")
dialog.set_copyright("by David Simmons")
dialog.set_license('''
This work is in the Public Domain. To view a copy of the public domain
certification, visit http://creativecommons.org/licenses/publicdomain/
or send a letter to Creative Commons, 171 Second Street, Suite 300,
San Francisco, California, 94105, USA.
''')
dialog.set_website('http://davidsimmons.com/')
dialog.run()
dialog.destroy()
pass
def quitApplet(*arguments, **keywords):
os._exit(0)
def invokeApplet(*arguments, **keywords):
firefoxSelect.show(None, None)
######################################################################
# if the program is run with the "run-in-window" argument, or
# it is run as ./ffselect.py then launch in non-applet mode.
# this is useful for testing.
if (len(sys.argv) == 2 and sys.argv[1] == "run-in-window") or (len(sys.argv) == 1 and sys.argv[0].startswith("./")):
mainWindow = gtk.Window(gtk.WINDOW_TOPLEVEL)
mainWindow.set_title("Ubuntu System Panel")
mainWindow.connect("destroy", gtk.main_quit)
applet = gnomeapplet.Applet()
factory(applet, None)
applet.reparent(mainWindow)
mainWindow.show_all()
gtk.main()
sys.exit()
# launch the program in applet mode.
if __name__ == '__main__':
gnomeapplet.bonobo_factory(
"OAFIID:GNOME_Ffselect_Factory",
gnomeapplet.Applet.__gtype__,
"Firefox Selection Applet",
"0.01",
factory
)
######################################################################