Repository navigation
ci(integration): combined MeowDisplay feature preview — do not merge - #30
Closed
raiseCatError wants to merge 45 commits into
Closed
raiseCatError wants to merge 45 commits into
raiseCatError wants to merge 45 commits into
Conversation
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
… source # Conflicts: # RELEASE.md
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.
…e updated checklist
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)
… unified Mac app (PRs 24-26)
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.
Owner
Author
|
Closing as included: this branch's head is an ancestor of Generated by Claude Code |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.77e3aba40b3cc6, which already contains #24 (per-device Mirror/Extend) and #25 (Mac Receiver keyboard/trackpad)00051032ea0025(iOS control UX, Return, 8× zoom)07935b24737a50(display arrangement restore timing)c57f422f81898b(Return shown to v1.20.0 upgraders whose Function Tray is untouched)Mac/VirtualDisplay.swift,project.yml,iOS/ReceiverControls.swift,Shared/ReceiverInputPrompt.swift,MacTests/ThirdQARefinementTests.swift).c57f422adds exactly fix(ios): touch-control UX fixes, a Return key and 8× local zoom #27's2ea0025..f81898bdelta (Shared/ControlProfile.swift,MacTests/ReceiverControlsTests.swift,ux-review/ios-control-surface.md). The fix(ios): build the edge-layout touch region from typed pieces #29 fix iniOS/ReceiverControls.swiftis unchanged.dev/opendisplay-nextcommits (launch announcement, 1.20.0 Mac download), merged onto the release source by feat(mac): independent Mirror/Extend per connected device #24's3432705.CI on head
c57f422(Tests run 40351884771), Xcode 26.6, test merge78a7f31(tree0203dab, identical to this branch's tree):Local results on this tree (Xcode 27.1):
c57f422:ReceiverControlsTests117/117 and the iOS receiver build07935b2): 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.