Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Textual markup #5485

Merged
merged 61 commits into from
Feb 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
8b1c2ad
Textual markup
willmcgugan Jan 14, 2025
d9f8125
parse styles
willmcgugan Jan 14, 2025
d657faf
parse syntax
willmcgugan Jan 14, 2025
d6e085c
parse syntax
willmcgugan Jan 14, 2025
aa319af
simplify color output
willmcgugan Jan 14, 2025
c9102ae
switch to Content
willmcgugan Jan 16, 2025
32a2624
markup property
willmcgugan Jan 17, 2025
6d3d64d
Border content
willmcgugan Jan 18, 2025
a745cda
textual style
willmcgugan Jan 18, 2025
b055673
test fix, fix outline
willmcgugan Jan 18, 2025
57525e4
content
willmcgugan Jan 19, 2025
1d5b1cb
docs and tidy
willmcgugan Jan 21, 2025
ceceda7
simplify Content
willmcgugan Jan 21, 2025
83daf3b
tests
willmcgugan Jan 21, 2025
6ec9531
tests of total_ordering
willmcgugan Jan 21, 2025
3f29339
more tests
willmcgugan Jan 21, 2025
d4543ff
tests
willmcgugan Jan 22, 2025
35aaec4
interface fixes
willmcgugan Jan 22, 2025
48815bf
fix test
willmcgugan Jan 22, 2025
284ced4
docstrings
willmcgugan Jan 22, 2025
ed79ffd
fix percentages
willmcgugan Jan 22, 2025
2197fd0
optimize style parse
willmcgugan Jan 23, 2025
79734ee
fix for content
willmcgugan Jan 24, 2025
04dbf88
traceback handling
willmcgugan Jan 24, 2025
aa159dd
Visual protocol
willmcgugan Jan 25, 2025
02f8e0d
textual markup app
willmcgugan Jan 25, 2025
9f93a35
no wrap
willmcgugan Jan 26, 2025
8224ac8
wrap and overflow
willmcgugan Jan 26, 2025
eb8a6ac
back to rulesmap
willmcgugan Jan 26, 2025
4c59821
Merge branch 'main' into textual-markup
willmcgugan Jan 26, 2025
c2f4c2a
overflow test
willmcgugan Jan 26, 2025
a0910ce
wrap and overflow docs
willmcgugan Jan 27, 2025
d44788a
docs
willmcgugan Jan 27, 2025
f0cfabb
css styles
willmcgugan Jan 27, 2025
9eb73f4
parser WIP
willmcgugan Jan 28, 2025
381a1bc
style parse
willmcgugan Jan 30, 2025
1eed935
markup parse and tests
willmcgugan Jan 31, 2025
2795477
markup parsing
willmcgugan Feb 1, 2025
7d228fd
content docs
willmcgugan Feb 2, 2025
8211370
error reporting in playground
willmcgugan Feb 2, 2025
649abb5
fix dim
willmcgugan Feb 3, 2025
bba0b4f
test fixes
willmcgugan Feb 3, 2025
97feb9c
content docs
willmcgugan Feb 3, 2025
3ec59ee
added substution
willmcgugan Feb 4, 2025
b49eb9e
docs and tests
willmcgugan Feb 5, 2025
4e03fea
docs
willmcgugan Feb 6, 2025
32e852b
keys and playground
willmcgugan Feb 6, 2025
bda56ab
words
willmcgugan Feb 6, 2025
3716641
snapshots
willmcgugan Feb 6, 2025
e9dac1c
docstrings
willmcgugan Feb 6, 2025
e676d17
docstrings
willmcgugan Feb 6, 2025
27bbddd
fix artifact
willmcgugan Feb 6, 2025
609a29a
words
willmcgugan Feb 6, 2025
11a7c4e
typing fix
willmcgugan Feb 6, 2025
9944d43
typing fix
willmcgugan Feb 6, 2025
0f3de98
docstring
willmcgugan Feb 6, 2025
df77f68
docs update
willmcgugan Feb 7, 2025
3585dbd
docs
willmcgugan Feb 7, 2025
9d6c242
try this
willmcgugan Feb 7, 2025
5d0900c
artifact glitch
willmcgugan Feb 7, 2025
a91d8cc
overwrite
willmcgugan Feb 7, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/pythonpackage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ jobs:
if: ${{ matrix.python-version == '3.8' }}
- name: Upload snapshot report
if: always()
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: snapshot-report-textual
name: snapshot_report_textual
path: snapshot_report.html
overwrite: true
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Added App.ALLOW_SELECT for a global switch to disable text selection https://github.com/Textualize/textual/pull/5409
- Added `DOMNode.query_ancestor` https://github.com/Textualize/textual/pull/5409
- Added selection to Log widget https://github.com/Textualize/textual/pull/5467
- Added `text-wrap` and `text-overflow` CSS values https://github.com/Textualize/textual/pull/5485
- Added Textual markup to replace Rich markup https://github.com/Textualize/textual/pull/5485
- Added `Content.from_markup` https://github.com/Textualize/textual/pull/5485

