Skip to content

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

Description

@despotak

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.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 patchNousResearch/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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions