Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .github/ISSUE_TEMPLATE/bug.yml
Original file line number Diff line number Diff line change
@@ -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
8 changes: 8 additions & 0 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -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.
76 changes: 76 additions & 0 deletions .github/ISSUE_TEMPLATE/connection.yml
Original file line number Diff line number Diff line change
@@ -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.
21 changes: 21 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
## What changes, and why

<!-- The reasoning matters more than the diff here. If a comment in the code
would not explain this to someone in six months, the description should. -->

## How it was verified

<!-- Please be specific, and say plainly what was NOT verified. A change that
builds and passes tests but was never run is a normal thing to submit —
saying so is what makes it safe to review. -->

- [ ] `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

<!-- Assertions that might not fail if the fix were reverted are worth calling
out. This project has shipped a few, and the habit of tamper-testing them —
break the fix, watch a test fail — has caught more than one. -->
152 changes: 152 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -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

<details>
<summary>A prompt that knows where the human steps are</summary>

```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.
```

</details>
68 changes: 8 additions & 60 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,64 +159,11 @@ xcodebuild -scheme DisplayShare -configuration Release -derivedDataPath ./.build
cd windows && npm ci && npx tauri build
```

<details>
<summary><b>Or hand it to a coding agent</b> — a prompt that knows where the human steps are</summary>

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.
````

</details>
### 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.

---

Expand Down Expand Up @@ -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.

---

Expand Down
Loading
Loading