Improve review spelling shortcuts

This commit is contained in:
Storm Dragon
2026-08-13 00:42:13 -04:00
parent cca688328b
commit a2e4a8a086
15 changed files with 404 additions and 271 deletions
+3 -6
View File
@@ -15,15 +15,12 @@ KEY_FENRIR,KEY_ALT,KEY_2=present_last_line
KEY_KP5=review_curr_word KEY_KP5=review_curr_word
KEY_KP4=review_prev_word KEY_KP4=review_prev_word
KEY_KP6=review_next_word KEY_KP6=review_next_word
KEY_FENRIR,KEY_SHIFT,KEY_KP5=review_curr_word_phonetic 2,KEY_KP5=review_curr_word_spell
KEY_FENRIR,KEY_SHIFT,KEY_KP4=review_prev_word_phonetic 3,KEY_KP5=review_curr_word_phonetic
KEY_FENRIR,KEY_SHIFT,KEY_KP6=review_next_word_phonetic
KEY_KP2=review_curr_char KEY_KP2=review_curr_char
KEY_KP1=review_prev_char KEY_KP1=review_prev_char
KEY_KP3=review_next_char KEY_KP3=review_next_char
KEY_FENRIR,KEY_SHIFT,KEY_KP2=review_curr_char_phonetic 2,KEY_KP2=review_curr_char_phonetic
KEY_FENRIR,KEY_SHIFT,KEY_KP1=review_prev_char_phonetic
KEY_FENRIR,KEY_SHIFT,KEY_KP3=review_next_char_phonetic
KEY_FENRIR,KEY_CTRL,KEY_KP8=review_up KEY_FENRIR,KEY_CTRL,KEY_KP8=review_up
KEY_FENRIR,KEY_CTRL,KEY_KP2=review_down KEY_FENRIR,KEY_CTRL,KEY_KP2=review_down
KEY_FENRIR,KEY_KPDOT=exit_review KEY_FENRIR,KEY_KPDOT=exit_review
+3 -6
View File
@@ -15,15 +15,12 @@ KEY_FENRIR,KEY_ALT,KEY_2=present_last_line
KEY_FENRIR,KEY_K=review_curr_word KEY_FENRIR,KEY_K=review_curr_word
KEY_FENRIR,KEY_J=review_prev_word KEY_FENRIR,KEY_J=review_prev_word
KEY_FENRIR,KEY_L=review_next_word KEY_FENRIR,KEY_L=review_next_word
KEY_FENRIR,KEY_ALT,KEY_K=review_curr_word_phonetic 2,KEY_FENRIR,KEY_K=review_curr_word_spell
KEY_FENRIR,KEY_ALT,KEY_J=review_prev_word_phonetic 3,KEY_FENRIR,KEY_K=review_curr_word_phonetic
KEY_FENRIR,KEY_ALT,KEY_L=review_next_word_phonetic
KEY_FENRIR,KEY_COMMA=review_curr_char KEY_FENRIR,KEY_COMMA=review_curr_char
KEY_FENRIR,KEY_M=review_prev_char KEY_FENRIR,KEY_M=review_prev_char
KEY_FENRIR,KEY_DOT=review_next_char KEY_FENRIR,KEY_DOT=review_next_char
KEY_FENRIR,KEY_ALT,KEY_COMMA=curr_char_phonetic 2,KEY_FENRIR,KEY_COMMA=review_curr_char_phonetic
KEY_FENRIR,KEY_ALT,KEY_M=prev_char_phonetic
KEY_FENRIR,KEY_ALT,KEY_DOT=next_char_phonetic
KEY_FENRIR,KEY_CTRL,KEY_I=review_up KEY_FENRIR,KEY_CTRL,KEY_I=review_up
KEY_FENRIR,KEY_CTRL,KEY_COMMA=review_down KEY_FENRIR,KEY_CTRL,KEY_COMMA=review_down
KEY_FENRIR,KEY_SLASH=exit_review KEY_FENRIR,KEY_SLASH=exit_review
@@ -38,7 +38,7 @@ class command:
self.env["screen"]["new_content_text"], self.env["screen"]["new_content_text"],
) )
if curr_word.isspace(): if not curr_word or curr_word.isspace():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
_("blank"), interrupt=True, flush=False _("blank"), interrupt=True, flush=False
) )
@@ -20,7 +20,7 @@ class command:
pass pass
def get_description(self): def get_description(self):
return _("Phonetically spells the next word and moves review to it") return _("Spells the current word")
def run(self): def run(self):
self.env["runtime"][ self.env["runtime"][
@@ -29,30 +29,28 @@ class command:
( (
self.env["screen"]["newCursorReview"]["x"], self.env["screen"]["newCursorReview"]["x"],
self.env["screen"]["newCursorReview"]["y"], self.env["screen"]["newCursorReview"]["y"],
next_word, curr_word,
end_of_screen, end_of_screen,
line_break, line_break,
) = word_utils.get_next_word( ) = word_utils.get_current_word(
self.env["screen"]["newCursorReview"]["x"], self.env["screen"]["newCursorReview"]["x"],
self.env["screen"]["newCursorReview"]["y"], self.env["screen"]["newCursorReview"]["y"],
self.env["screen"]["new_content_text"], self.env["screen"]["new_content_text"],
) )
if next_word.isspace(): if not curr_word or curr_word.isspace():
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
_("blank"), interrupt=True, flush=False _("blank"), interrupt=True, flush=False
) )
else: else:
first_sequence = True for index, curr_char in enumerate(curr_word):
for c in next_word: char_utils.present_char_for_review(
curr_char = char_utils.get_phonetic(c) self.env,
self.env["runtime"]["OutputManager"].present_text(
curr_char, curr_char,
interrupt=first_sequence, interrupt=index == 0,
announce_capital=True, announce_capital=True,
flush=False, flush=False,
) )
first_sequence = False
if end_of_screen: if end_of_screen:
if self.env["runtime"]["SettingsManager"].get_setting_as_bool( if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
"review", "end_of_screen" "review", "end_of_screen"
@@ -1,65 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors.
from fenrirscreenreader.core.i18n import _
from fenrirscreenreader.utils import char_utils
class command:
def __init__(self):
pass
def initialize(self, environment):
self.env = environment
def shutdown(self):
pass
def get_description(self):
return _(
"phonetically presents the next character and set review to it"
)
def run(self):
self.env["runtime"][
"CursorManager"
].enter_review_mode_curr_text_cursor()
(
self.env["screen"]["newCursorReview"]["x"],
self.env["screen"]["newCursorReview"]["y"],
next_char,
end_of_screen,
line_break,
) = char_utils.get_next_char(
self.env["screen"]["newCursorReview"]["x"],
self.env["screen"]["newCursorReview"]["y"],
self.env["screen"]["new_content_text"],
)
next_char = char_utils.get_phonetic(next_char)
self.env["runtime"]["OutputManager"].present_text(
next_char, interrupt=True, announce_capital=True, flush=False
)
if end_of_screen:
if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
"review", "end_of_screen"
):
self.env["runtime"]["OutputManager"].present_text(
_("end of screen"),
interrupt=True,
sound_icon="EndOfScreen",
)
if line_break:
if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
"review", "line_break"
):
self.env["runtime"]["OutputManager"].present_text(
_("line break"), interrupt=False, sound_icon="EndOfLine"
)
def set_callback(self, callback):
pass
@@ -1,65 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors.
from fenrirscreenreader.core.i18n import _
from fenrirscreenreader.utils import char_utils
class command:
def __init__(self):
pass
def initialize(self, environment):
self.env = environment
def shutdown(self):
pass
def get_description(self):
return _(
"phonetically presents the previous character and set review to it"
)
def run(self):
self.env["runtime"][
"CursorManager"
].enter_review_mode_curr_text_cursor()
(
self.env["screen"]["newCursorReview"]["x"],
self.env["screen"]["newCursorReview"]["y"],
prev_char,
end_of_screen,
line_break,
) = char_utils.get_prev_char(
self.env["screen"]["newCursorReview"]["x"],
self.env["screen"]["newCursorReview"]["y"],
self.env["screen"]["new_content_text"],
)
prev_char = char_utils.get_phonetic(prev_char)
self.env["runtime"]["OutputManager"].present_text(
prev_char, interrupt=True, announce_capital=True, flush=False
)
if end_of_screen:
if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
"review", "end_of_screen"
):
self.env["runtime"]["OutputManager"].present_text(
_("start of screen"),
interrupt=True,
sound_icon="StartOfScreen",
)
if line_break:
if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
"review", "line_break"
):
self.env["runtime"]["OutputManager"].present_text(
_("line break"), interrupt=False, sound_icon="EndOfLine"
)
def set_callback(self, callback):
pass
@@ -1,76 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors.
from fenrirscreenreader.core.i18n import _
from fenrirscreenreader.utils import char_utils
from fenrirscreenreader.utils import word_utils
class command:
def __init__(self):
pass
def initialize(self, environment):
self.env = environment
def shutdown(self):
pass
def get_description(self):
return _(
"Phonetically spells the previous word and moves review to it"
)
def run(self):
self.env["runtime"][
"CursorManager"
].enter_review_mode_curr_text_cursor()
(
self.env["screen"]["newCursorReview"]["x"],
self.env["screen"]["newCursorReview"]["y"],
prev_word,
end_of_screen,
line_break,
) = word_utils.get_prev_word(
self.env["screen"]["newCursorReview"]["x"],
self.env["screen"]["newCursorReview"]["y"],
self.env["screen"]["new_content_text"],
)
if prev_word.isspace():
self.env["runtime"]["OutputManager"].present_text(
_("blank"), interrupt=True, flush=False
)
else:
first_sequence = True
for c in prev_word:
curr_char = char_utils.get_phonetic(c)
self.env["runtime"]["OutputManager"].present_text(
curr_char,
interrupt=first_sequence,
announce_capital=True,
flush=False,
)
first_sequence = False
if end_of_screen:
if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
"review", "end_of_screen"
):
self.env["runtime"]["OutputManager"].present_text(
_("start of screen"),
interrupt=True,
sound_icon="StartOfScreen",
)
if line_break:
if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
"review", "line_break"
):
self.env["runtime"]["OutputManager"].present_text(
_("line break"), interrupt=False, sound_icon="EndOfLine"
)
def set_callback(self, callback):
pass
@@ -82,9 +82,6 @@ class FenrirManager:
else: else:
return return
if self.environment["runtime"]["InputManager"].no_key_pressed():
self.environment["runtime"]["InputManager"].clear_last_deep_input()
if self.environment["runtime"]["ScreenManager"].is_ignored_screen(): if self.environment["runtime"]["ScreenManager"].is_ignored_screen():
self.environment["runtime"]["InputManager"].write_event_buffer() self.environment["runtime"]["InputManager"].write_event_buffer()
else: else:
+6 -9
View File
@@ -196,8 +196,6 @@ class InputManager:
self.env["input"]["curr_input"] = sorted( self.env["input"]["curr_input"] = sorted(
self.env["input"]["curr_input"] self.env["input"]["curr_input"]
) )
elif len(self.env["input"]["curr_input"]) == 0:
self.env["input"]["shortcut_repeat"] = 1
self.lastInputTime = time.time() self.lastInputTime = time.time()
elif event_data["event_state"] == 1: elif event_data["event_state"] == 1:
if not event_data["event_name"] in self.env["input"]["curr_input"]: if not event_data["event_name"] in self.env["input"]["curr_input"]:
@@ -206,19 +204,18 @@ class InputManager:
self.env["input"]["curr_input"] = sorted( self.env["input"]["curr_input"] = sorted(
self.env["input"]["curr_input"] self.env["input"]["curr_input"]
) )
if len(self.lastDeepestInput) < len( if self.lastDeepestInput == self.env["input"]["curr_input"]:
self.env["input"]["curr_input"]
):
self.set_last_deepest_input(
self.env["input"]["curr_input"].copy()
)
elif self.lastDeepestInput == self.env["input"]["curr_input"]:
if time.time() - self.lastInputTime <= self.env["runtime"][ if time.time() - self.lastInputTime <= self.env["runtime"][
"SettingsManager" "SettingsManager"
].get_setting_as_float("keyboard", "double_tap_timeout"): ].get_setting_as_float("keyboard", "double_tap_timeout"):
self.env["input"]["shortcut_repeat"] += 1 self.env["input"]["shortcut_repeat"] += 1
else: else:
self.env["input"]["shortcut_repeat"] = 1 self.env["input"]["shortcut_repeat"] = 1
else:
self.env["input"]["shortcut_repeat"] = 1
self.set_last_deepest_input(
self.env["input"]["curr_input"].copy()
)
self.handle_led_states(event_data) self.handle_led_states(event_data)
self.lastInputTime = time.time() self.lastInputTime = time.time()
elif event_data["event_state"] == 2: elif event_data["event_state"] == 2:
+1 -1
View File
@@ -4,5 +4,5 @@
# Fenrir TTY screen reader # Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors. # By Chrys, Storm Dragon, and contributors.
version = "2026.08.12" version = "2026.08.13"
code_name = "testing" code_name = "testing"
@@ -172,6 +172,7 @@ class driver(inputDriver):
self.modifier_interrupt_state = 0 self.modifier_interrupt_state = 0
self.command_key_active = False self.command_key_active = False
self.chord_modifiers = set() self.chord_modifiers = set()
self.chord_keys = set()
def initialize(self, environment): def initialize(self, environment):
self.env = environment self.env = environment
@@ -306,6 +307,10 @@ class driver(inputDriver):
def handle_x_event(self, event, event_queue): def handle_x_event(self, event, event_queue):
event_type = getattr(event, "type", None) event_type = getattr(event, "type", None)
if event_type in [X.FocusIn, X.FocusOut] and getattr(
event, "mode", X.NotifyNormal
) in [X.NotifyGrab, X.NotifyUngrab]:
return
if event_type == X.FocusIn: if event_type == X.FocusIn:
self.active = True self.active = True
self.clear_event_buffer() self.clear_event_buffer()
@@ -329,6 +334,7 @@ class driver(inputDriver):
if is_command_key and input_event["event_state"] == 1: if is_command_key and input_event["event_state"] == 1:
self.start_command_chord(input_event, event_queue) self.start_command_chord(input_event, event_queue)
elif self.command_key_active and not is_command_key: elif self.command_key_active and not is_command_key:
self.track_chord_key(input_event)
self.track_chord_modifier(input_event) self.track_chord_modifier(input_event)
self.write_debug( self.write_debug(
"x11Driver key event " "x11Driver key event "
@@ -361,6 +367,7 @@ class driver(inputDriver):
def start_command_chord(self, input_event, event_queue): def start_command_chord(self, input_event, event_queue):
self.command_key_active = True self.command_key_active = True
self.chord_keys.clear()
raw_state = input_event.get("event_raw_state", 0) raw_state = input_event.get("event_raw_state", 0)
current_input = self.env["input"]["curr_input"] current_input = self.env["input"]["curr_input"]
for key_name, modifier_mask in self.canonical_modifier_masks: for key_name, modifier_mask in self.canonical_modifier_masks:
@@ -368,7 +375,7 @@ class driver(inputDriver):
continue continue
self.chord_modifiers.add(key_name) self.chord_modifiers.add(key_name)
if key_name not in current_input: if key_name not in current_input:
self.queue_synthetic_modifier( self.queue_synthetic_key(
key_name, 1, input_event, event_queue key_name, 1, input_event, event_queue
) )
@@ -381,6 +388,15 @@ class driver(inputDriver):
elif input_event["event_state"] == 0: elif input_event["event_state"] == 0:
self.chord_modifiers.discard(key_name) self.chord_modifiers.discard(key_name)
def track_chord_key(self, input_event):
key_name = input_event["event_name"]
if key_name in self.modifier_key_names:
return
if input_event["event_state"] == 1:
self.chord_keys.add(key_name)
elif input_event["event_state"] == 0:
self.chord_keys.discard(key_name)
def canonical_modifier_name(self, key_name): def canonical_modifier_name(self, key_name):
modifier_mask = self.modifier_masks.get(key_name, 0) modifier_mask = self.modifier_masks.get(key_name, 0)
for canonical_name, canonical_mask in self.canonical_modifier_masks: for canonical_name, canonical_mask in self.canonical_modifier_masks:
@@ -389,12 +405,15 @@ class driver(inputDriver):
return None return None
def finish_command_chord(self, input_event, event_queue): def finish_command_chord(self, input_event, event_queue):
for key_name in sorted(self.chord_keys):
self.queue_synthetic_key(key_name, 0, input_event, event_queue)
self.chord_keys.clear()
for key_name in sorted(self.chord_modifiers): for key_name in sorted(self.chord_modifiers):
self.queue_synthetic_modifier(key_name, 0, input_event, event_queue) self.queue_synthetic_key(key_name, 0, input_event, event_queue)
self.chord_modifiers.clear() self.chord_modifiers.clear()
self.command_key_active = False self.command_key_active = False
def queue_synthetic_modifier( def queue_synthetic_key(
self, key_name, event_state, source_event, event_queue self, key_name, event_state, source_event, event_queue
): ):
synthetic_event = { synthetic_event = {
@@ -781,6 +800,7 @@ class driver(inputDriver):
def reset_input_state(self): def reset_input_state(self):
self.command_key_active = False self.command_key_active = False
self.chord_modifiers.clear() self.chord_modifiers.clear()
self.chord_keys.clear()
try: try:
self.env["runtime"]["InputManager"].reset_input_state() self.env["runtime"]["InputManager"].reset_input_state()
except Exception: except Exception:
+131
View File
@@ -7,6 +7,42 @@ from fenrirscreenreader.core.fenrirManager import FenrirManager
from fenrirscreenreader.core.inputManager import InputManager from fenrirscreenreader.core.inputManager import InputManager
def create_input_manager():
input_driver = Mock(get_led_state=Mock(return_value=False))
settings_manager = Mock()
settings_manager.get_setting_as_float.return_value = 0.2
manager = InputManager()
manager.env = {
"input": {
"curr_input": [],
"prev_input": [],
"event_buffer": [object()],
"shortcut_repeat": 1,
"old_num_lock": False,
"new_num_lock": False,
"old_caps_lock": False,
"new_caps_lock": False,
"old_scroll_lock": False,
"new_scroll_lock": False,
},
"runtime": {
"DebugManager": Mock(),
"InputDriver": input_driver,
"SettingsManager": settings_manager,
},
}
manager.lastDeepestInput = []
manager.lastInputTime = 0
manager.handle_led_states = Mock()
return manager
def send_input(manager, event_name, event_state):
manager.handle_input_event(
{"event_name": event_name, "event_state": event_state}
)
def create_handle_input_manager(no_key_pressed): def create_handle_input_manager(no_key_pressed):
manager = FenrirManager.__new__(FenrirManager) manager = FenrirManager.__new__(FenrirManager)
manager.modifierInput = False manager.modifierInput = False
@@ -48,6 +84,100 @@ def test_input_manager_recognizes_conventional_modifier_prefix():
assert input_manager.curr_input_is_modifier_prefix() is False assert input_manager.curr_input_is_modifier_prefix() is False
@pytest.mark.unit
def test_bare_key_taps_advance_shortcut_repeat(monkeypatch):
manager = create_input_manager()
monkeypatch.setattr(
"fenrirscreenreader.core.inputManager.time.time", lambda: 1.0
)
send_input(manager, "KEY_KP5", 1)
assert manager.env["input"]["shortcut_repeat"] == 1
send_input(manager, "KEY_KP5", 0)
send_input(manager, "KEY_KP5", 1)
assert manager.env["input"]["shortcut_repeat"] == 2
send_input(manager, "KEY_KP5", 0)
send_input(manager, "KEY_KP5", 1)
assert manager.env["input"]["shortcut_repeat"] == 3
@pytest.mark.unit
def test_bare_key_taps_resolve_progressive_shortcuts(monkeypatch):
manager = create_input_manager()
monkeypatch.setattr(
"fenrirscreenreader.core.inputManager.time.time", lambda: 1.0
)
manager.env["runtime"]["CursorManager"] = Mock(
should_process_numpad_commands=Mock(return_value=True)
)
manager.env["bindings"] = {
str([1, ["KEY_KP5"]]): "REVIEW_CURR_WORD",
str([2, ["KEY_KP5"]]): "REVIEW_CURR_WORD_SPELL",
str([3, ["KEY_KP5"]]): "REVIEW_CURR_WORD_PHONETIC",
}
resolved_commands = []
for _tap in range(3):
send_input(manager, "KEY_KP5", 1)
shortcut = manager.get_curr_shortcut()
resolved_commands.append(manager.get_command_for_shortcut(shortcut))
send_input(manager, "KEY_KP5", 0)
assert resolved_commands == [
"REVIEW_CURR_WORD",
"REVIEW_CURR_WORD_SPELL",
"REVIEW_CURR_WORD_PHONETIC",
]
@pytest.mark.unit
def test_different_bare_key_starts_new_repeat_sequence(monkeypatch):
manager = create_input_manager()
monkeypatch.setattr(
"fenrirscreenreader.core.inputManager.time.time", lambda: 1.0
)
send_input(manager, "KEY_KP5", 1)
send_input(manager, "KEY_KP5", 0)
send_input(manager, "KEY_KP5", 1)
assert manager.env["input"]["shortcut_repeat"] == 2
send_input(manager, "KEY_KP5", 0)
send_input(manager, "KEY_KP2", 1)
assert manager.env["input"]["shortcut_repeat"] == 1
@pytest.mark.unit
def test_bare_key_tap_after_timeout_starts_new_sequence(monkeypatch):
manager = create_input_manager()
current_time = [1.0]
monkeypatch.setattr(
"fenrirscreenreader.core.inputManager.time.time",
lambda: current_time[0],
)
send_input(manager, "KEY_KP5", 1)
send_input(manager, "KEY_KP5", 0)
current_time[0] = 2.0
send_input(manager, "KEY_KP5", 1)
assert manager.env["input"]["shortcut_repeat"] == 1
@pytest.mark.unit
def test_modifier_chord_taps_still_advance_shortcut_repeat(monkeypatch):
manager = create_input_manager()
monkeypatch.setattr(
"fenrirscreenreader.core.inputManager.time.time", lambda: 1.0
)
send_input(manager, "KEY_FENRIR", 1)
send_input(manager, "KEY_T", 1)
assert manager.env["input"]["shortcut_repeat"] == 1
send_input(manager, "KEY_T", 0)
send_input(manager, "KEY_T", 1)
assert manager.env["input"]["shortcut_repeat"] == 2
@pytest.mark.unit @pytest.mark.unit
def test_first_conventional_modifier_starts_deferred_prefix(): def test_first_conventional_modifier_starts_deferred_prefix():
manager = FenrirManager.__new__(FenrirManager) manager = FenrirManager.__new__(FenrirManager)
@@ -196,4 +326,5 @@ def test_modifier_prefix_is_forwarded_when_released_unused():
) )
input_manager.write_event_buffer.assert_called_once_with() input_manager.write_event_buffer.assert_called_once_with()
input_manager.clear_last_deep_input.assert_not_called()
assert manager.modifier_prefix_input is False assert manager.modifier_prefix_input is False
+147
View File
@@ -0,0 +1,147 @@
import importlib.util
from pathlib import Path
from unittest.mock import Mock
import pytest
from fenrirscreenreader.core.inputManager import InputManager
PROJECT_ROOT = Path(__file__).resolve().parents[2]
COMMANDS_DIR = (
PROJECT_ROOT / "src" / "fenrirscreenreader" / "commands" / "commands"
)
KEYBOARD_DIR = PROJECT_ROOT / "config" / "keyboard"
def load_command(name):
spec = importlib.util.spec_from_file_location(
f"fenrir_{name}", COMMANDS_DIR / f"{name}.py"
)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module.command()
def run_command(name, content, cursor_x=0, cursor_y=0):
output_manager = Mock()
settings_manager = Mock()
settings_manager.get_setting_as_bool.return_value = True
env = {
"punctuation": {"PUNCTDICT": {" ": "space"}},
"screen": {
"newCursorReview": {"x": cursor_x, "y": cursor_y},
"new_content_text": content,
},
"runtime": {
"CursorManager": Mock(),
"OutputManager": output_manager,
"SettingsManager": settings_manager,
},
}
command = load_command(name)
command.initialize(env)
command.run()
return output_manager
def load_bindings(layout):
manager = InputManager()
manager.env = {
"bindings": {},
"rawBindings": {},
"runtime": {"DebugManager": Mock()},
}
manager.load_shortcuts(KEYBOARD_DIR / f"{layout}.conf")
return manager.env["bindings"]
@pytest.mark.unit
def test_current_word_is_spelled_one_character_at_a_time():
output_manager = run_command(
"review_curr_word_spell", "hello world", cursor_x=2
)
calls = output_manager.present_text.call_args_list
assert [call.args[0] for call in calls] == list("hello")
assert calls[0].kwargs == {
"interrupt": True,
"ignore_punctuation": True,
"announce_capital": True,
"flush": False,
}
assert all(call.kwargs["interrupt"] is False for call in calls[1:])
@pytest.mark.unit
def test_current_word_spelling_preserves_capitals_and_punctuation():
output_manager = run_command("review_curr_word_spell", "Hi!", cursor_x=1)
assert [
call.args[0] for call in output_manager.present_text.call_args_list
] == ["H", "i", "!"]
@pytest.mark.unit
@pytest.mark.parametrize(
"command_name",
["review_curr_word_spell", "review_curr_word_phonetic"],
)
def test_current_word_spelling_announces_blank_when_no_word_exists(
command_name,
):
output_manager = run_command(command_name, " ")
output_manager.present_text.assert_called_once_with(
"blank", interrupt=True, flush=False
)
@pytest.mark.unit
def test_current_word_phonetic_spelling_uses_phonetic_names():
output_manager = run_command(
"review_curr_word_phonetic", "Az!", cursor_x=1
)
calls = output_manager.present_text.call_args_list
assert [call.args[0] for call in calls] == ["Alpha", "zulu", "!"]
assert calls[0].kwargs["interrupt"] is True
assert all(call.kwargs["interrupt"] is False for call in calls[1:])
@pytest.mark.unit
@pytest.mark.parametrize(
("layout", "character_keys", "word_keys"),
[
("desktop", ["KEY_KP2"], ["KEY_KP5"]),
("laptop", ["KEY_COMMA", "KEY_FENRIR"], ["KEY_FENRIR", "KEY_K"]),
],
)
def test_progressive_review_bindings(layout, character_keys, word_keys):
bindings = load_bindings(layout)
assert bindings[str([1, character_keys])] == "REVIEW_CURR_CHAR"
assert bindings[str([2, character_keys])] == "REVIEW_CURR_CHAR_PHONETIC"
assert bindings[str([1, word_keys])] == "REVIEW_CURR_WORD"
assert bindings[str([2, word_keys])] == "REVIEW_CURR_WORD_SPELL"
assert bindings[str([3, word_keys])] == "REVIEW_CURR_WORD_PHONETIC"
removed_commands = {
"REVIEW_PREV_CHAR_PHONETIC",
"REVIEW_NEXT_CHAR_PHONETIC",
"REVIEW_PREV_WORD_PHONETIC",
"REVIEW_NEXT_WORD_PHONETIC",
}
assert removed_commands.isdisjoint(bindings.values())
@pytest.mark.unit
def test_obsolete_phonetic_navigation_commands_are_removed():
removed_commands = [
"review_prev_char_phonetic.py",
"review_next_char_phonetic.py",
"review_prev_word_phonetic.py",
"review_next_word_phonetic.py",
]
assert all(not (COMMANDS_DIR / name).exists() for name in removed_commands)
+74
View File
@@ -427,6 +427,43 @@ def test_x11_handle_key_event_keeps_event_buffer_for_input_manager():
event_queue.put.assert_called_once() event_queue.put.assert_called_once()
@pytest.mark.unit
@pytest.mark.parametrize(
("event_type", "event_mode"),
[
(X.FocusOut, X.NotifyGrab),
(X.FocusIn, X.NotifyUngrab),
],
)
def test_x11_transient_grab_focus_events_preserve_input_state(
event_type, event_mode
):
x11 = X11Driver()
x11.active = True
x11.clear_event_buffer = Mock()
x11.reset_input_state = Mock()
event = Mock(type=event_type, mode=event_mode)
x11.handle_x_event(event, Mock())
assert x11.active is True
x11.clear_event_buffer.assert_not_called()
x11.reset_input_state.assert_not_called()
@pytest.mark.unit
def test_x11_normal_focus_out_resets_input_state():
x11 = X11Driver()
x11.active = True
x11.reset_input_state = Mock()
event = Mock(type=X.FocusOut, mode=X.NotifyNormal)
x11.handle_x_event(event, Mock())
assert x11.active is False
x11.reset_input_state.assert_called_once_with()
@pytest.mark.unit @pytest.mark.unit
def test_x11_fenrir_press_emits_modifiers_from_raw_state_first(): def test_x11_fenrir_press_emits_modifiers_from_raw_state_first():
x11 = X11Driver() x11 = X11Driver()
@@ -485,3 +522,40 @@ def test_x11_fenrir_release_balances_synthesized_modifiers():
assert fenrir_event["data"]["event_name"] == "KEY_KP0" assert fenrir_event["data"]["event_name"] == "KEY_KP0"
assert modifier_event["data"]["event_name"] == "KEY_CTRL" assert modifier_event["data"]["event_name"] == "KEY_CTRL"
assert modifier_event["data"]["event_state"] == 0 assert modifier_event["data"]["event_state"] == 0
@pytest.mark.unit
def test_x11_fenrir_release_balances_held_chord_key():
x11 = X11Driver()
x11.active = True
x11.fenrir_keys = {"KEY_KP0"}
x11.interesting_keys = {"KEY_KP0", "KEY_KP9"}
x11.env = {
"input": {
"curr_input": [],
"event_buffer": [],
},
"runtime": {
"InputManager": Mock(convert_event_name=lambda key: key),
"DebugManager": Mock(),
},
}
event_queue = Mock()
key_names = {90: "KEY_KP0", 81: "KEY_KP9"}
x11.keycode_to_key_name = Mock(
side_effect=lambda keycode, state=0: key_names[keycode]
)
x11.handle_x_event(
Mock(type=X.KeyPress, detail=90, state=0), event_queue
)
x11.handle_x_event(
Mock(type=X.KeyPress, detail=81, state=0), event_queue
)
x11.handle_x_event(
Mock(type=X.KeyRelease, detail=90, state=0), event_queue
)
synthetic_release = event_queue.put.call_args_list[-1].args[0]
assert synthetic_release["data"]["event_name"] == "KEY_KP9"
assert synthetic_release["data"]["event_state"] == 0
+7 -26
View File
@@ -108,13 +108,12 @@ msgstr ""
#: ../src/fenrirscreenreader\commands\commands\review_curr_line.py:27 #: ../src/fenrirscreenreader\commands\commands\review_curr_line.py:27
#: ../src/fenrirscreenreader\commands\commands\review_curr_word.py:27 #: ../src/fenrirscreenreader\commands\commands\review_curr_word.py:27
#: ../src/fenrirscreenreader\commands\commands\review_curr_word_phonetic.py:27 #: ../src/fenrirscreenreader\commands\commands\review_curr_word_phonetic.py:27
#: ../src/fenrirscreenreader\commands\commands\review_curr_word_spell.py:43
#: ../src/fenrirscreenreader\commands\commands\review_line_begin.py:27 #: ../src/fenrirscreenreader\commands\commands\review_line_begin.py:27
#: ../src/fenrirscreenreader\commands\commands\review_next_line.py:29 #: ../src/fenrirscreenreader\commands\commands\review_next_line.py:29
#: ../src/fenrirscreenreader\commands\commands\review_next_word.py:29 #: ../src/fenrirscreenreader\commands\commands\review_next_word.py:29
#: ../src/fenrirscreenreader\commands\commands\review_next_word_phonetic.py:27
#: ../src/fenrirscreenreader\commands\commands\review_prev_line.py:27 #: ../src/fenrirscreenreader\commands\commands\review_prev_line.py:27
#: ../src/fenrirscreenreader\commands\commands\review_prev_word.py:27 #: ../src/fenrirscreenreader\commands\commands\review_prev_word.py:27
#: ../src/fenrirscreenreader\commands\commands\review_prev_word_phonetic.py:27
#: ../src/fenrirscreenreader\commands\onCursorChange\65000-present_line_if_cursor_change_vertical.py:37 #: ../src/fenrirscreenreader\commands\onCursorChange\65000-present_line_if_cursor_change_vertical.py:37
#: ../src/fenrirscreenreader\commands\onScreenUpdate\60000-history.py:59 #: ../src/fenrirscreenreader\commands\onScreenUpdate\60000-history.py:59
msgid "blank" msgid "blank"
@@ -513,31 +512,25 @@ msgstr ""
#: ../src/fenrirscreenreader\commands\commands\review_curr_word.py:32 #: ../src/fenrirscreenreader\commands\commands\review_curr_word.py:32
#: ../src/fenrirscreenreader\commands\commands\review_curr_word_phonetic.py:36 #: ../src/fenrirscreenreader\commands\commands\review_curr_word_phonetic.py:36
#: ../src/fenrirscreenreader\commands\commands\review_curr_word_spell.py:59
#: ../src/fenrirscreenreader\commands\commands\review_down.py:27 #: ../src/fenrirscreenreader\commands\commands\review_down.py:27
#: ../src/fenrirscreenreader\commands\commands\review_next_char.py:28 #: ../src/fenrirscreenreader\commands\commands\review_next_char.py:28
#: ../src/fenrirscreenreader\commands\commands\review_next_char_phonetic.py:30
#: ../src/fenrirscreenreader\commands\commands\review_next_line.py:34 #: ../src/fenrirscreenreader\commands\commands\review_next_line.py:34
#: ../src/fenrirscreenreader\commands\commands\review_next_word.py:34 #: ../src/fenrirscreenreader\commands\commands\review_next_word.py:34
#: ../src/fenrirscreenreader\commands\commands\review_next_word_phonetic.py:36
#: ../src/fenrirscreenreader\commands\commands\review_prev_char.py:31 #: ../src/fenrirscreenreader\commands\commands\review_prev_char.py:31
#: ../src/fenrirscreenreader\commands\commands\review_prev_char_phonetic.py:30
#: ../src/fenrirscreenreader\commands\commands\review_prev_line.py:32 #: ../src/fenrirscreenreader\commands\commands\review_prev_line.py:32
#: ../src/fenrirscreenreader\commands\commands\review_prev_word.py:32 #: ../src/fenrirscreenreader\commands\commands\review_prev_word.py:32
#: ../src/fenrirscreenreader\commands\commands\review_prev_word_phonetic.py:36
#: ../src/fenrirscreenreader\commands\commands\review_up.py:27 #: ../src/fenrirscreenreader\commands\commands\review_up.py:27
msgid "end of screen" msgid "end of screen"
msgstr "" msgstr ""
#: ../src/fenrirscreenreader\commands\commands\review_curr_word.py:35 #: ../src/fenrirscreenreader\commands\commands\review_curr_word.py:35
#: ../src/fenrirscreenreader\commands\commands\review_curr_word_phonetic.py:39 #: ../src/fenrirscreenreader\commands\commands\review_curr_word_phonetic.py:39
#: ../src/fenrirscreenreader\commands\commands\review_curr_word_spell.py:68
#: ../src/fenrirscreenreader\commands\commands\review_next_char.py:31 #: ../src/fenrirscreenreader\commands\commands\review_next_char.py:31
#: ../src/fenrirscreenreader\commands\commands\review_next_char_phonetic.py:33
#: ../src/fenrirscreenreader\commands\commands\review_next_word.py:37 #: ../src/fenrirscreenreader\commands\commands\review_next_word.py:37
#: ../src/fenrirscreenreader\commands\commands\review_next_word_phonetic.py:39
#: ../src/fenrirscreenreader\commands\commands\review_prev_char.py:34 #: ../src/fenrirscreenreader\commands\commands\review_prev_char.py:34
#: ../src/fenrirscreenreader\commands\commands\review_prev_char_phonetic.py:33
#: ../src/fenrirscreenreader\commands\commands\review_prev_word.py:35 #: ../src/fenrirscreenreader\commands\commands\review_prev_word.py:35
#: ../src/fenrirscreenreader\commands\commands\review_prev_word_phonetic.py:39
#: ../src/fenrirscreenreader\commands\commands\review_up.py:30 #: ../src/fenrirscreenreader\commands\commands\review_up.py:30
msgid "line break" msgid "line break"
msgstr "" msgstr ""
@@ -546,6 +539,10 @@ msgstr ""
msgid "Phonetically spells the current word" msgid "Phonetically spells the current word"
msgstr "" msgstr ""
#: ../src/fenrirscreenreader\commands\commands\review_curr_word_spell.py:23
msgid "Spells the current word"
msgstr ""
#: ../src/fenrirscreenreader\commands\commands\review_down.py:18 #: ../src/fenrirscreenreader\commands\commands\review_down.py:18
msgid "Move review to the character below the current position" msgid "Move review to the character below the current position"
msgstr "" msgstr ""
@@ -590,10 +587,6 @@ msgstr ""
msgid "Moves review to the next character " msgid "Moves review to the next character "
msgstr "" msgstr ""
#: ../src/fenrirscreenreader\commands\commands\review_next_char_phonetic.py:18
msgid "phonetically presents the next character and set review to it"
msgstr ""
#: ../src/fenrirscreenreader\commands\commands\review_next_line.py:18 #: ../src/fenrirscreenreader\commands\commands\review_next_line.py:18
msgid "moves review to the next line " msgid "moves review to the next line "
msgstr "" msgstr ""
@@ -602,18 +595,10 @@ msgstr ""
msgid "moves review to the next word " msgid "moves review to the next word "
msgstr "" msgstr ""
#: ../src/fenrirscreenreader\commands\commands\review_next_word_phonetic.py:19
msgid "Phonetically spells the next word and moves review to it"
msgstr ""
#: ../src/fenrirscreenreader\commands\commands\review_prev_char.py:18 #: ../src/fenrirscreenreader\commands\commands\review_prev_char.py:18
msgid "moves review to the previous character " msgid "moves review to the previous character "
msgstr "" msgstr ""
#: ../src/fenrirscreenreader\commands\commands\review_prev_char_phonetic.py:18
msgid "phonetically presents the previous character and set review to it"
msgstr ""
#: ../src/fenrirscreenreader\commands\commands\review_prev_line.py:18 #: ../src/fenrirscreenreader\commands\commands\review_prev_line.py:18
msgid "moves review to the previous line " msgid "moves review to the previous line "
msgstr "" msgstr ""
@@ -622,10 +607,6 @@ msgstr ""
msgid "moves review focus to the previous word " msgid "moves review focus to the previous word "
msgstr "" msgstr ""
#: ../src/fenrirscreenreader\commands\commands\review_prev_word_phonetic.py:19
msgid "Phonetically spells the previous word and moves review to it"
msgstr ""
#: ../src/fenrirscreenreader\commands\commands\review_top.py:18 #: ../src/fenrirscreenreader\commands\commands\review_top.py:18
msgid "move review to top of screen" msgid "move review to top of screen"
msgstr "" msgstr ""