Use toml instead of json for settings.
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
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
|
||||
|
||||
@@ -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'",
|
||||
|
||||
@@ -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)
|
||||
@@ -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"])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user