Skip to content

Adopt ktsu.TrayApp and delete what it now owns - #3

Merged
matt-edmondson merged 1 commit into
mainfrom
claude/adopt-trayapp
Sep 23, 2026
Merged

matt-edmondson merged 1 commit into
mainfrom
claude/adopt-trayapp

Conversation

@matt-edmondson

@matt-edmondson matt-edmondson commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

ktsu.TrayApp was extracted from this tool, so this migration is mostly deletion: NoSleep.Tool now describes itself to TrayAppBuilder and the library brings the tray host, the tray-versus-console decision and its fallback, Ctrl+C and SIGTERM, the --for timer, the standard flags, the --status report, debounced preference persistence, and the MSBuild props that trim the tool payload.

The NoSleep library — ISleepBlocker, KeepAwakeController, the platform blockers — is untouched.

What changed

Deleted (1,300 lines): Cli/CommandLineOptions.cs, Cli/CommandLineParser.cs, Cli/DurationParser.cs, Cli/HelpText.cs, Tray/TrayApplication.cs, Tray/TrayIconAssets.cs, ConsoleRunner.cs, DesktopSession.cs, StatusReport.cs, TrayPreferences.cs, the _TrimToolRuntimeAssets target and NoSleepToolRuntimeIdentifiers in the csproj, the Avalonia / Avalonia.Desktop / Avalonia.Native / ktsu.AppDataStorage package references, and the tests that covered the deleted code (CommandLineParserTests, DurationParserTests, and the CLI/status/desktop-session cases in PlatformIntegrationTests).

Added: ktsu.TrayApp 1.1.1, plus ktsu.Essentials and its ConfigHome / Json / Native providers, assembled in Program because the library depends on IPersistenceProvider<string> only.

