Compare commits

..

No commits in common. "master" and "2025.02.26" have entirely different histories.

28 changed files with 224 additions and 345 deletions

View File

@ -4,7 +4,7 @@
# the entrys are seperated with :===: in words colon tripple equal colon ( to not collide with substitutions) # the entrys are seperated with :===: in words colon tripple equal colon ( to not collide with substitutions)
[levelDict] [levelDict]
none:===: none:===:
some:===:-$~+*-/\@#_ some:===:-$~+*-/\@#
most:===:.,:-$~+*-/\@!#%^&*()[]}{<>; most:===:.,:-$~+*-/\@!#%^&*()[]}{<>;
all:===:!"#$%& \'()*+,-./:;<=>?@[\\]^_`{|}~ all:===:!"#$%& \'()*+,-./:;<=>?@[\\]^_`{|}~

View File

@ -4,7 +4,7 @@
# the entrys are seperated with :===: in words colon tripple equal colon ( to not collide with substitutions) # the entrys are seperated with :===: in words colon tripple equal colon ( to not collide with substitutions)
[levelDict] [levelDict]
none:===: none:===:
some:===:-$~+*-/\@_ some:===:-$~+*-/\@
most:===:.,:-$~+*-/\@!#%^&*()[]}{<>; most:===:.,:-$~+*-/\@!#%^&*()[]}{<>;
all:===:!"#$%& \'()*+,-./:;<=>?@[\\]^_`{|}~ all:===:!"#$%& \'()*+,-./:;<=>?@[\\]^_`{|}~

View File

@ -4,7 +4,7 @@
# the entrys are seperated with :===: in words colon tripple equal colon ( to not collide with substitutions) # the entrys are seperated with :===: in words colon tripple equal colon ( to not collide with substitutions)
[levelDict] [levelDict]
none:===: none:===:
some:===:-$~+*-/\@_ some:===:-$~+*-/\@
most:===:.,:-$~+*-/\@!#%^&*()[]}{<>; most:===:.,:-$~+*-/\@!#%^&*()[]}{<>;
all:===:!"#$%& \'()*+,-./:;<=>?@[\\]^_`{|}~ all:===:!"#$%& \'()*+,-./:;<=>?@[\\]^_`{|}~

View File

@ -15,7 +15,7 @@ theme=default
# Sound volume controls how loud the sounds for your selected soundpack are. # Sound volume controls how loud the sounds for your selected soundpack are.
# 0 is quietest, 1.0 is loudest. # 0 is quietest, 1.0 is loudest.
volume=0.7 volume=1.0
# shell commands for generic sound driver # shell commands for generic sound driver
# the folowing variable are substituted # the folowing variable are substituted
@ -92,8 +92,8 @@ fenrirMaxRate=450
driver=vcsaDriver driver=vcsaDriver
encoding=auto encoding=auto
screenUpdateDelay=0.05 screenUpdateDelay=0.05
ignoreScreen= suspendingScreen=
autodetectIgnoreScreen=True autodetectSuspendingScreen=True
[keyboard] [keyboard]
driver=evdevDriver driver=evdevDriver
@ -131,7 +131,7 @@ punctuationProfile=default
punctuationLevel=some punctuationLevel=some
respectPunctuationPause=True respectPunctuationPause=True
newLinePause=True newLinePause=True
numberOfClipboards=50 numberOfClipboards=10
# used path for "export_clipboard_to_file" # used path for "export_clipboard_to_file"
# $user is replaced by username # $user is replaced by username
#clipboardExportPath=/home/$user/fenrirClipboard #clipboardExportPath=/home/$user/fenrirClipboard
@ -190,7 +190,7 @@ enableSettingsRemote=True
enableCommandRemote=True enableCommandRemote=True
[barrier] [barrier]
enabled=False enabled=True
leftBarriers=│└┌─ leftBarriers=│└┌─
rightBarriers=│┘┐─ rightBarriers=│┘┐─

View File

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

View File

@ -1,9 +1,7 @@
daemonize evdev>=1.1.2
evdev daemonize>=2.5.0
dbus-python>=1.2.8
pyudev>=0.21.0
pexpect pexpect
pyenchant pyttsx3
pyperclip pyte>=0.7.0
pyte
pyudev
pyxdg
setproctitle

View File

