22 Commits

Author SHA1 Message Date
7a12992b88 latest release. 2025-04-17 00:36:26 -04:00
26a8c8cf86 Added setproctitle to dependencies. 2025-04-17 00:35:02 -04:00
bbfd2790a9 Attempt to fix import/export GUI clipboard for some distros. 2025-04-16 21:50:37 -04:00
6edb743c23 Removed rapidfuzz from dependencies. 2025-04-16 12:14:30 -04:00
a1fcee9a45 Removed the rapidfuzz dependency. There wasn't much of a gain in performance and it did not work on some distros. 2025-04-16 12:06:54 -04:00
4e6e6e2d17 Fixed a couple of bugs in setup.py. 2025-04-16 00:52:44 -04:00
7a87fb51bb Fixed version for master branch. 2025-04-14 20:04:14 -04:00
676c2b07a9 A couple of small improvements to install.sh. 2025-04-14 20:02:00 -04:00
2dda73ac87 Request to be able to use the numpad if numlock is on and only process fenrir commands if numlock is off. This should work, let me know if anything breaks. 2025-04-14 18:57:10 -04:00
f68a1af223 Fixed a typo in requirements.txt. 2025-04-10 05:46:22 -04:00
5ab66f6978 Attempt to fix the bug where Fenrir freezes the computer solid if it manages to start before sound is ready. It shuld at least release the keyboard now. 2025-03-20 02:55:39 -04:00
2cc2fda28c Actually fix the version file this time. 2025-03-02 17:59:20 -05:00
c99d0f6ee1 Fixed version.py. 2025-03-02 17:44:32 -05:00
1552b962a1 Updated dependencies. 2025-03-02 17:43:01 -05:00
09391bfe84 Experimental fix for evdev failures. 2025-03-02 17:24:45 -05:00
e76ca9889a Same update for export to x clipboard. Now using pyperclip. 2025-03-02 16:04:38 -05:00
73206ce393 Switched from xclip to pyperclip for import from x. 2025-03-02 15:25:06 -05:00
5b642cd9e2 Fixed error in settings file. 2025-02-26 17:41:01 -05:00
4966b87ba1 pyttsx removed from setup file because it's no longer a speech option. 2025-02-26 17:38:22 -05:00
145cab6221 Updated dependencies to include rapidfuzz. 2025-02-26 17:08:50 -05:00
e46926f145 Fixed a traceback on shutdown. Hopefully improved responsiveness with the diff. Trying rapidfuzz for smaller screen updates, add a catch to fall back to the original difflib if there are any problems. This is experimental, please watch for bugs. 2025-02-26 17:02:25 -05:00
8cd50c5070 Hopefully improve accuracy of blank line reporting. 2025-02-26 16:05:17 -05:00
27 changed files with 451 additions and 509 deletions

View File

@ -1 +0,0 @@
3.13

View File

