Files
tty7/crates/tty7-cli/src/stdio.rs
T
l0ng-ai e530d5f778 docs(rustdoc): fix the links that pointed nowhere, and gate rustdoc in CI
Nothing had ever run `cargo doc`, so 16 warnings had collected. Four were
links to items that do not exist, and two of those were worse than a dead
link: `git_badge` and `info_chip` documented their sizes in terms of
`PANEL_TEXT` and `PANEL_TEXT_META`, px constants deleted when the interface
font scale landed. The module comment twenty lines up already says they went;
the prose downstream still derived pixel arithmetic from them, so a reader was
being told the pill is 20px tall against a 19px neighbour when both are now
rems that move with `ui_font_size`.

Rewritten against the ladder that exists (`META_MONO` beside `TEXT_MONO`), and
`info_chip`'s comment now records what its own numbers imply: its padding and
radius are pixels wrapped around rem-sized text, so the two stop agreeing once
the interface scale leaves 100% — the same trap `PIP_SIZE` right below it is
written in rems to avoid. Left as a note rather than changed, because that is a
visual decision and this cannot see the result.

The other ten are `private_intra_doc_links`, and that lint does not apply here:
it exists so a *published* crate does not ship docs whose links dead-end, and
all four crates are `publish = false`. Allowed at the crate root with that
reason, because a public item explaining how it relates to a private one is the
useful half of these comments.

The `clippy` job becomes `lint` and runs rustdoc too, on the same warm cache.
Still non-required.

2932 tests pass.
2026-08-15 18:11:17 +08:00

77 lines
3.4 KiB
Rust

//! Stdout for a command that lives inside pipelines.
//!
//! A reader that hangs up early — `| head -1`, `| grep -q`, PowerShell's
//! `Select-Object -First` — is how a pipeline ends, not a failure. Rust makes
//! that awkward twice over: it ignores SIGPIPE at startup so the doomed write
//! comes back as an `io::Error` instead, and `println!` turns that error into a
//! panic. `tty7 capture %1 | head -1` therefore printed a panic and a backtrace
//! note where `cat` would have exited without a word — noise on stderr for a
//! correct invocation, from a CLI whose whole point is being driven by scripts
//! and agents.
//!
//! Two mechanisms, one contract:
//!
//! - On Unix [`end_pipelines_quietly`] hands the job back to the kernel. That
//! covers every write site at once, including ones added later, and gives
//! callers the ending they already know from `cat` (shells report 141).
//! - Windows has no SIGPIPE — the write fails with `ERROR_NO_DATA` instead — so
//! [`out`] is the stand-in that recognizes the hang-up and leaves quietly.
//!
//! Which is why every stdout write in this binary goes through [`out`] or
//! [`line()`]: the `print!` family has no way to express "the reader left".
use std::io::Write as _;
/// Exit code once the reader is gone. Unix rarely gets here — SIGPIPE has
/// already killed the process, and shells render that as 141 — so this is
/// Windows's answer, and Windows has no signal convention to imitate. Success
/// is the honest report: everything the reader asked for did reach it.
const READER_GONE: i32 = 0;
/// Let a hung-up pipe end this process the way it ends `cat`.
///
/// Called once, before anything is written, so no output can be lost to it.
#[cfg(unix)]
pub fn end_pipelines_quietly() {
// SAFETY: setting a signal disposition is safe from single-threaded startup
// code, and SIG_DFL is the disposition every other process starts with —
// Rust's runtime is the thing that changed it.
unsafe {
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
}
}
/// No-op: Windows has no SIGPIPE, so [`out`] carries the whole contract there.
#[cfg(not(unix))]
pub fn end_pipelines_quietly() {}
/// Write to stdout, leaving quietly if the reader has hung up.
///
/// Exiting from this depth is deliberate — it is what the signal does on Unix,
/// and it keeps every call site a plain statement. A `run` cut short this way
/// leaves its pane behind, same as any other interrupted `run`.
pub fn out(bytes: &[u8]) {
let mut stdout = std::io::stdout().lock();
// Flush here rather than at exit: `std::process::exit` runs no destructors,
// and stdout is a LineWriter, so anything not ending in a newline would be
// dropped on the floor.
if let Err(e) = stdout.write_all(bytes).and_then(|()| stdout.flush()) {
if e.kind() == std::io::ErrorKind::BrokenPipe {
std::process::exit(READER_GONE);
}
// A stdout that failed for any other reason (full disk, closed fd) is
// worth saying out loud — the caller is missing output either way, and
// silence would make it look like there was none.
eprintln!("tty7: writing to stdout: {e}");
std::process::exit(1);
}
}
/// One line and its newline, in a single write.
pub fn line(text: &str) {
let mut buf = String::with_capacity(text.len() + 1);
buf.push_str(text);
buf.push('\n');
out(buf.as_bytes());
}