Kept: Assets/*.png and their EmbeddedResource item, and the embedded-resource assertion — retargeted to the tool assembly, since the names are still passed to the library as strings and a renamed asset is otherwise only a run-time failure on a machine with a display.

Program.cs is now the menu, the three flags NoSleep adds, the --status rows only NoSleep can answer, and the wiring to KeepAwakeController.

The one load-bearing detail

Program keeps whether the user wants to be kept awake in a local bool and only acts on it in OnStart. A run restores preferences (tray only), then applies the command line, then starts; a toggle setter that called Enable() directly would have OnStart override an explicit --off, or a remembered "off", a moment later. Both paths are verified below.

Behaviour differences

These are reported rather than worked around.

1. nosleep --status now exits 0 on a machine with no inhibitor (it used to exit 1). TrayAppBuilder.RunAsync writes the report and returns ExitSuccess unconditionally, with no hook for the code. A script doing nosleep --status || ... changes behaviour; the Keep awake: row still says not supported on this machine. Verified here with env PATH=/nonexistent nosleep --status → exit 0.

Proposed follow-up on ktsu-dev/TrayApp: a .StatusExitCode(Func<int>) on the builder, defaulting to 0, consulted where RunAsync returns after printing the report. NoSleep would pass () => controller.IsSupported ? 0 : 1 and the difference disappears. (Not filed as an issue — say the word and I will.)

2. The status line lost the --for window. The tray used to read Keeping system awake for 2h; it now reads Keeping system awake. The library parses --for and does not expose the duration to the status callback. The console mode still prints , stopping after 0:00:05, so only the tray and the State: row of --status are affected.

Proposed follow-up: pass the resolved duration to the status callback (an overload taking TimeSpan?, or a TrayRunContext).

3. --off now applies in the console mode too, where it used to be documented and implemented as tray-only (ConsoleRunner enabled unconditionally). A flag handler cannot see which mode the run resolved to — --tray / --no-tray are the library's and are not surfaced — so the flag now means what it says in both modes. nosleep --no-tray --off --for 3s runs for three seconds holding nothing. The (tray only) note is gone from the README.

Proposed follow-up: hand OnStart the resolved mode, which would let a tool restore the old semantics.

4. Smaller ones, all cosmetic but user-visible:

  • The debug environment variable is TRAYAPP_DEBUG=1, not NOSLEEP_DEBUG=1 (README updated).
  • The generated usage block has no Examples: section; the hand-written one did.
  • Console wording comes from the library: nosleep: Keeping system awake, stopping after 0:00:05. Press Ctrl+C to stop. then nosleep: stopped., where it used to name the mechanism and end with released. This machine may sleep again.
  • --status gained a State: row and its rows reordered (Platform, Tray icon, State, then NoSleep's Mechanism / Keep awake / Keep display).
  • A tray refusal is now prefixed with the display name (NoSleep: ...) rather than the command name.
  • Remembered state moved from ktsu.AppDataStorage to nosleep/tray.json under the config home, so an existing install's remembered state is not carried over. It starts from the defaults once.

Dependency note

Pinned to 1.1.1, the latest published version, and the restore guard is not in it. 1.1.1 is dependabot's Avalonia and ktsu-group bumps (ktsu-dev/TrayApp #2 and #4), cut from main before the guard branched. The fix for ktsu-dev/TrayApp#6 — an unguarded preference restore, where a toggle setter that throws while a remembered value is applied escapes RunAsync and kills the process before the tray appears — is still unmerged and will land as 1.1.2 or later. Bump this pin once it publishes.

NoSleep does not depend on that fix, and not by swallowing anything. Both toggles were checked against the code rather than assumed:

  • Keep awake — its setter records intent into a local bool and only touches the controller once started is set in OnStart, so the restore never reaches Enable().
  • Keep display awake too — passes controller.SetKeepDisplayAwake straight through, and that is invoked during the restore, before OnStart. It cannot throw there. SetKeepDisplayAwake delegates to SetRequest, which touches the blocker only under if (isActive); nothing has enabled the controller yet, so there is no Acquire to refuse and no SleepBlockException or PlatformNotSupportedException to escape. Its only other throw is ObjectDisposedException, and the controller is constructed in Main and disposed only after RunAsync returns. Its StateChanged invocation is a no-op at that point too: the sole subscriber is wired by RefreshOn, which the library invokes in TrayApplication.OnFrameworkInitializationCompleted — after LoadPreferences.

So that toggle needs no intent-then-apply treatment of its own on 1.1.1. The refusal still surfaces from OnStart, which the library runs through TrayMenu.Run, so the tray comes up with the message in its status line — verified below. Worth re-checking if that setter ever gains work that can throw while the controller is inactive.

Verification

All on this branch, in this container.

Build and tests. dotnet build clean, 0 warnings, 0 errors. 27/27 passing through the test host directly (./NoSleep.Test/bin/Debug/net10.0/ktsu.NoSleep.Test), since dotnet test reports "Zero tests ran" here.

Packaging — the win holds.

dotnet pack NoSleep.Tool -c Release Bytes RIDs under runtimes/ native .pdb
main (csproj target) 50,257,775 7 0
this branch (package props) 50,209,047 7 (win-x64, win-x86, win-arm64, osx, linux-x64, linux-arm64, linux-arm) 0
this branch, -p:TrayAppTrimToolRuntimeAssets=false 190,046,536 17 6

The third row is the point: the props arriving from the package are doing the work, not a payload that happens to be small.

The tray, end to end (Xvfb + dbus-launch + the library's scripts/status-notifier-watcher.py and click-tray-menu.py, with shimmed inhibitor helpers since this container has no logind bus). The watcher logged registered item: org.kde.StatusNotifierItem-2123-0, the run lasted its full window and exited 0, and clicking Keep awake gave:

--- before ---
  id=1 label='Keeping system awake' checked=None enabled=False
  id=3 label='Keep awake' checked=1 enabled=True
  id=4 label='Keep display awake too' checked=0 enabled=True
  id=6 label='Quit NoSleep' checked=None enabled=True
clicking id=3 ('Keep awake')
--- after ---
  id=7 label='NoSleep is off - this machine may sleep' checked=None enabled=False
  id=9 label='Keep awake' checked=0 enabled=True

— the checked state flips, the status line re-reads itself, and ~/.config/nosleep/tray.json gains "keep-awake": false.

The start/stop ordering, which is the part that was easy to get wrong:

  • Next tray run with that remembered false: menu opens on NoSleep is off - this machine may sleep. OnStart did not override it.
  • --off against a remembered true: same, so the flag beats the preference.
  • Clicking Keep display awake too persists "keep-display-awake": true alongside it.

Failure and fallback:

  • Tray with no inhibitor on PATH: the icon still comes up, status line reads NoSleep failed: Neither systemd-inhibit nor gnome-session-inhibit is installed..., both toggles disabled, quit works, exit 0 — no regression against today.
  • --tray with no DISPLAY: nosleep: could not show a tray icon (XOpenDisplay failed). Staying in the terminal instead. then the run continues and exits 0.

Command line: --help and --version exit 0; --status exits 0; --for 5s runs 5.3s and exits 0; an unknown option prints the usage and exits 2; SIGTERM releases the helper process (no orphan) and exits 0; --display --reason "…" reaches the request before the inhibitor is taken.

Not done

Not merged by me, and nothing pushed to main — a push there runs ktsubuild release and publishes, which is the owner's call.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UfEY8ztrV4M9EuzX65T8QU

The tray host, the tray-versus-console decision and its fallback, Ctrl+C and
SIGTERM, the --for timer, the standard flags, the --status report, the
remembered tray state, and the MSBuild props that trim the tool payload were
all extracted into ktsu.TrayApp. NoSleep consumes the package and deletes its
copies: Cli/, Tray/, ConsoleRunner, DesktopSession, StatusReport,
TrayPreferences, and the _TrimToolRuntimeAssets target. The NoSleep library -
ISleepBlocker, KeepAwakeController, the platform blockers - is untouched.

What is left in the tool is the menu, the three flags NoSleep adds, the
--status rows only NoSleep can answer, and the wiring between them and
KeepAwakeController. Program keeps the user's keep-awake intent in a local
bool rather than calling Enable() from the toggle's setter, because a run
restores preferences first, then applies the command line, and only then runs
OnStart: a setter that enabled directly would have OnStart override an
explicit --off, or a remembered "off", a moment later.

Persistence moves from ktsu.AppDataStorage to ktsu.Essentials over the
ConfigHome/Json/Native providers, assembled here because the library takes
IPersistenceProvider<string> and nothing else. Remembered state now lives in
nosleep/tray.json under the config home, so a previous install's state is not
carried over.

Three behaviour changes come with the move, none of them papered over:

- nosleep --status now exits 0 on a machine with no inhibitor, where it used
  to exit 1. The library has no hook for the exit code.
- The tray's status line no longer mentions the --for window: the library
  parses the duration and does not pass it to the status callback.
- --off now applies in the console mode too, where it used to be ignored,
  because a flag handler cannot see which mode the run resolved to.

Also: the debug environment variable is TRAYAPP_DEBUG rather than
NOSLEEP_DEBUG, the generated usage block has no Examples section, and the
console mode's own wording comes from the library.

Verified: clean build with no warnings, 27 tests passing through the test
host, the tray registering a StatusNotifierItem under Xvfb and D-Bus with a
menu click flipping its state, the no-DISPLAY fallback, and a packed payload
of 50,209,047 bytes against 190,046,536 with the package's trimming disabled.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UfEY8ztrV4M9EuzX65T8QU
@matt-edmondson
matt-edmondson merged commit 6b42258 into main Sep 23, 2026
6 checks passed
@matt-edmondson
matt-edmondson deleted the claude/adopt-trayapp branch September 23, 2026 04:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants