diff --git a/README.md b/README.md index 7a809d23..da37b5ee 100644 --- a/README.md +++ b/README.md @@ -511,6 +511,7 @@ fenrir [OPTIONS] - `-e, --emulated-pty` - Use PTY emulation with escape sequences for input (enables desktop/X/Wayland usage) - `-E, --emulated-evdev` - Use PTY emulation with evdev for input (single instance) - `-F, --force-all-screens` - Force Fenrir to respond on all screens, ignoring ignoreScreen setting +- `-i, -I, --ignore-screen SCREEN` - Ignore specific screen(s). Can be used multiple times. Combines with existing ignore settings. ### Examples: ```bash @@ -525,6 +526,10 @@ sudo fenrir -o "speech#rate=0.8;sound#volume=0.5" # Force Fenrir to work on all screens (ignore ignoreScreen setting) sudo fenrir -F + +# Ignore specific screens +sudo fenrir --ignore-screen 1 +sudo fenrir -i 1 -i 2 # Ignore screens 1 and 2 ``` ## Localization diff --git a/config/keyboard/desktop.conf b/config/keyboard/desktop.conf index 561c4754..29b3eb02 100644 --- a/config/keyboard/desktop.conf +++ b/config/keyboard/desktop.conf @@ -73,7 +73,8 @@ KEY_FENRIR,KEY_SHIFT,KEY_0=set_bookmark_10 KEY_FENRIR,KEY_0=bookmark_10 KEY_FENRIR,KEY_KPSLASH=set_window_application 2,KEY_FENRIR,KEY_KPSLASH=clear_window_application -KEY_KPPLUS=last_incoming +KEY_KPPLUS=progress_bar_monitor +KEY_FENRIR,KEY_KPPLUS=silence_until_prompt KEY_FENRIR,KEY_F2=toggle_braille KEY_FENRIR,KEY_F3=toggle_sound KEY_FENRIR,KEY_F4=toggle_speech diff --git a/config/keyboard/laptop.conf b/config/keyboard/laptop.conf index 0493eae4..54180a27 100644 --- a/config/keyboard/laptop.conf +++ b/config/keyboard/laptop.conf @@ -75,9 +75,10 @@ KEY_FENRIR,KEY_F2=toggle_braille KEY_FENRIR,KEY_F3=toggle_sound KEY_FENRIR,KEY_F4=toggle_speech KEY_FENRIR,KEY_ENTER=temp_disable_speech +KEY_FENRIR,KEY_SHIFT,KEY_ENTER=silence_until_prompt KEY_FENRIR,KEY_SHIFT,KEY_CTRL,KEY_P=toggle_punctuation_level KEY_FENRIR,KEY_RIGHTBRACE=toggle_auto_spell_check -KEY_FENRIR,KEY_SHIFT,KEY_ENTER=toggle_output +KEY_FENRIR,KEY_CTRL,KEY_SHIFT,KEY_ENTER=toggle_output KEY_FENRIR,KEY_SHIFT,KEY_E=toggle_emoticons KEY_FENRIR,KEY_ENTER=toggle_auto_read KEY_FENRIR,KEY_CTRL,KEY_T=toggle_auto_time diff --git a/config/keyboard/nvda-desktop.conf b/config/keyboard/nvda-desktop.conf index 6c98c440..8c9ab1d9 100644 --- a/config/keyboard/nvda-desktop.conf +++ b/config/keyboard/nvda-desktop.conf @@ -72,7 +72,8 @@ KEY_FENRIR,KEY_SHIFT,KEY_0=set_bookmark_10 KEY_FENRIR,KEY_0=bookmark_10 KEY_FENRIR,KEY_KPSLASH=set_window_application 2,KEY_FENRIR,KEY_KPSLASH=clear_window_application -KEY_KPPLUS=last_incoming +KEY_KPPLUS=progress_bar_monitor +KEY_FENRIR,KEY_KPPLUS=silence_until_prompt #=toggle_braille KEY_FENRIR,KEY_F3=toggle_sound KEY_FENRIR,KEY_F4=toggle_speech diff --git a/config/settings/settings.conf b/config/settings/settings.conf index d65bdf3a..d6f74425 100644 --- a/config/settings/settings.conf +++ b/config/settings/settings.conf @@ -28,6 +28,9 @@ genericPlayFileCommand=play -q -v fenrirVolume fenrirSoundFile #the following command is used to generate a frequency beep genericFrequencyCommand=play -q -v fenrirVolume -n -c1 synth fenrirDuration sine fenrirFrequence +# Enable progress bar monitoring with ascending tones by default +progressMonitoring=True + [speech] # Turn speech on or off: enabled=True @@ -163,7 +166,7 @@ autoPresentIndent=False # 1 = sound only # 2 = speak only autoPresentIndentMode=1 -# play a sound when attributes are changeing +# play a sound when attributes change hasAttributes=True # shell for PTY emulatiun (empty = default shell) shell= @@ -211,8 +214,24 @@ list= vmenuPath= quickMenu=speech#rate;speech#pitch;speech#volume +[prompt] +# Custom prompt patterns for silence until prompt feature +# You can add your own shell prompt patterns as regular expressions +# Each pattern should be on a separate line, format: customPatterns=pattern1,pattern2,pattern3 +# Examples: +# For PS1='[\u@\h \W] \$ ' use: \[.*@.*\s.*\]\s*[$#>]\s* +# For "[user@hostname ~] $" use: \[.*@.*\s.*\]\s*[$#>]\s* +# For custom prompts ending with specific strings, use patterns like: .*your_prompt_ending$ +customPatterns= + +# Specific prompt strings to match exactly (useful for very specific custom prompts) +# Format: exactMatches=prompt1,prompt2,prompt3 +# Examples: +# exactMatches=[storm@fenrir ~] $,[root@fenrir ~] # +exactMatches= + [time] -# automatic time anouncement +# automatic time announcement enabled=False # present time presentTime=True diff --git a/docs/fenrir.1 b/docs/fenrir.1 index c6347dd0..3d50c0fb 100644 --- a/docs/fenrir.1 +++ b/docs/fenrir.1 @@ -65,6 +65,10 @@ Use PTY emulation with evdev for input (single instance mode). .BR \-F ", " \-\-force-all-screens Force Fenrir to respond on all screens, ignoring the ignoreScreen setting. This temporarily overrides screen filtering for the current session. +.TP +.BR \-i ", " \-I ", " \-\-ignore-screen " \fISCREEN\fR" +Ignore specific screen(s). Can be used multiple times to ignore multiple screens. This is equivalent to setting ignoreScreen in the configuration file and will be combined with any existing ignore settings. + .SH KEY CONCEPTS .SS Fenrir Key diff --git a/docs/fenrir.adoc b/docs/fenrir.adoc index 80a419da..1f2a5b4f 100644 --- a/docs/fenrir.adoc +++ b/docs/fenrir.adoc @@ -1240,6 +1240,9 @@ Use PTY emulation with evdev for input (single instance mode). `+-F, --force-all-screens+`:: Force Fenrir to respond on all screens, ignoring the ignoreScreen setting. This temporarily overrides screen filtering for the current session. +`+-i, -I, --ignore-screen +`:: +Ignore specific screen(s). Can be used multiple times to ignore multiple screens. This is equivalent to setting ignoreScreen in the configuration file and will be combined with any existing ignore settings. + ==== Set settings options You can specify options that overwrite the setting.conf. This is done diff --git a/docs/user.md b/docs/user.md index e1e9a04d..22af87ad 100644 --- a/docs/user.md +++ b/docs/user.md @@ -297,6 +297,7 @@ fenrir [OPTIONS] - `-e, --emulated-pty` - PTY emulation for desktop use - `-E, --emulated-evdev` - PTY + evdev emulation - `-F, --force-all-screens` - Ignore ignoreScreen setting +- `-i, -I, --ignore-screen SCREEN` - Ignore specific screen(s), can be used multiple times ## Troubleshooting diff --git a/src/fenrir b/src/fenrir index 02fb6347..8f91aba1 100755 --- a/src/fenrir +++ b/src/fenrir @@ -72,6 +72,12 @@ def create_argument_parser(): action='store_true', help='Force Fenrir to respond on all screens, ignoring ignoreScreen setting' ) + argumentParser.add_argument( + '-i', '-I', '--ignore-screen', + metavar='SCREEN', + action='append', + help='Ignore specific screen(s). Can be used multiple times. Same as ignoreScreen setting.' + ) return argumentParser def validate_arguments(cliArgs): diff --git a/src/fenrirscreenreader/commands/commands/last_incoming.py b/src/fenrirscreenreader/commands/commands/last_incoming.py deleted file mode 100644 index 75cef645..00000000 --- a/src/fenrirscreenreader/commands/commands/last_incoming.py +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -# Fenrir TTY screen reader -# By Chrys, Storm Dragon, and contributers. - -from fenrirscreenreader.core import debug - -class command(): - def __init__(self): - pass - def initialize(self, environment): - self.env = environment - def shutdown(self): - pass - def getDescription(self): - return _('Presents the text which was last received') - - def run(self): - self.env['runtime']['outputManager'].presentText(self.env['screen']['newDelta'], interrupt=True) - - def setCallback(self, callback): - pass diff --git a/src/fenrirscreenreader/commands/commands/progress_bar_monitor.py b/src/fenrirscreenreader/commands/commands/progress_bar_monitor.py new file mode 100644 index 00000000..083e4b6e --- /dev/null +++ b/src/fenrirscreenreader/commands/commands/progress_bar_monitor.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Fenrir TTY screen reader +# By Chrys, Storm Dragon, and contributers. + +from fenrirscreenreader.core import debug +import re +import time +import threading + +class command(): + def __init__(self): + pass + + def initialize(self, environment): + self.env = environment + # Use commandBuffer like other commands + if 'progressMonitoring' not in self.env['commandBuffer']: + # Check if progress monitoring should be enabled by default from settings + try: + defaultEnabled = self.env['runtime']['settingsManager'].getSettingAsBool('sound', 'progressMonitoring') + except: + # If setting doesn't exist, default to False + defaultEnabled = False + self.env['commandBuffer']['progressMonitoring'] = defaultEnabled + self.env['commandBuffer']['lastProgressTime'] = 0 + self.env['commandBuffer']['lastProgressValue'] = -1 + + def shutdown(self): + self.stopProgressMonitoring() + + def getDescription(self): + return _('Toggle progress bar monitoring with ascending tones') + + def run(self): + # Check if commandBuffer exists + if 'progressMonitoring' not in self.env['commandBuffer']: + self.env['commandBuffer']['progressMonitoring'] = False + self.env['commandBuffer']['lastProgressTime'] = 0 + self.env['commandBuffer']['lastProgressValue'] = -1 + + if self.env['commandBuffer']['progressMonitoring']: + self.stopProgressMonitoring() + self.env['runtime']['outputManager'].presentText(_("Progress monitoring disabled"), interrupt=True) + else: + self.startProgressMonitoring() + self.env['runtime']['outputManager'].presentText(_("Progress monitoring enabled"), interrupt=True) + + def startProgressMonitoring(self): + self.env['commandBuffer']['progressMonitoring'] = True + self.env['commandBuffer']['lastProgressTime'] = time.time() + self.env['commandBuffer']['lastProgressValue'] = -1 + # Don't control speech - let user decide with silence_until_prompt + + def stopProgressMonitoring(self): + self.env['commandBuffer']['progressMonitoring'] = False + # Don't control speech - progress monitor is beep-only + + def detectProgress(self, text): + if not self.env['runtime']['progressMonitoring']: + return + + currentTime = time.time() + + # Pattern 1: Percentage (50%, 25.5%, etc.) + percentMatch = re.search(r'(\d+(?:\.\d+)?)\s*%', text) + if percentMatch: + percentage = float(percentMatch.group(1)) + if percentage != self.env['runtime']['lastProgressValue']: + self.playProgressTone(percentage) + self.env['runtime']['lastProgressValue'] = percentage + self.env['runtime']['lastProgressTime'] = currentTime + return + + # Pattern 2: Fraction (15/100, 3 of 10, etc.) + fractionMatch = re.search(r'(\d+)\s*(?:of|/)\s*(\d+)', text) + if fractionMatch: + current = int(fractionMatch.group(1)) + total = int(fractionMatch.group(2)) + if total > 0: + percentage = (current / total) * 100 + if percentage != self.env['runtime']['lastProgressValue']: + self.playProgressTone(percentage) + self.env['runtime']['lastProgressValue'] = percentage + self.env['runtime']['lastProgressTime'] = currentTime + return + + # Pattern 3: Progress bars ([#### ], [====> ], etc.) + barMatch = re.search(r'\[([#=\-\*]+)([^\]]*)\]', text) + if barMatch: + filled = len(barMatch.group(1)) + total = filled + len(barMatch.group(2)) + if total > 0: + percentage = (filled / total) * 100 + if percentage != self.env['runtime']['lastProgressValue']: + self.playProgressTone(percentage) + self.env['runtime']['lastProgressValue'] = percentage + self.env['runtime']['lastProgressTime'] = currentTime + return + + # Pattern 4: Generic activity indicators (Loading..., Working..., etc.) + activityPattern = re.search(r'(loading|processing|working|installing|downloading|compiling|building).*\.{2,}', text, re.IGNORECASE) + if activityPattern: + # Play a steady beep every 2 seconds for ongoing activity + if currentTime - self.env['runtime']['lastProgressTime'] >= 2.0: + self.playActivityBeep() + self.env['runtime']['lastProgressTime'] = currentTime + + def playProgressTone(self, percentage): + # Map 0-100% to 400-1200Hz frequency range + frequency = 400 + (percentage * 8) + frequency = max(400, min(1200, frequency)) # Clamp to safe range + self.env['runtime']['outputManager'].playFrequence(frequency, 0.15, interrupt=False) + + def playActivityBeep(self): + # Single tone for generic activity + self.env['runtime']['outputManager'].playFrequence(800, 0.1, interrupt=False) + + def setCallback(self, callback): + pass \ No newline at end of file diff --git a/src/fenrirscreenreader/commands/commands/silence_until_prompt.py b/src/fenrirscreenreader/commands/commands/silence_until_prompt.py new file mode 100644 index 00000000..4c9008cf --- /dev/null +++ b/src/fenrirscreenreader/commands/commands/silence_until_prompt.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Fenrir TTY screen reader +# By Chrys, Storm Dragon, and contributers. + +from fenrirscreenreader.core import debug +import re + +class command(): + def __init__(self): + pass + + def initialize(self, environment): + self.env = environment + # Use commandBuffer like other commands + if 'silenceUntilPrompt' not in self.env['commandBuffer']: + self.env['commandBuffer']['silenceUntilPrompt'] = False + + def shutdown(self): + pass + + def getDescription(self): + return _('Toggle speech silence until shell prompt returns') + + def run(self): + if self.env['commandBuffer']['silenceUntilPrompt']: + self.disableSilence() + else: + self.enableSilence() + + def enableSilence(self): + self.env['commandBuffer']['silenceUntilPrompt'] = True + self.env['runtime']['outputManager'].presentText(_("Speech silenced until prompt returns"), soundIcon='SpeechOff', interrupt=True) + # Disable speech but don't use the normal temp disable that reactivates on keypress + self.env['runtime']['settingsManager'].setSetting('speech', 'enabled', 'False') + + def disableSilence(self): + self.env['commandBuffer']['silenceUntilPrompt'] = False + # Re-enable speech + self.env['runtime']['settingsManager'].setSetting('speech', 'enabled', 'True') + self.env['runtime']['outputManager'].presentText(_("Speech restored"), soundIcon='SpeechOn', interrupt=True) + + def checkForPrompt(self, text): + """Check if the current line contains a shell prompt pattern""" + if not self.env['commandBuffer']['silenceUntilPrompt']: + return False + + # First check for exact matches from settings (with backward compatibility) + try: + exactMatches = self.env['runtime']['settingsManager'].getSetting('prompt', 'exactMatches') + if exactMatches: + exactList = [match.strip() for match in exactMatches.split(',') if match.strip()] + for exactMatch in exactList: + if text.strip() == exactMatch: + self.env['runtime']['debug'].writeDebugOut("Found exact prompt match: " + exactMatch, debug.debugLevel.INFO) + self.disableSilence() + return True + except: + # Prompt section doesn't exist in settings, skip custom exact matches + pass + + # Get custom patterns from settings (with backward compatibility) + promptPatterns = [] + try: + customPatterns = self.env['runtime']['settingsManager'].getSetting('prompt', 'customPatterns') + # Add custom patterns from settings if they exist + if customPatterns: + customList = [pattern.strip() for pattern in customPatterns.split(',') if pattern.strip()] + promptPatterns.extend(customList) + except: + # Prompt section doesn't exist in settings, skip custom patterns + pass + + # Add default shell prompt patterns + promptPatterns.extend([ + r'^\s*\\\$\s*$', # Just $ (with whitespace) + r'^\s*#\s*$', # Just # (with whitespace) + r'^\s*>\s*$', # Just > (with whitespace) + r'.*@.*[\\\$#>]\s*$', # Contains @ and ends with prompt char (user@host style) + r'^\[.*\]\s*[\\\$#>]\s*$', # [anything]$ style prompts + r'^[a-zA-Z0-9._-]+[\\\$#>]\s*$', # Simple shell names like bash-5.1$ + ]) + + for pattern in promptPatterns: + try: + if re.search(pattern, text.strip()): + self.env['runtime']['debug'].writeDebugOut("Found prompt pattern: " + pattern, debug.debugLevel.INFO) + self.disableSilence() + return True + except re.error as e: + # Invalid regex pattern, skip it and log the error + self.env['runtime']['debug'].writeDebugOut("Invalid prompt pattern: " + pattern + " Error: " + str(e), debug.debugLevel.ERROR) + continue + + 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 new file mode 100644 index 00000000..270d3915 --- /dev/null +++ b/src/fenrirscreenreader/commands/onScreenUpdate/65000-progress_detector.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Fenrir TTY screen reader +# By Chrys, Storm Dragon, and contributers. + +from fenrirscreenreader.core import debug + +class command(): + def __init__(self): + pass + + def initialize(self, environment): + self.env = environment + + def shutdown(self): + pass + + def getDescription(self): + return 'Detects progress patterns for progress bar monitoring' + + def run(self): + # Only run if progress monitoring is enabled + try: + if 'progressMonitoring' in self.env['commandBuffer'] and self.env['commandBuffer']['progressMonitoring']: + # Only check new incoming text (newDelta), but filter out screen changes + if self.env['screen']['newDelta'] and self.isRealProgressUpdate(): + self.detectProgress(self.env['screen']['newDelta']) + except Exception as e: + # Silently ignore errors to avoid disrupting normal operation + pass + + def isRealProgressUpdate(self): + """Check if this is a real progress update vs screen change/window switch""" + # If the screen/application changed, it's not a progress update + if self.env['runtime']['screenManager'].isScreenChange(): + return False + + # If there was a large cursor movement, it's likely navigation, not progress + if self.env['runtime']['cursorManager'].isCursorVerticalMove(): + xMove = abs(self.env['screen']['newCursor']['x'] - self.env['screen']['oldCursor']['x']) + yMove = abs(self.env['screen']['newCursor']['y'] - self.env['screen']['oldCursor']['y']) + # Large movements suggest navigation, not progress output + if yMove > 2 or xMove > 20: + return False + + # Check if delta is too large (screen change) vs small incremental updates + deltaLength = len(self.env['screen']['newDelta']) + if deltaLength > 200: # Allow longer progress lines like Claude Code's status + return False + + return True + + def detectProgress(self, text): + import re + import time + + currentTime = time.time() + + # Debug: Print what we're checking + self.env['runtime']['debug'].writeDebugOut("Progress detector checking: '" + text + "'", debug.debugLevel.INFO) + + # Note: Auto-disable on 100% completion removed to respect user settings + + # Pattern 1: Percentage (50%, 25.5%, etc.) + percentMatch = re.search(r'(\d+(?:\.\d+)?)\s*%', text) + if percentMatch: + percentage = float(percentMatch.group(1)) + # Only trigger on realistic progress percentages (0-100%) + if 0 <= percentage <= 100: + self.env['runtime']['debug'].writeDebugOut("Found percentage: " + str(percentage), debug.debugLevel.INFO) + if percentage != self.env['commandBuffer']['lastProgressValue']: + self.env['runtime']['debug'].writeDebugOut("Playing tone for: " + str(percentage), debug.debugLevel.INFO) + self.playProgressTone(percentage) + self.env['commandBuffer']['lastProgressValue'] = percentage + self.env['commandBuffer']['lastProgressTime'] = currentTime + return + + # Pattern 1b: Time/token activity (not percentage-based, so use single beep) + timeMatch = re.search(r'(\d+)s\s', text) + tokenMatch = re.search(r'(\d+)\s+tokens', text) + # Pattern 1c: Curl-style transfer data (bytes, speed indicators) + curlMatch = re.search(r'(\d+\s+\d+\s+\d+\s+\d+.*?(?:k|M|G)?.*?--:--:--|Speed)', text) + + if timeMatch or tokenMatch or curlMatch: + # For non-percentage progress, use a single activity beep every 2 seconds + if currentTime - self.env['commandBuffer']['lastProgressTime'] >= 2.0: + self.env['runtime']['debug'].writeDebugOut("Playing activity beep for transfer progress", debug.debugLevel.INFO) + self.playActivityBeep() + self.env['commandBuffer']['lastProgressTime'] = currentTime + return + + # Pattern 2: Fraction (15/100, 3 of 10, etc.) + fractionMatch = re.search(r'(\d+)\s*(?:of|/)\s*(\d+)', text) + if fractionMatch: + current = int(fractionMatch.group(1)) + total = int(fractionMatch.group(2)) + if total > 0: + percentage = (current / total) * 100 + if percentage != self.env['commandBuffer']['lastProgressValue']: + self.playProgressTone(percentage) + self.env['commandBuffer']['lastProgressValue'] = percentage + self.env['commandBuffer']['lastProgressTime'] = currentTime + return + + # Pattern 3: Progress bars ([#### ], [====> ], etc.) + barMatch = re.search(r'\[([#=\-\*]+)([^\]]*)\]', text) + if barMatch: + filled = len(barMatch.group(1)) + total = filled + len(barMatch.group(2)) + if total > 0: + percentage = (filled / total) * 100 + if percentage != self.env['commandBuffer']['lastProgressValue']: + self.playProgressTone(percentage) + self.env['commandBuffer']['lastProgressValue'] = percentage + self.env['commandBuffer']['lastProgressTime'] = currentTime + return + + # Pattern 4: Generic activity indicators (Loading..., Working..., etc.) + activityPattern = re.search(r'(loading|processing|working|installing|downloading|compiling|building).*\.{2,}', text, re.IGNORECASE) + if activityPattern: + # Play a steady beep every 2 seconds for ongoing activity + if currentTime - self.env['commandBuffer']['lastProgressTime'] >= 2.0: + self.playActivityBeep() + self.env['commandBuffer']['lastProgressTime'] = currentTime + + def playProgressTone(self, percentage): + # Map 0-100% to 400-1200Hz frequency range + frequency = 400 + (percentage * 8) + frequency = max(400, min(1200, frequency)) # Clamp to safe range + + # Use Sox directly for clean quiet tones like: play -qn synth .1 tri 400 gain -8 + self.playQuietTone(frequency, 0.1) + + def playActivityBeep(self): + # Single tone for generic activity + self.playQuietTone(800, 0.08) + + def playQuietTone(self, frequency, duration): + """Play a quiet tone using Sox directly""" + import subprocess + import shlex + + # Build the Sox command: play -qn synth tri gain -8 + command = f"play -qn synth {duration} tri {frequency} gain -8" + + try: + # Only play if sound is enabled + if self.env['runtime']['settingsManager'].getSettingAsBool('sound', 'enabled'): + 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 setCallback(self, callback): + pass \ No newline at end of file diff --git a/src/fenrirscreenreader/commands/onScreenUpdate/66000-prompt_detector.py b/src/fenrirscreenreader/commands/onScreenUpdate/66000-prompt_detector.py new file mode 100644 index 00000000..d172ba8a --- /dev/null +++ b/src/fenrirscreenreader/commands/onScreenUpdate/66000-prompt_detector.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Fenrir TTY screen reader +# By Chrys, Storm Dragon, and contributers. + +from fenrirscreenreader.core import debug + +class command(): + def __init__(self): + pass + + def initialize(self, environment): + self.env = environment + + def shutdown(self): + pass + + def getDescription(self): + return 'Detects shell prompts for silence until prompt feature' + + def run(self): + # Only run if silence until prompt is active + try: + if 'silenceUntilPrompt' in self.env['commandBuffer'] and self.env['commandBuffer']['silenceUntilPrompt']: + # Check the current line for prompt patterns + if self.env['screen']['newContentText']: + lines = self.env['screen']['newContentText'].split('\n') + if lines and self.env['screen']['newCursor']['y'] < len(lines): + currentLine = lines[self.env['screen']['newCursor']['y']] + self.checkForPrompt(currentLine) + except Exception as e: + # Silently ignore errors to avoid disrupting normal operation + pass + + def checkForPrompt(self, text): + """Check if the current line contains a shell prompt pattern""" + import re + + # Debug: Print what we're checking + self.env['runtime']['debug'].writeDebugOut("Prompt detector checking: '" + text + "'", debug.debugLevel.INFO) + + # First check for exact matches from settings (with backward compatibility) + try: + exactMatches = self.env['runtime']['settingsManager'].getSetting('prompt', 'exactMatches') + if exactMatches: + exactList = [match.strip() for match in exactMatches.split(',') if match.strip()] + for exactMatch in exactList: + if text.strip() == exactMatch: + self.env['runtime']['debug'].writeDebugOut("Found exact prompt match: " + exactMatch, debug.debugLevel.INFO) + self._restoreSpeech() + return True + except: + # Prompt section doesn't exist in settings, skip custom exact matches + pass + + # Get custom patterns from settings (with backward compatibility) + promptPatterns = [] + try: + customPatterns = self.env['runtime']['settingsManager'].getSetting('prompt', 'customPatterns') + # Add custom patterns from settings if they exist + if customPatterns: + customList = [pattern.strip() for pattern in customPatterns.split(',') if pattern.strip()] + promptPatterns.extend(customList) + except: + # Prompt section doesn't exist in settings, skip custom patterns + pass + + # Add default shell prompt patterns + promptPatterns.extend([ + r'^\s*\\\$\s*$', # Just $ (with whitespace) + r'^\s*#\s*$', # Just # (with whitespace) + r'^\s*>\s*$', # Just > (with whitespace) + r'.*@.*[\\\$#>]\s*$', # Contains @ and ends with prompt char (user@host style) + r'^\[.*\]\s*[\\\$#>]\s*$', # [anything]$ style prompts + r'^[a-zA-Z0-9._-]+[\\\$#>]\s*$', # Simple shell names like bash-5.1$ + ]) + + for pattern in promptPatterns: + try: + if re.search(pattern, text.strip()): + self.env['runtime']['debug'].writeDebugOut("Found prompt pattern: " + pattern, debug.debugLevel.INFO) + self._restoreSpeech() + return True + except re.error as e: + # Invalid regex pattern, skip it and log the error + self.env['runtime']['debug'].writeDebugOut("Invalid prompt pattern: " + pattern + " Error: " + str(e), debug.debugLevel.ERROR) + continue + + return False + + def _restoreSpeech(self): + """Helper method to restore speech when prompt is detected""" + # Disable silence mode + self.env['commandBuffer']['silenceUntilPrompt'] = False + # Re-enable speech + self.env['runtime']['settingsManager'].setSetting('speech', 'enabled', 'True') + self.env['runtime']['outputManager'].presentText(_("Speech restored"), soundIcon='SpeechOn', interrupt=True) + + def setCallback(self, callback): + pass \ No newline at end of file diff --git a/src/fenrirscreenreader/core/settingsManager.py b/src/fenrirscreenreader/core/settingsManager.py index 83227365..897e2d12 100644 --- a/src/fenrirscreenreader/core/settingsManager.py +++ b/src/fenrirscreenreader/core/settingsManager.py @@ -320,6 +320,14 @@ class settingsManager(): if cliArgs.force_all_screens: environment['runtime']['force_all_screens'] = True + + if cliArgs.ignore_screen: + currentIgnoreScreen = self.getSetting('screen', 'ignoreScreen') + if currentIgnoreScreen: + ignoreScreens = currentIgnoreScreen.split(',') + cliArgs.ignore_screen + else: + ignoreScreens = cliArgs.ignore_screen + self.setSetting('screen', 'ignoreScreen', ','.join(ignoreScreens)) if not os.path.exists(self.getSetting('sound','theme') + '/soundicons.conf'): if os.path.exists(soundRoot + self.getSetting('sound','theme')): diff --git a/src/fenrirscreenreader/core/textManager.py b/src/fenrirscreenreader/core/textManager.py index f91a8d51..827c6391 100644 --- a/src/fenrirscreenreader/core/textManager.py +++ b/src/fenrirscreenreader/core/textManager.py @@ -38,7 +38,7 @@ class textManager(): if name[0] == name[1]: newText += ' ' + str(numberOfChars) + ' ' + self.env['runtime']['punctuationManager'].proceedPunctuation(name[0], True) + ' ' else: - newText += ' ' + str(int(numberOfChars / 2)) + ' ' + self.env['runtime']['punctuationManager'].proceedPunctuation(name, True) + ' ' + newText += ' ' + self.env['runtime']['punctuationManager'].proceedPunctuation(name[0], True) + ' ' + str(int(numberOfChars / 2)) + ' ' + self.env['runtime']['punctuationManager'].proceedPunctuation(name[1], True) + ' ' lastPos = span[1] if lastPos != 0: newText += ' ' @@ -46,7 +46,7 @@ class textManager(): lastPos = 0 for match in self.regExSingle.finditer(newText): span = match.span() - result += text[lastPos:span[0]] + result += newText[lastPos:span[0]] numberOfChars = len(newText[span[0]:span[1]]) name = newText[span[0]:span[1]][:2] if not self.env['runtime']['punctuationManager'].isPuctuation(name[0]): @@ -55,7 +55,7 @@ class textManager(): if name[0] == name[1]: result += ' ' + str(numberOfChars) + ' ' + self.env['runtime']['punctuationManager'].proceedPunctuation(name[0], True) + ' ' else: - result += ' ' + str(int(numberOfChars / 2)) + ' ' + self.env['runtime']['punctuationManager'].proceedPunctuation(name, True) + ' ' + result += ' ' + self.env['runtime']['punctuationManager'].proceedPunctuation(name[0], True) + ' ' + str(int(numberOfChars / 2)) + ' ' + self.env['runtime']['punctuationManager'].proceedPunctuation(name[1], True) + ' ' lastPos = span[1] if lastPos != 0: result += ' ' diff --git a/src/fenrirscreenreader/fenrirVersion.py b/src/fenrirscreenreader/fenrirVersion.py index cee7f638..4b8b2954 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.06" +version = "2025.06.07" codeName = "master"