To make Fenrir easier to approach for new developer, start code migration to be pep8 compliant.
This commit is contained in:
@@ -4,19 +4,30 @@
|
||||
# Fenrir TTY screen reader
|
||||
# By Chrys, Storm Dragon, and contributers.
|
||||
|
||||
from fenrirscreenreader.core.i18n import _
|
||||
|
||||
|
||||
class command():
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def initialize(self, environment):
|
||||
self.env = environment
|
||||
|
||||
def shutdown(self):
|
||||
pass
|
||||
pass
|
||||
|
||||
def getDescription(self):
|
||||
return _('enables or disables tracking of highlighted')
|
||||
|
||||
def run(self):
|
||||
if not self.env['runtime']['settingsManager'].getSettingAsBool('focus', 'highlight'):
|
||||
if not self.env['runtime']['settingsManager'].getSettingAsBool(
|
||||
'focus', 'highlight'):
|
||||
return
|
||||
attributeDelta = self.env['runtime']['attributeManager'].getAttributeDelta()
|
||||
self.env['runtime']['outputManager'].presentText(attributeDelta, soundIcon='', interrupt=True, flush=False)
|
||||
attributeDelta = self.env['runtime']['attributeManager'].getAttributeDelta(
|
||||
)
|
||||
self.env['runtime']['outputManager'].presentText(
|
||||
attributeDelta, soundIcon='', interrupt=True, flush=False)
|
||||
|
||||
def setCallback(self, callback):
|
||||
pass
|
||||
|
@@ -5,18 +5,25 @@
|
||||
# By Chrys, Storm Dragon, and contributers.
|
||||
|
||||
|
||||
from fenrirscreenreader.core.i18n import _
|
||||
|
||||
|
||||
class command():
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def initialize(self, environment):
|
||||
self.env = environment
|
||||
|
||||
def shutdown(self):
|
||||
pass
|
||||
|
||||
def getDescription(self):
|
||||
return ''
|
||||
|
||||
def run(self):
|
||||
if self.env['screen']['newAttribDelta'] != '':
|
||||
return
|
||||
return
|
||||
if self.env['runtime']['screenManager'].isScreenChange():
|
||||
return
|
||||
if self.env['runtime']['cursorManager'].isCursorVerticalMove():
|
||||
@@ -25,25 +32,34 @@ class command():
|
||||
return
|
||||
# hack for pdmenu and maybe other dialog apps that place the cursor at last cell/row
|
||||
# this is not to be identified as history
|
||||
if (self.env['screen']['newCursor']['x'] == self.env['runtime']['screenManager'].getColumns() - 1) and (self.env['screen']['newCursor']['y'] == self.env['runtime']['screenManager'].getRows() - 1):
|
||||
if (self.env['screen']['newCursor']['x'] == self.env['runtime']['screenManager'].getColumns(
|
||||
) - 1) and (self.env['screen']['newCursor']['y'] == self.env['runtime']['screenManager'].getRows() - 1):
|
||||
return
|
||||
if self.env['runtime']['inputManager'].getShortcutType() in ['KEY']:
|
||||
if not (self.env['runtime']['inputManager'].getLastDeepestInput() in [['KEY_UP'],['KEY_DOWN']]):
|
||||
return
|
||||
if not (
|
||||
self.env['runtime']['inputManager'].getLastDeepestInput() in [
|
||||
['KEY_UP'],
|
||||
['KEY_DOWN']]):
|
||||
return
|
||||
elif self.env['runtime']['inputManager'].getShortcutType() in ['BYTE']:
|
||||
if not (self.env['runtime']['byteManager'].getLastByteKey() in [b'^[[A',b'^[[B']):
|
||||
return
|
||||
if not (
|
||||
self.env['runtime']['byteManager'].getLastByteKey() in [
|
||||
b'^[[A',
|
||||
b'^[[B']):
|
||||
return
|
||||
|
||||
# Get the current cursor's line from both old and new content
|
||||
prevLine = self.env['screen']['oldContentText'].split('\n')[self.env['screen']['newCursor']['y']]
|
||||
currLine = self.env['screen']['newContentText'].split('\n')[self.env['screen']['newCursor']['y']]
|
||||
|
||||
prevLine = self.env['screen']['oldContentText'].split(
|
||||
'\n')[self.env['screen']['newCursor']['y']]
|
||||
currLine = self.env['screen']['newContentText'].split(
|
||||
'\n')[self.env['screen']['newCursor']['y']]
|
||||
|
||||
is_blank = currLine.strip() == ''
|
||||
|
||||
|
||||
if prevLine == currLine:
|
||||
if self.env['screen']['newDelta'] != '':
|
||||
return
|
||||
|
||||
|
||||
announce = currLine
|
||||
if not is_blank:
|
||||
currPrompt = currLine.find('$')
|
||||
@@ -55,18 +71,22 @@ class command():
|
||||
announce = currLine
|
||||
if currPrompt > 0:
|
||||
remove_digits = str.maketrans('0123456789', ' ')
|
||||
if prevLine[:currPrompt].translate(remove_digits) == currLine[:currPrompt].translate(remove_digits):
|
||||
announce = currLine[currPrompt+1:]
|
||||
if prevLine[:currPrompt].translate(
|
||||
remove_digits) == currLine[:currPrompt].translate(remove_digits):
|
||||
announce = currLine[currPrompt + 1:]
|
||||
else:
|
||||
announce = currLine
|
||||
|
||||
if is_blank:
|
||||
self.env['runtime']['outputManager'].presentText(_("blank"), soundIcon='EmptyLine', interrupt=True, flush=False)
|
||||
self.env['runtime']['outputManager'].presentText(
|
||||
_("blank"), soundIcon='EmptyLine', interrupt=True, flush=False)
|
||||
else:
|
||||
self.env['runtime']['outputManager'].presentText(announce, interrupt=True, flush=False)
|
||||
|
||||
self.env['runtime']['outputManager'].presentText(
|
||||
announce, interrupt=True, flush=False)
|
||||
|
||||
self.env['commandsIgnore']['onScreenUpdate']['CHAR_DELETE_ECHO'] = True
|
||||
self.env['commandsIgnore']['onScreenUpdate']['CHAR_ECHO'] = True
|
||||
self.env['commandsIgnore']['onScreenUpdate']['INCOMING_IGNORE'] = True
|
||||
|
||||
def setCallback(self, callback):
|
||||
pass
|
||||
|
@@ -4,110 +4,137 @@
|
||||
# Fenrir TTY screen reader
|
||||
# By Chrys, Storm Dragon, and contributers.
|
||||
|
||||
from fenrirscreenreader.core.i18n import _
|
||||
|
||||
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']:
|
||||
# Check if current line is a prompt - if so, reset progress state
|
||||
# Check if current line is a prompt - if so, reset progress
|
||||
# state
|
||||
if self.isCurrentLinePrompt():
|
||||
self.resetProgressState()
|
||||
# Only check new incoming text (newDelta), but filter out screen changes
|
||||
# Only check new incoming text (newDelta), but filter out
|
||||
# screen changes
|
||||
elif 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 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'])
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
# Check if current line looks like a prompt - progress unlikely during prompts
|
||||
|
||||
# Check if current line looks like a prompt - progress unlikely during
|
||||
# prompts
|
||||
if self.isCurrentLinePrompt():
|
||||
return False
|
||||
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def resetProgressState(self):
|
||||
"""Reset progress state when a prompt is detected, allowing new progress operations to start fresh"""
|
||||
self.env['runtime']['debug'].writeDebugOut("Resetting progress state due to prompt detection", debug.debugLevel.INFO)
|
||||
self.env['runtime']['debug'].writeDebugOut(
|
||||
"Resetting progress state due to prompt detection",
|
||||
debug.debugLevel.INFO)
|
||||
self.env['commandBuffer']['lastProgressValue'] = -1
|
||||
self.env['commandBuffer']['lastProgressTime'] = 0
|
||||
|
||||
|
||||
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
|
||||
|
||||
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.)
|
||||
# Filter out common non-progress percentages (weather, system stats, etc.)
|
||||
# Filter out common non-progress percentages (weather, system stats,
|
||||
# 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:
|
||||
# Filter out weather/system stats that contain percentages
|
||||
if not re.search(r'\b(?:humidity|cpu|memory|disk|usage|temp|weather|forecast)\b', text, re.IGNORECASE):
|
||||
self.env['runtime']['debug'].writeDebugOut("Found percentage: " + str(percentage), debug.debugLevel.INFO)
|
||||
if not re.search(
|
||||
r'\b(?:humidity|cpu|memory|disk|usage|temp|weather|forecast)\b',
|
||||
text,
|
||||
re.IGNORECASE):
|
||||
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.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)
|
||||
|
||||
# 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: dd command output (bytes copied with transfer rate)
|
||||
ddMatch = re.search(r'\d+\s+bytes.*copied.*\d+\s+s.*[kMGT]?B/s', text)
|
||||
# Pattern 1d: Curl-style transfer data (bytes, speed indicators)
|
||||
curlMatch = re.search(r'(\d+\s+\d+\s+\d+\s+\d+.*?(?:k|M|G)?.*?--:--:--|Speed)', text)
|
||||
|
||||
curlMatch = re.search(
|
||||
r'(\d+\s+\d+\s+\d+\s+\d+.*?(?:k|M|G)?.*?--:--:--|Speed)', text)
|
||||
|
||||
if timeMatch or tokenMatch or ddMatch 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)
|
||||
# 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:
|
||||
@@ -120,7 +147,7 @@ class command():
|
||||
self.env['commandBuffer']['lastProgressValue'] = percentage
|
||||
self.env['commandBuffer']['lastProgressTime'] = currentTime
|
||||
return
|
||||
|
||||
|
||||
# Pattern 3: Progress bars ([#### ], [====> ], etc.)
|
||||
# Improved pattern to avoid matching IRC channels like [#channel]
|
||||
barMatch = re.search(r'\[([#=\-\*]+)([\s\.]*)\]', text)
|
||||
@@ -128,131 +155,157 @@ class command():
|
||||
filled = len(barMatch.group(1))
|
||||
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))):
|
||||
# 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)
|
||||
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)
|
||||
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:
|
||||
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
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
# 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)
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
# 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']])
|
||||
|
||||
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*$', # 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
|
||||
# "continue? [Y/n]" style
|
||||
r'.*continue\?\s*\[[YyNn]/[YyNn]\].*$',
|
||||
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))
|
||||
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,
|
||||
# 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('#'),
|
||||
line.rfind('>'),
|
||||
line.rfind('\\')
|
||||
)
|
||||
if promptEnd >= 0 and promptEnd < len(line) - 5: # More than just whitespace after prompt
|
||||
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
|
||||
pass
|
||||
|
@@ -4,21 +4,24 @@
|
||||
# Fenrir TTY screen reader
|
||||
# By Chrys, Storm Dragon, and contributers.
|
||||
|
||||
from fenrirscreenreader.core.i18n import _
|
||||
|
||||
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:
|
||||
@@ -26,55 +29,69 @@ class command():
|
||||
# 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']]
|
||||
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
|
||||
self.env['runtime']['debug'].writeDebugOut('prompt_detector run: Error in prompt detection: ' + str(e), debug.debugLevel.ERROR)
|
||||
|
||||
self.env['runtime']['debug'].writeDebugOut(
|
||||
'prompt_detector run: Error in prompt detection: ' + str(e),
|
||||
debug.debugLevel.ERROR)
|
||||
|
||||
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)
|
||||
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')
|
||||
exactMatches = self.env['runtime']['settingsManager'].getSetting(
|
||||
'prompt', 'exactMatches')
|
||||
if exactMatches:
|
||||
exactList = [match.strip() for match in exactMatches.split(',') if match.strip()]
|
||||
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.env['runtime']['debug'].writeDebugOut(
|
||||
"Found exact prompt match: " + exactMatch, debug.debugLevel.INFO)
|
||||
self._restoreSpeech()
|
||||
return True
|
||||
except Exception as e:
|
||||
# Prompt section doesn't exist in settings, skip custom exact matches
|
||||
# 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')
|
||||
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()]
|
||||
customList = [
|
||||
pattern.strip() for pattern in customPatterns.split(',') if pattern.strip()]
|
||||
promptPatterns.extend(customList)
|
||||
except Exception as e:
|
||||
# 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*>\s*$', # Just > (with whitespace)
|
||||
r'.*@.*[\\\$#>]\s*$', # Contains @ and ends with prompt char (user@host style)
|
||||
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$
|
||||
|
||||
# Simple shell names like bash-5.1$
|
||||
r'^[a-zA-Z0-9._-]+[\\\$#>]\s*$',
|
||||
|
||||
# Interactive prompt patterns
|
||||
# Package manager confirmation prompts
|
||||
r'.*\?\s*\[[YyNn]/[YyNn]\]\s*$', # ? [Y/n] or ? [y/N] style
|
||||
@@ -83,47 +100,57 @@ class command():
|
||||
r'.*\?\s*\([Yy]es/[Nn]o\)\s*$', # ? (Yes/No) style
|
||||
r'.*continue\?\s*\[[YyNn]/[YyNn]\].*$', # "continue? [Y/n]" style
|
||||
r'.*ok\s*\[[YyNn]/[YyNn]\].*$', # "Is this ok [y/N]:" style
|
||||
r'^::.*\?\s*\[[YyNn]/[YyNn]\].*$', # pacman ":: Proceed? [Y/n]" style
|
||||
|
||||
# pacman ":: Proceed? [Y/n]" style
|
||||
r'^::.*\?\s*\[[YyNn]/[YyNn]\].*$',
|
||||
|
||||
# Authentication prompts
|
||||
r'^\[[Ss]udo\]\s*[Pp]assword\s*for\s+.*:\s*$', # [sudo] password for user:
|
||||
# [sudo] password for user:
|
||||
r'^\[[Ss]udo\]\s*[Pp]assword\s*for\s+.*:\s*$',
|
||||
r'^[Pp]assword\s*:\s*$', # Password:
|
||||
r'.*[Pp]assword\s*:\s*$', # general password prompts
|
||||
r".*'s\s*[Pp]assword\s*:\s*$", # user's password:
|
||||
r'^[Ee]nter\s+[Pp]assphrase.*:\s*$', # Enter passphrase:
|
||||
r'^[Pp]lease\s+enter\s+[Pp]assphrase.*:\s*$', # Please enter passphrase:
|
||||
|
||||
# Please enter passphrase:
|
||||
r'^[Pp]lease\s+enter\s+[Pp]assphrase.*:\s*$',
|
||||
|
||||
# General confirmation and continuation prompts
|
||||
r'^[Pp]ress\s+any\s+key\s+to\s+continue.*$', # Press any key to continue
|
||||
# Press any key to continue
|
||||
r'^[Pp]ress\s+any\s+key\s+to\s+continue.*$',
|
||||
r'^[Aa]re\s+you\s+sure\?\s*.*$', # Are you sure?
|
||||
r'^[Pp]lease\s+confirm.*$', # Please confirm
|
||||
r'.*confirm.*\([YyNn]/[YyNn]\).*$', # confirm (y/n)
|
||||
r'.*\([Yy]/[Nn]\)\s*$', # ends with (Y/n) or (y/N)
|
||||
])
|
||||
|
||||
|
||||
for pattern in promptPatterns:
|
||||
try:
|
||||
if re.search(pattern, text.strip()):
|
||||
self.env['runtime']['debug'].writeDebugOut("Found prompt pattern: " + pattern, debug.debugLevel.INFO)
|
||||
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)
|
||||
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
|
||||
# Also disable the keypress-based speech restoration since we're enabling it now
|
||||
# Also disable the keypress-based speech restoration since we're
|
||||
# enabling it now
|
||||
if 'enableSpeechOnKeypress' in self.env['commandBuffer']:
|
||||
self.env['commandBuffer']['enableSpeechOnKeypress'] = False
|
||||
# Re-enable speech
|
||||
self.env['runtime']['settingsManager'].setSetting('speech', 'enabled', 'True')
|
||||
self.env['runtime']['outputManager'].presentText(_("Speech restored"), soundIcon='SpeechOn', interrupt=True)
|
||||
|
||||
self.env['runtime']['settingsManager'].setSetting(
|
||||
'speech', 'enabled', 'True')
|
||||
self.env['runtime']['outputManager'].presentText(
|
||||
_("Speech restored"), soundIcon='SpeechOn', interrupt=True)
|
||||
|
||||
def setCallback(self, callback):
|
||||
pass
|
||||
pass
|
||||
|
@@ -5,34 +5,47 @@
|
||||
# By Chrys, Storm Dragon, and contributers.
|
||||
|
||||
|
||||
from fenrirscreenreader.core.i18n import _
|
||||
|
||||
|
||||
class command():
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def initialize(self, environment):
|
||||
self.env = environment
|
||||
|
||||
def shutdown(self):
|
||||
pass
|
||||
|
||||
def getDescription(self):
|
||||
return 'No Description found'
|
||||
|
||||
def run(self):
|
||||
if not self.env['runtime']['settingsManager'].getSettingAsBool('speech', 'autoReadIncoming'):
|
||||
if not self.env['runtime']['settingsManager'].getSettingAsBool(
|
||||
'speech', 'autoReadIncoming'):
|
||||
return
|
||||
# is there something to read?
|
||||
if not self.env['runtime']['screenManager'].isDelta(ignoreSpace=True):
|
||||
return
|
||||
|
||||
# this must be a keyecho or something
|
||||
#if len(self.env['screen']['newDelta'].strip(' \n\t')) <= 1:
|
||||
xMove = abs(self.env['screen']['newCursor']['x'] - self.env['screen']['oldCursor']['x'])
|
||||
yMove = abs(self.env['screen']['newCursor']['y'] - self.env['screen']['oldCursor']['y'])
|
||||
|
||||
# if len(self.env['screen']['newDelta'].strip(' \n\t')) <= 1:
|
||||
xMove = abs(
|
||||
self.env['screen']['newCursor']['x'] -
|
||||
self.env['screen']['oldCursor']['x'])
|
||||
yMove = abs(
|
||||
self.env['screen']['newCursor']['y'] -
|
||||
self.env['screen']['oldCursor']['y'])
|
||||
|
||||
if (xMove >= 1) and xMove == len(self.env['screen']['newDelta']):
|
||||
# if len(self.env['screen']['newDelta'].strip(' \n\t0123456789')) <= 2:
|
||||
if not '\n' in self.env['screen']['newDelta']:
|
||||
# if len(self.env['screen']['newDelta'].strip(' \n\t0123456789'))
|
||||
# <= 2:
|
||||
if '\n' not in self.env['screen']['newDelta']:
|
||||
return
|
||||
#print(xMove, yMove, len(self.env['screen']['newDelta']), len(self.env['screen']['newNegativeDelta']))
|
||||
self.env['runtime']['outputManager'].presentText(self.env['screen']['newDelta'], interrupt=False, flush=False)
|
||||
# print(xMove, yMove, len(self.env['screen']['newDelta']), len(self.env['screen']['newNegativeDelta']))
|
||||
self.env['runtime']['outputManager'].presentText(
|
||||
self.env['screen']['newDelta'], interrupt=False, flush=False)
|
||||
|
||||
def setCallback(self, callback):
|
||||
pass
|
||||
|
@@ -1,4 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from fenrirscreenreader.core.i18n import _
|
||||
|
||||
import time
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
@@ -9,26 +12,41 @@ import time
|
||||
class command():
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def initialize(self, environment):
|
||||
self.env = environment
|
||||
|
||||
def shutdown(self):
|
||||
pass
|
||||
|
||||
def getDescription(self):
|
||||
return 'No Description found'
|
||||
|
||||
def run(self):
|
||||
if not self.env['runtime']['settingsManager'].getSettingAsBool('promote', 'enabled'):
|
||||
if not self.env['runtime']['settingsManager'].getSettingAsBool(
|
||||
'promote', 'enabled'):
|
||||
return
|
||||
if self.env['runtime']['settingsManager'].getSetting('promote', 'list').strip(" \t\n") == '':
|
||||
if self.env['runtime']['settingsManager'].getSetting(
|
||||
'promote', 'list').strip(" \t\n") == '':
|
||||
return
|
||||
if int(time.time() - self.env['input']['lastInputTime']) < self.env['runtime']['settingsManager'].getSettingAsInt('promote', 'inactiveTimeoutSec'):
|
||||
if int(
|
||||
time.time() -
|
||||
self.env['input']['lastInputTime']) < self.env['runtime']['settingsManager'].getSettingAsInt(
|
||||
'promote',
|
||||
'inactiveTimeoutSec'):
|
||||
return
|
||||
if len(self.env['runtime']['settingsManager'].getSetting('promote', 'list')) == 0:
|
||||
if len(
|
||||
self.env['runtime']['settingsManager'].getSetting(
|
||||
'promote',
|
||||
'list')) == 0:
|
||||
return
|
||||
for promote in self.env['runtime']['settingsManager'].getSetting('promote', 'list').split(','):
|
||||
if promote in self.env['screen']['newDelta']:
|
||||
self.env['runtime']['outputManager'].playSoundIcon('PromotedText')
|
||||
for promote in self.env['runtime']['settingsManager'].getSetting(
|
||||
'promote', 'list').split(','):
|
||||
if promote in self.env['screen']['newDelta']:
|
||||
self.env['runtime']['outputManager'].playSoundIcon(
|
||||
'PromotedText')
|
||||
self.env['input']['lastInputTime'] = time.time()
|
||||
return
|
||||
|
||||
def setCallback(self, callback):
|
||||
pass
|
||||
|
||||
|
@@ -5,21 +5,32 @@
|
||||
# By Chrys, Storm Dragon, and contributers.
|
||||
|
||||
|
||||
from fenrirscreenreader.core.i18n import _
|
||||
|
||||
|
||||
class command():
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def initialize(self, environment):
|
||||
self.env = environment
|
||||
|
||||
def shutdown(self):
|
||||
pass
|
||||
|
||||
def getDescription(self):
|
||||
return 'No Description found'
|
||||
|
||||
def run(self):
|
||||
if not self.env['runtime']['settingsManager'].getSettingAsBool('barrier','enabled'):
|
||||
if not self.env['runtime']['settingsManager'].getSettingAsBool(
|
||||
'barrier', 'enabled'):
|
||||
return
|
||||
if not self.env['runtime']['screenManager'].isDelta(ignoreSpace=True):
|
||||
return
|
||||
self.env['runtime']['barrierManager'].handleLineBarrier(self.env['screen']['newContentText'].split('\n'), self.env['screen']['newCursor']['x'],self.env['screen']['newCursor']['y'])
|
||||
self.env['runtime']['barrierManager'].handleLineBarrier(
|
||||
self.env['screen']['newContentText'].split('\n'),
|
||||
self.env['screen']['newCursor']['x'],
|
||||
self.env['screen']['newCursor']['y'])
|
||||
|
||||
def setCallback(self, callback):
|
||||
pass
|
||||
|
||||
|
Reference in New Issue
Block a user