Files
tty7/crates/tty7-cli/Cargo.toml
T
l0ng-ai 86eba1e2c2 feat(cli): make a captured pane readable, and stop panicking on a closed pipe
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.
2026-07-31 19:18:44 +08:00

67 lines
2.4 KiB
TOML

# The thin console CLI: the `tty7` on the user's PATH.
# A control/pane-protocol client of tty7-server — the GUI is never required.
# Kept apart from the GUI binary on purpose (subsystem, upgrade file locks,
# millisecond cold start).
[package]
name = "tty7-cli"
version.workspace = true
edition.workspace = true
description = "tty7's command line: a thin client for the tty7 server — workspaces, tabs, panes, agents, no GUI required"
repository = "https://github.com/l0ng-ai/tty7"
license = "Apache-2.0"
publish = false
[[bin]]
name = "tty7"
path = "src/main.rs"
[dependencies]
# Protocol types only (ControlRequest/ReplyOk, the machine tree, PaneProcs).
# Default features: no gssapi, no remote-install — the CLI is a client of a
# server that already exists, it never links krb5 or an HTTP fetcher.
tty7-core = { path = "../tty7-core" }
anyhow.workspace = true
serde_json.workspace = true
# The command grammar. Not in [workspace.dependencies] because this is its only
# user, same as the GUI package's own single-user pins (regex, memchr, …).
clap = { version = "4", features = ["derive"] }
# Column alignment in the tables. Byte length is not display width: a CJK
# workspace name or cwd is two columns per char, and padding by len() puts
# every column after it out of line. Already in the tree via alacritty.
unicode-width = "0.2"
# The VT parser behind `capture --plain`. A pane's replay is a byte stream, and
# recovering the text from it is a terminal's job, not a regex's: only the grid
# knows that a break at column 120 was a wrap rather than a newline, that a CR
# meant "overwrite this line", or which cell a wide char and its spacer share.
# The same crate the GUI renders with, at the same rev, so the two agree about
# what a pane says.
alacritty_terminal.workspace = true
[target.'cfg(unix)'.dependencies]
libc = "0.2"
[dev-dependencies]
tempfile = "3"
# The e2e harness puts its throwaway daemon in a Job Object wired to
# KILL_ON_JOB_CLOSE, so a hard-killed test run cannot leak servers.
[target.'cfg(windows)'.dev-dependencies]
windows-sys = { version = "0.59", features = [
"Win32_Foundation",
"Win32_Security",
"Win32_System_JobObjects",
] }
# The end-to-end suite drives the compiled tty7.exe against a real isolated
# server; its own main doubles as that server process, so no libtest harness.
[[test]]
name = "cli_e2e"
harness = false
[lints]
workspace = true