mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Merge branch 'main' into datatable-cell-keys
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -110,6 +110,9 @@ venv.bak/
|
|||||||
|
|
||||||
# mkdocs documentation
|
# mkdocs documentation
|
||||||
/site
|
/site
|
||||||
|
/docs-offline
|
||||||
|
/mkdocs-nav-online.yml
|
||||||
|
/mkdocs-nav-offline.yml
|
||||||
|
|
||||||
# mypy
|
# mypy
|
||||||
.mypy_cache/
|
.mypy_cache/
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
|
|
||||||
- Breaking change: `TreeNode` can no longer be imported from `textual.widgets`; it is now available via `from textual.widgets.tree import TreeNode`. https://github.com/Textualize/textual/pull/1637
|
- Breaking change: `TreeNode` can no longer be imported from `textual.widgets`; it is now available via `from textual.widgets.tree import TreeNode`. https://github.com/Textualize/textual/pull/1637
|
||||||
- `Tree` now shows a (subdued) cursor for a highlighted node when focus has moved elsewhere https://github.com/Textualize/textual/issues/1471
|
- `Tree` now shows a (subdued) cursor for a highlighted node when focus has moved elsewhere https://github.com/Textualize/textual/issues/1471
|
||||||
- Breaking change: renamed `Checkbox` to `Switch`.
|
|
||||||
- `DataTable.add_row` now accepts `key` argument to uniquely identify the row https://github.com/Textualize/textual/pull/1638
|
- `DataTable.add_row` now accepts `key` argument to uniquely identify the row https://github.com/Textualize/textual/pull/1638
|
||||||
- `DataTable.add_column` now accepts `key` argument to uniquely identify the column https://github.com/Textualize/textual/pull/1638
|
- `DataTable.add_column` now accepts `key` argument to uniquely identify the column https://github.com/Textualize/textual/pull/1638
|
||||||
- `DataTable.add_row` and `DataTable.add_column` now return lists of keys identifying the added rows/columns https://github.com/Textualize/textual/pull/1638
|
- `DataTable.add_row` and `DataTable.add_column` now return lists of keys identifying the added rows/columns https://github.com/Textualize/textual/pull/1638
|
||||||
@@ -52,6 +51,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
- Breaking change: `DataTable.refresh_cell` was renamed to `DataTable.refresh_coordinate` https://github.com/Textualize/textual/pull/1638
|
- Breaking change: `DataTable.refresh_cell` was renamed to `DataTable.refresh_coordinate` https://github.com/Textualize/textual/pull/1638
|
||||||
- Breaking change: `DataTable.get_row_height` now takes a `RowKey` argument instead of a row index https://github.com/Textualize/textual/pull/1638
|
- Breaking change: `DataTable.get_row_height` now takes a `RowKey` argument instead of a row index https://github.com/Textualize/textual/pull/1638
|
||||||
- The `_filter` module was made public (now called `filter`) https://github.com/Textualize/textual/pull/1638
|
- The `_filter` module was made public (now called `filter`) https://github.com/Textualize/textual/pull/1638
|
||||||
|
- Breaking change: renamed `Checkbox` to `Switch` https://github.com/Textualize/textual/issues/1746
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|||||||
84
Makefile
84
Makefile
@@ -1,20 +1,78 @@
|
|||||||
|
run := poetry run
|
||||||
|
|
||||||
|
.PHONY: test
|
||||||
test:
|
test:
|
||||||
pytest --cov-report term-missing --cov=textual tests/ -vv
|
$(run) pytest --cov-report term-missing --cov=textual tests/ -vv
|
||||||
|
|
||||||
|
.PHONY: unit-test
|
||||||
unit-test:
|
unit-test:
|
||||||
pytest --cov-report term-missing --cov=textual tests/ -vv -m "not integration_test"
|
$(run) pytest --cov-report term-missing --cov=textual tests/ -vv -m "not integration_test"
|
||||||
|
|
||||||
|
.PHONY: test-snapshot-update
|
||||||
test-snapshot-update:
|
test-snapshot-update:
|
||||||
pytest --cov-report term-missing --cov=textual tests/ -vv --snapshot-update
|
$(run) pytest --cov-report term-missing --cov=textual tests/ -vv --snapshot-update
|
||||||
|
|
||||||
|
.PHONY: typecheck
|
||||||
typecheck:
|
typecheck:
|
||||||
mypy src/textual
|
$(run) mypy src/textual
|
||||||
|
|
||||||
|
.PHONY: format
|
||||||
format:
|
format:
|
||||||
black src
|
$(run) black src
|
||||||
|
|
||||||
|
.PHONY: format-check
|
||||||
format-check:
|
format-check:
|
||||||
black --check src
|
$(run) black --check src
|
||||||
docs-serve:
|
|
||||||
|
.PHONY: clean-screenshot-cache
|
||||||
|
clean-screenshot-cache:
|
||||||
rm -rf .screenshot_cache
|
rm -rf .screenshot_cache
|
||||||
mkdocs serve
|
|
||||||
docs-build:
|
.PHONY: docs-offline-nav
|
||||||
mkdocs build
|
docs-offline-nav:
|
||||||
docs-deploy:
|
echo "INHERIT: mkdocs-offline.yml" > mkdocs-nav-offline.yml
|
||||||
rm -rf .screenshot_cache
|
grep -v "\- \"*[Bb]log" mkdocs-nav.yml >> mkdocs-nav-offline.yml
|
||||||
mkdocs gh-deploy
|
|
||||||
|
.PHONY: docs-online-nav
|
||||||
|
docs-online-nav:
|
||||||
|
echo "INHERIT: mkdocs-online.yml" > mkdocs-nav-online.yml
|
||||||
|
cat mkdocs-nav.yml >> mkdocs-nav-online.yml
|
||||||
|
|
||||||
|
.PHONY: docs-serve
|
||||||
|
docs-serve: clean-screenshot-cache docs-online-nav
|
||||||
|
$(run) mkdocs serve --config-file mkdocs-nav-online.yml
|
||||||
|
rm -f mkdocs-nav-online.yml
|
||||||
|
|
||||||
|
.PHONY: docs-build
|
||||||
|
docs-build: docs-online-nav
|
||||||
|
$(run) mkdocs build --config-file mkdocs-nav-online.yml
|
||||||
|
rm -f mkdocs-nav-online.yml
|
||||||
|
|
||||||
|
.PHONY: docs-build-offline
|
||||||
|
docs-build-offline: docs-offline-nav
|
||||||
|
$(run) mkdocs build --config-file mkdocs-nav-offline.yml
|
||||||
|
rm -f mkdocs-nav-offline.yml
|
||||||
|
|
||||||
|
.PHONY: clean-offline-docs
|
||||||
|
clean-offline-docs:
|
||||||
|
rm -rf docs-offline
|
||||||
|
|
||||||
|
.PHONY: docs-deploy
|
||||||
|
docs-deploy: clean-screenshot-cache docs-online-nav
|
||||||
|
$(run) mkdocs gh-deploy --config-file mkdocs-nav-online.yml
|
||||||
|
rm -f mkdocs-nav-online.yml
|
||||||
|
|
||||||
|
.PHONY: build
|
||||||
|
build: docs-build-offline
|
||||||
|
poetry build
|
||||||
|
|
||||||
|
.PHONY: clean
|
||||||
|
clean: clean-screenshot-cache clean-offline-docs
|
||||||
|
|
||||||
|
.PHONY: setup
|
||||||
|
setup:
|
||||||
|
poetry install --extras dev
|
||||||
|
|
||||||
|
.PHONY: update
|
||||||
|
update:
|
||||||
|
poetry update
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
Textual is a Python framework for creating interactive applications that run in your terminal.
|
Textual is a *Rapid Application Development* framework for Python.
|
||||||
|
|
||||||
|
Build sophisticated user interfaces with a simple Python API. Run your apps in the terminal and (coming soon) a web browser!
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary> 🎬 Demonstration </summary>
|
<summary> 🎬 Demonstration </summary>
|
||||||
@@ -22,7 +24,7 @@ https://user-images.githubusercontent.com/554369/197355913-65d3c125-493d-4c05-a5
|
|||||||
|
|
||||||
## About
|
## About
|
||||||
|
|
||||||
Textual adds interactivity to [Rich](https://github.com/Textualize/rich) with a Python API inspired by modern web development.
|
Textual adds interactivity to [Rich](https://github.com/Textualize/rich) with an API inspired by modern web development.
|
||||||
|
|
||||||
On modern terminal software (installed by default on most systems), Textual apps can use **16.7 million** colors with mouse support and smooth flicker-free animation. A powerful layout engine and re-usable components makes it possible to build apps that rival the desktop and web experience.
|
On modern terminal software (installed by default on most systems), Textual apps can use **16.7 million** colors with mouse support and smooth flicker-free animation. A powerful layout engine and re-usable components makes it possible to build apps that rival the desktop and web experience.
|
||||||
|
|
||||||
|
|||||||
BIN
docs/blog/images/async-create-task.jpeg
Normal file
BIN
docs/blog/images/async-create-task.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 286 KiB |
28
docs/blog/posts/create-task-psa.md
Normal file
28
docs/blog/posts/create-task-psa.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
draft: false
|
||||||
|
date: 2023-02-11
|
||||||
|
categories:
|
||||||
|
- DevLog
|
||||||
|
authors:
|
||||||
|
- willmcgugan
|
||||||
|
---
|
||||||
|
|
||||||
|
# The Heisenbug lurking in your async code
|
||||||
|
|
||||||
|
I'm taking a brief break from blogging about [Textual](https://github.com/Textualize/textual) to bring you this brief PSA for Python developers who work with async code. I wanted to expand a little on this [tweet](https://twitter.com/willmcgugan/status/1624419352211603461).
|
||||||
|
|
||||||
|
<!-- more -->
|
||||||
|
|
||||||
|
If you have ever used `asyncio.create_task` you may have created a bug for yourself that is challenging (read *almost impossible*) to reproduce. If it occurs, your code will likely fail in unpredictable ways.
|
||||||
|
|
||||||
|
The root cause of this [Heisenbug](https://en.wikipedia.org/wiki/Heisenbug) is that if you don't hold a reference to the task object returned by `create_task` then the task may disappear without warning when Python runs garbage collection. In other words, the code in your task will stop running with no obvious indication why.
|
||||||
|
|
||||||
|
This behavior is [well documented](https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task), as you can see from this excerpt (emphasis mine):
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
But who reads all the docs? And who has perfect recall if they do? A search on GitHub indicates that there are a [lot of projects](https://github.com/search?q=%22asyncio.create_task%28%22&type=code) where this bug is waiting for just the right moment to ruin somebody's day.
|
||||||
|
|
||||||
|
I suspect the reason this mistake is so common is that tasks are a lot like threads (conceptually at least). With threads you can just launch them and forget. Unless you mark them as "daemon" threads they will exist for the lifetime of your app. Not so with Tasks.
|
||||||
|
|
||||||
|
The solution recommended in the docs is to keep a reference to the task for as long as you need it to live. On modern Python you could use [TaskGroups](https://docs.python.org/3/library/asyncio-task.html#task-groups) which will keep references to your tasks. As long as all the tasks you spin up are in TaskGroups, you should be fine.
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
#dialog {
|
#dialog {
|
||||||
grid-size: 2;
|
grid-size: 2;
|
||||||
grid-gutter: 1 2;
|
grid-gutter: 1 2;
|
||||||
margin: 1 2;
|
padding: 1 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
#question {
|
#question {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ Switch {
|
|||||||
background: darkslategrey;
|
background: darkslategrey;
|
||||||
}
|
}
|
||||||
|
|
||||||
#custom-design > .switch--switch {
|
#custom-design > .switch--slider {
|
||||||
color: dodgerblue;
|
color: dodgerblue;
|
||||||
background: darkslateblue;
|
background: darkslateblue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ Let's look at an example which looks up word definitions from an [api](https://d
|
|||||||
|
|
||||||
=== "Output"
|
=== "Output"
|
||||||
|
|
||||||
```{.textual path="docs/examples/events/dictionary.py" press="t,e,x,t,_,_,_,_,_,_,_,_,_,_,_"}
|
```{.textual path="docs/examples/events/dictionary.py"}
|
||||||
```
|
```
|
||||||
|
|
||||||
Note the highlighted line in the above code which calls `asyncio.create_task` to run a coroutine in the background. Without this you would find typing in to the text box to be unresponsive.
|
Note the highlighted line in the above code which calls `asyncio.create_task` to run a coroutine in the background. Without this you would find typing in to the text box to be unresponsive.
|
||||||
|
|||||||
@@ -81,10 +81,6 @@ Build sophisticated user interfaces with a simple Python API. Run your apps in t
|
|||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
```{.textual path="docs/examples/events/dictionary.py" columns="100" lines="30" press="tab,_,t,e,x,t,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_"}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
```{.textual path="docs/examples/guide/layout/combining_layouts.py" columns="100", lines="30"}
|
```{.textual path="docs/examples/guide/layout/combining_layouts.py" columns="100", lines="30"}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
38
hatch.toml
38
hatch.toml
@@ -1,38 +0,0 @@
|
|||||||
[envs.default]
|
|
||||||
features = ["dev"]
|
|
||||||
dependencies = [
|
|
||||||
"jinja2 < 3.1.0",
|
|
||||||
"pytest >= 7.1.3, <8",
|
|
||||||
"pytest-aiohttp >= 1.0.4, <2",
|
|
||||||
"pytest-cov >= 2.12.1, <3",
|
|
||||||
"syrupy >= 3.0.0, <4",
|
|
||||||
"time-machine >= 2.6.0, <3",
|
|
||||||
]
|
|
||||||
[envs.default.scripts]
|
|
||||||
test = "pytest --cov-report term-missing --cov=textual tests/ -vv"
|
|
||||||
test-snapshot-update = "pytest --cov-report term-missing --cov=textual tests/ -vv --snapshot-update"
|
|
||||||
unit-test = 'pytest --cov-report term-missing --cov=textual tests/ -vv -m "not integration_test"'
|
|
||||||
|
|
||||||
[envs.lint]
|
|
||||||
detached = true
|
|
||||||
dependencies = [
|
|
||||||
"black >= 22.3.0, <23",
|
|
||||||
"mypy >= 0.982, <1",
|
|
||||||
"pre-commit >= 2.12.1, <3",
|
|
||||||
]
|
|
||||||
[envs.lint.scripts]
|
|
||||||
format = "black src"
|
|
||||||
check = "black --check src"
|
|
||||||
typing = "mypy src/textual"
|
|
||||||
|
|
||||||
[envs.docs]
|
|
||||||
dependencies = [
|
|
||||||
"mkdocs >= 1.3.0, <2",
|
|
||||||
"mkdocs-material >= 8.2.15, <9",
|
|
||||||
"mkdocstrings[python] >= 0.19.0, <1",
|
|
||||||
]
|
|
||||||
[envs.docs.scripts]
|
|
||||||
build = "mkdocs build"
|
|
||||||
deploy = "mkdocs gh-deploy"
|
|
||||||
serve = "mkdocs serve"
|
|
||||||
help = "mkdocs --help"
|
|
||||||
100
mkdocs-common.yml
Normal file
100
mkdocs-common.yml
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
site_name: Textual
|
||||||
|
|
||||||
|
markdown_extensions:
|
||||||
|
- attr_list
|
||||||
|
- pymdownx.emoji:
|
||||||
|
emoji_index: !!python/name:materialx.emoji.twemoji
|
||||||
|
emoji_generator: !!python/name:materialx.emoji.to_svg
|
||||||
|
- md_in_html
|
||||||
|
- admonition
|
||||||
|
- def_list
|
||||||
|
- meta
|
||||||
|
|
||||||
|
- toc:
|
||||||
|
permalink: true
|
||||||
|
baselevel: 1
|
||||||
|
- pymdownx.keys
|
||||||
|
- pymdownx.tasklist:
|
||||||
|
custom_checkbox: true
|
||||||
|
- pymdownx.highlight:
|
||||||
|
anchor_linenums: true
|
||||||
|
- pymdownx.inlinehilite
|
||||||
|
- pymdownx.superfences:
|
||||||
|
custom_fences:
|
||||||
|
- name: textual
|
||||||
|
class: textual
|
||||||
|
format: !!python/name:textual._doc.format_svg
|
||||||
|
- name: rich
|
||||||
|
class: rich
|
||||||
|
format: !!python/name:textual._doc.rich
|
||||||
|
- pymdownx.inlinehilite
|
||||||
|
- pymdownx.superfences
|
||||||
|
- pymdownx.snippets
|
||||||
|
- pymdownx.tabbed:
|
||||||
|
alternate_style: true
|
||||||
|
- pymdownx.snippets
|
||||||
|
- markdown.extensions.attr_list
|
||||||
|
|
||||||
|
theme:
|
||||||
|
name: material
|
||||||
|
custom_dir: docs/custom_theme
|
||||||
|
features:
|
||||||
|
- navigation.tabs
|
||||||
|
- navigation.indexes
|
||||||
|
- navigation.tabs.sticky
|
||||||
|
- navigation.footer
|
||||||
|
- content.code.annotate
|
||||||
|
- content.code.copy
|
||||||
|
palette:
|
||||||
|
- media: "(prefers-color-scheme: light)"
|
||||||
|
scheme: default
|
||||||
|
accent: purple
|
||||||
|
toggle:
|
||||||
|
icon: material/weather-sunny
|
||||||
|
name: Switch to dark mode
|
||||||
|
- media: "(prefers-color-scheme: dark)"
|
||||||
|
scheme: slate
|
||||||
|
primary: black
|
||||||
|
toggle:
|
||||||
|
icon: material/weather-night
|
||||||
|
name: Switch to light mode
|
||||||
|
|
||||||
|
plugins:
|
||||||
|
search:
|
||||||
|
autorefs:
|
||||||
|
mkdocstrings:
|
||||||
|
custom_templates: docs/_templates
|
||||||
|
default_handler: python
|
||||||
|
handlers:
|
||||||
|
python:
|
||||||
|
options:
|
||||||
|
show_root_heading: true
|
||||||
|
show_root_full_path: false
|
||||||
|
show_source: false
|
||||||
|
filters:
|
||||||
|
- "!^_"
|
||||||
|
- "^__init__$"
|
||||||
|
- "!^can_replace$"
|
||||||
|
watch:
|
||||||
|
- src/textual
|
||||||
|
exclude:
|
||||||
|
glob:
|
||||||
|
- "**/_template.md"
|
||||||
|
|
||||||
|
|
||||||
|
extra_css:
|
||||||
|
- stylesheets/custom.css
|
||||||
|
|
||||||
|
|
||||||
|
extra:
|
||||||
|
social:
|
||||||
|
- icon: fontawesome/brands/twitter
|
||||||
|
link: https://twitter.com/textualizeio
|
||||||
|
name: textualizeio on Twitter
|
||||||
|
- icon: fontawesome/brands/github
|
||||||
|
link: https://github.com/textualize/textual/
|
||||||
|
name: Textual on Github
|
||||||
|
- icon: fontawesome/brands/discord
|
||||||
|
link: https://discord.gg/Enf6Z3qhVr
|
||||||
|
name: Textual Discord server
|
||||||
|
copyright: Copyright © Textualize, Inc
|
||||||
@@ -1,8 +1,3 @@
|
|||||||
site_name: Textual
|
|
||||||
site_url: https://textual.textualize.io/
|
|
||||||
repo_url: https://github.com/textualize/textual/
|
|
||||||
edit_uri: edit/main/docs/
|
|
||||||
|
|
||||||
nav:
|
nav:
|
||||||
- Introduction:
|
- Introduction:
|
||||||
- "index.md"
|
- "index.md"
|
||||||
@@ -178,114 +173,3 @@ nav:
|
|||||||
- "api/widget.md"
|
- "api/widget.md"
|
||||||
- "Blog":
|
- "Blog":
|
||||||
- blog/index.md
|
- blog/index.md
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
markdown_extensions:
|
|
||||||
- attr_list
|
|
||||||
- pymdownx.emoji:
|
|
||||||
emoji_index: !!python/name:materialx.emoji.twemoji
|
|
||||||
emoji_generator: !!python/name:materialx.emoji.to_svg
|
|
||||||
- md_in_html
|
|
||||||
- admonition
|
|
||||||
- def_list
|
|
||||||
- meta
|
|
||||||
|
|
||||||
- toc:
|
|
||||||
permalink: true
|
|
||||||
baselevel: 1
|
|
||||||
- pymdownx.keys
|
|
||||||
- pymdownx.tasklist:
|
|
||||||
custom_checkbox: true
|
|
||||||
- pymdownx.highlight:
|
|
||||||
anchor_linenums: true
|
|
||||||
- pymdownx.inlinehilite
|
|
||||||
- pymdownx.superfences:
|
|
||||||
custom_fences:
|
|
||||||
- name: textual
|
|
||||||
class: textual
|
|
||||||
format: !!python/name:textual._doc.format_svg
|
|
||||||
- name: rich
|
|
||||||
class: rich
|
|
||||||
format: !!python/name:textual._doc.rich
|
|
||||||
- pymdownx.inlinehilite
|
|
||||||
- pymdownx.superfences
|
|
||||||
- pymdownx.snippets
|
|
||||||
- pymdownx.tabbed:
|
|
||||||
alternate_style: true
|
|
||||||
- pymdownx.snippets
|
|
||||||
- markdown.extensions.attr_list
|
|
||||||
|
|
||||||
theme:
|
|
||||||
name: material
|
|
||||||
custom_dir: docs/custom_theme
|
|
||||||
features:
|
|
||||||
- navigation.tabs
|
|
||||||
- navigation.indexes
|
|
||||||
- navigation.tabs.sticky
|
|
||||||
- navigation.footer
|
|
||||||
- content.code.annotate
|
|
||||||
- content.code.copy
|
|
||||||
palette:
|
|
||||||
- media: "(prefers-color-scheme: light)"
|
|
||||||
scheme: default
|
|
||||||
accent: purple
|
|
||||||
toggle:
|
|
||||||
icon: material/weather-sunny
|
|
||||||
name: Switch to dark mode
|
|
||||||
- media: "(prefers-color-scheme: dark)"
|
|
||||||
scheme: slate
|
|
||||||
primary: black
|
|
||||||
toggle:
|
|
||||||
icon: material/weather-night
|
|
||||||
name: Switch to light mode
|
|
||||||
|
|
||||||
plugins:
|
|
||||||
|
|
||||||
- blog:
|
|
||||||
- rss:
|
|
||||||
match_path: blog/posts/.*
|
|
||||||
date_from_meta:
|
|
||||||
as_creation: date
|
|
||||||
categories:
|
|
||||||
- categories
|
|
||||||
- release
|
|
||||||
- tags
|
|
||||||
- search:
|
|
||||||
- autorefs:
|
|
||||||
- mkdocstrings:
|
|
||||||
custom_templates: docs/_templates
|
|
||||||
default_handler: python
|
|
||||||
handlers:
|
|
||||||
python:
|
|
||||||
options:
|
|
||||||
show_root_heading: true
|
|
||||||
show_root_full_path: false
|
|
||||||
show_source: false
|
|
||||||
filters:
|
|
||||||
- "!^_"
|
|
||||||
- "^__init__$"
|
|
||||||
- "!^can_replace$"
|
|
||||||
watch:
|
|
||||||
- src/textual
|
|
||||||
- exclude:
|
|
||||||
glob:
|
|
||||||
- "**/_template.md"
|
|
||||||
|
|
||||||
|
|
||||||
extra_css:
|
|
||||||
- stylesheets/custom.css
|
|
||||||
|
|
||||||
|
|
||||||
extra:
|
|
||||||
social:
|
|
||||||
- icon: fontawesome/brands/twitter
|
|
||||||
link: https://twitter.com/textualizeio
|
|
||||||
name: textualizeio on Twitter
|
|
||||||
- icon: fontawesome/brands/github
|
|
||||||
link: https://github.com/textualize/textual/
|
|
||||||
name: Textual on Github
|
|
||||||
- icon: fontawesome/brands/discord
|
|
||||||
link: https://discord.gg/Enf6Z3qhVr
|
|
||||||
name: Textual Discord server
|
|
||||||
copyright: Copyright © Textualize, Inc
|
|
||||||
11
mkdocs-offline.yml
Normal file
11
mkdocs-offline.yml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
INHERIT: mkdocs-common.yml
|
||||||
|
|
||||||
|
plugins:
|
||||||
|
offline:
|
||||||
|
privacy:
|
||||||
|
exclude:
|
||||||
|
glob:
|
||||||
|
- "**/_template.md"
|
||||||
|
- blog/*
|
||||||
|
|
||||||
|
site_dir: docs-offline
|
||||||
16
mkdocs-online.yml
Normal file
16
mkdocs-online.yml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
INHERIT: mkdocs-common.yml
|
||||||
|
|
||||||
|
repo_url: https://github.com/textualize/textual/
|
||||||
|
site_url: https://textual.textualize.io/
|
||||||
|
edit_uri: edit/main/docs/
|
||||||
|
|
||||||
|
plugins:
|
||||||
|
blog:
|
||||||
|
rss:
|
||||||
|
match_path: blog/posts/.*
|
||||||
|
date_from_meta:
|
||||||
|
as_creation: date
|
||||||
|
categories:
|
||||||
|
- categories
|
||||||
|
- release
|
||||||
|
- tags
|
||||||
167
poetry.lock
generated
167
poetry.lock
generated
@@ -490,19 +490,21 @@ mkdocs = "*"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mkdocs-material"
|
name = "mkdocs-material"
|
||||||
version = "8.5.11"
|
version = "9.0.11"
|
||||||
description = "Documentation that simply works"
|
description = "Documentation that simply works"
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
jinja2 = ">=3.0.2"
|
colorama = ">=0.4"
|
||||||
|
jinja2 = ">=3.0"
|
||||||
markdown = ">=3.2"
|
markdown = ">=3.2"
|
||||||
mkdocs = ">=1.4.0"
|
mkdocs = ">=1.4.2"
|
||||||
mkdocs-material-extensions = ">=1.1"
|
mkdocs-material-extensions = ">=1.1"
|
||||||
pygments = ">=2.12"
|
pygments = ">=2.14"
|
||||||
pymdown-extensions = ">=9.4"
|
pymdown-extensions = ">=9.9.1"
|
||||||
|
regex = ">=2022.4.24"
|
||||||
requests = ">=2.26"
|
requests = ">=2.26"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -654,7 +656,7 @@ python-versions = ">=3.7"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "platformdirs"
|
name = "platformdirs"
|
||||||
version = "2.6.2"
|
version = "3.0.0"
|
||||||
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
@@ -664,8 +666,8 @@ python-versions = ">=3.7"
|
|||||||
typing-extensions = {version = ">=4.4", markers = "python_version < \"3.8\""}
|
typing-extensions = {version = ">=4.4", markers = "python_version < \"3.8\""}
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"]
|
docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"]
|
||||||
test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"]
|
test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pluggy"
|
name = "pluggy"
|
||||||
@@ -827,6 +829,14 @@ python-versions = ">=3.6"
|
|||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
pyyaml = "*"
|
pyyaml = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex"
|
||||||
|
version = "2022.10.31"
|
||||||
|
description = "Alternative regular expression module, to replace re."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "requests"
|
name = "requests"
|
||||||
version = "2.28.2"
|
version = "2.28.2"
|
||||||
@@ -959,6 +969,25 @@ category = "dev"
|
|||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6"
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "types-docutils"
|
||||||
|
version = "0.19.1.3"
|
||||||
|
description = "Typing stubs for docutils"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "types-setuptools"
|
||||||
|
version = "67.2.0.1"
|
||||||
|
description = "Typing stubs for setuptools"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
types-docutils = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typing-extensions"
|
name = "typing-extensions"
|
||||||
version = "4.4.0"
|
version = "4.4.0"
|
||||||
@@ -990,7 +1019,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "virtualenv"
|
name = "virtualenv"
|
||||||
version = "20.18.0"
|
version = "20.19.0"
|
||||||
description = "Virtual Python Environment builder"
|
description = "Virtual Python Environment builder"
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
@@ -1000,7 +1029,7 @@ python-versions = ">=3.7"
|
|||||||
distlib = ">=0.3.6,<1"
|
distlib = ">=0.3.6,<1"
|
||||||
filelock = ">=3.4.1,<4"
|
filelock = ">=3.4.1,<4"
|
||||||
importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.8\""}
|
importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.8\""}
|
||||||
platformdirs = ">=2.4,<3"
|
platformdirs = ">=2.4,<4"
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"]
|
docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"]
|
||||||
@@ -1032,7 +1061,7 @@ typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""}
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zipp"
|
name = "zipp"
|
||||||
version = "3.12.1"
|
version = "3.13.0"
|
||||||
description = "Backport of pathlib-compatible object wrapper for zip files"
|
description = "Backport of pathlib-compatible object wrapper for zip files"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
@@ -1048,7 +1077,7 @@ dev = ["aiohttp", "click", "msgpack"]
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "1.1"
|
lock-version = "1.1"
|
||||||
python-versions = "^3.7"
|
python-versions = "^3.7"
|
||||||
content-hash = "425fac5cc893af33128a6baf4fc9e296781d322e64eea5d9341d1265c6637d3c"
|
content-hash = "857b1469b81d325df7d76ed9c727e8e0943514be4dae1c0989b65d8e655ab2f2"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
aiohttp = [
|
aiohttp = [
|
||||||
@@ -1483,8 +1512,8 @@ mkdocs-exclude = [
|
|||||||
{file = "mkdocs-exclude-1.0.2.tar.gz", hash = "sha256:ba6fab3c80ddbe3fd31d3e579861fd3124513708271180a5f81846da8c7e2a51"},
|
{file = "mkdocs-exclude-1.0.2.tar.gz", hash = "sha256:ba6fab3c80ddbe3fd31d3e579861fd3124513708271180a5f81846da8c7e2a51"},
|
||||||
]
|
]
|
||||||
mkdocs-material = [
|
mkdocs-material = [
|
||||||
{file = "mkdocs_material-8.5.11-py3-none-any.whl", hash = "sha256:c907b4b052240a5778074a30a78f31a1f8ff82d7012356dc26898b97559f082e"},
|
{file = "mkdocs_material-9.0.11-py3-none-any.whl", hash = "sha256:90a1e1ed41e90de5d0ab97c874b7bf6af488d0faf4aaea8e5868e01f3f1ed923"},
|
||||||
{file = "mkdocs_material-8.5.11.tar.gz", hash = "sha256:b0ea0513fd8cab323e8a825d6692ea07fa83e917bb5db042e523afecc7064ab7"},
|
{file = "mkdocs_material-9.0.11.tar.gz", hash = "sha256:aff49e4ce622a107ed563b3a6a37dc3660a45a0e4d9e7d4d2c13ce9dc02a7faf"},
|
||||||
]
|
]
|
||||||
mkdocs-material-extensions = [
|
mkdocs-material-extensions = [
|
||||||
{file = "mkdocs_material_extensions-1.1.1-py3-none-any.whl", hash = "sha256:e41d9f38e4798b6617ad98ca8f7f1157b1e4385ac1459ca1e4ea219b556df945"},
|
{file = "mkdocs_material_extensions-1.1.1-py3-none-any.whl", hash = "sha256:e41d9f38e4798b6617ad98ca8f7f1157b1e4385ac1459ca1e4ea219b556df945"},
|
||||||
@@ -1685,8 +1714,8 @@ pathspec = [
|
|||||||
{file = "pathspec-0.11.0.tar.gz", hash = "sha256:64d338d4e0914e91c1792321e6907b5a593f1ab1851de7fc269557a21b30ebbc"},
|
{file = "pathspec-0.11.0.tar.gz", hash = "sha256:64d338d4e0914e91c1792321e6907b5a593f1ab1851de7fc269557a21b30ebbc"},
|
||||||
]
|
]
|
||||||
platformdirs = [
|
platformdirs = [
|
||||||
{file = "platformdirs-2.6.2-py3-none-any.whl", hash = "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490"},
|
{file = "platformdirs-3.0.0-py3-none-any.whl", hash = "sha256:b1d5eb14f221506f50d6604a561f4c5786d9e80355219694a1b244bcd96f4567"},
|
||||||
{file = "platformdirs-2.6.2.tar.gz", hash = "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2"},
|
{file = "platformdirs-3.0.0.tar.gz", hash = "sha256:8a1228abb1ef82d788f74139988b137e78692984ec7b08eaa6c65f1723af28f9"},
|
||||||
]
|
]
|
||||||
pluggy = [
|
pluggy = [
|
||||||
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
|
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
|
||||||
@@ -1774,6 +1803,96 @@ pyyaml-env-tag = [
|
|||||||
{file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"},
|
{file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"},
|
||||||
{file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"},
|
{file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"},
|
||||||
]
|
]
|
||||||
|
regex = [
|
||||||
|
{file = "regex-2022.10.31-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a8ff454ef0bb061e37df03557afda9d785c905dab15584860f982e88be73015f"},
|
||||||
|
{file = "regex-2022.10.31-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1eba476b1b242620c266edf6325b443a2e22b633217a9835a52d8da2b5c051f9"},
|
||||||
|
{file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0e5af9a9effb88535a472e19169e09ce750c3d442fb222254a276d77808620b"},
|
||||||
|
{file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d03fe67b2325cb3f09be029fd5da8df9e6974f0cde2c2ac6a79d2634e791dd57"},
|
||||||
|
{file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9d0b68ac1743964755ae2d89772c7e6fb0118acd4d0b7464eaf3921c6b49dd4"},
|
||||||
|
{file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a45b6514861916c429e6059a55cf7db74670eaed2052a648e3e4d04f070e001"},
|
||||||
|
{file = "regex-2022.10.31-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8b0886885f7323beea6f552c28bff62cbe0983b9fbb94126531693ea6c5ebb90"},
|
||||||
|
{file = "regex-2022.10.31-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5aefb84a301327ad115e9d346c8e2760009131d9d4b4c6b213648d02e2abe144"},
|
||||||
|
{file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:702d8fc6f25bbf412ee706bd73019da5e44a8400861dfff7ff31eb5b4a1276dc"},
|
||||||
|
{file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a3c1ebd4ed8e76e886507c9eddb1a891673686c813adf889b864a17fafcf6d66"},
|
||||||
|
{file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:50921c140561d3db2ab9f5b11c5184846cde686bb5a9dc64cae442926e86f3af"},
|
||||||
|
{file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:7db345956ecce0c99b97b042b4ca7326feeec6b75facd8390af73b18e2650ffc"},
|
||||||
|
{file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:763b64853b0a8f4f9cfb41a76a4a85a9bcda7fdda5cb057016e7706fde928e66"},
|
||||||
|
{file = "regex-2022.10.31-cp310-cp310-win32.whl", hash = "sha256:44136355e2f5e06bf6b23d337a75386371ba742ffa771440b85bed367c1318d1"},
|
||||||
|
{file = "regex-2022.10.31-cp310-cp310-win_amd64.whl", hash = "sha256:bfff48c7bd23c6e2aec6454aaf6edc44444b229e94743b34bdcdda2e35126cf5"},
|
||||||
|
{file = "regex-2022.10.31-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b4b1fe58cd102d75ef0552cf17242705ce0759f9695334a56644ad2d83903fe"},
|
||||||
|
{file = "regex-2022.10.31-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:542e3e306d1669b25936b64917285cdffcd4f5c6f0247636fec037187bd93542"},
|
||||||
|
{file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c27cc1e4b197092e50ddbf0118c788d9977f3f8f35bfbbd3e76c1846a3443df7"},
|
||||||
|
{file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8e38472739028e5f2c3a4aded0ab7eadc447f0d84f310c7a8bb697ec417229e"},
|
||||||
|
{file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:76c598ca73ec73a2f568e2a72ba46c3b6c8690ad9a07092b18e48ceb936e9f0c"},
|
||||||
|
{file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c28d3309ebd6d6b2cf82969b5179bed5fefe6142c70f354ece94324fa11bf6a1"},
|
||||||
|
{file = "regex-2022.10.31-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9af69f6746120998cd9c355e9c3c6aec7dff70d47247188feb4f829502be8ab4"},
|
||||||
|
{file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a5f9505efd574d1e5b4a76ac9dd92a12acb2b309551e9aa874c13c11caefbe4f"},
|
||||||
|
{file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5ff525698de226c0ca743bfa71fc6b378cda2ddcf0d22d7c37b1cc925c9650a5"},
|
||||||
|
{file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:4fe7fda2fe7c8890d454f2cbc91d6c01baf206fbc96d89a80241a02985118c0c"},
|
||||||
|
{file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:2cdc55ca07b4e70dda898d2ab7150ecf17c990076d3acd7a5f3b25cb23a69f1c"},
|
||||||
|
{file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:44a6c2f6374e0033873e9ed577a54a3602b4f609867794c1a3ebba65e4c93ee7"},
|
||||||
|
{file = "regex-2022.10.31-cp311-cp311-win32.whl", hash = "sha256:d8716f82502997b3d0895d1c64c3b834181b1eaca28f3f6336a71777e437c2af"},
|
||||||
|
{file = "regex-2022.10.31-cp311-cp311-win_amd64.whl", hash = "sha256:61edbca89aa3f5ef7ecac8c23d975fe7261c12665f1d90a6b1af527bba86ce61"},
|
||||||
|
{file = "regex-2022.10.31-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0a069c8483466806ab94ea9068c34b200b8bfc66b6762f45a831c4baaa9e8cdd"},
|
||||||
|
{file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d26166acf62f731f50bdd885b04b38828436d74e8e362bfcb8df221d868b5d9b"},
|
||||||
|
{file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac741bf78b9bb432e2d314439275235f41656e189856b11fb4e774d9f7246d81"},
|
||||||
|
{file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75f591b2055523fc02a4bbe598aa867df9e953255f0b7f7715d2a36a9c30065c"},
|
||||||
|
{file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b30bddd61d2a3261f025ad0f9ee2586988c6a00c780a2fb0a92cea2aa702c54"},
|
||||||
|
{file = "regex-2022.10.31-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef4163770525257876f10e8ece1cf25b71468316f61451ded1a6f44273eedeb5"},
|
||||||
|
{file = "regex-2022.10.31-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7b280948d00bd3973c1998f92e22aa3ecb76682e3a4255f33e1020bd32adf443"},
|
||||||
|
{file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:d0213671691e341f6849bf33cd9fad21f7b1cb88b89e024f33370733fec58742"},
|
||||||
|
{file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:22e7ebc231d28393dfdc19b185d97e14a0f178bedd78e85aad660e93b646604e"},
|
||||||
|
{file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:8ad241da7fac963d7573cc67a064c57c58766b62a9a20c452ca1f21050868dfa"},
|
||||||
|
{file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:586b36ebda81e6c1a9c5a5d0bfdc236399ba6595e1397842fd4a45648c30f35e"},
|
||||||
|
{file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:0653d012b3bf45f194e5e6a41df9258811ac8fc395579fa82958a8b76286bea4"},
|
||||||
|
{file = "regex-2022.10.31-cp36-cp36m-win32.whl", hash = "sha256:144486e029793a733e43b2e37df16a16df4ceb62102636ff3db6033994711066"},
|
||||||
|
{file = "regex-2022.10.31-cp36-cp36m-win_amd64.whl", hash = "sha256:c14b63c9d7bab795d17392c7c1f9aaabbffd4cf4387725a0ac69109fb3b550c6"},
|
||||||
|
{file = "regex-2022.10.31-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4cac3405d8dda8bc6ed499557625585544dd5cbf32072dcc72b5a176cb1271c8"},
|
||||||
|
{file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23cbb932cc53a86ebde0fb72e7e645f9a5eec1a5af7aa9ce333e46286caef783"},
|
||||||
|
{file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74bcab50a13960f2a610cdcd066e25f1fd59e23b69637c92ad470784a51b1347"},
|
||||||
|
{file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78d680ef3e4d405f36f0d6d1ea54e740366f061645930072d39bca16a10d8c93"},
|
||||||
|
{file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce6910b56b700bea7be82c54ddf2e0ed792a577dfaa4a76b9af07d550af435c6"},
|
||||||
|
{file = "regex-2022.10.31-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:659175b2144d199560d99a8d13b2228b85e6019b6e09e556209dfb8c37b78a11"},
|
||||||
|
{file = "regex-2022.10.31-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1ddf14031a3882f684b8642cb74eea3af93a2be68893901b2b387c5fd92a03ec"},
|
||||||
|
{file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b683e5fd7f74fb66e89a1ed16076dbab3f8e9f34c18b1979ded614fe10cdc4d9"},
|
||||||
|
{file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2bde29cc44fa81c0a0c8686992c3080b37c488df167a371500b2a43ce9f026d1"},
|
||||||
|
{file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4919899577ba37f505aaebdf6e7dc812d55e8f097331312db7f1aab18767cce8"},
|
||||||
|
{file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:9c94f7cc91ab16b36ba5ce476f1904c91d6c92441f01cd61a8e2729442d6fcf5"},
|
||||||
|
{file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ae1e96785696b543394a4e3f15f3f225d44f3c55dafe3f206493031419fedf95"},
|
||||||
|
{file = "regex-2022.10.31-cp37-cp37m-win32.whl", hash = "sha256:c670f4773f2f6f1957ff8a3962c7dd12e4be54d05839b216cb7fd70b5a1df394"},
|
||||||
|
{file = "regex-2022.10.31-cp37-cp37m-win_amd64.whl", hash = "sha256:8e0caeff18b96ea90fc0eb6e3bdb2b10ab5b01a95128dfeccb64a7238decf5f0"},
|
||||||
|
{file = "regex-2022.10.31-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:131d4be09bea7ce2577f9623e415cab287a3c8e0624f778c1d955ec7c281bd4d"},
|
||||||
|
{file = "regex-2022.10.31-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e613a98ead2005c4ce037c7b061f2409a1a4e45099edb0ef3200ee26ed2a69a8"},
|
||||||
|
{file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:052b670fafbe30966bbe5d025e90b2a491f85dfe5b2583a163b5e60a85a321ad"},
|
||||||
|
{file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa62a07ac93b7cb6b7d0389d8ef57ffc321d78f60c037b19dfa78d6b17c928ee"},
|
||||||
|
{file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5352bea8a8f84b89d45ccc503f390a6be77917932b1c98c4cdc3565137acc714"},
|
||||||
|
{file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20f61c9944f0be2dc2b75689ba409938c14876c19d02f7585af4460b6a21403e"},
|
||||||
|
{file = "regex-2022.10.31-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29c04741b9ae13d1e94cf93fca257730b97ce6ea64cfe1eba11cf9ac4e85afb6"},
|
||||||
|
{file = "regex-2022.10.31-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:543883e3496c8b6d58bd036c99486c3c8387c2fc01f7a342b760c1ea3158a318"},
|
||||||
|
{file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7a8b43ee64ca8f4befa2bea4083f7c52c92864d8518244bfa6e88c751fa8fff"},
|
||||||
|
{file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6a9a19bea8495bb419dc5d38c4519567781cd8d571c72efc6aa959473d10221a"},
|
||||||
|
{file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6ffd55b5aedc6f25fd8d9f905c9376ca44fcf768673ffb9d160dd6f409bfda73"},
|
||||||
|
{file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4bdd56ee719a8f751cf5a593476a441c4e56c9b64dc1f0f30902858c4ef8771d"},
|
||||||
|
{file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8ca88da1bd78990b536c4a7765f719803eb4f8f9971cc22d6ca965c10a7f2c4c"},
|
||||||
|
{file = "regex-2022.10.31-cp38-cp38-win32.whl", hash = "sha256:5a260758454580f11dd8743fa98319bb046037dfab4f7828008909d0aa5292bc"},
|
||||||
|
{file = "regex-2022.10.31-cp38-cp38-win_amd64.whl", hash = "sha256:5e6a5567078b3eaed93558842346c9d678e116ab0135e22eb72db8325e90b453"},
|
||||||
|
{file = "regex-2022.10.31-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5217c25229b6a85049416a5c1e6451e9060a1edcf988641e309dbe3ab26d3e49"},
|
||||||
|
{file = "regex-2022.10.31-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4bf41b8b0a80708f7e0384519795e80dcb44d7199a35d52c15cc674d10b3081b"},
|
||||||
|
{file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cf0da36a212978be2c2e2e2d04bdff46f850108fccc1851332bcae51c8907cc"},
|
||||||
|
{file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d403d781b0e06d2922435ce3b8d2376579f0c217ae491e273bab8d092727d244"},
|
||||||
|
{file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a37d51fa9a00d265cf73f3de3930fa9c41548177ba4f0faf76e61d512c774690"},
|
||||||
|
{file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4f781ffedd17b0b834c8731b75cce2639d5a8afe961c1e58ee7f1f20b3af185"},
|
||||||
|
{file = "regex-2022.10.31-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d243b36fbf3d73c25e48014961e83c19c9cc92530516ce3c43050ea6276a2ab7"},
|
||||||
|
{file = "regex-2022.10.31-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:370f6e97d02bf2dd20d7468ce4f38e173a124e769762d00beadec3bc2f4b3bc4"},
|
||||||
|
{file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:597f899f4ed42a38df7b0e46714880fb4e19a25c2f66e5c908805466721760f5"},
|
||||||
|
{file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7dbdce0c534bbf52274b94768b3498abdf675a691fec5f751b6057b3030f34c1"},
|
||||||
|
{file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:22960019a842777a9fa5134c2364efaed5fbf9610ddc5c904bd3a400973b0eb8"},
|
||||||
|
{file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:7f5a3ffc731494f1a57bd91c47dc483a1e10048131ffb52d901bfe2beb6102e8"},
|
||||||
|
{file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7ef6b5942e6bfc5706301a18a62300c60db9af7f6368042227ccb7eeb22d0892"},
|
||||||
|
{file = "regex-2022.10.31-cp39-cp39-win32.whl", hash = "sha256:395161bbdbd04a8333b9ff9763a05e9ceb4fe210e3c7690f5e68cedd3d65d8e1"},
|
||||||
|
{file = "regex-2022.10.31-cp39-cp39-win_amd64.whl", hash = "sha256:957403a978e10fb3ca42572a23e6f7badff39aa1ce2f4ade68ee452dc6807692"},
|
||||||
|
{file = "regex-2022.10.31.tar.gz", hash = "sha256:a3a98921da9a1bf8457aeee6a551948a83601689e5ecdd736894ea9bbec77e83"},
|
||||||
|
]
|
||||||
requests = [
|
requests = [
|
||||||
{file = "requests-2.28.2-py3-none-any.whl", hash = "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa"},
|
{file = "requests-2.28.2-py3-none-any.whl", hash = "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa"},
|
||||||
{file = "requests-2.28.2.tar.gz", hash = "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf"},
|
{file = "requests-2.28.2.tar.gz", hash = "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf"},
|
||||||
@@ -1895,6 +2014,14 @@ typed-ast = [
|
|||||||
{file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"},
|
{file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"},
|
||||||
{file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"},
|
{file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"},
|
||||||
]
|
]
|
||||||
|
types-docutils = [
|
||||||
|
{file = "types-docutils-0.19.1.3.tar.gz", hash = "sha256:36fe30de56f1ece1a9f7a990d47daa781b5af831d2b3f2dcb7dfd01b857cc3d4"},
|
||||||
|
{file = "types_docutils-0.19.1.3-py3-none-any.whl", hash = "sha256:d608e6b91ccf0e8e01c586a0af5b0e0462382d3be65b734af82d40c9d010735d"},
|
||||||
|
]
|
||||||
|
types-setuptools = [
|
||||||
|
{file = "types-setuptools-67.2.0.1.tar.gz", hash = "sha256:07648088bc2cbf0f2745107d394e619ba2a747f68a5904e6e4089c0cb8322065"},
|
||||||
|
{file = "types_setuptools-67.2.0.1-py3-none-any.whl", hash = "sha256:f15b2924122dca5f99a9f6a96a872145721373fe1bb6d656cf269c2a8b73a74b"},
|
||||||
|
]
|
||||||
typing-extensions = [
|
typing-extensions = [
|
||||||
{file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"},
|
{file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"},
|
||||||
{file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"},
|
{file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"},
|
||||||
@@ -1908,8 +2035,8 @@ urllib3 = [
|
|||||||
{file = "urllib3-1.26.14.tar.gz", hash = "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72"},
|
{file = "urllib3-1.26.14.tar.gz", hash = "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72"},
|
||||||
]
|
]
|
||||||
virtualenv = [
|
virtualenv = [
|
||||||
{file = "virtualenv-20.18.0-py3-none-any.whl", hash = "sha256:9d61e4ec8d2c0345dab329fb825eb05579043766a4b26a2f66b28948de68c722"},
|
{file = "virtualenv-20.19.0-py3-none-any.whl", hash = "sha256:54eb59e7352b573aa04d53f80fc9736ed0ad5143af445a1e539aada6eb947dd1"},
|
||||||
{file = "virtualenv-20.18.0.tar.gz", hash = "sha256:f262457a4d7298a6b733b920a196bf8b46c8af15bf1fd9da7142995eff15118e"},
|
{file = "virtualenv-20.19.0.tar.gz", hash = "sha256:37a640ba82ed40b226599c522d411e4be5edb339a0c0de030c0dc7b646d61590"},
|
||||||
]
|
]
|
||||||
watchdog = [
|
watchdog = [
|
||||||
{file = "watchdog-2.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a09483249d25cbdb4c268e020cb861c51baab2d1affd9a6affc68ffe6a231260"},
|
{file = "watchdog-2.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a09483249d25cbdb4c268e020cb861c51baab2d1affd9a6affc68ffe6a231260"},
|
||||||
@@ -2018,6 +2145,6 @@ yarl = [
|
|||||||
{file = "yarl-1.8.2.tar.gz", hash = "sha256:49d43402c6e3013ad0978602bf6bf5328535c48d192304b91b97a3c6790b1562"},
|
{file = "yarl-1.8.2.tar.gz", hash = "sha256:49d43402c6e3013ad0978602bf6bf5328535c48d192304b91b97a3c6790b1562"},
|
||||||
]
|
]
|
||||||
zipp = [
|
zipp = [
|
||||||
{file = "zipp-3.12.1-py3-none-any.whl", hash = "sha256:6c4fe274b8f85ec73c37a8e4e3fa00df9fb9335da96fb789e3b96b318e5097b3"},
|
{file = "zipp-3.13.0-py3-none-any.whl", hash = "sha256:e8b2a36ea17df80ffe9e2c4fda3f693c3dad6df1697d3cd3af232db680950b0b"},
|
||||||
{file = "zipp-3.12.1.tar.gz", hash = "sha256:a3cac813d40993596b39ea9e93a18e8a2076d5c378b8bc88ec32ab264e04ad02"},
|
{file = "zipp-3.13.0.tar.gz", hash = "sha256:23f70e964bc11a34cef175bc90ba2914e1e4545ea1e3e2f67c079671883f9cb6"},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -24,7 +24,13 @@ classifiers = [
|
|||||||
include = [
|
include = [
|
||||||
"src/textual/py.typed",
|
"src/textual/py.typed",
|
||||||
{ path = "docs/examples", format = "sdist" },
|
{ path = "docs/examples", format = "sdist" },
|
||||||
{ path = "tests", format = "sdist" }
|
{ path = "tests", format = "sdist" },
|
||||||
|
# The reason for the slightly convoluted path specification here is that
|
||||||
|
# poetry populates the exclude list with the content of .gitignore, and
|
||||||
|
# it also seems like exclude trumps include. So here we specify that we
|
||||||
|
# want to package up the content of the docs-offline directory in a way
|
||||||
|
# that works around that.
|
||||||
|
{ path = "docs-offline/**/*", format = "sdist" }
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.poetry.scripts]
|
[tool.poetry.scripts]
|
||||||
@@ -54,7 +60,7 @@ mypy = "^1.0.0"
|
|||||||
pytest-cov = "^2.12.1"
|
pytest-cov = "^2.12.1"
|
||||||
mkdocs = "^1.3.0"
|
mkdocs = "^1.3.0"
|
||||||
mkdocstrings = {extras = ["python"], version = "^0.20.0"}
|
mkdocstrings = {extras = ["python"], version = "^0.20.0"}
|
||||||
mkdocs-material = "^8.2.15"
|
mkdocs-material = "^9.0.11"
|
||||||
pre-commit = "^2.13.0"
|
pre-commit = "^2.13.0"
|
||||||
pytest-aiohttp = "^1.0.4"
|
pytest-aiohttp = "^1.0.4"
|
||||||
time-machine = "^2.6.0"
|
time-machine = "^2.6.0"
|
||||||
@@ -63,6 +69,7 @@ syrupy = "^3.0.0"
|
|||||||
mkdocs-rss-plugin = "^1.5.0"
|
mkdocs-rss-plugin = "^1.5.0"
|
||||||
httpx = "^0.23.1"
|
httpx = "^0.23.1"
|
||||||
msgpack-types = "^0.2.0"
|
msgpack-types = "^0.2.0"
|
||||||
|
types-setuptools = "^67.2.0.1"
|
||||||
|
|
||||||
[tool.black]
|
[tool.black]
|
||||||
includes = "src"
|
includes = "src"
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ BORDER_LOCATIONS: dict[
|
|||||||
|
|
||||||
INVISIBLE_EDGE_TYPES = cast("frozenset[EdgeType]", frozenset(("", "none", "hidden")))
|
INVISIBLE_EDGE_TYPES = cast("frozenset[EdgeType]", frozenset(("", "none", "hidden")))
|
||||||
|
|
||||||
BorderValue: TypeAlias = Tuple[EdgeType, Union[str, Color, Style]]
|
BorderValue: TypeAlias = Tuple[EdgeType, Color]
|
||||||
|
|
||||||
BoxSegments: TypeAlias = Tuple[
|
BoxSegments: TypeAlias = Tuple[
|
||||||
Tuple[Segment, Segment, Segment],
|
Tuple[Segment, Segment, Segment],
|
||||||
|
|||||||
@@ -99,7 +99,10 @@ def take_svg_screenshot(
|
|||||||
async def auto_pilot(pilot: Pilot) -> None:
|
async def auto_pilot(pilot: Pilot) -> None:
|
||||||
app = pilot.app
|
app = pilot.app
|
||||||
await pilot.press(*press)
|
await pilot.press(*press)
|
||||||
|
await pilot.wait_for_scheduled_animations()
|
||||||
|
await pilot.pause()
|
||||||
svg = app.export_screenshot(title=title)
|
svg = app.export_screenshot(title=title)
|
||||||
|
|
||||||
app.exit(svg)
|
app.exit(svg)
|
||||||
|
|
||||||
svg = app.run(
|
svg = app.run(
|
||||||
|
|||||||
@@ -1,159 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class TextEditorBackend:
|
|
||||||
"""Represents a text editor (some text and a cursor)"""
|
|
||||||
|
|
||||||
content: str = ""
|
|
||||||
cursor_index: int = 0
|
|
||||||
|
|
||||||
def set_content(self, text: str) -> None:
|
|
||||||
"""Set the content of the editor
|
|
||||||
|
|
||||||
Args:
|
|
||||||
text: The text to set as the content
|
|
||||||
"""
|
|
||||||
self.content = text
|
|
||||||
|
|
||||||
def delete_back(self) -> bool:
|
|
||||||
"""Delete the character behind the cursor and moves cursor back. If the
|
|
||||||
cursor is at the start of the content, does nothing other than immediately
|
|
||||||
return False.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if the text content was modified. False otherwise.
|
|
||||||
"""
|
|
||||||
if self.cursor_index == 0:
|
|
||||||
return False
|
|
||||||
|
|
||||||
new_text = (
|
|
||||||
self.content[: self.cursor_index - 1] + self.content[self.cursor_index :]
|
|
||||||
)
|
|
||||||
self.content = new_text
|
|
||||||
self.cursor_index = max(0, self.cursor_index - 1)
|
|
||||||
return True
|
|
||||||
|
|
||||||
def delete_forward(self) -> bool:
|
|
||||||
"""Delete the character in front of the cursor without moving the cursor.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if the text content was modified. False otherwise.
|
|
||||||
"""
|
|
||||||
if self.cursor_index == len(self.content):
|
|
||||||
return False
|
|
||||||
|
|
||||||
new_text = (
|
|
||||||
self.content[: self.cursor_index] + self.content[self.cursor_index + 1 :]
|
|
||||||
)
|
|
||||||
self.content = new_text
|
|
||||||
return True
|
|
||||||
|
|
||||||
def cursor_left(self) -> bool:
|
|
||||||
"""Move the cursor 1 character left in the text. Is a noop if cursor is at start.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if the cursor moved. False otherwise.
|
|
||||||
"""
|
|
||||||
previous_index = self.cursor_index
|
|
||||||
new_index = max(0, previous_index - 1)
|
|
||||||
self.cursor_index = new_index
|
|
||||||
return previous_index != new_index
|
|
||||||
|
|
||||||
def cursor_right(self) -> bool:
|
|
||||||
"""Move the cursor 1 character right in the text. Is a noop if the cursor is at end.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if the cursor moved. False otherwise.
|
|
||||||
"""
|
|
||||||
previous_index = self.cursor_index
|
|
||||||
new_index = min(len(self.content), previous_index + 1)
|
|
||||||
self.cursor_index = new_index
|
|
||||||
return previous_index != new_index
|
|
||||||
|
|
||||||
def query_cursor_left(self) -> bool:
|
|
||||||
"""Check if the cursor can move 1 codepoint left in the text.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if the cursor can move left. False otherwise.
|
|
||||||
"""
|
|
||||||
previous_index = self.cursor_index
|
|
||||||
new_index = max(0, previous_index - 1)
|
|
||||||
return previous_index != new_index
|
|
||||||
|
|
||||||
def query_cursor_right(self) -> str | None:
|
|
||||||
"""Check if the cursor can move right (we can't move right if we're at the end)
|
|
||||||
and return the codepoint to the right of the cursor if it exists. If it doesn't
|
|
||||||
exist (e.g. we're at the end), then return None
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The codepoint to the right of the cursor if it exists, otherwise None.
|
|
||||||
"""
|
|
||||||
previous_index = self.cursor_index
|
|
||||||
new_index = min(len(self.content), previous_index + 1)
|
|
||||||
if new_index == len(self.content):
|
|
||||||
return None
|
|
||||||
elif previous_index != new_index:
|
|
||||||
return self.content[new_index]
|
|
||||||
return None
|
|
||||||
|
|
||||||
def cursor_text_start(self) -> bool:
|
|
||||||
"""Move the cursor to the start of the text
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if the cursor moved. False otherwise.
|
|
||||||
"""
|
|
||||||
if self.cursor_index == 0:
|
|
||||||
return False
|
|
||||||
|
|
||||||
self.cursor_index = 0
|
|
||||||
return True
|
|
||||||
|
|
||||||
def cursor_text_end(self) -> bool:
|
|
||||||
"""Move the cursor to the end of the text
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if the cursor moved. False otherwise.
|
|
||||||
"""
|
|
||||||
text_length = len(self.content)
|
|
||||||
if self.cursor_index == text_length:
|
|
||||||
return False
|
|
||||||
|
|
||||||
self.cursor_index = text_length
|
|
||||||
return True
|
|
||||||
|
|
||||||
def insert(self, text: str) -> bool:
|
|
||||||
"""Insert some text at the cursor position, and move the cursor
|
|
||||||
to the end of the newly inserted text.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
text: The text to insert
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Always returns True since text should be insertable regardless of cursor location
|
|
||||||
"""
|
|
||||||
new_text = (
|
|
||||||
self.content[: self.cursor_index] + text + self.content[self.cursor_index :]
|
|
||||||
)
|
|
||||||
self.content = new_text
|
|
||||||
self.cursor_index = min(len(self.content), self.cursor_index + len(text))
|
|
||||||
return True
|
|
||||||
|
|
||||||
def get_range(self, start: int, end: int) -> str:
|
|
||||||
"""Return the text between 2 indices. Useful for previews/views into
|
|
||||||
a subset of the content e.g. scrollable single-line input fields
|
|
||||||
|
|
||||||
Args:
|
|
||||||
start: The starting index to return text from (inclusive)
|
|
||||||
end: The index to return text up to (exclusive)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The sliced string between start and end.
|
|
||||||
"""
|
|
||||||
return self.content[start:end]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def cursor_at_end(self):
|
|
||||||
return self.cursor_index == len(self.content)
|
|
||||||
@@ -2,7 +2,7 @@ from asyncio import sleep
|
|||||||
from time import monotonic, process_time
|
from time import monotonic, process_time
|
||||||
|
|
||||||
SLEEP_GRANULARITY: float = 1 / 50
|
SLEEP_GRANULARITY: float = 1 / 50
|
||||||
SLEEP_IDLE: float = SLEEP_GRANULARITY / 2.0
|
SLEEP_IDLE: float = SLEEP_GRANULARITY / 10.0
|
||||||
|
|
||||||
|
|
||||||
async def wait_for_idle(
|
async def wait_for_idle(
|
||||||
|
|||||||
@@ -209,7 +209,11 @@ class _WriterThread(threading.Thread):
|
|||||||
self.join()
|
self.join()
|
||||||
|
|
||||||
|
|
||||||
CSSPathType = Union[str, PurePath, List[Union[str, PurePath]], None]
|
CSSPathType = Union[
|
||||||
|
str,
|
||||||
|
PurePath,
|
||||||
|
List[Union[str, PurePath]],
|
||||||
|
]
|
||||||
|
|
||||||
CallThreadReturnType = TypeVar("CallThreadReturnType")
|
CallThreadReturnType = TypeVar("CallThreadReturnType")
|
||||||
|
|
||||||
@@ -241,7 +245,7 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
|
|
||||||
SCREENS: dict[str, Screen | Callable[[], Screen]] = {}
|
SCREENS: dict[str, Screen | Callable[[], Screen]] = {}
|
||||||
_BASE_PATH: str | None = None
|
_BASE_PATH: str | None = None
|
||||||
CSS_PATH: CSSPathType = None
|
CSS_PATH: CSSPathType | None = None
|
||||||
|
|
||||||
TITLE: str | None = None
|
TITLE: str | None = None
|
||||||
"""str | None: The default title for the application.
|
"""str | None: The default title for the application.
|
||||||
@@ -270,7 +274,7 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
driver_class: Type[Driver] | None = None,
|
driver_class: Type[Driver] | None = None,
|
||||||
css_path: CSSPathType = None,
|
css_path: CSSPathType | None = None,
|
||||||
watch_css: bool = False,
|
watch_css: bool = False,
|
||||||
):
|
):
|
||||||
# N.B. This must be done *before* we call the parent constructor, because MessagePump's
|
# N.B. This must be done *before* we call the parent constructor, because MessagePump's
|
||||||
@@ -348,6 +352,7 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
css_path = css_path or self.CSS_PATH
|
css_path = css_path or self.CSS_PATH
|
||||||
if css_path is not None:
|
if css_path is not None:
|
||||||
# When value(s) are supplied for CSS_PATH, we normalise them to a list of Paths.
|
# When value(s) are supplied for CSS_PATH, we normalise them to a list of Paths.
|
||||||
|
css_paths: List[PurePath]
|
||||||
if isinstance(css_path, str):
|
if isinstance(css_path, str):
|
||||||
css_paths = [Path(css_path)]
|
css_paths = [Path(css_path)]
|
||||||
elif isinstance(css_path, PurePath):
|
elif isinstance(css_path, PurePath):
|
||||||
@@ -355,7 +360,9 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
elif isinstance(css_path, list):
|
elif isinstance(css_path, list):
|
||||||
css_paths = []
|
css_paths = []
|
||||||
for path in css_path:
|
for path in css_path:
|
||||||
css_paths.append(Path(path) if isinstance(path, str) else path)
|
css_paths.append(
|
||||||
|
Path(path) if isinstance(path, str) else path,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
raise CssPathError(
|
raise CssPathError(
|
||||||
"Expected a str, Path or list[str | Path] for the CSS_PATH."
|
"Expected a str, Path or list[str | Path] for the CSS_PATH."
|
||||||
@@ -657,7 +664,7 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
Like asyncio apps in general, Textual apps are not thread-safe. If you call methods
|
Like asyncio apps in general, Textual apps are not thread-safe. If you call methods
|
||||||
or set attributes on Textual objects from a thread, you may get unpredictable results.
|
or set attributes on Textual objects from a thread, you may get unpredictable results.
|
||||||
|
|
||||||
This method will ensure that your code is ran within the correct context.
|
This method will ensure that your code runs within the correct context.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
callback: A callable to run.
|
callback: A callable to run.
|
||||||
@@ -753,7 +760,7 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
svg_filename_stem = f"{self.title.lower()} {dt}"
|
svg_filename_stem = f"{self.title.lower()} {dt}"
|
||||||
for reserved in ' <>:"/\\|?*.':
|
for reserved in ' <>:"/\\|?*.':
|
||||||
svg_filename_stem = svg_filename_stem.replace(reserved, "_")
|
svg_filename_stem = svg_filename_stem.replace(reserved, "_")
|
||||||
svg_filename = svg_filename_stem + ".svg"
|
svg_filename = svg_filename_stem + ".svg"
|
||||||
else:
|
else:
|
||||||
svg_filename = filename
|
svg_filename = filename
|
||||||
svg_path = os.path.expanduser(os.path.join(path, svg_filename))
|
svg_path = os.path.expanduser(os.path.join(path, svg_filename))
|
||||||
@@ -832,7 +839,7 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
driver.send_event(key_event)
|
driver.send_event(key_event)
|
||||||
await wait_for_idle(0)
|
await wait_for_idle(0)
|
||||||
|
|
||||||
await app._animator.wait_for_idle()
|
await app._animator.wait_until_complete()
|
||||||
await wait_for_idle(0)
|
await wait_for_idle(0)
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
@@ -1230,7 +1237,7 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
return screen
|
return screen
|
||||||
|
|
||||||
def push_screen(self, screen: Screen | str) -> AwaitMount:
|
def push_screen(self, screen: Screen | str) -> AwaitMount:
|
||||||
"""Push a new screen on the screen stack.
|
"""Push a new screen on the screen stack, making it the current screen.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
screen: A Screen instance or the name of an installed screen.
|
screen: A Screen instance or the name of an installed screen.
|
||||||
@@ -1261,6 +1268,8 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
def install_screen(self, screen: Screen, name: str | None = None) -> AwaitMount:
|
def install_screen(self, screen: Screen, name: str | None = None) -> AwaitMount:
|
||||||
"""Install a screen.
|
"""Install a screen.
|
||||||
|
|
||||||
|
Installing a screen prevents Textual from destroying it when it is no longer on the screen stack.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
screen: Screen to install.
|
screen: Screen to install.
|
||||||
name: Unique name of screen or None to auto-generate.
|
name: Unique name of screen or None to auto-generate.
|
||||||
|
|||||||
@@ -10,11 +10,12 @@ when setting and getting.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
from typing import TYPE_CHECKING, Generic, Iterable, NamedTuple, TypeVar, cast
|
from typing import TYPE_CHECKING, Generic, Iterable, NamedTuple, Sequence, TypeVar, cast
|
||||||
|
|
||||||
import rich.errors
|
import rich.errors
|
||||||
import rich.repr
|
import rich.repr
|
||||||
from rich.style import Style
|
from rich.style import Style
|
||||||
|
from typing_extensions import TypeAlias
|
||||||
|
|
||||||
from .._border import normalize_border_value
|
from .._border import normalize_border_value
|
||||||
from ..color import Color, ColorParseError
|
from ..color import Color, ColorParseError
|
||||||
@@ -51,7 +52,7 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
from .types import AlignHorizontal, AlignVertical, DockEdge, EdgeType
|
from .types import AlignHorizontal, AlignVertical, DockEdge, EdgeType
|
||||||
|
|
||||||
BorderDefinition = (
|
BorderDefinition: TypeAlias = (
|
||||||
"Sequence[tuple[EdgeType, str | Color] | None] | tuple[EdgeType, str | Color]"
|
"Sequence[tuple[EdgeType, str | Color] | None] | tuple[EdgeType, str | Color]"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -153,7 +154,7 @@ class ScalarProperty:
|
|||||||
Returns:
|
Returns:
|
||||||
The Scalar object or ``None`` if it's not set.
|
The Scalar object or ``None`` if it's not set.
|
||||||
"""
|
"""
|
||||||
return obj.get_rule(self.name)
|
return cast("Scalar | None", obj.get_rule(self.name))
|
||||||
|
|
||||||
def __set__(
|
def __set__(
|
||||||
self, obj: StylesBase, value: float | int | Scalar | str | None
|
self, obj: StylesBase, value: float | int | Scalar | str | None
|
||||||
@@ -233,7 +234,7 @@ class ScalarListProperty:
|
|||||||
def __get__(
|
def __get__(
|
||||||
self, obj: StylesBase, objtype: type[StylesBase] | None = None
|
self, obj: StylesBase, objtype: type[StylesBase] | None = None
|
||||||
) -> tuple[Scalar, ...] | None:
|
) -> tuple[Scalar, ...] | None:
|
||||||
return obj.get_rule(self.name)
|
return cast("tuple[Scalar, ...]", obj.get_rule(self.name))
|
||||||
|
|
||||||
def __set__(
|
def __set__(
|
||||||
self, obj: StylesBase, value: str | Iterable[str | float] | None
|
self, obj: StylesBase, value: str | Iterable[str | float] | None
|
||||||
@@ -289,7 +290,10 @@ class BoxProperty:
|
|||||||
A ``tuple[EdgeType, Style]`` containing the string type of the box and
|
A ``tuple[EdgeType, Style]`` containing the string type of the box and
|
||||||
it's style. Example types are "rounded", "solid", and "dashed".
|
it's style. Example types are "rounded", "solid", and "dashed".
|
||||||
"""
|
"""
|
||||||
return obj.get_rule(self.name) or ("", self._default_color)
|
return cast(
|
||||||
|
"tuple[EdgeType, Color]",
|
||||||
|
obj.get_rule(self.name) or ("", self._default_color),
|
||||||
|
)
|
||||||
|
|
||||||
def __set__(self, obj: Styles, border: tuple[EdgeType, str | Color] | None):
|
def __set__(self, obj: Styles, border: tuple[EdgeType, str | Color] | None):
|
||||||
"""Set the box property.
|
"""Set the box property.
|
||||||
@@ -452,7 +456,7 @@ class BorderProperty:
|
|||||||
check_refresh()
|
check_refresh()
|
||||||
return
|
return
|
||||||
if isinstance(border, tuple) and len(border) == 2:
|
if isinstance(border, tuple) and len(border) == 2:
|
||||||
_border = normalize_border_value(border)
|
_border = normalize_border_value(border) # type: ignore
|
||||||
setattr(obj, top, _border)
|
setattr(obj, top, _border)
|
||||||
setattr(obj, right, _border)
|
setattr(obj, right, _border)
|
||||||
setattr(obj, bottom, _border)
|
setattr(obj, bottom, _border)
|
||||||
@@ -462,15 +466,15 @@ class BorderProperty:
|
|||||||
|
|
||||||
count = len(border)
|
count = len(border)
|
||||||
if count == 1:
|
if count == 1:
|
||||||
_border = normalize_border_value(border[0])
|
_border = normalize_border_value(border[0]) # type: ignore
|
||||||
setattr(obj, top, _border)
|
setattr(obj, top, _border)
|
||||||
setattr(obj, right, _border)
|
setattr(obj, right, _border)
|
||||||
setattr(obj, bottom, _border)
|
setattr(obj, bottom, _border)
|
||||||
setattr(obj, left, _border)
|
setattr(obj, left, _border)
|
||||||
elif count == 2:
|
elif count == 2:
|
||||||
_border1, _border2 = (
|
_border1, _border2 = (
|
||||||
normalize_border_value(border[0]),
|
normalize_border_value(border[0]), # type: ignore
|
||||||
normalize_border_value(border[1]),
|
normalize_border_value(border[1]), # type: ignore
|
||||||
)
|
)
|
||||||
setattr(obj, top, _border1)
|
setattr(obj, top, _border1)
|
||||||
setattr(obj, bottom, _border1)
|
setattr(obj, bottom, _border1)
|
||||||
@@ -478,10 +482,10 @@ class BorderProperty:
|
|||||||
setattr(obj, left, _border2)
|
setattr(obj, left, _border2)
|
||||||
elif count == 4:
|
elif count == 4:
|
||||||
_border1, _border2, _border3, _border4 = (
|
_border1, _border2, _border3, _border4 = (
|
||||||
normalize_border_value(border[0]),
|
normalize_border_value(border[0]), # type: ignore
|
||||||
normalize_border_value(border[1]),
|
normalize_border_value(border[1]), # type: ignore
|
||||||
normalize_border_value(border[2]),
|
normalize_border_value(border[2]), # type: ignore
|
||||||
normalize_border_value(border[3]),
|
normalize_border_value(border[3]), # type: ignore
|
||||||
)
|
)
|
||||||
setattr(obj, top, _border1)
|
setattr(obj, top, _border1)
|
||||||
setattr(obj, right, _border2)
|
setattr(obj, right, _border2)
|
||||||
@@ -513,7 +517,7 @@ class SpacingProperty:
|
|||||||
Returns:
|
Returns:
|
||||||
The Spacing. If unset, returns the null spacing ``(0, 0, 0, 0)``.
|
The Spacing. If unset, returns the null spacing ``(0, 0, 0, 0)``.
|
||||||
"""
|
"""
|
||||||
return obj.get_rule(self.name, NULL_SPACING)
|
return cast(Spacing, obj.get_rule(self.name, NULL_SPACING))
|
||||||
|
|
||||||
def __set__(self, obj: StylesBase, spacing: SpacingDimensions | None):
|
def __set__(self, obj: StylesBase, spacing: SpacingDimensions | None):
|
||||||
"""Set the Spacing.
|
"""Set the Spacing.
|
||||||
@@ -594,7 +598,7 @@ class LayoutProperty:
|
|||||||
Returns:
|
Returns:
|
||||||
The ``Layout`` object.
|
The ``Layout`` object.
|
||||||
"""
|
"""
|
||||||
return obj.get_rule(self.name)
|
return cast("Layout | None", obj.get_rule(self.name))
|
||||||
|
|
||||||
def __set__(self, obj: StylesBase, layout: str | Layout | None):
|
def __set__(self, obj: StylesBase, layout: str | Layout | None):
|
||||||
"""
|
"""
|
||||||
@@ -648,7 +652,7 @@ class OffsetProperty:
|
|||||||
The ``ScalarOffset`` indicating the adjustment that
|
The ``ScalarOffset`` indicating the adjustment that
|
||||||
will be made to widget position prior to it being rendered.
|
will be made to widget position prior to it being rendered.
|
||||||
"""
|
"""
|
||||||
return obj.get_rule(self.name, NULL_SCALAR)
|
return cast("ScalarOffset", obj.get_rule(self.name, NULL_SCALAR))
|
||||||
|
|
||||||
def __set__(
|
def __set__(
|
||||||
self, obj: StylesBase, offset: tuple[int | str, int | str] | ScalarOffset | None
|
self, obj: StylesBase, offset: tuple[int | str, int | str] | ScalarOffset | None
|
||||||
@@ -734,7 +738,7 @@ class StringEnumProperty:
|
|||||||
Returns:
|
Returns:
|
||||||
The string property value.
|
The string property value.
|
||||||
"""
|
"""
|
||||||
return obj.get_rule(self.name, self._default)
|
return cast(str, obj.get_rule(self.name, self._default))
|
||||||
|
|
||||||
def _before_refresh(self, obj: StylesBase, value: str | None) -> None:
|
def _before_refresh(self, obj: StylesBase, value: str | None) -> None:
|
||||||
"""Do any housekeeping before asking for a layout refresh after a value change."""
|
"""Do any housekeeping before asking for a layout refresh after a value change."""
|
||||||
@@ -795,7 +799,7 @@ class NameProperty:
|
|||||||
Returns:
|
Returns:
|
||||||
The name.
|
The name.
|
||||||
"""
|
"""
|
||||||
return obj.get_rule(self.name, "")
|
return cast(str, obj.get_rule(self.name, ""))
|
||||||
|
|
||||||
def __set__(self, obj: StylesBase, name: str | None):
|
def __set__(self, obj: StylesBase, name: str | None):
|
||||||
"""Set the name property.
|
"""Set the name property.
|
||||||
@@ -929,7 +933,7 @@ class StyleFlagsProperty:
|
|||||||
Returns:
|
Returns:
|
||||||
The ``Style`` object.
|
The ``Style`` object.
|
||||||
"""
|
"""
|
||||||
return obj.get_rule(self.name, Style.null())
|
return cast(Style, obj.get_rule(self.name, Style.null()))
|
||||||
|
|
||||||
def __set__(self, obj: StylesBase, style_flags: Style | str | None):
|
def __set__(self, obj: StylesBase, style_flags: Style | str | None):
|
||||||
"""Set the style using a style flag string.
|
"""Set the style using a style flag string.
|
||||||
@@ -992,7 +996,7 @@ class TransitionsProperty:
|
|||||||
e.g. ``{"offset": Transition(...), ...}``. If no transitions have been set, an empty ``dict``
|
e.g. ``{"offset": Transition(...), ...}``. If no transitions have been set, an empty ``dict``
|
||||||
is returned.
|
is returned.
|
||||||
"""
|
"""
|
||||||
return obj.get_rule("transitions", {})
|
return cast("dict[str, Transition]", obj.get_rule("transitions", {}))
|
||||||
|
|
||||||
def __set__(self, obj: Styles, transitions: dict[str, Transition] | None) -> None:
|
def __set__(self, obj: Styles, transitions: dict[str, Transition] | None) -> None:
|
||||||
_rich_traceback_omit = True
|
_rich_traceback_omit = True
|
||||||
|
|||||||
@@ -113,7 +113,6 @@ class StylesBuilder:
|
|||||||
suggested_property_name=suggested_property_name,
|
suggested_property_name=suggested_property_name,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return
|
|
||||||
|
|
||||||
tokens = declaration.tokens
|
tokens = declaration.tokens
|
||||||
|
|
||||||
@@ -182,7 +181,13 @@ class StylesBuilder:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
if len(tokens) != 1:
|
if len(tokens) != 1:
|
||||||
string_enum_help_text(name, valid_values=list(valid_values), context="css"),
|
self.error(
|
||||||
|
name,
|
||||||
|
tokens[0],
|
||||||
|
string_enum_help_text(
|
||||||
|
name, valid_values=list(valid_values), context="css"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
token = tokens[0]
|
token = tokens[0]
|
||||||
token_name, value, _, _, location, _ = token
|
token_name, value, _, _, location, _ = token
|
||||||
@@ -239,7 +244,7 @@ class StylesBuilder:
|
|||||||
return
|
return
|
||||||
if len(tokens) == 1:
|
if len(tokens) == 1:
|
||||||
try:
|
try:
|
||||||
self.styles._rules[name.replace("-", "_")] = Scalar.parse(
|
self.styles._rules[name.replace("-", "_")] = Scalar.parse( # type: ignore
|
||||||
tokens[0].value
|
tokens[0].value
|
||||||
)
|
)
|
||||||
except ScalarParseError:
|
except ScalarParseError:
|
||||||
@@ -390,7 +395,7 @@ class StylesBuilder:
|
|||||||
name, num_values_supplied=len(space), context="css"
|
name, num_values_supplied=len(space), context="css"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
self.styles._rules[name] = Spacing.unpack(cast(SpacingDimensions, tuple(space)))
|
self.styles._rules[name] = Spacing.unpack(cast(SpacingDimensions, tuple(space))) # type: ignore
|
||||||
|
|
||||||
def _process_space_partial(self, name: str, tokens: list[Token]) -> None:
|
def _process_space_partial(self, name: str, tokens: list[Token]) -> None:
|
||||||
"""Process granular margin / padding declarations."""
|
"""Process granular margin / padding declarations."""
|
||||||
@@ -418,7 +423,7 @@ class StylesBuilder:
|
|||||||
spacing_list = list(current_spacing)
|
spacing_list = list(current_spacing)
|
||||||
spacing_list[_EDGE_SPACING_MAP[edge]] = space
|
spacing_list[_EDGE_SPACING_MAP[edge]] = space
|
||||||
|
|
||||||
self.styles._rules[style_name] = Spacing(*spacing_list)
|
self.styles._rules[style_name] = Spacing(*spacing_list) # type: ignore
|
||||||
|
|
||||||
process_padding = _process_space
|
process_padding = _process_space
|
||||||
process_margin = _process_space
|
process_margin = _process_space
|
||||||
@@ -444,7 +449,7 @@ class StylesBuilder:
|
|||||||
token_name, value, _, _, _, _ = token
|
token_name, value, _, _, _, _ = token
|
||||||
if token_name == "token":
|
if token_name == "token":
|
||||||
if value in VALID_BORDER:
|
if value in VALID_BORDER:
|
||||||
border_type = value
|
border_type = value # type: ignore
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
border_color = Color.parse(value)
|
border_color = Color.parse(value)
|
||||||
@@ -464,7 +469,7 @@ class StylesBuilder:
|
|||||||
|
|
||||||
def _process_border_edge(self, edge: str, name: str, tokens: list[Token]) -> None:
|
def _process_border_edge(self, edge: str, name: str, tokens: list[Token]) -> None:
|
||||||
border = self._parse_border(name, tokens)
|
border = self._parse_border(name, tokens)
|
||||||
self.styles._rules[f"border_{edge}"] = border
|
self.styles._rules[f"border_{edge}"] = border # type: ignore
|
||||||
|
|
||||||
def process_border(self, name: str, tokens: list[Token]) -> None:
|
def process_border(self, name: str, tokens: list[Token]) -> None:
|
||||||
border = self._parse_border(name, tokens)
|
border = self._parse_border(name, tokens)
|
||||||
@@ -486,7 +491,7 @@ class StylesBuilder:
|
|||||||
|
|
||||||
def _process_outline(self, edge: str, name: str, tokens: list[Token]) -> None:
|
def _process_outline(self, edge: str, name: str, tokens: list[Token]) -> None:
|
||||||
border = self._parse_border(name, tokens)
|
border = self._parse_border(name, tokens)
|
||||||
self.styles._rules[f"outline_{edge}"] = border
|
self.styles._rules[f"outline_{edge}"] = border # type: ignore
|
||||||
|
|
||||||
def process_outline(self, name: str, tokens: list[Token]) -> None:
|
def process_outline(self, name: str, tokens: list[Token]) -> None:
|
||||||
border = self._parse_border(name, tokens)
|
border = self._parse_border(name, tokens)
|
||||||
@@ -579,14 +584,14 @@ class StylesBuilder:
|
|||||||
color: Color | None = None
|
color: Color | None = None
|
||||||
alpha: float | None = None
|
alpha: float | None = None
|
||||||
|
|
||||||
self.styles._rules[f"auto_{name}"] = False
|
self.styles._rules[f"auto_{name}"] = False # type: ignore
|
||||||
for token in tokens:
|
for token in tokens:
|
||||||
if (
|
if (
|
||||||
"background" not in name
|
"background" not in name
|
||||||
and token.name == "token"
|
and token.name == "token"
|
||||||
and token.value == "auto"
|
and token.value == "auto"
|
||||||
):
|
):
|
||||||
self.styles._rules[f"auto_{name}"] = True
|
self.styles._rules[f"auto_{name}"] = True # type: ignore
|
||||||
elif token.name == "scalar":
|
elif token.name == "scalar":
|
||||||
alpha_scalar = Scalar.parse(token.value)
|
alpha_scalar = Scalar.parse(token.value)
|
||||||
if alpha_scalar.unit != Unit.PERCENT:
|
if alpha_scalar.unit != Unit.PERCENT:
|
||||||
@@ -608,7 +613,7 @@ class StylesBuilder:
|
|||||||
if color is not None or alpha is not None:
|
if color is not None or alpha is not None:
|
||||||
if alpha is not None:
|
if alpha is not None:
|
||||||
color = (color or Color(255, 255, 255)).with_alpha(alpha)
|
color = (color or Color(255, 255, 255)).with_alpha(alpha)
|
||||||
self.styles._rules[name] = color
|
self.styles._rules[name] = color # type: ignore
|
||||||
|
|
||||||
process_tint = process_color
|
process_tint = process_color
|
||||||
process_background = process_color
|
process_background = process_color
|
||||||
@@ -636,7 +641,7 @@ class StylesBuilder:
|
|||||||
)
|
)
|
||||||
|
|
||||||
style_definition = " ".join(token.value for token in tokens)
|
style_definition = " ".join(token.value for token in tokens)
|
||||||
self.styles._rules[name.replace("-", "_")] = style_definition
|
self.styles._rules[name.replace("-", "_")] = style_definition # type: ignore
|
||||||
|
|
||||||
process_link_style = process_text_style
|
process_link_style = process_text_style
|
||||||
process_link_hover_style = process_text_style
|
process_link_hover_style = process_text_style
|
||||||
@@ -653,7 +658,7 @@ class StylesBuilder:
|
|||||||
text_align_help_text(),
|
text_align_help_text(),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.styles._rules["text_align"] = tokens[0].value
|
self.styles._rules["text_align"] = tokens[0].value # type: ignore
|
||||||
|
|
||||||
def process_dock(self, name: str, tokens: list[Token]) -> None:
|
def process_dock(self, name: str, tokens: list[Token]) -> None:
|
||||||
if not tokens:
|
if not tokens:
|
||||||
@@ -766,8 +771,8 @@ class StylesBuilder:
|
|||||||
align_error(name, token_horizontal)
|
align_error(name, token_horizontal)
|
||||||
|
|
||||||
name = name.replace("-", "_")
|
name = name.replace("-", "_")
|
||||||
self.styles._rules[f"{name}_horizontal"] = token_horizontal.value
|
self.styles._rules[f"{name}_horizontal"] = token_horizontal.value # type: ignore
|
||||||
self.styles._rules[f"{name}_vertical"] = token_vertical.value
|
self.styles._rules[f"{name}_vertical"] = token_vertical.value # type: ignore
|
||||||
|
|
||||||
def process_align_horizontal(self, name: str, tokens: list[Token]) -> None:
|
def process_align_horizontal(self, name: str, tokens: list[Token]) -> None:
|
||||||
try:
|
try:
|
||||||
@@ -779,7 +784,7 @@ class StylesBuilder:
|
|||||||
string_enum_help_text(name, VALID_ALIGN_HORIZONTAL, context="css"),
|
string_enum_help_text(name, VALID_ALIGN_HORIZONTAL, context="css"),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.styles._rules[name.replace("-", "_")] = value
|
self.styles._rules[name.replace("-", "_")] = value # type: ignore
|
||||||
|
|
||||||
def process_align_vertical(self, name: str, tokens: list[Token]) -> None:
|
def process_align_vertical(self, name: str, tokens: list[Token]) -> None:
|
||||||
try:
|
try:
|
||||||
@@ -791,7 +796,7 @@ class StylesBuilder:
|
|||||||
string_enum_help_text(name, VALID_ALIGN_VERTICAL, context="css"),
|
string_enum_help_text(name, VALID_ALIGN_VERTICAL, context="css"),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.styles._rules[name.replace("-", "_")] = value
|
self.styles._rules[name.replace("-", "_")] = value # type: ignore
|
||||||
|
|
||||||
process_content_align = process_align
|
process_content_align = process_align
|
||||||
process_content_align_horizontal = process_align_horizontal
|
process_content_align_horizontal = process_align_horizontal
|
||||||
@@ -807,7 +812,7 @@ class StylesBuilder:
|
|||||||
string_enum_help_text(name, VALID_SCROLLBAR_GUTTER, context="css"),
|
string_enum_help_text(name, VALID_SCROLLBAR_GUTTER, context="css"),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.styles._rules[name.replace("-", "_")] = value
|
self.styles._rules[name.replace("-", "_")] = value # type: ignore
|
||||||
|
|
||||||
def process_scrollbar_size(self, name: str, tokens: list[Token]) -> None:
|
def process_scrollbar_size(self, name: str, tokens: list[Token]) -> None:
|
||||||
def scrollbar_size_error(name: str, token: Token) -> None:
|
def scrollbar_size_error(name: str, token: Token) -> None:
|
||||||
@@ -876,7 +881,7 @@ class StylesBuilder:
|
|||||||
token,
|
token,
|
||||||
table_rows_or_columns_help_text(name, token.value, context="css"),
|
table_rows_or_columns_help_text(name, token.value, context="css"),
|
||||||
)
|
)
|
||||||
self.styles._rules[name.replace("-", "_")] = scalars
|
self.styles._rules[name.replace("-", "_")] = scalars # type: ignore
|
||||||
|
|
||||||
process_grid_rows = _process_grid_rows_or_columns
|
process_grid_rows = _process_grid_rows_or_columns
|
||||||
process_grid_columns = _process_grid_rows_or_columns
|
process_grid_columns = _process_grid_rows_or_columns
|
||||||
@@ -893,7 +898,7 @@ class StylesBuilder:
|
|||||||
value = int(token.value)
|
value = int(token.value)
|
||||||
if value == 0:
|
if value == 0:
|
||||||
self.error(name, token, integer_help_text(name))
|
self.error(name, token, integer_help_text(name))
|
||||||
self.styles._rules[name.replace("-", "_")] = value
|
self.styles._rules[name.replace("-", "_")] = value # type: ignore
|
||||||
|
|
||||||
process_grid_gutter_horizontal = _process_integer
|
process_grid_gutter_horizontal = _process_integer
|
||||||
process_grid_gutter_vertical = _process_integer
|
process_grid_gutter_vertical = _process_integer
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Iterable
|
|||||||
|
|
||||||
import rich.repr
|
import rich.repr
|
||||||
|
|
||||||
|
from ._help_renderables import HelpText
|
||||||
from .styles import Styles
|
from .styles import Styles
|
||||||
from .tokenize import Token
|
from .tokenize import Token
|
||||||
from .types import Specificity3
|
from .types import Specificity3
|
||||||
@@ -155,7 +156,7 @@ class SelectorSet:
|
|||||||
class RuleSet:
|
class RuleSet:
|
||||||
selector_set: list[SelectorSet] = field(default_factory=list)
|
selector_set: list[SelectorSet] = field(default_factory=list)
|
||||||
styles: Styles = field(default_factory=Styles)
|
styles: Styles = field(default_factory=Styles)
|
||||||
errors: list[tuple[Token, str]] = field(default_factory=list)
|
errors: list[tuple[Token, str | HelpText]] = field(default_factory=list)
|
||||||
|
|
||||||
is_default_rules: bool = False
|
is_default_rules: bool = False
|
||||||
tie_breaker: int = 0
|
tie_breaker: int = 0
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from pathlib import PurePath
|
|||||||
from typing import Iterable, Iterator, NoReturn
|
from typing import Iterable, Iterator, NoReturn
|
||||||
|
|
||||||
from ..suggestions import get_suggestion
|
from ..suggestions import get_suggestion
|
||||||
|
from ._help_renderables import HelpText
|
||||||
from ._styles_builder import DeclarationError, StylesBuilder
|
from ._styles_builder import DeclarationError, StylesBuilder
|
||||||
from .errors import UnresolvedVariableError
|
from .errors import UnresolvedVariableError
|
||||||
from .model import (
|
from .model import (
|
||||||
@@ -130,7 +131,7 @@ def parse_rule_set(
|
|||||||
|
|
||||||
declaration = Declaration(token, "")
|
declaration = Declaration(token, "")
|
||||||
|
|
||||||
errors: list[tuple[Token, str]] = []
|
errors: list[tuple[Token, str | HelpText]] = []
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
token = next(tokens)
|
token = next(tokens)
|
||||||
@@ -182,7 +183,7 @@ def parse_declarations(css: str, path: str) -> Styles:
|
|||||||
styles_builder = StylesBuilder()
|
styles_builder = StylesBuilder()
|
||||||
|
|
||||||
declaration: Declaration | None = None
|
declaration: Declaration | None = None
|
||||||
errors: list[tuple[Token, str]] = []
|
errors: list[tuple[Token, str | HelpText]] = []
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
token = next(tokens, None)
|
token = next(tokens, None)
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ class DOMQuery(Generic[QueryType]):
|
|||||||
# The IndexError was got, that's a good thing in this case. So
|
# The IndexError was got, that's a good thing in this case. So
|
||||||
# we return what we found.
|
# we return what we found.
|
||||||
pass
|
pass
|
||||||
return the_one
|
return cast("Widget", the_one)
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def last(self) -> Widget:
|
def last(self) -> Widget:
|
||||||
|
|||||||
@@ -367,8 +367,8 @@ class StylesBase(ABC):
|
|||||||
def auto_dimensions(self) -> bool:
|
def auto_dimensions(self) -> bool:
|
||||||
"""Check if width or height are set to 'auto'."""
|
"""Check if width or height are set to 'auto'."""
|
||||||
has_rule = self.has_rule
|
has_rule = self.has_rule
|
||||||
return (has_rule("width") and self.width.is_auto) or (
|
return (has_rule("width") and self.width.is_auto) or ( # type: ignore
|
||||||
has_rule("height") and self.height.is_auto
|
has_rule("height") and self.height.is_auto # type: ignore
|
||||||
)
|
)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
@@ -474,7 +474,7 @@ class StylesBase(ABC):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@lru_cache(maxsize=1024)
|
@lru_cache(maxsize=1024)
|
||||||
def parse(cls, css: str, path: str, *, node: DOMNode = None) -> Styles:
|
def parse(cls, css: str, path: str, *, node: DOMNode | None = None) -> Styles:
|
||||||
"""Parse CSS and return a Styles object.
|
"""Parse CSS and return a Styles object.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -603,7 +603,7 @@ class Styles(StylesBase):
|
|||||||
Returns:
|
Returns:
|
||||||
``True`` if a rule was cleared, or ``False`` if it was already not set.
|
``True`` if a rule was cleared, or ``False`` if it was already not set.
|
||||||
"""
|
"""
|
||||||
changed = self._rules.pop(rule, None) is not None
|
changed = self._rules.pop(rule, None) is not None # type: ignore
|
||||||
if changed:
|
if changed:
|
||||||
self._updates += 1
|
self._updates += 1
|
||||||
return changed
|
return changed
|
||||||
@@ -622,12 +622,12 @@ class Styles(StylesBase):
|
|||||||
``True`` if the rule changed, otherwise ``False``.
|
``True`` if the rule changed, otherwise ``False``.
|
||||||
"""
|
"""
|
||||||
if value is None:
|
if value is None:
|
||||||
changed = self._rules.pop(rule, None) is not None
|
changed = self._rules.pop(rule, None) is not None # type: ignore
|
||||||
if changed:
|
if changed:
|
||||||
self._updates += 1
|
self._updates += 1
|
||||||
return changed
|
return changed
|
||||||
current = self._rules.get(rule)
|
current = self._rules.get(rule)
|
||||||
self._rules[rule] = value
|
self._rules[rule] = value # type: ignore
|
||||||
changed = current != value
|
changed = current != value
|
||||||
if changed:
|
if changed:
|
||||||
self._updates += 1
|
self._updates += 1
|
||||||
@@ -646,7 +646,7 @@ class Styles(StylesBase):
|
|||||||
def reset(self) -> None:
|
def reset(self) -> None:
|
||||||
"""Reset the rules to initial state."""
|
"""Reset the rules to initial state."""
|
||||||
self._updates += 1
|
self._updates += 1
|
||||||
self._rules.clear()
|
self._rules.clear() # type: ignore
|
||||||
|
|
||||||
def merge(self, other: Styles) -> None:
|
def merge(self, other: Styles) -> None:
|
||||||
"""Merge values from another Styles.
|
"""Merge values from another Styles.
|
||||||
@@ -736,25 +736,25 @@ class Styles(StylesBase):
|
|||||||
left = get_rule(f"{name}_left")
|
left = get_rule(f"{name}_left")
|
||||||
|
|
||||||
if top == right and right == bottom and bottom == left:
|
if top == right and right == bottom and bottom == left:
|
||||||
border_type, border_color = rules[f"{name}_top"]
|
border_type, border_color = rules[f"{name}_top"] # type: ignore
|
||||||
yield name, f"{border_type} {border_color.hex}"
|
yield name, f"{border_type} {border_color.hex}"
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check for edges
|
# Check for edges
|
||||||
if has_top:
|
if has_top:
|
||||||
border_type, border_color = rules[f"{name}_top"]
|
border_type, border_color = rules[f"{name}_top"] # type: ignore
|
||||||
yield f"{name}-top", f"{border_type} {border_color.hex}"
|
yield f"{name}-top", f"{border_type} {border_color.hex}"
|
||||||
|
|
||||||
if has_right:
|
if has_right:
|
||||||
border_type, border_color = rules[f"{name}_right"]
|
border_type, border_color = rules[f"{name}_right"] # type: ignore
|
||||||
yield f"{name}-right", f"{border_type} {border_color.hex}"
|
yield f"{name}-right", f"{border_type} {border_color.hex}"
|
||||||
|
|
||||||
if has_bottom:
|
if has_bottom:
|
||||||
border_type, border_color = rules[f"{name}_bottom"]
|
border_type, border_color = rules[f"{name}_bottom"] # type: ignore
|
||||||
yield f"{name}-bottom", f"{border_type} {border_color.hex}"
|
yield f"{name}-bottom", f"{border_type} {border_color.hex}"
|
||||||
|
|
||||||
if has_left:
|
if has_left:
|
||||||
border_type, border_color = rules[f"{name}_left"]
|
border_type, border_color = rules[f"{name}_left"] # type: ignore
|
||||||
yield f"{name}-left", f"{border_type} {border_color.hex}"
|
yield f"{name}-left", f"{border_type} {border_color.hex}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -770,15 +770,14 @@ class Styles(StylesBase):
|
|||||||
|
|
||||||
rules = self.get_rules()
|
rules = self.get_rules()
|
||||||
get_rule = rules.get
|
get_rule = rules.get
|
||||||
has_rule = rules.__contains__
|
|
||||||
|
|
||||||
if has_rule("display"):
|
if "display" in rules:
|
||||||
append_declaration("display", rules["display"])
|
append_declaration("display", rules["display"])
|
||||||
if has_rule("visibility"):
|
if "visibility" in rules:
|
||||||
append_declaration("visibility", rules["visibility"])
|
append_declaration("visibility", rules["visibility"])
|
||||||
if has_rule("padding"):
|
if "padding" in rules:
|
||||||
append_declaration("padding", rules["padding"].css)
|
append_declaration("padding", rules["padding"].css)
|
||||||
if has_rule("margin"):
|
if "margin" in rules:
|
||||||
append_declaration("margin", rules["margin"].css)
|
append_declaration("margin", rules["margin"].css)
|
||||||
|
|
||||||
for name, rule in self._get_border_css_lines(rules, "border"):
|
for name, rule in self._get_border_css_lines(rules, "border"):
|
||||||
@@ -787,90 +786,90 @@ class Styles(StylesBase):
|
|||||||
for name, rule in self._get_border_css_lines(rules, "outline"):
|
for name, rule in self._get_border_css_lines(rules, "outline"):
|
||||||
append_declaration(name, rule)
|
append_declaration(name, rule)
|
||||||
|
|
||||||
if has_rule("offset"):
|
if "offset" in rules:
|
||||||
x, y = self.offset
|
x, y = self.offset
|
||||||
append_declaration("offset", f"{x} {y}")
|
append_declaration("offset", f"{x} {y}")
|
||||||
if has_rule("dock"):
|
if "dock" in rules:
|
||||||
append_declaration("dock", rules["dock"])
|
append_declaration("dock", rules["dock"])
|
||||||
if has_rule("layers"):
|
if "layers" in rules:
|
||||||
append_declaration("layers", " ".join(self.layers))
|
append_declaration("layers", " ".join(self.layers))
|
||||||
if has_rule("layer"):
|
if "layer" in rules:
|
||||||
append_declaration("layer", self.layer)
|
append_declaration("layer", self.layer)
|
||||||
if has_rule("layout"):
|
if "layout" in rules:
|
||||||
assert self.layout is not None
|
assert self.layout is not None
|
||||||
append_declaration("layout", self.layout.name)
|
append_declaration("layout", self.layout.name)
|
||||||
|
|
||||||
if has_rule("color"):
|
if "color" in rules:
|
||||||
append_declaration("color", self.color.hex)
|
append_declaration("color", self.color.hex)
|
||||||
if has_rule("background"):
|
if "background" in rules:
|
||||||
append_declaration("background", self.background.hex)
|
append_declaration("background", self.background.hex)
|
||||||
if has_rule("text_style"):
|
if "text_style" in rules:
|
||||||
append_declaration("text-style", str(get_rule("text_style")))
|
append_declaration("text-style", str(get_rule("text_style")))
|
||||||
if has_rule("tint"):
|
if "tint" in rules:
|
||||||
append_declaration("tint", self.tint.css)
|
append_declaration("tint", self.tint.css)
|
||||||
|
|
||||||
if has_rule("overflow_x"):
|
if "overflow_x" in rules:
|
||||||
append_declaration("overflow-x", self.overflow_x)
|
append_declaration("overflow-x", self.overflow_x)
|
||||||
if has_rule("overflow_y"):
|
if "overflow_y" in rules:
|
||||||
append_declaration("overflow-y", self.overflow_y)
|
append_declaration("overflow-y", self.overflow_y)
|
||||||
|
|
||||||
if has_rule("scrollbar_color"):
|
if "scrollbar_color" in rules:
|
||||||
append_declaration("scrollbar-color", self.scrollbar_color.css)
|
append_declaration("scrollbar-color", self.scrollbar_color.css)
|
||||||
if has_rule("scrollbar_color_hover"):
|
if "scrollbar_color_hover" in rules:
|
||||||
append_declaration("scrollbar-color-hover", self.scrollbar_color_hover.css)
|
append_declaration("scrollbar-color-hover", self.scrollbar_color_hover.css)
|
||||||
if has_rule("scrollbar_color_active"):
|
if "scrollbar_color_active" in rules:
|
||||||
append_declaration(
|
append_declaration(
|
||||||
"scrollbar-color-active", self.scrollbar_color_active.css
|
"scrollbar-color-active", self.scrollbar_color_active.css
|
||||||
)
|
)
|
||||||
|
|
||||||
if has_rule("scrollbar_corner_color"):
|
if "scrollbar_corner_color" in rules:
|
||||||
append_declaration(
|
append_declaration(
|
||||||
"scrollbar-corner-color", self.scrollbar_corner_color.css
|
"scrollbar-corner-color", self.scrollbar_corner_color.css
|
||||||
)
|
)
|
||||||
|
|
||||||
if has_rule("scrollbar_background"):
|
if "scrollbar_background" in rules:
|
||||||
append_declaration("scrollbar-background", self.scrollbar_background.css)
|
append_declaration("scrollbar-background", self.scrollbar_background.css)
|
||||||
if has_rule("scrollbar_background_hover"):
|
if "scrollbar_background_hover" in rules:
|
||||||
append_declaration(
|
append_declaration(
|
||||||
"scrollbar-background-hover", self.scrollbar_background_hover.css
|
"scrollbar-background-hover", self.scrollbar_background_hover.css
|
||||||
)
|
)
|
||||||
if has_rule("scrollbar_background_active"):
|
if "scrollbar_background_active" in rules:
|
||||||
append_declaration(
|
append_declaration(
|
||||||
"scrollbar-background-active", self.scrollbar_background_active.css
|
"scrollbar-background-active", self.scrollbar_background_active.css
|
||||||
)
|
)
|
||||||
|
|
||||||
if has_rule("scrollbar_gutter"):
|
if "scrollbar_gutter" in rules:
|
||||||
append_declaration("scrollbar-gutter", self.scrollbar_gutter)
|
append_declaration("scrollbar-gutter", self.scrollbar_gutter)
|
||||||
if has_rule("scrollbar_size"):
|
if "scrollbar_size" in rules:
|
||||||
append_declaration(
|
append_declaration(
|
||||||
"scrollbar-size",
|
"scrollbar-size",
|
||||||
f"{self.scrollbar_size_horizontal} {self.scrollbar_size_vertical}",
|
f"{self.scrollbar_size_horizontal} {self.scrollbar_size_vertical}",
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
if has_rule("scrollbar_size_horizontal"):
|
if "scrollbar_size_horizontal" in rules:
|
||||||
append_declaration(
|
append_declaration(
|
||||||
"scrollbar-size-horizontal", str(self.scrollbar_size_horizontal)
|
"scrollbar-size-horizontal", str(self.scrollbar_size_horizontal)
|
||||||
)
|
)
|
||||||
if has_rule("scrollbar_size_vertical"):
|
if "scrollbar_size_vertical" in rules:
|
||||||
append_declaration(
|
append_declaration(
|
||||||
"scrollbar-size-vertical", str(self.scrollbar_size_vertical)
|
"scrollbar-size-vertical", str(self.scrollbar_size_vertical)
|
||||||
)
|
)
|
||||||
|
|
||||||
if has_rule("box_sizing"):
|
if "box_sizing" in rules:
|
||||||
append_declaration("box-sizing", self.box_sizing)
|
append_declaration("box-sizing", self.box_sizing)
|
||||||
if has_rule("width"):
|
if "width" in rules:
|
||||||
append_declaration("width", str(self.width))
|
append_declaration("width", str(self.width))
|
||||||
if has_rule("height"):
|
if "height" in rules:
|
||||||
append_declaration("height", str(self.height))
|
append_declaration("height", str(self.height))
|
||||||
if has_rule("min_width"):
|
if "min_width" in rules:
|
||||||
append_declaration("min-width", str(self.min_width))
|
append_declaration("min-width", str(self.min_width))
|
||||||
if has_rule("min_height"):
|
if "min_height" in rules:
|
||||||
append_declaration("min-height", str(self.min_height))
|
append_declaration("min-height", str(self.min_height))
|
||||||
if has_rule("max_width"):
|
if "max_width" in rules:
|
||||||
append_declaration("max-width", str(self.min_width))
|
append_declaration("max-width", str(self.min_width))
|
||||||
if has_rule("max_height"):
|
if "max_height" in rules:
|
||||||
append_declaration("max-height", str(self.min_height))
|
append_declaration("max-height", str(self.min_height))
|
||||||
if has_rule("transitions"):
|
if "transitions" in rules:
|
||||||
append_declaration(
|
append_declaration(
|
||||||
"transition",
|
"transition",
|
||||||
", ".join(
|
", ".join(
|
||||||
@@ -879,74 +878,74 @@ class Styles(StylesBase):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
if has_rule("align_horizontal") and has_rule("align_vertical"):
|
if "align_horizontal" in rules and "align_vertical" in rules:
|
||||||
append_declaration(
|
append_declaration(
|
||||||
"align", f"{self.align_horizontal} {self.align_vertical}"
|
"align", f"{self.align_horizontal} {self.align_vertical}"
|
||||||
)
|
)
|
||||||
elif has_rule("align_horizontal"):
|
elif "align_horizontal" in rules:
|
||||||
append_declaration("align-horizontal", self.align_horizontal)
|
append_declaration("align-horizontal", self.align_horizontal)
|
||||||
elif has_rule("align_vertical"):
|
elif "align_vertical" in rules:
|
||||||
append_declaration("align-vertical", self.align_vertical)
|
append_declaration("align-vertical", self.align_vertical)
|
||||||
|
|
||||||
if has_rule("content_align_horizontal") and has_rule("content_align_vertical"):
|
if "content_align_horizontal" in rules and "content_align_vertical" in rules:
|
||||||
append_declaration(
|
append_declaration(
|
||||||
"content-align",
|
"content-align",
|
||||||
f"{self.content_align_horizontal} {self.content_align_vertical}",
|
f"{self.content_align_horizontal} {self.content_align_vertical}",
|
||||||
)
|
)
|
||||||
elif has_rule("content_align_horizontal"):
|
elif "content_align_horizontal" in rules:
|
||||||
append_declaration(
|
append_declaration(
|
||||||
"content-align-horizontal", self.content_align_horizontal
|
"content-align-horizontal", self.content_align_horizontal
|
||||||
)
|
)
|
||||||
elif has_rule("content_align_vertical"):
|
elif "content_align_vertical" in rules:
|
||||||
append_declaration("content-align-vertical", self.content_align_vertical)
|
append_declaration("content-align-vertical", self.content_align_vertical)
|
||||||
|
|
||||||
if has_rule("text_align"):
|
if "text_align" in rules:
|
||||||
append_declaration("text-align", self.text_align)
|
append_declaration("text-align", self.text_align)
|
||||||
|
|
||||||
if has_rule("opacity"):
|
if "opacity" in rules:
|
||||||
append_declaration("opacity", str(self.opacity))
|
append_declaration("opacity", str(self.opacity))
|
||||||
if has_rule("text_opacity"):
|
if "text_opacity" in rules:
|
||||||
append_declaration("text-opacity", str(self.text_opacity))
|
append_declaration("text-opacity", str(self.text_opacity))
|
||||||
|
|
||||||
if has_rule("grid_columns"):
|
if "grid_columns" in rules:
|
||||||
append_declaration(
|
append_declaration(
|
||||||
"grid-columns",
|
"grid-columns",
|
||||||
" ".join(str(scalar) for scalar in self.grid_columns or ()),
|
" ".join(str(scalar) for scalar in self.grid_columns or ()),
|
||||||
)
|
)
|
||||||
if has_rule("grid_rows"):
|
if "grid_rows" in rules:
|
||||||
append_declaration(
|
append_declaration(
|
||||||
"grid-rows",
|
"grid-rows",
|
||||||
" ".join(str(scalar) for scalar in self.grid_rows or ()),
|
" ".join(str(scalar) for scalar in self.grid_rows or ()),
|
||||||
)
|
)
|
||||||
if has_rule("grid_size_columns"):
|
if "grid_size_columns" in rules:
|
||||||
append_declaration("grid-size-columns", str(self.grid_size_columns))
|
append_declaration("grid-size-columns", str(self.grid_size_columns))
|
||||||
if has_rule("grid_size_rows"):
|
if "grid_size_rows" in rules:
|
||||||
append_declaration("grid-size-rows", str(self.grid_size_rows))
|
append_declaration("grid-size-rows", str(self.grid_size_rows))
|
||||||
|
|
||||||
if has_rule("grid_gutter_horizontal"):
|
if "grid_gutter_horizontal" in rules:
|
||||||
append_declaration(
|
append_declaration(
|
||||||
"grid-gutter-horizontal", str(self.grid_gutter_horizontal)
|
"grid-gutter-horizontal", str(self.grid_gutter_horizontal)
|
||||||
)
|
)
|
||||||
if has_rule("grid_gutter_vertical"):
|
if "grid_gutter_vertical" in rules:
|
||||||
append_declaration("grid-gutter-vertical", str(self.grid_gutter_vertical))
|
append_declaration("grid-gutter-vertical", str(self.grid_gutter_vertical))
|
||||||
|
|
||||||
if has_rule("row_span"):
|
if "row_span" in rules:
|
||||||
append_declaration("row-span", str(self.row_span))
|
append_declaration("row-span", str(self.row_span))
|
||||||
if has_rule("column_span"):
|
if "column_span" in rules:
|
||||||
append_declaration("column-span", str(self.column_span))
|
append_declaration("column-span", str(self.column_span))
|
||||||
|
|
||||||
if has_rule("link_color"):
|
if "link_color" in rules:
|
||||||
append_declaration("link-color", self.link_color.css)
|
append_declaration("link-color", self.link_color.css)
|
||||||
if has_rule("link_background"):
|
if "link_background" in rules:
|
||||||
append_declaration("link-background", self.link_background.css)
|
append_declaration("link-background", self.link_background.css)
|
||||||
if has_rule("link_style"):
|
if "link_style" in rules:
|
||||||
append_declaration("link-style", str(self.link_style))
|
append_declaration("link-style", str(self.link_style))
|
||||||
|
|
||||||
if has_rule("link_hover_color"):
|
if "link_hover_color" in rules:
|
||||||
append_declaration("link-hover-color", self.link_hover_color.css)
|
append_declaration("link-hover-color", self.link_hover_color.css)
|
||||||
if has_rule("link_hover_background"):
|
if "link_hover_background" in rules:
|
||||||
append_declaration("link-hover-background", self.link_hover_background.css)
|
append_declaration("link-hover-background", self.link_hover_background.css)
|
||||||
if has_rule("link_hover_style"):
|
if "link_hover_style" in rules:
|
||||||
append_declaration("link-hover-style", str(self.link_hover_style))
|
append_declaration("link-hover-style", str(self.link_hover_style))
|
||||||
|
|
||||||
lines.sort()
|
lines.sort()
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import os
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
from pathlib import Path, PurePath
|
from pathlib import Path, PurePath
|
||||||
from typing import Iterable, NamedTuple, cast
|
from typing import Iterable, NamedTuple, Sequence, cast
|
||||||
|
|
||||||
import rich.repr
|
import rich.repr
|
||||||
from rich.console import Console, ConsoleOptions, RenderableType, RenderResult
|
from rich.console import Console, ConsoleOptions, RenderableType, RenderResult
|
||||||
@@ -248,7 +248,7 @@ class Stylesheet:
|
|||||||
self.source[str(path)] = CssSource(css, False, 0)
|
self.source[str(path)] = CssSource(css, False, 0)
|
||||||
self._require_parse = True
|
self._require_parse = True
|
||||||
|
|
||||||
def read_all(self, paths: list[PurePath]) -> None:
|
def read_all(self, paths: Sequence[PurePath]) -> None:
|
||||||
"""Read multiple CSS files, in order.
|
"""Read multiple CSS files, in order.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ class DOMNode(MessagePump):
|
|||||||
_css_type_names: ClassVar[frozenset[str]] = frozenset()
|
_css_type_names: ClassVar[frozenset[str]] = frozenset()
|
||||||
|
|
||||||
# Generated list of bindings
|
# Generated list of bindings
|
||||||
_merged_bindings: ClassVar[Bindings] | None = None
|
_merged_bindings: ClassVar[Bindings | None] = None
|
||||||
|
|
||||||
_reactives: ClassVar[dict[str, Reactive]]
|
_reactives: ClassVar[dict[str, Reactive]]
|
||||||
|
|
||||||
@@ -132,10 +132,12 @@ class DOMNode(MessagePump):
|
|||||||
check_identifiers("class name", *_classes)
|
check_identifiers("class name", *_classes)
|
||||||
self._classes.update(_classes)
|
self._classes.update(_classes)
|
||||||
|
|
||||||
self.children = NodeList()
|
self.children: NodeList = NodeList()
|
||||||
self._css_styles: Styles = Styles(self)
|
self._css_styles: Styles = Styles(self)
|
||||||
self._inline_styles: Styles = Styles(self)
|
self._inline_styles: Styles = Styles(self)
|
||||||
self.styles = RenderStyles(self, self._css_styles, self._inline_styles)
|
self.styles: RenderStyles = RenderStyles(
|
||||||
|
self, self._css_styles, self._inline_styles
|
||||||
|
)
|
||||||
# A mapping of class names to Styles set in COMPONENT_CLASSES
|
# A mapping of class names to Styles set in COMPONENT_CLASSES
|
||||||
self._component_styles: dict[str, RenderStyles] = {}
|
self._component_styles: dict[str, RenderStyles] = {}
|
||||||
|
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ from ctypes import Structure, Union, byref, wintypes
|
|||||||
from ctypes.wintypes import BOOL, CHAR, DWORD, HANDLE, SHORT, UINT, WCHAR, WORD
|
from ctypes.wintypes import BOOL, CHAR, DWORD, HANDLE, SHORT, UINT, WCHAR, WORD
|
||||||
from typing import IO, Callable, List, Optional
|
from typing import IO, Callable, List, Optional
|
||||||
|
|
||||||
from .._types import EventTarget
|
from .._types import MessageTarget
|
||||||
from .._xterm_parser import XTermParser
|
from .._xterm_parser import XTermParser
|
||||||
from ..events import Event, Resize
|
from ..events import Event, Resize
|
||||||
from ..geometry import Size
|
from ..geometry import Size
|
||||||
|
|
||||||
KERNEL32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
KERNEL32 = ctypes.WinDLL("kernel32", use_last_error=True) # type: ignore
|
||||||
|
|
||||||
# Console input modes
|
# Console input modes
|
||||||
ENABLE_ECHO_INPUT = 0x0004
|
ENABLE_ECHO_INPUT = 0x0004
|
||||||
@@ -130,7 +130,7 @@ def _set_console_mode(file: IO, mode: int) -> bool:
|
|||||||
Returns:
|
Returns:
|
||||||
True on success, otherwise False.
|
True on success, otherwise False.
|
||||||
"""
|
"""
|
||||||
windows_filehandle = msvcrt.get_osfhandle(file.fileno())
|
windows_filehandle = msvcrt.get_osfhandle(file.fileno()) # type: ignore
|
||||||
success = KERNEL32.SetConsoleMode(windows_filehandle, mode)
|
success = KERNEL32.SetConsoleMode(windows_filehandle, mode)
|
||||||
return success
|
return success
|
||||||
|
|
||||||
@@ -144,7 +144,7 @@ def _get_console_mode(file: IO) -> int:
|
|||||||
Returns:
|
Returns:
|
||||||
The current console mode.
|
The current console mode.
|
||||||
"""
|
"""
|
||||||
windows_filehandle = msvcrt.get_osfhandle(file.fileno())
|
windows_filehandle = msvcrt.get_osfhandle(file.fileno()) # type: ignore
|
||||||
mode = wintypes.DWORD()
|
mode = wintypes.DWORD()
|
||||||
KERNEL32.GetConsoleMode(windows_filehandle, ctypes.byref(mode))
|
KERNEL32.GetConsoleMode(windows_filehandle, ctypes.byref(mode))
|
||||||
return mode.value
|
return mode.value
|
||||||
@@ -211,7 +211,7 @@ class EventMonitor(threading.Thread):
|
|||||||
self,
|
self,
|
||||||
loop: AbstractEventLoop,
|
loop: AbstractEventLoop,
|
||||||
app,
|
app,
|
||||||
target: EventTarget,
|
target: MessageTarget,
|
||||||
exit_event: threading.Event,
|
exit_event: threading.Event,
|
||||||
process_event: Callable[[Event], None],
|
process_event: Callable[[Event], None],
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
from pathlib import PurePath
|
from pathlib import PurePath
|
||||||
from typing import Callable
|
from typing import Callable, Sequence
|
||||||
|
|
||||||
import rich.repr
|
import rich.repr
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ from ._callback import invoke
|
|||||||
class FileMonitor:
|
class FileMonitor:
|
||||||
"""Monitors files for changes and invokes a callback when it does."""
|
"""Monitors files for changes and invokes a callback when it does."""
|
||||||
|
|
||||||
def __init__(self, paths: list[PurePath], callback: Callable) -> None:
|
def __init__(self, paths: Sequence[PurePath], callback: Callable) -> None:
|
||||||
self.paths = paths
|
self.paths = paths
|
||||||
self.callback = callback
|
self.callback = callback
|
||||||
self._modified = self._get_last_modified_time()
|
self._modified = self._get_last_modified_time()
|
||||||
|
|||||||
@@ -229,6 +229,7 @@ KEY_DISPLAY_ALIASES = {
|
|||||||
"escape": "ESC",
|
"escape": "ESC",
|
||||||
"enter": "⏎",
|
"enter": "⏎",
|
||||||
"minus": "-",
|
"minus": "-",
|
||||||
|
"space": "SPACE",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ class Pilot(Generic[ReturnType]):
|
|||||||
async def wait_for_scheduled_animations(self) -> None:
|
async def wait_for_scheduled_animations(self) -> None:
|
||||||
"""Wait for any current and scheduled animations to complete."""
|
"""Wait for any current and scheduled animations to complete."""
|
||||||
await self._app.animator.wait_until_complete()
|
await self._app.animator.wait_until_complete()
|
||||||
await wait_for_idle(0)
|
await wait_for_idle()
|
||||||
|
|
||||||
async def exit(self, result: ReturnType) -> None:
|
async def exit(self, result: ReturnType) -> None:
|
||||||
"""Exit the app with the given result.
|
"""Exit the app with the given result.
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ class Screen(Widget):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def is_current(self) -> bool:
|
def is_current(self) -> bool:
|
||||||
"""Check if this screen is current (i.e. visible to user)."""
|
"""Is the screen current (i.e. visible to user)?"""
|
||||||
from .app import ScreenStackError
|
from .app import ScreenStackError
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -231,7 +231,7 @@ class Screen(Widget):
|
|||||||
else:
|
else:
|
||||||
# Only move the focus if we are currently showing the focus
|
# Only move the focus if we are currently showing the focus
|
||||||
if direction:
|
if direction:
|
||||||
to_focus: Widget | None = None
|
to_focus = None
|
||||||
chain_length = len(focus_chain)
|
chain_length = len(focus_chain)
|
||||||
for step in range(1, len(focus_chain) + 1):
|
for step in range(1, len(focus_chain) + 1):
|
||||||
node = focus_chain[
|
node = focus_chain[
|
||||||
|
|||||||
@@ -30,14 +30,14 @@ class ScrollView(Widget):
|
|||||||
"""Not transparent, i.e. renders something."""
|
"""Not transparent, i.e. renders something."""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def watch_scroll_x(self, new_value: float) -> None:
|
def watch_scroll_x(self, old_value: float, new_value: float) -> None:
|
||||||
if self.show_horizontal_scrollbar:
|
if self.show_horizontal_scrollbar and round(old_value) != round(new_value):
|
||||||
self.horizontal_scrollbar.position = int(new_value)
|
self.horizontal_scrollbar.position = round(new_value)
|
||||||
self.refresh()
|
self.refresh()
|
||||||
|
|
||||||
def watch_scroll_y(self, new_value: float) -> None:
|
def watch_scroll_y(self, old_value: float, new_value: float) -> None:
|
||||||
if self.show_vertical_scrollbar:
|
if self.show_vertical_scrollbar and round(old_value) != round(new_value):
|
||||||
self.vertical_scrollbar.position = int(new_value)
|
self.vertical_scrollbar.position = round(new_value)
|
||||||
self.refresh()
|
self.refresh()
|
||||||
|
|
||||||
def on_mount(self):
|
def on_mount(self):
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ class _Styled:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, renderable: "RenderableType", style: Style, link_style: Style | None
|
self, renderable: "ConsoleRenderable", style: Style, link_style: Style | None
|
||||||
) -> None:
|
) -> None:
|
||||||
self.renderable = renderable
|
self.renderable = renderable
|
||||||
self.style = style
|
self.style = style
|
||||||
@@ -133,7 +133,7 @@ class _Styled:
|
|||||||
if style:
|
if style:
|
||||||
apply = style.__add__
|
apply = style.__add__
|
||||||
result_segments = (
|
result_segments = (
|
||||||
_Segment(text, apply(_style), control)
|
_Segment(text, apply(_style), None)
|
||||||
for text, _style, control in result_segments
|
for text, _style, control in result_segments
|
||||||
)
|
)
|
||||||
link_style = self.link_style
|
link_style = self.link_style
|
||||||
@@ -141,19 +141,22 @@ class _Styled:
|
|||||||
result_segments = (
|
result_segments = (
|
||||||
_Segment(
|
_Segment(
|
||||||
text,
|
text,
|
||||||
style
|
(
|
||||||
if style._meta is None
|
style
|
||||||
else (style + link_style if "@click" in style.meta else style),
|
if style._meta is None
|
||||||
|
else (style + link_style if "@click" in style.meta else style)
|
||||||
|
),
|
||||||
control,
|
control,
|
||||||
)
|
)
|
||||||
for text, style, control in result_segments
|
for text, style, control in result_segments
|
||||||
|
if style is not None
|
||||||
)
|
)
|
||||||
return result_segments
|
return result_segments
|
||||||
|
|
||||||
def __rich_measure__(
|
def __rich_measure__(
|
||||||
self, console: "Console", options: "ConsoleOptions"
|
self, console: "Console", options: "ConsoleOptions"
|
||||||
) -> Measurement:
|
) -> Measurement:
|
||||||
return self.renderable.__rich_measure__(console, options)
|
return Measurement.get(console, options, self.renderable)
|
||||||
|
|
||||||
|
|
||||||
class RenderCache(NamedTuple):
|
class RenderCache(NamedTuple):
|
||||||
@@ -1414,6 +1417,7 @@ class Widget(DOMNode):
|
|||||||
easing = DEFAULT_SCROLL_EASING
|
easing = DEFAULT_SCROLL_EASING
|
||||||
|
|
||||||
if maybe_scroll_x:
|
if maybe_scroll_x:
|
||||||
|
assert x is not None
|
||||||
self.scroll_target_x = x
|
self.scroll_target_x = x
|
||||||
if x != self.scroll_x:
|
if x != self.scroll_x:
|
||||||
self.animate(
|
self.animate(
|
||||||
@@ -1425,6 +1429,7 @@ class Widget(DOMNode):
|
|||||||
)
|
)
|
||||||
scrolled_x = True
|
scrolled_x = True
|
||||||
if maybe_scroll_y:
|
if maybe_scroll_y:
|
||||||
|
assert y is not None
|
||||||
self.scroll_target_y = y
|
self.scroll_target_y = y
|
||||||
if y != self.scroll_y:
|
if y != self.scroll_y:
|
||||||
self.animate(
|
self.animate(
|
||||||
@@ -1438,10 +1443,12 @@ class Widget(DOMNode):
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
if maybe_scroll_x:
|
if maybe_scroll_x:
|
||||||
|
assert x is not None
|
||||||
scroll_x = self.scroll_x
|
scroll_x = self.scroll_x
|
||||||
self.scroll_target_x = self.scroll_x = x
|
self.scroll_target_x = self.scroll_x = x
|
||||||
scrolled_x = scroll_x != self.scroll_x
|
scrolled_x = scroll_x != self.scroll_x
|
||||||
if maybe_scroll_y:
|
if maybe_scroll_y:
|
||||||
|
assert y is not None
|
||||||
scroll_y = self.scroll_y
|
scroll_y = self.scroll_y
|
||||||
self.scroll_target_y = self.scroll_y = y
|
self.scroll_target_y = self.scroll_y = y
|
||||||
scrolled_y = scroll_y != self.scroll_y
|
scrolled_y = scroll_y != self.scroll_y
|
||||||
|
|||||||
@@ -31,12 +31,12 @@ class Switch(Widget, can_focus=True):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
COMPONENT_CLASSES: ClassVar[set[str]] = {
|
COMPONENT_CLASSES: ClassVar[set[str]] = {
|
||||||
"switch--switch",
|
"switch--slider",
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
| Class | Description |
|
| Class | Description |
|
||||||
| :- | :- |
|
| :- | :- |
|
||||||
| `switch--switch` | Targets the switch of the switch. |
|
| `switch--slider` | Targets the slider of the switch. |
|
||||||
"""
|
"""
|
||||||
|
|
||||||
DEFAULT_CSS = """
|
DEFAULT_CSS = """
|
||||||
@@ -48,7 +48,7 @@ class Switch(Widget, can_focus=True):
|
|||||||
padding: 0 2;
|
padding: 0 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
Switch > .switch--switch {
|
Switch > .switch--slider {
|
||||||
background: $panel-darken-2;
|
background: $panel-darken-2;
|
||||||
color: $panel-lighten-2;
|
color: $panel-lighten-2;
|
||||||
}
|
}
|
||||||
@@ -65,7 +65,7 @@ class Switch(Widget, can_focus=True):
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Switch.-on > .switch--switch {
|
Switch.-on > .switch--slider {
|
||||||
color: $success;
|
color: $success;
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
@@ -128,7 +128,7 @@ class Switch(Widget, can_focus=True):
|
|||||||
self.set_class(slider_pos == 1, "-on")
|
self.set_class(slider_pos == 1, "-on")
|
||||||
|
|
||||||
def render(self) -> RenderableType:
|
def render(self) -> RenderableType:
|
||||||
style = self.get_component_rich_style("switch--switch")
|
style = self.get_component_rich_style("switch--slider")
|
||||||
return ScrollBarRender(
|
return ScrollBarRender(
|
||||||
virtual_size=100,
|
virtual_size=100,
|
||||||
window_size=50,
|
window_size=50,
|
||||||
@@ -144,12 +144,17 @@ class Switch(Widget, can_focus=True):
|
|||||||
return 1
|
return 1
|
||||||
|
|
||||||
def on_click(self) -> None:
|
def on_click(self) -> None:
|
||||||
|
"""Toggle the state of the switch."""
|
||||||
self.toggle()
|
self.toggle()
|
||||||
|
|
||||||
def action_toggle(self) -> None:
|
def action_toggle(self) -> None:
|
||||||
|
"""Toggle the state of the switch."""
|
||||||
self.toggle()
|
self.toggle()
|
||||||
|
|
||||||
def toggle(self) -> None:
|
def toggle(self) -> None:
|
||||||
"""Toggle the switch value. As a result of the value changing,
|
"""Toggle the switch value.
|
||||||
a Switch.Changed message will be posted."""
|
|
||||||
|
As a result of the value changing, a `Switch.Changed` message will
|
||||||
|
be posted.
|
||||||
|
"""
|
||||||
self.value = not self.value
|
self.value = not self.value
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING, ClassVar, Generic, NewType, TypeVar
|
from typing import TYPE_CHECKING, ClassVar, Generic, Iterable, NewType, TypeVar, cast
|
||||||
|
|
||||||
import rich.repr
|
import rich.repr
|
||||||
from rich.style import NULL_STYLE, Style
|
from rich.style import NULL_STYLE, Style
|
||||||
@@ -783,7 +783,7 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
|
|||||||
assert self._tree_lines_cached is not None
|
assert self._tree_lines_cached is not None
|
||||||
return self._tree_lines_cached
|
return self._tree_lines_cached
|
||||||
|
|
||||||
def _on_idle(self) -> None:
|
async def _on_idle(self, event: events.Idle) -> None:
|
||||||
"""Check tree needs a rebuild on idle."""
|
"""Check tree needs a rebuild on idle."""
|
||||||
# Property calls build if required
|
# Property calls build if required
|
||||||
self._tree_lines
|
self._tree_lines
|
||||||
@@ -891,6 +891,7 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
|
|||||||
Returns:
|
Returns:
|
||||||
Strings for space, vertical, terminator and cross.
|
Strings for space, vertical, terminator and cross.
|
||||||
"""
|
"""
|
||||||
|
lines: tuple[Iterable[str], Iterable[str], Iterable[str], Iterable[str]]
|
||||||
if self.show_guides:
|
if self.show_guides:
|
||||||
lines = self.LINES["default"]
|
lines = self.LINES["default"]
|
||||||
if style.bold:
|
if style.bold:
|
||||||
@@ -901,11 +902,11 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
|
|||||||
lines = (" ", " ", " ", " ")
|
lines = (" ", " ", " ", " ")
|
||||||
|
|
||||||
guide_depth = max(0, self.guide_depth - 2)
|
guide_depth = max(0, self.guide_depth - 2)
|
||||||
lines = tuple(
|
guide_lines = tuple(
|
||||||
f"{vertical}{horizontal * guide_depth} "
|
f"{characters[0]}{characters[1] * guide_depth} "
|
||||||
for vertical, horizontal in lines
|
for characters in lines
|
||||||
)
|
)
|
||||||
return lines
|
return cast("tuple[str, str, str, str]", guide_lines)
|
||||||
|
|
||||||
if is_hover:
|
if is_hover:
|
||||||
line_style = self.get_component_rich_style("tree--highlight-line")
|
line_style = self.get_component_rich_style("tree--highlight-line")
|
||||||
|
|||||||
@@ -1,165 +0,0 @@
|
|||||||
from textual._text_backend import TextEditorBackend
|
|
||||||
|
|
||||||
CONTENT = "Hello, world!"
|
|
||||||
|
|
||||||
|
|
||||||
def test_set_content():
|
|
||||||
editor = TextEditorBackend()
|
|
||||||
editor.set_content(CONTENT)
|
|
||||||
assert editor.content == CONTENT
|
|
||||||
|
|
||||||
|
|
||||||
def test_delete_back_cursor_at_start_is_noop():
|
|
||||||
editor = TextEditorBackend(CONTENT)
|
|
||||||
assert not editor.delete_back()
|
|
||||||
assert editor == TextEditorBackend(CONTENT, 0)
|
|
||||||
|
|
||||||
|
|
||||||
def test_delete_back_cursor_at_end():
|
|
||||||
editor = TextEditorBackend(CONTENT)
|
|
||||||
assert editor.cursor_text_end()
|
|
||||||
assert editor.delete_back()
|
|
||||||
assert editor == TextEditorBackend("Hello, world", 12)
|
|
||||||
|
|
||||||
|
|
||||||
def test_delete_back_cursor_in_middle():
|
|
||||||
editor = TextEditorBackend(CONTENT, 5)
|
|
||||||
assert editor.delete_back()
|
|
||||||
assert editor == TextEditorBackend("Hell, world!", 4)
|
|
||||||
|
|
||||||
|
|
||||||
def test_delete_forward_cursor_at_start():
|
|
||||||
editor = TextEditorBackend(CONTENT)
|
|
||||||
assert editor.delete_forward()
|
|
||||||
assert editor.content == "ello, world!"
|
|
||||||
|
|
||||||
|
|
||||||
def test_delete_forward_cursor_at_end_is_noop():
|
|
||||||
editor = TextEditorBackend(CONTENT)
|
|
||||||
assert editor.cursor_text_end()
|
|
||||||
assert not editor.delete_forward()
|
|
||||||
assert editor == TextEditorBackend(CONTENT, len(CONTENT))
|
|
||||||
|
|
||||||
|
|
||||||
def test_delete_forward_cursor_in_middle():
|
|
||||||
editor = TextEditorBackend(CONTENT, 5)
|
|
||||||
editor.cursor_index = 5
|
|
||||||
assert editor.delete_forward()
|
|
||||||
assert editor == TextEditorBackend("Hello world!", 5)
|
|
||||||
|
|
||||||
|
|
||||||
def test_cursor_left_cursor_at_start_is_noop():
|
|
||||||
editor = TextEditorBackend(CONTENT)
|
|
||||||
assert not editor.cursor_left()
|
|
||||||
assert editor == TextEditorBackend(CONTENT)
|
|
||||||
|
|
||||||
|
|
||||||
def test_cursor_left_cursor_in_middle():
|
|
||||||
editor = TextEditorBackend(CONTENT, 6)
|
|
||||||
assert editor.cursor_left()
|
|
||||||
assert editor == TextEditorBackend(CONTENT, 5)
|
|
||||||
|
|
||||||
|
|
||||||
def test_cursor_left_cursor_at_end():
|
|
||||||
editor = TextEditorBackend(CONTENT, len(CONTENT))
|
|
||||||
assert editor.cursor_left()
|
|
||||||
assert editor == TextEditorBackend(CONTENT, len(CONTENT) - 1)
|
|
||||||
|
|
||||||
|
|
||||||
def test_cursor_right_cursor_at_start():
|
|
||||||
editor = TextEditorBackend(CONTENT)
|
|
||||||
assert editor.cursor_right()
|
|
||||||
assert editor == TextEditorBackend(CONTENT, 1)
|
|
||||||
|
|
||||||
|
|
||||||
def test_cursor_right_cursor_in_middle():
|
|
||||||
editor = TextEditorBackend(CONTENT, 5)
|
|
||||||
assert editor.cursor_right()
|
|
||||||
assert editor == TextEditorBackend(CONTENT, 6)
|
|
||||||
|
|
||||||
|
|
||||||
def test_cursor_right_cursor_at_end_is_noop():
|
|
||||||
editor = TextEditorBackend(CONTENT, len(CONTENT))
|
|
||||||
editor.cursor_right()
|
|
||||||
assert editor == TextEditorBackend(CONTENT, len(CONTENT))
|
|
||||||
|
|
||||||
|
|
||||||
def test_query_cursor_left_cursor_at_start_returns_false():
|
|
||||||
editor = TextEditorBackend(CONTENT)
|
|
||||||
assert not editor.query_cursor_left()
|
|
||||||
|
|
||||||
|
|
||||||
def test_query_cursor_left_cursor_at_end_returns_true():
|
|
||||||
editor = TextEditorBackend(CONTENT, len(CONTENT))
|
|
||||||
assert editor.query_cursor_left()
|
|
||||||
|
|
||||||
|
|
||||||
def test_query_cursor_left_cursor_in_middle_returns_true():
|
|
||||||
editor = TextEditorBackend(CONTENT, 6)
|
|
||||||
assert editor.query_cursor_left()
|
|
||||||
|
|
||||||
|
|
||||||
def test_query_cursor_right_cursor_at_start_returns_true():
|
|
||||||
editor = TextEditorBackend(CONTENT)
|
|
||||||
assert editor.query_cursor_right()
|
|
||||||
|
|
||||||
|
|
||||||
def test_query_cursor_right_cursor_in_middle_returns_true():
|
|
||||||
editor = TextEditorBackend(CONTENT, 6)
|
|
||||||
assert editor.query_cursor_right()
|
|
||||||
|
|
||||||
|
|
||||||
def test_query_cursor_right_cursor_at_end_returns_false():
|
|
||||||
editor = TextEditorBackend(CONTENT, len(CONTENT))
|
|
||||||
assert not editor.query_cursor_right()
|
|
||||||
|
|
||||||
|
|
||||||
def test_cursor_text_start_cursor_already_at_start():
|
|
||||||
editor = TextEditorBackend(CONTENT)
|
|
||||||
assert not editor.cursor_text_start()
|
|
||||||
assert editor.cursor_index == 0
|
|
||||||
|
|
||||||
|
|
||||||
def test_cursor_text_start_cursor_in_middle():
|
|
||||||
editor = TextEditorBackend(CONTENT, 6)
|
|
||||||
assert editor.cursor_text_start()
|
|
||||||
assert editor.cursor_index == 0
|
|
||||||
|
|
||||||
|
|
||||||
def test_cursor_text_end_cursor_already_at_end():
|
|
||||||
editor = TextEditorBackend(CONTENT, len(CONTENT))
|
|
||||||
assert not editor.cursor_text_end()
|
|
||||||
assert editor.cursor_index == len(CONTENT)
|
|
||||||
|
|
||||||
|
|
||||||
def test_cursor_text_end_cursor_in_middle():
|
|
||||||
editor = TextEditorBackend(CONTENT, len(CONTENT))
|
|
||||||
assert not editor.cursor_text_end()
|
|
||||||
assert editor.cursor_index == len(CONTENT)
|
|
||||||
|
|
||||||
|
|
||||||
def test_insert_at_cursor_cursor_at_start():
|
|
||||||
editor = TextEditorBackend(CONTENT)
|
|
||||||
assert editor.insert("ABC")
|
|
||||||
assert editor.content == "ABC" + CONTENT
|
|
||||||
assert editor.cursor_index == len("ABC")
|
|
||||||
|
|
||||||
|
|
||||||
def test_insert_at_cursor_cursor_in_middle():
|
|
||||||
start_cursor_index = 6
|
|
||||||
editor = TextEditorBackend(CONTENT, start_cursor_index)
|
|
||||||
assert editor.insert("ABC")
|
|
||||||
assert editor.content == "Hello,ABC world!"
|
|
||||||
assert editor.cursor_index == start_cursor_index + len("ABC")
|
|
||||||
|
|
||||||
|
|
||||||
def test_insert_at_cursor_cursor_at_end():
|
|
||||||
editor = TextEditorBackend(CONTENT, len(CONTENT))
|
|
||||||
assert editor.insert("ABC")
|
|
||||||
assert editor.content == CONTENT + "ABC"
|
|
||||||
assert editor.cursor_index == len(editor.content)
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_range():
|
|
||||||
editor = TextEditorBackend(CONTENT)
|
|
||||||
assert editor.get_range(0, 5) == "Hello"
|
|
||||||
Reference in New Issue
Block a user