Merge branch 'css' of github.com:Textualize/textual into easing-examples

This commit is contained in:
Darren Burns
2022-09-07 12:23:24 +01:00
152 changed files with 1845 additions and 732 deletions

View File

@@ -12,3 +12,6 @@ docs-serve:
mkdocs serve mkdocs serve
docs-build: docs-build:
mkdocs build mkdocs build
docs-deploy:
mkdocs gh-deploy

View File

@@ -1 +0,0 @@
# Actions

3
docs/events/index.md Normal file
View File

@@ -0,0 +1,3 @@
# Events
A reference to Textual [events](../guide/events.md).

View File

@@ -0,0 +1,30 @@
from textual.app import App
from textual import events
class EventApp(App):
COLORS = [
"white",
"maroon",
"red",
"purple",
"fuchsia",
"olive",
"yellow",
"navy",
"teal",
"aqua",
]
def on_mount(self) -> None:
self.styles.background = "darkblue"
def on_key(self, event: events.Key) -> None:
if event.key.isdecimal():
self.styles.background = self.COLORS[int(event.key)]
app = EventApp()
if __name__ == "__main__":
app.run()

View File

@@ -0,0 +1,18 @@
from textual.app import App, ComposeResult
from textual.widgets import Static, Button
class QuestionApp(App[str]):
def compose(self) -> ComposeResult:
yield Static("Do you love Textual?")
yield Button("Yes", id="yes", variant="primary")
yield Button("No", id="no", variant="error")
def on_button_pressed(self, event: Button.Pressed) -> None:
self.exit(event.button.id)
app = QuestionApp()
if __name__ == "__main__":
reply = app.run()
print(reply)

View File

@@ -0,0 +1,17 @@
Screen {
layout: grid;
grid-size: 2;
grid-gutter: 2;
padding: 2;
}
#question {
width: 100%;
height: 100%;
column-span: 2;
content-align: center bottom;
text-style: bold;
}
Button {
width: 100%;
}

View File

@@ -0,0 +1,18 @@
from textual.app import App, ComposeResult
from textual.widgets import Static, Button
class QuestionApp(App[str]):
def compose(self) -> ComposeResult:
yield Static("Do you love Textual?", id="question")
yield Button("Yes", id="yes", variant="primary")
yield Button("No", id="no", variant="error")
def on_button_pressed(self, event: Button.Pressed) -> None:
self.exit(event.button.id)
app = QuestionApp(css_path="question02.css")
if __name__ == "__main__":
reply = app.run()
print(reply)

View File

@@ -0,0 +1,38 @@
from textual.app import App, ComposeResult
from textual.widgets import Static, Button
class QuestionApp(App[str]):
CSS = """
Screen {
layout: table;
table-size: 2;
table-gutter: 2;
padding: 2;
}
#question {
width: 100%;
height: 100%;
column-span: 2;
content-align: center bottom;
text-style: bold;
}
Button {
width: 100%;
}
"""
def compose(self) -> ComposeResult:
yield Static("Do you love Textual?", id="question")
yield Button("Yes", id="yes", variant="primary")
yield Button("No", id="no", variant="error")
def on_button_pressed(self, event: Button.Pressed) -> None:
self.exit(event.button.id)
app = QuestionApp()
if __name__ == "__main__":
reply = app.run()
print(reply)

View File

@@ -0,0 +1,14 @@
from textual.app import App, ComposeResult
from textual.widgets import Button
class ButtonsApp(App):
def compose(self) -> ComposeResult:
yield Button("Paul")
yield Button("Duncan")
yield Button("Chani")
app = ButtonsApp()
if __name__ == "__main__":
app.run()

View File

@@ -0,0 +1,5 @@
from textual.app import App
class MyApp(App):
pass

View File

@@ -0,0 +1,10 @@
from textual.app import App
class MyApp(App):
pass
app = MyApp()
if __name__ == "__main__":
app.run()

View File

@@ -0,0 +1,15 @@
from textual.app import App, ComposeResult
from textual.widgets import Welcome
class WelcomeApp(App):
def compose(self) -> ComposeResult:
yield Welcome()
def on_button_pressed(self) -> None:
self.exit()
app = WelcomeApp()
if __name__ == "__main__":
app.run()

View File

@@ -0,0 +1,15 @@
from textual.app import App
from textual.widgets import Welcome
class WelcomeApp(App):
def on_key(self) -> None:
self.mount(Welcome())
def on_button_pressed(self) -> None:
self.exit()
app = WelcomeApp()
if __name__ == "__main__":
app.run()

View File

@@ -7,7 +7,7 @@ from textual.widget import Widget
class Clock(Widget): class Clock(Widget):
"""A clock app.""" """A clock app."""
CSS = """ DEFAULT_CSS = """
Clock { Clock {
content-align: center middle; content-align: center middle;
} }

View File

@@ -4,7 +4,7 @@ from textual.widgets import Button
class ButtonApp(App): class ButtonApp(App):
CSS = """ DEFAULT_CSS = """
Button { Button {
width: 100%; width: 100%;
} }

View File

