mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Merge branch 'main' into list-view
This commit is contained in:
1
docs/api/placeholder.md
Normal file
1
docs/api/placeholder.md
Normal file
@@ -0,0 +1 @@
|
||||
::: textual.widgets.Placeholder
|
||||
1
docs/api/text_log.md
Normal file
1
docs/api/text_log.md
Normal file
@@ -0,0 +1 @@
|
||||
::: textual.widgets.TextLog
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
BIN
docs/blog/images/2022-12-08-davep-devlog/textual-qrcode.png
Normal file
BIN
docs/blog/images/2022-12-08-davep-devlog/textual-qrcode.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 287 KiB |
168
docs/blog/posts/be-the-keymaster.md
Normal file
168
docs/blog/posts/be-the-keymaster.md
Normal file
@@ -0,0 +1,168 @@
|
||||
---
|
||||
draft: false
|
||||
date: 2022-12-08
|
||||
categories:
|
||||
- DevLog
|
||||
authors:
|
||||
- davep
|
||||
---
|
||||
|
||||
# Be the Keymaster!
|
||||
|
||||
## That didn't go to plan
|
||||
|
||||
So... yeah... the blog. When I wrote [my previous (and first)
|
||||
post](https://textual.textualize.io/blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/)
|
||||
I had wanted to try and do a post towards the end of each week, highlighting
|
||||
what I'd done on the "dogfooding" front. Life kinda had other plans. Not in
|
||||
a terrible way, but it turns out that getting both flu and Covid jabs (AKA
|
||||
"jags" as they tend to say in my adopted home) on the same day doesn't
|
||||
really agree with me too well.
|
||||
|
||||
I *have* been working, but there's been some odd moments in the past week
|
||||
and a bit and, last week, once I got to the end, I was glad for it to end.
|
||||
So no blog post happened.
|
||||
|
||||
Anyway...
|
||||
|
||||
<!-- more -->
|
||||
|
||||
## What have I been up to?
|
||||
|
||||
While mostly sat feeling sorry for myself on my sofa, I have been coding.
|
||||
Rather than list all the different things here in detail, I'll quickly
|
||||
mention them with links to where to find them and play with them if you
|
||||
want:
|
||||
|
||||
### FivePyFive
|
||||
|
||||
While my Textual 5x5 puzzle is [one of the examples in the Textual
|
||||
repo](https://github.com/Textualize/textual/tree/main/examples), I wanted to
|
||||
make it more widely available so people can download it with `pip` or
|
||||
[`pipx`](https://pypa.github.io/pipx/). See [over on
|
||||
PyPi](https://pypi.org/project/fivepyfive/) and see if you can solve it. ;-)
|
||||
|
||||
<div class="video-wrapper">
|
||||
<iframe
|
||||
width="560" height="315"
|
||||
src="https://www.youtube.com/embed/Rf34Z5r7Q60"
|
||||
title="PISpy" frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen>
|
||||
</iframe>
|
||||
</div>
|
||||
|
||||
### textual-qrcode
|
||||
|
||||
I wanted to put together a very small example of how someone may put
|
||||
together a third party widget library, and in doing so selected what I
|
||||
thought was going to be a mostly-useless example: [a wrapper around a
|
||||
text-based QR code generator
|
||||
website](https://pypi.org/project/textual-qrcode/). Weirdly I've had a
|
||||
couple of people express a need for QR codes in the terminal since
|
||||
publishing that!
|
||||
|
||||

|
||||
|
||||
### PISpy
|
||||
|
||||
[PISpy](https://pypi.org/project/pispy-client/) is a very simple
|
||||
terminal-based client for the [PyPi
|
||||
API](https://warehouse.pypa.io/api-reference/). Mostly it provides a
|
||||
hypertext interface to Python package details, letting you look up a package
|
||||
and then follow its dependency links. It's *very* simple at the moment, but
|
||||
I think more fun things can be done with this.
|
||||
|
||||
<div class="video-wrapper">
|
||||
<iframe
|
||||
width="560" height="315"
|
||||
src="https://www.youtube.com/embed/yMGD6bXqIEo"
|
||||
title="PISpy" frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen>
|
||||
</iframe>
|
||||
</div>
|
||||
|
||||
### OIDIA
|
||||
|
||||
I'm a big fan of the use of streak-tracking in one form or another.
|
||||
Personally I use a [streak-tracking app](https://streaksapp.com/) for
|
||||
keeping tabs of all sorts of good (and bad) habits, and as a heavy user of
|
||||
all things Apple I make a lot of use of [the Fitness
|
||||
rings](https://www.apple.com/uk/watch/close-your-rings/), etc. So I got to
|
||||
thinking it might be fun to do a really simple, no shaming, no counting,
|
||||
just recording, steak app for the Terminal.
|
||||
[OIDIA](https://pypi.org/project/oidia/) is the result.
|
||||
|
||||
<div class="video-wrapper">
|
||||
<iframe
|
||||
width="560" height="315"
|
||||
src="https://www.youtube.com/embed/3Kz8eUzO9-8"
|
||||
title="YouTube video player"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen>
|
||||
</iframe>
|
||||
</div>
|
||||
|
||||
As of the time of writing I only finished the first version of this
|
||||
yesterday evening, so there are plenty of rough edges; but having got it to
|
||||
a point where it performed the basic tasks I wanted from it, that seemed
|
||||
like a good time to publish.
|
||||
|
||||
Expect to see this getting more updates and polish.
|
||||
|
||||
## Wait, what about this Keymaster thing?
|
||||
|
||||
Ahh, yes, about that... So one of the handy things I'm finding about Textual
|
||||
is its [key binding
|
||||
system](https://textual.textualize.io/guide/input/#bindings). The more
|
||||
I build Textual apps, the more I appreciate the bindings, how they can be
|
||||
associated with specific widgets, the use of actions (which can be used from
|
||||
other places too), etc.
|
||||
|
||||
But... (there's always a "but" right -- I mean, there'd be no blog post to
|
||||
be had here otherwise).
|
||||
|
||||
The terminal doesn't have access to all the key combinations you may want to
|
||||
use, and also, because some keys can't necessarily be "typed", at least not
|
||||
easily (think about it: there's no <kbd>F1</kbd> character, you have to type
|
||||
`F1`), many keys and key combinations need to be bound with specific names.
|
||||
|
||||
So there's two problems here: how do I discover what keys even turn up in my
|
||||
application, and when they do, what should I call them when I pass them to
|
||||
[`Binding`](https://textual.textualize.io/api/binding/#textual.binding.Binding)?
|
||||
|
||||
That felt like a *"well Dave just build an app for it!"* problem. So I did:
|
||||
|
||||
<div class="video-wrapper">
|
||||
<iframe
|
||||
width="560" height="315"
|
||||
src="https://www.youtube.com/embed/-MV8LFfEOZo"
|
||||
title="YouTube video player"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen>
|
||||
</iframe>
|
||||
</div>
|
||||
|
||||
If you're building apps with Textual and you want to discover what keys turn
|
||||
up from your terminal and are available to your application, you can:
|
||||
|
||||
```sh
|
||||
$ pipx install textual-keys
|
||||
```
|
||||
|
||||
and then just run `textual-keys` and start mashing the keyboard to find out.
|
||||
|
||||
There's a good chance that this app, or at least a version of it, will make
|
||||
it into Textual itself (very likely as one of the
|
||||
[devtools](https://textual.textualize.io/guide/devtools/)). But for now it's
|
||||
just an easy install away.
|
||||
|
||||
I think there's a call to be made here too: have you built anything to help
|
||||
speed up how you work with Textual, or just make the development experience
|
||||
"just so"? If so, do let us know, and come yell about it on the
|
||||
[`#show-and-tell`
|
||||
channel](https://discord.com/channels/1026214085173461072/1033752599112994867)
|
||||
in [our Discord server](https://discord.gg/Enf6Z3qhVr).
|
||||
130
docs/blog/posts/responsive-app-background-task.md
Normal file
130
docs/blog/posts/responsive-app-background-task.md
Normal file
@@ -0,0 +1,130 @@
|
||||
---
|
||||
draft: false
|
||||
date: 2022-12-07
|
||||
categories:
|
||||
- DevLog
|
||||
authors:
|
||||
- rodrigo
|
||||
---
|
||||
|
||||
# Letting your cook multitask while bringing water to a boil
|
||||
|
||||
Whenever you are cooking a time-consuming meal, you want to multitask as much as possible.
|
||||
For example, you **do not** want to stand still while you wait for a pot of water to start boiling.
|
||||
Similarly, you want your applications to remain responsive (i.e., you want the cook to “multitask”) while they do some time-consuming operations in the background (e.g., while the water heats up).
|
||||
|
||||
The animation below shows an example of an application that remains responsive (colours on the left still change on click) even while doing a bunch of time-consuming operations (shown on the right).
|
||||
|
||||

|
||||
|
||||
In this blog post, I will teach you how to multitask like a good cook.
|
||||
|
||||
<!-- more -->
|
||||
|
||||
|
||||
## Wasting time staring at pots
|
||||
|
||||
There is no point in me presenting a solution to a problem if you don't understand the problem I am trying to solve.
|
||||
Suppose we have an application that needs to display a huge amount of data that needs to be read and parsed from a file.
|
||||
The first time I had to do something like this, I ended up writing an application that “blocked”.
|
||||
This means that _while_ the application was reading and parsing the data, nothing else worked.
|
||||
|
||||
To exemplify this type of scenario, I created a simple application that spends five seconds preparing some data.
|
||||
After the data is ready, we display a `Label` on the right that says that the data has been loaded.
|
||||
On the left, the app has a big rectangle (a custom widget called `ColourChanger`) that you can click and that changes background colours randomly.
|
||||
|
||||
When you start the application, you can click the rectangle on the left to change the background colour of the `ColourChanger`, as the animation below shows:
|
||||
|
||||

|
||||
|
||||
However, as soon as you press `l` to trigger the data loading process, clicking the `ColourChanger` widget doesn't do anything.
|
||||
The app doesn't respond because it is busy working on the data.
|
||||
This is the code of the app so you can try it yourself:
|
||||
|
||||
```py hl_lines="11-13 21 35 36"
|
||||
--8<-- "docs/blog/snippets/2022-12-07-responsive-app-background-task/blocking01.py"
|
||||
```
|
||||
|
||||
1. The widget `ColourChanger` changes colours, randomly, when clicked.
|
||||
2. We create a binding to the key `l` that runs an action that we know will take some time (for example, reading and parsing a huge file).
|
||||
3. The method `action_load` is responsible for starting our time-consuming task and then reporting back.
|
||||
4. To simplify things a bit, our “time-consuming task” is just standing still for 5 seconds.
|
||||
|
||||
I think it is easy to understand why the widget `ColourChanger` stops working when we hit the `time.sleep` call if we consider [the cooking analogy](https://mathspp.com/blog/til/cooking-with-asyncio) I have written about before in my blog.
|
||||
In short, Python behaves like a lone cook in a kitchen:
|
||||
|
||||
- the cook can be clever and multitask. For example, while water is heating up and being brought to a boil, the cook can go ahead and chop some vegetables.
|
||||
- however, there is _only one_ cook in the kitchen, so if the cook is chopping up vegetables, they can't be seasoning a salad.
|
||||
|
||||
Things like “chopping up vegetables” and “seasoning a salad” are _blocking_, i.e., they need the cook's time and attention.
|
||||
In the app that I showed above, the call to `time.sleep` is blocking, so the cook can't go and do anything else until the time interval elapses.
|
||||
|
||||
## How can a cook multitask?
|
||||
|
||||
It makes a lot of sense to think that a cook would multitask in their kitchen, but Python isn't like a smart cook.
|
||||
Python is like a very dumb cook who only ever does one thing at a time and waits until each thing is completely done before doing the next thing.
|
||||
So, by default, Python would act like a cook who fills up a pan with water, starts heating the water, and then stands there staring at the water until it starts boiling instead of doing something else.
|
||||
It is by using the module `asyncio` from the standard library that our cook learns to do other tasks while _awaiting_ the completion of the things they already started doing.
|
||||
|
||||
[Textual](https://github.com/textualize/textual) is an async framework, which means it knows how to interoperate with the module `asyncio` and this will be the solution to our problem.
|
||||
By using `asyncio` with the tasks we want to run in the background, we will let the application remain responsive while we load and parse the data we need, or while we crunch the numbers we need to crunch, or while we connect to some slow API over the Internet, or whatever it is you want to do.
|
||||
|
||||
The module `asyncio` uses the keyword `async` to know which functions can be run asynchronously.
|
||||
In other words, you use the keyword `async` to identify functions that contain tasks that would otherwise force the cook to waste time.
|
||||
(Functions with the keyword `async` are called _coroutines_.)
|
||||
|
||||
The module `asyncio` also introduces a function `asyncio.create_task` that you can use to run coroutines concurrently.
|
||||
So, if we create a coroutine that is in charge of doing the time-consuming operation and then run it with `asyncio.create_task`, we are well on our way to fix our issues.
|
||||
|
||||
However, the keyword `async` and `asyncio.create_task` alone aren't enough.
|
||||
Consider this modification of the previous app, where the method `action_load` now uses `asyncio.create_task` to run a coroutine who does the sleeping:
|
||||
|
||||
```py hl_lines="36-37 39"
|
||||
--8<-- "docs/blog/snippets/2022-12-07-responsive-app-background-task/blocking02.py"
|
||||
```
|
||||
|
||||
1. The action method `action_load` now defers the heavy lifting to another method we created.
|
||||
2. The time-consuming operation can be run concurrently with `asyncio.create_task` because it is a coroutine.
|
||||
3. The method `_do_long_operation` has the keyword `async`, so it is a coroutine.
|
||||
|
||||
This modified app also works but it suffers from the same issue as the one before!
|
||||
The keyword `async` tells Python that there will be things inside that function that can be _awaited_ by the cook.
|
||||
That is, the function will do some time-consuming operation that doesn't require the cook's attention.
|
||||
However, we need to tell Python which time-consuming operation doesn't require the cook's attention, i.e., which time-consuming operation can be _awaited_, with the keyword `await`.
|
||||
|
||||
Whenever we want to use the keyword `await`, we need to do it with objects that are compatible with it.
|
||||
For many things, that means using specialised libraries:
|
||||
|
||||
- instead of `time.sleep`, one can use `await asyncio.sleep`;
|
||||
- instead of the module `requests` to make Internet requests, use `aiohttp`; or
|
||||
- instead of using the built-in tools to read files, use `aiofiles`.
|
||||
|
||||
## Achieving good multitasking
|
||||
|
||||
To fix the last example application, all we need to do is replace the call to `time.sleep` with a call to `asyncio.sleep` and then use the keyword `await` to signal Python that we can be doing something else while we sleep.
|
||||
The animation below shows that we can still change colours while the application is completing the time-consuming operation.
|
||||
|
||||
=== "Code"
|
||||
|
||||
```py hl_lines="40 41 42"
|
||||
--8<-- "docs\blog\snippets\2022-12-07-responsive-app-background-task\nonblocking01.py"
|
||||
```
|
||||
|
||||
1. We create a label that tells the user that we are starting our time-consuming operation.
|
||||
2. We `await` the time-consuming operation so that the application remains responsive.
|
||||
3. We create a label that tells the user that the time-consuming operation has been concluded.
|
||||
|
||||
=== "Animation"
|
||||
|
||||

|
||||
|
||||
Because our time-consuming operation runs concurrently, everything else in the application still works while we _await_ for the time-consuming operation to finish.
|
||||
In particular, we can keep changing colours (like the animation above showed) but we can also keep activating the binding with the key `l` to start multiple instances of the same time-consuming operation!
|
||||
The animation below shows just this:
|
||||
|
||||

|
||||
|
||||
!!! warning
|
||||
|
||||
The animation GIFs in this blog post show low-quality colours in an attempt to reduce the size of the media files you have to download to be able to read this blog post.
|
||||
If you run Textual locally you will see beautiful colours ✨
|
||||
@@ -0,0 +1,40 @@
|
||||
from random import randint
|
||||
import time
|
||||
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.color import Color
|
||||
from textual.containers import Grid, Vertical
|
||||
from textual.widget import Widget
|
||||
from textual.widgets import Footer, Label
|
||||
|
||||
|
||||
class ColourChanger(Widget): # (1)!
|
||||
def on_click(self) -> None:
|
||||
self.styles.background = Color(
|
||||
randint(1, 255),
|
||||
randint(1, 255),
|
||||
randint(1, 255),
|
||||
)
|
||||
|
||||
|
||||
class MyApp(App[None]):
|
||||
BINDINGS = [("l", "load", "Load data")] # (2)!
|
||||
CSS = """
|
||||
Grid {
|
||||
grid-size: 2;
|
||||
}
|
||||
"""
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Grid(
|
||||
ColourChanger(),
|
||||
Vertical(id="log"),
|
||||
)
|
||||
yield Footer()
|
||||
|
||||
def action_load(self) -> None: # (3)!
|
||||
time.sleep(5) # (4)!
|
||||
self.query_one("#log").mount(Label("Data loaded ✅"))
|
||||
|
||||
|
||||
MyApp().run()
|
||||
@@ -0,0 +1,44 @@
|
||||
import asyncio
|
||||
from random import randint
|
||||
import time
|
||||
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.color import Color
|
||||
from textual.containers import Grid, Vertical
|
||||
from textual.widget import Widget
|
||||
from textual.widgets import Footer, Label
|
||||
|
||||
|
||||
class ColourChanger(Widget):
|
||||
def on_click(self) -> None:
|
||||
self.styles.background = Color(
|
||||
randint(1, 255),
|
||||
randint(1, 255),
|
||||
randint(1, 255),
|
||||
)
|
||||
|
||||
|
||||
class MyApp(App[None]):
|
||||
BINDINGS = [("l", "load", "Load data")]
|
||||
CSS = """
|
||||
Grid {
|
||||
grid-size: 2;
|
||||
}
|
||||
"""
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Grid(
|
||||
ColourChanger(),
|
||||
Vertical(id="log"),
|
||||
)
|
||||
yield Footer()
|
||||
|
||||
def action_load(self) -> None: # (1)!
|
||||
asyncio.create_task(self._do_long_operation()) # (2)!
|
||||
|
||||
async def _do_long_operation(self) -> None: # (3)!
|
||||
time.sleep(5)
|
||||
self.query_one("#log").mount(Label("Data loaded ✅"))
|
||||
|
||||
|
||||
MyApp().run()
|
||||
@@ -0,0 +1,45 @@
|
||||
import asyncio
|
||||
from random import randint
|
||||
import time
|
||||
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.color import Color
|
||||
from textual.containers import Grid, Vertical
|
||||
from textual.widget import Widget
|
||||
from textual.widgets import Footer, Label
|
||||
|
||||
|
||||
class ColourChanger(Widget):
|
||||
def on_click(self) -> None:
|
||||
self.styles.background = Color(
|
||||
randint(1, 255),
|
||||
randint(1, 255),
|
||||
randint(1, 255),
|
||||
)
|
||||
|
||||
|
||||
class MyApp(App[None]):
|
||||
BINDINGS = [("l", "load", "Load data")]
|
||||
CSS = """
|
||||
Grid {
|
||||
grid-size: 2;
|
||||
}
|
||||
"""
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Grid(
|
||||
ColourChanger(),
|
||||
Vertical(id="log"),
|
||||
)
|
||||
yield Footer()
|
||||
|
||||
def action_load(self) -> None:
|
||||
asyncio.create_task(self._do_long_operation())
|
||||
|
||||
async def _do_long_operation(self) -> None:
|
||||
self.query_one("#log").mount(Label("Starting ⏳")) # (1)!
|
||||
await asyncio.sleep(5) # (2)!
|
||||
self.query_one("#log").mount(Label("Data loaded ✅")) # (3)!
|
||||
|
||||
|
||||
MyApp().run()
|
||||
@@ -1,6 +1,7 @@
|
||||
|
||||
/* The top level dialog (a Container) */
|
||||
#dialog {
|
||||
height: 100%;
|
||||
margin: 4 8;
|
||||
background: $panel;
|
||||
color: $text;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
grid-rows: 1fr;
|
||||
}
|
||||
|
||||
#left-pane > Static {
|
||||
#left-pane > Static {
|
||||
background: $boost;
|
||||
color: auto;
|
||||
margin-bottom: 1;
|
||||
@@ -13,12 +13,14 @@
|
||||
}
|
||||
|
||||
#left-pane {
|
||||
height: 100%;
|
||||
row-span: 2;
|
||||
background: $panel;
|
||||
border: dodgerblue;
|
||||
}
|
||||
|
||||
#top-right {
|
||||
height: 100%;
|
||||
background: $panel;
|
||||
border: mediumvioletred;
|
||||
}
|
||||
@@ -31,6 +33,7 @@
|
||||
}
|
||||
|
||||
#bottom-right {
|
||||
height: 100%;
|
||||
layout: grid;
|
||||
grid-size: 3;
|
||||
grid-columns: 1fr;
|
||||
|
||||
51
docs/examples/widgets/placeholder.css
Normal file
51
docs/examples/widgets/placeholder.css
Normal file
@@ -0,0 +1,51 @@
|
||||
Placeholder {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#top {
|
||||
height: 50%;
|
||||
width: 100%;
|
||||
layout: grid;
|
||||
grid-size: 2 2;
|
||||
}
|
||||
|
||||
#left {
|
||||
row-span: 2;
|
||||
}
|
||||
|
||||
#bot {
|
||||
height: 50%;
|
||||
width: 100%;
|
||||
layout: grid;
|
||||
grid-size: 8 8;
|
||||
}
|
||||
|
||||
#c1 {
|
||||
row-span: 4;
|
||||
column-span: 8;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#col1, #col2, #col3 {
|
||||
width: 1fr;
|
||||
}
|
||||
|
||||
#p1 {
|
||||
row-span: 4;
|
||||
column-span: 4;
|
||||
}
|
||||
|
||||
#p2 {
|
||||
row-span: 2;
|
||||
column-span: 4;
|
||||
}
|
||||
|
||||
#p3 {
|
||||
row-span: 2;
|
||||
column-span: 2;
|
||||
}
|
||||
|
||||
#p4 {
|
||||
row-span: 1;
|
||||
column-span: 2;
|
||||
}
|
||||
39
docs/examples/widgets/placeholder.py
Normal file
39
docs/examples/widgets/placeholder.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.containers import Container, Horizontal, Vertical
|
||||
from textual.widgets import Placeholder
|
||||
|
||||
|
||||
class PlaceholderApp(App):
|
||||
|
||||
CSS_PATH = "placeholder.css"
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Vertical(
|
||||
Container(
|
||||
Placeholder("This is a custom label for p1.", id="p1"),
|
||||
Placeholder("Placeholder p2 here!", id="p2"),
|
||||
Placeholder(id="p3"),
|
||||
Placeholder(id="p4"),
|
||||
Placeholder(id="p5"),
|
||||
Placeholder(),
|
||||
Horizontal(
|
||||
Placeholder(variant="size", id="col1"),
|
||||
Placeholder(variant="text", id="col2"),
|
||||
Placeholder(variant="size", id="col3"),
|
||||
id="c1",
|
||||
),
|
||||
id="bot"
|
||||
),
|
||||
Container(
|
||||
Placeholder(variant="text", id="left"),
|
||||
Placeholder(variant="size", id="topright"),
|
||||
Placeholder(variant="text", id="botright"),
|
||||
id="top",
|
||||
),
|
||||
id="content",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = PlaceholderApp()
|
||||
app.run()
|
||||
66
docs/examples/widgets/text_log.py
Normal file
66
docs/examples/widgets/text_log.py
Normal file
@@ -0,0 +1,66 @@
|
||||
import csv
|
||||
import io
|
||||
|
||||
from rich.table import Table
|
||||
from rich.syntax import Syntax
|
||||
|
||||
from textual.app import App, ComposeResult
|
||||
from textual import events
|
||||
from textual.widgets import TextLog
|
||||
|
||||
|
||||
CSV = """lane,swimmer,country,time
|
||||
4,Joseph Schooling,Singapore,50.39
|
||||
2,Michael Phelps,United States,51.14
|
||||
5,Chad le Clos,South Africa,51.14
|
||||
6,László Cseh,Hungary,51.14
|
||||
3,Li Zhuhao,China,51.26
|
||||
8,Mehdy Metella,France,51.58
|
||||
7,Tom Shields,United States,51.73
|
||||
1,Aleksandr Sadovnikov,Russia,51.84"""
|
||||
|
||||
|
||||
CODE = '''\
|
||||
def loop_first_last(values: Iterable[T]) -> Iterable[tuple[bool, bool, T]]:
|
||||
"""Iterate and generate a tuple with a flag for first and last value."""
|
||||
iter_values = iter(values)
|
||||
try:
|
||||
previous_value = next(iter_values)
|
||||
except StopIteration:
|
||||
return
|
||||
first = True
|
||||
for value in iter_values:
|
||||
yield first, False, previous_value
|
||||
first = False
|
||||
previous_value = value
|
||||
yield first, True, previous_value\
|
||||
'''
|
||||
|
||||
|
||||
class TextLogApp(App):
|
||||
def compose(self) -> ComposeResult:
|
||||
yield TextLog(highlight=True, markup=True)
|
||||
|
||||
def on_ready(self) -> None:
|
||||
"""Called when the DOM is ready."""
|
||||
text_log = self.query_one(TextLog)
|
||||
|
||||
text_log.write(Syntax(CODE, "python", indent_guides=True))
|
||||
|
||||
rows = iter(csv.reader(io.StringIO(CSV)))
|
||||
table = Table(*next(rows))
|
||||
for row in rows:
|
||||
table.add_row(*row)
|
||||
|
||||
text_log.write(table)
|
||||
text_log.write("[bold magenta]Write text or any Rich renderable!")
|
||||
|
||||
def on_key(self, event: events.Key) -> None:
|
||||
"""Write Key events to log."""
|
||||
text_log = self.query_one(TextLog)
|
||||
text_log.write(event)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = TextLogApp()
|
||||
app.run()
|
||||
47
docs/widgets/placeholder.md
Normal file
47
docs/widgets/placeholder.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# Placeholder
|
||||
|
||||
|
||||
A widget that is meant to have no complex functionality.
|
||||
Use the placeholder widget when studying the layout of your app before having to develop your custom widgets.
|
||||
|
||||
The placeholder widget has variants that display different bits of useful information.
|
||||
Clicking a placeholder will cycle through its variants.
|
||||
|
||||
- [ ] Focusable
|
||||
- [ ] Container
|
||||
|
||||
## Example
|
||||
|
||||
The example below shows each placeholder variant.
|
||||
|
||||
=== "Output"
|
||||
|
||||
```{.textual path="docs/examples/widgets/placeholder.py"}
|
||||
```
|
||||
|
||||
=== "placeholder.py"
|
||||
|
||||
```python
|
||||
--8<-- "docs/examples/widgets/placeholder.py"
|
||||
```
|
||||
|
||||
=== "placeholder.css"
|
||||
|
||||
```css
|
||||
--8<-- "docs/examples/widgets/placeholder.css"
|
||||
```
|
||||
|
||||
## Reactive Attributes
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| ---------- | ------ | ----------- | -------------------------------------------------- |
|
||||
| `variant` | `str` | `"default"` | Styling variant. One of `default`, `size`, `text`. |
|
||||
|
||||
|
||||
## Messages
|
||||
|
||||
This widget sends no messages.
|
||||
|
||||
## See Also
|
||||
|
||||
* [Placeholder](../api/placeholder.md) code reference
|
||||
44
docs/widgets/text_log.md
Normal file
44
docs/widgets/text_log.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# TextLog
|
||||
|
||||
A TextLog is a widget which displays scrollable content that may be appended to in realtime.
|
||||
|
||||
Call [TextLog.write][textual.widgets.TextLog.write] with a string or [Rich Renderable](https://rich.readthedocs.io/en/latest/protocol.html) to write content to the end of the TextLog. Call [TextLog.clear][textual.widgets.TextLog.clear] to clear the content.
|
||||
|
||||
- [X] Focusable
|
||||
- [ ] Container
|
||||
|
||||
## Example
|
||||
|
||||
The example below shows each placeholder variant.
|
||||
|
||||
=== "Output"
|
||||
|
||||
```{.textual path="docs/examples/widgets/text_log.py" press="_,H,i"}
|
||||
```
|
||||
|
||||
=== "text_log.py"
|
||||
|
||||
```python
|
||||
--8<-- "docs/examples/widgets/text_log.py"
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Reactive Attributes
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| ----------- | ------ | ------- | ------------------------------------------------------------ |
|
||||
| `highlight` | `bool` | `False` | Automatically highlight content. |
|
||||
| `markup` | `bool` | `False` | Apply Rich console markup. |
|
||||
| `max_lines` | `int` | `None` | Maximum number of lines in the log or `None` for no maximum. |
|
||||
| `min_width` | `int` | 78 | Minimum width of renderables. |
|
||||
| `wrap` | `bool` | `False` | Enable word wrapping. |
|
||||
|
||||
## Messages
|
||||
|
||||
This widget sends no messages.
|
||||
|
||||
|
||||
## See Also
|
||||
|
||||
* [TextLog](../api/textlog.md) code reference
|
||||
Reference in New Issue
Block a user