Character-valued `ANSI_SEQUENCES` entries insert their escape sequence instead of the character

Author: despotakCreated Aug 27, 2026Updated Aug 27, 2026

ANSI_SEQUENCES maps an escape sequence to a key, and a key may be a plain single character rather than a Keys member. Such an entry resolves correctly but types its own escape sequence into the buffer.

Repro (3.0.53, no other dependencies):

python
from prompt_toolkit.input.ansi_escape_sequences import ANSI_SEQUENCES
from prompt_toolkit.input.vt100_parser import Vt100Parser

SEQ = "\x1b[27;2;78~"          # what a terminal in xterm modifyOtherKeys=2 sends for Shift+N
ANSI_SEQUENCES[SEQ] = "N"

presses = []
p = Vt100Parser(presses.append)
p.feed(SEQ); p.flush()
print(presses[0].key, repr(presses[0].data))
# -> N '\x1b[27;2;78~'

End to end, through a real PromptSession:

python
import asyncio
from prompt_toolkit.input.ansi_escape_sequences import ANSI_SEQUENCES
from prompt_toolkit.input import create_pipe_input
from prompt_toolkit.output import DummyOutput
from prompt_toolkit.shortcuts import PromptSession

SEQ = "\x1b[27;2;78~"
ANSI_SEQUENCES[SEQ] = "N"

async def main():
    with create_pipe_input() as pipe:
        pipe.send_text("ab" + SEQ + "cd\r")
        return await PromptSession(input=pipe, output=DummyOutput()).prompt_async()

print(repr(asyncio.run(main())))
# expected: 'abNcd'
# actual:   'ab\x1b[27;2;78~cd'

Mechanism

  • Vt100Parser._call_handler(key, insert_text) is called with insert_text = the matched bytes, and builds KeyPress(key, insert_text).
  • key_binding/bindings/basic.py:168 binds Keys.Anyself-insert.
  • key_binding/bindings/named_commands.py:273self-insert does insert_text(event.data * event.arg).

So insertion is driven by data, and data is the raw sequence. This is invisible for every entry prompt_toolkit ships, because those resolve to Keys members whose bindings never read data. It only bites character-valued entries, which the table accepts without complaint.

Why anyone registers those

There's no built-in support for xterm modifyOtherKeys / the kitty keyboard protocol (#2006 has been open for this since 2025-08-19). The natural workaround is to push the terminal into CSI >4;2m and register the resulting sequences as character-valued aliases — at which point every shifted letter types its own escape sequence.

Downstream this is currently handled by monkeypatching _call_handler. Hermes Agent has four independent open implementations of that same patch — NousResearch/hermes-agent#87785, #88097, #94385, #96133 — plus roughly ten more PRs and six issues against variants of the symptom (Shift+letter, Shift+Space, Shift+symbol, numeric keypad, Cyrillic, Cmd/Super). That seemed like enough duplicated effort to be worth reporting at the source rather than patching around again.

Ask: either narrow data to the key for character-valued entries, or document that ANSI_SEQUENCES values must be Keys members. Silently accepting a character and then typing the bytes is the part that costs people time — either resolution fixes that.

Happy to send a PR for whichever shape you'd prefer. Deliberately not opening one first: data is read at ~46 sites in the library, including mouse handling that slices it positionally, so which way this should be resolved seems like your call rather than something to presume in a diff.

Versions: reproduced on 3.0.52 and 3.0.53, Python 3.11.16, Linux.


Repros written and run by Nyx, a Claude Code agent, on the reporting machine; both are copy-pasteable and were executed as shown.

Source: prompt-toolkit/python-prompt-toolkit