Fixed keyboard handling regression.

This commit is contained in:
Storm Dragon
2026-05-31 22:36:35 -04:00
parent 0c4fe50606
commit 2cb83632f9
3 changed files with 95 additions and 1 deletions
+1 -1
View File
@@ -4,5 +4,5 @@
# Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors.
version = "2026.05.30"
version = "2026.05.31"
code_name = "testing"
@@ -162,6 +162,7 @@ class driver(inputDriver):
self.fenrir_keys = set()
self.failed_grabs = 0
self.modifier_state = 0
self.modifier_interrupt_state = 0
def initialize(self, environment):
self.env = environment
@@ -194,6 +195,7 @@ class driver(inputDriver):
)
self.num_lock_mask = self.find_num_lock_mask()
self.refresh_modifier_state()
self.modifier_interrupt_state = self.modifier_state
self.refresh_interesting_keys()
self.refresh_grabs(force=True)
self.env["runtime"]["ProcessManager"].add_custom_event_thread(
@@ -274,6 +276,7 @@ class driver(inputDriver):
while active.value:
try:
self.refresh_grabs()
self.poll_modifier_interrupt_keys()
if not self.display.pending_events():
time.sleep(0.01)
continue
@@ -371,6 +374,56 @@ class driver(inputDriver):
"event_x_time": getattr(event, "time", X.CurrentTime),
}
def poll_modifier_interrupt_keys(self):
if not self.active or not self.should_poll_modifier_interrupt_keys():
return
try:
pointer = self.root.query_pointer()
current_state = getattr(pointer, "mask", 0)
except Exception:
return
previous_state = self.modifier_interrupt_state
self.modifier_interrupt_state = current_state
self.modifier_state = current_state
for key_name, modifier_mask in self.interrupt_modifier_masks():
if current_state & modifier_mask and not previous_state & modifier_mask:
self.interrupt_output_on_modifier_key(key_name)
def should_poll_modifier_interrupt_keys(self):
try:
settings_manager = self.env["runtime"]["SettingsManager"]
except Exception:
return False
if not settings_manager.get_setting_as_bool(
"keyboard", "interrupt_on_key_press"
):
return False
return (
settings_manager.get_setting(
"keyboard", "interrupt_on_key_press_filter"
).strip()
== ""
)
def interrupt_modifier_masks(self):
return [
("KEY_CTRL", X.ControlMask),
("KEY_SHIFT", X.ShiftMask),
("KEY_ALT", X.Mod1Mask),
]
def interrupt_output_on_modifier_key(self, key_name):
try:
self.env["runtime"]["OutputManager"].interrupt_output_async()
except Exception as e:
self.env["runtime"]["DebugManager"].write_debug_out(
"x11Driver modifier interrupt failed for "
+ key_name
+ ": "
+ str(e),
debug.DebugLevel.ERROR,
)
def refresh_modifier_state(self):
try:
pointer = self.root.query_pointer()
+41
View File
@@ -217,6 +217,47 @@ def test_x11_build_passive_grabs_for_fenrir_keys_and_shortcuts():
assert ("KEY_BACKSPACE", X.Mod4Mask, True) in grabs
@pytest.mark.unit
@pytest.mark.parametrize(
"modifier_mask",
[X.ControlMask, X.ShiftMask, X.Mod1Mask],
)
def test_x11_poll_modifier_interrupt_keys_interrupts_without_input_events(
modifier_mask,
):
x11 = X11Driver()
x11.active = True
x11.modifier_interrupt_state = 0
x11.modifier_state = 0
x11.root = Mock()
x11.root.query_pointer.return_value = Mock(mask=modifier_mask)
settings_manager = Mock()
settings_manager.get_setting_as_bool.return_value = True
settings_manager.get_setting.return_value = ""
output_manager = Mock()
x11.env = {
"input": {"event_buffer": []},
"runtime": {
"SettingsManager": settings_manager,
"OutputManager": output_manager,
"DebugManager": Mock(),
},
}
x11.poll_modifier_interrupt_keys()
output_manager.interrupt_output_async.assert_called_once()
assert x11.env["input"]["event_buffer"] == []
output_manager.interrupt_output_async.reset_mock()
x11.root.query_pointer.return_value = Mock(mask=0)
x11.poll_modifier_interrupt_keys()
output_manager.interrupt_output_async.assert_not_called()
assert x11.env["input"]["event_buffer"] == []
@pytest.mark.unit
def test_x11_optional_modifier_masks_can_exclude_numlock():
x11 = X11Driver()