[ADD] todo: persistent preferences and a Configuration menu
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) <noreply@anthropic.com>
This commit is contained in:
parent
b664128a05
commit
b8582f5bc6
3 changed files with 220 additions and 1 deletions
|
|
@ -27,7 +27,8 @@ from script.config import config_file
|
||||||
from script.execute import execute
|
from script.execute import execute
|
||||||
from script.todo.database_manager import DatabaseManager
|
from script.todo.database_manager import DatabaseManager
|
||||||
from script.todo.kdbx_manager import KdbxManager
|
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
|
from script.todo.version_manager import get_odoo_version
|
||||||
|
|
||||||
ERROR_LOG_PATH = ".erplibre.error.txt"
|
ERROR_LOG_PATH = ".erplibre.error.txt"
|
||||||
|
|
@ -130,6 +131,7 @@ class TODO:
|
||||||
[3] {t("Question")}
|
[3] {t("Question")}
|
||||||
[4] {t("Fork - Open TODO in a new tab")}
|
[4] {t("Fork - Open TODO in a new tab")}
|
||||||
[5] {t("Navigation telemetry (TUI)")}
|
[5] {t("Navigation telemetry (TUI)")}
|
||||||
|
[6] {t("Configuration")}
|
||||||
[0] {t("Quit")}
|
[0] {t("Quit")}
|
||||||
"""
|
"""
|
||||||
while True:
|
while True:
|
||||||
|
|
@ -163,6 +165,8 @@ class TODO:
|
||||||
self.execute.exec_command_live(cmd, source_erplibre=True)
|
self.execute.exec_command_live(cmd, source_erplibre=True)
|
||||||
elif status == "5":
|
elif status == "5":
|
||||||
self._todo_telemetry_tui()
|
self._todo_telemetry_tui()
|
||||||
|
elif status == "6":
|
||||||
|
self.prompt_configuration()
|
||||||
# elif status == "3" or status == "install":
|
# elif status == "3" or status == "install":
|
||||||
# print("install")
|
# print("install")
|
||||||
else:
|
else:
|
||||||
|
|
@ -547,6 +551,7 @@ class TODO:
|
||||||
"prompt_execute_deploy": "Deploy",
|
"prompt_execute_deploy": "Deploy",
|
||||||
"prompt_execute_deploy_ssh": "SSH",
|
"prompt_execute_deploy_ssh": "SSH",
|
||||||
"prompt_execute_qemu": "QEMU/KVM",
|
"prompt_execute_qemu": "QEMU/KVM",
|
||||||
|
"prompt_configuration": "Configuration",
|
||||||
}
|
}
|
||||||
|
|
||||||
def _menu_header(self):
|
def _menu_header(self):
|
||||||
|
|
@ -618,6 +623,92 @@ class TODO:
|
||||||
if ans.strip().lower() not in ("r", "revenir", "o", "oui", "y"):
|
if ans.strip().lower() not in ("r", "revenir", "o", "oui", "y"):
|
||||||
return
|
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):
|
def fill_help_info(self, choices):
|
||||||
# Une entrée {"section": "..."} affiche un titre de section SANS
|
# Une entrée {"section": "..."} affiche un titre de section SANS
|
||||||
# consommer de numéro : la numérotation reste continue sur les vraies
|
# consommer de numéro : la numérotation reste continue sur les vraies
|
||||||
|
|
|
||||||
|
|
@ -1351,6 +1351,62 @@ TRANSLATIONS = {
|
||||||
"fr": "📊 Télémétrie de navigation (TUI)",
|
"fr": "📊 Télémétrie de navigation (TUI)",
|
||||||
"en": "📊 Navigation telemetry (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": {
|
"TODO navigation telemetry": {
|
||||||
"fr": "TODO — télémétrie de navigation",
|
"fr": "TODO — télémétrie de navigation",
|
||||||
"en": "TODO navigation telemetry",
|
"en": "TODO navigation telemetry",
|
||||||
|
|
|
||||||
72
script/todo/todo_prefs.py
Normal file
72
script/todo/todo_prefs.py
Normal file
|
|
@ -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
|
||||||
Loading…
Reference in a new issue