That does it, I'm breaking out codex on this one. Take that tab completion!
This commit is contained in:
@@ -0,0 +1,93 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Fenrir TTY screen reader
|
||||||
|
# By Chrys, Storm Dragon, and contributors.
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
from fenrirscreenreader.core.i18n import _
|
||||||
|
|
||||||
|
|
||||||
|
class command:
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def initialize(self, environment):
|
||||||
|
self.env = environment
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_description(self):
|
||||||
|
return _("Announces large text insertions at the cursor")
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
if self.env["runtime"]["ScreenManager"].is_screen_change():
|
||||||
|
return
|
||||||
|
if not self.env["runtime"]["ScreenManager"].is_delta():
|
||||||
|
return
|
||||||
|
|
||||||
|
x_move = (
|
||||||
|
self.env["screen"]["new_cursor"]["x"]
|
||||||
|
- self.env["screen"]["old_cursor"]["x"]
|
||||||
|
)
|
||||||
|
if x_move < 5:
|
||||||
|
return
|
||||||
|
if self._is_recent_tab_input():
|
||||||
|
return
|
||||||
|
|
||||||
|
delta_text = self.env["screen"]["new_delta"]
|
||||||
|
if not self._matches_cursor_insert(x_move, delta_text):
|
||||||
|
return
|
||||||
|
|
||||||
|
curr_delta = delta_text
|
||||||
|
if (
|
||||||
|
len(curr_delta.strip()) != len(curr_delta)
|
||||||
|
and curr_delta.strip() != ""
|
||||||
|
):
|
||||||
|
curr_delta = curr_delta.strip()
|
||||||
|
if not curr_delta:
|
||||||
|
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(
|
||||||
|
curr_delta,
|
||||||
|
interrupt=do_interrupt,
|
||||||
|
announce_capital=True,
|
||||||
|
flush=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _is_recent_tab_input(self):
|
||||||
|
input_manager = self.env["runtime"].get("InputManager")
|
||||||
|
if not input_manager:
|
||||||
|
return False
|
||||||
|
if input_manager.get_last_deepest_input() in [["KEY_TAB"]]:
|
||||||
|
return True
|
||||||
|
|
||||||
|
last_event = input_manager.get_last_event()
|
||||||
|
if not last_event or last_event.get("event_name") != "KEY_TAB":
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
return time.time() - input_manager.get_last_input_time() <= 0.5
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _matches_cursor_insert(self, x_move, delta_text):
|
||||||
|
if not delta_text or "\n" in delta_text:
|
||||||
|
return False
|
||||||
|
|
||||||
|
delta_len = len(delta_text)
|
||||||
|
if x_move == delta_len:
|
||||||
|
return True
|
||||||
|
if abs(x_move - delta_len) <= 2:
|
||||||
|
return True
|
||||||
|
return delta_len > 10 and abs(x_move - delta_len) <= (delta_len * 0.2)
|
||||||
|
|
||||||
|
def set_callback(self, callback):
|
||||||
|
pass
|
||||||
@@ -41,7 +41,9 @@ class TabCompletionManager:
|
|||||||
state = self.env["commandBuffer"]["tabCompletion"]
|
state = self.env["commandBuffer"]["tabCompletion"]
|
||||||
pending = state.get("pending")
|
pending = state.get("pending")
|
||||||
if not pending:
|
if not pending:
|
||||||
return ""
|
pending = self._build_pending_from_recent_tab_update()
|
||||||
|
if not pending:
|
||||||
|
return ""
|
||||||
|
|
||||||
if time.time() - pending["timestamp"] > self.timeout:
|
if time.time() - pending["timestamp"] > self.timeout:
|
||||||
state["pending"] = None
|
state["pending"] = None
|
||||||
@@ -60,6 +62,33 @@ class TabCompletionManager:
|
|||||||
state["lastProcessedTime"] = time.time()
|
state["lastProcessedTime"] = time.time()
|
||||||
return spoken_text
|
return spoken_text
|
||||||
|
|
||||||
|
def _build_pending_from_recent_tab_update(self):
|
||||||
|
if not self._recent_tab_input():
|
||||||
|
return None
|
||||||
|
return {
|
||||||
|
"timestamp": time.time(),
|
||||||
|
"cursor": self.env["screen"]["old_cursor"].copy(),
|
||||||
|
"content": self.env["screen"]["old_content_text"],
|
||||||
|
"screen": self.env["screen"]["newTTY"],
|
||||||
|
}
|
||||||
|
|
||||||
|
def _recent_tab_input(self):
|
||||||
|
input_manager = self.env["runtime"]["InputManager"]
|
||||||
|
if input_manager.get_last_deepest_input() in [["KEY_TAB"]]:
|
||||||
|
return True
|
||||||
|
|
||||||
|
last_event = input_manager.get_last_event()
|
||||||
|
if not last_event or last_event.get("event_name") != "KEY_TAB":
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
return (
|
||||||
|
time.time() - input_manager.get_last_input_time()
|
||||||
|
<= self.timeout
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
def _last_event_is_tab_press(self):
|
def _last_event_is_tab_press(self):
|
||||||
input_manager = self.env["runtime"]["InputManager"]
|
input_manager = self.env["runtime"]["InputManager"]
|
||||||
last_event = input_manager.get_last_event()
|
last_event = input_manager.get_last_event()
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import importlib.util
|
||||||
import time
|
import time
|
||||||
|
from pathlib import Path
|
||||||
from unittest.mock import Mock
|
from unittest.mock import Mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -14,6 +16,8 @@ def _build_env(old_text="", cursor=None, screen="pty"):
|
|||||||
"event_name": "KEY_TAB",
|
"event_name": "KEY_TAB",
|
||||||
"event_state": 1,
|
"event_state": 1,
|
||||||
}
|
}
|
||||||
|
input_manager.get_last_deepest_input.return_value = [["KEY_TAB"]]
|
||||||
|
input_manager.get_last_input_time.return_value = time.time()
|
||||||
env = {
|
env = {
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"InputManager": input_manager,
|
"InputManager": input_manager,
|
||||||
@@ -32,6 +36,23 @@ def _build_env(old_text="", cursor=None, screen="pty"):
|
|||||||
return manager, env, input_manager
|
return manager, env, input_manager
|
||||||
|
|
||||||
|
|
||||||
|
def _load_large_insertion_module():
|
||||||
|
module_path = (
|
||||||
|
Path(__file__).resolve().parents[2]
|
||||||
|
/ "src"
|
||||||
|
/ "fenrirscreenreader"
|
||||||
|
/ "commands"
|
||||||
|
/ "onCursorChange"
|
||||||
|
/ "55000-large_insertion_echo.py"
|
||||||
|
)
|
||||||
|
spec = importlib.util.spec_from_file_location(
|
||||||
|
"fenrir_large_insertion_echo", module_path
|
||||||
|
)
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
def _set_screen_update(env, text, cursor, delta="", typing=False):
|
def _set_screen_update(env, text, cursor, delta="", typing=False):
|
||||||
env["screen"]["new_content_text"] = text
|
env["screen"]["new_content_text"] = text
|
||||||
env["screen"]["new_cursor"] = cursor.copy()
|
env["screen"]["new_cursor"] = cursor.copy()
|
||||||
@@ -124,7 +145,108 @@ def test_non_tab_key_does_not_capture():
|
|||||||
"event_name": "KEY_A",
|
"event_name": "KEY_A",
|
||||||
"event_state": 1,
|
"event_state": 1,
|
||||||
}
|
}
|
||||||
|
input_manager.get_last_deepest_input.return_value = [["KEY_A"]]
|
||||||
|
|
||||||
manager.capture_if_tab()
|
manager.capture_if_tab()
|
||||||
|
|
||||||
assert env["commandBuffer"]["tabCompletion"]["pending"] is None
|
assert env["commandBuffer"]["tabCompletion"]["pending"] is None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_recent_tab_screen_update_works_without_key_input_snapshot():
|
||||||
|
manager, env, input_manager = _build_env(
|
||||||
|
"cd Documents/".ljust(20), {"x": 13, "y": 0}
|
||||||
|
)
|
||||||
|
input_manager.get_last_event.return_value = {
|
||||||
|
"event_name": "KEY_TAB",
|
||||||
|
"event_state": 0,
|
||||||
|
}
|
||||||
|
env["screen"]["old_content_text"] = "cd Do".ljust(20)
|
||||||
|
env["screen"]["old_cursor"] = {"x": 5, "y": 0}
|
||||||
|
env["commandBuffer"]["tabCompletion"]["pending"] = None
|
||||||
|
_set_screen_update(
|
||||||
|
env,
|
||||||
|
"cd Documents/".ljust(20),
|
||||||
|
{"x": 13, "y": 0},
|
||||||
|
delta="cuments/",
|
||||||
|
typing=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert manager.process_update() == "cuments/"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_large_insertion_echo_speaks_pasted_cursor_text():
|
||||||
|
large_insertion_module = _load_large_insertion_module()
|
||||||
|
output_manager = Mock(present_text=Mock())
|
||||||
|
settings_manager = Mock()
|
||||||
|
settings_manager.get_setting_as_bool.return_value = False
|
||||||
|
input_manager = Mock()
|
||||||
|
input_manager.get_last_deepest_input.return_value = []
|
||||||
|
input_manager.get_last_event.return_value = None
|
||||||
|
screen_manager = Mock(
|
||||||
|
is_screen_change=Mock(return_value=False),
|
||||||
|
is_delta=Mock(return_value=True),
|
||||||
|
)
|
||||||
|
env = {
|
||||||
|
"runtime": {
|
||||||
|
"InputManager": input_manager,
|
||||||
|
"OutputManager": output_manager,
|
||||||
|
"ScreenManager": screen_manager,
|
||||||
|
"SettingsManager": settings_manager,
|
||||||
|
},
|
||||||
|
"screen": {
|
||||||
|
"old_cursor": {"x": 2, "y": 0},
|
||||||
|
"new_cursor": {"x": 13, "y": 0},
|
||||||
|
"new_delta": "hello world",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
command = large_insertion_module.command()
|
||||||
|
command.initialize(env)
|
||||||
|
|
||||||
|
command.run()
|
||||||
|
|
||||||
|
output_manager.present_text.assert_called_once_with(
|
||||||
|
"hello world",
|
||||||
|
interrupt=True,
|
||||||
|
announce_capital=True,
|
||||||
|
flush=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_large_insertion_echo_defers_recent_tab_to_tab_completion():
|
||||||
|
large_insertion_module = _load_large_insertion_module()
|
||||||
|
output_manager = Mock(present_text=Mock())
|
||||||
|
settings_manager = Mock()
|
||||||
|
settings_manager.get_setting_as_bool.return_value = False
|
||||||
|
input_manager = Mock()
|
||||||
|
input_manager.get_last_deepest_input.return_value = [["KEY_TAB"]]
|
||||||
|
input_manager.get_last_event.return_value = {
|
||||||
|
"event_name": "KEY_TAB",
|
||||||
|
"event_state": 1,
|
||||||
|
}
|
||||||
|
input_manager.get_last_input_time.return_value = time.time()
|
||||||
|
screen_manager = Mock(
|
||||||
|
is_screen_change=Mock(return_value=False),
|
||||||
|
is_delta=Mock(return_value=True),
|
||||||
|
)
|
||||||
|
env = {
|
||||||
|
"runtime": {
|
||||||
|
"InputManager": input_manager,
|
||||||
|
"OutputManager": output_manager,
|
||||||
|
"ScreenManager": screen_manager,
|
||||||
|
"SettingsManager": settings_manager,
|
||||||
|
},
|
||||||
|
"screen": {
|
||||||
|
"old_cursor": {"x": 5, "y": 0},
|
||||||
|
"new_cursor": {"x": 13, "y": 0},
|
||||||
|
"new_delta": "cuments/",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
command = large_insertion_module.command()
|
||||||
|
command.initialize(env)
|
||||||
|
|
||||||
|
command.run()
|
||||||
|
|
||||||
|
output_manager.present_text.assert_not_called()
|
||||||
|
|||||||
Reference in New Issue
Block a user