Modified arrow keys resolve to the opposite direction: ESC[1;6A is Ctrl+Shift+Up but maps to ControlShiftDown (10 entries)

Author: despotakCreated Sep 6, 2026Updated Sep 6, 2026

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

python
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.

Source: prompt-toolkit/python-prompt-toolkit