Support modifier-first Fenrir shortcuts

This commit is contained in:
Storm Dragon
2026-08-12 23:42:01 -04:00
parent ee441c319a
commit cca688328b
7 changed files with 386 additions and 9 deletions
+199
View File
@@ -0,0 +1,199 @@
from unittest.mock import Mock
import pytest
from fenrirscreenreader.core.eventData import FenrirEventType
from fenrirscreenreader.core.fenrirManager import FenrirManager
from fenrirscreenreader.core.inputManager import InputManager
def create_handle_input_manager(no_key_pressed):
manager = FenrirManager.__new__(FenrirManager)
manager.modifierInput = False
manager.modifier_prefix_input = True
manager.singleKeyCommand = False
manager.command = ""
manager.detect_shortcut_command = Mock()
manager.update_key_forward = Mock()
input_manager = Mock(
convert_event_name=Mock(side_effect=lambda key_name: key_name),
no_key_pressed=Mock(return_value=no_key_pressed),
)
manager.environment = {
"input": {"key_forward": 0},
"runtime": {
"DebugManager": Mock(write_debug_out=Mock()),
"InputManager": input_manager,
"ScreenManager": Mock(is_ignored_screen=Mock(return_value=False)),
"HelpManager": Mock(is_tutorial_mode=Mock(return_value=False)),
"VmenuManager": Mock(get_active=Mock(return_value=False)),
"DiffReviewManager": Mock(is_active=Mock(return_value=False)),
"SpeechHistoryManager": Mock(is_active=Mock(return_value=False)),
"CommandManager": Mock(execute_default_trigger=Mock()),
},
}
return manager, input_manager
@pytest.mark.unit
def test_input_manager_recognizes_conventional_modifier_prefix():
input_manager = InputManager()
input_manager.env = {"input": {"curr_input": ["KEY_CTRL", "KEY_SHIFT"]}}
assert input_manager.curr_input_is_modifier_prefix() is True
input_manager.env["input"]["curr_input"].append("KEY_S")
assert input_manager.curr_input_is_modifier_prefix() is False
@pytest.mark.unit
def test_first_conventional_modifier_starts_deferred_prefix():
manager = FenrirManager.__new__(FenrirManager)
manager.modifierInput = False
manager.modifier_prefix_input = False
manager.singleKeyCommand = False
manager.command = ""
input_manager = Mock(
is_key_press=Mock(return_value=True),
curr_input_has_command_modifier=Mock(return_value=False),
curr_input_is_modifier_prefix=Mock(return_value=True),
get_curr_shortcut=Mock(return_value=str([1, ["KEY_CTRL"]])),
get_command_for_shortcut=Mock(return_value=""),
)
manager.environment = {
"input": {
"key_forward": 0,
"prev_input": [],
"curr_input": ["KEY_CTRL"],
},
"runtime": {
"InputManager": input_manager,
"EventManager": Mock(put_to_event_queue=Mock()),
"VmenuManager": Mock(get_active=Mock(return_value=False)),
"DiffReviewManager": Mock(is_active=Mock(return_value=False)),
"SpeechHistoryManager": Mock(is_active=Mock(return_value=False)),
},
}
manager.detect_shortcut_command()
assert manager.modifier_prefix_input is True
@pytest.mark.unit
def test_fenrir_shortcut_is_dispatched_when_ctrl_is_pressed_first():
manager = FenrirManager.__new__(FenrirManager)
manager.modifierInput = False
manager.modifier_prefix_input = True
manager.singleKeyCommand = False
manager.command = ""
event_manager = Mock(put_to_event_queue=Mock())
input_manager = Mock(
is_key_press=Mock(return_value=False),
no_key_pressed=Mock(return_value=False),
curr_input_has_command_modifier=Mock(return_value=True),
get_curr_shortcut=Mock(
side_effect=[
str([1, ["KEY_CTRL", "KEY_FENRIR"]]),
str([1, ["KEY_CTRL", "KEY_FENRIR", "KEY_S"]]),
]
),
get_command_for_shortcut=Mock(side_effect=["", "SAVE_SETTINGS"]),
)
manager.environment = {
"input": {
"key_forward": 0,
"prev_input": ["KEY_CTRL"],
"curr_input": ["KEY_CTRL", "KEY_FENRIR"],
},
"runtime": {
"InputManager": input_manager,
"EventManager": event_manager,
"VmenuManager": Mock(get_active=Mock(return_value=False)),
"DiffReviewManager": Mock(is_active=Mock(return_value=False)),
"SpeechHistoryManager": Mock(is_active=Mock(return_value=False)),
},
}
manager.detect_shortcut_command()
assert manager.modifierInput is True
assert manager.modifier_prefix_input is False
event_manager.put_to_event_queue.assert_not_called()
manager.environment["input"]["prev_input"] = [
"KEY_CTRL",
"KEY_FENRIR",
]
manager.environment["input"]["curr_input"] = [
"KEY_CTRL",
"KEY_FENRIR",
"KEY_S",
]
manager.detect_shortcut_command()
event_manager.put_to_event_queue.assert_called_once_with(
FenrirEventType.execute_command, "SAVE_SETTINGS"
)
@pytest.mark.unit
def test_action_key_disarms_modifier_prefix():
manager = FenrirManager.__new__(FenrirManager)
manager.modifierInput = False
manager.modifier_prefix_input = True
manager.singleKeyCommand = False
manager.command = ""
input_manager = Mock(
is_key_press=Mock(return_value=False),
no_key_pressed=Mock(return_value=False),
curr_input_has_command_modifier=Mock(return_value=False),
curr_input_is_modifier_prefix=Mock(return_value=False),
get_curr_shortcut=Mock(return_value=str([1, ["KEY_C", "KEY_CTRL"]])),
get_command_for_shortcut=Mock(return_value=""),
)
manager.environment = {
"input": {
"key_forward": 0,
"prev_input": ["KEY_CTRL"],
"curr_input": ["KEY_C", "KEY_CTRL"],
},
"runtime": {
"InputManager": input_manager,
"EventManager": Mock(put_to_event_queue=Mock()),
"VmenuManager": Mock(get_active=Mock(return_value=False)),
"DiffReviewManager": Mock(is_active=Mock(return_value=False)),
"SpeechHistoryManager": Mock(is_active=Mock(return_value=False)),
},
}
manager.detect_shortcut_command()
assert manager.modifier_prefix_input is False
@pytest.mark.unit
def test_modifier_prefix_is_not_forwarded_before_chord_is_known():
manager, input_manager = create_handle_input_manager(False)
manager.handle_input(
{"data": {"event_name": "KEY_CTRL", "event_state": 1}}
)
input_manager.write_event_buffer.assert_not_called()
input_manager.clear_event_buffer.assert_not_called()
@pytest.mark.unit
def test_modifier_prefix_is_forwarded_when_released_unused():
manager, input_manager = create_handle_input_manager(True)
manager.handle_input(
{"data": {"event_name": "KEY_CTRL", "event_state": 0}}
)
input_manager.write_event_buffer.assert_called_once_with()
assert manager.modifier_prefix_input is False
@@ -10,6 +10,7 @@ from fenrirscreenreader.core.fenrirManager import FenrirManager
def test_speech_history_plain_key_modal_command_is_dispatched():
manager = FenrirManager.__new__(FenrirManager)
manager.modifierInput = False
manager.modifier_prefix_input = False
manager.singleKeyCommand = False
manager.command = ""
@@ -50,6 +51,7 @@ def test_speech_history_plain_key_modal_command_is_dispatched():
def test_vmenu_plain_key_modal_command_is_dispatched():
manager = FenrirManager.__new__(FenrirManager)
manager.modifierInput = False
manager.modifier_prefix_input = False
manager.singleKeyCommand = False
manager.command = ""
+60
View File
@@ -425,3 +425,63 @@ def test_x11_handle_key_event_keeps_event_buffer_for_input_manager():
assert x11.env["input"]["event_buffer"][0]["event_name"] == "KEY_KP0"
event_queue.put.assert_called_once()
@pytest.mark.unit
def test_x11_fenrir_press_emits_modifiers_from_raw_state_first():
x11 = X11Driver()
x11.active = True
x11.fenrir_keys = {"KEY_KP0"}
x11.interesting_keys = {"KEY_KP0"}
x11.env = {
"input": {
"curr_input": [],
"event_buffer": [],
},
"runtime": {
"InputManager": Mock(convert_event_name=lambda key: key),
"DebugManager": Mock(),
},
}
event_queue = Mock()
event = Mock(type=X.KeyPress, detail=90, state=X.ControlMask)
x11.keycode_to_key_name = Mock(return_value="KEY_KP0")
x11.handle_x_event(event, event_queue)
modifier_event = event_queue.put.call_args_list[0].args[0]
fenrir_event = event_queue.put.call_args_list[1].args[0]
assert modifier_event["data"]["event_name"] == "KEY_CTRL"
assert modifier_event["data"]["event_state"] == 1
assert fenrir_event["data"]["event_name"] == "KEY_KP0"
@pytest.mark.unit
def test_x11_fenrir_release_balances_synthesized_modifiers():
x11 = X11Driver()
x11.active = True
x11.fenrir_keys = {"KEY_KP0"}
x11.interesting_keys = {"KEY_KP0"}
x11.command_key_active = True
x11.chord_modifiers = {"KEY_CTRL"}
x11.env = {
"input": {
"curr_input": ["KEY_CTRL", "KEY_FENRIR"],
"event_buffer": [],
},
"runtime": {
"InputManager": Mock(convert_event_name=lambda key: key),
"DebugManager": Mock(),
},
}
event_queue = Mock()
event = Mock(type=X.KeyRelease, detail=90, state=X.ControlMask)
x11.keycode_to_key_name = Mock(return_value="KEY_KP0")
x11.handle_x_event(event, event_queue)
fenrir_event = event_queue.put.call_args_list[0].args[0]
modifier_event = event_queue.put.call_args_list[1].args[0]
assert fenrir_event["data"]["event_name"] == "KEY_KP0"
assert modifier_event["data"]["event_name"] == "KEY_CTRL"
assert modifier_event["data"]["event_state"] == 0