[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
|
||||
|
||||
|
||||
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)."""
|
||||
def _choice_entries(func) -> list:
|
||||
"""Entrées NUMÉROTÉES d'un menu « choices = [...] », dans l'ordre : chaque
|
||||
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):
|
||||
if (
|
||||
isinstance(node, ast.Assign)
|
||||
|
|
@ -126,16 +127,23 @@ def _choice_labels(func) -> list:
|
|||
)
|
||||
and isinstance(node.value, ast.List)
|
||||
):
|
||||
labels = []
|
||||
entries, section = [], None
|
||||
for el in node.value.elts:
|
||||
if not isinstance(el, ast.Dict):
|
||||
continue
|
||||
d = {}
|
||||
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
|
||||
if isinstance(k, ast.Constant):
|
||||
d[k.value] = _str_of(v)
|
||||
if d.get("section"):
|
||||
section = d["section"]
|
||||
continue
|
||||
lab = d.get("prompt_description") or d.get(
|
||||
"prompt_description_key"
|
||||
)
|
||||
if lab:
|
||||
entries.append({"label": lab, "section": section})
|
||||
return entries
|
||||
return []
|
||||
|
||||
|
||||
|
|
@ -236,15 +244,20 @@ def build_code_tree(todo_path=None) -> dict | None:
|
|||
seen.add(method)
|
||||
func = methods[method]
|
||||
disp = _dispatch(func)
|
||||
clabels = _choice_labels(func)
|
||||
centries = _choice_entries(func)
|
||||
for num in sorted(disp):
|
||||
target, kwargs = disp[num]
|
||||
if target in labels: # sous-menu
|
||||
node["children"].append(build(target))
|
||||
else: # commande (feuille) exécutable
|
||||
entry = (
|
||||
centries[num - 1]
|
||||
if 0 <= num - 1 < len(centries)
|
||||
else None
|
||||
)
|
||||
lab = (
|
||||
clabels[num - 1]
|
||||
if 0 <= num - 1 < len(clabels)
|
||||
entry["label"]
|
||||
if entry
|
||||
else target.lstrip("_").replace("_", " ")
|
||||
)
|
||||
node["children"].append(
|
||||
|
|
@ -254,6 +267,7 @@ def build_code_tree(todo_path=None) -> dict | None:
|
|||
"children": [],
|
||||
"method": target,
|
||||
"kwargs": kwargs,
|
||||
"section": entry["section"] if entry else None,
|
||||
}
|
||||
)
|
||||
seen.discard(method)
|
||||
|
|
@ -480,6 +494,41 @@ def _fmt_uptime(secs):
|
|||
# Modes d'affichage du Kanban (F4 les fait défiler).
|
||||
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):
|
||||
"""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; }
|
||||
Tree { border: solid $accent; }
|
||||
#kanban { display: none; height: 1fr; }
|
||||
#list { display: none; height: 1fr; }
|
||||
#system { display: none; height: 1fr; padding: 1 2; }
|
||||
.kcol { width: 40; border: solid $accent; margin: 0 1 0 0; }
|
||||
.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; }
|
||||
.lanetitle { height: 1; color: $secondary; }
|
||||
.kgrid { grid-size: 3; grid-gutter: 1; }
|
||||
|
|
@ -534,7 +587,7 @@ def run_tui(run_app: bool = True, state: dict | None = None):
|
|||
BINDINGS = [
|
||||
("q", "quit", "Quitter"),
|
||||
("f2", "system", "Système"),
|
||||
("f3", "toggle_view", "Arbre/Kanban"),
|
||||
("f3", "toggle_view", "Vue"),
|
||||
("f4", "kanban_layout", "Disposition Kanban"),
|
||||
("e", "expand_all", "Tout déplier"),
|
||||
("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 ------------------------------ #
|
||||
def _col_widget(self, label, cnt, cmds):
|
||||
items = [
|
||||
CmdItem(
|
||||
f"· {c['label']}",
|
||||
c.get("method"),
|
||||
c.get("kwargs"),
|
||||
c.get("path"),
|
||||
# Regroupe par section pour aider au choix ; icônes via _disp().
|
||||
children = [Static(f"{label} ({cnt})", classes="ktitle")]
|
||||
for section, group in _group_by_section(cmds):
|
||||
if section:
|
||||
children.append(
|
||||
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(
|
||||
Static(f"{label} ({cnt})", classes="ktitle"),
|
||||
ListView(*items),
|
||||
classes="kcol",
|
||||
)
|
||||
return VerticalScroll(*children, classes="kcol")
|
||||
|
||||
def _list_view_widget(self):
|
||||
# Vue Liste : tous les menus empilés verticalement, chaque menu
|
||||
# avec ses sections et ses commandes (icônes), pour aider le choix.
|
||||
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):
|
||||
cols = [
|
||||
|
|
@ -604,6 +696,7 @@ def run_tui(run_app: bool = True, state: dict | None = None):
|
|||
yield Static("", id="summary")
|
||||
yield Tree("📍 TODO", id="nav")
|
||||
yield Container(id="kanban")
|
||||
yield Container(id="list")
|
||||
yield Static("", id="system")
|
||||
yield Footer()
|
||||
|
||||
|
|
@ -619,11 +712,14 @@ def run_tui(run_app: bool = True, state: dict | None = None):
|
|||
def _apply_visibility(self):
|
||||
self.query_one("#nav").display = self._mode == "tree"
|
||||
self.query_one("#kanban").display = self._mode == "kanban"
|
||||
self.query_one("#list").display = self._mode == "list"
|
||||
self.query_one("#system").display = self._mode == "system"
|
||||
|
||||
async def _restore(self):
|
||||
if self._mode == "kanban":
|
||||
await self._enter_kanban()
|
||||
elif self._mode == "list":
|
||||
await self._enter_list()
|
||||
elif self._mode == "tree":
|
||||
self._focus_tree_path(state.get("path"))
|
||||
self._apply_visibility()
|
||||
|
|
@ -637,6 +733,14 @@ def run_tui(run_app: bool = True, state: dict | None = None):
|
|||
self._apply_visibility()
|
||||
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):
|
||||
if not path:
|
||||
return
|
||||
|
|
@ -667,15 +771,32 @@ def run_tui(run_app: bool = True, state: dict | None = None):
|
|||
tree.move_cursor(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):
|
||||
total = sum(paths.values())
|
||||
src = t("tree from code") if code_tree else t("visited paths only")
|
||||
extra = (
|
||||
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(
|
||||
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')}"
|
||||
)
|
||||
|
||||
|
|
@ -820,11 +941,14 @@ def run_tui(run_app: bool = True, state: dict | None = None):
|
|||
|
||||
# -- actions --------------------------------------------------------- #
|
||||
async def action_toggle_view(self):
|
||||
if self._mode == "tree":
|
||||
self._mode = "kanban"
|
||||
# Cycle Arbre -> Kanban -> Liste -> Arbre (depuis Système on
|
||||
# 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()
|
||||
else:
|
||||
self._mode = "tree"
|
||||
elif self._mode == "list":
|
||||
await self._enter_list()
|
||||
self._apply_visibility()
|
||||
self._update_summary()
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue