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):
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:
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.Any → self-insert.
key_binding/bindings/named_commands.py:273 — self-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.
ANSI_SEQUENCESmaps an escape sequence to a key, and a key may be a plain single character rather than aKeysmember. Such an entry resolves correctly but types its own escape sequence into the buffer.Repro (3.0.53, no other dependencies):
End to end, through a real
PromptSession:Mechanism
Vt100Parser._call_handler(key, insert_text)is called withinsert_text= the matched bytes, and buildsKeyPress(key, insert_text).key_binding/bindings/basic.py:168bindsKeys.Any→self-insert.key_binding/bindings/named_commands.py:273—self-insertdoesinsert_text(event.data * event.arg).So insertion is driven by
data, anddatais the raw sequence. This is invisible for every entry prompt_toolkit ships, because those resolve toKeysmembers whose bindings never readdata. 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 intoCSI >4;2mand 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
datato the key for character-valued entries, or document thatANSI_SEQUENCESvalues must beKeysmembers. 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:
datais 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.