intro docs

This commit is contained in:
Will McGugan
2022-08-09 14:26:56 +01:00
parent d6079deb90
commit 6740e5edf8
5 changed files with 525 additions and 19 deletions

240
docs/examples/demo.css Normal file
View File

@@ -0,0 +1,240 @@
/* CSS file for basic.py */
* {
transition: color 300ms linear, background 300ms linear;
}
*:hover {
/* tint: 30% red;
/* outline: heavy red; */
}
App > Screen {
background: $surface;
color: $text-surface;
layers: sidebar;
color: $text-background;
background: $background;
layout: vertical;
}
DataTable {
/*border:heavy red;*/
/* tint: 10% green; */
/* opacity: 50%; */
padding: 1;
margin: 1 2;
height: 12;
}
#sidebar {
color: $text-panel;
background: $panel;
dock: left;
width: 30;
offset-x: -100%;
transition: offset 500ms in_out_cubic;
layer: sidebar;
}
#sidebar.-active {
offset-x: 0;
}
#sidebar .title {
height: 1;
background: $primary-background-darken-1;
color: $text-primary-background-darken-1;
border-right: wide $background;
content-align: center middle;
}
#sidebar .user {
height: 8;
background: $panel-darken-1;
color: $text-panel-darken-1;
border-right: wide $background;
content-align: center middle;
}
#sidebar .content {
background: $panel-darken-2;
color: $text-surface;
border-right: wide $background;
content-align: center middle;
}
#header {
color: $text-secondary-background;
background: $secondary-background;
height: 1;
content-align: center middle;
dock: top;
}
Tweet {
height:12;
width: 100%;
background: $panel;
color: $text-panel;
layout: vertical;
/* border: outer $primary; */
padding: 1;
border: wide $panel;
overflow: auto;
/* scrollbar-gutter: stable; */
align-horizontal: center;
box-sizing: border-box;
}
.scrollable {
overflow-x: auto;
overflow-y: scroll;
margin: 1 2;
height: 20;
align-horizontal: center;
layout: vertical;
}
.code {
height: auto;
}
TweetHeader {
height:1;
background: $accent;
color: $text-accent
}
TweetBody {
width: 100%;
background: $panel;
color: $text-panel;
height: auto;
padding: 0 1 0 0;
}
Tweet.scroll-horizontal TweetBody {
width: 350;
}
.button {
background: $accent;
color: $text-accent;
width:20;
height: 3;
/* border-top: hidden $accent-darken-3; */
border: tall $accent-darken-2;
/* border-left: tall $accent-darken-1; */
/* padding: 1 0 0 0 ; */
transition: background 400ms in_out_cubic, color 400ms in_out_cubic;
}
.button:hover {
background: $accent-lighten-1;
color: $text-accent-lighten-1;
width: 20;
height: 3;
border: tall $accent-darken-1;
/* border-left: tall $accent-darken-3; */
}
#footer {
color: $text-accent;
background: $accent;
height: 1;
content-align: center middle;
dock:bottom;
}
#sidebar .content {
layout: vertical
}
OptionItem {
height: 3;
background: $panel;
border-right: wide $background;
border-left: blank;
content-align: center middle;
}
OptionItem:hover {
height: 3;
color: $text-primary;
background: $primary-darken-1;
/* border-top: hkey $accent2-darken-3;
border-bottom: hkey $accent2-darken-3; */
text-style: bold;
border-left: outer $secondary-darken-2;
}
Error {
width: 100%;
height:3;
background: $error;
color: $text-error;
border-top: tall $error-darken-2;
border-bottom: tall $error-darken-2;
padding: 0;
text-style: bold;
align-horizontal: center;
}
Warning {
width: 100%;
height:3;
background: $warning;
color: $text-warning-fade-1;
border-top: tall $warning-darken-2;
border-bottom: tall $warning-darken-2;
text-style: bold;
align-horizontal: center;
}
Success {
width: 100%;
height:auto;
box-sizing: border-box;
background: $success;
color: $text-success-fade-1;
border-top: hkey $success-darken-2;
border-bottom: hkey $success-darken-2;
text-style: bold ;
align-horizontal: center;
}
.horizontal {
layout: horizontal
}

