Newer
Older
navi-1 / clients / terminal / tui / screens / theme_picker.py
"""Theme picker modal screen for Navi Code TUI."""

from __future__ import annotations

from textual import events
from textual.app import ComposeResult
from textual.containers import Container
from textual.screen import ModalScreen
from textual.widgets import Input, Label, ListItem, ListView, Static

from clients.terminal.tui.themes import ThemeRegistry, set_active_theme


class ThemePickerScreen(ModalScreen[str | None]):
    """A palette-like screen for switching themes with live preview.

    Dismisses with the selected theme name or None if cancelled.
    """

    NAME = "ThemePickerScreen"

    DEFAULT_CSS = """
    ThemePickerScreen {
        align: center middle;
    }
    ThemePickerScreen > Container {
        width: 60;
        height: auto;
        max-height: 24;
        border: thick $tui-primary;
        background: $tui-surface;
        padding: 0 0 1 0;
    }
    ThemePickerScreen .title {
        text-style: bold;
        color: $tui-primary;
        background: $tui-panel;
        padding: 1;
        height: auto;
        text-align: center;
    }
    ThemePickerScreen Input {
        height: auto;
        border: none;
        border-bottom: solid $tui-border;
        background: $tui-background;
        color: $tui-text;
        padding: 0 1;
        margin: 0;
    }
    ThemePickerScreen ListView {
        height: auto;
        max-height: 16;
        border: none;
        background: $tui-surface;
        padding: 0;
        margin: 0;
    }
    ThemePickerScreen ListItem {
        color: $tui-text;
        background: transparent;
        height: auto;
        padding: 0 1;
    }
    ThemePickerScreen ListItem.--highlight {
        background: $tui-selection;
        color: $tui-background;
    }
    ThemePickerScreen .empty {
        color: $tui-text-dim;
        text-align: center;
        padding: 1;
    }
    """

    BINDINGS = [
        ("escape", "dismiss_cancel", "Cancel"),
    ]

    def __init__(self, current_theme: str) -> None:
        super().__init__()
        self._current_theme = current_theme
        self._original_theme = current_theme
        self._themes = ThemeRegistry.all()
        self._filtered = list(self._themes)
        self._list_items: list[ListItem] = []

    def compose(self) -> ComposeResult:
        with Container():
            yield Static("Pick a theme (live preview)", classes="title")
            yield Input(placeholder="Type to filter themes...", id="theme-input")
            yield ListView(id="theme-list")

    def on_mount(self) -> None:
        self._render_list()
        list_view = self.query_one("#theme-list", ListView)
        list_view.focus()

    def _render_list(self) -> None:
        list_view = self.query_one("#theme-list", ListView)
        list_view.clear()
        self._list_items = []
        if not self._filtered:
            list_view.append(ListItem(Static("No matching themes", classes="empty")))
            return
        selected_index = 0
        for index, name in enumerate(self._filtered):
            label = Label(name)
            if name == self._current_theme:
                label.update(f"[b]* {name}[/b]")
                selected_index = index
            self._list_items.append(ListItem(label))
            list_view.append(self._list_items[-1])
        list_view.index = selected_index

    def _preview_theme(self, name: str) -> None:
        self._current_theme = name
        app = self.app
        app.theme = name
        set_active_theme(name)
        if hasattr(app, "_apply_theme"):
            app._apply_theme()
        self._render_list()

    def _restore_original(self) -> None:
        app = self.app
        app.theme = self._original_theme
        set_active_theme(self._original_theme)
        if hasattr(app, "_apply_theme"):
            app._apply_theme()

    def _filter(self, query: str) -> None:
        query = query.strip().lower()
        if not query:
            self._filtered = list(self._themes)
        else:
            self._filtered = [name for name in self._themes if query in name.lower()]
        self._render_list()

    def _select_highlighted(self) -> None:
        list_view = self.query_one("#theme-list", ListView)
        highlighted = list_view.index
        if highlighted is not None and 0 <= highlighted < len(self._filtered):
            self.dismiss(self._filtered[highlighted])
        elif self._filtered:
            self.dismiss(self._filtered[0])

    def on_input_changed(self, event: Input.Changed) -> None:
        self._filter(event.value)

    def on_input_submitted(self, event: Input.Submitted) -> None:
        self._select_highlighted()

    def on_list_view_selected(self, event: ListView.Selected) -> None:
        index = self._list_items.index(event.item) if event.item in self._list_items else None
        if index is not None:
            self.dismiss(self._filtered[index])

    def on_list_view_highlighted(self, event: ListView.Highlighted) -> None:
        index = self._list_items.index(event.item) if event.item in self._list_items else None
        if index is not None:
            self._preview_theme(self._filtered[index])

    def on_key(self, event: events.Key) -> None:
        list_view = self.query_one("#theme-list", ListView)
        if event.key == "down":
            list_view.action_cursor_down()
            event.stop()
            event.prevent_default()
        elif event.key == "up":
            list_view.action_cursor_up()
            event.stop()
            event.prevent_default()
        elif event.key in ("enter", "return"):
            self._select_highlighted()
            event.stop()
            event.prevent_default()
        elif event.key == "escape":
            self._restore_original()
            self.dismiss(None)
            event.stop()
            event.prevent_default()

    def action_dismiss_cancel(self) -> None:
        self._restore_original()
        self.dismiss(None)