@ -99,10 +99,8 @@ setup(
"evdev>=1.1.2", "evdev>=1.1.2",
"daemonize>=2.5.0", "daemonize>=2.5.0",
"dbus-python>=1.2.8", "dbus-python>=1.2.8",
"pyperclip",
"pyudev>=0.21.0", "pyudev>=0.21.0",
"setuptools", "setuptools",
"setproctitle",
"pexpect", "pexpect",
"pyte>=0.7.0", "pyte>=0.7.0",
], ],
@ -113,10 +111,10 @@ if not forceSettingsFlag:
# create settings file from example if not exist # create settings file from example if not exist
if not os.path.isfile('/etc/fenrirscreenreader/settings/settings.conf'): if not os.path.isfile('/etc/fenrirscreenreader/settings/settings.conf'):
try: try:
copyfile('config/fenrirscreenreader/settings/settings.conf', '/etc/fenrirscreenreader/settings/settings.conf') copyfile('/etc/fenrirscreenreader/settings/settings.conf.example', '/etc/fenrirscreenreader/settings/settings.conf')
print('create settings file in /etc/fenrirscreenreader/settings/settings.conf') print('create settings file in /etc/fenrirscreenreader/settings/settings.conf')
except OSError as e: except:
print(f"Could not copy settings file to destination: {e}") pass
else: else:
print('settings.conf file found. It is not overwritten automatical') print('settings.conf file found. It is not overwritten automatical')

View File

@ -5,17 +5,15 @@
# By Chrys, Storm Dragon, and contributers. # By Chrys, Storm Dragon, and contributers.
from fenrirscreenreader.core import debug from fenrirscreenreader.core import debug
import os import subprocess, os
import importlib from subprocess import Popen, PIPE
import _thread import _thread
import pyperclip
class command(): class command():
def __init__(self): def __init__(self):
pass pass
def initialize(self, environment, scriptPath=''): def initialize(self, environment):
self.env = environment self.env = environment
self.scriptPath = scriptPath
def shutdown(self): def shutdown(self):
pass pass
def getDescription(self): def getDescription(self):
@ -24,48 +22,56 @@ class command():
_thread.start_new_thread(self._threadRun , ()) _thread.start_new_thread(self._threadRun , ())
def _threadRun(self): def _threadRun(self):
try: try:
# Check if clipboard is empty
if self.env['runtime']['memoryManager'].isIndexListEmpty('clipboardHistory'): if self.env['runtime']['memoryManager'].isIndexListEmpty('clipboardHistory'):
self.env['runtime']['outputManager'].presentText(_('clipboard empty'), interrupt=True) self.env['runtime']['outputManager'].presentText(_('clipboard empty'), interrupt=True)
return return
# Get current clipboard content
clipboard = self.env['runtime']['memoryManager'].getIndexListElement('clipboardHistory') clipboard = self.env['runtime']['memoryManager'].getIndexListElement('clipboardHistory')
user = self.env['general']['currUser']
# Remember original display environment variable if it exists # First try to find xclip in common locations
originalDisplay = os.environ.get('DISPLAY', '') xclip_paths = [
success = False '/usr/bin/xclip',
'/bin/xclip',
'/usr/local/bin/xclip'
]
# Try different display options xclip_path = None
for i in range(10): for path in xclip_paths:
display = f":{i}" if os.path.isfile(path) and os.access(path, os.X_OK):
try: xclip_path = path
# Set display environment variable
os.environ['DISPLAY'] = display
# Attempt to set clipboard content
importlib.reload(pyperclip) # Weird workaround for some distros
pyperclip.copy(clipboard)
# If we get here without exception, we found a working display
success = True
break break
except Exception:
# Failed for this display, try next one
continue
# Restore original display setting if not xclip_path:
if originalDisplay: self.env['runtime']['outputManager'].presentText(
os.environ['DISPLAY'] = originalDisplay 'xclip not found in common locations',
else: interrupt=True
os.environ.pop('DISPLAY', None) )
return
# Notify the user of the result for display in range(10):
if success: p = Popen(
self.env['runtime']['outputManager'].presentText(_('exported to the X session.'), interrupt=True) ['su', user, '-p', '-c', f"{xclip_path} -d :{display} -selection clipboard"],
stdin=PIPE, stdout=PIPE, stderr=PIPE, preexec_fn=os.setpgrp
)
stdout, stderr = p.communicate(input=clipboard.encode('utf-8'))
self.env['runtime']['outputManager'].interruptOutput()
stderr = stderr.decode('utf-8')
stdout = stdout.decode('utf-8')
if stderr == '':
break
if stderr != '':
self.env['runtime']['outputManager'].presentText(stderr, soundIcon='', interrupt=False)
else: else:
self.env['runtime']['outputManager'].presentText(_('failed to export to X clipboard. No available display found.'), interrupt=True) self.env['runtime']['outputManager'].presentText('exported to the X session.', interrupt=True)
except Exception as e: except Exception as e:
self.env['runtime']['outputManager'].presentText(str(e), soundIcon='', interrupt=False) self.env['runtime']['outputManager'].presentText(str(e), soundIcon='', interrupt=False)
def setCallback(self, callback): def setCallback(self, callback):
pass pass

