mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Merge branch 'main' of github.com:Textualize/textual into list-view
This commit is contained in:
6
.github/workflows/comment.yml
vendored
6
.github/workflows/comment.yml
vendored
@@ -13,6 +13,6 @@ jobs:
|
||||
with:
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
body: |
|
||||
Did we solve your problem?
|
||||
|
||||
Glad we could help!
|
||||
Don't forget to [star](https://github.com/Textualize/textual) the repository!
|
||||
|
||||
Follow [@textualizeio](https://twitter.com/textualizeio) for Textual updates.
|
||||
|
||||
12
CHANGELOG.md
12
CHANGELOG.md
@@ -1,5 +1,6 @@
|
||||
# Change Log
|
||||
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
||||
@@ -21,21 +22,30 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- It is now possible to `await` a `Widget.remove`.
|
||||
https://github.com/Textualize/textual/issues/1094
|
||||
- It is now possible to `await` a `DOMQuery.remove`. Note that this changes
|
||||
the return value of `DOMQuery.remove`, which uses to return `self`.
|
||||
the return value of `DOMQuery.remove`, which used to return `self`.
|
||||
https://github.com/Textualize/textual/issues/1094
|
||||
- Added Pilot.wait_for_animation
|
||||
- Added `Widget.move_child` https://github.com/Textualize/textual/issues/1121
|
||||
- Added a `Label` widget https://github.com/Textualize/textual/issues/1190
|
||||
- Support lazy-instantiated Screens (callables in App.SCREENS) https://github.com/Textualize/textual/pull/1185
|
||||
- Display of keys in footer has more sensible defaults https://github.com/Textualize/textual/pull/1213
|
||||
- Add App.get_key_display, allowing custom key_display App-wide https://github.com/Textualize/textual/pull/1213
|
||||
|
||||
### Changed
|
||||
|
||||
- Watchers are now called immediately when setting the attribute if they are synchronous. https://github.com/Textualize/textual/pull/1145
|
||||
- Widget.call_later has been renamed to Widget.call_after_refresh.
|
||||
- Button variant values are now checked at runtime. https://github.com/Textualize/textual/issues/1189
|
||||
- Added caching of some properties in Styles object
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed DataTable row not updating after add https://github.com/Textualize/textual/issues/1026
|
||||
- Fixed issues with animation. Now objects of different types may be animated.
|
||||
- Fixed containers with transparent background not showing borders https://github.com/Textualize/textual/issues/1175
|
||||
- Fixed auto-width in horizontal containers https://github.com/Textualize/textual/pull/1155
|
||||
- Fixed Input cursor invisible when placeholder empty https://github.com/Textualize/textual/pull/1202
|
||||
- Fixed deadlock when removing widgets from the App https://github.com/Textualize/textual/pull/1219
|
||||
|
||||
## [0.4.0] - 2022-11-08
|
||||
|
||||
|
||||
10
docs.md
10
docs.md
@@ -1,14 +1,14 @@
|
||||
# Documentation Workflow
|
||||
|
||||
* [Install Hatch](https://hatch.pypa.io/latest/install/)
|
||||
* Run the live-reload server using `hatch run docs:serve` from the project root
|
||||
* Ensure you're inside a *Python 3.10+* virtual environment
|
||||
* Run the live-reload server using `mkdocs serve` from the project root
|
||||
* Create new pages by adding new directories and Markdown files inside `docs/*`
|
||||
|
||||
## Commands
|
||||
|
||||
- `hatch run docs:serve` - Start the live-reloading docs server.
|
||||
- `hatch run docs:build` - Build the documentation site.
|
||||
- `hatch run docs:help` - Print help message and exit.
|
||||
- `mkdocs serve` - Start the live-reloading docs server.
|
||||
- `mkdocs build` - Build the documentation site.
|
||||
- `mkdocs -h` - Print help message and exit.
|
||||
|
||||
## Project layout
|
||||
|
||||
|
||||
1
docs/api/label.md
Normal file
1
docs/api/label.md
Normal file
@@ -0,0 +1 @@
|
||||
::: textual.widgets.Label
|
||||
@@ -2,3 +2,15 @@ willmcgugan:
|
||||
name: Will McGugan
|
||||
description: CEO / code-monkey
|
||||
avatar: https://github.com/willmcgugan.png
|
||||
darrenburns:
|
||||
name: Darren Burns
|
||||
description: Code-monkey
|
||||
avatar: https://github.com/darrenburns.png
|
||||
davep:
|
||||
name: Dave Pearson
|
||||
description: Code-monkey
|
||||
avatar: https://github.com/davep.png
|
||||
rodrigo:
|
||||
name: Rodrigo Girão Serrão
|
||||
description: Code-monkey
|
||||
avatar: https://github.com/rodrigogiraoserrao.png
|
||||
|
||||
12
docs/examples/widgets/label.py
Normal file
12
docs/examples/widgets/label.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.widgets import Label
|
||||
|
||||
|
||||
class LabelApp(App):
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Label("Hello, world!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = LabelApp()
|
||||
app.run()
|
||||
@@ -25,7 +25,7 @@ textual run my_app.py
|
||||
|
||||
The `run` sub-command will first look for a `App` instance called `app` in the global scope of your Python file. If there is no `app`, it will create an instance of the first `App` class it finds and run that.
|
||||
|
||||
Alternatively, you can add the name of an `App` instance or class after a colon to run a specific app in the Python file. Here's an example:
|
||||
Alternatively, you can add the name of an `App` instance or class after a colon to run a specific app in the Python file. Here's an example:
|
||||
|
||||
```bash
|
||||
textual run my_app.py:alternative_app
|
||||
@@ -119,6 +119,6 @@ class LogApp(App):
|
||||
self.log(self.tree)
|
||||
|
||||
if __name__ == "__main__":
|
||||
LogApp.run()
|
||||
LogApp().run()
|
||||
|
||||
```
|
||||
|
||||
33
docs/widgets/label.md
Normal file
33
docs/widgets/label.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Label
|
||||
|
||||
A widget which displays static text, but which can also contain more complex Rich renderables.
|
||||
|
||||
- [ ] Focusable
|
||||
- [ ] Container
|
||||
|
||||
## Example
|
||||
|
||||
The example below shows how you can use a `Label` widget to display some text.
|
||||
|
||||
=== "Output"
|
||||
|
||||
```{.textual path="docs/examples/widgets/label.py"}
|
||||
```
|
||||
|
||||
=== "label.py"
|
||||
|
||||
```python
|
||||
--8<-- "docs/examples/widgets/label.py"
|
||||
```
|
||||
|
||||
## Reactive Attributes
|
||||
|
||||
This widget has no reactive attributes.
|
||||
|
||||
## Messages
|
||||
|
||||
This widget sends no messages.
|
||||
|
||||
## See Also
|
||||
|
||||
* [Label](../api/label.md) code reference
|
||||
@@ -1,14 +1,14 @@
|
||||
# Static
|
||||
|
||||
A widget which displays static content.
|
||||
Can be used for simple text labels, but can also contain more complex Rich renderables.
|
||||
Can be used for Rich renderables and can also for the base for other types of widgets.
|
||||
|
||||
- [ ] Focusable
|
||||
- [x] Container
|
||||
- [ ] Container
|
||||
|
||||
## Example
|
||||
|
||||
The example below shows how you can use a `Static` widget as a simple text label.
|
||||
The example below shows how you can use a `Static` widget as a simple text label (but see [Label](./label.md) as a way of displaying text).
|
||||
|
||||
=== "Output"
|
||||
|
||||
@@ -32,3 +32,4 @@ This widget sends no messages.
|
||||
## See Also
|
||||
|
||||
* [Static](../api/static.md) code reference
|
||||
* [Label](./label.md)
|
||||
|
||||
@@ -13,7 +13,7 @@ from textual.containers import Horizontal
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.screen import Screen
|
||||
from textual.widget import Widget
|
||||
from textual.widgets import Footer, Button, Static
|
||||
from textual.widgets import Footer, Button, Label
|
||||
from textual.css.query import DOMQuery
|
||||
from textual.reactive import reactive
|
||||
from textual.binding import Binding
|
||||
@@ -33,10 +33,10 @@ class Help(Screen):
|
||||
Returns:
|
||||
ComposeResult: The result of composing the help screen.
|
||||
"""
|
||||
yield Static(Markdown(Path(__file__).with_suffix(".md").read_text()))
|
||||
yield Label(Markdown(Path(__file__).with_suffix(".md").read_text()))
|
||||
|
||||
|
||||
class WinnerMessage(Static):
|
||||
class WinnerMessage(Label):
|
||||
"""Widget to tell the user they have won."""
|
||||
|
||||
MIN_MOVES: Final = 14
|
||||
@@ -91,9 +91,9 @@ class GameHeader(Widget):
|
||||
ComposeResult: The result of composing the game header.
|
||||
"""
|
||||
yield Horizontal(
|
||||
Static(self.app.title, id="app-title"),
|
||||
Static(id="moves"),
|
||||
Static(id="progress"),
|
||||
Label(self.app.title, id="app-title"),
|
||||
Label(id="moves"),
|
||||
Label(id="progress"),
|
||||
)
|
||||
|
||||
def watch_moves(self, moves: int):
|
||||
@@ -102,7 +102,7 @@ class GameHeader(Widget):
|
||||
Args:
|
||||
moves (int): The number of moves made.
|
||||
"""
|
||||
self.query_one("#moves", Static).update(f"Moves: {moves}")
|
||||
self.query_one("#moves", Label).update(f"Moves: {moves}")
|
||||
|
||||
def watch_filled(self, filled: int):
|
||||
"""Watch the on-count reactive and update when it changes.
|
||||
@@ -110,7 +110,7 @@ class GameHeader(Widget):
|
||||
Args:
|
||||
filled (int): The number of cells that are currently on.
|
||||
"""
|
||||
self.query_one("#progress", Static).update(f"Filled: {filled}")
|
||||
self.query_one("#progress", Label).update(f"Filled: {filled}")
|
||||
|
||||
|
||||
class GameCell(Button):
|
||||
@@ -311,7 +311,7 @@ class FiveByFive(App[None]):
|
||||
CSS_PATH = "five_by_five.css"
|
||||
"""The name of the stylesheet for the app."""
|
||||
|
||||
SCREENS = {"help": Help()}
|
||||
SCREENS = {"help": Help}
|
||||
"""The pre-loaded screens for the application."""
|
||||
|
||||
BINDINGS = [("ctrl+d", "toggle_dark", "Toggle Dark Mode")]
|
||||
|
||||
@@ -96,6 +96,7 @@ nav:
|
||||
- "widgets/footer.md"
|
||||
- "widgets/header.md"
|
||||
- "widgets/input.md"
|
||||
- "widgets/label.md"
|
||||
- "widgets/static.md"
|
||||
- "widgets/tree_control.md"
|
||||
- API:
|
||||
@@ -109,6 +110,7 @@ nav:
|
||||
- "api/footer.md"
|
||||
- "api/geometry.md"
|
||||
- "api/header.md"
|
||||
- "api/label.md"
|
||||
- "api/list_view.md"
|
||||
- "api/message_pump.md"
|
||||
- "api/message.md"
|
||||
@@ -216,10 +218,10 @@ extra_css:
|
||||
|
||||
extra:
|
||||
social:
|
||||
- icon: fontawesome/brands/twitter
|
||||
- icon: fontawesome/brands/twitter
|
||||
link: https://twitter.com/textualizeio
|
||||
name: textualizeio on Twitter
|
||||
- icon: fontawesome/brands/github
|
||||
- icon: fontawesome/brands/github
|
||||
link: https://github.com/textualize/textual/
|
||||
name: Textual on Github
|
||||
- icon: fontawesome/brands/discord
|
||||
|
||||
170
poetry.lock
generated
170
poetry.lock
generated
@@ -31,6 +31,24 @@ python-versions = ">=3.7"
|
||||
[package.dependencies]
|
||||
frozenlist = ">=1.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "3.6.2"
|
||||
description = "High level compatibility layer for multiple asynchronous event loop implementations"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6.2"
|
||||
|
||||
[package.dependencies]
|
||||
idna = ">=2.8"
|
||||
sniffio = ">=1.1"
|
||||
typing-extensions = {version = "*", markers = "python_version < \"3.8\""}
|
||||
|
||||
[package.extras]
|
||||
doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
|
||||
test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "uvloop (>=0.15)"]
|
||||
trio = ["trio (>=0.16,<0.22)"]
|
||||
|
||||
[[package]]
|
||||
name = "async-timeout"
|
||||
version = "4.0.2"
|
||||
@@ -144,7 +162,7 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7
|
||||
|
||||
[[package]]
|
||||
name = "colored"
|
||||
version = "1.4.3"
|
||||
version = "1.4.4"
|
||||
description = "Simple library for color and formatting to terminal"
|
||||
category = "dev"
|
||||
optional = false
|
||||
@@ -182,7 +200,7 @@ python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.0.1"
|
||||
version = "1.0.4"
|
||||
description = "Backport of PEP 654 (exception groups)"
|
||||
category = "dev"
|
||||
optional = false
|
||||
@@ -250,7 +268,7 @@ typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.8\""
|
||||
|
||||
[[package]]
|
||||
name = "griffe"
|
||||
version = "0.23.0"
|
||||
version = "0.24.0"
|
||||
description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API."
|
||||
category = "dev"
|
||||
optional = false
|
||||
@@ -258,10 +276,60 @@ python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
cached-property = {version = "*", markers = "python_version < \"3.8\""}
|
||||
colorama = ">=0.4"
|
||||
|
||||
[package.extras]
|
||||
async = ["aiofiles (>=0.7,<1.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.14.0"
|
||||
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
typing-extensions = {version = "*", markers = "python_version < \"3.8\""}
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "0.16.1"
|
||||
description = "A minimal low-level HTTP client."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
anyio = ">=3.0,<5.0"
|
||||
certifi = "*"
|
||||
h11 = ">=0.13,<0.15"
|
||||
sniffio = ">=1.0.0,<2.0.0"
|
||||
|
||||
[package.extras]
|
||||
http2 = ["h2 (>=3,<5)"]
|
||||
socks = ["socksio (>=1.0.0,<2.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.23.1"
|
||||
description = "The next generation HTTP client."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
certifi = "*"
|
||||
httpcore = ">=0.15.0,<0.17.0"
|
||||
rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]}
|
||||
sniffio = "*"
|
||||
|
||||
[package.extras]
|
||||
brotli = ["brotli", "brotlicffi"]
|
||||
cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<13)"]
|
||||
http2 = ["h2 (>=3,<5)"]
|
||||
socks = ["socksio (>=1.0.0,<2.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "identify"
|
||||
version = "2.5.8"
|
||||
@@ -390,7 +458,7 @@ mkdocs = ">=1.1"
|
||||
|
||||
[[package]]
|
||||
name = "mkdocs-material"
|
||||
version = "8.5.9"
|
||||
version = "8.5.10"
|
||||
description = "Documentation that simply works"
|
||||
category = "dev"
|
||||
optional = false
|
||||
@@ -455,14 +523,14 @@ python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "mkdocstrings-python"
|
||||
version = "0.7.1"
|
||||
version = "0.8.0"
|
||||
description = "A Python handler for mkdocstrings."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
griffe = ">=0.11.1"
|
||||
griffe = ">=0.24"
|
||||
mkdocstrings = ">=0.19"
|
||||
|
||||
[[package]]
|
||||
@@ -541,7 +609,7 @@ pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
|
||||
|
||||
[[package]]
|
||||
name = "pathspec"
|
||||
version = "0.10.1"
|
||||
version = "0.10.2"
|
||||
description = "Utility library for gitignore style pattern matching of file paths."
|
||||
category = "dev"
|
||||
optional = false
|
||||
@@ -549,7 +617,7 @@ python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "2.5.3"
|
||||
version = "2.5.4"
|
||||
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||
category = "dev"
|
||||
optional = false
|
||||
@@ -663,7 +731,7 @@ testing = ["coverage (==6.2)", "mypy (==0.931)"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-asyncio"
|
||||
version = "0.20.1"
|
||||
version = "0.20.2"
|
||||
description = "Pytest support for asyncio"
|
||||
category = "dev"
|
||||
optional = false
|
||||
@@ -748,6 +816,20 @@ urllib3 = ">=1.21.1,<1.27"
|
||||
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
|
||||
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
|
||||
|
||||
[[package]]
|
||||
name = "rfc3986"
|
||||
version = "1.5.0"
|
||||
description = "Validating URI References per RFC 3986"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.dependencies]
|
||||
idna = {version = "*", optional = true, markers = "extra == \"idna2008\""}
|
||||
|
||||
[package.extras]
|
||||
idna2008 = ["idna"]
|
||||
|
||||
[[package]]
|
||||
name = "rich"
|
||||
version = "12.6.0"
|
||||
@@ -793,6 +875,14 @@ category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.0"
|
||||
description = "Sniff out which async library your code is running under"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "syrupy"
|
||||
version = "3.0.5"
|
||||
@@ -871,7 +961,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "virtualenv"
|
||||
version = "20.16.6"
|
||||
version = "20.16.7"
|
||||
description = "Virtual Python Environment builder"
|
||||
category = "dev"
|
||||
optional = false
|
||||
@@ -929,7 +1019,7 @@ dev = ["aiohttp", "click", "msgpack"]
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.7"
|
||||
content-hash = "0c33f462f79d31068c0d74743f47c1d713ecc8ba009eaef1bef832f1272e4b1a"
|
||||
content-hash = "578d7f611a797d406b8db7c61f28796e81af2e637d9671caab9b4ea2b1cf93c6"
|
||||
|
||||
[metadata.files]
|
||||
aiohttp = [
|
||||
@@ -1025,6 +1115,10 @@ aiosignal = [
|
||||
{file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"},
|
||||
{file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"},
|
||||
]
|
||||
anyio = [
|
||||
{file = "anyio-3.6.2-py3-none-any.whl", hash = "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3"},
|
||||
{file = "anyio-3.6.2.tar.gz", hash = "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421"},
|
||||
]
|
||||
async-timeout = [
|
||||
{file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"},
|
||||
{file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"},
|
||||
@@ -1085,7 +1179,7 @@ colorama = [
|
||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||
]
|
||||
colored = [
|
||||
{file = "colored-1.4.3.tar.gz", hash = "sha256:b7b48b9f40e8a65bbb54813d5d79dd008dc8b8c5638d5bbfd30fc5a82e6def7a"},
|
||||
{file = "colored-1.4.4.tar.gz", hash = "sha256:04ff4d4dd514274fe3b99a21bb52fb96f2688c01e93fba7bef37221e7cb56ce0"},
|
||||
]
|
||||
commonmark = [
|
||||
{file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"},
|
||||
@@ -1148,8 +1242,8 @@ distlib = [
|
||||
{file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"},
|
||||
]
|
||||
exceptiongroup = [
|
||||
{file = "exceptiongroup-1.0.1-py3-none-any.whl", hash = "sha256:4d6c0aa6dd825810941c792f53d7b8d71da26f5e5f84f20f9508e8f2d33b140a"},
|
||||
{file = "exceptiongroup-1.0.1.tar.gz", hash = "sha256:73866f7f842ede6cb1daa42c4af078e2035e5f7607f0e2c762cc51bb31bbe7b2"},
|
||||
{file = "exceptiongroup-1.0.4-py3-none-any.whl", hash = "sha256:542adf9dea4055530d6e1279602fa5cb11dab2395fa650b8674eaec35fc4a828"},
|
||||
{file = "exceptiongroup-1.0.4.tar.gz", hash = "sha256:bd14967b79cd9bdb54d97323216f8fdf533e278df937aa2a90089e7d6e06e5ec"},
|
||||
]
|
||||
filelock = [
|
||||
{file = "filelock-3.8.0-py3-none-any.whl", hash = "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4"},
|
||||
@@ -1244,8 +1338,20 @@ gitpython = [
|
||||
{file = "GitPython-3.1.29.tar.gz", hash = "sha256:cc36bfc4a3f913e66805a28e84703e419d9c264c1077e537b54f0e1af85dbefd"},
|
||||
]
|
||||
griffe = [
|
||||
{file = "griffe-0.23.0-py3-none-any.whl", hash = "sha256:cfca5f523808109da3f8cfaa46e325fa2e5bef51120d1146e908c121b56475f0"},
|
||||
{file = "griffe-0.23.0.tar.gz", hash = "sha256:a639e2968c8e27f56ebcc57f869a03cea7ac7e7f5684bd2429c665f761c4e7bd"},
|
||||
{file = "griffe-0.24.0-py3-none-any.whl", hash = "sha256:6c6b64716155f27ef63377e2b04749079c359f06d9a6e638bb2f885cbe463360"},
|
||||
{file = "griffe-0.24.0.tar.gz", hash = "sha256:afa92aeb8c5a4f2501693ffd607f820d7ade3ac2a36e34c43d39ee3486cec392"},
|
||||
]
|
||||
h11 = [
|
||||
{file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
|
||||
{file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
|
||||
]
|
||||
httpcore = [
|
||||
{file = "httpcore-0.16.1-py3-none-any.whl", hash = "sha256:8d393db683cc8e35cc6ecb02577c5e1abfedde52b38316d038932a84b4875ecb"},
|
||||
{file = "httpcore-0.16.1.tar.gz", hash = "sha256:3d3143ff5e1656a5740ea2f0c167e8e9d48c5a9bbd7f00ad1f8cff5711b08543"},
|
||||
]
|
||||
httpx = [
|
||||
{file = "httpx-0.23.1-py3-none-any.whl", hash = "sha256:0b9b1f0ee18b9978d637b0776bfd7f54e2ca278e063e3586d8f01cda89e042a8"},
|
||||
{file = "httpx-0.23.1.tar.gz", hash = "sha256:202ae15319be24efe9a8bd4ed4360e68fde7b38bcc2ce87088d416f026667d19"},
|
||||
]
|
||||
identify = [
|
||||
{file = "identify-2.5.8-py2.py3-none-any.whl", hash = "sha256:48b7925fe122720088aeb7a6c34f17b27e706b72c61070f27fe3789094233440"},
|
||||
@@ -1326,8 +1432,8 @@ mkdocs-autorefs = [
|
||||
{file = "mkdocs_autorefs-0.4.1-py3-none-any.whl", hash = "sha256:a2248a9501b29dc0cc8ba4c09f4f47ff121945f6ce33d760f145d6f89d313f5b"},
|
||||
]
|
||||
mkdocs-material = [
|
||||
{file = "mkdocs_material-8.5.9-py3-none-any.whl", hash = "sha256:143ea55843b3747b640e1110824d91e8a4c670352380e166e64959f9abe98862"},
|
||||
{file = "mkdocs_material-8.5.9.tar.gz", hash = "sha256:45eeabb23d2caba8fa3b85c91d9ec8e8b22add716e9bba8faf16d56af8aa5622"},
|
||||
{file = "mkdocs_material-8.5.10-py3-none-any.whl", hash = "sha256:51760fa4c9ee3ca0b3a661ec9f9817ec312961bb84ff19e5b523fdc5256e1d6c"},
|
||||
{file = "mkdocs_material-8.5.10.tar.gz", hash = "sha256:7623608f746c6d9ff68a8ef01f13eddf32fa2cae5e15badb251f26d1196bc8f1"},
|
||||
]
|
||||
mkdocs-material-extensions = [
|
||||
{file = "mkdocs_material_extensions-1.1-py3-none-any.whl", hash = "sha256:bcc2e5fc70c0ec50e59703ee6e639d87c7e664c0c441c014ea84461a90f1e902"},
|
||||
@@ -1342,8 +1448,8 @@ mkdocstrings = [
|
||||
{file = "mkdocstrings-0.19.0.tar.gz", hash = "sha256:efa34a67bad11229d532d89f6836a8a215937548623b64f3698a1df62e01cc3e"},
|
||||
]
|
||||
mkdocstrings-python = [
|
||||
{file = "mkdocstrings-python-0.7.1.tar.gz", hash = "sha256:c334b382dca202dfa37071c182418a6df5818356a95d54362a2b24822ca3af71"},
|
||||
{file = "mkdocstrings_python-0.7.1-py3-none-any.whl", hash = "sha256:a22060bfa374697678e9af4e62b020d990dad2711c98f7a9fac5c0345bef93c7"},
|
||||
{file = "mkdocstrings-python-0.8.0.tar.gz", hash = "sha256:67f674a8b252fca0b9411c10fb923dd6aacc49ac55c59f738b78b06592ace43d"},
|
||||
{file = "mkdocstrings_python-0.8.0-py3-none-any.whl", hash = "sha256:cbee42e53aeaae340d79d72e9bcf42f2b6abe4d11696597c76e3e86a4d9f05a0"},
|
||||
]
|
||||
msgpack = [
|
||||
{file = "msgpack-1.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4ab251d229d10498e9a2f3b1e68ef64cb393394ec477e3370c457f9430ce9250"},
|
||||
@@ -1509,12 +1615,12 @@ packaging = [
|
||||
{file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
|
||||
]
|
||||
pathspec = [
|
||||
{file = "pathspec-0.10.1-py3-none-any.whl", hash = "sha256:46846318467efc4556ccfd27816e004270a9eeeeb4d062ce5e6fc7a87c573f93"},
|
||||
{file = "pathspec-0.10.1.tar.gz", hash = "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d"},
|
||||
{file = "pathspec-0.10.2-py3-none-any.whl", hash = "sha256:88c2606f2c1e818b978540f73ecc908e13999c6c3a383daf3705652ae79807a5"},
|
||||
{file = "pathspec-0.10.2.tar.gz", hash = "sha256:8f6bf73e5758fd365ef5d58ce09ac7c27d2833a8d7da51712eac6e27e35141b0"},
|
||||
]
|
||||
platformdirs = [
|
||||
{file = "platformdirs-2.5.3-py3-none-any.whl", hash = "sha256:0cb405749187a194f444c25c82ef7225232f11564721eabffc6ec70df83b11cb"},
|
||||
{file = "platformdirs-2.5.3.tar.gz", hash = "sha256:6e52c21afff35cb659c6e52d8b4d61b9bd544557180440538f255d9382c8cbe0"},
|
||||
{file = "platformdirs-2.5.4-py3-none-any.whl", hash = "sha256:af0276409f9a02373d540bf8480021a048711d572745aef4b7842dad245eba10"},
|
||||
{file = "platformdirs-2.5.4.tar.gz", hash = "sha256:1006647646d80f16130f052404c6b901e80ee4ed6bef6792e1f238a8969106f7"},
|
||||
]
|
||||
pluggy = [
|
||||
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
|
||||
@@ -1545,8 +1651,8 @@ pytest-aiohttp = [
|
||||
{file = "pytest_aiohttp-1.0.4-py3-none-any.whl", hash = "sha256:1d2dc3a304c2be1fd496c0c2fb6b31ab60cd9fc33984f761f951f8ea1eb4ca95"},
|
||||
]
|
||||
pytest-asyncio = [
|
||||
{file = "pytest-asyncio-0.20.1.tar.gz", hash = "sha256:626699de2a747611f3eeb64168b3575f70439b06c3d0206e6ceaeeb956e65519"},
|
||||
{file = "pytest_asyncio-0.20.1-py3-none-any.whl", hash = "sha256:2c85a835df33fda40fe3973b451e0c194ca11bc2c007eabff90bb3d156fc172b"},
|
||||
{file = "pytest-asyncio-0.20.2.tar.gz", hash = "sha256:32a87a9836298a881c0ec637ebcc952cfe23a56436bdc0d09d1511941dd8a812"},
|
||||
{file = "pytest_asyncio-0.20.2-py3-none-any.whl", hash = "sha256:07e0abf9e6e6b95894a39f688a4a875d63c2128f76c02d03d16ccbc35bcc0f8a"},
|
||||
]
|
||||
pytest-cov = [
|
||||
{file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"},
|
||||
@@ -1610,6 +1716,10 @@ requests = [
|
||||
{file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"},
|
||||
{file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"},
|
||||
]
|
||||
rfc3986 = [
|
||||
{file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"},
|
||||
{file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"},
|
||||
]
|
||||
rich = [
|
||||
{file = "rich-12.6.0-py3-none-any.whl", hash = "sha256:a4eb26484f2c82589bd9a17c73d32a010b1e29d89f1604cd9bf3a2097b81bb5e"},
|
||||
{file = "rich-12.6.0.tar.gz", hash = "sha256:ba3a3775974105c221d31141f2c116f4fd65c5ceb0698657a11e9f295ec93fd0"},
|
||||
@@ -1626,6 +1736,10 @@ smmap = [
|
||||
{file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"},
|
||||
{file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"},
|
||||
]
|
||||
sniffio = [
|
||||
{file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"},
|
||||
{file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"},
|
||||
]
|
||||
syrupy = [
|
||||
{file = "syrupy-3.0.5-py3-none-any.whl", hash = "sha256:6dc0472cc690782b517d509a2854b5ca552a9851acf43b8a847cfa1aed6f6b01"},
|
||||
{file = "syrupy-3.0.5.tar.gz", hash = "sha256:928962a6c14abb2695be9a49b42a016a4e4abdb017a69104cde958f2faf01f98"},
|
||||
@@ -1729,8 +1843,8 @@ urllib3 = [
|
||||
{file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"},
|
||||
]
|
||||
virtualenv = [
|
||||
{file = "virtualenv-20.16.6-py3-none-any.whl", hash = "sha256:186ca84254abcbde98180fd17092f9628c5fe742273c02724972a1d8a2035108"},
|
||||
{file = "virtualenv-20.16.6.tar.gz", hash = "sha256:530b850b523c6449406dfba859d6345e48ef19b8439606c5d74d7d3c9e14d76e"},
|
||||
{file = "virtualenv-20.16.7-py3-none-any.whl", hash = "sha256:efd66b00386fdb7dbe4822d172303f40cd05e50e01740b19ea42425cbe653e29"},
|
||||
{file = "virtualenv-20.16.7.tar.gz", hash = "sha256:8691e3ff9387f743e00f6bb20f70121f5e4f596cae754531f2b3b3a1b1ac696e"},
|
||||
]
|
||||
watchdog = [
|
||||
{file = "watchdog-2.1.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a735a990a1095f75ca4f36ea2ef2752c99e6ee997c46b0de507ba40a09bf7330"},
|
||||
|
||||
@@ -44,7 +44,7 @@ nanoid = ">=2.0.0"
|
||||
[tool.poetry.extras]
|
||||
dev = ["aiohttp", "click", "msgpack"]
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pytest = "^7.1.3"
|
||||
black = "^22.3.0"
|
||||
mypy = "^0.990"
|
||||
@@ -57,9 +57,8 @@ pytest-aiohttp = "^1.0.4"
|
||||
time-machine = "^2.6.0"
|
||||
Jinja2 = "<3.1.0"
|
||||
syrupy = "^3.0.0"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
mkdocs-rss-plugin = "^1.5.0"
|
||||
httpx = "^0.23.1"
|
||||
|
||||
[tool.black]
|
||||
includes = "src"
|
||||
|
||||
@@ -18,6 +18,7 @@ from time import perf_counter
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
Generic,
|
||||
Iterable,
|
||||
List,
|
||||
@@ -44,7 +45,8 @@ from ._context import active_app
|
||||
from ._event_broker import NoHandler, extract_handler_actions
|
||||
from ._filter import LineFilter, Monochrome
|
||||
from ._path import _make_path_object_relative
|
||||
from ._typing import TypeAlias, Final
|
||||
from ._typing import Final, TypeAlias
|
||||
from .await_remove import AwaitRemove
|
||||
from .binding import Binding, Bindings
|
||||
from .css.query import NoMatches
|
||||
from .css.stylesheet import Stylesheet
|
||||
@@ -55,12 +57,13 @@ from .drivers.headless_driver import HeadlessDriver
|
||||
from .features import FeatureFlag, parse_features
|
||||
from .file_monitor import FileMonitor
|
||||
from .geometry import Offset, Region, Size
|
||||
from .keys import REPLACED_KEYS
|
||||
from .keys import REPLACED_KEYS, _get_key_display
|
||||
from .messages import CallbackType
|
||||
from .reactive import Reactive
|
||||
from .renderables.blank import Blank
|
||||
from .screen import Screen
|
||||
from .widget import AwaitMount, Widget, MountError
|
||||
from .widget import AwaitMount, MountError, Widget
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .devtools.client import DevtoolsClient
|
||||
@@ -101,7 +104,6 @@ DEFAULT_COLORS = {
|
||||
ComposeResult = Iterable[Widget]
|
||||
RenderResult = RenderableType
|
||||
|
||||
|
||||
AutopilotCallbackType: TypeAlias = "Callable[[Pilot], Coroutine[Any, Any, None]]"
|
||||
|
||||
|
||||
@@ -228,7 +230,7 @@ class App(Generic[ReturnType], DOMNode):
|
||||
}
|
||||
"""
|
||||
|
||||
SCREENS: dict[str, Screen] = {}
|
||||
SCREENS: dict[str, Screen | Callable[[], Screen]] = {}
|
||||
_BASE_PATH: str | None = None
|
||||
CSS_PATH: CSSPathType = None
|
||||
TITLE: str | None = None
|
||||
@@ -332,7 +334,7 @@ class App(Generic[ReturnType], DOMNode):
|
||||
self._registry: WeakSet[DOMNode] = WeakSet()
|
||||
|
||||
self._installed_screens: WeakValueDictionary[
|
||||
str, Screen
|
||||
str, Screen | Callable[[], Screen]
|
||||
] = WeakValueDictionary()
|
||||
self._installed_screens.update(**self.SCREENS)
|
||||
|
||||
@@ -354,6 +356,7 @@ class App(Generic[ReturnType], DOMNode):
|
||||
else None
|
||||
)
|
||||
self._screenshot: str | None = None
|
||||
self._dom_lock = asyncio.Lock()
|
||||
|
||||
@property
|
||||
def return_value(self) -> ReturnType | None:
|
||||
@@ -669,6 +672,22 @@ class App(Generic[ReturnType], DOMNode):
|
||||
keys, action, description, show=show, key_display=key_display
|
||||
)
|
||||
|
||||
def get_key_display(self, key: str) -> str:
|
||||
"""For a given key, return how it should be displayed in an app
|
||||
(e.g. in the Footer widget).
|
||||
By key, we refer to the string used in the "key" argument for
|
||||
a Binding instance. By overriding this method, you can ensure that
|
||||
keys are displayed consistently throughout your app, without
|
||||
needing to add a key_display to every binding.
|
||||
|
||||
Args:
|
||||
key (str): The binding key string.
|
||||
|
||||
Returns:
|
||||
str: The display string for the input key.
|
||||
"""
|
||||
return _get_key_display(key)
|
||||
|
||||
async def _press_keys(self, keys: Iterable[str]) -> None:
|
||||
"""A task to send key events."""
|
||||
app = self
|
||||
@@ -706,7 +725,7 @@ class App(Generic[ReturnType], DOMNode):
|
||||
# This conditional sleep can be removed after that issue is closed.
|
||||
if key == "tab":
|
||||
await asyncio.sleep(0.05)
|
||||
await asyncio.sleep(0.02)
|
||||
await asyncio.sleep(0.025)
|
||||
await app._animator.wait_for_idle()
|
||||
|
||||
@asynccontextmanager
|
||||
@@ -1000,12 +1019,15 @@ class App(Generic[ReturnType], DOMNode):
|
||||
next_screen = self._installed_screens[screen]
|
||||
except KeyError:
|
||||
raise KeyError(f"No screen called {screen!r} installed") from None
|
||||
if callable(next_screen):
|
||||
next_screen = next_screen()
|
||||
self._installed_screens[screen] = next_screen
|
||||
else:
|
||||
next_screen = screen
|
||||
return next_screen
|
||||
|
||||
def _get_screen(self, screen: Screen | str) -> tuple[Screen, AwaitMount]:
|
||||
"""Get an installed screen and a await mount object.
|
||||
"""Get an installed screen and an AwaitMount object.
|
||||
|
||||
If the screen isn't running, it will be registered before it is run.
|
||||
|
||||
@@ -1560,7 +1582,7 @@ class App(Generic[ReturnType], DOMNode):
|
||||
|
||||
# Close pre-defined screens
|
||||
for screen in self.SCREENS.values():
|
||||
if screen._running:
|
||||
if isinstance(screen, Screen) and screen._running:
|
||||
await self._prune_node(screen)
|
||||
|
||||
# Close any remaining nodes
|
||||
@@ -1938,6 +1960,48 @@ class App(Generic[ReturnType], DOMNode):
|
||||
for child in widget.children:
|
||||
push(child)
|
||||
|
||||
def _remove_nodes(self, widgets: list[Widget]) -> AwaitRemove:
|
||||
"""Remove nodes from DOM, and return an awaitable that awaits cleanup.
|
||||
|
||||
Args:
|
||||
widgets (list[Widget]): List of nodes to remvoe.
|
||||
|
||||
Returns:
|
||||
AwaitRemove: Awaitable that returns when the nodes have been fully removed.
|
||||
"""
|
||||
|
||||
async def prune_widgets_task(
|
||||
widgets: list[Widget], finished_event: asyncio.Event
|
||||
) -> None:
|
||||
"""Prune widgets as a background task.
|
||||
|
||||
Args:
|
||||
widgets (list[Widget]): Widgets to prune.
|
||||
finished_event (asyncio.Event): Event to set when complete.
|
||||
"""
|
||||
try:
|
||||
await self._prune_nodes(widgets)
|
||||
finally:
|
||||
finished_event.set()
|
||||
|
||||
removed_widgets = self._detach_from_dom(widgets)
|
||||
self.refresh(layout=True)
|
||||
|
||||
finished_event = asyncio.Event()
|
||||
asyncio.create_task(prune_widgets_task(removed_widgets, finished_event))
|
||||
|
||||
return AwaitRemove(finished_event)
|
||||
|
||||
async def _prune_nodes(self, widgets: list[Widget]) -> None:
|
||||
"""Remove nodes and children.
|
||||
|
||||
Args:
|
||||
widgets (Widget): _description_
|
||||
"""
|
||||
async with self._dom_lock:
|
||||
for widget in widgets:
|
||||
await self._prune_node(widget)
|
||||
|
||||
async def _prune_node(self, root: Widget) -> None:
|
||||
"""Remove a node and its children. Children are removed before parents.
|
||||
|
||||
|
||||
@@ -22,7 +22,3 @@ def camel_to_snake(
|
||||
return f"{lower}_{upper.lower()}"
|
||||
|
||||
return _re_snake.sub(repl, name).lower()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(camel_to_snake("HelloWorldEvent"))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.constants import BORDERS
|
||||
from textual.widgets import Button, Static
|
||||
from textual.widgets import Button, Label
|
||||
from textual.containers import Vertical
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ class BorderApp(App):
|
||||
|
||||
def compose(self):
|
||||
yield BorderButtons()
|
||||
self.text = Static(TEXT, id="text")
|
||||
self.text = Label(TEXT, id="text")
|
||||
yield self.text
|
||||
|
||||
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
|
||||
@@ -68,7 +68,7 @@ ColorGroup.-active {
|
||||
}
|
||||
|
||||
|
||||
ColorLabel {
|
||||
Label {
|
||||
padding: 0 0 1 0;
|
||||
content-align: center middle;
|
||||
color: $text;
|
||||
|
||||
@@ -2,7 +2,7 @@ from textual.app import App, ComposeResult
|
||||
from textual.containers import Horizontal, Vertical
|
||||
from textual.design import ColorSystem
|
||||
from textual.widget import Widget
|
||||
from textual.widgets import Button, Footer, Static
|
||||
from textual.widgets import Button, Footer, Static, Label
|
||||
|
||||
|
||||
class ColorButtons(Vertical):
|
||||
@@ -28,10 +28,6 @@ class Content(Vertical):
|
||||
pass
|
||||
|
||||
|
||||
class ColorLabel(Static):
|
||||
pass
|
||||
|
||||
|
||||
class ColorsView(Vertical):
|
||||
def compose(self) -> ComposeResult:
|
||||
|
||||
@@ -47,7 +43,7 @@ class ColorsView(Vertical):
|
||||
|
||||
for color_name in ColorSystem.COLOR_NAMES:
|
||||
|
||||
items: list[Widget] = [ColorLabel(f'"{color_name}"')]
|
||||
items: list[Widget] = [Label(f'"{color_name}"')]
|
||||
for level in LEVELS:
|
||||
color = f"{color_name}-{level}" if level else color_name
|
||||
item = ColorItem(
|
||||
|
||||
@@ -8,7 +8,7 @@ from textual.containers import Container, Horizontal, Vertical
|
||||
from textual.reactive import Reactive
|
||||
from textual.scrollbar import ScrollBarRender
|
||||
from textual.widget import Widget
|
||||
from textual.widgets import Button, Footer, Static, Input
|
||||
from textual.widgets import Button, Footer, Label, Input
|
||||
|
||||
VIRTUAL_SIZE = 100
|
||||
WINDOW_SIZE = 10
|
||||
@@ -27,7 +27,7 @@ class Bar(Widget):
|
||||
animation_running = Reactive(False)
|
||||
|
||||
DEFAULT_CSS = """
|
||||
|
||||
|
||||
Bar {
|
||||
background: $surface;
|
||||
color: $error;
|
||||
@@ -37,7 +37,7 @@ class Bar(Widget):
|
||||
background: $surface;
|
||||
color: $success;
|
||||
}
|
||||
|
||||
|
||||
"""
|
||||
|
||||
def watch_animation_running(self, running: bool) -> None:
|
||||
@@ -67,14 +67,14 @@ class EasingApp(App):
|
||||
self.animated_bar.position = START_POSITION
|
||||
duration_input = Input("1.0", placeholder="Duration", id="duration-input")
|
||||
|
||||
self.opacity_widget = Static(
|
||||
self.opacity_widget = Label(
|
||||
f"[b]Welcome to Textual![/]\n\n{TEXT}", id="opacity-widget"
|
||||
)
|
||||
|
||||
yield EasingButtons()
|
||||
yield Vertical(
|
||||
Horizontal(
|
||||
Static("Animation Duration:", id="label"), duration_input, id="inputs"
|
||||
Label("Animation Duration:", id="label"), duration_input, id="inputs"
|
||||
),
|
||||
Horizontal(
|
||||
self.animated_bar,
|
||||
|
||||
@@ -366,26 +366,3 @@ def parse(
|
||||
is_default_rules=is_default_rules,
|
||||
tie_breaker=tie_breaker,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(parse_selectors("Foo > Bar.baz { foo: bar"))
|
||||
|
||||
css = """#something {
|
||||
text: on red;
|
||||
transition: offset 5.51s in_out_cubic;
|
||||
offset-x: 100%;
|
||||
}
|
||||
"""
|
||||
|
||||
from textual.css.stylesheet import Stylesheet, StylesheetParseError
|
||||
from rich.console import Console
|
||||
|
||||
console = Console()
|
||||
stylesheet = Stylesheet()
|
||||
try:
|
||||
stylesheet.add_source(css)
|
||||
except StylesheetParseError as e:
|
||||
console.print(e.errors)
|
||||
print(stylesheet)
|
||||
print(stylesheet.css)
|
||||
|
||||
@@ -356,16 +356,9 @@ class DOMQuery(Generic[QueryType]):
|
||||
Returns:
|
||||
AwaitRemove: An awaitable object that waits for the widgets to be removed.
|
||||
"""
|
||||
prune_finished_event = asyncio.Event()
|
||||
app = active_app.get()
|
||||
app.post_message_no_wait(
|
||||
events.Prune(
|
||||
app,
|
||||
widgets=app._detach_from_dom(list(self)),
|
||||
finished_flag=prune_finished_event,
|
||||
)
|
||||
)
|
||||
return AwaitRemove(prune_finished_event)
|
||||
await_remove = app._remove_nodes(list(self))
|
||||
return await_remove
|
||||
|
||||
def set_styles(
|
||||
self, css: str | None = None, **update_styles
|
||||
|
||||
@@ -383,10 +383,3 @@ def percentage_string_to_float(string: str) -> float:
|
||||
else:
|
||||
float_percentage = float(string)
|
||||
return float_percentage
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(Scalar.parse("3.14fr"))
|
||||
s = Scalar.parse("23")
|
||||
print(repr(s))
|
||||
print(repr(s.cells))
|
||||
|
||||
@@ -557,6 +557,7 @@ class StylesBase(ABC):
|
||||
class Styles(StylesBase):
|
||||
node: DOMNode | None = None
|
||||
_rules: RulesMap = field(default_factory=dict)
|
||||
_updates: int = 0
|
||||
|
||||
important: set[str] = field(default_factory=set)
|
||||
|
||||
@@ -577,6 +578,7 @@ class Styles(StylesBase):
|
||||
Returns:
|
||||
bool: ``True`` if a rule was cleared, or ``False`` if it was already not set.
|
||||
"""
|
||||
self._updates += 1
|
||||
return self._rules.pop(rule, None) is not None
|
||||
|
||||
def get_rules(self) -> RulesMap:
|
||||
@@ -592,6 +594,7 @@ class Styles(StylesBase):
|
||||
Returns:
|
||||
bool: ``True`` if the rule changed, otherwise ``False``.
|
||||
"""
|
||||
self._updates += 1
|
||||
if value is None:
|
||||
return self._rules.pop(rule, None) is not None
|
||||
current = self._rules.get(rule)
|
||||
@@ -610,6 +613,7 @@ class Styles(StylesBase):
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Reset the rules to initial state."""
|
||||
self._updates += 1
|
||||
self._rules.clear()
|
||||
|
||||
def merge(self, other: Styles) -> None:
|
||||
@@ -618,10 +622,11 @@ class Styles(StylesBase):
|
||||
Args:
|
||||
other (Styles): A Styles object.
|
||||
"""
|
||||
|
||||
self._updates += 1
|
||||
self._rules.update(other._rules)
|
||||
|
||||
def merge_rules(self, rules: RulesMap) -> None:
|
||||
self._updates += 1
|
||||
self._rules.update(rules)
|
||||
|
||||
def extract_rules(
|
||||
@@ -929,6 +934,18 @@ class RenderStyles(StylesBase):
|
||||
self._base_styles = base
|
||||
self._inline_styles = inline_styles
|
||||
self._animate: BoundAnimator | None = None
|
||||
self._updates: int = 0
|
||||
self._rich_style: tuple[int, Style] | None = None
|
||||
self._gutter: tuple[int, Spacing] | None = None
|
||||
|
||||
@property
|
||||
def _cache_key(self) -> int:
|
||||
"""A key key, that changes when any style is changed.
|
||||
|
||||
Returns:
|
||||
int: An opaque integer.
|
||||
"""
|
||||
return self._updates + self._base_styles._updates + self._inline_styles._updates
|
||||
|
||||
@property
|
||||
def base(self) -> Styles:
|
||||
@@ -946,6 +963,21 @@ class RenderStyles(StylesBase):
|
||||
assert self.node is not None
|
||||
return self.node.rich_style
|
||||
|
||||
@property
|
||||
def gutter(self) -> Spacing:
|
||||
"""Get space around widget.
|
||||
|
||||
Returns:
|
||||
Spacing: Space around widget content.
|
||||
"""
|
||||
if self._gutter is not None:
|
||||
cache_key, gutter = self._gutter
|
||||
if cache_key == self._updates:
|
||||
return gutter
|
||||
gutter = self.padding + self.border.spacing
|
||||
self._gutter = (self._cache_key, gutter)
|
||||
return gutter
|
||||
|
||||
def animate(
|
||||
self,
|
||||
attribute: str,
|
||||
@@ -972,6 +1004,7 @@ class RenderStyles(StylesBase):
|
||||
|
||||
"""
|
||||
if self._animate is None:
|
||||
assert self.node is not None
|
||||
self._animate = self.node.app.animator.bind(self)
|
||||
assert self._animate is not None
|
||||
self._animate(
|
||||
@@ -1003,16 +1036,19 @@ class RenderStyles(StylesBase):
|
||||
|
||||
def merge_rules(self, rules: RulesMap) -> None:
|
||||
self._inline_styles.merge_rules(rules)
|
||||
self._updates += 1
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Reset the rules to initial state."""
|
||||
self._inline_styles.reset()
|
||||
self._updates += 1
|
||||
|
||||
def has_rule(self, rule: str) -> bool:
|
||||
"""Check if a rule has been set."""
|
||||
return self._inline_styles.has_rule(rule) or self._base_styles.has_rule(rule)
|
||||
|
||||
def set_rule(self, rule: str, value: object | None) -> bool:
|
||||
self._updates += 1
|
||||
return self._inline_styles.set_rule(rule, value)
|
||||
|
||||
def get_rule(self, rule: str, default: object = None) -> object:
|
||||
@@ -1022,6 +1058,7 @@ class RenderStyles(StylesBase):
|
||||
|
||||
def clear_rule(self, rule_name: str) -> bool:
|
||||
"""Clear a rule (from inline)."""
|
||||
self._updates += 1
|
||||
return self._inline_styles.clear_rule(rule_name)
|
||||
|
||||
def get_rules(self) -> RulesMap:
|
||||
@@ -1037,25 +1074,3 @@ class RenderStyles(StylesBase):
|
||||
styles.merge(self._inline_styles)
|
||||
combined_css = styles.css
|
||||
return combined_css
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
styles = Styles()
|
||||
|
||||
styles.display = "none"
|
||||
styles.visibility = "hidden"
|
||||
styles.border = ("solid", "rgb(10,20,30)")
|
||||
styles.outline_right = ("solid", "red")
|
||||
styles.text_style = "italic"
|
||||
styles.dock = "bar"
|
||||
styles.layers = "foo bar"
|
||||
|
||||
from rich import print
|
||||
|
||||
print(styles.text_style)
|
||||
print(styles.text)
|
||||
|
||||
print(styles)
|
||||
print(styles.css)
|
||||
|
||||
print(styles.extract_rules((0, 1, 0)))
|
||||
|
||||
@@ -197,18 +197,3 @@ def tokenize_values(values: dict[str, str]) -> dict[str, list[Token]]:
|
||||
name: list(tokenize_value(value, "__name__")) for name, value in values.items()
|
||||
}
|
||||
return value_tokens
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from rich import print
|
||||
|
||||
css = """#something {
|
||||
|
||||
color: rgb(10,12,23)
|
||||
}
|
||||
"""
|
||||
# transition: offset 500 in_out_cubic;
|
||||
tokens = tokenize(css, __name__)
|
||||
print(list(tokens))
|
||||
|
||||
print(tokenize_values({"primary": "rgb(10,20,30)", "secondary": "#ff00ff"}))
|
||||
|
||||
@@ -222,11 +222,3 @@ def show_design(light: ColorSystem, dark: ColorSystem) -> Table:
|
||||
table.add_column("Dark", justify="center")
|
||||
table.add_row(make_shades(light), make_shades(dark))
|
||||
return table
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from .app import DEFAULT_COLORS
|
||||
|
||||
from rich import print
|
||||
|
||||
print(show_design(DEFAULT_COLORS["light"], DEFAULT_COLORS["dark"]))
|
||||
|
||||
@@ -236,17 +236,3 @@ class LinuxDriver(Driver):
|
||||
finally:
|
||||
with timer("selector.close"):
|
||||
selector.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from rich.console import Console
|
||||
|
||||
console = Console()
|
||||
|
||||
from ..app import App
|
||||
|
||||
class MyApp(App):
|
||||
async def on_mount(self, event: events.Mount) -> None:
|
||||
self.set_timer(5, callback=self._close_messages)
|
||||
|
||||
MyApp.run()
|
||||
|
||||
@@ -127,28 +127,6 @@ class Unmount(Mount, bubble=False, verbose=False):
|
||||
"""Sent when a widget is unmounted and may not longer receive messages."""
|
||||
|
||||
|
||||
class Prune(Event, bubble=False):
|
||||
"""Sent to the app to ask it to prune one or more widgets from the DOM.
|
||||
|
||||
Attributes:
|
||||
widgets (list[Widgets]): The list of widgets to prune.
|
||||
finished_flag (asyncio.Event): An asyncio Event to that will be flagged when the prune is done.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, sender: MessageTarget, widgets: list[Widget], finished_flag: asyncio.Event
|
||||
) -> None:
|
||||
"""Initialise the event.
|
||||
|
||||
Args:
|
||||
widgets (list[Widgets]): The list of widgets to prune.
|
||||
finished_flag (asyncio.Event): An asyncio Event to that will be flagged when the prune is done.
|
||||
"""
|
||||
super().__init__(sender)
|
||||
self.finished_flag = finished_flag
|
||||
self.widgets = widgets
|
||||
|
||||
|
||||
class Show(Event, bubble=False):
|
||||
"""Sent when a widget has become visible."""
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unicodedata
|
||||
from enum import Enum
|
||||
|
||||
|
||||
@@ -219,7 +220,34 @@ KEY_ALIASES = {
|
||||
"ctrl+j": ["newline"],
|
||||
}
|
||||
|
||||
KEY_DISPLAY_ALIASES = {
|
||||
"up": "↑",
|
||||
"down": "↓",
|
||||
"left": "←",
|
||||
"right": "→",
|
||||
"backspace": "⌫",
|
||||
"escape": "ESC",
|
||||
"enter": "⏎",
|
||||
}
|
||||
|
||||
|
||||
def _get_key_aliases(key: str) -> list[str]:
|
||||
"""Return all aliases for the given key, including the key itself"""
|
||||
return [key] + KEY_ALIASES.get(key, [])
|
||||
|
||||
|
||||
def _get_key_display(key: str) -> str:
|
||||
"""Given a key (i.e. the `key` string argument to Binding __init__),
|
||||
return the value that should be displayed in the app when referring
|
||||
to this key (e.g. in the Footer widget)."""
|
||||
display_alias = KEY_DISPLAY_ALIASES.get(key)
|
||||
if display_alias:
|
||||
return display_alias
|
||||
|
||||
original_key = REPLACED_KEYS.get(key, key)
|
||||
try:
|
||||
unicode_character = unicodedata.lookup(original_key.upper().replace("_", " "))
|
||||
except KeyError:
|
||||
return original_key.upper()
|
||||
|
||||
return unicode_character
|
||||
|
||||
@@ -43,7 +43,7 @@ class Reactive(Generic[ReactiveType]):
|
||||
layout (bool, optional): Perform a layout on change. Defaults to False.
|
||||
repaint (bool, optional): Perform a repaint on change. Defaults to True.
|
||||
init (bool, optional): Call watchers on initialize (post mount). Defaults to False.
|
||||
always_update(bool, optional): Call watchers even when the new value equals the old value. Defaults to False.
|
||||
always_update (bool, optional): Call watchers even when the new value equals the old value. Defaults to False.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -76,7 +76,7 @@ class Reactive(Generic[ReactiveType]):
|
||||
default (ReactiveType | Callable[[], ReactiveType]): A default value or callable that returns a default.
|
||||
layout (bool, optional): Perform a layout on change. Defaults to False.
|
||||
repaint (bool, optional): Perform a repaint on change. Defaults to True.
|
||||
always_update(bool, optional): Call watchers even when the new value equals the old value. Defaults to False.
|
||||
always_update (bool, optional): Call watchers even when the new value equals the old value. Defaults to False.
|
||||
|
||||
Returns:
|
||||
Reactive: A Reactive instance which calls watchers or initialize.
|
||||
@@ -292,7 +292,7 @@ class reactive(Reactive[ReactiveType]):
|
||||
layout (bool, optional): Perform a layout on change. Defaults to False.
|
||||
repaint (bool, optional): Perform a repaint on change. Defaults to True.
|
||||
init (bool, optional): Call watchers on initialize (post mount). Defaults to True.
|
||||
always_update(bool, optional): Call watchers even when the new value equals the old value. Defaults to False.
|
||||
always_update (bool, optional): Call watchers even when the new value equals the old value. Defaults to False.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
|
||||
@@ -25,9 +25,3 @@ class Blank:
|
||||
for _ in range(height):
|
||||
yield segment
|
||||
yield line
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from rich import print
|
||||
|
||||
print(Blank("red"))
|
||||
|
||||
@@ -36,9 +36,3 @@ class VerticalGradient:
|
||||
),
|
||||
)
|
||||
yield Segment(f"{width * ' '}\n", line_color)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from rich import print
|
||||
|
||||
print(VerticalGradient("red", "blue"))
|
||||
|
||||
@@ -310,18 +310,19 @@ class Screen(Widget):
|
||||
# Check for any widgets marked as 'dirty' (needs a repaint)
|
||||
event.prevent_default()
|
||||
|
||||
if self.is_current:
|
||||
if self._layout_required:
|
||||
self._refresh_layout()
|
||||
self._layout_required = False
|
||||
self._dirty_widgets.clear()
|
||||
if self._repaint_required:
|
||||
self._dirty_widgets.clear()
|
||||
self._dirty_widgets.add(self)
|
||||
self._repaint_required = False
|
||||
async with self.app._dom_lock:
|
||||
if self.is_current:
|
||||
if self._layout_required:
|
||||
self._refresh_layout()
|
||||
self._layout_required = False
|
||||
self._dirty_widgets.clear()
|
||||
if self._repaint_required:
|
||||
self._dirty_widgets.clear()
|
||||
self._dirty_widgets.add(self)
|
||||
self._repaint_required = False
|
||||
|
||||
if self._dirty_widgets:
|
||||
self.update_timer.resume()
|
||||
if self._dirty_widgets:
|
||||
self.update_timer.resume()
|
||||
|
||||
# The Screen is idle - a good opportunity to invoke the scheduled callbacks
|
||||
await self._invoke_and_clear_callbacks()
|
||||
|
||||
@@ -320,18 +320,3 @@ class ScrollBarCorner(Widget):
|
||||
styles = self.parent.styles
|
||||
color = styles.scrollbar_corner_color
|
||||
return Blank(color)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from rich.console import Console
|
||||
|
||||
console = Console()
|
||||
|
||||
thickness = 2
|
||||
console.print(f"Bars thickness: {thickness}")
|
||||
|
||||
console.print("Vertical bar:")
|
||||
console.print(ScrollBarRender.render_bar(thickness=thickness))
|
||||
|
||||
console.print("Horizontal bar:")
|
||||
console.print(ScrollBarRender.render_bar(vertical=False, thickness=thickness))
|
||||
|
||||
@@ -523,15 +523,19 @@ class Widget(DOMNode):
|
||||
|
||||
# Decide the final resting place depending on what we've been asked
|
||||
# to do.
|
||||
insert_before: int | None = None
|
||||
insert_after: int | None = None
|
||||
if before is not None:
|
||||
parent, before = self._find_mount_point(before)
|
||||
parent, insert_before = self._find_mount_point(before)
|
||||
elif after is not None:
|
||||
parent, after = self._find_mount_point(after)
|
||||
parent, insert_after = self._find_mount_point(after)
|
||||
else:
|
||||
parent = self
|
||||
|
||||
return AwaitMount(
|
||||
self.app._register(parent, *widgets, before=before, after=after)
|
||||
self.app._register(
|
||||
parent, *widgets, before=insert_before, after=insert_after
|
||||
)
|
||||
)
|
||||
|
||||
def move_child(
|
||||
@@ -560,7 +564,7 @@ class Widget(DOMNode):
|
||||
if before is None and after is None:
|
||||
raise WidgetError("One of `before` or `after` is required.")
|
||||
elif before is not None and after is not None:
|
||||
raise WidgetError("Only one of `before`or `after` can be handled.")
|
||||
raise WidgetError("Only one of `before` or `after` can be handled.")
|
||||
|
||||
def _to_widget(child: int | Widget, called: str) -> Widget:
|
||||
"""Ensure a given child reference is a Widget."""
|
||||
@@ -697,7 +701,6 @@ class Widget(DOMNode):
|
||||
Returns:
|
||||
int: The height of the content.
|
||||
"""
|
||||
|
||||
if self.is_container:
|
||||
assert self._layout is not None
|
||||
height = (
|
||||
@@ -2114,15 +2117,9 @@ class Widget(DOMNode):
|
||||
Returns:
|
||||
AwaitRemove: An awaitable object that waits for the widget to be removed.
|
||||
"""
|
||||
prune_finished_event = AsyncEvent()
|
||||
self.app.post_message_no_wait(
|
||||
events.Prune(
|
||||
self,
|
||||
widgets=self.app._detach_from_dom([self]),
|
||||
finished_flag=prune_finished_event,
|
||||
)
|
||||
)
|
||||
return AwaitRemove(prune_finished_event)
|
||||
|
||||
await_remove = self.app._remove_nodes([self])
|
||||
return await_remove
|
||||
|
||||
def render(self) -> RenderableType:
|
||||
"""Get renderable for widget.
|
||||
|
||||
@@ -15,6 +15,7 @@ if typing.TYPE_CHECKING:
|
||||
from ._directory_tree import DirectoryTree
|
||||
from ._footer import Footer
|
||||
from ._header import Header
|
||||
from ._label import Label
|
||||
from ._list_view import ListView
|
||||
from ._list_item import ListItem
|
||||
from ._pretty import Pretty
|
||||
@@ -34,6 +35,7 @@ __all__ = [
|
||||
"Header",
|
||||
"ListItem",
|
||||
"ListView",
|
||||
"Label",
|
||||
"Placeholder",
|
||||
"Pretty",
|
||||
"Static",
|
||||
|
||||
@@ -5,6 +5,7 @@ from ._checkbox import Checkbox as Checkbox
|
||||
from ._directory_tree import DirectoryTree as DirectoryTree
|
||||
from ._footer import Footer as Footer
|
||||
from ._header import Header as Header
|
||||
from ._label import Label as Label
|
||||
from ._list_view import ListView as ListView
|
||||
from ._list_item import ListItem as ListItem
|
||||
from ._placeholder import Placeholder as Placeholder
|
||||
|
||||
@@ -168,9 +168,9 @@ class Button(Static, can_focus=True):
|
||||
"""Create a Button widget.
|
||||
|
||||
Args:
|
||||
label (str): The text that appears within the button.
|
||||
disabled (bool): Whether the button is disabled or not.
|
||||
variant (ButtonVariant): The variant of the button.
|
||||
label (str, optional): The text that appears within the button.
|
||||
disabled (bool, optional): Whether the button is disabled or not.
|
||||
variant (ButtonVariant, optional): The variant of the button.
|
||||
name (str | None, optional): The name of the button.
|
||||
id (str | None, optional): The ID of the button in the DOM.
|
||||
classes (str | None, optional): The CSS classes of the button.
|
||||
@@ -186,7 +186,7 @@ class Button(Static, can_focus=True):
|
||||
if disabled:
|
||||
self.add_class("-disabled")
|
||||
|
||||
self.variant = variant
|
||||
self.variant = self.validate_variant(variant)
|
||||
|
||||
label: Reactive[RenderableType] = Reactive("")
|
||||
variant = Reactive.init("default")
|
||||
@@ -267,8 +267,8 @@ class Button(Static, can_focus=True):
|
||||
"""Utility constructor for creating a success Button variant.
|
||||
|
||||
Args:
|
||||
label (str): The text that appears within the button.
|
||||
disabled (bool): Whether the button is disabled or not.
|
||||
label (str, optional): The text that appears within the button.
|
||||
disabled (bool, optional): Whether the button is disabled or not.
|
||||
name (str | None, optional): The name of the button.
|
||||
id (str | None, optional): The ID of the button in the DOM.
|
||||
classes(str | None, optional): The CSS classes of the button.
|
||||
@@ -298,8 +298,8 @@ class Button(Static, can_focus=True):
|
||||
"""Utility constructor for creating a warning Button variant.
|
||||
|
||||
Args:
|
||||
label (str): The text that appears within the button.
|
||||
disabled (bool): Whether the button is disabled or not.
|
||||
label (str, optional): The text that appears within the button.
|
||||
disabled (bool, optional): Whether the button is disabled or not.
|
||||
name (str | None, optional): The name of the button.
|
||||
id (str | None, optional): The ID of the button in the DOM.
|
||||
classes (str | None, optional): The CSS classes of the button.
|
||||
@@ -329,8 +329,8 @@ class Button(Static, can_focus=True):
|
||||
"""Utility constructor for creating an error Button variant.
|
||||
|
||||
Args:
|
||||
label (str): The text that appears within the button.
|
||||
disabled (bool): Whether the button is disabled or not.
|
||||
label (str, optional): The text that appears within the button.
|
||||
disabled (bool, optional): Whether the button is disabled or not.
|
||||
name (str | None, optional): The name of the button.
|
||||
id (str | None, optional): The ID of the button in the DOM.
|
||||
classes (str | None, optional): The CSS classes of the button.
|
||||
|
||||
@@ -7,6 +7,7 @@ from rich.console import RenderableType
|
||||
from rich.text import Text
|
||||
|
||||
from .. import events
|
||||
from ..keys import _get_key_display
|
||||
from ..reactive import Reactive, watch
|
||||
from ..widget import Widget
|
||||
|
||||
@@ -99,11 +100,12 @@ class Footer(Widget):
|
||||
|
||||
for action, bindings in action_to_bindings.items():
|
||||
binding = bindings[0]
|
||||
key_display = (
|
||||
binding.key.upper()
|
||||
if binding.key_display is None
|
||||
else binding.key_display
|
||||
)
|
||||
if binding.key_display is None:
|
||||
key_display = self.app.get_key_display(binding.key)
|
||||
if key_display is None:
|
||||
key_display = binding.key.upper()
|
||||
else:
|
||||
key_display = binding.key_display
|
||||
hovered = self.highlight_key == binding.key
|
||||
key_text = Text.assemble(
|
||||
(f" {key_display} ", highlight_key_style if hovered else key_style),
|
||||
|
||||
@@ -176,6 +176,10 @@ class Input(Widget, can_focus=True):
|
||||
if self.has_focus:
|
||||
cursor_style = self.get_component_rich_style("input--cursor")
|
||||
if self._cursor_visible:
|
||||
# If the placeholder is empty, there's no characters to stylise
|
||||
# to make the cursor flash, so use a single space character
|
||||
if len(placeholder) == 0:
|
||||
placeholder = Text(" ")
|
||||
placeholder.stylize(cursor_style, 0, 1)
|
||||
return placeholder
|
||||
return _InputRenderable(self, self._cursor_visible)
|
||||
|
||||
7
src/textual/widgets/_label.py
Normal file
7
src/textual/widgets/_label.py
Normal file
@@ -0,0 +1,7 @@
|
||||
"""Provides a simple Label widget."""
|
||||
|
||||
from ._static import Static
|
||||
|
||||
|
||||
class Label(Static):
|
||||
"""A simple label widget for displaying text-oriented renderables."""
|
||||
@@ -177,16 +177,16 @@ class Tabs(Widget):
|
||||
Args:
|
||||
tabs (list[Tab]): A list of Tab objects defining the tabs which should be rendered.
|
||||
active_tab (str, optional): The name of the tab that should be active on first render.
|
||||
active_tab_style (StyleType): Style to apply to the label of the active tab.
|
||||
active_bar_style (StyleType): Style to apply to the underline of the active tab.
|
||||
inactive_tab_style (StyleType): Style to apply to the label of inactive tabs.
|
||||
inactive_bar_style (StyleType): Style to apply to the underline of inactive tabs.
|
||||
inactive_text_opacity (float): Opacity of the text labels of inactive tabs.
|
||||
animation_duration (float): The duration of the tab change animation, in seconds.
|
||||
animation_function (str): The easing function to use for the tab change animation.
|
||||
active_tab_style (StyleType, optional): Style to apply to the label of the active tab.
|
||||
active_bar_style (StyleType, optional): Style to apply to the underline of the active tab.
|
||||
inactive_tab_style (StyleType, optional): Style to apply to the label of inactive tabs.
|
||||
inactive_bar_style (StyleType, optional): Style to apply to the underline of inactive tabs.
|
||||
inactive_text_opacity (float, optional): Opacity of the text labels of inactive tabs.
|
||||
animation_duration (float, optional): The duration of the tab change animation, in seconds.
|
||||
animation_function (str, optional): The easing function to use for the tab change animation.
|
||||
tab_padding (int, optional): The padding at the side of each tab. If None, tabs will
|
||||
automatically be padded such that they fit the available horizontal space.
|
||||
search_by_first_character (bool): If True, entering a character on your keyboard
|
||||
search_by_first_character (bool, optional): If True, entering a character on your keyboard
|
||||
will activate the next tab (in left-to-right order) with a label starting with
|
||||
that character.
|
||||
"""
|
||||
|
||||
@@ -6166,6 +6166,163 @@
|
||||
|
||||
'''
|
||||
# ---
|
||||
# name: test_key_display
|
||||
'''
|
||||
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Generated with Rich https://www.textualize.io -->
|
||||
<style>
|
||||
|
||||
@font-face {
|
||||
font-family: "Fira Code";
|
||||
src: local("FiraCode-Regular"),
|
||||
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
|
||||
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Fira Code";
|
||||
src: local("FiraCode-Bold"),
|
||||
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
|
||||
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
|
||||
font-style: bold;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.terminal-1765381587-matrix {
|
||||
font-family: Fira Code, monospace;
|
||||
font-size: 20px;
|
||||
line-height: 24.4px;
|
||||
font-variant-east-asian: full-width;
|
||||
}
|
||||
|
||||
.terminal-1765381587-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
font-family: arial;
|
||||
}
|
||||
|
||||
.terminal-1765381587-r1 { fill: #e1e1e1 }
|
||||
.terminal-1765381587-r2 { fill: #c5c8c6 }
|
||||
.terminal-1765381587-r3 { fill: #dde8f3;font-weight: bold }
|
||||
.terminal-1765381587-r4 { fill: #ddedf9 }
|
||||
</style>
|
||||
|
||||
<defs>
|
||||
<clipPath id="terminal-1765381587-clip-terminal">
|
||||
<rect x="0" y="0" width="975.0" height="584.5999999999999" />
|
||||
</clipPath>
|
||||
<clipPath id="terminal-1765381587-line-0">
|
||||
<rect x="0" y="1.5" width="976" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-1765381587-line-1">
|
||||
<rect x="0" y="25.9" width="976" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-1765381587-line-2">
|
||||
<rect x="0" y="50.3" width="976" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-1765381587-line-3">
|
||||
<rect x="0" y="74.7" width="976" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-1765381587-line-4">
|
||||
<rect x="0" y="99.1" width="976" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-1765381587-line-5">
|
||||
<rect x="0" y="123.5" width="976" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-1765381587-line-6">
|
||||
<rect x="0" y="147.9" width="976" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-1765381587-line-7">
|
||||
<rect x="0" y="172.3" width="976" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-1765381587-line-8">
|
||||
<rect x="0" y="196.7" width="976" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-1765381587-line-9">
|
||||
<rect x="0" y="221.1" width="976" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-1765381587-line-10">
|
||||
<rect x="0" y="245.5" width="976" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-1765381587-line-11">
|
||||
<rect x="0" y="269.9" width="976" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-1765381587-line-12">
|
||||
<rect x="0" y="294.3" width="976" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-1765381587-line-13">
|
||||
<rect x="0" y="318.7" width="976" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-1765381587-line-14">
|
||||
<rect x="0" y="343.1" width="976" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-1765381587-line-15">
|
||||
<rect x="0" y="367.5" width="976" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-1765381587-line-16">
|
||||
<rect x="0" y="391.9" width="976" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-1765381587-line-17">
|
||||
<rect x="0" y="416.3" width="976" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-1765381587-line-18">
|
||||
<rect x="0" y="440.7" width="976" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-1765381587-line-19">
|
||||
<rect x="0" y="465.1" width="976" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-1765381587-line-20">
|
||||
<rect x="0" y="489.5" width="976" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-1765381587-line-21">
|
||||
<rect x="0" y="513.9" width="976" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-1765381587-line-22">
|
||||
<rect x="0" y="538.3" width="976" height="24.65"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
|
||||
<rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="992" height="633.6" rx="8"/><text class="terminal-1765381587-title" fill="#c5c8c6" text-anchor="middle" x="496" y="27">KeyDisplayApp</text>
|
||||
<g transform="translate(26,22)">
|
||||
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
|
||||
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
|
||||
<circle cx="44" cy="0" r="7" fill="#28c840"/>
|
||||
</g>
|
||||
|
||||
<g transform="translate(9, 41)" clip-path="url(#terminal-1765381587-clip-terminal)">
|
||||
<rect fill="#1e1e1e" x="0" y="1.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="25.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="50.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="74.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="99.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="123.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="147.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="172.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="196.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="221.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="245.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="269.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="294.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="318.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="343.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="367.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="391.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="416.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="440.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="465.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="489.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="513.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="538.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#0053aa" x="0" y="562.7" width="36.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#0178d4" x="36.6" y="562.7" width="122" height="24.65" shape-rendering="crispEdges"/><rect fill="#0053aa" x="158.6" y="562.7" width="48.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#0178d4" x="207.4" y="562.7" width="122" height="24.65" shape-rendering="crispEdges"/><rect fill="#0053aa" x="329.4" y="562.7" width="109.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#0178d4" x="439.2" y="562.7" width="97.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#0053aa" x="536.8" y="562.7" width="36.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#0178d4" x="573.4" y="562.7" width="122" height="24.65" shape-rendering="crispEdges"/><rect fill="#0178d4" x="695.4" y="562.7" width="280.6" height="24.65" shape-rendering="crispEdges"/>
|
||||
<g class="terminal-1765381587-matrix">
|
||||
<text class="terminal-1765381587-r2" x="976" y="20" textLength="12.2" clip-path="url(#terminal-1765381587-line-0)">
|
||||
</text><text class="terminal-1765381587-r2" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-1765381587-line-1)">
|
||||
</text><text class="terminal-1765381587-r2" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-1765381587-line-2)">
|
||||
</text><text class="terminal-1765381587-r2" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-1765381587-line-3)">
|
||||
</text><text class="terminal-1765381587-r2" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-1765381587-line-4)">
|
||||
</text><text class="terminal-1765381587-r2" x="976" y="142" textLength="12.2" clip-path="url(#terminal-1765381587-line-5)">
|
||||
</text><text class="terminal-1765381587-r2" x="976" y="166.4" textLength="12.2" clip-path="url(#terminal-1765381587-line-6)">
|
||||
</text><text class="terminal-1765381587-r2" x="976" y="190.8" textLength="12.2" clip-path="url(#terminal-1765381587-line-7)">
|
||||
</text><text class="terminal-1765381587-r2" x="976" y="215.2" textLength="12.2" clip-path="url(#terminal-1765381587-line-8)">
|
||||
</text><text class="terminal-1765381587-r2" x="976" y="239.6" textLength="12.2" clip-path="url(#terminal-1765381587-line-9)">
|
||||
</text><text class="terminal-1765381587-r2" x="976" y="264" textLength="12.2" clip-path="url(#terminal-1765381587-line-10)">
|
||||
</text><text class="terminal-1765381587-r2" x="976" y="288.4" textLength="12.2" clip-path="url(#terminal-1765381587-line-11)">
|
||||
</text><text class="terminal-1765381587-r2" x="976" y="312.8" textLength="12.2" clip-path="url(#terminal-1765381587-line-12)">
|
||||
</text><text class="terminal-1765381587-r2" x="976" y="337.2" textLength="12.2" clip-path="url(#terminal-1765381587-line-13)">
|
||||
</text><text class="terminal-1765381587-r2" x="976" y="361.6" textLength="12.2" clip-path="url(#terminal-1765381587-line-14)">
|
||||
</text><text class="terminal-1765381587-r2" x="976" y="386" textLength="12.2" clip-path="url(#terminal-1765381587-line-15)">
|
||||
</text><text class="terminal-1765381587-r2" x="976" y="410.4" textLength="12.2" clip-path="url(#terminal-1765381587-line-16)">
|
||||
</text><text class="terminal-1765381587-r2" x="976" y="434.8" textLength="12.2" clip-path="url(#terminal-1765381587-line-17)">
|
||||
</text><text class="terminal-1765381587-r2" x="976" y="459.2" textLength="12.2" clip-path="url(#terminal-1765381587-line-18)">
|
||||
</text><text class="terminal-1765381587-r2" x="976" y="483.6" textLength="12.2" clip-path="url(#terminal-1765381587-line-19)">
|
||||
</text><text class="terminal-1765381587-r2" x="976" y="508" textLength="12.2" clip-path="url(#terminal-1765381587-line-20)">
|
||||
</text><text class="terminal-1765381587-r2" x="976" y="532.4" textLength="12.2" clip-path="url(#terminal-1765381587-line-21)">
|
||||
</text><text class="terminal-1765381587-r2" x="976" y="556.8" textLength="12.2" clip-path="url(#terminal-1765381587-line-22)">
|
||||
</text><text class="terminal-1765381587-r3" x="0" y="581.2" textLength="36.6" clip-path="url(#terminal-1765381587-line-23)"> ? </text><text class="terminal-1765381587-r4" x="36.6" y="581.2" textLength="122" clip-path="url(#terminal-1765381587-line-23)"> Question </text><text class="terminal-1765381587-r3" x="158.6" y="581.2" textLength="48.8" clip-path="url(#terminal-1765381587-line-23)"> ^q </text><text class="terminal-1765381587-r4" x="207.4" y="581.2" textLength="122" clip-path="url(#terminal-1765381587-line-23)"> Quit app </text><text class="terminal-1765381587-r3" x="329.4" y="581.2" textLength="109.8" clip-path="url(#terminal-1765381587-line-23)"> Escape! </text><text class="terminal-1765381587-r4" x="439.2" y="581.2" textLength="97.6" clip-path="url(#terminal-1765381587-line-23)"> Escape </text><text class="terminal-1765381587-r3" x="536.8" y="581.2" textLength="36.6" clip-path="url(#terminal-1765381587-line-23)"> A </text><text class="terminal-1765381587-r4" x="573.4" y="581.2" textLength="122" clip-path="url(#terminal-1765381587-line-23)"> Letter A </text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
'''
|
||||
# ---
|
||||
# name: test_layers
|
||||
'''
|
||||
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
36
tests/snapshot_tests/snapshot_apps/key_display.py
Normal file
36
tests/snapshot_tests/snapshot_apps/key_display.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.binding import Binding
|
||||
from textual.widgets import Footer
|
||||
|
||||
|
||||
class KeyDisplayApp(App):
|
||||
"""Tests how keys are displayed in the Footer, and ensures
|
||||
that overriding the key_displays works as expected.
|
||||
Exercises both the built-in Textual key display replacements,
|
||||
and user supplied replacements.
|
||||
Will break when we update the Footer - but we should add a similar
|
||||
test (or updated snapshot) for the updated Footer."""
|
||||
BINDINGS = [
|
||||
Binding("question_mark", "question", "Question"),
|
||||
Binding("ctrl+q", "quit", "Quit app"),
|
||||
Binding("escape", "escape", "Escape"),
|
||||
Binding("a", "a", "Letter A"),
|
||||
]
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Footer()
|
||||
|
||||
def get_key_display(self, key: str) -> str:
|
||||
key_display_replacements = {
|
||||
"escape": "Escape!",
|
||||
"ctrl+q": "^q",
|
||||
}
|
||||
display = key_display_replacements.get(key)
|
||||
if display:
|
||||
return display
|
||||
return super().get_key_display(key)
|
||||
|
||||
|
||||
app = KeyDisplayApp()
|
||||
if __name__ == '__main__':
|
||||
app.run()
|
||||
@@ -118,3 +118,9 @@ def test_css_property(file_name, snap_compare):
|
||||
def test_multiple_css(snap_compare):
|
||||
# Interaction between multiple CSS files and app-level/classvar CSS
|
||||
assert snap_compare("snapshot_apps/multiple_css/multiple_css.py")
|
||||
|
||||
|
||||
# --- Other ---
|
||||
|
||||
def test_key_display(snap_compare):
|
||||
assert snap_compare(SNAPSHOT_APPS_DIR / "key_display.py")
|
||||
|
||||
@@ -11,8 +11,37 @@ skip_py310 = pytest.mark.skipif(
|
||||
)
|
||||
|
||||
|
||||
async def test_installed_screens():
|
||||
class ScreensApp(App):
|
||||
SCREENS = {
|
||||
"home": Screen, # Screen type
|
||||
"one": Screen(), # Screen instance
|
||||
"two": lambda: Screen() # Callable[[], Screen]
|
||||
}
|
||||
|
||||
app = ScreensApp()
|
||||
async with app.run_test() as pilot:
|
||||
pilot.app.push_screen("home") # Instantiates and pushes the "home" screen
|
||||
pilot.app.push_screen("one") # Pushes the pre-instantiated "one" screen
|
||||
pilot.app.push_screen("home") # Pushes the single instance of "home" screen
|
||||
pilot.app.push_screen("two") # Calls the callable, pushes returned Screen instance
|
||||
|
||||
assert len(app.screen_stack) == 5
|
||||
assert app.screen_stack[1] is app.screen_stack[3]
|
||||
assert app.screen is app.screen_stack[4]
|
||||
assert isinstance(app.screen, Screen)
|
||||
assert app.is_screen_installed(app.screen)
|
||||
|
||||
assert pilot.app.pop_screen()
|
||||
assert pilot.app.pop_screen()
|
||||
assert pilot.app.pop_screen()
|
||||
assert pilot.app.pop_screen()
|
||||
with pytest.raises(ScreenStackError):
|
||||
pilot.app.pop_screen()
|
||||
|
||||
|
||||
|
||||
@skip_py310
|
||||
@pytest.mark.asyncio
|
||||
async def test_screens():
|
||||
|
||||
app = App()
|
||||
|
||||
Reference in New Issue
Block a user