The bug
Ten entries in src/prompt_toolkit/input/ansi_escape_sequences.py name the wrong key.
The arrow ones are transposed in pairs, so Ctrl+Shift+Up moves the cursor down and
Ctrl+Shift+Down moves it up — in any application built on prompt_toolkit, on any terminal
that reports modified arrows.
Present on main at 583b341 (the 3.0.53 release commit).
The evidence, from the table's own entries
The table already agrees that final byte A is cursor-up and B is cursor-down at four
of the eight modifier levels it carries. It disagrees with itself at the other four:
mod ESC[1;<n>A (cursor up) ESC[1;<n>B (cursor down)
2 ShiftUp ShiftDown ok
3 (Escape, Up) (Escape, Down) ok
4 (Escape, ShiftDown) (Escape, ShiftUp) SWAPPED
5 ControlUp ControlDown ok
6 ControlShiftDown ControlShiftUp SWAPPED
7 (Escape, ControlDown) (Escape, ControlUp) SWAPPED
8 (Escape, ControlShiftDown) (Escape, ControlShiftUp) SWAPPED
9 (Escape, Up) (Escape, Down) ok
And the same shape in the tilde family, where 5~ is PageUp and 6~ is PageDown:
ESC[5;7~ Ctrl+Alt+PageUp -> (Escape, ControlPageDown) should be ControlPageUp
ESC[5;8~ Ctrl+Alt+Shift+PageUp -> (Escape, ControlShiftPageDown) should be ControlShiftPageUp
ESC[6;7~ Ctrl+Alt+PageDown -> (Escape, ControlPageDown) correct
ESC[6;8~ Ctrl+Alt+Shift+PageDown -> (Escape, ControlShiftPageDown) correct
Those two are not a swap — 5;7~ and 6;7~ resolve to the same value, so the PageUp row
looks copied from the PageDown row rather than transposed with it.
Left/Right (C/D) and Home/End (H/F) are correct at every modifier level. Only
the up/down axis is affected. That is 8 arrow entries + 2 tilde entries = 10.
Independent confirmation from a terminal
Not derived from the table. kitty 0.48.2's own encoder, asked what it sends for those
presses using its own key identities:
Ctrl+Shift+Up -> ESC[1;6A table says ControlShiftDown
Ctrl+Shift+Down -> ESC[1;6B table says ControlShiftUp
Alt+Shift+Up -> ESC[1;4A table says (Escape, ShiftDown)
Alt+Shift+Down -> ESC[1;4B table says (Escape, ShiftUp)
Ctrl+Alt+Up -> ESC[1;7A table says (Escape, ControlDown)
Ctrl+Alt+PageUp -> ESC[5;7~ table says (Escape, ControlPageDown)
This also matches xterm's ctlseqs: CSI 1 ; modifier A is cursor-up, B is cursor-down,
CSI 5 ; modifier ~ is Prior/PageUp and CSI 6 ; modifier ~ is Next/PageDown.
Reproduce
from prompt_toolkit.input.ansi_escape_sequences import ANSI_SEQUENCES
ANSI_SEQUENCES["\x1b[1;6A"] # Keys.ControlShiftDown -- Ctrl+Shift+Up
ANSI_SEQUENCES["\x1b[1;6B"] # Keys.ControlShiftUp -- Ctrl+Shift+Down
Or bind c-s-up in any prompt_toolkit application and press Ctrl+Shift+Up on a terminal
that reports modified arrows.
Why it has survived
Every row in a literal lookup table is independently unfalsifiable — a wrong row looks
exactly like a right one, and nothing cross-checks A against B. I found these by
building a parser that computes the key from the sequence's grammar rather than looking it
up, and diffing its answers against the table across the whole space. A parser cannot
disagree with itself about which final byte means up, so the disagreements surfaced on
their own; there were exactly these ten, plus two documented divergences that are not
defects.
A test that walks the axis — for each modifier, assert 1;<n>A resolves to a key whose
name contains Up and 1;<n>B to one containing Down — would have caught all ten and
would catch the next one. Happy to open a PR with the ten corrections and that test if
that shape is welcome.
Environment
prompt_toolkit main @ 583b341, verified against a clean checkout with PYTHONPATH=src
(pointing pytest at the checkout without it silently tests the installed wheel instead).
Python 3.14.7, Linux. Terminal evidence from kitty 0.48.2.
The bug
Ten entries in
src/prompt_toolkit/input/ansi_escape_sequences.pyname the wrong key.The arrow ones are transposed in pairs, so Ctrl+Shift+Up moves the cursor down and
Ctrl+Shift+Down moves it up — in any application built on prompt_toolkit, on any terminal
that reports modified arrows.
Present on
mainat583b341(the 3.0.53 release commit).The evidence, from the table's own entries
The table already agrees that final byte
Ais cursor-up andBis cursor-down at fourof the eight modifier levels it carries. It disagrees with itself at the other four:
And the same shape in the tilde family, where
5~is PageUp and6~is PageDown:Those two are not a swap —
5;7~and6;7~resolve to the same value, so the PageUp rowlooks copied from the PageDown row rather than transposed with it.
Left/Right (
C/D) and Home/End (H/F) are correct at every modifier level. Onlythe up/down axis is affected. That is 8 arrow entries + 2 tilde entries = 10.
Independent confirmation from a terminal
Not derived from the table. kitty 0.48.2's own encoder, asked what it sends for those
presses using its own key identities:
This also matches xterm's
ctlseqs:CSI 1 ; modifier Ais cursor-up,Bis cursor-down,CSI 5 ; modifier ~is Prior/PageUp andCSI 6 ; modifier ~is Next/PageDown.Reproduce
Or bind
c-s-upin any prompt_toolkit application and press Ctrl+Shift+Up on a terminalthat reports modified arrows.
Why it has survived
Every row in a literal lookup table is independently unfalsifiable — a wrong row looks
exactly like a right one, and nothing cross-checks
AagainstB. I found these bybuilding a parser that computes the key from the sequence's grammar rather than looking it
up, and diffing its answers against the table across the whole space. A parser cannot
disagree with itself about which final byte means up, so the disagreements surfaced on
their own; there were exactly these ten, plus two documented divergences that are not
defects.
A test that walks the axis — for each modifier, assert
1;<n>Aresolves to a key whosename contains
Upand1;<n>Bto one containingDown— would have caught all ten andwould catch the next one. Happy to open a PR with the ten corrections and that test if
that shape is welcome.
Environment
prompt_toolkit
main@583b341, verified against a clean checkout withPYTHONPATH=src(pointing pytest at the checkout without it silently tests the installed wheel instead).
Python 3.14.7, Linux. Terminal evidence from kitty 0.48.2.