Integrate claude, improve selenium and support qemu #1
2 changed files with 197 additions and 10 deletions
|
|
@ -1302,6 +1302,14 @@ TRANSLATIONS = {
|
||||||
"fr": "Télémétrie réinitialisée.",
|
"fr": "Télémétrie réinitialisée.",
|
||||||
"en": "Telemetry reset.",
|
"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",
|
||||||
|
},
|
||||||
"Install textual for the telemetry TUI (pip).": {
|
"Install textual for the telemetry TUI (pip).": {
|
||||||
"fr": "Installez textual pour le TUI de télémétrie (pip).",
|
"fr": "Installez textual pour le TUI de télémétrie (pip).",
|
||||||
"en": "Install textual for the telemetry TUI (pip).",
|
"en": "Install textual for the telemetry TUI (pip).",
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ DIAGRAMME arborescent des fonctionnalités dans un TUI Textual, trié par usage.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import ast
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
|
@ -85,6 +86,162 @@ def _nested(paths: dict) -> dict:
|
||||||
return root
|
return root
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Métadonnées de navigation DÉRIVÉES DU CODE (structure réelle des menus)
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
def _str_of(node) -> str | None:
|
||||||
|
"""Chaîne d'un nœud AST : littéral, t("…") ou f-string (parties Constant)."""
|
||||||
|
if isinstance(node, ast.Constant) and isinstance(node.value, str):
|
||||||
|
return node.value
|
||||||
|
if (
|
||||||
|
isinstance(node, ast.Call)
|
||||||
|
and isinstance(node.func, ast.Name)
|
||||||
|
and node.func.id == "t"
|
||||||
|
and node.args
|
||||||
|
and isinstance(node.args[0], ast.Constant)
|
||||||
|
):
|
||||||
|
return node.args[0].value
|
||||||
|
if isinstance(node, ast.JoinedStr):
|
||||||
|
return "".join(
|
||||||
|
p.value
|
||||||
|
for p in node.values
|
||||||
|
if isinstance(p, ast.Constant) and isinstance(p.value, str)
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _choice_labels(func) -> list:
|
||||||
|
"""Libellés NUMÉROTÉS d'un menu « choices = [...] » (dans l'ordre, sections
|
||||||
|
exclues car non numérotées par fill_help_info)."""
|
||||||
|
for node in ast.walk(func):
|
||||||
|
if (
|
||||||
|
isinstance(node, ast.Assign)
|
||||||
|
and any(
|
||||||
|
isinstance(tg, ast.Name) and tg.id == "choices"
|
||||||
|
for tg in node.targets
|
||||||
|
)
|
||||||
|
and isinstance(node.value, ast.List)
|
||||||
|
):
|
||||||
|
labels = []
|
||||||
|
for el in node.value.elts:
|
||||||
|
if not isinstance(el, ast.Dict):
|
||||||
|
continue
|
||||||
|
for k, v in zip(el.keys, el.values):
|
||||||
|
if isinstance(k, ast.Constant) and k.value == "prompt_description":
|
||||||
|
s = _str_of(v)
|
||||||
|
if s:
|
||||||
|
labels.append(s)
|
||||||
|
return labels
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _dispatch(func) -> dict:
|
||||||
|
"""{numéro: nom_de_méthode} depuis les branches « status == "N": self.X() »."""
|
||||||
|
out = {}
|
||||||
|
for node in ast.walk(func):
|
||||||
|
if not isinstance(node, ast.If):
|
||||||
|
continue
|
||||||
|
test = node.test
|
||||||
|
if (
|
||||||
|
isinstance(test, ast.Compare)
|
||||||
|
and isinstance(test.left, ast.Name)
|
||||||
|
and test.left.id == "status"
|
||||||
|
and len(test.ops) == 1
|
||||||
|
and isinstance(test.ops[0], ast.Eq)
|
||||||
|
and isinstance(test.comparators[0], ast.Constant)
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
num = int(test.comparators[0].value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
for stmt in node.body:
|
||||||
|
for n in ast.walk(stmt):
|
||||||
|
if (
|
||||||
|
isinstance(n, ast.Call)
|
||||||
|
and isinstance(n.func, ast.Attribute)
|
||||||
|
and isinstance(n.func.value, ast.Name)
|
||||||
|
and n.func.value.id == "self"
|
||||||
|
):
|
||||||
|
out[num] = n.func.attr
|
||||||
|
break
|
||||||
|
if num in out:
|
||||||
|
break
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _menu_labels(cls) -> dict:
|
||||||
|
"""{méthode: label} depuis l'attribut de classe _MENU_LABELS."""
|
||||||
|
for node in ast.walk(cls):
|
||||||
|
if (
|
||||||
|
isinstance(node, ast.Assign)
|
||||||
|
and any(
|
||||||
|
isinstance(tg, ast.Name) and tg.id == "_MENU_LABELS"
|
||||||
|
for tg in node.targets
|
||||||
|
)
|
||||||
|
and isinstance(node.value, ast.Dict)
|
||||||
|
):
|
||||||
|
return {
|
||||||
|
k.value: v.value
|
||||||
|
for k, v in zip(node.value.keys, node.value.values)
|
||||||
|
if isinstance(k, ast.Constant) and isinstance(v, ast.Constant)
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def build_code_tree(todo_path=None) -> dict | None:
|
||||||
|
"""Construit l'arbre des menus/commandes EN LISANT le code de todo.py
|
||||||
|
(AST). Chaque menu (méthode de _MENU_LABELS) devient un nœud ; ses branches
|
||||||
|
« status == N » deviennent des sous-menus (si la cible est un menu) ou des
|
||||||
|
commandes (feuilles). None si l'analyse échoue."""
|
||||||
|
p = Path(todo_path) if todo_path else (Path(__file__).parent / "todo.py")
|
||||||
|
try:
|
||||||
|
mod = ast.parse(p.read_text(encoding="utf-8"))
|
||||||
|
except (OSError, SyntaxError):
|
||||||
|
return None
|
||||||
|
cls = next(
|
||||||
|
(n for n in ast.walk(mod) if isinstance(n, ast.ClassDef)), None
|
||||||
|
)
|
||||||
|
if cls is None:
|
||||||
|
return None
|
||||||
|
methods = {
|
||||||
|
n.name: n for n in cls.body if isinstance(n, ast.FunctionDef)
|
||||||
|
}
|
||||||
|
labels = _menu_labels(cls)
|
||||||
|
if "run" not in methods or not labels:
|
||||||
|
return None
|
||||||
|
seen = set()
|
||||||
|
|
||||||
|
def build(method):
|
||||||
|
node = {
|
||||||
|
"label": labels.get(method, method),
|
||||||
|
"is_menu": True,
|
||||||
|
"children": [],
|
||||||
|
}
|
||||||
|
if method in seen or method not in methods:
|
||||||
|
return node
|
||||||
|
seen.add(method)
|
||||||
|
func = methods[method]
|
||||||
|
disp = _dispatch(func)
|
||||||
|
clabels = _choice_labels(func)
|
||||||
|
for num in sorted(disp):
|
||||||
|
target = disp[num]
|
||||||
|
if target in labels: # sous-menu
|
||||||
|
node["children"].append(build(target))
|
||||||
|
else: # commande (feuille)
|
||||||
|
lab = (
|
||||||
|
clabels[num - 1]
|
||||||
|
if 0 <= num - 1 < len(clabels)
|
||||||
|
else target.lstrip("_").replace("_", " ")
|
||||||
|
)
|
||||||
|
node["children"].append(
|
||||||
|
{"label": lab, "is_menu": False, "children": []}
|
||||||
|
)
|
||||||
|
seen.discard(method)
|
||||||
|
return node
|
||||||
|
|
||||||
|
return build("run")
|
||||||
|
|
||||||
|
|
||||||
def run_tui(run_app: bool = True):
|
def run_tui(run_app: bool = True):
|
||||||
"""Ouvre le TUI arborescent. `run_app=False` renvoie l'app (tests)."""
|
"""Ouvre le TUI arborescent. `run_app=False` renvoie l'app (tests)."""
|
||||||
from textual.app import App, ComposeResult
|
from textual.app import App, ComposeResult
|
||||||
|
|
@ -115,24 +272,46 @@ def run_tui(run_app: bool = True):
|
||||||
data = load()
|
data = load()
|
||||||
paths = data.get("paths", {})
|
paths = data.get("paths", {})
|
||||||
total = sum(paths.values())
|
total = sum(paths.values())
|
||||||
self.query_one("#summary", Static).update(
|
|
||||||
f" {total} {t('navigations')} · {len(paths)} {t('menus')}"
|
|
||||||
)
|
|
||||||
tree = self.query_one("#nav", Tree)
|
tree = self.query_one("#nav", Tree)
|
||||||
tree.clear()
|
tree.clear()
|
||||||
nested = _nested(paths)
|
code_tree = build_code_tree()
|
||||||
todo = nested.get("TODO", {"count": 0, "children": nested})
|
if code_tree is not None:
|
||||||
tree.root.set_label(f"📍 TODO ({todo['count']})")
|
# Structure RÉELLE issue du code + compteurs de visites.
|
||||||
self._add(tree.root, todo["children"])
|
cnt = paths.get("TODO", 0)
|
||||||
|
tree.root.set_label(f"📍 TODO ({cnt})")
|
||||||
|
self._add_code(tree.root, code_tree["children"], "TODO", paths)
|
||||||
|
src = t("tree from code")
|
||||||
|
else:
|
||||||
|
# Repli : arbre des seuls chemins visités.
|
||||||
|
nested = _nested(paths)
|
||||||
|
todo = nested.get("TODO", {"count": 0, "children": nested})
|
||||||
|
tree.root.set_label(f"📍 TODO ({todo['count']})")
|
||||||
|
self._add_visited(tree.root, todo["children"])
|
||||||
|
src = t("visited paths only")
|
||||||
tree.root.expand()
|
tree.root.expand()
|
||||||
|
self.query_one("#summary", Static).update(
|
||||||
|
f" {total} {t('navigations')} · {len(paths)} "
|
||||||
|
f"{t('menus')} · {src}"
|
||||||
|
)
|
||||||
|
|
||||||
def _add(self, tnode, children):
|
def _add_code(self, tnode, children, parent_path, paths):
|
||||||
# Tri par visites décroissantes : les plus utilisés en tête.
|
"""Nœuds depuis le code : menus (avec compteur de visites) et
|
||||||
|
commandes (feuilles préfixées « · »)."""
|
||||||
|
for c in children:
|
||||||
|
if c["is_menu"]:
|
||||||
|
path = f"{parent_path} › {c['label']}"
|
||||||
|
cnt = paths.get(path, 0)
|
||||||
|
child = tnode.add(f"{c['label']} ({cnt})", expand=True)
|
||||||
|
self._add_code(child, c["children"], path, paths)
|
||||||
|
else:
|
||||||
|
tnode.add_leaf(f"· {c['label']}")
|
||||||
|
|
||||||
|
def _add_visited(self, tnode, children):
|
||||||
for name, node in sorted(
|
for name, node in sorted(
|
||||||
children.items(), key=lambda kv: -kv[1]["count"]
|
children.items(), key=lambda kv: -kv[1]["count"]
|
||||||
):
|
):
|
||||||
child = tnode.add(f"{name} ({node['count']})", expand=True)
|
child = tnode.add(f"{name} ({node['count']})", expand=True)
|
||||||
self._add(child, node["children"])
|
self._add_visited(child, node["children"])
|
||||||
|
|
||||||
def action_reset(self):
|
def action_reset(self):
|
||||||
reset()
|
reset()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue