Skip to content

Commit d91ea42

Browse files
authored
Merge pull request #1333 from Textualize/textlog-docs
textlog documentation
2 parents 2e1d2d0 + e063caf commit d91ea42

File tree

8 files changed

+141
-9
lines changed

8 files changed

+141
-9
lines changed

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

docs/examples/widgets/text_log.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import csv
2+
import io
3+
4+
from rich.table import Table
5+
from rich.syntax import Syntax
6+
7+
from textual.app import App, ComposeResult
8+
from textual import events
9+
from textual.widgets import TextLog
10+
11+
12+
CSV = """lane,swimmer,country,time
13+
4,Joseph Schooling,Singapore,50.39
14+
2,Michael Phelps,United States,51.14
15+
5,Chad le Clos,South Africa,51.14
16+
6,László Cseh,Hungary,51.14
17+
3,Li Zhuhao,China,51.26
18+
8,Mehdy Metella,France,51.58
19+
7,Tom Shields,United States,51.73
20+
1,Aleksandr Sadovnikov,Russia,51.84"""
21+
22+
23+
CODE = '''\
24+
def loop_first_last(values: Iterable[T]) -> Iterable[tuple[bool, bool, T]]:
25+
"""Iterate and generate a tuple with a flag for first and last value."""
26+
iter_values = iter(values)
27+
try:
28+
previous_value = next(iter_values)
29+
except StopIteration:
30+
return
31+
first = True
32+
for value in iter_values:
33+
yield first, False, previous_value
34+
first = False
35+
previous_value = value
36+
yield first, True, previous_value\
37+
'''
38+
39+
40+
class TextLogApp(App):
41+
def compose(self) -> ComposeResult:
42+
yield TextLog(highlight=True, markup=True)
43+
44+
def on_ready(self) -> None:
45+
"""Called when the DOM is ready."""
46+
text_log = self.query_one(TextLog)
47+
48+
text_log.write(Syntax(CODE, "python", indent_guides=True))
49+
50+
rows = iter(csv.reader(io.StringIO(CSV)))
51+
table = Table(*next(rows))
52+
for row in rows:
53+
table.add_row(*row)
54+
55+
text_log.write(table)
56+
text_log.write("[bold magenta]Write text or any Rich renderable!")
57+
58+
def on_key(self, event: events.Key) -> None:
59+
"""Write Key events to log."""
60+
text_log = self.query_one(TextLog)
61+
text_log.write(event)
62+
63+
64+
if __name__ == "__main__":
65+
app = TextLogApp()
66+
app.run()

docs/widgets/text_log.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# TextLog
2+
3+
A TextLog is a widget which displays scrollable content that may be appended to in realtime.
4+
5+
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.
6+
7+
- [X] Focusable
8+
- [ ] Container
9+
10+
## Example
11+
12+
The example below shows each placeholder variant.
13+
14+
=== "Output"
15+
16+
```{.textual path="docs/examples/widgets/text_log.py" press="_,H,i"}
17+
```
18+
19+
=== "text_log.py"
20+
21+
```python
22+
--8<-- "docs/examples/widgets/text_log.py"
23+
```
24+
25+
26+
27+
## Reactive Attributes
28+
29+
| Name | Type | Default | Description |
30+
| ----------- | ------ | ------- | ------------------------------------------------------------ |
31+
| `highlight` | `bool` | `False` | Automatically highlight content. |
32+
| `markup` | `bool` | `False` | Apply Rich console markup. |
33+
| `max_lines` | `int` | `None` | Maximum number of lines in the log or `None` for no maximum. |
34+
| `min_width` | `int` | 78 | Minimum width of renderables. |
35+
| `wrap` | `bool` | `False` | Enable word wrapping. |
36+
37+
## Messages
38+
39+
This widget sends no messages.
40+
41+
42+
## See Also
43+
44+
* [TextLog](../api/textlog.md) code reference

