fix(cli): let --enter press the key it is shorthand for (#581) (#606)

`--enter` is documented as sugar for `--key enter`, but the send dispatch
counted only `args.keys`, so `tty7 send %42 --enter` answered "needs TEXT
... or a --key to press" and pressed nothing. The key list is now built
before the dispatch and the dispatch counts it, so a marked address with
`--enter` and nothing else runs what the pane already has typed, and a
bare `send --enter` presses Enter where the caller sits.

An unmarked id is deliberately left out of that promotion. #567 made the
address slot take bare ids, and `send 83 --key C-c` addressing pane 83 is
fine because `--key` says "press this" and nothing else. `--enter` does
not: `send 2 --enter` reads at least as much like typing 2 into your own
pane and running it, and turning it into a keystroke at pane 2 would be
the silent retarget #567 spent its diff closing. It stays a loud error,
now naming both spellings (`send %83 --enter`, `send %PANE 83 --enter`)
rather than only the typing one.

The reference, the bundled skill reference, `send --help` and the
`--enter` help all said the old thing in slightly different words; they
now say the same thing as each other and as the code.

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
This commit is contained in:
l0ng-ai
2026-08-13 11:43:17 +08:00
committed by GitHub
co-authored by l0ng-ai
parent 664b766698
commit 49901d7f8a
7 changed files with 148 additions and 49 deletions
+10
View File
@@ -140,6 +140,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
the arguments that need it, and a value whose quotes do not close is refused
with an explanation under the input rather than saved as fragments. A path
spelled with backslashes still means itself. (#551)
- **`tty7 send --enter` now presses Enter when there is nothing to type** —
`--enter` is shorthand for `--key enter`, but it was never counted as a key,
so `tty7 send %42 --enter` answered "needs TEXT … or a --key to press"
instead of running what pane 42 already had typed. It counts now, with an
address or without one (`tty7 send --enter` presses Enter in your own pane).
An *unmarked* id is deliberately left out: `tty7 send 83 --enter` reads as
much like typing "83" where you are sitting as like pressing Enter in pane
83, so it stays a loud error that names both spellings (`send %83 --enter`,
`send %PANE 83 --enter`) rather than quietly retargeting the keystroke
(#581).
## [26.8.3] - 2026-08-12
+5 -1
View File
@@ -202,7 +202,11 @@ pub struct SendArgs {
#[arg(value_name = "TEXT")]
pub second: Option<String>,
#[arg(long, help = "Press Enter after the text")]
#[arg(
long,
help = "Press Enter after the text, or on its own when there is none \
(= --key enter)"
)]
pub enter: bool,
// Text covers "type this command"; it cannot express the keystrokes a pane
+97 -28
View File
@@ -484,11 +484,25 @@ fn pane_split(args: SplitArgs, ctx: &Context, backend: &mut dyn Backend) -> Resu
fn send(args: SendArgs, ctx: &Context, backend: &mut dyn Backend) -> Result<Outcome> {
const KEY_GAP: Duration = Duration::from_millis(200);
// `--enter` is the same thing as `--key enter`, and predates it. Keeping it
// as sugar rather than deprecating it: it reads better for the overwhelming
// case, which is typing one command and running it. Going through the same
// parser leaves one definition of what Enter puts on the wire — and the list
// is built here, before the dispatch, because the dispatch has to count it:
// `send %42 --enter` used to report "needs TEXT … or a --key to press" while
// the docs called `--enter` shorthand for exactly such a key (#581).
let mut pressed = args.keys.clone();
if args.enter {
pressed.push(crate::keys::parse("enter").expect("enter is in the vocabulary"));
}
// Three shapes reach here, and only the address is ever ambiguous:
// `send %3 "text"`, `send "text"` (this pane), and — new with --key —
// `send %3 --key C-c`, where there is no text at all and the lone
// positional is therefore an address rather than the missing-text error it
// has to stay in every other case.
// has to stay in every other case. `send %3 --enter` is that third shape
// too, with the one carve-out below: only a marked address is promoted by
// `--enter` alone.
//
// The address-shaped-but-broken case must not fall through to "type it":
// `send %3x --key C-c` used to type `%3x` into the *caller's own* pane and
@@ -501,12 +515,26 @@ fn send(args: SendArgs, ctx: &Context, backend: &mut dyn Backend) -> Result<Outc
(Some(first), Some(text)) => (Some(first.as_str()), Some(text.as_str())),
(Some(first), None) => match address::parse_pane(first) {
Ok(_) => {
if args.keys.is_empty() {
if pressed.is_empty() {
bail!(
"send needs TEXT after the pane address, or a --key to press \
— to type '{first}' literally, name the pane too: send %PANE {first}"
);
}
// A `--key` is always an explicit "press this", so it promotes
// either spelling of the address. `--enter` is not, for an
// *unmarked* id: `send 2 --enter` reads as "type 2 and run it"
// far more often than "press Enter in pane 2", and #538 was
// about never quietly retargeting a keystroke. The `%` is what
// says which was meant, so it stays the loud error it is today.
if args.keys.is_empty() && !first.starts_with('%') {
bail!(
"'{first}' is a bare pane id and --enter has nothing to type \
— to press Enter in pane {first}: send %{first} --enter; \
to type '{first}' and press Enter, name the pane too: \
send %PANE {first} --enter"
);
}
(Some(first.as_str()), None)
}
// `%` then a digit is someone writing an address, so the parse
@@ -515,7 +543,7 @@ fn send(args: SendArgs, ctx: &Context, backend: &mut dyn Backend) -> Result<Outc
Err(_) => (None, Some(first.as_str())),
},
(None, _) => {
if args.keys.is_empty() {
if pressed.is_empty() {
bail!("send needs TEXT to type or a --key to press");
}
(None, None)
@@ -528,14 +556,6 @@ fn send(args: SendArgs, ctx: &Context, backend: &mut dyn Backend) -> Result<Outc
backend.send_input(pane, text.as_bytes().to_vec())?;
already_wrote = true;
}
// `--enter` is the same thing as `--key enter`, and predates it. Keeping it
// as sugar rather than deprecating it: it reads better for the overwhelming
// case, which is typing one command and running it. Going through the same
// parser leaves one definition of what Enter puts on the wire.
let mut pressed = args.keys.clone();
if args.enter {
pressed.push(crate::keys::parse("enter").expect("enter is in the vocabulary"));
}
for key in &pressed {
// Raw-mode TUIs detect a fast stream as pasted input and intentionally
// absorb Enter as a newline — and a menu being driven by arrow keys has
@@ -2250,29 +2270,78 @@ mod tests {
/// A lone bare id now reads as an address, so `send 83` no longer types
/// "83" into the caller's pane — it says it has nothing to send. That is
/// the one behaviour this change takes away, and it has to fail loudly
/// rather than quietly press keys somewhere else: `--enter` is not a
/// `--key`, so it does not turn the id into a target either.
/// rather than quietly press keys somewhere else: `--enter` presses a key
/// now (#581), but it still does not turn an unmarked id into a target.
#[test]
fn a_lone_bare_id_refuses_loudly_rather_than_retargeting() {
let ctx = Context {
pane: Some("5".into()),
..Context::default()
};
for args in [
vec!["tty7", "send", "83"],
vec!["tty7", "send", "83", "--enter"],
] {
let mut backend = mock();
let err =
execute(cli(&args), &ctx, &mut backend).expect_err("a bare id has nothing to send");
assert!(err.to_string().contains("needs TEXT"), "{err}");
// The escape hatch for typing it anyway is in the message.
assert!(err.to_string().contains("send %PANE 83"), "{err}");
assert!(
backend.sent.is_empty(),
"no keystroke reached pane 83 or pane 5"
);
}
let mut backend = mock();
let err = execute(cli(&["tty7", "send", "83"]), &ctx, &mut backend)
.expect_err("a bare id has nothing to send");
assert!(err.to_string().contains("needs TEXT"), "{err}");
// The escape hatch for typing it anyway is in the message.
assert!(err.to_string().contains("send %PANE 83"), "{err}");
assert!(
backend.sent.is_empty(),
"no keystroke reached pane 83 or pane 5"
);
// `--enter` counts as the keystroke it always was (#581) — but not
// enough to promote an *unmarked* id, or `send 2 --enter` meaning "type
// 2 and run it" would press Enter in pane 2 instead. Both ways out are
// named, because either could have been meant.
let mut backend = mock();
let err = execute(cli(&["tty7", "send", "83", "--enter"]), &ctx, &mut backend)
.expect_err("--enter alone does not make a bare id a target");
assert!(err.to_string().contains("send %83 --enter"), "{err}");
assert!(err.to_string().contains("send %PANE 83 --enter"), "{err}");
assert!(
backend.sent.is_empty(),
"no keystroke reached pane 83 or pane 5"
);
}
/// `--enter` is documented as shorthand for `--key enter`, so it has to
/// give a lone address something to do exactly as `--key` does — it used to
/// report "needs TEXT … or a --key to press" and press nothing (#581).
#[test]
fn enter_alone_presses_enter_at_the_address_it_was_given() {
let mut backend = mock();
let json = json_of(run_cli(
&["tty7", "send", "%42", "--enter"],
&Context::default(),
&mut backend,
));
assert_eq!(backend.sent, vec![(42, b"\r".to_vec())]);
assert_eq!(json["sent"], "", "nothing was typed");
assert_eq!(json["keys"], serde_json::json!(["enter"]));
assert_eq!(json["enter"], true);
// With no address at all it is the caller's own pane, the same as
// `send --key enter` already was.
let ctx = Context {
pane: Some("5".into()),
..Context::default()
};
let mut backend = mock();
run_cli(&["tty7", "send", "--enter"], &ctx, &mut backend);
assert_eq!(backend.sent, vec![(5, b"\r".to_vec())]);
// The long way round stays open for a bare id, and means the same
// thing: an explicit `--key` promotes either spelling.
let mut backend = mock();
run_cli(
&["tty7", "send", "83", "--key", "enter"],
&ctx,
&mut backend,
);
assert_eq!(backend.sent, vec![(83, b"\r".to_vec())]);
let mut backend = mock();
run_cli(&["tty7", "send", "%83", "--enter"], &ctx, &mut backend);
assert_eq!(backend.sent, vec![(83, b"\r".to_vec())]);
}
/// The narrowing has to leave real text alone: `%` followed by a non-digit
+4
View File
@@ -86,6 +86,10 @@ pub fn send_long_help() -> String {
--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\
--enter is shorthand for --key enter: it presses Enter after TEXT, or on its own \
when there is none, so `send %42 --enter` runs whatever is already typed in pane 42. \
An unmarked id is not a target for it `send 83 --enter` is refused, because it \
reads just as much like typing 83 into your own pane; write %83 to mean the pane.\n\n\
Keys: {}. Aliases: {}.",
vocabulary(),
aliases.join(", ")
+14 -9
View File
@@ -93,15 +93,20 @@ share kept by the *existing* pane. Prints `%NN`. JSON: `{"pane"}`.
### `tty7 send [%PANE] [TEXT] [--enter] [--key KEY]…`
Types `TEXT` into the pane as keystrokes; `--enter` appends CR. With one
argument the text is the argument and the pane comes from `$TTY7_PANE` — but a
lone `%42` (or bare `42`, the shape `pane ls --json` prints) is rejected as a
missing-text error rather than typed, unless a `--key` gives it something to
do. A `%` followed by a digit that still doesn't parse (`%3x`) is an address
error, never text for your own 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`.
Types `TEXT` into the pane as keystrokes; `--enter` is shorthand for `--key
enter` — it appends CR to the text, or presses Enter on its own when there is
none, so `tty7 send %42 --enter` runs whatever pane 42 already has typed. With
one argument the text is the argument and the pane comes from `$TTY7_PANE` —
but a lone `%42` (or bare `42`, the shape `pane ls --json` prints) is rejected
as a missing-text error rather than typed, unless a `--key` gives it something
to do. `--enter` is that key only for the `%`-marked spelling: `tty7 send 83
--enter` is refused, because it reads as much like typing `83` into your own
pane as like pressing Enter in pane 83, and the error names both ways to say
which (`send %83 --enter`, `send %PANE 83 --enter`). A `%` followed by a digit
that still doesn't parse (`%3x`) is an address error, never text for your own
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`.
`--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
+4 -2
View File
@@ -118,8 +118,10 @@ read -r WS PANE < <(tty7 new --json /path/to/repo \
```
`send` types text into the pane exactly as a keyboard would; `--enter` appends
the carriage return. It does not wait and it does not tell you what happened —
reading is a separate step, and waiting is `tty7 wait`.
the carriage return, or presses Enter on its own when you give it no text
(`tty7 send "$PANE" --enter` runs what is already typed there). It does not
wait and it does not tell you what happened — reading is a separate step, and
waiting is `tty7 wait`.
For keystrokes rather than characters — Ctrl-C, Escape, the arrow keys — use
`--key` (see [Answering a prompt](#answering-a-prompt)). Typing `^C` as text
+14 -9
View File
@@ -100,15 +100,20 @@ pane below, `--h`/`--horizontal` to the right. `--ratio` (default 0.5) is the
share kept by the *existing* pane. Prints `%NN`. JSON: `{"pane"}`.
### `tty7 send [%PANE] [TEXT] [--enter] [--key KEY]…`
Types `TEXT` into the pane as keystrokes; `--enter` appends CR. With one
argument the text is the argument and the pane comes from `$TTY7_PANE` — but a
lone `%42` (or bare `42`, the shape `pane ls --json` prints) is rejected as a
missing-text error rather than typed, unless a `--key` gives it something to
do. A `%` followed by a digit that still doesn't parse (`%3x`) is an address
error, never text for your own 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`.
Types `TEXT` into the pane as keystrokes; `--enter` is shorthand for `--key
enter` — it appends CR to the text, or presses Enter on its own when there is
none, so `tty7 send %42 --enter` runs whatever pane 42 already has typed. With
one argument the text is the argument and the pane comes from `$TTY7_PANE`
but a lone `%42` (or bare `42`, the shape `pane ls --json` prints) is rejected
as a missing-text error rather than typed, unless a `--key` gives it something
to do. `--enter` is that key only for the `%`-marked spelling: `tty7 send 83
--enter` is refused, because it reads as much like typing `83` into your own
pane as like pressing Enter in pane 83, and the error names both ways to say
which (`send %83 --enter`, `send %PANE 83 --enter`). A `%` followed by a digit
that still doesn't parse (`%3x`) is an address error, never text for your own
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`.
JSON: `{"pane","sent","enter","keys"}`.
`--key` presses a key instead of typing characters — the arrow keys a