Skip to content

Commit

Permalink
added system commands
Browse files Browse the repository at this point in the history
  • Loading branch information
willmcgugan committed Aug 22, 2024
1 parent 5573697 commit 6995834
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 46 deletions.
65 changes: 60 additions & 5 deletions src/textual/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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.
Expand Down
44 changes: 7 additions & 37 deletions src/textual/system_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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,
)

Expand All @@ -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,
)
8 changes: 4 additions & 4 deletions tests/command_palette/test_declare_sources.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand All @@ -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,
}

Expand Down

0 comments on commit 6995834

Please sign in to comment.