Skip to content

Commit 91e4da1

Browse files
authored
Merge pull request #1417 from Textualize/key-refactor
keys refactor
2 parents 03d5075 + 6c5ba82 commit 91e4da1

File tree

21 files changed

+259
-146
lines changed

21 files changed

+259
-146
lines changed

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](http://keepachangelog.com/)
66
and this project adheres to [Semantic Versioning](http://semver.org/).
77

8+
89
## [0.8.0] - Unreleased
910

1011
### Fixed
@@ -18,10 +19,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1819

1920
- Added `textual.actions.SkipAction` exception which can be raised from an action to allow parents to process bindings.
2021
- Added `textual keys` preview.
22+
- Added ability to bind to a character in addition to key name. i.e. you can bind to "." or "full_stop".
23+
- Added TextLog.shrink attribute to allow renderable to reduce in size to fit width.
2124

2225
### Changed
2326

24-
- Moved Ctrl+C, tab, and shift+tab to App BINDINGS
27+
- Deprecated `PRIORITY_BINDINGS` class variable.
28+
- Renamed `char` to `character` on Key event.
29+
- Renamed `key_name` to `name` on Key event.
2530
- Queries/`walk_children` no longer includes self in results by default https://github.com/Textualize/textual/pull/1416
2631

2732
## [0.7.0] - 2022-12-17

docs/guide/actions.md

Lines changed: 8 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ Textual will run actions bound to keys. The following example adds key [bindings
9090

9191
If you run this example, you can change the background by pressing keys in addition to clicking links.
9292

93+
See the previous section on [input](./input.md#bindings) for more information on bindings.
94+
9395
## Namespaces
9496

9597
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.
@@ -124,37 +126,9 @@ In the previous example if you wanted a link to set the background on the app ra
124126

125127
Textual supports the following builtin actions which are defined on the app.
126128

127-
128-
### Bell
129-
130-
::: textual.app.App.action_bell
131-
options:
132-
show_root_heading: false
133-
134-
### Push screen
135-
136-
::: textual.app.App.action_push_screen
137-
138-
139-
### Pop screen
140-
141-
::: textual.app.App.action_pop_screen
142-
143-
144-
### Screenshot
145-
146-
::: textual.app.App.action_screenshot
147-
148-
149-
### Switch screen
150-
151-
::: textual.app.App.action_switch_screen
152-
153-
154-
### Toggle_dark
155-
156-
::: textual.app.App.action_toggle_dark
157-
158-
### Quit
159-
160-
::: textual.app.App.action_quit
129+
- [action_bell][textual.app.App.action_bell]
130+
- [action_push_screen][textual.app.App.action_push_screen]
131+
- [action_pop_screen][textual.app.App.action_pop_screen]
132+
- [action_switch_screen][textual.app.App.action_switch_screen]
133+
- [action_screenshot][textual.app.App.action_screenshot]
134+
- [action_toggle_dark][textual.app.App.action_toggle_dark]

docs/guide/input.md

Lines changed: 51 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ This chapter will discuss how to make your app respond to input in the form of k
1010

1111
## Keyboard input
1212

13-
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.
13+
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.
1414

1515
=== "key01.py"
1616

@@ -23,25 +23,52 @@ The most fundamental way to receive input is via [Key](./events/key) events. Let
2323
```{.textual path="docs/examples/guide/input/key01.py", press="T,e,x,t,u,a,l,!,_"}
2424
```
2525

26-
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.
26+
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.
2727

28-
### Attributes
28+
!!! tip
2929

30-
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.
30+
For a more feature feature rich version of this example, run `textual keys` from the command line.
3131

32-
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).
32+
### Key Event
3333

34-
To illustrate the difference between `key` and `char`, try `key01.py` with the space key. You should see something like the following:
34+
The key event contains the following attributes which your app can use to know how to respond.
3535

36-
```{.textual path="docs/examples/guide/input/key01.py", press="space,_"}
36+
#### key
3737

38-
```
38+
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.
39+
40+
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"`.
41+
42+
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"`.
43+
44+
!!! warning
45+
46+
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.
47+
48+
#### character
49+
50+
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`.
51+
52+
For example the ++p++ key will produce `character="p"` but ++f2++ will produce `character=None`.
53+
54+
#### name
55+
56+
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.
57+
58+
For example, ++ctrl+p++ produces `name="ctrl_p"` and ++shift+p++ produces `name="upper_p"`.
59+
60+
#### is_printable
61+
62+
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.
63+
64+
#### aliases
65+
66+
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"]`
3967

40-
Note that the `key` attribute contains the word "space" while the `char` attribute contains a literal space.
4168

4269
### Key methods
4370

44-
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.
71+
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.
4572

4673
Let's add a key method to the example code.
4774

@@ -127,24 +154,28 @@ Note how the footer displays bindings and makes them clickable.
127154
Multiple keys can be bound to a single action by comma-separating them.
128155
For example, `("r,t", "add_bar('red')", "Add Red")` means both ++r++ and ++t++ are bound to `add_bar('red')`.
129156

157+
### Binding class
130158

131-
!!! note
132-
133-
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.
159+
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.
134160

135-
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.
161+
### Priority bindings
136162

137-
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`.
163+
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.
138164

139-
### Binding class
165+
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:
140166

141-
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.
167+
```python
168+
BINDINGS = [
169+
Binding("ctrl+c", "quit", "Quit", show=False, priority=True),
170+
Binding("tab", "focus_next", "Focus Next", show=False),
171+
Binding("shift+tab", "focus_previous", "Focus Previous", show=False),
172+
]
173+
```
142174

143-
### Why use bindings?
175+
### Show bindings
144176

145-
Bindings are particularly useful for configurable hot-keys. Bindings can also be inspected in widgets such as [Footer](../widgets/footer.md).
177+
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.
146178

147-
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.
148179

149180
## Mouse Input
150181

src/textual/app.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -232,8 +232,6 @@ class App(Generic[ReturnType], DOMNode):
232232
}
233233
"""
234234

235-
PRIORITY_BINDINGS = True
236-
237235
SCREENS: dict[str, Screen | Callable[[], Screen]] = {}
238236
_BASE_PATH: str | None = None
239237
CSS_PATH: CSSPathType = None
@@ -242,10 +240,8 @@ class App(Generic[ReturnType], DOMNode):
242240

243241
BINDINGS = [
244242
Binding("ctrl+c", "quit", "Quit", show=False, priority=True),
245-
Binding("tab", "focus_next", "Focus Next", show=False, priority=False),
246-
Binding(
247-
"shift+tab", "focus_previous", "Focus Previous", show=False, priority=False
248-
),
243+
Binding("tab", "focus_next", "Focus Next", show=False),
244+
Binding("shift+tab", "focus_previous", "Focus Previous", show=False),
249245
]
250246

251247
title: Reactive[str] = Reactive("")

src/textual/binding.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import rich.repr
77

8+
from textual.keys import _character_to_key
89
from textual._typing import TypeAlias
910

1011
BindingType: TypeAlias = "Binding | tuple[str, str, str]"
@@ -18,6 +19,10 @@ class NoBinding(Exception):
1819
"""A binding was not found."""
1920

2021

22+
class InvalidBinding(Exception):
23+
"""Binding key is in an invalid format."""
24+
25+
2126
@dataclass(frozen=True)
2227
class Binding:
2328
"""The configuration of a key binding."""
@@ -32,8 +37,8 @@ class Binding:
3237
"""bool: Show the action in Footer, or False to hide."""
3338
key_display: str | None = None
3439
"""str | None: How the key should be shown in footer."""
35-
priority: bool | None = None
36-
"""bool | None: Is this a priority binding, checked form app down to focused widget?"""
40+
priority: bool = False
41+
"""bool: Enable priority binding for this key."""
3742

3843

3944
@rich.repr.auto
@@ -43,13 +48,11 @@ class Bindings:
4348
def __init__(
4449
self,
4550
bindings: Iterable[BindingType] | None = None,
46-
default_priority: bool | None = None,
4751
) -> None:
4852
"""Initialise a collection of bindings.
4953
5054
Args:
5155
bindings (Iterable[BindingType] | None, optional): An optional set of initial bindings.
52-
default_priority (bool | None, optional): The default priority of the bindings.
5356
5457
Note:
5558
The iterable of bindings can contain either a `Binding`
@@ -71,17 +74,20 @@ def make_bindings(bindings: Iterable[BindingType]) -> Iterable[Binding]:
7174
# be a list of keys, so now we unroll that single Binding
7275
# into a (potential) collection of Binding instances.
7376
for key in binding.key.split(","):
77+
key = key.strip()
78+
if not key:
79+
raise InvalidBinding(
80+
f"Can not bind empty string in {binding.key!r}"
81+
)
82+
if len(key) == 1:
83+
key = _character_to_key(key)
7484
yield Binding(
75-
key=key.strip(),
85+
key=key,
7686
action=binding.action,
7787
description=binding.description,
7888
show=binding.show,
7989
key_display=binding.key_display,
80-
priority=(
81-
default_priority
82-
if binding.priority is None
83-
else binding.priority
84-
),
90+
priority=binding.priority,
8591
)
8692

8793
self.keys: MutableMapping[str, Binding] = (

src/textual/cli/cli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ def colors():
127127

128128
@run.command("keys")
129129
def keys():
130-
"""Show key events"""
130+
"""Show key events."""
131131
from textual.cli.previews import keys
132132

133133
keys.app.run()

src/textual/cli/previews/keys.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
INSTRUCTIONS = """\
1010
Press some keys!
1111
12-
Because we want to display all the keys, Ctrl+C won't work for this example. Use the button below to quit.\
12+
Because we want to display all the keys, ctrl+C won't quit this example. Use the Quit button below to exit the app.\
1313
"""
1414

1515

src/textual/dom.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,6 @@ class DOMNode(MessagePump):
9292
# Virtual DOM nodes
9393
COMPONENT_CLASSES: ClassVar[set[str]] = set()
9494

95-
# Should the content of BINDINGS be treated as priority bindings?
96-
PRIORITY_BINDINGS: ClassVar[bool] = False
97-
9895
# Mapping of key bindings
9996
BINDINGS: ClassVar[list[BindingType]] = []
10097

@@ -233,13 +230,13 @@ def _merge_bindings(cls) -> Bindings:
233230

234231
for base in reversed(cls.__mro__):
235232
if issubclass(base, DOMNode):
236-
# See if the current class wants to set the bindings as
237-
# priority bindings. If it doesn't have that property on the
238-
# class, go with what we saw last.
239-
priority = base.__dict__.get("PRIORITY_BINDINGS", priority)
240233
if not base._inherit_bindings:
241234
bindings.clear()
242-
bindings.append(Bindings(base.__dict__.get("BINDINGS", []), priority))
235+
bindings.append(
236+
Bindings(
237+
base.__dict__.get("BINDINGS", []),
238+
)
239+
)
243240
keys = {}
244241
for bindings_ in bindings:
245242
keys.update(bindings_.keys)

0 commit comments

Comments
 (0)