Skip to content

ci(integration): combined MeowDisplay feature preview — do not merge - #30

Closed
raiseCatError wants to merge 45 commits into
release/app-store-rcfrom
integration/meowdisplay-next-preview
Closed

raiseCatError wants to merge 45 commits into
release/app-store-rcfrom
integration/meowdisplay-next-preview

Conversation

@raiseCatError

@raiseCatError raiseCatError commented Oct 26, 2026 •

Copy link
Copy Markdown
Owner

Temporary verification PR. Do not merge.

This branch combines the five draft PRs on top of release/app-store-rc (65d46e8, which includes the #29 type-check fix). It exists so the Tests workflow can check the complete feature set as one tree on Xcode 26.6. Each feature was reviewed in its own PR; nothing new is added here.

Merge Brings in
77e3aba #26 head 40b3cc6, which already contains #24 (per-device Mirror/Extend) and #25 (Mac Receiver keyboard/trackpad)
0005103 #27 at 2ea0025 (iOS control UX, Return, 8× zoom)
07935b2 #28 head 4737a50 (display arrangement restore timing)
c57f422 #27 head f81898b (Return shown to v1.20.0 upgraders whose Function Tray is untouched)

CI on head c57f422 (Tests run 40351884771), Xcode 26.6, test merge 78a7f31 (tree 0203dab, identical to this branch's tree):

  • Mac unit tests: 2050, 0 failures (2044 before plus six Return-migration tests)
  • unified Mac app, standalone Mac Receiver and iOS receiver builds succeed
  • reserved-region SDK gate passes (iOS SDK 26.5: not declared, not referenced)
  • 11 distinct compiler warnings, the same set as the previous run; none new

Local results on this tree (Xcode 27.1):

  • after c57f422: ReceiverControlsTests 117/117 and the iOS receiver build
  • before it (07935b2): 2044 Mac tests, 0 failures; unified Mac app (Release), standalone receiver and iOS receiver builds; iOS UI tests 9/9 on iPhone 17 and 9/9 on iPad Pro 13" (iOS 26.5)

Not hardware-tested.

When CI has run, this PR will be closed without merging. The features land through #28, #27, then #24 → #25 → #26.

raiseCatError and others added 30 commits October 25, 2026 15:26
Link the App Store listing (id6817552864) from the README and SETUP.md,
replace the pre-release availability note, and point Mac setup at the
source build that matches App Store version 1.20.0. The Mac app has no
signed download yet, so the docs keep it as a source build.

Add the launch announcement and social post drafts under launch/.
docs: announce MeowDisplay for iPhone and iPad on the App Store
The Mac app is now a signed, notarized DMG on the v1.20.0 GitHub Release.
Point the README, SETUP.md, launch announcement and social drafts at it,
and record in RELEASE.md how the release was built locally while CI's
signing secrets are incomplete.
docs: link the MeowDisplay 1.20.0 Mac download
Each receiver already owns its own MacSender pipeline, but every pipeline
was built from the Mac-wide mode and Mirror source, and any change rebuilt
every device. Add the pure decision layer that replaces that: per-device
preferences persisted by install id (following the Mac default when unset),
a coordinator that names exactly the sessions a change must rebuild, a
Mirror source resolver whose Automatic never picks another device's
MeowDisplay virtual display, and a live virtual-display registry with an
identity planner that skips identities another session holds.

Refs #20
Route every mode-affecting event through DisplayModeCoordinator and rebuild
only the session it names: a receiver's displayModeRequest and
mirrorDisplayRequest, Connect with Mirror/Extend, session approval and the
hello of a newly identified USB device now change that device alone and are
remembered for it. The Mac-wide mode and Mirror display become defaults for
devices without their own choice, so a single device behaves as before.
Video Off still hands every Extend session to Mirror in place.

Mirror's Automatic source, capture recovery and wake recovery no longer pick
another device's MeowDisplay virtual display, and an Extend display skips an
identity another live session holds instead of retrying into a refusal.
Forget clears the device's mode.

Refs #20
Add a Display Mode section to a connected device's settings: Mac Default,
Extend or Mirror, its own Mirror display (Mac Default, Automatic or a
specific display), what it is currently doing, and why a request was
refused. The Displays page now presents the mode and Mirror display as
defaults for devices without their own choice and lists each connected
device's mode when more than one is connected.

Refs #20
…m receivers

A receiving Mac's physical keyboard names keys the sender could not inject:
add HID usages for the grave key, F1-F20, Help/Insert, Home, End, Page
Up/Down, the ISO and JIS keys and the keypad. Older senders ignore them, as
an unrecognized usage must be.

A keyboard down may now carry repeat: true for the receiver's own key
repeat; the sender posts an auto-repeat key-down only while that key is
held. An older sender ignores the duplicate down, as before.

Scroll deltas carry their rounding remainder into the next event, so slow
trackpad scrolls (fractions of a point per event) no longer round away.
A single delta still rounds to nearest, as before.

Refs #12
…sending Mac

The Mac Receiver was display-only. Once the sending Mac grants the session
control through the existing per-session request flow, the video window
forwards pointer movement, left and right clicks with their click counts,
drags (clamped to the picture, released even outside it), precise and
wheel scrolling, pinch and rotate, and every key with Shift, Control,
Option, Command and Caps Lock, using the existing pointer, scroll,
keyboard and nativeAppGesture messages.

Forwarding needs the grant, the video window in front and no local pause.
Input is never captured globally: only events addressed to this app's
video window are looked at, so Command-Tab, Force Quit, Mission Control and
other system shortcuts stay on this Mac. A modifier still held from before
(the Command of a Command-Tab) is never sent. Everything sent down is
released on focus loss, the Control-Option-Escape pause, Release, revocation
or window close; a disconnect resets locally.

A titlebar indicator shows View Only / Requesting / Controlling / Paused
with Request Control or Release, a short hint names the pause shortcut,
and the streamed cursor follows the local pointer while controlling.

Refs #12
Document what is forwarded and what stays local, the pause shortcut, the
release guarantees and a physical test checklist; the additive keyboard
repeat field and the full key range in PROTOCOL.md; and drop the
display-only wording from README, SETUP and ARCHITECTURE.

Refs #12
Prepare for one Mac app with both roles. TrustStore gains namespaces:
shared keeps the original Keychain names, so every existing identity and
pin is found as before, and TrustStore.receiverRole is the store the
receiver path (StreamReceiver, its pairing and QUIC listeners, receiver
settings) now uses. It is the same object as shared everywhere except an
app that declares MeowDisplayReceiverRoleTrust = separate, where the
receiver role gets its own identity and pins — the separation the two
standalone apps had. PairingNetwork takes the role's store.

The receiver's Auto-Reconnect setting gets a role-scoped key in that app,
and SameAppPeerFilter lets each role ignore the other's advertisements.
The iPhone/iPad app and standalone receiver behave exactly as before.
MeowDisplay.app now also contains the Mac Receiver (its code, not its app
entry point or bundle resources) behind one switch, Use This Mac as a
Display, in Overview and the menu bar. It is off by default, so an
existing install never starts advertising a receiver it didn't ask for.

Exactly one role has live sessions: turning it on ends this Mac's sharing
sessions without suppressing their reconnect and starts the receiver;
while on, startSession — the gate every auto-connect, Connect, receiver
connect request and Remote knock goes through — refuses, and turning it
off stops the receiver and lets the devices reconnect. That rules out a
Mac streaming to itself or two Macs streaming into each other. Each role
also filters the other's Bonjour advertisements, the app becomes a regular
Command-Tab-able app while the display window is up, quitting sends the
receiver's closing message first, and the role doesn't start while the
standalone MeowDisplay Receiver is running.

The sender's bundle id, macOS 14 floor, Keychain names, settings and
Sparkle feed are unchanged; the standalone receiver still builds for
macOS 12 with its own id and feed.
Record why there were two Mac apps, the measured case against one
macOS 12+ bundle, how the roles and their trust are kept apart, what
existing sender and receiver users see, rollback, and what still needs
physical testing.
In the unified Mac app both roles read and wrote one "Automatically Allow
Connections" key and one per-peer policy dictionary. The sender's setting
(on by default) therefore auto-approved any paired Mac connecting to this
Mac as a display, and the receiver role's choices changed the sender's.

IncomingSessionPolicyStore, RemoteEndpointStore and WakeMetadataStore now
take an explicit RolePreferences at every call site. The sender and the
receiver-only apps (iPhone/iPad, standalone Mac Receiver) keep the original
key names and defaults. The unified app's receiver role uses receiver.-
prefixed keys, and its automatic approval defaults to off. Pairing records
routing hints in the role of the trust store it pinned into, and the
receiver's settings toggle binds the same key the receiver decides with.
InputAutoRequestStore's receiver-only key is scoped the same way.

RolePreferencesIsolationTests reproduce the leak (12 of 14 failed with the
shared keys) and pass with the fix.
…itching

Settings gains a "This Mac as a Display" page, separate from the sender's
pages: the role switch and status, the receiver name, the sending Macs
paired with this role (Automatically Allow Connections, each Mac's
Connection Requests, Connect, Forget) and audio with A/V Sync. Its sections
are the standalone receiver's own, extracted so both apps share them
(ReceiverPairedMacsSection, ReceiverAudioSection, ReceiverNameField). With
the role off, paired Macs can still be forgotten (StreamReceiver.
removeStoredTrust) without putting this Mac on the network. Overview links
to the page.

- The Mirror-unavailable "Use Extend" offer is back: the receiver's
  attention hook opens Settings, which shows the standalone receiver's
  alert (now MirrorUnavailableOfferAlert).
- Changing Dock/menu-bar presentation while the display window is up no
  longer drops the app out of Command-Tab (MacAppActivationPolicy).
- Turning the role off and straight back on waits for the old receiver
  to release its listeners before starting a new one.
- The receiver's own-sender discovery filter reads the sender's id on
  every update instead of caching a possibly missing one.

Tests cover the activation rule, the settings page and search, and guard
that the receiver's settings never reach the sender's trust or keys.
MeowDisplay's display role and the standalone MeowDisplay Receiver bind the
same ports and Bonjour services. A second bind fails with EADDRINUSE
(measured; the port is never shared), but that failure was silent: the
second receiver retried forever while saying "Waiting for a Mac…", and its
pairing listener never retried, so pairing stayed dead after the first
quit.

Both now take ReceiverOwnershipLock (a kernel flock on
/Users/Shared/.meowdisplay-receiver.lock, released by the kernel on quit or
crash) before creating any listener. Whichever starts second binds and
advertises nothing, shows what to quit, checks again every 2 s and starts
by itself; the owner releases only after its listeners closed. MeowDisplay
also waits while an already-released standalone receiver (no lock) runs.

A stopped StreamReceiver is retired, so a listener retry scheduled before
stop() can't reopen a port, and the pairing listener retries a failed bind
like the TLS listener.
With "Use This Mac as a Display" on, the sender kept advertising
_opendisplay-mac-pair._tcp, so other receivers listed this Mac as one that
would share with them, and their Connect went nowhere. The role now
withdraws that advertisement and restores it when turned off; receivers use
it only for listing and pairing, so reconnect is unaffected.

The display role no longer refuses to turn on (or switches itself off at
launch) while MeowDisplay Receiver runs: it stays on, says what to quit,
and starts by itself afterwards.
…il closed

The ownership lock lived in /Users/Shared, where another account could
pre-create it, plant a symlink, or hold it to block this Mac's display.

- Lock at ~/Library/Application Support/MeowDisplay/receiver.lock, reached
  with openat(O_DIRECTORY | O_NOFOLLOW) one component at a time from the
  account's home directory. Every folder must be this user's and not
  writable by others; the file is opened O_NOFOLLOW | O_NONBLOCK, created
  0600 and must be a single-link regular file this user owns (fstat).
  One location, no fallback, and release never unlinks it.
- An unusable lock no longer lets the receiver start: it says what's wrong
  and where, and Try Again checks again. No automatic retry.
- Media and pairing listener bind failures are reported
  (ReceiverPortHealth) so a port held by another account or app shows as
  "in use" instead of "Waiting for a Mac".
- Tests use a real second process for the lock (both orders, normal exit,
  SIGKILL, release and reacquire) and for a taken port, plus symlink,
  folder symlink, hard link, FIFO, foreign owner and permission cases.
…ves it

Avoid Notch lifts the iPhone tray off the home indicator (portrait) or the
notch (landscape), but the palette was avoided on its own and stayed where
the tray used to be, covering half the tray on every Face ID iPhone. Carry
the palette by the tray's offset, as the reserved-region pass already does.
… and Switch Control

The Main Tray, keyboard bar and iPad chips are drawn views whose taps
arrive through one hit-testing gesture, so assistive technologies saw
labelled text with no button trait and no action. Give each control the
button trait and its tap action directly (a modifier latches, as on a
quick tap); palette keys fire their shortcut.

The Explore Demo card wrapped its button in a separate accessibility
element, hiding the button trait (and the button from UI tests).

Adds a Demo Mode UI tour that captures the controls per device and
orientation and checks the tray controls are buttons.
Nine 40pt chips need 396pt, wider than a 390pt or 375pt iPhone in
portrait and taller than it in landscape, so Command and Settings ran
off the screen and out of the tray's touch area (the geometry clamps the
touch frame to the screen). Shrink the chips just enough to fit, never
below 30pt; trays that already fit are unchanged.
Demo Mode passed the keyboard's own frame where the control overlay
expects the visible area above it, so the modifier/esc/tab bar was laid
out behind the keyboard and the trays disappeared while typing. Hand the
overlay the same geometry a live session does.
A fresh iPad install (and Demo Mode) now opens on the radial Custom
layout around a bottom-right Settings anchor instead of Strip. Strip and
Overlay remain in Settings > Controls > Control Layout, and saved
preferences are never replaced. iPhone is unchanged.
At accessibility text sizes the fixed-height banner's text overflowed and
covered Exit Demo, and the status chips wrapped mid-word over the
desktop. Cap the banner at xxLarge and the chips at xxxLarge.
Feature inventory, findings with before/after simulator screenshots, the
Control Surface assessment and a hardware-only test checklist.
Return (HID usage 40, no modifiers of its own) is now a built-in Function
Tray action in its own group after Undo/Redo, a key in the keyboard bar
between tab and Hide Keyboard, and part of the radial iPad layout's
Two-Hand Assist arc. A latched modifier is held on the Mac and merges into
the press, so ⌘ then ↵ sends ⌘↵.

Fresh installs show it; saved Function Tray profiles keep their layout and
gain Return hidden, and saved Custom layouts keep their own assist arc. The
bar's keys shrink to fit eight (35 pt on 375 pt, 37 pt on 390 pt). The bar's
Return is sent only while input is allowed; the Mac enforces the grant too.

Function Tray chips and the iPad Wheel palette's wedges now perform their
action for VoiceOver, Voice Control and Switch Control instead of only
carrying a label; the iPad chip's ignored children had dropped its tap.
…creen

Manual viewport zoom now goes to 8× instead of 3×. It stays a layer
transform: stream resolution, decoding and networking are unchanged, and
Zoom While Typing keeps its own 1.5× limit.

Pan limits came from the rotated picture's bounding box, so a tilted
picture dragged along a diagonal could leave the screen entirely, more so
the further it was zoomed. The clamp now projects the view's center onto
the picture in the picture's own rotated frame, which is identical at 0°,
90° and 180°. An in-range state is kept exactly, so restore and relayout
never nudge it, and the at-rest snap is skipped when it would step the
center off a tilted picture's edge.

Tests cover 1×, 3×, 4×, 6× and 8×, continuous and anchored pinching,
extreme and non-finite pans, an angle grid with idempotence, reset and
restore, pointer round trips, and orientation, window and keyboard
changes; a Demo Mode UI tour pinches to 8×, rotates and pans.
Adds findings 8 (Return) and 9 (8× zoom and the rotated-pan clamp) to the
control surface review, with iPhone SE, iPhone 17e and iPad Pro 13
screenshots and new hardware checklist items. A new performance audit
lists the existing telemetry, software-only measurements (local gesture
math ~0.13 µs per update at any zoom), code-level findings, what needs a
device, and candidates for a separate performance PR.
The Settings walk in DemoModeSmokeTests fails in landscape on a 375 pt
iPhone SE on the shipped base and on this branch alike (portrait 3/3,
landscape 0/3 each, same list state at failure): the Gestures row sits
under the pinned Search field and the test's single swipe flings past it.
Also states that the Wheel and chip accessibility actions are checked in
source only.
After sleep, a mode change that blocked the main actor for ~10 s let the
first manageOrigin() tick run after the 6 s restore window armed at
creation had already closed. That tick reported macOS's default origin
as a user drag, and DisplayArrangement saved it over the device's
placement, so every later session put the display back there.

The restore/observe policy moves into a pure OriginRestore struct whose
window starts on the first tick. A window that closes without one
observation starts observing instead of saving wherever macOS left the
display; first contact still records the initial spot.

Ported onto this line's VirtualDisplay rather than cherry-picked (the
original conflicts). The fork-only repositionForHeadlessMain gets
retarget(to:), which re-arms the window without marking the new origin
observed, so a headless move whose applies all fail never saves the
display's current spot. Upstream's nine OriginRestore tests are kept;
two more cover the headless retarget.

