Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
261fdbb
Keep test runs out of the real BroLink data directory
MrBeldum Sep 30, 2026
9caed05
Answer only the same Tailscale account on the control API again
MrBeldum Sep 30, 2026
bb18995
Carry copies from Mac and Linux hosts to the viewer
MrBeldum Sep 30, 2026
928a33b
Keep the old host when a pushed update dies on start
MrBeldum Sep 30, 2026
cdd9842
Try each resolved address when connecting, not only the first
MrBeldum Sep 30, 2026
5784ac1
Name the right licence in the Windows executable's version block
MrBeldum Sep 30, 2026
f907166
Stop running a program for every status request on Mac and Linux
MrBeldum Sep 30, 2026
8abc71b
Make VideoToolbox's frame callback unable to outlive its target
MrBeldum Sep 30, 2026
5a1c340
Stop rewriting the engine helpers every 30 seconds on a silent PC
MrBeldum Sep 30, 2026
43846ab
Let Cancel stop the pairing retries, and name the codec actually used
MrBeldum Sep 30, 2026
b310ee5
Remove API that nothing outside the tests called
MrBeldum Sep 30, 2026
c46bc31
Drop the path argument that Auto quality never read
MrBeldum Sep 30, 2026
02a0fba
Keep the adapter name out of double-quoted strings in setup
MrBeldum Sep 30, 2026
dc5ee54
Parse every PowerShell script in CI, and test the whole workspace
MrBeldum Sep 30, 2026
e4f71ef
Compile only the moonlight-common-c sources BroLink uses, and say why
MrBeldum Sep 30, 2026
c19a220
Drop the stream crate's unused serde dependency
MrBeldum Sep 30, 2026
af6933e
End the audio output thread when the player goes, not on a timer
MrBeldum Sep 30, 2026
f158641
Redraw an idle window once a second, and only on news from the service
MrBeldum Sep 30, 2026
9dd9da7
Bring the docs up to date with what has shipped
MrBeldum Sep 30, 2026
949a670
Restart the Mac's background service when a script installs the app
MrBeldum Sep 30, 2026
afc6857
Check for a black picture in vector-sized steps
MrBeldum Sep 30, 2026
356f458
Render each icon size once
MrBeldum Sep 30, 2026
107f486
Bundle only the unified app
MrBeldum Sep 30, 2026
66d4aa8
Compile the clipboard sequence helper only where it is used
MrBeldum Sep 30, 2026
68a190f
Redesign the window around one design system
MrBeldum Sep 30, 2026
d35496f
Keep one copy of the tailnet check, the PowerShell probe and window s…
MrBeldum Sep 30, 2026
f4fcad7
Drop the widgets no screen uses
MrBeldum Sep 30, 2026
f70d2ce
Stop telling Windows and Linux viewers about "this Mac"
MrBeldum Sep 30, 2026
0715679
Run the release updater only on the Mac
MrBeldum Sep 30, 2026
ca87bcf
Tab through each row's controls in the order they read
MrBeldum Sep 30, 2026
0212d9a
Accept a Tailscale IPv6 address in the control API's Host header
MrBeldum Sep 30, 2026
4813cc6
Describe the tabs, the Sharing page and the new toolbar in the docs
MrBeldum Sep 30, 2026
181fac3
Make "Turn off HDR" the first thing a black picture offers
MrBeldum Sep 30, 2026
beef878
Serve the handover download from a blocking socket
MrBeldum Sep 30, 2026
d6433d6
Pass the user's data folder to elevated setup as one argument
MrBeldum Sep 30, 2026
1a8aa5b
Stop listing phones as machines missing BroLink
MrBeldum Sep 30, 2026
6a7ff9b
Log the NAT when it changes, not when the nearest relay does
MrBeldum Sep 30, 2026
4488f53
Log a peer once, not every time its whois answer is refreshed
MrBeldum Sep 30, 2026
cb2a282
Write down the UI audit and the design system it led to
MrBeldum Sep 30, 2026
4c07790
Say plainly when setup is done, and keep Mac power hints to what works
MrBeldum Sep 30, 2026
53daa01
Name the output device sound actually moved to
MrBeldum Sep 30, 2026
a84dcec
Count a pushed host that is already on the release as updated
MrBeldum Sep 30, 2026
ed251f6
Log a failing whois once, not at every request
MrBeldum Sep 30, 2026
b68c802
Give the PC's display report a minute to arrive
MrBeldum Sep 30, 2026
5191f92
Leave the PC as found after the live stream test, and note the speed-ups
MrBeldum Sep 30, 2026
30f2e4a
Word the service's log for any viewer, and name who asked
MrBeldum Sep 30, 2026
92eaaa6
Redraw the window from discovery only when the listing changes
MrBeldum Sep 30, 2026
9525449
Merge the engineering pass
MrBeldum Sep 30, 2026
f6b8c75
Merge the UI redesign
MrBeldum Sep 30, 2026
2fa888f
Stop re-sending a host update the PC rolled back, and say so
MrBeldum Sep 30, 2026
d69d3e0
Redraw what the merged branches left stale, and tidy three edges
MrBeldum Sep 30, 2026
3d021b6
Let a tagged machine answer people only, not other tagged machines
MrBeldum Sep 30, 2026
9f14348
Keep every menu inside the window at the smallest size
MrBeldum Sep 30, 2026
61de1b9
Line up a notice's actions with its text
MrBeldum Sep 30, 2026
58ff516
Merge the design follow-ups: menus inside the window, aligned notice …
MrBeldum Sep 30, 2026
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
7 changes: 6 additions & 1 deletion .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# audiopus_sys ships a libopus whose CMake files predate CMake 4.
[env]
# audiopus_sys ships a libopus whose CMake files predate CMake 4.
CMAKE_POLICY_VERSION_MINIMUM = "3.5"
# `cargo test` and `cargo run` keep settings, identities and logs under
# target/ rather than in the real per-user data directory, so a test run
# never reads or rewrites an installed BroLink's files. A shipped binary
# does not see this; see `brolink_core::config::data_dir`.
BROLINK_DATA_DIR = { value = "target/brolink-data", relative = true }

