"""Inline slash-command hints shown above the input box.
A non-interactive ``Static`` that renders the commands matching the current
input while the user is typing a ``/``-command. It is purely visual — it never
takes focus. Keyboard navigation (``Up``/``Down`` to move the highlight,
``Enter`` to run the highlighted command, ``Tab`` to complete its name) is
handled by ``_PromptInput`` via a reference to this widget.
Visibility rule: hints appear only while the input starts with ``/`` and
contains no whitespace yet (i.e. the command name is still being typed). The
moment a space appears the command name is complete and the hints hide.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from rich.text import Text
from textual.widgets import Static
if TYPE_CHECKING:
from clients.terminal.tui.commands.base import BaseCommand
# Cap the list so a one-character prefix does not push the whole input frame up
# by half a screen.
_MAX_HINTS = 8
class CommandHints(Static):
"""Render matching slash commands for the current input, with a highlight."""
DEFAULT_CSS = """
CommandHints {
display: none;
height: auto;
max-height: 10;
width: 100%;
padding: 0 1;
background: $tui-surface;
color: $tui-text;
border-top: solid $tui-border;
}
"""
def __init__(self) -> None:
super().__init__("")
self._matches: list[BaseCommand] = []
self._highlighted = 0
def update_for(self, text: str) -> None:
"""Recompute hints for ``text`` and show/hide the widget."""
# Show only while typing the command name: starts with '/' and no space.
if not text.startswith("/") or any(ch.isspace() for ch in text):
self._matches = []
self._highlighted = 0
self.display = False
return
from clients.terminal.tui.commands.registry import get_registry
self._matches = get_registry().match(text[1:])
if not self._matches:
self._highlighted = 0
self.display = False
return
# Keep the highlight valid when the match set shrinks; reset to top when
# the command name changed (highlight 0 is the exact-name-first match).
if self._highlighted >= len(self._matches):
self._highlighted = 0
self.update(self._build_content())
self.display = True
def visible(self) -> bool:
"""True when the hint list is currently shown with at least one match."""
return bool(self.display) and bool(self._matches)
def current_match(self) -> BaseCommand | None:
"""The highlighted match — what ``Enter``/``Tab`` act on."""
if not self._matches:
return None
return self._matches[self._highlighted]
def move_highlight(self, delta: int) -> bool:
"""Move the highlight by ``delta`` (wraps around). Returns True if moved."""
if len(self._matches) <= 1:
return False
self._highlighted = (self._highlighted + delta) % len(self._matches)
self.update(self._build_content())
return True
def _build_content(self) -> Text:
lines: list[Text] = []
for index, cmd in enumerate(self._matches[:_MAX_HINTS]):
meta = cmd.meta
aliases = f" ({', '.join(meta.aliases)})" if meta.aliases else ""
keybind = f" [{meta.keybind}]" if meta.keybind else ""
line = Text.assemble(
Text(f"/{meta.name}{aliases}", style="bold"),
Text(keybind, style="dim"),
Text(f" — {meta.description}", style="dim"),
)
if index == self._highlighted:
line.stylize("reverse")
lines.append(line)
return Text("\n").join(lines)