diff --git a/src/fenrirscreenreader/commands/onCursorChange/55000-tab_completion.py b/src/fenrirscreenreader/commands/onCursorChange/55000-tab_completion.py index 1dc95fc0..fa2e035e 100644 --- a/src/fenrirscreenreader/commands/onCursorChange/55000-tab_completion.py +++ b/src/fenrirscreenreader/commands/onCursorChange/55000-tab_completion.py @@ -4,7 +4,7 @@ # Fenrir TTY screen reader # By Chrys, Storm Dragon, and contributors. - +import time from fenrirscreenreader.core.i18n import _ @@ -14,55 +14,150 @@ class command: 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 "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): - # 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 = ( self.env["screen"]["new_cursor"]["x"] - self.env["screen"]["old_cursor"]["x"] ) if x_move <= 0: return - if self.env["runtime"]["InputManager"].get_shortcut_type() in ["KEY"]: - if not ( - self.env["runtime"]["InputManager"].get_last_deepest_input() - in [["KEY_TAB"]] - ): - if x_move < 5: - return - elif self.env["runtime"]["InputManager"].get_shortcut_type() in [ - "BYTE" - ]: - 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? + + # 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 - if not x_move == len(self.env["screen"]["new_delta"]): - return - # filter unneded space on word begin - curr_delta = self.env["screen"]["new_delta"] - if ( - len(curr_delta.strip()) != len(curr_delta) - and curr_delta.strip() != "" - ): + + 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() - 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): pass diff --git a/src/fenrirscreenreader/commands/onScreenUpdate/70000-incoming.py b/src/fenrirscreenreader/commands/onScreenUpdate/70000-incoming.py index 8bf0d239..40397ce7 100644 --- a/src/fenrirscreenreader/commands/onScreenUpdate/70000-incoming.py +++ b/src/fenrirscreenreader/commands/onScreenUpdate/70000-incoming.py @@ -4,7 +4,7 @@ # Fenrir TTY screen reader # By Chrys, Storm Dragon, and contributors. - +import time from fenrirscreenreader.core.i18n import _ @@ -19,7 +19,25 @@ class command: pass 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): 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): 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 # if len(self.env['screen']['new_delta'].strip(' \n\t')) <= 1: x_move = abs( @@ -41,14 +65,14 @@ class command: - 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')) # <= 2: - if "\n" not in self.env["screen"]["new_delta"]: + if "\n" not in delta_text: return # print(x_move, y_move, len(self.env['screen']['new_delta']), len(self.env['screen']['newNegativeDelta'])) 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):