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:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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` |
|
||||
|
||||
Reference in New Issue
Block a user