Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8312a842c1 | |||
| 07138197cb | |||
| 4add36f5ca | |||
| 40e63150a6 | |||
| 4dba0ec0cd | |||
| e6f780c38b | |||
| 0f7f73a6a0 | |||
| aa71d02036 | |||
| f873fcee11 | |||
| 13976b7235 |
@@ -1,7 +1,7 @@
|
|||||||
# Maintainer: Storm Dragon <storm_dragon@stormux.org>
|
# Maintainer: Storm Dragon <storm_dragon@stormux.org>
|
||||||
|
|
||||||
pkgname=cthulhu
|
pkgname=cthulhu
|
||||||
pkgver=2026.01.26
|
pkgver=2026.02.17
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Desktop-agnostic screen reader with plugin system, forked from Orca"
|
pkgdesc="Desktop-agnostic screen reader with plugin system, forked from Orca"
|
||||||
url="https://git.stormux.org/storm/cthulhu"
|
url="https://git.stormux.org/storm/cthulhu"
|
||||||
@@ -27,6 +27,7 @@ depends=(
|
|||||||
|
|
||||||
# Plugin system and D-Bus remote control
|
# Plugin system and D-Bus remote control
|
||||||
python-pluggy
|
python-pluggy
|
||||||
|
python-tomlkit
|
||||||
python-dasbus
|
python-dasbus
|
||||||
|
|
||||||
# AI Assistant dependencies (for screenshots, HTTP requests, and actions)
|
# AI Assistant dependencies (for screenshots, HTTP requests, and actions)
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Cthulhu Logging Guidelines
|
||||||
|
|
||||||
|
This document defines the preferred format for debug logging in Cthulhu.
|
||||||
|
The goal is to keep logs consistent, searchable, and easy to scan.
|
||||||
|
|
||||||
|
## Helpers
|
||||||
|
|
||||||
|
Use the helpers in `cthulhu.debug` for new logs:
|
||||||
|
|
||||||
|
- `debug.print_log(level, prefix, message, reason=None, timestamp=False, stack=False)`
|
||||||
|
- `debug.print_log_tokens(level, prefix, tokens, reason=None, timestamp=False, stack=False)`
|
||||||
|
|
||||||
|
These helpers ensure the prefix and optional reason tag are formatted consistently.
|
||||||
|
Timestamps are appended at the end of the message when enabled.
|
||||||
|
|
||||||
|
## Prefixes
|
||||||
|
|
||||||
|
Use short, uppercase prefixes that identify the subsystem:
|
||||||
|
|
||||||
|
- `EVENT MANAGER`
|
||||||
|
- `FOCUS MANAGER`
|
||||||
|
- `INPUT EVENT`
|
||||||
|
- `SCRIPT MANAGER`
|
||||||
|
- `WEB`
|
||||||
|
|
||||||
|
## Messages
|
||||||
|
|
||||||
|
- Keep messages short and action-focused.
|
||||||
|
- Do not include the prefix in the message.
|
||||||
|
- Prefer consistent verbs (e.g., "Not using …", "Setting …", "Ignoring …").
|
||||||
|
|
||||||
|
## Reason Tags
|
||||||
|
|
||||||
|
Use reason tags to explain decisions or early exits.
|
||||||
|
|
||||||
|
- Lowercase with hyphens (e.g., `focus-mode`, `no-active-script`).
|
||||||
|
- Use a short phrase rather than a full sentence.
|
||||||
|
- If a human-readable reason is already available, it can be used directly.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
```text
|
||||||
|
WEB: Not using caret navigation (reason=disabled)
|
||||||
|
FOCUS MANAGER: Setting locus of focus to existing locus of focus (reason=no-change)
|
||||||
|
SCRIPT MANAGER: Setting active script to [script] (reason=focus: active-window)
|
||||||
|
```
|
||||||
+1
-1
@@ -290,7 +290,7 @@ toggle the reading of tables, either by single cell or whole row.
|
|||||||
.B Cthulhu
|
.B Cthulhu
|
||||||
user preferences directory
|
user preferences directory
|
||||||
.TP
|
.TP
|
||||||
.BI ~/.local/share/cthulhu/user-settings.conf
|
.BI ~/.local/share/cthulhu/user-settings.toml
|
||||||
.B Cthulhu
|
.B Cthulhu
|
||||||
user preferences configuration file.
|
user preferences configuration file.
|
||||||
.TP
|
.TP
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
project('cthulhu',
|
project('cthulhu',
|
||||||
version: '2026.01.26-master',
|
version: '2026.02.17-master',
|
||||||
meson_version: '>= 1.0.0',
|
meson_version: '>= 1.0.0',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ license = { text = "LGPL-2.1-or-later" }
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"pygobject>=3.18",
|
"pygobject>=3.18",
|
||||||
"pluggy",
|
"pluggy",
|
||||||
|
"tomlkit",
|
||||||
"brlapi; extra == 'braille'",
|
"brlapi; extra == 'braille'",
|
||||||
"python-speechd; extra == 'speech'",
|
"python-speechd; extra == 'speech'",
|
||||||
"piper-tts; extra == 'piper'",
|
"piper-tts; extra == 'piper'",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
backends_python_sources = files([
|
backends_python_sources = files([
|
||||||
'__init__.py',
|
'__init__.py',
|
||||||
'json_backend.py',
|
'toml_backend.py',
|
||||||
])
|
])
|
||||||
|
|
||||||
python3.install_sources(
|
python3.install_sources(
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
# Forked from Orca screen reader.
|
# Forked from Orca screen reader.
|
||||||
# Cthulhu project: https://git.stormux.org/storm/cthulhu
|
# Cthulhu project: https://git.stormux.org/storm/cthulhu
|
||||||
|
|
||||||
"""JSON backend for Cthulhu settings"""
|
"""TOML backend for Cthulhu settings"""
|
||||||
|
|
||||||
__id__ = "$Id$"
|
__id__ = "$Id$"
|
||||||
__version__ = "$Revision$"
|
__version__ = "$Revision$"
|
||||||
@@ -31,20 +31,22 @@ __date__ = "$Date$"
|
|||||||
__copyright__ = "Copyright (c) 2010-2011 Consorcio Fernando de los Rios."
|
__copyright__ = "Copyright (c) 2010-2011 Consorcio Fernando de los Rios."
|
||||||
__license__ = "LGPL"
|
__license__ = "LGPL"
|
||||||
|
|
||||||
from json import load, dump
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from tomlkit import parse, dumps, document
|
||||||
|
|
||||||
from cthulhu import settings, acss
|
from cthulhu import settings, acss
|
||||||
|
|
||||||
class Backend:
|
class Backend:
|
||||||
|
|
||||||
def __init__(self, prefsDir):
|
def __init__(self, prefsDir):
|
||||||
""" Initialize the JSON Backend.
|
""" Initialize the TOML Backend.
|
||||||
"""
|
"""
|
||||||
self.general = {}
|
self.general = {}
|
||||||
self.pronunciations = {}
|
self.pronunciations = {}
|
||||||
self.keybindings = {}
|
self.keybindings = {}
|
||||||
self.profiles = {}
|
self.profiles = {}
|
||||||
self.settingsFile = os.path.join(prefsDir, "user-settings.conf")
|
self.settingsFile = os.path.join(prefsDir, "user-settings.toml")
|
||||||
self.appPrefsDir = os.path.join(prefsDir, "app-settings")
|
self.appPrefsDir = os.path.join(prefsDir, "app-settings")
|
||||||
|
|
||||||
self._defaultProfiles = {'default': { 'profile': settings.profile,
|
self._defaultProfiles = {'default': { 'profile': settings.profile,
|
||||||
@@ -53,46 +55,101 @@ class Backend:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _stripNone(self, value):
|
||||||
|
if isinstance(value, dict):
|
||||||
|
cleaned = {}
|
||||||
|
for key, item in value.items():
|
||||||
|
if item is None:
|
||||||
|
continue
|
||||||
|
cleanedItem = self._stripNone(item)
|
||||||
|
if cleanedItem is None:
|
||||||
|
continue
|
||||||
|
cleaned[key] = cleanedItem
|
||||||
|
return cleaned
|
||||||
|
if isinstance(value, list):
|
||||||
|
cleanedList = []
|
||||||
|
for item in value:
|
||||||
|
if item is None:
|
||||||
|
continue
|
||||||
|
cleanedList.append(self._stripNone(item))
|
||||||
|
return cleanedList
|
||||||
|
return value
|
||||||
|
|
||||||
|
def _readDocument(self, fileName):
|
||||||
|
if os.path.exists(fileName):
|
||||||
|
with open(fileName, 'r', encoding='utf-8') as settingsFile:
|
||||||
|
return parse(settingsFile.read())
|
||||||
|
return document()
|
||||||
|
|
||||||
|
def _writeDocument(self, fileName, prefsDoc):
|
||||||
|
with open(fileName, 'w', encoding='utf-8') as settingsFile:
|
||||||
|
settingsFile.write(dumps(prefsDoc))
|
||||||
|
|
||||||
|
def _updateTable(self, targetTable, newValues):
|
||||||
|
if not isinstance(newValues, dict):
|
||||||
|
return
|
||||||
|
|
||||||
|
for key in list(targetTable.keys()):
|
||||||
|
if key not in newValues:
|
||||||
|
del targetTable[key]
|
||||||
|
continue
|
||||||
|
newValue = newValues[key]
|
||||||
|
existingValue = targetTable.get(key)
|
||||||
|
if isinstance(newValue, dict) and isinstance(existingValue, dict):
|
||||||
|
self._updateTable(existingValue, newValue)
|
||||||
|
continue
|
||||||
|
if existingValue != newValue:
|
||||||
|
targetTable[key] = newValue
|
||||||
|
|
||||||
|
for key, newValue in newValues.items():
|
||||||
|
if key not in targetTable:
|
||||||
|
targetTable[key] = newValue
|
||||||
|
|
||||||
def saveDefaultSettings(self, general, pronunciations, keybindings):
|
def saveDefaultSettings(self, general, pronunciations, keybindings):
|
||||||
""" Save default settings for all the properties from
|
""" Save default settings for all the properties from
|
||||||
cthulhu.settings. """
|
cthulhu.settings. """
|
||||||
prefs = {'general': general,
|
prefs = {'general': self._stripNone(general),
|
||||||
'profiles': self._defaultProfiles,
|
'profiles': self._stripNone(self._defaultProfiles),
|
||||||
'pronunciations': pronunciations,
|
'pronunciations': self._stripNone(pronunciations),
|
||||||
'keybindings': keybindings}
|
'keybindings': self._stripNone(keybindings)}
|
||||||
|
|
||||||
self.general = general
|
self.general = general
|
||||||
self.profiles = self._defaultProfiles
|
self.profiles = self._defaultProfiles
|
||||||
self.pronunciations = pronunciations
|
self.pronunciations = pronunciations
|
||||||
self.keybindings = keybindings
|
self.keybindings = keybindings
|
||||||
|
|
||||||
settingsFile = open(self.settingsFile, 'w')
|
prefsDoc = document()
|
||||||
dump(prefs, settingsFile, indent=4)
|
prefsDoc['general'] = prefs['general']
|
||||||
settingsFile.close()
|
prefsDoc['profiles'] = prefs['profiles']
|
||||||
|
prefsDoc['pronunciations'] = prefs['pronunciations']
|
||||||
|
prefsDoc['keybindings'] = prefs['keybindings']
|
||||||
|
|
||||||
|
self._writeDocument(self.settingsFile, prefsDoc)
|
||||||
|
|
||||||
def getAppSettings(self, appName):
|
def getAppSettings(self, appName):
|
||||||
fileName = os.path.join(self.appPrefsDir, f"{appName}.conf")
|
fileName = os.path.join(self.appPrefsDir, f"{appName}.toml")
|
||||||
if os.path.exists(fileName):
|
return self._readDocument(fileName)
|
||||||
settingsFile = open(fileName, 'r')
|
|
||||||
prefs = load(settingsFile)
|
|
||||||
settingsFile.close()
|
|
||||||
else:
|
|
||||||
prefs = {}
|
|
||||||
|
|
||||||
return prefs
|
|
||||||
|
|
||||||
def saveAppSettings(self, appName, profile, general, pronunciations, keybindings):
|
def saveAppSettings(self, appName, profile, general, pronunciations, keybindings):
|
||||||
prefs = self.getAppSettings(appName)
|
prefsDoc = self.getAppSettings(appName)
|
||||||
profiles = prefs.get('profiles', {})
|
profiles = prefsDoc.get('profiles')
|
||||||
profiles[profile] = {'general': general,
|
if profiles is None or not isinstance(profiles, dict):
|
||||||
'pronunciations': pronunciations,
|
prefsDoc['profiles'] = {}
|
||||||
'keybindings': keybindings}
|
profiles = prefsDoc['profiles']
|
||||||
prefs['profiles'] = profiles
|
|
||||||
|
|
||||||
fileName = os.path.join(self.appPrefsDir, f"{appName}.conf")
|
profileTable = profiles.get(profile)
|
||||||
settingsFile = open(fileName, 'w')
|
if profileTable is None or not isinstance(profileTable, dict):
|
||||||
dump(prefs, settingsFile, indent=4)
|
profiles[profile] = {}
|
||||||
settingsFile.close()
|
profileTable = profiles[profile]
|
||||||
|
|
||||||
|
self._updateTable(profileTable, {
|
||||||
|
'general': self._stripNone(general),
|
||||||
|
'pronunciations': self._stripNone(pronunciations),
|
||||||
|
'keybindings': self._stripNone(keybindings),
|
||||||
|
})
|
||||||
|
|
||||||
|
fileName = os.path.join(self.appPrefsDir, f"{appName}.toml")
|
||||||
|
self._writeDocument(fileName, prefsDoc)
|
||||||
|
|
||||||
def saveProfileSettings(self, profile, general,
|
def saveProfileSettings(self, profile, general,
|
||||||
pronunciations, keybindings):
|
pronunciations, keybindings):
|
||||||
@@ -103,25 +160,32 @@ class Backend:
|
|||||||
|
|
||||||
general['pronunciations'] = pronunciations
|
general['pronunciations'] = pronunciations
|
||||||
general['keybindings'] = keybindings
|
general['keybindings'] = keybindings
|
||||||
|
general = self._stripNone(general)
|
||||||
|
|
||||||
with open(self.settingsFile, 'r+') as settingsFile:
|
prefsDoc = self._readDocument(self.settingsFile)
|
||||||
prefs = load(settingsFile)
|
profiles = prefsDoc.get('profiles')
|
||||||
prefs['profiles'][profile] = general
|
if profiles is None or not isinstance(profiles, dict):
|
||||||
settingsFile.seek(0)
|
prefsDoc['profiles'] = {}
|
||||||
settingsFile.truncate()
|
profiles = prefsDoc['profiles']
|
||||||
dump(prefs, settingsFile, indent=4)
|
|
||||||
|
profileTable = profiles.get(profile)
|
||||||
|
if profileTable is None or not isinstance(profileTable, dict):
|
||||||
|
profiles[profile] = {}
|
||||||
|
profileTable = profiles[profile]
|
||||||
|
|
||||||
|
self._updateTable(profileTable, general)
|
||||||
|
self._writeDocument(self.settingsFile, prefsDoc)
|
||||||
|
|
||||||
def _getSettings(self):
|
def _getSettings(self):
|
||||||
""" Load from config file all settings """
|
""" Load from config file all settings """
|
||||||
settingsFile = open(self.settingsFile)
|
prefsDoc = self._readDocument(self.settingsFile)
|
||||||
try:
|
try:
|
||||||
prefs = load(settingsFile)
|
self.general = dict(prefsDoc.get('general', {}))
|
||||||
except ValueError:
|
self.pronunciations = dict(prefsDoc.get('pronunciations', {}))
|
||||||
|
self.keybindings = dict(prefsDoc.get('keybindings', {}))
|
||||||
|
self.profiles = dict(prefsDoc.get('profiles', {}))
|
||||||
|
except Exception:
|
||||||
return
|
return
|
||||||
self.general = prefs['general'].copy()
|
|
||||||
self.pronunciations = prefs['pronunciations']
|
|
||||||
self.keybindings = prefs['keybindings']
|
|
||||||
self.profiles = prefs['profiles'].copy()
|
|
||||||
|
|
||||||
def _migrateSettings(self, settingsDict):
|
def _migrateSettings(self, settingsDict):
|
||||||
"""Migrate old setting names to new ones."""
|
"""Migrate old setting names to new ones."""
|
||||||
@@ -201,12 +265,18 @@ class Backend:
|
|||||||
def _setProfileKey(self, key, value):
|
def _setProfileKey(self, key, value):
|
||||||
self.general[key] = value
|
self.general[key] = value
|
||||||
|
|
||||||
with open(self.settingsFile, 'r+') as settingsFile:
|
prefsDoc = self._readDocument(self.settingsFile)
|
||||||
prefs = load(settingsFile)
|
general = prefsDoc.get('general')
|
||||||
prefs['general'][key] = value
|
if general is None or not isinstance(general, dict):
|
||||||
settingsFile.seek(0)
|
prefsDoc['general'] = {}
|
||||||
settingsFile.truncate()
|
general = prefsDoc['general']
|
||||||
dump(prefs, settingsFile, indent=4)
|
|
||||||
|
if value is None:
|
||||||
|
if key in general:
|
||||||
|
del general[key]
|
||||||
|
else:
|
||||||
|
general[key] = value
|
||||||
|
self._writeDocument(self.settingsFile, prefsDoc)
|
||||||
|
|
||||||
def setFirstStart(self, value=False):
|
def setFirstStart(self, value=False):
|
||||||
"""Set firstStart. This user-configurable setting is primarily
|
"""Set firstStart. This user-configurable setting is primarily
|
||||||
@@ -238,10 +308,8 @@ class Backend:
|
|||||||
if profile in self.profiles:
|
if profile in self.profiles:
|
||||||
removeProfileFrom(self.profiles)
|
removeProfileFrom(self.profiles)
|
||||||
|
|
||||||
with open(self.settingsFile, 'r+') as settingsFile:
|
prefsDoc = self._readDocument(self.settingsFile)
|
||||||
prefs = load(settingsFile)
|
profiles = prefsDoc.get('profiles')
|
||||||
if profile in prefs['profiles']:
|
if isinstance(profiles, dict) and profile in profiles:
|
||||||
removeProfileFrom(prefs['profiles'])
|
removeProfileFrom(profiles)
|
||||||
settingsFile.seek(0)
|
self._writeDocument(self.settingsFile, prefsDoc)
|
||||||
settingsFile.truncate()
|
|
||||||
dump(prefs, settingsFile, indent=4)
|
|
||||||
@@ -23,5 +23,5 @@
|
|||||||
# Forked from Orca screen reader.
|
# Forked from Orca screen reader.
|
||||||
# Cthulhu project: https://git.stormux.org/storm/cthulhu
|
# Cthulhu project: https://git.stormux.org/storm/cthulhu
|
||||||
|
|
||||||
version = "2026.01.26"
|
version = "2026.02.17"
|
||||||
codeName = "master"
|
codeName = "master"
|
||||||
|
|||||||
@@ -180,6 +180,8 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
|
|||||||
self.savedGain = None
|
self.savedGain = None
|
||||||
self.savedPitch = None
|
self.savedPitch = None
|
||||||
self.savedRate = None
|
self.savedRate = None
|
||||||
|
self.soundThemeCombo = None
|
||||||
|
self.roleSoundPresentationCombo = None
|
||||||
self._isInitialSetup = False
|
self._isInitialSetup = False
|
||||||
self.selectedFamilyChoices = {}
|
self.selectedFamilyChoices = {}
|
||||||
self.selectedLanguageChoices = {}
|
self.selectedLanguageChoices = {}
|
||||||
@@ -2671,6 +2673,8 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
|
|||||||
# Get widget references
|
# Get widget references
|
||||||
self.soundThemeCombo = self.get_widget("soundThemeCombo")
|
self.soundThemeCombo = self.get_widget("soundThemeCombo")
|
||||||
self.roleSoundPresentationCombo = self.get_widget("roleSoundPresentationCombo")
|
self.roleSoundPresentationCombo = self.get_widget("roleSoundPresentationCombo")
|
||||||
|
self.soundThemeCombo.set_can_focus(False)
|
||||||
|
self.roleSoundPresentationCombo.set_can_focus(False)
|
||||||
|
|
||||||
# Populate sound theme combo box
|
# Populate sound theme combo box
|
||||||
themeManager = sound_theme_manager.getManager()
|
themeManager = sound_theme_manager.getManager()
|
||||||
@@ -2728,6 +2732,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
|
|||||||
value = self._roleSoundPresentationChoices[activeIndex][0]
|
value = self._roleSoundPresentationChoices[activeIndex][0]
|
||||||
self.prefsDict["roleSoundPresentation"] = value
|
self.prefsDict["roleSoundPresentation"] = value
|
||||||
|
|
||||||
|
|
||||||
def _updateCthulhuModifier(self):
|
def _updateCthulhuModifier(self):
|
||||||
combobox = self.get_widget("cthulhuModifierComboBox")
|
combobox = self.get_widget("cthulhuModifierComboBox")
|
||||||
keystring = ", ".join(self.prefsDict["cthulhuModifierKeys"])
|
keystring = ", ".join(self.prefsDict["cthulhuModifierKeys"])
|
||||||
|
|||||||
@@ -844,6 +844,11 @@ class KeyboardEvent(InputEvent):
|
|||||||
"shouldConsume: No handler found",
|
"shouldConsume: No handler found",
|
||||||
reason="no-handler", timestamp=True)
|
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)
|
self._script.updateKeyboardEventState(self, self._handler)
|
||||||
scriptConsumes = self._script.shouldConsumeKeyboardEvent(self, self._handler)
|
scriptConsumes = self._script.shouldConsumeKeyboardEvent(self, self._handler)
|
||||||
if globalHandlerUsed:
|
if globalHandlerUsed:
|
||||||
@@ -881,6 +886,35 @@ class KeyboardEvent(InputEvent):
|
|||||||
return None
|
return None
|
||||||
return global_bindings.getInputHandler(self)
|
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):
|
def didConsume(self):
|
||||||
"""Returns True if this event was consumed."""
|
"""Returns True if this event was consumed."""
|
||||||
|
|
||||||
|
|||||||
@@ -276,7 +276,9 @@ class LiveRegionManager:
|
|||||||
utts = message['labels'] + message['content']
|
utts = message['labels'] + message['content']
|
||||||
|
|
||||||
if self.monitoring:
|
if self.monitoring:
|
||||||
self._script.presentMessage(utts)
|
# Live region content is user-generated text, not system messages.
|
||||||
|
# Use resetStyles=False to preserve the user's punctuation settings.
|
||||||
|
self._script.presentMessage(utts, resetStyles=False)
|
||||||
else:
|
else:
|
||||||
msg = "INFO: Not presenting message because monitoring is off"
|
msg = "INFO: Not presenting message because monitoring is off"
|
||||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||||
|
|||||||
@@ -140,21 +140,19 @@ Access comprehensive OCR settings through Cthulhu Preferences:
|
|||||||
|
|
||||||
### Configuration File
|
### Configuration File
|
||||||
Settings are automatically stored in Cthulhu's configuration system:
|
Settings are automatically stored in Cthulhu's configuration system:
|
||||||
- **Global Settings**: `~/.local/share/cthulhu/user-settings.conf`
|
- **Global Settings**: `~/.local/share/cthulhu/user-settings.toml`
|
||||||
- **Profile Settings**: `~/.local/share/cthulhu/app-settings/[profile]/`
|
- **Application Settings**: `~/.local/share/cthulhu/app-settings/<app>.toml`
|
||||||
|
|
||||||
### Example Configuration Values
|
### Example Configuration Values
|
||||||
```json
|
```toml
|
||||||
{
|
ocrLanguageCode = "eng"
|
||||||
"ocrLanguageCode": "eng",
|
ocrScaleFactor = 3
|
||||||
"ocrScaleFactor": 3,
|
ocrGrayscaleImg = false
|
||||||
"ocrGrayscaleImg": false,
|
ocrInvertImg = false
|
||||||
"ocrInvertImg": false,
|
ocrBlackWhiteImg = false
|
||||||
"ocrBlackWhiteImg": false,
|
ocrBlackWhiteImgValue = 200
|
||||||
"ocrBlackWhiteImgValue": 200,
|
ocrColorCalculation = false
|
||||||
"ocrColorCalculation": false,
|
ocrCopyToClipboard = true
|
||||||
"ocrCopyToClipboard": true
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|||||||
@@ -282,6 +282,16 @@ class ScriptManager:
|
|||||||
Returns an instance of a Script.
|
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
|
customScript = None
|
||||||
appScript = None
|
appScript = None
|
||||||
toolkitScript = None
|
toolkitScript = None
|
||||||
|
|||||||
@@ -1473,7 +1473,9 @@ class Script(script.Script):
|
|||||||
"""Callback for object:announcement events."""
|
"""Callback for object:announcement events."""
|
||||||
|
|
||||||
if isinstance(event.any_data, str):
|
if isinstance(event.any_data, str):
|
||||||
self.presentMessage(event.any_data)
|
# AT-SPI announcements contain application content, not system messages.
|
||||||
|
# Use resetStyles=False to preserve the user's punctuation settings.
|
||||||
|
self.presentMessage(event.any_data, resetStyles=False)
|
||||||
|
|
||||||
def onNameChanged(self, event):
|
def onNameChanged(self, event):
|
||||||
"""Callback for object:property-change:accessible-name events."""
|
"""Callback for object:property-change:accessible-name events."""
|
||||||
|
|||||||
@@ -75,9 +75,6 @@ class Script(default.Script):
|
|||||||
"""Called when this script is deactivated."""
|
"""Called when this script is deactivated."""
|
||||||
|
|
||||||
debug.printMessage(debug.LEVEL_INFO, "SLEEP MODE SCRIPT: Deactivating", True)
|
debug.printMessage(debug.LEVEL_INFO, "SLEEP MODE SCRIPT: Deactivating", True)
|
||||||
|
|
||||||
# Restore key grabs
|
|
||||||
self.addKeyGrabs()
|
|
||||||
cthulhu_modifier_manager.getManager().refreshCthulhuModifiers("Exiting sleep mode.")
|
cthulhu_modifier_manager.getManager().refreshCthulhuModifiers("Exiting sleep mode.")
|
||||||
|
|
||||||
super().deactivate()
|
super().deactivate()
|
||||||
@@ -86,18 +83,23 @@ class Script(default.Script):
|
|||||||
"""Remove key grabs except for sleep mode toggle."""
|
"""Remove key grabs except for sleep mode toggle."""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# First remove all grabs inherited from default activation,
|
||||||
|
# including modifier grabs.
|
||||||
|
super().removeKeyGrabs()
|
||||||
|
|
||||||
self.grab_ids = []
|
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') and hasattr(keyBinding.handler, 'function'):
|
||||||
if hasattr(keyBinding.handler.function, '__name__'):
|
if hasattr(keyBinding.handler.function, '__name__'):
|
||||||
if 'toggleSleepMode' in keyBinding.handler.function.__name__:
|
if 'toggleSleepMode' in keyBinding.handler.function.__name__:
|
||||||
# Keep sleep mode toggle
|
# Keep sleep mode toggle
|
||||||
try:
|
try:
|
||||||
import cthulhu
|
import cthulhu
|
||||||
grab_id = cthulhu.addKeyGrab(keyBinding)
|
grabIds = cthulhu.addKeyGrab(keyBinding)
|
||||||
if grab_id:
|
if grabIds:
|
||||||
self.grab_ids.append(grab_id)
|
for grabId in grabIds:
|
||||||
debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Kept sleep toggle key grab: {grab_id}", True)
|
self.grab_ids.append(grabId)
|
||||||
|
debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Kept sleep toggle key grab: {grabId}", True)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Error keeping key grab: {e}", True)
|
debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Error keeping key grab: {e}", True)
|
||||||
else:
|
else:
|
||||||
@@ -106,27 +108,6 @@ class Script(default.Script):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Error in removeKeyGrabs: {e}", True)
|
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
|
# Block common event handlers as an additional layer of protection
|
||||||
def onCaretMoved(self, event):
|
def onCaretMoved(self, event):
|
||||||
"""Block caret movement events."""
|
"""Block caret movement events."""
|
||||||
|
|||||||
@@ -1648,9 +1648,21 @@ class Script(default.Script):
|
|||||||
elif self.utilities.isContentEditableWithEmbeddedObjects(newFocus) \
|
elif self.utilities.isContentEditableWithEmbeddedObjects(newFocus) \
|
||||||
and (self._lastCommandWasCaretNav or self._lastCommandWasStructNav) \
|
and (self._lastCommandWasCaretNav or self._lastCommandWasStructNav) \
|
||||||
and not (AXUtilities.is_table_cell(newFocus) and AXObject.get_name(newFocus)):
|
and not (AXUtilities.is_table_cell(newFocus) and AXObject.get_name(newFocus)):
|
||||||
tokens = ["WEB: New focus", newFocus, "content editable. Generating line."]
|
# Check if we're entering the content editable from outside (e.g. down arrow
|
||||||
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
# from a message list into a message entry). In that case, generate full object
|
||||||
contents = self.utilities.getLineContentsAtOffset(newFocus, caretOffset)
|
# speech (with label and role) rather than just line contents.
|
||||||
|
enteredFromOutside = oldFocus is not None \
|
||||||
|
and oldFocus != newFocus \
|
||||||
|
and not AXObject.find_ancestor(oldFocus, lambda x: x == newFocus)
|
||||||
|
if enteredFromOutside:
|
||||||
|
tokens = ["WEB: New focus", newFocus,
|
||||||
|
"content editable entered from outside. Generating speech."]
|
||||||
|
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||||
|
args['priorObj'] = oldFocus
|
||||||
|
else:
|
||||||
|
tokens = ["WEB: New focus", newFocus, "content editable. Generating line."]
|
||||||
|
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||||
|
contents = self.utilities.getLineContentsAtOffset(newFocus, caretOffset)
|
||||||
elif self.utilities.isAnchor(newFocus):
|
elif self.utilities.isAnchor(newFocus):
|
||||||
tokens = ["WEB: New focus", newFocus, "is anchor. Generating line."]
|
tokens = ["WEB: New focus", newFocus, "is anchor. Generating line."]
|
||||||
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||||
|
|||||||
@@ -1834,6 +1834,16 @@ class Utilities(script_utilities.Utilities):
|
|||||||
prevObj, pOffset = self.findPreviousCaretInOrder(firstObj, firstStart)
|
prevObj, pOffset = self.findPreviousCaretInOrder(firstObj, firstStart)
|
||||||
nextObj, nOffset = self.findNextCaretInOrder(lastObj, lastEnd - 1)
|
nextObj, nOffset = self.findNextCaretInOrder(lastObj, lastEnd - 1)
|
||||||
|
|
||||||
|
# If we're inside a content editable, don't expand line contents beyond
|
||||||
|
# its boundaries (e.g. don't include a "More options" button adjacent to
|
||||||
|
# a message entry just because it's on the same visual line).
|
||||||
|
contentEditableBoundary = None
|
||||||
|
if self.isContentEditableWithEmbeddedObjects(obj):
|
||||||
|
contentEditableBoundary = obj
|
||||||
|
else:
|
||||||
|
contentEditableBoundary = AXObject.find_ancestor(
|
||||||
|
obj, self.isContentEditableWithEmbeddedObjects)
|
||||||
|
|
||||||
# Check for things on the same line to the left of this object.
|
# Check for things on the same line to the left of this object.
|
||||||
prevStartTime = time.time()
|
prevStartTime = time.time()
|
||||||
while prevObj and self.getDocumentForObject(prevObj) == document:
|
while prevObj and self.getDocumentForObject(prevObj) == document:
|
||||||
@@ -1848,6 +1858,10 @@ class Utilities(script_utilities.Utilities):
|
|||||||
if objRow != AXObject.find_ancestor(prevObj, AXUtilities.is_table_row):
|
if objRow != AXObject.find_ancestor(prevObj, AXUtilities.is_table_row):
|
||||||
break
|
break
|
||||||
|
|
||||||
|
if contentEditableBoundary and prevObj != contentEditableBoundary \
|
||||||
|
and not AXObject.find_ancestor(prevObj, lambda x: x == contentEditableBoundary):
|
||||||
|
break
|
||||||
|
|
||||||
onLeft = self._getContentsForObj(prevObj, pOffset, boundary)
|
onLeft = self._getContentsForObj(prevObj, pOffset, boundary)
|
||||||
onLeft = list(filter(_include, onLeft))
|
onLeft = list(filter(_include, onLeft))
|
||||||
if not onLeft:
|
if not onLeft:
|
||||||
@@ -1878,6 +1892,10 @@ class Utilities(script_utilities.Utilities):
|
|||||||
if objRow != AXObject.find_ancestor(nextObj, AXUtilities.is_table_row):
|
if objRow != AXObject.find_ancestor(nextObj, AXUtilities.is_table_row):
|
||||||
break
|
break
|
||||||
|
|
||||||
|
if contentEditableBoundary and nextObj != contentEditableBoundary \
|
||||||
|
and not AXObject.find_ancestor(nextObj, lambda x: x == contentEditableBoundary):
|
||||||
|
break
|
||||||
|
|
||||||
onRight = self._getContentsForObj(nextObj, nOffset, boundary)
|
onRight = self._getContentsForObj(nextObj, nOffset, boundary)
|
||||||
if onRight and self._contentIsSubsetOf(objects[0], onRight[-1]):
|
if onRight and self._contentIsSubsetOf(objects[0], onRight[-1]):
|
||||||
onRight = onRight[0:-1]
|
onRight = onRight[0:-1]
|
||||||
|
|||||||
@@ -361,7 +361,13 @@ class SpeechGenerator(speech_generator.SpeechGenerator):
|
|||||||
if self._script.utilities.isContentEditableWithEmbeddedObjects(obj) \
|
if self._script.utilities.isContentEditableWithEmbeddedObjects(obj) \
|
||||||
or self._script.utilities.isDocument(obj):
|
or self._script.utilities.isDocument(obj):
|
||||||
if input_event_manager.get_manager().last_event_was_caret_navigation():
|
if input_event_manager.get_manager().last_event_was_caret_navigation():
|
||||||
return []
|
# Still generate the label if we just entered this object from outside
|
||||||
|
# (e.g. down arrow from message list into message entry in Discord).
|
||||||
|
enteredFromOutside = priorObj is not None \
|
||||||
|
and priorObj != obj \
|
||||||
|
and not AXObject.find_ancestor(priorObj, lambda x: x == obj)
|
||||||
|
if not enteredFromOutside:
|
||||||
|
return []
|
||||||
|
|
||||||
if AXUtilities.is_page_tab(priorObj) and AXObject.get_name(priorObj) == objName:
|
if AXUtilities.is_page_tab(priorObj) and AXObject.get_name(priorObj) == objName:
|
||||||
return []
|
return []
|
||||||
@@ -554,6 +560,9 @@ class SpeechGenerator(speech_generator.SpeechGenerator):
|
|||||||
if roledescription:
|
if roledescription:
|
||||||
result = [roledescription]
|
result = [roledescription]
|
||||||
result.extend(self.voice(speech_generator.SYSTEM, obj=obj, **args))
|
result.extend(self.voice(speech_generator.SYSTEM, obj=obj, **args))
|
||||||
|
# aria-roledescription replaces the standard role name, so return
|
||||||
|
# early to avoid announcing both (e.g. "Message" + "article").
|
||||||
|
return result
|
||||||
|
|
||||||
role = args.get('role', AXObject.get_role(obj))
|
role = args.get('role', AXObject.get_role(obj))
|
||||||
roleSoundPresentation = cthulhu.cthulhuApp.settingsManager.getSetting('roleSoundPresentation')
|
roleSoundPresentation = cthulhu.cthulhuApp.settingsManager.getSetting('roleSoundPresentation')
|
||||||
|
|||||||
@@ -36,11 +36,11 @@ __license__ = "LGPL"
|
|||||||
|
|
||||||
import copy
|
import copy
|
||||||
import importlib
|
import importlib
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
|
||||||
|
|
||||||
from gi.repository import Gio, GLib
|
from gi.repository import Gio, GLib
|
||||||
|
from tomlkit import parse
|
||||||
|
|
||||||
from . import debug
|
from . import debug
|
||||||
from . import cthulhu_i18n
|
from . import cthulhu_i18n
|
||||||
@@ -62,7 +62,7 @@ class SettingsManager(object):
|
|||||||
"""Settings backend manager. This class manages cthulhu user's settings
|
"""Settings backend manager. This class manages cthulhu user's settings
|
||||||
using different backends"""
|
using different backends"""
|
||||||
|
|
||||||
def __init__(self, app: Cthulhu, backend: str = 'json') -> None: # Modified signature
|
def __init__(self, app: Cthulhu, backend: str = 'toml') -> None: # Modified signature
|
||||||
|
|
||||||
debug.printMessage(debug.LEVEL_INFO, 'SETTINGS MANAGER: Initializing', True)
|
debug.printMessage(debug.LEVEL_INFO, 'SETTINGS MANAGER: Initializing', True)
|
||||||
self.app: Cthulhu = app # Store app instance
|
self.app: Cthulhu = app # Store app instance
|
||||||
@@ -214,6 +214,25 @@ class SettingsManager(object):
|
|||||||
if not os.path.exists(userCustomFile):
|
if not os.path.exists(userCustomFile):
|
||||||
os.close(os.open(userCustomFile, os.O_CREAT, 0o700))
|
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:
|
if self.isFirstStart() and self._backend:
|
||||||
self._backend.saveDefaultSettings(self.defaultGeneral,
|
self._backend.saveDefaultSettings(self.defaultGeneral,
|
||||||
self.defaultPronunciations,
|
self.defaultPronunciations,
|
||||||
@@ -259,13 +278,13 @@ class SettingsManager(object):
|
|||||||
if not self._prefsDir:
|
if not self._prefsDir:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
settings_path = os.path.join(self._prefsDir, "user-settings.conf")
|
settings_path = os.path.join(self._prefsDir, "user-settings.toml")
|
||||||
if not os.path.exists(settings_path):
|
if not os.path.exists(settings_path):
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(settings_path, "r", encoding="utf-8") as settings_file:
|
with open(settings_path, "r", encoding="utf-8") as settings_file:
|
||||||
prefs = json.load(settings_file)
|
prefs = parse(settings_file.read())
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
msg = f"SETTINGS MANAGER: Unable to read default settings from {settings_path}: {error}"
|
msg = f"SETTINGS MANAGER: Unable to read default settings from {settings_path}: {error}"
|
||||||
debug.printMessage(debug.LEVEL_WARNING, msg, True)
|
debug.printMessage(debug.LEVEL_WARNING, msg, True)
|
||||||
@@ -572,9 +591,10 @@ class SettingsManager(object):
|
|||||||
"""Return the current general settings.
|
"""Return the current general settings.
|
||||||
Those settings comes from updating the default settings
|
Those settings comes from updating the default settings
|
||||||
with the profiles' ones"""
|
with the profiles' ones"""
|
||||||
|
general = self.defaultGeneral.copy()
|
||||||
if self._backend:
|
if self._backend:
|
||||||
return self._backend.getGeneral(profile)
|
general.update(self._backend.getGeneral(profile))
|
||||||
return {}
|
return general
|
||||||
|
|
||||||
def getPronunciations(self, profile: str = 'default') -> Dict[str, Any]:
|
def getPronunciations(self, profile: str = 'default') -> Dict[str, Any]:
|
||||||
"""Return the current pronunciations settings.
|
"""Return the current pronunciations settings.
|
||||||
@@ -821,4 +841,3 @@ def getManager() -> Optional[SettingsManager]:
|
|||||||
# During import phase, cthulhuApp may not exist yet
|
# During import phase, cthulhuApp may not exist yet
|
||||||
pass
|
pass
|
||||||
return _managerInstance
|
return _managerInstance
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,9 @@ __copyright__ = "Copyright (c) 2024 Stormux"
|
|||||||
__license__ = "LGPL"
|
__license__ = "LGPL"
|
||||||
|
|
||||||
import time
|
import time
|
||||||
|
import os
|
||||||
from gi.repository import GLib
|
from gi.repository import GLib
|
||||||
|
from tomlkit import parse
|
||||||
import cthulhu.braille as braille
|
import cthulhu.braille as braille
|
||||||
import cthulhu.cmdnames as cmdnames
|
import cthulhu.cmdnames as cmdnames
|
||||||
import cthulhu.debug as debug
|
import cthulhu.debug as debug
|
||||||
@@ -47,7 +49,12 @@ class SleepModeManager:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._handlers = self.getHandlers(True)
|
self._handlers = self.getHandlers(True)
|
||||||
self._bindings = keybindings.KeyBindings()
|
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._lastToggleTime = 0
|
||||||
self._toggleDebounceDelay = 0.1 # 100ms debounce (reduced for better responsiveness)
|
self._toggleDebounceDelay = 0.1 # 100ms debounce (reduced for better responsiveness)
|
||||||
|
|
||||||
@@ -76,12 +83,106 @@ class SleepModeManager:
|
|||||||
def isActiveForApp(self, app):
|
def isActiveForApp(self, app):
|
||||||
"""Returns True if sleep mode is active for 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:
|
if result:
|
||||||
tokens = ["SLEEP MODE MANAGER: Is active for", app]
|
tokens = ["SLEEP MODE MANAGER: Is active for", app]
|
||||||
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||||
return result
|
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):
|
def _setupHandlers(self):
|
||||||
"""Sets up and returns the sleep-mode-manager input event handlers."""
|
"""Sets up and returns the sleep-mode-manager input event handlers."""
|
||||||
|
|
||||||
@@ -132,12 +233,16 @@ class SleepModeManager:
|
|||||||
if not (script and script.app):
|
if not (script and script.app):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
from . import cthulhu_state
|
self.refreshAutoSleepConfig()
|
||||||
|
|
||||||
scriptManager = script_manager.get_manager()
|
scriptManager = script_manager.get_manager()
|
||||||
|
|
||||||
if self.isActiveForApp(script.app):
|
if self.isActiveForApp(script.app):
|
||||||
# Turning OFF sleep mode
|
# 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)
|
newScript = scriptManager.get_script(script.app)
|
||||||
if notifyUser:
|
if notifyUser:
|
||||||
newScript.presentMessage(
|
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: Active script set successfully", True)
|
||||||
|
|
||||||
debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Adding app to sleep list", 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)
|
debug.printMessage(debug.LEVEL_INFO, f"SLEEP MODE: Enabled for {AXObject.get_name(script.app)} (delayed)", True)
|
||||||
# Reset debounce timer after successful toggle
|
# Reset debounce timer after successful toggle
|
||||||
self._lastToggleTime = 0
|
self._lastToggleTime = 0
|
||||||
|
|||||||
@@ -215,16 +215,37 @@ class SoundThemeManager:
|
|||||||
def getSoundPath(self, themeName, soundName):
|
def getSoundPath(self, themeName, soundName):
|
||||||
"""Get path to a specific sound file.
|
"""Get path to a specific sound file.
|
||||||
|
|
||||||
Checks for common audio file extensions: .wav, .ogg, .mp3, .flac
|
Checks for common audio file extensions: .wav, .ogg, .mp3, .flac.
|
||||||
|
User themes are checked first, then system themes as fallback so
|
||||||
|
partial user themes can inherit missing sounds.
|
||||||
"""
|
"""
|
||||||
themePath = self.getThemePath(themeName)
|
themePaths = []
|
||||||
if not themePath:
|
|
||||||
|
userPath = os.path.join(self.getUserSoundsDir(), themeName)
|
||||||
|
if os.path.isdir(userPath):
|
||||||
|
themePaths.append(userPath)
|
||||||
|
|
||||||
|
systemPath = os.path.join(self.getSystemSoundsDir(), themeName)
|
||||||
|
if os.path.isdir(systemPath):
|
||||||
|
themePaths.append(systemPath)
|
||||||
|
|
||||||
|
fallbackDirs = [
|
||||||
|
'/usr/share/cthulhu/sounds',
|
||||||
|
'/usr/local/share/cthulhu/sounds',
|
||||||
|
]
|
||||||
|
for fallbackDir in fallbackDirs:
|
||||||
|
fallbackPath = os.path.join(fallbackDir, themeName)
|
||||||
|
if os.path.isdir(fallbackPath) and fallbackPath not in themePaths:
|
||||||
|
themePaths.append(fallbackPath)
|
||||||
|
|
||||||
|
if not themePaths:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
for ext in ['.wav', '.ogg', '.mp3', '.flac']:
|
for themePath in themePaths:
|
||||||
soundPath = os.path.join(themePath, soundName + ext)
|
for ext in ['.wav', '.ogg', '.mp3', '.flac']:
|
||||||
if os.path.isfile(soundPath):
|
soundPath = os.path.join(themePath, soundName + ext)
|
||||||
return soundPath
|
if os.path.isfile(soundPath):
|
||||||
|
return soundPath
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -395,4 +416,3 @@ def getManager():
|
|||||||
from . import cthulhu
|
from . import cthulhu
|
||||||
_manager = SoundThemeManager(cthulhu.cthulhuApp)
|
_manager = SoundThemeManager(cthulhu.cthulhuApp)
|
||||||
return _manager
|
return _manager
|
||||||
|
|
||||||
|
|||||||
@@ -748,15 +748,20 @@ class SpeechGenerator(generator.Generator):
|
|||||||
method for scripts to call.
|
method for scripts to call.
|
||||||
"""
|
"""
|
||||||
generated = self._generateRoleName(obj, **args)
|
generated = self._generateRoleName(obj, **args)
|
||||||
if generated:
|
return self._getFirstString(generated)
|
||||||
return generated[0]
|
|
||||||
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def getName(self, obj, **args):
|
def getName(self, obj, **args):
|
||||||
generated = self._generateName(obj, **args)
|
generated = self._generateName(obj, **args)
|
||||||
if generated:
|
return self._getFirstString(generated)
|
||||||
return generated[0]
|
|
||||||
|
def _getFirstString(self, generated):
|
||||||
|
for item in generated or []:
|
||||||
|
if isinstance(item, str):
|
||||||
|
return item
|
||||||
|
if isinstance(item, list):
|
||||||
|
nestedString = self._getFirstString(item)
|
||||||
|
if nestedString:
|
||||||
|
return nestedString
|
||||||
|
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user