Merged for new release.
This commit is contained in:
commit
3dca3e5b23
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 <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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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
|
121
src/fenrirscreenreader/commands/commands/progress_bar_monitor.py
Normal file
121
src/fenrirscreenreader/commands/commands/progress_bar_monitor.py
Normal file
@ -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
|
@ -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
|
@ -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 <duration> tri <frequency> 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
|
@ -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
|
@ -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')):
|
||||
|
@ -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 += ' '
|
||||
|
@ -4,5 +4,5 @@
|
||||
# Fenrir TTY screen reader
|
||||
# By Chrys, Storm Dragon, and contributers.
|
||||
|
||||
version = "2025.06.06"
|
||||
version = "2025.06.07"
|
||||
codeName = "master"
|
||||
|
Loading…
x
Reference in New Issue
Block a user