[ADD] todo: navigation telemetry

Records the menus visited and shows them as a tree, a kanban or a list.
The tree is derived from the code, so a command never visited appears too,
and can be run from there.

--- FR ---

Enregistre les menus visités et les présente en arbre, en kanban ou en
liste. L'arbre est dérivé du code, si bien qu'une commande jamais visitée
y figure aussi, et peut être lancée de là.

Assisted-by: Claude Opus 4.8
This commit is contained in:
Mathieu Benoit 2026-08-07 03:22:26 -04:00
parent 7d4922bf09
commit 4dfe058c70
3 changed files with 1506 additions and 0 deletions

View file

@ -117,6 +117,61 @@ class TODO:
set_lang("en")
print(t("Language changed to: English"))
def run(self):
with open(self.config_file.get_logo_ascii_file_path()) as my_file:
print(my_file.read())
self._ask_language()
print(t("Opening TODO ..."))
print(f"🤖 {t('=> Enter your choice by number and press Enter!')}")
help_info = f"""{self._menu_header()}
[1] {t("Execute")}
[2] {t("Install")}
[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:
try:
status = click.prompt(help_info)
except NameError:
print("Do")
print(f"source ./{VENV_ERPLIBRE}/bin/activate && make")
sys.exit(1)
except ImportError:
print("Do")
print(f"source ./{VENV_ERPLIBRE}/bin/activate && make")
sys.exit(1)
except click.exceptions.Abort:
sys.exit(0)
print()
if status == "0":
break
elif status == "1":
self.prompt_execute()
elif status == "2":
self.prompt_install()
elif status == "3":
self.execute_prompt_ia()
elif status == "4":
# cmd = (
# f"gnome-terminal --tab -- bash -c 'source"
# f" ./{VENV_ERPLIBRE}/bin/activate;make todo'"
# )
cmd = "make 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:
print(t("Command not found !"))
print(status)
def prompt_execute(self):
help_info = f"""{self._menu_header()}
@ -291,6 +346,107 @@ class TODO:
"prompt_configuration": "Configuration",
}
def _menu_header(self):
"""En-tête de menu : fil d'Ariane (dérivé de la pile d'appels) suivi de
la ligne « Commande : ». Le fil situe le menu courant et se copie pour
décrire sans ambiguïté où l'on se trouve."""
crumbs = []
for frame_info in reversed(inspect.stack()):
if frame_info.frame.f_locals.get("self") is not self:
continue
label = self._MENU_LABELS.get(frame_info.function)
if label and (not crumbs or crumbs[-1] != label):
crumbs.append(label)
header = ""
if crumbs:
header = "📍 " + " › ".join(crumbs) + "\n"
# Télémétrie de navigation (best-effort, ne casse jamais le menu).
try:
from script.todo import todo_telemetry
todo_telemetry.record(" › ".join(crumbs))
except Exception:
pass
return header + t("Command:")
def _todo_telemetry_tui(self):
"""Ouvre le TUI de télémétrie (arbre/Kanban). Une commande choisie est
exécutée au retour (hors du TUI) ; on propose ensuite de REVENIR (l'état
et la position du curseur sont restaurés) ou de quitter."""
from script.todo.todo_telemetry import run_tui
from script.todo import textual_setup
if not textual_setup.ensure():
return
state = None
while True:
try:
result = run_tui(state=state)
except ImportError:
return
if not result:
return
action, state = result
if not action:
return # quitté sans choisir de commande
method, kwargs = action
# Fil d'Ariane : la commande étant lancée DEPUIS la télémétrie (et
# non via la navigation), aucun menu n'a affiché le chemin. On le
# montre ici (dernier segment traduit + icône) et on l'enregistre.
path = state.get("path") if isinstance(state, dict) else None
if path:
segs = path.split(" › ")
segs[-1] = t(segs[-1])
print(f"\n📍 {' › '.join(segs)}")
try:
from script.todo import todo_telemetry
todo_telemetry.record(path)
except Exception:
pass
fn = getattr(self, method, None)
if not callable(fn):
print(f"{t('Command not found !')} ({method})")
else:
try:
fn(**(kwargs or {}))
except Exception as exc:
print(f"{t('Command failed: ')}{exc}")
# Revenir (curseur restauré) ou quitter ?
ans = input(f"\n{t('Back to telemetry (r) or quit (Enter)? ')}")
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"),
),
),
"migration_ui": (
"Odoo migration interface",
(
("ask", "Ask every time"),
("tui", "TUI form"),
("cli", "Classic questions (line by line)"),
),
),
}
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

