Repository navigation
feat(mac): independent Mirror/Extend per connected device - #24
Draft
raiseCatError wants to merge 32 commits into
Draft
raiseCatError wants to merge 32 commits into
raiseCatError wants to merge 32 commits into
Conversation
…reams only when ready On iPhone every QUIC dial failed. The listener's new-flow path received datagrams of an already accepted connection, failed to register a duplicate flow for the same 4-tuple (NECP ADD_FLOW EEXIST) and took the live connection down (EINVAL); its streams then failed their preface read with ENOTCONN, which was misreported as invalidStreamPreface. - The QUIC listener no longer opts into local-endpoint reuse, which the TCP listener needs but a UDP QUIC server does not: a second binding of UDP 9001 now fails and retries instead of silently sharing the port. - Listener parameters come from one factory shared with the loopback tests, so CI exercises the production configuration. - Accepted streams are read only after reaching .ready, and a failed read or stream is a transport failure, never a protocol violation. - Bounded DEBUG logging of accepted connections and ready streams.
…ramework's stream 0 Physical iPhone QUIC failed on every dial, and the same receiver path fails over macOS loopback once it is actually exercised: - Network.framework delivers one object of its own per QUIC connection to newConnectionHandler (no stream ID, never ready, no data). The receiver treated it as a MEOW stream: it counted it against the three-stream topology, armed a preface timeout on it and read from it. On iPhone that read fails at once (ENOTCONN) and every connection was closed as invalidStreamPreface. Delivered objects now count and are read only once they are .ready; a never-ready object that fails is dropped. Delivered objects stay bounded. - The sender's streams start at stream ID 4 because stream 0 is Network. framework's, so initial_max_streams_bidi = 3 left the sender's third stream blocked forever. The limit is now 4, set from one shared place. - Reverts the listener local-endpoint-reuse change: the evidence did not support it. - The loopback test now runs the production QUICReceiverGroup with three streams, production listener parameters and stream limits.
… gestures QUIC (Mac sender): NWConnectionGroup `.waiting` is no longer fatal. A physical Mac -> iPhone dial reported `.waiting(ENETDOWN)` and then `.ready`; failing on `.waiting` tore the tunnel down before its streams opened. The state handling moves into a pure QUICGroupStatePolicy: `.waiting` is uniformly nonterminal, `.failed` stays fatal, `.cancelled` is unchanged, and the existing 8 s application-handshake timeout still bounds a group that never becomes ready. Control-stream adoption (becomeReady), endpoint selection, includePeerToPeer, stream limits, pinned mutual TLS, ALPN and no-0-RTT are unchanged. iPad receiver: - VideoView opts out of the three-finger editing interactions. - Screen-edge system gestures are deferred only while the remote surface is shown and input may reach the Mac. - While VoiceOver runs, every custom multi-finger recognizer (two-finger viewport, two-finger double-tap, three-finger swipe/tap, 4/5-finger pinch/spread) steps aside, following runtime VoiceOver changes; force-cancel toggles restore the gate's state, never a blind `true`. - Opt-in (hidden) Function Tray items for Launchpad and Show Desktop, which have no reliable keyboard shortcut; saved profiles gain them hidden. - Document why UIRequiresFullScreen stays for now. Tests cover the group-state policy, the handshake-timeout class, explicit QUIC never falling back to TCP, a loopback run of the production dialer adopting the Control stream, the VoiceOver gate and the new tray items.
…ort freedom Preferences (schema 16): new installs default to Smart Touch on, Auto-hide on, Pinch -> Viewport, Rotation -> Disabled, Prefer MeowDisplay Gestures on and the iPad Strip layout. Pre-release installs migrate those four older values once; choices saved under schema 16 are never re-migrated. Custom layouts decode lossily so one unreadable entry can't reset preferences. Settings: the single NavigationStack + Form becomes one category model and per-category pages. Regular width shows a NavigationSplitView sidebar (presented full screen on iPad); compact width collapses the same view into a category list with pushed pages. Pages observe only what they show. iPad controls (never on iPhone): - Strip: a solid edge rail on any edge; the Mac canvas fits beside it. Optional control hints. - Overlay: full-screen Mac, floating controls with small material halos drawn only where a control covers the rendered display. - Main and Function controls choose independent edges; Main is centered and grows outward, Function groups split toward the edge's ends. - Control Size (0.85-1.4). - Custom: up to five layouts, separate landscape/portrait arrangements, a visual editor, and a radial corner template whose chord palettes bloom as arcs/rings/rows/columns clear of other controls. - Two-Hand Assist: opposite-side idle Function actions, replaced by helper modifiers that act on the same ControlInteractionState chord. Layouts store placement only; actions still come from Control Profiles and the Function Tray. Viewport: panning no longer requires zoom; any edge of the remote display may reach the viewport center but never pass it (rotation-aware). Move View (iPad) lets one finger pan and pinch zoom without sending input to the Mac; two-finger scrolling is unchanged. Gestures: Prefer MeowDisplay Gestures controls screen-edge deferral and the three-finger editing opt-out. VoiceOver always wins, and Move View parks the multi-finger recognizers. Home: centered two-card iPad composition and an optically aligned icon. Pairing: a high-contrast success banner announced to VoiceOver. No wire-protocol, transport or security changes.
Rail and controls: - Strip/Overlay rail groups: system actions (Menu Bar, Dock, Show Desktop, Control Center) at the rail's start, modifiers and keys centered, Move View and Settings at its end. A lane packer keeps Main and Function groups from ever overlapping on a shared edge, moving overflow to an inner lane (a Strip widens to hold it). - Standard Controls: per-control visibility on iPad; Settings always stays. - Overlay groups sit in compact dark trays; the per-control blur/backdrop and its display-footprint plumbing are removed. Control Hints default off. - Menu Bar (Control-F2) and Control Center (Fn-C) are new semantic gestures on the existing path; older Macs ignore them. - While the software keyboard is up, a keyboard accessory bar (⌘ ⌥ ⌃ ⇧ esc tab, hide) replaces the normal controls; its modifiers are the same chord state, and a latched chord combines with typed keys (⌘ then C → ⌘C). Normal controls return unchanged afterwards. Actions: - Editable Function Tray: add, remove, reorder, rename, and a face (SF Symbol, text, emoji, keys) stored apart from the action. - One-tap keyboard chords (including multi-key) and bounded sequences of existing keyboard/semantic actions, with balanced key events. - Chord palettes take user-defined actions, edited from the Control Profile or a Custom layout's modifier; runtime and Preview read the same data. Custom layouts: - Concentric corner arcs: Settings → utilities → modifiers → palette on the next arc; Two-Hand Assist mirrors the modifier arc; Arc fans into free space from any corner or moved anchor; new Wheel palette style. - Full-screen editor with Edit/Preview, Alignment Guides, Snap to Edges and Snap to Guides, contextual help, a dormant Two-Hand Assist while editing, per-layout Function profile, system controls, and shortcut buttons. - MDL1 share codes with validated import (as a pushed page); custom actions a layout uses travel with it as new local definitions. Input and viewport: - Allow Local View Navigation (default on) is separate from Allow Input: pan/zoom/rotate/reset/Move View work with input off and send nothing. - A blocked remote touch shows an ephemeral Request/Enable Input prompt. It never changes a setting; automatic requests need the explicit per-Mac Request Input Automatically switch (not the connection Always Allow policy), are rate-limited, and the Mac still decides. - Per-axis scroll inversion (both on by default). Settings: search on iPad and iPhone over a shared index, scroll-to and highlight of results, and Settings owning its status bar so its header stays below the system area. Preferences schema 16 → 18 with one-time migrations; explicit choices are never re-migrated.
…tzweg#320) Ported onto this line's capture-recovery design (CaptureRecoveryBudget / captureRecoveryScheduled) rather than upstream's replaced recovery loop: a recovery tick that finds the main display asleep or the screen locked re-arms without running a round, so a sleep longer than the budget no longer ends the session. The wait logs once and recovery resumes when the console can capture again. The existing single-pending-tick flag already prevents two recovery chains, so upstream's work-item/in-flight tracking is not needed here. (cherry picked from commit 09e717e)
The tracked copy predates the QUIC listener and lacked its _meowdisp-q._udp Bonjour type, which every generated build already declares.
…reconnect The encode-failure safety net only ever dropped the frame rate, so an HEVC session that was created but produced no frames stayed black or spent the capture-recovery budget recreating HEVC. It now falls back to H.264 once per sender (the existing runtimeFallback reason) and re-arms the streak so the H.264 encoder keeps its own one-time FPS recovery. Synchronous VTCompressionSessionEncodeFrame rejections now count toward the same zero-success streak as failed outputs. A hello that no longer offers HEVC while HEVC is running (a reconnect to an H.264-only receiver build) now rebuilds the live encoder for the codec that hello allows, instead of re-confirming HEVC. Auto stays H.264-first; the user's codec preference model is unchanged.
… link ProjectLinks is now the one definition of the project's external links. The Mac app, Mac Receiver and iPhone/iPad About sections link to the GitHub repository. A "Support MeowDisplay" link appears only once ProjectLinks.koFiPage holds a real https://ko-fi.com/<page> address; it is nil until that page exists, so nothing ships pointing nowhere.
README: iPad Strip/Overlay/Custom controls, the editable Function Tray, local view navigation apart from input, the input request prompt, system requirements, a GitHub link, and Smart Touch's new default. PROTOCOL.md: document optional HEVC (hello.codecs, streamCodecState, pv 19) and the missing pv 15/16/18 rows. APP_STORE_LISTING.md: iPad controls in the description, drop Apple Pencil per the listing's own rule, explain Local Network, Face ID and background audio in the review notes, and allow a manual Organizer upload. RELEASE.md: the manual first-release path and the two values left to fill.
Set AppStore.iOSAppID to 6817552864, the app's Apple ID in App Store Connect, so the iOS update screen and the Mac's updateRequired message open MeowDisplay's own listing instead of the GitHub project page. Record the app's identity in the listing reference and check off the release item.
Set ITSAppUsesNonExemptEncryption to NO in the iOS target's Info.plist properties, matching the App Store Connect export-compliance answer for 1.20.0: the app uses only standard, exempt encryption and needs no documentation, so later uploads skip the questionnaire. It declares no non-exempt encryption, not an absence of cryptography. The listing reference records the answer and that 1.20.0 is not distributed in France.
Both Mac apps now point Sparkle at MeowDisplay-owned feeds (meowdisplay.app/appcast.xml and appcast-receiver.xml) and trust a new MeowDisplay EdDSA public key. Signed feeds and verify-before-extraction are required. SUEnableAutomaticChecks is no longer forced off, so Sparkle asks the user before checking in the background. The release workflow now signs and verifies both appcasts with Sparkle 2.10.0 tools (tarball pinned by SHA-256) and uploads them to the GitHub Release instead of the unused GitHub Pages path. meowdisplay.app redirects the feed URLs to the latest release. The Mac build number is now the marketing version, so Sparkle sees every release as newer, and the signing key is scoped to the one step that uses it. SparkleConfigurationTests pins the feeds, key and consent behaviour, and rejects upstream OpenDisplay's feeds and key.
Adds an Explore Demo entry on the home screen that opens a clearly labeled sample session with a fictional "Review Mac", so the receiver, its controls and every Settings page can be explored without a Mac. - DemoSession holds all sample state in memory. It has no StreamReceiver, and never touches TrustStore, RemoteEndpointStore, the connection log or the standard defaults. - ReceiverControlOverlay is generic over ReceiverControlSource; StreamReceiver is the real source, DemoSession turns sends into local "Demo, not sent" feedback. - Demo control preferences use a separate defaults domain, cleared on entry and exit. Allow Input is a local demo state. - Settings routes Mac-dependent categories to demo pages; local-only pages are reused with the demo store. A real session ends the demo. - Unit tests for the isolation boundary and an iPad/iPhone UI smoke test (launch, Explore Demo, all Settings categories, exit).
- Use the reserved review-mac.example hostname for the sample Remote Access entry instead of a name under the real ts.net domain. - Shared Settings pages take their @AppStorage store from DemoSession.preferenceDefaults: the real .standard domain normally, the demo domain (wiped on entry and exit) in Demo Mode. Zoom While Typing and Cat Mode taps no longer reach real preferences. - Inset the Demo Settings banner inside each column so it no longer covers the lower Settings rows on iPhone in landscape. - Tests for the zoom and Cat Mode isolation and the real-mode store; UI smoke test waits for transitions and runs in landscape too.
- New preferences place the Function Tray on the opposite side of the Main Tray on iPhone, and the iPad Function controls on the leading edge, opposite the trailing Main controls. - Saved preferences always win. A saved blob without the key (written before this default) keeps the earlier same-side / trailing placement. - The schema 5 migration test now drops the Function Tray keys, as a real schema 5 save had none, and still expects same-side.
Demo Mode now shows a working sample Mac desktop instead of a static placeholder, so App Review can try display, input and controls without a Mac. Everything stays in memory and nothing is sent anywhere. - DemoDesktop: the sample desktop model (windows, Files list, Preview, Notes, pointer) and what each interaction did, for local feedback. - DemoSurface: the receiver screen. Direct Touch and Trackpad input, Extend (automatic 16:10 or Use Full Display) and Mirror with a dock, and Video Off as a Trackpad Preview that still moves the pointer. - A DEMO banner stays on screen with Allow Input, options and Exit Demo; with Allow Input off, touches meant for the Mac are blocked. - Extend Shape is demo-local and starts on automatic. - Explore Demo sits above the connection instructions in portrait. - Touch timing uses CACurrentMediaTime, like the rest of the app, so the privacy manifest needs no new required-reason category. - Unit tests for the desktop model, demo-local sends, blocked input and Extend Shape. A visual tour UI test walks every mode in portrait and landscape; the smoke test scrolls Explore Demo into view first.
Explore Demo sat below the fold on iPhone in landscape. The home screen now places it by whether a Mac is paired, in every layout. - No paired Mac: a compact Explore Demo card ahead of the connection instructions in portrait, in the left landscape column and on iPad. The left landscape column scrolls only when its content can't fit. - Paired Mac: a collapsed Demo Mode section, in the Connection Instructions style, after the connection sections. - Pairing state comes from the trust store's in-memory pin snapshot, re-read after pairing, Forget and Settings. Forgetting the last paired Mac brings the card back. No Keychain or network I/O. - The demo can't start while a real session's surface is up, so it never waits behind one. - The card uses the plain card color on iPhone, so it no longer disappears into the white page in light mode. - Tests for the presentation decision and the demo entry rule.
a38c4ab committed CFBundleName and CFBundleDisplayName entries that Xcode extracted from whichever build last ran: "MeowDisplay Dev" from a Debug build in the Mac catalog, and the iOS target name in the iOS one. These would override the configured names if they were ever compiled into the app. Restore both catalogs to their earlier content, so the names come only from project.yml.
Xcode's extraction of the current Demo Mode strings: adds the interactive desktop and home entry strings and drops the ones the earlier demo used. English source text only; no translations change.
Demo Mode for App Review, approved after physical-device testing.
App Store review needs the privacy policy reachable inside the app. Settings > About now has a Privacy Policy link to https://meowdisplay.app/privacy, kept with the other project links in ProjectLinks. Demo Mode shows the same About page, so the link is there in both modes. - Tests pin the URL and check that the About page links it and that Demo Mode uses the shared About page. - The Demo Mode smoke test checks for the link on About.
… 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
This was referenced Oct 25, 2026
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)
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.
Closes #20 once it's verified on devices.
What changed
Each connected iPhone, iPad or receiving Mac now has its own Mirror/Extend mode and its own Mirror display. Changing one device rebuilds only that device's stream.
Mac/DisplayModeAssignment.swift(new, pure):DeviceDisplayModeStorekeeps each device's choice under its install id.DisplayModeCoordinatordecides every live session's mode and source, and names exactly the sessions a change must rebuild.MirrorSourceResolverkeeps Automatic, capture recovery and wake recovery off other devices' MeowDisplay virtual displays.VirtualDisplayRegistryandVirtualDisplayIdentityPlannerlet an Extend display skip an identity another live session holds.SenderController: all mode-affecting events go through the coordinator and rebuild only the sessions it names. That covers a receiver's mode/source requests, Connect with Mirror/Extend, approval, the hello of a late-identified USB device, Video Off and the Displays defaults. The globalmode/mirrorDisplayUUIDbecome defaults, with unchanged storage. Forget clears the device's choice.MacSender/VirtualDisplay: Mirror source decisions use the resolver. Extend probes skip identities another live session holds. Virtual displays register for their own lifetime.MULTI_DEVICE.mdcovers the architecture, what changes what, the limits and a 12-step physical checklist.ARCHITECTURE.mdlinks to it.No wire-protocol change: these messages were already per connection, so existing receivers work unchanged.
Why
Every device already had its own pipeline, but each one was built from the Mac-wide mode, and any change rebuilt every device. Automatic Mirror also took the first display ScreenCaptureKit listed, which could be another device's extended desktop.
Base
This branch sits on the shipped 1.20.0 source (
cbbad7f,release/app-store-rc) plusdev/opendisplay-next's launch docs (merge3432705; oneRELEASE.mdconflict resolved to dev's newer text).dev/opendisplay-nextlacks the 25 release commits, so this PR's diff against it includes them. The feature itself is the 5 commits after3432705.Verification
xcodebuild test -scheme OpenSidecarMac(unsigned, Xcode 27.1): 1892 tests, 0 failures. The baseline on the base commit was 1845; 47 are new inDisplayModeAssignmentTests.The new tests drive the real coordinator and registry with synthetic receivers: Mirror+Extend, Extend+Extend, Mirror+Mirror, 3 and 5 receivers, switching one while others run, interleaved connect/disconnect, crash mid-switch, sender restart, route changes, late USB identification, rejections, Video Off/On, lost displays, identity collisions, and per-receiver input mapping.
git diff --checkclean. Receiver targets don't compile any changed file, andShared/is unchanged.Not verified on hardware. No multi-device run with real displays has been done yet; see the checklist in
MULTI_DEVICE.md.The change is focused, without unrelated refactors
Relevant behaviour was verified (tests and/or on device): tests only, devices pending
Documentation updated if needed
No credentials, secrets or signing material included
No unrelated generated or build artifacts