diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000..8248234 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,51 @@ +name: Bug report +description: Something behaves differently from how it is documented +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Please check **Known limits** in the README first. The 60 Hz ceiling, the + ~1920×1200 geometry ceiling, and protected video refusing to play are + properties of the private display API rather than defects, and are not + going to change. + + If the receiver cannot connect at all, the *It will not connect* + template asks better questions than this one. + - type: input + id: versions + attributes: + label: Versions + placeholder: "Mac 0.16.1, receiver 0.16.1, macOS 26.2, Windows 11" + validations: + required: true + - type: textarea + id: expected + attributes: + label: What did you expect to happen? + validations: + required: true + - type: textarea + id: actual + attributes: + label: What happened instead? + validations: + required: true + - type: textarea + id: steps + attributes: + label: How can it be reproduced? + description: > + Including what was on the second display at the time. An idle desktop + legitimately sends almost no frames, so "0 fps" on an empty screen is + correct rather than a fault. + validations: + required: true + - type: textarea + id: hud + attributes: + label: The receiver's HUD, if the stream was running + description: > + Press H to show it. The per-stage rows say far more than a description + of the symptom can. + render: text diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..b85310e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: true +contact_links: + - name: Why the app is unsigned, and what the warnings mean + url: https://github.com/nbkdoesntknowcoding/display-share/blob/main/docs/distribution.md + about: The macOS and SmartScreen warnings on first launch are expected and explained here. + - name: Security vulnerability + url: https://github.com/nbkdoesntknowcoding/display-share/security/advisories/new + about: Please report these privately rather than as a public issue. diff --git a/.github/ISSUE_TEMPLATE/connection.yml b/.github/ISSUE_TEMPLATE/connection.yml new file mode 100644 index 0000000..66fb438 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/connection.yml @@ -0,0 +1,76 @@ +name: It will not connect +description: The receiver cannot find the Mac, or connects and immediately drops +labels: ["connection"] +body: + - type: markdown + attributes: + value: | + Most connection reports come down to one of a handful of causes, and the + questions below are the ones that separate them. Answering them saves a + round trip each. + + **Two worth ruling out before filing**, because they look like faults and + are not: + + - **Is the Mac actually sharing?** Running is not the same as sharing. + The menu bar panel has to say it is sharing — if it says *Press Start + to begin*, the receiver has nothing to connect to. + - **Is another receiver already attached?** Only one at a time is + allowed. A second one is refused, and the receiver will keep retrying. + Close any other receiver, including a browser tab open on the Mac's + own viewer page. + - type: input + id: versions + attributes: + label: Versions + description: Both halves. The receiver shows its version on the connect card. + placeholder: "Mac 0.16.1, receiver 0.16.1" + validations: + required: true + - type: dropdown + id: stage + attributes: + label: How far does it get? + options: + - The receiver never lists the Mac at all + - It lists the Mac, but connecting fails + - It connects, then drops straight away + - It connects and then drops after a while + - It asks for a PIN and the PIN is refused + validations: + required: true + - type: textarea + id: message + attributes: + label: What does the receiver say, word for word? + description: > + The exact text matters. "Disconnected. Reconnecting…" and a named + refusal are different problems with different causes. + validations: + required: true + - type: textarea + id: mac-state + attributes: + label: What does the Mac's menu bar panel say? + placeholder: "Sharing, or Not sharing — and anything under it" + validations: + required: true + - type: dropdown + id: network + attributes: + label: How are the two machines connected? + options: + - Same Wi-Fi network + - Same router, one or both on Ethernet + - A cable straight between the two machines + - Different networks, or not sure + validations: + required: true + - type: textarea + id: extra + attributes: + label: Anything else + description: > + Guest or workplace Wi-Fi often blocks the discovery protocol, which + shows up as the Mac never being listed. Typing the Mac's address by + hand tells the two cases apart — please say whether that works. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..3bfa566 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,21 @@ +## What changes, and why + + + +## How it was verified + + + +- [ ] `xcodebuild -scheme DisplayShareCore -derivedDataPath ./.build test` (from `mac/`) +- [ ] `cargo test --manifest-path src-tauri/Cargo.toml` (from `windows/`) +- [ ] The `node --experimental-strip-types scripts/verify-*.mjs` checks (from `windows/`) +- [ ] Run against a real Mac and receiver + +## Anything a reviewer should be suspicious of + + diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..e5f25b0 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,152 @@ +# Working in this repository + +Notes for coding agents. [CONTRIBUTING.md](CONTRIBUTING.md) covers the licensing +rules; this covers how to build, what to run, and the specific ways this codebase +will mislead you if you assume the usual conventions. + +## The shape of it + +Two applications that talk over a documented wire protocol. + +| Path | What it is | +|---|---| +| `mac/DisplayShare/` | SwiftUI menu bar app — no window, `LSUIElement` | +| `mac/DisplayShareCore/` | capture, encode, transport, pairing, input. Most logic lives here | +| `mac/vd_helper/` | separate process that owns the `CGVirtualDisplay` | +| `mac/Shared/` | wire protocol and helper IPC, compiled into both | +| `windows/src/` | TypeScript frontend: WebCodecs decode, canvas, input | +| `windows/src-tauri/` | Rust backend that owns the WebSocket | +| `protocol/` | `SPEC.md` and golden vectors | + +## Four things that will catch you out + +**The Xcode project is generated.** Edit `mac/project.yml`, then run +`xcodegen generate`. Changes made directly to `DisplayShare.xcodeproj` are +overwritten, including new files — a test file added only to the `.xcodeproj` +silently never runs. + +**The protocol has golden vectors.** The Swift and TypeScript parsers are each +tested against the same bytes in `protocol/vectors/`, never against each other, +so a shared misunderstanding cannot pass. Changing the wire format means +revising `protocol/SPEC.md` and both implementations. Do not add a field to the +header for convenience. + +**macOS permissions cannot be granted by a script.** Screen Recording and +Accessibility are granted by a human in System Settings, and a freshly built +copy is a *new identity* even if permission was granted to a previous build. If +a task needs them, stop and ask. There is no API, entitlement or `sudo` that +does it. + +**`mac/build/` is hundreds of megabytes** and must never be committed. It is +ignored; do not add it back with `git add -f`. + +## Commands + +```bash +# Mac +brew install xcodegen +cd mac && xcodegen generate +xcodebuild -scheme DisplayShareCore -derivedDataPath ./.build test + +# Windows receiver — the Rust half builds and tests on macOS too +cd windows && npm ci +cargo test --manifest-path src-tauri/Cargo.toml +npm run build + +# Frontend checks, all run in CI +node scripts/verify-vectors.mjs +node --experimental-strip-types scripts/verify-window-states.mjs +node --experimental-strip-types scripts/verify-timing.mjs +node --experimental-strip-types scripts/verify-refusals.mjs +``` + +## How this codebase tests things that need two machines + +Almost every defect that ever reached a user here passed a green build, because +the failing path needed a Mac, a receiver and a network to reach. The response +has been consistent, and new work should follow it: + +**Extract the decision as a pure value, then assert it.** `SendGate` decides +what to shed without a socket. `Cadence` picks a frame rate without a display. +`PopoverLayout.ordered` returns the sections without a view. `DisplayPlacement` +answers which edge without a second monitor. Frontend equivalents live in +`windows/src/*.ts` with a `scripts/verify-*.mjs` runner wired into both CI jobs. + +**Then break it on purpose.** An assertion that still passes with the fix +reverted is worth nothing, and this project has written a few. Delete the guard, +watch a named test fail, put it back. If nothing fails, the test is decoration — +say so rather than keeping it. + +## The failure this project keeps repeating + +Worth knowing before writing anything user-facing, because it has shipped +repeatedly in different clothes: **the app knows a specific fact and displays a +generic one.** + +A refused session showed "Reconnecting…". A display placed to the left said +nothing at all. An idle desktop was read as a dead capture and as a congested +link, and both times something was restarted or degraded for it. In every case +the information existed and the interface declined to say it, and in every case +the user reasonably concluded the app was broken — because from outside there is +no difference. + +When adding a state, the question is not "what should this show" but "what does +the app already know here that it is not saying". + +## Setting it up on a machine + +
+A prompt that knows where the human steps are + +```text +Set up Display Share on this machine. It turns a Windows laptop into a real +second display for a Mac. Repo: https://github.com/nbkdoesntknowcoding/display-share + +MAC SENDER (only runs on macOS 14+): +1. git clone https://github.com/nbkdoesntknowcoding/display-share.git && cd display-share +2. Run ./install.sh and show me its output. It installs xcodegen if needed, + builds a Release build, and installs to /Applications. +3. If it fails, read the build log path it prints and fix or report the error. + Do NOT skip failures or fall back to a Debug build. + +THEN STOP AND ASK ME. You cannot do the next part: +macOS permissions are granted by a human in System Settings — no command, +script, or API can grant them, and a freshly built copy counts as a NEW app +identity even if I granted them before. Tell me to: + - System Settings > Privacy & Security > Screen Recording > enable Display Share + - (only if I want to control the Mac from the laptop) + Privacy & Security > Accessibility > enable Display Share +Then wait for me to confirm. + +VERIFY (after I confirm): +4. Launch /Applications/DisplayShare.app. It is a MENU BAR app — no Dock icon, + no window. Click its icon and press Start. +5. Confirm a virtual display exists: `system_profiler SPDisplaysDataType | grep -i display` + should show one more display than the physical monitors. +6. Open http://localhost:8787 in a browser on the Mac. Expect a black canvas and + a HUD. It stays black until something is actually on that display — drag a + window onto the new display, then the HUD should show frames arriving. + A near-black screen with capture near 0 fps is CORRECT for an empty desktop. + NOTE: this browser page counts as a receiver, and only one is allowed at a + time. Close it before connecting the real receiver, or that one is refused. + +WINDOWS RECEIVER (run this part on the Windows laptop): +7. Install Rust (https://rustup.rs) and Node 22+. +8. cd windows && npm ci && npx tauri build +9. Run the installer from windows/src-tauri/target/release/bundle/nsis/ + SmartScreen will warn because it is unsigned: More info > Run anyway. +10. The app finds the Mac over Bonjour. Enter the 4-digit PIN the Mac shows. + Both machines must be on the same network, on 5 GHz Wi-Fi or Ethernet. + +USEFUL TO KNOW: +- Ports 8787 and 8788 must not be blocked. +- Keys in the receiver: F fullscreen, H toggle HUD, K force a keyframe, + A cycle decode mode, F8 forward input to the Mac. +- macOS decides where the second display goes and usually puts it on the LEFT. + Push the cursor that way to reach it; System Settings > Displays moves it. +- Read README.md "Known limits" before reporting a bug — the 60 Hz cap, the + ~1920x1200 geometry ceiling and no-HDCP are properties of Apple's private + API, not defects. +``` + +
diff --git a/README.md b/README.md index 11ebeda..e25cf9d 100644 --- a/README.md +++ b/README.md @@ -159,64 +159,11 @@ xcodebuild -scheme DisplayShare -configuration Release -derivedDataPath ./.build cd windows && npm ci && npx tauri build ``` -
-Or hand it to a coding agent — a prompt that knows where the human steps are - -Paste this into Claude Code, Cursor, or any agent with shell access. The -important part is that it stops before the permission grant, because no script -can grant macOS permissions: - -````text -Set up Display Share on this machine. It turns a Windows laptop into a real -second display for a Mac. Repo: https://github.com/nbkdoesntknowcoding/display-share - -MAC SENDER (only runs on macOS 14+): -1. git clone https://github.com/nbkdoesntknowcoding/display-share.git && cd display-share -2. Run ./install.sh and show me its output. It installs xcodegen if needed, - builds a Release build, and installs to /Applications. -3. If it fails, read the build log path it prints and fix or report the error. - Do NOT skip failures or fall back to a Debug build. - -THEN STOP AND ASK ME. You cannot do the next part: -macOS permissions are granted by a human in System Settings — no command, -script, or API can grant them, and a freshly built copy counts as a NEW app -identity even if I granted them before. Tell me to: - - System Settings > Privacy & Security > Screen Recording > enable Display Share - - (only if I want to control the Mac from the laptop) - Privacy & Security > Accessibility > enable Display Share -Then wait for me to confirm. - -VERIFY (after I confirm): -4. Launch /Applications/DisplayShare.app. It is a MENU BAR app — no Dock icon, - no window. Click its icon and press Start. -5. Confirm a virtual display exists: `system_profiler SPDisplaysDataType | grep -i display` - should show one more display than the physical monitors. -6. Open http://localhost:8787 in a browser on the Mac. Expect a black canvas and - a HUD. It stays black until something is actually on that display — drag a - window onto the new display, then the HUD should show ~58 fps. - A near-black screen with capture near 0 fps is CORRECT for an empty desktop - — that is not a bug. - -WINDOWS RECEIVER (run this part on the Windows laptop): -7. Install Rust (https://rustup.rs) and Node 22+. -8. cd windows && npm ci && npx tauri build -9. Run the installer from windows/src-tauri/target/release/bundle/nsis/ - SmartScreen will warn because it is unsigned: More info > Run anyway. -10. The app finds the Mac over Bonjour. Enter the 4-digit PIN the Mac shows. - Both machines must be on the same network, on 5 GHz Wi-Fi or Ethernet. - -USEFUL TO KNOW: -- Ports 8787 (viewer page) and 8788 (video + control) must not be blocked. -- Keys in the receiver: F fullscreen, H toggle HUD, K force a keyframe, - A cycle decode mode, F8 forward input to the Mac. -- Read README.md "Known limits" before reporting a bug — the 60 Hz cap, the - ~1920x1200 geometry ceiling and no-HDCP are properties of Apple's private - API, not defects. -- If anything is ambiguous, read docs/distribution.md and protocol/SPEC.md - rather than guessing. -```` - -
+### Or hand it to a coding agent + +[AGENTS.md](AGENTS.md) has a setup prompt written for agents with shell access. +It stops before the permission grant, because no script can grant macOS +permissions — that part is always a human in System Settings. --- @@ -538,8 +485,9 @@ see [What isn't proven yet](#what-isnt-proven-yet). A HUD screenshot from a real Mac-to-Windows session over a real LAN would settle several open questions at once. -Found a security issue? Please open a private advisory through GitHub's -**Security** tab rather than a public issue. +Found a security issue? [SECURITY.md](SECURITY.md) says how to report it +privately, and what counts — the unsigned binaries and the private API use are +deliberate, documented trade-offs rather than vulnerabilities. --- diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..1091023 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,56 @@ +# Security + +## Reporting + +Please report vulnerabilities **privately**, through this repository's +**Security** tab → *Report a vulnerability*. That opens a draft advisory only +maintainers can see. + +Please do not open a public issue for anything in the "in scope" list below. + +This is a small project with no security team and no paid support. A realistic +expectation is a first reply within a week. If something is being actively +exploited, say so in the title and it will be looked at sooner. + +## In scope + +Display Share puts a video stream and an input channel on your local network, so +the interesting failures are about who is allowed to reach them: + +- **Pairing bypass** — receiving video, or having input accepted, without + completing the PIN exchange. Input forwarding in particular is refused until a + device is paired, and a way around that is the highest-severity report here. +- **Token handling** — pairing tokens are stored hashed. A way to recover a + usable token from what is written to disk, or to replay one from another + device, is in scope. +- **Input injection** — the Mac injects `CGEvent`s on behalf of a paired + receiver. Anything that lets an unpaired party drive that is in scope. +- **The helper process** — `vd_helper` holds the virtual display and speaks a + local IPC protocol. Anything that lets an unrelated local process drive it, or + escalate through it, is in scope. +- **Path handling in the updater** — both apps download release assets. Anything + that writes outside the intended location is in scope. + +## Not vulnerabilities + +Stated plainly, because these get reported and they are deliberate: + +- **The binaries are unsigned.** Display Share is open source and does not buy + code signing certificates, so macOS and SmartScreen both warn on first launch. + That is a known, documented trade — see [docs/distribution.md](docs/distribution.md). + Build it yourself and the warning does not appear. +- **It uses a private Apple API.** `CGVirtualDisplay` is how a real virtual + display is created at all. It is the project's central technique, not an + oversight, and it is why this cannot ship on the Mac App Store. +- **It records the screen.** It captures the virtual display it creates, and + macOS shows the purple recording indicator exactly as it does for any other + capture app. That indicator appearing is correct behaviour. +- **Traffic on your LAN is not encrypted.** The stream is plain WebSocket on the + local network, gated by pairing. If your threat model includes someone on your + own LAN reading it, this is not the tool for you today — but a report saying + so is a feature request, and a welcome one, rather than a vulnerability. + +## Scope of the code + +Only this repository. The reference projects named in the README are separate +and should be reported to their own maintainers.