Skip to content

Commit 48b20f1

Browse files
authored
Merge pull request #22 from sirfuzzalot/feat/custom-message-names
feat: custom message names
2 parents 4c6a5b7 + fc7349b commit 48b20f1

File tree

8 files changed

+312
-44
lines changed

8 files changed

+312
-44
lines changed

README.md

Lines changed: 148 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,60 @@
77

88
Textual Inputs is a collection of input widgets for the [Textual](https://github.com/willmcgugan/textual) TUI framework.
99

10-
⚠️ This library is experimental and its interfaces are likely
11-
to change, much like the underlying Textual library.
10+
> ⚠️ This library is experimental and its interfaces will change. While
11+
> Textual Inputs is pre-alpha please pin your projects to the minor release
12+
> number to avoid breaking changes. For example: textual-inputs=0.2.\*
1213
13-
## Supported Widgets
14+
---
15+
16+
## News
17+
18+
### v0.2.4
19+
20+
Adds support for customizing the message handler names for on change and
21+
on focus events emitted by the inputs. Under the hood this will generate
22+
a `Message` class with the appropriate name for Textual to send it to
23+
the handler name provided. You'll then want add the handler to the input's
24+
parent or the App instance. If you opt not to customize these handlers,
25+
their values will be the default `handle_input_on_change` and `handle_input_on_focus`.
26+
See `examples/simple_form.py` for a working example.
27+
28+
```python
29+
email = TextInput(name="email", title="Email")
30+
email.on_change_handler_name = "handle_email_on_change"
31+
email.on_focus_handler_name = "handle_email_on_focus"
32+
```
33+
34+
---
35+
36+
## Quick Start
37+
38+
Installation
39+
40+
```bash
41+
python -m pip install textual-inputs=0.2.*
42+
```
43+
44+
To use Textual Inputs
45+
46+
```python
47+
from textual_inputs import TextInput, IntegerInput
48+
```
49+
50+
Checkout the [examples](https://github.com/sirfuzzalot/textual-inputs/tree/main/examples) for reference.
51+
52+
```bash
53+
git clone https://github.com/sirfuzzalot/textual-inputs.git
54+
cd textual-inputs
55+
python3 -m venv venv
56+
source venv/bin/activate
57+
python -m pip install -e .
58+
python examples/simple_form.py
59+
```
60+
61+
---
62+
63+
## Widgets
1464

1565
### TextInput 🔡
1666

@@ -30,25 +80,105 @@ to change, much like the underlying Textual library.
3080
- controls: arrow right/left, home, end, delete, backspace/ctrl+h, escape
3181
- emits - InputOnChange, InputOnFocus messages
3282

33-
## Quick Start
83+
---
3484

35-
```bash
36-
python -m pip install textual-inputs
37-
```
85+
## API Reference
3886

39-
Checkout the [examples](https://github.com/sirfuzzalot/textual-inputs/tree/main/examples) for reference.
87+
Textual Inputs has two widgets, here are their attributes.
4088

41-
```bash
42-
git clone https://github.com/sirfuzzalot/textual-inputs.git
43-
cd textual-inputs
44-
python3 -m venv venv
45-
source venv/bin/activate
46-
python -m pip install -r requirements.txt
47-
python examples/simple_form.py
48-
```
89+
```python
90+
class TextInput(Widget):
91+
"""
92+
A simple text input widget.
4993
50-
To use Textual Inputs
94+
Args:
95+
name (Optional[str]): The unique name of the widget. If None, the
96+
widget will be automatically named.
97+
value (str, optional): Defaults to "". The starting text value.
98+
placeholder (str, optional): Defaults to "". Text that appears
99+
in the widget when value is "" and the widget is not focused.
100+
title (str, optional): Defaults to "". A title on the top left
101+
of the widget's border.
102+
password (bool, optional): Defaults to False. Hides the text
103+
input, replacing it with bullets.
104+
105+
Attributes:
106+
value (str): the value of the text field
107+
placeholder (str): The placeholder message.
108+
title (str): The displayed title of the widget.
109+
has_password (bool): True if the text field masks the input.
110+
has_focus (bool): True if the widget is focused.
111+
cursor (Tuple[str, Style]): The character used for the cursor
112+
and a rich Style object defining its appearance.
113+
on_change_handler_name (str): name of handler function to be
114+
called when an on change event occurs. Defaults to
115+
handle_input_on_change.
116+
on_focus_handler_name (name): name of handler function to be
117+
called when an on focus event occurs. Defaults to
118+
handle_input_on_focus.
119+
120+
Events:
121+
InputOnChange: Emitted when the contents of the input changes.
122+
InputOnFocus: Emitted when the widget becomes focused.
123+
124+
Examples:
125+
126+
.. code-block:: python
127+
128+
from textual_inputs import TextInput
129+
130+
email_input = TextInput(
131+
name="email",
132+
placeholder="enter your email address...",
133+
title="Email",
134+
)
135+
136+
"""
137+
```
51138

52139
```python
53-
from textual_inputs import TextInput, IntegerInput
140+
class IntegerInput(Widget):
141+
"""
142+
A simple integer input widget.
143+
144+
Args:
145+
name (Optional[str]): The unique name of the widget. If None, the
146+
widget will be automatically named.
147+
value (Optional[int]): The starting integer value.
148+
placeholder (Union[str, int, optional): Defaults to "". Text that
149+
appears in the widget when value is "" and the widget is not focused.
150+
title (str, optional): Defaults to "". A title on the top left
151+
of the widget's border.
152+
153+
Attributes:
154+
value (Union[int, None]): the value of the input field
155+
placeholder (str): The placeholder message.
156+
title (str): The displayed title of the widget.
157+
has_focus (bool): True if the widget is focused.
158+
cursor (Tuple[str, Style]): The character used for the cursor
159+
and a rich Style object defining its appearance.
160+
on_change_handler_name (str): name of handler function to be
161+
called when an on change event occurs. Defaults to
162+
handle_input_on_change.
163+
on_focus_handler_name (name): name of handler function to be
164+
called when an on focus event occurs. Defaults to
165+
handle_input_on_focus.
166+
167+
Events:
168+
InputOnChange: Emitted when the contents of the input changes.
169+
InputOnFocus: Emitted when the widget becomes focused.
170+
171+
Examples:
172+
173+
.. code-block:: python
174+
175+
from textual_inputs import IntegerInput
176+
177+
age_input = IntegerInput(
178+
name="age",
179+
placeholder="enter your age...",
180+
title="Age",
181+
)
182+
183+
"""
54184
```

examples/simple_form.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
# app.py
2+
from __future__ import annotations
3+
4+
from typing import TYPE_CHECKING
5+
26
import rich.box
37
from rich.panel import Panel
48
from rich.style import Style
@@ -11,17 +15,20 @@
1115

1216
from textual_inputs import IntegerInput, TextInput
1317

18+
if TYPE_CHECKING:
19+
from textual.message import Message
20+
1421

1522
class CustomHeader(Header):
1623
"""Override the default Header for Styling"""
1724

1825
def __init__(self) -> None:
1926
super().__init__()
2027
self.tall = False
28+
self.style = Style(color="white", bgcolor="rgb(98,98,98)")
2129

2230
def render(self) -> Table:
2331
header_table = Table.grid(padding=(0, 1), expand=True)
24-
header_table.style = Style(color="white", bgcolor="rgb(98,98,98)")
2532
header_table.add_column(justify="left", ratio=0, width=8)
2633
header_table.add_column("title", justify="center", ratio=1)
2734
header_table.add_column("clock", justify="right", width=8)
@@ -62,7 +69,7 @@ def make_key_text(self) -> Text:
6269
return text
6370

6471

65-
class Demo(App):
72+
class SimpleForm(App):
6673

6774
current_index: Reactive[int] = Reactive(-1)
6875

@@ -88,22 +95,26 @@ async def on_mount(self) -> None:
8895
placeholder="enter your username...",
8996
title="Username",
9097
)
98+
self.username.on_change_handler_name = "handle_username_on_change"
99+
91100
self.password = TextInput(
92101
name="password",
93102
title="Password",
94103
password=True,
95104
)
105+
96106
self.age = IntegerInput(
97107
name="age",
98108
placeholder="enter your age...",
99109
title="Age",
100110
)
111+
self.age.on_change_handler_name = "handle_age_on_change"
112+
101113
self.output = Static(
102114
renderable=Panel(
103115
"", title="Report", border_style="blue", box=rich.box.SQUARE
104116
)
105117
)
106-
107118
await self.view.dock(self.output, edge="left", size=40)
108119
await self.view.dock(self.username, self.password, self.age, edge="top")
109120

@@ -134,12 +145,15 @@ async def action_reset_focus(self) -> None:
134145
self.current_index = -1
135146
await self.header.focus()
136147

137-
async def handle_input_on_change(self, message) -> None:
138-
self.log(f"Input: {message.sender.name} changed")
148+
async def handle_username_on_change(self, message: Message) -> None:
149+
self.log(f"Username Field Contains: {message.sender.value}")
150+
151+
async def handle_age_on_change(self, message: Message) -> None:
152+
self.log(f"Age Field Contains: {message.sender.value}")
139153

140-
async def handle_input_on_focus(self, message) -> None:
154+
async def handle_input_on_focus(self, message: Message) -> None:
141155
self.current_index = self.tab_index.index(message.sender.name)
142156

143157

144158
if __name__ == "__main__":
145-
Demo.run(title="Textual-Inputs Demo", log="textual.log")
159+
SimpleForm.run(title="Textual-Inputs Demo", log="textual.log")

requirements.txt

Lines changed: 0 additions & 1 deletion
This file was deleted.

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ package_dir =
3333
packages = find:
3434
python_requires = >=3.7
3535
install_requires =
36-
textual >= 0.1.11,< 0.2
36+
textual >= 0.1.14,< 0.2
3737

3838
[options.packages.find]
3939
where = src

src/textual_inputs/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from .integer_input import IntegerInput
22
from .text_input import TextInput
33

4-
__version__ = "0.2.3"
4+
__version__ = "0.2.4"
55

66
__all__ = ["IntegerInput", "TextInput"]

src/textual_inputs/events.py

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,67 @@
11
"""
22
Events for Textual-Inputs
33
"""
4+
from __future__ import annotations
5+
6+
from string import ascii_lowercase, digits
7+
48
from textual.message import Message
59

10+
HANDLER_SAFE = ascii_lowercase + digits + "_"
11+
612

713
class InputOnChange(Message, bubble=True):
8-
"""Emitted when the value of an input changes"""
14+
"""Generic on change message for inputs"""
915

10-
pass
16+
_handler: str = "handle_input_on_change"
1117

1218

1319
class InputOnFocus(Message, bubble=True):
14-
"""Emitted when the input becomes focused"""
20+
"""Generic on focus message for inputs"""
21+
22+
_handler: str = "handle_input_on_focus"
23+
24+
25+
def make_message_class(handler_name: str) -> Message:
26+
"""
27+
Produces an appropriately named Message subclass that will call the
28+
handler with the same name as handler_name.
29+
30+
Args:
31+
handler_name (str): handler function name. Must start with "handle_" and
32+
contain only lowercase ASCII letters, numbers and underscores.
33+
34+
Returns:
35+
Message: a subclass of the Message class.
36+
37+
Raises:
38+
ValueError: handler_name must start with 'handle_'.
39+
ValueError: handler_name must contain only lowercase ASCII
40+
letters, numbers and underscores.
41+
42+
Example:
43+
44+
.. code-block:: python
45+
46+
>>>make_message_class("handle_username_on_change")
47+
<class 'textual_inputs.events.username_on_change>
48+
>>>def handle_username_on_change(self, event: Message) -> None:
49+
50+
"""
51+
if not handler_name.startswith("handle_"):
52+
raise ValueError("handler_name must start with 'handle_'")
53+
54+
if len(handler_name) < 7:
55+
raise ValueError("handler_name must be greater than 7 characters")
1556

16-
pass
57+
for char in handler_name:
58+
if char not in HANDLER_SAFE:
59+
raise ValueError(
60+
"handler_name must contain only lowercase ASCII letters, "
61+
+ "numbers and underscores."
62+
)
63+
name = handler_name.lower()[7:]
64+
t = type(name, (Message,), {})
65+
t._handler = handler_name
66+
t.bubble = True
67+
return t

0 commit comments

Comments
 (0)