From f873fcee11b9f4c6fe394e0f670e0961b515c04c Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Fri, 13 Feb 2026 11:13:38 -0500 Subject: [PATCH] Use toml instead of json for settings. --- distro-packages/Arch-Linux/PKGBUILD | 1 + docs/LOGGING.md | 46 +++++ docs/man/cthulhu.1 | 2 +- pyproject.toml | 1 + src/cthulhu/backends/meson.build | 4 +- .../{json_backend.py => toml_backend.py} | 186 ++++++++++++------ src/cthulhu/cthulhu_gui_prefs.py | 5 + src/cthulhu/plugins/OCR/README.md | 24 ++- src/cthulhu/settings_manager.py | 14 +- 9 files changed, 201 insertions(+), 82 deletions(-) create mode 100644 docs/LOGGING.md rename src/cthulhu/backends/{json_backend.py => toml_backend.py} (59%) diff --git a/distro-packages/Arch-Linux/PKGBUILD b/distro-packages/Arch-Linux/PKGBUILD index 1eaa08f..49506a5 100644 --- a/distro-packages/Arch-Linux/PKGBUILD +++ b/distro-packages/Arch-Linux/PKGBUILD @@ -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) diff --git a/docs/LOGGING.md b/docs/LOGGING.md new file mode 100644 index 0000000..be8db6a --- /dev/null +++ b/docs/LOGGING.md @@ -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) +``` diff --git a/docs/man/cthulhu.1 b/docs/man/cthulhu.1 index dc3fc48..b66f9dd 100644 --- a/docs/man/cthulhu.1 +++ b/docs/man/cthulhu.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 diff --git a/pyproject.toml b/pyproject.toml index ffac654..9527f5b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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'", diff --git a/src/cthulhu/backends/meson.build b/src/cthulhu/backends/meson.build index fed0017..f8a2b53 100644 --- a/src/cthulhu/backends/meson.build +++ b/src/cthulhu/backends/meson.build @@ -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' -) \ No newline at end of file +) diff --git a/src/cthulhu/backends/json_backend.py b/src/cthulhu/backends/toml_backend.py similarity index 59% rename from src/cthulhu/backends/json_backend.py rename to src/cthulhu/backends/toml_backend.py index f1e9d1a..400c6dd 100644 --- a/src/cthulhu/backends/json_backend.py +++ b/src/cthulhu/backends/toml_backend.py @@ -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) diff --git a/src/cthulhu/cthulhu_gui_prefs.py b/src/cthulhu/cthulhu_gui_prefs.py index 1273a49..843b694 100644 --- a/src/cthulhu/cthulhu_gui_prefs.py +++ b/src/cthulhu/cthulhu_gui_prefs.py @@ -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"]) diff --git a/src/cthulhu/plugins/OCR/README.md b/src/cthulhu/plugins/OCR/README.md index e88b580..8378d1b 100644 --- a/src/cthulhu/plugins/OCR/README.md +++ b/src/cthulhu/plugins/OCR/README.md @@ -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/.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 diff --git a/src/cthulhu/settings_manager.py b/src/cthulhu/settings_manager.py index 3c6e1bb..1d9d3a9 100644 --- a/src/cthulhu/settings_manager.py +++ b/src/cthulhu/settings_manager.py @@ -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 -