Skip to content

Commit cebeafb

Browse files
authored
Merge branch 'main' into list-view
2 parents 29f4887 + d91ea42 commit cebeafb

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+2742
-454
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ repos:
99
- id: check-yaml
1010
args: ['--unsafe']
1111
- repo: https://github.com/psf/black
12-
rev: 22.3.0
12+
rev: 22.10.0
1313
hooks:
1414
- id: black
1515
exclude: ^tests/

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,21 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1212

1313
- Added "inherited bindings" -- BINDINGS classvar will be merged with base classes, unless inherit_bindings is set to False
1414
- Added `Tree` widget which replaces `TreeControl`.
15+
- Added widget `Placeholder` https://github.com/Textualize/textual/issues/1200.
1516

1617
### Changed
1718

1819
- Rebuilt `DirectoryTree` with new `Tree` control.
20+
- Empty containers with a dimension set to `"auto"` will now collapse instead of filling up the available space.
21+
- Container widgets now have default height of `1fr`.
22+
- The default `width` of a `Label` is now `auto`.
1923

2024
### Fixed
2125

2226
- Type selectors can now contain numbers https://github.com/Textualize/textual/issues/1253
27+
- Fixed visibility not affecting children https://github.com/Textualize/textual/issues/1313
28+
- Fixed issue with auto width/height and relative children https://github.com/Textualize/textual/issues/1319
29+
- Fixed issue with offset applied to containers https://github.com/Textualize/textual/issues/1256
2330

2431
## [0.5.0] - 2022-11-20
2532

docs/api/placeholder.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
::: textual.widgets.Placeholder

