Compare commits

...

6 Commits

4 changed files with 96 additions and 38 deletions

View File

@@ -1,20 +1,20 @@
# App Mode Jupyter Environment and Shortcut
Adds a shortcut for running [JupyterLab](https://jupyter.org/) in a Chromium-based browser's "app" mode (i.e. The application takes up the full browser window and has no browser UI e.g. the address bar, as if Jupyter Lab were a native application) for an existing environment with Jupyter Lab installed or creating one for you.
Adds a shortcut for running [JupyterLab](https://jupyter.org/) in a Chromium-based browser's "app" mode (i.e. The application takes up the full browser window and has no browser UI e.g. the address bar, as if Jupyter Lab were a native application) for an existing environment with JupyterLab installed or creating one for you. This is useful if you do not want to run an Electron app in addition to your browser or if you wish to use Jupyter like a desktop app, but do not have administrator access to install the JupyterLab desktop app.
## Requirements
* An conda-based installation of [Anaconda](https://anaconda.org/), [Miniconda](https://docs.conda.io/en/latest/miniconda.html) or [Miniforge](https://github.com/conda-forge/miniforge). Miniforge is recommended.
* [`menuinst`](https://conda.github.io/menuinst/) >= 2.0.0 must be installed in the `base` environment alongside `conda` or in whichever environment contains conda
* A Chromium-based browser
* On Windows, [Google Chrome](https://www.google.com/chrome/), [Brave](https://brave.com/), and [Microsoft Edge](https://www.microsoft.com/en-us/edge) are supported. Preference will be given to the first one of these browsers found. Microsoft Edge is guaranteed to be installed on Windows 10+ and is a fall-back if no other supported browser is found.
* On Windows, [Google Chrome](https://www.google.com/chrome/), [Brave](https://brave.com/), and [Microsoft Edge](https://www.microsoft.com/en-us/edge) are supported. Preference will be given to your default browser, if it appears to be a Chromium-based browser, or to the first one of these browsers found. Microsoft Edge is guaranteed to be installed on Windows 10+ and is a fall-back if no other supported browser is found.
* On MacOS, [Google Chrome](https://www.google.com/chrome/), [Microsoft Edge](https://www.microsoft.com/en-us/edge), [Brave](https://brave.com/), and [Chromium](https://www.chromium.org/getting-involved/download-chromium/) are preferred in that order.
* On Linux, [Google Chrome](https://flathub.org/apps/com.google.Chrome), [Microsoft Edge](https://flathub.org/apps/com.microsoft.Edge), [Brave](https://flathub.org/apps/com.brave.Browser), and [Chromium](https://flathub.org/apps/org.chromium.Chromium) are supported and preferred in that order. If [flatpak](https://flatpak.org/) is installed and one of these browsers is installed with Flatpak (recommended), it will be preferred over any snap-installed or package manager-installed versions of any of these browsers.
* On Linux, [Google Chrome](https://flathub.org/apps/com.google.Chrome), [Microsoft Edge](https://flathub.org/apps/com.microsoft.Edge), [Brave](https://flathub.org/apps/com.brave.Browser), [Opera](https://flathub.org/apps/com.opera.Opera), [Vivaldi](https://flathub.org/apps/com.vivaldi.Vivaldi), [Ungoogled Chromium](https://flathub.org/apps/io.github.ungoogled_software.ungoogled_chromium), and [Chromium](https://flathub.org/apps/org.chromium.Chromium) are supported and preferred in that order. If [flatpak](https://flatpak.org/) is installed and one of these browsers is installed with Flatpak (recommended), it will be preferred over any snap-installed or package manager-installed versions of any of these browsers.
## Setup
1. Clone or download this repository
2. From a terminal, activate your `base` conda environment (or whichever environment conda is installed in)
2. From a terminal, activate your `base` conda environment (or whichever environment conda is installed in)
3. Run `python ./setup_jupyter.py your_jupyter_env_name_here`
## Remove Shortcut
@@ -24,12 +24,12 @@ To remove the shortcut created by this script, run `python ./setup_jupyter.py --
## FAQ
1. Why don't you support Mozilla Firefox or Safari?
To my knowledge, neither have an "app-mode" like Chromium-based browsers do. When/if they ever do, they will be supported.
2. Why am I being prompted for admin permissions?
By default, `menuinst` tries to install/remove the shortcut from a system-level location. You can safely cancel/ignore that prompt(s) (it may prompt you multiple times) which will cause `menuinst` to fallback to a user-specific location instead. Unfortunately, there does not seem to be a way to have `menuinst` prefer to install/remove shortcuts for the current user rather than system-wide.
By default, `menuinst` tries to install/remove the shortcut from a system-level location. You can safely cancel/ignore that prompt(s) (it may prompt you multiple times) which will cause `menuinst` to fallback to a user-specific location instead. Unfortunately, there does not seem to be a way to have `menuinst` prefer to install/remove shortcuts for the current user rather than system-wide.
3. What about other Chromium-based browsers?
@@ -49,8 +49,16 @@ To remove the shortcut created by this script, run `python ./setup_jupyter.py --
7. What is `nb_conda_kernels` and why does this script want to install it in my Jupyter environment.
`nb_conda_kernels` allows JupyterLab to run kernels in **any** installed conda environment which has a Jupyter kernel (e.g. ipykernel for Python) installed in it without having to install Jupyter and all its dependencies into that conda environment. It allows you to have a single version of JupyterLab installed in a single, purpose-made environment so you are not maintaining multiple JupyterLab versions or configurations.
`nb_conda_kernels` allows JupyterLab to run kernels in **any** installed conda environment which has a Jupyter kernel (e.g. ipykernel for Python) installed in it without having to install Jupyter and all its dependencies into that conda environment. It allows you to have a single version of JupyterLab installed in a single, purpose-made environment so you are not maintaining multiple JupyterLab versions or configurations.
8. How should I update my Jupyter environment?
That's up to you. I will periodically run `conda update -n jupyter --all && conda update -n jupyter python` to update everything in my Jupyter environment (named `jupyter`).
That's up to you. I will periodically run `conda update -n jupyter --all && conda update -n jupyter python` to update everything in my Jupyter environment (named `jupyter`).
9. Why not just use the JupyterLab desktop app?
The JupyterLab desktop app is based on the Electron framework, which has relatively high memory usage. Your chromium-based browser also has relatively high memory usage. Running JupyterLab in an app mode browser window allows it to share resources with your browser, which is you also probably have open anyway. JupyterLab desktop also requires administrator privileges to install (at least on Windows), but not all organizations permit their employees to have administrator privileges to install software. If you can install a conda-based Python distribution with only user privileges, then you can also use JupyterLab like a desktop app by opening it in a browser "app" window.
10. What about setting JupyterLab as the default app for .ipynb files? The JupyterLab desktop app can do that.
If I knew how to set file type associations, I would add that functionality to this script, but I don't currently have the necessary knowledge, nor do I have a MacBook to test it on MacOS.

View File

@@ -3,8 +3,9 @@ import platform
from io import StringIO
from itertools import product
from os import environ
from os import environ, pathsep
from pathlib import Path
from re import search
from subprocess import CalledProcessError, run
logging.getLogger(__name__).propagate=True
@@ -12,7 +13,7 @@ logging.getLogger(__name__).propagate=True
def find_flatpak_browser():
if platform.system() != "Linux":
return
for path_element in map(Path, environ["PATH"].split(":")):
if (path_element/"flatpak").is_file():
flatpak_bin = path_element/"flatpak"
@@ -20,8 +21,16 @@ def find_flatpak_browser():
else:
# flatpak is not installed
return
flatpak_apps = ("com.google.Chrome", "com.microsoft.Edge", "com.brave.Browser", "org.chromium.Chromium")
flatpak_apps = (
"com.google.Chrome",
"com.microsoft.Edge",
"com.brave.Browser",
"com.opera.Opera",
"com.vivaldi.Vivaldi",
"io.github.ungoogled_software.ungoogled_chromium",
"org.chromium.Chromium",
)
try:
flatpak_process = run(
@@ -39,15 +48,17 @@ def find_flatpak_browser():
else:
with StringIO(flatpak_process.stdout.decode()) as flatpak_output:
installed_flatpaks = flatpak_output.readlines()[1:]
for flatpak_app in flatpak_apps:
if flatpak_app in [_.strip() for _ in installed_flatpaks]:
return f"flatpak run --filesystem={Path.home()/'.local/share/jupyter/runtime'} {flatpak_app} --start-maximized --profile-directory=Default --app=%s"
def find_browser():
# find a chromium-based browser on the system to use.
# find a chromium-based browser on the system to use.
if platform.system() == "Windows":
from winreg import HKEY_CLASSES_ROOT, OpenKey, QueryValue
cmds = (
"Google/Chrome/Application/chrome.exe",
"BraveSoftware/Brave-Browser/Application/brave.exe",
@@ -55,16 +66,35 @@ def find_browser():
)
path_elements = map(Path, (environ["ProgramFiles"], environ["ProgramFiles(x86)"]))
# do a lookup of default browser command in windows registry
try:
with OpenKey(HKEY_CLASSES_ROOT, r"http\shell\open") as k:
default_browser_cmd = QueryValue(k, "command").replace('"%1"', "").replace('"', "").strip()
except FileNotFoundError:
pass
else:
# if the command looks like one of the known chromium browsers or even vaguely resembles one, return immediately with a browser command
if any(default_browser_cmd.endswith(cmd.replace("/", pathsep)) for cmd in cmds) or search(r"\\Application\\\w+\.exe$", default_browser_cmd):
return f'"{default_browser_cmd}" --start-maximized --profile-directory=Default --app=%s'
elif platform.system() == "Linux":
# prefers Google Chrome on Linux due to popularity of the browser
cmds = ("google-chrome", "microsoft-edge", "brave", "chromium")
cmds = (
"google-chrome",
"microsoft-edge",
"brave",
"opera",
"vivaldi",
"ungoogled-chromium",
"chromium",
)
path_elements = map(Path, environ["PATH"].split(":"))
elif platform.system() == "Darwin":
# also prefers Google Chrome on MacOS due to popularity of the browser
cmds = (
"Contents/MacOS/Google Chrome",
"Contents/MacOS/Microsoft Edge",
"Contents/MacOS/Google Chrome",
"Contents/MacOS/Microsoft Edge",
"Contents/MacOS/Brave Browser",
"Contents/MacOS/Chromium",
)

View File

@@ -15,9 +15,18 @@
"icon": "{{ MENU_DIR }}/jupyterlab.{{ ICON_EXT }}",
"platforms": {
"win": {
"file_extensions": [
".ipynb"
],
"precreate": "{{ BASE_PREFIX }}\\condabin\\conda.bat init cmd.exe",
"command": [
"{{ BASE_PREFIX }}\\condabin\\conda.bat",
"run",
"--live-stream",
"--prefix",
"{{ PREFIX }}",
"jupyter",
"lab",
"--config={{ MENU_DIR }}/jupyter_lab_config.py"
],
"activate": false,
"quicklaunch": false
},
"osx": {

View File

@@ -31,7 +31,7 @@ def get_current_prefix() -> Path:
current_prefix = Path(environ["_CONDA_PREFIX"])
except KeyError as e:
raise RuntimeError("No active conda environment detected. Please activate your base conda environment") from e
LOGGER.debug(f"Current prefix is {current_prefix}")
return current_prefix
@@ -50,7 +50,7 @@ def get_base_prefix() -> Path:
# your base environment will be the grandparent of the conda executable
return conda_exe.parents[1]
def get_menuinst_version() -> tuple[str, str, str]:
conda_process = run(["conda", "list", "--prefix", str(get_base_prefix()), "--json"], capture_output=True, check=True)
@@ -59,13 +59,13 @@ def get_menuinst_version() -> tuple[str, str, str]:
conda_pkgs = loads(conda_process.stdout)
except JSONDecodeError as decode_exception:
raise JSONDecodeError(f"Error parsing conda output as JSON: {decode_exception}") from decode_exception
for pkg in conda_pkgs:
if pkg["name"] == "menuinst":
break
else:
raise RuntimeError("menuinst was not found in the base conda envirionment")
LOGGER.debug(f"menuinst=={pkg['version']} found")
return pkg["version"].split(".")
@@ -74,7 +74,7 @@ def get_menuinst_version() -> tuple[str, str, str]:
def in_base_env() -> bool:
return get_current_prefix().samefile(get_base_prefix())
def menuinst_gt_v2_present() -> bool:
major, minor, patch = get_menuinst_version()
@@ -89,7 +89,7 @@ def stage_configs(destination_dir: Path) -> Path:
repo_dir = Path(__file__).parent
shortcut_json = repo_dir / "jupyterlab_shortcut.json"
jupyterlab_config = repo_dir / "jupyter_lab_config.py"
LOGGER.debug(f"Repository directory is {repo_dir}")
LOGGER.debug(f"menuinst spec json file is {shortcut_json}")
LOGGER.debug(f"JupyterLab config file is {jupyterlab_config}")
@@ -153,14 +153,14 @@ def svg_to_icns(svg_file: PathLike) -> None:
with TemporaryDirectory() as temp_dir:
iconset_dir = Path(temp_dir)/"jupyterlab.iconset"
iconset_dir.mkdir(exist_ok=True)
for size in [2**n for n in range(4, 11)]:
outfile = iconset_dir/f"icon_{size}x{size}.png"
try:
run(
[
"qlmanage",
"-t",
"-t",
"-s",
str(size),
"-o",
@@ -281,9 +281,9 @@ def ensure_env(env_name: str) -> Path:
end=linesep,
file=condarc
)
else:
# add minimal new jupyter packages t existing environment
# add minimal new jupyter packages t existing environment
pkgs = {pkg["name"] for pkg in env_spec}
missing_pkgs = {"jupyterlab", "nb_conda_kernels", "ipykernel"} - pkgs
@@ -306,15 +306,25 @@ def ensure_env(env_name: str) -> Path:
(env_prefix/"Menu").mkdir(exist_ok=True)
return env_prefix
return env_prefix
def main(target_env_name: str, remove_shortcut: bool=False) -> Union[int, None]:
if not in_base_env():
LOGGER.warning("Not in base environment. Re-running this script from the base environment")
# I don't know why, but Windows *really* does not want to resolve conda in PATH if you aren't in the base environment
try:
conda_exe = environ["CONDA_EXE"]
except KeyError:
try:
conda_exe = environ["_CONDA_EXE"]
except KeyError:
conda_exe = "conda"
# call conda run to re-run this in the base prefix
rerun_proces = run(
["conda", "run", "--prefix", str(get_base_prefix()), "--no-capture-output", "python", *argv],
[conda_exe, "run", "--prefix", str(get_base_prefix()), "--no-capture-output", "python", *argv],
capture_output=False,
check=False
)
@@ -331,10 +341,10 @@ def main(target_env_name: str, remove_shortcut: bool=False) -> Union[int, None]:
run(["python", *argv], capture_output=False, check=True)
elif meets_prerequisites() and remove_shortcut:
from menuinst.api import remove
target_prefix = get_base_prefix() / f"envs/{target_env_name}"
menu_dir = target_prefix / "Menu"
shortcut_json = menu_dir / "jupyterlab_shortcut.json"
jupyterlab_config = menu_dir / "jupyter_lab_config.py"
match platform.system():
@@ -346,10 +356,10 @@ def main(target_env_name: str, remove_shortcut: bool=False) -> Union[int, None]:
icon_file = menu_dir/"jupyterlab.ico"
case _:
raise RuntimeError(f"{platform.system()} is not a supported platform")
if shortcut_json.is_file():
LOGGER.debug("Removing JupyterLab shortcut")
try:
try:
remove(shortcut_json, target_prefix=target_prefix)
except:
pass
@@ -357,7 +367,7 @@ def main(target_env_name: str, remove_shortcut: bool=False) -> Union[int, None]:
LOGGER.info("JupyterLab shortcut removed")
else:
LOGGER.error(f"Shortcut spec file '{shortcut_json}' does not exist, therefore the shotcut cannot be removed by this script. Please delete it manually")
if menu_dir.exists() and menu_dir.is_dir():
LOGGER.debug(f"Cleaning up {target_env_name} environment's Menu directory {menu_dir}")
jupyterlab_config.unlink(missing_ok=True)
@@ -424,7 +434,7 @@ if __name__ == "__main__":
parser.add_argument(
"name",
help = "Name of the target conda environment containing JupyterLab. If it does not exist, one will be created"
)
)
args = parser.parse_args()
@@ -442,6 +452,7 @@ if __name__ == "__main__":
logging.captureWarnings(True)
try:
logging.info("Creating Jupyterlab shortcut. This may take a moment...")
exit(main(args.name, args.remove))
except CalledProcessError as e:
# catch any exceptions raised if calling conda returns an unsuccessful return code