feat(output): --output json machine-readable event stream

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>
This commit is contained in:
2026-05-24 16:17:38 -04:00
parent 6619bc9b0f
commit e7ded10db8
6 changed files with 135 additions and 8 deletions
+2 -1
View File
@@ -5,7 +5,7 @@ use iroh_tickets::endpoint::EndpointTicket;
use tokio::net::TcpListener;
use crate::cli::ViewerOpts;
use crate::common::{alpn::ALPN, signal};
use crate::common::{alpn::ALPN, output, signal};
pub async fn run(ticket: EndpointTicket, opts: ViewerOpts) -> Result<()> {
let cancel = signal::install_ctrl_c();
@@ -23,6 +23,7 @@ pub async fn run(ticket: EndpointTicket, opts: ViewerOpts) -> Result<()> {
let listener = TcpListener::bind(("127.0.0.1", opts.port)).await?;
let port = listener.local_addr()?.port();
let url = format!("http://127.0.0.1:{port}");
output::emit(output::Event::Connected { url: &url });
if opts.interactive {
let player = crate::interactive::prompt_player()?;