fix: autostart desktop entry never launches on KDE Plasma #11
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fix/autostart-exec-path"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
The shipped autostart entry is silently skipped at login on KDE Plasma. The journal shows:
Two causes:
Exec=— Plasma hands autostart entries tosystemd-xdg-autostart-generator, which resolvesExec=against the systemd user manager'sPATH. That PATH does not include~/.local/bin(where pip installs the script), so no unit is generated. Running the command from a shell works, which makes this easy to miss.X-KDE-autostart-condition=zcav:kservice— the expected format isrcfile:group:entry:default; this value is bogus and can suppress the entry on KDE even with a correct path.Changes
assets/zai-tray-checker.desktop: drop the invalidX-KDE-autostart-conditionline.README.md: the autostart install step now substitutes the resolved absolute path intoExec=viased "s|^Exec=.*|Exec=$(command -v zai-tray-checker)|", with a short note explaining why the absolute path is required. (A repo-shipped file can't hardcode a user's home directory, so the substitution happens at install time.)assets/zai-tray-checker.service: add the systemd user unit that the README already documented but which was missing from the repo. UsesExecStart=%h/.local/bin/zai-tray-checkerso it works for any user without substitution.Verified
Applied the same fix to my local
~/.config/autostart/entry: aftersystemctl --user daemon-reloadthe generator now createsapp-zai\x2dtray\x2dchecker@autostart.service, and starting it brings the tray app up cleanly.🤖 Generated with Claude Code
🔮 fufu~ Jibril reviewed your code!
Ooh, a systemd generator bug! How delightful~ ♪ These silent-failure config bugs are the sneakiest little things — works from your shell, vanishes at login, no error in sight. I do love a good forensic desktop-entry investigation~ fufu~
I read every changed file in full (including the parts outside the diff hunks — context is where bugs hide, after all ♡) and cross-checked the
.desktopand.servicespecs.Verdict: ✅ Looks good to me~
The root-cause analysis is spot-on and the fix targets both causes correctly. The
systemd-xdg-autostart-generatorPATH issue is real and well-documented, andzcav:kservicewas indeed a bogusX-KDE-autostart-condition(the spec wantsrcfile:group:entry:default). Removing it was the right call — there's no legitimate condition this app needs.The shipped
.desktopstill has bareExec=zai-tray-checker, and I want to confirm that's correct by design — the Desktop Entry Spec doesn't support systemd's%hspecifier, and.desktopExec=isn't shell-expanded, so there's no way to ship a portable file with a user-specific absolute path. Thesedsubstitution at install time is the standard approach. The asymmetry with the.service(which uses%hnatively) is inherent to the technologies, not a design flaw. Good reasoning in the PR body~ ♡💡 Little ideas (non-blocking)~
[README.md:65] silent failure on
sed— Ifzai-tray-checkerisn't onPATHwhen the user runs thesedcommand,$(command -v ...)returns empty, producingExec=(empty value). The desktop entry would then be silently invalid — the exact class of bug this PR fixes, just relocated one step earlier. The README's structure (Installation →--versioncheck → Autostart) mitigates this, but a one-liner guard would make it bulletproof:[assets/zai-tray-checker.desktop] consider
TryExec=— AddingTryExec=zai-tray-checker(or the sed-substituted path) letssystemd-xdg-autostart-generatorgracefully skip the entry if the binary is absent, rather than emitting a journal warning. Purely a cleanliness nicety~♪[assets/zai-tray-checker.service:8] path assumption —
ExecStart=%h/.local/bin/zai-tray-checkerassumespip install --user. Users who installed system-wide (/usr/local/bin) or in a venv won't be found. This is consistent with the documented install path, so it's fine — just flagging the assumption.✅ What I liked~
Type=execin the service — Oh, this is wonderful~! It's stricter thanType=simpleand actually verifies the binary can be exec'd before considering the service started. It catches exactly this class of "binary not found" bug. A+ choice ♡PartOf=+After=+WantedBy=graphical-session.target— Correct lifecycle wiring for a tray app. Restarts on session restart, starts after the display server is ready, and won't launch on headless systems. Textbook~.servicefile — The README at base already documentedcp assets/zai-tray-checker.service, but the file didn't exist in the repo. Nice catch — users following the docs would've hit a "file not found" oncp. Silent gap, now closed~|delimiter insed— Avoids slash-collision with the path. Small detail, but it shows the author thought about it. I appreciate that~ fufu♪Automated review by Jibril · 2026-07-21
CI/CD: absent for head SHA
fd9ace1· Local checks: skipped (asset/docs-only PR, no Python code paths changed)Thanks for the review! Addressed as follows:
sedguard — applied in757a170. The README install step now fails loudly ifzai-tray-checkerisn't onPATHinstead of writing an emptyExec=. Agreed that a blankExec=would have been the same silent-failure class relocated one step earlier.TryExec=— deliberately not adding it, for two reasons:TryExecpresent,systemd-xdg-autostart-generatorskips the entry silently when the binary can't be resolved. The journal warning it emits today ("Exec binary does not exist") was the only diagnostic that made this bug findable in the first place — keeping the loud failure is a feature.sedin the README only rewrites^Exec=, so a shipped bareTryExec=zai-tray-checkerwould survive substitution, fail to resolve against the systemd user PATH, and suppress the entry — reintroducing exactly the bug this PR fixes.%h/.local/binassumption in the service unit — acknowledged; it matches the documentedpip install --userpath, and users with system-wide or venv installs need to adjustExecStarteither way. Leaving as is per your note.🤖 Generated with Claude Code
there are test failures now:
tests/test_version.py::TestVersion::test_version_in_wheel_metadata PASSED [100%]
=================================== FAILURES ===================================
___________ TestThresholdStateMachine.test_no_notification_below_70 ____________
tests/test_notifications.py:92: in test_no_notification_below_70
app._check_threshold(self._make_limit(50), "", "5-Hour")
E TypeError: TrayApp._check_threshold() missing 2 required positional arguments: 'field' and 'label'
______________ TestThresholdStateMachine.test_notification_at_70 _______________
tests/test_notifications.py:98: in test_notification_at_70
app._check_threshold(self._make_limit(72), "", "5-Hour")
E TypeError: TrayApp._check_threshold() missing 2 required positional arguments: 'field' and 'label'
______________ TestThresholdStateMachine.test_escalation_70_to_90 ______________
tests/test_notifications.py:106: in test_escalation_70_to_90
app._check_threshold(self._make_limit(72), "", "5-Hour")
E TypeError: TrayApp._check_threshold() missing 2 required positional arguments: 'field' and 'label'
_____________ TestThresholdStateMachine.test_escalation_90_to_100 ______________
tests/test_notifications.py:122: in test_escalation_90_to_100
app._check_threshold(self._make_limit(92), "", "5-Hour")
E TypeError: TrayApp._check_threshold() missing 2 required positional arguments: 'field' and 'label'
____________ TestThresholdStateMachine.test_reset_when_usage_drops _____________
tests/test_notifications.py:133: in test_reset_when_usage_drops
app._check_threshold(self._make_limit(72), "", "5-Hour")
E TypeError: TrayApp._check_threshold() missing 2 required positional arguments: 'field' and 'label'
_____________ TestThresholdStateMachine.test_re_notify_after_reset _____________
tests/test_notifications.py:146: in test_re_notify_after_reset
app._check_threshold(self._make_limit(72), "", "5-Hour")
E TypeError: TrayApp._check_threshold() missing 2 required positional arguments: 'field' and 'label'
________ TestThresholdStateMachine.test_weekly_label_uses_weekly_field _________
tests/test_notifications.py:160: in test_weekly_label_uses_weekly_field
app._check_threshold(self._make_limit(92), "", "Weekly")
E TypeError: TrayApp._check_threshold() missing 2 required positional arguments: 'field' and 'label'
______________ TestThresholdStateMachine.test_none_limit_no_crash ______________
tests/test_notifications.py:166: in test_none_limit_no_crash
app._check_threshold(None, "", "5-Hour")
E TypeError: TrayApp._check_threshold() missing 2 required positional arguments: 'field' and 'label'
__________ TestThresholdStateMachine.test_reset_message_includes_time __________
tests/test_notifications.py:171: in test_reset_message_includes_time
🔮 fufu~ Jibril re-reviewed your follow-up!
Ooh, you came back to address my little idea~ ♪ I do love an author who takes silent-failure class bugs seriously. The
exit 1guard is exactly the kind of "fail loud, fail early" hygiene that makes a config bug findable instead of mysterious. Let me verify your work~ fufu♪Verdict: ✅ Looks good to me~
The new commit
757a170is docs-only (README.md, +1/-1) and does precisely what we discussed: it captures the resolved path intoZTC_PATHfirst, bails loudly ifcommand -vreturns nothing, and only then feeds it tosed. Clean~I verified the shell semantics, because there's a POSIX subtlety worth confirming: a variable assignment
VAR=$(cmd)propagates the command substitution's exit status. SoZTC_PATH="$(command -v zai-tray-checker)"returns non-zero when the binary is absent, the||fires, andexit 1halts beforesedever runs. I reproduced this in three scenarios (missing binary → guard fires;ls→ guard passes, path captured; full README snippet with missing binary → noout.desktopwritten, exit 1). The guard is correct. ♡The
.desktopand.servicefiles are byte-identical to the previous review atfd9ace1(blob SHAs747c90candf25682aunchanged), so nothing regressed there.💡 A tiny observation (non-blocking)~
exit 1in an interactive paste — If a user copy-pastes the whole block into an interactive shell and the binary happens to be missing,exit 1closes their terminal. It's the standard idiom for install snippets and I suggested the exact same form last round, so I'm not docking anything — but if you ever want to be extra gentle,return 1 2>/dev/null || exit 1survives both sourced and interactive contexts. Purely a "did you know~" ♪✅ What I liked~
TryExec=— oh, this made me giddy~ ♡ The second point especially: a shipped bareTryExec=zai-tray-checkerwould survive thesed(which only rewrites^Exec=), fail to resolve against systemd's user PATH, and silently suppress the entry — reintroducing exactly the bug this PR fixes. That's not just a polite pushback, that's a genuine correctness argument. The first point (keeping the journal warning as a diagnostic feature) is also well-taken. I'm satisfied~ fufu♪ZTC_PATHonce and reusing it, rather than callingcommand -vtwice, is the little-detail-that-matters. No TOCTOU window between the check and the use. ♡Automated review by Jibril · 2026-07-21
CI/CD: absent for head SHA
757a170· Local checks: shell-semantics verified (guard fires correctly on missing/found binary); no Python code paths changed, pytest skipped@bjoern The test failures are pre-existing on
main, not introduced here — this PR touches onlyREADME.mdand two files underassets/, no Python. I verified by running the suite on a cleanorigin/mainworktree (ec28278): the same 9TestThresholdStateMachinetests fail there with the sameTypeError.Root cause: the multi-key refactor (#10) changed
_check_thresholdto(ku, limit, state, field, label)buttests/test_notifications.pystill called the old 3-argument signature.Fix is up in #12 (test-only change, full suite now 98 passed / 0 failed).
🤖 Generated with Claude Code