"""Reusable search input screen for TUI apps. A modal input dialog that can be used for search across all apps. """ from typing import Optional from textual.app import ComposeResult from textual.binding import Binding from textual.containers import Vertical, Horizontal from textual.screen import ModalScreen from textual.widgets import Input, Static, Label, Button class SearchScreen(ModalScreen[str | None]): """A modal screen for search input. Returns the search query string on submit, or None on cancel. """ DEFAULT_CSS = """ SearchScreen { align: center middle; } SearchScreen > Vertical { width: 60; height: auto; border: solid $primary; background: $surface; padding: 1 2; } SearchScreen > Vertical > Label { margin-bottom: 1; } SearchScreen > Vertical > Input { margin-bottom: 1; } SearchScreen > Vertical > Horizontal { height: auto; align: right middle; } SearchScreen > Vertical > Horizontal > Button { margin-left: 1; } """ BINDINGS = [ Binding("escape", "cancel", "Cancel", show=True), ] def __init__( self, title: str = "Search", placeholder: str = "Enter search query...", initial_value: str = "", name: Optional[str] = None, id: Optional[str] = None, classes: Optional[str] = None, ) -> None: super().__init__(name=name, id=id, classes=classes) self._title = title self._placeholder = placeholder self._initial_value = initial_value def compose(self) -> ComposeResult: with Vertical(): yield Label(self._title) yield Input( placeholder=self._placeholder, value=self._initial_value, id="search-input", ) with Horizontal(): yield Button("Search", variant="primary", id="search-btn") yield Button("Cancel", variant="default", id="cancel-btn") def on_mount(self) -> None: """Focus the input on mount.""" self.query_one("#search-input", Input).focus() def on_input_submitted(self, event: Input.Submitted) -> None: """Handle Enter key in input.""" self.dismiss(event.value) def on_button_pressed(self, event: Button.Pressed) -> None: """Handle button presses.""" if event.button.id == "search-btn": query = self.query_one("#search-input", Input).value self.dismiss(query) elif event.button.id == "cancel-btn": self.dismiss(None) def action_cancel(self) -> None: """Cancel the search.""" self.dismiss(None) class ClearableSearchInput(Static): """A search input widget with clear button for use in sidebars/headers. Emits SearchInput.Submitted message when user submits a query. Emits SearchInput.Cleared message when user clears the search. """ DEFAULT_CSS = """ ClearableSearchInput { height: 3; padding: 0 1; } ClearableSearchInput > Horizontal { height: auto; } ClearableSearchInput > Horizontal > Input { width: 1fr; } ClearableSearchInput > Horizontal > Button { width: 3; min-width: 3; } """ from textual.message import Message class Submitted(Message): """Search query was submitted.""" def __init__(self, query: str) -> None: super().__init__() self.query = query class Cleared(Message): """Search was cleared.""" pass def __init__( self, placeholder: str = "Search...", name: Optional[str] = None, id: Optional[str] = None, classes: Optional[str] = None, ) -> None: super().__init__(name=name, id=id, classes=classes) self._placeholder = placeholder def compose(self) -> ComposeResult: with Horizontal(): yield Input(placeholder=self._placeholder, id="search-input") yield Button("X", id="clear-btn", variant="error") def on_input_submitted(self, event: Input.Submitted) -> None: """Handle search submission.""" self.post_message(self.Submitted(event.value)) def on_button_pressed(self, event: Button.Pressed) -> None: """Handle clear button.""" if event.button.id == "clear-btn": input_widget = self.query_one("#search-input", Input) input_widget.value = "" input_widget.focus() self.post_message(self.Cleared()) @property def value(self) -> str: """Get the current search value.""" return self.query_one("#search-input", Input).value @value.setter def value(self, new_value: str) -> None: """Set the search value.""" self.query_one("#search-input", Input).value = new_value