diff --git a/src/fenrir b/src/fenrir index 735e09e6..71c89edd 100755 --- a/src/fenrir +++ b/src/fenrir @@ -2,20 +2,115 @@ # -*- coding: utf-8 -*- # Fenrir TTY screen reader -# By Chrys, Storm Dragon, and contributers. +# By Chrys, Storm Dragon, and contributors. -import os, sys, inspect +import os +import sys +import inspect +import argparse +from daemonize import Daemonize + +# Get the fenrir installation path fenrirPath = os.path.dirname(os.path.realpath(os.path.abspath(inspect.getfile(inspect.currentframe())))) - if not fenrirPath in sys.path: sys.path.append(fenrirPath) from fenrirscreenreader.core import fenrirManager +from fenrirscreenreader import fenrirVersion + +def create_argument_parser(): + """Create and return the argument parser for Fenrir""" + argumentParser = argparse.ArgumentParser( + description="Fenrir - A console screen reader for Linux", + formatter_class=argparse.RawDescriptionHelpFormatter + ) + argumentParser.add_argument( + '-v', '--version', + action='version', + version=f'Fenrir screen reader version {fenrirVersion.version}-{fenrirVersion.codeName}', + help='Show version information and exit' + ) + argumentParser.add_argument( + '-f', '--foreground', + action='store_true', + help='Run Fenrir in the foreground (default: run as daemon)' + ) + argumentParser.add_argument( + '-s', '--setting', + metavar='SETTING-FILE', + default='/etc/fenrir/settings/settings.conf', + help='Path to custom settings file' + ) + argumentParser.add_argument( + '-o', '--options', + metavar='SECTION#SETTING=VALUE;..', + default='', + help='Override settings file options. Format: SECTION#SETTING=VALUE;... (case sensitive)' + ) + argumentParser.add_argument( + '-d', '--debug', + action='store_true', + help='Enable debug mode' + ) + argumentParser.add_argument( + '-p', '--print', + action='store_true', + help='Print debug messages to screen' + ) + argumentParser.add_argument( + '-e', '--emulated-pty', + action='store_true', + help='Use PTY emulation with escape sequences for input (enables desktop/X/Wayland usage)' + ) + argumentParser.add_argument( + '-E', '--emulated-evdev', + action='store_true', + help='Use PTY emulation with evdev for input (single instance)' + ) + return argumentParser + +def validate_arguments(cliArgs): + """Validate command line arguments""" + if cliArgs.options: + for option in cliArgs.options.split(';'): + if option and ('#' not in option or '=' not in option): + return False, f"Invalid option format: {option}\nExpected format: SECTION#SETTING=VALUE" + + if cliArgs.emulated_pty and cliArgs.emulated_evdev: + return False, "Cannot use both --emulated-pty and --emulated-evdev simultaneously" + + return True, None + +def run_fenrir(): + """Main function that runs Fenrir""" + fenrirApp = fenrirManager.fenrirManager(cliArgs) + fenrirApp.proceed() + del fenrirApp def main(): - app = fenrirManager.fenrirManager() - app.proceed() - del app + global cliArgs + argumentParser = create_argument_parser() + cliArgs = argumentParser.parse_args() + + # Validate arguments + isValid, errorMsg = validate_arguments(cliArgs) + if not isValid: + argumentParser.error(errorMsg) + sys.exit(1) + + if cliArgs.foreground: + # Run directly in foreground + run_fenrir() + else: + # Run as daemon + pidFile = "/run/fenrir.pid" + daemonProcess = Daemonize( + app="fenrir", + pid=pidFile, + action=run_fenrir, + chdir=fenrirPath + ) + daemonProcess.start() if __name__ == "__main__": - main() + main() diff --git a/src/fenrir-daemon b/src/fenrir-daemon deleted file mode 100755 index e5abb41c..00000000 --- a/src/fenrir-daemon +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -# Fenrir TTY screen reader -# By Chrys, Storm Dragon, and contributers. - -import os, sys, inspect -fenrirPath = os.path.dirname(os.path.realpath(os.path.abspath(inspect.getfile(inspect.currentframe())))) - -if not fenrirPath in sys.path: - sys.path.append(fenrirPath) - -from fenrirscreenreader.core import fenrirManager -from daemonize import Daemonize - -pidFile = "/run/fenrir.pid" - -def main(): - app = fenrirManager.fenrirManager() - app.proceed() - del app - -if __name__ == "__main__": - # for debug in foreground - #daemon = Daemonize(app="fenrir-daemon", pid=pidFile, action=main, foreground=True,chdir=fenrirPath) - daemon = Daemonize(app="fenrir-daemon", pid=pidFile, action=main, chdir=fenrirPath) - daemon.start() - diff --git a/src/fenrir-daemon-pypy b/src/fenrir-daemon-pypy deleted file mode 100755 index 363b2b6c..00000000 --- a/src/fenrir-daemon-pypy +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env pypy3 -# -*- coding: utf-8 -*- - -# Fenrir TTY screen reader -# By Chrys, Storm Dragon, and contributers. - -import os, sys, inspect -fenrirPath = os.path.dirname(os.path.realpath(os.path.abspath(inspect.getfile(inspect.currentframe())))) - -if not fenrirPath in sys.path: - sys.path.append(fenrirPath) - -from fenrirscreenreader.core import fenrirManager -from daemonize import Daemonize - -pidFile = "/run/fenrir.pid" - -def main(): - app = fenrirManager.fenrirManager() - app.proceed() - del app - -if __name__ == "__main__": - # for debug in foreground - #daemon = Daemonize(app="fenrir-daemon", pid=pidFile, action=main, foreground=True,chdir=os.path.dirname(os.path.realpath(fenrirVersion.__file__))) - daemon = Daemonize(app="fenrir-daemon", pid=pidFile, action=main, chdir=fenrirPath) - daemon.start() - diff --git a/src/fenrirscreenreader/core/fenrirManager.py b/src/fenrirscreenreader/core/fenrirManager.py index d7bf2ff8..f88ee62c 100644 --- a/src/fenrirscreenreader/core/fenrirManager.py +++ b/src/fenrirscreenreader/core/fenrirManager.py @@ -2,109 +2,50 @@ # -*- coding: utf-8 -*- # Fenrir TTY screen reader -# By Chrys, Storm Dragon, and contributers. +# By Chrys, Storm Dragon, and contributors. -import signal, time, argparse, os, sys +import signal +import time +import os +import sys from fenrirscreenreader.core import i18n from fenrirscreenreader.core import settingsManager from fenrirscreenreader.core import debug from fenrirscreenreader.core.eventData import fenrirEventType -from fenrirscreenreader import fenrirVersion class fenrirManager(): - def __init__(self): - self.initialized = False - cliArgs = self.handleArgs() - if not cliArgs: - return + def __init__(self, cliArgs): + self.isInitialized = False try: self.environment = settingsManager.settingsManager().initFenrirConfig(cliArgs, self) if not self.environment: raise RuntimeError('Cannot Initialize. Maybe the configfile is not available or not parseable') except RuntimeError: raise + self.environment['runtime']['outputManager'].presentText(_("Start Fenrir"), soundIcon='ScreenReaderOn', interrupt=True) signal.signal(signal.SIGINT, self.captureSignal) signal.signal(signal.SIGTERM, self.captureSignal) - self.initialized = True + + self.isInitialized = True self.modifierInput = False self.singleKeyCommand = False self.command = '' self.setProcessName() - def handleArgs(self): - """ - Parse and handle command line arguments for Fenrir. - - Returns: - argparse.Namespace: Parsed command line arguments - None: If argument parsing fails - """ - parser = argparse.ArgumentParser( - description="Fenrir - A console screen reader for Linux", - formatter_class=argparse.RawDescriptionHelpFormatter - ) - parser.add_argument( - '-v', '--version', - action='version', - version=f'Fenrir screen reader version {fenrirVersion.version}-{fenrirVersion.codeName}', - help='Show version information and exit' - ) - parser.add_argument( - '-s', '--setting', - metavar='SETTING-FILE', - default='/etc/fenrir/settings/settings.conf', - help='Path to custom settings file' - ) - parser.add_argument( - '-o', '--options', - metavar='SECTION#SETTING=VALUE;..', - default='', - help='Override settings file options. Format: SECTION#SETTING=VALUE;... (case sensitive)' - ) - parser.add_argument( - '-d', '--debug', - action='store_true', - help='Enable debug mode' - ) - parser.add_argument( - '-p', '--print', - action='store_true', - help='Print debug messages to screen' - ) - parser.add_argument( - '-e', '--emulated-pty', - action='store_true', - help='Use PTY emulation with escape sequences for input (enables desktop/X/Wayland usage)' - ) - parser.add_argument( - '-E', '--emulated-evdev', - action='store_true', - help='Use PTY emulation with evdev for input (single instance)' - ) - try: - args = parser.parse_args() - # Only do format validation, let file existence be handled by the config initialization - if args.options: - for option in args.options.split(';'): - if option and ('#' not in option or '=' not in option): - parser.error(f"Invalid option format: {option}\nExpected format: SECTION#SETTING=VALUE") - if args.emulated_pty and args.emulated_evdev: - parser.error("Cannot use both --emulated-pty and --emulated-evdev simultaneously") - return args - except Exception as e: - print(f"Error parsing arguments: {str(e)}", file=sys.stderr) - return None + def proceed(self): - if not self.initialized: + if not self.isInitialized: return self.environment['runtime']['eventManager'].startMainEventLoop() self.shutdown() + def handleInput(self, event): - #startTime = time.time() - 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']: event['Data'] = self.environment['runtime']['inputManager'].getInputEvent() + if event['Data']: event['Data']['EventName'] = self.environment['runtime']['inputManager'].convertEventName(event['Data']['EventName']) self.environment['runtime']['inputManager'].handleInputEvent(event['Data']) @@ -113,6 +54,7 @@ class fenrirManager(): if self.environment['runtime']['inputManager'].noKeyPressed(): self.environment['runtime']['inputManager'].clearLastDeepInput() + if self.environment['runtime']['screenManager'].isSuspendingScreen(): self.environment['runtime']['inputManager'].writeEventBuffer() else: @@ -132,6 +74,7 @@ class fenrirManager(): self.environment['runtime']['inputManager'].clearEventBuffer() else: self.environment['runtime']['inputManager'].writeEventBuffer() + if self.environment['runtime']['inputManager'].noKeyPressed(): self.modifierInput = False self.singleKeyCommand = False @@ -139,73 +82,59 @@ class fenrirManager(): self.environment['runtime']['inputManager'].handleDeviceGrab() if self.environment['input']['keyForeward'] > 0: - self.environment['input']['keyForeward'] -=1 + self.environment['input']['keyForeward'] -= 1 + self.environment['runtime']['commandManager'].executeDefaultTrigger('onKeyInput') - #print('handleInput:',time.time() - startTime) + def handleByteInput(self, event): - if not event['Data']: - return - if event['Data'] == b'': + if not event['Data'] or event['Data'] == b'': return self.environment['runtime']['byteManager'].handleByteInput(event['Data']) self.environment['runtime']['commandManager'].executeDefaultTrigger('onByteInput') - def handleExecuteCommand(self, event): - if not event['Data']: + + def handleExecuteCommand(self, event): + if not event['Data'] or event['Data'] == '': return - if event['Data'] == '': - return - command = event['Data'] + currentCommand = event['Data'] # special modes if self.environment['runtime']['helpManager'].isTutorialMode(): - if self.environment['runtime']['commandManager'].commandExists( command, 'help'): - self.environment['runtime']['commandManager'].executeCommand( command, 'help') + if self.environment['runtime']['commandManager'].commandExists(currentCommand, 'help'): + self.environment['runtime']['commandManager'].executeCommand(currentCommand, 'help') return elif self.environment['runtime']['vmenuManager'].getActive(): - if self.environment['runtime']['commandManager'].commandExists( command, 'vmenu-navigation'): - self.environment['runtime']['commandManager'].executeCommand( command, 'vmenu-navigation') + if self.environment['runtime']['commandManager'].commandExists(currentCommand, 'vmenu-navigation'): + self.environment['runtime']['commandManager'].executeCommand(currentCommand, 'vmenu-navigation') return # default - self.environment['runtime']['commandManager'].executeCommand( command, 'commands') + self.environment['runtime']['commandManager'].executeCommand(currentCommand, 'commands') + def handleRemoteIncomming(self, event): if not event['Data']: return self.environment['runtime']['remoteManager'].handleRemoteIncomming(event['Data']) + def handleScreenChange(self, event): self.environment['runtime']['screenManager'].hanldeScreenChange(event['Data']) - ''' - if self.environment['runtime']['applicationManager'].isApplicationChange(): - self.environment['runtime']['commandManager'].executeDefaultTrigger('onApplicationChange') - self.environment['runtime']['commandManager'].executeSwitchTrigger('onSwitchApplicationProfile', \ - self.environment['runtime']['applicationManager'].getPrevApplication(), \ - self.environment['runtime']['applicationManager'].getCurrentApplication()) - ''' if self.environment['runtime']['vmenuManager'].getActive(): return - self.environment['runtime']['commandManager'].executeDefaultTrigger('onScreenChanged') self.environment['runtime']['screenDriver'].getCurrScreen() + def handleScreenUpdate(self, event): - #startTime = time.time() self.environment['runtime']['screenManager'].handleScreenUpdate(event['Data']) - ''' - if self.environment['runtime']['applicationManager'].isApplicationChange(): - self.environment['runtime']['commandManager'].executeDefaultTrigger('onApplicationChange') - self.environment['runtime']['commandManager'].executeSwitchTrigger('onSwitchApplicationProfile', \ - self.environment['runtime']['applicationManager'].getPrevApplication(), \ - self.environment['runtime']['applicationManager'].getCurrentApplication()) - ''' - # timout for the last keypress + if time.time() - self.environment['runtime']['inputManager'].getLastInputTime() >= 0.3: self.environment['runtime']['inputManager'].clearLastDeepInput() - # has cursor changed? - if self.environment['runtime']['cursorManager'].isCursorVerticalMove() or \ - self.environment['runtime']['cursorManager'].isCursorHorizontalMove(): + + if (self.environment['runtime']['cursorManager'].isCursorVerticalMove() or + self.environment['runtime']['cursorManager'].isCursorHorizontalMove()): self.environment['runtime']['commandManager'].executeDefaultTrigger('onCursorChange') + self.environment['runtime']['commandManager'].executeDefaultTrigger('onScreenUpdate') self.environment['runtime']['inputManager'].clearLastDeepInput() - #print('handleScreenUpdate:',time.time() - startTime) + def handlePlugInputDevice(self, event): try: self.environment['runtime']['inputManager'].setLastDetectedDevices(event['Data']) @@ -214,26 +143,27 @@ class fenrirManager(): self.environment['runtime']['inputManager'].handlePlugInputDevice(event['Data']) self.environment['runtime']['commandManager'].executeDefaultTrigger('onPlugInputDevice', force=True) self.environment['runtime']['inputManager'].setLastDetectedDevices(None) - def handleHeartBeat(self, event): - self.environment['runtime']['commandManager'].executeDefaultTrigger('onHeartBeat',force=True) - #self.environment['runtime']['outputManager'].brailleText(flush=False) + def handleHeartBeat(self, event): + self.environment['runtime']['commandManager'].executeDefaultTrigger('onHeartBeat', force=True) def detectShortcutCommand(self): if self.environment['input']['keyForeward'] > 0: return + if len(self.environment['input']['prevInput']) > len(self.environment['input']['currInput']): return + if self.environment['runtime']['inputManager'].isKeyPress(): self.modifierInput = self.environment['runtime']['inputManager'].currKeyIsModifier() else: if not self.environment['runtime']['inputManager'].noKeyPressed(): if self.singleKeyCommand: - self.singleKeyCommand = len(self.environment['input']['currInput'])== 1 - # key is already released. we need the old one - if not( self.singleKeyCommand and self.environment['runtime']['inputManager'].noKeyPressed()): - shortcut = self.environment['runtime']['inputManager'].getCurrShortcut() - self.command = self.environment['runtime']['inputManager'].getCommandForShortcut(shortcut) + self.singleKeyCommand = len(self.environment['input']['currInput']) == 1 + + if not(self.singleKeyCommand and self.environment['runtime']['inputManager'].noKeyPressed()): + currentShortcut = self.environment['runtime']['inputManager'].getCurrShortcut() + self.command = self.environment['runtime']['inputManager'].getCommandForShortcut(currentShortcut) if not self.modifierInput: if self.environment['runtime']['inputManager'].isKeyPress(): @@ -252,7 +182,8 @@ class fenrirManager(): if self.singleKeyCommand: self.environment['runtime']['eventManager'].putToEventQueue(fenrirEventType.ExecuteCommand, self.command) self.command = '' - def setProcessName(self, name = 'fenrir'): + + def setProcessName(self, name='fenrir'): """Attempts to set the process name to 'fenrir'.""" try: from setproctitle import setproctitle @@ -273,12 +204,14 @@ class fenrirManager(): pass return False + def shutdownRequest(self): try: self.environment['runtime']['eventManager'].stopMainEventLoop() except: pass - def captureSignal(self, siginit, frame): + + def captureSignal(self, sigInit, frame): self.shutdownRequest() def shutdown(self): @@ -287,10 +220,10 @@ class fenrirManager(): self.environment['runtime']['outputManager'].presentText(_("Quit Fenrir"), soundIcon='ScreenReaderOff', interrupt=True) self.environment['runtime']['eventManager'].cleanEventQueue() time.sleep(0.6) - for currManager in self.environment['general']['managerList']: - if self.environment['runtime'][currManager]: - self.environment['runtime'][currManager].shutdown() - del self.environment['runtime'][currManager] + + for currentManager in self.environment['general']['managerList']: + if self.environment['runtime'][currentManager]: + self.environment['runtime'][currentManager].shutdown() + del self.environment['runtime'][currentManager] self.environment = None -