Merge pull request #1409 from Textualize/cli-keys

keys command
This commit is contained in:
Will McGugan
2022-12-20 13:34:23 +00:00
committed by GitHub
9 changed files with 202 additions and 103 deletions

View File

@@ -10,14 +10,16 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Fixed
- Fixed issues with nested auto dimensions https://github.com/Textualize/textual/issues/1402
- Fixed watch method incorrectly running on first set when value hasn't changed and init=False https://github.com/Textualize/textual/pull/1367
### Added
- Added `textual.actions.SkipAction` exception which can be raised from an action to allow parents to process bindings.
- Added `textual keys` preview.
### Fixed
### Changed
- Fixed watch method incorrectly running on first set when value hasnt changed and init=False https://github.com/Textualize/textual/pull/1367
- Moved Ctrl+C, tab, and shift+tab to App BINDINGS
## [0.7.0] - 2022-12-17

View File

@@ -240,6 +240,14 @@ class App(Generic[ReturnType], DOMNode):
TITLE: str | None = None
SUB_TITLE: str | None = None
BINDINGS = [
Binding("ctrl+c", "quit", "Quit", show=False, priority=True),
Binding("tab", "focus_next", "Focus Next", show=False, priority=False),
Binding(
"shift+tab", "focus_previous", "Focus Previous", show=False, priority=False
),
]
title: Reactive[str] = Reactive("")
sub_title: Reactive[str] = Reactive("")
dark: Reactive[bool] = Reactive(True)
@@ -301,7 +309,6 @@ class App(Generic[ReturnType], DOMNode):
self._logger = Logger(self._log)
self._bindings.bind("ctrl+c", "quit", show=False, priority=True)
self._refresh_required = False
self.design = DEFAULT_COLORS
@@ -1898,13 +1905,8 @@ class App(Generic[ReturnType], DOMNode):
message.stop()
async def _on_key(self, event: events.Key) -> None:
if event.key == "tab":
self.screen.focus_next()
elif event.key == "shift+tab":
self.screen.focus_previous()
else:
if not (await self.check_bindings(event.key)):
await self.dispatch_key(event)
if not (await self.check_bindings(event.key)):
await self.dispatch_key(event)
async def _on_shutdown_request(self, event: events.ShutdownRequest) -> None:
log("shutdown request")
@@ -2124,6 +2126,14 @@ class App(Generic[ReturnType], DOMNode):
async def action_toggle_class(self, selector: str, class_name: str) -> None:
self.screen.query(selector).toggle_class(class_name)
def action_focus_next(self) -> None:
"""Focus the next widget."""
self.screen.focus_next()
def action_focus_previous(self) -> None:
"""Focus the previous widget."""
self.screen.focus_previous()
def _on_terminal_supports_synchronized_output(
self, message: messages.TerminalSupportsSynchronizedOutput
) -> None:

View File

@@ -123,3 +123,11 @@ def colors():
from textual.cli.previews import colors
colors.app.run()
@run.command("keys")
def keys():
"""Show key events"""
from textual.cli.previews import keys
keys.app.run()

View File

@@ -0,0 +1,60 @@
from rich.panel import Panel
from textual.app import App, ComposeResult
from textual import events
from textual.containers import Horizontal
from textual.widgets import Button, Header, TextLog
INSTRUCTIONS = """\
Press some keys!
Because we want to display all the keys, Ctrl+C won't work for this example. Use the button below to quit.\
"""
class KeyLog(TextLog, inherit_bindings=False):
"""We don't want to handle scroll keys."""
class KeysApp(App, inherit_bindings=False):
"""Show key events in a text log."""
TITLE = "Textual Keys"
BINDINGS = [("c", "clear", "Clear")]
CSS = """
#buttons {
dock: bottom;
height: 3;
}
Button {
width: 1fr;
}
"""
def compose(self) -> ComposeResult:
yield Header()
yield Horizontal(
Button("Clear", id="clear", variant="warning"),
Button("Quit", id="quit", variant="error"),
id="buttons",
)
yield KeyLog()
def on_ready(self) -> None:
self.query_one(KeyLog).write(Panel(INSTRUCTIONS), expand=True)
def on_key(self, event: events.Key) -> None:
self.query_one(KeyLog).write(event)
def on_button_pressed(self, event: Button.Pressed) -> None:
if event.button.id == "quit":
self.exit()
elif event.button.id == "clear":
self.query_one(KeyLog).clear()
app = KeysApp()
if __name__ == "__main__":
app.run()

