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 | ||||
| # 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 | ||||
|   | ||||
| @@ -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): | ||||
|   | ||||
		Reference in New Issue
	
	Block a user