View File

@ -5,11 +5,9 @@
# By Chrys, Storm Dragon, and contributers. # By Chrys, Storm Dragon, and contributers.
from fenrirscreenreader.core import debug from fenrirscreenreader.core import debug
import importlib import subprocess, os
from subprocess import Popen, PIPE
import _thread import _thread
import pyperclip
import os
class command(): class command():
def __init__(self): def __init__(self):
pass pass
@ -24,41 +22,33 @@ class command():
_thread.start_new_thread(self._threadRun , ()) _thread.start_new_thread(self._threadRun , ())
def _threadRun(self): def _threadRun(self):
try: try:
# Remember original display environment variable if it exists # Find xclip path
originalDisplay = os.environ.get('DISPLAY', '') xclip_paths = ['/usr/bin/xclip', '/bin/xclip', '/usr/local/bin/xclip']
clipboardContent = None xclip_path = None
for path in xclip_paths:
# Try different display options if os.path.isfile(path) and os.access(path, os.X_OK):
for i in range(10): xclip_path = path
display = f":{i}"
try:
# Set display environment variable
os.environ['DISPLAY'] = display
# Attempt to get clipboard content
importlib.reload(pyperclip) # Weird workaround for some distros
clipboardContent = pyperclip.paste()
# If we get here without exception, we found a working display
if clipboardContent:
break break
except Exception: if not xclip_path:
# Failed for this display, try next one self.env['runtime']['outputManager'].presentText('xclip not found in common locations', interrupt=True)
continue return
xClipboard = ''
# Restore original display setting for display in range(10):
if originalDisplay: p = Popen('su ' + self.env['general']['currUser'] + ' -p -c "' + xclip_path + ' -d :' + str(display) + ' -o"', stdout=PIPE, stderr=PIPE, shell=True)
os.environ['DISPLAY'] = originalDisplay stdout, stderr = p.communicate()
self.env['runtime']['outputManager'].interruptOutput()
stderr = stderr.decode('utf-8')
xClipboard = stdout.decode('utf-8')
if (stderr == ''):
break
if stderr != '':
self.env['runtime']['outputManager'].presentText(stderr , soundIcon='', interrupt=False)
else: else:
os.environ.pop('DISPLAY', None) self.env['runtime']['memoryManager'].addValueToFirstIndex('clipboardHistory', xClipboard)
# Process the clipboard content if we found any
if clipboardContent and isinstance(clipboardContent, str):
self.env['runtime']['memoryManager'].addValueToFirstIndex('clipboardHistory', clipboardContent)
self.env['runtime']['outputManager'].presentText('Import to Clipboard', soundIcon='CopyToClipboard', interrupt=True) self.env['runtime']['outputManager'].presentText('Import to Clipboard', soundIcon='CopyToClipboard', interrupt=True)
self.env['runtime']['outputManager'].presentText(clipboardContent, soundIcon='', interrupt=False) self.env['runtime']['outputManager'].presentText(xClipboard, soundIcon='', interrupt=False)
else:
self.env['runtime']['outputManager'].presentText('No text found in clipboard or no accessible display', interrupt=True)
except Exception as e: except Exception as e:
self.env['runtime']['outputManager'].presentText(str(e), soundIcon='', interrupt=False) self.env['runtime']['outputManager'].presentText(e , soundIcon='', interrupt=False)
def setCallback(self, callback): def setCallback(self, callback):
pass pass

View File

@ -35,18 +35,12 @@ class command():
if not (self.env['runtime']['byteManager'].getLastByteKey() in [b'^[[A',b'^[[B']): if not (self.env['runtime']['byteManager'].getLastByteKey() in [b'^[[A',b'^[[B']):
return 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']] prevLine = self.env['screen']['oldContentText'].split('\n')[self.env['screen']['newCursor']['y']]
currLine = self.env['screen']['newContentText'].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 prevLine == currLine:
if self.env['screen']['newDelta'] != '': if self.env['screen']['newDelta'] != '':
return return
if not currLine.isspace():
announce = currLine
if not is_blank:
currPrompt = currLine.find('$') currPrompt = currLine.find('$')
rootPrompt = currLine.find('#') rootPrompt = currLine.find('#')
if currPrompt <= 0: if currPrompt <= 0:
@ -61,13 +55,13 @@ class command():
else: else:
announce = currLine announce = currLine
if is_blank: if currLine.isspace():
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: 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_DELETE_ECHO'] = True
self.env['commandsIgnore']['onScreenUpdate']['CHAR_ECHO'] = True self.env['commandsIgnore']['onScreenUpdate']['CHAR_ECHO'] = True
self.env['commandsIgnore']['onScreenUpdate']['INCOMING_IGNORE'] = True self.env['commandsIgnore']['onScreenUpdate']['INCOMING_IGNORE'] = True
def setCallback(self, callback): def setCallback(self, callback):
pass pass