@@ -10,7 +10,7 @@ CSS stands for _Cascading Stylesheets_. A stylesheet is a list of styles and rul
Depending on what you want to build with Textual, you may not need to learn Textual CSS at all. Widgets are packaged with CSS styles so apps with exclusively pre-built widgets may not need any additional CSS. Depending on what you want to build with Textual, you may not need to learn Textual CSS at all. Widgets are packaged with CSS styles so apps with exclusively pre-built widgets may not need any additional CSS.
Textual CSS defines a set of rules which apply visual _styles_ to your application and widgets. These style can customize a large variety of visual settings, such as color, border, size, alignment; and more dynamic features such as animation and hover effects. As powerful as it is, CSS in Textual is quite straightforward. Textual CSS defines a set of rules which apply visual _styles_ to your application and widgets. These style can customize settings for properties such as color, border, size, alignment; and more dynamic features such as animation and hover effects. As powerful as it is, CSS in Textual is quite straightforward.
CSS is typically stored in an external file with the extension `.css` alongside your Python code. CSS is typically stored in an external file with the extension `.css` alongside your Python code.
@@ -30,7 +30,7 @@ This is an example of a CSS _rule set_. There may be many such sections in any g
Let's break this CSS code down a bit. Let's break this CSS code down a bit.
```css hl_lines="1" ```sass hl_lines="1"
Header { Header {
dock: top; dock: top;
height: 3; height: 3;
@@ -42,7 +42,7 @@ Header {
The first line is a _selector_ which tells Textual which Widget(s) to modify. In the above example, the styles will be applied to a widget defined by the Python class `Header`. The first line is a _selector_ which tells Textual which Widget(s) to modify. In the above example, the styles will be applied to a widget defined by the Python class `Header`.
```css hl_lines="2 3 4 5 6" ```sass hl_lines="2 3 4 5 6"
Header { Header {
dock: top; dock: top;
height: 3; height: 3;
@@ -58,7 +58,7 @@ The first rule in the above example reads `"dock: top;"`. The rule name is `dock
## The DOM ## The DOM
The DOM, or _Document Object Model_, is a term borrowed from the web world. Textual doesn't use documents but the term has stuck. In Textual CSS, the DOM is a an arrangement of widgets you can visualize as a tree-like structure. The DOM, or _Document Object Model_, is a term borrowed from the web world. Textual doesn't use documents but the term has stuck. In Textual CSS, the DOM is an arrangement of widgets you can visualize as a tree-like structure.
Some widgets contain other widgets: for instance, a list control widget will likely also have item widgets, or a dialog widget may contain button widgets. These _child_ widgets form the branches of the tree. Some widgets contain other widgets: for instance, a list control widget will likely also have item widgets, or a dialog widget may contain button widgets. These _child_ widgets form the branches of the tree.
@@ -391,3 +391,7 @@ Button:hover {
background: blue !important; background: blue !important;
} }
``` ```
## CSS Variables
TODO: Variables

3
docs/guide/actions.md Normal file
View File

@@ -0,0 +1,3 @@
# Actions
TODO: Actions docs

3
docs/guide/animator.md Normal file
View File

@@ -0,0 +1,3 @@
# Animator
TODO: Animator docs

179
docs/guide/app.md Normal file
View File

@@ -0,0 +1,179 @@
# App Basics
In this chapter we will cover how to use Textual's App class to create an application. Just enough to get you up to speed. We will go in to more detail in the following chapters.
## The App class
The first step in building a Textual app is to import the [App][textual.app.App] class and create a subclass. Let's look at the simplest app class:
```python
--8<-- "docs/examples/app/simple01.py"
```
### The run method
To run an app we create an instance and call [run()][textual.app.App.run].
```python hl_lines="8-10" title="simple02.py"
--8<-- "docs/examples/app/simple02.py"
```
Apps don't get much simpler than this&mdash;don't expect it to do much.
!!! tip
The `__name__ == "__main__":` condition is true only if you run the file with `python` command. This allows us to import `app` without running the app immediately. It also allows the [devtools run](devtools.md#run) command to run the app in development mode. See the [Python docs](https://docs.python.org/3/library/__main__.html#idiomatic-usage) for more information.
If we run this app with `python simple02.py` you will see a blank terminal, something like the following:
```{.textual path="docs/examples/app/simple02.py"}
```
When you call [App.run()][textual.app.App.run] Textual puts the terminal in to a special state called *application mode*. When in application mode the terminal will no longer echo what you type. Textual will take over responding to user input (keyboard and mouse) and will update the visible portion of the terminal (i.e. the *screen*).
If you hit ++ctrl+c++ Textual will exit application mode and return you to the command prompt. Any content you had in the terminal prior to application mode will be restored.
## Events
Textual has an event system you can use to respond to key presses, mouse actions, and internal state changes. Event handlers are methods which are prefixed with `on_` followed by the name of the event.
One such event is the *mount* event which is sent to an application after it enters application mode. You can respond to this event by defining a method called `on_mount`.
!!! info
You may have noticed we use the term "send" and "sent" in relation to event handler methods in preference to "calling". This is because Textual uses a message passing system where events are passed (or *sent*) between components. We will cover the details in [events][./events.md].
Another such event is the *key* event which is sent when the user presses a key. The following example contains handlers for both those events:
```python title="event01.py"
--8<-- "docs/examples/app/event01.py"
```
The `on_mount` handler sets the `self.styles.background` attribute to `"darkblue"` which (as you can probably guess) turns the background blue. Since the mount event is sent immediately after entering application mode, you will see a blue screen when you run the code:
```{.textual path="docs/examples/app/event01.py" hl_lines="23-25"}
```
The key event handler (`on_key`) specifies an `event` parameter which will receive a [events.Key][textual.events.Key] instance. Every event has an associated event object which will be passed to the handler method if it is present in the method's parameter list.
!!! note
It is unusual (but not unprecedented) for a method's parameters to affect how it is called. Textual accomplishes this by inspecting the method prior to calling it.
For some events, such as the key event, the event object contains additional information. In the case of [events.Key][textual.events.Key] it will contain the key that was pressed.
The `on_key` method above uses the `key` attribute on the Key event to change the background color if any of the keys ++0++ to ++9++ are pressed.
### Async events
Textual is powered by Python's [asyncio](https://docs.python.org/3/library/asyncio.html) framework which uses the `async` and `await` keywords to coordinate events.
Textual knows to *await* your event handlers if they are generators (i.e. prefixed with the `async` keyword).
!!! note
Don't worry if you aren't familiar with the async programming in Python. You can build many apps without using them.
## Widgets
Widgets are self-contained components responsible for generating the output for a portion of the screen and can respond to events in much the same way as the App. Most apps that do anything interesting will contain at least one (and probably many) widgets which together form a User Interface.
Widgets can be as simple as a piece of text, a button, or a fully-fledge component like a text editor or file browser (which may contain widgets of their own).
### Composing
To add widgets to your app implement a [`compose()`][textual.app.App.compose] method which should return a iterable of Widget instances. A list would work, but it is convenient to yield widgets, making the method a *generator*.
The following example imports a builtin Welcome widget and yields it from compose.
```python title="widgets01.py"
--8<-- "docs/examples/app/widgets01.py"
```
When you run this code, Textual will *mount* the Welcome widget which contains a Markdown content area and a button:
```{.textual path="docs/examples/app/widgets01.py"}
```
Notice the `on_button_pressed` method which handles the [Button.Pressed][textual.widgets.Button] event sent by a button contained in the Welcome widget. The handler calls [App.exit()][textual.app.App] to exit the app.
### Mounting
While composing is the preferred way of adding widgets when your app starts it is sometimes necessary to add new widget(s) in response to events. You can do this by calling [mount()][textual.widget.Widget.mount] which will add a new widget to the UI.
Here's an app which adds the welcome widget in response to any key press:
```python title="widgets02.py"
--8<-- "docs/examples/app/widgets02.py"
```
When you first run this you will get a blank screen. Press any key to add the welcome widget. You can even press a key multiple times to add several widgets.
```{.textual path="docs/examples/app/widgets02.py" press="a,a,a,down,down,down,down,down,down,_,_,_,_,_,_"}
```
### Exiting
An app will run until you call [App.exit()][textual.app.App.exit] which will exit application mode and the [run][textual.app.App.run] method will return. If this is the last line in your code you will return to the command prompt.
The exit method will also accept an optional positional value to be returned by `run()`. The following example uses this to return the `id` (identifier) of a clicked button.
```python title="question01.py"
--8<-- "docs/examples/app/question01.py"
```
Running this app will give you the following:
```{.textual path="docs/examples/app/question01.py"}
```
Clicking either of those buttons will exit the app, and the `run()` method will return either `"yes"` or `"no"` depending on button clicked.
#### Return type
You may have noticed that we subclassed `App[str]` rather than the usual `App`.
```python title="question01.py" hl_lines="5"
--8<-- "docs/examples/app/question01.py"
```
The addition of `[str]` tells Mypy that `run()` is expected to return a string. It may also return `None` if [App.exit()][textual.app.App.exit] is called without a return value, so the return type of `run` will be `str | None`.
You can change the type to match the values you intend to pass to App.exit()][textual.app.App.exit].
!!! note
Type annotations are entirely optional (but recommended) with Textual.
## CSS
Textual apps can reference [CSS](CSS.md) files which define how your app and widgets will look, while keeping your Python code free of display related code (which tends to be messy).
The following chapter on [Textual CSS](CSS.md) will describe how to use CSS in detail. For now lets look at how your app references external CSS files.
The following example sets the `css_path` attribute on the app:
```python title="question02.py" hl_lines="15"
--8<-- "docs/examples/app/question02.py"
```
If the path is relative (as it is above) then it is taken as relative to where the app is defined. Hence this example references `"question01.css"` in the same directory as the Python code. Here is that CSS file:
```sass title="question02.css"
--8<-- "docs/examples/app/question02.css"
```
When `"question02.py"` runs it will load `"question02.css"` and update the app and widgets accordingly. Even though the code is almost identical to the previous sample, the app now looks quite different:
```{.textual path="docs/examples/app/question02.py"}
```
### Classvar CSS
While external CSS files are recommended for most applications, and enable some cool features like *live editing* (see below), you can also specify the CSS directly within the Python code. To do this you can set the `CSS` class variable on the app which contains the CSS content.
Here's the question app with classvar CSS:
```python title="question03.py" hl_lines="6-24"
--8<-- "docs/examples/app/question03.py"
```

View File

@@ -1,5 +1,13 @@
# Devtools # Devtools
!!! note inline end
If you don't have the `textual` command on your path, you may have forgotten so install with the `dev` switch.
See [getting started](../getting_started.md#installation) for details.
Textual comes with a command line application of the same name. The `textual` command is a super useful tool that will help you to build apps. Textual comes with a command line application of the same name. The `textual` command is a super useful tool that will help you to build apps.
Take a moment to look through the available sub-commands. There will be even more helpful tools here in the future. Take a moment to look through the available sub-commands. There will be even more helpful tools here in the future.
@@ -8,6 +16,7 @@ Take a moment to look through the available sub-commands. There will be even mor
textual --help textual --help
``` ```
## Run ## Run
You can run Textual apps with the `run` subcommand. If you supply a path to a Python file it will load and run the application. You can run Textual apps with the `run` subcommand. If you supply a path to a Python file it will load and run the application.
@@ -18,7 +27,7 @@ textual run my_app.py
The `run` sub-command assumes you have an App instance called `app` in the global scope of your Python file. If the application is called something different, you can specify it with a colon following the filename: The `run` sub-command assumes you have an App instance called `app` in the global scope of your Python file. If the application is called something different, you can specify it with a colon following the filename:
``` ```bash
textual run my_app.py:alternative_app textual run my_app.py:alternative_app
``` ```
@@ -26,26 +35,92 @@ textual run my_app.py:alternative_app
If the Python file contains a call to app.run() then you can launch the file as you normally would any other Python program. Running your app via `textual run` will give you access to a few Textual features such as live editing of CSS files. If the Python file contains a call to app.run() then you can launch the file as you normally would any other Python program. Running your app via `textual run` will give you access to a few Textual features such as live editing of CSS files.
## Console
When running any terminal application, you can no longer use `print` when debugging (or log to the console). This is because anything you write to standard output would overwrite application content, making it unreadable. Fortunately Textual supplies a debug console of its own which has some super helpful features. ## Live editing
To use the console, open up 2 terminal emulators. In the first one, run the following: If you combine the `run` command with the `--dev` switch your app will run in *development mode*.
```bash
textual console
```
This should look something like the following:
```{.textual title="textual console" path="docs/examples/getting_started/console.py", press="_,_"}
```
In the other console, run your application using `textual run` and the `--dev` switch:
```bash ```bash
textual run --dev my_app.py textual run --dev my_app.py
``` ```
Anything you `print` from your application will be displayed in the console window. You can also call the [`log()`][textual.message_pump.MessagePump.log] method on App and Widget objects for advanced formatting. Try it with `self.log(self.tree)`. One of the the features of *dev* mode is live editing of CSS files: any changes to your CSS will be reflected in the terminal a few milliseconds later.
This is a great feature for iterating on your app's look and feel. Open the CSS in your editor and have your app running in a terminal. Edits to your CSS will appear almost immediately after you save.
## Console
When building a typical terminal application you are generally unable to use `print` when debugging (or log to the console). This is because anything you write to standard output will overwrite application content. Textual has a solution to this in the form of a debug console which restores `print` and adds a few additional features to help you debug.
To use the console, open up **two** terminal emulators. Run the following in one of the terminals:
```bash
textual console
```
You should see the Textual devtools welcome message:
```{.textual title="textual console" path="docs/examples/getting_started/console.py", press="_,_"}
```
In the other console, run your application with `textual run` and the `--dev` switch:
```bash
textual run --dev my_app.py
```
Anything you `print` from your application will be displayed in the console window. Textual will also write log messages to this window which may be helpful when debugging your application.
### Verbosity
Textual writes log messages to inform you about certain events, such as when the user presses a key or clicks on the terminal. To avoid swamping you with too much information, some events are marked as "verbose" and will be excluded from the logs. If you want to see these log messages, you can add the `-v` switch.
```bash
textual console -v
```
## Textual log
In addition to simple strings, Textual console supports [Rich](https://rich.readthedocs.io/en/latest/) formatting. To write rich logs, import `log` as follows:
```python
from textual import log
```
This method will pretty print data structures (like lists and dicts) as well as [Rich renderables](https://rich.readthedocs.io/en/stable/protocol.html). Here are some examples:
```python
log("Hello, World") # simple string
log(locals()) # Log local variables
log(children=self.children, pi=3.141592) # key/values
log(self.tree) # Rich renderables
```
Textual log messages may contain [console Markup](https://rich.readthedocs.io/en/stable/markup.html):
```python
log("[bold red]DANGER![/] We're having too much fun")
```
### Log method
There's a convenient shortcut to `log` available on the App and Widget objects. This is useful in event handlers. Here's an example:
```python
from textual.app import App
class LogApp(App):
def on_load(self):
self.log("In the log handler!", pi=3.141529)
def on_mount(self):
self.log(self.tree)
app = LogApp()
if __name__ == "__main__":
app.run()
```

View File

@@ -1,5 +1,13 @@
## Events ## Events
TODO: events docs
- What are events
- Handling events
- Auto calling base classes
- Event bubbling
- Posting / emitting events
<div class="excalidraw"> <div class="excalidraw">
--8<-- "docs/images/test.excalidraw.svg" --8<-- "docs/images/test.excalidraw.svg"
</div> </div>

9
docs/guide/index.md Normal file
View File

@@ -0,0 +1,9 @@
# Textual Guide
Welcome to the Textual Guide! An in-depth reference on how to build app with Textual.
## Example code
Most of the code in this guide is fully working&mdash;you could cut and paste it if you wanted to.
Although it is probably easier to check out the [Textual repository](https://github.com/Textualize/textual) and navigate to the `docs/examples/guide` directory and run the examples from there.

View File

@@ -0,0 +1,78 @@
# Layout
In textual the *layout* defines how widgets will be arranged (or *layed out*) on the screen. Textual supports a number of layouts which can be set either via a widgets `styles` object or via CSS.
TODO: layout docs
## Vertical
A vertical layout will place new widgets below previous widgets, starting from the top of the screen.
<div class="excalidraw">
--8<-- "docs/images/layout/vertical.excalidraw.svg"
</div>
TODO: Explanation of vertical layout
## Horizontal
A horizontal layout will place the first widget at the top left of the screen, and new widgets will be place directly to the right of the previous widget.
<div class="excalidraw">
--8<-- "docs/images/layout/horizontal.excalidraw.svg"
</div>
TODO: Explantion of horizontal layout
## Center
A center widget will place the widget directly in the center of the screen. New widgets will also be placed in the center of the screen, overlapping previous widgets.
There probably isn't a practical use for such overlapping widgets. In practice this layout is probably only useful where you have a single child widget.
<div class="excalidraw">
--8<-- "docs/images/layout/center.excalidraw.svg"
</div>
TODO: Explanation of center layout
## Grid
A grid layout arranges widgets within a grid composed of columns and rows. Widgets can span multiple rows or columns to create more complex layouts.
<div class="excalidraw">
--8<-- "docs/images/layout/grid.excalidraw.svg"
</div>
TODO: Explanation of grid layout
## Docking
Widgets may be *docked*. Docking a widget removes it from the layout and fixes it position, aligned to either the top, right, bottom, or left edges of the screen. Docked widgets will not scroll, making them ideal for fixed headers / footers / sidebars.
<div class="excalidraw">
--8<-- "docs/images/layout/dock.excalidraw.svg"
</div>
TODO: Diagram
TODO: Explanation of dock
## Offsets
Widgets have a relative offset which is added to the widget's location, after its location has been determined via its layout.
<div class="excalidraw">
--8<-- "docs/images/layout/offset.excalidraw.svg"
</div>
TODO: Diagram
TODO: Offsets

10
docs/guide/reactivity.md Normal file
View File

@@ -0,0 +1,10 @@
# Reactivity
TODO: Reactivity docs
- What is reactivity
- Reactive variables
- Demo
- repaint vs layout
- Validation
- Watch methods

12
docs/guide/screens.md Normal file
View File

@@ -0,0 +1,12 @@
# Screens
TODO: Screens docs
- Explanation of screens
- Screens API
- Install screen
- Uninstall screen
- Push screen
- Pop screen
- Switch Screen
- Screens example

15
docs/guide/styles.md Normal file
View File

@@ -0,0 +1,15 @@
# Styles
TODO: Styles docs
- What are styles
- Styles object on widgets / app
- Setting styles via CSS
- Box model
- Color / Background
- Borders / Outline
<div class="excalidraw">
--8<-- "docs/images/styles/box.excalidraw.svg"
</div>

11
docs/guide/widgets.md Normal file
View File

@@ -0,0 +1,11 @@
# Widgets
TODO: Widgets docs
- What is a widget
- Defining a basic widget
- Base classes Widget or Static
- Text widgets
- Rich renderable widgets
- Complete widget
- Render line widget API

1
docs/how-to/animation.md Normal file
View File

@@ -0,0 +1 @@
# Animation

3
docs/how-to/index.md Normal file
View File

@@ -0,0 +1,3 @@
# How to ...
For those who want more focused information on Textual features.

View File

@@ -0,0 +1 @@
# Mouse and Keyboard

1
docs/how-to/scroll.md Normal file
View File

@@ -0,0 +1 @@
# Scroll

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 21 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 55 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 46 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 36 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 28 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 44 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -4,7 +4,7 @@ Welcome to the [Textual](https://github.com/Textualize/textual) framework docume
<hr> <hr>
Textual is a framework for building applications that run within your terminal. Such Text User Interfaces (TUIs) have a number of advantages over traditional web and desktop apps. Textual is a framework for building applications that run within your terminal. Text User Interfaces (TUIs) have a number of advantages over web and desktop apps.
<div class="grid cards" markdown> <div class="grid cards" markdown>
@@ -59,18 +59,10 @@ Textual is a framework for building applications that run within your terminal.
<hr> <hr>
```{.textual path="docs/examples/demo.py" columns=100 lines=48}
<!-- TODO: More examples split in to tabs --> ```
=== "Example 1" TODO: Add more example screenshots
```{.textual path="docs/examples/demo.py" columns=100 lines=48}
```
=== "Example 2"
```{.textual path="docs/examples/introduction/timers.py"}
```

1
docs/reference/button.md Normal file
View File

@@ -0,0 +1 @@
::: textual.widgets.Button

3
docs/reference/index.md Normal file
View File

@@ -0,0 +1,3 @@
# Reference
A reference to the Textual public APIs.

View File

@@ -0,0 +1 @@
::: textual.reactive.Reactive

3
docs/styles/index.md Normal file
View File

@@ -0,0 +1,3 @@
# Styles
A reference to Widget [styles](../guide/styles.md).

View File

@@ -2,19 +2,22 @@
The `layout` property defines how a widget arranges its children. The `layout` property defines how a widget arranges its children.
See [layout](../guide/layout.md) guide for more information.
## Syntax ## Syntax
``` ```
layout: [vertical|horizontal|center]; layout: [center|grid|horizontal|vertical];
``` ```
### Values ### Values
| Value | Description | | Value | Description |
|----------------------|-------------------------------------------------------------------------------| |----------------------|-------------------------------------------------------------------------------|
| `vertical` (default) | Child widgets will be arranged along the vertical axis, from top to bottom. |
| `horizontal` | Child widgets will be arranged along the horizontal axis, from left to right. |
| `center` | A single child widget will be placed in the center. | | `center` | A single child widget will be placed in the center. |
| `grid` | Child widgets will be arranged in a grid. |
| `horizontal` | Child widgets will be arranged along the horizontal axis, from left to right. |
| `vertical` (default) | Child widgets will be arranged along the vertical axis, from top to bottom. |
## Example ## Example

View File

@@ -1,31 +1,31 @@
# Introduction # Tutorial
Welcome to the Textual Introduction! Welcome to the Textual Tutorial!
By the end of this page you should have a solid understanding of app development with Textual. By the end of this page you should have a solid understanding of app development with Textual.
!!! quote !!! quote
This page goes in to more detail than you may expect from an introduction. I like documentation to have complete working examples and I wanted the first app to be realistic. I've always thought the secret sauce in making a popular framework is for it to be fun.
&mdash; **Will McGugan** (creator of Rich and Textual) &mdash; **Will McGugan** (creator of Rich and Textual)
## Stopwatch Application ## Stopwatch Application
We're going to build a stopwatch application. It should show a list of stopwatches with a time display the user can start, stop, and reset. We also want the user to be able to add and remove stopwatches as required. We're going to build a stopwatch application. This application should show a list of stopwatches with a time display the user can start, stop, and reset. We also want the user to be able to add and remove stopwatches as required.
This will be a simple yet **fully featured** app &mdash; you could distribute this app if you wanted to! This will be a simple yet **fully featured** app &mdash; you could distribute this app if you wanted to!
Here's what the finished app will look like: Here's what the finished app will look like:
```{.textual path="docs/examples/introduction/stopwatch.py" press="tab,enter,_,tab,enter,_,tab,_,enter,_,tab,enter,_,_"} ```{.textual path="docs/examples/tutorial/stopwatch.py" press="tab,enter,_,tab,enter,_,tab,_,enter,_,tab,enter,_,_"}
``` ```
### Get the code ### Get the code
If you want to try the finished Stopwatch app and follow along with the code, first make sure you have [Textual installed](getting_started.md) and then check out the [Textual](https://github.com/Textualize/textual) GitHub repository: If you want to try the finished Stopwatch app and follow along with the code, first make sure you have [Textual installed](getting_started.md) then check out the [Textual](https://github.com/Textualize/textual) repository:
=== "HTTPS" === "HTTPS"
@@ -45,10 +45,10 @@ If you want to try the finished Stopwatch app and follow along with the code, fi
gh repo clone Textualize/textual gh repo clone Textualize/textual
``` ```
With the repository cloned, navigate to `docs/examples/introduction` and run `stopwatch.py`. With the repository cloned, navigate to `docs/examples/tutorial` and run `stopwatch.py`.
```bash ```bash
cd textual/docs/examples/introduction cd textual/docs/examples/tutorial
python stopwatch.py python stopwatch.py
``` ```
@@ -58,7 +58,7 @@ python stopwatch.py
Type hints are entirely optional in Textual. We've included them in the example code but it's up to you whether you add them to your own projects. Type hints are entirely optional in Textual. We've included them in the example code but it's up to you whether you add them to your own projects.
We're a big fan of Python type hints at Textualize. If you haven't encountered type hinting, it's a way to express the types of your data, parameters, and return values. Type hinting allows tools like [Mypy](https://mypy.readthedocs.io/en/stable/) to catch potential bugs before your code runs. We're a big fan of Python type hints at Textualize. If you haven't encountered type hinting, it's a way to express the types of your data, parameters, and return values. Type hinting allows tools like [Mypy](https://mypy.readthedocs.io/en/stable/) to catch bugs before your code runs.
The following function contains type hints: The following function contains type hints:
@@ -68,8 +68,9 @@ def repeat(text: str, count: int) -> str:
return text * count return text * count
``` ```
- Parameter types follow a colon. So `text: str` indicates that `text` requires a string and `count: int` means that `count` requires an integer. Parameter types follow a colon. So `text: str` indicates that `text` requires a string and `count: int` means that `count` requires an integer.
- Return types follow `->`. So `-> str:` indicates this method returns a string.
Return types follow `->`. So `-> str:` indicates this method returns a string.
## The App class ## The App class
@@ -77,18 +78,18 @@ def repeat(text: str, count: int) -> str:
The first step in building a Textual app is to import and extend the `App` class. Here's our basic app class with a few methods we will cover below. The first step in building a Textual app is to import and extend the `App` class. Here's our basic app class with a few methods we will cover below.
```python title="stopwatch01.py" ```python title="stopwatch01.py"
--8<-- "docs/examples/introduction/stopwatch01.py" --8<-- "docs/examples/tutorial/stopwatch01.py"
``` ```
If you run this code, you should see something like the following: If you run this code, you should see something like the following:
```{.textual path="docs/examples/introduction/stopwatch01.py"} ```{.textual path="docs/examples/tutorial/stopwatch01.py"}
``` ```
Hit the ++d++ key to toggle dark mode. Hit the ++d++ key to toggle dark mode.
```{.textual path="docs/examples/introduction/stopwatch01.py" press="d" title="TimerApp + dark"} ```{.textual path="docs/examples/tutorial/stopwatch01.py" press="d" title="TimerApp + dark"}
``` ```
Hit ++ctrl+c++ to exit the app and return to the command prompt. Hit ++ctrl+c++ to exit the app and return to the command prompt.
@@ -98,27 +99,27 @@ Hit ++ctrl+c++ to exit the app and return to the command prompt.
Let's examine stopwatch01.py in more detail. Let's examine stopwatch01.py in more detail.
```python title="stopwatch01.py" hl_lines="1 2" ```python title="stopwatch01.py" hl_lines="1 2"
--8<-- "docs/examples/introduction/stopwatch01.py" --8<-- "docs/examples/tutorial/stopwatch01.py"
``` ```
The first line imports the Textual `App` class. The second line imports two builtin widgets: `Footer` which shows available keys and `Header` which shows a title and the current time. The first line imports the Textual `App` class. The second line imports two builtin widgets: `Footer` which shows available keys and `Header` which shows a title and the current time.
Widgets are re-usable components responsible for managing a part of the screen. We will cover how to build such widgets in this introduction. Widgets are re-usable components responsible for managing a part of the screen. We will cover how to build such widgets in this tutorial.
```python title="stopwatch01.py" hl_lines="5-19" ```python title="stopwatch01.py" hl_lines="5-19"
--8<-- "docs/examples/introduction/stopwatch01.py" --8<-- "docs/examples/tutorial/stopwatch01.py"
``` ```
The App class is where most of the logic of Textual apps is written. It is responsible for loading configuration, setting up widgets, handling keys, and more. The App class is where most of the logic of Textual apps is written. It is responsible for loading configuration, setting up widgets, handling keys, and more.
Currently, there are three methods in our stopwatch app. Currently, there are three methods in our stopwatch app.
- **`compose()`** is where we construct a user interface with widgets. The `compose()` method may return a list of widgets, but it is generally easier to _yield_ them (making this method a generator). In the example code we yield instances of the widget classes we imported, i.e. the header and the footer. - `compose()` is where we construct a user interface with widgets. The `compose()` method may return a list of widgets, but it is generally easier to _yield_ them (making this method a generator). In the example code we yield instances of the widget classes we imported, i.e. the header and the footer.
- **`on_load()`** is an _event handler_ method. Event handlers are called by Textual in response to external events like keys and mouse movements, and internal events needed to manage your application. Event handler methods begin with `on_` followed by the name of the event (in lower case). Hence, `on_load` is called in response to the Load event which is sent just after the app starts. We're using this event to call `App.bind()` which connects a key to an _action_. - `on_load()` is an _event handler_ method. Event handlers are called by Textual in response to external events like keys and mouse movements, and internal events needed to manage your application. Event handler methods begin with `on_` followed by the name of the event (in lower case). Hence, `on_load` is called in response to the Load event which is sent just after the app starts. We're using this event to call `App.bind()` which connects a key to an _action_.
- **`action_toggle_dark()`** defines an _action_ method. Actions are methods beginning with `action_` followed by the name of the action. The call to `bind()` in `on_load()` binds this the ++d++ key to this action. The body of this method flips the state of the `dark` Boolean to toggle dark mode. - `action_toggle_dark()` defines an _action_ method. Actions are methods beginning with `action_` followed by the name of the action. The call to `bind()` in `on_load()` binds this the ++d++ key to this action. The body of this method flips the state of the `dark` Boolean to toggle dark mode.
!!! note !!! note
@@ -126,7 +127,7 @@ Currently, there are three methods in our stopwatch app.
```python title="stopwatch01.py" hl_lines="22-24" ```python title="stopwatch01.py" hl_lines="22-24"
--8<-- "docs/examples/introduction/stopwatch01.py" --8<-- "docs/examples/tutorial/stopwatch01.py"
``` ```
The last few lines create an instance of the app at the module scope. Followed by a call to `run()` within a `__name__ == "__main__"` block. This is so that we could import `app` if we want to. Or we could run it with `python stopwatch01.py`. The last few lines create an instance of the app at the module scope. Followed by a call to `run()` within a `__name__ == "__main__"` block. This is so that we could import `app` if we want to. Or we could run it with `python stopwatch01.py`.
@@ -153,7 +154,7 @@ Textual has a builtin `Button` widget which takes care of the first three compon
Let's add those to the app. Just a skeleton for now, we will add the rest of the features as we go. Let's add those to the app. Just a skeleton for now, we will add the rest of the features as we go.
```python title="stopwatch02.py" hl_lines="3 6-7 10-18 28" ```python title="stopwatch02.py" hl_lines="3 6-7 10-18 28"
--8<-- "docs/examples/introduction/stopwatch02.py" --8<-- "docs/examples/tutorial/stopwatch02.py"
``` ```
### Extending widget classes ### Extending widget classes
@@ -180,7 +181,7 @@ The new line in `Stopwatch.compose()` yields a single `Container` object which w
Let's see what happens when we run "stopwatch02.py". Let's see what happens when we run "stopwatch02.py".
```{.textual path="docs/examples/introduction/stopwatch02.py" title="stopwatch02.py"} ```{.textual path="docs/examples/tutorial/stopwatch02.py" title="stopwatch02.py"}
``` ```
The elements of the stopwatch application are there. The buttons are clickable and you can scroll the container but it doesn't look like the sketch. This is because we have yet to apply any _styles_ to our new widgets. The elements of the stopwatch application are there. The buttons are clickable and you can scroll the container but it doesn't look like the sketch. This is because we have yet to apply any _styles_ to our new widgets.
@@ -204,18 +205,18 @@ While it's possible to set all styles for an app this way, it is rarely necessar
Let's add a CSS file to our application. Let's add a CSS file to our application.
```python title="stopwatch03.py" hl_lines="39" ```python title="stopwatch03.py" hl_lines="39"
--8<-- "docs/examples/introduction/stopwatch03.py" --8<-- "docs/examples/tutorial/stopwatch03.py"
``` ```
Adding the `css_path` attribute to the app constructor tells Textual to load the following file when it starts the app: Adding the `css_path` attribute to the app constructor tells Textual to load the following file when it starts the app:
```sass title="stopwatch03.css" ```sass title="stopwatch03.css"
--8<-- "docs/examples/introduction/stopwatch03.css" --8<-- "docs/examples/tutorial/stopwatch03.css"
``` ```
If we run the app now, it will look *very* different. If we run the app now, it will look *very* different.
```{.textual path="docs/examples/introduction/stopwatch03.py" title="stopwatch03.py"} ```{.textual path="docs/examples/tutorial/stopwatch03.py" title="stopwatch03.py"}
``` ```
This app looks much more like our sketch. Textual has read style information from `stopwatch03.css` and applied it to the widgets. This app looks much more like our sketch. Textual has read style information from `stopwatch03.css` and applied it to the widgets.
@@ -295,7 +296,7 @@ We can accomplish this with a CSS _class_. Not to be confused with a Python clas
Here's the new CSS: Here's the new CSS:
```sass title="stopwatch04.css" hl_lines="33-53" ```sass title="stopwatch04.css" hl_lines="33-53"
--8<-- "docs/examples/introduction/stopwatch04.css" --8<-- "docs/examples/tutorial/stopwatch04.css"
``` ```
These new rules are prefixed with `.started`. The `.` indicates that `.started` refers to a CSS class called "started". The new styles will be applied only to widgets that have this CSS class. These new rules are prefixed with `.started`. The `.` indicates that `.started` refers to a CSS class called "started". The new styles will be applied only to widgets that have this CSS class.
@@ -323,24 +324,24 @@ You can add and remove CSS classes with the `add_class()` and `remove_class()` m
The following code adds an event handler for the `Button.Pressed` event. The following code adds an event handler for the `Button.Pressed` event.
```python title="stopwatch04.py" hl_lines="13-18" ```python title="stopwatch04.py" hl_lines="13-18"
--8<-- "docs/examples/introduction/stopwatch04.py" --8<-- "docs/examples/tutorial/stopwatch04.py"
``` ```
The `on_button_pressed` event handler is called when the user clicks a button. This method adds the "started" class when the "start" button was clicked, and removes the class when the "stop" button is clicked. The `on_button_pressed` event handler is called when the user clicks a button. This method adds the "started" class when the "start" button was clicked, and removes the class when the "stop" button is clicked.
If you run "stopwatch04.py" now you will be able to toggle between the two states by clicking the first button: If you run "stopwatch04.py" now you will be able to toggle between the two states by clicking the first button:
```{.textual path="docs/examples/introduction/stopwatch04.py" title="stopwatch04.py" press="tab,tab,tab,enter"} ```{.textual path="docs/examples/tutorial/stopwatch04.py" title="stopwatch04.py" press="tab,tab,tab,enter"}
``` ```
## Reactive attributes ## Reactive attributes
A recurring theme in Textual is that you rarely need to explicitly update a widget. It is possible: you can call [`refresh()`][textual.widget.Widget.refresh] to display new data. However, Textual prefers to do this automatically via _reactive_ attributes. A recurring theme in Textual is that you rarely need to explicitly update a widget. It is possible: you can call [`refresh()`][textual.widget.Widget.refresh] to display new data. However, Textual prefers to do this automatically via _reactive_ attributes.
You can declare a reactive attribute with `textual.reactive.Reactive`. Let's use this feature to create a timer that displays elapsed time and keeps it updated. You can declare a reactive attribute with [Reactive][textual.reactive.Reactive]. Let's use this feature to create a timer that displays elapsed time and keeps it updated.
```python title="stopwatch04.py" hl_lines="1 5 12-27" ```python title="stopwatch04.py" hl_lines="1 5 12-27"
--8<-- "docs/examples/introduction/stopwatch05.py" --8<-- "docs/examples/tutorial/stopwatch05.py"
``` ```
We have added two reactive attributes: `start_time` will contain the time in seconds when the stopwatch was started, and `time` will contain the time to be displayed on the Stopwatch. We have added two reactive attributes: `start_time` will contain the time in seconds when the stopwatch was started, and `time` will contain the time to be displayed on the Stopwatch.
@@ -368,7 +369,7 @@ Because `watch_time` watches the `time` attribute, when we update `self.time` 60
The end result is that the `Stopwatch` widgets show the time elapsed since the widget was created: The end result is that the `Stopwatch` widgets show the time elapsed since the widget was created:
```{.textual path="docs/examples/introduction/stopwatch05.py" title="stopwatch05.py"} ```{.textual path="docs/examples/tutorial/stopwatch05.py" title="stopwatch05.py"}
``` ```
We've seen how we can update widgets with a timer, but we still need to wire up the buttons so we can operate Stopwatches independently. We've seen how we can update widgets with a timer, but we still need to wire up the buttons so we can operate Stopwatches independently.
@@ -379,7 +380,7 @@ We need to be able to start, stop, and reset each stopwatch independently. We ca
```python title="stopwatch06.py" hl_lines="14-44 50-61" ```python title="stopwatch06.py" hl_lines="14-44 50-61"
--8<-- "docs/examples/introduction/stopwatch06.py" --8<-- "docs/examples/tutorial/stopwatch06.py"
``` ```
Here's a summary of the changes made to `TimeDisplay`. Here's a summary of the changes made to `TimeDisplay`.
@@ -415,7 +416,7 @@ This code supplies missing features and makes our app useful. We've made the fol
If you run stopwatch06.py you will be able to use the stopwatches independently. If you run stopwatch06.py you will be able to use the stopwatches independently.
```{.textual path="docs/examples/introduction/stopwatch06.py" title="stopwatch06.py" press="tab,enter,_,_,tab,enter,_,tab"} ```{.textual path="docs/examples/tutorial/stopwatch06.py" title="stopwatch06.py" press="tab,enter,_,_,tab,enter,_,tab"}
``` ```
The only remaining feature of the Stopwatch app left to implement is the ability to add and remove timers. The only remaining feature of the Stopwatch app left to implement is the ability to add and remove timers.
@@ -429,7 +430,7 @@ To add a new child widget call `mount()` on the parent. To remove a widget, call
Let's use these to implement adding and removing stopwatches to our app. Let's use these to implement adding and removing stopwatches to our app.
```python title="stopwatch.py" hl_lines="83-84 86-90 92-96" ```python title="stopwatch.py" hl_lines="83-84 86-90 92-96"
--8<-- "docs/examples/introduction/stopwatch.py" --8<-- "docs/examples/tutorial/stopwatch.py"
``` ```
We've added two new actions: `action_add_stopwatch` to add a new stopwatch, and `action_remove_stopwatch` to remove the last stopwatch. The `on_load` handler binds these actions to the ++a++ and ++r++ keys. We've added two new actions: `action_add_stopwatch` to add a new stopwatch, and `action_remove_stopwatch` to remove the last stopwatch. The `on_load` handler binds these actions to the ++a++ and ++r++ keys.
@@ -440,11 +441,11 @@ The `action_remove_stopwatch` calls `query` with a CSS selector of `"Stopwatch"`
If you run `stopwatch.py` now you can add a new stopwatch with the ++a++ key and remove a stopwatch with ++r++. If you run `stopwatch.py` now you can add a new stopwatch with the ++a++ key and remove a stopwatch with ++r++.
```{.textual path="docs/examples/introduction/stopwatch.py" press="d,a,a,a,a,a,a,a,tab,enter,_,_,_,_,tab,_"} ```{.textual path="docs/examples/tutorial/stopwatch.py" press="d,a,a,a,a,a,a,a,tab,enter,_,_,_,_,tab,_"}
``` ```
## What next? ## What next?
Congratulations on building your first Textual application! This introduction has covered a lot of ground. If you are the type that prefers to learn a framework by coding, feel free. You could tweak stopwatch.py or look through the examples. Congratulations on building your first Textual application! This tutorial has covered a lot of ground. If you are the type that prefers to learn a framework by coding, feel free. You could tweak stopwatch.py or look through the examples.
Read the guide for the full details on how to build sophisticated TUI applications with Textual. Read the guide for the full details on how to build sophisticated TUI applications with Textual.

View File

@@ -1,42 +0,0 @@
# Messages & Events
Each component of a Textual application has it its heart a queue of messages and a task which monitors this queue and calls Python code in response. The queue and task are collectively known as a _message pump_.
You will most often deal with _events_ which are a particular type of message that are created in response to user actions, such as key presses and mouse clicks, but also internal events such as timers. These events typically originate from a Driver class which sends them to an App class which is where you write code to respond to those events.
Lets write an _app_ which responds to a key event. This is probably the simplest Textual application that I can conceive of:
```python
from textual.app import App
class Beeper(App):
async def on_key(self, event):
self.console.bell()
Beeper.run()
```
If you run the above code, Textual will switch the terminal in to _application mode_. The terminal will go blank and the app will start processing events. If you hit any key you should hear a beep. Hit ctrl+C (control key and C key at the same time) to exit application mode and return to the terminal.
Although simple, this app follows the same pattern as more sophisticated applications. It starts by deriving a class from `App`; in this case `Beeper`. Calling the classmethod `run()` starts the application.
In our Beeper class there is a single event handler `on_key` which is called in response to a `Key` event. The method name is assumed by concatenating `on_` with the event name, hence `on_key` for a Key event, `on_timer` for a Timer event, etc. In Beeper, the on_key event calls `self.console.bell()` which is what plays the beep noise (if supported by your terminal).
The `on_key` method is preceded by the keyword `async` making it an asynchronous method. Textual is an asynchronous framework so event handlers and most methods are async.
Our Beeper app is missing typing information. Although completely optional, I recommend adding typing information which will help catch bugs (using tools such as [Mypy](https://mypy.readthedocs.io/en/stable/)). Here is the Beeper class with added typing:
```python
from textual.app import App
from textual import events
class Beeper(App):
async def on_key(self, event: events.Key) -> None:
self.console.bell()
Beeper.run()
```

1
docs/widgets/button.md Normal file
View File

@@ -0,0 +1 @@
# Button

View File

@@ -0,0 +1 @@
# DataTable

1
docs/widgets/footer.md Normal file
View File

@@ -0,0 +1 @@
# Footer

1
docs/widgets/header.md Normal file
View File

@@ -0,0 +1 @@
# Header

3
docs/widgets/index.md Normal file
View File

@@ -0,0 +1,3 @@
# Widgets
A reference to the builtin [widgets](../guide/widgets.md).

1
docs/widgets/static.md Normal file
View File

@@ -0,0 +1 @@
# Static

View File

@@ -1 +0,0 @@
::: textual.widgets.tabs.Tabs

View File

@@ -0,0 +1 @@
# TextInput

View File

@@ -0,0 +1 @@
# TreeControl

View File

@@ -3,11 +3,11 @@ Screen {
} }
#calculator { #calculator {
layout: table; layout: grid;
table-size: 4; grid-size: 4;
table-gutter: 1 2; grid-gutter: 1 2;
table-columns: 1fr; grid-columns: 1fr;
table-rows: 2fr 1fr 1fr 1fr 1fr 1fr; grid-rows: 2fr 1fr 1fr 1fr 1fr 1fr;
margin: 1 2; margin: 1 2;
min-height:25; min-height:25;
min-width: 26; min-width: 26;

View File

@@ -4,19 +4,31 @@ site_url: https://www.textualize.io/
nav: nav:
- "index.md" - "index.md"
- "getting_started.md" - "getting_started.md"
- "introduction.md" - "tutorial.md"
- Guide: - Guide:
- "guide/index.md"
- "guide/devtools.md" - "guide/devtools.md"
- "guide/app.md"
- "guide/styles.md"
- "guide/CSS.md" - "guide/CSS.md"
- "guide/layout.md"
- "guide/events.md" - "guide/events.md"
- "guide/actions.md"
- "actions.md" - "guide/reactivity.md"
- "guide/widgets.md"
- "guide/animator.md"
- "guide/screens.md"
- How to:
- "how-to/index.md"
- "how-to/animation.md"
- "how-to/mouse-and-keyboard.md"
- "how-to/scroll.md"
- Events: - Events:
- "events/index.md"
- "events/blur.md" - "events/blur.md"
- "events/descendant_blur.md" - "events/descendant_blur.md"
- "events/descendant_focus.md" - "events/descendant_focus.md"
- "events/enter.md" - "events/enter.md"
- "events/enter.md"
- "events/focus.md" - "events/focus.md"
- "events/hide.md" - "events/hide.md"
- "events/key.md" - "events/key.md"
@@ -37,6 +49,7 @@ nav:
- "events/screen_suspend.md" - "events/screen_suspend.md"
- "events/show.md" - "events/show.md"
- Styles: - Styles:
- "styles/index.md"
- "styles/background.md" - "styles/background.md"
- "styles/border.md" - "styles/border.md"
- "styles/box_sizing.md" - "styles/box_sizing.md"
@@ -64,9 +77,19 @@ nav:
- "styles/tint.md" - "styles/tint.md"
- "styles/visibility.md" - "styles/visibility.md"
- "styles/width.md" - "styles/width.md"
- Widgets: "/widgets/" - Widgets:
- "widgets/index.md"
- "widgets/button.md"
- "widgets/data_table.md"
- "widgets/footer.md"
- "widgets/header.md"
- "widgets/static.md"
- "widgets/text_input.md"
- "widgets/tree_control.md"
- Reference: - Reference:
- "reference/index.md"
- "reference/app.md" - "reference/app.md"
- "reference/button.md"
- "reference/color.md" - "reference/color.md"
- "reference/dom_node.md" - "reference/dom_node.md"
- "reference/events.md" - "reference/events.md"
@@ -94,6 +117,7 @@ markdown_extensions:
custom_checkbox: true custom_checkbox: true
- pymdownx.highlight: - pymdownx.highlight:
anchor_linenums: true anchor_linenums: true
- pymdownx.inlinehilite
- pymdownx.superfences: - pymdownx.superfences:
custom_fences: custom_fences:
- name: textual - name: textual
@@ -110,8 +134,9 @@ markdown_extensions:
theme: theme:
name: material name: material
custom_dir: custom_theme custom_dir: custom_theme
# features: features:
# - navigation.tabs - navigation.tabs
- navigation.indexes
palette: palette:
- media: "(prefers-color-scheme: light)" - media: "(prefers-color-scheme: light)"
scheme: default scheme: default

View File

@@ -35,4 +35,4 @@ class SmoothApp(App):
# self.set_timer(10, lambda: self.action("quit")) # self.set_timer(10, lambda: self.action("quit"))
SmoothApp.run(log_path="textual.log", log_verbosity=2) SmoothApp.run(log_path="textual.log")

2
poetry.lock generated
View File

@@ -345,7 +345,7 @@ mkdocs = ">=1.1"
[[package]] [[package]]
name = "mkdocs-material" name = "mkdocs-material"
version = "8.4.1" version = "8.4.2"
description = "Documentation that simply works" description = "Documentation that simply works"
category = "dev" category = "dev"
optional = false optional = false

View File

@@ -8,7 +8,7 @@ from textual.widgets import Placeholder
class VerticalContainer(Widget): class VerticalContainer(Widget):
CSS = """ DEFAULT_CSS = """
VerticalContainer { VerticalContainer {
layout: vertical; layout: vertical;
overflow: hidden auto; overflow: hidden auto;
@@ -24,7 +24,7 @@ class VerticalContainer(Widget):
class Introduction(Widget): class Introduction(Widget):
CSS = """ DEFAULT_CSS = """
Introduction { Introduction {
background: indigo; background: indigo;
color: white; color: white;

View File

@@ -23,7 +23,9 @@ class ButtonsApp(App[str]):
app = ButtonsApp( app = ButtonsApp(
log_path="textual.log", css_path="buttons.css", watch_css=True, log_verbosity=2 log_path="textual.log",
css_path="buttons.css",
watch_css=True,
) )
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -23,7 +23,7 @@ class ColorDisplay(Widget, can_focus=True):
class ColorNames(App): class ColorNames(App):
CSS = """ DEFAULT_CSS = """
ColorDisplay { ColorDisplay {
height: 1; height: 1;
} }

View File

@@ -23,7 +23,9 @@ class ButtonsApp(App[str]):
app = ButtonsApp( app = ButtonsApp(
log_path="textual.log", css_path="buttons.css", watch_css=True, log_verbosity=2 log_path="textual.log",
css_path="buttons.css",
watch_css=True,
) )
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -62,9 +62,7 @@ class FileSearchApp(App):
self.file_table.filter = event.value self.file_table.filter = event.value
app = FileSearchApp( app = FileSearchApp(css_path="file_search.scss", watch_css=True)
log_path="textual.log", css_path="file_search.scss", watch_css=True, log_verbosity=2
)
if __name__ == "__main__": if __name__ == "__main__":
result = app.run() result = app.run()

View File

@@ -1,6 +1,6 @@
Screen { Screen {
layout: dock;
docks: top=top bottom=bottom;
} }
#file_table_wrapper { #file_table_wrapper {

View File

@@ -5,7 +5,7 @@ from textual.widget import Widget
class FiftyApp(App): class FiftyApp(App):
CSS = """ DEFAULT_CSS = """
Screen { Screen {
layout: vertical; layout: vertical;
} }
@@ -24,6 +24,7 @@ class FiftyApp(App):
yield layout.Horizontal(Widget(), Widget()) yield layout.Horizontal(Widget(), Widget())
yield layout.Horizontal(Widget(), Widget()) yield layout.Horizontal(Widget(), Widget())
app = FiftyApp() app = FiftyApp()
if __name__ == "__main__": if __name__ == "__main__":
app.run() app.run()

View File

@@ -61,9 +61,7 @@ class InputApp(App[str]):
self.celsius.value = f"{celsius:.1f}" self.celsius.value = f"{celsius:.1f}"
app = InputApp( app = InputApp(log_path="textual.log", css_path="input.scss", watch_css=True)
log_path="textual.log", css_path="input.scss", watch_css=True, log_verbosity=2
)
if __name__ == "__main__": if __name__ == "__main__":
result = app.run() result = app.run()

View File

@@ -9,7 +9,7 @@ placeholders_count = 12
class VerticalContainer(Widget): class VerticalContainer(Widget):
CSS = """ DEFAULT_CSS = """
VerticalContainer { VerticalContainer {
layout: vertical; layout: vertical;
overflow: hidden auto; overflow: hidden auto;
@@ -26,7 +26,7 @@ class VerticalContainer(Widget):
class Introduction(Widget): class Introduction(Widget):
CSS = """ DEFAULT_CSS = """
Introduction { Introduction {
background: indigo; background: indigo;
color: white; color: white;

View File

@@ -10,7 +10,7 @@ initial_placeholders_count = 4
class VerticalContainer(Widget): class VerticalContainer(Widget):
CSS = """ DEFAULT_CSS = """
VerticalContainer { VerticalContainer {
layout: vertical; layout: vertical;
overflow: hidden auto; overflow: hidden auto;
@@ -30,7 +30,7 @@ class VerticalContainer(Widget):
class Introduction(Widget): class Introduction(Widget):
CSS = """ DEFAULT_CSS = """
Introduction { Introduction {
background: indigo; background: indigo;
color: white; color: white;

View File

@@ -11,7 +11,7 @@ class Thing(Static):
class AddRemoveApp(App): class AddRemoveApp(App):
CSS = """ DEFAULT_CSS = """
#buttons { #buttons {
dock: top; dock: top;
height: auto; height: auto;

View File

@@ -3,14 +3,14 @@ Screen {
} }
#calculator { #calculator {
layout: table; layout: grid;
table-size: 4; grid-size: 4;
table-gutter: 1 2; grid-gutter: 1 2;
table-columns: 1fr; grid-columns: 1fr;
table-rows: 2fr 1fr 1fr 1fr 1fr 1fr; grid-rows: 2fr 1fr 1fr 1fr 1fr 1fr;
margin: 1 2; margin: 1 2;
min-height:26; min-height:25;
min-width: 50; min-width: 26;
} }
Button { Button {
@@ -18,18 +18,15 @@ Button {
height: 100%; height: 100%;
} }
.display { #numbers {
column-span: 4; column-span: 4;
content-align: right middle; content-align: right middle;
padding: 0 1; padding: 0 1;
height: 100%; height: 100%;
background: $panel-darken-2; background: $primary-lighten-2;
color: $text-primary-lighten-2;
} }
.special { #number-0 {
tint: $text-panel 20%;
}
.zero {
column-span: 2; column-span: 2;
} }

View File

@@ -1,35 +1,143 @@
from textual.app import App from decimal import Decimal
from textual.app import App, ComposeResult
from textual import events
from textual.layout import Container from textual.layout import Container
from textual.reactive import Reactive
from textual.widgets import Button, Static from textual.widgets import Button, Static
class CalculatorApp(App): class CalculatorApp(App):
def compose(self): """A working 'desktop' calculator."""
numbers = Reactive.var("0")
show_ac = Reactive.var(True)
left = Reactive.var(Decimal("0"))
right = Reactive.var(Decimal("0"))
value = Reactive.var("")
operator = Reactive.var("plus")
KEY_MAP = {
"+": "plus",
"-": "minus",
".": "point",
"*": "multiply",
"/": "divide",
"_": "plus-minus",
"%": "percent",
"=": "equals",
}
def watch_numbers(self, value: str) -> None:
"""Called when numbers is updated."""
# Update the Numbers widget
self.query_one("#numbers", Static).update(value)
def compute_show_ac(self) -> bool:
"""Compute switch to show AC or C button"""
return self.value in ("", "0") and self.numbers == "0"
def watch_show_ac(self, show_ac: bool) -> None:
"""Called when show_ac changes."""
self.query_one("#c").display = not show_ac
self.query_one("#ac").display = show_ac
def compose(self) -> ComposeResult:
"""Add our buttons."""
yield Container( yield Container(
Static("0", classes="display"), Static(id="numbers"),
Button("AC", classes="special"), Button("AC", id="ac", variant="primary"),
Button("+/-", classes="special"), Button("C", id="c", variant="primary"),
Button("%", classes="special"), Button("+/-", id="plus-minus", variant="primary"),
Button("÷", variant="warning"), Button("%", id="percent", variant="primary"),
Button("7"), Button("÷", id="divide", variant="warning"),
Button("8"), Button("7", id="number-7"),
Button("9"), Button("8", id="number-8"),
Button("×", variant="warning"), Button("9", id="number-9"),
Button("4"), Button("×", id="multiply", variant="warning"),
Button("5"), Button("4", id="number-4"),
Button("6"), Button("5", id="number-5"),
Button("-", variant="warning"), Button("6", id="number-6"),
Button("1"), Button("-", id="minus", variant="warning"),
Button("2"), Button("1", id="number-1"),
Button("3"), Button("2", id="number-2"),
Button("+", variant="warning"), Button("3", id="number-3"),
Button("0", classes="operator zero"), Button("+", id="plus", variant="warning"),
Button("."), Button("0", id="number-0"),
Button("=", variant="warning"), Button(".", id="point"),
Button("=", id="equals", variant="warning"),
id="calculator", id="calculator",
) )
def on_key(self, event: events.Key) -> None:
"""Called when the user presses a key."""
print(f"KEY {event} was pressed!")
def press(button_id: str) -> None:
self.query_one(f"#{button_id}", Button).press()
self.set_focus(None)
key = event.key
if key.isdecimal():
press(f"number-{key}")
elif key == "c":
press("c")
press("ac")
elif key in self.KEY_MAP:
press(self.KEY_MAP[key])
def on_button_pressed(self, event: Button.Pressed) -> None:
"""Called when a button is pressed."""
button_id = event.button.id
assert button_id is not None
self.bell() # Terminal bell
def do_math() -> None:
"""Does the math: LEFT OPERATOR RIGHT"""
try:
if self.operator == "plus":
self.left += self.right
elif self.operator == "minus":
self.left -= self.right
elif self.operator == "divide":
self.left /= self.right
elif self.operator == "multiply":
self.left *= self.right
self.numbers = str(self.left)
self.value = ""
except Exception:
self.numbers = "Error"
if button_id.startswith("number-"):
number = button_id.partition("-")[-1]
self.numbers = self.value = self.value.lstrip("0") + number
elif button_id == "plus-minus":
self.numbers = self.value = str(Decimal(self.value or "0") * -1)
elif button_id == "percent":
self.numbers = self.value = str(Decimal(self.value or "0") / Decimal(100))
elif button_id == "point":
if "." not in self.value:
self.numbers = self.value = (self.value or "0") + "."
elif button_id == "ac":
self.value = ""
self.left = self.right = Decimal(0)
self.operator = "plus"
self.numbers = "0"
elif button_id == "c":
self.value = ""
self.numbers = "0"
elif button_id in ("plus", "minus", "divide", "multiply"):
self.right = Decimal(self.value or "0")
do_math()
self.operator = button_id
elif button_id == "equals":
if self.value:
self.right = Decimal(self.value)
do_math()
app = CalculatorApp(css_path="calculator.css") app = CalculatorApp(css_path="calculator.css")
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -3,7 +3,7 @@ from textual.widgets import Static
class CenterApp(App): class CenterApp(App):
CSS = """ DEFAULT_CSS = """
CenterApp Screen { CenterApp Screen {
layout: center; layout: center;

View File

@@ -4,7 +4,7 @@ from textual.widgets import Static
class CenterApp(App): class CenterApp(App):
CSS = """ DEFAULT_CSS = """
#sidebar { #sidebar {
dock: left; dock: left;
@@ -52,4 +52,4 @@ class CenterApp(App):
) )
app = CenterApp(log_verbosity=3) app = CenterApp()

17
sandbox/will/input.py Normal file
View File

@@ -0,0 +1,17 @@
from textual.app import App
from textual.widgets import TextInput
class InputApp(App):
CSS = """
TextInput {
}
"""
def compose(self):
yield TextInput(initial="foo")
app = InputApp()

View File

@@ -10,7 +10,7 @@ from textual.widget import Widget
class Box(Widget, can_focus=True): class Box(Widget, can_focus=True):
CSS = "#box {background: blue;}" DEFAULT_CSS = "#box {background: blue;}"
def render(self) -> RenderableType: def render(self) -> RenderableType:
return Panel("Box") return Panel("Box")

View File

@@ -21,7 +21,7 @@ class NewScreen(Screen):
class ScreenApp(App): class ScreenApp(App):
CSS = """ DEFAULT_CSS = """
ScreenApp Screen { ScreenApp Screen {
background: #111144; background: #111144;
color: white; color: white;

View File

@@ -37,9 +37,7 @@ class ButtonsApp(App[str]):
self.dark = not self.dark self.dark = not self.dark
app = ButtonsApp( app = ButtonsApp(log_path="textual.log", css_path="buttons.css", watch_css=True)
log_path="textual.log", css_path="buttons.css", watch_css=True, log_verbosity=3
)
if __name__ == "__main__": if __name__ == "__main__":
result = app.run() result = app.run()

View File

@@ -8,5 +8,6 @@ Static {
background: blue 20%; background: blue 20%;
height: 100%; height: 100%;
margin: 2 4; margin: 2 4;
min-width: 30; min-width: 80;
min-height: 40;
} }

View File

@@ -2,9 +2,14 @@ from textual.app import App
from textual.widgets import Static from textual.widgets import Static
class Clickable(Static):
def on_click(self):
self.app.bell()
class SpacingApp(App): class SpacingApp(App):
def compose(self): def compose(self):
yield Static() yield Static(id="2332")
app = SpacingApp(css_path="spacing.css") app = SpacingApp(css_path="spacing.css")

View File

@@ -1,8 +1,8 @@
Screen { Screen {
layout: table; layout: grid;
table-columns: 2fr 1fr 1fr; grid-columns: 2fr 1fr 1fr;
table-rows: 1fr 1fr; grid-rows: 1fr 1fr;
table-gutter: 1 2; grid-gutter: 1 2;
} }
Static { Static {

View File

@@ -5,7 +5,7 @@ from textual.widgets import DirectoryTree
class TreeApp(App): class TreeApp(App):
CSS = """ DEFAULT_CSS = """
Screen { Screen {
overflow: auto; overflow: auto;

View File

@@ -1,18 +1,104 @@
import inspect from __future__ import annotations
import inspect
from typing import TYPE_CHECKING
import rich.repr
from rich.console import RenderableType from rich.console import RenderableType
__all__ = ["log", "panic"] __all__ = ["log", "panic"]
def log(*args: object, verbosity: int = 0, **kwargs) -> None: from ._log import LogGroup, LogVerbosity, LogSeverity
# TODO: There may be an early-out here for when there is no endpoint for logs
from ._context import active_app
app = active_app.get()
caller = inspect.stack()[1] @rich.repr.auto
app.log(*args, verbosity=verbosity, _textual_calling_frame=caller, **kwargs) class Logger:
"""A Textual logger."""
def __init__(
self,
group: LogGroup = LogGroup.INFO,
verbosity: LogVerbosity = LogVerbosity.NORMAL,
severity: LogSeverity = LogSeverity.NORMAL,
) -> None:
self._group = group
self._verbosity = verbosity
self._severity = severity
def __rich_repr__(self) -> rich.repr.Result:
yield self._group, LogGroup.INFO
yield self._verbosity, LogVerbosity.NORMAL
yield self._severity, LogSeverity.NORMAL
def __call__(self, *args: object, **kwargs) -> None:
from ._context import active_app
app = active_app.get()
caller = inspect.stack()[1]
app._log(
self._group,
self._verbosity,
self._severity,
*args,
_textual_calling_frame=caller,
**kwargs,
)
def verbosity(self, verbose: bool) -> Logger:
"""Get a new logger with selective verbosity.
Args:
verbose (bool): True to use HIGH verbosity, otherwise NORMAL.
Returns:
Logger: New logger.
"""
verbosity = LogVerbosity.HIGH if verbose else LogVerbosity.NORMAL
return Logger(self._group, verbosity, LogSeverity.NORMAL)
@property
def verbose(self) -> Logger:
"""A verbose logger."""
return Logger(self._group, LogVerbosity.HIGH)
@property
def critical(self) -> Logger:
"""A critical logger."""
return Logger(self._group, self._verbosity, LogSeverity.CRITICAL)
@property
def event(self) -> Logger:
"""An event logger."""
return Logger(LogGroup.EVENT)
@property
def debug(self) -> Logger:
"""A debug logger."""
return Logger(LogGroup.DEBUG)
@property
def info(self) -> Logger:
"""An info logger."""
return Logger(LogGroup.INFO)
@property
def warning(self) -> Logger:
"""An info logger."""
return Logger(LogGroup.WARNING)
@property
def error(self) -> Logger:
"""An error logger."""
return Logger(LogGroup.ERROR)
@property
def system(self) -> Logger:
"""A system logger."""
return Logger(LogGroup.SYSTEM)
log = Logger()
def panic(*args: RenderableType) -> None: def panic(*args: RenderableType) -> None:

View File

@@ -222,6 +222,7 @@ class Compositor:
for y in range(region_y, region_y + height): for y in range(region_y, region_y + height):
setdefault(y, []).append(span) setdefault(y, []).append(span)
slice_remaining = slice(1, None)
for y, ranges in sorted(inline_ranges.items()): for y, ranges in sorted(inline_ranges.items()):
if len(ranges) == 1: if len(ranges) == 1:
# Special case of 1 span # Special case of 1 span
@@ -229,7 +230,7 @@ class Compositor:
else: else:
ranges.sort() ranges.sort()
x1, x2 = ranges[0] x1, x2 = ranges[0]
for next_x1, next_x2 in ranges[1:]: for next_x1, next_x2 in ranges[slice_remaining]:
if next_x1 <= x2: if next_x1 <= x2:
if next_x2 > x2: if next_x2 > x2:
x2 = next_x2 x2 = next_x2
@@ -487,7 +488,7 @@ class Compositor:
# TODO: Optimize with some line based lookup # TODO: Optimize with some line based lookup
contains = Region.contains contains = Region.contains
for widget, cropped_region, region, *_ in self: for widget, cropped_region, region, *_ in self:
if contains(cropped_region, x, y): if contains(cropped_region, x, y) and widget.visible:
return widget, region return widget, region
raise errors.NoWidget(f"No widget under screen coordinate ({x}, {y})") raise errors.NoWidget(f"No widget under screen coordinate ({x}, {y})")

Some files were not shown because too many files have changed in this diff Show More