214
docs/examples/demo.py Normal file
View File

@@ -0,0 +1,214 @@
from rich.console import RenderableType
from rich.syntax import Syntax
from rich.text import Text
from textual.app import App, ComposeResult
from textual.reactive import Reactive
from textual.widget import Widget
from textual.widgets import Static, DataTable
CODE = '''
from __future__ import annotations
from typing import Iterable, TypeVar
T = TypeVar("T")
def loop_first(values: Iterable[T]) -> Iterable[tuple[bool, T]]:
"""Iterate and generate a tuple with a flag for first value."""
iter_values = iter(values)
try:
value = next(iter_values)
except StopIteration:
return
yield True, value
for value in iter_values:
yield False, value
def loop_last(values: Iterable[T]) -> Iterable[tuple[bool, T]]:
"""Iterate and generate a tuple with a flag for last value."""
iter_values = iter(values)
try:
previous_value = next(iter_values)
except StopIteration:
return
for value in iter_values:
yield False, previous_value
previous_value = value
yield True, previous_value
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
'''
lorem_short = """Lorem ipsum dolor sit amet, consectetur adipiscing elit. In velit liber a a a, volutpat nec hendrerit at, faucibus in odio. Aliquam hendrerit nibh sed quam volutpat maximus. Nullam suscipit convallis lorem quis sodales. In tristique lobortis ante et dictum. Ut at finibus ipsum."""
lorem = (
lorem_short
+ """ In urna dolor, placerat et mi facilisis, congue sollicitudin massa. Phasellus felis turpis, cursus eu lectus et, porttitor malesuada augue. Sed feugiat volutpat velit, sollicitudin fringilla velit bibendum faucibus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. In velit libero, volutpat nec hendrerit at, faucibus in odio. Aliquam hendrerit nibh sed quam volutpat maximus. Nullam suscipit convallis lorem quis sodales. In tristique lobortis ante et dictum. Ut at finibus ipsum. In urna dolor, placerat et mi facilisis, congue sollicitudin massa. Phasellus felis turpis, cursus eu lectus et, porttitor malesuada augue. Sed feugiat volutpat velit, sollicitudin fringilla velit bibendum faucibus. """
)
lorem_short_text = Text.from_markup(lorem_short)
lorem_long_text = Text.from_markup(lorem * 2)
class TweetHeader(Widget):
def render(self) -> RenderableType:
return Text("Lorem Impsum", justify="center")
class TweetBody(Widget):
short_lorem = Reactive(False)
def render(self) -> Text:
return lorem_short_text if self.short_lorem else lorem_long_text
class Tweet(Widget):
pass
class OptionItem(Widget):
def render(self) -> Text:
return Text("Option")
class Error(Widget):
def render(self) -> Text:
return Text("This is an error message", justify="center")
class Warning(Widget):
def render(self) -> Text:
return Text("This is a warning message", justify="center")
class Success(Widget):
def render(self) -> Text:
return Text("This is a success message", justify="center")
class BasicApp(App, css_path="demo.css"):
"""A basic app demonstrating CSS"""
def on_load(self):
"""Bind keys here."""
self.bind("s", "toggle_class('#sidebar', '-active')")
def compose(self) -> ComposeResult:
table = DataTable()
self.scroll_to_target = Tweet(TweetBody())
yield Static(
Text.from_markup(
"[b]This is a [u]Textual[/u] app, running in the terminal"
),
id="header",
)
yield from (
Tweet(TweetBody()),
Widget(
Static(
Syntax(CODE, "python", line_numbers=True, indent_guides=True),
classes="code",
),
classes="scrollable",
),
table,
Error(),
Tweet(TweetBody(), classes="scrollbar-size-custom"),
Warning(),
Tweet(TweetBody(), classes="scroll-horizontal"),
Success(),
Tweet(TweetBody(), classes="scroll-horizontal"),
Tweet(TweetBody(), classes="scroll-horizontal"),
Tweet(TweetBody(), classes="scroll-horizontal"),
Tweet(TweetBody(), classes="scroll-horizontal"),
Tweet(TweetBody(), classes="scroll-horizontal"),
)
yield Widget(id="footer")
yield Widget(
Widget(classes="title"),
Widget(classes="user"),
OptionItem(),
OptionItem(),
OptionItem(),
Widget(classes="content"),
id="sidebar",
)
table.add_column("Foo", width=20)
table.add_column("Bar", width=20)
table.add_column("Baz", width=20)
table.add_column("Foo", width=20)
table.add_column("Bar", width=20)
table.add_column("Baz", width=20)
table.zebra_stripes = True
for n in range(100):
table.add_row(*[f"Cell ([b]{n}[/b], {col})" for col in range(6)])
async def on_key(self, event) -> None:
await self.dispatch_key(event)
def key_d(self):
self.dark = not self.dark
async def key_q(self):
await self.shutdown()
def key_x(self):
self.panic(self.tree)
def key_escape(self):
self.app.bell()
def key_t(self):
# Pressing "t" toggles the content of the TweetBody widget, from a long "Lorem ipsum..." to a shorter one.
tweet_body = self.query("TweetBody").first()
tweet_body.short_lorem = not tweet_body.short_lorem
def key_v(self):
self.get_child(id="content").scroll_to_widget(self.scroll_to_target)
def key_space(self):
self.bell()
app = BasicApp()
if __name__ == "__main__":
app.run()
# from textual.geometry import Region
# from textual.color import Color
# print(Region.intersection.cache_info())
# print(Region.overlaps.cache_info())
# print(Region.union.cache_info())
# print(Region.split_vertical.cache_info())
# print(Region.__contains__.cache_info())
# from textual.css.scalar import Scalar
# print(Scalar.resolve_dimension.cache_info())
# from rich.style import Style
# from rich.cells import cached_cell_len
# print(Style._add.cache_info())
# print(cached_cell_len.cache_info())