docs/api/text_log.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
::: textual.widgets.TextLog
47.4 KB
Loading
47.1 KB
Loading
1.08 MB
Loading
287 KB
Loading
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
---
2+
draft: false
3+
date: 2022-12-08
4+
categories:
5+
- DevLog
6+
authors:
7+
- davep
8+
---
9+
10+
# Be the Keymaster!
11+
12+
## That didn't go to plan
13+
14+
So... yeah... the blog. When I wrote [my previous (and first)
15+
post](https://textual.textualize.io/blog/2022/11/26/on-dog-food-the-original-metaverse-and-not-being-bored/)
16+
I had wanted to try and do a post towards the end of each week, highlighting
17+
what I'd done on the "dogfooding" front. Life kinda had other plans. Not in
18+
a terrible way, but it turns out that getting both flu and Covid jabs (AKA
19+
"jags" as they tend to say in my adopted home) on the same day doesn't
20+
really agree with me too well.
21+
22+
I *have* been working, but there's been some odd moments in the past week
23+
and a bit and, last week, once I got to the end, I was glad for it to end.
24+
So no blog post happened.
25+
26+
Anyway...
27+
28+
<!-- more -->
29+
30+
## What have I been up to?
31+
32+
While mostly sat feeling sorry for myself on my sofa, I have been coding.
33+
Rather than list all the different things here in detail, I'll quickly
34+
mention them with links to where to find them and play with them if you
35+
want:
36+
37+
### FivePyFive
38+
39+
While my Textual 5x5 puzzle is [one of the examples in the Textual
40+
repo](https://github.com/Textualize/textual/tree/main/examples), I wanted to
41+
make it more widely available so people can download it with `pip` or
42+
[`pipx`](https://pypa.github.io/pipx/). See [over on
43+
PyPi](https://pypi.org/project/fivepyfive/) and see if you can solve it. ;-)
44+
45+
<div class="video-wrapper">
46+
<iframe
47+
width="560" height="315"
48+
src="https://www.youtube.com/embed/Rf34Z5r7Q60"
49+
title="PISpy" frameborder="0"
50+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
51+
allowfullscreen>
52+
</iframe>
53+
</div>
54+
55+
### textual-qrcode
56+
57+
I wanted to put together a very small example of how someone may put
58+
together a third party widget library, and in doing so selected what I
59+
thought was going to be a mostly-useless example: [a wrapper around a
60+
text-based QR code generator
61+
website](https://pypi.org/project/textual-qrcode/). Weirdly I've had a
62+
couple of people express a need for QR codes in the terminal since
63+
publishing that!
64+
65+
![A Textual QR Code](../images/2022-12-08-davep-devlog/textual-qrcode.png)
66+
67+
### PISpy
68+
69+
[PISpy](https://pypi.org/project/pispy-client/) is a very simple
70+
terminal-based client for the [PyPi
71+
API](https://warehouse.pypa.io/api-reference/). Mostly it provides a
72+
hypertext interface to Python package details, letting you look up a package
73+
and then follow its dependency links. It's *very* simple at the moment, but
74+
I think more fun things can be done with this.
75+
76+
<div class="video-wrapper">
77+
<iframe
78+
width="560" height="315"
79+
src="https://www.youtube.com/embed/yMGD6bXqIEo"
80+
title="PISpy" frameborder="0"
81+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
82+
allowfullscreen>
83+
</iframe>
84+
</div>
85+
86+
### OIDIA
87+
88+
I'm a big fan of the use of streak-tracking in one form or another.
89+
Personally I use a [streak-tracking app](https://streaksapp.com/) for
90+
keeping tabs of all sorts of good (and bad) habits, and as a heavy user of
91+
all things Apple I make a lot of use of [the Fitness
92+
rings](https://www.apple.com/uk/watch/close-your-rings/), etc. So I got to
93+
thinking it might be fun to do a really simple, no shaming, no counting,
94+
just recording, steak app for the Terminal.
95+
[OIDIA](https://pypi.org/project/oidia/) is the result.
96+
97+
<div class="video-wrapper">
98+
<iframe
99+
width="560" height="315"
100+
src="https://www.youtube.com/embed/3Kz8eUzO9-8"
101+
title="YouTube video player"
102+
frameborder="0"
103+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
104+
allowfullscreen>
105+
</iframe>
106+
</div>
107+
108+
As of the time of writing I only finished the first version of this
109+
yesterday evening, so there are plenty of rough edges; but having got it to
110+
a point where it performed the basic tasks I wanted from it, that seemed
111+
like a good time to publish.
112+
113+
Expect to see this getting more updates and polish.
114+
115+
## Wait, what about this Keymaster thing?
116+
117+
Ahh, yes, about that... So one of the handy things I'm finding about Textual
118+
is its [key binding
119+
system](https://textual.textualize.io/guide/input/#bindings). The more
120+
I build Textual apps, the more I appreciate the bindings, how they can be
121+
associated with specific widgets, the use of actions (which can be used from
122+
other places too), etc.
123+
124+
But... (there's always a "but" right -- I mean, there'd be no blog post to
125+
be had here otherwise).
126+
127+
The terminal doesn't have access to all the key combinations you may want to
128+
use, and also, because some keys can't necessarily be "typed", at least not
129+
easily (think about it: there's no <kbd>F1</kbd> character, you have to type
130+
`F1`), many keys and key combinations need to be bound with specific names.
131+
132+
So there's two problems here: how do I discover what keys even turn up in my
133+
application, and when they do, what should I call them when I pass them to
134+
[`Binding`](https://textual.textualize.io/api/binding/#textual.binding.Binding)?
135+
136+
That felt like a *"well Dave just build an app for it!"* problem. So I did:
137+
138+
<div class="video-wrapper">
139+
<iframe
140+
width="560" height="315"
141+
src="https://www.youtube.com/embed/-MV8LFfEOZo"
142+
title="YouTube video player"
143+
frameborder="0"
144+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
145+
allowfullscreen>
146+
</iframe>
147+
</div>
148+
149+
If you're building apps with Textual and you want to discover what keys turn
150+
up from your terminal and are available to your application, you can:
151+
152+
```sh
153+
$ pipx install textual-keys
154+
```
155+
156+
and then just run `textual-keys` and start mashing the keyboard to find out.
157+
158+
There's a good chance that this app, or at least a version of it, will make
159+
it into Textual itself (very likely as one of the
160+
[devtools](https://textual.textualize.io/guide/devtools/)). But for now it's
161+
just an easy install away.
162+
163+
I think there's a call to be made here too: have you built anything to help
164+
speed up how you work with Textual, or just make the development experience
165+
"just so"? If so, do let us know, and come yell about it on the
166+
[`#show-and-tell`
167+
channel](https://discord.com/channels/1026214085173461072/1033752599112994867)
168+
in [our Discord server](https://discord.gg/Enf6Z3qhVr).
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
---
2+
draft: false
3+
date: 2022-12-07
4+
categories:
5+
- DevLog
6+
authors:
7+
- rodrigo
8+
---
9+
10+
# Letting your cook multitask while bringing water to a boil
11+
12+
Whenever you are cooking a time-consuming meal, you want to multitask as much as possible.
13+
For example, you **do not** want to stand still while you wait for a pot of water to start boiling.
14+
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).
15+
16+
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).
17+
18+
![](../images/2022-12-07-responsive-app-background-task/responsive-demo.gif)
19+
20+
In this blog post, I will teach you how to multitask like a good cook.
21+
22+
<!-- more -->
23+
24+
25+
## Wasting time staring at pots
26+
27+
There is no point in me presenting a solution to a problem if you don't understand the problem I am trying to solve.
28+
Suppose we have an application that needs to display a huge amount of data that needs to be read and parsed from a file.
29+
The first time I had to do something like this, I ended up writing an application that “blocked”.
30+
This means that _while_ the application was reading and parsing the data, nothing else worked.
31+
32+
To exemplify this type of scenario, I created a simple application that spends five seconds preparing some data.
33+
After the data is ready, we display a `Label` on the right that says that the data has been loaded.
34+
On the left, the app has a big rectangle (a custom widget called `ColourChanger`) that you can click and that changes background colours randomly.
35+
36+
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:
37+
38+
![](../images/2022-12-07-responsive-app-background-task/blocking01-colour-changer.gif)
39+
40+
However, as soon as you press `l` to trigger the data loading process, clicking the `ColourChanger` widget doesn't do anything.
41+
The app doesn't respond because it is busy working on the data.
42+
This is the code of the app so you can try it yourself:
43+
44+
```py hl_lines="11-13 21 35 36"
45+
--8<-- "docs/blog/snippets/2022-12-07-responsive-app-background-task/blocking01.py"
46+
```
47+
48+
1. The widget `ColourChanger` changes colours, randomly, when clicked.
49+
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).
50+
3. The method `action_load` is responsible for starting our time-consuming task and then reporting back.
51+
4. To simplify things a bit, our “time-consuming task” is just standing still for 5 seconds.
52+
53+
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.
54+
In short, Python behaves like a lone cook in a kitchen:
55+
56+
- 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.
57+
- however, there is _only one_ cook in the kitchen, so if the cook is chopping up vegetables, they can't be seasoning a salad.
58+
59+
Things like “chopping up vegetables” and “seasoning a salad” are _blocking_, i.e., they need the cook's time and attention.
60+
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.
61+
62+
## How can a cook multitask?
63+
64+
It makes a lot of sense to think that a cook would multitask in their kitchen, but Python isn't like a smart cook.
65+
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.
66+
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.
67+
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.
68+
69+
[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.
70+
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.
71+
72+
The module `asyncio` uses the keyword `async` to know which functions can be run asynchronously.
73+
In other words, you use the keyword `async` to identify functions that contain tasks that would otherwise force the cook to waste time.
74+
(Functions with the keyword `async` are called _coroutines_.)
75+
76+
The module `asyncio` also introduces a function `asyncio.create_task` that you can use to run coroutines concurrently.
77+
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.
78+
79+
However, the keyword `async` and `asyncio.create_task` alone aren't enough.
80+
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:
81+
82+
```py hl_lines="36-37 39"
83+
--8<-- "docs/blog/snippets/2022-12-07-responsive-app-background-task/blocking02.py"
84+
```
85+
86+
1. The action method `action_load` now defers the heavy lifting to another method we created.
87+
2. The time-consuming operation can be run concurrently with `asyncio.create_task` because it is a coroutine.
88+
3. The method `_do_long_operation` has the keyword `async`, so it is a coroutine.
89+
90+
This modified app also works but it suffers from the same issue as the one before!
91+
The keyword `async` tells Python that there will be things inside that function that can be _awaited_ by the cook.
92+
That is, the function will do some time-consuming operation that doesn't require the cook's attention.
93+
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`.
94+
95+
Whenever we want to use the keyword `await`, we need to do it with objects that are compatible with it.
96+
For many things, that means using specialised libraries:
97+
98+
- instead of `time.sleep`, one can use `await asyncio.sleep`;
99+
- instead of the module `requests` to make Internet requests, use `aiohttp`; or
100+
- instead of using the built-in tools to read files, use `aiofiles`.
101+
102+
## Achieving good multitasking
103+
104+
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.
105+
The animation below shows that we can still change colours while the application is completing the time-consuming operation.
106+
107+
=== "Code"
108+
109+
```py hl_lines="40 41 42"
110+
--8<-- "docs\blog\snippets\2022-12-07-responsive-app-background-task\nonblocking01.py"
111+
```
112+
113+
1. We create a label that tells the user that we are starting our time-consuming operation.
114+
2. We `await` the time-consuming operation so that the application remains responsive.
115+
3. We create a label that tells the user that the time-consuming operation has been concluded.
116+
117+
=== "Animation"
118+
119+
![](../images/2022-12-07-responsive-app-background-task/non-blocking.gif)
120+
121+
Because our time-consuming operation runs concurrently, everything else in the application still works while we _await_ for the time-consuming operation to finish.
122+
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!
123+
The animation below shows just this:
124+
125+
![](../images/2022-12-07-responsive-app-background-task/responsive-demo.gif)
126+
127+
!!! warning
128+
129+
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.
130+
If you run Textual locally you will see beautiful colours ✨

0 commit comments

Comments
 (0)