Fixed version for master branch.

This commit is contained in:
Storm Dragon 2025-04-14 20:04:14 -04:00
commit 7a87fb51bb
18 changed files with 141 additions and 70 deletions

View File

@ -1,12 +1,11 @@
#!/bin/bash
#!/usr/bin/env bash
#Basic install script for Fenrir.
read -p "This will install Fenrir. Press ctrl+C to cancel, or enter to continue." continue
read -rp "This will install Fenrir. Press ctrl+C to cancel, or enter to continue."
# Fenrir main application
install -m755 -d /opt/fenrirscreenreader
cp -af src/* /opt/fenrirscreenreader
ln -fs /opt/fenrirscreenreader/fenrir-daemon /usr/bin/fenrir-daemon
ln -fs /opt/fenrirscreenreader/fenrir /usr/bin/fenrir
# tools
install -m755 -d /usr/share/fenrirscreenreader/tools
@ -33,8 +32,9 @@ cp -af config/sound/template /usr/share/sounds/fenrirscreenreader/template
# config
if [ -f "/etc/fenrirscreenreader/settings/settings.conf" ]; then
echo "Do you want to overwrite your current global settings? (y/n)"
read yn
if [ $yn = "Y" -o $yn = "y" ]; then
read -r yn
yn="${yn:0:1}"
if [[ "${yn^}" == "Y" ]]; then
mv /etc/fenrirscreenreader/settings/settings.conf /etc/fenrirscreenreader/settings/settings.conf.bak
echo "Your old settings.conf has been backed up to settings.conf.bak."
install -m644 -D "config/settings/settings.conf" /etc/fenrirscreenreader/settings/settings.conf

View File

@ -3,6 +3,6 @@ daemonize>=2.5.0
dbus-python>=1.2.8
pyudev>=0.21.0
pexpect
ppyperclip
pyperclip
pyte>=0.7.0
rapidfuzz>=2.0.0

View File

@ -105,7 +105,7 @@ class attributeManager():
cursorPos = cursor.copy()
try:
attribute = self.getAttributeByXY( cursorPos['x'], cursorPos['y'])
if update:
self.setLastCursorAttribute(attribute)
if not self.isLastCursorAttributeChange():
@ -155,13 +155,13 @@ class attributeManager():
attributeFormatString = attributeFormatString.replace('fenrirFGColor', _(attribute[0]))
except Exception as e:
attributeFormatString = attributeFormatString.replace('fenrirFGColor', '')
# 1 BG color (name)
try:
attributeFormatString = attributeFormatString.replace('fenrirBGColor', _(attribute[1]))
except Exception as e:
attributeFormatString = attributeFormatString.replace('fenrirBGColor', '')
# 2 bold (True/ False)
try:
if attribute[2]:
@ -169,7 +169,7 @@ class attributeManager():
except Exception as e:
pass
attributeFormatString = attributeFormatString.replace('fenrirBold', '')
# 3 italics (True/ False)
try:
if attribute[3]:
@ -177,7 +177,7 @@ class attributeManager():
except Exception as e:
pass
attributeFormatString = attributeFormatString.replace('fenrirItalics', '')
# 4 underline (True/ False)
try:
if attribute[4]:
@ -185,7 +185,7 @@ class attributeManager():
except Exception as e:
pass
attributeFormatString = attributeFormatString.replace('fenrirUnderline', '')
# 5 strikethrough (True/ False)
try:
if attribute[5]:
@ -193,7 +193,7 @@ class attributeManager():
except Exception as e:
pass
attributeFormatString = attributeFormatString.replace('fenrirStrikethrough', '')
# 6 reverse (True/ False)
try:
if attribute[6]:
@ -201,7 +201,7 @@ class attributeManager():
except Exception as e:
pass
attributeFormatString = attributeFormatString.replace('fenrirReverse', '')
# 7 blink (True/ False)
try:
if attribute[7]:
@ -209,7 +209,7 @@ class attributeManager():
except Exception as e:
pass
attributeFormatString = attributeFormatString.replace('fenrirBlink', '')
# 8 font size (int/ string)
try:
try:
@ -223,14 +223,14 @@ class attributeManager():
except Exception as e:
pass
attributeFormatString = attributeFormatString.replace('fenrirFontSize', _('default'))
# 9 font family (string)
try:
attributeFormatString = attributeFormatString.replace('fenrirFont', attribute[9])
except Exception as e:
pass
attributeFormatString = attributeFormatString.replace('fenrirFont', _('default'))
return attributeFormatString
def trackHighlights(self):
result = ''
@ -287,4 +287,4 @@ class attributeManager():
useful = True
return useful

View File

@ -18,7 +18,7 @@ class barrierManager():
def updateBarrierChange(self, isBarrier):
self.prefIsBarrier = self.currIsBarrier
self.currIsBarrier = isBarrier
def resetBarrierChange(self):
self.currIsBarrier = False
self.prefIsBarrier = False
@ -38,7 +38,7 @@ class barrierManager():
self.env['runtime']['outputManager'].playSoundIcon(soundIcon='BarrierStart', interrupt=doInterrupt)
else:
self.env['runtime']['outputManager'].playSoundIcon(soundIcon='BarrierEnd', interrupt=doInterrupt)
if not isBarrier:
sayLine = ''
return isBarrier, sayLine

View File

@ -27,7 +27,7 @@ class commandManager():
# scripts for scriptKey
self.env['runtime']['commandManager'].loadScriptCommands()
def shutdown(self):
for commandFolder in self.env['general']['commandFolderList']:
self.env['runtime']['commandManager'].shutdownCommands(commandFolder)
@ -99,7 +99,7 @@ class commandManager():
self.env['runtime']['debug'].writeDebugOut("loadCommands: Loading command:" + command ,debug.debugLevel.ERROR)
self.env['runtime']['debug'].writeDebugOut(str(e),debug.debugLevel.ERROR)
continue
def loadScriptCommands(self, section='commands', scriptPath=''):
if scriptPath =='':
scriptPath = self.env['runtime']['settingsManager'].getSetting('general', 'scriptPath')
@ -165,7 +165,7 @@ class commandManager():
if section not in self.env['commands']:
self.env['runtime']['debug'].writeDebugOut("shutdownCommands: section not found:" + section, debug.debugLevel.WARNING)
return
for command in sorted(self.env['commands'][section]):
try:
self.env['commands'][section][command].shutdown()
@ -228,7 +228,7 @@ class commandManager():
except Exception as e:
self.env['runtime']['debug'].writeDebugOut("Executing command:" + section + "." + command +' ' + str(e),debug.debugLevel.ERROR)
def runCommand(self, command, section = 'commands'):
if self.commandExists(command, section):
try:
@ -237,7 +237,7 @@ class commandManager():
except Exception as e:
self.env['runtime']['debug'].writeDebugOut("runCommand command:" + section + "." + command +' ' + str(e),debug.debugLevel.ERROR)
self.env['commandInfo']['lastCommandExecutionTime'] = time.time()
def getCommandDescription(self, command, section = 'commands'):
if self.commandExists(command, section):
try:
@ -245,7 +245,7 @@ class commandManager():
except Exception as e:
self.env['runtime']['debug'].writeDebugOut('commandManager.getCommandDescription:' + str(e),debug.debugLevel.ERROR)
self.env['commandInfo']['lastCommandExecutionTime'] = time.time()
def commandExists(self, command, section = 'commands'):
try:
return( command in self.env['commands'][section])

View File

@ -11,6 +11,14 @@ class cursorManager():
pass
def initialize(self, environment):
self.env = environment
def shouldProcessNumpadCommands(self):
"""
Check if numpad commands should be processed based on numlock state
Return True if numlock is OFF (commands should work)
Return False if numlock is ON (let keys type numbers)
"""
# Return False if numlock is ON
return not self.env['input']['newNumLock']
def shutdown(self):
pass
def clearMarks(self):
@ -47,7 +55,7 @@ class cursorManager():
return
self.env['screen']['oldCursorReview'] = None
self.env['screen']['newCursorReview'] = None
def isCursorHorizontalMove(self):
return self.env['screen']['newCursor']['x'] != self.env['screen']['oldCursor']['x']
@ -56,7 +64,7 @@ class cursorManager():
def isReviewMode(self):
return self.env['screen']['newCursorReview'] != None
def enterReviewModeCurrTextCursor(self, overwrite=False):
if self.isReviewMode() and not overwrite:
return
@ -73,7 +81,7 @@ class cursorManager():
self.env['screen']['oldCursorReview'] = self.env['screen']['newCursorReview']
self.env['screen']['newCursorReview']['x'] = x
self.env['screen']['newCursorReview']['y'] = y
def isApplicationWindowSet(self):
try:
currApp = self.env['runtime']['applicationManager'].getCurrentApplication()
@ -108,7 +116,7 @@ class cursorManager():
currApp = self.env['runtime']['applicationManager'].getCurrentApplication()
self.env['commandBuffer']['windowArea'][currApp] = {}
if x1 * y1 <= \
x2 * y2:
self.env['commandBuffer']['windowArea'][currApp]['1'] = {'x':x1, 'y':y1}

View File

@ -36,14 +36,14 @@ class debugManager():
except Exception as e:
print(e)
def writeDebugOut(self, text, level = debug.debugLevel.DEACTIVE, onAnyLevel=False):
mode = self.env['runtime']['settingsManager'].getSetting('general','debugMode')
if mode == '':
mode = 'FILE'
mode = mode.upper().split(',')
fileMode = 'FILE' in mode
printMode = 'PRINT' in mode
if (self.env['runtime']['settingsManager'].getSettingAsInt('general','debugLevel') < int(level)) and \
not (onAnyLevel and self.env['runtime']['settingsManager'].getSettingAsInt('general','debugLevel') > int(debug.debugLevel.DEACTIVE)) :
if self._fileOpened:

View File

@ -21,7 +21,7 @@ class eventManager():
self.env = environment
def shutdown(self):
self.cleanEventQueue()
def proceedEventLoop(self):
event = self._eventQueue.get()
st = time.time()

View File

@ -23,11 +23,11 @@ class fenrirManager():
raise RuntimeError('Cannot Initialize. Maybe the configfile is not available or not parseable')
except RuntimeError:
raise
self.environment['runtime']['outputManager'].presentText(_("Start Fenrir"), soundIcon='ScreenReaderOn', interrupt=True)
signal.signal(signal.SIGINT, self.captureSignal)
signal.signal(signal.SIGTERM, self.captureSignal)
self.isInitialized = True
self.modifierInput = False
self.singleKeyCommand = False
@ -42,10 +42,10 @@ class fenrirManager():
def handleInput(self, event):
self.environment['runtime']['debug'].writeDebugOut('DEBUG INPUT fenrirMan:' + str(event), debug.debugLevel.INFO)
if not event['Data']:
event['Data'] = self.environment['runtime']['inputManager'].getInputEvent()
if event['Data']:
event['Data']['EventName'] = self.environment['runtime']['inputManager'].convertEventName(event['Data']['EventName'])
self.environment['runtime']['inputManager'].handleInputEvent(event['Data'])
@ -54,7 +54,7 @@ class fenrirManager():
if self.environment['runtime']['inputManager'].noKeyPressed():
self.environment['runtime']['inputManager'].clearLastDeepInput()
if self.environment['runtime']['screenManager'].isSuspendingScreen():
self.environment['runtime']['inputManager'].writeEventBuffer()
else:
@ -74,7 +74,7 @@ class fenrirManager():
self.environment['runtime']['inputManager'].clearEventBuffer()
else:
self.environment['runtime']['inputManager'].writeEventBuffer()
if self.environment['runtime']['inputManager'].noKeyPressed():
self.modifierInput = False
self.singleKeyCommand = False
@ -83,7 +83,7 @@ class fenrirManager():
if self.environment['input']['keyForeward'] > 0:
self.environment['input']['keyForeward'] -= 1
self.environment['runtime']['commandManager'].executeDefaultTrigger('onKeyInput')
def handleByteInput(self, event):
@ -124,14 +124,14 @@ class fenrirManager():
def handleScreenUpdate(self, event):
self.environment['runtime']['screenManager'].handleScreenUpdate(event['Data'])
if time.time() - self.environment['runtime']['inputManager'].getLastInputTime() >= 0.3:
self.environment['runtime']['inputManager'].clearLastDeepInput()
if (self.environment['runtime']['cursorManager'].isCursorVerticalMove() or
self.environment['runtime']['cursorManager'].isCursorHorizontalMove()):
self.environment['runtime']['commandManager'].executeDefaultTrigger('onCursorChange')
self.environment['runtime']['commandManager'].executeDefaultTrigger('onScreenUpdate')
self.environment['runtime']['inputManager'].clearLastDeepInput()
@ -150,17 +150,17 @@ class fenrirManager():
def detectShortcutCommand(self):
if self.environment['input']['keyForeward'] > 0:
return
if len(self.environment['input']['prevInput']) > len(self.environment['input']['currInput']):
return
if self.environment['runtime']['inputManager'].isKeyPress():
self.modifierInput = self.environment['runtime']['inputManager'].currKeyIsModifier()
else:
if not self.environment['runtime']['inputManager'].noKeyPressed():
if self.singleKeyCommand:
self.singleKeyCommand = len(self.environment['input']['currInput']) == 1
if not(self.singleKeyCommand and self.environment['runtime']['inputManager'].noKeyPressed()):
currentShortcut = self.environment['runtime']['inputManager'].getCurrShortcut()
self.command = self.environment['runtime']['inputManager'].getCommandForShortcut(currentShortcut)
@ -220,7 +220,7 @@ class fenrirManager():
self.environment['runtime']['outputManager'].presentText(_("Quit Fenrir"), soundIcon='ScreenReaderOff', interrupt=True)
self.environment['runtime']['eventManager'].cleanEventQueue()
time.sleep(0.6)
for currentManager in self.environment['general']['managerList']:
if self.environment['runtime'][currentManager]:
self.environment['runtime'][currentManager].shutdown()

View File

@ -42,6 +42,25 @@ class inputDriver():
if not self._initialized:
return True
return True
def forceUngrab(self):
"""Emergency method to release grabbed devices in case of failure"""
if not self._initialized:
return True
try:
# Try standard ungrab first
return self.ungrabAllDevices()
except Exception as e:
# Just log the failure and inform the user
if hasattr(self, 'env') and 'runtime' in self.env and 'debug' in self.env['runtime']:
self.env['runtime']['debug'].writeDebugOut(
f"Emergency device release failed: {str(e)}",
debug.debugLevel.ERROR
)
else:
print(f"Emergency device release failed: {str(e)}")
return False
def hasIDevices(self):
if not self._initialized:
return False

View File

@ -49,6 +49,7 @@ class inputManager():
return event
def setExecuteDeviceGrab(self, newExecuteDeviceGrab = True):
self.executeDeviceGrab = newExecuteDeviceGrab
def handleDeviceGrab(self, force = False):
if force:
self.setExecuteDeviceGrab()
@ -61,17 +62,38 @@ class inputManager():
if not self.env['runtime']['settingsManager'].getSettingAsBool('keyboard', 'grabDevices'):
self.executeDeviceGrab = False
return
# Add maximum retries to prevent infinite loops
maxRetries = 5
retryCount = 0
grabTimeout = 3 # Timeout in seconds
startTime = time.time()
if self.env['runtime']['screenManager'].getCurrScreenIgnored():
while not self.ungrabAllDevices():
retryCount += 1
if retryCount >= maxRetries or (time.time() - startTime) > grabTimeout:
self.env['runtime']['debug'].writeDebugOut("Failed to ungrab devices after multiple attempts", debug.debugLevel.ERROR)
# Force a release of devices if possible through alternative means
try:
self.env['runtime']['inputDriver'].forceUngrab()
except:
pass
break
time.sleep(0.25)
self.env['runtime']['debug'].writeDebugOut("retry ungrabAllDevices " ,debug.debugLevel.WARNING)
self.env['runtime']['debug'].writeDebugOut("All devices ungrabbed" ,debug.debugLevel.INFO)
self.env['runtime']['debug'].writeDebugOut(f"retry ungrabAllDevices {retryCount}/{maxRetries}", debug.debugLevel.WARNING)
else:
while not self.grabAllDevices():
retryCount += 1
if retryCount >= maxRetries or (time.time() - startTime) > grabTimeout:
self.env['runtime']['debug'].writeDebugOut("Failed to grab devices after multiple attempts", debug.debugLevel.ERROR)
# Continue without grabbing input - limited functionality but not locked
break
time.sleep(0.25)
self.env['runtime']['debug'].writeDebugOut("retry grabAllDevices" ,debug.debugLevel.WARNING)
self.env['runtime']['debug'].writeDebugOut("All devices grabbed" ,debug.debugLevel.INFO)
self.env['runtime']['debug'].writeDebugOut(f"retry grabAllDevices {retryCount}/{maxRetries}", debug.debugLevel.WARNING)
self.executeDeviceGrab = False
def sendKeys(self, keyMacro):
for e in keyMacro:
key = ''
@ -252,17 +274,39 @@ class inputManager():
def getCurrShortcut(self, inputSequence = None):
shortcut = []
shortcut.append(self.env['input']['shortcutRepeat'])
numpadKeys = ['KEY_KP0', 'KEY_KP1', 'KEY_KP2', 'KEY_KP3', 'KEY_KP4',
'KEY_KP5', 'KEY_KP6', 'KEY_KP7', 'KEY_KP8', 'KEY_KP9',
'KEY_KPDOT', 'KEY_KPPLUS', 'KEY_KPMINUS', 'KEY_KPASTERISK',
'KEY_KPSLASH', 'KEY_KPENTER', 'KEY_KPEQUAL']
if inputSequence:
# Check if any key in the sequence is a numpad key and numlock is ON
# If numlock is ON and any key in the sequence is a numpad key, return an empty shortcut
if not self.env['runtime']['cursorManager'].shouldProcessNumpadCommands():
for key in inputSequence:
if key in numpadKeys:
# Return an empty/invalid shortcut that won't match any command
return "[]"
shortcut.append(inputSequence)
else:
# Same check for current input
if not self.env['runtime']['cursorManager'].shouldProcessNumpadCommands():
for key in self.env['input']['currInput']:
if key in numpadKeys:
# Return an empty/invalid shortcut that won't match any command
return "[]"
shortcut.append(self.env['input']['currInput'])
if len(self.env['input']['prevInput']) < len(self.env['input']['currInput']):
if self.env['input']['shortcutRepeat'] > 1 and not self.shortcutExists(str(shortcut)):
if self.env['input']['shortcutRepeat'] > 1 and not self.shortcutExists(str(shortcut)):
shortcut = []
self.env['input']['shortcutRepeat'] = 1
shortcut.append(self.env['input']['shortcutRepeat'])
shortcut.append(self.env['input']['currInput'])
self.env['runtime']['debug'].writeDebugOut("currShortcut " + str(shortcut) ,debug.debugLevel.INFO)
self.env['runtime']['debug'].writeDebugOut("currShortcut " + str(shortcut), debug.debugLevel.INFO)
return str(shortcut)
def currKeyIsModifier(self):

View File

@ -22,7 +22,7 @@ class outputManager():
def shutdown(self):
self.env['runtime']['settingsManager'].shutdownDriver('soundDriver')
self.env['runtime']['settingsManager'].shutdownDriver('speechDriver')
def presentText(self, text, interrupt=True, soundIcon='', ignorePunctuation=False, announceCapital=False, flush=True):
if text == '':
return
@ -58,13 +58,13 @@ class outputManager():
except Exception as e:
self.env['runtime']['debug'].writeDebugOut("setting speech language in outputManager.speakText", debug.debugLevel.ERROR)
self.env['runtime']['debug'].writeDebugOut(str(e), debug.debugLevel.ERROR)
try:
self.env['runtime']['speechDriver'].setVoice(self.env['runtime']['settingsManager'].getSetting('speech', 'voice'))
except Exception as e:
self.env['runtime']['debug'].writeDebugOut("Error while setting speech voice in outputManager.speakText", debug.debugLevel.ERROR)
self.env['runtime']['debug'].writeDebugOut(str(e), debug.debugLevel.ERROR)
try:
if announceCapital:
self.env['runtime']['speechDriver'].setPitch(self.env['runtime']['settingsManager'].getSettingAsFloat('speech', 'capitalPitch'))
@ -73,13 +73,13 @@ class outputManager():
except Exception as e:
self.env['runtime']['debug'].writeDebugOut("setting speech pitch in outputManager.speakText", debug.debugLevel.ERROR)
self.env['runtime']['debug'].writeDebugOut(str(e), debug.debugLevel.ERROR)
try:
self.env['runtime']['speechDriver'].setRate(self.env['runtime']['settingsManager'].getSettingAsFloat('speech', 'rate'))
except Exception as e:
self.env['runtime']['debug'].writeDebugOut("setting speech rate in outputManager.speakText", debug.debugLevel.ERROR)
self.env['runtime']['debug'].writeDebugOut(str(e), debug.debugLevel.ERROR)
try:
self.env['runtime']['speechDriver'].setModule(self.env['runtime']['settingsManager'].getSetting('speech', 'module'))
except Exception as e:
@ -91,7 +91,7 @@ class outputManager():
except Exception as e:
self.env['runtime']['debug'].writeDebugOut("setting speech volume in outputManager.speakText ", debug.debugLevel.ERROR)
self.env['runtime']['debug'].writeDebugOut(str(e), debug.debugLevel.ERROR)
try:
if self.env['runtime']['settingsManager'].getSettingAsBool('general', 'newLinePause'):
cleanText = text.replace('\n', ' , ')

View File

@ -20,7 +20,7 @@ class processManager():
self.addSimpleEventThread(fenrirEventType.HeartBeat, self.heartBeatTimer, multiprocess=True)
def shutdown(self):
self.terminateAllProcesses()
def terminateAllProcesses(self):
for proc in self._Processes:
#try:

View File

@ -172,7 +172,7 @@ class remoteManager():
self.env['runtime']['outputManager'].presentText(_('clipboard exported to file'), interrupt=True)
except Exception as e:
self.env['runtime']['debug'].writeDebugOut('export_clipboard_to_file:run: Filepath:'+ clipboardFile +' trace:' + str(e),debug.debugLevel.ERROR)
def saveSettings(self, settingConfigPath = None):
if not settingConfigPath:
settingConfigPath = self.env['runtime']['settingsManager'].getSettingsFile()

View File

@ -83,7 +83,7 @@ class screenManager():
def updateScreenIgnored(self):
self.prevScreenIgnored = self.currScreenIgnored
self.currScreenIgnored = self.isSuspendingScreen(self.env['screen']['newTTY'])
def update(self, eventData, trigger='onUpdate'):
# set new "old" values
self.env['screen']['oldContentBytes'] = self.env['screen']['newContentBytes']
@ -146,11 +146,11 @@ class screenManager():
cursorLineEndOffset = cursorLineStart + self.env['screen']['newCursor']['x'] + 3
oldScreenText = self.env['screen']['oldContentText'][cursorLineStartOffset:cursorLineEndOffset]
newScreenText = self.env['screen']['newContentText'][cursorLineStartOffset:cursorLineEndOffset]
# Use the original differ for typing mode to preserve behavior
diff = self.differ.compare(oldScreenText, newScreenText)
diffList = list(diff)
typing = True
tempNewDelta = ''.join(x[2:] for x in diffList if x[0] == '+')
if tempNewDelta.strip() != '':
@ -169,11 +169,11 @@ class screenManager():
# Process line by line using rapidfuzz
old_lines = oldScreenText.split('\n')
new_lines = newScreenText.split('\n')
# Use standard differ for better word grouping
diff = self.differ.compare(old_lines, new_lines)
diffList = list(diff)
except Exception as e:
# Fall back to standard differ if there's any issue
self.env['runtime']['debug'].writeDebugOut('screenManager:update:rapidfuzz: ' + str(e), debug.debugLevel.ERROR)
@ -209,7 +209,7 @@ class screenManager():
ignoreScreens.extend(self.env['screen']['autoIgnoreScreens'])
self.env['runtime']['debug'].writeDebugOut('screenManager:isSuspendingScreen ignore:' + str(ignoreScreens) + ' current:'+ str(screen ), debug.debugLevel.INFO)
return (screen in ignoreScreens)
def isScreenChange(self):
if not self.env['screen']['oldTTY']:
return False

View File

@ -29,7 +29,7 @@ class speechDriver():
return
if not queueable:
self.cancel()
def cancel(self):
if not self._isInitialized:
return

View File

@ -34,7 +34,7 @@ class tableManager():
return ''
def setRowColumnSep(self, columnSep = ''):
self.rowColumnSep = columnSep
def setHeadLine(self, headLine = ''):
self.setHeadColumnSep()
self.setRowColumnSep()

View File

@ -4,5 +4,5 @@
# Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributers.
version = "2025.03.02"
version = "2025.04.14"
codeName = "master"