Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/api/text_log.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
::: textual.widgets.TextLog
66 changes: 66 additions & 0 deletions docs/examples/widgets/text_log.py
Original file line number Diff line number Diff line change
@@ -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()
44 changes: 44 additions & 0 deletions docs/widgets/text_log.md
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ nav:
- "widgets/button.md"
- "widgets/checkbox.md"
- "widgets/data_table.md"
- "widgets/text_log.md"
- "widgets/directory_tree.md"
- "widgets/footer.md"
- "widgets/header.md"
Expand All @@ -109,6 +110,7 @@ nav:
- "api/color.md"
- "api/containers.md"
- "api/data_table.md"
- "api/text_log.md"
- "api/directory_tree.md"
- "api/dom_node.md"
- "api/events.md"
Expand Down
1 change: 1 addition & 0 deletions src/textual/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,7 @@ def __init__(
)
self._screenshot: str | None = None
self._dom_lock = asyncio.Lock()
self._dom_ready = False

@property
def return_value(self) -> ReturnType | None:
Expand Down
4 changes: 4 additions & 0 deletions src/textual/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,10 @@ class Hide(Event, bubble=False):
"""


class Ready(Event, bubble=False):
"""Sent to the app when the DOM is ready."""


@rich.repr.auto
class MouseCapture(Event, bubble=False):
"""Sent when the mouse has been captured.
Expand Down
8 changes: 6 additions & 2 deletions src/textual/screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -379,8 +379,6 @@ def _refresh_layout(self, size: Size | None = None, full: bool = False) -> None:

for widget in hidden:
widget.post_message_no_wait(Hide(self))
for widget in shown:
widget.post_message_no_wait(Show(self))

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

for widget in shown:
widget.post_message_no_wait(Show(self))

except Exception as error:
self.app._handle_exception(error)
return
display_update = self._compositor.render(full=full)
self.app._display(self, display_update)
if not self.app._dom_ready:
self.app.post_message_no_wait(events.Ready(self))
self.app._dom_ready = True

async def _on_update(self, message: messages.Update) -> None:
message.stop()
Expand Down
24 changes: 17 additions & 7 deletions src/textual/widgets/_text_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from rich.console import RenderableType
from rich.highlighter import ReprHighlighter
from rich.measure import measure_renderables
from rich.pretty import Pretty
from rich.protocol import is_renderable
from rich.segment import Segment
Expand Down Expand Up @@ -73,22 +74,31 @@ def write(self, content: RenderableType | object) -> None:
else:
if isinstance(content, str):
if self.markup:
content = Text.from_markup(content)
if self.highlight:
renderable = self.highlighter(content)
renderable = Text.from_markup(content)
else:
renderable = Text(content)
if self.highlight:
renderable = self.highlighter(content)
else:
renderable = cast(RenderableType, content)

console = self.app.console
width = max(self.min_width, self.size.width or self.min_width)
render_options = console.options

render_options = console.options.update_width(width)
if not self.wrap:
if isinstance(renderable, Text) and not self.wrap:
render_options = render_options.update(overflow="ignore", no_wrap=True)
segments = self.app.console.render(renderable, render_options.update_width(80))

width = max(
self.min_width,
measure_renderables(console, render_options, [renderable]).maximum,
)

segments = self.app.console.render(
renderable, render_options.update_width(width)
)
lines = list(Segment.split_lines(segments))
if not lines:
return

self.max_width = max(
self.max_width,
Expand Down