Repository navigation
Wake the host display for active streams - #151
acalatrava wants to merge 15 commits into
Conversation
…ity#134) A Mac whose display is asleep gives the stream black frames until someone wakes it at the Mac (issue Saffsanity#134). The PR as opened declared remote user activity once, as the first device connected, and then held a PreventUserIdleDisplaySleep assertion for every session: the panel stayed lit for the whole session (the opposite of Saffsanity#133's privacy mode), the Mac could not idle-sleep at home while a device was connected, nothing woke the display again if it slept mid-session, it ran on --synthetic test hosts, and its failures went to NSLog, outside Sill.log. Now the host declares user activity the way a key press does: IOPMAssertionDeclareUserActivity with kIOPMUserActiveLocal (what `caffeinate -u` makes, the reporters' workaround), when a device's session connects (every connection), and for a pick or an app launched (kinds 6, 7) and input (kinds 8, 15, 25, 28) only 5 s or more after the last declaration. Nothing is held: the display sleeps again by the Mac's own setting, counted from the last declaration. IOPMLib.h: a declaration postpones display sleep "up to the user's display sleep Energy Saver settings"; WindowServer's UserIsActive assertion for a key press shows that setting as its timeout in `pmset -g assertions`. The ID goes back with each call, as the header asks, and is never released early. - DisplayWake (pure, Foundation only) decides when, and the lines; Tests/checks/display-wake has 57 checks (the rule at its edges, a clock set back, test hosts, the lines and when they are said, 20,000 random sessions against five invariants) and 24 of 24 mutants, in CI's matrix and the checks README. - DisplayActivity (the PR's file, rewritten) makes the call on a queue of its own, since it is an IPC to powerd, reads CGDisplayIsAsleep just before, and prints "Woke the display for <device>, which connected." only when the display was asleep; disp.wake counts declarations. - A test host (--synthetic, as InputInjector.dryRun) declares nothing and counts disp.wakeDry instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The PR also turned the device's idle timer off while the stream screen shows. With two Sill windows on an iPad, one stream screen leaving put auto-lock back while the other window's session went on, and a window closed mid-session may never run its onDisappear. Now that the Mac's display sleeps by its own setting, a device kept awake beside an idle session would also sit lit on a black picture. Keeping the device awake is its own decision, with its own power trade-off, so it is taken out of this change; iOSClient is as on main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/DEVELOPMENT.md gains "A sleeping display" (what wakes it, that nothing is held, the log line, disp.wake and disp.wakeDry, what pmset shows) and a troubleshooting line for black windows; its remote access notes and the pure checks' list follow. CLAUDE.md's Layout names DisplayWake, DisplayActivity and the check, and a short Current step entry says what changed from the PR as opened, what was verified here and what waits for a device. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main gained Saffsanity#142 (a typed destination survives a key change) and Saffsanity#153 (build.sh quoting); no conflicts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t reads on (Saffsanity#134) The verification of PR Saffsanity#151 found two of the device's own messages counted as a person's: the Desktop it asks for by itself (a connection's first list, a Desktop the Mac stopped, a watched window that closed; and what it watched, picked again after a move) and the move to a new frame's middle it makes as new parameter sets arrive (StreamClient.pointerFrameChanged). Either woke a sleeping display with nobody at either end. Which kinds counted lived in the coordinator's switch, unchecked, and the woke line was said before anything showed the display had woken. - DisplayWake decides by the message's kind and input event: a connect and kinds 7, 8, 15, 25 and 28 count; never a pick (6), nor 9, 17, 21, 27, 32 or any other kind, nor input that does not decode, nor a bare pointer move within 2 s after the host's own stream restart or new frame size (streamChanged). The coordinator's handle forwards every message once; parameterSetsSent stamps a stream's first parameter sets and any that differ, at the time they went out. - DisplayActivity reads the main display again 0.5 s after the declaration, on its own queue: "Woke the display for ..." only when it read asleep before and on then, once for one wake (two connects in a second share it); any other declaration says "Declared activity for ..." at most once a minute, a woke line counting as that minute's. - TEST ONLY SILL_TEST_USER_ACTIVE=remote, taken by every real host and Sill.app too (it lets nobody in), declares kIOPMUserActiveRemote instead of Local, so Noah can see on a real Mac whether Remote wakes the panel; a test host ignores it with one line, and the value is never echoed. - The coordinator's comment on the device's own picks said it never picks a window by itself; it does after a move (resumeSource). - Tests/checks/display-wake compiles the rule with StreamMessage.swift and Input.swift (build.sh): every kind there is decided, every input event, the window, the interval, the lines and the second look, the switch, 20,000 random sessions of the rule and 20,000 of the lines: 190 checks, 60 of 60 mutants. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Saffsanity#134) The triage asked twice for a support line on a sleeping display, and the verification found none. "The picture is black" (after "Sill can't see your windows") says what is true for the builds out now, Sill for Mac 0.5.1 included: the Mac's display is asleep; wake it at the Mac, keep it on longer in System Settings > Lock Screen (where macOS 14 and later keep "Turn display off ... when inactive", read from this Mac's Lock Screen pane), and keep the Mac awake in Energy or Battery. The paragraph on Sill waking the display itself (a connect, the device's input; a tap on the black picture is a click nobody sees, so move the pointer, scroll or press Shift; a lock screen; never a sleeping Mac or a closed lid) waits for the release that ships PR Saffsanity#151 between <!-- Display wake --> markers. docs/release-checklist.md part 1 section 3 names the markers beside the clipboard's, with the perl line a republish before that release runs and a grep that must then print nothing (tried on a copy of site/: only the marked paragraph goes, and the page stays balanced). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sanity#134) docs/DEVELOPMENT.md, "A sleeping display", said what the verification found untrue: the display sleeps by the Mac's setting counted from the last declaration (up to 5 s before the device's last input), not from the last input; every admitted session counts, the CLI's plain home door (no --pairing) admitting any device on this Mac's networks or over Direct Wireless; and a tap on a black picture is a click posted at a spot nobody can see, where a pointer move on the trackpad or a modifier key wakes the display without clicking (its troubleshooting bullet said "touch the stream"). It now also says what never counts (a pick, the viewport, settings, menu fetches, the clipboard, a move within 2 s of the host's own restart), the woke and "Declared activity" lines, and the TEST ONLY SILL_TEST_USER_ACTIVE=remote, with the `open --env` line to try it. CLAUDE.md's Current step entry follows the rule and its check, and its Untested list gains what the triage asked the device test to settle: whether kIOPMUserActiveRemote also wakes the panel (or keeps the picture live with it dark, Saffsanity#133), Virtual Display on (a staged window streams from Sill's own CGVirtualDisplay: whether the panel needs waking at all), and a laptop with its lid closed. Its Layout entry and Build and run follow. Tests/checks/README.md's row: what the check compiles and checks, 190 checks, 60 mutants. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main gained Saffsanity#136 (the Apps list's apps at any depth, its installed-apps check). Two conflicts, both unions: CI's mutants matrix lists display-wake and installed-apps (every check but ledger, which has no mutants), and Tests/checks/README.md's provenance note names both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tree (Saffsanity#134) The Verified bullet said two sillclient.py devices; the run used a scratch stand-in instead, which moves its own pointer to the middle as a restarted stream's parameter sets arrive, as the device's pointerFrameChanged does (sillclient only acts on a timer). It now says so, that a restart stood in for a new frame size (the software encoder's test pattern never changes size), and gives the counts on the tree merged with main at 9a8e30c: run-all's 46, the previews' 140 files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…'s (Saffsanity#134) The portrait key row's modifier caps only latch: they send nothing until the next key, so they do not wake a sleeping display. A hardware keyboard's modifier goes out as kind 8 and does. DEVELOPMENT.md's section and troubleshooting bullet, and CLAUDE.md's device list, now say so; the support page already said "a keyboard attached to your device". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, Saffsanity#87, Saffsanity#88) Main gained Shift+Return and Shift+Tab from the iPad (Saffsanity#110), the virtual display's BOOL encodings with their check virtual-display-api (Saffsanity#87), and a universal Sill.app on request (Saffsanity#88). Three files conflicted, each resolved keeping both sides: ci.yml's mutants matrix as the sorted union (display-wake and virtual-display-api, every check folder but ledger), and the lists of what the pure checks decide in Tests/checks/README.md (its intro and provenance note) and docs/DEVELOPMENT.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… display (Saffsanity#134) The support page told a person facing a black picture to press Shift on a keyboard attached to the device, and DEVELOPMENT.md and CLAUDE.md said any modifier key wakes the display. Shift alone never leaves the device: ForwardedKeys.goesAsKey sends ⌘, ⌥ and ⌃ by themselves as keys (kind 8, which counts), but gives Shift to the text system, which sends nothing for it (Tests/checks/key-strokes: "shift and caps lock with shift: to the text system"). The docs now name ⌘ (and ⌥ or ⌃ in DEVELOPMENT.md) and say Shift alone does not reach the Mac. The marked support paragraph also promised that whatever a person does next on the device wakes the display. A pick, the Settings panel and the Apps list never do (DisplayWake: no pick counts), so it now says what does: a pointer move, a scroll, the Command key, or a tap, which also clicks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sanity#134) Every connect counts, so a connection the device opens by itself wakes a sleeping display as a person's would: the automatic reconnect after a dropped connection, and a move to the cable, to Wi-Fi or home. So does the device's own move to a new frame's middle when it reaches the Mac more than 2 s after the new stream's first parameter sets left, on a link that slow. The earlier review listed both as open points; DEVELOPMENT.md's section and CLAUDE.md's entry now say so, and that telling them apart would need a wire change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…anity#134) The Verified bullet gains this head's runs: the release build with no warning, the check's 190, its mutants re-run until a pause stopped them (54 of 60 caught, none missed; the rule, the check and the wire files it compiles are byte for byte those of the earlier 60 of 60 run), run-all's 47, the 140 previews identical, and the loopback runs with stand-in devices and with sillclient.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks for the fix, @acalatrava. Following Noah's direction on #134 (user activity when a device connects, rather than keeping the display on for the whole session), commits were added on top of yours on this branch. Your commit is unchanged. Host: user activity instead of a hold.
Device: the idle-timer change is out of this PR. Turning auto-lock off while the stream screen shows is a separate decision: with two Sill windows, one window's stream screen leaving turned auto-lock back on while the other was still streaming, and now that the Mac's display sleeps by its own setting, an idle device kept awake would sit on a black picture. It would make a good PR of its own if it is safe across windows and says what it does to battery life. Docs: the support page has "The picture is black" (the part about Sill waking the display is marked for the release that ships it), docs/DEVELOPMENT.md has "A sleeping display", and CLAUDE.md the details. The support page now names the Command key for waking the display from an attached keyboard: Shift alone never leaves the device, since its text system takes it. Main was merged in three times (most recently at 4d990e9); only lists in the docs and CI's mutants matrix conflicted. The description now says "Towards #134" rather than "Fixes": a test on a real Mac with a sleeping display comes before the issue closes. |
A sleeping display wakes for a device
While the Mac's display is asleep, macOS draws nothing new, so a device that connects streams black windows, thumbnails too (#134).
With this change Sill for Mac tells macOS that someone is using the Mac, the way a key press does, when a device connects and while a person uses the Mac from it. A sleeping display wakes and stays on while the device is used. Once nobody uses it, it sleeps again by the Mac's own setting.
Host only: no wire change, nothing on the device, no setting. Devices and Macs from before this change work as before.
What it does
The call.
IOPMAssertionDeclareUserActivitywithkIOPMUserActiveLocal, the declarationcaffeinate -umakes (the workaround in #134).DisplayActivitymakes it on a queue of its own, because it is an IPC to powerd. Nothing is held: the display sleeps again by the Mac's own setting (System Settings › Lock Screen), counted from the last declaration.What counts (a connect every time, the rest at most once every 5 s):
What never counts, because the device sends it by itself or it acts on nothing:
StreamClient.pointerFrameChanged).So a device left connected and untouched lets the display sleep, and nothing it sends by itself wakes it.
DisplayWake(pure) is the whole rule; the coordinator forwards every message to it once, and the connect from the client-connected path.A black picture. A tap on it is a click, posted at a spot nobody can see. A pointer move on the trackpad, a scroll, or ⌘, ⌥ or ⌃ pressed alone on a keyboard attached to the device wakes the display without clicking. Shift alone does not: the device's text system takes it, so nothing reaches the Mac.
Sill.log.
disp.wake;pmset -g assertionslists it asUserIsActive named: "Sill: a device is using this Mac".Test hosts (
--synthetic, the CLI's and the app's test-pattern mode) never touch the display. They countdisp.wakeDryinstead.TEST ONLY.
SILL_TEST_USER_ACTIVE=remote(every real host, Sill.app included, with a "TEST:" line at start) declareskIOPMUserActiveRemoteinstead of Local, to learn on a real Mac whether Remote also wakes the panel, or keeps the picture live with the panel dark (#133).Changed from the PR as opened
PreventUserIdleDisplaySleepassertion held for every session is gone. It kept the panel lit the whole session, against Feature Request: Privacy Mode (blank physical display during remote sessions) #133's privacy mode, and kept a Mac at home from idle sleep.kIOPMUserActiveRemotedeclaration at the first device becamekIOPMUserActiveLocal, at every connect and on a person's activity.Docs
site/support.html, "The picture is black": what is true for Sill for Mac 0.5.1 (the display is asleep, so wake it at the Mac; keep it on longer in Lock Screen; keep the Mac awake in Energy or Battery). The paragraph on Sill waking the display sits between<!-- Display wakemarkers until the release that ships it.docs/release-checklist.mdpart 1 §3: the perl line that cuts those markers for a republish before that release, and a grep that must then print nothing.docs/DEVELOPMENT.md, "A sleeping display" and a troubleshooting bullet;CLAUDE.md;Tests/checks/README.md.Verified
No device was used, and the real IOKit call never ran here.
swift build -c releaseon the tree merged with main at 4d990e9 (iPad: Shift+Return and Shift+Tab reach the Mac with the shift #110, VirtualDisplay: the BOOL return's encoding follows the architecture #87, Sill.app: one universal build for Apple silicon and Intel Macs (#62) #88), no warning. iOSClient is main's, so no iOS build.Tests/checks/display-wake: 190 checks and 60 of 60 mutants, in CI's mutants matrix (re-run on this head until a pause stopped it: 54 of 60 caught, none missed; the rule, its check and the two wire files are byte for byte those of the full run). Every message kind decided, every input event, the 2 s window, the 5 s interval, the lines and their 0.5 s second look, the TEST ONLY switch, and two random models of 20,000 sessions each.Tests/checks/run-all.sh, all 47 pass.SillHost --syntheticon 127.0.0.1 with the software encoder, no router and a scripted pointer:disp.wakeDryin exactly six seconds: both connects (2 s apart), a tap, a person's move 3.3 s after a restart, a gesture, and ⌘ alone. Nothing for the other device's tap 2 s after the first, a pick of nothing, the Desktop picked again, the device's own re-centre (after the Desktop was picked, and after the other device's settings change restarted the stream, 8.2 s and 7.6 s after the last declaration), the viewport, a settings change, the clipboard, a menu fetch, or input that does not decode.sillclient.pydevices: 3 counts (both connects and a move 4.5 s after a restart), where the rule before counted the Desktop picks too.disp.wakekey, a "Woke" or "Declared" line, or an assertion of the host's inpmset -g assertions; the test host printed "SILL_TEST_USER_ACTIVE ignored: a test host declares no user activity (it counts disp.wakeDry)."For Noah to try
Use Sill.app from this branch, release-signed with the embedded profile, and the iPad.
pmset displaysleepnow, then connect at home and through Tailscale. Expect the picture, not black windows, and Sill.log "Woke the display for …, which connected.".pmset -g assertionsshould listUserIsActive named: "Sill: a device is using this Mac", its timeout the display-sleep setting.open --env SILL_TEST_USER_ACTIVE=remote /Applications/Sill.app, runpmset displaysleepnow, then connect. Does the panel light, or does the picture come back with the panel dark?Towards #134
🤖 Generated with Claude Code