2 """Interfaces for launching and remotely controlling Web browsers."""
10 __all__
= ["Error", "open", "open_new", "open_new_tab", "get", "register"]
12 class Error(Exception):
15 _browsers
= {} # Dictionary of available browser controllers
16 _tryorder
= [] # Preference order of available browsers
18 def register(name
, klass
, instance
=None, update_tryorder
=1):
19 """Register a browser connector and, optionally, connection."""
20 _browsers
[name
.lower()] = [klass
, instance
]
21 if update_tryorder
> 0:
22 _tryorder
.append(name
)
23 elif update_tryorder
< 0:
24 _tryorder
.insert(0, name
)
27 """Return a browser launcher instance appropriate for the environment."""
29 alternatives
= [using
]
31 alternatives
= _tryorder
32 for browser
in alternatives
:
34 # User gave us a command line, split it into name and args
35 return GenericBrowser(browser
.split())
37 # User gave us a browser name or path.
39 command
= _browsers
[browser
.lower()]
41 command
= _synthesize(browser
)
42 if command
[1] is not None:
44 elif command
[0] is not None:
46 raise Error("could not locate runnable browser")
48 # Please note: the following definition hides a builtin function.
49 # It is recommended one does "import webbrowser" and uses webbrowser.open(url)
50 # instead of "from webbrowser import *".
52 def open(url
, new
=0, autoraise
=1):
53 for name
in _tryorder
:
55 if browser
.open(url
, new
, autoraise
):
62 def open_new_tab(url
):
66 def _synthesize(browser
, update_tryorder
=1):
67 """Attempt to synthesize a controller base on existing controllers.
69 This is useful to create a controller when a user specifies a path to
70 an entry in the BROWSER environment variable -- we can copy a general
71 controller to operate using a specific installation of the desired
74 If we can't create a controller in this way, or if there is no
75 executable for the requested browser, return [None, None].
78 cmd
= browser
.split()[0]
79 if not _iscommand(cmd
):
81 name
= os
.path
.basename(cmd
)
83 command
= _browsers
[name
.lower()]
86 # now attempt to clone to fit the new name:
87 controller
= command
[1]
88 if controller
and name
.lower() == controller
.basename
:
90 controller
= copy
.copy(controller
)
91 controller
.name
= browser
92 controller
.basename
= os
.path
.basename(browser
)
93 register(browser
, None, controller
, update_tryorder
)
94 return [None, controller
]
98 if sys
.platform
[:3] == "win":
99 def _isexecutable(cmd
):
101 if os
.path
.isfile(cmd
) and (cmd
.endswith(".exe") or
102 cmd
.endswith(".bat")):
104 for ext
in ".exe", ".bat":
105 if os
.path
.isfile(cmd
+ ext
):
109 def _isexecutable(cmd
):
110 if os
.path
.isfile(cmd
):
111 mode
= os
.stat(cmd
)[stat
.ST_MODE
]
112 if mode
& stat
.S_IXUSR
or mode
& stat
.S_IXGRP
or mode
& stat
.S_IXOTH
:
117 """Return True if cmd is executable or can be found on the executable
119 if _isexecutable(cmd
):
121 path
= os
.environ
.get("PATH")
124 for d
in path
.split(os
.pathsep
):
125 exe
= os
.path
.join(d
, cmd
)
126 if _isexecutable(exe
):
131 # General parent classes
133 class BaseBrowser(object):
134 """Parent class for all browsers. Do not use directly."""
138 def __init__(self
, name
=""):
142 def open(self
, url
, new
=0, autoraise
=1):
143 raise NotImplementedError
145 def open_new(self
, url
):
146 return self
.open(url
, 1)
148 def open_new_tab(self
, url
):
149 return self
.open(url
, 2)
152 class GenericBrowser(BaseBrowser
):
153 """Class for all browsers started with a command
154 and without remote functionality."""
156 def __init__(self
, name
):
157 if isinstance(name
, basestring
):
160 # name should be a list with arguments
163 self
.basename
= os
.path
.basename(self
.name
)
165 def open(self
, url
, new
=0, autoraise
=1):
166 cmdline
= [self
.name
] + [arg
.replace("%s", url
)
167 for arg
in self
.args
]
169 p
= subprocess
.Popen(cmdline
, close_fds
=True)
175 class BackgroundBrowser(GenericBrowser
):
176 """Class for all browsers which are to be started in the
179 def open(self
, url
, new
=0, autoraise
=1):
180 cmdline
= [self
.name
] + [arg
.replace("%s", url
)
181 for arg
in self
.args
]
182 setsid
= getattr(os
, 'setsid', None)
184 setsid
= getattr(os
, 'setpgrp', None)
186 p
= subprocess
.Popen(cmdline
, close_fds
=True, preexec_fn
=setsid
)
187 return (p
.poll() is None)
192 class UnixBrowser(BaseBrowser
):
193 """Parent class for all Unix browsers with remote functionality."""
196 remote_args
= ['%action', '%s']
198 remote_action_newwin
= None
199 remote_action_newtab
= None
201 redirect_stdout
= True
203 def _invoke(self
, args
, remote
, autoraise
):
205 if remote
and self
.raise_opts
:
206 # use autoraise argument only for remote invocation
207 autoraise
= int(bool(autoraise
))
208 opt
= self
.raise_opts
[autoraise
]
209 if opt
: raise_opt
= [opt
]
211 cmdline
= [self
.name
] + raise_opt
+ args
213 if remote
or self
.background
:
214 inout
= file(os
.devnull
, "r+")
216 # for TTY browsers, we need stdin/out
218 # if possible, put browser in separate process group, so
219 # keyboard interrupts don't affect browser as well as Python
220 setsid
= getattr(os
, 'setsid', None)
222 setsid
= getattr(os
, 'setpgrp', None)
224 p
= subprocess
.Popen(cmdline
, close_fds
=True, stdin
=inout
,
225 stdout
=(self
.redirect_stdout
and inout
or None),
226 stderr
=inout
, preexec_fn
=setsid
)
228 # wait five secons. If the subprocess is not finished, the
229 # remote invocation has (hopefully) started a new instance.
237 # if remote call failed, open() will try direct invocation
239 elif self
.background
:
247 def open(self
, url
, new
=0, autoraise
=1):
249 action
= self
.remote_action
251 action
= self
.remote_action_newwin
253 if self
.remote_action_newtab
is None:
254 action
= self
.remote_action_newwin
256 action
= self
.remote_action_newtab
258 raise Error("Bad 'new' parameter to open(); " +
259 "expected 0, 1, or 2, got %s" % new
)
261 args
= [arg
.replace("%s", url
).replace("%action", action
)
262 for arg
in self
.remote_args
]
263 success
= self
._invoke
(args
, True, autoraise
)
265 # remote invocation failed, try straight way
266 args
= [arg
.replace("%s", url
) for arg
in self
.args
]
267 return self
._invoke
(args
, False, False)
272 class Mozilla(UnixBrowser
):
273 """Launcher class for Mozilla/Netscape browsers."""
275 raise_opts
= ["-noraise", "-raise"]
277 remote_args
= ['-remote', 'openURL(%s%action)']
279 remote_action_newwin
= ",new-window"
280 remote_action_newtab
= ",new-tab"
287 class Galeon(UnixBrowser
):
288 """Launcher class for Galeon/Epiphany browsers."""
290 raise_opts
= ["-noraise", ""]
291 remote_args
= ['%action', '%s']
293 remote_action_newwin
= "-w"
298 class Opera(UnixBrowser
):
299 "Launcher class for Opera browser."
301 raise_opts
= ["", "-raise"]
303 remote_args
= ['-remote', 'openURL(%s%action)']
305 remote_action_newwin
= ",new-window"
306 remote_action_newtab
= ",new-page"
310 class Elinks(UnixBrowser
):
311 "Launcher class for Elinks browsers."
313 remote_args
= ['-remote', 'openURL(%s%action)']
315 remote_action_newwin
= ",new-window"
316 remote_action_newtab
= ",new-tab"
319 # elinks doesn't like its stdout to be redirected -
320 # it uses redirected stdout as a signal to do -dump
321 redirect_stdout
= False
324 class Konqueror(BaseBrowser
):
325 """Controller for the KDE File Manager (kfm, or Konqueror).
327 See the output of ``kfmclient --commands``
328 for more information on the Konqueror remote-control interface.
331 def open(self
, url
, new
=0, autoraise
=1):
332 # XXX Currently I know no way to prevent KFM from opening a new win.
338 devnull
= file(os
.devnull
, "r+")
339 # if possible, put browser in separate process group, so
340 # keyboard interrupts don't affect browser as well as Python
341 setsid
= getattr(os
, 'setsid', None)
343 setsid
= getattr(os
, 'setpgrp', None)
346 p
= subprocess
.Popen(["kfmclient", action
, url
],
347 close_fds
=True, stdin
=devnull
,
348 stdout
=devnull
, stderr
=devnull
)
350 # fall through to next variant
354 # kfmclient's return code unfortunately has no meaning as it seems
358 p
= subprocess
.Popen(["konqueror", "--silent", url
],
359 close_fds
=True, stdin
=devnull
,
360 stdout
=devnull
, stderr
=devnull
,
363 # fall through to next variant
367 # Should be running now.
371 p
= subprocess
.Popen(["kfm", "-d", url
],
372 close_fds
=True, stdin
=devnull
,
373 stdout
=devnull
, stderr
=devnull
,
378 return (p
.poll() is None)
381 class Grail(BaseBrowser
):
382 # There should be a way to maintain a connection to Grail, but the
383 # Grail remote control protocol doesn't really allow that at this
384 # point. It probably never will!
385 def _find_grail_rc(self
):
390 tempdir
= os
.path
.join(tempfile
.gettempdir(),
392 user
= pwd
.getpwuid(os
.getuid())[0]
393 filename
= os
.path
.join(tempdir
, user
+ "-*")
394 maybes
= glob
.glob(filename
)
397 s
= socket
.socket(socket
.AF_UNIX
, socket
.SOCK_STREAM
)
399 # need to PING each one until we find one that's live
403 # no good; attempt to clean it out, but don't fail:
411 def _remote(self
, action
):
412 s
= self
._find
_grail
_rc
()
419 def open(self
, url
, new
=0, autoraise
=1):
421 ok
= self
._remote
("LOADNEW " + url
)
423 ok
= self
._remote
("LOAD " + url
)
428 # Platform support for Unix
431 # These are the right tests because all these Unix browsers require either
432 # a console terminal or an X display to run.
434 def register_X_browsers():
435 # The default Gnome browser
436 if _iscommand("gconftool-2"):
437 # get the web browser string from gconftool
438 gc
= 'gconftool-2 -g /desktop/gnome/url-handlers/http/command'
440 commd
= out
.read().strip()
441 retncode
= out
.close()
443 # if successful, register it
444 if retncode
== None and len(commd
) != 0:
445 register("gnome", None, BackgroundBrowser(commd
))
447 # First, the Mozilla/Netscape browsers
448 for browser
in ("mozilla-firefox", "firefox",
449 "mozilla-firebird", "firebird",
450 "mozilla", "netscape"):
451 if _iscommand(browser
):
452 register(browser
, None, Mozilla(browser
))
454 # Konqueror/kfm, the KDE browser.
455 if _iscommand("kfm"):
456 register("kfm", Konqueror
, Konqueror("kfm"))
457 elif _iscommand("konqueror"):
458 register("konqueror", Konqueror
, Konqueror("konqueror"))
460 # Gnome's Galeon and Epiphany
461 for browser
in ("galeon", "epiphany"):
462 if _iscommand(browser
):
463 register(browser
, None, Galeon(browser
))
465 # Skipstone, another Gtk/Mozilla based browser
466 if _iscommand("skipstone"):
467 register("skipstone", None, BackgroundBrowser("skipstone"))
469 # Opera, quite popular
470 if _iscommand("opera"):
471 register("opera", None, Opera("opera"))
473 # Next, Mosaic -- old but still in use.
474 if _iscommand("mosaic"):
475 register("mosaic", None, BackgroundBrowser("mosaic"))
477 # Grail, the Python browser. Does anybody still use it?
478 if _iscommand("grail"):
479 register("grail", Grail
, None)
481 # Prefer X browsers if present
482 if os
.environ
.get("DISPLAY"):
483 register_X_browsers()
485 # Also try console browsers
486 if os
.environ
.get("TERM"):
487 # The Links/elinks browsers <http://artax.karlin.mff.cuni.cz/~mikulas/links/>
488 if _iscommand("links"):
489 register("links", None, GenericBrowser("links"))
490 if _iscommand("elinks"):
491 register("elinks", None, Elinks("elinks"))
492 # The Lynx browser <http://lynx.isc.org/>, <http://lynx.browser.org/>
493 if _iscommand("lynx"):
494 register("lynx", None, GenericBrowser("lynx"))
495 # The w3m browser <http://w3m.sourceforge.net/>
496 if _iscommand("w3m"):
497 register("w3m", None, GenericBrowser("w3m"))
500 # Platform support for Windows
503 if sys
.platform
[:3] == "win":
504 class WindowsDefault(BaseBrowser
):
505 def open(self
, url
, new
=0, autoraise
=1):
507 return True # Oh, my...
511 # Prefer mozilla/netscape/opera if present
512 for browser
in ("firefox", "firebird", "mozilla", "netscape", "opera"):
513 if _iscommand(browser
):
514 register(browser
, None, BackgroundBrowser(browser
))
515 register("windows-default", WindowsDefault
)
518 # Platform support for MacOS
526 class InternetConfig(BaseBrowser
):
527 def open(self
, url
, new
=0, autoraise
=1):
529 return True # Any way to get status?
531 register("internet-config", InternetConfig
, update_tryorder
=-1)
533 if sys
.platform
== 'darwin':
534 # Adapted from patch submitted to SourceForge by Steven J. Burr
535 class MacOSX(BaseBrowser
):
536 """Launcher class for Aqua browsers on Mac OS X
538 Optionally specify a browser name on instantiation. Note that this
539 will not work for Aqua browsers if the user has moved the application
540 package after installation.
542 If no browser is specified, the default browser, as specified in the
543 Internet System Preferences panel, will be used.
545 def __init__(self
, name
):
548 def open(self
, url
, new
=0, autoraise
=1):
549 assert "'" not in url
550 # hack for local urls
556 if self
.name
== "default":
557 # User called open, open_new or get without a browser parameter
558 script
= 'open location "%s"' % url
.replace('"', '%22') # opens in default browser
560 # User called get and chose a browser
561 if self
.name
== "OmniWeb":
564 # Include toWindow parameter of OpenURL command for browsers
565 # that support it. 0 == new window; -1 == existing
566 toWindow
= "toWindow %d" % (new
- 1)
567 cmd
= 'OpenURL "%s"' % url
.replace('"', '%22')
568 script
= '''tell application "%s"
571 end tell''' % (self
.name
, cmd
, toWindow
)
572 # Open pipe to AppleScript through osascript command
573 osapipe
= os
.popen("osascript", "w")
576 # Write script to osascript's stdin
577 osapipe
.write(script
)
581 # Don't clear _tryorder or _browsers since OS X can use above Unix support
582 # (but we prefer using the OS X specific stuff)
583 register("MacOSX", None, MacOSX('default'), -1)
587 # Platform support for OS/2
590 if sys
.platform
[:3] == "os2" and _iscommand("netscape"):
593 register("os2netscape", None,
594 GenericBrowser(["start", "netscape", "%s"]), -1)
597 # OK, now that we know what the default preference orders for each
598 # platform are, allow user to override them with the BROWSER variable.
599 if "BROWSER" in os
.environ
:
600 _userchoices
= os
.environ
["BROWSER"].split(os
.pathsep
)
601 _userchoices
.reverse()
603 # Treat choices in same way as if passed into get() but do register
604 # and prepend to _tryorder
605 for cmdline
in _userchoices
:
607 _synthesize(cmdline
, -1)
608 cmdline
= None # to make del work if _userchoices was empty
612 # what to do if _tryorder is now empty?
617 usage
= """Usage: %s [-n | -t] url
619 -t: open new tab""" % sys
.argv
[0]
621 opts
, args
= getopt
.getopt(sys
.argv
[1:], 'ntd')
622 except getopt
.error
, msg
:
623 print >>sys
.stderr
, msg
624 print >>sys
.stderr
, usage
628 if o
== '-n': new_win
= 1
629 elif o
== '-t': new_win
= 2
631 print >>sys
.stderr
, usage
639 if __name__
== "__main__":