@ -1,12 +1,11 @@
#!/bin/bash #!/usr/bin/env bash
#Basic install script for Fenrir. #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 # 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
@ -33,8 +32,9 @@ 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 yn read -r yn
if [ $yn = "Y" -o $yn = "y" ]; then yn="${yn:0:1}"
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,42 +0,0 @@
[build-system]
requires = ["setuptools >= 61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "fenrir-screenreader"
version="2025.01.28"
authors = [
{name = "Hunter Jozwiak", email = "hunter.t.joz@gmail.com"},
{name="Storm Dragon", email="storm_dragon@stormux.org"},
{name="Jeremiah Ticket", email="seashellpromises@gmail.com"},
{name="Chrys", email="chrys@linux-a11y.org"},
]
maintainers = [
{name = "Hunter Jozwiak", email = "hunter.t.joz@gmail.com"},
{name = "Storm dragon", email = "storm_dragon@stormux.org"}]
keywords=['screenreader', 'a11y', 'accessibility', 'terminal', 'TTY', 'console']
classifiers=[
"Programming Language :: Python",
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
"Development Status :: 5 - Production/Stable",
"Topic :: Multimedia :: Sound/Audio :: Speech",
"Environment :: Console",
]
description = "A TTY screenreader for Linux."
readme = "README.md"
requires-python = ">=3.6"
dependencies = [
"daemonize>=2.5.0",
"dbus-python>=1.2.18",
"evdev>=1.7.1",
"pexpect>=4.9.0",
"pyte>=0.8.1",
"pyudev>=0.23.2",
]
[project.scripts]
fenrir = "fenrirscreenreader:cli.main"
[dependency-groups]
dev = [
"ruff>=0.0.17",
]

View File

@ -1,10 +0,0 @@
[project]
name = "fenrir-screenreader"
author="Storm Dragon, Jeremiah, Chrys and others"
version = "2024.12.20"
description = "A TTY screenreader for Linux."
readme = "README.md"
requires-python = ">=3.6"
dependencies = [
"evdev>=1.7.1",
]

8
requirements.txt Normal file
View File

@ -0,0 +1,8 @@
evdev>=1.1.2
daemonize>=2.5.0
dbus-python>=1.2.8
pyudev>=0.21.0
pexpect
pyperclip
pyte>=0.7.0
setproctitle

132
setup.py Executable file
View File

@ -0,0 +1,132 @@
#!/usr/bin/env python3
import os, glob, sys
import os.path
from shutil import copyfile
from setuptools import find_namespace_packages
from setuptools import setup
# handle flags for package manager like aurman and pacaur.
# Allow both environment variable and command line flag
forceSettingsFlag = (
"--force-settings" in sys.argv or
os.environ.get('FENRIR_FORCE_SETTINGS') == '1'
)
if "--force-settings" in sys.argv:
sys.argv.remove("--force-settings")
dataFiles = []
# Handle locale files
localeFiles = glob.glob('locale/*/LC_MESSAGES/*.mo')
for localeFile in localeFiles:
lang = localeFile.split(os.sep)[1]
destDir = f'/usr/share/locale/{lang}/LC_MESSAGES'
dataFiles.append((destDir, [localeFile]))
# Handle other configuration files
directories = glob.glob('config/*')
for directory in directories:
files = glob.glob(directory+'/*')
destDir = ''
if 'config/punctuation' in directory :
destDir = '/etc/fenrirscreenreader/punctuation'
elif 'config/keyboard' in directory:
destDir = '/etc/fenrirscreenreader/keyboard'
elif 'config/settings' in directory:
destDir = '/etc/fenrirscreenreader/settings'
if not forceSettingsFlag:
try:
files = [f for f in files if not f.endswith('settings.conf')]
except:
pass
elif 'config/scripts' in directory:
destDir = '/usr/share/fenrirscreenreader/scripts'
if destDir != '':
dataFiles.append((destDir, files))
files = glob.glob('config/sound/default/*')
destDir = '/usr/share/sounds/fenrirscreenreader/default'
dataFiles.append((destDir, files))
files = glob.glob('config/sound//template/*')
destDir = '/usr/share/sounds/fenrirscreenreader/template'
dataFiles.append((destDir, files))
files = glob.glob('tools/*')
dataFiles.append(('/usr/share/fenrirscreenreader/tools', files))
dataFiles.append(('/usr/share/man/man1', ['docs/fenrir.1']))
def read(fname):
return open(os.path.join(os.path.dirname(__file__), fname)).read()
setup(
# Application name:
name="fenrir-screenreader",
# description
description="A TTY Screen Reader for Linux.",
long_description=read('README.md'),
long_description_content_type="text/markdown",
keywords=['screenreader', 'a11y', 'accessibility', 'terminal', 'TTY', 'console'],
license="License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
url="https://git.stormux.org/storm/fenrir/",
classifiers=[
"Programming Language :: Python",
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
"Development Status :: 5 - Production/Stable",
"Topic :: Multimedia :: Sound/Audio :: Speech",
"Environment :: Console",
],
# Application author details:
author="Storm Dragon, Jeremiah, Chrys and others",
author_email="storm_dragon@stormux.org",
# Packages
package_dir={'': 'src'},
packages=find_namespace_packages(
where='src',
include=['fenrirscreenreader*']
),
scripts=['src/fenrir'],
# Include additional files into the package
include_package_data=True,
zip_safe=False,
data_files=dataFiles,
# Dependent packages (distributions)
python_requires='>=3.6',
install_requires=[
"evdev>=1.1.2",
"daemonize>=2.5.0",
"dbus-python>=1.2.8",
"pyperclip",
"pyudev>=0.21.0",
"setuptools",
"setproctitle",
"pexpect",
"pyte>=0.7.0",
],
)
if not forceSettingsFlag:
print('')
# create settings file from example if not exist
if not os.path.isfile('/etc/fenrirscreenreader/settings/settings.conf'):
try:
copyfile('config/fenrirscreenreader/settings/settings.conf', '/etc/fenrirscreenreader/settings/settings.conf')
print('create settings file in /etc/fenrirscreenreader/settings/settings.conf')
except OSError as e:
print(f"Could not copy settings file to destination: {e}")
else:
print('settings.conf file found. It is not overwritten automatical')
print('')
print('To have Fenrir start at boot:')
print('sudo systemctl enable fenrir')
print('Pulseaudio users may want to run:')
print('/usr/share/fenrirscreenreader/tools/configure_pulse.sh')
print('once as their user account and once as root to configure Pulseaudio.')
print('Please install the following packages manually:')
print('- Speech-dispatcher: for the default speech driver')
print('- Espeak: as basic TTS engine')
print('- sox: is a player for the generic sound driver')

7
src/fenrirscreenreader/cli.py → src/fenrir Normal file → Executable file
View File

@ -1,8 +1,9 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Fenrir TTY screen reader # Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors. # By Chrys, Storm Dragon, and contributors.
"""Module for managing the command line interface of Fenrir."""
import os import os
import sys import sys
import inspect import inspect
@ -14,8 +15,8 @@ fenrirPath = os.path.dirname(os.path.realpath(os.path.abspath(inspect.getfile(in
if not fenrirPath in sys.path: if not fenrirPath in sys.path:
sys.path.append(fenrirPath) sys.path.append(fenrirPath)
from .core import fenrirManager from fenrirscreenreader.core import fenrirManager
from . import fenrirVersion from fenrirscreenreader import fenrirVersion
def create_argument_parser(): def create_argument_parser():
"""Create and return the argument parser for Fenrir""" """Create and return the argument parser for Fenrir"""

View File

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

@ -35,12 +35,18 @@ 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:
@ -55,13 +61,13 @@ class command():
else: else:
announce = currLine announce = currLine
if currLine.isspace(): 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: 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

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

View File

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

View File

@ -27,7 +27,7 @@ class commandManager():
# scripts for scriptKey # scripts for scriptKey
self.env['runtime']['commandManager'].loadScriptCommands() self.env['runtime']['commandManager'].loadScriptCommands()
def shutdown(self): def shutdown(self):
for commandFolder in self.env['general']['commandFolderList']: for commandFolder in self.env['general']['commandFolderList']:
self.env['runtime']['commandManager'].shutdownCommands(commandFolder) 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("loadCommands: Loading command:" + command ,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 loadScriptCommands(self, section='commands', scriptPath=''): def loadScriptCommands(self, section='commands', scriptPath=''):
if scriptPath =='': if scriptPath =='':
scriptPath = self.env['runtime']['settingsManager'].getSetting('general', 'scriptPath') scriptPath = self.env['runtime']['settingsManager'].getSetting('general', 'scriptPath')
@ -159,14 +159,20 @@ 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()
del self.env['commands'][section][command] del self.env['commands'][section][command]
except Exception as e: except Exception as e:
self.env['runtime']['debug'].writeDebugOut("Shutdown command:" + section + "." + command ,debug.debugLevel.ERROR) self.env['runtime']['debug'].writeDebugOut("Shutdown command:" + section + "." + command, 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 executeSwitchTrigger(self, trigger, unLoadScript, loadScript): def executeSwitchTrigger(self, trigger, unLoadScript, loadScript):
@ -222,7 +228,7 @@ class commandManager():
except Exception as e: except Exception as e:
self.env['runtime']['debug'].writeDebugOut("Executing command:" + section + "." + command +' ' + str(e),debug.debugLevel.ERROR) self.env['runtime']['debug'].writeDebugOut("Executing command:" + section + "." + command +' ' + str(e),debug.debugLevel.ERROR)
def runCommand(self, command, section = 'commands'): def runCommand(self, command, section = 'commands'):
if self.commandExists(command, section): if self.commandExists(command, section):
try: try:
@ -231,7 +237,7 @@ class commandManager():
except Exception as e: except Exception as e:
self.env['runtime']['debug'].writeDebugOut("runCommand command:" + section + "." + command +' ' + str(e),debug.debugLevel.ERROR) self.env['runtime']['debug'].writeDebugOut("runCommand command:" + section + "." + command +' ' + str(e),debug.debugLevel.ERROR)
self.env['commandInfo']['lastCommandExecutionTime'] = time.time() self.env['commandInfo']['lastCommandExecutionTime'] = time.time()
def getCommandDescription(self, command, section = 'commands'): def getCommandDescription(self, command, section = 'commands'):
if self.commandExists(command, section): if self.commandExists(command, section):
try: try:
@ -239,7 +245,7 @@ class commandManager():
except Exception as e: except Exception as e:
self.env['runtime']['debug'].writeDebugOut('commandManager.getCommandDescription:' + str(e),debug.debugLevel.ERROR) self.env['runtime']['debug'].writeDebugOut('commandManager.getCommandDescription:' + str(e),debug.debugLevel.ERROR)
self.env['commandInfo']['lastCommandExecutionTime'] = time.time() self.env['commandInfo']['lastCommandExecutionTime'] = time.time()
def commandExists(self, command, section = 'commands'): def commandExists(self, command, section = 'commands'):
try: try:
return( command in self.env['commands'][section]) return( command in self.env['commands'][section])

View File

@ -11,6 +11,14 @@ 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):
@ -47,7 +55,7 @@ class cursorManager():
return return
self.env['screen']['oldCursorReview'] = None self.env['screen']['oldCursorReview'] = None
self.env['screen']['newCursorReview'] = None self.env['screen']['newCursorReview'] = None
def isCursorHorizontalMove(self): def isCursorHorizontalMove(self):
return self.env['screen']['newCursor']['x'] != self.env['screen']['oldCursor']['x'] return self.env['screen']['newCursor']['x'] != self.env['screen']['oldCursor']['x']
@ -56,7 +64,7 @@ class cursorManager():
def isReviewMode(self): def isReviewMode(self):
return self.env['screen']['newCursorReview'] != None return self.env['screen']['newCursorReview'] != None
def enterReviewModeCurrTextCursor(self, overwrite=False): def enterReviewModeCurrTextCursor(self, overwrite=False):
if self.isReviewMode() and not overwrite: if self.isReviewMode() and not overwrite:
return return
@ -73,7 +81,7 @@ class cursorManager():
self.env['screen']['oldCursorReview'] = self.env['screen']['newCursorReview'] self.env['screen']['oldCursorReview'] = self.env['screen']['newCursorReview']
self.env['screen']['newCursorReview']['x'] = x self.env['screen']['newCursorReview']['x'] = x
self.env['screen']['newCursorReview']['y'] = y self.env['screen']['newCursorReview']['y'] = y
def isApplicationWindowSet(self): def isApplicationWindowSet(self):
try: try:
currApp = self.env['runtime']['applicationManager'].getCurrentApplication() currApp = self.env['runtime']['applicationManager'].getCurrentApplication()
@ -108,7 +116,7 @@ class cursorManager():
currApp = self.env['runtime']['applicationManager'].getCurrentApplication() currApp = self.env['runtime']['applicationManager'].getCurrentApplication()
self.env['commandBuffer']['windowArea'][currApp] = {} self.env['commandBuffer']['windowArea'][currApp] = {}
if x1 * y1 <= \ if x1 * y1 <= \
x2 * y2: x2 * y2:
self.env['commandBuffer']['windowArea'][currApp]['1'] = {'x':x1, 'y':y1} self.env['commandBuffer']['windowArea'][currApp]['1'] = {'x':x1, 'y':y1}

View File

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

View File

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

View File

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

View File

@ -42,6 +42,25 @@ 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,6 +49,7 @@ 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()
@ -61,17 +62,38 @@ 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("retry ungrabAllDevices " ,debug.debugLevel.WARNING) self.env['runtime']['debug'].writeDebugOut(f"retry ungrabAllDevices {retryCount}/{maxRetries}", 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("retry grabAllDevices" ,debug.debugLevel.WARNING) self.env['runtime']['debug'].writeDebugOut(f"retry grabAllDevices {retryCount}/{maxRetries}", 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 = ''
@ -252,17 +274,39 @@ 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 = []
self.env['input']['shortcutRepeat'] = 1 self.env['input']['shortcutRepeat'] = 1
shortcut.append(self.env['input']['shortcutRepeat']) shortcut.append(self.env['input']['shortcutRepeat'])
shortcut.append(self.env['input']['currInput']) 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) return str(shortcut)
def currKeyIsModifier(self): def currKeyIsModifier(self):

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,5 +3,6 @@
# Fenrir TTY screen reader # Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributers. # By Chrys, Storm Dragon, and contributers.
version = "2025.01.28"
codeName = "testing" version = "2025.04.14"
codeName = "master"

View File

@ -59,7 +59,7 @@ class driver(inputDriver):
self.env['runtime']['processManager'].addCustomEventThread(self.inputWatchdog) self.env['runtime']['processManager'].addCustomEventThread(self.inputWatchdog)
self._initialized = True self._initialized = True
def plugInputDeviceWatchdogUdev(self,active , eventQueue): def plugInputDeviceWatchdogUdev(self, active, eventQueue):
context = pyudev.Context() context = pyudev.Context()
monitor = pyudev.Monitor.from_netlink(context) monitor = pyudev.Monitor.from_netlink(context)
monitor.filter_by(subsystem='input') monitor.filter_by(subsystem='input')
@ -72,31 +72,33 @@ 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:
if device.name.upper() in ['','SPEAKUP','FENRIR-UINPUT']: # FIX: Check if attributes exist before accessing them
if hasattr(device, 'name') and device.name and device.name.upper() in ['','SPEAKUP','FENRIR-UINPUT']:
ignorePlug = True ignorePlug = True
if device.phys.upper() in ['','SPEAKUP','FENRIR-UINPUT']: if hasattr(device, 'phys') and device.phys and device.phys.upper() in ['','SPEAKUP','FENRIR-UINPUT']:
ignorePlug = True ignorePlug = True
if 'BRLTTY' in device.name.upper(): if hasattr(device, 'name') and device.name and '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:
validDevices.append({'device': device.device_node, 'virtual': virtual}) validDevices.append({'device': device.device_node, 'virtual': virtual})
except Exception as e: except Exception as e:
self.env['runtime']['debug'].writeDebugOut("plugInputDeviceWatchdogUdev APPEND CRASH: " + str(e),debug.debugLevel.ERROR) self.env['runtime']['debug'].writeDebugOut("plugInputDeviceWatchdogUdev APPEND CRASH: " + str(e), debug.debugLevel.ERROR)
try: try:
pollTimeout = 1 pollTimeout = 1
device = monitor.poll(pollTimeout) device = monitor.poll(pollTimeout)
except: except Exception:
device = None device = None
ignorePlug = False ignorePlug = False
if validDevices: if validDevices:
eventQueue.put({"Type":fenrirEventType.PlugInputDevice,"Data":validDevices}) eventQueue.put({"Type": fenrirEventType.PlugInputDevice, "Data": validDevices})
return time.time() return time.time()
def inputWatchdog(self,active , eventQueue): def inputWatchdog(self, active, eventQueue):
try: try:
while active.value: while active.value:
r, w, x = select(self.iDevices, [], [], 0.8) r, w, x = select(self.iDevices, [], [], 0.8)
@ -111,7 +113,7 @@ class driver(inputDriver):
self.removeDevice(fd) self.removeDevice(fd)
while(event): while(event):
self.env['runtime']['debug'].writeDebugOut('inputWatchdog: EVENT:' + str(event), debug.debugLevel.INFO) self.env['runtime']['debug'].writeDebugOut('inputWatchdog: EVENT:' + str(event), debug.debugLevel.INFO)
self.env['input']['eventBuffer'].append( [self.iDevices[fd], self.uDevices[fd], event]) self.env['input']['eventBuffer'].append([self.iDevices[fd], self.uDevices[fd], event])
if event.type == evdev.events.EV_KEY: if event.type == evdev.events.EV_KEY:
if not foundKeyInSequence: if not foundKeyInSequence:
foundKeyInSequence = True foundKeyInSequence = True
@ -123,11 +125,11 @@ class driver(inputDriver):
if not isinstance(currMapEvent['EventName'], str): if not isinstance(currMapEvent['EventName'], str):
event = self.iDevices[fd].read_one() event = self.iDevices[fd].read_one()
continue continue
if currMapEvent['EventState'] in [0,1,2]: if currMapEvent['EventState'] in [0, 1, 2]:
eventQueue.put({"Type":fenrirEventType.KeyboardInput,"Data":currMapEvent.copy()}) eventQueue.put({"Type": fenrirEventType.KeyboardInput, "Data": currMapEvent.copy()})
eventFired = True eventFired = True
else: else:
if event.type in [2,3]: if event.type in [2, 3]:
foreward = True foreward = True
event = self.iDevices[fd].read_one() event = self.iDevices[fd].read_one()
@ -136,7 +138,7 @@ class driver(inputDriver):
self.writeEventBuffer() self.writeEventBuffer()
self.clearEventBuffer() self.clearEventBuffer()
except Exception as e: except Exception as e:
self.env['runtime']['debug'].writeDebugOut("INPUT WATCHDOG CRASH: "+str(e),debug.debugLevel.ERROR) self.env['runtime']['debug'].writeDebugOut("INPUT WATCHDOG CRASH: " + str(e), debug.debugLevel.ERROR)
def writeEventBuffer(self): def writeEventBuffer(self):
if not self._initialized: if not self._initialized:
@ -146,7 +148,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 as e: except Exception:
pass pass
def writeUInput(self, uDevice, event): def writeUInput(self, uDevice, event):
@ -156,7 +158,7 @@ class driver(inputDriver):
time.sleep(0.0000002) time.sleep(0.0000002)
uDevice.syn() uDevice.syn()
def updateInputDevices(self, newDevices = None, init = False): def updateInputDevices(self, newDevices=None, init=False):
if init: if init:
self.removeAllDevices() self.removeAllDevices()
@ -191,7 +193,7 @@ class driver(inputDriver):
try: try:
with open(deviceFile) as f: with open(deviceFile) as f:
pass pass
except Exception as e: except Exception:
continue continue
# 3 pos absolute # 3 pos absolute
# 2 pos relative # 2 pos relative
@ -201,22 +203,23 @@ class driver(inputDriver):
except: except:
continue continue
try: try:
if currDevice.name.upper() in ['','SPEAKUP','FENRIR-UINPUT']: # FIX: Check if attributes exist before accessing them
if hasattr(currDevice, 'name') and currDevice.name and currDevice.name.upper() in ['', 'SPEAKUP', 'FENRIR-UINPUT']:
continue continue
if currDevice.phys.upper() in ['','SPEAKUP','FENRIR-UINPUT']: if hasattr(currDevice, 'phys') and currDevice.phys and currDevice.phys.upper() in ['', 'SPEAKUP', 'FENRIR-UINPUT']:
continue continue
if 'BRLTTY' in currDevice.name.upper(): if hasattr(currDevice, 'name') and currDevice.name and 'BRLTTY' in currDevice.name.upper():
continue continue
except: except:
pass pass
cap = currDevice.capabilities() cap = currDevice.capabilities()
if mode in ['ALL','NOMICE']: if mode in ['ALL', 'NOMICE']:
if eventType.EV_KEY in cap: if eventType.EV_KEY in cap:
if 116 in cap[eventType.EV_KEY] and len(cap[eventType.EV_KEY]) < 10: if 116 in cap[eventType.EV_KEY] and len(cap[eventType.EV_KEY]) < 10:
self.env['runtime']['debug'].writeDebugOut('Device Skipped (has 116):' + currDevice.name,debug.debugLevel.INFO) self.env['runtime']['debug'].writeDebugOut('Device Skipped (has 116):' + currDevice.name, debug.debugLevel.INFO)
continue continue
if len(cap[eventType.EV_KEY]) < 60: if len(cap[eventType.EV_KEY]) < 60:
self.env['runtime']['debug'].writeDebugOut('Device Skipped (< 60 keys):' + currDevice.name,debug.debugLevel.INFO) self.env['runtime']['debug'].writeDebugOut('Device Skipped (< 60 keys):' + currDevice.name, debug.debugLevel.INFO)
continue continue
if mode == 'ALL': if mode == 'ALL':
self.addDevice(currDevice) self.addDevice(currDevice)
@ -224,16 +227,20 @@ class driver(inputDriver):
elif mode == 'NOMICE': elif mode == 'NOMICE':
if not ((eventType.EV_REL in cap) or (eventType.EV_ABS in cap)): if not ((eventType.EV_REL in cap) or (eventType.EV_ABS in cap)):
self.addDevice(currDevice) self.addDevice(currDevice)
self.env['runtime']['debug'].writeDebugOut('Device added (NOMICE):' + self.iDevices[currDevice.fd].name,debug.debugLevel.INFO) self.env['runtime']['debug'].writeDebugOut('Device added (NOMICE):' + self.iDevices[currDevice.fd].name, debug.debugLevel.INFO)
else: else:
self.env['runtime']['debug'].writeDebugOut('Device Skipped (NOMICE):' + currDevice.name,debug.debugLevel.INFO) self.env['runtime']['debug'].writeDebugOut('Device Skipped (NOMICE):' + currDevice.name, debug.debugLevel.INFO)
else: else:
self.env['runtime']['debug'].writeDebugOut('Device Skipped (no EV_KEY):' + currDevice.name,debug.debugLevel.INFO) self.env['runtime']['debug'].writeDebugOut('Device Skipped (no EV_KEY):' + currDevice.name, debug.debugLevel.INFO)
elif currDevice.name.upper() in mode.split(','): elif currDevice.name.upper() in mode.split(','):
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:
self.env['runtime']['debug'].writeDebugOut("Device Skipped (Exception): " + deviceFile +' ' + currDevice.name +' '+ str(e),debug.debugLevel.INFO) try:
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()
@ -247,6 +254,7 @@ 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
@ -266,12 +274,12 @@ class driver(inputDriver):
mEvent['EventSec'] = event.sec mEvent['EventSec'] = event.sec
mEvent['EventUsec'] = event.usec mEvent['EventUsec'] = event.usec
mEvent['EventState'] = event.value mEvent['EventState'] = event.value
mEvent['EventType'] = event.type mEvent['EventType'] = event.type
return mEvent return mEvent
except Exception as e: except Exception:
return None return None
def getLedState(self, led = 0): def getLedState(self, led=0):
if not self.hasIDevices(): if not self.hasIDevices():
return False return False
# 0 = Numlock # 0 = Numlock
@ -281,7 +289,8 @@ 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
ledState = self.getLedState(led) ledState = self.getLedState(led)
@ -290,9 +299,10 @@ class driver(inputDriver):
# 17 LEDs # 17 LEDs
if 17 in self.iDevices[i].capabilities(): if 17 in self.iDevices[i].capabilities():
if ledState == 1: if ledState == 1:
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
@ -301,6 +311,7 @@ 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
@ -309,6 +320,7 @@ 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
@ -324,20 +336,21 @@ class driver(inputDriver):
self.uDevices[fd] = UInput.from_device(self.iDevices[fd], name='fenrir-uinput', phys='fenrir-uinput') self.uDevices[fd] = UInput.from_device(self.iDevices[fd], name='fenrir-uinput', phys='fenrir-uinput')
except Exception as e: except Exception as e:
try: try:
self.env['runtime']['debug'].writeDebugOut('InputDriver evdev: compat fallback: ' + str(e),debug.debugLevel.WARNING) self.env['runtime']['debug'].writeDebugOut('InputDriver evdev: compat fallback: ' + str(e), debug.debugLevel.WARNING)
dev = self.iDevices[fd] dev = self.iDevices[fd]
cap = dev.capabilities() cap = dev.capabilities()
del cap[0] del cap[0]
self.uDevices[fd] = UInput( self.uDevices[fd] = UInput(
cap, cap,
name = 'fenrir-uinput', name='fenrir-uinput',
phys = 'fenrir-uinput' phys='fenrir-uinput'
) )
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:
self.iDevices[newDevice.fd] = newDevice self.iDevices[newDevice.fd] = newDevice
self.createUInputDev(newDevice.fd) self.createUInputDev(newDevice.fd)
@ -360,10 +373,13 @@ 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
self.env['runtime']['debug'].writeDebugOut('InputDriver evdev: grab device ('+ str(self.iDevices[fd].name) + ')',debug.debugLevel.INFO) self.env['runtime']['debug'].writeDebugOut('InputDriver evdev: grab device (' + str(self.iDevices[fd].name) + ')', debug.debugLevel.INFO)
# Reset modifier keys on successful grab # Reset modifier keys on successful grab
if self.uDevices[fd]: if self.uDevices[fd]:
modifierKeys = [e.KEY_LEFTCTRL, e.KEY_RIGHTCTRL, e.KEY_LEFTALT, e.KEY_RIGHTALT, e.KEY_LEFTSHIFT, e.KEY_RIGHTSHIFT] modifierKeys = [e.KEY_LEFTCTRL, e.KEY_RIGHTCTRL, e.KEY_LEFTALT, e.KEY_RIGHTALT, e.KEY_LEFTSHIFT, e.KEY_RIGHTSHIFT]
@ -371,33 +387,44 @@ 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 e: except Exception as mod_error:
self.env['runtime']['debug'].writeDebugOut('Failed to reset modifier key: ' + str(e), debug.debugLevel.WARNING) self.env['runtime']['debug'].writeDebugOut('Failed to reset modifier key: ' + str(mod_error), debug.debugLevel.WARNING)
except IOError: except IOError:
if not self.gDevices[fd]: if not self.gDevices[fd]:
return False return False
except Exception as e: except Exception as ex:
self.env['runtime']['debug'].writeDebugOut('InputDriver evdev: grabing not possible: ' + str(e),debug.debugLevel.ERROR) grab_error = ex
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
self.env['runtime']['debug'].writeDebugOut('InputDriver evdev: ungrab device ('+ str(self.iDevices[fd].name) + ')',debug.debugLevel.INFO) self.env['runtime']['debug'].writeDebugOut('InputDriver evdev: ungrab device (' + str(self.iDevices[fd].name) + ')', debug.debugLevel.INFO)
except IOError: except IOError:
if self.gDevices[fd]: if self.gDevices[fd]:
return False return False
# self.gDevices[fd] = False except Exception as ex:
# #self.removeDevice(fd) ungrab_error = ex
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):
self.env['runtime']['debug'].writeDebugOut('InputDriver evdev: device removed: ' + str(fd) + ' ' +str(self.iDevices[fd]),debug.debugLevel.INFO) def removeDevice(self, fd):
self.env['runtime']['debug'].writeDebugOut('InputDriver evdev: device removed: ' + str(fd) + ' ' + str(self.iDevices[fd]), debug.debugLevel.INFO)
self.clearEventBuffer() self.clearEventBuffer()
try: try:
self.ungrabDevice(fd) self.ungrabDevice(fd)
@ -452,4 +479,4 @@ class driver(inputDriver):
self.iDevices.clear() self.iDevices.clear()
self.uDevices.clear() self.uDevices.clear()
self.gDevices.clear() self.gDevices.clear()
self.iDeviceNo = 0 self.iDeviceNo = 0

261
uv.lock generated
View File

@ -1,261 +0,0 @@
version = 1
requires-python = ">=3.6"
resolution-markers = [
"python_full_version >= '3.8'",
"python_full_version == '3.7.*'",
"python_full_version < '3.7'",
]
[[package]]
name = "daemonize"
version = "2.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8c/20/96f7dbc23812cfe4cf479c87af3e4305d0d115fd1fffec32ddeee7b9c82b/daemonize-2.5.0.tar.gz", hash = "sha256:dd026e4ff8d22cb016ed2130bc738b7d4b1da597ef93c074d2adb9e4dea08bc3", size = 8759 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/45/ad/1b20db02287afd40d3130a218ac5ce2f7d2ab581cfda29bada5e1c4bee17/daemonize-2.5.0-py2.py3-none-any.whl", hash = "sha256:9b6b91311a9d934ff3f5f766666635ca280d3de8e7137e4cd7d3f052543b989f", size = 5231 },
]
[[package]]
name = "dbus-python"
version = "1.2.18"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.7'",
]
sdist = { url = "https://files.pythonhosted.org/packages/b1/5c/ccfc167485806c1936f7d3ba97db6c448d0089c5746ba105b6eb22dba60e/dbus-python-1.2.18.tar.gz", hash = "sha256:92bdd1e68b45596c833307a5ff4b217ee6929a1502f5341bae28fd120acf7260", size = 578204 }
[[package]]
name = "dbus-python"
version = "1.3.2"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.8'",
"python_full_version == '3.7.*'",
]
sdist = { url = "https://files.pythonhosted.org/packages/c1/d3/6be85a9c772d6ebba0cc3ab37390dd6620006dcced758667e0217fb13307/dbus-python-1.3.2.tar.gz", hash = "sha256:ad67819308618b5069537be237f8e68ca1c7fcc95ee4a121fe6845b1418248f8", size = 605495 }
[[package]]
name = "evdev"
version = "1.7.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/12/bb/f622a8a5e64d46ca83020a761877c0ead19140903c9aaf1431f3c531fdf6/evdev-1.7.1.tar.gz", hash = "sha256:0c72c370bda29d857e188d931019c32651a9c1ea977c08c8d939b1ced1637fde", size = 30705 }
[[package]]
name = "fenrir-screenreader"
version = "2025.1.28"
source = { editable = "." }
dependencies = [
{ name = "daemonize" },
{ name = "dbus-python", version = "1.2.18", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.7'" },
{ name = "dbus-python", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.7'" },
{ name = "evdev" },
{ name = "pexpect" },
{ name = "pyte", version = "0.8.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" },
{ name = "pyte", version = "0.8.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.8'" },
{ name = "pyudev", version = "0.23.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.7'" },
{ name = "pyudev", version = "0.24.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.7'" },
]
[package.dev-dependencies]
dev = [
{ name = "ruff", version = "0.0.17", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.7'" },
{ name = "ruff", version = "0.9.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.7'" },
]
[package.metadata]
requires-dist = [
{ name = "daemonize", specifier = ">=2.5.0" },
{ name = "dbus-python", specifier = ">=1.2.18" },
{ name = "evdev", specifier = ">=1.7.1" },
{ name = "pexpect", specifier = ">=4.9.0" },
{ name = "pyte", specifier = ">=0.8.1" },
{ name = "pyudev", specifier = ">=0.23.2" },
]
[package.metadata.requires-dev]
dev = [{ name = "ruff", specifier = ">=0.0.17" }]
[[package]]
name = "pexpect"
version = "4.9.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "ptyprocess" },
]
sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772 },
]
[[package]]
name = "ptyprocess"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993 },
]
[[package]]
name = "pyte"
version = "0.8.1"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version == '3.7.*'",
"python_full_version < '3.7'",
]
dependencies = [
{ name = "wcwidth", marker = "python_full_version < '3.8'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9f/60/442cdc1cba83710770672ef61e186be8746f419a12b2c84ba36e9a96276d/pyte-0.8.1.tar.gz", hash = "sha256:b9bfd1b781759e7572a6e553c010cc93eef58a19d8d1590446d84c19b1b097b0", size = 51657 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c1/c8/c7313e4e1849a86ff8bdbb9731fd6a32cb555feb27f33529a1cdc2c0427a/pyte-0.8.1-py3-none-any.whl", hash = "sha256:d760ea9a7d455d179d9d7a4288fac3d231190b5226715f1fe8c62547bed9b9aa", size = 30767 },
]
[[package]]
name = "pyte"
version = "0.8.2"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.8'",
]
dependencies = [
{ name = "wcwidth", marker = "python_full_version >= '3.8'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ab/ab/b599762933eba04de7dc5b31ae083112a6c9a9db15b01d3109ad797559d9/pyte-0.8.2.tar.gz", hash = "sha256:5af970e843fa96a97149d64e170c984721f20e52227a2f57f0a54207f08f083f", size = 92301 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/59/d0/bb522283b90853afbf506cd5b71c650cf708829914efd0003d615cf426cd/pyte-0.8.2-py3-none-any.whl", hash = "sha256:85db42a35798a5aafa96ac4d8da78b090b2c933248819157fc0e6f78876a0135", size = 31627 },
]
[[package]]
name = "pyudev"
version = "0.23.2"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.7'",
]
dependencies = [
{ name = "six", marker = "python_full_version < '3.7'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f8/fa/ae6c1a1a75f19560bbd875a579b2ca9b32deeae6a4c4a1997f4ec69a013e/pyudev-0.23.2.tar.gz", hash = "sha256:32ae3585b320a51bc283e0a04000fd8a25599edb44541e2f5034f6afee5d15cc", size = 87199 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/95/4c67255c65da9c939903cb95c57bd1ad7c920a7b711066aaa56cd7d149ab/pyudev-0.23.2-py3-none-any.whl", hash = "sha256:50d94bef0669f9aabd323a2259be67e8d49b9ebab9eae27b2cf8262767f9a2ae", size = 63903 },
]
[[package]]
name = "pyudev"
version = "0.24.3"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.8'",
"python_full_version == '3.7.*'",
]
sdist = { url = "https://files.pythonhosted.org/packages/c4/5c/6cc034da13830e3da123ccf9a30910bc868fa16670362f004e4b788d0df1/pyudev-0.24.3.tar.gz", hash = "sha256:2e945427a21674893bb97632401db62139d91cea1ee96137cc7b07ad22198fc7", size = 55970 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9d/3b/c37870f68ceb067707ca7b04db364a1478fcd40c6194007fb6e492ff9a92/pyudev-0.24.3-py3-none-any.whl", hash = "sha256:e8246f0a014fe370119ba2bc781bfbe62c0298d0d6b39c94e83102a8a3f56960", size = 62677 },
]
[[package]]
name = "ruff"
version = "0.0.17"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.7'",
]
sdist = { url = "https://files.pythonhosted.org/packages/aa/ed/7adc91572c08f346976335f6b1b22774ea555d11043a9ff013f962affab5/ruff-0.0.17.tar.gz", hash = "sha256:5815383171ccbab333d6b6d54253e91003ee6be4627738d56855cbefc393df41", size = 54259 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/10/3b/4a6b289ab3ca80109402c15dd0fc83ef1c77572453b346a74ebc55666db5/ruff-0.0.17-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:2fa56d385b31462e26a605477c626023b16fb5a399b619ba0966d0d2b8d88eca", size = 1665731 },
{ url = "https://files.pythonhosted.org/packages/3d/23/5e519dd38ae42a75f8e6a952a3c5ea842804e2cb8c60a1d72807131d8aba/ruff-0.0.17-cp310-cp310-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:4f5694f9876cde21b95ad9c1691d0513617d2e88c0749f400b866505217fd5a0", size = 3202527 },
{ url = "https://files.pythonhosted.org/packages/9c/05/69872574ea3044cfbac4becef1c25c0b1227499c91c2232b83bad9bb104c/ruff-0.0.17-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d1349f4e5a4d53294fce92f42ecf881a73c180d71f14121461cac7d251abafd4", size = 1543942 },
{ url = "https://files.pythonhosted.org/packages/24/16/e5c7cb9b77d1f64d94d507d0c3d7ef491a10dc75825f458cb1bb05ae41cf/ruff-0.0.17-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f4e60d690898be3c3bf24387e67ac89496a97eb8814b8c0f0670ea43b6e83ee2", size = 1788392 },
{ url = "https://files.pythonhosted.org/packages/76/a4/dad616f277880963968d8a5e5719556f4ecadc7333c0da8bfb5060c5750c/ruff-0.0.17-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:522cacc6e550a7d59ead3b0ca65623582d51bfe32f6c780770ccf5d1bc3246cb", size = 1758753 },
{ url = "https://files.pythonhosted.org/packages/bc/44/08d68e219e2e7659991b467122f41326fde1e2e959107746ed7559f5e498/ruff-0.0.17-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e608511d0349a6211a0a123744cc0960f88539dbf62a0b8a77e3ee483237a6da", size = 1677835 },
{ url = "https://files.pythonhosted.org/packages/d6/7d/4d897311a299007b8542bfcd83dff9b09db6a7130f48ad3252e8ba3740b9/ruff-0.0.17-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:78f14cf1056ded6bda77162d7483e11a2f2a29763538422adaa5412654ff1a94", size = 1700213 },
{ url = "https://files.pythonhosted.org/packages/90/af/2c5dfa97f6994cf462e2b1934662f730608eec7ccfd6b992bab1af002ce9/ruff-0.0.17-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7cd7180893a3ed789c82838c8082fda074ba1cec46f383e255e696533f634be", size = 1894523 },
{ url = "https://files.pythonhosted.org/packages/40/e8/74569b7b05ece82a9e298eed7e01b2ae18205cee88de0283d0647370d120/ruff-0.0.17-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a90917b0e9d2f851294e445f0b898fa94051c4d9edcc1ab6d40bc1129fb9bb1e", size = 2119346 },
{ url = "https://files.pythonhosted.org/packages/7c/46/2cb84ccb0944c37208a2cea880ba8c6cb2a1752759771d385d6217de46b2/ruff-0.0.17-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9bb485eb3e0ba0c19ffd14b533659448c1c5e2958171e818fc1bc42c76f3d99a", size = 1679168 },
{ url = "https://files.pythonhosted.org/packages/47/65/cfd4e305851fdb94b7c8147d9d839ed83f9a93a941cc89d71e633a094642/ruff-0.0.17-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f3a399e4d211cdbec9229a89b1b7e77345eae881e9c3682fef7e90044de6a864", size = 1715985 },
{ url = "https://files.pythonhosted.org/packages/23/7a/96a4e5c51bab9538d9ee27a0eba95bd790d120bcc76a195417abbfb2cc81/ruff-0.0.17-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:80fe80b12ea042b9f0d8e80608db400e0c8e419d74a4dcf8b3b4fea9ec03362f", size = 1782188 },
{ url = "https://files.pythonhosted.org/packages/fb/3c/92267e9b9336bb1ba9ba7f5e3b9028e57ee4d77f32f728992c23da69ab53/ruff-0.0.17-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3b067ed2bd3fd0d4be591ea9afc796c07706291d78efe5a8eda4172c4d43525c", size = 1812203 },
{ url = "https://files.pythonhosted.org/packages/ca/d9/52cef313261a61931c4f3be228a774c5dad89f45042518d0f483864902ca/ruff-0.0.17-cp310-none-win32.whl", hash = "sha256:e3aba30e3aad77f260095ea1dbcf2834ab64d75133ff8d260625bb22887e2799", size = 1626413 },
{ url = "https://files.pythonhosted.org/packages/fe/d7/741e229667d0038d004783286ea6fa4554ff7a3b52bfe9b26b0380462e56/ruff-0.0.17-cp310-none-win_amd64.whl", hash = "sha256:a068bced7aff34173319931972fde3d7e68e3894915edac4e0f8c9b7bec7a226", size = 1654976 },
{ url = "https://files.pythonhosted.org/packages/20/50/470c8688e96fecac2096205cb45438676f6277b9c713a0aa1c4e633af503/ruff-0.0.17-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:b708d650c2ba25458d9e735c51981b687bf6747a4b28403eb7f6bae1aa93cfcf", size = 1665730 },
{ url = "https://files.pythonhosted.org/packages/13/13/f68c059cafb95122214c2defac7005c8a5ce278c7f1768d6849c167df07a/ruff-0.0.17-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:91019d271c223a4c562dbc2fbf2a2a96157524999a1173a4c858816d0c1bb9e7", size = 1543941 },
{ url = "https://files.pythonhosted.org/packages/70/e9/353db015927b6336bffbb7fe16af3bf76e860ab0fbddd66bf0a6995a2bfa/ruff-0.0.17-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:edbaf1c7f6b6483a206e549026f03ef7e04e480b5204437e21370de508dcf736", size = 1788393 },
{ url = "https://files.pythonhosted.org/packages/32/e5/2c41a62d58763948fa332619226cb11ded2ec161c678d70b7991b839ecf5/ruff-0.0.17-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:52110ed886d674497531d44ead5fa2fe99be930adfc9cec4b1f39409043efb13", size = 1758755 },
{ url = "https://files.pythonhosted.org/packages/3f/a9/f0cc3416b6f2e3755c8b37271112c609dfdf862b2825d86f4fc926eee037/ruff-0.0.17-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:febfbe4fa02e680b93b3fc2dcb0ffe5d601435c9719a51e35fe51fff1d0cc2c6", size = 1677834 },
{ url = "https://files.pythonhosted.org/packages/ed/54/ad7286c9e136260eaf8d6236202384dfe71a884a7b31288e129a43c33cb7/ruff-0.0.17-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6e72d62bd47f086d14ea5796ea18d1b98089a839dad693afa471a1fcdb6ae0d", size = 1700213 },
{ url = "https://files.pythonhosted.org/packages/bd/f7/12a3cfa5b88485ecdc724d0e915a8ebe4dfebfa94422855a445850564d01/ruff-0.0.17-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:27d1bd7c71a90522e383853e411fcb402ccdcaf8778c6e0d54359153772f7870", size = 1894524 },
{ url = "https://files.pythonhosted.org/packages/e2/1a/1ab955830e84dcad9f606954d5b1094e186de144a2ad37247aa37145cbcd/ruff-0.0.17-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bed7beec36834e6fc40a03af92bc0da67599c70fbe24d8d820a1a2110b25eaf", size = 2119348 },
{ url = "https://files.pythonhosted.org/packages/3c/00/a67b2904d92034abd2f35a4d930eae08abf64106f64a90a67770395db8a7/ruff-0.0.17-cp37-none-win32.whl", hash = "sha256:c664d897e21b9aab2b20c764434653aa394e32c32d38e751fd4f381ace3a4e58", size = 1626410 },
{ url = "https://files.pythonhosted.org/packages/9c/a9/dacfff99065b0588ff9cf07411d7bbc8a167d1542d92a7e46f5825e262fa/ruff-0.0.17-cp37-none-win_amd64.whl", hash = "sha256:8c5900fd09baa2c7a4aecbdc754d3a43f2842906ee571812fa3eb28b8e7973a5", size = 1654975 },
{ url = "https://files.pythonhosted.org/packages/f3/a9/e5a048d209e246b3702a23540b1e09faa79b313b1b23f27993e33df3b01a/ruff-0.0.17-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:db04c29f114c68f447aeec23f9be6118ea11a18a2444416cb4afb0fd918e50db", size = 1665728 },
{ url = "https://files.pythonhosted.org/packages/32/3d/13114ef5793e43fd5c8ee17047fbdc86e9eacb81f708c0e01ca9d5db773f/ruff-0.0.17-cp38-cp38-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:8527c8aacdd0e911c6f7d6f1b109a17e68300ac0f255ccce73e748bb8835c722", size = 3202527 },
{ url = "https://files.pythonhosted.org/packages/09/3f/657daee09a7412bda8af1963d6dd1b4adfdbc0e2d37c7261984fc4953e19/ruff-0.0.17-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:889aa57c771140ec6b17e15af8308e88e43a07b7369b97cb0426e1393e3d10b5", size = 1543942 },
{ url = "https://files.pythonhosted.org/packages/c1/6b/15e1c6744216236a3a08a8e40e318c329f86febdee7a1d62e3c449d75009/ruff-0.0.17-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7be31e77c1f98d9d02a7f6f2d4c05e8236cd9c82d0c3356b083162a011fc4d23", size = 1788391 },
{ url = "https://files.pythonhosted.org/packages/ff/fa/0cf5c91b2d415416ef0d534a8f19bdc07d11a52fd54aface524d006be1ce/ruff-0.0.17-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fe0c047d6fc74b55fcd1ac4f18110500ba871de2014039e4838ed7444d3459ff", size = 1758753 },
{ url = "https://files.pythonhosted.org/packages/8f/8b/ac8d7a1cce151c74fa9458f0297ca6fc287f9fe1a57fe710f99de970bfdb/ruff-0.0.17-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4933e275f0c3af3a78ecf1f9a4e12cc0426cd76398c7e904786f99c1ea8a0dd", size = 1677834 },
{ url = "https://files.pythonhosted.org/packages/9f/38/e51bff666939e38155ef12930e6c1d4970f5a28c7f1ff3867e97b00a4cd0/ruff-0.0.17-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a9f2fb8b3bac67d0fa9e50411ab424a863e4bc29a3336a046cc38f06d3ec17f", size = 1700211 },
{ url = "https://files.pythonhosted.org/packages/e2/96/949c17bed22816136d8e0456a242059da202e316ca4510715560c026d0d8/ruff-0.0.17-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d45439cbe73332a064306c39528d6bbf4856abb6d377ad8244b6e74a737daaa", size = 1894522 },
{ url = "https://files.pythonhosted.org/packages/89/61/cec1cd7093eea58e083060fc66c3fcf758737785a21b752fc568f57eabab/ruff-0.0.17-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f7433f20d39a3819e322a3497dce037c6110f9588ec51ba136a938109dd31e71", size = 2119345 },
{ url = "https://files.pythonhosted.org/packages/a9/32/27bb21e91e0e7d6e76be6d5fce844c3ac52278ae49d8964eb356a024cf26/ruff-0.0.17-cp38-none-win32.whl", hash = "sha256:5f8f4f4310018807402c77d81ae020666b742d2173a73b147ce0d1e0a08f022f", size = 1626409 },
{ url = "https://files.pythonhosted.org/packages/d0/3f/cd9d27f2dbbd4975c7880d8284f893ea99cf73c646f453b9cef1b3924db5/ruff-0.0.17-cp38-none-win_amd64.whl", hash = "sha256:f0d0e8058d903b8fe899e04e1a957127ca97452553cf70ba9b4d1b277f034ad1", size = 1654979 },
{ url = "https://files.pythonhosted.org/packages/d3/4e/c854d4587c180936b33eac57344b11f52564878d2939fd6d9d842fa6e5ae/ruff-0.0.17-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:e6f24c3746d199bdb0d47149ed5353a41f0192630911396822fda0f8a6feaa0b", size = 1665735 },
{ url = "https://files.pythonhosted.org/packages/49/8b/8d2de1c9f7e2056bd4edaea393d6ad3494e75e99136cf127402afb4c496c/ruff-0.0.17-cp39-cp39-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:71cb773b19240f1be64c5f71aa2ad52b9f44fde1605c2c2f4089a5d61cb552d2", size = 3202528 },
{ url = "https://files.pythonhosted.org/packages/aa/9f/7235c23ed12dfd44823d3a127d5654cc2fbfbe3daa1aca00c43ad2ccd519/ruff-0.0.17-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bab716debcab46d9a1d7c8d00e2acf2b48ff28ec519b2b4c0eba873236782c21", size = 1543942 },
{ url = "https://files.pythonhosted.org/packages/2e/69/1d4a2819146458d478e8c8a194452da263ab60202083d8eba02307fde216/ruff-0.0.17-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ef57186452c0cfe71f09dac434bda0f1a804808f92221142adb9de28c3f422e6", size = 1788392 },
{ url = "https://files.pythonhosted.org/packages/c2/35/c3e2a3c3690e732860342b16e606795e977d87a90176ed1dae13c001ee86/ruff-0.0.17-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8b00be13abaf107c30b8bcaf2ac89dc2b3abd164728c229339910211c05e8c43", size = 1758753 },
{ url = "https://files.pythonhosted.org/packages/ad/3d/7d71456c7e1c543ed223b400a2e962a344b5467a27e196c4dd6f2d1d30c3/ruff-0.0.17-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9c1ba3383995091ce6f3618e89f1bb0ed5caa730f64bb79a1c60184682dc5c3", size = 1677834 },
{ url = "https://files.pythonhosted.org/packages/f3/73/e87e31367fe7af0a027672e49703b61ea6566912d09e34a8e3e43bce3455/ruff-0.0.17-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:865114aa655dc54e5699f18b258a33a15a36da915de4936d7a458425e7f6351d", size = 1700211 },
{ url = "https://files.pythonhosted.org/packages/2b/27/7449e2a8bca1957c0e2d57316ca8fdcdf8d83277b23d50a33bdded703aa6/ruff-0.0.17-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a390b4657cc1eebd9bb0e581da768aa557b1157f5eeed6fc8b5b920991061b04", size = 1894520 },
{ url = "https://files.pythonhosted.org/packages/80/39/db6441f33216e25e5dba811e9d908cff898df7c9930006c735ba6578dd65/ruff-0.0.17-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d3fec0e9e8f285324127b97c55b525fe61e8e16e93e1a03d34aba80e3aff9f21", size = 2119346 },
{ url = "https://files.pythonhosted.org/packages/73/cb/d7ae9d2276f23f89642df0af808c85acc632aceca5d7039ae3afe4585afb/ruff-0.0.17-cp39-none-win32.whl", hash = "sha256:3f063c889d65d71fb189d6246ccd537c23c9d0f6e483c961ac0b5e8477d6e3ca", size = 1626409 },
{ url = "https://files.pythonhosted.org/packages/d8/a7/3ccc344a2b228a15b52217ed2a2982214ad77684745c3e09ace2b1f8e9bf/ruff-0.0.17-cp39-none-win_amd64.whl", hash = "sha256:4ba403b8a5f38753ed3ba7ca16fb7c67eaee96a4e4a9e9709f3ad8cd3909012c", size = 1654973 },
]
[[package]]
name = "ruff"
version = "0.9.3"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.8'",
"python_full_version == '3.7.*'",
]
sdist = { url = "https://files.pythonhosted.org/packages/1e/7f/60fda2eec81f23f8aa7cbbfdf6ec2ca11eb11c273827933fb2541c2ce9d8/ruff-0.9.3.tar.gz", hash = "sha256:8293f89985a090ebc3ed1064df31f3b4b56320cdfcec8b60d3295bddb955c22a", size = 3586740 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f9/77/4fb790596d5d52c87fd55b7160c557c400e90f6116a56d82d76e95d9374a/ruff-0.9.3-py3-none-linux_armv6l.whl", hash = "sha256:7f39b879064c7d9670197d91124a75d118d00b0990586549949aae80cdc16624", size = 11656815 },
{ url = "https://files.pythonhosted.org/packages/a2/a8/3338ecb97573eafe74505f28431df3842c1933c5f8eae615427c1de32858/ruff-0.9.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a187171e7c09efa4b4cc30ee5d0d55a8d6c5311b3e1b74ac5cb96cc89bafc43c", size = 11594821 },
{ url = "https://files.pythonhosted.org/packages/8e/89/320223c3421962762531a6b2dd58579b858ca9916fb2674874df5e97d628/ruff-0.9.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c59ab92f8e92d6725b7ded9d4a31be3ef42688a115c6d3da9457a5bda140e2b4", size = 11040475 },
{ url = "https://files.pythonhosted.org/packages/b2/bd/1d775eac5e51409535804a3a888a9623e87a8f4b53e2491580858a083692/ruff-0.9.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dc153c25e715be41bb228bc651c1e9b1a88d5c6e5ed0194fa0dfea02b026439", size = 11856207 },
{ url = "https://files.pythonhosted.org/packages/7f/c6/3e14e09be29587393d188454064a4aa85174910d16644051a80444e4fd88/ruff-0.9.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:646909a1e25e0dc28fbc529eab8eb7bb583079628e8cbe738192853dbbe43af5", size = 11420460 },
{ url = "https://files.pythonhosted.org/packages/ef/42/b7ca38ffd568ae9b128a2fa76353e9a9a3c80ef19746408d4ce99217ecc1/ruff-0.9.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a5a46e09355695fbdbb30ed9889d6cf1c61b77b700a9fafc21b41f097bfbba4", size = 12605472 },
{ url = "https://files.pythonhosted.org/packages/a6/a1/3167023f23e3530fde899497ccfe239e4523854cb874458ac082992d206c/ruff-0.9.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c4bb09d2bbb394e3730d0918c00276e79b2de70ec2a5231cd4ebb51a57df9ba1", size = 13243123 },
{ url = "https://files.pythonhosted.org/packages/d0/b4/3c600758e320f5bf7de16858502e849f4216cb0151f819fa0d1154874802/ruff-0.9.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:96a87ec31dc1044d8c2da2ebbed1c456d9b561e7d087734336518181b26b3aa5", size = 12744650 },
{ url = "https://files.pythonhosted.org/packages/be/38/266fbcbb3d0088862c9bafa8b1b99486691d2945a90b9a7316336a0d9a1b/ruff-0.9.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb7554aca6f842645022fe2d301c264e6925baa708b392867b7a62645304df4", size = 14458585 },
{ url = "https://files.pythonhosted.org/packages/63/a6/47fd0e96990ee9b7a4abda62de26d291bd3f7647218d05b7d6d38af47c30/ruff-0.9.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cabc332b7075a914ecea912cd1f3d4370489c8018f2c945a30bcc934e3bc06a6", size = 12419624 },
{ url = "https://files.pythonhosted.org/packages/84/5d/de0b7652e09f7dda49e1a3825a164a65f4998175b6486603c7601279baad/ruff-0.9.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:33866c3cc2a575cbd546f2cd02bdd466fed65118e4365ee538a3deffd6fcb730", size = 11843238 },
{ url = "https://files.pythonhosted.org/packages/9e/be/3f341ceb1c62b565ec1fb6fd2139cc40b60ae6eff4b6fb8f94b1bb37c7a9/ruff-0.9.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:006e5de2621304c8810bcd2ee101587712fa93b4f955ed0985907a36c427e0c2", size = 11484012 },
{ url = "https://files.pythonhosted.org/packages/a3/c8/ff8acbd33addc7e797e702cf00bfde352ab469723720c5607b964491d5cf/ruff-0.9.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ba6eea4459dbd6b1be4e6bfc766079fb9b8dd2e5a35aff6baee4d9b1514ea519", size = 12038494 },
{ url = "https://files.pythonhosted.org/packages/73/b1/8d9a2c0efbbabe848b55f877bc10c5001a37ab10aca13c711431673414e5/ruff-0.9.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:90230a6b8055ad47d3325e9ee8f8a9ae7e273078a66401ac66df68943ced029b", size = 12473639 },
{ url = "https://files.pythonhosted.org/packages/cb/44/a673647105b1ba6da9824a928634fe23186ab19f9d526d7bdf278cd27bc3/ruff-0.9.3-py3-none-win32.whl", hash = "sha256:eabe5eb2c19a42f4808c03b82bd313fc84d4e395133fb3fc1b1516170a31213c", size = 9834353 },
{ url = "https://files.pythonhosted.org/packages/c3/01/65cadb59bf8d4fbe33d1a750103e6883d9ef302f60c28b73b773092fbde5/ruff-0.9.3-py3-none-win_amd64.whl", hash = "sha256:040ceb7f20791dfa0e78b4230ee9dce23da3b64dd5848e40e3bf3ab76468dcf4", size = 10821444 },
{ url = "https://files.pythonhosted.org/packages/69/cb/b3fe58a136a27d981911cba2f18e4b29f15010623b79f0f2510fd0d31fd3/ruff-0.9.3-py3-none-win_arm64.whl", hash = "sha256:800d773f6d4d33b0a3c60e2c6ae8f4c202ea2de056365acfa519aa48acf28e0b", size = 10038168 },
]
[[package]]
name = "six"
version = "1.17.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 },
]
[[package]]
name = "wcwidth"
version = "0.2.13"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 },
]