use std::io::{self, Write};
use std::process::ExitCode;
use std::time::Duration;
use termlens::Terminal;
const USAGE: &str = "\
usage: inspect [--size COLSxROWS] [--timeout SECONDS] [--idle MILLIS]
[--inherit-env] [--ansi] [--env KEY=VALUE]... <program> [args…]
Runs <program> in an 80x24 pseudo-terminal (or --size), waits for it to
exit or for the deadline (--timeout, default 5 seconds), and prints the
rendered screen. A program still running at the deadline is snapshotted
after --idle milliseconds (default 300) of output silence, then killed.
The child environment is cleared by default except for PATH; --inherit-env
keeps the caller's environment, and repeatable --env sets selected values.
Exit code 0: a screen was printed; the trailer under it says what the
program did. Exit code 1: inspect itself could not run — bad arguments,
or a program that could not be spawned.
-h, --help print this text
--version print the termlens version this example was built from
-- end of options; the program name follows";
fn take<T>(
args: &mut impl Iterator<Item = String>,
flag: &str,
kind: &str,
example: &str,
parse: impl Fn(&str) -> Option<T>,
) -> Result<T, String> {
let Some(raw) = args.next() else {
return Err(format!("{flag} needs a {kind} argument"));
};
parse(&raw).ok_or_else(|| format!("bad {flag} {raw:?}, expected e.g. {example}"))
}
fn render(screen: &termlens::Screen, ansi: bool) -> String {
if ansi {
let header = screen.to_string();
let header = header.lines().next().unwrap_or_default();
format!("{header}\n{}", screen.to_ansi())
} else {
screen.to_string()
}
}
fn main() -> ExitCode {
let mut args = std::env::args().skip(1).peekable();
let mut size = (80u16, 24u16);
let mut timeout = Duration::from_secs(5);
let mut idle = Duration::from_millis(300);
let mut inherit_env = false;
let mut ansi = false;
let mut env = Vec::new();
while args.peek().is_some_and(|a| a.starts_with('-') && a != "-") {
let flag = args.next().unwrap_or_default();
let parsed = match flag.as_str() {
"-h" | "--help" => {
println!("{USAGE}");
return ExitCode::SUCCESS;
}
"--version" => {
println!("inspect (termlens {})", env!("CARGO_PKG_VERSION"));
return ExitCode::SUCCESS;
}
"--" => break,
"--size" => take(&mut args, "--size", "COLSxROWS", "120x40", |spec| {
let (c, r) = spec.split_once('x')?;
Some((c.parse().ok()?, r.parse().ok()?))
})
.map(|s| size = s),
"--timeout" => take(&mut args, "--timeout", "SECONDS", "30", |s| s.parse().ok())
.map(|secs| timeout = Duration::from_secs(secs)),
"--idle" => take(&mut args, "--idle", "MILLIS", "1000", |s| s.parse().ok())
.map(|millis| idle = Duration::from_millis(millis)),
"--inherit-env" => {
inherit_env = true;
Ok(())
}
"--ansi" => {
ansi = true;
Ok(())
}
"--env" => take(&mut args, "--env", "KEY=VALUE", "NO_COLOR=1", |s| {
let (key, value) = s.split_once('=')?;
(!key.is_empty()).then(|| (key.to_owned(), value.to_owned()))
})
.map(|pair| env.push(pair)),
other => Err(format!("unknown option {other:?} (try --help)")),
};
if let Err(message) = parsed {
eprintln!("inspect: {message}");
return ExitCode::FAILURE;
}
}
let Some(program) = args.next() else {
eprintln!("{USAGE}");
return ExitCode::FAILURE;
};
let mut builder = Terminal::builder()
.size(size.0, size.1)
.timeout(timeout)
.args(args);
if !inherit_env {
builder = builder.env_clear();
if let Some(path) = std::env::var_os("PATH") {
builder = builder.env("PATH", path);
}
}
for (key, value) in env {
builder = builder.env(key, value);
}
let mut t = match builder.spawn(&program) {
Ok(t) => t,
Err(e) => {
eprintln!("inspect: {e}");
return ExitCode::FAILURE;
}
};
let mut out = String::new();
match t.wait_exit() {
Ok(status) => {
out.push_str(&render(&t.screen(), ansi));
out.push_str(&format!("\n--- exited: {status} ---\n"));
}
Err(termlens::Error::Timeout { .. }) => {
let _ = t.wait_idle_for(idle, timeout.max(idle));
out.push_str(&render(&t.screen(), ansi));
out.push_str("\n--- still running at the deadline (killed on exit) ---\n");
}
Err(e) => {
out.push_str(&render(&t.screen(), ansi));
out.push_str(&format!("\n--- waiting for the program failed: {e} ---\n"));
}
}
let mut stdout = io::stdout().lock();
if let Err(e) = stdout
.write_all(out.as_bytes())
.and_then(|()| stdout.flush())
{
if e.kind() == io::ErrorKind::BrokenPipe {
return ExitCode::SUCCESS;
}
eprintln!("inspect: writing the screen failed: {e}");
return ExitCode::FAILURE;
}
ExitCode::SUCCESS
}