From 5e2e0fe5660eb9b7042cd31a3cfe38c7f28d1092 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 20 Dec 2022 21:18:10 +0000 Subject: [PATCH 01/23] keys refactor --- docs/guide/actions.md | 40 ++++--------------------- docs/guide/input.md | 50 +++++++++++++++++++++++++------- src/textual/cli/cli.py | 2 +- src/textual/cli/previews/keys.py | 2 +- src/textual/events.py | 40 ++++++++++++++----------- src/textual/keys.py | 8 ++--- src/textual/message.py | 6 ++-- src/textual/message_pump.py | 6 ++-- src/textual/widgets/_input.py | 4 +-- src/textual/widgets/_text_log.py | 20 ++++++++----- 10 files changed, 94 insertions(+), 84 deletions(-) diff --git a/docs/guide/actions.md b/docs/guide/actions.md index 063986069..72293bc02 100644 --- a/docs/guide/actions.md +++ b/docs/guide/actions.md @@ -124,37 +124,9 @@ In the previous example if you wanted a link to set the background on the app ra Textual supports the following builtin actions which are defined on the app. - -### Bell - -::: textual.app.App.action_bell - options: - show_root_heading: false - -### Push screen - -::: textual.app.App.action_push_screen - - -### Pop screen - -::: textual.app.App.action_pop_screen - - -### Screenshot - -::: textual.app.App.action_screenshot - - -### Switch screen - -::: textual.app.App.action_switch_screen - - -### Toggle_dark - -::: textual.app.App.action_toggle_dark - -### Quit - -::: textual.app.App.action_quit +- [action_bell][textual.app.App.action_bell] +- [action_push_screen][textual.app.App.action_push_screen] +- [action_pop_screen][textual.app.App.action_pop_screen] +- [action_switch_screen][textual.app.App.action_switch_screen] +- [action_screenshot][textual.app.App.action_screenshot] +- [action_toggle_dark][textual.app.App.action_toggle_dark] diff --git a/docs/guide/input.md b/docs/guide/input.md index 122599ae7..44892743a 100644 --- a/docs/guide/input.md +++ b/docs/guide/input.md @@ -10,7 +10,7 @@ This chapter will discuss how to make your app respond to input in the form of k ## Keyboard input -The most fundamental way to receive input is via [Key](./events/key) events. Let's write an app to show key events as you type. +The most fundamental way to receive input is via [Key][textual.events.Key] events. Let's write an app to show key events as you type. === "key01.py" @@ -23,25 +23,53 @@ The most fundamental way to receive input is via [Key](./events/key) events. Let ```{.textual path="docs/examples/guide/input/key01.py", press="T,e,x,t,u,a,l,!,_"} ``` -Note the key event handler on the app which logs all key events. If you press any key it will show up on the screen. +When you press a key, the app will receive the associated event and write it to a [TextLog](../widgets/text_log.md) widget. Try pressing a few keys to see what happens. -### Attributes +!!! tip -There are two main attributes on a key event. The `key` attribute is the _name_ of the key which may be a single character, or a longer identifier. Textual ensures that the `key` attribute could always be used in a method name. + For a more feature feature rich version of this example, run `textual keys` from the command line. -Key events also contain a `char` attribute which contains a single character if it is printable, or ``None`` if it is not printable (like a function key which has no corresponding character). +### Key Event -To illustrate the difference between `key` and `char`, try `key01.py` with the space key. You should see something like the following: +The key event contains a number of attributes which tell you what key (or keys) have been pressed. -```{.textual path="docs/examples/guide/input/key01.py", press="space,_"} -``` +#### key + +The `key` attribute is a string which identifies the key that was pressed. The value of `key` will be a single character for letter and numbers, or a longer identifier for other keys. + +Some keys may be combined with ++shift++ key. In the case of letters, this will result in a capital letter as you might expect. For non-printable keys, the `key` attribute will be prefixed with `shift+`. For example ++shift+home++ will produce an event with `key="shift+home"`. + +Many keys can also be combined with ++ctrl++ which will prefix the key with `ctrl+`. For instance ++ctrl+p++ will produce an event with `key="ctrl+p"`. + +!!! warning + + Not all keys combinations are supported in terminals, and some keys may be intercepted by your OS. If in doubt, run `textual keys` from the command line. + +#### character + +If the key has an associated printable character then the `character` will contain a string containing a single unicode character. If there is no printable character for the key (such as for function keys) then `character` will be `None`. + +For example the ++p++ key will produce `character="p"` but ++f2++ will produce `character=None`. + +#### name + +The `name` attribute is similar to `key` but unlike `key` is guaranteed to be valid within a Python function name. Textual derives `name` from the `key` key attribute by lower casing it and replacing `+` with `_`. Upper case letters are prefixed with `upper_` to distinguish them from lower case names. + +For example, ++ctrl+q++ produces `name="ctrl_p"` and ++shift+p++ produces `name="upper_p"`. + +#### is_printable + +The `is_printable` attribute is a boolean which indicates if the key would typically result in something that could be used in an input widget. If `is_printable` is `False` then the key is a control code or function key that you wouldn't expect to produce anything in an input. + +#### aliases + +Some keys or combinations of keys can produce the same key. For instance, the ++tab++ key is indistinguishable from ++ctrl+i++ in the terminal. For such keys, Textual events will contain a list of the possible keys that may have produced this event. In the case of ++tab++, the `aliases` attribute will contain `["tab", "ctrl+i"]` -Note that the `key` attribute contains the word "space" while the `char` attribute contains a literal space. ### Key methods -Textual offers a convenient way of handling specific keys. If you create a method beginning with `key_` followed by the name of a key, then that method will be called in response to the key. +Textual offers a convenient way of handling specific keys. If you create a method beginning with `key_` followed by the key name (the event's `name` attribute), then that method will be called in response to the key. Let's add a key method to the example code. @@ -134,7 +162,7 @@ Note how the footer displays bindings and makes them clickable. The priority of a single binding can be controlled with the `priority` parameter of a `Binding` instance. Set it to `True` to give it priority, or `False` to not. - The default priority of all bindings on a class can be controlled with the `PRIORITY_BINDINGS` class variable. Set it to `True` or `False` to set the default priroty for all `BINDINGS`. + The default priority of all bindings on a class can be controlled with the `PRIORITY_BINDINGS` class variable. Set it to `True` or `False` to set the default priority for all `BINDINGS`. ### Binding class diff --git a/src/textual/cli/cli.py b/src/textual/cli/cli.py index e3f93c4ca..9113338f1 100644 --- a/src/textual/cli/cli.py +++ b/src/textual/cli/cli.py @@ -127,7 +127,7 @@ def colors(): @run.command("keys") def keys(): - """Show key events""" + """Show key events.""" from textual.cli.previews import keys keys.app.run() diff --git a/src/textual/cli/previews/keys.py b/src/textual/cli/previews/keys.py index 4facbcb4a..a3b30e162 100644 --- a/src/textual/cli/previews/keys.py +++ b/src/textual/cli/previews/keys.py @@ -9,7 +9,7 @@ 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.\ +Because we want to display all the keys, ctrl+C won't quit this example. Use the Quit button below to exit the app.\ """ diff --git a/src/textual/events.py b/src/textual/events.py index 96602a0bd..4a3e880a1 100644 --- a/src/textual/events.py +++ b/src/textual/events.py @@ -16,7 +16,6 @@ if TYPE_CHECKING: from .timer import Timer as TimerClass from .timer import TimerCallback from .widget import Widget - import asyncio @rich.repr.auto @@ -183,7 +182,7 @@ class MouseRelease(Event, bubble=False): class InputEvent(Event): - pass + """Base class for input events.""" @rich.repr.auto @@ -191,47 +190,54 @@ class Key(InputEvent): """Sent when the user hits a key on the keyboard. Args: - sender (MessageTarget): The sender of the event (the App). - key (str): A key name (textual.keys.Keys). - char (str | None, optional): A printable character or None if it is not printable. + sender (MessageTarget): The sender of the event (always the App). + key (str): The key that was pressed. + char (str | None, optional): A printable character or ``None`` if it is not printable. Attributes: key_aliases (list[str]): The aliases for the key, including the key itself """ - __slots__ = ["key", "char", "key_aliases"] + __slots__ = ["key", "character", "aliases"] def __init__(self, sender: MessageTarget, key: str, char: str | None) -> None: super().__init__(sender) self.key = key - self.char = (key if len(key) == 1 else None) if char is None else char - self.key_aliases = [_normalize_key(alias) for alias in _get_key_aliases(key)] + self.character = (key if len(key) == 1 else None) if char is None else char + self.aliases = _get_key_aliases(key) def __rich_repr__(self) -> rich.repr.Result: yield "key", self.key - yield "char", self.char + yield "character", self.character + yield "name", self.name yield "is_printable", self.is_printable - yield "key_aliases", self.key_aliases, [self.key_name] + yield "aliases", self.aliases, [self.key] @property - def key_name(self) -> str | None: + def name(self) -> str | None: """Name of a key suitable for use as a Python identifier.""" - return _normalize_key(self.key) + return _key_to_identifier(self.key).lower() + + @property + def name_aliases(self) -> list[str]: + """The corresponding name for every alias in `aliases` list.""" + return [_key_to_identifier(key) for key in self.aliases] @property def is_printable(self) -> bool: - """Return True if the key is printable. Currently, we assume any key event that - isn't defined in key bindings is printable. + """Check if the key is printable (produces a unicode character). Returns: bool: True if the key is printable. """ - return False if self.char is None else self.char.isprintable() + return False if self.character is None else self.character.isprintable() -def _normalize_key(key: str) -> str: +def _key_to_identifier(key: str) -> str: """Convert the key string to a name suitable for use as a Python identifier.""" - return key.replace("+", "_") + if len(key) == 1 and key.isupper(): + key = f"upper_{key.lower()}" + return key.replace("+", "_").lower() @rich.repr.auto diff --git a/src/textual/keys.py b/src/textual/keys.py index 25fc148df..4656895ae 100644 --- a/src/textual/keys.py +++ b/src/textual/keys.py @@ -70,10 +70,10 @@ class Keys(str, Enum): ControlShift9 = "ctrl+shift+9" ControlShift0 = "ctrl+shift+0" - ControlBackslash = "ctrl+\\" - ControlSquareClose = "ctrl+]" - ControlCircumflex = "ctrl+^" - ControlUnderscore = "ctrl+_" + ControlBackslash = "ctrl+backslash" + ControlSquareClose = "ctrl+right_square_bracked" + ControlCircumflex = "ctrl+circumflex_accent" + ControlUnderscore = "ctrl+underscore" Left = "left" Right = "right" diff --git a/src/textual/message.py b/src/textual/message.py index aafe13dea..45c0b2d67 100644 --- a/src/textual/message.py +++ b/src/textual/message.py @@ -24,7 +24,6 @@ class Message: __slots__ = [ "sender", - "name", "time", "_forwarded", "_no_default_action", @@ -40,13 +39,14 @@ class Message: def __init__(self, sender: MessageTarget) -> None: self.sender = sender - self.name = camel_to_snake(self.__class__.__name__) + self.time = _clock.get_time_no_wait() self._forwarded = False self._no_default_action = False self._stop_propagation = False + name = camel_to_snake(self.__class__.__name__) self._handler_name = ( - f"on_{self.namespace}_{self.name}" if self.namespace else f"on_{self.name}" + f"on_{self.namespace}_{name}" if self.namespace else f"on_{name}" ) super().__init__() diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 9d28f738b..5c3fca3f6 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -593,12 +593,12 @@ class MessagePump(metaclass=MessagePumpMeta): handled = False invoked_method = None - key_name = event.key_name + key_name = event.name if not key_name: return False - for key_alias in event.key_aliases: - key_method = get_key_handler(self, key_alias) + for key_method_name in event.name_aliases: + key_method = get_key_handler(self, key_method_name) if key_method is not None: if invoked_method: _raise_duplicate_key_handlers_error( diff --git a/src/textual/widgets/_input.py b/src/textual/widgets/_input.py index 6434d321d..c2bfa638a 100644 --- a/src/textual/widgets/_input.py +++ b/src/textual/widgets/_input.py @@ -233,8 +233,8 @@ class Input(Widget, can_focus=True): return elif event.is_printable: event.stop() - assert event.char is not None - self.insert_text_at_cursor(event.char) + assert event.character is not None + self.insert_text_at_cursor(event.character) event.prevent_default() def on_paste(self, event: events.Paste) -> None: diff --git a/src/textual/widgets/_text_log.py b/src/textual/widgets/_text_log.py index f3338fa8d..667381d5a 100644 --- a/src/textual/widgets/_text_log.py +++ b/src/textual/widgets/_text_log.py @@ -66,6 +66,7 @@ class TextLog(ScrollView, can_focus=True): content: RenderableType | object, width: int | None = None, expand: bool = False, + shrink: bool = True, ) -> None: """Write text or a rich renderable. @@ -95,14 +96,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) - 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 - ) + render_width = measure_renderables( + console, render_options, [renderable] + ).maximum + container_width = ( + self.scrollable_content_region.width if width is None else width + ) + + if expand and render_width < container_width: + render_width = container_width + if shrink and render_width > container_width: + render_width = container_width segments = self.app.console.render( renderable, render_options.update_width(render_width) From 7a46672d64d93b23b280b876202dc4ae01df6918 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 21 Dec 2022 11:38:20 +0000 Subject: [PATCH 02/23] docs and expanded keys --- CHANGELOG.md | 1 + docs/guide/actions.md | 2 ++ docs/guide/input.md | 41 +++++++++++++++++-------------- src/textual/app.py | 8 ++---- src/textual/binding.py | 14 +++++------ src/textual/dom.py | 13 ++++------ src/textual/keys.py | 11 +++++++++ src/textual/screen.py | 1 - tests/test_binding_inheritance.py | 10 +++----- tests/test_xterm_parser.py | 2 +- 10 files changed, 54 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6a880737..d59f04af6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Changed - Moved Ctrl+C, tab, and shift+tab to App BINDINGS +- Deprecated `PRIORITY_BINDINGS` class variable. ## [0.7.0] - 2022-12-17 diff --git a/docs/guide/actions.md b/docs/guide/actions.md index 72293bc02..cbbd12975 100644 --- a/docs/guide/actions.md +++ b/docs/guide/actions.md @@ -90,6 +90,8 @@ Textual will run actions bound to keys. The following example adds key [bindings If you run this example, you can change the background by pressing keys in addition to clicking links. +See the previous section on [input](./input.md#bindings) for more information on bindings. + ## Namespaces Textual will look for action methods in the class where they are defined (App, Screen, or Widget). If we were to create a [custom widget](./widgets.md#custom-widgets) it can have its own set of actions. diff --git a/docs/guide/input.md b/docs/guide/input.md index 44892743a..10f7fcfbe 100644 --- a/docs/guide/input.md +++ b/docs/guide/input.md @@ -10,7 +10,7 @@ This chapter will discuss how to make your app respond to input in the form of k ## Keyboard input -The most fundamental way to receive input is via [Key][textual.events.Key] events. Let's write an app to show key events as you type. +The most fundamental way to receive input is via [Key][textual.events.Key] events which are sent to your app when the user presses a key. Let's write an app to show key events as you type. === "key01.py" @@ -23,7 +23,7 @@ The most fundamental way to receive input is via [Key][textual.events.Key] event ```{.textual path="docs/examples/guide/input/key01.py", press="T,e,x,t,u,a,l,!,_"} ``` -When you press a key, the app will receive the associated event and write it to a [TextLog](../widgets/text_log.md) widget. Try pressing a few keys to see what happens. +When you press a key, the app will receive the event and write it to a [TextLog](../widgets/text_log.md) widget. Try pressing a few keys to see what happens. !!! tip @@ -31,8 +31,7 @@ When you press a key, the app will receive the associated event and write it to ### Key Event -The key event contains a number of attributes which tell you what key (or keys) have been pressed. - +The key event contains following attributes which your app can use to know how to respond. #### key @@ -48,13 +47,13 @@ Many keys can also be combined with ++ctrl++ which will prefix the key with `ctr #### character -If the key has an associated printable character then the `character` will contain a string containing a single unicode character. If there is no printable character for the key (such as for function keys) then `character` will be `None`. +If the key has an associated printable character then `character` will contain a string with a single unicode character. If there is no printable character for the key (such as for function keys) then `character` will be `None`. For example the ++p++ key will produce `character="p"` but ++f2++ will produce `character=None`. #### name -The `name` attribute is similar to `key` but unlike `key` is guaranteed to be valid within a Python function name. Textual derives `name` from the `key` key attribute by lower casing it and replacing `+` with `_`. Upper case letters are prefixed with `upper_` to distinguish them from lower case names. +The `name` attribute is similar to `key` but, unlike `key`, is guaranteed to be valid within a Python function name. Textual derives `name` from the `key` attribute by lower casing it and replacing `+` with `_`. Upper case letters are prefixed with `upper_` to distinguish them from lower case names. For example, ++ctrl+q++ produces `name="ctrl_p"` and ++shift+p++ produces `name="upper_p"`. @@ -64,7 +63,7 @@ The `is_printable` attribute is a boolean which indicates if the key would typic #### aliases -Some keys or combinations of keys can produce the same key. For instance, the ++tab++ key is indistinguishable from ++ctrl+i++ in the terminal. For such keys, Textual events will contain a list of the possible keys that may have produced this event. In the case of ++tab++, the `aliases` attribute will contain `["tab", "ctrl+i"]` +Some keys or combinations of keys can produce the same event. For instance, the ++tab++ key is indistinguishable from ++ctrl+i++ in the terminal. For such keys, Textual events will contain a list of the possible keys that may have produced this event. In the case of ++tab++, the `aliases` attribute will contain `["tab", "ctrl+i"]` ### Key methods @@ -155,24 +154,28 @@ Note how the footer displays bindings and makes them clickable. Multiple keys can be bound to a single action by comma-separating them. For example, `("r,t", "add_bar('red')", "Add Red")` means both ++r++ and ++t++ are bound to `add_bar('red')`. - -!!! note - - Ordinarily a binding on a focused widget has precedence over the same key binding at a higher level. However, bindings at the `App` or `Screen` level always have priority. - - The priority of a single binding can be controlled with the `priority` parameter of a `Binding` instance. Set it to `True` to give it priority, or `False` to not. - - The default priority of all bindings on a class can be controlled with the `PRIORITY_BINDINGS` class variable. Set it to `True` or `False` to set the default priority for all `BINDINGS`. - ### Binding class The tuple of three strings may be enough for simple bindings, but you can also replace the tuple with a [Binding][textual.binding.Binding] instance which exposes a few more options. -### Why use bindings? +### Priority bindings -Bindings are particularly useful for configurable hot-keys. Bindings can also be inspected in widgets such as [Footer](../widgets/footer.md). +Textual checks bindings for the focused widget first. If there is no matching binding, then the parent widget(s) will be checked. Occasionally, you may want bindings to be checked *before* the focused widget. Typically to create hot-keys. + +You can create priority key bindings by setting `priority=True` on the Binding object. Textual uses this feature to add a default binding for ++ctrl+c++ so there is always a way to exit the app. Here's the bindings from the App base class. Note the first binding is set as a priority: + +```python + BINDINGS = [ + Binding("ctrl+c", "quit", "Quit", show=False, priority=True), + Binding("tab", "focus_next", "Focus Next", show=False), + Binding("shift+tab", "focus_previous", "Focus Previous", show=False), + ] +``` + +### Show bindings + +The [footer](../widgets/footer.md) widget can inspect bindings to display available keys. If you don't want a binding to display in the footer you can set `show=False`. The default bindings on App do this so that the standard ++ctrl+c++, ++tab++ and ++shift+tab++ bindings don't typically appear in the footer. -In a future version of Textual it will also be possible to specify bindings in a configuration file, which will allow users to override app bindings. ## Mouse Input diff --git a/src/textual/app.py b/src/textual/app.py index 1462eeb3e..e84998609 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -232,8 +232,6 @@ class App(Generic[ReturnType], DOMNode): } """ - PRIORITY_BINDINGS = True - SCREENS: dict[str, Screen | Callable[[], Screen]] = {} _BASE_PATH: str | None = None CSS_PATH: CSSPathType = None @@ -242,10 +240,8 @@ class App(Generic[ReturnType], DOMNode): 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 - ), + Binding("tab", "focus_next", "Focus Next", show=False), + Binding("shift+tab", "focus_previous", "Focus Previous", show=False), ] title: Reactive[str] = Reactive("") diff --git a/src/textual/binding.py b/src/textual/binding.py index 7170d3dfa..353158218 100644 --- a/src/textual/binding.py +++ b/src/textual/binding.py @@ -5,6 +5,7 @@ from typing import Iterable, MutableMapping import rich.repr +from textual.keys import _character_to_key from textual._typing import TypeAlias BindingType: TypeAlias = "Binding | tuple[str, str, str]" @@ -32,7 +33,7 @@ class Binding: """bool: Show the action in Footer, or False to hide.""" key_display: str | None = None """str | None: How the key should be shown in footer.""" - priority: bool | None = None + priority: bool = False """bool | None: Is this a priority binding, checked form app down to focused widget?""" @@ -43,13 +44,11 @@ class Bindings: def __init__( self, bindings: Iterable[BindingType] | None = None, - default_priority: bool | None = None, ) -> None: """Initialise a collection of bindings. Args: bindings (Iterable[BindingType] | None, optional): An optional set of initial bindings. - default_priority (bool | None, optional): The default priority of the bindings. Note: The iterable of bindings can contain either a `Binding` @@ -71,17 +70,16 @@ class Bindings: # be a list of keys, so now we unroll that single Binding # into a (potential) collection of Binding instances. for key in binding.key.split(","): + key = key.strip() + if len(key) == 1: + key = _character_to_key(key) yield Binding( key=key.strip(), action=binding.action, description=binding.description, show=binding.show, key_display=binding.key_display, - priority=( - default_priority - if binding.priority is None - else binding.priority - ), + priority=binding.priority, ) self.keys: MutableMapping[str, Binding] = ( diff --git a/src/textual/dom.py b/src/textual/dom.py index 3a4c18435..ffdb85caa 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -92,9 +92,6 @@ class DOMNode(MessagePump): # Virtual DOM nodes COMPONENT_CLASSES: ClassVar[set[str]] = set() - # Should the content of BINDINGS be treated as priority bindings? - PRIORITY_BINDINGS: ClassVar[bool] = False - # Mapping of key bindings BINDINGS: ClassVar[list[BindingType]] = [] @@ -233,13 +230,13 @@ class DOMNode(MessagePump): for base in reversed(cls.__mro__): if issubclass(base, DOMNode): - # See if the current class wants to set the bindings as - # priority bindings. If it doesn't have that property on the - # class, go with what we saw last. - priority = base.__dict__.get("PRIORITY_BINDINGS", priority) if not base._inherit_bindings: bindings.clear() - bindings.append(Bindings(base.__dict__.get("BINDINGS", []), priority)) + bindings.append( + Bindings( + base.__dict__.get("BINDINGS", []), + ) + ) keys = {} for bindings_ in bindings: keys.update(bindings_.keys) diff --git a/src/textual/keys.py b/src/textual/keys.py index 4656895ae..7d742dae9 100644 --- a/src/textual/keys.py +++ b/src/textual/keys.py @@ -256,3 +256,14 @@ def _get_key_display(key: str) -> str: if unicode_character.isprintable(): return unicode_character return upper_original + + +def _character_to_key(character: str) -> str: + """Convert a single character to a key value.""" + assert len(character) == 1 + if not character.isalnum(): + key = unicodedata.name(character).lower().replace("-", "_").replace(" ", "_") + else: + key = character + key = KEY_NAME_REPLACEMENTS.get(key, key) + return key diff --git a/src/textual/screen.py b/src/textual/screen.py index 3f89e40f4..32ea3e7a2 100644 --- a/src/textual/screen.py +++ b/src/textual/screen.py @@ -29,7 +29,6 @@ class Screen(Widget): # The screen is a special case and unless a class that inherits from us # says otherwise, all screen-level bindings should be treated as having # priority. - PRIORITY_BINDINGS = True DEFAULT_CSS = """ Screen { diff --git a/tests/test_binding_inheritance.py b/tests/test_binding_inheritance.py index b5f2a35d4..6aad85e37 100644 --- a/tests/test_binding_inheritance.py +++ b/tests/test_binding_inheritance.py @@ -57,7 +57,7 @@ async def test_just_app_no_bindings() -> None: class AlphaBinding(App[None]): """An app with a simple alpha key binding.""" - BINDINGS = [Binding("a", "a", "a")] + BINDINGS = [Binding("a", "a", "a", priority=True)] async def test_just_app_alpha_binding() -> None: @@ -81,8 +81,7 @@ async def test_just_app_alpha_binding() -> None: class LowAlphaBinding(App[None]): """An app with a simple low-priority alpha key binding.""" - PRIORITY_BINDINGS = False - BINDINGS = [Binding("a", "a", "a")] + BINDINGS = [Binding("a", "a", "a", priority=False)] async def test_just_app_low_priority_alpha_binding() -> None: @@ -106,7 +105,7 @@ async def test_just_app_low_priority_alpha_binding() -> None: class ScreenWithBindings(Screen): """A screen with a simple alpha key binding.""" - BINDINGS = [Binding("a", "a", "a")] + BINDINGS = [Binding("a", "a", "a", priority=True)] class AppWithScreenThatHasABinding(App[None]): @@ -144,8 +143,7 @@ async def test_app_screen_with_bindings() -> None: class ScreenWithLowBindings(Screen): """A screen with a simple low-priority alpha key binding.""" - PRIORITY_BINDINGS = False - BINDINGS = [Binding("a", "a", "a")] + BINDINGS = [Binding("a", "a", "a", priority=False)] class AppWithScreenThatHasALowBinding(App[None]): diff --git a/tests/test_xterm_parser.py b/tests/test_xterm_parser.py index 993903a05..6e352d87d 100644 --- a/tests/test_xterm_parser.py +++ b/tests/test_xterm_parser.py @@ -106,7 +106,7 @@ def test_cant_match_escape_sequence_too_long(parser): # The rest of the characters correspond to the expected key presses events = events[1:] for index, character in enumerate(sequence[1:]): - assert events[index].char == character + assert events[index].character == character @pytest.mark.parametrize( From 55c58e0f8f16ebe88e31f8811a00958f698ec977 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 21 Dec 2022 11:39:36 +0000 Subject: [PATCH 03/23] added tests --- tests/test_keys.py | 47 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 tests/test_keys.py diff --git a/tests/test_keys.py b/tests/test_keys.py new file mode 100644 index 000000000..19a9dc136 --- /dev/null +++ b/tests/test_keys.py @@ -0,0 +1,47 @@ +import pytest + +from textual.app import App +from textual.keys import _character_to_key + + +@pytest.mark.parametrize( + "character,key", + [ + ("1", "1"), + ("2", "2"), + ("a", "a"), + ("z", "z"), + ("_", "underscore"), + (" ", "space"), + ("~", "tilde"), + ("?", "question_mark"), + ("£", "pound_sign"), + (",", "comma"), + ], +) +def test_character_to_key(character: str, key: str) -> None: + assert _character_to_key(character) == key + + +async def test_character_bindings(): + """Test you can bind to a character as well as a longer key name.""" + counter = 0 + + class BindApp(App): + BINDINGS = [(".,~,space", "increment", "foo")] + + def action_increment(self) -> None: + nonlocal counter + counter += 1 + + app = BindApp() + print(app._bindings) + async with app.run_test() as pilot: + await pilot.press(".") + assert counter == 1 + await pilot.press("~") + assert counter == 2 + await pilot.press(" ") + assert counter == 3 + await pilot.press("x") + assert counter == 3 From 6e7181a6547e05d980eea2a7a85bb2eae241fc51 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 21 Dec 2022 13:27:26 +0000 Subject: [PATCH 04/23] changelog --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d59f04af6..715590478 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,11 +16,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Added `textual.actions.SkipAction` exception which can be raised from an action to allow parents to process bindings. - Added `textual keys` preview. +- Added ability to bind to a character in addition to key name. i.e. you can bind to "." or "full_stop" ### Changed -- Moved Ctrl+C, tab, and shift+tab to App BINDINGS +- Moved Ctrl+C, tab, and shift+tab to App BINDINGS. - Deprecated `PRIORITY_BINDINGS` class variable. +- Renamed `char` to `character` on Key event. +- Renamed `key_name` to `name` on Key event. ## [0.7.0] - 2022-12-17 From 59e60f729206ab0af5765513b1ca30308928eee0 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 21 Dec 2022 13:30:49 +0000 Subject: [PATCH 05/23] docstrings and changelog --- CHANGELOG.md | 3 ++- src/textual/widgets/_text_log.py | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 715590478..d73264f30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Added `textual.actions.SkipAction` exception which can be raised from an action to allow parents to process bindings. - Added `textual keys` preview. -- Added ability to bind to a character in addition to key name. i.e. you can bind to "." or "full_stop" +- Added ability to bind to a character in addition to key name. i.e. you can bind to "." or "full_stop". +- Added TextLog.shrink attribute to allow renderable to reduce in size to fit width. ### Changed diff --git a/src/textual/widgets/_text_log.py b/src/textual/widgets/_text_log.py index 667381d5a..980df5f4c 100644 --- a/src/textual/widgets/_text_log.py +++ b/src/textual/widgets/_text_log.py @@ -72,8 +72,9 @@ class TextLog(ScrollView, can_focus=True): 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`. + 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`. Defaults to `False`. + shrink (bool): Enable shrinking of content to fit width. Defaults to `True`. """ renderable: RenderableType From 7b3474eff3b18087dce203bac82deff2930b00e8 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 21 Dec 2022 14:45:35 +0000 Subject: [PATCH 06/23] ws --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0652dd3c..4ba48e22f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). + ## [0.8.0] - Unreleased ### Fixed From 46cfbddad2244393629c06551c1b14f18798cc8c Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 21 Dec 2022 14:57:25 +0000 Subject: [PATCH 07/23] check for empty bindings --- src/textual/binding.py | 10 +++++++- tests/test_binding.py | 56 +++++++++++++++++++++++++++++++++--------- 2 files changed, 54 insertions(+), 12 deletions(-) diff --git a/src/textual/binding.py b/src/textual/binding.py index 353158218..927434868 100644 --- a/src/textual/binding.py +++ b/src/textual/binding.py @@ -19,6 +19,10 @@ class NoBinding(Exception): """A binding was not found.""" +class InvalidBinding(Exception): + """Binding key is in an invalid format.""" + + @dataclass(frozen=True) class Binding: """The configuration of a key binding.""" @@ -71,10 +75,14 @@ class Bindings: # into a (potential) collection of Binding instances. for key in binding.key.split(","): key = key.strip() + if not key: + raise InvalidBinding( + f"Can not bind empty string in {binding.key!r}" + ) if len(key) == 1: key = _character_to_key(key) yield Binding( - key=key.strip(), + key=key, action=binding.action, description=binding.description, show=binding.show, diff --git a/tests/test_binding.py b/tests/test_binding.py index 3c12f5a7c..9a24b3e2d 100644 --- a/tests/test_binding.py +++ b/tests/test_binding.py @@ -2,7 +2,8 @@ from string import ascii_lowercase import pytest -from textual.binding import Bindings, Binding, BindingError, NoBinding +from textual.app import App +from textual.binding import Bindings, Binding, BindingError, NoBinding, InvalidBinding BINDING1 = Binding("a,b", action="action1", description="description1") BINDING2 = Binding("c", action="action2", description="description2") @@ -13,45 +14,78 @@ BINDING3 = Binding(" d , e ", action="action3", description="description3") def bindings(): yield Bindings([BINDING1, BINDING2]) + @pytest.fixture def more_bindings(): yield Bindings([BINDING1, BINDING2, BINDING3]) + def test_bindings_get_key(bindings): - assert bindings.get_key("b") == Binding("b", action="action1", description="description1") + assert bindings.get_key("b") == Binding( + "b", action="action1", description="description1" + ) assert bindings.get_key("c") == BINDING2 with pytest.raises(NoBinding): bindings.get_key("control+meta+alt+shift+super+hyper+t") + def test_bindings_get_key_spaced_list(more_bindings): assert more_bindings.get_key("d").action == more_bindings.get_key("e").action + def test_bindings_merge_simple(bindings): left = Bindings([BINDING1]) right = Bindings([BINDING2]) assert Bindings.merge([left, right]).keys == bindings.keys + def test_bindings_merge_overlap(): left = Bindings([BINDING1]) - another_binding = Binding("a", action="another_action", description="another_description") + another_binding = Binding( + "a", action="another_action", description="another_description" + ) assert Bindings.merge([left, Bindings([another_binding])]).keys == { "a": another_binding, "b": Binding("b", action="action1", description="description1"), } + def test_bad_binding_tuple(): with pytest.raises(BindingError): _ = Bindings((("a", "action"),)) with pytest.raises(BindingError): - _ = Bindings((("a", "action", "description","too much"),)) + _ = Bindings((("a", "action", "description", "too much"),)) + def test_binding_from_tuples(): - assert Bindings((( BINDING2.key, BINDING2.action, BINDING2.description),)).get_key("c") == BINDING2 + assert ( + Bindings(((BINDING2.key, BINDING2.action, BINDING2.description),)).get_key("c") + == BINDING2 + ) + def test_shown(): - bindings = Bindings([ - Binding( - key, action=f"action_{key}", description=f"Emits {key}",show=bool(ord(key)%2) - ) for key in ascii_lowercase - ]) - assert len(bindings.shown_keys)==(len(ascii_lowercase)/2) + bindings = Bindings( + [ + Binding( + key, + action=f"action_{key}", + description=f"Emits {key}", + show=bool(ord(key) % 2), + ) + for key in ascii_lowercase + ] + ) + assert len(bindings.shown_keys) == (len(ascii_lowercase) / 2) + + +def test_invalid_binding(): + with pytest.raises(InvalidBinding): + + class BrokenApp(App): + BINDINGS = [(",,,", "foo", "Broken")] + + with pytest.raises(InvalidBinding): + + class BrokenApp(App): + BINDINGS = [(", ,", "foo", "Broken")] From a8e30fe7bc403f43c87ff79719f5b888a110a3ed Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 21 Dec 2022 14:59:24 +0000 Subject: [PATCH 08/23] update docstring --- src/textual/events.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/textual/events.py b/src/textual/events.py index 4a3e880a1..b723c1948 100644 --- a/src/textual/events.py +++ b/src/textual/events.py @@ -192,10 +192,10 @@ class Key(InputEvent): Args: sender (MessageTarget): The sender of the event (always the App). key (str): The key that was pressed. - char (str | None, optional): A printable character or ``None`` if it is not printable. + character (str | None, optional): A printable character or ``None`` if it is not printable. Attributes: - key_aliases (list[str]): The aliases for the key, including the key itself + aliases (list[str]): The aliases for the key, including the key itself. """ __slots__ = ["key", "character", "aliases"] From c6c2dd69beadd3fd3b330a370ac64272faac0db5 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 21 Dec 2022 15:01:54 +0000 Subject: [PATCH 09/23] fix type --- src/textual/events.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/events.py b/src/textual/events.py index b723c1948..6f229b358 100644 --- a/src/textual/events.py +++ b/src/textual/events.py @@ -214,7 +214,7 @@ class Key(InputEvent): yield "aliases", self.aliases, [self.key] @property - def name(self) -> str | None: + def name(self) -> str: """Name of a key suitable for use as a Python identifier.""" return _key_to_identifier(self.key).lower() From 91da9d0103de424f3e2cbbe171bb475c910f92c1 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 21 Dec 2022 15:03:52 +0000 Subject: [PATCH 10/23] Update CHANGELOG.md Co-authored-by: darrenburns --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ba48e22f..b8b15a173 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,7 +23,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Changed -- Moved Ctrl+C, tab, and shift+tab to App BINDINGS. - Deprecated `PRIORITY_BINDINGS` class variable. - Renamed `char` to `character` on Key event. - Renamed `key_name` to `name` on Key event. From 95b22851729e0dd2c73e7fe3198bd609955b17ba Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 21 Dec 2022 15:04:03 +0000 Subject: [PATCH 11/23] Update docs/guide/input.md Co-authored-by: darrenburns --- docs/guide/input.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/input.md b/docs/guide/input.md index 10f7fcfbe..02a9abc4b 100644 --- a/docs/guide/input.md +++ b/docs/guide/input.md @@ -35,7 +35,7 @@ The key event contains following attributes which your app can use to know how t #### key -The `key` attribute is a string which identifies the key that was pressed. The value of `key` will be a single character for letter and numbers, or a longer identifier for other keys. +The `key` attribute is a string which identifies the key that was pressed. The value of `key` will be a single character for letters and numbers, or a longer identifier for other keys. Some keys may be combined with ++shift++ key. In the case of letters, this will result in a capital letter as you might expect. For non-printable keys, the `key` attribute will be prefixed with `shift+`. For example ++shift+home++ will produce an event with `key="shift+home"`. From 0325c50e7aa030b681c092b9d6bc0425e8148ce7 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 21 Dec 2022 15:04:19 +0000 Subject: [PATCH 12/23] Update docs/guide/input.md Co-authored-by: darrenburns --- docs/guide/input.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/input.md b/docs/guide/input.md index 02a9abc4b..164e416a7 100644 --- a/docs/guide/input.md +++ b/docs/guide/input.md @@ -37,7 +37,7 @@ The key event contains following attributes which your app can use to know how t The `key` attribute is a string which identifies the key that was pressed. The value of `key` will be a single character for letters and numbers, or a longer identifier for other keys. -Some keys may be combined with ++shift++ key. In the case of letters, this will result in a capital letter as you might expect. For non-printable keys, the `key` attribute will be prefixed with `shift+`. For example ++shift+home++ will produce an event with `key="shift+home"`. +Some keys may be combined with the ++shift++ key. In the case of letters, this will result in a capital letter as you might expect. For non-printable keys, the `key` attribute will be prefixed with `shift+`. For example, ++shift+home++ will produce an event with `key="shift+home"`. Many keys can also be combined with ++ctrl++ which will prefix the key with `ctrl+`. For instance ++ctrl+p++ will produce an event with `key="ctrl+p"`. From 8b7f3c0968cbfd5ac07f0e134d15387d442e3ae0 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 21 Dec 2022 15:04:29 +0000 Subject: [PATCH 13/23] Update docs/guide/input.md Co-authored-by: darrenburns --- docs/guide/input.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/input.md b/docs/guide/input.md index 164e416a7..9c03815f7 100644 --- a/docs/guide/input.md +++ b/docs/guide/input.md @@ -39,7 +39,7 @@ The `key` attribute is a string which identifies the key that was pressed. The v Some keys may be combined with the ++shift++ key. In the case of letters, this will result in a capital letter as you might expect. For non-printable keys, the `key` attribute will be prefixed with `shift+`. For example, ++shift+home++ will produce an event with `key="shift+home"`. -Many keys can also be combined with ++ctrl++ which will prefix the key with `ctrl+`. For instance ++ctrl+p++ will produce an event with `key="ctrl+p"`. +Many keys can also be combined with ++ctrl++ which will prefix the key with `ctrl+`. For instance, ++ctrl+p++ will produce an event with `key="ctrl+p"`. !!! warning From b04a1166200587def2354cf2040fdbe9e7f52345 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 21 Dec 2022 15:04:40 +0000 Subject: [PATCH 14/23] Update docs/guide/input.md Co-authored-by: darrenburns --- docs/guide/input.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/input.md b/docs/guide/input.md index 9c03815f7..a966f8c68 100644 --- a/docs/guide/input.md +++ b/docs/guide/input.md @@ -55,7 +55,7 @@ For example the ++p++ key will produce `character="p"` but ++f2++ will produce ` The `name` attribute is similar to `key` but, unlike `key`, is guaranteed to be valid within a Python function name. Textual derives `name` from the `key` attribute by lower casing it and replacing `+` with `_`. Upper case letters are prefixed with `upper_` to distinguish them from lower case names. -For example, ++ctrl+q++ produces `name="ctrl_p"` and ++shift+p++ produces `name="upper_p"`. +For example, ++ctrl+p++ produces `name="ctrl_p"` and ++shift+p++ produces `name="upper_p"`. #### is_printable From 77eadc9fa1c095b6476a7c32d55e9fbe517700ed Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 21 Dec 2022 15:17:17 +0000 Subject: [PATCH 15/23] words --- CHANGELOG.md | 1 - docs/guide/input.md | 2 +- src/textual/events.py | 6 ++++-- tests/test_message_pump.py | 6 +++--- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8b15a173..36ac7188f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Deprecated `PRIORITY_BINDINGS` class variable. - Renamed `char` to `character` on Key event. - Renamed `key_name` to `name` on Key event. -- Moved Ctrl+C, tab, and shift+tab to App BINDINGS - Queries/`walk_children` no longer includes self in results by default https://github.com/Textualize/textual/pull/1416 ## [0.7.0] - 2022-12-17 diff --git a/docs/guide/input.md b/docs/guide/input.md index a966f8c68..cf147cb62 100644 --- a/docs/guide/input.md +++ b/docs/guide/input.md @@ -160,7 +160,7 @@ The tuple of three strings may be enough for simple bindings, but you can also r ### Priority bindings -Textual checks bindings for the focused widget first. If there is no matching binding, then the parent widget(s) will be checked. Occasionally, you may want bindings to be checked *before* the focused widget. Typically to create hot-keys. +Individual bindings may be marked as a *priority*, which means they will be checked prior to the bindings of the focused widget. This feature is often used to create hot-keys on the app or screen. Such bindings can not be disabled by binding the same key on a widget. You can create priority key bindings by setting `priority=True` on the Binding object. Textual uses this feature to add a default binding for ++ctrl+c++ so there is always a way to exit the app. Here's the bindings from the App base class. Note the first binding is set as a priority: diff --git a/src/textual/events.py b/src/textual/events.py index 6f229b358..1f509a072 100644 --- a/src/textual/events.py +++ b/src/textual/events.py @@ -200,10 +200,12 @@ class Key(InputEvent): __slots__ = ["key", "character", "aliases"] - def __init__(self, sender: MessageTarget, key: str, char: str | None) -> None: + def __init__(self, sender: MessageTarget, key: str, character: str | None) -> None: super().__init__(sender) self.key = key - self.character = (key if len(key) == 1 else None) if char is None else char + self.character = ( + (key if len(key) == 1 else None) if character is None else character + ) self.aliases = _get_key_aliases(key) def __rich_repr__(self) -> rich.repr.Result: diff --git a/tests/test_message_pump.py b/tests/test_message_pump.py index 749afa1d8..ce665f7c9 100644 --- a/tests/test_message_pump.py +++ b/tests/test_message_pump.py @@ -17,7 +17,7 @@ class ValidWidget(Widget): async def test_dispatch_key_valid_key(): widget = ValidWidget() - result = await widget.dispatch_key(Key(widget, key="x", char="x")) + result = await widget.dispatch_key(Key(widget, key="x", character="x")) assert result is True assert widget.called_by == widget.key_x @@ -26,7 +26,7 @@ async def test_dispatch_key_valid_key_alias(): """When you press tab or ctrl+i, it comes through as a tab key event, but handlers for tab and ctrl+i are both considered valid.""" widget = ValidWidget() - result = await widget.dispatch_key(Key(widget, key="tab", char="\t")) + result = await widget.dispatch_key(Key(widget, key="tab", character="\t")) assert result is True assert widget.called_by == widget.key_ctrl_i @@ -52,5 +52,5 @@ async def test_dispatch_key_raises_when_conflicting_handler_aliases(): In the terminal, they're the same thing, so we fail fast via exception here.""" widget = DuplicateHandlersWidget() with pytest.raises(DuplicateKeyHandlers): - await widget.dispatch_key(Key(widget, key="tab", char="\t")) + await widget.dispatch_key(Key(widget, key="tab", character="\t")) assert widget.called_by == widget.key_tab From 9aebdd9484b477023b4ae72ebafbd5667c0ac175 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 21 Dec 2022 15:19:02 +0000 Subject: [PATCH 16/23] updated docstring --- src/textual/binding.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/binding.py b/src/textual/binding.py index 927434868..c8e145977 100644 --- a/src/textual/binding.py +++ b/src/textual/binding.py @@ -38,7 +38,7 @@ class Binding: key_display: str | None = None """str | None: How the key should be shown in footer.""" priority: bool = False - """bool | None: Is this a priority binding, checked form app down to focused widget?""" + """bool: Enable priority binding for this key.""" @rich.repr.auto From 689ec477eada2d798473b0c78430fc838cc33744 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 21 Dec 2022 15:19:46 +0000 Subject: [PATCH 17/23] Update tests/test_keys.py Co-authored-by: darrenburns --- tests/test_keys.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_keys.py b/tests/test_keys.py index 19a9dc136..64f28219f 100644 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -35,7 +35,6 @@ async def test_character_bindings(): counter += 1 app = BindApp() - print(app._bindings) async with app.run_test() as pilot: await pilot.press(".") assert counter == 1 From b793b06b56cd99a2434bbd1cdd638866fbac07ce Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 21 Dec 2022 15:22:47 +0000 Subject: [PATCH 18/23] Update docs/guide/input.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Rodrigo Girão Serrão <5621605+rodrigogiraoserrao@users.noreply.github.com> --- docs/guide/input.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/input.md b/docs/guide/input.md index cf147cb62..e4611626c 100644 --- a/docs/guide/input.md +++ b/docs/guide/input.md @@ -31,7 +31,7 @@ When you press a key, the app will receive the event and write it to a [TextLog] ### Key Event -The key event contains following attributes which your app can use to know how to respond. +The key event contains the following attributes which your app can use to know how to respond. #### key From 17a0d5294539d53492a33f94dc6ceb383d789b39 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 21 Dec 2022 15:23:05 +0000 Subject: [PATCH 19/23] Update docs/guide/input.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Rodrigo Girão Serrão <5621605+rodrigogiraoserrao@users.noreply.github.com> --- docs/guide/input.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/input.md b/docs/guide/input.md index e4611626c..b380965b7 100644 --- a/docs/guide/input.md +++ b/docs/guide/input.md @@ -43,7 +43,7 @@ Many keys can also be combined with ++ctrl++ which will prefix the key with `ctr !!! warning - Not all keys combinations are supported in terminals, and some keys may be intercepted by your OS. If in doubt, run `textual keys` from the command line. + Not all keys combinations are supported in terminals and some keys may be intercepted by your OS. If in doubt, run `textual keys` from the command line. #### character From 611dffcbba11dc421dab6ccd4d8c5860803d57ec Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 21 Dec 2022 15:23:19 +0000 Subject: [PATCH 20/23] Update docs/guide/input.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Rodrigo Girão Serrão <5621605+rodrigogiraoserrao@users.noreply.github.com> --- docs/guide/input.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/input.md b/docs/guide/input.md index b380965b7..1915b8640 100644 --- a/docs/guide/input.md +++ b/docs/guide/input.md @@ -47,7 +47,7 @@ Many keys can also be combined with ++ctrl++ which will prefix the key with `ctr #### character -If the key has an associated printable character then `character` will contain a string with a single unicode character. If there is no printable character for the key (such as for function keys) then `character` will be `None`. +If the key has an associated printable character, then `character` will contain a string with a single Unicode character. If there is no printable character for the key (such as for function keys) then `character` will be `None`. For example the ++p++ key will produce `character="p"` but ++f2++ will produce `character=None`. From b2516c5ce3842ab639bc5653de36c081a59963a6 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 21 Dec 2022 15:27:58 +0000 Subject: [PATCH 21/23] Update docs/guide/input.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Rodrigo Girão Serrão <5621605+rodrigogiraoserrao@users.noreply.github.com> --- docs/guide/input.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/input.md b/docs/guide/input.md index 1915b8640..5230b6ab2 100644 --- a/docs/guide/input.md +++ b/docs/guide/input.md @@ -53,7 +53,7 @@ For example the ++p++ key will produce `character="p"` but ++f2++ will produce ` #### name -The `name` attribute is similar to `key` but, unlike `key`, is guaranteed to be valid within a Python function name. Textual derives `name` from the `key` attribute by lower casing it and replacing `+` with `_`. Upper case letters are prefixed with `upper_` to distinguish them from lower case names. +The `name` attribute is similar to `key` but, unlike `key`, is guaranteed to be valid within a Python function name. Textual derives `name` from the `key` attribute by lower casing it and replacing `+` with `_`. Upper case letters are prefixed with `upper_` to distinguish them from lower case names. For example, ++ctrl+p++ produces `name="ctrl_p"` and ++shift+p++ produces `name="upper_p"`. From 14953cd1db5aa0cc525907e0bd8b394e4a2e9671 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 21 Dec 2022 15:28:52 +0000 Subject: [PATCH 22/23] Update docs/guide/input.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Rodrigo Girão Serrão <5621605+rodrigogiraoserrao@users.noreply.github.com> --- docs/guide/input.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/input.md b/docs/guide/input.md index 5230b6ab2..489955249 100644 --- a/docs/guide/input.md +++ b/docs/guide/input.md @@ -68,7 +68,7 @@ Some keys or combinations of keys can produce the same event. For instance, the ### Key methods -Textual offers a convenient way of handling specific keys. If you create a method beginning with `key_` followed by the key name (the event's `name` attribute), then that method will be called in response to the key. +Textual offers a convenient way of handling specific keys. If you create a method beginning with `key_` followed by the key name (the event's `name` attribute), then that method will be called in response to the key press. Let's add a key method to the example code. From 6c5ba82bff5e7db72f0e7794401ea4562578500f Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 21 Dec 2022 15:43:29 +0000 Subject: [PATCH 23/23] fix for win --- tests/snapshot_tests/conftest.py | 3 +-- tests/test_keys.py | 4 ++++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/snapshot_tests/conftest.py b/tests/snapshot_tests/conftest.py index 782f757d4..127d53596 100644 --- a/tests/snapshot_tests/conftest.py +++ b/tests/snapshot_tests/conftest.py @@ -191,10 +191,9 @@ def pytest_terminal_summary( console = Console(legacy_windows=False, force_terminal=True) if diffs: snapshot_report_location = config._textual_snapshot_html_report - console.rule("[b red]Textual Snapshot Report", style="red") + console.print("[b red]Textual Snapshot Report", style="red") console.print( f"\n[black on red]{len(diffs)} mismatched snapshots[/]\n" f"\n[b]View the [link=file://{snapshot_report_location}]failure report[/].\n" ) console.print(f"[dim]{snapshot_report_location}\n") - console.rule(style="red") diff --git a/tests/test_keys.py b/tests/test_keys.py index 64f28219f..b0948757c 100644 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -37,10 +37,14 @@ async def test_character_bindings(): app = BindApp() async with app.run_test() as pilot: await pilot.press(".") + await pilot.pause() assert counter == 1 await pilot.press("~") + await pilot.pause() assert counter == 2 await pilot.press(" ") + await pilot.pause() assert counter == 3 await pilot.press("x") + await pilot.pause() assert counter == 3