From 8a223282dfd9e589f2b32b17e12a456d55de3c9c Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Mon, 9 Jun 2025 02:19:39 -0400 Subject: [PATCH] Some modifications to progress bar detection, can revert if needed. --- .../commands/commands/progress_bar_monitor.py | 95 ++++++++++++++++++- .../onScreenUpdate/65000-progress_detector.py | 94 +++++++++++++++++- src/fenrirscreenreader/fenrirVersion.py | 2 +- 3 files changed, 184 insertions(+), 7 deletions(-) diff --git a/src/fenrirscreenreader/commands/commands/progress_bar_monitor.py b/src/fenrirscreenreader/commands/commands/progress_bar_monitor.py index b72344fb..43b828d6 100644 --- a/src/fenrirscreenreader/commands/commands/progress_bar_monitor.py +++ b/src/fenrirscreenreader/commands/commands/progress_bar_monitor.py @@ -60,6 +60,10 @@ class command(): if not self.env['runtime']['progressMonitoring']: return + # Skip progress detection if current screen looks like a prompt + if self.isCurrentLinePrompt(): + return + currentTime = time.time() # Pattern 1: Percentage (50%, 25.5%, etc.) @@ -86,11 +90,14 @@ class command(): return # Pattern 3: Progress bars ([#### ], [====> ], etc.) - barMatch = re.search(r'\[([#=\-\*]+)([^\]]*)\]', text) + # Improved pattern to avoid matching IRC channels like [#channel] + barMatch = re.search(r'\[([#=\-\*]+)([\s\.]*)\]', text) if barMatch: filled = len(barMatch.group(1)) - total = filled + len(barMatch.group(2)) - if total > 0: + unfilled = len(barMatch.group(2)) + total = filled + unfilled + # Require at least 2 progress chars total and unfilled portion must be spaces/dots + if total >= 2 and (not barMatch.group(2) or re.match(r'^[\s\.]*$', barMatch.group(2))): percentage = (filled / total) * 100 if percentage != self.env['runtime']['lastProgressValue']: self.playProgressTone(percentage) @@ -115,6 +122,88 @@ class command(): def playActivityBeep(self): # Single tone for generic activity self.env['runtime']['outputManager'].playFrequence(800, 0.1, interrupt=False) + + def isCurrentLinePrompt(self): + """Check if the current line looks like a standalone prompt (not command with progress)""" + import re + + try: + # Get the current screen content + if not self.env['screen']['newContentText']: + return False + + lines = self.env['screen']['newContentText'].split('\n') + if not lines: + return False + + # Check the last line (most common) and current cursor line for prompt patterns + linesToCheck = [] + + # Add last line (most common for prompts) + if lines: + linesToCheck.append(lines[-1]) + + # Add current cursor line if different from last line + if (self.env['screen']['newCursor']['y'] < len(lines) and + self.env['screen']['newCursor']['y'] != len(lines) - 1): + linesToCheck.append(lines[self.env['screen']['newCursor']['y']]) + + # Standalone prompt patterns (no commands mixed in) + standalonePromptPatterns = [ + r'^\s*\$\s*$', # Just $ (with whitespace) + r'^\s*#\s*$', # Just # (with whitespace) + r'^\s*>\s*$', # Just > (with whitespace) + r'^\[.*\]\s*[\\\$#>]\s*$', # [path]$ without commands + r'^[a-zA-Z0-9._-]+[\\\$#>]\s*$', # bash-5.1$ without commands + + # Interactive prompt patterns (these ARE standalone) + r'.*\?\s*\[[YyNn]/[YyNn]\]\s*$', # ? [Y/n] or ? [y/N] style + r'.*\?\s*\[[Yy]es/[Nn]o\]\s*$', # ? [Yes/No] style + r'.*continue\?\s*\[[YyNn]/[YyNn]\].*$', # "continue? [Y/n]" style + r'^::.*\?\s*\[[YyNn]/[YyNn]\].*$', # pacman style prompts + + # Authentication prompts (these ARE standalone) + r'^\[[Ss]udo\]\s*[Pp]assword\s*for\s+.*:\s*$', # [sudo] password + r'^[Pp]assword\s*:\s*$', # Password: + r'.*[Pp]assword\s*:\s*$', # general password prompts + + # Continuation prompts (these ARE standalone) + r'^[Pp]ress\s+any\s+key\s+to\s+continue.*$', # Press any key + r'^[Aa]re\s+you\s+sure\?\s*.*$', # Are you sure? + ] + + for line in linesToCheck: + line = line.strip() + if not line: + continue + + # Check if this line contains both a prompt AND other content (like commands) + # If so, don't treat it as a standalone prompt + hasPromptMarker = bool(re.search(r'.*@.*[\\\$#>]', line) or re.search(r'^\[.*\]\s*[\\\$#>]', line)) + if hasPromptMarker: + # If line has prompt marker but also has significant content after it, + # it's a command line, not a standalone prompt + promptEnd = max( + line.rfind('$'), + line.rfind('#'), + line.rfind('>'), + line.rfind('\\') + ) + if promptEnd >= 0 and promptEnd < len(line) - 5: # More than just whitespace after prompt + continue # This is a command line, not a standalone prompt + + for pattern in standalonePromptPatterns: + try: + if re.search(pattern, line): + return True + except re.error: + continue + + return False + + except Exception: + # If anything fails, assume it's not a prompt to be safe + return False def setCallback(self, callback): pass \ No newline at end of file diff --git a/src/fenrirscreenreader/commands/onScreenUpdate/65000-progress_detector.py b/src/fenrirscreenreader/commands/onScreenUpdate/65000-progress_detector.py index dcd51dda..108ca002 100644 --- a/src/fenrirscreenreader/commands/onScreenUpdate/65000-progress_detector.py +++ b/src/fenrirscreenreader/commands/onScreenUpdate/65000-progress_detector.py @@ -49,6 +49,10 @@ class command(): if deltaLength > 200: # Allow longer progress lines like Claude Code's status return False + # Check if current line looks like a prompt - progress unlikely during prompts + if self.isCurrentLinePrompt(): + return False + return True def detectProgress(self, text): @@ -106,11 +110,14 @@ class command(): return # Pattern 3: Progress bars ([#### ], [====> ], etc.) - barMatch = re.search(r'\[([#=\-\*]+)([^\]]*)\]', text) + # Improved pattern to avoid matching IRC channels like [#channel] + barMatch = re.search(r'\[([#=\-\*]+)([\s\.]*)\]', text) if barMatch: filled = len(barMatch.group(1)) - total = filled + len(barMatch.group(2)) - if total > 0: + unfilled = len(barMatch.group(2)) + total = filled + unfilled + # Require at least 2 progress chars total and unfilled portion must be spaces/dots + if total >= 2 and (not barMatch.group(2) or re.match(r'^[\s\.]*$', barMatch.group(2))): percentage = (filled / total) * 100 if percentage != self.env['commandBuffer']['lastProgressValue']: self.playProgressTone(percentage) @@ -152,7 +159,88 @@ class command(): subprocess.Popen(shlex.split(command), stdin=None, stdout=None, stderr=None, shell=False) except Exception as e: self.env['runtime']['debug'].writeDebugOut("Sox tone error: " + str(e), debug.debugLevel.ERROR) + + def isCurrentLinePrompt(self): + """Check if the current line looks like a standalone prompt (not command with progress)""" + import re + try: + # Get the current screen content + if not self.env['screen']['newContentText']: + return False + + lines = self.env['screen']['newContentText'].split('\n') + if not lines: + return False + + # Check the last line (most common) and current cursor line for prompt patterns + linesToCheck = [] + + # Add last line (most common for prompts) + if lines: + linesToCheck.append(lines[-1]) + + # Add current cursor line if different from last line + if (self.env['screen']['newCursor']['y'] < len(lines) and + self.env['screen']['newCursor']['y'] != len(lines) - 1): + linesToCheck.append(lines[self.env['screen']['newCursor']['y']]) + + # Standalone prompt patterns (no commands mixed in) + standalonePromptPatterns = [ + r'^\s*\$\s*$', # Just $ (with whitespace) + r'^\s*#\s*$', # Just # (with whitespace) + r'^\s*>\s*$', # Just > (with whitespace) + r'^\[.*\]\s*[\\\$#>]\s*$', # [path]$ without commands + r'^[a-zA-Z0-9._-]+[\\\$#>]\s*$', # bash-5.1$ without commands + + # Interactive prompt patterns (these ARE standalone) + r'.*\?\s*\[[YyNn]/[YyNn]\]\s*$', # ? [Y/n] or ? [y/N] style + r'.*\?\s*\[[Yy]es/[Nn]o\]\s*$', # ? [Yes/No] style + r'.*continue\?\s*\[[YyNn]/[YyNn]\].*$', # "continue? [Y/n]" style + r'^::.*\?\s*\[[YyNn]/[YyNn]\].*$', # pacman style prompts + + # Authentication prompts (these ARE standalone) + r'^\[[Ss]udo\]\s*[Pp]assword\s*for\s+.*:\s*$', # [sudo] password + r'^[Pp]assword\s*:\s*$', # Password: + r'.*[Pp]assword\s*:\s*$', # general password prompts + + # Continuation prompts (these ARE standalone) + r'^[Pp]ress\s+any\s+key\s+to\s+continue.*$', # Press any key + r'^[Aa]re\s+you\s+sure\?\s*.*$', # Are you sure? + ] + + for line in linesToCheck: + line = line.strip() + if not line: + continue + + # Check if this line contains both a prompt AND other content (like commands) + # If so, don't treat it as a standalone prompt + hasPromptMarker = bool(re.search(r'.*@.*[\\\$#>]', line) or re.search(r'^\[.*\]\s*[\\\$#>]', line)) + if hasPromptMarker: + # If line has prompt marker but also has significant content after it, + # it's a command line, not a standalone prompt + promptEnd = max( + line.rfind('$'), + line.rfind('#'), + line.rfind('>'), + line.rfind('\\') + ) + if promptEnd >= 0 and promptEnd < len(line) - 5: # More than just whitespace after prompt + continue # This is a command line, not a standalone prompt + + for pattern in standalonePromptPatterns: + try: + if re.search(pattern, line): + return True + except re.error: + continue + + return False + + except Exception: + # If anything fails, assume it's not a prompt to be safe + return False def setCallback(self, callback): pass \ No newline at end of file diff --git a/src/fenrirscreenreader/fenrirVersion.py b/src/fenrirscreenreader/fenrirVersion.py index 542115d6..d60f9cce 100644 --- a/src/fenrirscreenreader/fenrirVersion.py +++ b/src/fenrirscreenreader/fenrirVersion.py @@ -4,5 +4,5 @@ # Fenrir TTY screen reader # By Chrys, Storm Dragon, and contributers. -version = "2025.06.08" +version = "2025.06.09" codeName = "testing"