From b01f0a23339290eb81925f6ad2eba3e151157fe0 Mon Sep 17 00:00:00 2001 From: Christos Date: Sun, 6 Sep 2026 10:20:21 +0300 Subject: [PATCH] Fix modified arrow and page keys resolving to the opposite direction Ten entries in ANSI_SEQUENCES named the wrong key, so Ctrl+Shift+Up moved the cursor down and Ctrl+Shift+Down moved it up, in any application on any terminal that reports modified arrows. The table already agreed that final byte A is cursor-up and B is cursor-down at modifiers 2, 3, 5 and 9; it disagreed with itself at 4, 6, 7 and 8, where the pairs were transposed. ESC[5;7~ and ESC[5;8~ are a different slip: they resolve to the same values as ESC[6;7~ and ESC[6;8~, so the PageUp rows read as copied from the PageDown rows rather than swapped with them. Left/Right and Home/End were correct at every level; only the up/down axis was affected. input/win32.py already maps Up to ControlShiftUp and Down to ControlShiftDown, so this makes the two input paths agree rather than changing what the library thinks these keys mean. The tests assert the property rather than the ten values: for each final byte, every modifier level the table carries must resolve to a key naming the right direction. Three of the six parametrised cases fail without the fix and three pass, which are exactly the families that were already correct. Fixes #2092 --- .../input/ansi_escape_sequences.py | 20 ++++----- tests/test_inputstream.py | 45 +++++++++++++++++++ 2 files changed, 55 insertions(+), 10 deletions(-) diff --git a/src/prompt_toolkit/input/ansi_escape_sequences.py b/src/prompt_toolkit/input/ansi_escape_sequences.py index 1fba418b7..1e81dbb9c 100644 --- a/src/prompt_toolkit/input/ansi_escape_sequences.py +++ b/src/prompt_toolkit/input/ansi_escape_sequences.py @@ -185,10 +185,10 @@ "\x1b[5;6~": Keys.ControlShiftPageUp, "\x1b[6;6~": Keys.ControlShiftPageDown, "\x1b[2;7~": (Keys.Escape, Keys.ControlInsert), - "\x1b[5;7~": (Keys.Escape, Keys.ControlPageDown), + "\x1b[5;7~": (Keys.Escape, Keys.ControlPageUp), "\x1b[6;7~": (Keys.Escape, Keys.ControlPageDown), "\x1b[2;8~": (Keys.Escape, Keys.ControlShiftInsert), - "\x1b[5;8~": (Keys.Escape, Keys.ControlShiftPageDown), + "\x1b[5;8~": (Keys.Escape, Keys.ControlShiftPageUp), "\x1b[6;8~": (Keys.Escape, Keys.ControlShiftPageDown), # -- # Arrows. @@ -232,8 +232,8 @@ "\x1b[1;3F": (Keys.Escape, Keys.End), "\x1b[1;3H": (Keys.Escape, Keys.Home), # Alt+shift+number. - "\x1b[1;4A": (Keys.Escape, Keys.ShiftDown), - "\x1b[1;4B": (Keys.Escape, Keys.ShiftUp), + "\x1b[1;4A": (Keys.Escape, Keys.ShiftUp), + "\x1b[1;4B": (Keys.Escape, Keys.ShiftDown), "\x1b[1;4C": (Keys.Escape, Keys.ShiftRight), "\x1b[1;4D": (Keys.Escape, Keys.ShiftLeft), "\x1b[1;4F": (Keys.Escape, Keys.ShiftEnd), @@ -255,22 +255,22 @@ "\x1bOc": Keys.ControlRight, # rxvt "\x1bOd": Keys.ControlLeft, # rxvt # Control + shift + arrows. - "\x1b[1;6A": Keys.ControlShiftDown, - "\x1b[1;6B": Keys.ControlShiftUp, + "\x1b[1;6A": Keys.ControlShiftUp, + "\x1b[1;6B": Keys.ControlShiftDown, "\x1b[1;6C": Keys.ControlShiftRight, "\x1b[1;6D": Keys.ControlShiftLeft, "\x1b[1;6F": Keys.ControlShiftEnd, "\x1b[1;6H": Keys.ControlShiftHome, # Control + Meta + arrows. - "\x1b[1;7A": (Keys.Escape, Keys.ControlDown), - "\x1b[1;7B": (Keys.Escape, Keys.ControlUp), + "\x1b[1;7A": (Keys.Escape, Keys.ControlUp), + "\x1b[1;7B": (Keys.Escape, Keys.ControlDown), "\x1b[1;7C": (Keys.Escape, Keys.ControlRight), "\x1b[1;7D": (Keys.Escape, Keys.ControlLeft), "\x1b[1;7F": (Keys.Escape, Keys.ControlEnd), "\x1b[1;7H": (Keys.Escape, Keys.ControlHome), # Meta + Shift + arrows. - "\x1b[1;8A": (Keys.Escape, Keys.ControlShiftDown), - "\x1b[1;8B": (Keys.Escape, Keys.ControlShiftUp), + "\x1b[1;8A": (Keys.Escape, Keys.ControlShiftUp), + "\x1b[1;8B": (Keys.Escape, Keys.ControlShiftDown), "\x1b[1;8C": (Keys.Escape, Keys.ControlShiftRight), "\x1b[1;8D": (Keys.Escape, Keys.ControlShiftLeft), "\x1b[1;8F": (Keys.Escape, Keys.ControlShiftEnd), diff --git a/tests/test_inputstream.py b/tests/test_inputstream.py index ab1b03689..62708a5ce 100644 --- a/tests/test_inputstream.py +++ b/tests/test_inputstream.py @@ -139,3 +139,48 @@ def test_cpr_response_2(processor, stream): assert len(processor.keys) == 2 assert processor.keys[0].key == Keys.CPRResponse assert processor.keys[1].key == Keys.ControlJ + + +def _key_names(value): + "Flatten an ANSI_SEQUENCES value into the key names it resolves to." + if isinstance(value, tuple): + return [k.name for k in value] + return [value.name if hasattr(value, "name") else str(value)] + + +@pytest.mark.parametrize( + "final,direction", + [("A", "Up"), ("B", "Down"), ("C", "Right"), ("D", "Left")], +) +def test_modified_cursor_keys_keep_their_direction(final, direction): + """CSI 1 ; A is cursor-up at every modifier level, and so on for B/C/D. + + Regression test for https://github.com/prompt-toolkit/python-prompt-toolkit/issues/2092 + where A and B were transposed at modifiers 4, 6, 7 and 8 but correct at 2, 3, 5 and 9, + so Ctrl+Shift+Up moved the cursor down. + """ + from prompt_toolkit.input.ansi_escape_sequences import ANSI_SEQUENCES + + for modifier in range(2, 17): + value = ANSI_SEQUENCES.get(f"\x1b[1;{modifier}{final}") + if value is None: + continue + names = _key_names(value) + assert any(direction in name for name in names), ( + f"\\x1b[1;{modifier}{final} should name {direction}, got {names}" + ) + + +@pytest.mark.parametrize("number,direction", [(5, "PageUp"), (6, "PageDown")]) +def test_modified_page_keys_keep_their_direction(number, direction): + """CSI 5 ; ~ is PageUp and CSI 6 ; ~ is PageDown, at every level.""" + from prompt_toolkit.input.ansi_escape_sequences import ANSI_SEQUENCES + + for modifier in range(2, 17): + value = ANSI_SEQUENCES.get(f"\x1b[{number};{modifier}~") + if value is None: + continue + names = _key_names(value) + assert any(direction in name for name in names), ( + f"\\x1b[{number};{modifier}~ should name {direction}, got {names}" + )