latest changed merged, seems to be reasonably stable.

This commit is contained in:
Storm Dragon
2026-02-17 08:01:47 -05:00
8 changed files with 188 additions and 37 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
# Maintainer: Storm Dragon <storm_dragon@stormux.org>
pkgname=cthulhu
pkgver=2026.02.15
pkgver=2026.02.17
pkgrel=1
pkgdesc="Desktop-agnostic screen reader with plugin system, forked from Orca"
url="https://git.stormux.org/storm/cthulhu"
+1 -1
View File
@@ -1,5 +1,5 @@
project('cthulhu',
version: '2026.02.15-master',
version: '2026.02.17-master',
meson_version: '>= 1.0.0',
)
+1 -1
View File
@@ -23,5 +23,5 @@
# Forked from Orca screen reader.
# Cthulhu project: https://git.stormux.org/storm/cthulhu
version = "2026.02.15"
version = "2026.02.17"
codeName = "master"
+34
View File
@@ -844,6 +844,11 @@ class KeyboardEvent(InputEvent):
"shouldConsume: No handler found",
reason="no-handler", timestamp=True)
if self._isSleepModeActive():
if self._isSleepModeToggleHandler():
return True, 'Sleep mode toggle command'
return False, 'Sleep mode active'
self._script.updateKeyboardEventState(self, self._handler)
scriptConsumes = self._script.shouldConsumeKeyboardEvent(self, self._handler)
if globalHandlerUsed:
@@ -881,6 +886,35 @@ class KeyboardEvent(InputEvent):
return None
return global_bindings.getInputHandler(self)
def _isSleepModeActive(self):
"""Returns True if the script for this event is in sleep mode."""
if not self._script:
return False
if "scripts.sleepmode" in self._script.__module__:
return True
app = getattr(self._script, "app", None)
if app is None:
return False
try:
from . import sleep_mode_manager
manager = sleep_mode_manager.getManager()
return bool(manager and manager.isActiveForApp(app))
except Exception:
return False
def _isSleepModeToggleHandler(self):
"""Returns True if the resolved handler toggles sleep mode."""
if not self._handler or not self._handler.function:
return False
functionName = getattr(self._handler.function, "__name__", "")
return "toggleSleepMode" in functionName
def didConsume(self):
"""Returns True if this event was consumed."""
+10
View File
@@ -282,6 +282,16 @@ class ScriptManager:
Returns an instance of a Script.
"""
if app:
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:
_log_tokens(["Could not check sleep mode for", app, ":", error], "sleep-mode-check-failed")
customScript = None
appScript = None
toolkitScript = None
+10 -29
View File
@@ -75,9 +75,6 @@ class Script(default.Script):
"""Called when this script is deactivated."""
debug.printMessage(debug.LEVEL_INFO, "SLEEP MODE SCRIPT: Deactivating", True)
# Restore key grabs
self.addKeyGrabs()
cthulhu_modifier_manager.getManager().refreshCthulhuModifiers("Exiting sleep mode.")
super().deactivate()
@@ -86,18 +83,23 @@ class Script(default.Script):
"""Remove key grabs except for sleep mode toggle."""
try:
# First remove all grabs inherited from default activation,
# including modifier grabs.
super().removeKeyGrabs()
self.grab_ids = []
for keyBinding in self.keyBindings:
for keyBinding in self.keyBindings.keyBindings:
if hasattr(keyBinding, 'handler') and hasattr(keyBinding.handler, 'function'):
if hasattr(keyBinding.handler.function, '__name__'):
if 'toggleSleepMode' in keyBinding.handler.function.__name__:
# Keep sleep mode toggle
try:
import cthulhu
grab_id = cthulhu.addKeyGrab(keyBinding)
if grab_id:
self.grab_ids.append(grab_id)
debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Kept sleep toggle key grab: {grab_id}", True)
grabIds = cthulhu.addKeyGrab(keyBinding)
if grabIds:
for grabId in grabIds:
self.grab_ids.append(grabId)
debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Kept sleep toggle key grab: {grabId}", True)
except Exception as e:
debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Error keeping key grab: {e}", True)
else:
@@ -106,27 +108,6 @@ class Script(default.Script):
except Exception as e:
debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Error in removeKeyGrabs: {e}", True)
def addKeyGrabs(self):
"""Add back all key grabs."""
try:
# Remove our limited grabs first
if hasattr(self, 'grab_ids'):
import cthulhu
for grab_id in self.grab_ids:
try:
cthulhu.removeKeyGrab(grab_id)
debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Removed key grab: {grab_id}", True)
except Exception as e:
debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Error removing key grab {grab_id}: {e}", True)
self.grab_ids = []
# Let the parent class restore all grabs
super().addKeyGrabs()
except Exception as e:
debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Error in addKeyGrabs: {e}", True)
# Block common event handlers as an additional layer of protection
def onCaretMoved(self, event):
"""Block caret movement events."""
+19
View File
@@ -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,
+112 -5
View File
@@ -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