From 6995834c3cccc177a19d20cf0efb6d8751a3a681 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 22 Aug 2024 11:46:56 +0100 Subject: [PATCH] added system commands --- src/textual/app.py | 65 +++++++++++++++++-- src/textual/system_commands.py | 44 ++----------- tests/command_palette/test_declare_sources.py | 8 +-- 3 files changed, 71 insertions(+), 46 deletions(-) diff --git a/src/textual/app.py b/src/textual/app.py index 1f1a32b824..dd7b38fb25 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -128,7 +128,7 @@ from .filter import LineFilter from .message import Message from .pilot import Pilot - from .system_commands import SystemCommands + from .system_commands import SystemCommandsProvider from .widget import MountError # type: ignore # noqa: F401 WINDOWS = sys.platform == "win32" @@ -171,16 +171,19 @@ ) """Signature for valid callbacks that can be used to control apps.""" +CommandCallback: TypeAlias = "Callable[[], Awaitable[Any]] | Callable[[], Any]" +"""Signature for callbacks used in [`get_system_commands`][textual.app.App.get_system_commands]""" -def get_system_commands() -> type[SystemCommands]: + +def get_system_commands_provider() -> type[SystemCommandsProvider]: """Callable to lazy load the system commands. Returns: System commands class. """ - from .system_commands import SystemCommands + from .system_commands import SystemCommandsProvider - return SystemCommands + return SystemCommandsProvider class AppError(Exception): @@ -359,7 +362,7 @@ class MyApp(App[None]): """Default number of seconds to show notifications before removing them.""" COMMANDS: ClassVar[set[type[Provider] | Callable[[], type[Provider]]]] = { - get_system_commands + get_system_commands_provider } """Command providers used by the [command palette](/guide/command_palette). @@ -922,6 +925,58 @@ def active_bindings(self) -> dict[str, ActiveBinding]: """ return self.screen.active_bindings + def get_system_commands( + self, + ) -> Iterable[tuple[str, str, CommandCallback]]: + """A generator of system commands used in the command palette. + + Implement this method in your App subclass if you want to add custom commands. + Here is an example: + + ```python + def get_system_commands(self): + yield from super().get_system_commands() + yield ("Bell", "Ring the bell", self.bell) + ``` + + !!! note + Requires that [`SystemCommandProvider`][textual.system_commands.SystemCommandProvider] is in `App.COMMANDS` class variable. + + Yields: + tuples of (TITLE, HELP TEXT, CALLBACK) + """ + if self.dark: + yield ( + "Light mode", + "Switch to a light background", + self.action_toggle_dark, + ) + else: + yield ( + "Dark mode", + "Switch to a dark background", + self.action_toggle_dark, + ) + + yield ( + "Quit the application", + "Quit the application as soon as possible", + self.action_quit, + ) + + if self.screen.query("HelpPanel"): + yield ( + "Hide keys and help panel", + "Hide the keys and widget help panel", + self.action_hide_help_panel, + ) + else: + yield ( + "Show keys and help panel", + "Show help for the focused widget and a summary of available keys", + self.action_show_help_panel, + ) + def get_default_screen(self) -> Screen: """Get the default screen. diff --git a/src/textual/system_commands.py b/src/textual/system_commands.py index d2ad79c5b6..fa1e8d7884 100644 --- a/src/textual/system_commands.py +++ b/src/textual/system_commands.py @@ -19,46 +19,16 @@ from .types import IgnoreReturnCallbackType -class SystemCommands(Provider): +class SystemCommandsProvider(Provider): """A [source][textual.command.Provider] of command palette commands that run app-wide tasks. Used by default in [`App.COMMANDS`][textual.app.App.COMMANDS]. """ @property - def _system_commands(self) -> Iterable[tuple[str, IgnoreReturnCallbackType, str]]: + def _system_commands(self) -> Iterable[tuple[str, str, IgnoreReturnCallbackType]]: """The system commands to reveal to the command palette.""" - if self.app.dark: - yield ( - "Light mode", - self.app.action_toggle_dark, - "Switch to a light background", - ) - else: - yield ( - "Dark mode", - self.app.action_toggle_dark, - "Switch to a dark background", - ) - - yield ( - "Quit the application", - self.app.action_quit, - "Quit the application as soon as possible", - ) - - if self.screen.query("HelpPanel"): - yield ( - "Hide keys and help panel", - self.app.action_hide_help_panel, - "Hide the keys and widget help panel", - ) - else: - yield ( - "Show keys and help panel", - self.app.action_show_help_panel, - "Show help for the focused widget and a summary of available keys", - ) + yield from self.app.get_system_commands() async def discover(self) -> Hits: """Handle a request for the discovery commands for this provider. @@ -67,10 +37,10 @@ async def discover(self) -> Hits: Commands that can be discovered. """ commands = sorted(self._system_commands, key=lambda command: command[0]) - for name, runnable, help_text in commands: + for name, help_text, callback in commands: yield DiscoveryHit( name, - runnable, + callback, help=help_text, ) @@ -89,11 +59,11 @@ async def search(self, query: str) -> Hits: # Loop over all applicable commands, find those that match and offer # them up to the command palette. - for name, runnable, help_text in self._system_commands: + for name, help_text, callback in self._system_commands: if (match := matcher.match(name)) > 0: yield Hit( match, matcher.highlight(name), - runnable, + callback, help=help_text, ) diff --git a/tests/command_palette/test_declare_sources.py b/tests/command_palette/test_declare_sources.py index 340a9b1666..e7691476be 100644 --- a/tests/command_palette/test_declare_sources.py +++ b/tests/command_palette/test_declare_sources.py @@ -1,7 +1,7 @@ from textual.app import App from textual.command import CommandPalette, Hit, Hits, Provider from textual.screen import Screen -from textual.system_commands import SystemCommands +from textual.system_commands import SystemCommandsProvider async def test_sources_with_no_known_screen() -> None: @@ -30,7 +30,7 @@ async def test_no_app_command_sources() -> None: """An app with no sources declared should work fine.""" async with AppWithNoSources().run_test() as pilot: assert isinstance(pilot.app.screen, CommandPalette) - assert pilot.app.screen._provider_classes == {SystemCommands} + assert pilot.app.screen._provider_classes == {SystemCommandsProvider} class AppWithSources(AppWithActiveCommandPalette): @@ -62,7 +62,7 @@ async def test_no_screen_command_sources() -> None: """An app with a screen with no sources declared should work fine.""" async with AppWithInitialScreen(ScreenWithNoSources()).run_test() as pilot: assert isinstance(pilot.app.screen, CommandPalette) - assert pilot.app.screen._provider_classes == {SystemCommands} + assert pilot.app.screen._provider_classes == {SystemCommandsProvider} class ScreenWithSources(ScreenWithNoSources): @@ -74,7 +74,7 @@ async def test_screen_command_sources() -> None: async with AppWithInitialScreen(ScreenWithSources()).run_test() as pilot: assert isinstance(pilot.app.screen, CommandPalette) assert pilot.app.screen._provider_classes == { - SystemCommands, + SystemCommandsProvider, ExampleCommandSource, }