### Fixed

Expand Down
5 changes: 5 additions & 0 deletions docs/api/content.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
title: "textual.content"
---

::: textual.content
5 changes: 5 additions & 0 deletions docs/api/style.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
title: "textual.style"
---

::: textual.style
36 changes: 36 additions & 0 deletions docs/examples/guide/content/content01.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from textual.app import App, ComposeResult
from textual.widgets import Static

TEXT1 = """\
Hello, [bold $text on $primary]World[/]!

[@click=app.notify('Hello, World!')]Click me[/]
"""

TEXT2 = """\
Markup will [bold]not[/bold] be displayed.

Tags will be left in the output.

"""


class ContentApp(App):
CSS = """
Screen {
Static {
height: 1fr;
}
#text1 { background: $primary-muted; }
#text2 { background: $error-muted; }
}
"""

def compose(self) -> ComposeResult:
yield Static(TEXT1, id="text1")
yield Static(TEXT2, id="text2", markup=False) # (1)!


if __name__ == "__main__":
app = ContentApp()
app.run()
5 changes: 5 additions & 0 deletions docs/examples/guide/content/playground.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from textual._markup_playground import MarkupPlayground

if __name__ == "__main__":
app = MarkupPlayground()
app.run()
36 changes: 36 additions & 0 deletions docs/examples/guide/content/renderables.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from rich.syntax import Syntax

from textual.app import App, ComposeResult, RenderResult
from textual.reactive import reactive
from textual.widget import Widget


class CodeView(Widget):
"""Widget to display Python code."""

DEFAULT_CSS = """
CodeView { height: auto; }
"""

code = reactive("")

def render(self) -> RenderResult:
# Syntax is a Rich renderable that displays syntax highlighted code
syntax = Syntax(self.code, "python", line_numbers=True, indent_guides=True)
return syntax


class CodeApp(App):
"""App to demonstrate Rich renderables in Textual."""

def compose(self) -> ComposeResult:
with open(__file__) as self_file:
code = self_file.read()
code_view = CodeView()
code_view.code = code
yield code_view


