diff --git a/src/textual/app.py b/src/textual/app.py index b2169ff76..03ee3fcc2 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -122,7 +122,7 @@ from textual.screen import ( SystemModalScreen, ) from textual.signal import Signal -from textual.theme import BUILTIN_THEMES, Theme +from textual.theme import BUILTIN_THEMES, Theme, ThemeProvider from textual.timer import Timer from textual.widget import AwaitMount, Widget from textual.widgets._toast import ToastRack @@ -1527,7 +1527,12 @@ class App(Generic[ReturnType], DOMNode): def action_change_theme(self) -> None: """An [action](/guide/actions) to change the current theme.""" - self.push_screen(CommandPalette()) + self.app.push_screen( + CommandPalette( + [ThemeProvider], + placeholder="Search for themes…", + ), + ) def action_screenshot( self, filename: str | None = None, path: str | None = None @@ -4138,7 +4143,7 @@ class App(Generic[ReturnType], DOMNode): def action_command_palette(self) -> None: """Show the Textual command palette.""" if self.use_command_palette and not CommandPalette.is_open(self): - self.push_screen(CommandPalette()) + self.push_screen(CommandPalette(id="--command-palette")) def _suspend_signal(self) -> None: """Signal that the application is being suspended.""" diff --git a/src/textual/command.py b/src/textual/command.py index 5aca01caa..da4fcbc5e 100644 --- a/src/textual/command.py +++ b/src/textual/command.py @@ -597,9 +597,6 @@ class CommandPalette(SystemModalScreen): _calling_screen: var[Screen[Any] | None] = var(None) """A record of the screen that was active when we were called.""" - _PALETTE_ID: Final[str] = "--command-palette" - """The internal ID for the command palette.""" - @dataclass class OptionHighlighted(Message): """Posted to App when an option is highlighted in the command palette.""" @@ -618,14 +615,29 @@ class CommandPalette(SystemModalScreen): option_selected: bool """True if an option was selected, False if the palette was closed without selecting an option.""" - def __init__(self, providers: ProviderSource | None = None) -> None: + def __init__( + self, + providers: ProviderSource | None = None, + *, + placeholder: str = "Search for commands…", + name: str | None = None, + id: str | None = None, + classes: str | None = None, + ) -> None: """Initialise the command palette. Args: providers: An optional list of providers to use. If None, the providers supplied in the App or Screen will be used. + placeholder: The placeholder text for the command palette. """ - super().__init__(id=self._PALETTE_ID) + super().__init__( + id=id, + classes=classes, + name=name, + ) + self.add_class("--textual-command-palette") + self._selected_command: DiscoveryHit | Hit | None = None """The command that was selected by the user.""" self._busy_timer: Timer | None = None @@ -637,18 +649,19 @@ class CommandPalette(SystemModalScreen): """List of Provider instances involved in searches.""" self._hit_count: int = 0 """Number of hits displayed.""" + self._placeholder = placeholder @staticmethod def is_open(app: App[object]) -> bool: - """Is the command palette current open? + """Is a command palette current open? Args: app: The app to test. Returns: - `True` if the command palette is currently open, `False` if not. + `True` if a command palette is currently open, `False` if not. """ - return app.screen.id == CommandPalette._PALETTE_ID + return app.screen.has_class("--textual-command-palette") @property def _provider_classes(self) -> set[type[Provider]]: @@ -697,7 +710,7 @@ class CommandPalette(SystemModalScreen): with Vertical(id="--container"): with Horizontal(id="--input"): yield SearchIcon() - yield CommandInput(placeholder="Search for commands…") + yield CommandInput(placeholder=self._placeholder) if not self.run_on_select: yield Button("\u25b6") with Vertical(id="--results"): diff --git a/src/textual/theme.py b/src/textual/theme.py index 11d9eecf0..59f520a67 100644 --- a/src/textual/theme.py +++ b/src/textual/theme.py @@ -1,7 +1,10 @@ from __future__ import annotations from dataclasses import dataclass +from functools import partial +from typing import Callable +from textual.command import DiscoveryHit, Hit, Hits, Provider from textual.design import ColorSystem @@ -366,3 +369,34 @@ BUILTIN_THEMES: dict[str, Theme] = { panel="#2A2A2A", # Dark Gray ), } + + +class ThemeProvider(Provider): + """A provider for themes.""" + + @property + def commands(self) -> list[tuple[str, Callable[[], None]]]: + themes = self.app.available_themes + + def set_app_theme(name: str) -> None: + self.app.theme = name + + return [ + (theme.name, partial(set_app_theme, theme.name)) + for theme in themes.values() + ] + + async def discover(self) -> Hits: + for command in self.commands: + yield DiscoveryHit(*command) + + async def search(self, query: str) -> Hits: + matcher = self.matcher(query) + + for name, callback in self.commands: + if (match := matcher.match(name)) > 0: + yield Hit( + match, + matcher.highlight(name), + callback, + )