"""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)