if __name__ == "__main__":
app = CodeApp()
app.run()
2 changes: 1 addition & 1 deletion docs/examples/styles/border_sub_title_align_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def compose(self):
"had to fill up",
"lbl4",
"", # (4)!
"[link=https://textual.textualize.io]Left[/]", # (5)!
"[link='https://textual.textualize.io']Left[/]", # (5)!
)
yield make_label_container( # (6)!
"nine labels", "lbl5", "Title", "Subtitle"
Expand Down
2 changes: 1 addition & 1 deletion docs/examples/styles/link_background.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class LinkBackgroundApp(App):

def compose(self):
yield Label(
"Visit the [link=https://textualize.io]Textualize[/link] website.",
"Visit the [link='https://textualize.io']Textualize[/link] website.",
id="lbl1", # (1)!
)
yield Label(
Expand Down
2 changes: 1 addition & 1 deletion docs/examples/styles/link_background_hover.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class LinkHoverBackgroundApp(App):

def compose(self):
yield Label(
"Visit the [link=https://textualize.io]Textualize[/link] website.",
"Visit the [link='https://textualize.io']Textualize[/link] website.",
id="lbl1", # (1)!
)
yield Label(
Expand Down
2 changes: 1 addition & 1 deletion docs/examples/styles/link_color.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class LinkColorApp(App):

def compose(self):
yield Label(
"Visit the [link=https://textualize.io]Textualize[/link] website.",
"Visit the [link='https://textualize.io']Textualize[/link] website.",
id="lbl1", # (1)!
)
yield Label(
Expand Down
2 changes: 1 addition & 1 deletion docs/examples/styles/link_color_hover.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class LinkHoverColorApp(App):

def compose(self):
yield Label(
"Visit the [link=https://textualize.io]Textualize[/link] website.",
"Visit the [link='https://textualize.io']Textualize[/link] website.",
id="lbl1", # (1)!
)
yield Label(
Expand Down
2 changes: 1 addition & 1 deletion docs/examples/styles/link_style.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class LinkStyleApp(App):

def compose(self):
yield Label(
"Visit the [link=https://textualize.io]Textualize[/link] website.",
"Visit the [link='https://textualize.io']Textualize[/link] website.",
id="lbl1", # (1)!
)
yield Label(
Expand Down
2 changes: 1 addition & 1 deletion docs/examples/styles/link_style_hover.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class LinkHoverStyleApp(App):

def compose(self):
yield Label(
"Visit the [link=https://textualize.io]Textualize[/link] website.",
"Visit the [link='https://textualize.io']Textualize[/link] website.",
id="lbl1", # (1)!
)
yield Label(
Expand Down
18 changes: 18 additions & 0 deletions docs/examples/styles/text_overflow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from textual.app import App, ComposeResult
from textual.widgets import Static

TEXT = """I must not fear. Fear is the mind-killer. Fear is the little-death that brings total obliteration. I will face my fear."""


class WrapApp(App):
CSS_PATH = "text_overflow.tcss"

def compose(self) -> ComposeResult:
yield Static(TEXT, id="static1")
yield Static(TEXT, id="static2")
yield Static(TEXT, id="static3")


if __name__ == "__main__":
app = WrapApp()
app.run()
17 changes: 17 additions & 0 deletions docs/examples/styles/text_overflow.tcss
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Static {
height: 1fr;
text-wrap: nowrap;
}

#static1 {
text-overflow: clip; # Overflowing text is clipped
background: red 20%;
}
#static2 {
text-overflow: fold; # Overflowing text is folded on to the next line
background: green 20%;
}
#static3 {
text-overflow: ellipsis; # Overflowing text is truncated with an ellipsis
background: blue 20%;
}
17 changes: 17 additions & 0 deletions docs/examples/styles/text_wrap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from textual.app import App, ComposeResult
from textual.widgets import Static

TEXT = """I must not fear. Fear is the mind-killer. Fear is the little-death that brings total obliteration. I will face my fear."""


class WrapApp(App):
CSS_PATH = "text_wrap.tcss"

def compose(self) -> ComposeResult:
yield Static(TEXT, id="static1")
yield Static(TEXT, id="static2")


if __name__ == "__main__":
app = WrapApp()
app.run()
12 changes: 12 additions & 0 deletions docs/examples/styles/text_wrap.tcss
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Static {
height: 1fr;
}

#static1 {
text-wrap: wrap; /* this is the default */
background: blue 20%;
}
#static2 {
text-wrap: nowrap; /* disable wrapping */
background: green 20%;
}
35 changes: 25 additions & 10 deletions docs/guide/CSS.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,25 +100,25 @@ With a header and a footer widget the DOM looks like this:
--8<-- "docs/images/dom2.excalidraw.svg"
</div>

!!! note
!!! note "What we didn't show"

We've simplified the above example somewhat. Both the Header and Footer widgets contain children of their own. When building an app with pre-built widgets you rarely need to know how they are constructed unless you plan on changing the styles of individual components.

Both Header and Footer are children of the Screen object.

To further explore the DOM, we're going to build a simple dialog with a question and two buttons. To do this we're going to import and use a few more builtin widgets:

- `textual.layout.Container` For our top-level dialog.
- `textual.layout.Horizontal` To arrange widgets left to right.
- `textual.widgets.Static` For simple content.
- `textual.widgets.Button` For a clickable button.
- [`textual.containers.Container`][textual.containers.Container] For our top-level dialog.
- [`textual.containers.Horizontal`][textual.containers.Horizontal] To arrange widgets left to right.
- [`textual.widgets.Static`][textual.widgets.Static] For simple content.
- [`textual.widgets.Button`][textual.widgets.Button] For a clickable button.


```python hl_lines="12 13 14 15 16 17 18 19 20" title="dom3.py"
--8<-- "docs/examples/guide/dom3.py"
```

We've added a Container to our DOM which (as the name suggests) is a container for other widgets. The container has a number of other widgets passed as positional arguments which will be added as the children of the container. Not all widgets accept child widgets in this way. A Button widget doesn't require any children, for example.
We've added a Container to our DOM which (as the name suggests) contains other widgets. The container has a number of other widgets passed as positional arguments which will be added as the children of the container. Not all widgets accept child widgets in this way. A Button widget doesn't require any children, for example.

Here's the DOM created by the above code:

Expand All @@ -139,7 +139,7 @@ You may recognize some elements in the above screenshot, but it doesn't quite lo
To add a stylesheet set the `CSS_PATH` classvar to a relative path:


!!! note
!!! note "What are TCSS files?"

Textual CSS files are typically given the extension `.tcss` to differentiate them from browser CSS (`.css`).

Expand Down Expand Up @@ -223,7 +223,7 @@ Static {
}
```

!!! note
!!! note "This is different to browser CSS"

The fact that the type selector matches base classes is a departure from browser CSS which doesn't have the same concept.

Expand Down Expand Up @@ -312,6 +312,18 @@ For example, the following will draw a red outline around all widgets:
}
```

While it is rare to need to style all widgets, you can combine the universal selector with a parent, to select all children of that parent.

For instance, here's how we would make all children of a `VerticalScroll` have a red background:

```css
VerticalScroll * {
background: red;
}
```

See [Combinators](#combinators) for more details on combining selectors like this.

### Pseudo classes

Pseudo classes can be used to match widgets in a particular state. Pseudo classes are set automatically by Textual. For instance, you might want a button to have a green background when the mouse cursor moves over it. We can do this with the `:hover` pseudo selector.
Expand Down Expand Up @@ -403,7 +415,7 @@ It is possible that several selectors match a given widget. If the same style is

The specificity rules are usually enough to fix any conflicts in your stylesheets. There is one last way of resolving conflicting selectors which applies to individual rules. If you add the text `!important` to the end of a rule then it will "win" regardless of the specificity.

!!! warning
!!! warning "If everything is Important, nothing is Important"

Use `!important` sparingly (if at all) as it can make it difficult to modify your CSS in the future.

Expand Down Expand Up @@ -445,7 +457,7 @@ This will be translated into:
Variables allow us to define reusable styling in a single place.
If we decide we want to change some aspect of our design in the future, we only have to update a single variable.

!!! note
!!! note "Where can variables be used?"

Variables can only be used in the _values_ of a CSS declaration. You cannot, for example, refer to a variable inside a selector.

Expand Down Expand Up @@ -576,3 +588,6 @@ If we were to add other selectors for additional screens or widgets, it would be
### Why use nesting?

There is no requirement to use nested CSS, but it can help to group related rule sets together (which makes it easier to edit). Nested CSS can also help you avoid some repetition in your selectors, i.e. in the nested CSS we only need to type `#questions` once, rather than four times in the non-nested CSS.

Nesting CSS will also make rules that are *more* specific.
This is useful if you find your rules are applying to widgets that you didn't intend.
4 changes: 2 additions & 2 deletions docs/guide/actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,9 @@ Consequently `"set_background('blue')"` is a valid action string, but `"set_back

## Links

Actions may be embedded as links within console markup. You can create such links with a `@click` tag.
Actions may be embedded in [markup](./content.md#actions) with the `@click` tag.

The following example mounts simple static text with embedded action links.
The following example mounts simple static text with embedded action links:

=== "actions03.py"

Expand Down
Loading
Loading