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
- Fixed issues with nested auto dimensions https://github.com/Textualize/textual/issues/1402 - 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
- Added `textual.actions.SkipAction` exception which can be raised from an action to allow parents to process bindings. - 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 ## [0.7.0] - 2022-12-17

View File

@@ -240,6 +240,14 @@ class App(Generic[ReturnType], DOMNode):
TITLE: str | None = None TITLE: str | None = None
SUB_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("") title: Reactive[str] = Reactive("")
sub_title: Reactive[str] = Reactive("") sub_title: Reactive[str] = Reactive("")
dark: Reactive[bool] = Reactive(True) dark: Reactive[bool] = Reactive(True)
@@ -301,7 +309,6 @@ class App(Generic[ReturnType], DOMNode):
self._logger = Logger(self._log) self._logger = Logger(self._log)
self._bindings.bind("ctrl+c", "quit", show=False, priority=True)
self._refresh_required = False self._refresh_required = False
self.design = DEFAULT_COLORS self.design = DEFAULT_COLORS
@@ -1898,11 +1905,6 @@ class App(Generic[ReturnType], DOMNode):
message.stop() message.stop()
async def _on_key(self, event: events.Key) -> None: 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)): if not (await self.check_bindings(event.key)):
await self.dispatch_key(event) await self.dispatch_key(event)
@@ -2124,6 +2126,14 @@ class App(Generic[ReturnType], DOMNode):
async def action_toggle_class(self, selector: str, class_name: str) -> None: async def action_toggle_class(self, selector: str, class_name: str) -> None:
self.screen.query(selector).toggle_class(class_name) 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( def _on_terminal_supports_synchronized_output(
self, message: messages.TerminalSupportsSynchronizedOutput self, message: messages.TerminalSupportsSynchronizedOutput
) -> None: ) -> None:

View File

@@ -123,3 +123,11 @@ def colors():
from textual.cli.previews import colors from textual.cli.previews import colors
colors.app.run() 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 ._context import NoActiveAppError
from ._node_list import NodeList from ._node_list import NodeList
from .binding import Binding, Bindings, BindingType from .binding import Bindings, BindingType
from .color import BLACK, WHITE, Color from .color import BLACK, WHITE, Color
from .css._error_tools import friendly_list from .css._error_tools import friendly_list
from .css.constants import VALID_DISPLAY, VALID_VISIBILITY 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 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: def __init__(self, sender: MessageTarget, key: str, char: str | None) -> None:
super().__init__(sender) super().__init__(sender)
@@ -209,7 +209,9 @@ class Key(InputEvent):
def __rich_repr__(self) -> rich.repr.Result: def __rich_repr__(self) -> rich.repr.Result:
yield "key", self.key 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 @property
def key_name(self) -> str | None: def key_name(self) -> str | None:

View File

@@ -61,11 +61,18 @@ class TextLog(ScrollView, can_focus=True):
def _on_styles_updated(self) -> None: def _on_styles_updated(self) -> None:
self._line_cache.clear() 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. """Write text or a rich renderable.
Args: Args:
content (RenderableType): Rich renderable (or text). 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 renderable: RenderableType
@@ -88,13 +95,17 @@ class TextLog(ScrollView, can_focus=True):
if isinstance(renderable, Text) and not self.wrap: if isinstance(renderable, Text) and not self.wrap:
render_options = render_options.update(overflow="ignore", no_wrap=True) render_options = render_options.update(overflow="ignore", no_wrap=True)
width = max( if expand:
self.min_width, render_width = self.scrollable_content_region.width
measure_renderables(console, render_options, [renderable]).maximum, else:
render_width = (
measure_renderables(console, render_options, [renderable]).maximum
if width is None
else width
) )
segments = self.app.console.render( segments = self.app.console.render(
renderable, render_options.update_width(width) renderable, render_options.update_width(render_width)
) )
lines = list(Segment.split_lines(segments)) lines = list(Segment.split_lines(segments))
if not lines: 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: async def test_just_app_no_bindings() -> None:
"""An app with no bindings should have no bindings, other than ctrl+c.""" """An app with no bindings should have no bindings, other than ctrl+c."""
async with NoBindings().run_test() as pilot: 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("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: async def test_just_app_alpha_binding() -> None:
"""An app with a single binding should have just the one binding.""" """An app with a single binding should have just the one binding."""
async with AlphaBinding().run_test() as pilot: 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("ctrl+c").priority is True
assert pilot.app._bindings.get_key("a").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: async def test_just_app_low_priority_alpha_binding() -> None:
"""An app with a single low-priority binding should have just the one binding.""" """An app with a single low-priority binding should have just the one binding."""
async with LowAlphaBinding().run_test() as pilot: 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("ctrl+c").priority is True
assert pilot.app._bindings.get_key("a").priority is False assert pilot.app._bindings.get_key("a").priority is False