mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-28 08:02:39 +00:00
The CLI's own --help calls it "built for coding agents", but `capture` handed back the daemon's raw PTY bytes, which is the least readable thing it emits, and every verb panicked when its reader hung up. `capture --plain` replays those bytes through a terminal grid instead of stripping escapes from them, using the same alacritty_terminal rev the GUI renders panes with. The difference is not cosmetic: only the grid knows that a break at the pane's width was a wrap rather than a newline, that a CR meant "overwrite this line" rather than "end it", and which cell a wide char shares with its spacer. A regex gets the easy 90% and then invents the rest — on one real pane it turned 1193 lines into 2806. The size each segment needs comes for free: the daemon already sends DaemonMsg::Size right before every Snapshot, and the CLI was discarding it. Panes here measure 249 and 86 columns, so the hardcoded 120 would have wrapped both in the wrong places. Observing still resizes nothing. The pipe fix is two mechanisms with one contract. On Unix SIGPIPE goes back to its default disposition, which covers every write site at once and ends the process the way it ends `cat` (141). Windows has no such signal, so stdio::out recognizes the hung-up write and leaves quietly. Before this, 16 of 19 verbs printed a panic and a backtrace note for `tty7 ls | head -1`; `run` instead reported it as a failure with exit 1. Also adds skills/tty7, the Claude skill for driving this CLI. It shipped with a Python ANSI stripper, which is what prompted --plain; the script is gone. alacritty_terminal moves to [workspace.dependencies] so the GUI and the CLI cannot drift onto two revs of the fork.
64 lines
1.8 KiB
Rust
64 lines
1.8 KiB
Rust
mod address;
|
|
mod backend;
|
|
mod cli;
|
|
mod commands;
|
|
mod output;
|
|
mod resolve;
|
|
mod screen;
|
|
mod server;
|
|
mod stdio;
|
|
#[cfg(test)]
|
|
mod testbed;
|
|
|
|
use clap::Parser;
|
|
|
|
fn main() -> std::process::ExitCode {
|
|
// Before the first byte goes out, and before clap can print a usage error.
|
|
stdio::end_pipelines_quietly();
|
|
let cli = cli::Cli::parse();
|
|
let json = cli.json;
|
|
let quiet = cli.quiet;
|
|
let ctx = address::Context::from_env();
|
|
let mut backend = backend::RealBackend::new(cli.machine.clone());
|
|
match commands::execute(cli, &ctx, &mut backend) {
|
|
// `run` stands in for the command it launched, so its exit code is
|
|
// ours. The report rides along anyway: --json must not go silent just
|
|
// because the verb also carries an exit code.
|
|
Ok(commands::Outcome::Exit(code, report)) => {
|
|
// No flush needed before the exit below: `stdio::out` already did.
|
|
emit(report, json, quiet);
|
|
std::process::exit(code)
|
|
}
|
|
Ok(commands::Outcome::Report(report)) => {
|
|
emit(report, json, quiet);
|
|
std::process::ExitCode::SUCCESS
|
|
}
|
|
// --quiet suppresses output on success, not the reason for a failure:
|
|
// swallowing this leaves a bare exit code and nothing to debug.
|
|
Err(e) => {
|
|
eprintln!("tty7: {e:#}");
|
|
std::process::ExitCode::FAILURE
|
|
}
|
|
}
|
|
}
|
|
|
|
fn emit(report: commands::Report, json: bool, quiet: bool) {
|
|
if quiet {
|
|
return;
|
|
}
|
|
if json {
|
|
if !report.json.is_null() {
|
|
stdio::line(&report.json.to_string());
|
|
}
|
|
} else if !report.human.is_empty() {
|
|
stdio::out(ensure_newline(report.human).as_bytes());
|
|
}
|
|
}
|
|
|
|
fn ensure_newline(mut s: String) -> String {
|
|
if !s.ends_with('\n') {
|
|
s.push('\n');
|
|
}
|
|
s
|
|
}
|