Every main-menu item leads with its own emoji

Tools gain an icon attribute — menu-only, so result windows and the
CLI keep the plain title — and the menu loop composes it into the
aligned name lane. Single-codepoint, emoji-presentation glyphs only,
so the two-cell width holds across terminals: 💬 Chat, 📻 Channels,
🌍 Map, 👥 Nodes, 📡 Send advert, 🔧 configuration, 🔨 actions,
📋 info, 🎯/👣 the traces, 🎧 Monitor, 📶 TX optimize, 🚪 Quit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jean-Pierre MartineauandClaude Fable 5 committed 2026-07-11 03:27:35 -04:00
1 parent 5027d05943
commit 87d2d10dff
13 files changed
+25 -3

No files matched your search

+1
View File
@@ -27,6 +27,7 @@ class SendAdvertTool(Tool):
name = "advert"
title = "Send advert"
icon = "📡"
help = "Announce this node to the mesh — zero-hop, flood, or share as QR"
category = "Device"
order = 10 # right after Device actions, before Device info
+5
View File
@@ -42,6 +42,10 @@ class Tool(ABC):
window. Capitalized, and kept identical to the screen the item opens so the
menu never promises one name and delivers another. Falls back to ``name``.
help: One-line description shown in the menu and ``--help`` (no trailing period).
icon: Emoji drawn before the title in the interactive menu (and only there — the
result window keeps the plain title). Single-codepoint, emoji-presentation
glyphs only, so the two-cell width holds across terminals (see
:mod:`meshterm.ui.tui.emoji_width`).
category: Grouping label used to organize the interactive menu.
order: Sort key within a category (lower sorts first).
menu_visible: Whether the tool appears in the interactive menu. Set ``False`` for
@@ -52,6 +56,7 @@ class Tool(ABC):
name: str = ""
title: str = ""
help: str = ""
icon: str = ""
category: str = "General"
order: int = 100
menu_visible: bool = True
+1
View File
@@ -36,6 +36,7 @@ class ChannelsTool(Tool):
name = "channels"
title = "Channels"
icon = "📻"
help = "Create, join, and share mesh channels (with QR codes)"
category = "Messaging"
order = 20
+1
View File
@@ -44,6 +44,7 @@ class ChatTool(Tool):
name = "chat"
title = "Chat"
icon = "💬"
help = "Channel and direct messaging — live chat, with stored history"
category = "Messaging"
order = 10
+1
View File
@@ -36,6 +36,7 @@ class ConfigTool(Tool):
name = "config"
title = "Device configuration"
icon = "🔧"
help = "View and change device settings — identity, radio, tuning, …"
category = "Device"
order = 8
+1
View File
@@ -30,6 +30,7 @@ class DeviceActionsTool(Tool):
name = "device-actions"
title = "Device actions"
icon = "🔨"
help = "Act on the device itself — advert, backup, identity key, reboot, reset"
category = "Device"
order = 9
+1
View File
@@ -26,6 +26,7 @@ class InfoTool(Tool):
name = "info"
title = "Device info"
icon = "📋"
help = "Show device status, identity, and radio config"
category = "Device"
order = 11 # after Send advert (10), which slots in behind Device actions
+1
View File
@@ -35,6 +35,7 @@ class MapTool(Tool):
name = "map"
title = "Map"
icon = "🌍"
help = "Show mesh nodes on a street map (pannable; repeaters highlighted)"
category = "Messaging"
order = 30
+1
View File
@@ -36,6 +36,7 @@ class MonitorTool(Tool):
name = "monitor"
title = "Monitor"
icon = "🎧"
help = "Capture overheard packets live for a while and summarize them"
category = "Diagnostics"
order = 20
+1
View File
@@ -16,6 +16,7 @@ class NodesTool(Tool):
name = "nodes"
title = "Nodes"
icon = "👥"
help = "List this node and known contacts (last heard, packets, type, key)"
category = "Device"
order = 12
+2
View File
@@ -48,6 +48,7 @@ class TraceTool(Tool):
name = "trace"
title = "Trace target"
icon = "🎯"
help = "Trace a target over a mirrored route and watch per-hop SNR"
category = "Diagnostics"
order = 10
@@ -222,6 +223,7 @@ class TracePathTool(Tool):
name = "trace-path"
title = "Trace path"
icon = "👣"
help = "Walk a hand-composed route — out and back your way"
category = "Diagnostics"
order = 11
+1
View File
@@ -37,6 +37,7 @@ class TxOptimizeTool(Tool):
name = "tx-optimize"
title = "TX optimize"
icon = "📶"
help = "Tune a remote node's TX power for the best signal at a target"
category = "Diagnostics"
order = 15 # right after Trace, its measurement sibling
+8 -3
View File
@@ -322,20 +322,25 @@ async def _menu_loop(ctx: AppContext, session: TuiSession) -> None:
# Two aligned columns — tool name, then its muted description — with no header
# line: these are commands, not tabular data, so the alignment alone carries it.
tools = [tool for tool in all_tools() if tool.menu_visible]
name_w = max((cell_len(tool.title or tool.name) for tool in tools), default=0)
def _label(tool) -> str: # noqa: ANN001 - registry Tool; typed at the source
title = tool.title or tool.name
return f"{tool.icon} {title}" if tool.icon else title
name_w = max((cell_len(_label(tool)) for tool in tools), default=0)
items: list = []
current_category: str | None = None
for tool in tools:
if tool.category != current_category:
current_category = tool.category
items.append(Separator(f"── {current_category} ──", style="accent"))
label = tool.title or tool.name
label = _label(tool)
row = Text(label)
row.append(" " * (name_w - cell_len(label) + 2))
row.append(tool.help, style="muted")
items.append(Choice(title=row, value=tool.name))
items.append(Separator(" "))
items.append(Choice(title="Quit", value="__quit__"))
items.append(Choice(title="🚪 Quit", value="__quit__"))
# Drive the menu list ourselves (rather than via session.select) so it stays on the
# stack while the quit dialog floats over it: the confirm is drawn as a centered box