summaryrefslogtreecommitdiffstats
path: root/contrib/python
diff options
context:
space:
mode:
authorYDBot <[email protected]>2025-10-20 12:59:43 +0000
committerYDBot <[email protected]>2025-10-20 12:59:43 +0000
commit7874efeb56a4beed3df6a5a5a413dc6583d23e7a (patch)
treeddd3dc43ad00a7ea058cf35070b98e5d6475fdbb /contrib/python
parentb0dc87f6d3013af373dfd9a5c670acd18ea1dbe6 (diff)
parent14e27f7771734f0b5d08cb303cc469bf64d6c772 (diff)
Merge pull request #26940 from ydb-platform/merge-rightlib-251016-0050
Diffstat (limited to 'contrib/python')
-rw-r--r--contrib/python/google-auth/py3/.dist-info/METADATA2
-rw-r--r--contrib/python/google-auth/py3/google/auth/_default.py29
-rw-r--r--contrib/python/google-auth/py3/google/auth/_default_async.py26
-rw-r--r--contrib/python/google-auth/py3/google/auth/version.py2
-rw-r--r--contrib/python/google-auth/py3/ya.make2
-rw-r--r--contrib/python/textual/.dist-info/METADATA207
-rw-r--r--contrib/python/textual/README.md184
-rw-r--r--contrib/python/textual/textual/_arrange.py28
-rw-r--r--contrib/python/textual/textual/_border.py16
-rw-r--r--contrib/python/textual/textual/_compositor.py34
-rw-r--r--contrib/python/textual/textual/_text_area_theme.py65
-rw-r--r--contrib/python/textual/textual/_tree_sitter.py49
-rw-r--r--contrib/python/textual/textual/app.py8
-rw-r--r--contrib/python/textual/textual/color.py8
-rw-r--r--contrib/python/textual/textual/command.py2
-rw-r--r--contrib/python/textual/textual/content.py41
-rw-r--r--contrib/python/textual/textual/css/_help_text.py24
-rw-r--r--contrib/python/textual/textual/css/_style_properties.py8
-rw-r--r--contrib/python/textual/textual/css/_styles_builder.py31
-rw-r--r--contrib/python/textual/textual/css/constants.py2
-rw-r--r--contrib/python/textual/textual/css/styles.py65
-rw-r--r--contrib/python/textual/textual/css/stylesheet.py2
-rw-r--r--contrib/python/textual/textual/css/types.py2
-rw-r--r--contrib/python/textual/textual/demo/demo_app.py30
-rw-r--r--contrib/python/textual/textual/demo/game.py586
-rw-r--r--contrib/python/textual/textual/demo/page.py2
-rw-r--r--contrib/python/textual/textual/demo/projects.py11
-rw-r--r--contrib/python/textual/textual/demo/widgets.py30
-rw-r--r--contrib/python/textual/textual/document/_document.py28
-rw-r--r--contrib/python/textual/textual/document/_languages.py19
-rw-r--r--contrib/python/textual/textual/document/_syntax_aware_document.py30
-rw-r--r--contrib/python/textual/textual/dom.py3
-rw-r--r--contrib/python/textual/textual/drivers/linux_driver.py5
-rw-r--r--contrib/python/textual/textual/drivers/linux_inline_driver.py3
-rw-r--r--contrib/python/textual/textual/events.py2
-rw-r--r--contrib/python/textual/textual/geometry.py3
-rw-r--r--contrib/python/textual/textual/layout.py70
-rw-r--r--contrib/python/textual/textual/layouts/grid.py16
-rw-r--r--contrib/python/textual/textual/layouts/horizontal.py20
-rw-r--r--contrib/python/textual/textual/layouts/vertical.py20
-rw-r--r--contrib/python/textual/textual/screen.py53
-rw-r--r--contrib/python/textual/textual/scroll_view.py9
-rw-r--r--contrib/python/textual/textual/tree-sitter/highlights/css.scm2
-rw-r--r--contrib/python/textual/textual/tree-sitter/highlights/javascript.scm65
-rw-r--r--contrib/python/textual/textual/tree-sitter/highlights/kotlin.scm380
-rw-r--r--contrib/python/textual/textual/tree-sitter/highlights/markdown.scm60
-rw-r--r--contrib/python/textual/textual/tree-sitter/highlights/sql.scm520
-rw-r--r--contrib/python/textual/textual/tree-sitter/highlights/toml.scm2
-rw-r--r--contrib/python/textual/textual/tree-sitter/highlights/xml.scm168
-rw-r--r--contrib/python/textual/textual/visual.py10
-rw-r--r--contrib/python/textual/textual/widget.py24
-rw-r--r--contrib/python/textual/textual/widgets/_digits.py3
-rw-r--r--contrib/python/textual/textual/widgets/_footer.py40
-rw-r--r--contrib/python/textual/textual/widgets/_list_view.py77
-rw-r--r--contrib/python/textual/textual/widgets/_loading_indicator.py10
-rw-r--r--contrib/python/textual/textual/widgets/_option_list.py44
-rw-r--r--contrib/python/textual/textual/widgets/_select.py25
-rw-r--r--contrib/python/textual/textual/widgets/_selection_list.py3
-rw-r--r--contrib/python/textual/textual/widgets/_tabbed_content.py14
-rw-r--r--contrib/python/textual/textual/widgets/_text_area.py95
-rw-r--r--contrib/python/textual/textual/widgets/_toast.py2
-rw-r--r--contrib/python/textual/textual/widgets/_toggle_button.py2
-rw-r--r--contrib/python/textual/textual/widgets/_tree.py4
-rw-r--r--contrib/python/textual/textual/widgets/text_area.py2
-rw-r--r--contrib/python/textual/ya.make6
65 files changed, 2218 insertions, 1117 deletions
diff --git a/contrib/python/google-auth/py3/.dist-info/METADATA b/contrib/python/google-auth/py3/.dist-info/METADATA
index da8612e97b4..3017ab4ef0d 100644
--- a/contrib/python/google-auth/py3/.dist-info/METADATA
+++ b/contrib/python/google-auth/py3/.dist-info/METADATA
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: google-auth
-Version: 2.41.0
+Version: 2.41.1
Summary: Google Authentication Library
Home-page: https://github.com/googleapis/google-auth-library-python
Author: Google Cloud Platform
diff --git a/contrib/python/google-auth/py3/google/auth/_default.py b/contrib/python/google-auth/py3/google/auth/_default.py
index 2df2b4e02b6..a96f7108be2 100644
--- a/contrib/python/google-auth/py3/google/auth/_default.py
+++ b/contrib/python/google-auth/py3/google/auth/_default.py
@@ -305,15 +305,17 @@ def _get_gcloud_sdk_credentials(quota_project_id=None):
_LOGGER.debug("Cloud SDK credentials not found on disk; not using them")
return None, None
- credentials, project_id = load_credentials_from_file(
- credentials_filename, quota_project_id=quota_project_id
- )
- credentials._cred_file_path = credentials_filename
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore", DeprecationWarning)
+ credentials, project_id = load_credentials_from_file(
+ credentials_filename, quota_project_id=quota_project_id
+ )
+ credentials._cred_file_path = credentials_filename
- if not project_id:
- project_id = _cloud_sdk.get_project_id()
+ if not project_id:
+ project_id = _cloud_sdk.get_project_id()
- return credentials, project_id
+ return credentials, project_id
def _get_explicit_environ_credentials(quota_project_id=None):
@@ -339,12 +341,15 @@ def _get_explicit_environ_credentials(quota_project_id=None):
return _get_gcloud_sdk_credentials(quota_project_id=quota_project_id)
if explicit_file is not None:
- credentials, project_id = load_credentials_from_file(
- os.environ[environment_vars.CREDENTIALS], quota_project_id=quota_project_id
- )
- credentials._cred_file_path = f"{explicit_file} file via the GOOGLE_APPLICATION_CREDENTIALS environment variable"
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore", DeprecationWarning)
+ credentials, project_id = load_credentials_from_file(
+ os.environ[environment_vars.CREDENTIALS],
+ quota_project_id=quota_project_id,
+ )
+ credentials._cred_file_path = f"{explicit_file} file via the GOOGLE_APPLICATION_CREDENTIALS environment variable"
- return credentials, project_id
+ return credentials, project_id
else:
return None, None
diff --git a/contrib/python/google-auth/py3/google/auth/_default_async.py b/contrib/python/google-auth/py3/google/auth/_default_async.py
index 2e53e208875..44bc6719f97 100644
--- a/contrib/python/google-auth/py3/google/auth/_default_async.py
+++ b/contrib/python/google-auth/py3/google/auth/_default_async.py
@@ -20,6 +20,7 @@ Implements application default credentials and project ID detection.
import io
import json
import os
+import warnings
from google.auth import _default
from google.auth import environment_vars
@@ -116,14 +117,16 @@ def _get_gcloud_sdk_credentials(quota_project_id=None):
if not os.path.isfile(credentials_filename):
return None, None
- credentials, project_id = load_credentials_from_file(
- credentials_filename, quota_project_id=quota_project_id
- )
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore", DeprecationWarning)
+ credentials, project_id = load_credentials_from_file(
+ credentials_filename, quota_project_id=quota_project_id
+ )
- if not project_id:
- project_id = _cloud_sdk.get_project_id()
+ if not project_id:
+ project_id = _cloud_sdk.get_project_id()
- return credentials, project_id
+ return credentials, project_id
def _get_explicit_environ_credentials(quota_project_id=None):
@@ -141,11 +144,14 @@ def _get_explicit_environ_credentials(quota_project_id=None):
return _get_gcloud_sdk_credentials(quota_project_id=quota_project_id)
if explicit_file is not None:
- credentials, project_id = load_credentials_from_file(
- os.environ[environment_vars.CREDENTIALS], quota_project_id=quota_project_id
- )
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore", DeprecationWarning)
+ credentials, project_id = load_credentials_from_file(
+ os.environ[environment_vars.CREDENTIALS],
+ quota_project_id=quota_project_id,
+ )
- return credentials, project_id
+ return credentials, project_id
else:
return None, None
diff --git a/contrib/python/google-auth/py3/google/auth/version.py b/contrib/python/google-auth/py3/google/auth/version.py
index f309c8d48f3..6f67e6b34ee 100644
--- a/contrib/python/google-auth/py3/google/auth/version.py
+++ b/contrib/python/google-auth/py3/google/auth/version.py
@@ -12,4 +12,4 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-__version__ = "2.41.0"
+__version__ = "2.41.1"
diff --git a/contrib/python/google-auth/py3/ya.make b/contrib/python/google-auth/py3/ya.make
index f4ad9bdb5f6..3394fdb9d5b 100644
--- a/contrib/python/google-auth/py3/ya.make
+++ b/contrib/python/google-auth/py3/ya.make
@@ -2,7 +2,7 @@
PY3_LIBRARY()
-VERSION(2.41.0)
+VERSION(2.41.1)
LICENSE(Apache-2.0)
diff --git a/contrib/python/textual/.dist-info/METADATA b/contrib/python/textual/.dist-info/METADATA
index 850f9446219..92fb26a5660 100644
--- a/contrib/python/textual/.dist-info/METADATA
+++ b/contrib/python/textual/.dist-info/METADATA
@@ -1,13 +1,13 @@
Metadata-Version: 2.1
Name: textual
-Version: 0.86.3
+Version: 0.89.1
Summary: Modern Text User Interface framework
Home-page: https://github.com/Textualize/textual
License: MIT
Author: Will McGugan
Author-email: [email protected]
Requires-Python: >=3.8.1,<4.0.0
-Classifier: Development Status :: 4 - Beta
+Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
@@ -20,14 +20,29 @@ Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
+Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.8
Classifier: Typing :: Typed
Provides-Extra: syntax
Requires-Dist: markdown-it-py[linkify,plugins] (>=2.1.0)
Requires-Dist: platformdirs (>=3.6.0,<5)
Requires-Dist: rich (>=13.3.3)
-Requires-Dist: tree-sitter (>=0.20.1,<0.21.0) ; extra == "syntax"
-Requires-Dist: tree-sitter-languages (==1.10.2) ; extra == "syntax"
+Requires-Dist: tree-sitter (>=0.23.0) ; (python_version >= "3.9") and (extra == "syntax")
+Requires-Dist: tree-sitter-bash (>=0.23.0) ; (python_version >= "3.9") and (extra == "syntax")
+Requires-Dist: tree-sitter-css (>=0.23.0) ; (python_version >= "3.9") and (extra == "syntax")
+Requires-Dist: tree-sitter-go (>=0.23.0) ; (python_version >= "3.9") and (extra == "syntax")
+Requires-Dist: tree-sitter-html (>=0.23.0) ; (python_version >= "3.9") and (extra == "syntax")
+Requires-Dist: tree-sitter-java (>=0.23.0) ; (python_version >= "3.9") and (extra == "syntax")
+Requires-Dist: tree-sitter-javascript (>=0.23.0) ; (python_version >= "3.9") and (extra == "syntax")
+Requires-Dist: tree-sitter-json (>=0.24.0) ; (python_version >= "3.9") and (extra == "syntax")
+Requires-Dist: tree-sitter-markdown (>=0.3.0) ; (python_version >= "3.9") and (extra == "syntax")
+Requires-Dist: tree-sitter-python (>=0.23.0) ; (python_version >= "3.9") and (extra == "syntax")
+Requires-Dist: tree-sitter-regex (>=0.24.0) ; (python_version >= "3.9") and (extra == "syntax")
+Requires-Dist: tree-sitter-rust (>=0.23.0) ; (python_version >= "3.9") and (extra == "syntax")
+Requires-Dist: tree-sitter-sql (>=0.3.0) ; (python_version >= "3.9") and (extra == "syntax")
+Requires-Dist: tree-sitter-toml (>=0.6.0) ; (python_version >= "3.9") and (extra == "syntax")
+Requires-Dist: tree-sitter-xml (>=0.7.0) ; (python_version >= "3.9") and (extra == "syntax")
+Requires-Dist: tree-sitter-yaml (>=0.6.0) ; (python_version >= "3.9") and (extra == "syntax")
Requires-Dist: typing-extensions (>=4.4.0,<5.0.0)
Project-URL: Bug Tracker, https://github.com/Textualize/textual/issues
Project-URL: Documentation, https://textual.textualize.io/
@@ -36,173 +51,139 @@ Description-Content-Type: text/markdown
+[![Discord](https://img.shields.io/discord/1026214085173461072)](https://discord.gg/Enf6Z3qhVr)
+[![Supported Python Versions](https://img.shields.io/pypi/pyversions/textual/0.87.1)](https://pypi.org/project/textual/)
+[![PyPI version](https://badge.fury.io/py/textual.svg)](https://badge.fury.io/py/textual)
+![OS support](https://img.shields.io/badge/OS-macOS%20Linux%20Windows-red)
-![Textual splash image](https://raw.githubusercontent.com/Textualize/textual/main/imgs/textual.png)
-[![Discord](https://img.shields.io/discord/1026214085173461072)](https://discord.gg/Enf6Z3qhVr)
+![textual-splash](https://github.com/user-attachments/assets/4caeb77e-48c0-4cf7-b14d-c53ded855ffd)
# Textual
-Textual is a *Rapid Application Development* framework for Python.
+Build cross-platform user interfaces with a simple Python API.
-Build sophisticated user interfaces with a simple Python API. Run your apps in the terminal and a [web browser](https://github.com/Textualize/textual-web)!
+Run your apps in the terminal *or* a web browser.
-<details>
- <summary> 🎬 Demonstration </summary>
- <hr>
+## Widgets
-A quick run through of some Textual features.
+Textual's library of [widgets](https://textual.textualize.io/widget_gallery/) covers everything from buttons, tree controls, data tables, inputs, text areas, and more…
+Combined with a flexible [layout](https://textual.textualize.io/how-to/design-a-layout/) system, you can realize any User Interface you need.
+Predefined themes ensure your apps will look good out of the box.
-https://user-images.githubusercontent.com/554369/197355913-65d3c125-493d-4c05-a590-5311f16c40ff.mov
+<table>
+<tr>
+ <td>
+
+ ![buttons](https://github.com/user-attachments/assets/2ac26387-aaa3-41ed-bc00-7d488600343c)
+
+ </td>
- </details>
+ <td>
+
+![tree](https://github.com/user-attachments/assets/61ccd6e9-97ea-4918-8eda-3ee0f0d3770e)
+
+ </td>
+
+</tr>
-## About
+<tr>
-Textual adds interactivity to [Rich](https://github.com/Textualize/rich) with an API inspired by modern web development.
+ <td>
+
+ ![datatables](https://github.com/user-attachments/assets/3e1f9f7a-f965-4901-a114-3c188bd17695)
+
+ </td>
-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.
+ <td>
+
+![inputs](https://github.com/user-attachments/assets/b02aa203-7c37-42da-a1bb-2cb244b7d0d3)
+
+ </td>
+
+</tr>
+<tr>
-## Compatibility
+<td>
-Textual runs on Linux, macOS, and Windows. Textual requires Python 3.8 or above.
+![listview](https://github.com/user-attachments/assets/963603bc-aa07-4688-bd24-379962ece871)
-## Installing
+</td>
-Install Textual via pip:
+<td>
-```
-pip install textual
-```
+![textarea](https://github.com/user-attachments/assets/cd4ba787-5519-40e2-8d86-8224e1b7e506)
+
+</td>
-If you plan on developing Textual apps, you should also install the development tools with the following command:
+
+</tr>
-```
-pip install textual-dev
-```
+</table>
-See the [docs](https://textual.textualize.io/getting_started/) if you need help getting started.
-## Demo
-Run the following command to see a little of what Textual can do:
+## Installing
+
+Install Textual via pip:
```
-python -m textual
+pip install textual textual-dev
```
-![Textual demo](https://raw.githubusercontent.com/Textualize/textual/main/imgs/demo.png)
-
-## Documentation
-
-Head over to the [Textual documentation](http://textual.textualize.io/) to start building!
-
-## Join us on Discord
-
-Join the Textual developers and community on our [Discord Server](https://discord.gg/Enf6Z3qhVr).
-
-## Examples
-
-The Textual repository comes with a number of examples you can experiment with or use as a template for your own projects.
-
-
-<details>
- <summary> 🎬 Code browser </summary>
- <hr>
-
- This is the [code_browser.py](https://github.com/Textualize/textual/blob/main/examples/code_browser.py) example which clocks in at 61 lines (*including* docstrings and blank lines).
-
-https://user-images.githubusercontent.com/554369/197188237-88d3f7e4-4e5f-40b5-b996-c47b19ee2f49.mov
-
- </details>
-
+See [getting started](https://textual.textualize.io/getting_started/) for details.
-<details>
- <summary> πŸ“· Calculator </summary>
- <hr>
-This is [calculator.py](https://github.com/Textualize/textual/blob/main/examples/calculator.py) which demonstrates Textual grid layouts.
-
-![calculator screenshot](https://raw.githubusercontent.com/Textualize/textual/main/imgs/calculator.png)
-</details>
-
-
-<details>
- <summary> 🎬 Stopwatch </summary>
- <hr>
-
- This is the Stopwatch example from the [tutorial](https://textual.textualize.io/tutorial/).
-
-
-
-https://user-images.githubusercontent.com/554369/197360718-0c834ef5-6285-4d37-85cf-23eed4aa56c5.mov
-
-
-
-</details>
-
-
-
-## Reference commands
-
-The `textual` command has a few sub-commands to preview Textual styles.
+## Demo
-<details>
- <summary> 🎬 Easing reference </summary>
- <hr>
-This is the *easing* reference which demonstrates the easing parameter on animation, with both movement and opacity. You can run it with the following command:
+Run the following command to see a little of what Textual can do:
-```bash
-textual easing
+```
+python -m textual
```
-
-https://user-images.githubusercontent.com/554369/196157100-352852a6-2b09-4dc8-a888-55b53570aff9.mov
-
-
- </details>
-
-<details>
- <summary> 🎬 Borders reference </summary>
- <hr>
-
-This is the borders reference which demonstrates some of the borders styles in Textual. You can run it with the following command:
+Or try the [textual demo](https://github.com/textualize/textual-demo) *without* installing (requires [uv](https://docs.astral.sh/uv/)):
```bash
-textual borders
+uvx --python 3.12 textual-demo
```
-https://user-images.githubusercontent.com/554369/196158235-4b45fb78-053d-4fd5-b285-e09b4f1c67a8.mov
+## Textual ❀️ Web
+<img align="right" width="40%" alt="textual-serve" src="https://github.com/user-attachments/assets/a25820fb-87ae-433a-858b-ac3940169242">
-</details>
+Textual apps are equally at home in the browser as they are the terminal.
-<details>
- <summary> 🎬 Colors reference </summary>
- <hr>
+Any Textual app may be served with `textual serve` &mdash; so you can share your creations on the web.
-This is a reference for Textual's color design system.
+Here's how to serve the demo app:
-```bash
-textual colors
+```
+textual serve "python -m textual"
```
+In addition to serving your apps locally, you can serve apps with [Textual-Web](https://github.com/Textualize/textual-web).
+Textual Web's firewall-busting technology can serve an unlimited number of applications.
-https://user-images.githubusercontent.com/554369/197357417-2d407aac-8969-44d3-8250-eea45df79d57.mov
+Since Textual apps have low system requirements, you can install them anywhere Python also runs. Turning any device in to a connected device.
+No desktop required!
+## Documentation
+Head over to the [Textual documentation](http://textual.textualize.io/) to start building.
+## Join us on Discord
-</details>
+Join the Textual developers and community on our [Discord Server](https://discord.gg/Enf6Z3qhVr).
diff --git a/contrib/python/textual/README.md b/contrib/python/textual/README.md
index e000b74c6c1..ea086dc588f 100644
--- a/contrib/python/textual/README.md
+++ b/contrib/python/textual/README.md
@@ -1,171 +1,137 @@
+[![Discord](https://img.shields.io/discord/1026214085173461072)](https://discord.gg/Enf6Z3qhVr)
+[![Supported Python Versions](https://img.shields.io/pypi/pyversions/textual/0.87.1)](https://pypi.org/project/textual/)
+[![PyPI version](https://badge.fury.io/py/textual.svg)](https://badge.fury.io/py/textual)
+![OS support](https://img.shields.io/badge/OS-macOS%20Linux%20Windows-red)
-![Textual splash image](https://raw.githubusercontent.com/Textualize/textual/main/imgs/textual.png)
-[![Discord](https://img.shields.io/discord/1026214085173461072)](https://discord.gg/Enf6Z3qhVr)
+![textual-splash](https://github.com/user-attachments/assets/4caeb77e-48c0-4cf7-b14d-c53ded855ffd)
# Textual
-Textual is a *Rapid Application Development* framework for Python.
+Build cross-platform user interfaces with a simple Python API.
-Build sophisticated user interfaces with a simple Python API. Run your apps in the terminal and a [web browser](https://github.com/Textualize/textual-web)!
+Run your apps in the terminal *or* a web browser.
-<details>
- <summary> 🎬 Demonstration </summary>
- <hr>
+## Widgets
-A quick run through of some Textual features.
+Textual's library of [widgets](https://textual.textualize.io/widget_gallery/) covers everything from buttons, tree controls, data tables, inputs, text areas, and more…
+Combined with a flexible [layout](https://textual.textualize.io/how-to/design-a-layout/) system, you can realize any User Interface you need.
+Predefined themes ensure your apps will look good out of the box.
-https://user-images.githubusercontent.com/554369/197355913-65d3c125-493d-4c05-a590-5311f16c40ff.mov
+<table>
+<tr>
+ <td>
+
+ ![buttons](https://github.com/user-attachments/assets/2ac26387-aaa3-41ed-bc00-7d488600343c)
+
+ </td>
- </details>
+ <td>
+
+![tree](https://github.com/user-attachments/assets/61ccd6e9-97ea-4918-8eda-3ee0f0d3770e)
+
+ </td>
+
+</tr>
-## About
+<tr>
-Textual adds interactivity to [Rich](https://github.com/Textualize/rich) with an API inspired by modern web development.
+ <td>
+
+ ![datatables](https://github.com/user-attachments/assets/3e1f9f7a-f965-4901-a114-3c188bd17695)
+
+ </td>
-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.
+ <td>
+
+![inputs](https://github.com/user-attachments/assets/b02aa203-7c37-42da-a1bb-2cb244b7d0d3)
+
+ </td>
+
+</tr>
+<tr>
-## Compatibility
+<td>
-Textual runs on Linux, macOS, and Windows. Textual requires Python 3.8 or above.
+![listview](https://github.com/user-attachments/assets/963603bc-aa07-4688-bd24-379962ece871)
-## Installing
+</td>
-Install Textual via pip:
+<td>
-```
-pip install textual
-```
+![textarea](https://github.com/user-attachments/assets/cd4ba787-5519-40e2-8d86-8224e1b7e506)
+
+</td>
-If you plan on developing Textual apps, you should also install the development tools with the following command:
+
+</tr>
-```
-pip install textual-dev
-```
+</table>
-See the [docs](https://textual.textualize.io/getting_started/) if you need help getting started.
-## Demo
-Run the following command to see a little of what Textual can do:
+## Installing
+
+Install Textual via pip:
```
-python -m textual
+pip install textual textual-dev
```
-![Textual demo](https://raw.githubusercontent.com/Textualize/textual/main/imgs/demo.png)
-
-## Documentation
-
-Head over to the [Textual documentation](http://textual.textualize.io/) to start building!
-
-## Join us on Discord
-
-Join the Textual developers and community on our [Discord Server](https://discord.gg/Enf6Z3qhVr).
-
-## Examples
-
-The Textual repository comes with a number of examples you can experiment with or use as a template for your own projects.
-
-
-<details>
- <summary> 🎬 Code browser </summary>
- <hr>
-
- This is the [code_browser.py](https://github.com/Textualize/textual/blob/main/examples/code_browser.py) example which clocks in at 61 lines (*including* docstrings and blank lines).
-
-https://user-images.githubusercontent.com/554369/197188237-88d3f7e4-4e5f-40b5-b996-c47b19ee2f49.mov
-
- </details>
-
+See [getting started](https://textual.textualize.io/getting_started/) for details.
-<details>
- <summary> πŸ“· Calculator </summary>
- <hr>
-This is [calculator.py](https://github.com/Textualize/textual/blob/main/examples/calculator.py) which demonstrates Textual grid layouts.
-
-![calculator screenshot](https://raw.githubusercontent.com/Textualize/textual/main/imgs/calculator.png)
-</details>
-
-
-<details>
- <summary> 🎬 Stopwatch </summary>
- <hr>
-
- This is the Stopwatch example from the [tutorial](https://textual.textualize.io/tutorial/).
-
-
-
-https://user-images.githubusercontent.com/554369/197360718-0c834ef5-6285-4d37-85cf-23eed4aa56c5.mov
-
-
-
-</details>
-
-
-
-## Reference commands
-
-The `textual` command has a few sub-commands to preview Textual styles.
+## Demo
-<details>
- <summary> 🎬 Easing reference </summary>
- <hr>
-This is the *easing* reference which demonstrates the easing parameter on animation, with both movement and opacity. You can run it with the following command:
+Run the following command to see a little of what Textual can do:
-```bash
-textual easing
+```
+python -m textual
```
-
-https://user-images.githubusercontent.com/554369/196157100-352852a6-2b09-4dc8-a888-55b53570aff9.mov
-
-
- </details>
-
-<details>
- <summary> 🎬 Borders reference </summary>
- <hr>
-
-This is the borders reference which demonstrates some of the borders styles in Textual. You can run it with the following command:
+Or try the [textual demo](https://github.com/textualize/textual-demo) *without* installing (requires [uv](https://docs.astral.sh/uv/)):
```bash
-textual borders
+uvx --python 3.12 textual-demo
```
-https://user-images.githubusercontent.com/554369/196158235-4b45fb78-053d-4fd5-b285-e09b4f1c67a8.mov
+## Textual ❀️ Web
+<img align="right" width="40%" alt="textual-serve" src="https://github.com/user-attachments/assets/a25820fb-87ae-433a-858b-ac3940169242">
-</details>
+Textual apps are equally at home in the browser as they are the terminal.
-<details>
- <summary> 🎬 Colors reference </summary>
- <hr>
+Any Textual app may be served with `textual serve` &mdash; so you can share your creations on the web.
-This is a reference for Textual's color design system.
+Here's how to serve the demo app:
-```bash
-textual colors
+```
+textual serve "python -m textual"
```
+In addition to serving your apps locally, you can serve apps with [Textual-Web](https://github.com/Textualize/textual-web).
+Textual Web's firewall-busting technology can serve an unlimited number of applications.
-https://user-images.githubusercontent.com/554369/197357417-2d407aac-8969-44d3-8250-eea45df79d57.mov
+Since Textual apps have low system requirements, you can install them anywhere Python also runs. Turning any device in to a connected device.
+No desktop required!
+## Documentation
+Head over to the [Textual documentation](http://textual.textualize.io/) to start building.
+## Join us on Discord
-</details>
+Join the Textual developers and community on our [Discord Server](https://discord.gg/Enf6Z3qhVr).
diff --git a/contrib/python/textual/textual/_arrange.py b/contrib/python/textual/textual/_arrange.py
index 8c2469670f1..f6dcbf8baf4 100644
--- a/contrib/python/textual/textual/_arrange.py
+++ b/contrib/python/textual/textual/_arrange.py
@@ -50,11 +50,12 @@ def arrange(
get_dock = attrgetter("styles.is_docked")
get_split = attrgetter("styles.is_split")
+ get_display = attrgetter("styles.display")
styles = widget.styles
# Widgets which will be displayed
- display_widgets = [child for child in children if child.styles.display != "none"]
+ display_widgets = [child for child in children if get_display(child) != "none"]
# Widgets organized into layers
layers = _build_layers(display_widgets)
@@ -91,9 +92,7 @@ def arrange(
if layout_widgets:
# Arrange layout widgets (i.e. not docked)
layout_placements = widget.layout.arrange(
- widget,
- layout_widgets,
- dock_region.size,
+ widget, layout_widgets, dock_region.size
)
scroll_spacing = scroll_spacing.grow_maximum(dock_spacing)
placement_offset = dock_region.offset
@@ -110,6 +109,8 @@ def arrange(
layout_placements, placement_offset
)
+ WidgetPlacement.apply_absolute(layout_placements)
+
placements.extend(layout_placements)
return DockArrangeResult(placements, set(display_widgets), scroll_spacing)
@@ -134,7 +135,6 @@ def _arrange_dock_widgets(
size = region.size
width, height = size
null_spacing = NULL_SPACING
- null_offset = NULL_OFFSET
top = right = bottom = left = 0
@@ -167,20 +167,28 @@ def _arrange_dock_widgets(
# Should not occur, mainly to keep Mypy happy
raise AssertionError("invalid value for dock edge") # pragma: no-cover
- align_offset = dock_widget.styles._align_size(
- (widget_width, widget_height), size
+ dock_region = dock_region.shrink(margin)
+ styles = dock_widget.styles
+ offset = (
+ styles.offset.resolve(
+ size,
+ viewport,
+ )
+ if styles.has_rule("offset")
+ else NULL_OFFSET
)
- dock_region = dock_region.shrink(margin).translate(align_offset)
append_placement(
_WidgetPlacement(
dock_region.translate(region_offset),
- null_offset,
+ offset,
null_spacing,
dock_widget,
top_z,
True,
+ False,
)
)
+
dock_spacing = Spacing(top, right, bottom, left)
return (placements, dock_spacing)
@@ -230,7 +238,7 @@ def _arrange_split_widgets(
append_placement(
_WidgetPlacement(
- split_region, null_offset, null_spacing, split_widget, 1, True
+ split_region, null_offset, null_spacing, split_widget, 1, True, False
)
)
diff --git a/contrib/python/textual/textual/_border.py b/contrib/python/textual/textual/_border.py
index e551ec7a610..7e35d7fc54e 100644
--- a/contrib/python/textual/textual/_border.py
+++ b/contrib/python/textual/textual/_border.py
@@ -107,6 +107,11 @@ BORDER_CHARS: dict[
("β–Š", " ", "β–Ž"),
("β–Š", "▁", "β–Ž"),
),
+ "tab": (
+ ("▁", "▁", "▁"),
+ ("β–Ž", " ", "β–Š"),
+ ("β–”", "β–”", "β–”"),
+ ),
"wide": (
("▁", "▁", "▁"),
("β–Ž", " ", "β–Š"),
@@ -205,6 +210,11 @@ BORDER_LOCATIONS: dict[
(2, 0, 1),
(2, 0, 1),
),
+ "tab": (
+ (1, 1, 1),
+ (0, 1, 3),
+ (1, 1, 1),
+ ),
"wide": (
(1, 1, 1),
(0, 1, 3),
@@ -215,7 +225,10 @@ BORDER_LOCATIONS: dict[
# Some borders (such as panel) require that the title (and subtitle) be draw in reverse.
# This is a mapping of the border type on to a tuple for the top and bottom borders, to indicate
# reverse colors is required.
-BORDER_TITLE_FLIP: dict[str, tuple[bool, bool]] = {"panel": (True, False)}
+BORDER_TITLE_FLIP: dict[str, tuple[bool, bool]] = {
+ "panel": (True, False),
+ "tab": (True, True),
+}
# In a similar fashion, we extract the border _label_ locations for easier access when
# rendering a border label.
@@ -343,6 +356,7 @@ def render_border_label(
cells_reserved = 2 * corners_needed
text_label, label_style = label
+
if not text_label.cell_len or width <= cells_reserved:
return
diff --git a/contrib/python/textual/textual/_compositor.py b/contrib/python/textual/textual/_compositor.py
index 7aac04393c4..f35e22f1c4c 100644
--- a/contrib/python/textual/textual/_compositor.py
+++ b/contrib/python/textual/textual/_compositor.py
@@ -598,6 +598,7 @@ class Compositor:
if widget.is_container:
# Arrange the layout
arrange_result = widget._arrange(child_region.size)
+
arranged_widgets = arrange_result.widgets
widgets.update(arranged_widgets)
@@ -616,6 +617,11 @@ class Compositor:
placement_offset = container_region.offset
placement_scroll_offset = placement_offset - widget.scroll_offset
+ placements = [
+ placement.process_offset(size.region, placement_scroll_offset)
+ for placement in placements
+ ]
+
layers_to_index = {
layer_name: index
for index, layer_name in enumerate(widget.layers)
@@ -643,6 +649,7 @@ class Compositor:
z,
fixed,
overlay,
+ absolute,
) in reversed(placements):
layer_index = get_layer_index(sub_widget.layer, 0)
# Combine regions with children to calculate the "virtual size"
@@ -657,17 +664,6 @@ class Compositor:
widget_order = order + ((layer_index, z, layer_order),)
- if overlay:
- styles = sub_widget.styles
- has_rule = styles.has_rule
- if has_rule("constrain_x") or has_rule("constrain_y"):
- widget_region = widget_region.constrain(
- styles.constrain_x,
- styles.constrain_y,
- styles.margin,
- no_clip,
- )
-
if widget._cover_widget is None:
add_widget(
sub_widget,
@@ -712,22 +708,6 @@ class Compositor:
elif visible:
# Add the widget to the map
-
- if widget.absolute_offset is not None:
- margin = styles.margin
- region = region.at_offset(widget.absolute_offset + margin.top_left)
- region = region.translate(
- styles.offset.resolve(region.grow(margin).size, size)
- )
- has_rule = styles.has_rule
- if has_rule("constrain_x") or has_rule("constrain_y"):
- region = region.constrain(
- styles.constrain_x,
- styles.constrain_y,
- styles.margin,
- size.region,
- )
-
map[widget._render_widget] = _MapGeometry(
region,
order,
diff --git a/contrib/python/textual/textual/_text_area_theme.py b/contrib/python/textual/textual/_text_area_theme.py
index e315716f30a..0585bf81b27 100644
--- a/contrib/python/textual/textual/_text_area_theme.py
+++ b/contrib/python/textual/textual/_text_area_theme.py
@@ -148,7 +148,7 @@ class TextAreaTheme:
self.selection_style = selection_style
else:
selection_background_color = background_color.blend(
- app_theme.primary, factor=0.75
+ app_theme.primary, factor=0.5
)
self.selection_style = Style.from_color(
bgcolor=selection_background_color.rich_color
@@ -204,8 +204,9 @@ _MONOKAI = TextAreaTheme(
"string": Style(color="#E6DB74"),
"string.documentation": Style(color="#E6DB74"),
"comment": Style(color="#75715E"),
+ "heading.marker": Style(color="#90908a"),
"keyword": Style(color="#F92672"),
- "operator": Style(color="#F92672"),
+ "operator": Style(color="#f8f8f2"),
"repeat": Style(color="#F92672"),
"exception": Style(color="#F92672"),
"include": Style(color="#F92672"),
@@ -216,7 +217,10 @@ _MONOKAI = TextAreaTheme(
"number": Style(color="#AE81FF"),
"float": Style(color="#AE81FF"),
"class": Style(color="#A6E22E"),
+ "type": Style(color="#A6E22E"),
"type.class": Style(color="#A6E22E"),
+ "type.builtin": Style(color="#F92672"),
+ "variable.builtin": Style(color="#f8f8f2"),
"function": Style(color="#A6E22E"),
"function.call": Style(color="#A6E22E"),
"method": Style(color="#A6E22E"),
@@ -232,12 +236,18 @@ _MONOKAI = TextAreaTheme(
"json.label": Style(color="#F92672", bold=True),
"toml.type": Style(color="#F92672"),
"toml.datetime": Style(color="#AE81FF"),
+ "css.property": Style(color="#AE81FF"),
"heading": Style(color="#F92672", bold=True),
"bold": Style(bold=True),
"italic": Style(italic=True),
"strikethrough": Style(strike=True),
- "link": Style(color="#66D9EF", underline=True),
+ "link.label": Style(color="#F92672"),
+ "link.uri": Style(color="#66D9EF", underline=True),
+ "list.marker": Style(color="#90908a"),
"inline_code": Style(color="#E6DB74"),
+ "punctuation.bracket": Style(color="#f8f8f2"),
+ "punctuation.delimiter": Style(color="#f8f8f2"),
+ "punctuation.special": Style(color="#f8f8f2"),
},
)
@@ -254,8 +264,9 @@ _DRACULA = TextAreaTheme(
"string": Style(color="#f1fa8c"),
"string.documentation": Style(color="#f1fa8c"),
"comment": Style(color="#6272a4"),
+ "heading.marker": Style(color="#6272a4"),
"keyword": Style(color="#ff79c6"),
- "operator": Style(color="#ff79c6"),
+ "operator": Style(color="#f8f8f2"),
"repeat": Style(color="#ff79c6"),
"exception": Style(color="#ff79c6"),
"include": Style(color="#ff79c6"),
@@ -266,12 +277,15 @@ _DRACULA = TextAreaTheme(
"number": Style(color="#bd93f9"),
"float": Style(color="#bd93f9"),
"class": Style(color="#50fa7b"),
+ "type": Style(color="#ff79c6"),
"type.class": Style(color="#50fa7b"),
+ "type.builtin": Style(color="#bd93f9"),
+ "variable.builtin": Style(color="#f8f8f2"),
"function": Style(color="#50fa7b"),
"function.call": Style(color="#50fa7b"),
"method": Style(color="#50fa7b"),
"method.call": Style(color="#50fa7b"),
- "boolean": Style(color="#bd93f9"),
+ "boolean": Style(color="#50fa7b"),
"constant.builtin": Style(color="#bd93f9"),
"json.null": Style(color="#bd93f9"),
"regex.punctuation.bracket": Style(color="#ff79c6"),
@@ -282,12 +296,18 @@ _DRACULA = TextAreaTheme(
"json.label": Style(color="#ff79c6", bold=True),
"toml.type": Style(color="#ff79c6"),
"toml.datetime": Style(color="#bd93f9"),
+ "css.property": Style(color="#bd93f9"),
"heading": Style(color="#ff79c6", bold=True),
"bold": Style(bold=True),
"italic": Style(italic=True),
"strikethrough": Style(strike=True),
- "link": Style(color="#bd93f9", underline=True),
+ "link.label": Style(color="#ff79c6"),
+ "link.uri": Style(color="#bd93f9", underline=True),
+ "list.marker": Style(color="#6272a4"),
"inline_code": Style(color="#f1fa8c"),
+ "punctuation.bracket": Style(color="#f8f8f2"),
+ "punctuation.delimiter": Style(color="#f8f8f2"),
+ "punctuation.special": Style(color="#f8f8f2"),
},
)
@@ -304,8 +324,9 @@ _DARK_VS = TextAreaTheme(
"string": Style(color="#ce9178"),
"string.documentation": Style(color="#ce9178"),
"comment": Style(color="#6A9955"),
- "keyword": Style(color="#569cd6"),
- "operator": Style(color="#569cd6"),
+ "heading.marker": Style(color="#6E7681"),
+ "keyword": Style(color="#C586C0"),
+ "operator": Style(color="#CCCCCC"),
"conditional": Style(color="#569cd6"),
"keyword.function": Style(color="#569cd6"),
"keyword.return": Style(color="#569cd6"),
@@ -316,11 +337,14 @@ _DARK_VS = TextAreaTheme(
"number": Style(color="#b5cea8"),
"float": Style(color="#b5cea8"),
"class": Style(color="#4EC9B0"),
+ "type": Style(color="#EFCB43"),
"type.class": Style(color="#4EC9B0"),
- "function": Style(color="#4EC9B0"),
- "function.call": Style(color="#4EC9B0"),
+ "type.builtin": Style(color="#9CDCFE"),
+ "function": Style(color="#DCDCAA"),
+ "function.call": Style(color="#DCDCAA"),
"method": Style(color="#4EC9B0"),
"method.call": Style(color="#4EC9B0"),
+ "constructor": Style(color="#4EC9B0"),
"boolean": Style(color="#7DAF9C"),
"constant.builtin": Style(color="#7DAF9C"),
"json.null": Style(color="#7DAF9C"),
@@ -328,13 +352,20 @@ _DARK_VS = TextAreaTheme(
"yaml.field": Style(color="#569cd6", bold=True),
"json.label": Style(color="#569cd6", bold=True),
"toml.type": Style(color="#569cd6"),
+ "toml.datetime": Style(color="#C586C0", italic=True),
+ "css.property": Style(color="#569cd6"),
"heading": Style(color="#569cd6", bold=True),
"bold": Style(bold=True),
"italic": Style(italic=True),
"strikethrough": Style(strike=True),
- "link": Style(color="#40A6FF", underline=True),
+ "link.uri": Style(color="#40A6FF", underline=True),
+ "link.label": Style(color="#569cd6"),
+ "list.marker": Style(color="#6E7681"),
"inline_code": Style(color="#ce9178"),
"info_string": Style(color="#ce9178", bold=True, italic=True),
+ "punctuation.bracket": Style(color="#CCCCCC"),
+ "punctuation.delimiter": Style(color="#CCCCCC"),
+ "punctuation.special": Style(color="#CCCCCC"),
},
)
@@ -351,6 +382,10 @@ _GITHUB_LIGHT = TextAreaTheme(
"string": Style(color="#093069"),
"string.documentation": Style(color="#093069"),
"comment": Style(color="#6a737d"),
+ "heading.marker": Style(color="#A4A4A4"),
+ "type": Style(color="#A4A4A4"),
+ "type.class": Style(color="#A4A4A4"),
+ "type.builtin": Style(color="#7DAF9C"),
"keyword": Style(color="#d73a49"),
"operator": Style(color="#0450AE"),
"conditional": Style(color="#CF222E"),
@@ -373,12 +408,18 @@ _GITHUB_LIGHT = TextAreaTheme(
"yaml.field": Style(color="#6639BB"),
"json.label": Style(color="#6639BB"),
"toml.type": Style(color="#6639BB"),
+ "css.property": Style(color="#6639BB"),
"heading": Style(color="#24292e", bold=True),
"bold": Style(bold=True),
"italic": Style(italic=True),
"strikethrough": Style(strike=True),
- "link": Style(color="#40A6FF", underline=True),
+ "link.uri": Style(color="#40A6FF", underline=True),
+ "link.label": Style(color="#6639BB"),
+ "list.marker": Style(color="#A4A4A4"),
"inline_code": Style(color="#093069"),
+ "punctuation.bracket": Style(color="#24292e"),
+ "punctuation.delimiter": Style(color="#24292e"),
+ "punctuation.special": Style(color="#24292e"),
},
)
diff --git a/contrib/python/textual/textual/_tree_sitter.py b/contrib/python/textual/textual/_tree_sitter.py
index 01e300115cf..193bf16fd41 100644
--- a/contrib/python/textual/textual/_tree_sitter.py
+++ b/contrib/python/textual/textual/_tree_sitter.py
@@ -1,10 +1,49 @@
from __future__ import annotations
try:
- from tree_sitter import Language, Parser, Tree
- from tree_sitter.binding import Query
- from tree_sitter_languages import get_language, get_parser
+ import tree_sitter_bash
+ import tree_sitter_css
+ import tree_sitter_go
+ import tree_sitter_html
+ import tree_sitter_java
+ import tree_sitter_javascript
+ import tree_sitter_json
+ import tree_sitter_markdown
+ import tree_sitter_python
+ import tree_sitter_regex
+ import tree_sitter_rust
+ import tree_sitter_sql
+ import tree_sitter_toml
+ import tree_sitter_xml
+ import tree_sitter_yaml
+ from tree_sitter import Language
+
+ _tree_sitter = True
+
+ _languages = {
+ "python": Language(tree_sitter_python.language()),
+ "json": Language(tree_sitter_json.language()),
+ "markdown": Language(tree_sitter_markdown.language()),
+ "yaml": Language(tree_sitter_yaml.language()),
+ "toml": Language(tree_sitter_toml.language()),
+ "rust": Language(tree_sitter_rust.language()),
+ "html": Language(tree_sitter_html.language()),
+ "css": Language(tree_sitter_css.language()),
+ "xml": Language(tree_sitter_xml.language_xml()),
+ "regex": Language(tree_sitter_regex.language()),
+ "sql": Language(tree_sitter_sql.language()),
+ "javascript": Language(tree_sitter_javascript.language()),
+ "java": Language(tree_sitter_java.language()),
+ "bash": Language(tree_sitter_bash.language()),
+ "go": Language(tree_sitter_go.language()),
+ }
+
+ def get_language(language_name: str) -> Language | None:
+ return _languages.get(language_name)
- TREE_SITTER = True
except ImportError:
- TREE_SITTER = False
+ _tree_sitter = False
+ _languages = {}
+
+TREE_SITTER = _tree_sitter
+BUILTIN_LANGUAGES: dict[str, "Language"] = _languages
diff --git a/contrib/python/textual/textual/app.py b/contrib/python/textual/textual/app.py
index e94c9595b08..d54f5f49f74 100644
--- a/contrib/python/textual/textual/app.py
+++ b/contrib/python/textual/textual/app.py
@@ -123,6 +123,7 @@ from textual.screen import (
from textual.signal import Signal
from textual.theme import BUILTIN_THEMES, Theme, ThemeProvider
from textual.timer import Timer
+from textual.visual import SupportsVisual, Visual
from textual.widget import AwaitMount, Widget
from textual.widgets._toast import ToastRack
from textual.worker import NoActiveWorker, get_current_worker
@@ -152,7 +153,7 @@ if constants.DEBUG:
_ASYNCIO_GET_EVENT_LOOP_IS_DEPRECATED = sys.version_info >= (3, 10, 0)
ComposeResult = Iterable[Widget]
-RenderResult = "RenderableType | Visual | SupportsTextualize"
+RenderResult: TypeAlias = "RenderableType | Visual | SupportsVisual"
"""Result of Widget.render()"""
AutopilotCallbackType: TypeAlias = (
@@ -632,8 +633,6 @@ class App(Generic[ReturnType], DOMNode):
self._logger = Logger(self._log)
- self._refresh_required = False
-
self._css_has_errors = False
self.theme_variables: dict[str, str] = {}
@@ -3068,6 +3067,9 @@ class App(Generic[ReturnType], DOMNode):
try:
try:
await self._dispatch_message(events.Compose())
+ await self._dispatch_message(
+ events.Resize.from_dimensions(self.size, None)
+ )
default_screen = self.screen
self.stylesheet.apply(self)
await self._dispatch_message(events.Mount())
diff --git a/contrib/python/textual/textual/color.py b/contrib/python/textual/textual/color.py
index c5f5abcf3f4..e0ddf3b944e 100644
--- a/contrib/python/textual/textual/color.py
+++ b/contrib/python/textual/textual/color.py
@@ -40,6 +40,7 @@ import rich.repr
from rich.color import Color as RichColor
from rich.color import ColorType
from rich.color_triplet import ColorTriplet
+from rich.terminal_theme import TerminalTheme
from typing_extensions import Final
from textual._color_constants import ANSI_COLORS, COLOR_NAME_TO_RGB
@@ -176,18 +177,21 @@ class Color(NamedTuple):
return cls(0, 0, 0, alpha_percentage / 100.0, auto=True)
@classmethod
- def from_rich_color(cls, rich_color: RichColor | None) -> Color:
+ def from_rich_color(
+ cls, rich_color: RichColor | None, theme: TerminalTheme | None = None
+ ) -> Color:
"""Create a new color from Rich's Color class.
Args:
rich_color: An instance of [Rich color][rich.color.Color].
+ theme: Optional Rich [terminal theme][rich.terminal_theme.TerminalTheme].
Returns:
A new Color instance.
"""
if rich_color is None:
return TRANSPARENT
- r, g, b = rich_color.get_truecolor()
+ r, g, b = rich_color.get_truecolor(theme)
return cls(r, g, b)
@classmethod
diff --git a/contrib/python/textual/textual/command.py b/contrib/python/textual/textual/command.py
index 68677ba0018..9e28eddb318 100644
--- a/contrib/python/textual/textual/command.py
+++ b/contrib/python/textual/textual/command.py
@@ -1120,7 +1120,7 @@ class CommandPalette(SystemModalScreen[None]):
help_style = VisualStyle.from_styles(
self.get_component_styles("command-palette--help-text")
)
- yield Content.styled(hit.help, help_style)
+ yield Content.from_rich_text(hit.help).stylize_before(help_style)
prompt = Content("\n").join(build_prompt())
diff --git a/contrib/python/textual/textual/content.py b/contrib/python/textual/textual/content.py
index 6df51e99cee..7b402de45e7 100644
--- a/contrib/python/textual/textual/content.py
+++ b/contrib/python/textual/textual/content.py
@@ -19,9 +19,11 @@ from rich._wrap import divide_line
from rich.cells import set_cell_size
from rich.console import OverflowMethod
from rich.segment import Segment, Segments
+from rich.terminal_theme import TerminalTheme
from rich.text import Text
from textual._cells import cell_len
+from textual._context import active_app
from textual._loop import loop_last
from textual.color import Color
from textual.css.types import TextAlign
@@ -180,15 +182,28 @@ class Content(Visual):
New Content.
"""
if isinstance(text, str):
- return cls(text, align=align, no_wrap=no_wrap, ellipsis=ellipsis)
- spans = [
- Span(
- start,
- end,
- style if isinstance(style, str) else Style.from_rich_style(style),
- )
- for start, end, style in text._spans
- ]
+ text = Text.from_markup(text)
+ if text._spans:
+ ansi_theme: TerminalTheme | None
+ try:
+ ansi_theme = active_app.get().ansi_theme
+ except LookupError:
+ ansi_theme = None
+ spans = [
+ Span(
+ start,
+ end,
+ (
+ style
+ if isinstance(style, str)
+ else Style.from_rich_style(style, ansi_theme)
+ ),
+ )
+ for start, end, style in text._spans
+ ]
+ else:
+ spans = []
+
return cls(
text.plain,
spans,
@@ -681,9 +696,15 @@ class Content(Visual):
return
if parse_style is None:
+ app = active_app.get()
+ # TODO: Update when we add Content.from_markup
def get_style(style: str, /) -> Style:
- return TRANSPARENT_STYLE if isinstance(style, str) else style
+ return (
+ Style.from_rich_style(app.console.get_style(style), app.ansi_theme)
+ if isinstance(style, str)
+ else style
+ )
else:
get_style = parse_style
diff --git a/contrib/python/textual/textual/css/_help_text.py b/contrib/python/textual/textual/css/_help_text.py
index 1bed8bdce1e..2e925e0de68 100644
--- a/contrib/python/textual/textual/css/_help_text.py
+++ b/contrib/python/textual/textual/css/_help_text.py
@@ -14,6 +14,7 @@ from textual.css.constants import (
VALID_BORDER,
VALID_KEYLINE,
VALID_LAYOUT,
+ VALID_POSITION,
VALID_STYLE_FLAGS,
VALID_TEXT_ALIGN,
)
@@ -305,6 +306,7 @@ def color_property_help_text(
context: StylingContext,
*,
error: Exception | None = None,
+ value: str | None = None,
) -> HelpText:
"""Help text to show when the user supplies an invalid value for a color
property. For example, an unparsable color string.
@@ -318,7 +320,10 @@ def color_property_help_text(
Renderable for displaying the help text for this property.
"""
property_name = _contextualize_property_name(property_name, context)
- summary = f"Invalid value for the [i]{property_name}[/] property"
+ if value is None:
+ summary = f"Invalid value for the [i]{property_name}[/] property"
+ else:
+ summary = f"Invalid value ({value!r}) for the [i]{property_name}[/] property"
suggested_color = (
error.suggested_color if error and isinstance(error, ColorParseError) else None
)
@@ -766,6 +771,23 @@ def offset_single_axis_help_text(property_name: str) -> HelpText:
)
+def position_help_text(property_name: str) -> HelpText:
+ """Help text to show when the user supplies the wrong value for position.
+
+ Args:
+ property_name: The name of the property.
+
+ Returns:
+ Renderable for displaying the help text for this property.
+ """
+ return HelpText(
+ summary=f"Invalid value for [i]{property_name}[/]",
+ bullets=[
+ Bullet(f"Valid values are {friendly_list(VALID_POSITION)}"),
+ ],
+ )
+
+
def style_flags_property_help_text(
property_name: str, value: str, context: StylingContext
) -> HelpText:
diff --git a/contrib/python/textual/textual/css/_style_properties.py b/contrib/python/textual/textual/css/_style_properties.py
index d7f8f8a70bb..f4fc9199b21 100644
--- a/contrib/python/textual/textual/css/_style_properties.py
+++ b/contrib/python/textual/textual/css/_style_properties.py
@@ -745,10 +745,10 @@ class OffsetProperty:
_rich_traceback_omit = True
if offset is None:
if obj.clear_rule(self.name):
- obj.refresh(layout=True)
+ obj.refresh(layout=True, repaint=False)
elif isinstance(offset, ScalarOffset):
if obj.set_rule(self.name, offset):
- obj.refresh(layout=True)
+ obj.refresh(layout=True, repaint=False)
else:
x, y = offset
@@ -771,7 +771,7 @@ class OffsetProperty:
_offset = ScalarOffset(scalar_x, scalar_y)
if obj.set_rule(self.name, _offset):
- obj.refresh(layout=True)
+ obj.refresh(layout=True, repaint=False)
class StringEnumProperty(Generic[EnumType]):
@@ -988,7 +988,7 @@ class ColorProperty:
raise StyleValueError(
f"Invalid color value '{token}'",
help_text=color_property_help_text(
- self.name, context="inline", error=error
+ self.name, context="inline", error=error, value=token
),
)
parsed_color = parsed_color.with_alpha(alpha)
diff --git a/contrib/python/textual/textual/css/_styles_builder.py b/contrib/python/textual/textual/css/_styles_builder.py
index 11ff7e370ce..b5ccc4802d3 100644
--- a/contrib/python/textual/textual/css/_styles_builder.py
+++ b/contrib/python/textual/textual/css/_styles_builder.py
@@ -22,6 +22,7 @@ from textual.css._help_text import (
layout_property_help_text,
offset_property_help_text,
offset_single_axis_help_text,
+ position_help_text,
property_invalid_value_help_text,
scalar_help_text,
scrollbar_size_property_help_text,
@@ -47,6 +48,7 @@ from textual.css.constants import (
VALID_KEYLINE,
VALID_OVERFLOW,
VALID_OVERLAY,
+ VALID_POSITION,
VALID_SCROLLBAR_GUTTER,
VALID_STYLE_FLAGS,
VALID_TEXT_ALIGN,
@@ -553,7 +555,9 @@ class StylesBuilder:
self.error(
name,
token,
- color_property_help_text(name, context="css", error=error),
+ color_property_help_text(
+ name, context="css", error=error, value=token.value
+ ),
)
elif token.name == "token":
try:
@@ -620,6 +624,17 @@ class StylesBuilder:
x = self.styles.offset.x
self.styles._rules["offset"] = ScalarOffset(x, y)
+ def process_position(self, name: str, tokens: list[Token]):
+ if not tokens:
+ return
+ if len(tokens) != 1:
+ self.error(name, tokens[0], offset_single_axis_help_text(name))
+ else:
+ token = tokens[0]
+ if token.value not in VALID_POSITION:
+ self.error(name, tokens[0], position_help_text(name))
+ self.styles._rules["position"] = token.value
+
def process_layout(self, name: str, tokens: list[Token]) -> None:
from textual.layouts.factory import MissingLayout, get_layout
@@ -668,10 +683,16 @@ class StylesBuilder:
self.error(
name,
token,
- color_property_help_text(name, context="css", error=error),
+ color_property_help_text(
+ name, context="css", error=error, value=token.value
+ ),
)
else:
- self.error(name, token, color_property_help_text(name, context="css"))
+ self.error(
+ name,
+ token,
+ color_property_help_text(name, context="css", value=token.value),
+ )
if color is not None or alpha is not None:
if alpha is not None:
@@ -1149,7 +1170,9 @@ class StylesBuilder:
self.error(
name,
color_token,
- color_property_help_text(name, context="css", error=error),
+ color_property_help_text(
+ name, context="css", error=error, value=color_token.value
+ ),
)
else:
self.error(
diff --git a/contrib/python/textual/textual/css/constants.py b/contrib/python/textual/textual/css/constants.py
index 82af4e49127..160ff033309 100644
--- a/contrib/python/textual/textual/css/constants.py
+++ b/contrib/python/textual/textual/css/constants.py
@@ -22,6 +22,7 @@ VALID_BORDER: Final = {
"round",
"solid",
"tall",
+ "tab",
"thick",
"vkey",
"wide",
@@ -33,6 +34,7 @@ VALID_BOX_SIZING: Final = {"border-box", "content-box"}
VALID_OVERFLOW: Final = {"scroll", "hidden", "auto"}
VALID_ALIGN_HORIZONTAL: Final = {"left", "center", "right"}
VALID_ALIGN_VERTICAL: Final = {"top", "middle", "bottom"}
+VALID_POSITION: Final = {"relative", "absolute"}
VALID_TEXT_ALIGN: Final = {
"start",
"end",
diff --git a/contrib/python/textual/textual/css/styles.py b/contrib/python/textual/textual/css/styles.py
index 244c2d26165..0a08b42dd3e 100644
--- a/contrib/python/textual/textual/css/styles.py
+++ b/contrib/python/textual/textual/css/styles.py
@@ -45,6 +45,7 @@ from textual.css.constants import (
VALID_DISPLAY,
VALID_OVERFLOW,
VALID_OVERLAY,
+ VALID_POSITION,
VALID_SCROLLBAR_GUTTER,
VALID_TEXT_ALIGN,
VALID_VISIBILITY,
@@ -99,6 +100,7 @@ class RulesMap(TypedDict, total=False):
padding: Spacing
margin: Spacing
offset: ScalarOffset
+ position: str
border_top: tuple[str, Color]
border_right: tuple[str, Color]
@@ -219,6 +221,7 @@ class StylesBase:
"background",
"background_tint",
"opacity",
+ "position",
"text_opacity",
"tint",
"scrollbar_color",
@@ -235,9 +238,7 @@ class StylesBase:
node: DOMNode | None = None
- display = StringEnumProperty(
- VALID_DISPLAY, "block", layout=True, refresh_parent=True, refresh_children=True
- )
+ display = StringEnumProperty(VALID_DISPLAY, "block", layout=True)
"""Set the display of the widget, defining how it's rendered.
Valid values are "block" or "none".
@@ -250,9 +251,7 @@ class StylesBase:
StyleValueError: If an invalid display is specified.
"""
- visibility = StringEnumProperty(
- VALID_VISIBILITY, "visible", layout=True, refresh_parent=True
- )
+ visibility = StringEnumProperty(VALID_VISIBILITY, "visible", layout=True)
"""Set the visibility of the widget.
Valid values are "visible" or "hidden".
@@ -278,6 +277,7 @@ class StylesBase:
"""
auto_color = BooleanProperty(default=False)
+ """Enable automatic picking of best contrasting color."""
color = ColorProperty(Color(255, 255, 255))
"""Set the foreground (text) color of the widget.
Supports `Color` objects but also strings e.g. "red" or "#ff0000".
@@ -307,6 +307,9 @@ class StylesBase:
"""Set the margin (spacing outside the border) of the widget."""
offset = OffsetProperty()
"""Set the offset of the widget relative to where it would have been otherwise."""
+ position = StringEnumProperty(VALID_POSITION, "relative")
+ """If `relative` offset is applied to widgets current position, if `absolute` it is applied to (0, 0)."""
+
border = BorderProperty(layout=True)
"""Set the border of the widget e.g. ("rounded", "green") or "none"."""
@@ -320,7 +323,9 @@ class StylesBase:
"""Set the left border of the widget e.g. ("rounded", "green") or "none"."""
border_title_align = StringEnumProperty(VALID_ALIGN_HORIZONTAL, "left")
+ """The alignment of the border title text."""
border_subtitle_align = StringEnumProperty(VALID_ALIGN_HORIZONTAL, "right")
+ """The alignment of the border subtitle text."""
outline = BorderProperty(layout=False)
"""Set the outline of the widget e.g. ("rounded", "green") or "none".
@@ -336,8 +341,10 @@ class StylesBase:
"""Set the left outline of the widget e.g. ("rounded", "green") or "none"."""
keyline = KeylineProperty()
+ """Keyline parameters."""
box_sizing = StringEnumProperty(VALID_BOX_SIZING, "border-box", layout=True)
+ """Box sizing method ("border-box" or "conetnt-box")"""
width = ScalarProperty(percent_unit=Unit.WIDTH)
"""Set the width of the widget."""
height = ScalarProperty(percent_unit=Unit.HEIGHT)
@@ -626,7 +633,12 @@ class StylesBase:
raise NotImplementedError()
def refresh(
- self, *, layout: bool = False, children: bool = False, parent: bool = False
+ self,
+ *,
+ layout: bool = False,
+ children: bool = False,
+ parent: bool = False,
+ repaint: bool = True,
) -> None:
"""Mark the styles as requiring a refresh.
@@ -634,6 +646,7 @@ class StylesBase:
layout: Also require a layout.
children: Also refresh children.
parent: Also refresh the parent.
+ repaint: Repaint the widgets.
"""
def reset(self) -> None:
@@ -722,6 +735,7 @@ class StylesBase:
offset_x = (parent_width - width) // 2
else:
offset_x = parent_width - width
+
return offset_x
def _align_height(self, height: int, parent_height: int) -> int:
@@ -844,17 +858,22 @@ class Styles(StylesBase):
return changed
def refresh(
- self, *, layout: bool = False, children: bool = False, parent: bool = False
+ self,
+ *,
+ layout: bool = False,
+ children: bool = False,
+ parent: bool = False,
+ repaint=True,
) -> None:
node = self.node
if node is None or not node._is_mounted:
return
if parent and node._parent is not None:
- node._parent.refresh()
+ node._parent.refresh(repaint=repaint)
node.refresh(layout=layout)
if children:
for child in node.walk_children(with_self=False, reverse=True):
- child.refresh(layout=layout)
+ child.refresh(layout=layout, repaint=repaint)
def reset(self) -> None:
"""Reset the rules to initial state."""
@@ -1003,6 +1022,8 @@ class Styles(StylesBase):
if "offset" in rules:
x, y = self.offset
append_declaration("offset", f"{x} {y}")
+ if "position" in rules:
+ append_declaration("position", self.position)
if "dock" in rules:
append_declaration("dock", rules["dock"])
if "split" in rules:
@@ -1326,9 +1347,16 @@ class RenderStyles(StylesBase):
yield rule_name, getattr(self, rule_name)
def refresh(
- self, *, layout: bool = False, children: bool = False, parent: bool = False
+ self,
+ *,
+ layout: bool = False,
+ children: bool = False,
+ parent: bool = False,
+ repaint: bool = True,
) -> None:
- self._inline_styles.refresh(layout=layout, children=children, parent=parent)
+ self._inline_styles.refresh(
+ layout=layout, children=children, parent=parent, repaint=repaint
+ )
def merge(self, other: StylesBase) -> None:
"""Merge values from another Styles.
@@ -1353,6 +1381,19 @@ class RenderStyles(StylesBase):
rule_name
)
+ def has_any_rules(self, *rule_names: str) -> bool:
+ """Check if any of the supplied rules have been set.
+
+ Args:
+ rule_names: Number of rules.
+
+ Returns:
+ `True` if any of the supplied rules have been set, `False` if none have.
+ """
+ inline_has_rule = self._inline_styles.has_rule
+ base_has_rule = self._base_styles.has_rule
+ return any(inline_has_rule(name) or base_has_rule(name) for name in rule_names)
+
def set_rule(self, rule_name: str, value: object | None) -> bool:
self._updates += 1
return self._inline_styles.set_rule(rule_name, value)
diff --git a/contrib/python/textual/textual/css/stylesheet.py b/contrib/python/textual/textual/css/stylesheet.py
index fe3e3fac6ea..82f4ffae778 100644
--- a/contrib/python/textual/textual/css/stylesheet.py
+++ b/contrib/python/textual/textual/css/stylesheet.py
@@ -276,7 +276,7 @@ class Stylesheet:
"""
filename = os.path.expanduser(filename)
try:
- with open(filename, "rt") as css_file:
+ with open(filename, "rt", encoding="utf-8") as css_file:
css = css_file.read()
path = os.path.abspath(filename)
except Exception:
diff --git a/contrib/python/textual/textual/css/types.py b/contrib/python/textual/textual/css/types.py
index f02ff80ed05..a8f6b1f7998 100644
--- a/contrib/python/textual/textual/css/types.py
+++ b/contrib/python/textual/textual/css/types.py
@@ -24,6 +24,7 @@ EdgeType = Literal[
"hkey",
"vkey",
"tall",
+ "tab",
"panel",
"wide",
]
@@ -38,6 +39,7 @@ EdgeStyle = Tuple[EdgeType, Color]
TextAlign = Literal["left", "start", "center", "right", "end", "justify"]
Constrain = Literal["none", "inflect", "inside"]
Overlay = Literal["none", "screen"]
+Position = Literal["relative", "absolute"]
Specificity3 = Tuple[int, int, int]
Specificity6 = Tuple[int, int, int, int, int, int]
diff --git a/contrib/python/textual/textual/demo/demo_app.py b/contrib/python/textual/textual/demo/demo_app.py
index 66b4d5837eb..9fc391f96ef 100644
--- a/contrib/python/textual/textual/demo/demo_app.py
+++ b/contrib/python/textual/textual/demo/demo_app.py
@@ -2,6 +2,7 @@ from __future__ import annotations
from textual.app import App
from textual.binding import Binding
+from textual.demo.game import GameScreen
from textual.demo.home import HomeScreen
from textual.demo.projects import ProjectsScreen
from textual.demo.widgets import WidgetsScreen
@@ -12,8 +13,8 @@ class DemoApp(App):
CSS = """
.column {
- align: center top;
- &>*{ max-width: 100; }
+ align: center top;
+ &>*{ max-width: 100; }
}
Screen .-maximized {
margin: 1 2;
@@ -26,6 +27,7 @@ class DemoApp(App):
"""
MODES = {
+ "game": GameScreen,
"home": HomeScreen,
"projects": ProjectsScreen,
"widgets": WidgetsScreen,
@@ -35,19 +37,25 @@ class DemoApp(App):
Binding(
"h",
"app.switch_mode('home')",
- "home",
+ "Home",
tooltip="Show the home screen",
),
Binding(
+ "g",
+ "app.switch_mode('game')",
+ "Game",
+ tooltip="Unwind with a Textual game",
+ ),
+ Binding(
"p",
"app.switch_mode('projects')",
- "projects",
+ "Projects",
tooltip="A selection of Textual projects",
),
Binding(
"w",
"app.switch_mode('widgets')",
- "widgets",
+ "Widgets",
tooltip="Test the builtin widgets",
),
Binding(
@@ -60,7 +68,7 @@ class DemoApp(App):
"ctrl+a",
"app.maximize",
"Maximize",
- tooltip="Maximized the focused widget (if possible)",
+ tooltip="Maximize the focused widget (if possible)",
),
]
@@ -85,3 +93,13 @@ class DemoApp(App):
title="Maximize",
severity="warning",
)
+
+ def check_action(self, action: str, parameters: tuple[object, ...]) -> bool | None:
+ """Disable switching to a mode we are already on."""
+ if (
+ action == "switch_mode"
+ and parameters
+ and self.current_mode == parameters[0]
+ ):
+ return None
+ return True
diff --git a/contrib/python/textual/textual/demo/game.py b/contrib/python/textual/textual/demo/game.py
new file mode 100644
index 00000000000..116f076f71f
--- /dev/null
+++ b/contrib/python/textual/textual/demo/game.py
@@ -0,0 +1,586 @@
+"""
+An implementation of the "Sliding Tile" puzzle.
+
+Textual isn't a game engine exactly, but it wasn't hard to build this.
+
+"""
+
+from __future__ import annotations
+
+from asyncio import sleep
+from collections import defaultdict
+from dataclasses import dataclass
+from itertools import product
+from random import choice
+from time import monotonic
+
+from rich.console import ConsoleRenderable
+from rich.syntax import Syntax
+
+from textual import containers, events, on, work
+from textual._loop import loop_last
+from textual.app import ComposeResult
+from textual.binding import Binding
+from textual.demo.page import PageScreen
+from textual.geometry import Offset, Size
+from textual.reactive import reactive
+from textual.screen import ModalScreen, Screen
+from textual.timer import Timer
+from textual.widgets import Button, Digits, Footer, Markdown, Select, Static
+
+
+@dataclass
+class NewGame:
+ """A dataclass to report the desired game type."""
+
+ language: str
+ code: str
+ size: tuple[int, int]
+
+
+PYTHON_CODE = '''\
+class SpatialMap(Generic[ValueType]):
+ """A spatial map allows for data to be associated with rectangular regions
+ in Euclidean space, and efficiently queried.
+
+ When the SpatialMap is populated, a reference to each value is placed into one or
+ more buckets associated with a regular grid that covers 2D space.
+
+ The SpatialMap is able to quickly retrieve the values under a given "window" region
+ by combining the values in the grid squares under the visible area.
+ """
+
+ def __init__(self, grid_width: int = 100, grid_height: int = 20) -> None:
+ """Create a spatial map with the given grid size.
+
+ Args:
+ grid_width: Width of a grid square.
+ grid_height: Height of a grid square.
+ """
+ self._grid_size = (grid_width, grid_height)
+ self.total_region = Region()
+ self._map: defaultdict[GridCoordinate, list[ValueType]] = defaultdict(list)
+ self._fixed: list[ValueType] = []
+
+ def _region_to_grid_coordinates(self, region: Region) -> Iterable[GridCoordinate]:
+ """Get the grid squares under a region.
+
+ Args:
+ region: A region.
+
+ Returns:
+ Iterable of grid coordinates (tuple of 2 values).
+ """
+ # (x1, y1) is the coordinate of the top left cell
+ # (x2, y2) is the coordinate of the bottom right cell
+ x1, y1, width, height = region
+ x2 = x1 + width - 1
+ y2 = y1 + height - 1
+ grid_width, grid_height = self._grid_size
+
+ return product(
+ range(x1 // grid_width, x2 // grid_width + 1),
+ range(y1 // grid_height, y2 // grid_height + 1),
+ )
+'''
+
+XML_CODE = """\
+<?xml version="1.0" encoding="UTF-8"?>
+<movies>
+ <movie>
+ <title>Back to the Future</title> <year>1985</year> <director>Robert Zemeckis</director>
+ <genre>Science Fiction</genre> <rating>PG</rating>
+ <cast>
+ <actor> <name>Michael J. Fox</name> <role>Marty McFly</role> </actor>
+ <actor> <name>Christopher Lloyd</name> <role>Dr. Emmett Brown</role> </actor>
+ </cast>
+ </movie>
+ <movie>
+ <title>The Breakfast Club</title> <year>1985</year> <director>John Hughes</director>
+ <genre>Drama</genre> <rating>R</rating>
+ <cast>
+ <actor> <name>Emilio Estevez</name> <role>Andrew Clark</role> </actor>
+ <actor> <name>Molly Ringwald</name> <role>Claire Standish</role> </actor>
+ </cast>
+ </movie>
+ <movie>
+ <title>Ghostbusters</title> <year>1984</year> <director>Ivan Reitman</director>
+ <genre>Comedy</genre> <rating>PG</rating>
+ <cast>
+ <actor> <name>Bill Murray</name> <role>Dr. Peter Venkman</role> </actor>
+ <actor> <name>Dan Aykroyd</name> <role>Dr. Raymond Stantz</role> </actor>
+ </cast>
+ </movie>
+ <movie>
+ <title>Die Hard</title> <year>1988</year> <director>John McTiernan</director>
+ <genre>Action</genre> <rating>R</rating>
+ <cast>
+ <actor> <name>Bruce Willis</name> <role>John McClane</role> </actor>
+ <actor> <name>Alan Rickman</name> <role>Hans Gruber</role> </actor>
+ </cast>
+ </movie>
+ <movie>
+ <title>E.T. the Extra-Terrestrial</title> <year>1982</year> <director>Steven Spielberg</director>
+ <genre>Science Fiction</genre> <rating>PG</rating>
+ <cast>
+ <actor> <name>Henry Thomas</name> <role>Elliott</role> </actor>
+ <actor> <name>Drew Barrymore</name> <role>Gertie</role> </actor>
+ </cast>
+ </movie>
+</movies>"""
+
+BF_CODE = """\
+[life.b -- John Horton Conway's Game of Life
+(c) 2021 Daniel B. Cristofani
+]
+
+>>>->+>+++++>(++++++++++)[[>>>+<<<-]>+++++>+>>+[<<+>>>>>+<<<-]<-]>>>>[
+ [>>>+>+<<<<-]+++>>+[<+>>>+>+<<<-]>>[>[[>>>+<<<-]<]<<++>+>>>>>>-]<-
+]+++>+>[[-]<+<[>+++++++++++++++++<-]<+]>>[
+ [+++++++++.-------->>>]+[-<<<]>>>[>>,----------[>]<]<<[
+ <<<[
+ >--[<->>+>-<<-]<[[>>>]+>-[+>>+>-]+[<<<]<-]>++>[<+>-]
+ >[[>>>]+[<<<]>>>-]+[->>>]<-[++>]>[------<]>+++[<<<]>
+ ]<
+ ]>[
+ -[+>>+>-]+>>+>>>+>[<<<]>->+>[
+ >[->+>+++>>++[>>>]+++<<<++<<<++[>>>]>>>]<<<[>[>>>]+>>>]
+ <<<<<<<[<<++<+[-<<<+]->++>>>++>>>++<<<<]<<<+[-<<<+]+>->>->>
+ ]<<+<<+<<<+<<-[+<+<<-]+<+[
+ ->+>[-<-<<[<<<]>[>>[>>>]<<+<[<<<]>-]]
+ <[<[<[<<<]>+>>[>>>]<<-]<[<<<]]>>>->>>[>>>]+>
+ ]>+[-<<[-]<]-[
+ [>>>]<[<<[<<<]>>>>>+>[>>>]<-]>>>[>[>>>]<<<<+>[<<<]>>-]>
+ ]<<<<<<[---<-----[-[-[<->>+++<+++++++[-]]]]<+<+]>
+ ]>>
+]
+
+[This program simulates the Game of Life cellular automaton.
+
+Type e.g. "be" to toggle the fifth cell in the second row, "q" to quit,
+or a bare linefeed to advance one generation.
+
+Grid wraps toroidally. Board size in parentheses in first line (2-166 work).
+
+This program is licensed under a Creative Commons Attribution-ShareAlike 4.0
+International License (http://creativecommons.org/licenses/by-sa/4.0/).]
+"""
+
+
+LEVELS = {"Python": PYTHON_CODE, "XML": XML_CODE, "BF": BF_CODE}
+
+
+class Tile(containers.Vertical):
+ """An individual tile in the puzzle.
+
+ A Tile is a container with a static inside it.
+ The static contains the code (as a Rich Syntax object), scrolled so the
+ relevant portion is visible.
+ """
+
+ DEFAULT_CSS = """
+ Tile {
+ position: absolute;
+ Static {
+ width: auto;
+ height: auto;
+ &:hover { tint: $primary 30%; }
+ }
+ &#blank { visibility: hidden; }
+ }
+ """
+
+ position: reactive[Offset] = reactive(Offset)
+
+ def __init__(
+ self,
+ renderable: ConsoleRenderable,
+ tile: int | None,
+ size: Size,
+ position: Offset,
+ ) -> None:
+ self.renderable = renderable
+ self.tile = tile
+ self.tile_size = size
+ self.start_position = position
+
+ super().__init__(id="blank" if tile is None else f"tile{self.tile}")
+ self.set_reactive(Tile.position, position)
+
+ def compose(self) -> ComposeResult:
+ static = Static(
+ self.renderable,
+ classes="tile",
+ name="blank" if self.tile is None else str(self.tile),
+ )
+ assert self.parent is not None
+ static.styles.width = self.parent.styles.width
+ static.styles.height = self.parent.styles.height
+ yield static
+
+ def on_mount(self) -> None:
+ if self.tile is not None:
+ width, height = self.tile_size
+ self.styles.width = width
+ self.styles.height = height
+ column, row = self.position
+ self.set_scroll(column * width, row * height)
+ self.offset = self.position * self.tile_size
+
+ def watch_position(self, position: Offset) -> None:
+ """The 'position' is in tile coordinate.
+ When it changes we animate it to the cell coordinates."""
+ self.animate("offset", position * self.tile_size, duration=0.2)
+
+
+class GameDialog(containers.VerticalGroup):
+ """A dialog to ask the user for the initial game parameters."""
+
+ DEFAULT_CSS = """
+ GameDialog {
+ background: $boost;
+ border: thick $primary-muted;
+ padding: 0 2;
+ width: 50;
+ #values {
+ width: 1fr;
+ Select { margin: 1 0;}
+ }
+ Button {
+ margin: 0 1 1 1;
+ width: 1fr;
+ }
+ }
+ """
+
+ def compose(self) -> ComposeResult:
+ with containers.VerticalGroup(id="values"):
+ yield Select.from_values(
+ LEVELS.keys(),
+ prompt="Language",
+ value="Python",
+ id="language",
+ allow_blank=False,
+ )
+ yield Select(
+ [
+ ("Easy (3x3)", (3, 3)),
+ ("Medium (4x4)", (4, 4)),
+ ("Hard (5x5)", (5, 5)),
+ ],
+ prompt="Level",
+ value=(4, 4),
+ id="level",
+ allow_blank=False,
+ )
+ yield Button("Start", variant="primary")
+
+ @on(Button.Pressed)
+ def on_button_pressed(self) -> None:
+ language = self.query_one("#language", Select).selection
+ level = self.query_one("#level", Select).selection
+ assert language is not None and level is not None
+ self.screen.dismiss(NewGame(language, LEVELS[language], level))
+
+
+class GameDialogScreen(ModalScreen):
+ """Modal screen containing the dialog."""
+
+ CSS = """
+ GameDialogScreen {
+ align: center middle;
+ }
+ """
+
+ BINDINGS = [("escape", "dismiss")]
+
+ def compose(self) -> ComposeResult:
+ yield GameDialog()
+
+
+class Game(containers.Vertical, can_focus=True):
+ """Widget for the game board."""
+
+ ALLOW_MAXIMIZE = False
+ DEFAULT_CSS = """
+ Game {
+ visibility: hidden;
+ align: center middle;
+ hatch: right $panel;
+ border: heavy transparent;
+ &:focus {
+ border: heavy $success;
+ }
+ #grid {
+ border: heavy $primary;
+ hatch: right $panel;
+ box-sizing: content-box;
+ }
+ Digits {
+ width: auto;
+ color: $foreground;
+ }
+ }
+ """
+
+ BINDINGS = [
+ Binding("up", "move('up')", "up", priority=True),
+ Binding("down", "move('down')", "down", priority=True),
+ Binding("left", "move('left')", "left", priority=True),
+ Binding("right", "move('right')", "right", priority=True),
+ ]
+
+ state = reactive("waiting")
+ play_start_time: reactive[float] = reactive(monotonic)
+ play_time = reactive(0.0, init=False)
+ code = reactive("")
+ dimensions = reactive(Size(3, 3))
+ code = reactive("")
+ language = reactive("")
+
+ def __init__(
+ self,
+ code: str,
+ language: str,
+ dimensions: tuple[int, int],
+ tile_size: tuple[int, int],
+ ) -> None:
+ self.set_reactive(Game.code, code)
+ self.set_reactive(Game.language, language)
+ self.locations: defaultdict[Offset, int | None] = defaultdict(None)
+ super().__init__()
+ self.dimensions = Size(*dimensions)
+ self.tile_size = Size(*tile_size)
+ self.play_timer: Timer | None = None
+
+ def check_win(self) -> bool:
+ return all(tile.start_position == tile.position for tile in self.query(Tile))
+
+ def watch_dimensions(self, dimensions: Size) -> None:
+ self.locations.clear()
+ tile_width, tile_height = dimensions
+ for last, tile_no in loop_last(range(0, tile_width * tile_height)):
+ position = Offset(*divmod(tile_no, tile_width))
+ self.locations[position] = None if last else tile_no
+
+ def compose(self) -> ComposeResult:
+ syntax = Syntax(
+ self.code,
+ self.language.lower(),
+ indent_guides=True,
+ line_numbers=True,
+ theme="material",
+ )
+ tile_width, tile_height = self.dimensions
+ self.state = "waiting"
+ yield Digits("")
+ with containers.HorizontalGroup(id="grid") as grid:
+ grid.styles.width = tile_width * self.tile_size[0]
+ grid.styles.height = tile_height * self.tile_size[1]
+ for row, column in product(range(tile_width), range(tile_height)):
+ position = Offset(row, column)
+ tile_no = self.locations[position]
+ yield Tile(syntax, tile_no, self.tile_size, position)
+ if self.language:
+ self.call_after_refresh(self.shuffle)
+
+ def update_clock(self) -> None:
+ if self.state == "playing":
+ elapsed = monotonic() - self.play_start_time
+ self.play_time = elapsed
+
+ def watch_play_time(self, play_time: float) -> None:
+ minutes, seconds = divmod(play_time, 60)
+ hours, minutes = divmod(minutes, 60)
+ self.query_one(Digits).update(f"{hours:02,.0f}:{minutes:02.0f}:{seconds:04.1f}")
+
+ def watch_state(self, old_state: str, new_state: str) -> None:
+ if self.play_timer is not None:
+ self.play_timer.stop()
+
+ if new_state == "playing":
+ self.play_start_time = monotonic()
+ self.play_timer = self.set_interval(1 / 10, self.update_clock)
+
+ def get_tile(self, tile: int | None) -> Tile:
+ """Get a tile (int) or the blank (None)."""
+ return self.query_one("#blank" if tile is None else f"#tile{tile}", Tile)
+
+ def get_tile_at(self, position: Offset) -> Tile:
+ """Get a tile at the given position, or raise an IndexError."""
+ if position not in self.locations:
+ raise IndexError("No tile")
+ return self.get_tile(self.locations[position])
+
+ def move_tile(self, tile_no: int | None) -> None:
+ """Move a tile to the blank.
+ Note: this doesn't do any validation of legal moves.
+ """
+ tile = self.get_tile(tile_no)
+ blank = self.get_tile(None)
+ blank_position = blank.position
+
+ self.locations[tile.position] = None
+ blank.position = tile.position
+
+ self.locations[blank_position] = tile_no
+ tile.position = blank_position
+
+ if self.state == "playing" and self.check_win():
+ self.state = "won"
+ self.notify("You won!", title="Sliding Tile Puzzle")
+
+ def can_move(self, tile: int) -> bool:
+ """Check if a tile may move."""
+ blank_position = self.get_tile(None).position
+ tile_position = self.get_tile(tile).position
+ return blank_position in (
+ tile_position + (1, 0),
+ tile_position - (1, 0),
+ tile_position + (0, 1),
+ tile_position - (0, 1),
+ )
+
+ def action_move(self, direction: str) -> None:
+ if self.state != "playing":
+ self.app.bell()
+ return
+ blank = self.get_tile(None).position
+ if direction == "up":
+ position = blank + (0, +1)
+ elif direction == "down":
+ position = blank + (0, -1)
+ elif direction == "left":
+ position = blank + (+1, 0)
+ elif direction == "right":
+ position = blank + (-1, 0)
+ try:
+ tile = self.get_tile_at(position)
+ except IndexError:
+ return
+ self.move_tile(tile.tile)
+
+ def get_legal_moves(self) -> set[Offset]:
+ """Get the positions of all tiles that can move."""
+ blank = self.get_tile(None).position
+ moves: list[Offset] = []
+
+ DIRECTIONS = [(-1, 0), (+1, -0), (0, -1), (0, +1)]
+ moves = [
+ blank + direction
+ for direction in DIRECTIONS
+ if (blank + direction) in self.locations
+ ]
+ return {self.get_tile_at(position).position for position in moves}
+
+ @work(exclusive=True)
+ async def shuffle(self, shuffles: int = 150) -> None:
+ """A worker to do the shuffling."""
+ self.visible = True
+ if self.play_timer is not None:
+ self.play_timer.stop()
+ self.query_one("#grid").border_title = "[reverse bold] SHUFFLING - Please Wait "
+ self.state = "shuffling"
+ previous_move: Offset = Offset(-1, -1)
+ for _ in range(shuffles):
+ legal_moves = self.get_legal_moves()
+ legal_moves.discard(previous_move)
+ previous_move = self.get_tile(None).position
+ move_position = choice(list(legal_moves))
+ move_tile = self.get_tile_at(move_position)
+ self.move_tile(move_tile.tile)
+ await sleep(0.05)
+ self.query_one("#grid").border_title = ""
+ self.state = "playing"
+
+ @on(events.Click, ".tile")
+ def on_tile_clicked(self, event: events.Click) -> None:
+ assert event.widget is not None
+ tile = int(event.widget.name or 0)
+ if self.state != "playing" or not self.can_move(tile):
+ self.app.bell()
+ return
+ self.move_tile(tile)
+
+
+class GameInstructions(containers.VerticalGroup):
+ DEFAULT_CSS = """\
+ GameInstructions {
+ layer: instructions;
+ width: 60;
+ background: $panel;
+ border: thick $primary-darken-2;
+ Markdown {
+ background: $panel;
+ }
+
+ }
+
+"""
+ INSTRUCTIONS = """\
+# Instructions
+
+This is an implementation of the *sliding tile puzzle*.
+
+The board consists of a number of tiles and a blank space.
+After shuffling, the goal is to restore the original "image" by moving a square either horizontally or vertically into the blank space.
+
+This version is like the physical game, but rather than an image, you need to restore code.
+ """
+
+ def compose(self) -> ComposeResult:
+ yield Markdown(self.INSTRUCTIONS)
+ with containers.Center():
+ yield Button("New Game", action="screen.new_game", variant="success")
+
+
+class GameScreen(PageScreen):
+ """The screen containing the game."""
+
+ DEFAULT_CSS = """
+ GameScreen{
+ align: center middle;
+ layers: instructions game;
+ }
+ """
+
+ BINDINGS = [("n", "new_game", "New Game")]
+
+ def compose(self) -> ComposeResult:
+ yield GameInstructions()
+ yield Game("\n" * 100, "", dimensions=(4, 4), tile_size=(16, 8))
+ yield Footer()
+
+ def action_shuffle(self) -> None:
+ self.query_one(Game).shuffle()
+
+ def action_new_game(self) -> None:
+ self.app.push_screen(GameDialogScreen(), callback=self.new_game)
+
+ async def new_game(self, new_game: NewGame | None) -> None:
+ if new_game is None:
+ return
+ self.query_one(GameInstructions).display = False
+ game = self.query_one(Game)
+ game.state = "waiting"
+ game.code = new_game.code
+ game.language = new_game.language
+ game.dimensions = Size(*new_game.size)
+ await game.recompose()
+ game.focus()
+
+ def check_action(self, action: str, parameters: tuple[object, ...]) -> bool | None:
+ if action == "shuffle" and self.query_one(Game).state == "waiting":
+ return None
+ return True
+
+
+if __name__ == "__main__":
+ from textual.app import App
+
+ class GameApp(App):
+ def get_default_screen(self) -> Screen:
+ return GameScreen()
+
+ app = GameApp()
+ app.run()
diff --git a/contrib/python/textual/textual/demo/page.py b/contrib/python/textual/textual/demo/page.py
index 35a98df3876..a176403712e 100644
--- a/contrib/python/textual/textual/demo/page.py
+++ b/contrib/python/textual/textual/demo/page.py
@@ -59,7 +59,7 @@ class PageScreen(Screen):
Binding(
"c",
"show_code",
- "show code",
+ "Code",
tooltip="Show the code used to generate this screen",
)
]
diff --git a/contrib/python/textual/textual/demo/projects.py b/contrib/python/textual/textual/demo/projects.py
index a8e5c4406be..e264fd71434 100644
--- a/contrib/python/textual/textual/demo/projects.py
+++ b/contrib/python/textual/textual/demo/projects.py
@@ -222,3 +222,14 @@ class ProjectsScreen(PageScreen):
for project in PROJECTS:
yield Project(project)
yield Footer()
+
+
+if __name__ == "__main__":
+ from textual.app import App
+
+ class GameApp(App):
+ def get_default_screen(self) -> Screen:
+ return ProjectsScreen()
+
+ app = GameApp()
+ app.run()
diff --git a/contrib/python/textual/textual/demo/widgets.py b/contrib/python/textual/textual/demo/widgets.py
index 11e4b0419dc..16eb437a368 100644
--- a/contrib/python/textual/textual/demo/widgets.py
+++ b/contrib/python/textual/textual/demo/widgets.py
@@ -166,7 +166,7 @@ class Datatables(containers.VerticalGroup):
A fully-featured DataTable, with cell, row, and columns cursors.
Cells may be individually styled, and may include Rich renderables.
-**Tip:** Focus the table and press `ctrl+m`
+**Tip:** Focus the table and press `ctrl+a`
"""
DEFAULT_CSS = """
@@ -300,12 +300,12 @@ And a RichLog widget to display Rich renderables.
Logs {
Log, RichLog {
width: 1fr;
- height: 20;
- border: blank;
- padding: 0;
+ height: 20;
+ padding: 1;
overflow-x: auto;
+ border: wide transparent;
&:focus {
- border: heavy $accent;
+ border: wide $border;
}
}
TabPane { padding: 0; }
@@ -674,11 +674,13 @@ There is also the Tree widget's cousin, DirectoryTree, to navigate folders and f
Trees {
Tree {
height: 16;
- &.-maximized { height: 1fr; }
+ padding: 1;
+ &.-maximized { height: 1fr; }
+ border: wide transparent;
+ &:focus { border: wide $border; }
}
VerticalGroup {
- border: heavy transparent;
- &:focus-within { border: heavy $border; }
+
}
}
@@ -729,7 +731,6 @@ from textual import App, ComposeResult
"Java",
"Javascript",
"JSON",
- "Kotlin",
"Markdown",
"Python",
"Rust",
@@ -806,3 +807,14 @@ class WidgetsScreen(PageScreen):
yield Trees()
yield YourWidgets()
yield Footer()
+
+
+if __name__ == "__main__":
+ from textual.app import App
+
+ class GameApp(App):
+ def get_default_screen(self) -> Screen:
+ return WidgetsScreen()
+
+ app = GameApp()
+ app.run()
diff --git a/contrib/python/textual/textual/document/_document.py b/contrib/python/textual/textual/document/_document.py
index 386a6a7713f..47e87eb09b3 100644
--- a/contrib/python/textual/textual/document/_document.py
+++ b/contrib/python/textual/textual/document/_document.py
@@ -8,8 +8,7 @@ from typing import TYPE_CHECKING, NamedTuple, Tuple, overload
from typing_extensions import Literal, get_args
if TYPE_CHECKING:
- from tree_sitter import Node
- from tree_sitter.binding import Query
+ from tree_sitter import Node, Query
from textual._cells import cell_len
from textual.geometry import Size
@@ -143,10 +142,10 @@ class DocumentBase(ABC):
def query_syntax_tree(
self,
- query: Query,
+ query: "Query",
start_point: tuple[int, int] | None = None,
end_point: tuple[int, int] | None = None,
- ) -> list[tuple[Node, str]]:
+ ) -> dict[str, list["Node"]]:
"""Query the tree-sitter syntax tree.
The default implementation always returns an empty list.
@@ -159,11 +158,11 @@ class DocumentBase(ABC):
end_point: The (row, column byte) to end the query at.
Returns:
- A tuple containing the nodes and text captured by the query.
+ A dict mapping captured node names to lists of Nodes with that name.
"""
- return []
+ return {}
- def prepare_query(self, query: str) -> Query | None:
+ def prepare_query(self, query: str) -> "Query | None":
return None
@property
@@ -206,7 +205,7 @@ class Document(DocumentBase):
"""A document which can be opened in a TextArea."""
def __init__(self, text: str) -> None:
- self._newline = _detect_newline_style(text)
+ self._newline: Newline = _detect_newline_style(text)
"""The type of newline used in the text."""
self._lines: list[str] = text.splitlines(keepends=False)
"""The lines of the document, excluding newline characters.
@@ -371,14 +370,23 @@ class Document(DocumentBase):
return index
def get_location_from_index(self, index: int) -> Location:
- """Given an index in the document's text, returns the corresponding location.
+ """Given a codepoint index in the document's text, returns the corresponding location.
Args:
index: The index in the document's text.
Returns:
The corresponding location.
+
+ Raises:
+ ValueError: If the index is doesn't correspond to a location in the document.
"""
+ error_message = (
+ f"Index {index!r} does not correspond to a location in the document."
+ )
+ if index < 0 or index > len(self.text):
+ raise ValueError(error_message)
+
column_index = 0
newline_length = len(self.newline)
for line_index in range(self.line_count):
@@ -391,6 +399,8 @@ class Document(DocumentBase):
return (line_index + 1, 0)
column_index = next_column_index
+ raise ValueError(error_message)
+
def get_line(self, index: int) -> str:
"""Returns the line with the given index from the document.
diff --git a/contrib/python/textual/textual/document/_languages.py b/contrib/python/textual/textual/document/_languages.py
deleted file mode 100644
index fb313cf7cab..00000000000
--- a/contrib/python/textual/textual/document/_languages.py
+++ /dev/null
@@ -1,19 +0,0 @@
-BUILTIN_LANGUAGES = sorted(
- [
- "bash",
- "css",
- "go",
- "html",
- "java",
- "javascript",
- "json",
- "kotlin",
- "markdown",
- "python",
- "rust",
- "regex",
- "sql",
- "toml",
- "yaml",
- ]
-)
diff --git a/contrib/python/textual/textual/document/_syntax_aware_document.py b/contrib/python/textual/textual/document/_syntax_aware_document.py
index 32dbc191b33..162d3fbd544 100644
--- a/contrib/python/textual/textual/document/_syntax_aware_document.py
+++ b/contrib/python/textual/textual/document/_syntax_aware_document.py
@@ -1,16 +1,15 @@
from __future__ import annotations
try:
- from tree_sitter import Language, Node, Parser, Tree
- from tree_sitter.binding import Query
- from tree_sitter_languages import get_language, get_parser
+ from tree_sitter import Language, Node, Parser, Query, Tree
TREE_SITTER = True
except ImportError:
TREE_SITTER = False
+
+from textual._tree_sitter import BUILTIN_LANGUAGES
from textual.document._document import Document, EditResult, Location, _utf8_encode
-from textual.document._languages import BUILTIN_LANGUAGES
class SyntaxAwareDocumentError(Exception):
@@ -52,7 +51,8 @@ class SyntaxAwareDocument(Document):
"""The tree-sitter Language or None if tree-sitter is unavailable."""
self._parser: Parser | None = None
- """The tree-sitter Parser or None if tree-sitter is unavailable."""
+
+ from textual._tree_sitter import get_language
# If the language is `None`, then avoid doing any parsing related stuff.
if isinstance(language, str):
@@ -61,25 +61,22 @@ class SyntaxAwareDocument(Document):
# If tree-sitter-languages is not installed properly, get_language
# and get_parser may raise an OSError when unable to load their
# resources
+
try:
self.language = get_language(language)
- self._parser = get_parser(language)
except OSError as e:
raise SyntaxAwareDocumentError(
f"Could not find binaries for {language!r}"
) from e
else:
self.language = language
- self._parser = Parser()
- self._parser.set_language(language)
+
+ self._parser = Parser(self.language)
+ """The tree-sitter Parser or None if tree-sitter is unavailable."""
self._syntax_tree: Tree = self._parser.parse(self._read_callable) # type: ignore
"""The tree-sitter Tree (syntax tree) built from the document."""
- @property
- def language_name(self) -> str | None:
- return self.language.name if self.language else None
-
def prepare_query(self, query: str) -> Query | None:
"""Prepare a tree-sitter tree query.
@@ -110,7 +107,7 @@ class SyntaxAwareDocument(Document):
query: Query,
start_point: tuple[int, int] | None = None,
end_point: tuple[int, int] | None = None,
- ) -> list[tuple["Node", str]]:
+ ) -> dict[str, list["Node"]]:
"""Query the tree-sitter syntax tree.
The default implementation always returns an empty list.
@@ -176,12 +173,13 @@ class SyntaxAwareDocument(Document):
)
# Incrementally parse the document.
self._syntax_tree = self._parser.parse(
- self._read_callable, self._syntax_tree # type: ignore[arg-type]
+ self._read_callable,
+ self._syntax_tree, # type: ignore[arg-type]
)
return replace_result
- def get_line(self, line_index: int) -> str:
+ def get_line(self, index: int) -> str:
"""Return the string representing the line, not including new line characters.
Args:
@@ -190,7 +188,7 @@ class SyntaxAwareDocument(Document):
Returns:
The string representing the line.
"""
- line_string = self[line_index]
+ line_string = self[index]
return line_string
def _location_to_byte_offset(self, location: Location) -> int:
diff --git a/contrib/python/textual/textual/dom.py b/contrib/python/textual/textual/dom.py
index 68212bc8b63..d7218c60292 100644
--- a/contrib/python/textual/textual/dom.py
+++ b/contrib/python/textual/textual/dom.py
@@ -10,6 +10,7 @@ import re
import threading
from functools import lru_cache, partial
from inspect import getfile
+from operator import attrgetter
from typing import (
TYPE_CHECKING,
Any,
@@ -1214,7 +1215,7 @@ class DOMNode(MessagePump):
Returns:
A list of nodes.
"""
- return [child for child in self._nodes if child.display]
+ return list(filter(attrgetter("display"), self._nodes))
def watch(
self,
diff --git a/contrib/python/textual/textual/drivers/linux_driver.py b/contrib/python/textual/textual/drivers/linux_driver.py
index 510b0992f0e..7e04f4cf26e 100644
--- a/contrib/python/textual/textual/drivers/linux_driver.py
+++ b/contrib/python/textual/textual/drivers/linux_driver.py
@@ -216,8 +216,6 @@ class LinuxDriver(Driver):
loop = asyncio.get_running_loop()
def send_size_event() -> None:
- if self._in_band_window_resize:
- return
terminal_size = self._get_terminal_size()
width, height = terminal_size
textual_size = Size(width, height)
@@ -231,7 +229,8 @@ class LinuxDriver(Driver):
self._writer_thread.start()
def on_terminal_resize(signum, stack) -> None:
- send_size_event()
+ if not self._in_band_window_resize:
+ send_size_event()
signal.signal(signal.SIGWINCH, on_terminal_resize)
diff --git a/contrib/python/textual/textual/drivers/linux_inline_driver.py b/contrib/python/textual/textual/drivers/linux_inline_driver.py
index 03e55d528f1..3fc62df9dee 100644
--- a/contrib/python/textual/textual/drivers/linux_inline_driver.py
+++ b/contrib/python/textual/textual/drivers/linux_inline_driver.py
@@ -208,10 +208,7 @@ class LinuxInlineDriver(Driver):
self.write("\x1b[?25l") # Hide cursor
self.write("\033[?1004h") # Enable FocusIn/FocusOut.
-
self.write("\x1b[>1u") # https://sw.kovidgoyal.net/kitty/keyboard-protocol/
- # Disambiguate escape codes https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement
- self.write("\x1b[=1;u")
self.flush()
self._enable_mouse_support()
diff --git a/contrib/python/textual/textual/events.py b/contrib/python/textual/textual/events.py
index 6e7a21e8da3..fd4dea3eddc 100644
--- a/contrib/python/textual/textual/events.py
+++ b/contrib/python/textual/textual/events.py
@@ -567,7 +567,7 @@ class Timer(Event, bubble=False, verbose=True):
- [X] Verbose
"""
- __slots__ = ["time", "count", "callback"]
+ __slots__ = ["timer", "time", "count", "callback"]
def __init__(
self,
diff --git a/contrib/python/textual/textual/geometry.py b/contrib/python/textual/textual/geometry.py
index b91fc8bf069..e6d190f9392 100644
--- a/contrib/python/textual/textual/geometry.py
+++ b/contrib/python/textual/textual/geometry.py
@@ -126,6 +126,9 @@ class Offset(NamedTuple):
if isinstance(other, (float, int)):
x, y = self
return Offset(int(x * other), int(y * other))
+ if isinstance(other, tuple):
+ x, y = self
+ return Offset(int(x * other[0]), int(y * other[1]))
return NotImplemented
def __neg__(self) -> Offset:
diff --git a/contrib/python/textual/textual/layout.py b/contrib/python/textual/textual/layout.py
index 8e494fcb74d..a3ef9ec006d 100644
--- a/contrib/python/textual/textual/layout.py
+++ b/contrib/python/textual/textual/layout.py
@@ -91,12 +91,18 @@ class WidgetPlacement(NamedTuple):
order: int = 0
fixed: bool = False
overlay: bool = False
+ absolute: bool = False
+
+ @property
+ def reset_origin(self) -> WidgetPlacement:
+ """Reset the origin in the placement (moves it to (0, 0))."""
+ return self._replace(region=self.region.reset_offset)
@classmethod
def translate(
cls, placements: list[WidgetPlacement], translate_offset: Offset
) -> list[WidgetPlacement]:
- """Move all placements by a given offset.
+ """Move all non-absolute placements by a given offset.
Args:
placements: List of placements.
@@ -108,19 +114,35 @@ class WidgetPlacement(NamedTuple):
if translate_offset:
return [
cls(
- region + translate_offset,
+ (
+ region + translate_offset
+ if layout_widget.absolute_offset is None
+ else region
+ ),
offset,
margin,
layout_widget,
order,
fixed,
overlay,
+ absolute,
)
- for region, offset, margin, layout_widget, order, fixed, overlay in placements
+ for region, offset, margin, layout_widget, order, fixed, overlay, absolute in placements
]
return placements
@classmethod
+ def apply_absolute(cls, placements: list[WidgetPlacement]) -> None:
+ """Applies absolute offsets (in place).
+
+ Args:
+ placements: A list of placements.
+ """
+ for index, placement in enumerate(placements):
+ if placement.absolute:
+ placements[index] = placement.reset_origin
+
+ @classmethod
def get_bounds(cls, placements: Iterable[WidgetPlacement]) -> Region:
"""Get a bounding region around all placements.
@@ -135,6 +157,48 @@ class WidgetPlacement(NamedTuple):
)
return bounding_region
+ def process_offset(
+ self, constrain_region: Region, absolute_offset: Offset
+ ) -> WidgetPlacement:
+ """Apply any absolute offset or constrain rules to the placement.
+
+ Args:
+ constrain_region: The container region when applying constrain rules.
+ absolute_offset: Default absolute offset that moves widget in to screen coordinates.
+
+ Returns:
+ Processes placement, may be the same instance.
+ """
+ widget = self.widget
+ styles = widget.styles
+ if not widget.absolute_offset and not styles.has_any_rules(
+ "constrain_x", "constrain_y"
+ ):
+ # Bail early if there is nothing to do
+ return self
+ region = self.region
+ margin = self.margin
+ if widget.absolute_offset is not None:
+ region = region.at_offset(
+ widget.absolute_offset + margin.top_left - absolute_offset
+ )
+
+ region = region.translate(self.offset).constrain(
+ styles.constrain_x,
+ styles.constrain_y,
+ self.margin,
+ constrain_region - absolute_offset,
+ )
+
+ offset = region.offset - self.region.offset
+ if offset != self.offset:
+ region, _offset, margin, widget, order, fixed, overlay, absolute = self
+ placement = WidgetPlacement(
+ region, offset, margin, widget, order, fixed, overlay, absolute
+ )
+ return placement
+ return self
+
class Layout(ABC):
"""Base class of the object responsible for arranging Widgets within a container."""
diff --git a/contrib/python/textual/textual/layouts/grid.py b/contrib/python/textual/textual/layouts/grid.py
index a1b0823d6d4..6b4304092b0 100644
--- a/contrib/python/textual/textual/layouts/grid.py
+++ b/contrib/python/textual/textual/layouts/grid.py
@@ -260,8 +260,6 @@ class GridLayout(Layout):
placements: list[WidgetPlacement] = []
_WidgetPlacement = WidgetPlacement
add_placement = placements.append
- widgets: list[Widget] = []
- add_widget = widgets.append
max_column = len(columns) - 1
max_row = len(rows) - 1
@@ -289,17 +287,21 @@ class GridLayout(Layout):
)
.crop_size(cell_size)
.shrink(margin)
- )
+ ) + offset
+ widget_styles = widget.styles
placement_offset = (
- styles.offset.resolve(cell_size, viewport)
- if styles.has_rule("offset")
+ widget_styles.offset.resolve(cell_size, viewport)
+ if widget_styles.has_rule("offset")
else NULL_OFFSET
)
+ absolute = (
+ widget_styles.has_rule("position") and styles.position == "absolute"
+ )
add_placement(
_WidgetPlacement(
- region + offset,
+ region,
placement_offset,
(
margin
@@ -307,8 +309,8 @@ class GridLayout(Layout):
else margin.grow_maximum(gutter_spacing)
),
widget,
+ absolute,
)
)
- add_widget(widget)
return placements
diff --git a/contrib/python/textual/textual/layouts/horizontal.py b/contrib/python/textual/textual/layouts/horizontal.py
index c62fae3c1da..660ce860538 100644
--- a/contrib/python/textual/textual/layouts/horizontal.py
+++ b/contrib/python/textual/textual/layouts/horizontal.py
@@ -22,6 +22,7 @@ class HorizontalLayout(Layout):
def arrange(
self, parent: Widget, children: list[Widget], size: Size
) -> ArrangeResult:
+ parent.pre_layout(self)
placements: list[WidgetPlacement] = []
add_placement = placements.append
viewport = parent.app.size
@@ -81,6 +82,7 @@ class HorizontalLayout(Layout):
children, box_models, margins
):
styles = widget.styles
+
overlay = styles.overlay == "screen"
offset = (
styles.offset.resolve(
@@ -92,23 +94,27 @@ class HorizontalLayout(Layout):
)
offset_y = box_margin.top
next_x = x + content_width
+
+ region = _Region(
+ x.__floor__(),
+ offset_y,
+ (next_x - x.__floor__()).__floor__(),
+ content_height.__floor__(),
+ )
+ absolute = styles.has_rule("position") and styles.position == "absolute"
add_placement(
_WidgetPlacement(
- _Region(
- x.__floor__(),
- offset_y,
- (next_x - x.__floor__()).__floor__(),
- content_height.__floor__(),
- ),
+ region,
offset,
box_margin,
widget,
0,
False,
overlay,
+ absolute,
)
)
- if not overlay:
+ if not overlay and not absolute:
x = next_x + margin
return placements
diff --git a/contrib/python/textual/textual/layouts/vertical.py b/contrib/python/textual/textual/layouts/vertical.py
index c7b8087f7fb..91b06bab09e 100644
--- a/contrib/python/textual/textual/layouts/vertical.py
+++ b/contrib/python/textual/textual/layouts/vertical.py
@@ -20,6 +20,7 @@ class VerticalLayout(Layout):
def arrange(
self, parent: Widget, children: list[Widget], size: Size
) -> ArrangeResult:
+ parent.pre_layout(self)
placements: list[WidgetPlacement] = []
add_placement = placements.append
viewport = parent.app.size
@@ -96,23 +97,28 @@ class VerticalLayout(Layout):
if styles.has_rule("offset")
else NULL_OFFSET
)
+
+ region = _Region(
+ box_margin.left,
+ y.__floor__(),
+ content_width.__floor__(),
+ next_y.__floor__() - y.__floor__(),
+ )
+
+ absolute = styles.has_rule("position") and styles.position == "absolute"
add_placement(
_WidgetPlacement(
- _Region(
- box_margin.left,
- y.__floor__(),
- content_width.__floor__(),
- next_y.__floor__() - y.__floor__(),
- ),
+ region,
offset,
box_margin,
widget,
0,
False,
overlay,
+ absolute,
)
)
- if not overlay:
+ if not overlay and not absolute:
y = next_y + margin
return placements
diff --git a/contrib/python/textual/textual/screen.py b/contrib/python/textual/textual/screen.py
index 81cc3c733ab..f120c95cbaf 100644
--- a/contrib/python/textual/textual/screen.py
+++ b/contrib/python/textual/textual/screen.py
@@ -261,8 +261,6 @@ class Screen(Generic[ScreenResultType], Widget):
)
"""The signal that is published when the screen's layout is refreshed."""
- self._bindings_updated = False
- """Indicates that a binding update was requested."""
self.bindings_updated_signal: Signal[Screen] = Signal(self, "bindings_updated")
"""A signal published when the bindings have been updated"""
@@ -315,8 +313,7 @@ class Screen(Generic[ScreenResultType], Widget):
def refresh_bindings(self) -> None:
"""Call to request a refresh of bindings."""
- self._bindings_updated = True
- self.check_idle()
+ self.bindings_updated_signal.publish(self)
def _watch_maximized(
self, previously_maximized: Widget | None, maximized: Widget | None
@@ -562,6 +559,10 @@ class Screen(Generic[ScreenResultType], Widget):
except NoWidget:
return None
+ if widget.has_class("-textual-system") or widget.loading:
+ # Clicking Textual system widgets should not focus anything
+ return None
+
for node in widget.ancestors_with_self:
if isinstance(node, Widget) and node.focusable:
return node
@@ -661,8 +662,6 @@ class Screen(Generic[ScreenResultType], Widget):
the CSS selectors given in the argument.
"""
- # TODO: This shouldn't be required
- self._compositor._full_map_invalidated = True
if not isinstance(selector, str):
selector = selector.__name__
selector_set = parse_selectors(selector)
@@ -915,7 +914,7 @@ class Screen(Generic[ScreenResultType], Widget):
self.log.debug(widget, "was focused")
self._update_focus_styles(focused, blurred)
- self.refresh_bindings()
+ self.call_after_refresh(self.refresh_bindings)
def _extend_compose(self, widgets: list[Widget]) -> None:
"""Insert Textual's own internal widgets.
@@ -936,32 +935,22 @@ class Screen(Generic[ScreenResultType], Widget):
self.screen_layout_refresh_signal.subscribe(
self, self._maybe_clear_tooltip, immediate=True
)
- self.refresh_bindings()
- # Send this signal so we don't get an initial frame with no bindings in the footer
- self.bindings_updated_signal.publish(self)
async def _on_idle(self, event: events.Idle) -> None:
# Check for any widgets marked as 'dirty' (needs a repaint)
event.prevent_default()
+ if not self.app._batch_count and self.is_current:
+ if (
+ self._layout_required
+ or self._scroll_required
+ or self._repaint_required
+ or self._recompose_required
+ or self._dirty_widgets
+ ):
+ self._update_timer.resume()
+ return
- try:
- if not self.app._batch_count and self.is_current:
- if (
- self._layout_required
- or self._scroll_required
- or self._repaint_required
- or self._recompose_required
- or self._dirty_widgets
- ):
- self._update_timer.resume()
- return
-
- await self._invoke_and_clear_callbacks()
- finally:
- if self._bindings_updated:
- self._bindings_updated = False
- if self.is_attached and not self.app._exit:
- self.app.call_later(self.bindings_updated_signal.publish, self)
+ await self._invoke_and_clear_callbacks()
def _compositor_refresh(self) -> None:
"""Perform a compositor refresh."""
@@ -1344,7 +1333,7 @@ class Screen(Generic[ScreenResultType], Widget):
if widget is self:
self.post_message(event)
else:
- mouse_event = self._translate_mouse_move_event(event, region)
+ mouse_event = self._translate_mouse_move_event(event, widget, region)
mouse_event._set_forwarded()
widget._forward_event(mouse_event)
@@ -1369,14 +1358,14 @@ class Screen(Generic[ScreenResultType], Widget):
@staticmethod
def _translate_mouse_move_event(
- event: events.MouseMove, region: Region
+ event: events.MouseMove, widget: Widget, region: Region
) -> events.MouseMove:
"""
Returns a mouse move event whose relative coordinates are translated to
the origin of the specified region.
"""
return events.MouseMove(
- event.widget,
+ widget,
event.x - region.x,
event.y - region.y,
event.delta_x,
@@ -1417,6 +1406,8 @@ class Screen(Generic[ScreenResultType], Widget):
if focusable_widget:
self.set_focus(focusable_widget, scroll_visible=False)
event.style = self.get_style_at(event.screen_x, event.screen_y)
+ if widget.loading:
+ return
if widget is self:
event._set_forwarded()
self.post_message(event)
diff --git a/contrib/python/textual/textual/scroll_view.py b/contrib/python/textual/textual/scroll_view.py
index 42c89ebb233..f905d766730 100644
--- a/contrib/python/textual/textual/scroll_view.py
+++ b/contrib/python/textual/textual/scroll_view.py
@@ -82,10 +82,8 @@ class ScrollView(ScrollableContainer):
layout: Perform layout if required.
Returns:
- True if anything changed, or False if nothing changed.
+ True if a resize event should be sent, otherwise False.
"""
- if self._size != size or self._container_size != container_size:
- self.refresh()
if (
self._size != size
or virtual_size != self.virtual_size
@@ -96,9 +94,8 @@ class ScrollView(ScrollableContainer):
virtual_size = self.virtual_size
self._container_size = size - self.styles.gutter.totals
self._scroll_update(virtual_size)
- return True
- else:
- return False
+
+ return self._size != size or self._container_size != container_size
def render(self) -> RenderableType:
"""Render the scrollable region (if `render_lines` is not implemented).
diff --git a/contrib/python/textual/textual/tree-sitter/highlights/css.scm b/contrib/python/textual/textual/tree-sitter/highlights/css.scm
index b26f0ec96c9..19b0fbfdebe 100644
--- a/contrib/python/textual/textual/tree-sitter/highlights/css.scm
+++ b/contrib/python/textual/textual/tree-sitter/highlights/css.scm
@@ -52,7 +52,7 @@
(property_name)
(feature_name)
(attribute_name)
- ] @property
+ ] @css.property
(namespace_name) @namespace
diff --git a/contrib/python/textual/textual/tree-sitter/highlights/javascript.scm b/contrib/python/textual/textual/tree-sitter/highlights/javascript.scm
index 613a49a86f2..9312d682880 100644
--- a/contrib/python/textual/textual/tree-sitter/highlights/javascript.scm
+++ b/contrib/python/textual/textual/tree-sitter/highlights/javascript.scm
@@ -1,29 +1,17 @@
-; Special identifiers
-;--------------------
-
-([
- (identifier)
- (shorthand_property_identifier)
- (shorthand_property_identifier_pattern)
- ] @constant
- (#match? @constant "^[A-Z_][A-Z\\d_]+$"))
-
+; Variables
+;----------
-((identifier) @constructor
- (#match? @constructor "^[A-Z]"))
+(identifier) @variable
-((identifier) @variable.builtin
- (#match? @variable.builtin "^(arguments|module|console|window|document)$")
- (#is-not? local))
+; Properties
+;-----------
-((identifier) @function.builtin
- (#eq? @function.builtin "require")
- (#is-not? local))
+(property_identifier) @property
; Function and method definitions
;--------------------------------
-(function
+(function_expression
name: (identifier) @function)
(function_declaration
name: (identifier) @function)
@@ -32,20 +20,20 @@
(pair
key: (property_identifier) @function.method
- value: [(function) (arrow_function)])
+ value: [(function_expression) (arrow_function)])
(assignment_expression
left: (member_expression
property: (property_identifier) @function.method)
- right: [(function) (arrow_function)])
+ right: [(function_expression) (arrow_function)])
(variable_declarator
name: (identifier) @function
- value: [(function) (arrow_function)])
+ value: [(function_expression) (arrow_function)])
(assignment_expression
left: (identifier) @function
- right: [(function) (arrow_function)])
+ right: [(function_expression) (arrow_function)])
; Function and method calls
;--------------------------
@@ -57,15 +45,26 @@
function: (member_expression
property: (property_identifier) @function.method))
-; Variables
-;----------
+; Special identifiers
+;--------------------
-(identifier) @variable
+((identifier) @constructor
+ (#match? @constructor "^[A-Z]"))
-; Properties
-;-----------
+([
+ (identifier)
+ (shorthand_property_identifier)
+ (shorthand_property_identifier_pattern)
+ ] @constant
+ (#match? @constant "^[A-Z_][A-Z\\d_]+$"))
-(property_identifier) @property
+((identifier) @variable.builtin
+ (#match? @variable.builtin "^(arguments|module|console|window|document)$")
+ (#is-not? local))
+
+((identifier) @function.builtin
+ (#eq? @function.builtin "require")
+ (#is-not? local))
; Literals
;---------
@@ -93,10 +92,6 @@
; Tokens
;-------
-(template_substitution
- "${" @punctuation.special
- "}" @punctuation.special) @embedded
-
[
";"
(optional_chain)
@@ -160,6 +155,10 @@
"}"
] @punctuation.bracket
+(template_substitution
+ "${" @punctuation.special
+ "}" @punctuation.special) @embedded
+
[
"as"
"async"
diff --git a/contrib/python/textual/textual/tree-sitter/highlights/kotlin.scm b/contrib/python/textual/textual/tree-sitter/highlights/kotlin.scm
deleted file mode 100644
index d2e15a68ee5..00000000000
--- a/contrib/python/textual/textual/tree-sitter/highlights/kotlin.scm
+++ /dev/null
@@ -1,380 +0,0 @@
-;; Based on the nvim-treesitter highlighting, which is under the Apache license.
-;; See https://github.com/nvim-treesitter/nvim-treesitter/blob/f8ab59861eed4a1c168505e3433462ed800f2bae/queries/kotlin/highlights.scm
-;;
-;; The only difference in this file is that queries using #lua-match?
-;; have been removed.
-
-;;; Identifiers
-
-(simple_identifier) @variable
-
-; `it` keyword inside lambdas
-; FIXME: This will highlight the keyword outside of lambdas since tree-sitter
-; does not allow us to check for arbitrary nestation
-((simple_identifier) @variable.builtin
-(#eq? @variable.builtin "it"))
-
-; `field` keyword inside property getter/setter
-; FIXME: This will highlight the keyword outside of getters and setters
-; since tree-sitter does not allow us to check for arbitrary nestation
-((simple_identifier) @variable.builtin
-(#eq? @variable.builtin "field"))
-
-; `this` this keyword inside classes
-(this_expression) @variable.builtin
-
-; `super` keyword inside classes
-(super_expression) @variable.builtin
-
-(class_parameter
- (simple_identifier) @property)
-
-(class_body
- (property_declaration
- (variable_declaration
- (simple_identifier) @property)))
-
-; id_1.id_2.id_3: `id_2` and `id_3` are assumed as object properties
-(_
- (navigation_suffix
- (simple_identifier) @property))
-
-(enum_entry
- (simple_identifier) @constant)
-
-(type_identifier) @type
-
-((type_identifier) @type.builtin
- (#any-of? @type.builtin
- "Byte"
- "Short"
- "Int"
- "Long"
- "UByte"
- "UShort"
- "UInt"
- "ULong"
- "Float"
- "Double"
- "Boolean"
- "Char"
- "String"
- "Array"
- "ByteArray"
- "ShortArray"
- "IntArray"
- "LongArray"
- "UByteArray"
- "UShortArray"
- "UIntArray"
- "ULongArray"
- "FloatArray"
- "DoubleArray"
- "BooleanArray"
- "CharArray"
- "Map"
- "Set"
- "List"
- "EmptyMap"
- "EmptySet"
- "EmptyList"
- "MutableMap"
- "MutableSet"
- "MutableList"
-))
-
-(package_header
- . (identifier)) @namespace
-
-(import_header
- "import" @include)
-
-
-; TODO: Seperate labeled returns/breaks/continue/super/this
-; Must be implemented in the parser first
-(label) @label
-
-;;; Function definitions
-
-(function_declaration
- . (simple_identifier) @function)
-
-(getter
- ("get") @function.builtin)
-(setter
- ("set") @function.builtin)
-
-(primary_constructor) @constructor
-(secondary_constructor
- ("constructor") @constructor)
-
-(constructor_invocation
- (user_type
- (type_identifier) @constructor))
-
-(anonymous_initializer
- ("init") @constructor)
-
-(parameter
- (simple_identifier) @parameter)
-
-(parameter_with_optional_type
- (simple_identifier) @parameter)
-
-; lambda parameters
-(lambda_literal
- (lambda_parameters
- (variable_declaration
- (simple_identifier) @parameter)))
-
-;;; Function calls
-
-; function()
-(call_expression
- . (simple_identifier) @function)
-
-; object.function() or object.property.function()
-(call_expression
- (navigation_expression
- (navigation_suffix
- (simple_identifier) @function) . ))
-
-(call_expression
- . (simple_identifier) @function.builtin
- (#any-of? @function.builtin
- "arrayOf"
- "arrayOfNulls"
- "byteArrayOf"
- "shortArrayOf"
- "intArrayOf"
- "longArrayOf"
- "ubyteArrayOf"
- "ushortArrayOf"
- "uintArrayOf"
- "ulongArrayOf"
- "floatArrayOf"
- "doubleArrayOf"
- "booleanArrayOf"
- "charArrayOf"
- "emptyArray"
- "mapOf"
- "setOf"
- "listOf"
- "emptyMap"
- "emptySet"
- "emptyList"
- "mutableMapOf"
- "mutableSetOf"
- "mutableListOf"
- "print"
- "println"
- "error"
- "TODO"
- "run"
- "runCatching"
- "repeat"
- "lazy"
- "lazyOf"
- "enumValues"
- "enumValueOf"
- "assert"
- "check"
- "checkNotNull"
- "require"
- "requireNotNull"
- "with"
- "suspend"
- "synchronized"
-))
-
-;;; Literals
-
-[
- (line_comment)
- (multiline_comment)
- (shebang_line)
-] @comment
-
-(real_literal) @float
-[
- (integer_literal)
- (long_literal)
- (hex_literal)
- (bin_literal)
- (unsigned_literal)
-] @number
-
-[
- "null" ; should be highlighted the same as booleans
- (boolean_literal)
-] @boolean
-
-(character_literal) @character
-
-(string_literal) @string
-
-(character_escape_seq) @string.escape
-
-; There are 3 ways to define a regex
-; - "[abc]?".toRegex()
-(call_expression
- (navigation_expression
- ((string_literal) @string.regex)
- (navigation_suffix
- ((simple_identifier) @_function
- (#eq? @_function "toRegex")))))
-
-; - Regex("[abc]?")
-(call_expression
- ((simple_identifier) @_function
- (#eq? @_function "Regex"))
- (call_suffix
- (value_arguments
- (value_argument
- (string_literal) @string.regex))))
-
-; - Regex.fromLiteral("[abc]?")
-(call_expression
- (navigation_expression
- ((simple_identifier) @_class
- (#eq? @_class "Regex"))
- (navigation_suffix
- ((simple_identifier) @_function
- (#eq? @_function "fromLiteral"))))
- (call_suffix
- (value_arguments
- (value_argument
- (string_literal) @string.regex))))
-
-;;; Keywords
-
-(type_alias "typealias" @keyword)
-[
- (class_modifier)
- (member_modifier)
- (function_modifier)
- (property_modifier)
- (platform_modifier)
- (variance_modifier)
- (parameter_modifier)
- (visibility_modifier)
- (reification_modifier)
- (inheritance_modifier)
-]@keyword
-
-[
- "val"
- "var"
- "enum"
- "class"
- "object"
- "interface"
-; "typeof" ; NOTE: It is reserved for future use
-] @keyword
-
-("fun") @keyword.function
-
-(jump_expression) @keyword.return
-
-[
- "if"
- "else"
- "when"
-] @conditional
-
-[
- "for"
- "do"
- "while"
-] @repeat
-
-[
- "try"
- "catch"
- "throw"
- "finally"
-] @exception
-
-
-(annotation
- "@" @attribute (use_site_target)? @attribute)
-(annotation
- (user_type
- (type_identifier) @attribute))
-(annotation
- (constructor_invocation
- (user_type
- (type_identifier) @attribute)))
-
-(file_annotation
- "@" @attribute "file" @attribute ":" @attribute)
-(file_annotation
- (user_type
- (type_identifier) @attribute))
-(file_annotation
- (constructor_invocation
- (user_type
- (type_identifier) @attribute)))
-
-;;; Operators & Punctuation
-
-[
- "!"
- "!="
- "!=="
- "="
- "=="
- "==="
- ">"
- ">="
- "<"
- "<="
- "||"
- "&&"
- "+"
- "++"
- "+="
- "-"
- "--"
- "-="
- "*"
- "*="
- "/"
- "/="
- "%"
- "%="
- "?."
- "?:"
- "!!"
- "is"
- "!is"
- "in"
- "!in"
- "as"
- "as?"
- ".."
- "->"
-] @operator
-
-[
- "(" ")"
- "[" "]"
- "{" "}"
-] @punctuation.bracket
-
-[
- "."
- ","
- ";"
- ":"
- "::"
-] @punctuation.delimiter
-
-; NOTE: `interpolated_identifier`s can be highlighted in any way
-(string_literal
- "$" @punctuation.special
- (interpolated_identifier) @none)
-(string_literal
- "${" @punctuation.special
- (interpolated_expression) @none
- "}" @punctuation.special)
diff --git a/contrib/python/textual/textual/tree-sitter/highlights/markdown.scm b/contrib/python/textual/textual/tree-sitter/highlights/markdown.scm
index 1d6691bcfc6..563c5138f56 100644
--- a/contrib/python/textual/textual/tree-sitter/highlights/markdown.scm
+++ b/contrib/python/textual/textual/tree-sitter/highlights/markdown.scm
@@ -1,9 +1,51 @@
-(heading_content) @heading
-(list_marker) @comment
-(strong_emphasis) @bold
-(emphasis) @italic
-(strikethrough) @strikethrough
-(link) @link
-(code_span) @inline_code
-(info_string) @info_string
-(fenced_code_block) @fenced_code_block
+(atx_heading (inline) @heading)
+(setext_heading (paragraph) @heading)
+
+[
+ (atx_h1_marker)
+ (atx_h2_marker)
+ (atx_h3_marker)
+ (atx_h4_marker)
+ (atx_h5_marker)
+ (atx_h6_marker)
+ (setext_h1_underline)
+ (setext_h2_underline)
+] @heading.marker
+
+[
+ (link_title)
+ (indented_code_block)
+ (fenced_code_block)
+] @text.literal
+
+[
+ (fenced_code_block_delimiter)
+] @punctuation.delimiter
+
+(code_fence_content) @none
+
+[
+ (link_destination)
+] @link.uri
+
+[
+ (link_label)
+] @link.label
+
+[
+ (list_marker_plus)
+ (list_marker_minus)
+ (list_marker_star)
+ (list_marker_dot)
+ (list_marker_parenthesis)
+ (thematic_break)
+] @list.marker
+
+[
+ (block_continuation)
+ (block_quote_marker)
+] @punctuation.special
+
+[
+ (backslash_escape)
+] @string.escape
diff --git a/contrib/python/textual/textual/tree-sitter/highlights/sql.scm b/contrib/python/textual/textual/tree-sitter/highlights/sql.scm
index 03a15fe381f..fe4913026c0 100644
--- a/contrib/python/textual/textual/tree-sitter/highlights/sql.scm
+++ b/contrib/python/textual/textual/tree-sitter/highlights/sql.scm
@@ -1,114 +1,444 @@
-(string) @string
-(number) @number
-(comment) @comment
+(object_reference
+ name: (identifier) @type)
-(function_call
- function: (identifier) @function)
+(invocation
+ (object_reference
+ name: (identifier) @function.call))
[
- (NULL)
- (TRUE)
- (FALSE)
-] @constant.builtin
-
-([
- (type_cast
- (type (identifier) @type.builtin))
- (create_function_statement
- (type (identifier) @type.builtin))
- (create_function_statement
- (create_function_parameters
- (create_function_parameter (type (identifier) @type.builtin))))
- (create_type_statement
- (type_spec_composite (type (identifier) @type.builtin)))
- (create_table_statement
- (table_parameters
- (table_column (type (identifier) @type.builtin))))
- ]
- (#match?
- @type.builtin
- "^(bigint|BIGINT|int8|INT8|bigserial|BIGSERIAL|serial8|SERIAL8|bit|BIT|varbit|VARBIT|boolean|BOOLEAN|bool|BOOL|box|BOX|bytea|BYTEA|character|CHARACTER|char|CHAR|varchar|VARCHAR|cidr|CIDR|circle|CIRCLE|date|DATE|float8|FLOAT8|inet|INET|integer|INTEGER|int|INT|int4|INT4|interval|INTERVAL|json|JSON|jsonb|JSONB|line|LINE|lseg|LSEG|macaddr|MACADDR|money|MONEY|numeric|NUMERIC|decimal|DECIMAL|path|PATH|pg_lsn|PG_LSN|point|POINT|polygon|POLYGON|real|REAL|float4|FLOAT4|smallint|SMALLINT|int2|INT2|smallserial|SMALLSERIAL|serial2|SERIAL2|serial|SERIAL|serial4|SERIAL4|text|TEXT|time|TIME|time|TIME|timestamp|TIMESTAMP|tsquery|TSQUERY|tsvector|TSVECTOR|txid_snapshot|TXID_SNAPSHOT|enum|ENUM|range|RANGE)$"))
-
-(identifier) @variable
+ (keyword_gist)
+ (keyword_btree)
+ (keyword_hash)
+ (keyword_spgist)
+ (keyword_gin)
+ (keyword_brin)
+ (keyword_array)
+] @function.call
+
+(relation
+ alias: (identifier) @variable)
+
+(field
+ name: (identifier) @field)
+
+(term
+ alias: (identifier) @variable)
+
+((term
+ value: (cast
+ name: (keyword_cast) @function.call
+ parameter: [(literal)]?)))
+
+(comment) @comment @spell
+(marginalia) @comment
+
+((literal) @number
+ (#match? @number "^[-+]?%d+$"))
+
+((literal) @float
+ (#match? @float "^[-+]?%d*\.%d*$"))
+
+(literal) @string
+
+(parameter) @parameter
+
+[
+ (keyword_true)
+ (keyword_false)
+] @boolean
+
+[
+ (keyword_asc)
+ (keyword_desc)
+ (keyword_terminated)
+ (keyword_escaped)
+ (keyword_unsigned)
+ (keyword_nulls)
+ (keyword_last)
+ (keyword_delimited)
+ (keyword_replication)
+ (keyword_auto_increment)
+ (keyword_default)
+ (keyword_collate)
+ (keyword_concurrently)
+ (keyword_engine)
+ (keyword_always)
+ (keyword_generated)
+ (keyword_preceding)
+ (keyword_following)
+ (keyword_first)
+ (keyword_current_timestamp)
+ (keyword_immutable)
+ (keyword_atomic)
+ (keyword_parallel)
+ (keyword_leakproof)
+ (keyword_safe)
+ (keyword_cost)
+ (keyword_strict)
+] @attribute
+
+[
+ (keyword_materialized)
+ (keyword_recursive)
+ (keyword_temp)
+ (keyword_temporary)
+ (keyword_unlogged)
+ (keyword_external)
+ (keyword_parquet)
+ (keyword_csv)
+ (keyword_rcfile)
+ (keyword_textfile)
+ (keyword_orc)
+ (keyword_avro)
+ (keyword_jsonfile)
+ (keyword_sequencefile)
+ (keyword_volatile)
+] @storageclass
+
+[
+ (keyword_case)
+ (keyword_when)
+ (keyword_then)
+ (keyword_else)
+] @conditional
+
+[
+ (keyword_select)
+ (keyword_from)
+ (keyword_where)
+ (keyword_index)
+ (keyword_join)
+ (keyword_primary)
+ (keyword_delete)
+ (keyword_create)
+ (keyword_show)
+ (keyword_insert)
+ (keyword_merge)
+ (keyword_distinct)
+ (keyword_replace)
+ (keyword_update)
+ (keyword_into)
+ (keyword_overwrite)
+ (keyword_matched)
+ (keyword_values)
+ (keyword_value)
+ (keyword_attribute)
+ (keyword_set)
+ (keyword_left)
+ (keyword_right)
+ (keyword_outer)
+ (keyword_inner)
+ (keyword_full)
+ (keyword_order)
+ (keyword_partition)
+ (keyword_group)
+ (keyword_with)
+ (keyword_without)
+ (keyword_as)
+ (keyword_having)
+ (keyword_limit)
+ (keyword_offset)
+ (keyword_table)
+ (keyword_tables)
+ (keyword_key)
+ (keyword_references)
+ (keyword_foreign)
+ (keyword_constraint)
+ (keyword_force)
+ (keyword_use)
+ (keyword_for)
+ (keyword_if)
+ (keyword_exists)
+ (keyword_column)
+ (keyword_columns)
+ (keyword_cross)
+ (keyword_lateral)
+ (keyword_natural)
+ (keyword_alter)
+ (keyword_drop)
+ (keyword_add)
+ (keyword_view)
+ (keyword_end)
+ (keyword_is)
+ (keyword_using)
+ (keyword_between)
+ (keyword_window)
+ (keyword_no)
+ (keyword_data)
+ (keyword_type)
+ (keyword_rename)
+ (keyword_to)
+ (keyword_schema)
+ (keyword_owner)
+ (keyword_authorization)
+ (keyword_all)
+ (keyword_any)
+ (keyword_some)
+ (keyword_returning)
+ (keyword_begin)
+ (keyword_commit)
+ (keyword_rollback)
+ (keyword_transaction)
+ (keyword_only)
+ (keyword_like)
+ (keyword_similar)
+ (keyword_over)
+ (keyword_change)
+ (keyword_modify)
+ (keyword_after)
+ (keyword_before)
+ (keyword_range)
+ (keyword_rows)
+ (keyword_groups)
+ (keyword_exclude)
+ (keyword_current)
+ (keyword_ties)
+ (keyword_others)
+ (keyword_zerofill)
+ (keyword_format)
+ (keyword_fields)
+ (keyword_row)
+ (keyword_sort)
+ (keyword_compute)
+ (keyword_comment)
+ (keyword_location)
+ (keyword_cached)
+ (keyword_uncached)
+ (keyword_lines)
+ (keyword_stored)
+ (keyword_virtual)
+ (keyword_partitioned)
+ (keyword_analyze)
+ (keyword_explain)
+ (keyword_verbose)
+ (keyword_truncate)
+ (keyword_rewrite)
+ (keyword_optimize)
+ (keyword_vacuum)
+ (keyword_cache)
+ (keyword_language)
+ (keyword_called)
+ (keyword_conflict)
+ (keyword_declare)
+ (keyword_filter)
+ (keyword_function)
+ (keyword_input)
+ (keyword_name)
+ (keyword_oid)
+ (keyword_oids)
+ (keyword_precision)
+ (keyword_regclass)
+ (keyword_regnamespace)
+ (keyword_regproc)
+ (keyword_regtype)
+ (keyword_restricted)
+ (keyword_return)
+ (keyword_returns)
+ (keyword_separator)
+ (keyword_setof)
+ (keyword_stable)
+ (keyword_support)
+ (keyword_tblproperties)
+ (keyword_trigger)
+ (keyword_unsafe)
+ (keyword_admin)
+ (keyword_connection)
+ (keyword_cycle)
+ (keyword_database)
+ (keyword_encrypted)
+ (keyword_increment)
+ (keyword_logged)
+ (keyword_none)
+ (keyword_owned)
+ (keyword_password)
+ (keyword_reset)
+ (keyword_role)
+ (keyword_sequence)
+ (keyword_start)
+ (keyword_restart)
+ (keyword_tablespace)
+ (keyword_until)
+ (keyword_user)
+ (keyword_valid)
+ (keyword_action)
+ (keyword_definer)
+ (keyword_invoker)
+ (keyword_security)
+ (keyword_extension)
+ (keyword_version)
+ (keyword_out)
+ (keyword_inout)
+ (keyword_variadic)
+ (keyword_ordinality)
+ (keyword_session)
+ (keyword_isolation)
+ (keyword_level)
+ (keyword_serializable)
+ (keyword_repeatable)
+ (keyword_read)
+ (keyword_write)
+ (keyword_committed)
+ (keyword_uncommitted)
+ (keyword_deferrable)
+ (keyword_names)
+ (keyword_zone)
+ (keyword_immediate)
+ (keyword_deferred)
+ (keyword_constraints)
+ (keyword_snapshot)
+ (keyword_characteristics)
+ (keyword_off)
+ (keyword_follows)
+ (keyword_precedes)
+ (keyword_each)
+ (keyword_instead)
+ (keyword_of)
+ (keyword_initially)
+ (keyword_old)
+ (keyword_new)
+ (keyword_referencing)
+ (keyword_statement)
+ (keyword_execute)
+ (keyword_procedure)
+ (keyword_copy)
+ (keyword_delimiter)
+ (keyword_encoding)
+ (keyword_escape)
+ (keyword_force_not_null)
+ (keyword_force_null)
+ (keyword_force_quote)
+ (keyword_freeze)
+ (keyword_header)
+ (keyword_match)
+ (keyword_program)
+ (keyword_quote)
+ (keyword_stdin)
+ (keyword_extended)
+ (keyword_main)
+ (keyword_plain)
+ (keyword_storage)
+ (keyword_compression)
+ (keyword_duplicate)
+] @keyword
[
- "::"
+ (keyword_restrict)
+ (keyword_unbounded)
+ (keyword_unique)
+ (keyword_cascade)
+ (keyword_delayed)
+ (keyword_high_priority)
+ (keyword_low_priority)
+ (keyword_ignore)
+ (keyword_nothing)
+ (keyword_check)
+ (keyword_option)
+ (keyword_local)
+ (keyword_cascaded)
+ (keyword_wait)
+ (keyword_nowait)
+ (keyword_metadata)
+ (keyword_incremental)
+ (keyword_bin_pack)
+ (keyword_noscan)
+ (keyword_stats)
+ (keyword_statistics)
+ (keyword_maxvalue)
+ (keyword_minvalue)
+] @type.qualifier
+
+[
+ (keyword_int)
+ (keyword_null)
+ (keyword_boolean)
+ (keyword_binary)
+ (keyword_varbinary)
+ (keyword_image)
+ (keyword_bit)
+ (keyword_inet)
+ (keyword_character)
+ (keyword_smallserial)
+ (keyword_serial)
+ (keyword_bigserial)
+ (keyword_smallint)
+ (keyword_mediumint)
+ (keyword_bigint)
+ (keyword_tinyint)
+ (keyword_decimal)
+ (keyword_float)
+ (keyword_double)
+ (keyword_numeric)
+ (keyword_real)
+ (double)
+ (keyword_money)
+ (keyword_smallmoney)
+ (keyword_char)
+ (keyword_nchar)
+ (keyword_varchar)
+ (keyword_nvarchar)
+ (keyword_varying)
+ (keyword_text)
+ (keyword_string)
+ (keyword_uuid)
+ (keyword_json)
+ (keyword_jsonb)
+ (keyword_xml)
+ (keyword_bytea)
+ (keyword_enum)
+ (keyword_date)
+ (keyword_datetime)
+ (keyword_time)
+ (keyword_datetime2)
+ (keyword_datetimeoffset)
+ (keyword_smalldatetime)
+ (keyword_timestamp)
+ (keyword_timestamptz)
+ (keyword_geometry)
+ (keyword_geography)
+ (keyword_box2d)
+ (keyword_box3d)
+ (keyword_interval)
+] @type.builtin
+
+[
+ (keyword_in)
+ (keyword_and)
+ (keyword_or)
+ (keyword_not)
+ (keyword_by)
+ (keyword_on)
+ (keyword_do)
+ (keyword_union)
+ (keyword_except)
+ (keyword_intersect)
+] @keyword.operator
+
+[
+ "+"
+ "-"
+ "*"
+ "/"
+ "%"
+ "^"
+ ":="
+ "="
"<"
"<="
- "<>"
- "="
- ">"
+ "!="
">="
+ ">"
+ "<>"
+ (op_other)
+ (op_unary_other)
] @operator
[
"("
")"
- "["
- "]"
] @punctuation.bracket
[
";"
+ ","
"."
] @punctuation.delimiter
-
-[
- (type)
- (array_type)
-] @type
-
-[
- (primary_key_constraint)
- (unique_constraint)
- (null_constraint)
-] @keyword
-
-[
- "AND"
- "AS"
- "AUTO_INCREMENT"
- "CREATE"
- "CREATE_DOMAIN"
- "CREATE_OR_REPLACE_FUNCTION"
- "CREATE_SCHEMA"
- "TABLE"
- "TEMPORARY"
- "CREATE_TYPE"
- "DATABASE"
- "FROM"
- "GRANT"
- "GROUP_BY"
- "IF_NOT_EXISTS"
- "INDEX"
- "INNER"
- "INSERT"
- "INTO"
- "IN"
- "JOIN"
- "LANGUAGE"
- "LEFT"
- "LOCAL"
- "NOT"
- "ON"
- "OR"
- "ORDER_BY"
- "OUTER"
- "PRIMARY_KEY"
- "PUBLIC"
- "RETURNS"
- "SCHEMA"
- "SELECT"
- "SESSION"
- "SET"
- "TABLE"
- "TIME_ZONE"
- "TO"
- "UNIQUE"
- "UPDATE"
- "USAGE"
- "VALUES"
- "WHERE"
- "WITH"
- "WITHOUT"
-] @keyword
diff --git a/contrib/python/textual/textual/tree-sitter/highlights/toml.scm b/contrib/python/textual/textual/tree-sitter/highlights/toml.scm
index 26e3982fef4..58ba6eb2d3e 100644
--- a/contrib/python/textual/textual/tree-sitter/highlights/toml.scm
+++ b/contrib/python/textual/textual/tree-sitter/highlights/toml.scm
@@ -24,7 +24,7 @@
"." @punctuation.delimiter
"," @punctuation.delimiter
-"=" @toml.operator
+"=" @operator
"[" @punctuation.bracket
"]" @punctuation.bracket
diff --git a/contrib/python/textual/textual/tree-sitter/highlights/xml.scm b/contrib/python/textual/textual/tree-sitter/highlights/xml.scm
new file mode 100644
index 00000000000..9861eea1784
--- /dev/null
+++ b/contrib/python/textual/textual/tree-sitter/highlights/xml.scm
@@ -0,0 +1,168 @@
+;; XML declaration
+
+"xml" @keyword
+
+[ "version" "encoding" "standalone" ] @property
+
+(EncName) @string.special
+
+(VersionNum) @number
+
+[ "yes" "no" ] @boolean
+
+;; Processing instructions
+
+(PI) @embedded
+
+(PI (PITarget) @keyword)
+
+;; Element declaration
+
+(elementdecl
+ "ELEMENT" @keyword
+ (Name) @tag)
+
+(contentspec
+ (_ (Name) @property))
+
+"#PCDATA" @type.builtin
+
+[ "EMPTY" "ANY" ] @string.special.symbol
+
+[ "*" "?" "+" ] @operator
+
+;; Entity declaration
+
+(GEDecl
+ "ENTITY" @keyword
+ (Name) @constant)
+
+(GEDecl (EntityValue) @string)
+
+(NDataDecl
+ "NDATA" @keyword
+ (Name) @label)
+
+;; Parsed entity declaration
+
+(PEDecl
+ "ENTITY" @keyword
+ "%" @operator
+ (Name) @constant)
+
+(PEDecl (EntityValue) @string)
+
+;; Notation declaration
+
+(NotationDecl
+ "NOTATION" @keyword
+ (Name) @constant)
+
+(NotationDecl
+ (ExternalID
+ (SystemLiteral (URI) @string.special)))
+
+;; Attlist declaration
+
+(AttlistDecl
+ "ATTLIST" @keyword
+ (Name) @tag)
+
+(AttDef (Name) @property)
+
+(AttDef (Enumeration (Nmtoken) @string))
+
+(DefaultDecl (AttValue) @string)
+
+[
+ (StringType)
+ (TokenizedType)
+] @type.builtin
+
+(NotationType "NOTATION" @type.builtin)
+
+[
+ "#REQUIRED"
+ "#IMPLIED"
+ "#FIXED"
+] @attribute
+
+;; Entities
+
+(EntityRef) @constant
+
+((EntityRef) @constant.builtin
+ (#any-of? @constant.builtin
+ "&amp;" "&lt;" "&gt;" "&quot;" "&apos;"))
+
+(CharRef) @constant
+
+(PEReference) @constant
+
+;; External references
+
+[ "PUBLIC" "SYSTEM" ] @keyword
+
+(PubidLiteral) @string.special
+
+(SystemLiteral (URI) @markup.link)
+
+;; Processing instructions
+
+(XmlModelPI "xml-model" @keyword)
+
+(StyleSheetPI "xml-stylesheet" @keyword)
+
+(PseudoAtt (Name) @property)
+
+(PseudoAtt (PseudoAttValue) @string)
+
+;; Doctype declaration
+
+(doctypedecl "DOCTYPE" @keyword)
+
+(doctypedecl (Name) @type)
+
+;; Tags
+
+(STag (Name) @tag)
+
+(ETag (Name) @tag)
+
+(EmptyElemTag (Name) @tag)
+
+;; Attributes
+
+(Attribute (Name) @property)
+
+(Attribute (AttValue) @string)
+
+;; Delimiters & punctuation
+
+[
+ "<?" "?>"
+ "<!" "]]>"
+ "<" ">"
+ "</" "/>"
+] @punctuation.delimiter
+
+[ "(" ")" "[" "]" ] @punctuation.bracket
+
+[ "\"" "'" ] @punctuation.delimiter
+
+[ "," "|" "=" ] @operator
+
+;; Text
+
+(CharData) @markup
+
+(CDSect
+ (CDStart) @markup.heading
+ (CData) @markup.raw
+ "]]>" @markup.heading)
+
+;; Misc
+
+(Comment) @comment
+
+(ERROR) @error
diff --git a/contrib/python/textual/textual/visual.py b/contrib/python/textual/textual/visual.py
index ed21c26fe1c..29303b8d3a5 100644
--- a/contrib/python/textual/textual/visual.py
+++ b/contrib/python/textual/textual/visual.py
@@ -13,6 +13,7 @@ from rich.measure import Measurement
from rich.protocol import is_renderable, rich_cast
from rich.segment import Segment
from rich.style import Style as RichStyle
+from rich.terminal_theme import TerminalTheme
from rich.text import Text
from textual._context import active_app
@@ -141,18 +142,21 @@ class Style:
return new_style
@classmethod
- def from_rich_style(cls, rich_style: RichStyle) -> Style:
+ def from_rich_style(
+ cls, rich_style: RichStyle, theme: TerminalTheme | None = None
+ ) -> Style:
"""Build a Style from a (Rich) Style.
Args:
rich_style: A Rich Style object.
+ theme: Optional Rich [terminal theme][rich.terminal_theme.TerminalTheme].
Returns:
New Style.
"""
return Style(
- Color.from_rich_color(rich_style.bgcolor),
- Color.from_rich_color(rich_style.color),
+ Color.from_rich_color(rich_style.bgcolor, theme),
+ Color.from_rich_color(rich_style.color, theme),
bold=rich_style.bold,
dim=rich_style.dim,
italic=rich_style.italic,
diff --git a/contrib/python/textual/textual/widget.py b/contrib/python/textual/textual/widget.py
index 827288b0422..a543aee836b 100644
--- a/contrib/python/textual/textual/widget.py
+++ b/contrib/python/textual/textual/widget.py
@@ -593,7 +593,7 @@ class Widget(DOMNode):
@property
def is_anchored(self) -> bool:
"""Is this widget anchored?"""
- return self._parent is not None and self._parent is self
+ return isinstance(self._parent, Widget) and self._parent._anchored is self
@property
def is_mouse_over(self) -> bool:
@@ -2446,6 +2446,21 @@ class Widget(DOMNode):
"""
+ def set_scroll(self, x: float | None, y: float | None) -> None:
+ """Set the scroll position without any validation.
+
+ This is a low-level method for when you want to see the scroll position in the next frame.
+ For a more fully featured method, see [`scroll_to`][textual.widget.Widget.scroll_to].
+
+ Args:
+ x: Desired `X` coordinate.
+ y: Desired `Y` coordinate.
+ """
+ if x is not None:
+ self.set_reactive(Widget.scroll_x, round(x))
+ if y is not None:
+ self.set_reactive(Widget.scroll_y, round(y))
+
def scroll_to(
self,
x: float | None = None,
@@ -3680,8 +3695,9 @@ class Widget(DOMNode):
layout: Perform layout if required.
Returns:
- True if anything changed, or False if nothing changed.
+ True if a resize event should be sent, otherwise False.
"""
+
self._layout_cache.clear()
if (
self._size != size
@@ -4065,9 +4081,7 @@ class Widget(DOMNode):
self._check_refresh()
if self.is_anchored:
- self.scroll_visible(animate=self._anchor_animate)
- if self._anchored:
- self._anchored.scroll_visible(animate=self._anchor_animate)
+ self.scroll_visible(animate=self._anchor_animate, immediate=True)
def _check_refresh(self) -> None:
"""Check if a refresh was requested."""
diff --git a/contrib/python/textual/textual/widgets/_digits.py b/contrib/python/textual/textual/widgets/_digits.py
index b85fe275257..86492ebe8f8 100644
--- a/contrib/python/textual/textual/widgets/_digits.py
+++ b/contrib/python/textual/textual/widgets/_digits.py
@@ -6,6 +6,7 @@ from rich.align import Align, AlignMethod
if TYPE_CHECKING:
from textual.app import RenderResult
+
from textual.geometry import Size
from textual.renderables.digits import Digits as DigitsRenderable
from textual.widget import Widget
@@ -59,7 +60,7 @@ class Digits(Widget):
value: New value to display.
Raises:
- ValueError: If the value isn't a `str`.
+ TypeError: If the value isn't a `str`.
"""
if not isinstance(value, str):
raise TypeError("value must be a str")
diff --git a/contrib/python/textual/textual/widgets/_footer.py b/contrib/python/textual/textual/widgets/_footer.py
index 142a7512806..7cc543bbe9d 100644
--- a/contrib/python/textual/textual/widgets/_footer.py
+++ b/contrib/python/textual/textual/widgets/_footer.py
@@ -47,12 +47,7 @@ class FooterKey(Widget):
}
&.-disabled {
- text-style: dim;
- &:hover {
- .footer-key--key {
- background: $foreground-disabled;
- }
- }
+ text-style: dim;
}
&.-compact {
@@ -207,9 +202,10 @@ class Footer(ScrollableContainer, can_focus=False, can_focus_children=False):
def compose(self) -> ComposeResult:
if not self._bindings_ready:
return
+ active_bindings = self.screen.active_bindings
bindings = [
(binding, enabled, tooltip)
- for (_, binding, enabled, tooltip) in self.screen.active_bindings.values()
+ for (_, binding, enabled, tooltip) in active_bindings.values()
if binding.show
]
action_to_bindings: defaultdict[str, list[tuple[Binding, bool, str]]]
@@ -229,20 +225,22 @@ class Footer(ScrollableContainer, can_focus=False, can_focus_children=False):
tooltip=tooltip,
).data_bind(Footer.compact)
if self.show_command_palette and self.app.ENABLE_COMMAND_PALETTE:
- for key, binding in self.app._bindings:
- if binding.action in (
- "app.command_palette",
- "command_palette",
- ):
- yield FooterKey(
- key,
- self.app.get_key_display(binding),
- binding.description,
- binding.action,
- classes="-command-palette",
- tooltip=binding.tooltip or binding.description,
- )
- break
+ try:
+ _node, binding, enabled, tooltip = active_bindings[
+ self.app.COMMAND_PALETTE_BINDING
+ ]
+ except KeyError:
+ pass
+ else:
+ yield FooterKey(
+ binding.key,
+ self.app.get_key_display(binding),
+ binding.description,
+ binding.action,
+ classes="-command-palette",
+ disabled=not enabled,
+ tooltip=binding.tooltip or binding.description,
+ )
async def bindings_changed(self, screen: Screen) -> None:
self._bindings_ready = True
diff --git a/contrib/python/textual/textual/widgets/_list_view.py b/contrib/python/textual/textual/widgets/_list_view.py
index e2363a80f84..b8c36bdd4cb 100644
--- a/contrib/python/textual/textual/widgets/_list_view.py
+++ b/contrib/python/textual/textual/widgets/_list_view.py
@@ -5,6 +5,7 @@ from typing import ClassVar, Iterable, Optional
from typing_extensions import TypeGuard
from textual._loop import loop_from_index
+from textual.await_complete import AwaitComplete
from textual.await_remove import AwaitRemove
from textual.binding import Binding, BindingType
from textual.containers import VerticalScroll
@@ -30,10 +31,6 @@ class ListView(VerticalScroll, can_focus=True, can_focus_children=False):
DEFAULT_CSS = """
ListView {
background: $surface;
- &:focus-within {
- background-tint: $foreground 5%;
- }
-
& > ListItem {
color: $foreground;
height: auto;
@@ -51,12 +48,15 @@ class ListView(VerticalScroll, can_focus=True, can_focus_children=False):
}
}
- &:focus > ListItem.-highlight > Widget {
- width: 1fr;
- color: $block-cursor-foreground;
- background: $block-cursor-background;
- text-style: $block-cursor-text-style;
+ &:focus {
+ background-tint: $foreground 5%;
+ & > ListItem.-highlight {
+ color: $block-cursor-foreground;
+ background: $block-cursor-background;
+ text-style: $block-cursor-text-style;
+ }
}
+
}
"""
@@ -281,7 +281,7 @@ class ListView(VerticalScroll, can_focus=True, can_focus_children=False):
await_mount = self.mount(*items, before=index)
return await_mount
- def pop(self, index: Optional[int] = None) -> AwaitRemove:
+ def pop(self, index: Optional[int] = None) -> AwaitComplete:
"""Remove last ListItem from ListView or
Remove ListItem from ListView by index
@@ -292,13 +292,31 @@ class ListView(VerticalScroll, can_focus=True, can_focus_children=False):
An awaitable that yields control to the event loop until
the DOM has been updated to reflect item being removed.
"""
- if index is None:
- await_remove = self.query("ListItem").last().remove()
- else:
- await_remove = self.query("ListItem")[index].remove()
- return await_remove
-
- def remove_items(self, indices: Iterable[int]) -> AwaitRemove:
+ if len(self) == 0:
+ raise IndexError("pop from empty list")
+
+ index = index if index is not None else -1
+ item_to_remove = self.query("ListItem")[index]
+ normalized_index = index if index >= 0 else index + len(self)
+
+ async def do_pop() -> None:
+ """Remove the item and update the highlighted index."""
+ await item_to_remove.remove()
+ if self.index is not None:
+ if normalized_index < self.index:
+ self.index -= 1
+ elif normalized_index == self.index:
+ old_index = self.index
+ # Force a re-validation of the index
+ self.index = self.index
+ # If the index hasn't changed, the watcher won't be called
+ # but we need to update the highlighted item
+ if old_index == self.index:
+ self.watch_index(old_index, self.index)
+
+ return AwaitComplete(do_pop())
+
+ def remove_items(self, indices: Iterable[int]) -> AwaitComplete:
"""Remove ListItems from ListView by indices
Args:
@@ -309,8 +327,29 @@ class ListView(VerticalScroll, can_focus=True, can_focus_children=False):
"""
items = self.query("ListItem")
items_to_remove = [items[index] for index in indices]
- await_remove = self.remove_children(items_to_remove)
- return await_remove
+ normalized_indices = set(
+ index if index >= 0 else index + len(self) for index in indices
+ )
+
+ async def do_remove_items() -> None:
+ """Remove the items and update the highlighted index."""
+ await self.remove_children(items_to_remove)
+ if self.index is not None:
+ removed_before_highlighted = sum(
+ 1 for index in normalized_indices if index < self.index
+ )
+ if removed_before_highlighted:
+ self.index -= removed_before_highlighted
+ elif self.index in normalized_indices:
+ old_index = self.index
+ # Force a re-validation of the index
+ self.index = self.index
+ # If the index hasn't changed, the watcher won't be called
+ # but we need to update the highlighted item
+ if old_index == self.index:
+ self.watch_index(old_index, self.index)
+
+ return AwaitComplete(do_remove_items())
def action_select_cursor(self) -> None:
"""Select the current item in the list."""
diff --git a/contrib/python/textual/textual/widgets/_loading_indicator.py b/contrib/python/textual/textual/widgets/_loading_indicator.py
index 7a8cb43bf84..355e458f01b 100644
--- a/contrib/python/textual/textual/widgets/_loading_indicator.py
+++ b/contrib/python/textual/textual/widgets/_loading_indicator.py
@@ -8,8 +8,10 @@ from rich.text import Text
if TYPE_CHECKING:
from textual.app import RenderResult
+
+from textual import on
from textual.color import Gradient
-from textual.events import Mount
+from textual.events import InputEvent, Mount
from textual.widget import Widget
@@ -56,6 +58,12 @@ class LoadingIndicator(Widget):
self._start_time = time()
self.auto_refresh = 1 / 16
+ @on(InputEvent)
+ def on_input(self, event: InputEvent) -> None:
+ """Prevent all input events from bubbling, thus disabling widgets in a loading state."""
+ event.stop()
+ event.prevent_default()
+
def render(self) -> RenderResult:
if self.app.animation_level == "none":
return Text("Loading...")
diff --git a/contrib/python/textual/textual/widgets/_option_list.py b/contrib/python/textual/textual/widgets/_option_list.py
index 963f521d26a..6aeac230ce2 100644
--- a/contrib/python/textual/textual/widgets/_option_list.py
+++ b/contrib/python/textual/textual/widgets/_option_list.py
@@ -195,7 +195,7 @@ class OptionList(ScrollView, can_focus=True):
| `option-list--separator` | Target the separators. |
"""
- highlighted: reactive[int | None] = reactive["int | None"](None)
+ highlighted: reactive[int | None] = reactive(None)
"""The index of the currently-highlighted option, or `None` if no option is highlighted."""
class OptionMessage(Message):
@@ -295,7 +295,7 @@ class OptionList(ScrollView, can_focus=True):
"""A dictionary of option IDs and the option indexes they relate to."""
self._content_render_cache: LRUCache[tuple[int, str, int], list[Strip]]
- self._content_render_cache = LRUCache(256)
+ self._content_render_cache = LRUCache(1024)
self._lines: list[tuple[int, int]] | None = None
self._spans: list[OptionLineSpan] | None = None
@@ -324,7 +324,7 @@ class OptionList(ScrollView, can_focus=True):
self._lines = None
self._spans = None
self._content_render_cache.clear()
- self.check_idle()
+ self._populate()
def notify_style_update(self) -> None:
self._content_render_cache.clear()
@@ -332,10 +332,6 @@ class OptionList(ScrollView, can_focus=True):
def _on_resize(self):
self._refresh_lines()
- def on_idle(self):
- if self._lines is None:
- self._populate()
-
def _add_lines(
self, new_content: list[OptionListContent], width: int, option_index=0
) -> None:
@@ -366,11 +362,12 @@ class OptionList(ScrollView, can_focus=True):
self._lines.append(OptionLineSpan(-1, 0))
self.virtual_size = Size(width, len(self._lines))
+ self.refresh(layout=self.styles.auto_dimensions)
+ self._scroll_update(self.virtual_size)
def _populate(self) -> None:
"""Populate the lines data-structure."""
- if self._lines is not None:
- return
+
self._lines = []
self._spans = []
@@ -378,15 +375,19 @@ class OptionList(ScrollView, can_focus=True):
self._contents,
self.scrollable_content_region.width - self._left_gutter_width(),
)
- self.refresh()
def get_content_width(self, container: Size, viewport: Size) -> int:
"""Get maximum width of options."""
console = self.app.console
options = console.options
- return max(
- Measurement.get(console, options, option.prompt).maximum
- for option in self._options
+ padding = self.get_component_styles("option-list--option").padding
+ padding_width = padding.width
+ return (
+ max(
+ Measurement.get(console, options, option.prompt).maximum
+ for option in self._options
+ )
+ + padding_width
)
def get_content_height(self, container: Size, viewport: Size, width: int) -> int:
@@ -550,7 +551,7 @@ class OptionList(ScrollView, can_focus=True):
self.scrollable_content_region.width - self._left_gutter_width(),
option_index=option_index,
)
- self.refresh()
+ self.refresh(layout=True)
return self
def add_option(self, item: NewOptionListContent = None) -> Self:
@@ -823,8 +824,9 @@ class OptionList(ScrollView, can_focus=True):
) from None
def render_line(self, y: int) -> Strip:
- self._populate()
assert self._lines is not None
+ if not self._lines:
+ self._populate()
_scroll_x, scroll_y = self.scroll_offset
line_number = scroll_y + y
@@ -878,9 +880,13 @@ class OptionList(ScrollView, can_focus=True):
top: Scroll highlight to top of the list.
"""
highlighted = self.highlighted
- if highlighted is None or self._spans is None:
+
+ if highlighted is None or not self.is_mounted:
return
+ if not self._spans:
+ self._populate()
+
try:
y, height = self._spans[highlighted]
except IndexError:
@@ -893,8 +899,13 @@ class OptionList(ScrollView, can_focus=True):
force=True,
animate=False,
top=top,
+ immediate=True,
)
+ def on_show(self) -> None:
+ if self.highlighted is not None:
+ self.scroll_to_highlight()
+
def validate_highlighted(self, highlighted: int | None) -> int | None:
"""Validate the `highlighted` property value on access."""
if highlighted is None or not self._options:
@@ -952,7 +963,6 @@ class OptionList(ScrollView, can_focus=True):
# If we find ourselves in a position where we don't know where we're
# going, we need a fallback location. Where we go will depend on the
# direction.
- self._populate()
assert self._spans is not None
assert self._lines is not None
diff --git a/contrib/python/textual/textual/widgets/_select.py b/contrib/python/textual/textual/widgets/_select.py
index 8d651cbe446..fa9af348c38 100644
--- a/contrib/python/textual/textual/widgets/_select.py
+++ b/contrib/python/textual/textual/widgets/_select.py
@@ -66,7 +66,7 @@ class SelectOverlay(OptionList):
index: Index of new selection.
"""
self.highlighted = index
- self.scroll_to_highlight(top=True)
+ self.scroll_to_highlight()
def action_dismiss(self) -> None:
"""Dismiss the overlay."""
@@ -234,11 +234,9 @@ class Select(Generic[SelectType], Vertical, can_focus=True):
.down-arrow {
display: none;
}
-
.up-arrow {
display: block;
}
-
& > SelectOverlay {
display: block;
}
@@ -380,6 +378,18 @@ class Select(Generic[SelectType], Vertical, can_focus=True):
disabled=disabled,
)
+ @property
+ def selection(self) -> SelectType | None:
+ """The currently selected item.
+
+ Unlike [value][textual.widgets.Select.value], this will not return Blanks.
+ If nothing is selected, this will return `None`.
+
+ """
+ value = self.value
+ assert not isinstance(value, NoSelection)
+ return value
+
def _setup_variables_for_options(
self,
options: Iterable[tuple[RenderableType, SelectType]],
@@ -404,7 +414,7 @@ class Select(Generic[SelectType], Vertical, can_focus=True):
def _setup_options_renderables(self) -> None:
"""Sets up the `Option` renderables associated with the `Select` options."""
- self._select_options: list[Option] = [
+ options: list[Option] = [
(
Option(Text(self.prompt, style="dim"))
if value == self.BLANK
@@ -415,8 +425,7 @@ class Select(Generic[SelectType], Vertical, can_focus=True):
option_list = self.query_one(SelectOverlay)
option_list.clear_options()
- for option in self._select_options:
- option_list.add_option(option)
+ option_list.add_options(options)
def _init_selected_option(self, hint: SelectType | NoSelection = BLANK) -> None:
"""Initialises the selected option for the `Select`."""
@@ -503,7 +512,7 @@ class Select(Generic[SelectType], Vertical, can_focus=True):
return
self.set_class(expanded, "-expanded")
if expanded:
- overlay.focus()
+ overlay.focus(scroll_visible=False)
if self.value is self.BLANK:
overlay.select(None)
self.query_one(SelectCurrent).has_value = False
@@ -511,7 +520,7 @@ class Select(Generic[SelectType], Vertical, can_focus=True):
value = self.value
for index, (_prompt, prompt_value) in enumerate(self._options):
if value == prompt_value:
- overlay.select(index)
+ self.call_after_refresh(overlay.select, index)
break
self.query_one(SelectCurrent).has_value = True
diff --git a/contrib/python/textual/textual/widgets/_selection_list.py b/contrib/python/textual/textual/widgets/_selection_list.py
index 7493990f2e9..1d147377cf0 100644
--- a/contrib/python/textual/textual/widgets/_selection_list.py
+++ b/contrib/python/textual/textual/widgets/_selection_list.py
@@ -50,7 +50,8 @@ class Selection(Generic[SelectionType], Option):
disabled: The initial enabled/disabled state. Enabled by default.
"""
if isinstance(prompt, str):
- prompt = Text.from_markup(prompt)
+ prompt = Text.from_markup(prompt, overflow="ellipsis")
+ prompt.no_wrap = True
super().__init__(prompt.split()[0], id, disabled)
self._value: SelectionType = value
"""The value associated with the selection."""
diff --git a/contrib/python/textual/textual/widgets/_tabbed_content.py b/contrib/python/textual/textual/widgets/_tabbed_content.py
index 37630b05dc1..2d625e7cd0c 100644
--- a/contrib/python/textual/textual/widgets/_tabbed_content.py
+++ b/contrib/python/textual/textual/widgets/_tabbed_content.py
@@ -332,6 +332,7 @@ class TabbedContent(Widget):
self.titles = [self.render_str(title) for title in titles]
self._tab_content: list[Widget] = []
self._initial = initial
+ self._tab_counter = 0
super().__init__(name=name, id=id, classes=classes, disabled=disabled)
@property
@@ -357,6 +358,15 @@ class TabbedContent(Widget):
content.id = f"tab-{new_id}"
return content
+ def _generate_tab_id(self) -> int:
+ """Auto generate a new tab id.
+
+ Returns:
+ An auto-incrementing integer.
+ """
+ self._tab_counter += 1
+ return self._tab_counter
+
def compose(self) -> ComposeResult:
"""Compose the tabbed content."""
@@ -368,7 +378,7 @@ class TabbedContent(Widget):
if isinstance(content, TabPane)
else TabPane(title or self.render_str(f"Tab {index}"), content)
),
- index,
+ self._generate_tab_id(),
)
for index, (title, content) in enumerate(
zip_longest(self.titles, self._tab_content), 1
@@ -424,7 +434,7 @@ class TabbedContent(Widget):
if isinstance(after, TabPane):
after = after.id
tabs = self.get_child_by_type(ContentTabs)
- pane = self._set_id(pane, tabs.tab_count + 1)
+ pane = self._set_id(pane, self._generate_tab_id())
assert pane.id is not None
pane.display = False
return AwaitComplete(
diff --git a/contrib/python/textual/textual/widgets/_text_area.py b/contrib/python/textual/textual/widgets/_text_area.py
index 6540889c6fc..e4982103362 100644
--- a/contrib/python/textual/textual/widgets/_text_area.py
+++ b/contrib/python/textual/textual/widgets/_text_area.py
@@ -14,7 +14,7 @@ from rich.text import Text
from typing_extensions import Literal
from textual._text_area_theme import TextAreaTheme
-from textual._tree_sitter import TREE_SITTER
+from textual._tree_sitter import BUILTIN_LANGUAGES, TREE_SITTER
from textual.color import Color
from textual.document._document import (
Document,
@@ -27,7 +27,6 @@ from textual.document._document import (
from textual.document._document_navigator import DocumentNavigator
from textual.document._edit import Edit
from textual.document._history import EditHistory
-from textual.document._languages import BUILTIN_LANGUAGES
from textual.document._syntax_aware_document import (
SyntaxAwareDocument,
SyntaxAwareDocumentError,
@@ -36,7 +35,7 @@ from textual.document._wrapped_document import WrappedDocument
from textual.expand_tabs import expand_tabs_inline, expand_text_tabs_from_widths
if TYPE_CHECKING:
- from tree_sitter import Language
+ from tree_sitter import Language, Query
from textual import events, log
from textual._cells import cell_len, cell_width_to_column_index
@@ -415,6 +414,13 @@ TextArea {
self._languages: dict[str, TextAreaLanguage] = {}
"""Maps language names to TextAreaLanguage."""
+ for language_name, language_object in BUILTIN_LANGUAGES.items():
+ self._languages[language_name] = TextAreaLanguage(
+ language_name,
+ language_object,
+ self._get_builtin_highlight_query(language_name),
+ )
+
self._themes: dict[str, TextAreaTheme] = {}
"""Maps theme names to TextAreaTheme."""
@@ -583,26 +589,28 @@ TextArea {
return
captures = self.document.query_syntax_tree(self._highlight_query)
- for capture in captures:
- node, highlight_name = capture
- node_start_row, node_start_column = node.start_point
- node_end_row, node_end_column = node.end_point
-
- if node_start_row == node_end_row:
- highlight = (node_start_column, node_end_column, highlight_name)
- highlights[node_start_row].append(highlight)
- else:
- # Add the first line of the node range
- highlights[node_start_row].append(
- (node_start_column, None, highlight_name)
- )
+ for highlight_name, nodes in captures.items():
+ for node in nodes:
+ node_start_row, node_start_column = node.start_point
+ node_end_row, node_end_column = node.end_point
+
+ if node_start_row == node_end_row:
+ highlight = (node_start_column, node_end_column, highlight_name)
+ highlights[node_start_row].append(highlight)
+ else:
+ # Add the first line of the node range
+ highlights[node_start_row].append(
+ (node_start_column, None, highlight_name)
+ )
- # Add the middle lines - entire row of this node is highlighted
- for node_row in range(node_start_row + 1, node_end_row):
- highlights[node_row].append((0, None, highlight_name))
+ # Add the middle lines - entire row of this node is highlighted
+ for node_row in range(node_start_row + 1, node_end_row):
+ highlights[node_row].append((0, None, highlight_name))
- # Add the last line of the node range
- highlights[node_end_row].append((0, node_end_column, highlight_name))
+ # Add the last line of the node range
+ highlights[node_end_row].append(
+ (0, node_end_column, highlight_name)
+ )
def _watch_has_focus(self, focus: bool) -> None:
self._cursor_visible = focus
@@ -721,6 +729,9 @@ TextArea {
def _watch_language(self, language: str | None) -> None:
"""When the language is updated, update the type of document."""
+ if not TREE_SITTER:
+ return
+
if language is not None and language not in self.available_languages:
raise LanguageDoesNotExist(
f"{language!r} is not a builtin language, or it has not been registered. "
@@ -830,7 +841,8 @@ TextArea {
def register_language(
self,
- language: "str | Language",
+ name: str,
+ language: "Language",
highlight_query: str,
) -> None:
"""Register a language and corresponding highlight query.
@@ -845,34 +857,29 @@ TextArea {
Registering a language only registers it to this instance of `TextArea`.
Args:
- language: A string referring to a builtin language or a tree-sitter `Language` object.
+ name: The name of the language.
+ language: A tree-sitter `Language` object.
highlight_query: The highlight query to use for syntax highlighting this language.
"""
-
- # If tree-sitter is unavailable, do nothing.
if not TREE_SITTER:
return
+ self._languages[name] = TextAreaLanguage(name, language, highlight_query)
- from tree_sitter_languages import get_language
-
- if isinstance(language, str):
- language_name = language
- language = get_language(language_name)
- else:
- language_name = language.name
+ def update_highlight_query(self, name: str, highlight_query: str) -> None:
+ """Update the highlight query for an already registered language.
- # Update the custom languages. When changing the document,
- # we should first look in here for a language specification.
- # If nothing is found, then we can go to the builtin languages.
- self._languages[language_name] = TextAreaLanguage(
- name=language_name,
- language=language,
- highlight_query=highlight_query,
- )
- # If we updated the currently set language, rebuild the highlights
- # using the newly updated highlights query.
- if language_name == self.language:
- self._set_document(self.text, language_name)
+ Args:
+ name: The name of the language.
+ highlight_query: The highlight query to use for syntax highlighting this language.
+ """
+ if name not in self._languages:
+ raise LanguageDoesNotExist(
+ f"{name!r} is not a registered language.\n"
+ f"To register a language, call `TextArea.register_language`."
+ )
+ self._languages[name].highlight_query = highlight_query
+ if name == self.language:
+ self._set_document(self.text, name)
def _set_document(self, text: str, language: str | None) -> None:
"""Construct and return an appropriate document.
diff --git a/contrib/python/textual/textual/widgets/_toast.py b/contrib/python/textual/textual/widgets/_toast.py
index 1fc87648ae1..8111a1538fa 100644
--- a/contrib/python/textual/textual/widgets/_toast.py
+++ b/contrib/python/textual/textual/widgets/_toast.py
@@ -150,7 +150,7 @@ class ToastRack(Container, inherit_css=False):
layer: _toastrack;
width: 1fr;
height: auto;
- dock: top;
+ dock: bottom;
align: right bottom;
visibility: hidden;
layout: vertical;
diff --git a/contrib/python/textual/textual/widgets/_toggle_button.py b/contrib/python/textual/textual/widgets/_toggle_button.py
index 34a40d6bea2..865dca3ea50 100644
--- a/contrib/python/textual/textual/widgets/_toggle_button.py
+++ b/contrib/python/textual/textual/widgets/_toggle_button.py
@@ -84,7 +84,7 @@ class ToggleButton(Static, can_focus=True):
}
}
}
- """ # TODO: https://github.com/Textualize/textual/issues/1780
+ """
BUTTON_LEFT: str = "▐"
"""The character used for the left side of the toggle button."""
diff --git a/contrib/python/textual/textual/widgets/_tree.py b/contrib/python/textual/textual/widgets/_tree.py
index 79c64be2244..40f73ba6b7b 100644
--- a/contrib/python/textual/textual/widgets/_tree.py
+++ b/contrib/python/textual/textual/widgets/_tree.py
@@ -374,7 +374,7 @@ class TreeNode(Generic[TreeDataType]):
before: Optional index or `TreeNode` to add the node before.
after: Optional index or `TreeNode` to add the node after.
expand: Node should be expanded.
- allow_expand: Allow use to expand the node via keyboard or mouse.
+ allow_expand: Allow user to expand the node via keyboard or mouse.
Returns:
A new Tree node
@@ -1525,6 +1525,8 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
will cause both an expand/collapse event to occur, as well as a
selected event.
"""
+ if self.cursor_line < 0:
+ return
try:
line = self._tree_lines[self.cursor_line]
except IndexError:
diff --git a/contrib/python/textual/textual/widgets/text_area.py b/contrib/python/textual/textual/widgets/text_area.py
index 058f8235420..1d4621a78b8 100644
--- a/contrib/python/textual/textual/widgets/text_area.py
+++ b/contrib/python/textual/textual/widgets/text_area.py
@@ -1,4 +1,5 @@
from textual._text_area_theme import TextAreaTheme
+from textual._tree_sitter import BUILTIN_LANGUAGES
from textual.document._document import (
Document,
DocumentBase,
@@ -9,7 +10,6 @@ from textual.document._document import (
from textual.document._document_navigator import DocumentNavigator
from textual.document._edit import Edit
from textual.document._history import EditHistory
-from textual.document._languages import BUILTIN_LANGUAGES
from textual.document._syntax_aware_document import SyntaxAwareDocument
from textual.document._wrapped_document import WrappedDocument
from textual.widgets._text_area import (
diff --git a/contrib/python/textual/ya.make b/contrib/python/textual/ya.make
index 8a8ef5f7b3b..21a6be3c442 100644
--- a/contrib/python/textual/ya.make
+++ b/contrib/python/textual/ya.make
@@ -2,7 +2,7 @@
PY3_LIBRARY()
-VERSION(0.86.3)
+VERSION(0.89.1)
LICENSE(MIT)
@@ -119,6 +119,7 @@ PY_SRCS(
textual/demo/__main__.py
textual/demo/data.py
textual/demo/demo_app.py
+ textual/demo/game.py
textual/demo/home.py
textual/demo/page.py
textual/demo/projects.py
@@ -129,7 +130,6 @@ PY_SRCS(
textual/document/_document_navigator.py
textual/document/_edit.py
textual/document/_history.py
- textual/document/_languages.py
textual/document/_syntax_aware_document.py
textual/document/_wrapped_document.py
textual/dom.py
@@ -273,13 +273,13 @@ RESOURCE_FILES(
textual/tree-sitter/highlights/java.scm
textual/tree-sitter/highlights/javascript.scm
textual/tree-sitter/highlights/json.scm
- textual/tree-sitter/highlights/kotlin.scm
textual/tree-sitter/highlights/markdown.scm
textual/tree-sitter/highlights/python.scm
textual/tree-sitter/highlights/regex.scm
textual/tree-sitter/highlights/rust.scm
textual/tree-sitter/highlights/sql.scm
textual/tree-sitter/highlights/toml.scm
+ textual/tree-sitter/highlights/xml.scm
textual/tree-sitter/highlights/yaml.scm
)