Initial commit
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
"""Command palette for the CLI board."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from rich.console import Group, RenderableType
|
||||
from rich.text import Text
|
||||
from textual.app import ComposeResult
|
||||
from textual.containers import Vertical
|
||||
from textual.screen import ModalScreen
|
||||
from textual.widgets import Input, Static
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PaletteCommand:
|
||||
command_id: str
|
||||
label: str
|
||||
description: str = ""
|
||||
keys: str = ""
|
||||
|
||||
|
||||
class CommandPaletteScreen(ModalScreen[str | None]):
|
||||
"""Small command palette with filtering and keyboard navigation."""
|
||||
|
||||
DEFAULT_CSS = """
|
||||
CommandPaletteScreen {
|
||||
align: center middle;
|
||||
}
|
||||
|
||||
.palette-dialog {
|
||||
width: 96;
|
||||
max-width: 95%;
|
||||
height: auto;
|
||||
border: solid $primary;
|
||||
background: $surface;
|
||||
padding: 1 2;
|
||||
}
|
||||
|
||||
#palette-filter {
|
||||
margin-top: 1;
|
||||
}
|
||||
|
||||
#palette-list {
|
||||
margin-top: 1;
|
||||
max-height: 18;
|
||||
}
|
||||
|
||||
.palette-help {
|
||||
margin-top: 1;
|
||||
color: $text-muted;
|
||||
}
|
||||
"""
|
||||
|
||||
BINDINGS = [
|
||||
("escape", "close", "Close"),
|
||||
("down", "cursor_down", "Down"),
|
||||
("up", "cursor_up", "Up"),
|
||||
("enter", "submit", "Run"),
|
||||
]
|
||||
|
||||
def __init__(self, *, title: str, commands: list[PaletteCommand]) -> None:
|
||||
super().__init__()
|
||||
self.title_text = title
|
||||
self.commands = list(commands)
|
||||
self.cursor = 0
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Vertical(classes="palette-dialog"):
|
||||
yield Static(self.title_text, id="palette-title")
|
||||
yield Input(placeholder="Type to filter commands", id="palette-filter")
|
||||
yield Static(id="palette-list")
|
||||
yield Static("Enter to run, Esc to close, Up/Down to navigate.", classes="palette-help")
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self.set_focus(self.query_one("#palette-filter", Input))
|
||||
self._refresh_list()
|
||||
|
||||
def on_input_changed(self, event: Input.Changed) -> None:
|
||||
if event.input.id != "palette-filter":
|
||||
return
|
||||
self.cursor = 0
|
||||
self._refresh_list()
|
||||
|
||||
def on_input_submitted(self, event: Input.Submitted) -> None:
|
||||
if event.input.id == "palette-filter":
|
||||
self.action_submit()
|
||||
|
||||
def action_cursor_down(self) -> None:
|
||||
commands = self._filtered_commands()
|
||||
if not commands:
|
||||
return
|
||||
self.cursor = min(len(commands) - 1, self.cursor + 1)
|
||||
self._refresh_list()
|
||||
|
||||
def action_cursor_up(self) -> None:
|
||||
commands = self._filtered_commands()
|
||||
if not commands:
|
||||
return
|
||||
self.cursor = max(0, self.cursor - 1)
|
||||
self._refresh_list()
|
||||
|
||||
def action_submit(self) -> None:
|
||||
commands = self._filtered_commands()
|
||||
if not commands:
|
||||
self.dismiss(None)
|
||||
return
|
||||
self.dismiss(commands[self.cursor].command_id)
|
||||
|
||||
def action_close(self) -> None:
|
||||
self.dismiss(None)
|
||||
|
||||
def _refresh_list(self) -> None:
|
||||
self.query_one("#palette-list", Static).update(self._render_list())
|
||||
|
||||
def _render_list(self) -> RenderableType:
|
||||
commands = self._filtered_commands()
|
||||
if not commands:
|
||||
return Text("No commands match the current query.", style="dim")
|
||||
rows: list[Text] = []
|
||||
for index, command in enumerate(commands):
|
||||
selected = index == self.cursor
|
||||
row = Text(style="black on #22d3ee" if selected else "white")
|
||||
row.append(command.label, style="bold" if not selected else "bold black on #22d3ee")
|
||||
if command.keys:
|
||||
row.append(f" {command.keys}", style="dim" if not selected else "black on #22d3ee")
|
||||
if command.description:
|
||||
row.append(f"\n{command.description}", style="dim" if not selected else "black on #22d3ee")
|
||||
rows.append(row)
|
||||
return Group(*rows)
|
||||
|
||||
def _filtered_commands(self) -> list[PaletteCommand]:
|
||||
query = self.query_one("#palette-filter", Input).value.strip().casefold()
|
||||
if not query:
|
||||
return self.commands
|
||||
filtered = [
|
||||
command
|
||||
for command in self.commands
|
||||
if query in " ".join([command.label, command.description, command.keys]).casefold()
|
||||
]
|
||||
if self.cursor >= len(filtered):
|
||||
self.cursor = max(0, len(filtered) - 1)
|
||||
return filtered
|
||||
Reference in New Issue
Block a user