[IMP] script todo: télémétrie navigation — vue Liste (F3) + sections + icônes
- F3 fait maintenant défiler Arbre -> Kanban -> Liste -> Arbre. Le résumé
et le footer annoncent la PROCHAINE vue (« F3 → Liste »).
- Nouvelle vue Liste : tous les menus empilés verticalement, chacun avec
ses sections et ses commandes exécutables.
- Les colonnes Kanban et la vue Liste affichent les SECTIONS (── … ──)
pour guider le choix, et chaque commande porte son ICÔNE.
L'icône vient de t() : l'AST extrait la clé i18n (prompt_description /
prompt_description_key), et _disp() la résout en libellé traduit + icône.
Les sections sont capturées via _choice_entries (marqueurs {"section": …}).
Validé headless : cycle F3, 11 en-têtes de menu, sections en Liste/Kanban.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4bebf5a397
commit
ec88b5f1f0
1 changed files with 155 additions and 31 deletions
|
|
@ -114,9 +114,10 @@ def _str_of(node) -> str | None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _choice_labels(func) -> list:
|
def _choice_entries(func) -> list:
|
||||||
"""Libellés NUMÉROTÉS d'un menu « choices = [...] » (dans l'ordre, sections
|
"""Entrées NUMÉROTÉES d'un menu « choices = [...] », dans l'ordre : chaque
|
||||||
exclues car non numérotées par fill_help_info)."""
|
commande est {« label », « section »}, la section étant le dernier marqueur
|
||||||
|
{"section": …} rencontré (fill_help_info ne numérote pas les sections)."""
|
||||||
for node in ast.walk(func):
|
for node in ast.walk(func):
|
||||||
if (
|
if (
|
||||||
isinstance(node, ast.Assign)
|
isinstance(node, ast.Assign)
|
||||||
|
|
@ -126,16 +127,23 @@ def _choice_labels(func) -> list:
|
||||||
)
|
)
|
||||||
and isinstance(node.value, ast.List)
|
and isinstance(node.value, ast.List)
|
||||||
):
|
):
|
||||||
labels = []
|
entries, section = [], None
|
||||||
for el in node.value.elts:
|
for el in node.value.elts:
|
||||||
if not isinstance(el, ast.Dict):
|
if not isinstance(el, ast.Dict):
|
||||||
continue
|
continue
|
||||||
|
d = {}
|
||||||
for k, v in zip(el.keys, el.values):
|
for k, v in zip(el.keys, el.values):
|
||||||
if isinstance(k, ast.Constant) and k.value == "prompt_description":
|
if isinstance(k, ast.Constant):
|
||||||
s = _str_of(v)
|
d[k.value] = _str_of(v)
|
||||||
if s:
|
if d.get("section"):
|
||||||
labels.append(s)
|
section = d["section"]
|
||||||
return labels
|
continue
|
||||||
|
lab = d.get("prompt_description") or d.get(
|
||||||
|
"prompt_description_key"
|
||||||
|
)
|
||||||
|
if lab:
|
||||||
|
entries.append({"label": lab, "section": section})
|
||||||
|
return entries
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -236,15 +244,20 @@ def build_code_tree(todo_path=None) -> dict | None:
|
||||||
seen.add(method)
|
seen.add(method)
|
||||||
func = methods[method]
|
func = methods[method]
|
||||||
disp = _dispatch(func)
|
disp = _dispatch(func)
|
||||||
clabels = _choice_labels(func)
|
centries = _choice_entries(func)
|
||||||
for num in sorted(disp):
|
for num in sorted(disp):
|
||||||
target, kwargs = disp[num]
|
target, kwargs = disp[num]
|
||||||
if target in labels: # sous-menu
|
if target in labels: # sous-menu
|
||||||
node["children"].append(build(target))
|
node["children"].append(build(target))
|
||||||
else: # commande (feuille) exécutable
|
else: # commande (feuille) exécutable
|
||||||
|
entry = (
|
||||||
|
centries[num - 1]
|
||||||
|
if 0 <= num - 1 < len(centries)
|
||||||
|
else None
|
||||||
|
)
|
||||||
lab = (
|
lab = (
|
||||||
clabels[num - 1]
|
entry["label"]
|
||||||
if 0 <= num - 1 < len(clabels)
|
if entry
|
||||||
else target.lstrip("_").replace("_", " ")
|
else target.lstrip("_").replace("_", " ")
|
||||||
)
|
)
|
||||||
node["children"].append(
|
node["children"].append(
|
||||||
|
|
@ -254,6 +267,7 @@ def build_code_tree(todo_path=None) -> dict | None:
|
||||||
"children": [],
|
"children": [],
|
||||||
"method": target,
|
"method": target,
|
||||||
"kwargs": kwargs,
|
"kwargs": kwargs,
|
||||||
|
"section": entry["section"] if entry else None,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
seen.discard(method)
|
seen.discard(method)
|
||||||
|
|
@ -480,6 +494,41 @@ def _fmt_uptime(secs):
|
||||||
# Modes d'affichage du Kanban (F4 les fait défiler).
|
# Modes d'affichage du Kanban (F4 les fait défiler).
|
||||||
KANBAN_MODES = ("columns", "swimlanes", "grid")
|
KANBAN_MODES = ("columns", "swimlanes", "grid")
|
||||||
|
|
||||||
|
# Les vues défilent avec F3 ; chaque vue affiche le NOM de la suivante.
|
||||||
|
VIEWS = ("tree", "kanban", "list")
|
||||||
|
VIEW_LABELS = {
|
||||||
|
"tree": "Arbre",
|
||||||
|
"kanban": "Kanban",
|
||||||
|
"list": "Liste",
|
||||||
|
"system": "Système",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _disp(label: str) -> str:
|
||||||
|
"""Libellé d'affichage d'une commande : passe par t() pour récupérer la
|
||||||
|
traduction ET l'icône (les valeurs i18n portent l'icône). Renvoie le
|
||||||
|
libellé inchangé s'il n'est pas une clé de traduction."""
|
||||||
|
return t(label) if label else label
|
||||||
|
|
||||||
|
|
||||||
|
_SENTINEL = object()
|
||||||
|
|
||||||
|
|
||||||
|
def _group_by_section(cmds):
|
||||||
|
"""Itère les commandes en groupes (section, [cmds]) dans l'ordre, la
|
||||||
|
section pouvant être None (aucune)."""
|
||||||
|
groups, cur, bucket = [], _SENTINEL, []
|
||||||
|
for c in cmds:
|
||||||
|
sec = c.get("section")
|
||||||
|
if sec != cur:
|
||||||
|
if bucket:
|
||||||
|
groups.append((cur, bucket))
|
||||||
|
cur, bucket = sec, []
|
||||||
|
bucket.append(c)
|
||||||
|
if bucket:
|
||||||
|
groups.append((cur, bucket))
|
||||||
|
return groups
|
||||||
|
|
||||||
|
|
||||||
def run_tui(run_app: bool = True, state: dict | None = None):
|
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
|
"""TUI de télémétrie : vue Arbre (issue du code) et vue Kanban (F3), la
|
||||||
|
|
@ -523,9 +572,13 @@ def run_tui(run_app: bool = True, state: dict | None = None):
|
||||||
#summary { height: 1; color: $text-muted; }
|
#summary { height: 1; color: $text-muted; }
|
||||||
Tree { border: solid $accent; }
|
Tree { border: solid $accent; }
|
||||||
#kanban { display: none; height: 1fr; }
|
#kanban { display: none; height: 1fr; }
|
||||||
|
#list { display: none; height: 1fr; }
|
||||||
#system { display: none; height: 1fr; padding: 1 2; }
|
#system { display: none; height: 1fr; padding: 1 2; }
|
||||||
.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; }
|
||||||
|
.ksec { height: 1; color: $secondary; text-style: italic; }
|
||||||
|
.listmenu { height: 1; color: $accent; text-style: bold; }
|
||||||
|
.listsec { height: 1; color: $secondary; text-style: italic; }
|
||||||
.lane { height: auto; }
|
.lane { height: auto; }
|
||||||
.lanetitle { height: 1; color: $secondary; }
|
.lanetitle { height: 1; color: $secondary; }
|
||||||
.kgrid { grid-size: 3; grid-gutter: 1; }
|
.kgrid { grid-size: 3; grid-gutter: 1; }
|
||||||
|
|
@ -534,7 +587,7 @@ def run_tui(run_app: bool = True, state: dict | None = None):
|
||||||
BINDINGS = [
|
BINDINGS = [
|
||||||
("q", "quit", "Quitter"),
|
("q", "quit", "Quitter"),
|
||||||
("f2", "system", "Système"),
|
("f2", "system", "Système"),
|
||||||
("f3", "toggle_view", "Arbre/Kanban"),
|
("f3", "toggle_view", "Vue"),
|
||||||
("f4", "kanban_layout", "Disposition Kanban"),
|
("f4", "kanban_layout", "Disposition Kanban"),
|
||||||
("e", "expand_all", "Tout déplier"),
|
("e", "expand_all", "Tout déplier"),
|
||||||
("i", "install_sensors", "Installer capteurs"),
|
("i", "install_sensors", "Installer capteurs"),
|
||||||
|
|
@ -550,20 +603,59 @@ def run_tui(run_app: bool = True, state: dict | None = None):
|
||||||
|
|
||||||
# -- helpers de construction de widgets ------------------------------ #
|
# -- helpers de construction de widgets ------------------------------ #
|
||||||
def _col_widget(self, label, cnt, cmds):
|
def _col_widget(self, label, cnt, cmds):
|
||||||
items = [
|
# Regroupe par section pour aider au choix ; icônes via _disp().
|
||||||
CmdItem(
|
children = [Static(f"{label} ({cnt})", classes="ktitle")]
|
||||||
f"· {c['label']}",
|
for section, group in _group_by_section(cmds):
|
||||||
c.get("method"),
|
if section:
|
||||||
c.get("kwargs"),
|
children.append(
|
||||||
c.get("path"),
|
Static(f"── {_disp(section)} ──", classes="ksec")
|
||||||
|
)
|
||||||
|
children.append(
|
||||||
|
ListView(
|
||||||
|
*[
|
||||||
|
CmdItem(
|
||||||
|
f"· {_disp(c['label'])}",
|
||||||
|
c.get("method"),
|
||||||
|
c.get("kwargs"),
|
||||||
|
c.get("path"),
|
||||||
|
)
|
||||||
|
for c in group
|
||||||
|
]
|
||||||
|
)
|
||||||
)
|
)
|
||||||
for c in cmds
|
return VerticalScroll(*children, classes="kcol")
|
||||||
]
|
|
||||||
return VerticalScroll(
|
def _list_view_widget(self):
|
||||||
Static(f"{label} ({cnt})", classes="ktitle"),
|
# Vue Liste : tous les menus empilés verticalement, chaque menu
|
||||||
ListView(*items),
|
# avec ses sections et ses commandes (icônes), pour aider le choix.
|
||||||
classes="kcol",
|
blocks = []
|
||||||
)
|
for (label, path, cnt, cmds) in columns:
|
||||||
|
blocks.append(
|
||||||
|
Static(f"▸ {path} ({cnt})", classes="listmenu")
|
||||||
|
)
|
||||||
|
for section, group in _group_by_section(cmds):
|
||||||
|
if section:
|
||||||
|
blocks.append(
|
||||||
|
Static(
|
||||||
|
f" ── {_disp(section)} ──", classes="listsec"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
blocks.append(
|
||||||
|
ListView(
|
||||||
|
*[
|
||||||
|
CmdItem(
|
||||||
|
f" · {_disp(c['label'])}",
|
||||||
|
c.get("method"),
|
||||||
|
c.get("kwargs"),
|
||||||
|
c.get("path"),
|
||||||
|
)
|
||||||
|
for c in group
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if not blocks:
|
||||||
|
return Static(t("No command found."))
|
||||||
|
return VerticalScroll(*blocks)
|
||||||
|
|
||||||
def _kanban_layout_widget(self):
|
def _kanban_layout_widget(self):
|
||||||
cols = [
|
cols = [
|
||||||
|
|
@ -604,6 +696,7 @@ def run_tui(run_app: bool = True, state: dict | None = None):
|
||||||
yield Static("", id="summary")
|
yield Static("", id="summary")
|
||||||
yield Tree("📍 TODO", id="nav")
|
yield Tree("📍 TODO", id="nav")
|
||||||
yield Container(id="kanban")
|
yield Container(id="kanban")
|
||||||
|
yield Container(id="list")
|
||||||
yield Static("", id="system")
|
yield Static("", id="system")
|
||||||
yield Footer()
|
yield Footer()
|
||||||
|
|
||||||
|
|
@ -619,11 +712,14 @@ def run_tui(run_app: bool = True, state: dict | None = None):
|
||||||
def _apply_visibility(self):
|
def _apply_visibility(self):
|
||||||
self.query_one("#nav").display = self._mode == "tree"
|
self.query_one("#nav").display = self._mode == "tree"
|
||||||
self.query_one("#kanban").display = self._mode == "kanban"
|
self.query_one("#kanban").display = self._mode == "kanban"
|
||||||
|
self.query_one("#list").display = self._mode == "list"
|
||||||
self.query_one("#system").display = self._mode == "system"
|
self.query_one("#system").display = self._mode == "system"
|
||||||
|
|
||||||
async def _restore(self):
|
async def _restore(self):
|
||||||
if self._mode == "kanban":
|
if self._mode == "kanban":
|
||||||
await self._enter_kanban()
|
await self._enter_kanban()
|
||||||
|
elif self._mode == "list":
|
||||||
|
await self._enter_list()
|
||||||
elif self._mode == "tree":
|
elif self._mode == "tree":
|
||||||
self._focus_tree_path(state.get("path"))
|
self._focus_tree_path(state.get("path"))
|
||||||
self._apply_visibility()
|
self._apply_visibility()
|
||||||
|
|
@ -637,6 +733,14 @@ def run_tui(run_app: bool = True, state: dict | None = None):
|
||||||
self._apply_visibility()
|
self._apply_visibility()
|
||||||
self._focus_kanban_path(state.get("path"))
|
self._focus_kanban_path(state.get("path"))
|
||||||
|
|
||||||
|
# -- vue Liste ------------------------------------------------------- #
|
||||||
|
async def _enter_list(self):
|
||||||
|
box = self.query_one("#list", Container)
|
||||||
|
await box.remove_children()
|
||||||
|
await box.mount(self._list_view_widget())
|
||||||
|
self._apply_visibility()
|
||||||
|
self._focus_kanban_path(state.get("path"))
|
||||||
|
|
||||||
def _focus_kanban_path(self, path):
|
def _focus_kanban_path(self, path):
|
||||||
if not path:
|
if not path:
|
||||||
return
|
return
|
||||||
|
|
@ -667,15 +771,32 @@ def run_tui(run_app: bool = True, state: dict | None = None):
|
||||||
tree.move_cursor(node)
|
tree.move_cursor(node)
|
||||||
tree.scroll_to_node(node)
|
tree.scroll_to_node(node)
|
||||||
|
|
||||||
|
def _next_view(self):
|
||||||
|
cur = self._mode if self._mode in VIEWS else "tree"
|
||||||
|
return VIEWS[(VIEWS.index(cur) + 1) % len(VIEWS)]
|
||||||
|
|
||||||
|
def _update_f3_hint(self):
|
||||||
|
# Le footer F3 annonce la PROCHAINE vue (ex. « → Liste »).
|
||||||
|
nxt = VIEW_LABELS.get(self._next_view(), self._next_view())
|
||||||
|
try:
|
||||||
|
self.bind("f3", "toggle_view", description=f"→ {nxt}")
|
||||||
|
self.refresh_bindings()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
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 = (
|
extra = (
|
||||||
f" [{self._kanban_mode}]" if self._mode == "kanban" else ""
|
f" [{self._kanban_mode}]" if self._mode == "kanban" else ""
|
||||||
)
|
)
|
||||||
|
cur_lbl = VIEW_LABELS.get(self._mode, self._mode)
|
||||||
|
nxt_lbl = VIEW_LABELS.get(self._next_view(), self._next_view())
|
||||||
|
self._update_f3_hint()
|
||||||
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('view')}: {self._mode}{extra} · "
|
f"{src} · {t('view')}: {cur_lbl}{extra} · "
|
||||||
|
f"F3 → {nxt_lbl} · "
|
||||||
f"{t('F2 system · F3/F4 views · Enter run')}"
|
f"{t('F2 system · F3/F4 views · Enter run')}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -820,11 +941,14 @@ def run_tui(run_app: bool = True, state: dict | None = None):
|
||||||
|
|
||||||
# -- actions --------------------------------------------------------- #
|
# -- actions --------------------------------------------------------- #
|
||||||
async def action_toggle_view(self):
|
async def action_toggle_view(self):
|
||||||
if self._mode == "tree":
|
# Cycle Arbre -> Kanban -> Liste -> Arbre (depuis Système on
|
||||||
self._mode = "kanban"
|
# revient dans le cycle sur la vue précédente).
|
||||||
|
cur = self._mode if self._mode in VIEWS else "tree"
|
||||||
|
self._mode = VIEWS[(VIEWS.index(cur) + 1) % len(VIEWS)]
|
||||||
|
if self._mode == "kanban":
|
||||||
await self._enter_kanban()
|
await self._enter_kanban()
|
||||||
else:
|
elif self._mode == "list":
|
||||||
self._mode = "tree"
|
await self._enter_list()
|
||||||
self._apply_visibility()
|
self._apply_visibility()
|
||||||
self._update_summary()
|
self._update_summary()
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue