docs(cli): say that a newline inside send's TEXT presses Enter

$ tty7 send %1 "$(printf 'echo ONE\necho TWO')"
    ... echo ONE
    ONE
    ... echo TWO          <- typed, waiting

Correct for a verb whose first line is "types TEXT into the pane exactly
as a keyboard would" -- typing a newline is pressing Enter. But `--enter`
is documented right next to it as the way to submit, which reads as
though TEXT alone cannot, and an orchestrator passing along text it did
not write runs it a line at a time.

So `--help` and the reference now say it, with the shape it takes: the
text before the newline runs, and what follows is left typed at the
prompt it asked for. A test pins the bytes -- neither stripped nor split
into two writes -- so the pages and the behaviour cannot drift apart.

Found while feeding a pane input it does not expect. Three neighbours
came through that unchanged and are worth recording: invalid UTF-8 in
output becomes U+FFFD with correct recovery (`\xc3\x28` is one
replacement and a literal `(`), `capture --json` stays valid JSON over
it, and an OSC title carrying bad bytes is folded before storage so
`machine.json` stays parseable and the tab table stays aligned.
This commit is contained in:
l0ng-ai
2026-08-16 13:33:46 +08:00
parent 9509ff549c
commit db275b7770
3 changed files with 32 additions and 0 deletions
+22
View File
@@ -2625,6 +2625,28 @@ mod tests {
assert_eq!(backend.sent, vec![(3, b"echo hi".to_vec())]);
}
/// A newline in TEXT goes out as a newline, which a shell runs.
///
/// `--enter` exists to submit, so it reads as though TEXT alone cannot —
/// and text that came from somewhere else then runs a line at a time. The
/// behaviour is right for a verb that types what a keyboard would; what it
/// needs is to be written down, and both the `--help` and the reference
/// now say so. This keeps the bytes matching what they say.
#[test]
fn a_newline_in_the_text_is_sent_as_one() {
let mut backend = mock();
run_cli(
&["tty7", "send", "%1", "deploy\nyes"],
&Context::default(),
&mut backend,
);
assert_eq!(
backend.sent,
vec![(1, b"deploy\nyes".to_vec())],
"neither stripped nor split into two writes"
);
}
/// The keystrokes text cannot express. Each goes out as its own write, in
/// the order given, because a pane reads them as separate key events —
/// which is what walking a menu and then confirming it requires.
+5
View File
@@ -83,6 +83,11 @@ pub fn send_long_help() -> String {
let aliases: Vec<&str> = ALIASES.iter().map(|(from, _)| *from).collect();
format!(
"Types TEXT into the pane exactly as a keyboard would.\n\n\
Exactly as a keyboard would includes the newlines: one inside TEXT is \
Enter, and runs what stands before it. `send %1 $'deploy\\nyes'` runs \
`deploy` and leaves `yes` typed at the prompt it asked. Worth knowing \
before passing along text somebody else wrote — --enter is the way to \
submit on purpose.\n\n\
--key sends a keystroke rather than characters, which is what a pane wants once \
something is already running in it: answering a prompt that only takes arrow keys, \
closing a TUI with escape, stopping a build with C-c. Repeat it for a sequence.\n\n\
+5
View File
@@ -134,6 +134,11 @@ pane — while text that merely starts with `%` (`%s/foo/bar/`, `%!sort`) types
as given, as does anything unmarked that is not a plain number (`3x`, `+5`). To
type an address-shaped string, name the pane as well: `tty7 send %42 %3x`.
A newline inside `TEXT` is Enter, because typing one is. `tty7 send %1
$'deploy\nyes'` runs `deploy` and leaves `yes` typed at the prompt it asked
for — so text that came from somewhere else runs a line at a time, and `--enter`
is the way to submit on purpose.
`--key` presses a key instead of typing characters, which is what a pane wants
once something is already running in it: answering a prompt that only takes
arrow keys, closing a TUI with `escape`, stopping a build with `C-c`. Repeat it