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>
gui::run() dropped the parsed --relay value, so a relay chosen on the
GUI command line (pixelpass --gui --relay URL) never reached the
headless host/viewer children -- only the PIXELPASS_RELAY env-var form
propagated (via inheritance). Thread the flag into the app and append
--relay <url> to both child arg vectors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
common::output documents the contract: JSON events on stdout, banner + tracing
on stderr, so a parser reading stdout sees only events. But init_tracing relied
on tracing_subscriber::fmt()'s default writer, which is stdout — so every log
line was interleaved into the JSON event stream the --gui front-end parses.
The GUI tolerated it (non-JSON lines are skipped), but two real consequences:
a tracing write could corrupt a JSON event line intermittently, and all
diagnostics landed on stdout where the GUI discards them — leaving its
stderr-tail ring empty, so a failed host/viewer child surfaced no clue in the
window. Pin the fmt writer to stderr. Verified: every stdout line now parses as
JSON; iroh/tracing output appears on stderr.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds the opt-in graphical front-end (pixelpass --gui), default-off via the
`gui` cargo feature so the headless build never pulls the toolkit tree.
eframe 0.34 on the glow/OpenGL backend (no wgpu); 69 feature-gated crates,
vetted. --gui on a headless build errors with a rebuild hint.
This commit is just the shell: a window with a Host/View menu and back
navigation. The shell-out child-spawning + JSON event parsing that drives
real host/viewer controls come next. Window verified to open and render
cleanly on Wayland (glow).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds common/output.rs: a process-global JSON-lines emitter for
non-interactive front-ends. With --output json, host and viewer emit one
JSON object per line on stdout (ticket, host_info, viewer_count, capture
start/stop, viewer_refused, connected), flushed per line; the human banner
and tracing logs stay on stderr so the two never interleave. No-op when the
flag is absent, so call sites emit unconditionally.
This is the shell-out counterpart to an in-process event channel: the
upcoming --gui front-end re-execs this binary as `pixelpass --host
--output json` and parses these lines to drive its window. serde_json was
already in the tree from the bandwidth pre-flight.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Hosting was only reachable through the interactive dialoguer menu; there
was no way to start a host non-interactively. Add a --host flag that runs
host::run directly (interactive=false), bypassing the menu. Useful for
scripting and required by the upcoming --gui front-end, which drives this
binary as a child process. Guards against --host + ticket (contradictory).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When opts.app is set, a dedicated OS thread runs a libpipewire
MainLoop, subscribes to the registry, and writes target.object to
the "default" metadata so WirePlumber reroutes matching streams to
our per-PID null-sink. Activation is now opts.app.is_some() OR the
existing PIXELPASS_AUDIO_VIA_NULL_SINK env var (kept for
no-filter dogfooding).
Threading: tokio side spawns a std::thread; the two sides bridge via
pipewire::channel for cmd→thread (Shutdown) and tokio::sync::mpsc
for event→tokio (FirstRoutedStream). Cross-thread quit goes through
the libpipewire channel so MainLoop is only mutated from its own
thread. Shutdown clears target.object on every routed stream before
quitting so WirePlumber doesn't log orphans.
Routing decisions:
- Filter is case-insensitive equality on application.name (predictable;
no surprise matches from substring).
- target.object is written as Spa:Id with the sink's object.serial.
- Default-sink loopback stays loaded until the first stream is
actually routed — avoids viewer silence if the user picks an app
that isn't producing sound yet. On first route, the event task
takes() the loopback module ID and unloads it.
Session 2 picker explainer + (app pick saved: ...) banner softening
both removed; banner is back to plain app-audio=NAME.
Verified end-to-end cross-machine: desktop host with Strawberry
selected, laptop viewer over mpv. Strawberry audible on the laptop;
YouTube playback started on the desktop was NOT audible on the
laptop. Routing isolates the filtered app.
Session 4 still open: recreate loopback when the last filtered stream
disappears (avoid silence), handle app-disappears-mid-session,
multi-instance, --repair coupling for orphan sink cleanup.
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>
Bare `pixelpass` now opens a dialoguer-driven Host/View menu instead of
going straight to host mode. Host path copies the ticket to the system
clipboard via arboard with silent print-only fallback. View path
prompts for the ticket, then after the local listener binds prompts
mpv-vs-VLC and spawns it detached (setsid + null stdio) so the player
survives pixelpass exiting.
Headless invocations (`pixelpass <ticket>`, `pixelpass --repair`)
unchanged. Per spec at ~/Documents/pixelpass-interactive-mode-spec.md.