mkdocs.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ nav:
9292
- "widgets/button.md"
9393
- "widgets/checkbox.md"
9494
- "widgets/data_table.md"
95+
- "widgets/text_log.md"
9596
- "widgets/directory_tree.md"
9697
- "widgets/footer.md"
9798
- "widgets/header.md"
@@ -109,6 +110,7 @@ nav:
109110
- "api/color.md"
110111
- "api/containers.md"
111112
- "api/data_table.md"
113+
- "api/text_log.md"
112114
- "api/directory_tree.md"
113115
- "api/dom_node.md"
114116
- "api/events.md"

src/textual/app.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,7 @@ def __init__(
356356
)
357357
self._screenshot: str | None = None
358358
self._dom_lock = asyncio.Lock()
359+
self._dom_ready = False
359360

360361
@property
361362
def return_value(self) -> ReturnType | None:

src/textual/events.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,10 @@ class Hide(Event, bubble=False):
140140
"""
141141

142142

143+
class Ready(Event, bubble=False):
144+
"""Sent to the app when the DOM is ready."""
145+
146+
143147
@rich.repr.auto
144148
class MouseCapture(Event, bubble=False):
145149
"""Sent when the mouse has been captured.

src/textual/screen.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -379,8 +379,6 @@ def _refresh_layout(self, size: Size | None = None, full: bool = False) -> None:
379379

380380
for widget in hidden:
381381
widget.post_message_no_wait(Hide(self))
382-
for widget in shown:
383-
widget.post_message_no_wait(Show(self))
384382

385383
# We want to send a resize event to widgets that were just added or change since last layout
386384
send_resize = shown | resized
@@ -401,11 +399,17 @@ def _refresh_layout(self, size: Size | None = None, full: bool = False) -> None:
401399
ResizeEvent(self, region.size, virtual_size, container_size)
402400
)
403401

402+
for widget in shown:
403+
widget.post_message_no_wait(Show(self))
404+
404405
except Exception as error:
405406
self.app._handle_exception(error)
406407
return
407408
display_update = self._compositor.render(full=full)
408409
self.app._display(self, display_update)
410+
if not self.app._dom_ready:
411+
self.app.post_message_no_wait(events.Ready(self))
412+
self.app._dom_ready = True
409413

410414
async def _on_update(self, message: messages.Update) -> None:
411415
message.stop()

src/textual/widgets/_text_log.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from rich.console import RenderableType
66
from rich.highlighter import ReprHighlighter
7+
from rich.measure import measure_renderables
78
from rich.pretty import Pretty
89
from rich.protocol import is_renderable
910
from rich.segment import Segment
@@ -73,22 +74,31 @@ def write(self, content: RenderableType | object) -> None:
7374
else:
7475
if isinstance(content, str):
7576
if self.markup:
76-
content = Text.from_markup(content)
77-
if self.highlight:
78-
renderable = self.highlighter(content)
77+
renderable = Text.from_markup(content)
7978
else:
8079
renderable = Text(content)
80+
if self.highlight:
81+
renderable = self.highlighter(content)
8182
else:
8283
renderable = cast(RenderableType, content)
8384

8485
console = self.app.console
85-
width = max(self.min_width, self.size.width or self.min_width)
86+
render_options = console.options
8687

87-
render_options = console.options.update_width(width)
88-
if not self.wrap:
88+
if isinstance(renderable, Text) and not self.wrap:
8989
render_options = render_options.update(overflow="ignore", no_wrap=True)
90-
segments = self.app.console.render(renderable, render_options.update_width(80))
90+
91+
width = max(
92+
self.min_width,
93+
measure_renderables(console, render_options, [renderable]).maximum,
94+
)
95+
96+
segments = self.app.console.render(
97+
renderable, render_options.update_width(width)
98+
)
9199
lines = list(Segment.split_lines(segments))
100+
if not lines:
101+
return
92102

93103
self.max_width = max(
94104
self.max_width,

0 commit comments

Comments
 (0)