Skip to content

Commit 1f7bd47

Browse files
- Added 4 new recipes by NSPC911: (#14)
- Animation Effects: shaking.py - Architecture patterns: screen_push_wait.py - Modifying Widgets: progress_colors.py - Textual API usage: workers_exclusive.py - Changed unicode characters in the recipe runner to normal emojis for better compatibility
1 parent a03623a commit 1f7bd47

File tree

9 files changed

+249
-7
lines changed

9 files changed

+249
-7
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# Textual Cookbook Changelog
22

3+
## [0.3.0] 2025-08-16
4+
5+
- Added 4 new recipes by NSPC911:
6+
- Animation Effects: shaking.py
7+
- Architecture patterns: screen_push_wait.py
8+
- Modifying Widgets: progress_colors.py
9+
- Textual API usage: workers_exclusive.py
10+
- Changed unicode characters in the recipe runner to normal emojis for better compatibility
11+
312
## [0.2.0] 2025-08-15
413

514
- Added context menu recipe in Animations section

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "textual-cookbook"
3-
version = "0.2.0"
3+
version = "0.3.0"
44
description = "Textual Cookbook: Recipes for Textual Applications"
55
readme = "README.md"
66
requires-python = ">=3.10"

src/textual_cookbook/main.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,8 @@ def compose(self) -> ComposeResult:
189189
)
190190

