From 1705620c4f37e86b8414aa3bf70d4fdd1ed29ded Mon Sep 17 00:00:00 2001 From: Duck <31627079+duckboycool@users.noreply.github.com> Date: Sun, 29 Mar 2026 12:07:55 -0600 Subject: [PATCH] Launcher: Add konsole to terminal list and rework launch dialog (#5684) * Make component launching indicate if no terminal window, add konsole * Attempt to spell better and remove whitespace * Update terminal priority * Make helper for clearing LD_LIBRARY_PATH * Add handling to linux launch * Hopefully fix setter * Apply suggestions from code review Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --------- Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- Launcher.py | 49 +++++++++++++++++++++++++++++-------------------- Utils.py | 25 ++++++++++++++++--------- 2 files changed, 45 insertions(+), 29 deletions(-) diff --git a/Launcher.py b/Launcher.py index cffd96b85d..0e7d4796c4 100644 --- a/Launcher.py +++ b/Launcher.py @@ -29,8 +29,8 @@ if __name__ == "__main__": import settings import Utils -from Utils import (init_logging, is_frozen, is_linux, is_macos, is_windows, local_path, messagebox, open_filename, - user_path) +from Utils import (env_cleared_lib_path, init_logging, is_frozen, is_linux, is_macos, is_windows, local_path, + messagebox, open_filename, user_path) if __name__ == "__main__": init_logging('Launcher') @@ -52,10 +52,7 @@ def open_host_yaml(): webbrowser.open(file) return - env = os.environ - if "LD_LIBRARY_PATH" in env: - env = env.copy() - del env["LD_LIBRARY_PATH"] # exe is a system binary, so reset LD_LIBRARY_PATH + env = env_cleared_lib_path() subprocess.Popen([exe, file], env=env) def open_patch(): @@ -106,10 +103,7 @@ def open_folder(folder_path): return if exe: - env = os.environ - if "LD_LIBRARY_PATH" in env: - env = env.copy() - del env["LD_LIBRARY_PATH"] # exe is a system binary, so reset LD_LIBRARY_PATH + env = env_cleared_lib_path() subprocess.Popen([exe, folder_path], env=env) else: logging.warning(f"No file browser available to open {folder_path}") @@ -202,22 +196,32 @@ def get_exe(component: str | Component) -> Sequence[str] | None: return [sys.executable, local_path(f"{component.script_name}.py")] if component.script_name else None -def launch(exe, in_terminal=False): +def launch(exe: Sequence[str], in_terminal: bool = False) -> bool: + """Runs the given command/args in `exe` in a new process. + + If `in_terminal` is True, it will attempt to run in a terminal window, + and the return value will indicate whether one was found.""" if in_terminal: if is_windows: # intentionally using a window title with a space so it gets quoted and treated as a title subprocess.Popen(["start", "Running Archipelago", *exe], shell=True) - return + return True elif is_linux: - terminal = which('x-terminal-emulator') or which('gnome-terminal') or which('xterm') + terminal = which("x-terminal-emulator") or which("konsole") or which("gnome-terminal") or which("xterm") if terminal: - subprocess.Popen([terminal, '-e', shlex.join(exe)]) - return + # Clear LD_LIB_PATH during terminal startup, but set it again when running command in case it's needed + ld_lib_path = os.environ.get("LD_LIBRARY_PATH") + lib_path_setter = f"env LD_LIBRARY_PATH={shlex.quote(ld_lib_path)} " if ld_lib_path else "" + env = env_cleared_lib_path() + + subprocess.Popen([terminal, "-e", lib_path_setter + shlex.join(exe)], env=env) + return True elif is_macos: - terminal = [which('open'), '-W', '-a', 'Terminal.app'] + terminal = [which("open"), "-W", "-a", "Terminal.app"] subprocess.Popen([*terminal, *exe]) - return + return True subprocess.Popen(exe) + return False def create_shortcut(button: Any, component: Component) -> None: @@ -406,12 +410,17 @@ def run_gui(launch_components: list[Component], args: Any) -> None: @staticmethod def component_action(button): - MDSnackbar(MDSnackbarText(text="Opening in a new window..."), y=dp(24), pos_hint={"center_x": 0.5}, - size_hint_x=0.5).open() + open_text = "Opening in a new window..." if button.component.func: + # Note: if we want to draw the Snackbar before running func, func needs to be wrapped in schedule_once button.component.func() else: - launch(get_exe(button.component), button.component.cli) + # if launch returns False, it started the process in background (not in a new terminal) + if not launch(get_exe(button.component), button.component.cli) and button.component.cli: + open_text = "Running in the background..." + + MDSnackbar(MDSnackbarText(text=open_text), y=dp(24), pos_hint={"center_x": 0.5}, + size_hint_x=0.5).open() def _on_drop_file(self, window: Window, filename: bytes, x: int, y: int) -> None: """ When a patch file is dropped into the window, run the associated component. """ diff --git a/Utils.py b/Utils.py index cd4af275da..0210086274 100644 --- a/Utils.py +++ b/Utils.py @@ -22,7 +22,7 @@ from datetime import datetime, timezone from settings import Settings, get_settings from time import sleep -from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union, TypeGuard +from typing import BinaryIO, Coroutine, Mapping, Optional, Set, Dict, Any, Union, TypeGuard from yaml import load, load_all, dump from pathspec import PathSpec, GitIgnoreSpec from typing_extensions import deprecated @@ -236,10 +236,7 @@ def open_file(filename: typing.Union[str, "pathlib.Path"]) -> None: open_command = which("open") if is_macos else (which("xdg-open") or which("gnome-open") or which("kde-open")) assert open_command, "Didn't find program for open_file! Please report this together with system details." - env = os.environ - if "LD_LIBRARY_PATH" in env: - env = env.copy() - del env["LD_LIBRARY_PATH"] # exe is a system binary, so reset LD_LIBRARY_PATH + env = env_cleared_lib_path() subprocess.call([open_command, filename], env=env) @@ -756,6 +753,19 @@ def is_kivy_running() -> bool: return False +def env_cleared_lib_path() -> Mapping[str, str]: + """ + Creates a copy of the current environment vars with the LD_LIBRARY_PATH removed if set, as this can interfere when + launching something in a subprocess. + """ + env = os.environ + if "LD_LIBRARY_PATH" in env: + env = env.copy() + del env["LD_LIBRARY_PATH"] + + return env + + def _mp_open_filename(res: "multiprocessing.Queue[typing.Optional[str]]", *args: Any) -> None: if is_kivy_running(): raise RuntimeError("kivy should not be running in multiprocess") @@ -768,10 +778,7 @@ def _mp_save_filename(res: "multiprocessing.Queue[typing.Optional[str]]", *args: res.put(save_filename(*args)) def _run_for_stdout(*args: str): - env = os.environ - if "LD_LIBRARY_PATH" in env: - env = env.copy() - del env["LD_LIBRARY_PATH"] # exe is a system binary, so reset LD_LIBRARY_PATH + env = env_cleared_lib_path() return subprocess.run(args, capture_output=True, text=True, env=env).stdout.split("\n", 1)[0] or None