View file

@ -1649,6 +1649,42 @@ TRANSLATIONS = {
"fr": "Rien à copier.",
"en": "Nothing to copy.",
},
"TODO navigation telemetry": {
"fr": "TODO — télémétrie de navigation",
"en": "TODO navigation telemetry",
},
"navigations": {
"fr": "navigations",
"en": "navigations",
},
"menus": {
"fr": "menus",
"en": "menus",
},
"Telemetry reset.": {
"fr": "Télémétrie réinitialisée.",
"en": "Telemetry reset.",
},
"tree from code": {
"fr": "arbre issu du code",
"en": "tree from code",
},
"visited paths only": {
"fr": "chemins visités seulement",
"en": "visited paths only",
},
"Enter = run, F3 = view": {
"fr": "Entrée = exécuter, F3 = vue",
"en": "Enter = run, F3 = view",
},
"Enter = run, F3/F4 = views": {
"fr": "Entrée = exécuter, F3/F4 = vues",
"en": "Enter = run, F3/F4 = views",
},
"view": {
"fr": "vue",
"en": "view",
},
"avg": {
"fr": "moy",
"en": "avg",
@ -1657,6 +1693,63 @@ TRANSLATIONS = {
"fr": "Dernière install :",
"en": "Last install:",
},
"No command found.": {
"fr": "Aucune commande trouvée.",
"en": "No command found.",
},
"Back to telemetry (r) or quit (Enter)? ": {
"fr": "Revenir à la télémétrie (r) ou quitter (Entrée) ? ",
"en": "Back to telemetry (r) or quit (Enter)? ",
},
"F2 system · F3/F4 views · Enter run": {
"fr": "F2 système · F3/F4 vues · Entrée exécute",
"en": "F2 system · F3/F4 views · Enter run",
},
"State": {"fr": "État", "en": "State"},
"uptime": {"fr": "actif depuis", "en": "uptime"},
"load": {"fr": "charge", "en": "load"},
"cores": {"fr": "cœurs", "en": "cores"},
"Memory": {"fr": "Mémoire", "en": "Memory"},
"Disk": {"fr": "Disque", "en": "Disk"},
"Network": {"fr": "Réseau", "en": "Network"},
"Battery": {"fr": "Batterie", "en": "Battery"},
"Temperature": {"fr": "Température", "en": "Temperature"},
"lm-sensors absent — press i to install": {
"fr": "lm-sensors absent — appuyez sur i pour installer",
"en": "lm-sensors absent — press i to install",
},
"Sensors already available.": {
"fr": "Capteurs déjà disponibles.",
"en": "Sensors already available.",
},
"Sensors now available.": {
"fr": "Capteurs désormais disponibles.",
"en": "Sensors now available.",
},
"Still no temperature (reboot/modprobe may be needed).": {
"fr": "Toujours pas de température (redémarrage/modprobe requis ?).",
"en": "Still no temperature (reboot/modprobe may be needed).",
},
"Unknown package manager for lm-sensors.": {
"fr": "Gestionnaire de paquets inconnu pour lm-sensors.",
"en": "Unknown package manager for lm-sensors.",
},
"Proposed install command:": {
"fr": "Commande d'installation proposée :",
"en": "Proposed install command:",
},
"Install lm-sensors now? (y/N): ": {
"fr": "Installer lm-sensors maintenant ? (o/N) : ",
"en": "Install lm-sensors now? (y/N): ",
},
"Command failed: ": {
"fr": "Échec de la commande : ",
"en": "Command failed: ",
},
"Your distribution may package it as python3-textual.": {
"fr": "Votre distribution le fournit peut-être en python3-textual.",
"en": "Your distribution may package it as python3-textual.",
},
"Resize a VM disk": {
"fr": "📐 Redimensionner le disque d'une VM",
"en": "📐 Resize a VM disk",
@ -1987,6 +2080,18 @@ TRANSLATIONS = {
"fr": "Attente de l'arrêt de la VM…",
"en": "Waiting for the VM to shut down...",
},
"timeout": {
"fr": "délai max",
"en": "timeout",
},
"shutting down": {
"fr": "arrêt en cours",
"en": "shutting down",
},
"remaining": {
"fr": "restantes",
"en": "remaining",
},
"VM is off.": {
"fr": "VM éteinte.",
"en": "VM is off.",

File diff suppressed because it is too large Load diff