I don't wanna say this too loud, but I think tab completion is much more reliable now. No more not reading when you press tab and something appears.
This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
# Fenrir TTY screen reader
|
# Fenrir TTY screen reader
|
||||||
# By Chrys, Storm Dragon, and contributors.
|
# By Chrys, Storm Dragon, and contributors.
|
||||||
|
|
||||||
|
import time
|
||||||
from fenrirscreenreader.core.i18n import _
|
from fenrirscreenreader.core.i18n import _
|
||||||
|
|
||||||
|
|
||||||
@@ -14,55 +14,150 @@ class command:
|
|||||||
|
|
||||||
def initialize(self, environment):
|
def initialize(self, environment):
|
||||||
self.env = 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):
|
def shutdown(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def get_description(self):
|
def get_description(self):
|
||||||
return "No Description found"
|
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
|
||||||
|
|
||||||
|
# Check KEY mode
|
||||||
|
if self.env["runtime"]["InputManager"].get_shortcut_type() in ["KEY"]:
|
||||||
|
if (self.env["runtime"]["InputManager"].get_last_deepest_input()
|
||||||
|
in [["KEY_TAB"]]):
|
||||||
|
tab_detected = True
|
||||||
|
self.env["commandBuffer"]["tabCompletion"]["lastTabTime"] = current_time
|
||||||
|
|
||||||
|
# Check BYTE mode
|
||||||
|
elif self.env["runtime"]["InputManager"].get_shortcut_type() in ["BYTE"]:
|
||||||
|
for currByte in self.env["runtime"]["ByteManager"].get_last_byte_key():
|
||||||
|
if currByte == 9: # Tab character
|
||||||
|
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):
|
def run(self):
|
||||||
# try to detect the tab completion by cursor change
|
"""Enhanced tab completion detection with improved reliability"""
|
||||||
|
# Basic cursor movement check (preserve original logic)
|
||||||
x_move = (
|
x_move = (
|
||||||
self.env["screen"]["new_cursor"]["x"]
|
self.env["screen"]["new_cursor"]["x"]
|
||||||
- self.env["screen"]["old_cursor"]["x"]
|
- self.env["screen"]["old_cursor"]["x"]
|
||||||
)
|
)
|
||||||
if x_move <= 0:
|
if x_move <= 0:
|
||||||
return
|
return
|
||||||
if self.env["runtime"]["InputManager"].get_shortcut_type() in ["KEY"]:
|
|
||||||
if not (
|
# Enhanced tab input detection with persistence
|
||||||
self.env["runtime"]["InputManager"].get_last_deepest_input()
|
tab_detected = self._is_recent_tab_input()
|
||||||
in [["KEY_TAB"]]
|
|
||||||
):
|
# Fallback for non-tab movements (preserve original thresholds)
|
||||||
if x_move < 5:
|
if not tab_detected:
|
||||||
return
|
if x_move < 5:
|
||||||
elif self.env["runtime"]["InputManager"].get_shortcut_type() in [
|
return
|
||||||
"BYTE"
|
|
||||||
]:
|
# Screen delta availability check
|
||||||
found = False
|
|
||||||
for currByte in self.env["runtime"][
|
|
||||||
"ByteManager"
|
|
||||||
].get_last_byte_key():
|
|
||||||
if currByte == 9:
|
|
||||||
found = True
|
|
||||||
if not found:
|
|
||||||
if x_move < 5:
|
|
||||||
return
|
|
||||||
# is there any change?
|
|
||||||
if not self.env["runtime"]["ScreenManager"].is_delta():
|
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
|
return
|
||||||
if not x_move == len(self.env["screen"]["new_delta"]):
|
|
||||||
return
|
delta_text = self.env["screen"]["new_delta"]
|
||||||
# filter unneded space on word begin
|
|
||||||
curr_delta = self.env["screen"]["new_delta"]
|
# Enhanced correlation checking with flexible matching
|
||||||
if (
|
if not self._is_flexible_completion_match(x_move, delta_text):
|
||||||
len(curr_delta.strip()) != len(curr_delta)
|
# Additional pattern-based validation for edge cases
|
||||||
and curr_delta.strip() != ""
|
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()
|
curr_delta = curr_delta.strip()
|
||||||
self.env["runtime"]["OutputManager"].present_text(
|
|
||||||
curr_delta, interrupt=True, announce_capital=True, flush=False
|
# Enhanced announcement with better handling of empty completions
|
||||||
)
|
if curr_delta:
|
||||||
|
self.env["runtime"]["OutputManager"].present_text(
|
||||||
|
curr_delta, interrupt=True, announce_capital=True, flush=False
|
||||||
|
)
|
||||||
|
|
||||||
def set_callback(self, callback):
|
def set_callback(self, callback):
|
||||||
pass
|
pass
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
# Fenrir TTY screen reader
|
# Fenrir TTY screen reader
|
||||||
# By Chrys, Storm Dragon, and contributors.
|
# By Chrys, Storm Dragon, and contributors.
|
||||||
|
|
||||||
|
import time
|
||||||
from fenrirscreenreader.core.i18n import _
|
from fenrirscreenreader.core.i18n import _
|
||||||
|
|
||||||
|
|
||||||
@@ -19,7 +19,25 @@ class command:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def get_description(self):
|
def get_description(self):
|
||||||
return "No Description found"
|
return _("Announces incoming text changes")
|
||||||
|
|
||||||
|
def _was_handled_by_tab_completion(self, delta_text):
|
||||||
|
"""Check if this delta was already handled by tab completion to avoid duplicates"""
|
||||||
|
if "tabCompletion" not in self.env["commandBuffer"]:
|
||||||
|
return False
|
||||||
|
|
||||||
|
tab_state = self.env["commandBuffer"]["tabCompletion"]
|
||||||
|
|
||||||
|
# Check if this exact delta was processed recently by tab completion
|
||||||
|
if (tab_state.get("lastProcessedDelta") == delta_text and
|
||||||
|
tab_state.get("lastProcessedTime")):
|
||||||
|
|
||||||
|
# Only suppress if processed within the last 100ms to avoid stale suppression
|
||||||
|
time_since_processed = time.time() - tab_state["lastProcessedTime"]
|
||||||
|
if time_since_processed <= 0.1:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
if not self.env["runtime"]["SettingsManager"].get_setting_as_bool(
|
if not self.env["runtime"]["SettingsManager"].get_setting_as_bool(
|
||||||
@@ -30,6 +48,12 @@ class command:
|
|||||||
if not self.env["runtime"]["ScreenManager"].is_delta(ignoreSpace=True):
|
if not self.env["runtime"]["ScreenManager"].is_delta(ignoreSpace=True):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
delta_text = self.env["screen"]["new_delta"]
|
||||||
|
|
||||||
|
# Skip if tab completion already handled this delta
|
||||||
|
if self._was_handled_by_tab_completion(delta_text):
|
||||||
|
return
|
||||||
|
|
||||||
# this must be a keyecho or something
|
# this must be a keyecho or something
|
||||||
# if len(self.env['screen']['new_delta'].strip(' \n\t')) <= 1:
|
# if len(self.env['screen']['new_delta'].strip(' \n\t')) <= 1:
|
||||||
x_move = abs(
|
x_move = abs(
|
||||||
@@ -41,14 +65,14 @@ class command:
|
|||||||
- self.env["screen"]["old_cursor"]["y"]
|
- self.env["screen"]["old_cursor"]["y"]
|
||||||
)
|
)
|
||||||
|
|
||||||
if (x_move >= 1) and x_move == len(self.env["screen"]["new_delta"]):
|
if (x_move >= 1) and x_move == len(delta_text):
|
||||||
# if len(self.env['screen']['new_delta'].strip(' \n\t0123456789'))
|
# if len(self.env['screen']['new_delta'].strip(' \n\t0123456789'))
|
||||||
# <= 2:
|
# <= 2:
|
||||||
if "\n" not in self.env["screen"]["new_delta"]:
|
if "\n" not in delta_text:
|
||||||
return
|
return
|
||||||
# print(x_move, y_move, len(self.env['screen']['new_delta']), len(self.env['screen']['newNegativeDelta']))
|
# print(x_move, y_move, len(self.env['screen']['new_delta']), len(self.env['screen']['newNegativeDelta']))
|
||||||
self.env["runtime"]["OutputManager"].present_text(
|
self.env["runtime"]["OutputManager"].present_text(
|
||||||
self.env["screen"]["new_delta"], interrupt=False, flush=False
|
delta_text, interrupt=False, flush=False
|
||||||
)
|
)
|
||||||
|
|
||||||
def set_callback(self, callback):
|
def set_callback(self, callback):
|
||||||
|
Reference in New Issue
Block a user