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.
This commit is contained in:
HoneyTyagii
2026-09-07 00:28:25 -07:00
committed by Teknium
parent 94ff4fe8f9
commit 381d6064d7
4 changed files with 77 additions and 1 deletions
+3
View File
@@ -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.
+30 -1
View File
@@ -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
@@ -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
+8
View File
@@ -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` |