Integrate claude, improve selenium and support qemu #1
3 changed files with 222 additions and 57 deletions
|
|
@ -578,26 +578,38 @@ class TODO:
|
||||||
return header + t("Command:")
|
return header + t("Command:")
|
||||||
|
|
||||||
def _todo_telemetry_tui(self):
|
def _todo_telemetry_tui(self):
|
||||||
"""Ouvre le TUI de télémétrie (arbre/Kanban). Si l'utilisateur choisit
|
"""Ouvre le TUI de télémétrie (arbre/Kanban). Une commande choisie est
|
||||||
une commande, on l'exécute au retour (hors du TUI)."""
|
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.todo_telemetry import run_tui
|
||||||
|
|
||||||
|
state = None
|
||||||
|
while True:
|
||||||
try:
|
try:
|
||||||
action = run_tui()
|
result = run_tui(state=state)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
print(t("Install textual for the telemetry TUI (pip)."))
|
print(t("Install textual for the telemetry TUI (pip)."))
|
||||||
return
|
return
|
||||||
if not action:
|
if not result:
|
||||||
return
|
return
|
||||||
|
action, state = result
|
||||||
|
if not action:
|
||||||
|
return # quitté sans choisir de commande
|
||||||
method, kwargs = action
|
method, kwargs = action
|
||||||
fn = getattr(self, method, None)
|
fn = getattr(self, method, None)
|
||||||
if not callable(fn):
|
if not callable(fn):
|
||||||
print(f"{t('Command not found !')} ({method})")
|
print(f"{t('Command not found !')} ({method})")
|
||||||
return
|
else:
|
||||||
try:
|
try:
|
||||||
fn(**(kwargs or {}))
|
fn(**(kwargs or {}))
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
print(f"{t('Command failed: ')}{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
|
||||||
|
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -1314,6 +1314,22 @@ TRANSLATIONS = {
|
||||||
"fr": "Entrée = exécuter, F3 = vue",
|
"fr": "Entrée = exécuter, F3 = vue",
|
||||||
"en": "Enter = run, F3 = view",
|
"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",
|
||||||
|
},
|
||||||
|
"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)? ",
|
||||||
|
},
|
||||||
"Command failed: ": {
|
"Command failed: ": {
|
||||||
"fr": "Échec de la commande : ",
|
"fr": "Échec de la commande : ",
|
||||||
"en": "Command failed: ",
|
"en": "Command failed: ",
|
||||||
|
|
|
||||||
|
|
@ -276,12 +276,23 @@ def _command_columns(tree, paths):
|
||||||
return cols
|
return cols
|
||||||
|
|
||||||
|
|
||||||
def run_tui(run_app: bool = True):
|
# Modes d'affichage du Kanban (F4 les fait défiler).
|
||||||
"""TUI de télémétrie : arbre (issu du code) + vue Kanban (F3), sélection
|
KANBAN_MODES = ("columns", "swimlanes", "grid")
|
||||||
d'une commande = exécution. Renvoie (méthode, kwargs) à exécuter, ou None.
|
|
||||||
`run_app=False` renvoie l'app (tests)."""
|
|
||||||
|
def run_tui(run_app: bool = True, state: dict | None = None):
|
||||||
|
"""TUI de télémétrie : vue Arbre (issue du code) et vue Kanban (F3), la
|
||||||
|
disposition du Kanban défilant par F4 (colonnes / swimlanes / grille).
|
||||||
|
Sélectionner une COMMANDE = l'exécuter. `state` restaure la vue + le
|
||||||
|
curseur au retour. Renvoie (action|None, state) ; run_app=False -> l'app."""
|
||||||
from textual.app import App, ComposeResult
|
from textual.app import App, ComposeResult
|
||||||
from textual.containers import HorizontalScroll, VerticalScroll
|
from textual.containers import (
|
||||||
|
Container,
|
||||||
|
Grid,
|
||||||
|
HorizontalScroll,
|
||||||
|
Vertical,
|
||||||
|
VerticalScroll,
|
||||||
|
)
|
||||||
from textual.widgets import (
|
from textual.widgets import (
|
||||||
Footer,
|
Footer,
|
||||||
Header,
|
Header,
|
||||||
|
|
@ -294,14 +305,17 @@ def run_tui(run_app: bool = True):
|
||||||
|
|
||||||
paths = load().get("paths", {})
|
paths = load().get("paths", {})
|
||||||
code_tree = build_code_tree()
|
code_tree = build_code_tree()
|
||||||
|
columns = _command_columns(code_tree, paths) # (label, path, cnt, cmds)
|
||||||
|
state = state or {}
|
||||||
|
|
||||||
class CmdItem(ListItem):
|
class CmdItem(ListItem):
|
||||||
"""Carte Kanban portant la commande à exécuter."""
|
"""Carte : commande à exécuter + son chemin (pour restaurer le curseur)."""
|
||||||
|
|
||||||
def __init__(self, label, method, kwargs):
|
def __init__(self, label, method, kwargs, path):
|
||||||
super().__init__(Label(label))
|
super().__init__(Label(label))
|
||||||
self.cmd_method = method
|
self.cmd_method = method
|
||||||
self.cmd_kwargs = kwargs or {}
|
self.cmd_kwargs = kwargs or {}
|
||||||
|
self.cmd_path = path
|
||||||
|
|
||||||
class Telemetry(App):
|
class Telemetry(App):
|
||||||
CSS = """
|
CSS = """
|
||||||
|
|
@ -310,10 +324,14 @@ def run_tui(run_app: bool = True):
|
||||||
#kanban { display: none; height: 1fr; }
|
#kanban { display: none; height: 1fr; }
|
||||||
.kcol { width: 40; border: solid $accent; margin: 0 1 0 0; }
|
.kcol { width: 40; border: solid $accent; margin: 0 1 0 0; }
|
||||||
.ktitle { height: 1; color: $accent; }
|
.ktitle { height: 1; color: $accent; }
|
||||||
|
.lane { height: auto; }
|
||||||
|
.lanetitle { height: 1; color: $secondary; }
|
||||||
|
.kgrid { grid-size: 3; grid-gutter: 1; }
|
||||||
"""
|
"""
|
||||||
BINDINGS = [
|
BINDINGS = [
|
||||||
("q", "quit", "Quitter"),
|
("q", "quit", "Quitter"),
|
||||||
("f3", "toggle_view", "Vue arbre/Kanban"),
|
("f3", "toggle_view", "Arbre/Kanban"),
|
||||||
|
("f4", "kanban_layout", "Disposition Kanban"),
|
||||||
("e", "expand_all", "Tout déplier"),
|
("e", "expand_all", "Tout déplier"),
|
||||||
("r", "reset", "Réinitialiser"),
|
("r", "reset", "Réinitialiser"),
|
||||||
]
|
]
|
||||||
|
|
@ -321,47 +339,139 @@ def run_tui(run_app: bool = True):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._action = None
|
self._action = None
|
||||||
self._mode = "tree"
|
self._mode = state.get("mode", "tree")
|
||||||
|
self._kanban_mode = state.get("kanban_mode", "columns")
|
||||||
|
|
||||||
|
# -- helpers de construction de widgets ------------------------------ #
|
||||||
|
def _col_widget(self, label, cnt, cmds):
|
||||||
|
items = [
|
||||||
|
CmdItem(
|
||||||
|
f"· {c['label']}",
|
||||||
|
c.get("method"),
|
||||||
|
c.get("kwargs"),
|
||||||
|
c.get("path"),
|
||||||
|
)
|
||||||
|
for c in cmds
|
||||||
|
]
|
||||||
|
return VerticalScroll(
|
||||||
|
Static(f"{label} ({cnt})", classes="ktitle"),
|
||||||
|
ListView(*items),
|
||||||
|
classes="kcol",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _kanban_layout_widget(self):
|
||||||
|
cols = [
|
||||||
|
self._col_widget(label, cnt, cmds)
|
||||||
|
for (label, _p, cnt, cmds) in columns
|
||||||
|
]
|
||||||
|
if not cols:
|
||||||
|
return Static(t("No command found."))
|
||||||
|
if self._kanban_mode == "grid":
|
||||||
|
return Grid(*cols, classes="kgrid")
|
||||||
|
if self._kanban_mode == "swimlanes":
|
||||||
|
# Une rangée (swimlane) par menu de NIVEAU 1 (Execute, …).
|
||||||
|
groups, order = {}, []
|
||||||
|
for (label, path, cnt, cmds) in columns:
|
||||||
|
parts = path.split(" › ")
|
||||||
|
g = parts[1] if len(parts) > 1 else "TODO"
|
||||||
|
if g not in groups:
|
||||||
|
groups[g] = []
|
||||||
|
order.append(g)
|
||||||
|
groups[g].append((label, cnt, cmds))
|
||||||
|
lanes = []
|
||||||
|
for g in order:
|
||||||
|
lane_cols = [
|
||||||
|
self._col_widget(lb, cn, cm) for (lb, cn, cm) in groups[g]
|
||||||
|
]
|
||||||
|
lanes.append(
|
||||||
|
Vertical(
|
||||||
|
Static(f"━━ {g} ━━", classes="lanetitle"),
|
||||||
|
HorizontalScroll(*lane_cols),
|
||||||
|
classes="lane",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return VerticalScroll(*lanes)
|
||||||
|
return HorizontalScroll(*cols) # columns
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
yield Header(show_clock=True)
|
yield Header(show_clock=True)
|
||||||
yield Static("", id="summary")
|
yield Static("", id="summary")
|
||||||
yield Tree("📍 TODO", id="nav")
|
yield Tree("📍 TODO", id="nav")
|
||||||
with HorizontalScroll(id="kanban"):
|
yield Container(id="kanban")
|
||||||
for label, _path, cnt, cmds in _command_columns(
|
|
||||||
code_tree, paths
|
|
||||||
):
|
|
||||||
with VerticalScroll(classes="kcol"):
|
|
||||||
yield Static(f"{label} ({cnt})", classes="ktitle")
|
|
||||||
yield ListView(
|
|
||||||
*[
|
|
||||||
CmdItem(
|
|
||||||
f"· {c['label']}",
|
|
||||||
c.get("method"),
|
|
||||||
c.get("kwargs"),
|
|
||||||
)
|
|
||||||
for c in cmds
|
|
||||||
]
|
|
||||||
)
|
|
||||||
yield Footer()
|
yield Footer()
|
||||||
|
|
||||||
def on_mount(self):
|
def on_mount(self):
|
||||||
self.title = t("TODO navigation telemetry")
|
self.title = t("TODO navigation telemetry")
|
||||||
self._populate_tree()
|
self._populate_tree()
|
||||||
self._update_summary()
|
self._update_summary()
|
||||||
|
# Restaure la vue / la disposition / le curseur au retour.
|
||||||
|
self.run_worker(self._restore())
|
||||||
|
|
||||||
|
async def _restore(self):
|
||||||
|
if self._mode == "kanban":
|
||||||
|
await self._enter_kanban()
|
||||||
|
else:
|
||||||
|
self.query_one("#nav").display = True
|
||||||
|
self.query_one("#kanban").display = False
|
||||||
|
self._focus_tree_path(state.get("path"))
|
||||||
|
self._update_summary()
|
||||||
|
|
||||||
|
# -- vue Kanban ------------------------------------------------------ #
|
||||||
|
async def _enter_kanban(self):
|
||||||
|
box = self.query_one("#kanban", Container)
|
||||||
|
await box.remove_children()
|
||||||
|
await box.mount(self._kanban_layout_widget())
|
||||||
|
self.query_one("#nav").display = False
|
||||||
|
box.display = True
|
||||||
|
self._focus_kanban_path(state.get("path"))
|
||||||
|
|
||||||
|
def _focus_kanban_path(self, path):
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
for lv in self.query(ListView):
|
||||||
|
for i, item in enumerate(lv.children):
|
||||||
|
if getattr(item, "cmd_path", None) == path:
|
||||||
|
lv.index = i
|
||||||
|
lv.focus()
|
||||||
|
return
|
||||||
|
|
||||||
|
def _focus_tree_path(self, path):
|
||||||
|
if not path:
|
||||||
|
return
|
||||||
|
tree = self.query_one("#nav", Tree)
|
||||||
|
|
||||||
|
def walk(node):
|
||||||
|
d = getattr(node, "data", None)
|
||||||
|
if isinstance(d, dict) and d.get("path") == path:
|
||||||
|
return node
|
||||||
|
for ch in node.children:
|
||||||
|
found = walk(ch)
|
||||||
|
if found:
|
||||||
|
return found
|
||||||
|
return None
|
||||||
|
|
||||||
|
node = walk(tree.root)
|
||||||
|
if node is not None:
|
||||||
|
tree.move_cursor(node)
|
||||||
|
tree.scroll_to_node(node)
|
||||||
|
|
||||||
def _update_summary(self):
|
def _update_summary(self):
|
||||||
total = sum(paths.values())
|
total = sum(paths.values())
|
||||||
src = t("tree from code") if code_tree else t("visited paths only")
|
src = t("tree from code") if code_tree else t("visited paths only")
|
||||||
|
extra = (
|
||||||
|
f" [{self._kanban_mode}]" if self._mode == "kanban" else ""
|
||||||
|
)
|
||||||
self.query_one("#summary", Static).update(
|
self.query_one("#summary", Static).update(
|
||||||
f" {total} {t('navigations')} · {len(paths)} {t('menus')} · "
|
f" {total} {t('navigations')} · {len(paths)} {t('menus')} · "
|
||||||
f"{src} · {t('Enter = run, F3 = view')}"
|
f"{src} · {t('view')}: {self._mode}{extra} · "
|
||||||
|
f"{t('Enter = run, F3/F4 = views')}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def _populate_tree(self):
|
def _populate_tree(self):
|
||||||
tree = self.query_one("#nav", Tree)
|
tree = self.query_one("#nav", Tree)
|
||||||
tree.clear()
|
tree.clear()
|
||||||
if code_tree is not None:
|
if code_tree is not None:
|
||||||
|
tree.root.data = {"path": "TODO"}
|
||||||
tree.root.set_label(f"📍 TODO ({paths.get('TODO', 0)})")
|
tree.root.set_label(f"📍 TODO ({paths.get('TODO', 0)})")
|
||||||
self._add_code(tree.root, code_tree["children"], "TODO")
|
self._add_code(tree.root, code_tree["children"], "TODO")
|
||||||
else:
|
else:
|
||||||
|
|
@ -373,19 +483,22 @@ def run_tui(run_app: bool = True):
|
||||||
|
|
||||||
def _add_code(self, tnode, children, parent_path):
|
def _add_code(self, tnode, children, parent_path):
|
||||||
for c in children:
|
for c in children:
|
||||||
if c["is_menu"]:
|
|
||||||
path = f"{parent_path} › {c['label']}"
|
path = f"{parent_path} › {c['label']}"
|
||||||
|
if c["is_menu"]:
|
||||||
child = tnode.add(
|
child = tnode.add(
|
||||||
f"{c['label']} ({paths.get(path, 0)})", expand=True
|
f"{c['label']} ({paths.get(path, 0)})",
|
||||||
|
data={"path": path},
|
||||||
|
expand=True,
|
||||||
)
|
)
|
||||||
self._add_code(child, c["children"], path)
|
self._add_code(child, c["children"], path)
|
||||||
else:
|
else:
|
||||||
# Feuille EXÉCUTABLE : la méthode est portée en data.
|
# Feuille EXÉCUTABLE : méthode + chemin portés en data.
|
||||||
tnode.add_leaf(
|
tnode.add_leaf(
|
||||||
f"· {c['label']}",
|
f"· {c['label']}",
|
||||||
data={
|
data={
|
||||||
"method": c.get("method"),
|
"method": c.get("method"),
|
||||||
"kwargs": c.get("kwargs"),
|
"kwargs": c.get("kwargs"),
|
||||||
|
"path": path,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -396,25 +509,48 @@ def run_tui(run_app: bool = True):
|
||||||
child = tnode.add(f"{name} ({node['count']})", expand=True)
|
child = tnode.add(f"{name} ({node['count']})", expand=True)
|
||||||
self._add_visited(child, node["children"])
|
self._add_visited(child, node["children"])
|
||||||
|
|
||||||
def _run(self, method, kwargs):
|
# -- sélection / exécution ------------------------------------------- #
|
||||||
|
def _run(self, method, kwargs, path):
|
||||||
if not method:
|
if not method:
|
||||||
return
|
return
|
||||||
self._action = (method, kwargs or {})
|
self._action = (method, kwargs or {})
|
||||||
|
self._exit_state = {
|
||||||
|
"mode": self._mode,
|
||||||
|
"kanban_mode": self._kanban_mode,
|
||||||
|
"path": path,
|
||||||
|
}
|
||||||
self.exit()
|
self.exit()
|
||||||
|
|
||||||
def on_tree_node_selected(self, event):
|
def on_tree_node_selected(self, event):
|
||||||
data = getattr(event.node, "data", None)
|
d = getattr(event.node, "data", None)
|
||||||
if isinstance(data, dict) and data.get("method"):
|
if isinstance(d, dict) and d.get("method"):
|
||||||
self._run(data["method"], data.get("kwargs"))
|
self._run(d["method"], d.get("kwargs"), d.get("path"))
|
||||||
|
|
||||||
def on_list_view_selected(self, event):
|
def on_list_view_selected(self, event):
|
||||||
if isinstance(event.item, CmdItem):
|
if isinstance(event.item, CmdItem):
|
||||||
self._run(event.item.cmd_method, event.item.cmd_kwargs)
|
self._run(
|
||||||
|
event.item.cmd_method,
|
||||||
|
event.item.cmd_kwargs,
|
||||||
|
event.item.cmd_path,
|
||||||
|
)
|
||||||
|
|
||||||
def action_toggle_view(self):
|
# -- actions --------------------------------------------------------- #
|
||||||
self._mode = "kanban" if self._mode == "tree" else "tree"
|
async def action_toggle_view(self):
|
||||||
self.query_one("#nav").display = self._mode == "tree"
|
if self._mode == "tree":
|
||||||
self.query_one("#kanban").display = self._mode == "kanban"
|
self._mode = "kanban"
|
||||||
|
await self._enter_kanban()
|
||||||
|
else:
|
||||||
|
self._mode = "tree"
|
||||||
|
self.query_one("#kanban").display = False
|
||||||
|
self.query_one("#nav").display = True
|
||||||
|
self._update_summary()
|
||||||
|
|
||||||
|
async def action_kanban_layout(self):
|
||||||
|
if self._mode != "kanban":
|
||||||
|
return
|
||||||
|
i = KANBAN_MODES.index(self._kanban_mode)
|
||||||
|
self._kanban_mode = KANBAN_MODES[(i + 1) % len(KANBAN_MODES)]
|
||||||
|
await self._enter_kanban()
|
||||||
self._update_summary()
|
self._update_summary()
|
||||||
|
|
||||||
def action_expand_all(self):
|
def action_expand_all(self):
|
||||||
|
|
@ -429,7 +565,8 @@ def run_tui(run_app: bool = True):
|
||||||
self.notify(t("Telemetry reset."))
|
self.notify(t("Telemetry reset."))
|
||||||
|
|
||||||
app = Telemetry()
|
app = Telemetry()
|
||||||
|
app._exit_state = state
|
||||||
if run_app:
|
if run_app:
|
||||||
app.run()
|
app.run()
|
||||||
return app._action
|
return app._action, getattr(app, "_exit_state", state)
|
||||||
return app
|
return app
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue