-
Notifications
You must be signed in to change notification settings - Fork 815
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' into app-focus-style
- Loading branch information
Showing
17 changed files
with
1,487 additions
and
114 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
from textual.app import App, ComposeResult | ||
from textual.widgets import Label, MaskedInput | ||
|
||
|
||
class MaskedInputApp(App): | ||
# (1)! | ||
CSS = """ | ||
MaskedInput.-valid { | ||
border: tall $success 60%; | ||
} | ||
MaskedInput.-valid:focus { | ||
border: tall $success; | ||
} | ||
MaskedInput { | ||
margin: 1 1; | ||
} | ||
Label { | ||
margin: 1 2; | ||
} | ||
""" | ||
|
||
def compose(self) -> ComposeResult: | ||
yield Label("Enter a valid credit card number.") | ||
yield MaskedInput( | ||
template="9999-9999-9999-9999;0", # (2)! | ||
) | ||
|
||
|
||
app = MaskedInputApp() | ||
|
||
if __name__ == "__main__": | ||
app.run() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
# MaskedInput | ||
|
||
!!! tip "Added in version 0.80.0" | ||
|
||
A masked input derived from `Input`, allowing to restrict user input and give visual aid via a simple template mask, which also acts as an implicit *[validator][textual.validation.Validator]*. | ||
|
||
- [x] Focusable | ||
- [ ] Container | ||
|
||
## Example | ||
|
||
The example below shows a masked input to ease entering a credit card number. | ||
|
||
=== "Output" | ||
|
||
```{.textual path="docs/examples/widgets/masked_input.py"} | ||
``` | ||
|
||
=== "checkbox.py" | ||
|
||
```python | ||
--8<-- "docs/examples/widgets/masked_input.py" | ||
``` | ||
|
||
## Reactive Attributes | ||
|
||
| Name | Type | Default | Description | | ||
| ---------- | ----- | ------- | ------------------------- | | ||
| `template` | `str` | `""` | The template mask string. | | ||
|
||
### The template string format | ||
|
||
A `MaskedInput` template length defines the maximum length of the input value. Each character of the mask defines a regular expression used to restrict what the user can insert in the corresponding position, and whether the presence of the character in the user input is required for the `MaskedInput` value to be considered valid, according to the following table: | ||
|
||
| Mask character | Regular expression | Required? | | ||
| -------------- | ------------------ | --------- | | ||
| `A` | `[A-Za-z]` | Yes | | ||
| `a` | `[A-Za-z]` | No | | ||
| `N` | `[A-Za-z0-9]` | Yes | | ||
| `n` | `[A-Za-z0-9]` | No | | ||
| `X` | `[^ ]` | Yes | | ||
| `x` | `[^ ]` | No | | ||
| `9` | `[0-9]` | Yes | | ||
| `0` | `[0-9]` | No | | ||
| `D` | `[1-9]` | Yes | | ||
| `d` | `[1-9]` | No | | ||
| `#` | `[0-9+\-]` | No | | ||
| `H` | `[A-Fa-f0-9]` | Yes | | ||
| `h` | `[A-Fa-f0-9]` | No | | ||
| `B` | `[0-1]` | Yes | | ||
| `b` | `[0-1]` | No | | ||
|
||
There are some special characters that can be used to control automatic case conversion during user input: `>` converts all subsequent user input to uppercase; `<` to lowercase; `!` disables automatic case conversion. Any other character that appears in the template mask is assumed to be a separator, which is a character that is automatically inserted when user reaches its position. All mask characters can be escaped by placing `\` in front of them, allowing any character to be used as separator. | ||
The mask can be terminated by `;c`, where `c` is any character you want to be used as placeholder character. The `placeholder` parameter inherited by `Input` can be used to override this allowing finer grain tuning of the placeholder string. | ||
|
||
## Messages | ||
|
||
- [MaskedInput.Changed][textual.widgets.MaskedInput.Changed] | ||
- [MaskedInput.Submitted][textual.widgets.MaskedInput.Submitted] | ||
|
||
## Bindings | ||
|
||
The masked input widget defines the following bindings: | ||
|
||
::: textual.widgets.MaskedInput.BINDINGS | ||
options: | ||
show_root_heading: false | ||
show_root_toc_entry: false | ||
|
||
## Component Classes | ||
|
||
The masked input widget provides the following component classes: | ||
|
||
::: textual.widgets.MaskedInput.COMPONENT_CLASSES | ||
options: | ||
show_root_heading: false | ||
show_root_toc_entry: false | ||
|
||
--- | ||
|
||
|
||
::: textual.widgets.MaskedInput | ||
options: | ||
heading_level: 2 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
""" | ||
A simple example of chatting to an LLM with Textual. | ||
Lots of room for improvement here. | ||
See https://textual.textualize.io/blog/2024/09/15/anatomy-of-a-textual-user-interface/ | ||
""" | ||
|
||
# /// script | ||
# requires-python = ">=3.12" | ||
# dependencies = [ | ||
# "llm", | ||
# "textual", | ||
# ] | ||
# /// | ||
from textual import on, work | ||
from textual.app import App, ComposeResult | ||
from textual.containers import VerticalScroll | ||
from textual.widgets import Footer, Header, Input, Markdown | ||
|
||
try: | ||
import llm | ||
except ImportError: | ||
raise ImportError("install the 'llm' package or run with 'uv run mother.py'") | ||
|
||
# The system prompt | ||
SYSTEM = """Formulate all responses as if you where the sentient AI named Mother from the Alien movies.""" | ||
|
||
|
||
class Prompt(Markdown): | ||
"""Markdown for the user prompt.""" | ||
|
||
|
||
class Response(Markdown): | ||
"""Markdown for the reply from the LLM.""" | ||
|
||
BORDER_TITLE = "Mother" | ||
|
||
|
||
class MotherApp(App): | ||
"""Simple app to demonstrate chatting to an LLM.""" | ||
|
||
AUTO_FOCUS = "Input" | ||
|
||
CSS = """ | ||
Prompt { | ||
background: $primary 10%; | ||
color: $text; | ||
margin: 1; | ||
margin-right: 8; | ||
padding: 1 2 0 2; | ||
} | ||
Response { | ||
border: wide $success; | ||
background: $success 10%; | ||
color: $text; | ||
margin: 1; | ||
margin-left: 8; | ||
padding: 1 2 0 2; | ||
} | ||
""" | ||
|
||
def compose(self) -> ComposeResult: | ||
yield Header() | ||
with VerticalScroll(id="chat-view"): | ||
yield Response("INTERFACE 2037 READY FOR INQUIRY") | ||
yield Input(placeholder="How can I help you?") | ||
yield Footer() | ||
|
||
def on_mount(self) -> None: | ||
"""You might want to change the model if you don't have access to it.""" | ||
self.model = llm.get_model("gpt-4o") | ||
|
||
@on(Input.Submitted) | ||
async def on_input(self, event: Input.Submitted) -> None: | ||
"""When the user hits return.""" | ||
chat_view = self.query_one("#chat-view") | ||
event.input.clear() | ||
await chat_view.mount(Prompt(event.value)) | ||
await chat_view.mount(response := Response()) | ||
response.anchor() | ||
self.send_prompt(event.value, response) | ||
|
||
@work(thread=True) | ||
def send_prompt(self, prompt: str, response: Response) -> None: | ||
"""Get the response in a thread.""" | ||
response_content = "" | ||
llm_response = self.model.prompt(prompt, system=SYSTEM) | ||
for chunk in llm_response: | ||
response_content += chunk | ||
self.call_from_thread(response.update, response_content) | ||
|
||
|
||
if __name__ == "__main__": | ||
print( | ||
"https://textual.textualize.io/blog/2024/09/15/anatomy-of-a-textual-user-interface/" | ||
) | ||
print( | ||
"You will need an OpenAI API key for this example.\nSee https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key" | ||
) | ||
app = MotherApp() | ||
app.run() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.