Skip to content

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

Description

@despotak

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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