191191
def update(self, recipe_data: RecipeData):
192-
self.query_one("#current_recipe", Static).update(f"🗎 {recipe_data['name']}.py │")
193-
self.query_one("#current_category", Static).update(f"🗁 {recipe_data['category']}")
192+
self.query_one("#current_recipe", Static).update(f"📄 {recipe_data['name']}.py │")
193+
self.query_one("#current_category", Static).update(f"📁 {recipe_data['category']}")
194194
text_area = self.query_one(TextArea)
195195
text_area.language = "python"
196196
text_area.text = recipe_data['text_blob']
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""This file demonstrates how to createa a "shaking" effect on a button.
2+
3+
Recipe by NSPC911"""
4+
5+
6+
import sys
7+
from textual import work
8+
from textual.app import App, ComposeResult
9+
from textual.widgets import Button
10+
11+
from asyncio import sleep
12+
13+
class TextualApp(App[None]):
14+
CSS = """
15+
Button {
16+
margin-left: 10
17+
}
18+
"""
19+
def compose(self) -> ComposeResult:
20+
yield Button("Shake me!")
21+
@work
22+
async def on_button_pressed(self, event: Button.Pressed):
23+
self.query_one(Button).styles.margin = (0,0,0,11)
24+
await sleep(0.1)
25+
self.query_one(Button).styles.margin = (0,0,0,10)
26+
await sleep(0.1)
27+
self.query_one(Button).styles.margin = (0,0,0,11)
28+
await sleep(0.1)
29+
self.query_one(Button).styles.margin = (0,0,0,10)
30+
31+
32+
33+
if __name__ == "__main__":
34+
app = TextualApp()
35+
app.run()
36+
sys.exit(app.return_code)
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
"""This example demonstrates how the `push_screen_wait` method
2+
(A wrapper around `push_screen` with the `wait_for_dismiss` argument
3+
set to True) can be used to push a screen in the middle of a worker
4+
that will cause the worker to temporarily pause what it is doing. This
5+
is demonstrated in both a normal async worker and a threaded worker.
6+
7+
Recipe by NSPC911
8+
https://github.com/NSPC911"""
9+
10+
import sys
11+
from typing import Any
12+
import asyncio
13+
14+
from textual import on, work
15+
from textual.app import App, ComposeResult
16+
from textual.containers import Grid, Container
17+
from textual.screen import ModalScreen
18+
from textual.widgets import Button, Label, ProgressBar
19+
20+
class Dismissable(ModalScreen[None]):
21+
"""Super simple screen that can be dismissed."""
22+
23+
DEFAULT_CSS = """
24+
Dismissable {
25+
align: center middle
26+
}
27+
#dialog {
28+
grid-size: 1;
29+
grid-gutter: 1 2;
30+
grid-rows: 1fr 3;
31+
padding: 1 3;
32+
width: 50vw;
33+
max-height: 13;
34+
border: round $primary-lighten-3;
35+
column-span: 3
36+
}
37+
#message {
38+
height: 1fr;
39+
width: 1fr;
40+
content-align: center middle
41+
}
42+
Container {
43+
align: center middle
44+
}
45+
Button {
46+
width: 50%
47+
}
48+
"""
49+
50+
def __init__(self, message: str, **kwargs: Any):
51+
super().__init__(**kwargs)
52+
self.message = message
53+
54+
def compose(self) -> ComposeResult:
55+
with Grid(id="dialog"):
56+
yield Label(self.message, id="message")
57+
with Container():
58+
yield Button("Ok", variant="primary", id="ok")
59+
60+
def on_mount(self) -> None:
61+
self.query_one("#ok").focus()
62+
63+
@on(Button.Pressed, "#ok")
64+
def on_button_pressed(self) -> None:
65+
"""Handle button presses."""
66+
self.dismiss()
67+
68+
69+
class TextualApp(App[None]):
70+
71+
def compose(self) -> ComposeResult:
72+
yield Container()
73+
yield Button("test in normal worker", id="test")
74+
yield Button("test in threaded worker", id="test_threaded")
75+
76+
@on(Button.Pressed, "#test")
77+
@work
78+
async def if_button_pressed(self, event: Button.Pressed) -> None:
79+
80+
progress = ProgressBar(total=10)
81+
self.mount(progress)
82+
while progress.percentage != 1:
83+
progress.advance()
84+
await asyncio.sleep(0.5)
85+
if progress.percentage == 0.5:
86+
await self.push_screen_wait(Dismissable("hi"))
87+
88+
@on(Button.Pressed, "#test_threaded")
89+
@work(thread=True)
90+
def if_button_pressed_thread(self) -> None:
91+
92+
progress = ProgressBar(total=10)
93+
self.call_from_thread(self.mount, progress)
94+
while progress.percentage != 1:
95+
self.call_from_thread(progress.advance)
96+
self.call_from_thread(asyncio.sleep, 0.5)
97+
if progress.percentage == 0.5:
98+
self.call_from_thread(self.push_screen_wait, Dismissable("hi"))
99+
100+
101+
if __name__ == "__main__":
102+
app = TextualApp()
103+
app.run()
104+
sys.exit(app.return_code)
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""This example demonstrates making a progress bar with different colors
2+
for different states: indeterminate, complete, incomplete, and error.
3+
4+
Recipe by NSPC911
5+
https://github.com/NSPC911"""
6+
7+
import sys
8+
from textual.app import App, ComposeResult
9+
from textual.widgets import ProgressBar
10+
11+
class TextualApp(App[None]):
12+
CSS = """
13+
.bar {
14+
color: $warning
15+
}
16+
.bar--indeterminate {
17+
color: $accent
18+
}
19+
.bar--complete {
20+
color: $success;
21+
}
22+
.error .bar--complete,
23+
.error .bar--bar {
24+
color: $error;
25+
}
26+
"""
27+
def compose(self) -> ComposeResult:
28+
yield ProgressBar(total=None, id="indeterminate")
29+
yield ProgressBar(total=100, id="complete", classes="complete")
30+
yield ProgressBar(total=100, id="incomplete", classes="incomplete")
31+
yield ProgressBar(total=100, id="error", classes="error complete")
32+
yield ProgressBar(total=100, id="incompleteerror", classes="error incomplete")
33+
34+
def on_mount(self) -> None:
35+
for widget in self.query("ProgressBar.incomplete"):
36+
widget.update(progress=50)
37+
for widget in self.query("ProgressBar.complete"):
38+
widget.update(progress=100)
39+
40+
41+
42+
if __name__ == "__main__":
43+
app = TextualApp()
44+
app.run()
45+
sys.exit(app.return_code)
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"""This file demonstrates how the `exclusive` parameter of the `work`
2+
decorator can only be used to cancel/debounce workers that are normal
3+
async workers (non-threaded). The normal worker will be restarted
4+
each time the button is pressed and effectively cancel the currently
5+
running worker. But threaded workers will not be cancelled
6+
and will run to completion, even if the button is pressed multiple
7+
times (This is a limitation of threading in Python and not because of Textual).
8+
9+
Recipe by NSPC911
10+
https://github.com/NSPC911"""
11+
12+
13+
from asyncio import sleep
14+
15+
import sys
16+
from textual import work, on
17+
from textual.app import App, ComposeResult
18+
from textual.widgets import Button, RichLog
19+
from textual.containers import HorizontalGroup
20+
21+
class TextualApp(App[None]):
22+
def compose(self) -> ComposeResult:
23+
self.richlog = RichLog()
24+
yield self.richlog
25+
with HorizontalGroup():
26+
yield Button("Run worker", id="worker")
27+
yield Button("Run worker THREAD", id="thread")
28+
29+
def on_button_pressed(self, event:Button.Pressed):
30+
self.richlog.write(event.button.id)
31+
32+
@on(Button.Pressed, "#worker")
33+
@work(exclusive=True, thread=False)
34+
async def worker_runner(self, event:Button.Pressed):
35+
await sleep(1) # simulate process intensive thing
36+
self.richlog.write("Worker completed!")
37+
38+
@on(Button.Pressed, "#thread")
39+
@work(exclusive=True, thread=True)
40+
def thread_runner(self, event:Button.Pressed):
41+
self.call_from_thread(sleep, 1)
42+
self.richlog.write("Thread completed!")
43+
44+
45+
if __name__ == "__main__":
46+
app = TextualApp()
47+
app.run()
48+
sys.exit(app.return_code)

src/textual_cookbook/styles.tcss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ TableScreen {
7575
Static {
7676
color: $success;
7777
width: auto;
78-
padding: 0 1;
78+
padding: 0 0 0 1;
7979
}
8080
Button {
8181
min-width: 7;

uv.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)