View File

@@ -1,11 +1,43 @@
# Welcome
Welcome to the [Textual](https://github.com/Textualize/textual) framework documentation, built with ❤️ by [Textualize.io](https://www.textualize.io)
Welcome to the [Textual](https://github.com/Textualize/textual) framework documentation. Built with ❤️ by [Textualize.io](https://www.textualize.io)
## Getting started
Textual is a Python framework for building applications that run within your terminal. Known as TUIs (Text User Interfaces), such applications have a multitude of benefits:
Textual is a Python framework which you can install via Pypi.
- **Quick to develop:** Textual is a modern Python API.
- **Low requirements:** Run Textual apps anywhere with a Python interpreter, even single-board computers.
- **Cross platform:** The same code will run on Linux, Windows, MacOS and more.
- **Remote:** Fully featured UIs can run over SSH.
- **CLI integration:** Textual apps integrate with your shell and other CLI tools.
Textual TUIs are quick and easy to build with pure Python (not to mention _fun_).
<!-- TODO: More examples split in to tabs -->
```{.textual path="docs/examples/demo.py" columns=100 lines=48}
```
## Installation
You can install Textual via Pypi.
If you plan on developing Textual apps, then you can install `textual[dev]`. The `[dev]` part installs a few extra dependencies for development.
```bash
pip install textual[dev]
```
If you only plan on running Textual apps, then you can drop the `[dev]` part:
```bash
pip install textual
```
## Textual CLI app
If you installed the dev dependencies, you have have access to the `textual` CLI command. There are a number of sub-commands which will aid you in building Textual apps. See the help for more details:
```python
textual --help
```

View File

@@ -2,7 +2,7 @@
Welcome to the Textual Introduction!
This is a very gentle introduction to creating Textual applications. By the end of this document you should have an understanding of the basic concepts involved in using the Textual framework.
By the end of this page you should have a good idea of the steps involved in creating an application with Textual.
## Pre-requisites
@@ -12,7 +12,7 @@ This is a very gentle introduction to creating Textual applications. By the end
## A Simple App
Let's looks at the simplest possible Textual app. It doesn't do much, but will demonstrate the basic steps you will need to create any application.
Let's looks at the simplest possible Textual app.
If you would like to follow along and run the examples, navigate to the `docs/examples/introduction` directory from the command prompt. We will be looking at `intro01.py`, which you can see here:
@@ -26,7 +26,7 @@ Enter the following command to run the application:
python intro01.py
```
The command prompt should disappear and you will see a blank screen. It will look something like the following:
The command prompt should disappear and you will see a blank screen:
```{.textual path="docs/examples/introduction/intro01.py"}
@@ -34,7 +34,7 @@ The command prompt should disappear and you will see a blank screen. It will loo
Hit ++ctrl+c++ to exit and return to the command prompt.
### The code
### Application mode
The first step in all Textual applications is to import the `App` class from `textual.app` and extend it:
@@ -54,15 +54,15 @@ The `run` method will put your terminal in to "application mode" which disables
## Handling Events
Most real-world applications will want to interact with the user in some way. To do this we can make use of _event handler_ methods, which are called in response to things the user does such as pressing a key(s), moving the mouse, resizing the terminal, etc.
Most real-world applications will want to interact with the user in some way. To do this we can make use of _event handler_ methods, which are called in response to things the user does such as pressing keys, moving the mouse, resizing the terminal, etc.
Each event type is represented by an event object, which is an instance of a class containing information you may need to respond the the event. For instance the `Key` event contains the key the user pressed and a `Mouse` event will contain the coordinates of the mouse cursor.
Each event type is represented by an event object, which is an instance of a class containing information you may need to respond the the event. For instance, the `Key` event contains the key the user pressed and a `Mouse` event will contain the coordinates of the mouse cursor.
!!! note
Although `intro01.py` did not explicitly define any event handlers, Textual still had to respond to events to catch ++ctrl+c++, otherwise you wouldn't be able to exit the app.
The next example demonstrates handling events. Try running `intro02.py` in the `docs/examples/introduction`:
The next example demonstrates handling events. Try running `intro02.py` in the `docs/examples/introduction` directory:
```python title="intro02.py"
--8<-- "docs/examples/introduction/intro02.py"
@@ -82,17 +82,17 @@ If you hit any of the number keys ++0++-++9++, the background will change color
There are two event handlers in this app. Event handlers start with the text `on_` followed by the name of the event in lower case. Hence `on_mount` is called for the `Mount` event, and `on_key` is called for the `Key` event.
The first event handler to run is `on_mount`. The `Mount` is sent to your application immediately after entering application mode.
The first event handler to run is `on_mount`. The `Mount` event is sent to your application immediately after entering application mode.
```python hl_lines="19 20" title="intro02.py"
--8<-- "docs/examples/introduction/intro02.py"
```
This `on_mount` method sets the `background` attribute of `self.styles` to `"darkblue"` which makes the background blue when the application starts. There are a lot of other properties on the Styles object, which define how your app looks. We will explore what you can do with this object later.
This `on_mount` method sets the `background` attribute of `self.styles` to `"darkblue"` which makes the background blue when the application starts. There are a lot of other style properties which define how your app looks. We will explore those later.
!!! note
You may have noticed there is no function call to repaint the screen in this example. Textual is generally quite smart in detecting when a refresh is required, and updating the screen automatically.
You may have noticed there is no function call to repaint the screen in this example. Textual is generally smart enough to refresh the screen automatically.
The second event handler will receive `Key` events whenever you press a key on the keyboard:
@@ -100,7 +100,7 @@ The second event handler will receive `Key` events whenever you press a key on t
--8<-- "docs/examples/introduction/intro02.py"
```
This method has an `event` positional argument which will receive the event object; in this case the `Key` event. The body of the method sets the background to a corresponding color in the `COLORS` list when you press one of the digit keys. It also calls `bell()` which is a method on App that plays your terminal's bell.
This method has an `event` positional argument which will receive the event object; in this case the `Key` event. The body of the method sets the background to a corresponding color in the `COLORS` list when you press one of the digit keys. It also calls `bell()` to plays your terminal's bell sound.
!!! note
@@ -122,15 +122,15 @@ Here's what you will see if you run this code:
```
This script imports App as before, but also the `Widget` class from `textual.widget`, which is the base class for all Widgets. To create a Clock widget we extend from the Widget base class:
This script imports `App` and also the `Widget` class from `textual.widget`. To create a Clock widget we extend from the Widget base class:
```python title="clock01.py" hl_lines="7 8 9 10 11 12 13"
--8<-- "docs/examples/introduction/clock01.py"
```
Widgets support many of the same events as the Application itself, and can be thought of as mini-applications in their own right. The Clock widget responds to a Mount event which is the first event received when a widget is _mounted_ (added to the App). The code in `Clock.on_mount` sets `styles.content_align` to tuple of `("center", "middle")` which tells Textual to display the Widget's content aligned to the horizontal center, and in the middle vertically. If you resize the terminal, you should find the time remains in the center.
Widgets support many of the same events as the Application itself, and can be thought of as mini-applications in their own right. The Clock widget responds to a Mount event which is the first event received when a widget is _mounted_ (added to the App). The code in `Clock.on_mount` sets `styles.content_align` to tuple of `("center", "middle")` which tells Textual to center align its contents. If you size the terminal you should see that the text remains centered.
The second line in `on_mount` calls `self.set_interval` which tells Textual to invoke the `self.refresh` method once per second.
The second line in `on_mount` calls `self.set_interval` which tells Textual to invoke the `self.refresh` method once per second, so our clock remains up-to-date.
When Textual refreshes a widget it calls it's `render` method:
@@ -138,9 +138,9 @@ When Textual refreshes a widget it calls it's `render` method:
--8<-- "docs/examples/introduction/clock01.py"
```
The Clocks `render` method uses the datetime module to format the current date and time. It returns a string, but can also return a _Rich renderable_. Don't worry if you aren't familiar with [Rich](https://github.com/Textualize/rich), we will cover that later.
The Clock's `render` method uses the datetime module to format the current date and time. It returns a string, but can also return a [Rich](https://github.com/Textualize/rich) _renderable_. Don't worry if you aren't familiar with Rich, we will cover that later.
Before a Widget can be displayed, it must first be mounted on the app. This is typically done within the applications Mount handler, so that an application's widgets are added when the application first starts:
Before a Widget can be displayed, it must first be mounted on the app. This is typically done within the application's Mount handler:
```python title="clock01.py" hl_lines="17 18"
--8<-- "docs/examples/introduction/clock01.py"
@@ -149,3 +149,23 @@ Before a Widget can be displayed, it must first be mounted on the app. This is t
In the case of the clock application, we call `mount` with an instance of the `Clock` widget.
That's all there is to this Clock example. It will display the current time until you hit ++ctrl+c++
## Compose
Mounting "child" widgets from from an `on_mount` event is such a common pattern that Textual offers a convenience method to do that.
If you implement a `compose()` method on your App or Widget, Textual will invoke it to get your "sub-widgets". This method should return an _iterable_ such as a list, but you may find it easier to use the `yield` statement to turn it in to a Python generator:
```python title="clock02.py" hl_lines="17 18"
--8<-- "docs/examples/introduction/clock02.py"
```
Here's the clock example again using `compose()` rather than `on_mount`. Any Widgets yielded from this method will be mounted on to the App or Widget. In this case we mount our Clock widget as before.
More sophisticated apps will likely yield multiple widgets from `compose()`, and many widgets will also yield child widgets of their own.
## Next Steps
We've seen how Textual apps can respond to events, and how to mount widgets which are like mini-applications in their own right. These are key concepts in Textual which you can use to build more sophisticated apps.
The Guide covers this in much more detail, and describes how arrange widgets on the screen and connect them with the core logic of your application.