From 1208c919a1e9fccbef539de938f562e3db854322 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Fri, 7 Aug 2026 03:22:26 -0400 Subject: [PATCH] [ADD] todo: preferences and Configuration menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stores the per-user settings in ~/.erplibre/todo_prefs.json: which deploy interface to use, what to display while deploying, which migration interface. A missing or corrupt file reads as defaults. --- FR --- Range les réglages propres à l'utilisateur dans ~/.erplibre/todo_prefs.json : interface de déploiement, affichage pendant le déploiement, interface de migration. Un fichier absent ou corrompu se lit comme les valeurs par défaut. Assisted-by: Claude Opus 4.8 --- script/todo/todo.py | 91 ++++++++++++++++++++++++++++++++++ script/todo/todo_i18n.py | 101 ++++++++++++++++++++++++++++++++++++++ script/todo/todo_prefs.py | 74 ++++++++++++++++++++++++++++ 3 files changed, 266 insertions(+) create mode 100644 script/todo/todo_prefs.py diff --git a/script/todo/todo.py b/script/todo/todo.py index 6e4275d..7c9733e 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -27,6 +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 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" @@ -447,6 +449,79 @@ class TODO: ), } + 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')})" + ) + }, + { + "prompt_description": ( + f"{t('Odoo migration interface')} " + f"({self._pref_label('migration_ui')})" + ) + }, + {"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": + self._pref_edit("migration_ui") + elif status == "5": + 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 @@ -1149,6 +1224,22 @@ class TODO: # serait prématurée. Elle est posée à la première identité refusée. self._qemu_ssh_walk(roots, max_depth) + def _qemu_pick_domains(self): + """Fait choisir des VM parmi celles définies. Vide = toutes.""" + names = self._qemu_list_domains() + if not names: + print(t("No VM found.")) + return [] + for i, name in enumerate(names, 1): + print(f" [{i}] {name}") + raw = input( + t("Which VMs? (numbers, comma-separated; blank = all): ") + ).strip() + if not raw: + return names + chosen = self._parse_index_selection(raw, names) + return chosen or names + def _ssh_ensure_key(self): """Chemin de la clé PUBLIQUE, générée si aucune n'existe. diff --git a/script/todo/todo_i18n.py b/script/todo/todo_i18n.py index 55d558f..951439b 100644 --- a/script/todo/todo_i18n.py +++ b/script/todo/todo_i18n.py @@ -1246,6 +1246,54 @@ 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):", + }, "SSH port forwarding (open Odoo in the browser)": { "fr": "🔌 Redirection de port SSH (ouvrir Odoo dans le navigateur)", "en": "🔌 SSH port forwarding (open Odoo in the browser)", @@ -1351,6 +1399,14 @@ TRANSLATIONS = { "fr": "Tunnel refermé.", "en": "Tunnel closed.", }, + "SSH configuration (~/.ssh/config, ProxyJump)": { + "fr": "🔑 Configuration SSH (~/.ssh/config, ProxyJump)", + "en": "🔑 SSH configuration (~/.ssh/config, ProxyJump)", + }, + "SSH configuration for QEMU VMs": { + "fr": "Configuration SSH des VM QEMU", + "en": "SSH configuration for QEMU VMs", + }, "Where should the machines come from?": { "fr": "D'où viennent les machines à configurer ?", "en": "Where should the machines come from?", @@ -1392,6 +1448,35 @@ TRANSLATIONS = { "fr": "Profondeur (1 = ces machines seulement, défaut :", "en": "Depth (1 = these machines only, default:", }, + "Which VMs? (numbers, comma-separated; blank = all): ": { + "fr": "Quelles VM ? (numéros séparés par des virgules ; " + "vide = toutes) : ", + "en": "Which VMs? (numbers, comma-separated; blank = all): ", + }, + "Depth (default:": { + "fr": "Profondeur (défaut :", + "en": "Depth (default:", + }, + "entry": { + "fr": "entrée", + "en": "entry", + }, + "written": { + "fr": "écrite(s)", + "en": "written", + }, + "skipped": { + "fr": "ignorée(s)", + "en": "skipped", + }, + "Level": { + "fr": "Niveau", + "en": "Level", + }, + "machines to probe": { + "fr": "machines à sonder", + "en": "machines to probe", + }, "SSH refused the identity.": { "fr": "SSH a refusé l'identité.", "en": "SSH refused the identity.", @@ -1489,6 +1574,22 @@ TRANSLATIONS = { "fr": "les noms s'appliquent à chaud", "en": "the names apply live", }, + "SSH hosts written": { + "fr": "Hôtes SSH écrits", + "en": "SSH hosts written", + }, + "via": { + "fr": "via", + "en": "via", + }, + "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", + }, "Interface:": { "fr": "Interface :", "en": "Interface:", diff --git a/script/todo/todo_prefs.py b/script/todo/todo_prefs.py new file mode 100644 index 0000000..9273c24 --- /dev/null +++ b/script/todo/todo_prefs.py @@ -0,0 +1,74 @@ +#!/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", + # Interface de la migration Odoo : "ask" / "tui" / "cli". + "migration_ui": "ask", +} + + +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