View File

@@ -22,7 +22,7 @@ from rich.tree import Tree
from ._context import NoActiveAppError
from ._node_list import NodeList
from .binding import Binding, Bindings, BindingType
from .binding import Bindings, BindingType
from .color import BLACK, WHITE, Color
from .css._error_tools import friendly_list
from .css.constants import VALID_DISPLAY, VALID_VISIBILITY

View File

@@ -199,7 +199,7 @@ class Key(InputEvent):
key_aliases (list[str]): The aliases for the key, including the key itself
"""
__slots__ = ["key", "char"]
__slots__ = ["key", "char", "key_aliases"]
def __init__(self, sender: MessageTarget, key: str, char: str | None) -> None:
super().__init__(sender)
@@ -209,7 +209,9 @@ class Key(InputEvent):
def __rich_repr__(self) -> rich.repr.Result:
yield "key", self.key
yield "char", self.char, None
yield "char", self.char
yield "is_printable", self.is_printable
yield "key_aliases", self.key_aliases, [self.key_name]
@property
def key_name(self) -> str | None:

View File

@@ -61,11 +61,18 @@ class TextLog(ScrollView, can_focus=True):
def _on_styles_updated(self) -> None:
self._line_cache.clear()
def write(self, content: RenderableType | object) -> None:
def write(
self,
content: RenderableType | object,
width: int | None = None,
expand: bool = False,
) -> None:
"""Write text or a rich renderable.
Args:
content (RenderableType): Rich renderable (or text).
width (int): Width to render or None to use optimal width. Defaults to None.
expand (bool): Enable expand to widget width, or False to use `width`.
"""
renderable: RenderableType
@@ -88,13 +95,17 @@ class TextLog(ScrollView, can_focus=True):
if isinstance(renderable, Text) and not self.wrap:
render_options = render_options.update(overflow="ignore", no_wrap=True)
width = max(
self.min_width,
measure_renderables(console, render_options, [renderable]).maximum,
)
if expand:
render_width = self.scrollable_content_region.width
else:
render_width = (
measure_renderables(console, render_options, [renderable]).maximum
if width is None
else width
)
segments = self.app.console.render(
renderable, render_options.update_width(width)
renderable, render_options.update_width(render_width)
)
lines = list(Segment.split_lines(segments))
if not lines:

File diff suppressed because one or more lines are too long

View File

@@ -39,8 +39,10 @@ class NoBindings(App[None]):
async def test_just_app_no_bindings() -> None:
"""An app with no bindings should have no bindings, other than ctrl+c."""
async with NoBindings().run_test() as pilot:
assert list(pilot.app._bindings.keys.keys()) == ["ctrl+c"]
assert list(pilot.app._bindings.keys.keys()) == ["ctrl+c", "tab", "shift+tab"]
assert pilot.app._bindings.get_key("ctrl+c").priority is True
assert pilot.app._bindings.get_key("tab").priority is False
assert pilot.app._bindings.get_key("shift+tab").priority is False
##############################################################################
@@ -61,7 +63,9 @@ class AlphaBinding(App[None]):
async def test_just_app_alpha_binding() -> None:
"""An app with a single binding should have just the one binding."""
async with AlphaBinding().run_test() as pilot:
assert sorted(pilot.app._bindings.keys.keys()) == sorted(["ctrl+c", "a"])
assert sorted(pilot.app._bindings.keys.keys()) == sorted(
["ctrl+c", "tab", "shift+tab", "a"]
)
assert pilot.app._bindings.get_key("ctrl+c").priority is True
assert pilot.app._bindings.get_key("a").priority is True
@@ -84,7 +88,9 @@ class LowAlphaBinding(App[None]):
async def test_just_app_low_priority_alpha_binding() -> None:
"""An app with a single low-priority binding should have just the one binding."""
async with LowAlphaBinding().run_test() as pilot:
assert sorted(pilot.app._bindings.keys.keys()) == sorted(["ctrl+c", "a"])
assert sorted(pilot.app._bindings.keys.keys()) == sorted(
["ctrl+c", "tab", "shift+tab", "a"]
)
assert pilot.app._bindings.get_key("ctrl+c").priority is True
assert pilot.app._bindings.get_key("a").priority is False