From 90cc9c48b28e258dc8eca6016af844e273108898 Mon Sep 17 00:00:00 2001 From: subzeroid <143403577+subzeroid@users.noreply.github.com> Date: Thu, 28 May 2026 02:23:23 +0300 Subject: [PATCH] feat: per-theme descriptions + accent colour in /theme completion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `/theme ` completion dropdown showed the same generic argparse-help line as the meta for every theme. Give each theme its own one-line description (theme_description in ui/theme.py) and render the theme name in its own accent colour in the popup, so the list hints at each theme before you commit. The picker footer shows the description too. Full live banner preview stays in the bare-`/theme` picker — completion menus are passive (no on-highlight hook), so a colour + description hint is the most the dropdown can offer. Co-Authored-By: Claude Opus 4.7 (1M context) --- insto/repl.py | 26 ++++++++++++++++++++------ insto/ui/theme.py | 17 +++++++++++++++++ tests/test_repl_completer.py | 13 +++++++++++++ tests/test_theme.py | 15 +++++++++++++++ 4 files changed, 65 insertions(+), 6 deletions(-) diff --git a/insto/repl.py b/insto/repl.py index d0aab99..1259010 100644 --- a/insto/repl.py +++ b/insto/repl.py @@ -45,7 +45,7 @@ from insto.config import Config, cli_history_path, load_config from insto.exceptions import BackendError from insto.ui.banner import render_welcome -from insto.ui.theme import get_palette, get_theme, list_themes +from insto.ui.theme import get_palette, get_theme, list_themes, theme_description if TYPE_CHECKING: from prompt_toolkit.key_binding import KeyPressEvent @@ -95,6 +95,11 @@ def _first_positional_choices(spec: Any) -> tuple[str, ...]: return () +def _theme_label(name: str) -> FormattedText: + """Theme name rendered in its own accent colour (for completion display).""" + return FormattedText([(f"fg:{get_palette(name).accent}", name)]) + + class _SlashCommandCompleter(Completer): """Slack/Claude-Code-style command completer. @@ -165,12 +170,13 @@ def get_completions( spec = COMMANDS.get(user_typed) if spec is not None: + is_theme = user_typed == "theme" for choice in _first_positional_choices(spec): yield Completion( text=f"{prefix} {choice}", start_position=-len(prefix), - display=choice, - display_meta="", + display=_theme_label(choice) if is_theme else choice, + display_meta=theme_description(choice) if is_theme else "", ) def _argument_completions(self, text: str, stripped: str) -> Iterable[Completion]: @@ -209,6 +215,7 @@ def _argument_completions(self, text: str, stripped: str) -> Iterable[Completion if not choices: return lower = current_word.lower() + is_theme = cmd_name == "theme" for choice in choices: s = str(choice) if not s.lower().startswith(lower): @@ -216,8 +223,12 @@ def _argument_completions(self, text: str, stripped: str) -> Iterable[Completion yield Completion( text=s, start_position=-len(current_word), - display=s, - display_meta=str(action.help or ""), + # Themes render their name in their own accent colour + # with a per-theme description, so the popup hints at + # the look before you commit (full preview lives in the + # bare-`/theme` picker). + display=_theme_label(s) if is_theme else s, + display_meta=theme_description(s) if is_theme else str(action.help or ""), ) return pos_index += 1 @@ -415,7 +426,10 @@ def banner() -> ANSI: def footer() -> ANSI: name = themes[state["i"]] - return ANSI(f"\n ↑/↓ preview · Enter apply · Esc cancel theme: {name}") + return ANSI( + f"\n ↑/↓ preview · Enter apply · Esc cancel " + f"theme: {name} — {theme_description(name)}" + ) kb = KeyBindings() diff --git a/insto/ui/theme.py b/insto/ui/theme.py index a896fcc..e75abb9 100644 --- a/insto/ui/theme.py +++ b/insto/ui/theme.py @@ -133,6 +133,23 @@ def _make_theme(p: _Palette) -> Theme: } +# One-line descriptions surfaced in /theme completion and the picker. Keep a +# key for every palette above (guarded by tests/test_theme.py). +_DESCRIPTIONS: dict[str, str] = { + "aiograpi": "instagrapi violet → blue (default)", + "claude": "Claude Code burnt orange", + "instagram": "Instagram brand gradient", + "hacker": "matrix phosphor green on black", + "amber": "80s amber CRT phosphor", + "cyberpunk": "neon cyan → magenta → green", +} + + +def theme_description(name: str | None) -> str: + """One-line description of a theme (for /theme completion + picker footer).""" + return _DESCRIPTIONS.get(name or DEFAULT_THEME_NAME, "") + + THEMES: dict[str, Theme] = {name: _make_theme(p) for name, p in _PALETTES.items()} DEFAULT_THEME_NAME: str = "aiograpi" diff --git a/tests/test_repl_completer.py b/tests/test_repl_completer.py index a0d02fb..e80c068 100644 --- a/tests/test_repl_completer.py +++ b/tests/test_repl_completer.py @@ -393,3 +393,16 @@ async def runner() -> None: asyncio.run(runner()) out = _console.export_text(styles=False) assert "unknown command" in out + + +def test_theme_arg_completions_have_per_theme_meta() -> None: + # `/theme ` shows each theme with its OWN description (not one generic + # argparse-help line repeated for every choice). + completer = _completer() + doc = Document(text="/theme ", cursor_position=len("/theme ")) + comps = list(completer.get_completions(doc, complete_event=None)) # type: ignore[arg-type] + metas = {c.text: c.display_meta_text for c in comps} + assert "hacker" in metas + assert "green" in metas["hacker"].lower() + # descriptions differ across themes + assert len({m for m in metas.values() if m}) > 1 diff --git a/tests/test_theme.py b/tests/test_theme.py index c57628c..dade143 100644 --- a/tests/test_theme.py +++ b/tests/test_theme.py @@ -14,6 +14,7 @@ get_theme, is_known, list_themes, + theme_description, ) # Style keys every theme must resolve (mirrors `_make_theme`). @@ -65,3 +66,17 @@ def test_unknown_theme_falls_back_to_default() -> None: assert get_theme("nope") is THEMES[DEFAULT_THEME_NAME] assert get_palette(None) is get_palette(DEFAULT_THEME_NAME) assert not is_known("nope") + + +def test_every_theme_has_a_description() -> None: + seen = set() + for name in list_themes(): + desc = theme_description(name) + assert desc, f"theme {name!r} has no description" + seen.add(desc) + # descriptions are distinct (not the same generic line for all) + assert len(seen) == len(list_themes()) + + +def test_theme_description_hacker() -> None: + assert "green" in theme_description("hacker").lower()