diff --git a/nextstrain/cli/browser.py b/nextstrain/cli/browser.py new file mode 100644 index 00000000..e668df13 --- /dev/null +++ b/nextstrain/cli/browser.py @@ -0,0 +1,51 @@ +""" +Web browser interaction. +""" +import webbrowser +from threading import Thread, ThreadError +from os import environ +from .util import warn + + +# Avoid text-mode browsers +TERM = environ.pop("TERM", None) +try: + BROWSER = webbrowser.get() +except: + BROWSER = None +finally: + if TERM is not None: + environ["TERM"] = TERM + + +def open_browser(url: str, new_thread: bool = True): + """ + Opens *url* in a web browser. + + Opens in a new tab, if possible, and raises the window to the top, if + possible. + + Launches the browser from a separate thread by default so waiting on the + browser child process doesn't block the main (or calling) thread. Set + *new_thread* to False to launch from the same thread as the caller (e.g. if + you've already spawned a dedicated thread or process for the browser). + Note that some registered browsers launch in the background themselves, but + not all do, so this feature makes launch behaviour consistent across + browsers. + + Prints a warning to stderr if a browser can't be found or can't be + launched, as automatically opening a browser is considered a + nice-but-not-necessary feature. + """ + if not BROWSER: + warn(f"Couldn't open <{url}> in browser: no browser found") + return + + try: + if new_thread: + Thread(target = open_browser, args = (url, False), daemon = True).start() + else: + # new = 2 means new tab, if possible + BROWSER.open(url, new = 2, autoraise = True) + except (ThreadError, webbrowser.Error) as err: + warn(f"Couldn't open <{url}> in browser: {err!r}") diff --git a/nextstrain/cli/command/view.py b/nextstrain/cli/command/view.py index 2a5eb008..f8eb5309 100644 --- a/nextstrain/cli/command/view.py +++ b/nextstrain/cli/command/view.py @@ -48,7 +48,6 @@ from multiprocessing import Process, ProcessError import re import requests -import webbrowser from inspect import cleandoc from os import environ from pathlib import Path @@ -57,6 +56,7 @@ from typing import Iterable, NamedTuple, Tuple, Union from .. import runner from ..argparse import add_extended_help_flags, SUPPRESS, SKIP_AUTO_DEFAULT_IN_HELP +from ..browser import BROWSER, open_browser as __open_browser from ..runner import docker, ambient, conda, singularity from ..util import colored, remove_suffix, warn from ..volume import NamedVolume @@ -67,16 +67,6 @@ PORT = environ.get("PORT") or "4000" -# Avoid text-mode browsers -TERM = environ.pop("TERM", None) -try: - BROWSER = webbrowser.get() -except: - BROWSER = None -finally: - if TERM is not None: - environ["TERM"] = TERM - OPEN_DEFAULT = bool(BROWSER) @@ -454,8 +444,4 @@ def _open_browser(url: str): warn(f"Couldn't open <{url}> in browser: Auspice never started listening") return - try: - # new = 2 means new tab, if possible - BROWSER.open(url, new = 2, autoraise = True) - except webbrowser.Error as err: - warn(f"Couldn't open <{url}> in browser: {err!r}") + __open_browser(url, new_thread = False)