# The host is a standalone exe. Without this it needs vcruntime140.dll,
# which is not inbox on a fresh Windows install.
Expand Down
38 changes: 33 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,34 @@ jobs:
run: cargo clippy --workspace --all-targets -- -D warnings
- name: test
run: cargo test --workspace
# The setup script is generated, so only a dump of it can be parsed.
# Every plan shape, with names that need quoting, through the parser
# of the Windows PowerShell that runs it; plus the shipped scripts.
- name: PowerShell scripts parse
shell: powershell
run: |
$ErrorActionPreference = 'Stop'
$env:BROLINK_DUMP_EXE = "C:\Users\O'Brien `$lab\AppData\Local\BroLink\brolink-host.exe"
$env:BROLINK_DUMP_ADAPTER = "Ethernet 'Realtek' `"2`" `$(x)"
$env:BROLINK_DUMP_ADAPTER_DESC = "Realtek PCIe GbE (`"x`")"
$files = @('scripts\install-host.ps1', 'crates\host\windows\take-over-engine.ps1')
foreach ($migrate in '0', '1') {
foreach ($dry in '0', '1') {
$out = Join-Path $env:RUNNER_TEMP "setup-$migrate$dry.ps1"
$env:BROLINK_DUMP_SETUP_SCRIPT = $out
$env:BROLINK_DUMP_MIGRATE = $migrate
$env:BROLINK_DUMP_DRY_RUN = $dry
cargo test -q -p brolink-host --lib the_generated_script_can_be_dumped_for_a_syntax_check
if ($LASTEXITCODE -ne 0) { throw "dumping the setup script failed" }
$files += $out
}
}
foreach ($f in $files) {
$errors = $null
[System.Management.Automation.Language.Parser]::ParseFile((Resolve-Path $f), [ref]$null, [ref]$errors) | Out-Null
if ($errors.Count -gt 0) { $errors | Format-List | Out-String | Write-Host; throw "$f does not parse" }
Write-Host "parses: $f"
}
- name: release build
run: cargo build --workspace --release
- uses: actions/upload-artifact@v4
Expand All @@ -42,9 +70,9 @@ jobs:
components: clippy
- uses: Swatinem/rust-cache@v2
- name: clippy
run: cargo clippy -p brolink-core -p brolink-stream -p brolink-client -p brolink-host --all-targets -- -D warnings
run: cargo clippy --workspace --all-targets -- -D warnings
- name: test
run: cargo test -p brolink-core -p brolink-stream -p brolink-client -p brolink-host
run: cargo test --workspace
- name: release app
run: cargo build --release -p brolink-host --target aarch64-apple-darwin
- name: bundle
Expand All @@ -59,11 +87,11 @@ jobs:
components: clippy
- uses: Swatinem/rust-cache@v2
- name: native deps
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends cmake clang nasm pkg-config libssl-dev libasound2-dev libpulse-dev libxkbcommon-dev libwayland-dev libx11-dev libxcursor-dev libxrandr-dev libxi-dev libx11-xcb-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends cmake clang nasm pkg-config libasound2-dev libxkbcommon-dev libwayland-dev libx11-dev libxcursor-dev libxrandr-dev libxi-dev libx11-xcb-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev
- name: clippy
run: cargo clippy -p brolink-core -p brolink-stream -p brolink-client -p brolink-host --all-targets -- -D warnings
run: cargo clippy --workspace --all-targets -- -D warnings
- name: test
run: cargo test -p brolink-core -p brolink-stream -p brolink-client -p brolink-host
run: cargo test --workspace
- name: release host
run: cargo build --release -p brolink-host

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
- uses: Swatinem/rust-cache@v2
# Never ship a binary whose tests were not run.
- name: test
run: cargo test -p brolink-core -p brolink-stream -p brolink-client -p brolink-host
run: cargo test --workspace
- name: build app
run: cargo build --release -p brolink-host --target aarch64-apple-darwin
# bundle-macos.sh ad-hoc signs and verifies the bundle itself.
Expand Down
133 changes: 133 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,139 @@ The release workflow publishes the section that matches the tag as the
GitHub release notes, so each version gets a heading of the form
`## X.Y.Z (date)`.

