diff --git a/src/cthulhu/script_manager.py b/src/cthulhu/script_manager.py index 6d78962..8855d24 100644 --- a/src/cthulhu/script_manager.py +++ b/src/cthulhu/script_manager.py @@ -286,6 +286,7 @@ class ScriptManager: try: from . import sleep_mode_manager sleepModeManager = sleep_mode_manager.getManager() + sleepModeManager.refreshAutoSleepConfig() if sleepModeManager and sleepModeManager.isActiveForApp(app): return self.get_or_create_sleep_mode_script(app) except Exception as error: diff --git a/src/cthulhu/settings_manager.py b/src/cthulhu/settings_manager.py index 1d9d3a9..2b18db7 100644 --- a/src/cthulhu/settings_manager.py +++ b/src/cthulhu/settings_manager.py @@ -214,6 +214,25 @@ class SettingsManager(object): if not os.path.exists(userCustomFile): os.close(os.open(userCustomFile, os.O_CREAT, 0o700)) + sleepConfigFile = os.path.join(cthulhuDir, "sleep.toml") + if not os.path.exists(sleepConfigFile): + sleepTemplate = ( + "# Cthulhu auto-sleep apps\n" + "#\n" + "# List current app names with:\n" + "# cthulhu --list-apps\n" + "# Use the middle app-name column from that output.\n" + "#\n" + "# Add app names to auto-enable sleep mode:\n" + "# apps = [\"qemu\"]\n" + "#\n" + "# Or use a section:\n" + "# [sleep]\n" + "# apps = [\"qemu\"]\n" + ) + with open(sleepConfigFile, "w", encoding="utf-8") as configFile: + configFile.write(sleepTemplate) + if self.isFirstStart() and self._backend: self._backend.saveDefaultSettings(self.defaultGeneral, self.defaultPronunciations, diff --git a/src/cthulhu/sleep_mode_manager.py b/src/cthulhu/sleep_mode_manager.py index 14f44f9..75c3f45 100644 --- a/src/cthulhu/sleep_mode_manager.py +++ b/src/cthulhu/sleep_mode_manager.py @@ -31,7 +31,9 @@ __copyright__ = "Copyright (c) 2024 Stormux" __license__ = "LGPL" import time +import os from gi.repository import GLib +from tomlkit import parse import cthulhu.braille as braille import cthulhu.cmdnames as cmdnames import cthulhu.debug as debug @@ -47,7 +49,12 @@ class SleepModeManager: def __init__(self): self._handlers = self.getHandlers(True) self._bindings = keybindings.KeyBindings() - self._apps = [] + self._apps = set() + self._disabledAutoSleepApps = set() + self._autoSleepAppNames = set() + self._autoSleepPath = self._getAutoSleepPath() + self._autoSleepConfigMTime = None + self._loadAutoSleepConfig() self._lastToggleTime = 0 self._toggleDebounceDelay = 0.1 # 100ms debounce (reduced for better responsiveness) @@ -76,12 +83,106 @@ class SleepModeManager: def isActiveForApp(self, app): """Returns True if sleep mode is active for app.""" - result = bool(app and hash(app) in self._apps) + if not app: + return False + + appHash = hash(app) + result = appHash in self._apps + if not result and self._isAutoSleepConfiguredForApp(app): + result = appHash not in self._disabledAutoSleepApps + if result: tokens = ["SLEEP MODE MANAGER: Is active for", app] debug.printTokens(debug.LEVEL_INFO, tokens, True) return result + def _getAutoSleepPath(self): + prefsDir = os.path.join(GLib.get_user_data_dir(), "cthulhu") + try: + from . import cthulhu + app = cthulhu.cthulhuApp + if app and app.settingsManager: + configuredDir = app.settingsManager.getPrefsDir() + if configuredDir: + prefsDir = configuredDir + except Exception: + pass + return os.path.join(prefsDir, "sleep.toml") + + def _refreshAutoSleepPath(self): + latestPath = self._getAutoSleepPath() + if latestPath != self._autoSleepPath: + self._autoSleepPath = latestPath + self._autoSleepConfigMTime = None + self._loadAutoSleepConfig() + return + + try: + latestMTime = os.path.getmtime(self._autoSleepPath) + except OSError: + latestMTime = None + + if latestMTime != self._autoSleepConfigMTime: + self._loadAutoSleepConfig() + + def refreshAutoSleepConfig(self): + """Refresh auto-sleep config if prefs directory has changed.""" + + self._refreshAutoSleepPath() + + def _loadAutoSleepConfig(self): + self._autoSleepAppNames = set() + self._autoSleepConfigMTime = None + + if not os.path.isfile(self._autoSleepPath): + msg = f"SLEEP MODE MANAGER: No sleep config at {self._autoSleepPath}" + debug.printMessage(debug.LEVEL_INFO, msg, True) + return + + try: + self._autoSleepConfigMTime = os.path.getmtime(self._autoSleepPath) + except OSError: + self._autoSleepConfigMTime = None + + try: + with open(self._autoSleepPath, "r", encoding="utf-8") as configFile: + config = parse(configFile.read() or "") + except Exception as error: + tokens = ["SLEEP MODE MANAGER: Failed to parse", self._autoSleepPath, ":", error] + debug.printTokens(debug.LEVEL_WARNING, tokens, True) + return + + appNames = [] + topLevelApps = config.get("apps", []) + if isinstance(topLevelApps, list): + appNames.extend(topLevelApps) + + sleepSection = config.get("sleep", {}) + if isinstance(sleepSection, dict): + sectionApps = sleepSection.get("apps", []) + if isinstance(sectionApps, list): + appNames.extend(sectionApps) + + for appName in appNames: + if not isinstance(appName, str): + continue + normalizedName = appName.strip().lower() + if normalizedName: + self._autoSleepAppNames.add(normalizedName) + + msg = f"SLEEP MODE MANAGER: Loaded {len(self._autoSleepAppNames)} auto-sleep apps from {self._autoSleepPath}" + debug.printMessage(debug.LEVEL_INFO, msg, True) + + def _isAutoSleepConfiguredForApp(self, app): + if not app: + return False + + if not self._autoSleepAppNames: + return False + + appName = (AXObject.get_name(app) or "").strip().lower() + return bool(appName and appName in self._autoSleepAppNames) + def _setupHandlers(self): """Sets up and returns the sleep-mode-manager input event handlers.""" @@ -132,12 +233,16 @@ class SleepModeManager: if not (script and script.app): return True - from . import cthulhu_state + self.refreshAutoSleepConfig() + scriptManager = script_manager.get_manager() if self.isActiveForApp(script.app): # Turning OFF sleep mode - self._apps.remove(hash(script.app)) + appHash = hash(script.app) + self._apps.discard(appHash) + if self._isAutoSleepConfiguredForApp(script.app): + self._disabledAutoSleepApps.add(appHash) newScript = scriptManager.get_script(script.app) if notifyUser: newScript.presentMessage( @@ -177,7 +282,9 @@ class SleepModeManager: debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Active script set successfully", True) debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Adding app to sleep list", True) - self._apps.append(hash(script.app)) + appHash = hash(script.app) + self._disabledAutoSleepApps.discard(appHash) + self._apps.add(appHash) debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Enabled for {AXObject.get_name(script.app)} (delayed)", True) # Reset debounce timer after successful toggle self._lastToggleTime = 0