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", ".bat")):
103 for ext
in ".exe", ".bat":
104 if os
.path
.isfile(cmd
+ ext
):
108 def _isexecutable(cmd
):
109 if os
.path
.isfile(cmd
):
110 mode
= os
.stat(cmd
)[stat
.ST_MODE
]
111 if mode
& stat
.S_IXUSR
or mode
& stat
.S_IXGRP
or mode
& stat
.S_IXOTH
:
116 """Return True if cmd is executable or can be found on the executable
118 if _isexecutable(cmd
):
120 path
= os
.environ
.get("PATH")
123 for d
in path
.split(os
.pathsep
):
124 exe
= os
.path
.join(d
, cmd
)
125 if _isexecutable(exe
):
130 # General parent classes
132 class BaseBrowser(object):
133 """Parent class for all browsers. Do not use directly."""
137 def __init__(self
, name
=""):
141 def open(self
, url
, new
=0, autoraise
=1):
142 raise NotImplementedError
144 def open_new(self
, url
):
145 return self
.open(url
, 1)
147 def open_new_tab(self
, url
):
148 return self
.open(url
, 2)
151 class GenericBrowser(BaseBrowser
):
152 """Class for all browsers started with a command
153 and without remote functionality."""
155 def __init__(self
, name
):
156 if isinstance(name
, basestring
):
159 # name should be a list with arguments
162 self
.basename
= os
.path
.basename(self
.name
)
164 def open(self
, url
, new
=0, autoraise
=1):
165 cmdline
= [self
.name
] + [arg
.replace("%s", url
)
166 for arg
in self
.args
]
168 p
= subprocess
.Popen(cmdline
, close_fds
=True)
174 class BackgroundBrowser(GenericBrowser
):
175 """Class for all browsers which are to be started in the
178 def open(self
, url
, new
=0, autoraise
=1):
179 cmdline
= [self
.name
] + [arg
.replace("%s", url
)
180 for arg
in self
.args
]
181 setsid
= getattr(os
, 'setsid', None)
183 setsid
= getattr(os
, 'setpgrp', None)
185 p
= subprocess
.Popen(cmdline
, close_fds
=True, preexec_fn
=setsid
)
186 return (p
.poll() is None)
191 class UnixBrowser(BaseBrowser
):
192 """Parent class for all Unix browsers with remote functionality."""
195 remote_args
= ['%action', '%s']
197 remote_action_newwin
= None
198 remote_action_newtab
= None
200 redirect_stdout
= True
202 def _invoke(self
, args
, remote
, autoraise
):
204 if remote
and self
.raise_opts
:
205 # use autoraise argument only for remote invocation
206 autoraise
= int(bool(autoraise
))
207 opt
= self
.raise_opts
[autoraise
]
208 if opt
: raise_opt
= [opt
]
210 cmdline
= [self
.name
] + raise_opt
+ args
212 if remote
or self
.background
:
213 inout
= file(os
.devnull
, "r+")
215 # for TTY browsers, we need stdin/out
217 # if possible, put browser in separate process group, so
218 # keyboard interrupts don't affect browser as well as Python
219 setsid
= getattr(os
, 'setsid', None)
221 setsid
= getattr(os
, 'setpgrp', None)
223 p
= subprocess
.Popen(cmdline
, close_fds
=True, stdin
=inout
,
224 stdout
=(self
.redirect_stdout
and inout
or None),
225 stderr
=inout
, preexec_fn
=setsid
)
227 # wait five secons. If the subprocess is not finished, the
228 # remote invocation has (hopefully) started a new instance.
236 # if remote call failed, open() will try direct invocation
238 elif self
.background
:
246 def open(self
, url
, new
=0, autoraise
=1):
248 action
= self
.remote_action
250 action
= self
.remote_action_newwin
252 if self
.remote_action_newtab
is None:
253 action
= self
.remote_action_newwin
255 action
= self
.remote_action_newtab
257 raise Error("Bad 'new' parameter to open(); " +
258 "expected 0, 1, or 2, got %s" % new
)
260 args
= [arg
.replace("%s", url
).replace("%action", action
)
261 for arg
in self
.remote_args
]
262 success
= self
._invoke
(args
, True, autoraise
)
264 # remote invocation failed, try straight way
265 args
= [arg
.replace("%s", url
) for arg
in self
.args
]
266 return self
._invoke
(args
, False, False)
271 class Mozilla(UnixBrowser
):
272 """Launcher class for Mozilla/Netscape browsers."""
274 raise_opts
= ["-noraise", "-raise"]
276 remote_args
= ['-remote', 'openURL(%s%action)']
278 remote_action_newwin
= ",new-window"
279 remote_action_newtab
= ",new-tab"
286 class Galeon(UnixBrowser
):
287 """Launcher class for Galeon/Epiphany browsers."""
289 raise_opts
= ["-noraise", ""]
290 remote_args
= ['%action', '%s']
292 remote_action_newwin
= "-w"
297 class Opera(UnixBrowser
):
298 "Launcher class for Opera browser."
300 raise_opts
= ["", "-raise"]
302 remote_args
= ['-remote', 'openURL(%s%action)']
304 remote_action_newwin
= ",new-window"
305 remote_action_newtab
= ",new-page"
309 class Elinks(UnixBrowser
):
310 "Launcher class for Elinks browsers."
312 remote_args
= ['-remote', 'openURL(%s%action)']
314 remote_action_newwin
= ",new-window"
315 remote_action_newtab
= ",new-tab"
318 # elinks doesn't like its stdout to be redirected -
319 # it uses redirected stdout as a signal to do -dump
320 redirect_stdout
= False
323 class Konqueror(BaseBrowser
):
324 """Controller for the KDE File Manager (kfm, or Konqueror).
326 See the output of ``kfmclient --commands``
327 for more information on the Konqueror remote-control interface.
330 def open(self
, url
, new
=0, autoraise
=1):
331 # XXX Currently I know no way to prevent KFM from opening a new win.
337 devnull
= file(os
.devnull
, "r+")
338 # if possible, put browser in separate process group, so
339 # keyboard interrupts don't affect browser as well as Python
340 setsid
= getattr(os
, 'setsid', None)
342 setsid
= getattr(os
, 'setpgrp', None)
345 p
= subprocess
.Popen(["kfmclient", action
, url
],
346 close_fds
=True, stdin
=devnull
,
347 stdout
=devnull
, stderr
=devnull
)
349 # fall through to next variant
353 # kfmclient's return code unfortunately has no meaning as it seems
357 p
= subprocess
.Popen(["konqueror", "--silent", url
],
358 close_fds
=True, stdin
=devnull
,
359 stdout
=devnull
, stderr
=devnull
,
362 # fall through to next variant
366 # Should be running now.
370 p
= subprocess
.Popen(["kfm", "-d", url
],
371 close_fds
=True, stdin
=devnull
,
372 stdout
=devnull
, stderr
=devnull
,
377 return (p
.poll() is None)
380 class Grail(BaseBrowser
):
381 # There should be a way to maintain a connection to Grail, but the
382 # Grail remote control protocol doesn't really allow that at this
383 # point. It probably never will!
384 def _find_grail_rc(self
):
389 tempdir
= os
.path
.join(tempfile
.gettempdir(),
391 user
= pwd
.getpwuid(os
.getuid())[0]
392 filename
= os
.path
.join(tempdir
, user
+ "-*")
393 maybes
= glob
.glob(filename
)
396 s
= socket
.socket(socket
.AF_UNIX
, socket
.SOCK_STREAM
)
398 # need to PING each one until we find one that's live
402 # no good; attempt to clean it out, but don't fail:
410 def _remote(self
, action
):
411 s
= self
._find
_grail
_rc
()
418 def open(self
, url
, new
=0, autoraise
=1):
420 ok
= self
._remote
("LOADNEW " + url
)
422 ok
= self
._remote
("LOAD " + url
)
427 # Platform support for Unix
430 # These are the right tests because all these Unix browsers require either
431 # a console terminal or an X display to run.
433 def register_X_browsers():
434 # The default Gnome browser
435 if _iscommand("gconftool-2"):
436 # get the web browser string from gconftool
437 gc
= 'gconftool-2 -g /desktop/gnome/url-handlers/http/command'
439 commd
= out
.read().strip()
440 retncode
= out
.close()
442 # if successful, register it
443 if retncode
== None and len(commd
) != 0:
444 register("gnome", None, BackgroundBrowser(commd
))
446 # First, the Mozilla/Netscape browsers
447 for browser
in ("mozilla-firefox", "firefox",
448 "mozilla-firebird", "firebird",
449 "seamonkey", "mozilla", "netscape"):
450 if _iscommand(browser
):
451 register(browser
, None, Mozilla(browser
))
453 # Konqueror/kfm, the KDE browser.
454 if _iscommand("kfm"):
455 register("kfm", Konqueror
, Konqueror("kfm"))
456 elif _iscommand("konqueror"):
457 register("konqueror", Konqueror
, Konqueror("konqueror"))
459 # Gnome's Galeon and Epiphany
460 for browser
in ("galeon", "epiphany"):
461 if _iscommand(browser
):
462 register(browser
, None, Galeon(browser
))
464 # Skipstone, another Gtk/Mozilla based browser
465 if _iscommand("skipstone"):
466 register("skipstone", None, BackgroundBrowser("skipstone"))
468 # Opera, quite popular
469 if _iscommand("opera"):
470 register("opera", None, Opera("opera"))
472 # Next, Mosaic -- old but still in use.
473 if _iscommand("mosaic"):
474 register("mosaic", None, BackgroundBrowser("mosaic"))
476 # Grail, the Python browser. Does anybody still use it?
477 if _iscommand("grail"):
478 register("grail", Grail
, None)
480 # Prefer X browsers if present
481 if os
.environ
.get("DISPLAY"):
482 register_X_browsers()
484 # Also try console browsers
485 if os
.environ
.get("TERM"):
486 # The Links/elinks browsers <http://artax.karlin.mff.cuni.cz/~mikulas/links/>
487 if _iscommand("links"):
488 register("links", None, GenericBrowser("links"))
489 if _iscommand("elinks"):
490 register("elinks", None, Elinks("elinks"))
491 # The Lynx browser <http://lynx.isc.org/>, <http://lynx.browser.org/>
492 if _iscommand("lynx"):
493 register("lynx", None, GenericBrowser("lynx"))
494 # The w3m browser <http://w3m.sourceforge.net/>
495 if _iscommand("w3m"):
496 register("w3m", None, GenericBrowser("w3m"))
499 # Platform support for Windows
502 if sys
.platform
[:3] == "win":
503 class WindowsDefault(BaseBrowser
):
504 def open(self
, url
, new
=0, autoraise
=1):
506 return True # Oh, my...
510 # Prefer mozilla/netscape/opera if present
511 for browser
in ("firefox", "firebird", "seamonkey", "mozilla",
512 "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__":