## Unreleased

- The window is redesigned around one design system, in the product
site's black, white and hairlines, set in Geist and Geist Mono. Colour
now means something: green for ready, amber for needs attention, red
for broken, and the logo's violet only for keyboard focus and
progress. Every text colour meets WCAG AA on every surface it sits on;
the old footer and captions did not.
- Tabs across the top: **Machines**, **Sharing** and **Settings**
(Command-1, 2 and Command-comma on a Mac; Ctrl elsewhere; Escape goes
back). The Sharing tab is new in the unified app: this machine's setup,
what it reports (Tailscale, streaming engine, network, Wake-on-LAN,
controllers), the devices paired with it, its options and its log.
Until now the unified window showed only a one-line summary and a
setup button; paired devices, the log and Stop service were
unreachable.
- The machine list shows each machine's state as a dot and one line,
with **Connect** (or **Wake and connect**) and a menu for Wake, Test
wake, Sleep, Restart, Shut down and Copy Tailscale address. Buttons
stay put and dim while a connection is in progress instead of
disappearing. This machine leads the list and opens its Sharing tab.
- Connecting says what is happening and what to expect; pairing shows
the PIN in boxes. A failure is titled "Couldn't connect to …" or
"… disconnected", with **Try again**, and a lighter profile is offered
only when the stream itself failed. The empty list, "looking",
Tailscale missing and Tailscale stopped each say what to do.
- Settings is grouped (Stream, Window and input, Power, Updates, Relay,
About) and the quality profile is one control: Recommended, Smooth,
Balanced, Sharp or Custom. Mouse capture is now a setting as well as a
toolbar choice. "Command acts as Ctrl" shows only on a Mac, and "Keep
this PC awake" only on Windows, where it does something.
- In a stream, the toolbar's menus look and behave the same, open
towards the window's middle, and fold into one **More** menu on narrow
windows. The host key is shown as keycaps (control option on a Mac,
Ctrl Alt elsewhere) when a stream starts and on the toolbar. The
connecting screen has a **Cancel** button, so leaving no longer needs
the host key. Stream settings and Stream performance are proper
panels with a close button; Escape closes Stream settings.
- Every menu, dropdown and panel fits the window down to its smallest
size (640×420). A menu opens upwards when there is more room above,
and scrolls, with its scroll bar showing, when neither side has room
for all of it; at the smallest size the stream toolbar's More menu cut
off "Command acts as Ctrl" and the PC's power items. Stream settings
and Stream performance stay below the toolbar, and Tab scrolls the
control it reaches into view.
- A notice's actions line up with its text: "Dismiss" sat 12 points to
the right of the words above it.
- A relayed path is amber, not red: it works, and Connection details
says how to get a direct one.
- The idle window no longer repaints on a timer (it redrew 2.5 times a
second forever), machine discovery redraws it only when the list
changes, and the Tailscale check that could start a process
every two seconds on the UI thread now runs on its own thread.
Unpairing and installing the controller driver no longer block the
window.
- The app is about 470 KB smaller (11.7 MB for the macOS release
binary): Geist's five cuts replace Inter's three.
- The control service again answers only machines signed in to the same
Tailscale account. Since 4.0.0 it answered any machine Tailscale could
name, so a machine shared in from someone else's tailnet could pair with
the PC, sleep or restart it, read and write its clipboard, and push it a
new `brolink-host.exe`. A tagged server such as the relay VPS belongs to
no account, so it answers the members of its own tailnet, which keeps a
Mac able to open the VPS desktop.
- Text copied on a Mac or Linux machine you are streaming from now reaches
the machine you are watching on. Only Windows reported a clipboard
sequence number; everywhere else it was always 0, so the viewer took
every copy for the one it had already seen. On a Linux desktop, text
pasted in from the viewer also stays on the clipboard now instead of
vanishing when no clipboard manager picks it up.
- A Windows PC whose pushed update crashes on start keeps the version it
had. The service used to exit as soon as the new executable had been
launched, so a replacement that died straight away left the PC with no
BroLink service until someone logged in again. It now waits two seconds,
and puts the previous executable back if the new one has already exited.
The PC's log says so, and the Mac warns once instead of sending the same
release again every three minutes.
- Update downloads try each of a server's addresses in turn. Only the first
one the name resolved to was ever tried, so one unreachable address (an
IPv6 one on a network without IPv6, say) failed the whole update check.
- `brolink-host.exe`'s version details (Explorer's Details tab) named the
wrong licence, MIT; they now say GPL-3.0-or-later, the licence BroLink
and moonlight-common-c are under.
- Windows setup no longer breaks on a network adapter whose name holds a
`"` or a `$`. The name was written into a double-quoted PowerShell
string, where a `"` ends the string (a syntax error that stops the whole
setup) and a `$` expands. It is now only ever a single-quoted literal.
- The macOS install scripts restart the background service after putting
the new app in place. It kept running the old version until the next
login, so a Mac that shares its desktop served other machines from the
previous release.
- A Windows machine no longer runs the Mac's updater. Since 4.0 every
machine runs the same window, so a Windows PC also checked GitHub and
would try to send other Windows PCs their update with a Mac-only unzip,
failing every few minutes. Its settings now say that updates arrive
from the Mac.
- Installing BroLink Host through the stream (for a PC on 3.0) can finish
on a Mac. The Mac served the file from a socket that macOS left
non-blocking, so sending the executable stopped as soon as the network
buffer filled and the PC rejected the truncated download.
- **Share this machine** works on a Windows account whose folder has a
space in it ("C:\Users\Ada Lovelace"). The elevated setup was handed
that folder through PowerShell's Start-Process, which splits unquoted
arguments at spaces, so it received half a path and an unexpected
extra argument and failed.
- The service log keeps what matters. Every machine that polled the
service was logged again each minute ("hermes (…) asked" was 2,707 of
the 2,810 lines in this Mac's `service.log`), and every change of the
nearest Tailscale relay region logged a network line. Each peer is now
logged when first seen, when its answer changes or after an hour away,
and the network line only when the NAT itself changes.
- When the Mac's sound output changes mid-stream (AirPods connect, say),
the stream's stats name the device the sound moved to. macOS already
moved it; BroLink kept naming the old one and logged a warning.
- A host update that lands is no longer reported as a failure. A PC can
take the new executable and restart before its reply reaches the Mac;
the Mac then retried, heard "this PC already runs BroLink Host 4.0.2",
and showed "Could not update" in red. It now asks the PC which version
it runs before calling a push failed.
- Cancel stops a connection attempt straight away while the PC is not
answering. The pairing check retried eight times regardless, which could
hold the window on "Checking pairing…" for most of a minute after Cancel.
- A Windows or Linux viewer no longer calls its stream HEVC: those decode
H.264 only, and the toolbar named the codec the PC could offer instead
of the one in use.
- The check that notices a black picture costs the decoder 0.14 ms a frame
instead of 2.5 ms at 3024×1964 when the picture really is black (15% of
a core at 60 fps, on the path every frame takes to the screen).
- A Mac or Linux machine that shares its desktop no longer runs a program
(`scutil` or `hostname`) for every status request it answers.
- When the picture is black, the PC's explanation gets a minute to arrive
instead of 25 seconds, which its slower checks sometimes needed.

## 4.0.2 (2026-09-22)

- `panel.log` and `service.log` no longer grow without end. Both were
Expand Down
33 changes: 30 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,21 @@ cargo test --workspace
example `BROLINK_SKIP_C=1 cargo check -p brolink-client --target
aarch64-apple-darwin` from Windows after `rustup target add`.

`cargo test` and `cargo run` keep BroLink's settings, pairing identity and
logs in `target/brolink-data` (`.cargo/config.toml` sets
`BROLINK_DATA_DIR`), so neither touches an installed BroLink's files. To
point a development build at an installed one's data on purpose, set
`BROLINK_DATA_DIR` yourself, for example to `%LOCALAPPDATA%\BroLink` on
the development PC before `cargo run -p brolink-host -- --background`.

## Where things live

- `crates/stream`: everything that talks to Sunshine. `nvhttp` is the
pairing and launch API, `session` drives moonlight-common-c through the C
shim in `csrc/`, `video` and `audio` decode. It knows nothing about
windows or Tailscale.
windows or Tailscale. The vendored moonlight-common-c, what was trimmed
from it and its two local patches are described in
`third_party/moonlight-common-c/VERSION`.
- `crates/client`: the viewer UI (a library plus `brolink-client`).
`session.rs` is the path from a listed machine to a live stream;
`stream.rs` is the toolbar and input while streaming; `video.rs` draws
Expand All @@ -45,12 +54,24 @@ keep their meaning.
cargo test -p brolink-client -p brolink-host snapshots -- --ignored
```

renders every screen to `target/ui-snapshots/*.png`.
renders every screen to `target/ui-snapshots/*.png`: each state of the
machine list, Settings, the Sharing page and the stream overlay at the
minimum window (640×420), a typical one (1280×800) and a large one
(1920×1200), at 1× and 2× (`client-lobby-1280x800@2x.png`); `*-full.png`
show whole scrolling pages. The window is built headless with default
settings, so nothing reads or writes your real configuration. The same
states run unignored, checking that nothing runs past the window and no
two controls overlap, at the minimum and the large size.

Every colour, size and distance comes from `crates/ui/src/theme.rs`, and
every control from `crates/ui/src/widgets.rs`; a screen that needs
something new adds it there, with accesskit info and a focus ring.

## Tests against a real Sunshine

With Sunshine running on the development PC and BroLink Host's service
started (`cargo run -p brolink-host -- --background`):
started (`cargo run -p brolink-host -- --background`, pointed at the
installed data directory as above so it has the engine login):

```bash
cargo test -p brolink-stream pair_real -- --ignored --nocapture # five-phase pairing
Expand All @@ -74,6 +95,12 @@ BROLINK_TEST_PC='Gaming-PC' cargo test -p brolink-client stream_snapshot -- --ig
cargo test -p brolink-client decoded_frame_reaches -- --ignored
```

`stream_real` streams from another machine too: `BROLINK_TEST_IP` is its
Tailscale address, `BROLINK_TEST_DIR` a directory holding copies of the files in the
Mac's `identity/` directory and the PC's certificate as `server.der`, and
`BROLINK_TEST_WIDTH`, `_HEIGHT`, `_FPS`, `_BITRATE_KBPS`, `_SECONDS` and
`BROLINK_TEST_HEVC=1` shape the stream.

`BROLINK_TEST_CODEC=h264` or `hevc` selects the codec. A black capture fails
the live test by default; `BROLINK_TEST_EXPECT_BLACK=1` instead verifies the
persistent black-picture diagnosis when testing a PC without an active
Expand Down
3 changes: 0 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 8 additions & 4 deletions NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ Compiled into the client (crates/stream):
Includes ENet (MIT, Lee Salzman) and nanors (MIT, sleepybishop).
Opus, BSD-3-Clause, Xiph.Org Foundation; statically linked through the
audiopus_sys crate. https://opus-codec.org
OpenH264, BSD-2-Clause, Cisco Systems. Only the Windows development
client uses it, as a software H.264 decoder; the Mac uses VideoToolbox.
OpenH264, BSD-2-Clause, Cisco Systems. Only a Windows or Linux viewer
uses it, as a software H.264 decoder; the Mac uses VideoToolbox.
It is built from source, so Cisco's binary patent grant does not cover
it. https://github.com/cisco/openh264

Expand All @@ -34,5 +34,9 @@ Used when present, never redistributed:

Fonts:

Inter, SIL Open Font License 1.1, Rasmus Andersson and the Inter Project
Authors. crates/ui/assets/LICENSE-Inter.txt https://github.com/rsms/inter
Geist and Geist Mono, SIL Open Font License 1.1, The Geist Project
Authors (Vercel). Regular, Medium and SemiBold of Geist and Regular
and Medium of Geist Mono, unmodified, v1.7.2.
crates/ui/assets/LICENSE-Geist.txt https://github.com/vercel/geist-font
egui's fallback fonts: Hack (MIT), Ubuntu Light (Ubuntu Font Licence),
Noto Emoji (OFL 1.1) and emoji-icon-font (MIT), through epaint.
Loading
Loading