feat(autostart): user-toggleable OS-level autostart (login item) support - #127
Conversation
Adds a "Start at login" checkbox to the tray menu, backed by a small
platform-abstraction module (aw_qt/autostart.py) with is_enabled(),
enable() and disable().
Until now the only OS-level autostart was scripts/config-autostart.sh,
which is Linux-only, so macOS had no autostart at all and Windows only
had the installer's opt-in checkbox. A user who reboots silently stops
recording, with nothing in the UI to notice or fix it.
Backends (stdlib only, no new dependencies):
- Linux: $XDG_CONFIG_HOME/autostart/aw-qt.desktop, reusing the packaged
resources/aw-qt.desktop when it can be found. Hidden=true and
X-GNOME-Autostart-enabled=false (written by desktop environments to
disable an entry) are read as "disabled".
- macOS: a LaunchAgent plist at
~/Library/LaunchAgents/net.activitywatch.aw-qt.plist with RunAtLoad.
No launchctl call: load -w would immediately start a duplicate aw-qt
(RunAtLoad), and unload would kill the instance launchd started at
login. launchd rescans ~/Library/LaunchAgents at each login, which is
exactly the semantics of a login item.
- Windows: the HKCU\...\CurrentVersion\Run value, plus detection of the
Startup-folder shortcut created by the installer ({userstartup} in
activitywatch-setup.iss). The shortcut takes precedence: if it exists
the toggle reads "on" and enable() does not also write the Run value
(which would start aw-qt twice); disable() removes both.
All operations are idempotent and never raise when already in the
desired state. is_enabled() never raises; enable()/disable() raise
AutostartError, which the tray catches and shows in a dialog instead of
crashing.
Unit tests mock the OS layer, so they pass anywhere without touching the
real home directory or registry.
Greptile SummaryThe PR adds user-toggleable OS-level autostart support across Linux/BSD, macOS, and Windows, exposed through the tray menu.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains. Important Files Changed
Reviews (2): Last reviewed commit: "fix(autostart): use Desktop Entry quotin..." | Re-trigger Greptile |
shlex.quote() produces POSIX shell single quotes, which desktop entry parsers do not strip: an install under a path containing a space would have written Exec='/home/me/My Apps/aw-qt' and silently failed to launch at login. Per the Desktop Entry Specification, arguments with reserved characters are enclosed in double quotes, inside which " ` $ and \ are escaped with a backslash; those backslashes are then doubled because the value is also subject to the string escape rules, and a literal % is written %%. Tests now round-trip eight commands (spaces, $, backslash, quote, %, multi-arg) through a reference Desktop Entry parser instead of asserting a quoting style. The same cases were verified against GLib's real implementation (GLib.KeyFile + g_shell_parse_argv).
|
The Greptile finding about
The function wraps arguments in double quotes and escapes The test |
|
@greptileai review |
Problem
The only OS-level autostart in this repo is
scripts/config-autostart.sh: Linux-only, run manually viamake install, and it prints "Platform not supported in script, exiting" everywhere else. So:{userstartup}inactivitywatch-setup.iss), which cannot be changed afterwards from within the app.(Note:
AwQtSettings.autostart_modulesis a different thing — it decides which AW modules aw-qt starts once it is already running.)The practical consequence is silent data loss: a user reboots, aw-qt never comes back, and nothing indicates that recording stopped. This is especially painful for research/study deployments where participants run the Qt bundle unattended for weeks.
Change
A "Start at login" checkable item in the tray menu, backed by a new platform-abstraction module
aw_qt/autostart.pyexposingis_enabled(),enable(),disable()(plusis_supported()so the menu item is hidden where no backend exists). Stdlib only, no new dependencies.$XDG_CONFIG_HOME/autostart/aw-qt.desktop(defaults to~/.config/autostart) — the same locationscripts/config-autostart.shuses, reusing the packagedresources/aw-qt.desktopas the template when it can be found (also inside the PyInstaller bundle, where the spec places it)~/Library/LaunchAgents/net.activitywatch.aw-qt.plistwithRunAtLoad, written withplistlibHKCU\Software\Microsoft\Windows\CurrentVersion\Runviawinreg, plus detection of the installer's Startup-folder shortcutWindows precedence (the interesting bit)
is_enabled()checks both theRunregistry value and%APPDATA%\...\Startup\ActivityWatch.lnk, the shortcut created by the installer's "Start ActivityWatch when Windows starts" task. Checking only the registry would make the toggle read "off" for everyone who ticked that box, and switching it on would create a second autostart entry.Chosen precedence:
enable()does not also write theRunvalue (that would launch aw-qt twice).disable()removes both, otherwise the toggle would appear to do nothing for installer users.The Startup folder is resolved from
Shell Folders\Startupin the registry (localized/redirected paths), falling back to%APPDATA%.macOS: no
launchctlDeliberately no
launchctl load/unload:load -wtriggersRunAtLoadimmediately, starting a duplicate aw-qt that the single-instance lock then kills.unloadwould terminate the instance launchd started at login — i.e. quit the app the user just clicked in.launchd rescans
~/Library/LaunchAgentsat every login, so writing/removing the plist is sufficient and matches login-item semantics (takes effect from the next login). There is a test asserting no subprocess is spawned.Robustness
os.replace).is_enabled()never raises — an unreadable file, missing registry key or unsupported platform is reported asFalseand logged.enable()/disable()raiseAutostartError, which the tray catches and shows in aQMessageBox; the checkbox is then re-synced with the real state, so a failed toggle cannot leave the menu lying. The menu also re-reads the state onaboutToShow, so changes made outside aw-qt are picked up.is_enabled()treatsHidden=true/X-GNOME-Autostart-enabled=false(what desktop environments write when the user disables an entry in their own UI) as disabled, andenable()clears them.Testing
tests/test_autostart.py: 39 unit tests covering platform selection, idempotency of enable/disable on all three backends, Windows shortcut-vs-registry precedence, error propagation, and command resolution (frozen bundle /aw-qton PATH /python -m aw_qt). The OS layer is mocked and the home directory redirected to atmp_path, so nothing touches the real environment and the suite runs on any platform.make typecheckpasses;flake8reports nothing new for the added files; the added files areblack-clean.make testandmake test-integration(underxvfb-run) pass..desktopfile with a correctExec=, toggling off removes it, and the checkbox tracks the real state.Note that
tests/is not currently run bymake test(which only does an import check) and pytest is not a dev dependency, so the new tests follow the existingtests/test_manager.pyconvention rather than changing the CI/dependency setup. Happy to wire pytest intomake testin a follow-up if that is wanted.