#6663·textual

Alt modifier dropped on named keys (Alt+Enter arrives as plain `enter`)

Author: vlebourlCreated Jul 21, 2026Updated Sep 2, 2026

Summary

XTermParser._sequence_to_key_events only applies the alt modifier when the resolved key name is a single character (if len(name) == 1 and alt). For a Meta-prefixed named key — e.g. Alt/Option+Enter, which a terminal sends as ESC + CR (\x1b\r) — the resolved name is enter (length 5), so the alt is silently dropped. The event arrives as plain enter, indistinguishable from a real Enter.

alt+j works; alt+enter, alt+tab, alt+backspace, etc. do not.

Impact

Apps can't bind alt+enter — the common "insert newline vs. submit" split in chat/agent CLIs — on terminals that deliver Meta as an ESC prefix rather than via the Kitty keyboard protocol (e.g. xterm.js-based terminals like Termius, macOS "Option as Meta").

Textual version

8.2.8 (latest on PyPI). The same guard is present on main.

Reproduction

python
from textual._xterm_parser import XTermParser

p = XTermParser(False)
keys = [t.key for t in p.feed("\x1b\r") if hasattr(t, "key")]
keys += [t.key for t in p.feed("") if hasattr(t, "key")]  # EOF flush
print(keys)   # ['enter']  -- expected ['alt+enter']

Location

src/textual/_xterm_parser.py, in _sequence_to_key_events:

python
if len(name) == 1 and alt:      # <-- drops alt for named keys
    if name.isupper():
        name = f"shift+{name.lower()}"
    name = f"alt+{name}"
yield events.Key(name, sequence)

Proposed fix

Apply the alt prefix regardless of name length; keep the shift/upper handling scoped to single characters:

python
if alt:
    if len(name) == 1 and name.isupper():
        name = f"shift+{name.lower()}"
    name = f"alt+{name}"
yield events.Key(name, sequence)

With this, \x1b\r yields alt+enter, and alt+j / plain enter / plain j are unchanged.