Another attempt at improving tab completion detection.
This commit is contained in:
@@ -1,161 +0,0 @@
|
||||
#!/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
|
||||
# Initialize tab completion state tracking
|
||||
if "tabCompletion" not in self.env["commandBuffer"]:
|
||||
self.env["commandBuffer"]["tabCompletion"] = {
|
||||
"lastTabTime": 0,
|
||||
"pendingCompletion": None,
|
||||
"retryCount": 0
|
||||
}
|
||||
|
||||
def shutdown(self):
|
||||
pass
|
||||
|
||||
def get_description(self):
|
||||
return _("Announces tab completions when detected")
|
||||
|
||||
def _is_recent_tab_input(self):
|
||||
"""Check if TAB was pressed recently (within 200ms window)"""
|
||||
current_time = time.time()
|
||||
tab_detected = False
|
||||
|
||||
if (self.env["runtime"]["InputManager"].get_last_deepest_input()
|
||||
in [["KEY_TAB"]]):
|
||||
tab_detected = True
|
||||
self.env["commandBuffer"]["tabCompletion"]["lastTabTime"] = current_time
|
||||
|
||||
# Check if tab was pressed recently (200ms window)
|
||||
if not tab_detected:
|
||||
time_since_tab = current_time - self.env["commandBuffer"]["tabCompletion"]["lastTabTime"]
|
||||
if time_since_tab <= 0.2: # 200ms window
|
||||
tab_detected = True
|
||||
|
||||
return tab_detected
|
||||
|
||||
def _is_flexible_completion_match(self, x_move, delta_text):
|
||||
"""Use flexible matching instead of strict equality"""
|
||||
if not delta_text:
|
||||
return False
|
||||
|
||||
delta_len = len(delta_text)
|
||||
|
||||
# Exact match (preserve original behavior)
|
||||
if x_move == delta_len:
|
||||
return True
|
||||
|
||||
# Flexible range: allow ±2 characters difference
|
||||
# Handles spacing adjustments and unicode width variations
|
||||
if abs(x_move - delta_len) <= 2 and delta_len > 0:
|
||||
return True
|
||||
|
||||
# For longer completions, allow proportional variance
|
||||
if delta_len > 10 and abs(x_move - delta_len) <= (delta_len * 0.2):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _detect_completion_patterns(self, delta_text):
|
||||
"""Detect common tab completion patterns for improved accuracy"""
|
||||
if not delta_text:
|
||||
return False
|
||||
|
||||
delta_stripped = delta_text.strip()
|
||||
|
||||
# File extension completion
|
||||
if '.' in delta_stripped and delta_stripped.count('.') <= 2:
|
||||
return True
|
||||
|
||||
# Path completion (contains / or \)
|
||||
if '/' in delta_stripped or '\\' in delta_stripped:
|
||||
return True
|
||||
|
||||
# Command parameter completion (starts with -)
|
||||
if delta_stripped.startswith('-') and len(delta_stripped) > 1:
|
||||
return True
|
||||
|
||||
# Word boundary completion (alphanumeric content)
|
||||
if delta_stripped.isalnum() and len(delta_stripped) >= 2:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def run(self):
|
||||
"""Enhanced tab completion detection with improved reliability"""
|
||||
# Basic cursor movement check (preserve original logic)
|
||||
x_move = (
|
||||
self.env["screen"]["new_cursor"]["x"]
|
||||
- self.env["screen"]["old_cursor"]["x"]
|
||||
)
|
||||
if x_move <= 0:
|
||||
return
|
||||
|
||||
# Enhanced tab input detection with persistence
|
||||
tab_detected = self._is_recent_tab_input()
|
||||
|
||||
# Fallback for non-tab movements (preserve original thresholds)
|
||||
if not tab_detected:
|
||||
if x_move < 5:
|
||||
return
|
||||
|
||||
# Screen delta availability check
|
||||
if not self.env["runtime"]["ScreenManager"].is_delta():
|
||||
# If tab was detected but no delta yet, store for potential retry
|
||||
if tab_detected and self.env["commandBuffer"]["tabCompletion"]["retryCount"] < 2:
|
||||
self.env["commandBuffer"]["tabCompletion"]["pendingCompletion"] = {
|
||||
"x_move": x_move,
|
||||
"timestamp": time.time()
|
||||
}
|
||||
self.env["commandBuffer"]["tabCompletion"]["retryCount"] += 1
|
||||
return
|
||||
|
||||
delta_text = self.env["screen"]["new_delta"]
|
||||
|
||||
# Enhanced correlation checking with flexible matching
|
||||
if not self._is_flexible_completion_match(x_move, delta_text):
|
||||
# Additional pattern-based validation for edge cases
|
||||
if not (tab_detected and self._detect_completion_patterns(delta_text)):
|
||||
return
|
||||
|
||||
# Reset retry counter on successful detection
|
||||
self.env["commandBuffer"]["tabCompletion"]["retryCount"] = 0
|
||||
self.env["commandBuffer"]["tabCompletion"]["pendingCompletion"] = None
|
||||
|
||||
# Mark that we've handled this delta to prevent duplicate announcements
|
||||
# This prevents the incoming text handler from also announcing the same content
|
||||
self.env["commandBuffer"]["tabCompletion"]["lastProcessedDelta"] = delta_text
|
||||
self.env["commandBuffer"]["tabCompletion"]["lastProcessedTime"] = time.time()
|
||||
|
||||
# Text filtering and announcement (preserve original behavior)
|
||||
curr_delta = delta_text
|
||||
if (len(curr_delta.strip()) != len(curr_delta) and curr_delta.strip() != ""):
|
||||
curr_delta = curr_delta.strip()
|
||||
|
||||
# Don't interrupt ongoing auto-read announcements
|
||||
do_interrupt = True
|
||||
if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
|
||||
"speech", "auto_read_incoming"
|
||||
):
|
||||
do_interrupt = False
|
||||
|
||||
# Enhanced announcement with better handling of empty completions
|
||||
if curr_delta:
|
||||
self.env["runtime"]["OutputManager"].present_text(
|
||||
curr_delta, interrupt=do_interrupt, announce_capital=True, flush=False
|
||||
)
|
||||
|
||||
def set_callback(self, callback):
|
||||
pass
|
||||
@@ -1,128 +0,0 @@
|
||||
#!/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 _("Handles delayed retry for tab completion detection")
|
||||
|
||||
def run(self):
|
||||
"""Check for and process pending tab completions with slight delay"""
|
||||
# Only process if we have tab completion state
|
||||
if "tabCompletion" not in self.env["commandBuffer"]:
|
||||
return
|
||||
|
||||
tab_state = self.env["commandBuffer"]["tabCompletion"]
|
||||
pending = tab_state.get("pendingCompletion")
|
||||
|
||||
if not pending:
|
||||
return
|
||||
|
||||
current_time = time.time()
|
||||
|
||||
# Process pending completion after 50ms delay
|
||||
if current_time - pending["timestamp"] < 0.05:
|
||||
return
|
||||
|
||||
# Check if screen delta is now available
|
||||
if not self.env["runtime"]["ScreenManager"].is_delta():
|
||||
# Give up after 200ms total
|
||||
if current_time - pending["timestamp"] > 0.2:
|
||||
tab_state["pendingCompletion"] = None
|
||||
tab_state["retryCount"] = 0
|
||||
return
|
||||
|
||||
# Process the delayed completion
|
||||
delta_text = self.env["screen"]["new_delta"]
|
||||
x_move = pending["x_move"]
|
||||
|
||||
# Use the same flexible matching logic as main tab completion
|
||||
match_found = self._is_flexible_completion_match(x_move, delta_text)
|
||||
|
||||
if not match_found:
|
||||
# Try pattern-based detection as final fallback
|
||||
match_found = self._detect_completion_patterns(delta_text)
|
||||
|
||||
if match_found and delta_text:
|
||||
# Mark that we've handled this delta to prevent duplicate announcements
|
||||
tab_state["lastProcessedDelta"] = delta_text
|
||||
tab_state["lastProcessedTime"] = current_time
|
||||
|
||||
# Filter and announce the completion
|
||||
curr_delta = delta_text
|
||||
if (len(curr_delta.strip()) != len(curr_delta) and
|
||||
curr_delta.strip() != ""):
|
||||
curr_delta = curr_delta.strip()
|
||||
|
||||
if curr_delta:
|
||||
self.env["runtime"]["OutputManager"].present_text(
|
||||
curr_delta, interrupt=True, announce_capital=True, flush=False
|
||||
)
|
||||
|
||||
# Clear pending completion
|
||||
tab_state["pendingCompletion"] = None
|
||||
tab_state["retryCount"] = 0
|
||||
|
||||
def _is_flexible_completion_match(self, x_move, delta_text):
|
||||
"""Use flexible matching (duplicated from main command for heartbeat use)"""
|
||||
if not delta_text:
|
||||
return False
|
||||
|
||||
delta_len = len(delta_text)
|
||||
|
||||
# Exact match
|
||||
if x_move == delta_len:
|
||||
return True
|
||||
|
||||
# Flexible range: allow ±2 characters difference
|
||||
if abs(x_move - delta_len) <= 2 and delta_len > 0:
|
||||
return True
|
||||
|
||||
# For longer completions, allow proportional variance
|
||||
if delta_len > 10 and abs(x_move - delta_len) <= (delta_len * 0.2):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _detect_completion_patterns(self, delta_text):
|
||||
"""Detect common tab completion patterns (duplicated from main command)"""
|
||||
if not delta_text:
|
||||
return False
|
||||
|
||||
delta_stripped = delta_text.strip()
|
||||
|
||||
# File extension completion
|
||||
if '.' in delta_stripped and delta_stripped.count('.') <= 2:
|
||||
return True
|
||||
|
||||
# Path completion
|
||||
if '/' in delta_stripped or '\\' in delta_stripped:
|
||||
return True
|
||||
|
||||
# Command parameter completion
|
||||
if delta_stripped.startswith('-') and len(delta_stripped) > 1:
|
||||
return True
|
||||
|
||||
# Word boundary completion
|
||||
if delta_stripped.isalnum() and len(delta_stripped) >= 2:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def set_callback(self, callback):
|
||||
pass
|
||||
@@ -0,0 +1,29 @@
|
||||
#!/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 _("Tracks tab keypresses for completion announcements")
|
||||
|
||||
def run(self):
|
||||
self.manager.capture_if_tab()
|
||||
|
||||
def set_callback(self, callback):
|
||||
pass
|
||||
@@ -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 visible tab completion results")
|
||||
|
||||
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
|
||||
@@ -0,0 +1,192 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Fenrir TTY screen reader
|
||||
# By Chrys, Storm Dragon, and contributors.
|
||||
|
||||
import difflib
|
||||
import time
|
||||
|
||||
|
||||
class TabCompletionManager:
|
||||
def __init__(self, timeout=0.5):
|
||||
self.timeout = timeout
|
||||
|
||||
def initialize(self, environment):
|
||||
self.env = environment
|
||||
self._ensure_state()
|
||||
|
||||
def _ensure_state(self):
|
||||
if "tabCompletion" not in self.env["commandBuffer"]:
|
||||
self.env["commandBuffer"]["tabCompletion"] = {}
|
||||
state = self.env["commandBuffer"]["tabCompletion"]
|
||||
state.setdefault("pending", None)
|
||||
state.setdefault("lastProcessedDelta", "")
|
||||
state.setdefault("lastProcessedTime", 0)
|
||||
|
||||
def capture_if_tab(self):
|
||||
if not self._last_event_is_tab_press():
|
||||
return
|
||||
|
||||
self._ensure_state()
|
||||
self.env["commandBuffer"]["tabCompletion"]["pending"] = {
|
||||
"timestamp": time.time(),
|
||||
"cursor": self.env["screen"]["new_cursor"].copy(),
|
||||
"content": self.env["screen"]["new_content_text"],
|
||||
"screen": self.env["screen"]["newTTY"],
|
||||
}
|
||||
|
||||
def process_update(self):
|
||||
self._ensure_state()
|
||||
state = self.env["commandBuffer"]["tabCompletion"]
|
||||
pending = state.get("pending")
|
||||
if not pending:
|
||||
return ""
|
||||
|
||||
if time.time() - pending["timestamp"] > self.timeout:
|
||||
state["pending"] = None
|
||||
return ""
|
||||
|
||||
if pending.get("screen") != self.env["screen"]["newTTY"]:
|
||||
state["pending"] = None
|
||||
return ""
|
||||
|
||||
spoken_text = self._get_completion_text(pending)
|
||||
if not spoken_text:
|
||||
return ""
|
||||
|
||||
state["pending"] = None
|
||||
state["lastProcessedDelta"] = self.env["screen"]["new_delta"]
|
||||
state["lastProcessedTime"] = time.time()
|
||||
return spoken_text
|
||||
|
||||
def _last_event_is_tab_press(self):
|
||||
input_manager = self.env["runtime"]["InputManager"]
|
||||
last_event = input_manager.get_last_event()
|
||||
if not last_event:
|
||||
return False
|
||||
return (
|
||||
last_event.get("event_name") == "KEY_TAB"
|
||||
and last_event.get("event_state") == 1
|
||||
)
|
||||
|
||||
def _get_completion_text(self, pending):
|
||||
old_lines = pending["content"].split("\n")
|
||||
new_lines = self.env["screen"]["new_content_text"].split("\n")
|
||||
cursor = pending["cursor"]
|
||||
cursor_y = cursor["y"]
|
||||
|
||||
if cursor_y < len(old_lines) and cursor_y < len(new_lines):
|
||||
inserted_text = self._get_cursor_line_inserted_text(
|
||||
old_lines[cursor_y],
|
||||
new_lines[cursor_y],
|
||||
cursor["x"],
|
||||
self.env["screen"]["new_cursor"]["x"],
|
||||
)
|
||||
if inserted_text:
|
||||
return self._clean_text(inserted_text)
|
||||
|
||||
candidate_text = self._get_candidate_text(
|
||||
old_lines, new_lines, cursor_y
|
||||
)
|
||||
if candidate_text:
|
||||
return self._clean_text(candidate_text)
|
||||
|
||||
delta_text = self.env["screen"]["new_delta"]
|
||||
if (
|
||||
delta_text
|
||||
and not self.env["screen"].get("new_delta_is_typing", False)
|
||||
):
|
||||
return self._clean_text(delta_text)
|
||||
|
||||
return ""
|
||||
|
||||
def _get_cursor_line_inserted_text(
|
||||
self, old_line, new_line, old_cursor_x, new_cursor_x
|
||||
):
|
||||
if new_cursor_x <= old_cursor_x:
|
||||
return ""
|
||||
if old_line == new_line:
|
||||
return ""
|
||||
|
||||
matcher = difflib.SequenceMatcher(
|
||||
None, old_line, new_line, autojunk=False
|
||||
)
|
||||
inserted_parts = []
|
||||
for (
|
||||
tag,
|
||||
old_start,
|
||||
old_end,
|
||||
new_start,
|
||||
new_end,
|
||||
) in matcher.get_opcodes():
|
||||
if tag == "equal":
|
||||
continue
|
||||
if tag == "insert" and old_cursor_x <= new_start <= new_cursor_x:
|
||||
inserted_parts.append(new_line[new_start:new_end])
|
||||
continue
|
||||
if (
|
||||
tag == "replace"
|
||||
and old_line[old_start:old_end].strip() == ""
|
||||
and old_cursor_x <= new_start <= new_cursor_x
|
||||
):
|
||||
inserted_parts.append(new_line[new_start:new_end])
|
||||
continue
|
||||
if (
|
||||
tag == "delete"
|
||||
and old_line[old_start:old_end].strip() == ""
|
||||
and old_start >= old_cursor_x
|
||||
):
|
||||
continue
|
||||
if old_start >= old_cursor_x:
|
||||
return ""
|
||||
|
||||
return "".join(inserted_parts)
|
||||
|
||||
def _get_candidate_text(self, old_lines, new_lines, cursor_y):
|
||||
if len(old_lines) != len(new_lines):
|
||||
return self._get_inserted_lines(old_lines, new_lines, cursor_y)
|
||||
|
||||
changed_lines = []
|
||||
old_cursor_line = (
|
||||
old_lines[cursor_y].strip() if cursor_y < len(old_lines) else ""
|
||||
)
|
||||
for index, old_line in enumerate(old_lines):
|
||||
if index == cursor_y:
|
||||
continue
|
||||
if index < len(new_lines) and old_line != new_lines[index]:
|
||||
if new_lines[index].strip() == old_cursor_line:
|
||||
continue
|
||||
changed_lines.append(new_lines[index])
|
||||
|
||||
return "\n".join(
|
||||
line.rstrip() for line in changed_lines if line.strip()
|
||||
)
|
||||
|
||||
def _get_inserted_lines(self, old_lines, new_lines, cursor_y):
|
||||
matcher = difflib.SequenceMatcher(
|
||||
None, old_lines, new_lines, autojunk=False
|
||||
)
|
||||
inserted_lines = []
|
||||
for (
|
||||
tag,
|
||||
old_start,
|
||||
old_end,
|
||||
new_start,
|
||||
new_end,
|
||||
) in matcher.get_opcodes():
|
||||
if tag not in ["insert", "replace"]:
|
||||
continue
|
||||
if new_end <= cursor_y:
|
||||
continue
|
||||
for line in new_lines[new_start:new_end]:
|
||||
if line.strip():
|
||||
inserted_lines.append(line.rstrip())
|
||||
return "\n".join(inserted_lines)
|
||||
|
||||
def _clean_text(self, text):
|
||||
if not text:
|
||||
return ""
|
||||
if len(text.strip()) != len(text) and text.strip() != "":
|
||||
return text.strip()
|
||||
return text
|
||||
@@ -0,0 +1,130 @@
|
||||
import time
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from fenrirscreenreader.core.tabCompletionManager import TabCompletionManager
|
||||
|
||||
|
||||
def _build_env(old_text="", cursor=None, screen="pty"):
|
||||
if cursor is None:
|
||||
cursor = {"x": 0, "y": 0}
|
||||
input_manager = Mock()
|
||||
input_manager.get_last_event.return_value = {
|
||||
"event_name": "KEY_TAB",
|
||||
"event_state": 1,
|
||||
}
|
||||
env = {
|
||||
"runtime": {
|
||||
"InputManager": input_manager,
|
||||
},
|
||||
"screen": {
|
||||
"new_cursor": cursor.copy(),
|
||||
"new_content_text": old_text,
|
||||
"new_delta": "",
|
||||
"new_delta_is_typing": False,
|
||||
"newTTY": screen,
|
||||
},
|
||||
"commandBuffer": {},
|
||||
}
|
||||
manager = TabCompletionManager()
|
||||
manager.initialize(env)
|
||||
return manager, env, input_manager
|
||||
|
||||
|
||||
def _set_screen_update(env, text, cursor, delta="", typing=False):
|
||||
env["screen"]["new_content_text"] = text
|
||||
env["screen"]["new_cursor"] = cursor.copy()
|
||||
env["screen"]["new_delta"] = delta
|
||||
env["screen"]["new_delta_is_typing"] = typing
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_unique_completion_speaks_inserted_suffix():
|
||||
manager, env, _input_manager = _build_env(
|
||||
"cd Do".ljust(20), {"x": 5, "y": 0}
|
||||
)
|
||||
|
||||
manager.capture_if_tab()
|
||||
_set_screen_update(
|
||||
env,
|
||||
"cd Documents/".ljust(20),
|
||||
{"x": 13, "y": 0},
|
||||
delta="cuments/",
|
||||
typing=True,
|
||||
)
|
||||
|
||||
assert manager.process_update() == "cuments/"
|
||||
state = env["commandBuffer"]["tabCompletion"]
|
||||
assert state["pending"] is None
|
||||
assert state["lastProcessedDelta"] == "cuments/"
|
||||
|
||||
|
||||
@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)])
|
||||
manager, env, _input_manager = _build_env(old_text, {"x": 7, "y": 0})
|
||||
|
||||
manager.capture_if_tab()
|
||||
_set_screen_update(
|
||||
env,
|
||||
"\n".join(
|
||||
[
|
||||
"$ cd Do".ljust(20),
|
||||
"Documents/ Downloads/".ljust(20),
|
||||
"$ cd Do".ljust(20),
|
||||
]
|
||||
),
|
||||
{"x": 7, "y": 2},
|
||||
delta="Documents/ Downloads/",
|
||||
)
|
||||
|
||||
assert manager.process_update() == "Documents/ Downloads/"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_no_screen_change_stays_silent_and_keeps_pending_briefly():
|
||||
manager, env, _input_manager = _build_env(
|
||||
"cd Do".ljust(20), {"x": 5, "y": 0}
|
||||
)
|
||||
|
||||
manager.capture_if_tab()
|
||||
|
||||
assert manager.process_update() == ""
|
||||
assert env["commandBuffer"]["tabCompletion"]["pending"] is not None
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_timeout_clears_pending_without_speech():
|
||||
manager, env, _input_manager = _build_env(
|
||||
"cd Do".ljust(20), {"x": 5, "y": 0}
|
||||
)
|
||||
|
||||
manager.capture_if_tab()
|
||||
env["commandBuffer"]["tabCompletion"]["pending"]["timestamp"] = (
|
||||
time.time() - 1
|
||||
)
|
||||
_set_screen_update(
|
||||
env,
|
||||
"unrelated output".ljust(20),
|
||||
{"x": 0, "y": 0},
|
||||
delta="unrelated output",
|
||||
)
|
||||
|
||||
assert manager.process_update() == ""
|
||||
assert env["commandBuffer"]["tabCompletion"]["pending"] is None
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_non_tab_key_does_not_capture():
|
||||
manager, env, input_manager = _build_env(
|
||||
"cd Do".ljust(20), {"x": 5, "y": 0}
|
||||
)
|
||||
input_manager.get_last_event.return_value = {
|
||||
"event_name": "KEY_A",
|
||||
"event_state": 1,
|
||||
}
|
||||
|
||||
manager.capture_if_tab()
|
||||
|
||||
assert env["commandBuffer"]["tabCompletion"]["pending"] is None
|
||||
Reference in New Issue
Block a user