Skip to content

Commit 4d5b346

Browse files
authored
Merge branch 'main' into lg/fix-select-selection
2 parents bf4e0b9 + e27bcda commit 4d5b346

File tree

101 files changed

+4080
-3853
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

101 files changed

+4080
-3853
lines changed

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,37 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
77

88
## Unreleased
99

10+
11+
### Changed
12+
13+
- Breaking change: `App.query` and friends will now always query the default (first) screen, not necessarily the active screen.
14+
- Content now has a default argument of an empty string, so `Content()` is equivalent to `Content("")`
15+
- Assigned names to Textual-specific threads: `textual-input`, `textual-output`. These should become visible in monitoring tools (ps, top, htop) as of Python 3.14. https://github.com/Textualize/textual/pull/5654
16+
- Tabs now accept Content or content markup https://github.com/Textualize/textual/pull/5657
17+
- Buttons will now use Textual markup rather than console markup
18+
- tree-sitter languages are now loaded lazily, improving cold-start time https://github.com/Textualize/textual/pull/563
19+
1020
### Fixed
1121

1222
- Static and Label now accept Content objects, satisfying type checkers https://github.com/Textualize/textual/pull/5618
1323
- Fixed click selection not being disabled when allow_select was set to false https://github.com/Textualize/textual/issues/5627
1424
- Fixed crash on clicking line API border https://github.com/Textualize/textual/pull/5641
1525
- Fixed Select.selection now correctly returns None if Select.BLANK is selected instead of an AssertionError
26+
- Fixed additional spaces after text-wrapping https://github.com/Textualize/textual/pull/5657
27+
- Added missing `scroll_end` parameter to the `Log.write_line` method https://github.com/Textualize/textual/pull/5672
28+
- Restored support for blink https://github.com/Textualize/textual/pull/5675
29+
- Fixed scrolling breaking on DataTable with `overflow: hidden` https://github.com/Textualize/textual/pull/5681
1630

1731
### Added
1832

1933
- Added Widget.preflight_checks to perform some debug checks after a widget is instantiated, to catch common errors. https://github.com/Textualize/textual/pull/5588
34+
- Added text-padding style https://github.com/Textualize/textual/pull/5657
35+
- Added `Content.first_line` property https://github.com/Textualize/textual/pull/5657
36+
- Added `Content.from_text` constructor https://github.com/Textualize/textual/pull/5657
37+
- Added `Content.empty` constructor https://github.com/Textualize/textual/pull/5657
38+
- Added `Content.pad` method https://github.com/Textualize/textual/pull/5657
39+
- Added `Style.has_transparent_foreground` property https://github.com/Textualize/textual/pull/5657
40+
2041

2142
## [2.1.2] - 2025-02-26
2243

docs/examples/widgets/radio_button.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from rich.text import Text
2+
13
from textual.app import App, ComposeResult
24
from textual.widgets import RadioButton, RadioSet
35

@@ -15,7 +17,9 @@ def compose(self) -> ComposeResult:
1517
yield RadioButton("Star Wars: A New Hope")
1618
yield RadioButton("The Last Starfighter")
1719
yield RadioButton(
18-
"Total Recall :backhand_index_pointing_right: :red_circle:"
20+
Text.from_markup(
21+
"Total Recall :backhand_index_pointing_right: :red_circle:"
22+
)
1923
)
2024
yield RadioButton("Wing Commander")
2125

docs/examples/widgets/radio_set.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from rich.text import Text
2+
13
from textual.app import App, ComposeResult
24
from textual.containers import Horizontal
35
from textual.widgets import RadioButton, RadioSet
@@ -18,7 +20,9 @@ def compose(self) -> ComposeResult:
1820
yield RadioButton("Star Wars: A New Hope")
1921
yield RadioButton("The Last Starfighter")
2022
yield RadioButton(
21-
"Total Recall :backhand_index_pointing_right: :red_circle:"
23+
Text.from_markup(
24+
"Total Recall :backhand_index_pointing_right: :red_circle:"
25+
)
2226
)
2327
yield RadioButton("Wing Commander")
2428
# A RadioSet built up from a collection of strings.

docs/examples/widgets/radio_set_changed.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from rich.text import Text
2+
13
from textual.app import App, ComposeResult
24
from textual.containers import Horizontal, VerticalScroll
35
from textual.widgets import Label, RadioButton, RadioSet
@@ -18,7 +20,9 @@ def compose(self) -> ComposeResult:
1820
yield RadioButton("Star Wars: A New Hope")
1921
yield RadioButton("The Last Starfighter")
2022
yield RadioButton(
21-
"Total Recall :backhand_index_pointing_right: :red_circle:"
23+
Text.from_markup(
24+
"Total Recall :backhand_index_pointing_right: :red_circle:"
25+
)
2226
)
2327
yield RadioButton("Wing Commander")
2428
with Horizontal():

docs/widgets/button.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ This widget has no component classes.
5151

5252
## Additional Notes
5353

