# Sill Open-source, free app that streams individual Mac app windows to iPhone and iPad (designed for iPhone Duo first), with a free Mac companion. Tip jar, no subscription, no servers. Read `docs/BRIEF.md` before doing product-level work. Formerly winstream; the folder still carries the old name. ## Current step **The clipboard both ways (2026-10-14, branch `clipboard` from main at 985a733; issue #64; PR #78; the plan, its open questions with the default taken for each, the results and the review's fixes are in `docs/clipboard-plan.md`).** Noah: work on the clipboard (after #67/#68, #69, #59, #57, #58), text first. Wire, host and device; additive. - Wire: kind 32 `clipboard`, both ways, JSON `Clipboard {text, image, stop}` (Clipboard.swift): pull request #66's number and shape (`image` reserved for a later generation; `stop`, a device's switch going off, is this change's); `WindowList.clipboard: Int?` (1: text), so #66's `Bool` becomes an Int as it rebases (a Bool does not decode). A device sends kind 32 only to a host that says 1 or more; any kind 32 subscribes, and `{}` is text alone: a host sends `image` only to a subscription that says it takes images (the floor; #66's host sends one to every subscriber). Text at most 256 KB of UTF-8, the JSON at most 1 MiB either way. Kinds 29–31 stay free (audio, #66's). - Rules, pure, both ends: `ClipboardPolicy` (StreamProtocol). Never sent: an item a password manager or another app marks `org.nspasteboard.ConcealedType` or `org.nspasteboard.TransientType` (and three older markers), one from another device through Universal Clipboard (`com.apple.is-remote-clipboard`: reading it fetches it from there, found while building), the host's own write; types are judged before any data is read. A device's subscriptions and texts are rated 4 a second, its stop never (`hostReceives`). - Host: `HostClipboard` (main actor; the pasteboard read and written on a queue of its own, `Board`, so a slow read never holds up input): the look at the change count four times a second while a device subscribes (Task.sleep, not a RunLoop timer), and only while macOS allows every read (`MacClipboard.access`, NSPasteboard's `accessBehavior`, macOS 15.4 and later: while it would ask, an alert at every copy, or denies, nothing is read, nothing copied meanwhile is sent, one line when that changes); an empty pasteboard waits up to a second for its contents (`hostWaits`: found live, 9 of 12 slow copies lost before, none now); what was on the clipboard when the first subscribed is never sent; a device's text written `.currentHostOnly` with this launch's marker and passed to the other subscribers; one line per subscription or stop ("Clipboard: iPad (iPad14,1) shares the clipboard."), none per copy, never contents; `clip.sent`/`clip.recv`. `HostConfig.shareClipboard` (on): Settings › General's `ClipboardSection`, "Share clipboard with devices" (`shareClipboard`), and while macOS asks or denies, why and "Allow in System Settings…" (Paste from Other Apps, `Privacy_Pasteboard`; one read on that click first while macOS has not asked about Sill, so it is listed); the CLI's `--no-clipboard`; the Mac's alone, applied at once by `setTarget`. A synthetic host shares a pasteboard of its own (TEST ONLY `SILL_TEST_PASTEBOARD`, `SILL_TEST_CLIPBOARD_LOG=1`, `SILL_TEST_CLIPBOARD_ACCESS`), never this Mac's clipboard. - Device: `StreamClient+Clipboard` (subscription, `stop`, the Mac's text written `.localOnly` with a 10-minute expiration, the handled change count `Sill.clipboardHandled`) and `ClipboardChip`: when Sill becomes active (or a session starts within 15 s after) with new text on the clipboard (`changeCount`, `hasStrings`: no prompt), "Paste on ‹Mac›" / "Then press ⌘V there." with the system's Paste control (`PasteButton`, UIPasteControl) and Dismiss; its 20 s run only while it shows with Sill active, never under VoiceOver, which is told when it first shows (`chipCounts`); under the bar sideways (bottom corner while the link's line shows), above the window bar or row 1 upright. A Paste is "Sent to ‹Mac›’s clipboard" only once a ping sent after the text comes back (a 16-byte random payload, the receipt; none in max(4 s, 4 × the worst round trip): "Didn’t reach ‹Mac›. Try Paste again." and the chip again). Settings › This iPad: Share Clipboard (`Sill.shareClipboard`, on) and Paste on ‹Mac›, while the Mac says it. Harness: `-SillClipboardChip 1`, `-SillClipboardNote sent|long|notsent`; live, `-SillSettingsScript ' paste TEXT'` and `-SillSettingsCloseAt `. - The review (2026-10-14, eight findings, all taken; the plan's "The review"): the stop lost to the rate and macOS's paste privacy (the two should-fixes), the floor's image rule, "Sent" before the Mac had it, the chip unannounced and running out unseen, the Mac's footnote and the support page's sentence, the site's words marked ``, to cut for a release without it. `docs/release-checklist.md` is the order of work: the one-time setup (Developer ID, notary credentials, hosting and DNS, the App Store Connect record) and every release's steps. - `Sources/VirtualDisplayProbe/` — CLI experiment for milestone 3; run it from Terminal (needs Screen Recording + Accessibility): `.build/release/VirtualDisplayProbe "Activity Monitor" --seconds 20`. - `iOSClient/` — `Sill.xcodeproj` and its sources: `StreamClient` (Bonjour: a network browser and, when `DiscoveryPolicy` says, a nearby peer-to-peer one, each made again after the background and after a failure (`cameForward`, `rebuildBrowser`); `FoundMac` rows; connection, parsing, reconnect, the move of a session over AWDL to the network, a live session following the best path (`followBestPath`: to the cable, to Wi-Fi, made again over either), a remote session's move home (`moveHomeIfListed`, `MoveKind.fromRemote`), ping, generic `send`, and the pointer: kind 26 judged on its queue, `renderPointer`, `setOwnPointer`, `sendFromKeyRow`, a hand-over's carry-over), `SessionLink` (the session's connection and the one door out to the Mac; the moves' fenced hand-overs, which chain, and the hold of a move off a lost path: `handOver`, `hold`, `adopt`, `unhold`; the input messages counted for the session's connection, `inputsOnSession`, which a kind 26's `seen` is judged against; Foundation and Network only, checked with swiftc), `MessageReader` (the session's reader: each header, then the payload in pieces of at most 256 KB, every piece a sign of life for the liveness check; Network and StreamProtocol only, checked with swiftc), `DiscoveryPolicy` (when to look nearby, the rows and the word each ends in, the session's route word for the Settings panel, when a reconnect may take a Direct row, when a session over AWDL moves to the network, when a live session at home moves to the cable or to Wi-Fi or is made again (`pathPlan`, `upWait`; a remote one only home: `moveHome`, over TLS pinned to the saved Mac's key: `moveHomeTrust`, to a row chosen and refused by row: `moveHomeRow`, `HomeRows`, its kind 18 held to the floor taken as the move starts: `HomeMoveInfo`), the memory of Macs with Direct Wireless on, the Remote rows and when a lost saved Mac is dialed away from home; when a browser is made again and what the rows show meanwhile (`rebuildBrowser`, `browserFailures`, `browserResults`), when the automatic reconnect dials a row that stays listed and its line (`redialRow`, `reconnectLine`), and when the device asks for the Desktop by itself (`autoDesktop`; issue #69); pairing at home's rows and words, a tap's dial, the device's cable check and how a session at home ends: `rowWord`, `homeDial`, `onCable`, `rowMac`, `reconnectEnd`, `macInfoNamesSession`, `removalRevokes`; pure, checked with swiftc), `StreamScreen` (landscape: top bar, thumbnails, drawer, Aa, Keyboard, Desktop; layout selection by size incl. Duo outer display, and `DuoLayout.phoneArrangement`, the idiom: which arrangement `.outerPortrait` draws; `DuoLayout.isPortrait`, the laptop layouts, where the trackpad's pointer shows), `PortraitStreamScreen` (portrait in two arrangements: the halves, the picture over the window bar, the key row and the trackpad, at the inner display's size and, for an iPad window narrower than 600 pt, the compact one with the key row in two lines; and a phone held upright, from `PhonePortraitLayout`; the key row's two sets, twelve and six), `PhonePortraitLayout` (a phone upright, `.outerPortrait` on an iPhone, as rects from the stream screen's size: the picture's fixed 16:10 pane, row 1's five buttons in their band, the strip, six caps, the trackpad and its vertical span, the Aa ruler, the drawer and the Settings panel across row 1, and the dim, and the Menus button ending the strip's row (`menus`, `stripBesideMenus`); with Larger Picture's keyboard (`keyboard`, from `keyboardHeight(for:)`, only while a window streams: `largerPicture(switchOn: streaming:)` and `Streaming`, and only where it is larger: `largerPictureGains`, `offersLargerPicture(screen:)`) no strip, the room over the keyboard, Menus in row 1 (`buttonsBesideMenus`), the drawer above row 1, the pane at the picture's shape (`pane(video:)`) and its span (`trackpadSpan(video:)`); pure, `Tests/checks/phone-portrait`), `DuoPosture` (the iPhone Duo's posture and the rules that follow from it, docs/iphone-duo-plan.md: `FoldInfo`, inferred before iOS 27.1 or the fold's band; the laptop pose's split at the real fold and open flat's at the picture's shape, the book pose's bar, one page for a card (on its targets' page), the connect column and the room above the laptop pose's fold, the pairing overlay and the window lights; the stream screen's status bar (hidden on the inner display in every pose); the hinge from the fold before its first report; the harness's stand-in and the displays' safe areas; pure, `Tests/checks/duo`; pbxproj AB01/FB01), `DuoPostureReader` (iOS 27.1: `onHingeChange` at the app's root into `\.duoEnvironment`, `DuoPosture.read`, the `.division` region as a screen's own GeometryReader sees it; StreamScreen hands it to its layouts as `\.duoFold` and hides the status bar with `duoStatusBar`; AB02/FB02), `InputOverlay` (direct touch, Pencil, keyboard, scroll momentum; each input says what drew this device's pointer; hardware keys through `ForwardedKeys`, let go when the overlay stops taking keys; Esc's key commands; the iPad's trackpad's and a wheel's scroll pans beside the finger pan, one scroll path (`scroll(_:at:translation:velocity:from:)`); `InputOverlayProxy`, who holds the keyboard, carrying out `HardwareKeyboardPolicy`; `HardwareKeyboardMonitor`, GameController's keyboard), `HardwareKeyboardPolicy` (when the stream's input view holds the keyboard for a hardware keyboard and when it is the person's own; pure, `Tests/checks/hardware-keyboard`; pbxproj AC01/FC01), `KeyChords` (`KeyModifiers`; a shortcut with its modifiers' own keys pressed around it, the Spotlight key's ⌘Space, the trackpad's modifier keys around a click; `ForwardedKeys`, a hardware key's up after its down, and Esc's key command sent once beside pressesBegan; pure, `Tests/checks/key-strokes` with the host's KeyStrokes), `TrackpadView` (the relative pad: its cursor a `PadCursor`, from the anchor; a recognizer that only counts fingers; `verticalSpan`, a phone's width ÷ 1.6, and the DEBUG input test), `HEVCDisplayView` (shared display view, the one pointer sprite + DEBUG HUD), `DiagnosticsHUD` (client stats reporter), `StreamClient+Viewport`, `ContentView` (connect screen with rows ending in Wired, Wi-Fi, Direct or Remote, the hint and Search Nearby, Add a Mac…, and a footer along the bottom, "Needs the free Sill app on your Mac." with links to the download, support and the privacy policy, which open in Safari (one line while they fit, else the download link over the other two, as wide as the column); the column stays where it would be without the footer (centred; in the top half on the Duo's 710×1000, the footer still along the bottom; at the top while a field has the keyboard), rises only to keep 24 pt clear of the footer, never closer than 16 pt to the top, and scrolls above it, 16 pt from the top, when even that does not fit (a 12 pt fade, then 12 pt clear, both inside the gap); one scroll view whatever the fit (`ColumnOverFooter`, measuring a hidden copy of the footer), so a fit that changes never builds the card anew (its fields, the camera); + DEBUG harness, and `InputScript`, `-SillInputScript`'s steps), `SillLinks` (the site's addresses, written once; getsill.app is live since 2026-09-25; and the App Store address for a Mac's update notice, the App Store Connect record's since PR #27), `MockCatalog` (harness data and the settings cases), `HostSettingsLedger` (the Mac's settings with this device's unanswered picks; pure logic, checked with swiftc), `HostSettingsPanel` (the Settings panel; the route line, Away from home, the away line in the header, the link's callout and its button, the footnote, the slow-link callout for older Macs), `AwayCopy` (every string of away from home and the link on the device, which callout shows, and `LinkLine`, the stream screen's line over time; pure, `Tests/checks/away-copy`; pbxproj A801/F801). Remote access: `DeviceIdentity`, `SavedMacs` (pure; `homeTLS` and `revoked` for pairing at home), `RemoteDialPolicy` (pure), `RemoteConnector`, `StreamClient+Remote` (pairing, remote dials, the reconnect order, links), `StreamClient+Home` (pairing at home: `DeviceTLS`, the one TLS builder for every connection to a Mac, the session gate at the first window list, the ask, the cable's proof-less ok, the proofs sent to a row, how a session at home ends; pbxproj A601/F601), `AddMacCard` (the card, the fields, `EscapeKey`; the home card for a row), `CodeScanner` (VisionKit), `PairingOverlay` (Pair This iPad…), `GoodbyePolicy` (the words and the reconnect after a session ends, a Mac's notice included; pure, checked with swiftc). The Mac's menus (docs/menu-bar-plan.md §7): `MacMenuState` (one connection's view: the top level, the menus asked for and the choices sent, each opened menu's completion settled exactly once, the rows and sections; pure, `Tests/checks/menu-state`), `MacMenuElements` (the one builder of `UIMenuElement`s for the iPad's bar and the button: uncached deferred elements, `UIAction` leaves with the Mac's shortcut as the subtitle, never a `UIKeyCommand` or `UICommand`), `SillAppDelegate` (`SillApp`'s `@UIApplicationDelegateAdaptor`; only `buildMenu(with:)`) with `MacMenuBar` (where the Mac's menus go in the iPadOS 26 bar, once per build, every anchor looked up first), `MacMenuHub` (whose session the bar shows: the app's one window's while exactly one window scene is connected, none with two or more; from each scene's `WindowSessionObserver`; asks for a rebuild) and `MacMenuButton` (the bars' Menus button: the bar's look, and `MacMenuTrigger`'s clear UIButton with the pull-down over it; `fits`, where a bar holds it and still a whole thumbnail). `PointerPresence` (the rules of the one pointer sprite: the Mac's arrow while the Mac or another device moved it last, this device's own only for the portrait trackpad; a kind 26's freshness; the network queue's feed with a hand-over's carry-over; the key row keeping what shows; the portrait pad's cursor; pure, `Tests/checks/pointer-presence`; pbxproj A301/F301), `TrackpadGestures` (which stroke on the glass goes silent and which arms, the gesture decided as one of its three fingers lifts, and whether it goes to the Mac; pure, `Tests/checks/gestures`; pbxproj A701/F701) and, in InputOverlay.swift, `StrokeObserver` (the recognizer on both surfaces that feeds it every direct touch and takes none). The clipboard: `StreamClient+Clipboard` (the subscription at the first window list that says `clipboard` and on a move's new connection, `stop` when the switch goes off, the Mac's text written `.localOnly` for 10 minutes, the chip's look when Sill becomes active and its time while it shows, Paste's send and its receipt, a ping whose pong says the Mac read it; pbxproj AC64/FC64) and `ClipboardChip` (the chip and `ClipboardPasteButton`, the system's Paste control; AD64/FD64); StreamScreen places the chip, the Settings panel's This iPad group has Share Clipboard and Paste on ‹Mac›. The first-run tour: `TourPolicy` (its steps and targets per layout, the halves, a phone's rows and sideways; when it shows by itself, what it remembers, what a session decided and the reconnect's, a rotation mid-run, the crease, where the card goes, every word; pure, Foundation and CoreGraphics, `Tests/checks/tour`) and `TourOverlay` (the dim with the lit cutout and its ring, the card placed by a Layout, the targets' preference in the stream screen's named space `sill.screen`, `TourStore` for `Sill.tourSeen` and `Sill.tourSkipped`, `TouchWatcher`, a recognizer on the window that counts every touch and takes none, and DEBUG `TourDebug`); StreamScreen runs it (targets, the rule's inputs, the layer above the layouts and under the pairing overlay, Take the Tour from the Settings panel's last row). `Info-Debug.plist` is the Debug configuration's Info.plist: Info.plist's keys, with `_silltest._tcp` beside `_sill._tcp` for the harness's `-SillServiceType` (Release declares `_sill._tcp` alone). `PrivacyInfo.xcprivacy`, a resource of the target, is the privacy manifest: it declares UserDefaults (CA92.1) and `systemUptime` (35F9.1), and any new use of a required-reason API (file dates, disk space, `mach_absolute_time`, active keyboards) must add its category and reason there before the next upload. New files need their four pbxproj entries by hand. Swift 5 language mode. - `docs/BRIEF.md` — product decisions, competition, scope, risks. - `docs/DEVELOPMENT.md` — building, running and testing from source; the README's developer material until 2026-09-25, so a plan's "README" means a section there: the toolchain, make-app.sh, Sill.app's menu and Quality, the CLI's flags, the iOS project, Permissions (the TCC reset), settings from a device, Direct Wireless, pairing at home (the cable, Require pairing, older builds, starting over), remote access (setup, troubleshooting, reset), the test tools in brief (Build and run below has them all), measuring latency, troubleshooting (slow, frozen, the encoder), releasing, known limitations. - `README.md` — the public front page: the site's lede, links to getsill.app and its download, support, privacy and terms pages, the App Store badge (live since 2026-10-10), requirements, how it works, tips, building from source in brief, contributing, the license. `LICENSE` — the Apache License 2.0. `design/screenshots/` — the README's screenshots: the App Store's iPad set at half size. Their raw URLs go to open-source app lists: keep the names. `.github/FUNDING.yml` — the Sponsor button: GitHub Sponsors (a `ko_fi:` line joins it once there is a Ko-fi handle). Tip links live there, in the README's Tips and on the site, never in the iOS app. `SECURITY.md` — how to report a vulnerability (support@getsill.app, never a public issue) and what is in scope. `.github/ISSUE_TEMPLATE/` — issue forms: a bug report, an idea, a question, no blank issue. `.gitignore` also leaves out signing keys, profiles, keychains and design/reel/. - `.github/workflows/` — GitHub Actions on the `xcode-27` runner (macOS 27 with Xcode 27, a public preview; the only image with Xcode 27). `ci.yml`: pull requests and pushes to main that touch more than documents or the site (`design/` counts: `dmg-layout` reads its SVG), and by hand; `swift build -c release`, `Tests/checks/run-all.sh`, the iOS app for the generic simulator (Debug, arm64, `CODE_SIGNING_ALLOWED=NO`; with Xcode 27.1, its beta while the image has no 27.1 release), and the CLI's paths that exit before the host starts (`--internet` alone, exit 2; `--print-reachability`); by hand with "mutants", each check's mutants in a job of its own. `release.yml`: a pushed tag `v*`, or by hand with one; verify only (the tag, the checks, `make-app.sh` signed ad hoc, zipped, and `make-dmg.sh`'s image of it, ad hoc too, as two artifacts) unless the repository variable `SILL_SIGN_IN_CI` is `true`, then the Developer ID .p12 into a temporary keychain, the notary key stored as a profile in it, `release.sh --publish`, Apple's notary logs kept as an artifact (`include-hidden-files: true`: upload-artifact skips whatever is under a folder whose name starts with a dot, `.build` included), and the keychain deleted in an always() step. Secrets, variables, rotation and costs: docs/release-checklist.md, "Releasing from GitHub Actions". `testflight.yml`: by hand only; `release-ios.sh --unsigned` without an App Store Connect key, `--sign-at-export` with one (the secrets `SILL_TESTFLIGHT_KEY_ID`, `SILL_TESTFLIGHT_ISSUER_ID`, `SILL_TESTFLIGHT_KEY_P8`, an Admin key: cloud signing, no certificate or profile stored), and `--upload` too when the variable `SILL_TESTFLIGHT_IN_CI` is `true`; the .ipa is the artifact for 14 days (docs/release-checklist.md, "TestFlight from GitHub Actions"). `.github/actions/select-xcode` — selects the newest Xcode of the `version` asked for (27; CI's build 27.1) whose folder is not a beta and prints `xcodebuild -version`; with `allow-beta: 'true'` (CI's build alone, never a build for App Store Connect), a version with no release yet takes its newest release candidate or beta; without one, CI warns and takes the newest Xcode there and a release (`fallback: false`) fails (CI only: it runs `sudo xcode-select`). The runner's `bash` is 3.2: try `run:` steps with `/bin/bash`. Actions are pinned by commit hash. - `Tests/checks/` — the pure checks, a folder each: `main.swift`, `run.sh` (compiles the app's files it names with swiftc into `.build/checks//` and runs; `--mutants` runs `mutants.py`, passing only when every mutant is caught), and `build.sh` where a check compiles a module (StreamProtocol's sources with `import StreamProtocol` stripped), or a `module.txt` naming the files `module.py` compiles as one module (pairing at home's checks): `addresses`, `ask-limits`, `cable-link`, `clientlink`, `clipboard`, `compatibility`, `device-gate`, `dmg-layout` (Scripts/dmg-layout's `.DS_Store` and alias writer, against Finder's own layout of the file, make-dmg.sh's layout arguments and the SVG's size and edge), `door-policy`, `duo` (DuoPosture; `tour` and `phone-portrait` take the Duo's real sizes too), `encoder-mailbox`, `encoder-slowstate`, `fence`, `gesture-chords`, `gestures`, `goodbye`, `hardware-keyboard` (iOSClient's HardwareKeyboardPolicy), `home-device`, `home-model`, `home-records`, `home-txt`, `key-strokes` (the host's KeyStrokes and the device's KeyChords, against a model of the Mac's HID state table and of a Mac from before), `ledger`, `menu-state` (iOSClient's MacMenuState), `menus` (the host's MenuFormat and MenuPolicy, and MacMenu.swift's JSON), `message-reader`, `origin`, `pairing-address`, `phone-portrait`, `pointer-control`, `pointer-presence`, `pointer-watch`, `policy` (with its `home.swift`), `protocol`, `remote-rules` (with its `home.swift`), `settings-window` (Sill.app's `SettingsWindowFit`), `tour`, `update-policy`, `window-restore` (WindowRestore's rule and sequences against a stand-in Mac, with a copy of the coordinator's glue around them) (the two encoder checks refuse a binary that links VideoToolbox). `run-all.sh [--mutants] [-v] [name…]` runs them and exits with the number that failed (a folder whose `run.sh` is not executable fails); `common.sh` is sourced by each `run.sh`; `README.md` lists what each compiles and the checks that belong to open branches. A change to a checked file updates its check (and a mutant's pattern) in the same commit. - `Tests/touchrig/` — the touch rig (docs/trackpad-gestures-plan.md §9.2): the real `TrackpadView.swift` and `InputOverlay.swift` in a scratch simulator app, driven with synthesized touches (`TouchSynth`, private UIKit calls, test only, never in the app), every event logged; `run.sh [--rev REV] [--switch off] NAME` builds and runs it on a simulator of its own (`SILL_TOUCHRIG_SIM`), `compare.py` judges a run against the surfaces before, and a stroke made right after a gesture against the same stroke made fresh. - `Tests/duorig/` — TEST ONLY, private interfaces, simulator only: folds and turns the iPhone Duo simulator with Device Hub's own events (usage page 0xFF61, usage 0x5B, an IOCFSerialize dictionary from `com.apple.Virtualization.VirtualMachines`), posted from a virtual HID service inside the simulator (`xcrun simctl spawn`); `run.sh build`, `run.sh UDID pose `, `run.sh UDID shot FILE.png`, `hinge`, `orient`, `watch`; only a booted iPhone Duo simulator, by UDID (its README). - `Tests/keyrig/` — TEST ONLY, private interfaces, simulator only: a hardware keyboard and a mouse for the iOS Simulator, key presses, pointer moves and wheel scrolls posted from a virtual HID device inside the simulator (`xcrun simctl spawn`, as duorig), which UIKit routes as a real one's (pressesBegan, key commands, the text system, GameController, hover, scroll pans); `run.sh build`, `run.sh UDID keys SPEC`, `run.sh UDID point SCRIPT`; only a booted simulator of your own, by UDID (its README has what it showed: after its keys UIKit there counts a hardware keyboard for good). ## Build and run ``` swift build -c release swift run -c release SillHost # nothing streams until the iOS app picks a window swift run -c release SillHost Safari # optional: preselect a matching window swift run -c release SillHost --synthetic # Desktop streams a test pattern; no Screen Recording needed swift run -c release SillHost --encoder-selftest # is the hardware encoder alive? 5 s, exits swift run -c release SillHost --virtual-display # picked windows stream from their own HiDPI display (off by default) swift run -c release SillHost --virtual-display-selftest # create/destroy one display, report what sees it swift run -c release SillHost --direct-wireless # also over peer-to-peer Wi-Fi (AWDL): devices without a shared network (off by default) swift run -c release SillHost --remote # the remote door for this run on any free port (--remote=PORT), a throwaway identity; the code and link print here swift run -c release SillHost --remote --internet # also admit paired devices from outside this Mac's networks and VPNs swift run -c release SillHost --print-reachability # the addresses a device would get away from home, then exit swift run -c release SillHost --pairing # the home door speaks TLS, pairing required, a throwaway identity; the code and link print here (with --remote, one pairing for both doors) swift run -c release SillHost --print-cable # what the cable rule reads on each interface, and whether the console is unlocked; read-only, then exit swift run -c release SillHost --menu-selftest=TextEdit # the menus a device would be sent for that app (a pid, or the start of its name), read once; read-only python3 Scripts/sillclient.py PORT 5 none --pair-ask --then-code=$T/code --identity=$T/a # a device's ask at a TLS home door, then the code the host printed (saved in $T/code) python3 Scripts/sillclient.py PORT 8 desktop --set=bitrate=25000000@3 --expect=bitrate=25000000 # a device's settings change SILL_TEST_LOOPBACK=1 SILL_TEST_SOFTWARE_ENCODER=1 SILL_TEST_POINTER_PATH=$T/path .build/release/SillHost --synthetic # a scripted pointer on the test pattern, on 127.0.0.1 alone, never the hardware encoder python3 Scripts/sillclient.py PORT 6 desktop --pointer --move=0.25,0.25@3 # each kind 26 as it arrives; input goes only to a --synthetic host SILL_TEST_INPUT_LOG=1 SILL_TEST_LOOPBACK=1 SILL_TEST_SOFTWARE_ENCODER=1 SILL_TEST_POINTER_PATH=$T/path .build/release/SillHost --synthetic # prints each event a dry run would post ("Test input: key 49 up (none)") python3 Scripts/sillclient.py PORT 3 desktop --input='{"key":{"hidUsage":44,"down":true,"modifiers":1048576}}@1' --input='{"key":{"hidUsage":44,"down":false,"modifiers":1048576}}@1.05' --tap=0.5,0.5@1.5 # an older device's Spotlight key, then a tap python3 Scripts/sillclient.py PORT 8 none --gesture=swipeUp@3 --gesture=swipeDown@4 # a synthetic host logs "Gesture from …: swipe up → Mission Control (shortcut 108: key 160, fn) (not posted: a test host)" SILL_TEST_LOOPBACK=1 SILL_TEST_HOTKEYS='108=off,173=off' .build/release/SillHost --synthetic # TEST ONLY: the gestures read this table (over macOS 27's own; `defaults` alone) instead of this Mac's shortcuts swift run -c release SillHost --no-clipboard # share nothing of the clipboard with devices (on by default, as in Sill.app) SILL_TEST_LOOPBACK=1 SILL_TEST_SOFTWARE_ENCODER=1 SILL_TEST_NO_ROUTER=1 SILL_TEST_PASTEBOARD=me.saffer.sill.test.x SILL_TEST_CLIPBOARD_LOG=1 .build/release/SillHost --synthetic # the clipboard on a pasteboard of its own (never this Mac's), each decision printed with sizes python3 Scripts/sillclient.py PORT 6 none --clipboard --copy=hello@2 --expect=clipboard=none # subscribe, send text as a device's Paste does; each kind 32 printed SILL_TOUCHRIG_SIM='Sill touchrig' Tests/touchrig/run.sh new && Tests/touchrig/run.sh --rev origin/main base && Tests/touchrig/compare.py .build/touchrig/base.log .build/touchrig/new.log # the surfaces under synthesized touches Tests/checks/run-all.sh # every pure check, as CI runs them (~5 min; --mutants adds the mutants, well over an hour) Scripts/pacing/run.sh # remote pacing and the link's judge, this tree against origin/main, no encoder, all on loopback (~25 min; --full ~45; --list) SILL_TEST_LOOPBACK=1 SILL_TEST_SOFTWARE_ENCODER=1 SILL_TEST_NO_ROUTER=1 SILL_TEST_ORIGIN=vpn SILL_TEST_PATTERN=noise .build/release/SillHost --synthetic --remote # an away session whose stream follows its quality Scripts/make-app.sh # .build/Sill.app, signed with the Apple Development identity (~2 s unchanged) Scripts/make-app.sh --install --open # Noah: replace /Applications/Sill.app (a running one quits first), launch it SILL_SIGN_IDENTITY='Developer ID Application: … (9B2KKVM937)' Scripts/make-app.sh --release # M6 SILL_SIGN_IDENTITY='Developer ID Application: … (9B2KKVM937)' SILL_NOTARY_PROFILE=sill-notary Scripts/release.sh [--dry-run] # M6: the notarized download (docs/release-checklist.md) Scripts/release-ios.sh # the iOS app for App Store Connect: archive, export .build/ios/export/Sill.ipa, check it; uploads nothing Scripts/release-ios.sh --bump --upload # Noah: the next build to TestFlight (docs/release-checklist.md, TestFlight; --privacy-report, --print-version) Scripts/make-dmg.sh --sign - .build/Sill.app .build/Sill.dmg # the download's disk image of any build, ad hoc (release.sh makes the real one) python3 -m http.server 8000 --directory site # the website at http://localhost:8000 Scripts/encoder-check/run.sh # the encoder checks that never touch an encoder (safe while Sill.app streams) Scripts/menu-check/run.sh # the menus' reader and mirror against Scripts/menufixture.swift, no host, no encoder (needs Accessibility) Scripts/restore-probe/run.sh # TEST ONLY: a window Sill sized goes back, against a window of its own (needs Accessibility, Screen Recording, an unlocked screen) Tests/duorig/run.sh UDID pose book && Tests/duorig/run.sh UDID shot book book.png # TEST ONLY: fold and photograph an iPhone Duo simulator (laptop, flat-portrait, flat-landscape, closed-upright, closed-side) Tests/keyrig/run.sh UDID keys 29,04,e3:down,1a,e3:up && Tests/keyrig/run.sh UDID point 'move 300,200|wheel -3' # TEST ONLY: Esc, a and ⌘W from a hardware keyboard, then a wheel's notch, inside a simulator of your own SILL_TEST_ENCODER_RECYCLE=0 swift run -c release SillHost # =0 keeps every hardware session, =1 replaces one in the slow state, as by default (A/B on the real Desktop: --synthetic moves every frame and never reaches the slow state) Scripts/encoder-check/verify-hardware.sh harness # USES THE HARDWARE ENCODER; skips each run while a device is connected ``` Needs Xcode as the active developer directory with its license accepted; with Command Line Tools only, add `--build-system native`. First run prompts for Screen Recording: the CLI's belongs to Terminal (or whatever launched it), Sill.app's to Sill itself. Sill.app: the log is `~/Library/Logs/Sill/Sill.log` (`tail -F`, not `-f`: at 10 MB it moves to Sill.1.log; Show Log… in the menu); settings are `defaults read me.saffer.sill.mac` (maxFPS, captureScale, bitrate, prioritizeSpeed, virtualDisplay, directWireless, shareClipboard, updateCheck; the update check keeps updateLastCheck, updateETag, updateLatestTag and updateLatestURL), and a launch argument such as `-maxFPS 60` or `-updateCheck 0` overrides one for one run. A device's change from its Settings panel is saved there too, like a menu click; the CLI keeps a device's change until SillHost quits. Test arguments for the bare binary (`.build/release/SillMenuBar`, defaults domain `SillMenuBar`; delete it after): `--synthetic` (test pattern, off Bonjour; the port is in the "Status: Test Pattern Mode" line), `-SillLogFile `, `-SillSetAfter ' key=value[,key=value][; …]'`, `-SillQuitAfter `, `-SillRenderPreviews ` (panes, cards, glyphs, menu.txt; no permission needed), and for the Settings window's height `-SillSettingsAfter ' |close|report|end|height=[; …]'` (opens it, then prints its frame, the pane's whole height and how much shows), `-SillTestScreenHeight ` and `-SillSettingsShot ` (docs/DEVELOPMENT.md, Test arguments for the bare app). The update check, in the bare binary (which has no version and never checks without these) and the app: `-SillUpdateFeed http://127.0.0.1:P/latest` (a feed on this Mac only: 127.0.0.1, ::1 or localhost; `Scripts/sillfeed.py P` serves one; test pattern mode checks only such a feed), `-SillUpdateVersion 0.3.0`, `-SillUpdateNow 1` (one check at start, as Check Now), `-SillUpdateInterval ` (24 h become s seconds, the retry s/24), `-SillPrintMenuAfter ` (the status menu as it would open, and Settings › General's update line, in the log), `-SillSetAfter ' updateCheck=0'`. Never run a test against GitHub. To make Sill.app check again at its next launch: `for k in updateLastCheck updateETag updateLatestTag updateLatestURL; do defaults delete me.saffer.sill.mac $k; done`. Render previews from `.build/Sill.app/Contents/MacOS/Sill` to see what Sill.app looks like: only the bundle's copy records the real SDK (make-app.sh sets it with vtool; see Current step), and the bare binary draws the pre-26 look. `--encoder-selftest` and `--virtual-display-selftest` work in the app too. iOS side: open `iOSClient/Sill.xcodeproj`, set your team, run on a real device on the same Wi-Fi (or with the Mac's Direct Wireless Connection on). AWDL, headless and without touching a running Sill.app: the synthetic hosts do not advertise, but `SILL_TEST_SERVICE_TYPE=_silltest._tcp` registers them as "Sill test ‹pid›" under that test type (never `_sill._tcp`, so no device sees them) with the listener's own peer-to-peer flag, and prints which one it got; `SILL_TEST_SWAP_FAIL=port|all` makes a replacement's same-port (and any-port) bind fail; `SILL_TEST_PEER_TO_PEER_INTERFACE=en0` counts a client scoped to that interface as one on peer-to-peer Wi-Fi, so a test client on the Mac's own en0 link-local address (`ifconfig en0`, `fe80::…%en0`; Python's socket takes it) stands in for a device on awdl0 when Direct Wireless turns off (a local connection to the Mac's own awdl0 address never became ready, so awdl0 cannot be tested headless); all three are honoured only by a host that does not advertise. `dns-sd -t 3 -includeAWDL -B _silltest._tcp local` lists a registration that includes AWDL a second time on awdl0's index (`python3 -c 'import socket; print(socket.if_nametoindex("awdl0"))'`, 16 here). dns-sd options go before the command: `dns-sd -R … -includeAWDL` registers a TXT record "-includeAWDL" and sets no flag. The kernel's count of AWDL services: `/usr/bin/log show --last 1m --style compact --predicate 'process == "kernel" AND eventMessage CONTAINS "BonJourTrig"'` ("ValidSvc N" once a second; a registration with AWDL adds 2, a peer-to-peer browse 1); `log` is a zsh builtin, hence `/usr/bin/log`, and `process == "kernel"` keeps the log tool's own line out. Enable and disable lines ("Enabling AWDL due to Mdns") show only when nothing else holds AWDL. Keep AWDL-on tests under 20 s and on test types. Remote access, headless: TEST ONLY variables, honoured only by a host that does not advertise: `SILL_TEST_REMOTE_DIR=` (the identity and trust list in a 0700 directory instead of memory or the keychain; the bare app's `-SillPairAfter ` leaves `pairing.url` and `pairing.code` there, 0600, never printed), `SILL_TEST_PAIRING_TTL=`, `SILL_TEST_BACKOFF_SECONDS=`, `SILL_TEST_ORIGIN=vpn|internet` (loopback counts as that origin), `SILL_TEST_REMOTE_ORIGIN=vpn|internet` (the same at the remote door alone, so one loopback host serves a device away and one at home: away from home's H8 and S4) and `SILL_TEST_NO_ROUTER=1` (never ask the router; set it on every headless host). Away from home and the link, headless (docs/remote-bundle- plan.md, PR B): `SILL_TEST_PATTERN=picture:` gives a synthetic host a still picture for its Desktop (any image ImageIO reads, filling the frame: photographs of a device, docs/iphone-duo-plan.md); `SILL_TEST_PATTERN=noise` gives it a square of noise, so its stream follows the quality (on the software encoder about 14 frames a second, 3.6 Mbit/s at Low and 10.6 at Balanced), for a test of the link through `sillrelay.py --rate-mbps 5`; `sillclient.py` prints `away=` and `link=` on each kind 16, and `--expect` knows `awayBitrate`, `awayCaptureScale`, `homeBitrate`, `homeCaptureScale`, `thisConnectionAway`, `awayRunning` and `linkState`. The Mac's pointer, headless: every synthetic host is a dry run (its input is counted as `in.dry`, never posted, and activates or raises nothing) and never reads the real pointer. TEST ONLY, honoured only by a synthetic host (anywhere else one line says it is ignored): `SILL_TEST_POINTER_PATH=` is a scripted pointer on the test pattern, lines `T X Y` (seconds since the stream's first sample, points in the 1512×949 pattern, outside allowed; `#` comments; at most 10,000 steps and 1 MiB, a bad file refused whole with one line): the test pattern then has a pointer, a test client's input lands in the pattern's space and a dry-run pointer event moves the scripted pointer (the newer of it and a step wins); `SILL_TEST_SOFTWARE_ENCODER=1` starts on the software encoder, with no launch probe and no re-check, so a test never touches the hardware encoder (60 fps at most: a 120 fps sampler test needs the hardware, and so the no-device check); `SILL_TEST_INPUT_LOG=1` prints each event the dry run would post, "Test input: key 49 down (command)" with the flags it was made with, and a pointer or scroll event with those the last keyboard event would have left in the HID state table, a button's with where it goes ("Test input: left mouse down (none) at 756,474"), and each check a key's up arms ("Test input: key 55 up: the table read again in 0.25 s for command"): what a device's keys and buttons leave for its next click (KeyStrokes), without posting anything. The device floor, headless: `SILL_TEST_MIN_DEVICE_VERSION=1.2` raises the floor of a host that does not advertise (a device below it, or one that sends no hello, gets kind 22 "update" and is closed; a value that does not parse is ignored with one line), and `SILL_TEST_GOODBYE=''` makes its refusals send that payload instead (a reason the device does not know). Pairing at home, headless: `SillHost --synthetic --pairing` (the bare app's home door always speaks TLS, as Sill.app's does) with `SILL_TEST_REMOTE_DIR`, `SILL_TEST_NO_ROUTER=1`, `SILL_TEST_SOFTWARE_ENCODER=1` and `SILL_TEST_LOOPBACK=1`; the CLI prints its code and link, and again after each ask. The door and pairing hooks are TEST ONLY and honoured only by a test host (DoorPolicy.isTestHost: one that does not advertise and is not Sill.app's own executable; anywhere else TestHooks prints one "ignored" line for each set), and so are the ones above (SILL_TEST_SERVICE_TYPE and the rest): `SILL_TEST_CABLE_INTERFACE=` counts a client scoped to that interface as one on the cable to an "iPad" (serial TEST) and lets this Mac's own address there through, so a client at this Mac's link-local address on it (`sillclient.py --host=fe80::…% --pair-ask=cable`) stands in for a device on the cable, and the simulator's `-SillCableTest 1` for the device's side (lo0's `fe80::1%lo0` keeps such a test on this Mac; it needs a listener there, which SILL_TEST_LOOPBACK's 127.0.0.1 alone is not); `SILL_TEST_LOCKED=1` makes the Mac locked; `SILL_TEST_ASK_QUIET=` shortens the 10 minutes of quiet after the Mac's Cancel or a stop; `SILL_TEST_ASK_FROM_THIS_MAC=1` skips the ask rule's step for an ask from this Mac itself (answered `openOnMac` unless a window is open, and lighting no menu item), so a test client on this Mac opens windows. The bare app takes `-requirePairing NO`, `-SillSetAfter ' requirePairing=0|1'`, `-SillCancelPairingAfter '[; …]'` (the window closed as its Cancel does), `-SillCableNoticeAfter ` (the cable notice for a sample device) and `-SillMenuAfter '[; …]'` (the status menu in the log, line by line). Sill.app itself ignores every `-Sill…After` hook, `-requirePairing` and `SILL_TEST_REMOTE_DIR` with one line each (any process can start it with arguments and its grants); `-SillRenderPreviews`, `-SillLogFile` and the encoder's variables still work there. Require pairing and the trust list live in the login keychain (service `me.saffer.sill.remote`, accounts `paired-devices` and `require-pairing`, beside the key "Sill Remote Access"), never in UserDefaults; Settings › Devices' Remove forgets one device, and deleting those items makes this Mac new to every device. The Mac's menus, headless: `swiftc -O Scripts/menufixture.swift -o $T/menufixture -framework AppKit`, then `$T/menufixture serve $T/fixture.log 60` (it exits by itself, 120 s at most, and can never take the front) and a synthetic host with `SILL_TEST_MENU_PID=` (honoured only by a host that does not advertise; "Test menus: …" at start): the test pattern's menus are then the fixture's, read and pressed without activating it. `python3 Scripts/sillclient.py PORT 20 desktop --menus --fetch=4@3 --press=4.0@5` (with SILL_TEST_MENU_PID in the client's environment too) prints each kind 24 on a line, and `$T/menufixture label PID` what the last press set ("A"); `kill -HUP` inserts an item at the top of Probe in place (the ids below it move: the host's next read of a moved place moves the version), `-ALRM` gives Probe a new menu, and `-USR1`/`-USR2` give Probe › Rebuilt one. `--menu-selftest=menufixture` reads it. Presses go only to the fixture, and a real app's menus are read only with `--menu-selftest=TextEdit`, never a bare `--menu-selftest` (the frontmost app); the plan's §10 has the gates. The bare app takes `-remoteAccess 1 -remotePort P`, `-SillSetAfter '3 remotePort=P2'`, `-SillPairAfter ` and `-SillUnpairAfter `; its `-SillRenderPreviews` adds the Remote Access pane's states and the pairing window's. A session through a shaped link: `python3 Scripts/sillrelay.py --listen 0 --to 127.0.0.1:P --delay-ms 150 --rate-mbps 2`, then `sillclient.py RELAYPORT 90 desktop --tls --identity=$T/a --stats` after `--pair-url=URL` once. Never let a test binary take a connection from another machine: the Application Firewall prompts. `SILL_TEST_LOOPBACK=1`, honoured only by a host that does not advertise, makes both doors of a test host listen on 127.0.0.1 alone ("Test listener: loopback only …"; `lsof` lists `127.0.0.1:PORT`); a device on this Mac, the simulator included, and the test clients reach it by 127.0.0.1. Reset the app's remote settings with `for k in remoteAccess remotePort internetAccess remoteAddressName remoteDevicesSeen; do defaults delete me.saffer.sill.mac $k; done`. Debug harness (simulator; the iPhone Duo's from the iOS 27.1 runtime, folded by Tests/duorig): launch arguments `-SillLayout 951x669` (inner landscape) / `669x951` / `466x678` / `678x466` (the cover), each with the safe area its display gives Sill, the status bar's strip included but on the stream screen upright with `-SillHinge half` or `flat` (`-SillSafeArea T,L,B,R` sets another; the first guesses, `1000x710` / `710x1000` / `500x710` / `710x500`, have none), `-SillHinge closed|half|flat` (the hinge and a 40 pt fold across the middle of the longer side; without it the harness knows no hinge, even on the Duo's simulator), `-SillLive 1` (real client inside the frame), `-SillDrawer 1`, `-SillActive none|desktop|` (mock), `-SillHUD 1` (diagnostics overlay), `-SillSettings 1` (the Settings panel open), `-SillMacMenu code|blender|long|stale|noaccess|none|slow|timeout|refuse` (the mock Mac's menus; `code` by default, VS Code's ten), `-SillMenusOpen 1` (the Menus pull-down opens after launch) or `-SillMenusOpen 'File/Open Recent'` (opens on that menu, each level fetched on the way), `-SillMenuPress 'File/Save'` (chooses that item once the menus are in; both live too, but only on the test app's menus through a test host: `-SillConnect` to a loopback address, no host version, menufixture's top level; anything else prints "refused"), `-SillMenusAt ` and `-SillMenusCloseAfter ` (when the pull-down opens, and closes after it), and on an iPad `-SillMenuBarLayout perMenu|replace|one`, `-SillMenuDump 1` (the main menu's root after each build, on the console), `-SillMenuBuildTwice 1` and `-SillMenuNoView 1` (the insertion's guards), `-SillSecondWindow 1` (a second window 2 s after launch: the bar then gets none of the Mac's menus; the console's "menus: …" and "menubar: …" lines say what happened), `-SillSettingsCase default|cli|software|custom|vdproblem|vdstream|legacy|pending|timeout|direct| directlink|nodirect|wired|noroute` (the mock Mac's settings; it answers a pick after 0.35 s; the readout's route is Wi-Fi except `directlink` Direct, `wired` Wired, `noroute` none, and `remote`, `remoteinternet` and `remoteslow` none, where the route line says how), away from home's and the link's settings cases `away|awaymixed|awayhome|linkbehind|linkmixed|linklow|linkstalled` (the header's away line, the footnote, the callout; `linkstalled` shows none) and `-SillLinkLine behind` (the mock reports the link behind: the stream's line), `-SillSettingsScript ' set K=V[,K=V] | suggestion; …'` (a live session to a synthetic host on loopback: the panel's controls and the link callout's button, t seconds after the first window list), `-SillMoveHomeTest to:HOST:PORT|refused| other:PORT` (with `-SillDialSaved 1`: the saved Mac listed as a network row a second into the remote session at a door that speaks TLS, so the move home runs against a synthetic `--remote --pairing` host with SILL_TEST_REMOTE_ORIGIN and SILL_TEST_REMOTE_DIR, dialed pinned to the saved Mac's key), `-SillDialSaved remotely` (as Connect Remotely: never moves home), `-SillConnectCase looking|hint|nearby|methods|denied|update|notice` (the connect screen in a discovery state; `methods` has a row ending in each word, none, and long names; `update` a Mac's refusal with "Update Sill in the App Store" when `-SillAppStoreURL https://apps.apple.com/app/id000000000` gives it an address, `notice` a goodbye reason the device does not know; the mock never browses) and remote access's `remote|addmac|addcode|addcodeerror| pairing|remotedial|remotefail|camera|externalpair` (`-SillRemoteFailure vpnoff|timeout|timeoutip|refused|dns|wrongmac|revoked|notsill|gaveup|quit|removed| remoteoff` picks remotefail's words), the settings cases `remote|remoteinternet| remoteslow|remotepair|remoteoff|noremote`, `-SillSettingsEnd 1` (the panel scrolled to its end), `-SillSettingsScroll gestures` (the panel scrolled to its This iPad group; the mock Mac takes gestures in every case but `legacy`), `-Sill.trackpadGestures 0` (the device's Three-Finger Gestures off for one run, in the normal app too), `-Sill.largerPicture 1` (its Larger Picture on for one run, in effect while a window streams, as the mock does but with `-SillActive desktop|none`; the console's `larger picture: …` line says what the rule made of it; `-SillVideoSize WxH` the mock's frame, `-SillLargerPictureToggle [,…]` the switch at those seconds), `-SillVoiceOver 1` (that group as with VoiceOver on: its footnote says the rows do the gestures), `-SillScanOverlay 1` (Pair This iPad…'s overlay; `-SillOverlayLine asking|shown|openonmac|locked|noanswer` the line its ask's answer sets; under `-SillLive 1` it opens over the live session and asks), pairing at home's connect cases `homerows|homeasking|homecard|homecode| homecodeerror|homelocked|homeopenonmac|homerevoked|homecabledone|homeolder| pairingrequired` and settings cases `paired|pairedoff|openpair`, the connect case `asleep` (issue #69: a saved Mac's row listed, the reconnect's line after three dials of it that did not connect), `-SillBrowserFail N` (the normal app and `-SillLive 1`: each of the first N network browsers is reported failed with -65569, as iOS reports a suspended app's, half a second after it starts, so it is made again after 1, 2, 4… s; the console's "browser: …" lines, and the reconnect's "reconnect: …"), `-SillPointer mac@X,Y|device@X,Y|hidden|pencil@X,Y` (the pointer sprite in one of docs/pointer-visibility-plan.md's states over the mock's frame, which it draws as a dim rectangle; `-SillPencilPointer 1` is Q2's flip), `-SillClipboardChip 1` and `-SillClipboardNote sent|long|notsent` (the clipboard's chip and a Paste's note on the mock's stream, docs/clipboard-plan.md; the mock Mac shares its clipboard in every settings case but `legacy`; live, `-SillSettingsScript ' paste TEXT'` sends TEXT as the Paste control hands it over, and `-SillSettingsCloseAt ` closes the panel `-SillSettings 1` opened), and in the normal app and under `-SillLive 1` `-SillPairURL ''` (pair at launch, no confirmation, at the link's own addresses only: a confirmed link tries the home rows first, which `-SillTapRow` and `-SillHomeLink` below reach), `-SillPairCode <12 digits> -SillPairAddress host:port`, `-SillDialSaved 1`, `-SillForgetMacs 1`, `-Sill.savedMacs ''` (one run; `'[]'` empties), `-SillRemoteRoute vpn|internet` (a loopback session counts as that route), `-SillHelloVersion ` (the version the hello gives, against a host's floor), `-SillScreenFPS 120` (a 120 Hz screen) and `-SillDeviceKeySE 1` (a Secure Enclave device key, R0-a); for pairing at home (the normal app and `-SillLive 1`), `-SillServiceType _silltest._tcp` (the browsers look for the test type, so a test host registered with SILL_TEST_SERVICE_TYPE is a real row with its TXT record and no real Mac is; only the Debug build's Info-Debug.plist declares it), `-SillHomeDoor paired|open|plain` (what a `-SillConnect` address counts as, since an address has no TXT record: `plain`, the default, dials it plainly as before; `paired` pins the one saved Mac, else asks; `open` an open door), `-SillCableTest 1` (this device's path counts as the cable), `-SillForgetHomeTLS 1` (forget which Macs were seen over TLS, to dial another branch's older Sill.app plainly), `-SillTapRow ` (tap that row once it is listed), `-SillHomeCode ` and `-SillHomeLink ` (the home card's code or scan once the Mac answers `shown`), `-SillOverlayCode ` (Pair This iPad…'s ask, then that code, on a TLS session at home) and `-SillNoAutoDesktop 1` (the device never picks the Desktop by itself: a live gate streams nothing); `xcrun simctl openurl 'sill://pair…'` shows the link's confirmation after the system's "Open in Sill?". `-Sill.directWirelessMacs '("Mac mini")'` (seeds the device's memory of Macs with Direct Wireless on for one run; `'()'` empties it), `-SillConnect 127.0.0.1:PORT` (connect by address, also in the normal app: the only way to reach the off-Bonjour synthetic hosts from the simulator), `-SillMoveTest 1|refused|other:PORT|to:HOST:PORT` (with `-SillConnect`: that session counts as direct and a second later the same address, or its port 1, or its port PORT, or HOST:PORT, is listed as the Mac's network row, so the move to the network runs against a synthetic host; `other:` with a second synthetic host on PORT is another launch, refused at its first window list and not tried again, and with `-SillConnect` through a delay proxy to the first host's own PORT the fence waits out the proxy's round trip; `to:` reads HOST:PORT as `-SillConnect` does, `[::1]:P` too, and this Mac's `fe80::…%en0` address from `127.0.0.1` shows the panel's route word change at the hand-over; the console's "discovery: …" and "session: …" lines, `xcrun simctl launch --console-pty`, say what happened), `-SillWiredTest HOST:PORT` (a wired dial's fallback under test: `-SillConnect`'s dial, a network row's and a move's go to HOST:PORT first, `192.0.2.1:9` giving way after 2.5 s and `127.0.0.1:1` at once), `-SillPathTest ''` (with `-SillConnect`: the session's Mac listed as a network row whose cable and Wi-Fi come and go on cue, so the session follows the best path for real; the spec is ContentView's contract, the console's "path: …" lines say what happened), `-SillInputScript ' ; …'` (with `-SillLive 1`: input with no finger, t seconds after the session's first window list: `down`, `pad DX,DY`, `lift` and `click` on the portrait pad, `tap X,Y` on the stream at a frame fraction, `key USAGE` a hardware key, `row USAGE` a key of the portrait key row, `gesture NAME[,FINGERS]` a three-finger gesture through `StreamClient.sendGesture`, `esc [MODS]` Esc through the stream's key command (only while the input view takes Esc), `scroll DX,DY` a trackpad's two-finger scroll with its coast, `wheel DX,DY` a wheel's plain deltas, `keyboard attach|detach` a hardware keyboard coming or going, `orient landscape|portrait` the scene turned (an iPhone's); the console's "input script: …" and "pointer: …" lines say what happened; it runs only when the session was dialled by `-SillConnect` to a loopback address, `127.0.0.1:P`, `::1:P` or `localhost:P`, and the host's first window list has no version: Sill.app's always has, and would post the input to this Mac, so the console says "input script: refused: …" and nothing is sent; point it only at `--synthetic` hosts; `[::1]:P` works too: the argument domain drops a launch argument that starts with `[`, and StreamClient.init registers `-SillConnect`'s, `-SillWiredTest`'s and `-SillPairAddress`'s from the command line). The first-run tour, in the mock, under `-SillLive 1` and in the normal app: `-SillTourState fresh|landscape|done|skipped|saved` turns the automatic tour on (a Debug build never shows it by itself otherwise; `saved` reads and writes `Sill.tourSeen` and `Sill.tourSkipped`, the others last one run), `-SillTour touch|bar|settings|laptop` starts it at that step, and the stand-ins `-SillTourPress next@S|skip@S`, `-SillTourActivityAt S` (the app run's first session only), `-SillTakeTourAt S` (Settings, then its Take the Tour) and `-SillTourVoiceOver 1`; `-SillOrientation landscape|portrait` asks the normal app's scene for that orientation (a phone simulator sideways with its real safe areas); the console's "tour: …" lines say what happened, and a card's line gives its frame and tail. `xcrun simctl launch --console-pty` streams the console; `--stdout=PATH` writes inside the simulated device's own file system. A fake screen wider than the simulator but fitting on its side (1133x744 on an upright iPad Pro 13") is drawn a quarter turn clockwise; `sips -r 270` the screenshot. One that fits neither way (the Duo's or an iPad's size on an iPhone simulator) is drawn scaled down and laid out at its own size ("harness: 710x1000 drawn at 0.62"); a phone's whole stream screen on that phone (440x894 on an 18 Pro Max, 402x812 on an 18 Pro) reaches into the simulator's safe area and runs its trackpad past its bottom edge: photograph it on a larger simulator, or in the normal app. `-SillKeyboard 1` raises the software keyboard for real in a live session (the mock only lights the button), `-SillKeyboardToggle [,…]` toggles it at those seconds as the Keyboard button does, and in a live session (the normal app with `-SillConnect`, or `-SillLive 1`) `-SillDrawer 1`, `-SillSettings 1` and `-SillScaleOpen 1` open theirs 1.5 s after the stream starts (`-SillDrawerSearch `: the open drawer searches with its field's keyboard up). The console prints `viewport: 386×241 pt, scale none, 60 fps` for each viewport sent, and on a phone upright `trackpad: …, vertical span …` and, once the keyboard has moved, `keyboard: up, 301 pt tall, its top at …; the key row ends 10 pt above it`. `-SillAXDump ` lists the stream screen's accessibility elements in the tree's order (VoiceOver's), on a private simulator once `xcrun simctl spawn UDID defaults write com.apple.Accessibility ApplicationAccessibilityEnabled -bool true`. A hardware keyboard (docs/hardware-keyboard-plan.md): a simulator's GameController always counts the simulator's own keyboard, so the stream's input view takes the keyboard by itself there (an empty input view: nothing drawn); `-SillHardwareKeyboard 1|0` says a keyboard is attached or not for the run instead. The console prints `keyboard: …` for what HardwareKeyboardPolicy does and `hwkey: usage 41 down via pressesBegan (none): to the Mac` (or `keyCommand`) for every hardware key the input view sees; `-SillKeyProbe 1` adds GameController's own view of each key, `-SillEscViaCommand 1` hands Esc on from pressesBegan so its key commands take it, and `-SillTurnAt [,…]` turns an iPhone's scene mid-session (iPadOS's windowing refuses). `Tests/keyrig` presses real keys and turns a wheel inside the simulator; after its keys UIKit there counts a hardware keyboard, so a fresh simulator is the one that shows the software keyboard. A test host's Desktop takes input only with `SILL_TEST_POINTER_PATH` (else it has nowhere to land). `-SillIdiom pad` draws a screen taller than wide and narrower than 600 pt as an iPad does (the compact halves) rather than as a phone, so an iPhone simulator photographs it (`phone` the other way round; the normal app too). `-SillInputTest 1`, with `-SillConnect` on this Mac's loopback: once, the portrait key row taps cmd, esc, shift and ctrl through its caps' action, then the trackpad checks that a touch at its centre lands on it, strokes, taps with the latched keys held and scrolls, through its own code, and in every layout the stream's input view sends Esc and ⇧Esc through its key command, a trackpad's scroll with its coast and a wheel's ("input test: …"); a synthetic host counts what it gets as `in.dry` and posts nothing, but a real host (Sill.app, SillHost without `--synthetic`) posts it on this Mac and the test does not look at which host it reached, so point it only at a synthetic host, or put a relay in front that reads the device's frames (the 14-byte header: kind, then the payload's length at bytes 10–13) and drops kind 8 (`sillrelay.py` passes everything). ## Compatibility floor The first public builds (Sill for iPhone and iPad from the App Store, Sill.app from GitHub) set it for good. Sill.app changes only when its user downloads a new version (its update check only points at one), and a device can stay on an old version (automatic updates off, an iOS the next version dropped). So every later host keeps serving devices from the first public build on, and every later device keeps working with Macs from the first public build on, or each says why (docs/update-notice-plan.md): - Kept as they are: the home door as Sill.app 1.0 ships it, TLS with pairing at home (docs/home-pairing-plan.md, branch `home-pairing`; it ships before 1.0, Noah 2026-09-25), not the plain-TCP `_sill._tcp` door, which only development builds and the CLI without `--pairing` keep; the ALPNs `sill/1` and `sill-pair/1`, which every host offers for good at both doors (`RemoteTLS.serverALPNs`: a later generation's `sill/2` only ever goes beside them); the 14-byte header; kinds 0–28 and 32 and their payloads (HEVC with ParameterSets; the JSON of Switcher, Input, Viewport, HostSettings, Remote, Compatibility, Pointer, MacMenu, Gesture and Clipboard; 29 to 31 stay free, 29 the audio plan's, 30 and 31 pull request #66's), inside `sill/1` at both doors (19 and 20 on their own `sill-pair/1` connection, the next bullet); the ping echo; a kind 16 within 2 s of the first window list, one per connection since Sill for Mac 0.5.1 (away from home, docs/remote-bundle-plan.md §4): its `settings` the pair that connection's controls set (the away quality for a device away), and its optional `away` (AwayQuality, from a host with an identity: Sill.app, SillHost --remote or --pairing) and `link` (LinkReport, only while a link does not keep up; its `state` a string, which a device reads as keeping up when it does not know it); kind 22's `reason`, `message` and `reconnect`; the Mac's menus as MacMenu.swift has them (docs/menu-bar-plan.md §3): a kind 27 without an id is the device's subscription, and only a subscriber is served (another connection's fetches and choices are answered "The menus changed…", nothing read or pressed); an id is a path of child indexes that names one item for every device within one version, and a request of another version is answered with that note and never acted on; a fetch and a choice carry the title the device showed, and the host reads or presses only while the item there still has it; a device's tokens come back in `answering`; the fields as they are (MacMenu's `version`, `app`, `bundleID`, `menus`, `answering`, `menu`, `items`, `more`, `pressed`, `stale`, `note`; MacMenuItem's `id`, `title`, `separator`, `enabled`, `mark`, `key`, `submenu`; FetchMenu's and PressMenuItem's `version`, `id`, `title`, `token`); a trackpad gesture (kind 28, Gesture.swift) goes only to a host whose window list says `gestures` 1 or more, as one of its six names with `fingers`, and a later generation of gestures says 2 and goes only to a host that says so; the clipboard (kind 32, Clipboard.swift, docs/clipboard-plan.md) goes from a device only to a host whose window list says `clipboard` 1 or more (an Int: 1 is text, a later generation with images says 2), and from a host only to a connection that sent a kind 32 and no `stop` since: any kind 32 subscribes, `{"stop":true}` ends it, `text` (at most 256 KB, the JSON at most 1 MiB) becomes the receiver's clipboard, `image` is reserved for that later generation; a subscription says what the device takes, `{}` being text alone, so a host sends `image` only to a connection whose subscription carries a later field saying it takes images (`"accepts":2`, say), never to one that sent `{}`. - Pairing as 1.0 does it (docs/home-pairing-plan.md §3): the Bonjour TXT keys `r` (the recognition tag) and `p` ("1" pairing required, "0" open, no `p` a plain door, any other value read as "1"); a `sill-pair/1` connection is exactly one kind 19, with no hello (the device gate judges sessions only), and one kind 20, then the Mac closes it; kind 19's methods `qr`, `code` and, at the home door, `ask` and `cancel` (the remote door answers both `closed`), at `v` 1: a host answers any other `v` `closed` without counting a try, so a later method or proof format comes with a later `v`, never as a new method at 1, which a 1.0 host counts as a wrong code; kind 20's reasons `code`, `closed`, `expired`, `stopped`, `busy` and, after an ask, `shown`, `openOnMac` and `locked`, with their meanings (`PairResult.knownReasons`: a later reason comes with its `message`, which 1.0 devices show as it is); an ok without a proof only as `method` "cable", to an ask that claimed the cable; and pairing is never refused for the device's age. - Additive only (HostSettings.swift's rules): new fields optional, never renamed or retyped; kind numbers never reused; no new case in an enum an older peer decodes. `StreamSource` keeps its three cases (a new source goes in an optional field, with `active` still one of the three). A new `InputEvent` case, scroll phase or window command goes only to a host that said it takes it (a kind or a field only newer hosts send), and a gesture ends with a scroll phase the first build knows. - A device is refused, never served wrong. A host that can no longer serve older devices raises `DeviceGate.minimumDeviceVersion` ("0" today) by the plan's §4.6, and they get kind 22 "update" before anything else. A device from 2026-09-25 on that receives it shows the host's message word for word, with its App Store link, and does not reconnect; older development builds cannot. At the TLS home door only devices from home pairing on receive it (Decided, below). Kind 22 reaches a 1.0 device only inside a `sill/1` session, so such a host still completes that device's `sill/1` handshake and reads its hello through the gate before refusing it: a host that stopped offering `sill/1` would leave it a failed handshake (-9838), "disconnected" and a redial every few seconds, for ever. - Every device says hello first (kind 23: its version, build, protocol and name), and every host's window list gives its version and protocol (`hostVersion`, nil from SillHost and from Macs before 2026-09-25). A later device facing an older Mac tells what it lacks from these and from which kinds and fields arrive, and says "Update Sill on ‹Mac›", as the Settings panel already does for a Mac without kind 16. - `SillProtocol.current` (1) rises only with a change an older peer cannot skip, and the floor rises with it. - Decided (Noah, 2026-09-25; the plan's open question 14): home pairing ships before 1.0, so the floor is the TLS home door with pairing, and no 1.0 device speaks plain TCP to Sill.app. Settled at the merge of main into `home-pairing` (2026-09-27, d11aa60; home pairing landed second): the hello goes first inside TLS at both doors, and the device gate runs in `Door`, once the key is admitted, one place for both TLS doors (`StreamServer.gate`; the CLI's plain door without `--pairing` runs it at `.ready`); what changed while it held a session (the key removed, Require pairing on, Remote Access or internet access off, the session limit, Direct Wireless off) is judged again by `DoorPolicy.afterGate`, with the goodbye and the line that change's own close gives. 1.0's TLS home door is `SillProtocol` 1 with ALPN `sill/1`, as the remote door is: pairing on the home door is part of protocol 1, no longer an example of what raises it (Compatibility.swift, docs/update-notice-plan.md §3.2 and §4.6), and a later generation raises both: `sill/2` is offered beside `sill/1` and `sill-pair/1`, never instead of them (the first bullet). Builds from before home pairing dial plain TCP and say hello in plaintext: at the TLS door they get a failed handshake and EOF, never kind 22 "update", and redial. Only development and TestFlight builds are that old, so no plaintext path or sniffer answers them. ## Conventions - Swift, Apple frameworks only. No third-party dependencies unless a decision in `docs/BRIEF.md` says so. Zero operating costs is a hard rule. - Threading in the spike is by hand: capture queue → VT callback thread → network queue. Keep it explicit and commented rather than reaching for actors until the design settles. - HEVC, no B-frames, real-time mode. AV1 is out (no hardware encode on Apple silicon as far as we know). - Latency beats quality. Drop frames before queuing them. - Don't add features the current milestone doesn't need. ## Decisions already made (don't relitigate without asking Noah) - Free app, tip jar, open source. Not paid, not subscription. - iCloud auto-pairing (same Apple Account, Mac just appears) for milestone 5. Bonjour only for the spike. - Native feel is the bar: Flighty-level polish, iOS conventions, Duo layouts. - License: Apache-2.0, LICENSE since PR #15, kept when the repository went public on 2026-09-26 (paid plan is gone, so no GPL/CLA needed). - v1 out of scope: hole punching, multi-window, layout customization, audio.