Build the friends feature on top of the phase-2 control plane: you can
now befriend someone you've connected with and manage a contacts list.
- common/friends.rs: a persisted FriendStore in its own friends.toml
(kept out of config.toml so a headless --reconfigure can't clobber it,
same as identity.key). Friends are keyed by stable control EndpointId;
state is PendingOutgoing / PendingIncoming / Accepted. The handshake
transitions (on_friend_request → mutual-match detection, on_friend_
accept) are pure and unit-tested.
- gui/code.rs: the bootstrap. The GUI host wraps its share code as
`pixelpassF1:<control-id>.<ticket>` so a viewer learns the host's
stable id; unwrap is lenient, so a bare/CLI ticket still works (no
friend offer). The video/streaming path is untouched.
- presence service gains an outbound path (unbounded channel → per-msg
send tasks) and exposes our control id for wrapping codes.
- gui wiring: on connect, the viewer announces itself to the host with a
Hello (carrying our display name); the host replies once, so both ends
learn each other and an "Add friend" offer appears on the running
host/view screens. Incoming requests/accepts/declines fold into the
store with desktop notifications. New Friends screen (accept/decline/
remove, edit your display name, see your id) reachable from the menu,
which shows a pending-request count. New [gui] display_name setting,
seeded from $USER.
Verified: friends store + handshake transitions covered by unit tests
(7); code wrap/unwrap round-trips (4); the control loopback still passes;
the live GUI starts clean with the presence endpoint online. fmt +
clippy clean on both features; 41 gui + 8 headless tests pass. The full
two-party UX (connect → mutual add → persisted) wants a cross-machine
manual check, as usual.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A newer rustfmt wraps over-long match arms and call expressions that the
version main was last formatted with left on one line. Pure formatting,
no semantic change — split out so the friends-list feature commits stay
focused on real changes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New gui::theme module: a Theme is a curated semantic palette (backgrounds,
text, accent, button, and the status colours) that serialises to TOML with
#rrggbb hex colours and builds an egui::Visuals. Missing fields fall back to
the built-in Default Dark via #[serde(default)], so partial/hand-trimmed
files still load. Three built-ins ship (Default Dark, Catppuccin Mocha,
Catppuccin Latte); user themes live as *.toml in ~/.config/pixelpass/themes/
and a user file overrides a built-in of the same name. Adds a `theme` field
to the GUI config (default "Default Dark"). Zero new deps (toml + a few
lines of hex parsing). 6 unit tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a `show_qr` preference (default on) to GuiSettings, with a
checkbox in Settings and a corresponding gate on the host-screen render.
Persists to config.toml alongside the existing close-to-tray setting.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add a StatusNotifierItem tray (ksni — pure-Rust over the zbus stack
notify-rust already pulls; only new crate is the pastey macro helper).
The icon reflects host/viewer status via its tooltip and offers
Show / Quit; it runs on its own thread, channel-wired to the egui app.
Add a Settings screen with a persisted toggle 'keep running in the tray
when I close the window' (config.toml [gui] close_to_tray), defaulting
OFF so the close button quits as users expect. When ON, closing hides
to the tray on X11 / minimizes on Wayland (which has no protocol to hide
a toplevel) and keeps any live stream running. If no tray is present the
close behaves normally, so the window can never be stranded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
`cargo clippy --fix`: drop needless borrows in interactive.rs, remove an
unneeded `return`, and derive `Default` for `HostState` / the config struct
instead of hand-writing it. No behaviour change.
README: the GUI host screen now lists connected viewers with a Kick button
and notifies on join/leave — update the description, which still mentioned
only a "live viewer count".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
First-run host launch now offers a one-time upstream measurement
against speed.cloudflare.com/__up via ureq (~5 MB POST, ~5s). The
result lives at ~/.config/pixelpass/config.toml under [bandwidth]
and feeds the default --max-viewers calculation on subsequent runs.
Sticky semantics for the dialog:
- Unmeasured: first-run prompt (Run / Skip)
- Measured / Skipped: silent — never re-prompts
- Failed: ask again on next launch (Retry / give up → Skipped)
`pixelpass --reconfigure` re-runs the test unconditionally for users
whose connection has changed (new ISP, moved house, etc.).
--max-viewers is now Option<u32>. When unset, host startup loads the
saved measurement, runs recommended_max_viewers(safe_mbps, bitrate),
and surfaces the source in the banner: "max viewers : N (auto: X.X
Mbps measured upstream)" — or user-specified / default fallback.
User verified end-to-end on 2026-05-21 16:54 EDT: first-run dialog,
skip path, run path, --reconfigure refresh, and banner integration
all work as expected.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>