View File

@ -159,13 +159,7 @@ class commandManager():
self.env['runtime']['debug'].writeDebugOut("Loading script:" + fileName ,debug.debugLevel.ERROR) self.env['runtime']['debug'].writeDebugOut("Loading script:" + fileName ,debug.debugLevel.ERROR)
self.env['runtime']['debug'].writeDebugOut(str(e),debug.debugLevel.ERROR) self.env['runtime']['debug'].writeDebugOut(str(e),debug.debugLevel.ERROR)
continue continue
def shutdownCommands(self, section): def shutdownCommands(self, section):
# Check if the section exists in the commands dictionary
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]): for command in sorted(self.env['commands'][section]):
try: try:
self.env['commands'][section][command].shutdown() self.env['commands'][section][command].shutdown()
@ -176,7 +170,7 @@ class commandManager():
continue continue
def executeSwitchTrigger(self, trigger, unLoadScript, loadScript): def executeSwitchTrigger(self, trigger, unLoadScript, loadScript):
if self.env['runtime']['screenManager'].isIgnoredScreen(): if self.env['runtime']['screenManager'].isSuspendingScreen():
return return
#unload #unload
oldScript = unLoadScript oldScript = unLoadScript
@ -199,7 +193,7 @@ class commandManager():
def executeDefaultTrigger(self, trigger, force=False): def executeDefaultTrigger(self, trigger, force=False):
if not force: if not force:
if self.env['runtime']['screenManager'].isIgnoredScreen(): if self.env['runtime']['screenManager'].isSuspendingScreen():
return return
for command in sorted(self.env['commands'][trigger]): for command in sorted(self.env['commands'][trigger]):
if self.commandExists(command, trigger): if self.commandExists(command, trigger):
@ -214,7 +208,7 @@ class commandManager():
self.env['runtime']['debug'].writeDebugOut("Executing trigger:" + trigger + "." + command + str(e) ,debug.debugLevel.ERROR) self.env['runtime']['debug'].writeDebugOut("Executing trigger:" + trigger + "." + command + str(e) ,debug.debugLevel.ERROR)
def executeCommand(self, command, section = 'commands'): def executeCommand(self, command, section = 'commands'):
if self.env['runtime']['screenManager'].isIgnoredScreen(): if self.env['runtime']['screenManager'].isSuspendingScreen():
return return
if self.commandExists(command, section): if self.commandExists(command, section):
try: try:

View File

@ -11,14 +11,6 @@ class cursorManager():
pass pass
def initialize(self, environment): def initialize(self, environment):
self.env = 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): def shutdown(self):
pass pass
def clearMarks(self): def clearMarks(self):

View File

