From b8582f5bc6ddaec1000df92f35c8efc95269eed5 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Sat, 1 Aug 2026 05:59:12 -0400 Subject: [PATCH] [ADD] todo: persistent preferences and a Configuration menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nothing in the CLI could be remembered from one session to the next except the language, which lives in env_var.sh with its own ad-hoc parser. Entry [6] Configuration, below Telemetry, now groups the settings that belong to the USER rather than to the repository. todo_prefs.py stores them in ~/.erplibre/todo_prefs.json — the same place and the same best-effort shape as todo_telemetry.py, since both are per-user and per-machine, and neither must ever prevent the CLI from starting. A DEFAULTS table gives every known key its fallback, so a missing or corrupt file simply reads as defaults. The two keys added here prepare the QEMU deploy form: which interface to use (ask / TUI / classic) and what to display while deploying (CLI output or TUI). They are declared once in _PREF_CHOICES — the screen, the current-value label and the editor all derive from that single table, so adding a preference is one entry, not three edits. Language keeps its own mechanism: it is read before the preferences file exists and is consumed by shell scripts too. Verified against a temporary HOME: defaults returned with no file on disk, a change persisted and reflected in the menu, and reset falling back to defaults. Co-Authored-By: Claude Opus 4.8 (1M context) --- script/todo/todo.py | 93 ++++++++++++++++++++++++++++++++++++++- script/todo/todo_i18n.py | 56 +++++++++++++++++++++++ script/todo/todo_prefs.py | 72 ++++++++++++++++++++++++++++++ 3 files changed, 220 insertions(+), 1 deletion(-) create mode 100644 script/todo/todo_prefs.py diff --git a/script/todo/todo.py b/script/todo/todo.py index 577455c..2948aae 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -27,7 +27,8 @@ from script.config import config_file from script.execute import execute from script.todo.database_manager import DatabaseManager from script.todo.kdbx_manager import KdbxManager -from script.todo.todo_i18n import lang_is_configured, set_lang, t +from script.todo import todo_prefs +from script.todo.todo_i18n import get_lang, lang_is_configured, set_lang, t from script.todo.version_manager import get_odoo_version ERROR_LOG_PATH = ".erplibre.error.txt" @@ -130,6 +131,7 @@ class TODO: [3] {t("Question")} [4] {t("Fork - Open TODO in a new tab")} [5] {t("Navigation telemetry (TUI)")} +[6] {t("Configuration")} [0] {t("Quit")} """ while True: @@ -163,6 +165,8 @@ class TODO: self.execute.exec_command_live(cmd, source_erplibre=True) elif status == "5": self._todo_telemetry_tui() + elif status == "6": + self.prompt_configuration() # elif status == "3" or status == "install": # print("install") else: @@ -547,6 +551,7 @@ class TODO: "prompt_execute_deploy": "Deploy", "prompt_execute_deploy_ssh": "SSH", "prompt_execute_qemu": "QEMU/KVM", + "prompt_configuration": "Configuration", } def _menu_header(self): @@ -618,6 +623,92 @@ class TODO: if ans.strip().lower() not in ("r", "revenir", "o", "oui", "y"): return + # Préférences éditables depuis le menu Configuration : clé, libellé, et + # valeurs proposées (valeur stockée -> libellé affiché). Une seule table : + # l'écran, la lecture et l'écriture en découlent. + _PREF_CHOICES = { + "qemu_deploy_ui": ( + "QEMU deployment interface", + ( + ("ask", "Ask every time"), + ("tui", "TUI form"), + ("cli", "Classic questions (line by line)"), + ), + ), + "qemu_deploy_progress": ( + "Display while deploying", + ( + ("cli", "CLI output (easy to copy)"), + ("tui", "TUI, collapsible blocks per VM"), + ), + ), + } + + def _pref_label(self, key): + """Libellé traduit de la valeur courante d'une préférence.""" + value = todo_prefs.get(key) + for stored, label in self._PREF_CHOICES[key][1]: + if stored == value: + return t(label) + return str(value) + + def _pref_edit(self, key): + """Fait choisir une valeur parmi celles proposées pour `key`.""" + title, options = self._PREF_CHOICES[key] + current = todo_prefs.get(key) + print(f"\n{t(title)} :") + for i, (stored, label) in enumerate(options, 1): + star = " *" if stored == current else "" + print(f" [{i}] {t(label)}{star}") + sel = input(f"{t('Choice (number, blank = keep):')} ").strip() + try: + idx = int(sel) - 1 + except ValueError: + return + if 0 <= idx < len(options): + todo_prefs.set(key, options[idx][0]) + print(f" ✅ {t(title)} : {self._pref_label(key)}") + + def prompt_configuration(self): + """Réglages persistants de l'utilisateur (~/.erplibre/todo_prefs.json). + La langue vit à part, dans env_var.sh, et garde son propre mécanisme. + """ + while True: + lang = "français" if get_lang() == "fr" else "English" + choices = [ + {"section": t("Interface")}, + {"prompt_description": f"{t('Language / Langue')} ({lang})"}, + { + "prompt_description": ( + f"{t('QEMU deployment interface')} " + f"({self._pref_label('qemu_deploy_ui')})" + ) + }, + { + "prompt_description": ( + f"{t('Display while deploying')} " + f"({self._pref_label('qemu_deploy_progress')})" + ) + }, + {"section": t("Maintenance")}, + {"prompt_description": t("Reset all preferences")}, + ] + status = click.prompt(self.fill_help_info(choices)) + print() + if status == "0": + return + elif status == "1": + self._change_language() + elif status == "2": + self._pref_edit("qemu_deploy_ui") + elif status == "3": + self._pref_edit("qemu_deploy_progress") + elif status == "4": + n = todo_prefs.reset() + print(f"✅ {t('Preferences reset')} ({n})") + else: + print(t("Command not found !")) + def fill_help_info(self, choices): # Une entrée {"section": "..."} affiche un titre de section SANS # consommer de numéro : la numérotation reste continue sur les vraies diff --git a/script/todo/todo_i18n.py b/script/todo/todo_i18n.py index f044b47..aab02af 100644 --- a/script/todo/todo_i18n.py +++ b/script/todo/todo_i18n.py @@ -1351,6 +1351,62 @@ TRANSLATIONS = { "fr": "📊 Télémétrie de navigation (TUI)", "en": "📊 Navigation telemetry (TUI)", }, + "Configuration": { + "fr": "⚙ Configuration", + "en": "⚙ Configuration", + }, + "Interface": { + "fr": "Interface", + "en": "Interface", + }, + "Maintenance": { + "fr": "Maintenance", + "en": "Maintenance", + }, + "Language / Langue": { + "fr": "🌐 Langue / Language", + "en": "🌐 Language / Langue", + }, + "QEMU deployment interface": { + "fr": "🖥 Interface de déploiement QEMU", + "en": "🖥 QEMU deployment interface", + }, + "Display while deploying": { + "fr": "📜 Affichage pendant le déploiement", + "en": "📜 Display while deploying", + }, + "Ask every time": { + "fr": "Demander à chaque fois", + "en": "Ask every time", + }, + "TUI form": { + "fr": "Formulaire TUI", + "en": "TUI form", + }, + "Classic questions (line by line)": { + "fr": "Questions classiques (ligne par ligne)", + "en": "Classic questions (line by line)", + }, + "CLI output (easy to copy)": { + "fr": "Sortie CLI (facile à copier)", + "en": "CLI output (easy to copy)", + }, + "TUI, collapsible blocks per VM": { + "fr": "TUI, blocs repliables par VM", + "en": "TUI, collapsible blocks per VM", + }, + "Choice (number, blank = keep):": { + "fr": "Choix (numéro, vide = garder) :", + "en": "Choice (number, blank = keep):", + }, + "Reset all preferences": { + "fr": "🧹 Réinitialiser toutes les préférences", + "en": "🧹 Reset all preferences", + }, + "Preferences reset": { + "fr": "Préférences réinitialisées", + "en": "Preferences reset", + }, "TODO navigation telemetry": { "fr": "TODO — télémétrie de navigation", "en": "TODO navigation telemetry", diff --git a/script/todo/todo_prefs.py b/script/todo/todo_prefs.py new file mode 100644 index 0000000..3f2992a --- /dev/null +++ b/script/todo/todo_prefs.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +# © 2021-2026 TechnoLibre (http://www.technolibre.ca) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +"""Préférences persistantes du CLI TODO. + +Réglages qui survivent d'une session à l'autre et qui appartiennent à +l'UTILISATEUR, pas au dépôt : ils vivent donc dans ~/.erplibre (comme la +télémétrie de navigation) et non dans un fichier versionné. + +- get(key, default) / set(key, value) : accès unitaire. +- reset() : efface tout et revient aux défauts. + +Tout est best-effort : une préférence illisible ou un disque plein ne doivent +JAMAIS empêcher le CLI de démarrer. +""" +from __future__ import annotations + +import json +import os +from pathlib import Path + +# Clés connues et leur valeur par défaut. Une clé absente de ce dictionnaire +# reste lisible/écrivable, mais n'apparaît pas dans l'écran de configuration. +DEFAULTS = { + # Interface du déploiement QEMU : "ask" pose la question à chaque fois, + # "tui" ouvre le formulaire directement, "cli" garde les invites en ligne. + "qemu_deploy_ui": "ask", + # Affichage pendant le déploiement : "cli" (sortie texte, facile à copier + # depuis le terminal) ou "tui" (blocs repliables + copie OSC 52). + "qemu_deploy_progress": "cli", +} + + +def _path() -> Path: + base = Path(os.path.expanduser("~/.erplibre")) + base.mkdir(parents=True, exist_ok=True) + return base / "todo_prefs.json" + + +def load() -> dict: + try: + data = json.loads(_path().read_text()) + except (OSError, ValueError): + return {} + return data if isinstance(data, dict) else {} + + +def _save(data: dict) -> None: + try: + _path().write_text(json.dumps(data, ensure_ascii=False, indent=2)) + except OSError: + pass + + +def get(key: str, default=None): + """Valeur d'une préférence : fichier, puis DEFAULTS, puis `default`.""" + if default is None: + default = DEFAULTS.get(key) + return load().get(key, default) + + +def set(key: str, value) -> None: # noqa: A001 - API voulue : prefs.set(...) + data = load() + data[key] = value + _save(data) + + +def reset() -> int: + """Efface toutes les préférences. Renvoie le nombre de clés effacées.""" + count = len(load()) + _save({}) + return count