Use toml instead of json for settings.

This commit is contained in:
Storm Dragon
2026-02-13 11:13:38 -05:00
parent 13976b7235
commit f873fcee11
9 changed files with 201 additions and 82 deletions
+1
View File
@@ -27,6 +27,7 @@ depends=(
# Plugin system and D-Bus remote control
python-pluggy
python-tomlkit
python-dasbus
# AI Assistant dependencies (for screenshots, HTTP requests, and actions)
+46
View File
@@ -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
View File
@@ -290,7 +290,7 @@ toggle the reading of tables, either by single cell or whole row.
.B Cthulhu
user preferences directory
.TP
.BI ~/.local/share/cthulhu/user-settings.conf
.BI ~/.local/share/cthulhu/user-settings.toml
.B Cthulhu
user preferences configuration file.
.TP
+1
View File
@@ -12,6 +12,7 @@ license = { text = "LGPL-2.1-or-later" }
dependencies = [
"pygobject>=3.18",
"pluggy",
"tomlkit",
"brlapi; extra == 'braille'",
"python-speechd; extra == 'speech'",
"piper-tts; extra == 'piper'",
+2 -2
View File
@@ -1,9 +1,9 @@
backends_python_sources = files([
'__init__.py',
'json_backend.py',
'toml_backend.py',
])
python3.install_sources(
backends_python_sources,
subdir: 'cthulhu/backends'
)
)
@@ -23,7 +23,7 @@
# Forked from Orca screen reader.
# Cthulhu project: https://git.stormux.org/storm/cthulhu
"""JSON backend for Cthulhu settings"""
"""TOML backend for Cthulhu settings"""
__id__ = "$Id$"
__version__ = "$Revision$"
@@ -31,20 +31,22 @@ __date__ = "$Date$"
__copyright__ = "Copyright (c) 2010-2011 Consorcio Fernando de los Rios."
__license__ = "LGPL"
from json import load, dump
import os
from tomlkit import parse, dumps, document
from cthulhu import settings, acss
class Backend:
def __init__(self, prefsDir):
""" Initialize the JSON Backend.
"""
""" Initialize the TOML Backend.
"""
self.general = {}
self.pronunciations = {}
self.keybindings = {}
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._defaultProfiles = {'default': { 'profile': settings.profile,
@@ -53,75 +55,137 @@ 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):
""" Save default settings for all the properties from
cthulhu.settings. """
prefs = {'general': general,
'profiles': self._defaultProfiles,
'pronunciations': pronunciations,
'keybindings': keybindings}
prefs = {'general': self._stripNone(general),
'profiles': self._stripNone(self._defaultProfiles),
'pronunciations': self._stripNone(pronunciations),
'keybindings': self._stripNone(keybindings)}
self.general = general
self.profiles = self._defaultProfiles
self.pronunciations = pronunciations
self.keybindings = keybindings
settingsFile = open(self.settingsFile, 'w')
dump(prefs, settingsFile, indent=4)
settingsFile.close()
prefsDoc = document()
prefsDoc['general'] = prefs['general']
prefsDoc['profiles'] = prefs['profiles']
prefsDoc['pronunciations'] = prefs['pronunciations']
prefsDoc['keybindings'] = prefs['keybindings']
self._writeDocument(self.settingsFile, prefsDoc)
def getAppSettings(self, appName):
fileName = os.path.join(self.appPrefsDir, f"{appName}.conf")
if os.path.exists(fileName):
settingsFile = open(fileName, 'r')
prefs = load(settingsFile)
settingsFile.close()
else:
prefs = {}
return prefs
fileName = os.path.join(self.appPrefsDir, f"{appName}.toml")
return self._readDocument(fileName)
def saveAppSettings(self, appName, profile, general, pronunciations, keybindings):
prefs = self.getAppSettings(appName)
profiles = prefs.get('profiles', {})
profiles[profile] = {'general': general,
'pronunciations': pronunciations,
'keybindings': keybindings}
prefs['profiles'] = profiles
prefsDoc = self.getAppSettings(appName)
profiles = prefsDoc.get('profiles')
if profiles is None or not isinstance(profiles, dict):
prefsDoc['profiles'] = {}
profiles = prefsDoc['profiles']
fileName = os.path.join(self.appPrefsDir, f"{appName}.conf")
settingsFile = open(fileName, 'w')
dump(prefs, settingsFile, indent=4)
settingsFile.close()
profileTable = profiles.get(profile)
if profileTable is None or not isinstance(profileTable, dict):
profiles[profile] = {}
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,
pronunciations, keybindings):
""" Save minimal subset defined in the profile against current
""" Save minimal subset defined in the profile against current
defaults. """
if profile is None:
profile = 'default'
general['pronunciations'] = pronunciations
general['keybindings'] = keybindings
general = self._stripNone(general)
with open(self.settingsFile, 'r+') as settingsFile:
prefs = load(settingsFile)
prefs['profiles'][profile] = general
settingsFile.seek(0)
settingsFile.truncate()
dump(prefs, settingsFile, indent=4)
prefsDoc = self._readDocument(self.settingsFile)
profiles = prefsDoc.get('profiles')
if profiles is None or not isinstance(profiles, dict):
prefsDoc['profiles'] = {}
profiles = prefsDoc['profiles']
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):
""" Load from config file all settings """
settingsFile = open(self.settingsFile)
prefsDoc = self._readDocument(self.settingsFile)
try:
prefs = load(settingsFile)
except ValueError:
self.general = dict(prefsDoc.get('general', {}))
self.pronunciations = dict(prefsDoc.get('pronunciations', {}))
self.keybindings = dict(prefsDoc.get('keybindings', {}))
self.profiles = dict(prefsDoc.get('profiles', {}))
except Exception:
return
self.general = prefs['general'].copy()
self.pronunciations = prefs['pronunciations']
self.keybindings = prefs['keybindings']
self.profiles = prefs['profiles'].copy()
def _migrateSettings(self, settingsDict):
"""Migrate old setting names to new ones."""
@@ -195,18 +259,24 @@ class Backend:
def isFirstStart(self):
""" Check if we're in first start. """
return not os.path.exists(self.settingsFile)
def _setProfileKey(self, key, value):
self.general[key] = value
with open(self.settingsFile, 'r+') as settingsFile:
prefs = load(settingsFile)
prefs['general'][key] = value
settingsFile.seek(0)
settingsFile.truncate()
dump(prefs, settingsFile, indent=4)
prefsDoc = self._readDocument(self.settingsFile)
general = prefsDoc.get('general')
if general is None or not isinstance(general, dict):
prefsDoc['general'] = {}
general = prefsDoc['general']
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):
"""Set firstStart. This user-configurable setting is primarily
@@ -238,10 +308,8 @@ class Backend:
if profile in self.profiles:
removeProfileFrom(self.profiles)
with open(self.settingsFile, 'r+') as settingsFile:
prefs = load(settingsFile)
if profile in prefs['profiles']:
removeProfileFrom(prefs['profiles'])
settingsFile.seek(0)
settingsFile.truncate()
dump(prefs, settingsFile, indent=4)
prefsDoc = self._readDocument(self.settingsFile)
profiles = prefsDoc.get('profiles')
if isinstance(profiles, dict) and profile in profiles:
removeProfileFrom(profiles)
self._writeDocument(self.settingsFile, prefsDoc)
+5
View File
@@ -180,6 +180,8 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
self.savedGain = None
self.savedPitch = None
self.savedRate = None
self.soundThemeCombo = None
self.roleSoundPresentationCombo = None
self._isInitialSetup = False
self.selectedFamilyChoices = {}
self.selectedLanguageChoices = {}
@@ -2671,6 +2673,8 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
# Get widget references
self.soundThemeCombo = self.get_widget("soundThemeCombo")
self.roleSoundPresentationCombo = self.get_widget("roleSoundPresentationCombo")
self.soundThemeCombo.set_can_focus(False)
self.roleSoundPresentationCombo.set_can_focus(False)
# Populate sound theme combo box
themeManager = sound_theme_manager.getManager()
@@ -2728,6 +2732,7 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
value = self._roleSoundPresentationChoices[activeIndex][0]
self.prefsDict["roleSoundPresentation"] = value
def _updateCthulhuModifier(self):
combobox = self.get_widget("cthulhuModifierComboBox")
keystring = ", ".join(self.prefsDict["cthulhuModifierKeys"])
+11 -13
View File
@@ -140,21 +140,19 @@ Access comprehensive OCR settings through Cthulhu Preferences:
### Configuration File
Settings are automatically stored in Cthulhu's configuration system:
- **Global Settings**: `~/.local/share/cthulhu/user-settings.conf`
- **Profile Settings**: `~/.local/share/cthulhu/app-settings/[profile]/`
- **Global Settings**: `~/.local/share/cthulhu/user-settings.toml`
- **Application Settings**: `~/.local/share/cthulhu/app-settings/<app>.toml`
### Example Configuration Values
```json
{
"ocrLanguageCode": "eng",
"ocrScaleFactor": 3,
"ocrGrayscaleImg": false,
"ocrInvertImg": false,
"ocrBlackWhiteImg": false,
"ocrBlackWhiteImgValue": 200,
"ocrColorCalculation": false,
"ocrCopyToClipboard": true
}
```toml
ocrLanguageCode = "eng"
ocrScaleFactor = 3
ocrGrayscaleImg = false
ocrInvertImg = false
ocrBlackWhiteImg = false
ocrBlackWhiteImgValue = 200
ocrColorCalculation = false
ocrCopyToClipboard = true
```
## Troubleshooting
+7 -7
View File
@@ -36,11 +36,11 @@ __license__ = "LGPL"
import copy
import importlib
import json
import os
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
from gi.repository import Gio, GLib
from tomlkit import parse
from . import debug
from . import cthulhu_i18n
@@ -62,7 +62,7 @@ class SettingsManager(object):
"""Settings backend manager. This class manages cthulhu user's settings
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)
self.app: Cthulhu = app # Store app instance
@@ -259,13 +259,13 @@ class SettingsManager(object):
if not self._prefsDir:
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):
return {}
try:
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:
msg = f"SETTINGS MANAGER: Unable to read default settings from {settings_path}: {error}"
debug.printMessage(debug.LEVEL_WARNING, msg, True)
@@ -572,9 +572,10 @@ class SettingsManager(object):
"""Return the current general settings.
Those settings comes from updating the default settings
with the profiles' ones"""
general = self.defaultGeneral.copy()
if self._backend:
return self._backend.getGeneral(profile)
return {}
general.update(self._backend.getGeneral(profile))
return general
def getPronunciations(self, profile: str = 'default') -> Dict[str, Any]:
"""Return the current pronunciations settings.
@@ -821,4 +822,3 @@ def getManager() -> Optional[SettingsManager]:
# During import phase, cthulhuApp may not exist yet
pass
return _managerInstance