@ -52,15 +52,12 @@ class debugManager():
else: else:
if not self._fileOpened and fileMode: if not self._fileOpened and fileMode:
self.openDebugFile() self.openDebugFile()
timestamp = str(datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S.%f'))
if onAnyLevel: if onAnyLevel:
levelInfo = 'INFO ANY' msg = 'ANY '+ str(level) + ' ' + str(datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S.%f'))
else: else:
levelInfo = str(level) msg = str(level) +' ' + str(datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S.%f')
)
# Changed order: text comes first, then level and timestamp msg += ': ' + text
msg = text + ' - ' + levelInfo + ' ' + timestamp
if printMode: if printMode:
print(msg) print(msg)
if fileMode: if fileMode:

View File

@ -55,7 +55,7 @@ class fenrirManager():
if self.environment['runtime']['inputManager'].noKeyPressed(): if self.environment['runtime']['inputManager'].noKeyPressed():
self.environment['runtime']['inputManager'].clearLastDeepInput() self.environment['runtime']['inputManager'].clearLastDeepInput()
if self.environment['runtime']['screenManager'].isIgnoredScreen(): if self.environment['runtime']['screenManager'].isSuspendingScreen():
self.environment['runtime']['inputManager'].writeEventBuffer() self.environment['runtime']['inputManager'].writeEventBuffer()
else: else:
if self.environment['runtime']['helpManager'].isTutorialMode(): if self.environment['runtime']['helpManager'].isTutorialMode():

View File

@ -42,25 +42,6 @@ class inputDriver():
if not self._initialized: if not self._initialized:
return True return True
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): def hasIDevices(self):
if not self._initialized: if not self._initialized:
return False return False

View File

@ -49,7 +49,6 @@ class inputManager():
return event return event
def setExecuteDeviceGrab(self, newExecuteDeviceGrab = True): def setExecuteDeviceGrab(self, newExecuteDeviceGrab = True):
self.executeDeviceGrab = newExecuteDeviceGrab self.executeDeviceGrab = newExecuteDeviceGrab
def handleDeviceGrab(self, force = False): def handleDeviceGrab(self, force = False):
if force: if force:
self.setExecuteDeviceGrab() self.setExecuteDeviceGrab()
@ -62,38 +61,17 @@ class inputManager():
if not self.env['runtime']['settingsManager'].getSettingAsBool('keyboard', 'grabDevices'): if not self.env['runtime']['settingsManager'].getSettingAsBool('keyboard', 'grabDevices'):
self.executeDeviceGrab = False self.executeDeviceGrab = False
return 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(): if self.env['runtime']['screenManager'].getCurrScreenIgnored():
while not self.ungrabAllDevices(): 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) time.sleep(0.25)
self.env['runtime']['debug'].writeDebugOut(f"retry ungrabAllDevices {retryCount}/{maxRetries}", debug.debugLevel.WARNING) self.env['runtime']['debug'].writeDebugOut("retry ungrabAllDevices " ,debug.debugLevel.WARNING)
self.env['runtime']['debug'].writeDebugOut("All devices ungrabbed" ,debug.debugLevel.INFO)
else: else:
while not self.grabAllDevices(): 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) time.sleep(0.25)
self.env['runtime']['debug'].writeDebugOut(f"retry grabAllDevices {retryCount}/{maxRetries}", debug.debugLevel.WARNING) self.env['runtime']['debug'].writeDebugOut("retry grabAllDevices" ,debug.debugLevel.WARNING)
self.env['runtime']['debug'].writeDebugOut("All devices grabbed" ,debug.debugLevel.INFO)
self.executeDeviceGrab = False self.executeDeviceGrab = False
def sendKeys(self, keyMacro): def sendKeys(self, keyMacro):
for e in keyMacro: for e in keyMacro:
key = '' key = ''
@ -274,32 +252,10 @@ class inputManager():
def getCurrShortcut(self, inputSequence = None): def getCurrShortcut(self, inputSequence = None):
shortcut = [] shortcut = []
shortcut.append(self.env['input']['shortcutRepeat']) 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: 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) shortcut.append(inputSequence)
else: 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']) shortcut.append(self.env['input']['currInput'])
if len(self.env['input']['prevInput']) < len(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 = [] shortcut = []

View File

@ -59,7 +59,7 @@ class screenManager():
if self.isCurrScreenIgnoredChanged(): if self.isCurrScreenIgnoredChanged():
self.env['runtime']['inputManager'].setExecuteDeviceGrab() self.env['runtime']['inputManager'].setExecuteDeviceGrab()
self.env['runtime']['inputManager'].handleDeviceGrab() self.env['runtime']['inputManager'].handleDeviceGrab()
if not self.isIgnoredScreen(self.env['screen']['newTTY']): if not self.isSuspendingScreen(self.env['screen']['newTTY']):
self.update(eventData, 'onScreenChange') self.update(eventData, 'onScreenChange')
self.env['screen']['lastScreenUpdate'] = time.time() self.env['screen']['lastScreenUpdate'] = time.time()
else: else:
@ -81,7 +81,7 @@ class screenManager():
return self.prevScreenIgnored return self.prevScreenIgnored
def updateScreenIgnored(self): def updateScreenIgnored(self):
self.prevScreenIgnored = self.currScreenIgnored self.prevScreenIgnored = self.currScreenIgnored
self.currScreenIgnored = self.isIgnoredScreen(self.env['screen']['newTTY']) self.currScreenIgnored = self.isSuspendingScreen(self.env['screen']['newTTY'])
def update(self, eventData, trigger='onUpdate'): def update(self, eventData, trigger='onUpdate'):
# set new "old" values # set new "old" values
self.env['screen']['oldContentBytes'] = self.env['screen']['newContentBytes'] self.env['screen']['oldContentBytes'] = self.env['screen']['newContentBytes']
@ -174,16 +174,16 @@ class screenManager():
except Exception as e: except Exception as e:
self.env['runtime']['debug'].writeDebugOut('screenManager:update:highlight: ' + str(e),debug.debugLevel.ERROR) self.env['runtime']['debug'].writeDebugOut('screenManager:update:highlight: ' + str(e),debug.debugLevel.ERROR)
def isIgnoredScreen(self, screen = None): def isSuspendingScreen(self, screen = None):
if screen == None: if screen == None:
screen = self.env['screen']['newTTY'] screen = self.env['screen']['newTTY']
ignoreScreens = [] ignoreScreens = []
fixIgnoreScreens = self.env['runtime']['settingsManager'].getSetting('screen', 'ignoreScreen') fixIgnoreScreens = self.env['runtime']['settingsManager'].getSetting('screen', 'suspendingScreen')
if fixIgnoreScreens != '': if fixIgnoreScreens != '':
ignoreScreens.extend(fixIgnoreScreens.split(',')) ignoreScreens.extend(fixIgnoreScreens.split(','))
if self.env['runtime']['settingsManager'].getSettingAsBool('screen', 'autodetectIgnoreScreen'): if self.env['runtime']['settingsManager'].getSettingAsBool('screen', 'autodetectSuspendingScreen'):
ignoreScreens.extend(self.env['screen']['autoIgnoreScreens']) ignoreScreens.extend(self.env['screen']['autoIgnoreScreens'])
self.env['runtime']['debug'].writeDebugOut('screenManager:isIgnoredScreen ignore:' + str(ignoreScreens) + ' current:'+ str(screen ), debug.debugLevel.INFO) self.env['runtime']['debug'].writeDebugOut('screenManager:isSuspendingScreen ignore:' + str(ignoreScreens) + ' current:'+ str(screen ), debug.debugLevel.INFO)
return (screen in ignoreScreens) return (screen in ignoreScreens)
def isScreenChange(self): def isScreenChange(self):

View File

@ -40,8 +40,8 @@ settingsData = {
'driver': 'vcsaDriver', 'driver': 'vcsaDriver',
'encoding': 'auto', 'encoding': 'auto',
'screenUpdateDelay': 0.1, 'screenUpdateDelay': 0.1,
'ignoreScreen': '', 'suspendingScreen': '',
'autodetectIgnoreScreen': False, 'autodetectSuspendingScreen': False,
}, },
'general':{ 'general':{
'debugLevel': debug.debugLevel.DEACTIVE, 'debugLevel': debug.debugLevel.DEACTIVE,

View File

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

View File

@ -72,16 +72,14 @@ class driver(inputDriver):
self.env['runtime']['debug'].writeDebugOut('plugInputDeviceWatchdogUdev:' + str(device), debug.debugLevel.INFO) self.env['runtime']['debug'].writeDebugOut('plugInputDeviceWatchdogUdev:' + str(device), debug.debugLevel.INFO)
try: try:
try: try:
# FIX: Check if attributes exist before accessing them if device.name.upper() in ['','SPEAKUP','FENRIR-UINPUT']:
if hasattr(device, 'name') and device.name and device.name.upper() in ['','SPEAKUP','FENRIR-UINPUT']:
ignorePlug = True ignorePlug = True
if hasattr(device, 'phys') and device.phys and device.phys.upper() in ['','SPEAKUP','FENRIR-UINPUT']: if device.phys.upper() in ['','SPEAKUP','FENRIR-UINPUT']:
ignorePlug = True ignorePlug = True
if hasattr(device, 'name') and device.name and 'BRLTTY' in device.name.upper(): if 'BRLTTY' in device.name.upper():
ignorePlug = True ignorePlug = True
except Exception as e: except Exception as e:
self.env['runtime']['debug'].writeDebugOut("plugInputDeviceWatchdogUdev CHECK NAME CRASH: " + str(e),debug.debugLevel.ERROR) self.env['runtime']['debug'].writeDebugOut("plugInputDeviceWatchdogUdev CHECK NAME CRASH: " + str(e),debug.debugLevel.ERROR)
if not ignorePlug: if not ignorePlug:
virtual = '/sys/devices/virtual/input/' in device.sys_path virtual = '/sys/devices/virtual/input/' in device.sys_path
if device.device_node: if device.device_node:
@ -91,7 +89,7 @@ class driver(inputDriver):
try: try:
pollTimeout = 1 pollTimeout = 1
device = monitor.poll(pollTimeout) device = monitor.poll(pollTimeout)
except Exception: except:
device = None device = None
ignorePlug = False ignorePlug = False
if validDevices: if validDevices:
@ -148,7 +146,7 @@ class driver(inputDriver):
if uDevice: if uDevice:
if self.gDevices[iDevice.fd]: if self.gDevices[iDevice.fd]:
self.writeUInput(uDevice, event) self.writeUInput(uDevice, event)
except Exception: except Exception as e:
pass pass
def writeUInput(self, uDevice, event): def writeUInput(self, uDevice, event):
@ -193,7 +191,7 @@ class driver(inputDriver):
try: try:
with open(deviceFile) as f: with open(deviceFile) as f:
pass pass
except Exception: except Exception as e:
continue continue
# 3 pos absolute # 3 pos absolute
# 2 pos relative # 2 pos relative
@ -203,12 +201,11 @@ class driver(inputDriver):
except: except:
continue continue
try: try:
# FIX: Check if attributes exist before accessing them if currDevice.name.upper() in ['','SPEAKUP','FENRIR-UINPUT']:
if hasattr(currDevice, 'name') and currDevice.name and currDevice.name.upper() in ['', 'SPEAKUP', 'FENRIR-UINPUT']:
continue continue
if hasattr(currDevice, 'phys') and currDevice.phys and currDevice.phys.upper() in ['', 'SPEAKUP', 'FENRIR-UINPUT']: if currDevice.phys.upper() in ['','SPEAKUP','FENRIR-UINPUT']:
continue continue
if hasattr(currDevice, 'name') and currDevice.name and 'BRLTTY' in currDevice.name.upper(): if 'BRLTTY' in currDevice.name.upper():
continue continue
except: except:
pass pass
@ -236,11 +233,7 @@ class driver(inputDriver):
self.addDevice(currDevice) self.addDevice(currDevice)
self.env['runtime']['debug'].writeDebugOut('Device added (Name):' + self.iDevices[currDevice.fd].name,debug.debugLevel.INFO) self.env['runtime']['debug'].writeDebugOut('Device added (Name):' + self.iDevices[currDevice.fd].name,debug.debugLevel.INFO)
except Exception as e: except Exception as e:
try: self.env['runtime']['debug'].writeDebugOut("Device Skipped (Exception): " + deviceFile +' ' + currDevice.name +' '+ str(e),debug.debugLevel.INFO)
device_name = currDevice.name if hasattr(currDevice, 'name') else "unknown"
self.env['runtime']['debug'].writeDebugOut("Device Skipped (Exception): " + deviceFile + ' ' + device_name + ' ' + str(e), debug.debugLevel.INFO)
except:
self.env['runtime']['debug'].writeDebugOut("Device Skipped (Exception): " + deviceFile + ' ' + str(e), debug.debugLevel.INFO)
self.iDeviceNo = len(evdev.list_devices()) self.iDeviceNo = len(evdev.list_devices())
self.updateMPiDevicesFD() self.updateMPiDevicesFD()
@ -254,7 +247,6 @@ class driver(inputDriver):
self.iDevicesFD.remove(fd) self.iDevicesFD.remove(fd)
except: except:
pass pass
def mapEvent(self, event): def mapEvent(self, event):
if not self._initialized: if not self._initialized:
return None return None
@ -276,7 +268,7 @@ class driver(inputDriver):
mEvent['EventState'] = event.value mEvent['EventState'] = event.value
mEvent['EventType'] = event.type mEvent['EventType'] = event.type
return mEvent return mEvent
except Exception: except Exception as e:
return None return None
def getLedState(self, led = 0): def getLedState(self, led = 0):
@ -289,7 +281,6 @@ class driver(inputDriver):
if led in dev.leds(): if led in dev.leds():
return True return True
return False return False
def toggleLedState(self, led = 0): def toggleLedState(self, led = 0):
if not self.hasIDevices(): if not self.hasIDevices():
return False return False
@ -302,7 +293,6 @@ class driver(inputDriver):
self.iDevices[i].set_led(led , 0) self.iDevices[i].set_led(led , 0)
else: else:
self.iDevices[i].set_led(led , 1) self.iDevices[i].set_led(led , 1)
def grabAllDevices(self): def grabAllDevices(self):
if not self._initialized: if not self._initialized:
return True return True
@ -311,7 +301,6 @@ class driver(inputDriver):
if not self.gDevices[fd]: if not self.gDevices[fd]:
ok = ok and self.grabDevice(fd) ok = ok and self.grabDevice(fd)
return ok return ok
def ungrabAllDevices(self): def ungrabAllDevices(self):
if not self._initialized: if not self._initialized:
return True return True
@ -320,7 +309,6 @@ class driver(inputDriver):
if self.gDevices[fd]: if self.gDevices[fd]:
ok = ok and self.ungrabDevice(fd) ok = ok and self.ungrabDevice(fd)
return ok return ok
def createUInputDev(self, fd): def createUInputDev(self, fd):
if not self.env['runtime']['settingsManager'].getSettingAsBool('keyboard', 'grabDevices'): if not self.env['runtime']['settingsManager'].getSettingAsBool('keyboard', 'grabDevices'):
self.uDevices[fd] = None self.uDevices[fd] = None
@ -348,7 +336,6 @@ class driver(inputDriver):
except Exception as e: except Exception as e:
self.env['runtime']['debug'].writeDebugOut('InputDriver evdev: init Uinput not possible: ' + str(e),debug.debugLevel.ERROR) self.env['runtime']['debug'].writeDebugOut('InputDriver evdev: init Uinput not possible: ' + str(e),debug.debugLevel.ERROR)
return return
def addDevice(self, newDevice): def addDevice(self, newDevice):
self.env['runtime']['debug'].writeDebugOut('InputDriver evdev: device added: ' + str(newDevice.fd) + ' ' +str(newDevice),debug.debugLevel.INFO) self.env['runtime']['debug'].writeDebugOut('InputDriver evdev: device added: ' + str(newDevice.fd) + ' ' +str(newDevice),debug.debugLevel.INFO)
try: try:
@ -373,9 +360,6 @@ class driver(inputDriver):
def grabDevice(self, fd): def grabDevice(self, fd):
if not self.env['runtime']['settingsManager'].getSettingAsBool('keyboard', 'grabDevices'): if not self.env['runtime']['settingsManager'].getSettingAsBool('keyboard', 'grabDevices'):
return True return True
# FIX: Handle exception variable scope correctly
grab_error = None
try: try:
self.iDevices[fd].grab() self.iDevices[fd].grab()
self.gDevices[fd] = True self.gDevices[fd] = True
@ -387,26 +371,19 @@ class driver(inputDriver):
try: try:
self.uDevices[fd].write(e.EV_KEY, key, 0) # 0 = key up self.uDevices[fd].write(e.EV_KEY, key, 0) # 0 = key up
self.uDevices[fd].syn() self.uDevices[fd].syn()
except Exception as mod_error: except Exception as e:
self.env['runtime']['debug'].writeDebugOut('Failed to reset modifier key: ' + str(mod_error), debug.debugLevel.WARNING) self.env['runtime']['debug'].writeDebugOut('Failed to reset modifier key: ' + str(e), debug.debugLevel.WARNING)
except IOError: except IOError:
if not self.gDevices[fd]: if not self.gDevices[fd]:
return False return False
except Exception as ex: except Exception as e:
grab_error = ex self.env['runtime']['debug'].writeDebugOut('InputDriver evdev: grabing not possible: ' + str(e),debug.debugLevel.ERROR)
if grab_error:
self.env['runtime']['debug'].writeDebugOut('InputDriver evdev: grabing not possible: ' + str(grab_error), debug.debugLevel.ERROR)
return False return False
return True return True
def ungrabDevice(self,fd): def ungrabDevice(self,fd):
if not self.env['runtime']['settingsManager'].getSettingAsBool('keyboard', 'grabDevices'): if not self.env['runtime']['settingsManager'].getSettingAsBool('keyboard', 'grabDevices'):
return True return True
# FIX: Handle exception variable scope correctly
ungrab_error = None
try: try:
self.iDevices[fd].ungrab() self.iDevices[fd].ungrab()
self.gDevices[fd] = False self.gDevices[fd] = False
@ -414,15 +391,11 @@ class driver(inputDriver):
except IOError: except IOError:
if self.gDevices[fd]: if self.gDevices[fd]:
return False return False
except Exception as ex: # self.gDevices[fd] = False
ungrab_error = ex # #self.removeDevice(fd)
except Exception as e:
if ungrab_error:
self.env['runtime']['debug'].writeDebugOut('InputDriver evdev: ungrabing not possible: ' + str(ungrab_error), debug.debugLevel.ERROR)
return False return False
return True return True
def removeDevice(self,fd): def removeDevice(self,fd):
self.env['runtime']['debug'].writeDebugOut('InputDriver evdev: device removed: ' + str(fd) + ' ' +str(self.iDevices[fd]),debug.debugLevel.INFO) self.env['runtime']['debug'].writeDebugOut('InputDriver evdev: device removed: ' + str(fd) + ' ' +str(self.iDevices[fd]),debug.debugLevel.INFO)
self.clearEventBuffer() self.clearEventBuffer()