54-
- The spacing between the text and the edges of a button are _not_ due to padding. The default styling for a `Button` has the `height` set to 3 lines and a `min-width` of 16 columns. To create a button with zero visible padding, you will need to change these values and also remove the border with `border: none;`.
54+
- The spacing between the text and the edges of a button are _not_ due to padding. The default styling for a `Button` includes borders and a `min-width` of 16 columns. To remove the spacing, set `border: none;` in your CSS and adjust the minimum width as needed.
5555

5656
---
5757

src/textual/_tree_sitter.py

Lines changed: 31 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,44 @@
11
from __future__ import annotations
2+
from importlib import import_module
3+
4+
from textual import log
5+
26

37
try:
4-
import tree_sitter_bash
5-
import tree_sitter_css
6-
import tree_sitter_go
7-
import tree_sitter_html
8-
import tree_sitter_java
9-
import tree_sitter_javascript
10-
import tree_sitter_json
11-
import tree_sitter_markdown
12-
import tree_sitter_python
13-
import tree_sitter_regex
14-
import tree_sitter_rust
15-
import tree_sitter_sql
16-
import tree_sitter_toml
17-
import tree_sitter_xml
18-
import tree_sitter_yaml
198
from tree_sitter import Language
209

21-
_tree_sitter = True
10+
_LANGUAGE_CACHE: dict[str, Language] = {}
2211

23-
_languages = {
24-
"python": Language(tree_sitter_python.language()),
25-
"json": Language(tree_sitter_json.language()),
26-
"markdown": Language(tree_sitter_markdown.language()),
27-
"yaml": Language(tree_sitter_yaml.language()),
28-
"toml": Language(tree_sitter_toml.language()),
29-
"rust": Language(tree_sitter_rust.language()),
30-
"html": Language(tree_sitter_html.language()),
31-
"css": Language(tree_sitter_css.language()),
32-
"xml": Language(tree_sitter_xml.language_xml()),
33-
"regex": Language(tree_sitter_regex.language()),
34-
"sql": Language(tree_sitter_sql.language()),
35-
"javascript": Language(tree_sitter_javascript.language()),
36-
"java": Language(tree_sitter_java.language()),
37-
"bash": Language(tree_sitter_bash.language()),
38-
"go": Language(tree_sitter_go.language()),
39-
}
12+
_tree_sitter = True
4013

4114
def get_language(language_name: str) -> Language | None:
42-
return _languages.get(language_name)
15+
if language_name in _LANGUAGE_CACHE:
16+
return _LANGUAGE_CACHE[language_name]
17+
18+
try:
19+
module = import_module(f"tree_sitter_{language_name}")
20+
except ImportError:
21+
return None
22+
else:
23+
try:
24+
if language_name == "xml":
25+
# xml uses language_xml() instead of language()
26+
# it's the only outlier amongst the languages in the `textual[syntax]` extra
27+
language = Language(module.language_xml(), name=language_name)
28+
else:
29+
language = Language(module.language(), name=language_name)
30+
except (OSError, AttributeError):
31+
log.warning(f"Could not load language {language_name!r}.")
32+
return None
33+
else:
34+
_LANGUAGE_CACHE[language_name] = language
35+
return language
4336

4437
except ImportError:
4538
_tree_sitter = False
46-
_languages = {}
39+
40+
def get_language(language_name: str) -> Language | None:
41+
return None
42+
4743

4844
TREE_SITTER = _tree_sitter
49-
BUILTIN_LANGUAGES: dict[str, "Language"] = _languages

src/textual/app.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -798,6 +798,9 @@ def __init__(
798798
self.supports_smooth_scrolling: bool = False
799799
"""Does the terminal support smooth scrolling?"""
800800

801+
self._compose_screen: Screen | None = None
802+
"""The screen composed by App.compose."""
803+
801804
if self.ENABLE_COMMAND_PALETTE:
802805
for _key, binding in self._bindings:
803806
if binding.action in {"command_palette", "app.command_palette"}:
@@ -833,6 +836,10 @@ def __init_subclass__(cls, *args, **kwargs) -> None:
833836

834837
return super().__init_subclass__(*args, **kwargs)
835838

839+
def _get_dom_base(self) -> DOMNode:
840+
"""When querying from the app, we want to query the default screen."""
841+
return self.default_screen
842+
836843
def validate_title(self, title: Any) -> str:
837844
"""Make sure the title is set to a string."""
838845
return str(title)
@@ -841,6 +848,11 @@ def validate_sub_title(self, sub_title: Any) -> str:
841848
"""Make sure the subtitle is set to a string."""
842849
return str(sub_title)
843850

851+
@property
852+
def default_screen(self) -> Screen:
853+
"""The default screen instance."""
854+
return self.screen if self._compose_screen is None else self._compose_screen
855+
844856
@property
845857
def workers(self) -> WorkerManager:
846858
"""The [worker](/guide/workers/) manager.
@@ -3244,6 +3256,7 @@ async def take_screenshot() -> None:
32443256

32453257
async def _on_compose(self) -> None:
32463258
_rich_traceback_omit = True
3259+
self._compose_screen = self.screen
32473260
try:
32483261
widgets = [*self.screen._nodes, *compose(self)]
32493262
except TypeError as error:

0 commit comments

Comments
 (0)