From 381d6064d76ff549d83e781dd4b53067bba5dee9 Mon Sep 17 00:00:00 2001 From: HoneyTyagii Date: Mon, 7 Sep 2026 00:28:25 -0700 Subject: [PATCH] fix(desktop): preserve custom Linux launcher entries when opted out Salvage #101453 (03a3f466d38134ba416764185884b3d655197a1d). Preserve its opt-out and first-run behavior; replace predicate-mocked tests with one native config/filesystem invariant and clarify XDG docs. Real venv/XDG probe: base clobbers custom entry, fix preserves it; targeted suite 94 passed. --- hermes_cli/config_defaults.py | 3 ++ hermes_cli/linux_desktop_entry.py | 31 +++++++++++++++- .../test_linux_launcher_management.py | 36 +++++++++++++++++++ website/docs/user-guide/desktop.md | 8 +++++ 4 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 tests/hermes_cli/test_linux_launcher_management.py diff --git a/hermes_cli/config_defaults.py b/hermes_cli/config_defaults.py index dfe64c7fbc..a5f9221424 100644 --- a/hermes_cli/config_defaults.py +++ b/hermes_cli/config_defaults.py @@ -2273,6 +2273,9 @@ DEFAULT_CONFIG = { # gnome-libsecret|kwallet|kwallet5|kwallet6|basic force one (basic = unencrypted). Bridged # to HERMES_DESKTOP_PASSWORD_STORE; ignored off-Linux. "password_store": "auto", + # Linux: False preserves an existing custom XDG launcher entry; missing entries + # are still created. True keeps the generated entry current on each launch. + "manage_launcher_entry": True, # macOS only: code-signing identity (login-keychain cert; self-signed works) to re-sign # locally rebuilt apps so the Designated Requirement — and thus TCC grants — survives # updates. Empty = default ad-hoc identifier-pinned signing. diff --git a/hermes_cli/linux_desktop_entry.py b/hermes_cli/linux_desktop_entry.py index ebd8fcc3f7..9047ef5626 100644 --- a/hermes_cli/linux_desktop_entry.py +++ b/hermes_cli/linux_desktop_entry.py @@ -548,8 +548,31 @@ def _install_icon_to_hicolor(icon: Path) -> bool: return False +def _launcher_entry_management_enabled() -> bool: + """Whether config.yaml allows rewriting an EXISTING launcher entry. + + ``desktop.manage_launcher_entry: false`` opts out of the every-launch + rewrite: a hand-edited ``hermes.desktop`` is then left alone instead + of silently reverting (#101097's clobber complaint). A MISSING entry + is still created regardless — the opt-out protects user edits, not + first-run presence. Any config error reads as enabled (default). + """ + try: + from hermes_cli.config import load_config_readonly + + desktop_cfg = (load_config_readonly() or {}).get("desktop") or {} + raw = desktop_cfg.get("manage_launcher_entry", True) + if isinstance(raw, bool): + return raw + if isinstance(raw, str): + return raw.strip().lower() not in ("false", "0", "no", "off") + return True + except Exception: + return True + + def install_desktop_entry(project_root: Path) -> Optional[Path]: - """Write (or refresh) the Hermes desktop entry and return its path. + """Create or refresh the entry, respecting the opt-out for existing entries. ``None`` on non-Linux platforms or when the write fails — a convenience, never a reason to fail a launch. @@ -558,6 +581,12 @@ def install_desktop_entry(project_root: Path) -> Optional[Path]: return None entry_path = desktop_entry_path() + + # Opt-out honored only for an entry that already exists: the flag + # stops the every-launch clobber, not first-run creation. + if entry_path.is_file() and not _launcher_entry_management_enabled(): + return entry_path + icon = icon_path(project_root) # Prefer the themed name: the icon is COPIED into the hicolor tree, so the entry outlives the # checkout (an absolute Icon= path breaks when the checkout moves). Absolute path only when diff --git a/tests/hermes_cli/test_linux_launcher_management.py b/tests/hermes_cli/test_linux_launcher_management.py new file mode 100644 index 0000000000..e2fd5e22a6 --- /dev/null +++ b/tests/hermes_cli/test_linux_launcher_management.py @@ -0,0 +1,36 @@ +"""Native Linux launcher management through real config and filesystem I/O.""" +from pathlib import Path + +import pytest + +from hermes_cli.linux_desktop_entry import install_desktop_entry + + +@pytest.mark.linux_only +def test_launcher_optout_preserves_custom_entry_but_creates_missing(tmp_path, monkeypatch): + home = tmp_path / "home" + home.mkdir() + monkeypatch.setenv("HOME", str(home)) + monkeypatch.setenv("HERMES_HOME", str(home)) + monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path / "xdg")) + monkeypatch.setenv("PATH", "/usr/bin:/bin") + monkeypatch.setattr(Path, "home", lambda: home) + config = home / "config.yaml" + config.write_text("desktop:\n manage_launcher_entry: false\n", encoding="utf-8") + root = tmp_path / "checkout" + root.mkdir() + entry = tmp_path / "xdg/applications/hermes.desktop" + entry.parent.mkdir(parents=True) + custom = b"[Desktop Entry]\nType=Application\nName=Custom Hermes\nExec=/opt/custom-hermes desktop\n" + for setting in ("false", '"false"'): + config.write_text(f"desktop:\n manage_launcher_entry: {setting}\n", encoding="utf-8") + entry.write_bytes(custom) + assert install_desktop_entry(root) == entry + assert entry.read_bytes() == custom + entry.unlink() + assert install_desktop_entry(root) == entry + assert b"Name=Hermes\n" in entry.read_bytes() + config.write_text("desktop: {}\n", encoding="utf-8") + entry.write_bytes(custom) + assert install_desktop_entry(root) == entry + assert entry.read_bytes() != custom diff --git a/website/docs/user-guide/desktop.md b/website/docs/user-guide/desktop.md index 01f55d3cb9..763ba247a6 100644 --- a/website/docs/user-guide/desktop.md +++ b/website/docs/user-guide/desktop.md @@ -292,6 +292,14 @@ Running `hermes uninstall --gui` from a **source checkout** (a `hermes desktop` To launch via the CLI, simply run `hermes desktop`. By default it installs workspace Node dependencies, builds the current OS's unpacked Electron app, then launches that packaged artifact. +On Linux, launches refresh `$XDG_DATA_HOME/applications/hermes.desktop` (by default `~/.local/share/applications/hermes.desktop`) so Hermes appears in the application menu. To preserve a hand-edited entry, disable refreshes: + +```bash +hermes config set desktop.manage_launcher_entry false +``` + +A missing entry is still created; the flag only stops `hermes desktop` from rewriting an entry that already exists. + | Flag | Description | | -------------------- | ----------------------------------------------------------------------------------------- | | `--skip-build` | Skip npm install/package and launch the existing unpacked app from `apps/desktop/release` |