Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 20 additions & 6 deletions insto/repl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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]:
Expand Down Expand Up @@ -209,15 +215,20 @@ 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):
continue
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
Expand Down Expand Up @@ -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()

Expand Down
17 changes: 17 additions & 0 deletions insto/ui/theme.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
13 changes: 13 additions & 0 deletions tests/test_repl_completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Tab>` 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
15 changes: 15 additions & 0 deletions tests/test_theme.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
get_theme,
is_known,
list_themes,
theme_description,
)

# Style keys every theme must resolve (mirrors `_make_theme`).
Expand Down Expand Up @@ -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()
Loading