Original change by peetzweg in upstream OpenDisplay (GPL-3.0), PR 392,
closing upstream issue 390.
(adapted from commit 1522cd2)
Xcode 26.6's type checker gives up on the Strip/Overlay hit region in
edgeLayoutContent (ReceiverControls.swift:1426): two `+` concatenations
over an untyped ternary, `Optional.map { [...] } ?? []` and a `??` chain
make the solver try every `+` overload together with each `??` and
literal choice. Built as three [CGRect] lets ahead of the ZStack, the
same rects, in the same order, reach ControlRegionShape.

(cherry picked from commit 18bc616)
Saving any receiver setting also writes the Function Tray profiles, so a
v1.20.0 tray nobody customized was stored and, on upgrade, treated like a
customized one: Return arrived hidden.

A saved tray that still matches v1.20.0's starting tray exactly (action
IDs, order, visibility and groups, with no renamed or restyled built-in)
now takes today's default, with Return shown. Any other tray keeps its
layout and gains Return hidden, as before. An upgraded tray contains
Return, so it never matches again: the upgrade is idempotent and a later
hide sticks. Tested against a literal v1.20.0 stored profile in every
slot.

Copy link
Copy Markdown
Owner Author

Closing as included: this branch's head is an ancestor of dev (merged via #34, merge commit d2e765f), so every commit here is already in the 2.0 development line. The branch itself is left in place.


Generated by Claude Code

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.

1 participant