Fix keyboard shortcuts for shifted punctuation and letters

- Preserve Shift+R semantics for DNS refresh
- Resolve shifted punctuation by produced text across layouts
- Expand keyboard binding coverage tests
This commit is contained in:
Philip Henning 2026-09-08 20:44:11 +02:00
parent 9040cf72a8
commit d4130c332a
4 changed files with 124 additions and 2 deletions

View file

@ -160,6 +160,74 @@ def test_enhanced_caps_lock_n_does_not_become_shift_n():
assert event.character == "N"
@pytest.mark.parametrize(
"sequence",
(
"\x1b[47;2;63u", # US: Shift+/
"\x1b[223;2;63u", # German: Shift+ß
),
)
def test_enhanced_question_mark_uses_the_produced_text_key(sequence):
"""Shifted punctuation binds by its text across keyboard layouts."""
event = list(HostsXTermParser().feed(sequence))[0]
assert isinstance(event, Key)
assert event.key == "question_mark"
assert event.character == "?"
def test_keyboard_protocol_events_match_all_character_and_modifier_bindings():
"""Terminal key events resolve to every app-level shortcut action."""
sequences_and_actions = {
"\x1b[110u": "add_entry",
"\x1b[110;2;78u": "new_from_selected",
"N": "new_from_selected",
"\x1b[100u": "delete_entry",
"\x1b[101u": "edit_entry",
"\x1b[32u": "toggle_entry",
"\x1b[101;5u": "toggle_edit_mode",
"\x1b[99u": "config",
"\x1b[120;5u": "export_entries",
"\x1b[111;5u": "import_entries",
"\x1b[102;5u": "show_filters",
"\x1b[47;2;63u": "help",
"?": "help",
"\x1b[113u": "quit",
"\x1b[114;5u": "reload",
"\x1b[105u": "sort_by_ip",
"\x1b[104u": "sort_by_hostname",
"\x1b[115;5u": "save_file",
"\x1b[98u": "restore_backup",
"\x1b[1;2A": "move_entry_up",
"\x1b[1;2B": "move_entry_down",
"\x1b[122;5u": "undo",
"\x1b[121;5u": "redo",
"\x1b[114;2;82u": "refresh_dns",
"R": "refresh_dns",
"\x1b[114u": "update_single_dns",
"\x1b[27u": "exit_edit_entry",
"\t": "next_field",
"\x1b[Z": "prev_field",
"\x1b[9;2;9u": "prev_field",
"\x1b[99;5u": "quit",
}
actions_by_key = {
key: binding.action if hasattr(binding, "action") else binding[1]
for binding in HostsManagerApp.BINDINGS
for key in str(binding.key if hasattr(binding, "key") else binding[0]).split(
","
)
}
for sequence, expected_action in sequences_and_actions.items():
event = next(
event
for event in HostsXTermParser().feed(sequence)
if isinstance(event, Key)
)
assert actions_by_key[event.key] == expected_action
@pytest.mark.asyncio
async def test_enhanced_shift_n_opens_new_from_selected_but_caps_lock_n_does_not():
"""The real enhanced-key events distinguish Shift from Caps Lock at the app."""
@ -234,6 +302,43 @@ async def test_new_from_selected_is_available_at_the_minimum_viewport():
assert not isinstance(app.screen, AddEntryModal)
@pytest.mark.parametrize("sequence", ("\x1b[47;2;63u", "\x1b[223;2;63u"))
@pytest.mark.asyncio
async def test_enhanced_question_mark_shortcut_opens_and_closes_help_overlay(sequence):
"""Kitty Shift+? controls the dedicated help overlay."""
app = app_with_filterable_entries()
async with app.run_test(size=(120, 40)) as pilot:
app.query_one("#entries-table").focus()
question_mark = list(HostsXTermParser().feed(sequence))[0]
app.post_message(question_mark)
await pilot.pause()
assert isinstance(app.screen, HelpModal)
question_mark = list(HostsXTermParser().feed(sequence))[0]
app.post_message(question_mark)
await pilot.pause()
assert not isinstance(app.screen, HelpModal)
@pytest.mark.asyncio
async def test_enhanced_shift_r_shortcut_refreshes_all_dns_entries():
"""Kitty Shift+R invokes the documented refresh-all action."""
app = app_with_filterable_entries()
async with app.run_test(size=(120, 40)) as pilot:
app.query_one("#entries-table").focus()
shift_r = list(HostsXTermParser().feed("\x1b[114;2;82u"))[0]
app.post_message(shift_r)
await pilot.pause()
assert "Cannot resolve DNS names" in str(
app.query_one("#message-rail", Static).render()
)
@pytest.mark.asyncio
async def test_help_overlay_closes_to_the_control_that_opened_it():
"""Help is an overlay and returns keyboard focus to its opener."""