diff --git a/src/fenrirscreenreader/commands/onCursorChange/54000-tab_completion.py b/src/fenrirscreenreader/commands/onCursorChange/54000-tab_completion.py new file mode 100644 index 00000000..a925f937 --- /dev/null +++ b/src/fenrirscreenreader/commands/onCursorChange/54000-tab_completion.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Fenrir TTY screen reader +# By Chrys, Storm Dragon, and contributors. + +from fenrirscreenreader.core.i18n import _ +from fenrirscreenreader.core.tabCompletionManager import TabCompletionManager + + +class command: + def __init__(self): + self.manager = TabCompletionManager() + + def initialize(self, environment): + self.env = environment + self.manager.initialize(environment) + + def shutdown(self): + pass + + def get_description(self): + return _("Announces tab completions on cursor movement") + + def run(self): + text = self.manager.process_update() + if not text: + return + + do_interrupt = True + if self.env["runtime"]["SettingsManager"].get_setting_as_bool( + "speech", "auto_read_incoming" + ): + do_interrupt = False + + self.env["runtime"]["OutputManager"].present_text( + text, + interrupt=do_interrupt, + announce_capital=True, + flush=False, + ) + + def set_callback(self, callback): + pass diff --git a/src/fenrirscreenreader/core/tabCompletionManager.py b/src/fenrirscreenreader/core/tabCompletionManager.py index f8d74e4f..54d86b06 100644 --- a/src/fenrirscreenreader/core/tabCompletionManager.py +++ b/src/fenrirscreenreader/core/tabCompletionManager.py @@ -39,6 +39,9 @@ class TabCompletionManager: def process_update(self): self._ensure_state() state = self.env["commandBuffer"]["tabCompletion"] + if self._was_recently_processed(state): + return "" + pending = state.get("pending") if not pending: pending = self._build_pending_from_recent_tab_update() @@ -62,6 +65,14 @@ class TabCompletionManager: state["lastProcessedTime"] = time.time() return spoken_text + def _was_recently_processed(self, state): + if state.get("lastProcessedDelta") != self.env["screen"]["new_delta"]: + return False + last_time = state.get("lastProcessedTime") + if not last_time: + return False + return time.time() - last_time <= self.timeout + def _build_pending_from_recent_tab_update(self): if not self._recent_tab_input(): return None diff --git a/src/fenrirscreenreader/fenrirVersion.py b/src/fenrirscreenreader/fenrirVersion.py index f0a3c583..1d1a9adc 100644 --- a/src/fenrirscreenreader/fenrirVersion.py +++ b/src/fenrirscreenreader/fenrirVersion.py @@ -4,5 +4,5 @@ # Fenrir TTY screen reader # By Chrys, Storm Dragon, and contributors. -version = "2026.05.08" +version = "2026.05.09" code_name = "testing" diff --git a/tests/unit/test_tab_completion_manager.py b/tests/unit/test_tab_completion_manager.py index b5ed1c56..1eb9241f 100644 --- a/tests/unit/test_tab_completion_manager.py +++ b/tests/unit/test_tab_completion_manager.py @@ -81,6 +81,43 @@ def test_unique_completion_speaks_inserted_suffix(): assert state["lastProcessedDelta"] == "cuments/" +@pytest.mark.unit +def test_small_completion_speaks_inserted_suffix(): + manager, env, _input_manager = _build_env( + "cd gi".ljust(20), {"x": 5, "y": 0} + ) + + manager.capture_if_tab() + _set_screen_update( + env, + "cd git/".ljust(20), + {"x": 7, "y": 0}, + delta="t/", + typing=True, + ) + + assert manager.process_update() == "t/" + + +@pytest.mark.unit +def test_completion_processed_once_for_cursor_and_screen_update(): + manager, env, _input_manager = _build_env( + "cd gi".ljust(20), {"x": 5, "y": 0} + ) + + manager.capture_if_tab() + _set_screen_update( + env, + "cd git/".ljust(20), + {"x": 7, "y": 0}, + delta="t/", + typing=True, + ) + + assert manager.process_update() == "t/" + assert manager.process_update() == "" + + @pytest.mark.unit def test_candidate_list_speaks_visible_list_without_cursor_advance(): old_text = "\n".join(["$ cd Do".ljust(20), "".ljust(20), "".ljust(20)])