Skip to content

fix: restore tablet cursor after display sleep/wake#43

Open
lhysilicon wants to merge 7 commits into
tranvuongquocdat:mainfrom
lhysilicon:fix/tablet-cursor-after-display-wake
Open

fix: restore tablet cursor after display sleep/wake#43
lhysilicon wants to merge 7 commits into
tranvuongquocdat:mainfrom
lhysilicon:fix/tablet-cursor-after-display-wake

Conversation

@lhysilicon

Copy link
Copy Markdown

Problem

When the Mac's display idle-sleeps (pmset displaysleep) and then wakes, the tablet keeps showing a live picture but the mouse cursor is gone — and stays gone until the stream is manually restarted.

Root cause

SCStreamConfiguration.showsCursor = true lets ScreenCaptureKit composite the cursor into captured frames. But cursor-only movement over an idle virtual display does not dirty the surface, so ScreenCaptureKit delivers no new frames. The idle keepalive added in 0.6.8 then re-sends the last cached buffer indefinitely and resets lastFrameTime, so the stall-based restart in startFrameMonitor never fires — the tablet is frozen on a stale, cursor-less frame. There was no display sleep/wake handling, so nothing rebuilt the capture.

Confirmed empirically: with a static desktop, sweeping the real cursor across the virtual display's global coordinates produced zero new SCStream frames (the host stayed at the ~1 fps keepalive the whole time).

Fix

  1. Prevent the trigger — hold kIOPMAssertionTypePreventUserIdleDisplaySleep while streaming so the display (hence the virtual display) never idle-sleeps. Released in stopStreaming.
  2. Recover if sleep happens anyway (manual/forced display sleep, reconnect) — on NSWorkspace.screensDidWakeNotification / didWakeNotification, rebuild the SCStream via a new ScreenCapture.hardRestart(reason:). Guarded by an isStreaming flag + a streamGeneration token so a wake restart can never resurrect a stream that was stopped mid-flight, and debounced 2 s (both notifications can fire on a full-system wake).
  3. Defensive — the CGDisplayStream fallback was created with properties: nil, which omits the cursor; it now passes [kCGDisplayStreamShowCursor: true].

Two files changed: MacHost/Sources/ScreenCapture.swift, MacHost/Sources/AppDelegate.swift.

Notes / testing

  • I could not produce a local release build to smoke-test: my machine has only Command Line Tools (no full Xcode), and the SwiftUI @State macro does not expand under CLT, which blocks the link (unrelated to this change). In whole-module compilation the two changed files produced no compiler errors — only the pre-existing SwiftUI/toolchain issue in SettingsWindow.swift stopped the final binary. Please verify the build on a full-Xcode setup before merging.
  • The display-sleep assertion keeps the main display awake while streaming. If you'd rather preserve idle display-sleep and rely only on the wake-triggered rebuild, gate the assertion on an active client instead of the whole streaming session.
  • didChangeScreenParametersNotification was intentionally not hooked: it fires on benign arrangement/resolution changes and on every virtual-display create/destroy, which would cause spurious restarts; screensDidWake is the precise signal for this bug.

🤖 Generated with Claude Code

After the display idle-sleeps and wakes, ScreenCaptureKit delivers no
frames for cursor-only movement over the static virtual display, so the
0.6.8 idle keepalive re-sends a stale, cursor-less frame indefinitely and
the stall-based restart never fires. The tablet shows a live picture with
no cursor until a manual restart.

- Hold kIOPMAssertionTypePreventUserIdleDisplaySleep while streaming so the
  display (and the virtual display) never idle-sleeps.
- On NSWorkspace.screensDidWake/didWake, rebuild the SCStream via
  hardRestart(reason:), guarded by an isStreaming flag + streamGeneration
  token so a wake restart cannot resurrect a stopped stream; debounced 2s.
- Pass kCGDisplayStreamShowCursor to the CGDisplayStream fallback (was nil).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 12, 2026

Copy link
Copy Markdown

@lhysilicon is attempting to deploy a commit to the tranvuongquocdat's projects Team on Vercel.

A member of the Team first needs to authorize it.

lhysilicon and others added 2 commits July 12, 2026 23:21
- kCGDisplayStreamShowCursor was obsoleted in the 26.x SDK; use the renamed
  CGDisplayStream.showCursor so the CGDisplayStream cursor fallback compiles.
- restartStream() lacked the isStreaming + streamGeneration guard that
  hardRestart() has, so a stale stall-monitor restart could resurrect capture
  after stopStreaming(). Add the same generation-token guard at each async
  continuation point.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…activation

- A Finder/open-launched app has no ~/.local/bin in PATH, so setupADBReverse()
  and adb probing failed to find adb and USB mode never tunnelled. Resolve adb
  via a single StatusDetector.adbExecutablePath() that also searches
  ~/.local/bin, Homebrew, /usr/local/bin and the Android SDK; reuse it in
  AppDelegate instead of a duplicate inline search.
- Screen Recording grant was not re-checked after the user granted it, so Start
  stayed disabled until relaunch. Re-check passively on applicationDidBecomeActive
  (no system prompt); only the explicit permission button requests access.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@luisdavim

luisdavim commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Is this similar to #42?

lhysilicon and others added 4 commits July 12, 2026 23:58
- Touch input needs Accessibility to inject CGEvents; the app never prompted
  for it (promptAccessibilityPermission had no caller), so enabling touch
  silently did nothing. Prompt for Accessibility when touch is enabled (launch
  + toggle) and reflect the grant in the UI.
- Connection status showed a green 'Listening' even when stopped (it keyed off
  IP presence, not listener state). Show real states: Server stopped / Waiting
  for tablet / connected.
- 'Gaming Boost' claimed '1000 Mbps' and '120 Hz' in the UI but the encoder
  forces 50 Mbps. Rename to 'Low-Latency Mode' and state honestly (50 Mbps
  target, speed prioritized, quality may drop). Normal mode now respects the
  user's chosen bitrate (dropped the hidden max(bitrate,60) floor).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Low-Latency Mode (ex-Gaming Boost): finish honest reporting — effectiveBitrate
  and the settings notices now reflect the real 50 Mbps target instead of 1000;
  refresh rate is the user's pick (not a forced 120); diagnostics/labels no
  longer say 'Gaming Boost'.
- adb probe: single-flight + per-command timeout + generation check so slow/hung
  adb can't pile up overlapping detached probes; skip adb entirely in Wireless
  mode; cache the not-found result under the same TTL.
- UI polish: real VoiceOver labels; no QR while the LAN address is empty (was
  0.0.0.0), rebuild on change; Reset uses one shared Defaults source so a reset
  no longer flips 60Hz->120Hz; bitrate slider commits once on drag-end instead
  of rebuilding the encoder every 10 Mbps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stop previously drained frameQueue/receiveQueue synchronously on the main
thread, freezing the settings window under backlog. Move teardown to a
background queue with a lifecycle generation token; the UI drops to a
'stopping' state immediately and mode/resolution auto-restart happens once,
after teardown. Compiles + no streaming/start regression; Stop-path smoothness
not yet driven-tested via UI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Port used TextField(value:format:) which only commits on Return/focus-loss, so
  clicking Start could read a stale port; port 0 was not rejected. Use a local
  string draft parsed live to 1...65535, commit before Start, disable Start when
  invalid.
- adb status collapsed missing/unauthorized/offline/multiple-devices/command-error
  into one 'Not detected'. Return a small ADBStatus enum (parsed from stderr +
  exit code) and show the matching actionable hint in the status row.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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