#! /usr/libexec/platform-python
"""Interfaces for launching and remotely controlling Web browsers."""
# Maintained by Georg Brandl.
__all__ = ["Error", "open", "open_new", "open_new_tab", "get", "register"]
_browsers = {} # Dictionary of available browser controllers
_tryorder = [] # Preference order of available browsers
def register(name, klass, instance=None, update_tryorder=1):
"""Register a browser connector and, optionally, connection."""
_browsers[name.lower()] = [klass, instance]
elif update_tryorder < 0:
_tryorder.insert(0, name)
"""Return a browser launcher instance appropriate for the environment."""
for browser in alternatives:
# User gave us a command line, split it into name and args
browser = shlex.split(browser)
return BackgroundBrowser(browser[:-1])
return GenericBrowser(browser)
# User gave us a browser name or path.
command = _browsers[browser.lower()]
command = _synthesize(browser)
if command[1] is not None:
elif command[0] is not None:
raise Error("could not locate runnable browser")
# Please note: the following definition hides a builtin function.
# It is recommended one does "import webbrowser" and uses webbrowser.open(url)
# instead of "from webbrowser import *".
def open(url, new=0, autoraise=True):
if browser.open(url, new, autoraise):
def _synthesize(browser, update_tryorder=1):
"""Attempt to synthesize a controller base on existing controllers.
This is useful to create a controller when a user specifies a path to
an entry in the BROWSER environment variable -- we can copy a general
controller to operate using a specific installation of the desired
If we can't create a controller in this way, or if there is no
executable for the requested browser, return [None, None].
if not shutil.which(cmd):
name = os.path.basename(cmd)
command = _browsers[name.lower()]
# now attempt to clone to fit the new name:
if controller and name.lower() == controller.basename:
controller = copy.copy(controller)
controller.name = browser
controller.basename = os.path.basename(browser)
register(browser, None, controller, update_tryorder)
return [None, controller]
class BaseBrowser(object):
"""Parent class for all browsers. Do not use directly."""
def __init__(self, name=""):
def open(self, url, new=0, autoraise=True):
raise NotImplementedError
def open_new_tab(self, url):
class GenericBrowser(BaseBrowser):
"""Class for all browsers started with a command
and without remote functionality."""
def __init__(self, name):
if isinstance(name, str):
# name should be a list with arguments
self.basename = os.path.basename(self.name)
def open(self, url, new=0, autoraise=True):
cmdline = [self.name] + [arg.replace("%s", url)
if sys.platform[:3] == 'win':
p = subprocess.Popen(cmdline)
p = subprocess.Popen(cmdline, close_fds=True)
class BackgroundBrowser(GenericBrowser):
"""Class for all browsers which are to be started in the
def open(self, url, new=0, autoraise=True):
cmdline = [self.name] + [arg.replace("%s", url)
if sys.platform[:3] == 'win':
p = subprocess.Popen(cmdline)
p = subprocess.Popen(cmdline, close_fds=True,
return (p.poll() is None)
class UnixBrowser(BaseBrowser):
"""Parent class for all Unix browsers with remote functionality."""
# In remote_args, %s will be replaced with the requested URL. %action will
# be replaced depending on the value of 'new' passed to open.
# remote_action is used for new=0 (open). If newwin is not None, it is
# used for new=1 (open_new). If newtab is not None, it is used for
# new=3 (open_new_tab). After both substitutions are made, any empty
# strings in the transformed remote_args list will be removed.
remote_args = ['%action', '%s']
remote_action_newwin = None
remote_action_newtab = None
def _invoke(self, args, remote, autoraise):
if remote and self.raise_opts:
# use autoraise argument only for remote invocation
autoraise = int(autoraise)
opt = self.raise_opts[autoraise]
if opt: raise_opt = [opt]
cmdline = [self.name] + raise_opt + args
if remote or self.background:
inout = subprocess.DEVNULL
# for TTY browsers, we need stdin/out
p = subprocess.Popen(cmdline, close_fds=True, stdin=inout,
stdout=(self.redirect_stdout and inout or None),
stderr=inout, start_new_session=True)
# wait at most five seconds. If the subprocess is not finished, the
# remote invocation has (hopefully) started a new instance.
# if remote call failed, open() will try direct invocation
except subprocess.TimeoutExpired:
def open(self, url, new=0, autoraise=True):
action = self.remote_action
action = self.remote_action_newwin
if self.remote_action_newtab is None:
action = self.remote_action_newwin
action = self.remote_action_newtab
raise Error("Bad 'new' parameter to open(); " +
"expected 0, 1, or 2, got %s" % new)
args = [arg.replace("%s", url).replace("%action", action)
for arg in self.remote_args]
args = [arg for arg in args if arg]
success = self._invoke(args, True, autoraise)
# remote invocation failed, try straight way
args = [arg.replace("%s", url) for arg in self.args]
return self._invoke(args, False, False)
class Mozilla(UnixBrowser):
"""Launcher class for Mozilla browsers."""
remote_args = ['%action', '%s']
remote_action_newwin = "-new-window"
remote_action_newtab = "-new-tab"
class Netscape(UnixBrowser):
"""Launcher class for Netscape browser."""
raise_opts = ["-noraise", "-raise"]
remote_args = ['-remote', 'openURL(%s%action)']
remote_action_newwin = ",new-window"
remote_action_newtab = ",new-tab"
class Galeon(UnixBrowser):
"""Launcher class for Galeon/Epiphany browsers."""
raise_opts = ["-noraise", ""]
remote_args = ['%action', '%s']
remote_action_newwin = "-w"
class Chrome(UnixBrowser):
"Launcher class for Google Chrome browser."
remote_args = ['%action', '%s']
remote_action_newwin = "--new-window"
remote_action_newtab = ""
class Opera(UnixBrowser):
"Launcher class for Opera browser."
remote_args = ['%action', '%s']
remote_action_newwin = "--new-window"
remote_action_newtab = ""
class Elinks(UnixBrowser):
"Launcher class for Elinks browsers."
remote_args = ['-remote', 'openURL(%s%action)']
remote_action_newwin = ",new-window"
remote_action_newtab = ",new-tab"
# elinks doesn't like its stdout to be redirected -
# it uses redirected stdout as a signal to do -dump
class Konqueror(BaseBrowser):
"""Controller for the KDE File Manager (kfm, or Konqueror).
See the output of ``kfmclient --commands``
for more information on the Konqueror remote-control interface.
def open(self, url, new=0, autoraise=True):
# XXX Currently I know no way to prevent KFM from opening a new win.
devnull = subprocess.DEVNULL
p = subprocess.Popen(["kfmclient", action, url],
close_fds=True, stdin=devnull,
stdout=devnull, stderr=devnull)
# fall through to next variant
# kfmclient's return code unfortunately has no meaning as it seems
p = subprocess.Popen(["konqueror", "--silent", url],
close_fds=True, stdin=devnull,
stdout=devnull, stderr=devnull,
# fall through to next variant
p = subprocess.Popen(["kfm", "-d", url],
close_fds=True, stdin=devnull,
stdout=devnull, stderr=devnull,
return (p.poll() is None)
class Grail(BaseBrowser):
# There should be a way to maintain a connection to Grail, but the
# Grail remote control protocol doesn't really allow that at this
# point. It probably never will!
def _find_grail_rc(self):
tempdir = os.path.join(tempfile.gettempdir(),
user = pwd.getpwuid(os.getuid())[0]
filename = os.path.join(tempdir, user + "-*")
maybes = glob.glob(filename)
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
# need to PING each one until we find one that's live
# no good; attempt to clean it out, but don't fail:
def _remote(self, action):
s = self._find_grail_rc()
def open(self, url, new=0, autoraise=True):
ok = self._remote("LOADNEW " + url)
ok = self._remote("LOAD " + url)
# Platform support for Unix
# These are the right tests because all these Unix browsers require either
# a console terminal or an X display to run.
def register_X_browsers():
if shutil.which("xdg-open"):
register("xdg-open", None, BackgroundBrowser("xdg-open"))
# The default GNOME3 browser
if "GNOME_DESKTOP_SESSION_ID" in os.environ and shutil.which("gvfs-open"):
register("gvfs-open", None, BackgroundBrowser("gvfs-open"))
# The default GNOME browser
if "GNOME_DESKTOP_SESSION_ID" in os.environ and shutil.which("gnome-open"):
register("gnome-open", None, BackgroundBrowser("gnome-open"))
# The default KDE browser
if "KDE_FULL_SESSION" in os.environ and shutil.which("kfmclient"):
register("kfmclient", Konqueror, Konqueror("kfmclient"))
if shutil.which("x-www-browser"):
register("x-www-browser", None, BackgroundBrowser("x-www-browser"))
for browser in ("firefox", "iceweasel", "iceape", "seamonkey"):
if shutil.which(browser):
register(browser, None, Mozilla(browser))
# The Netscape and old Mozilla browsers
for browser in ("mozilla-firefox",
"mozilla-firebird", "firebird",
if shutil.which(browser):
register(browser, None, Netscape(browser))
# Konqueror/kfm, the KDE browser.
register("kfm", Konqueror, Konqueror("kfm"))
elif shutil.which("konqueror"):
register("konqueror", Konqueror, Konqueror("konqueror"))
# Gnome's Galeon and Epiphany
for browser in ("galeon", "epiphany"):
if shutil.which(browser):
register(browser, None, Galeon(browser))
# Skipstone, another Gtk/Mozilla based browser
if shutil.which("skipstone"):
register("skipstone", None, BackgroundBrowser("skipstone"))
# Google Chrome/Chromium browsers
for browser in ("google-chrome", "chrome", "chromium", "chromium-browser"):
if shutil.which(browser):
register(browser, None, Chrome(browser))
if shutil.which("opera"):
register("opera", None, Opera("opera"))
# Next, Mosaic -- old but still in use.
if shutil.which("mosaic"):
register("mosaic", None, BackgroundBrowser("mosaic"))
# Grail, the Python browser. Does anybody still use it?
if shutil.which("grail"):
register("grail", Grail, None)
# Prefer X browsers if present
if os.environ.get("DISPLAY"):
# Also try console browsers
if os.environ.get("TERM"):
if shutil.which("www-browser"):
register("www-browser", None, GenericBrowser("www-browser"))
# The Links/elinks browsers <http://artax.karlin.mff.cuni.cz/~mikulas/links/>
if shutil.which("links"):
register("links", None, GenericBrowser("links"))
if shutil.which("elinks"):
register("elinks", None, Elinks("elinks"))
# The Lynx browser <http://lynx.isc.org/>, <http://lynx.browser.org/>
register("lynx", None, GenericBrowser("lynx"))
# The w3m browser <http://w3m.sourceforge.net/>