Make the terminal usable: control keys, paste, scroll, restart, terminate

The largest defect was not the one the plan named. Every control character was
being dropped before it reached the PTY — ctrl-c, ctrl-d, ctrl-a, ctrl-r, not
just ctrl-k — because terminal_key_from_parts required a key_char and GPUI
reports none for a control chord, since ctrl-k produces no printable character.
Verified with `cat -v`, which now prints ^K^A^R; ctrl-c interrupts a sleep and
ctrl-d ends a heredoc. The engine had always encoded these correctly; nothing
ever handed them to it.

The binding shadowing was real too. OpenPalette was on secondary-k, which is
ctrl-k on Linux, and GPUI stops dispatching once a binding claims an event, so
readline's kill-line was unreachable in every pane. Pane selection sat on
alt-1..6, which readline reads as a digit argument, and focus movement on
alt-arrows, which is word motion in most terminals.

Bindings now live in keymap.rs with the rule written down and tested: no binding
may be a bare control character or a bare Meta sequence, because those are what
a terminal application actually receives. A leader chord was considered and
rejected — GPUI parks a chord prefix for a second and drops it if focus moves.

Also in this pass:

- Paste on secondary-shift-v, through the engine's bracketed-paste path so a
  shell that asked for bracketed paste is told this is a paste. secondary-v
  would have been ctrl-v, which readline reads as quoted-insert. There is no
  matching copy: the engine has no selection yet, and a key that copied the
  whole screen would not be the same feature under the same name.
- A scroll wheel on the terminal surface. Shift+PageUp was the only route to
  scrollback, which is not something anyone guesses.
- Restart and Terminate. RuntimeRegistry::shutdown existed and was called only
  from its own crate's tests, so nothing in the application could ever stop a
  PTY. Terminate is the literal words with a confirmation naming the pid, per
  decision 0010, never a close icon; restart keeps the pane and replaces the
  process, per decision 0011.
- A dead or faulted pane now says so over its stale screen instead of looking
  idle, and typing into a pane with no terminal explains where the keystroke
  went instead of silently discarding it.
- The twelve reachable .expect panics on live-terminal state are gone. A pane
  can outlive its runtime — failed spawn, terminate, restored snapshot — and
  every one of those paths used to be a panic in the middle of a paint.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Metal Agent
2026-08-31 23:35:32 -07:00
co-authored by Claude Opus 5
parent 1556b87f37
commit d76da3babb
3 changed files with 552 additions and 113 deletions
+3 -1
View File
@@ -38,8 +38,9 @@ pub(crate) struct ThemeColors {
pub(crate) success: Rgba,
/// No surface reports failure in colour yet; a dead pane is described in
/// words. Defined here so the role exists when one does.
#[expect(dead_code, reason = "no failure surface paints yet")]
pub(crate) danger: Rgba,
/// A danger fill quiet enough to sit behind text.
pub(crate) danger_container: Rgba,
pub(crate) attention: Rgba,
/// The needs-input row tint, which arrives with the sidebar rework.
#[expect(dead_code, reason = "the attention row is rebuilt with the sidebar")]
@@ -75,6 +76,7 @@ impl From<&Palette> for ThemeColors {
accent: rgba(palette.accent),
success: rgba(palette.success),
danger: rgba(palette.danger),
danger_container: rgba(palette.danger_container),
attention: rgba(palette.attention),
attention_wash: rgba(palette.attention_wash),
}