Terminal input: consume the ESC-as-Meta flag on every key

ESC sets a flag that makes the next key a Meta command, but arrows,
mouse events, Enter, Tab and control chords returned before the flag
was checked and left it set. A later plain key then ran as a Meta
command: ESC, Right, d deleted a word instead of inserting 'd'. The
flag is now consumed up front, and the Meta lookup runs before the
control-chord and Enter handling so ESC ^H (meta-backspace) and ESC
Enter (SmartNewline) reach their bindings.

Verified by driving kte in a pty: ESC, Right, d on 'hello world' now
gives 'hdello world' (was 'hworld').

Audit finding B3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
This commit is contained in:
Claude
2026-09-24 09:16:04 -07:00
committed by Kyle Isom
parent a69664037d
commit f861e42c2c
+40 -26
View File
@@ -53,6 +53,15 @@ map_key_to_command(const int ch,
// (e.g. U+0107 'ć' equals KEY_BACKSPACE's value), which would wrongly
// swallow it as a special key instead of inserting it.
// These keys exit k-prefix mode if active (user pressed C-k then a special key).
//
// ESC marks the next key as Meta. Consume that flag on every other key up
// front: keys handled before the Meta lookup below (arrows, mouse, Enter,
// control chords) used to leave it set, turning a later plain key into a
// Meta command (ESC, Left, d => delete word).
const bool meta = esc_meta && ch != 27;
if (ch != 27)
esc_meta = false;
switch (is_keycode ? ch : -1) {
case KEY_ENTER:
// Some terminals send KEY_ENTER distinct from '\n'/'\r'
@@ -184,6 +193,37 @@ map_key_to_command(const int ch,
return true;
}
// Meta (ESC-prefixed) keys: backspace, Enter and printable keys use the
// ESC keymap. Checked before control chords so ESC ^H (meta-backspace) is
// not taken as C-h, and before Enter so ESC Enter reaches SmartNewline.
// Other control chords after ESC act as themselves.
const bool is_backspace = (is_keycode && ch == KEY_BACKSPACE) || ch == 127 || ch == CTRL('H');
const bool meta_key = meta && (is_backspace || (!is_keycode && (ch == '\n' || ch == '\r' || ch >= 0x20)));
if (meta_key) {
k_prefix = false;
k_ctrl_pending = false;
int ascii_key = ch;
if (is_backspace) {
ascii_key = KEY_BACKSPACE; // normalized value for lookup
} else if (ch == ',') {
// Some terminals emit ',' when Shift state is lost after ESC; treat as '<'
ascii_key = '<';
} else if (ch == '.') {
// Likewise, map '.' to '>'
ascii_key = '>';
} else if (ascii_key >= 'A' && ascii_key <= 'Z') {
ascii_key = ascii_key - 'A' + 'a';
}
CommandId id;
if (KLookupEscCommand(ascii_key, id)) {
out = {true, id, "", 0};
return true;
}
// Unhandled ESC sequence: exit escape mode and show status
out = {true, CommandId::UnknownEscCommand, "", 0};
return true;
}
// Control keys
if (ch == CTRL('K')) {
// C-k prefix
@@ -278,32 +318,6 @@ map_key_to_command(const int ch,
out = {true, CommandId::Newline, "", 0};
return true;
}
// If previous key was ESC, interpret as meta and use ESC keymap
if (esc_meta) {
esc_meta = false;
int ascii_key = ch;
// Handle ESC + BACKSPACE (meta-backspace, Alt-Backspace)
if ((is_keycode && ch == KEY_BACKSPACE) || ch == 127 || ch == CTRL('H')) {
ascii_key = KEY_BACKSPACE; // normalized value for lookup
} else if (ch == ',') {
// Some terminals emit ',' when Shift state is lost after ESC; treat as '<'
ascii_key = '<';
} else if (ch == '.') {
// Likewise, map '.' to '>'
ascii_key = '>';
} else if (ascii_key >= 'A' && ascii_key <= 'Z') {
ascii_key = ascii_key - 'A' + 'a';
}
CommandId id;
if (KLookupEscCommand(ascii_key, id)) {
out = {true, id, "", 0};
return true;
}
// Unhandled ESC sequence: exit escape mode and show status
out = {true, CommandId::UnknownEscCommand, "", 0};
return true;
}
// Backspace in ncurses can be KEY_BACKSPACE or 127
if ((is_keycode && ch == KEY_BACKSPACE) || ch == 127 || ch == CTRL('H')) {
k_prefix = false;