Third review round found three more blocking P2s, and they shared one root cause:
`pactl`'s text output cannot carry the guarantees repair was claiming. All three
are closed by talking to the protocol instead. New dependency taken with the
user's explicit sign-off after vetting.
**Record boundaries were unprovable.** `pactl list short modules` prints a
module's argument raw into a tab/newline-delimited format with no escaping. A
*genuine* module whose argument contains a newline renders a first line that reads
byte-exactly like one of our canonical forms, with the remainder dropped as an
unparseable continuation — no forged index, so the duplicate-index check could not
see it. Repair would have classified and unloaded a module it never saw in full.
**Field-confirmed on the live server**, because this needed no adversary: loading a
loopback whose argument is canonical-then-newline-then-`remix=false` (a real
loopback option) produces exactly that listing. A tab in the same position is
worse: it hid a sink reference from the gate that protects a still-referenced sink.
**Index and argument could be mis-paired.** The listing carrying exact arguments
(`-f json`) carries no index at all on pactl 17; the one carrying the index cannot
carry the argument faithfully. Correlating them by position — which the previous
commit did — is unsound whenever module names repeat: another client loading one
module and unloading another between the two calls leaves counts and names aligned
while every argument has shifted by one, so a foreign module inherits a canonical
fingerprint. The name check cannot see it and the retry never fires.
**Locality was a guess.** `PULSE_SERVER` is a fallback *list*, so
`unix:/missing tcp:remote:4713` passes any "starts with unix:" test and then
connects to another machine, where local pids mean nothing and a live remote host's
modules look dead. A remote server can also be selected by client config with the
variable unset entirely.
New `repair/introspect.rs` owns one verified-local connection: `pa_module_info`
gives index, name and exact argument in a single record, `pa_context_is_local()`
answers locality about the connection actually established, and unloading goes back
through that same connection so listing and destruction cannot disagree about which
server they mean. It holds no policy beyond refusing the wrong server; every
decision stays in the pure planner.
⚠️ **The field test caught a real bug that no unit test could have.** The first
version did its work correctly and then aborted on the way out:
Assertion '!e->dead' failed at ../pulseaudio/src/pulse/mainloop.c:207,
function mainloop_io_free(). Aborting.
SIGABRT, core dumped, exit 134 — a fully successful repair reporting failure to its
caller. Cause: Rust drops fields in declaration order and the context's teardown
frees IO events living in the mainloop, which I had declared first. **This is
exactly the invariant phase 0b exists for, met again one layer down.** Fixed, and
then hardened past the fix: `Drop` explicitly takes and destroys the context before
the mainloop, so the ordering no longer depends on where the fields are written.
Liveness keeps its `NSpid`/container checks but the claim is corrected: `NSpid > 1`
means "definitely nested", while `NSpid == 1` is NOT proof of the initial namespace
— its leftmost value is relative to the procfs that was mounted, so a nested
namespace with its own `/proc` reports one entry legitimately. These are negative
signals that fail closed, not a proof of trustworthy pids. Closing that properly
needs modules to carry an owner token (machine/boot plus pid-namespace identity),
which changes what pixelpass writes into the graph and how far back `--repair` can
clean up: recorded as a design decision, not guessed at.
libpulse-binding 2.30.1 vetted before use: MIT/Apache-2.0, 5.5M downloads, 3 new
crates total, build script does nothing but probe pkg-config, no network or
subprocess use anywhere in the sources, and all three historical RustSec advisories
(2018-0020, 2018-0021, 2019-0038) were fixed by 2.6.0. The reasoning is recorded in
Cargo.toml beside the dependency.
247 tests, clippy clean, fmt clean apart from the pre-existing taint/tests.rs:2683.
The text parser's tests are gone with the parser; the liveness probe keeps its own,
and the live field gates — A/B orphan removal, the reference/unrecognised fixture,
and the newline fixture — all pass with exit 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pixelpass invokes the GStreamer tools and pactl as subprocesses, not as
linked libraries, so dpkg-shlibdeps (`depends = "$auto"`) never sees them.
On a fresh Ubuntu host that means `deps::check_host_binaries` bails before
the host emits its ticket — peerspeak then reports the generic "pixelpass
host exited before emitting a ticket" (first 2-human field hit, 2026-06-26).
List the runtime stack explicitly so `apt install ./pixelpass.deb` pulls in
gstreamer1.0-{tools,plugins-base,plugins-good,plugins-bad,plugins-ugly,libav,
pipewire,pulseaudio}, pulseaudio-utils and x11-utils. Recommends mpv.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a [package.metadata.deb] block so the headless default build (no `gui`
feature) — the variant peerspeak spawns as a child — can be packaged with
`cargo deb` from inside a Debian/Ubuntu distrobox. Ships only the pixelpass
binary; runtime shared-lib deps resolved by dpkg-shlibdeps.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Encodes the relay-only ticket as a QR with a 4-module quiet zone so a
phone (or a second laptop with a webcam) can pick the room up without
typing 140+ characters. Built lazily on the first draw after a Ticket
event, NEAREST-filtered, 200x200 logical; cleared on session start and
stop.
Pulls `qrcode` 0.14 with `default-features = false` so the heavy `image`
crate tree is skipped — we render modules straight to an
`egui::ColorImage` ourselves.
Reapplies the idea from Gemini's stale `feat/gemini-branch-qrcode`
(`7f07583`) against the post-hand-rolled-loop GUI; the original commit
no longer cherry-picks because gui/mod.rs was rewritten for the
true-Wayland-window-hide work.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace eframe::run_native with a winit ApplicationHandler + glutin +
egui_glow loop so "keep running in the tray" can genuinely hide the
window. winit's set_visible(false) is a deliberate no-op on Wayland
(xdg-shell has no unmap-but-keep-alive request), so the only way to hide
a toplevel is to destroy its surface: hide-to-tray now drops the Window +
GL surface (parking the GL context as not-current) and a tray click
recreates them and makes the context current again. The GL context,
glutin display/config, egui_glow painter (uploaded textures), and
egui-winit state (clipboard) all persist across the cycle — only the OS
window and its surface churn.
Wakeups route through winit's EventLoopProxy (the new Waker, and the
tray) instead of egui's repaint callback, so a child event or tray click
wakes the loop even while the window is dropped and no frame is running —
keeping viewer join/leave notifications and the tray tooltip live while
hidden. Removes the old Wayland minimize-to-tray fallback (window stayed
in the taskbar); hide is now uniform on Wayland and X11.
Deps: winit/glutin/glutin-winit/egui_glow promoted to direct (gui-gated,
optional) — all already transitive via eframe, so no new crates. winit's
default features minus wayland-csd-adwaita, so sctk-adwaita/tiny-skia/
ttf-parser aren't pulled for a CSD fallback titlebar (KWin draws
server-side decorations, and eframe never had CSD either).
Verified end-to-end on KWin Wayland: launch->render; close->window AND
taskbar entry gone (true hide, process stays alive); tray activate->
window + GL surface recreated and renders; tray quit->clean exit; stderr
clean throughout. cargo test --features gui: 15 pass; clippy clean;
headless dependency tree unchanged.
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>
The host screen pops a desktop notification on each viewer join/leave,
so you know someone connected while the window is in the background.
Fired on a detached thread (the D-Bus call never touches the egui
frame) and gated on the same viewer-list transitions, so stopping the
host — which drops the child and stops pumping events — doesn't spray a
notification per remaining viewer.
notify-rust's default features give the pure-Rust zbus backend, so this
adds no system libdbus dependency and no GTK event loop (gui feature
only; the headless build is untouched).
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>
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>
ffmpeg was removed from the Wayland path on 2026-05-16 (commit 7b8b6bc).
The description was stale.
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.