Titles speak one language: Sentence case, no emoji, — for subjects

Every screen and dialog title now follows the same three rules. Titles
are sentence-cased ("Trace — YUL-Poly", "Dashboard — mesh overview",
"Nodes"), never bare lowercase ("trace · x", "nodes", "command line ·
x", "path width", "admin login" and a dozen siblings). An em dash
introduces a title's subject or qualifier while middle dots stay for
chained status atoms ("Map · z12 · 34 nodes · 2 km across"), so the two
separators mean different things instead of being used at random. And
no title carries an emoji — the icon belongs to the menu row; a
content-sized floating dialog whose title emoji the terminal draws a
cell narrower than Rich measures leaves the border short (the bug the
Send advert popup already documented), so the Watchtower, Courier,
Time machine, identity-key, reboot, sync, restore, location, factory
reset, and advert dialogs all drop theirs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Jean-Pierre MartineauandClaude Opus 4.8 committed 2026-07-12 03:55:12 -04:00
1 parent 241be536d8
commit d1c8ed73a0
16 files changed
+64 -62

No files matched your search

+1 -1
View File
@@ -384,7 +384,7 @@ class AtlasScreen(Screen):
def _compose_title(self, links: list[Link], matches: list[str]) -> str:
"""The status title: counts, the zoom level, and the find tally when active."""
nodes = len(self._layout_nodes(links))
title = f"Mesh Atlas · {nodes} nodes · {len(links)} links"
title = f"Mesh atlas · {nodes} nodes · {len(links)} links"
if self._zoom > _ZOOM_MIN:
title += f" · {self._zoom:.1f}×"
if self._filter:
+10 -10
View File
@@ -490,7 +490,7 @@ async def _stage_advert_cadence(
else:
prompt = "How often this node announces itself to neighbours in range:"
selected = await ctx.ui.select(
"📡 Flood advert" if flood else "📡 Direct advert",
"Flood advert" if flood else "Direct advert",
items,
prompt=prompt,
default=current,
@@ -515,7 +515,7 @@ async def _stage_location(
choice = await ctx.ui.dialog(
f"Advertised location: {_format_coords(lat, lon)}",
[("Pick on map", "map"), ("Type coordinates", "type"), ("Clear", "clear")],
title="📍 Location",
title="Location",
)
if choice is None:
return
@@ -834,7 +834,7 @@ async def _reboot(ctx: "AppContext", device: "Device", snapshot: dict) -> bool:
choice = await ctx.ui.dialog(
"Reboot the device now?",
[("Cancel", None), ("Reboot", "reboot")],
title="🔄 Reboot device",
title="Reboot device",
default=1,
danger=True,
)
@@ -895,7 +895,7 @@ async def _sync_clock(ctx: "AppContext", device: "Device", snapshot: dict) -> No
choice = await ctx.ui.dialog(
prompt,
[("Cancel", None), ("Sync", "sync")],
title="🕒 Sync clock",
title="Sync clock",
default=1,
)
if choice == "sync":
@@ -948,7 +948,7 @@ async def _restore_now(ctx: "AppContext", device: "Device", snapshot: dict) -> b
choice = await ctx.ui.dialog(
"Apply the backup now, or preview the changes first?",
[("Cancel", None), ("Preview", "preview"), ("Apply", "apply")],
title="📂 Restore from backup",
title="Restore from backup",
default=1,
)
if choice == "preview":
@@ -956,7 +956,7 @@ async def _restore_now(ctx: "AppContext", device: "Device", snapshot: dict) -> b
choice = await ctx.ui.dialog(
"Apply these changes to the device?",
[("Cancel", None), ("Apply", "apply")],
title="📂 Restore from backup",
title="Restore from backup",
default=1,
)
if choice != "apply":
@@ -973,7 +973,7 @@ async def _identity_key_menu(ctx: "AppContext", device: "Device", snapshot: dict
its snapshot.
"""
choice = await ctx.ui.select(
"🔐 Identity key",
"Identity key",
[
*menu_rows(
[
@@ -994,7 +994,7 @@ async def _identity_key_menu(ctx: "AppContext", device: "Device", snapshot: dict
"The private key IS the node's identity — anyone who sees it can impersonate "
"this node. Show it on screen?",
[("Cancel", None), ("Show key", "show")],
title="🔐 Show private key",
title="Show private key",
default=1,
danger=True,
)
@@ -1024,7 +1024,7 @@ async def _identity_key_menu(ctx: "AppContext", device: "Device", snapshot: dict
"Importing a key permanently overwrites this device's identity. Contacts and "
"messages keyed to the old identity will no longer match it.",
"IMPORT",
title="🔐 Import private key",
title="Import private key",
)
if not confirmed:
return False
@@ -1043,7 +1043,7 @@ async def _factory_reset(ctx: "AppContext", device: "Device", snapshot: dict) ->
"This erases EVERYTHING on the device — identity, contacts, channels, and every "
"setting — and cannot be undone.",
"RESET",
title="⚠ Factory reset",
title="Factory reset",
)
if not confirmed:
return False
+8 -8
View File
@@ -117,7 +117,7 @@ async def open_courier(ctx: "AppContext") -> Optional[dict[str, Any]]:
while True:
items = _menu_items(ctx, store.entries())
menu = SelectScreen(
"📨 Courier — store-and-forward outbox",
"Courier — store-and-forward outbox",
items,
default=cursor,
wrap=False,
@@ -225,7 +225,7 @@ async def _queue_flow(ctx: "AppContext", contacts: list[Contact]) -> None:
"No contacts available — connect a device that knows some nodes first.",
style="muted",
),
title="queue a message",
title="Queue a message",
)
return
@@ -246,7 +246,7 @@ async def _queue_flow(ctx: "AppContext", contacts: list[Contact]) -> None:
row.append(f" heard {_format_age(secs)}", style="muted")
items.append(Choice(row, contact))
contact = await session.select(
"✉ Courier — recipient",
"Courier — recipient",
items,
prompt="The message waits in the outbox until this node can take it:",
)
@@ -265,7 +265,7 @@ async def _queue_flow(ctx: "AppContext", contacts: list[Contact]) -> None:
if key is None:
await session.message_dialog(
Text(f"{contact.name!r} has no usable key to address.", style="err"),
title="queue a message",
title="Queue a message",
)
return
ctx.courier_store.queue(key, contact.name, text, not_before=not_before)
@@ -325,14 +325,14 @@ async def _entry_actions(ctx: "AppContext", ident: int) -> None:
f"{'s' if message.attempts != 1 else ''}",
style="muted",
)
await session.message_dialog(body, title=f"📨 {message.node_name}")
await session.message_dialog(body, title=message.node_name)
return
items = [
Choice("📤 Send now — one forced attempt", "send"),
Choice(Text.assemble(("✗ ", "err"), "Cancel this message"), "cancel"),
]
picked = await session.select(
f"📨 {message.node_name} — “{_shorten(message.text, 28)}”",
f"{message.node_name} — “{_shorten(message.text, 28)}”",
items, filterable=False,
footer_hint="↑↓ move · Enter select · Esc back",
)
@@ -344,7 +344,7 @@ async def _entry_actions(ctx: "AppContext", ident: int) -> None:
outcome = await ctx.courier.attempt_now(ident)
except Exception as exc: # noqa: BLE001 - surface the failure, keep the queue
await session.message_dialog(
Text(f"send failed: {exc}", style="err"), title="courier"
Text(f"send failed: {exc}", style="err"), title="Courier"
)
return
notes = {
@@ -362,5 +362,5 @@ async def _entry_actions(ctx: "AppContext", ident: int) -> None:
"gone": Text("this entry is no longer queued", style="muted"),
}
await session.message_dialog(
notes.get(outcome, Text(outcome)), title="courier"
notes.get(outcome, Text(outcome)), title="Courier"
)
+1 -1
View File
@@ -155,7 +155,7 @@ class DashboardScreen(Screen):
so it can attempt to decrypt an overheard channel-text packet.
"""
super().__init__()
self.title = "dashboard · mesh overview"
self.title = "Dashboard — mesh overview"
self._session = session
self._resolve = resolve
self._activity = activity
+1 -1
View File
@@ -415,7 +415,7 @@ class LocationPickScreen(MapScreen):
"""A live status title: the coordinates under the crosshair and the scale."""
base = super()._title(vp)
scale = base.rsplit("·", 1)[-1].strip()
return f"set location · {vp.center_lat:.5f}, {vp.center_lon:.5f} · z{vp.zoom} · {scale}"
return f"Set location · {vp.center_lat:.5f}, {vp.center_lon:.5f} · z{vp.zoom} · {scale}"
def handle(self, action: str, data: str = "") -> None:
"""Commit the centre on Enter; ``Home`` returns to the initial spot; else map keys."""
+1 -1
View File
@@ -49,7 +49,7 @@ class NodesScreen(Screen):
sort: The initial sort; mutated in place as the user presses the arrows.
"""
super().__init__()
self.title = "nodes"
self.title = "Nodes"
self._self_name = self_name
self._self_key = self_key
self._contacts = contacts
+1 -1
View File
@@ -137,7 +137,7 @@ class PathComposerScreen(Screen):
them, the *fetch neighbours* row appears.
"""
super().__init__()
self.title = f"compose path · {target_label}" if target_label else "compose path"
self.title = f"Compose path — {target_label}" if target_label else "Compose path"
self._target_id = target_id
self._target_hash = (target_hash or "").lower().removeprefix("0x")
self._target_label = target_label
+1 -1
View File
@@ -61,7 +61,7 @@ class RemoteCliScreen(Screen):
session: The running TUI session (for repaints).
"""
super().__init__()
self.title = f"command line · {node_label}"
self.title = f"Command line — {node_label}"
self._node_label = node_label
self._send = send
self._session = session
+9 -9
View File
@@ -126,7 +126,7 @@ async def _login(ctx: "AppContext", device: "Device", node: Contact) -> bool:
+ "try again to enter a new one.",
style="err",
),
title="admin login",
title="Admin login",
)
return False
ctx.admin_store.remember(node, password)
@@ -175,14 +175,14 @@ async def _admin_session(
elif choice == _ADVERT:
await _simple_action(
ctx, device, node, "advert",
title="📡 Send advert",
title="Send advert",
prompt=f"Ask {node.name} to announce itself (flood) now?",
commit="Send",
)
elif choice == _CLOCK:
await _simple_action(
ctx, device, node, "clock sync",
title="🕒 Sync clock",
title="Sync clock",
prompt=f"Set {node.name}'s clock from this companion's time?",
commit="Sync",
)
@@ -191,7 +191,7 @@ async def _admin_session(
elif choice == _REBOOT:
await _simple_action(
ctx, device, node, "reboot",
title="🔄 Reboot node",
title="Reboot node",
prompt=f"Reboot {node.name} now? It drops off the mesh while booting.",
commit="Reboot",
danger=True,
@@ -375,7 +375,7 @@ async def _apply(
if i < total:
await asyncio.sleep(ctx.settings.trace_cooldown_s)
aborted = await _run_under_dialog(ctx, f"applying · {node.name}", work)
aborted = await _run_under_dialog(ctx, f"Applying — {node.name}", work)
ctx.repo.finish_run(
run_id, "error" if aborted else "ok",
{"applied": applied, "staged_left": len(pending)},
@@ -387,7 +387,7 @@ async def _apply(
summary.append(" (aborted — the rest stay staged)", style="warn")
await ctx.ui.session.message_dialog(
Text("\n").join([summary, Text(), *outcomes]) if outcomes else summary,
title=f"apply · {node.name}",
title=f"Apply — {node.name}",
)
return applied
@@ -420,7 +420,7 @@ async def _read_all(ctx: "AppContext", device: "Device", node: Contact) -> None:
if i < len(specs):
await asyncio.sleep(ctx.settings.trace_cooldown_s)
aborted = await _run_under_dialog(ctx, f"reading · {node.name}", work)
aborted = await _run_under_dialog(ctx, f"Reading — {node.name}", work)
ctx.repo.finish_run(
run_id, "error" if aborted else "ok", {"read": read, "asked": len(specs)}
)
@@ -517,7 +517,7 @@ async def _change_password(ctx: "AppContext", device: "Device", node: Contact) -
confirmed = await ctx.ui.dialog(
f"Change {node.name}'s admin password now? You will need the new one everywhere.",
[("Cancel", None), ("Change", "go")],
title="🔐 Admin password",
title="Admin password",
default=1,
danger=True,
)
@@ -538,7 +538,7 @@ async def _change_password(ctx: "AppContext", device: "Device", node: Contact) -
)
else:
body = Text(reply.strip(), style="err")
await ctx.ui.session.message_dialog(body, title="admin password")
await ctx.ui.session.message_dialog(body, title="Admin password")
# --- the command line ---------------------------------------------------------------
+3 -3
View File
@@ -165,7 +165,7 @@ class TimeMachineScreen(Screen):
def _set_title(self) -> None:
name, _delta = _WINDOWS[self._window_index]
self.title = f"⏳ {self._label} · {name}"
self.title = f"{self._label} · {name}"
def handle(self, action: str, data: str = "") -> None:
"""Scroll, cycle the window, or dismiss."""
@@ -742,7 +742,7 @@ async def open_timemachine(ctx: "AppContext") -> None:
"MeshTerm listens. Come back after the mesh has talked a while.",
style="muted",
),
title="⏳ Time machine",
title="Time machine",
)
return
# Stored names first, the contact resolver filling the blanks (the app-wide
@@ -770,7 +770,7 @@ async def open_timemachine(ctx: "AppContext") -> None:
)
)
picked = await session.select(
"⏳ Time machine — pick a subject",
"Time machine — pick a subject",
items,
prompt="Everything the recorder ever heard, explorable:",
)
+16 -14
View File
@@ -148,7 +148,7 @@ class TracingDialog(Screen):
"""Build the dialog.
Args:
title: Heading for the dialog border (e.g. ``tracing · YUL-Poly``).
title: Heading for the dialog border (e.g. ``Tracing — YUL-Poly``).
spinner: The spinner to animate (shared with the owner's ticker).
on_abort: Invoked when the user asks to abort (idempotent expected).
"""
@@ -289,7 +289,7 @@ class TraceScreen(Screen):
the screen never claims a route the radio wasn't given.
"""
super().__init__()
self.title = f"trace · {target}" if mode == "target" else "trace path"
self.title = f"Trace — {target}" if mode == "target" else "Trace path"
self._target = target
self._mode = mode
self._flight_label = target if mode == "target" else "path"
@@ -364,7 +364,7 @@ class TraceScreen(Screen):
"""
total = max(1, int(self._sample_count()))
dialog = TracingDialog(
f"tracing · {self._flight_label}", spinner=self._spinner, on_abort=self.cancel
f"Tracing — {self._flight_label}", spinner=self._spinner, on_abort=self.cancel
)
self._flight = dialog
self._session.push(dialog)
@@ -1047,7 +1047,7 @@ async def _open_session(ctx: "AppContext", target: Optional[str]) -> int:
f"{target!r} isn't a known contact or a hex key prefix, so a forced "
"path can't end at it. Trace it device-routed, or pick a contact.",
),
title="no destination hash",
title="No destination hash",
)
async def fetch_neighbours_via(repeater: Contact, repeater_id: str) -> bool:
@@ -1083,7 +1083,7 @@ async def _open_session(ctx: "AppContext", target: Optional[str]) -> int:
)
spinner = Spinner()
dialog = TracingDialog(
f"neighbours · {repeater.name}", spinner=spinner, on_abort=lambda: None
f"Fetch neighbours — {repeater.name}", spinner=spinner, on_abort=lambda: None
)
dialog.show_last = False # a fetch has no streaming replies to echo
dialog.status = f"logging in to {repeater.name}…"
@@ -1138,7 +1138,7 @@ async def _open_session(ctx: "AppContext", target: Optional[str]) -> int:
run_id, "error", {"error": str(error) or type(error).__name__}
)
await session.message_dialog(
Text(str(error), style="err"), title="fetch neighbours"
Text(str(error), style="err"), title="Fetch neighbours"
)
return False
ctx.repo.record_neighbours(run_id, repeater_id, entries)
@@ -1154,7 +1154,7 @@ async def _open_session(ctx: "AppContext", target: Optional[str]) -> int:
"it relearns neighbours from received adverts, so ask again later.",
style="muted",
),
title="fetch neighbours",
title="Fetch neighbours",
)
return False
return True
@@ -1251,7 +1251,7 @@ async def _open_session(ctx: "AppContext", target: Optional[str]) -> int:
items.append(Choice(title=title, value=w))
picked = await session.run_screen(
SelectScreen(
"path width",
"Path width",
items,
prompt="Forced hops are addressed by this many leading key bytes.",
default=width_bytes,
@@ -1288,7 +1288,7 @@ async def _open_session(ctx: "AppContext", target: Optional[str]) -> int:
items.append(Choice(title=title, value=n))
picked = await session.run_screen(
SelectScreen(
"sample count",
"Sample count",
items,
prompt=f"One Trace action runs this many traces, {pace:g} s apart.",
default=sample_count,
@@ -1367,7 +1367,9 @@ async def _open_session(ctx: "AppContext", target: Optional[str]) -> int:
ctx.profile_name,
)
spinner = Spinner()
dialog = TracingDialog(f"probing · {target_label}", spinner=spinner, on_abort=lambda: None)
dialog = TracingDialog(
f"Probing — {target_label}", spinner=spinner, on_abort=lambda: None
)
dialog.status = f"path 1/{len(candidates)} · one trace each"
def on_result(index: int, done: int, result: TraceResult) -> None:
@@ -1431,7 +1433,7 @@ async def _open_session(ctx: "AppContext", target: Optional[str]) -> int:
run_id, "error", {"error": str(error) or type(error).__name__}
)
await session.message_dialog(
Text(f"probe failed: {error}", style="err"), title="path probe"
Text(f"probe failed: {error}", style="err"), title="Path probe"
)
return None
best = outcomes[0] if outcomes else None
@@ -1460,7 +1462,7 @@ async def _open_session(ctx: "AppContext", target: Optional[str]) -> int:
await session.message_dialog(
Text("no observed evidence involving this target yet — run a trace or "
"let monitoring accumulate paths first.", style="muted"),
title="explore paths",
title="Explore paths",
)
return None
@@ -1481,7 +1483,7 @@ async def _open_session(ctx: "AppContext", target: Optional[str]) -> int:
items.extend(back_rows(("back", None)))
picked = await session.run_screen(
SelectScreen(
f"reach {target_label} · scenarios",
f"Explore paths — {target_label}",
items,
prompt="Choose the outbound leg — the return mirrors it.",
footer_hint="↑↓ move · Enter adopt/probe · Esc back",
@@ -1512,7 +1514,7 @@ async def _open_session(ctx: "AppContext", target: Optional[str]) -> int:
result_items.append(Choice(title="Keep current path", value=None)) # the exit: no adoption
adopted = await session.run_screen(
SelectScreen(
f"probe results · {target_label}",
f"Probe results — {target_label}",
result_items,
footer_hint="↑↓ move · Enter adopt path · Esc keep current",
wrap=False,
+6 -6
View File
@@ -126,7 +126,7 @@ class TxSweepScreen(Screen):
pick_samples: Floats the per-level sample-count dialog.
"""
super().__init__()
self.title = f"tx optimize · {admin_label} → {target_label}"
self.title = f"TX optimize — {admin_label} → {target_label}"
self._admin_label = admin_label
self._target_label = target_label
self._device_label = device_label
@@ -671,7 +671,7 @@ async def open_tx_optimize(
]
picked = await session.run_screen(
SelectScreen(
"sweep range",
"Sweep range",
items,
prompt="The TX window the sweep explores:",
default=(screen.tx_min, screen.tx_max),
@@ -710,7 +710,7 @@ async def open_tx_optimize(
]
picked = await session.run_screen(
SelectScreen(
"coarse step",
"Coarse step",
items,
prompt="The coarse sweep measures the window at this spacing, then refines.",
default=screen.step,
@@ -735,7 +735,7 @@ async def open_tx_optimize(
]
picked = await session.run_screen(
SelectScreen(
"samples per level",
"Samples per level",
items,
prompt=f"Each TX level is measured with this many traces, {pace:g} s apart.",
default=screen.samples,
@@ -779,7 +779,7 @@ async def open_tx_optimize(
"The saved password was cleared; sweep again to enter a new one.",
style="err",
),
title="admin login",
title="Admin login",
)
return False
ctx.admin_store.remember(admin_node, password)
@@ -831,7 +831,7 @@ async def open_tx_optimize(
spinner = Spinner()
flight = TracingDialog(
f"sweeping · {admin_node.name} → {target_label}",
f"Sweeping — {admin_node.name} → {target_label}",
spinner=spinner,
on_abort=screen.cancel,
)
+3 -3
View File
@@ -88,7 +88,7 @@ async def open_watchtower(ctx: "AppContext") -> Optional[dict[str, Any]]:
while True:
items = _menu_items(store.alerts(), store.watched(), store.new_node_alerts)
menu = SelectScreen(
"🚨 Watchtower — alerts & watched nodes",
"Watchtower — alerts & watched nodes",
items,
default=cursor,
wrap=False,
@@ -206,7 +206,7 @@ async def _pick_node(ctx: "AppContext", contacts: list[Contact]) -> None:
if contacts
else "No contacts available — connect a device that knows some nodes first."
)
await session.message_dialog(Text(message, style="muted"), title="watch a node")
await session.message_dialog(Text(message, style="muted"), title="Watch a node")
return
def recency(pair: tuple[str, Contact]) -> float:
@@ -221,7 +221,7 @@ async def _pick_node(ctx: "AppContext", contacts: list[Contact]) -> None:
row.append(f" heard {_format_age(_age_seconds(contact.last_seen))}", style="muted")
items.append(Choice(row, (key, contact)))
picked = await session.select(
"⭐ Watch a node",
"Watch a node",
items,
prompt="Silence and SNR rules will watch it from now on:",
)
+1 -1
View File
@@ -127,7 +127,7 @@ def test_atlas_renders_graph_and_overview_panel() -> None:
assert "Homestead" in body and "YUL-Cartierville" in body and "Alice" in body
assert "trace 1" in body and "packet 1" in body
assert "rings = hops out" in body # the overview hint row
assert "Mesh Atlas · 3 nodes · 2 links" == screen.title
assert "Mesh atlas · 3 nodes · 2 links" == screen.title
def test_atlas_empty_graph_renders_guidance() -> None:
+1 -1
View File
@@ -218,7 +218,7 @@ def test_location_picker_draws_a_live_crosshair_without_keeping_it() -> None:
body = _plain(screen.render_body(80))
assert "⌖" in body and "45.5" in body # crosshair + its live coordinates
assert screen._markers == markers # not accumulated across frames
assert "set location" in screen._title(screen._viewport)
assert "Set location" in screen._title(screen._viewport)
def test_location_picker_without_nodes_or_initial_shows_the_world() -> None:
+1 -1
View File
@@ -396,7 +396,7 @@ def test_composer_path_mode_opens_us_to_us_without_auto() -> None:
the device to route to.
"""
screen = _composer(_topo(), target=False)
assert screen.title == "compose path" # no target to name
assert screen.title == "Compose path" # no target to name
preview = screen._route_preview()
assert preview.plain == "Us (aa) → Us (aa)" # endpoints carry our hash
faint = sum(