ai-station/.venv/lib/python3.12/site-packages/textual/_text_area_theme.py

435 lines
18 KiB
Python
Raw Normal View History

2025-12-25 14:54:33 +00:00
from __future__ import annotations
from dataclasses import dataclass, field, fields
from typing import TYPE_CHECKING
from rich.style import Style
from textual.color import Color
if TYPE_CHECKING:
from textual.widgets import TextArea
@dataclass
class TextAreaTheme:
"""A theme for the `TextArea` widget.
Allows theming the general widget (gutter, selections, cursor, and so on) and
mapping of tree-sitter tokens to Rich styles.
For example, consider the following snippet from the `markdown.scm` highlight
query file. We've assigned the `heading_content` token type to the name `heading`.
```
(heading_content) @heading
```
Now, we can map this `heading` name to a Rich style, and it will be styled as
such in the `TextArea`, assuming a parser which returns a `heading_content`
node is used (as will be the case when language="markdown").
```
TextAreaTheme('my_theme', syntax_styles={'heading': Style(color='cyan', bold=True)})
```
We can register this theme with our `TextArea` using the [`TextArea.register_theme`][textual.widgets._text_area.TextArea.register_theme] method,
and headings in our markdown files will be styled bold cyan.
"""
name: str
"""The name of the theme."""
base_style: Style | None = None
"""The background style of the text area. If `None` the parent style will be used."""
gutter_style: Style | None = None
"""The style of the gutter. If `None`, a legible Style will be generated."""
cursor_style: Style | None = None
"""The style of the cursor. If `None`, a legible Style will be generated."""
cursor_line_style: Style | None = None
"""The style to apply to the line the cursor is on."""
cursor_line_gutter_style: Style | None = None
"""The style to apply to the gutter of the line the cursor is on. If `None`, a legible Style will be
generated."""
bracket_matching_style: Style | None = None
"""The style to apply to matching brackets. If `None`, a legible Style will be generated."""
selection_style: Style | None = None
"""The style of the selection. If `None` a default selection Style will be generated."""
syntax_styles: dict[str, Style] = field(default_factory=dict)
"""The mapping of tree-sitter names from the `highlight_query` to Rich styles."""
_theme_configured_attributes: set[str] = field(init=False, default_factory=set)
"""Records which attributes were set via the theme object (as opposed to CSS components)."""
def __post_init__(self) -> None:
theme_fields = fields(self)
for field in theme_fields:
if getattr(self, field.name) is not None:
self._theme_configured_attributes.add(field.name)
def apply_css(self, text_area: TextArea) -> None:
"""Apply CSS rules from a TextArea to be used for fallback styling.
If any attributes in the theme aren't supplied, they'll be filled with the appropriate
base CSS (e.g. color, background, etc.) and component CSS (e.g. text-area--cursor) from
the supplied TextArea.
Args:
text_area: The TextArea instance to retrieve fallback styling from.
"""
self.base_style = text_area.rich_style or Style()
get_style = text_area.get_component_rich_style
if self.base_style.color is None:
self.base_style = Style(color="#f3f3f3", bgcolor=self.base_style.bgcolor)
app_theme = text_area.app.current_theme
if self.base_style.bgcolor is None:
self.base_style = Style(
color=self.base_style.color, bgcolor=app_theme.surface
)
configured = self._theme_configured_attributes.__contains__
assert self.base_style is not None
assert self.base_style.color is not None
assert self.base_style.bgcolor is not None
if not configured("gutter_style"):
gutter_style = get_style("text-area--gutter")
if gutter_style:
self.gutter_style = gutter_style
else:
self.gutter_style = self.base_style.copy()
background_color = Color.from_rich_color(self.base_style.bgcolor)
if not configured("cursor_style"):
# If the theme doesn't contain a cursor style, fallback to component styles.
cursor_style = get_style("text-area--cursor")
if cursor_style:
self.cursor_style = cursor_style
else:
# There's no component style either, fallback to a default.
self.cursor_style = Style.from_color(
color=background_color.rich_color,
bgcolor=background_color.inverse.rich_color,
)
# Apply fallbacks for the styles of the active line and active line gutter.
if not configured("cursor_line_style"):
self.cursor_line_style = get_style("text-area--cursor-line")
if not configured("cursor_line_gutter_style"):
self.cursor_line_gutter_style = get_style("text-area--cursor-gutter")
if not configured("bracket_matching_style"):
matching_bracket_style = get_style("text-area--matching-bracket")
if matching_bracket_style:
self.bracket_matching_style = matching_bracket_style
else:
bracket_matching_background = background_color.blend(
background_color.inverse, factor=0.05
)
self.bracket_matching_style = Style(
bgcolor=bracket_matching_background.rich_color
)
if not configured("selection_style"):
selection_style = get_style("text-area--selection")
if selection_style:
self.selection_style = selection_style
else:
selection_background_color = background_color.blend(
app_theme.primary, factor=0.5
)
self.selection_style = Style.from_color(
bgcolor=selection_background_color.rich_color
)
@classmethod
def get_builtin_theme(cls, theme_name: str) -> TextAreaTheme | None:
"""Get a `TextAreaTheme` by name.
Given a `theme_name`, return the corresponding `TextAreaTheme` object.
Args:
theme_name: The name of the theme.
Returns:
The `TextAreaTheme` corresponding to the name or `None` if the theme isn't
found.
"""
return _BUILTIN_THEMES.get(theme_name)
def get_highlight(self, name: str) -> Style | None:
"""Return the Rich style corresponding to the name defined in the tree-sitter
highlight query for the current theme.
Args:
name: The name of the highlight.
Returns:
The `Style` to use for this highlight, or `None` if no style.
"""
return self.syntax_styles.get(name)
@classmethod
def builtin_themes(cls) -> list[TextAreaTheme]:
"""Get a list of all builtin TextAreaThemes.
Returns:
A list of all builtin TextAreaThemes.
"""
return list(_BUILTIN_THEMES.values())
_MONOKAI = TextAreaTheme(
name="monokai",
base_style=Style(color="#f8f8f2", bgcolor="#272822"),
gutter_style=Style(color="#90908a", bgcolor="#272822"),
cursor_style=Style(color="#272822", bgcolor="#f8f8f0"),
cursor_line_style=Style(bgcolor="#3e3d32"),
cursor_line_gutter_style=Style(color="#c2c2bf", bgcolor="#3e3d32"),
bracket_matching_style=Style(bgcolor="#838889", bold=True),
selection_style=Style(bgcolor="#65686a"),
syntax_styles={
"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="#f8f8f2"),
"repeat": Style(color="#F92672"),
"exception": Style(color="#F92672"),
"include": Style(color="#F92672"),
"keyword.function": Style(color="#F92672"),
"keyword.return": Style(color="#F92672"),
"keyword.operator": Style(color="#F92672"),
"conditional": Style(color="#F92672"),
"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"),
"method.call": Style(color="#A6E22E"),
"boolean": Style(color="#66D9EF", italic=True),
"constant.builtin": Style(color="#66D9EF", italic=True),
"json.null": Style(color="#66D9EF", italic=True),
"regex.punctuation.bracket": Style(color="#F92672"),
"regex.operator": Style(color="#F92672"),
"html.end_tag_error": Style(color="red", underline=True),
"tag": Style(color="#F92672"),
"yaml.field": Style(color="#F92672", bold=True),
"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.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"),
},
)
_DRACULA = TextAreaTheme(
name="dracula",
base_style=Style(color="#f8f8f2", bgcolor="#1E1F35"),
gutter_style=Style(color="#6272a4"),
cursor_style=Style(color="#282a36", bgcolor="#f8f8f0"),
cursor_line_style=Style(bgcolor="#282b45"),
cursor_line_gutter_style=Style(color="#c2c2bf", bgcolor="#282b45", bold=True),
bracket_matching_style=Style(bgcolor="#99999d", bold=True, underline=True),
selection_style=Style(bgcolor="#44475A"),
syntax_styles={
"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="#f8f8f2"),
"repeat": Style(color="#ff79c6"),
"exception": Style(color="#ff79c6"),
"include": Style(color="#ff79c6"),
"keyword.function": Style(color="#ff79c6"),
"keyword.return": Style(color="#ff79c6"),
"keyword.operator": Style(color="#ff79c6"),
"conditional": Style(color="#ff79c6"),
"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="#50fa7b"),
"constant.builtin": Style(color="#bd93f9"),
"json.null": Style(color="#bd93f9"),
"regex.punctuation.bracket": Style(color="#ff79c6"),
"regex.operator": Style(color="#ff79c6"),
"html.end_tag_error": Style(color="#F83333", underline=True),
"tag": Style(color="#ff79c6"),
"yaml.field": Style(color="#ff79c6", bold=True),
"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.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"),
},
)
_DARK_VS = TextAreaTheme(
name="vscode_dark",
base_style=Style(color="#CCCCCC", bgcolor="#1F1F1F"),
gutter_style=Style(color="#6E7681", bgcolor="#1F1F1F"),
cursor_style=Style(color="#1e1e1e", bgcolor="#f0f0f0"),
cursor_line_style=Style(bgcolor="#2b2b2b"),
bracket_matching_style=Style(bgcolor="#3a3a3a", bold=True),
cursor_line_gutter_style=Style(color="#CCCCCC", bgcolor="#2b2b2b"),
selection_style=Style(bgcolor="#264F78"),
syntax_styles={
"string": Style(color="#ce9178"),
"string.documentation": Style(color="#ce9178"),
"comment": Style(color="#6A9955"),
"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"),
"keyword.operator": Style(color="#569cd6"),
"repeat": Style(color="#569cd6"),
"exception": Style(color="#569cd6"),
"include": Style(color="#569cd6"),
"number": Style(color="#b5cea8"),
"float": Style(color="#b5cea8"),
"class": Style(color="#4EC9B0"),
"type": Style(color="#EFCB43"),
"type.class": 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"),
"tag": Style(color="#EFCB43"),
"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.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"),
},
)
_GITHUB_LIGHT = TextAreaTheme(
name="github_light",
base_style=Style(color="#24292e", bgcolor="#f0f0f0"),
gutter_style=Style(color="#BBBBBB", bgcolor="#f0f0f0"),
cursor_style=Style(color="#fafbfc", bgcolor="#24292e"),
cursor_line_style=Style(bgcolor="#ebebeb"),
bracket_matching_style=Style(color="#24292e", underline=True),
cursor_line_gutter_style=Style(color="#A4A4A4", bgcolor="#ebebeb"),
selection_style=Style(bgcolor="#c8c8fa"),
syntax_styles={
"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"),
"keyword.function": Style(color="#CF222E"),
"keyword.return": Style(color="#CF222E"),
"keyword.operator": Style(color="#CF222E"),
"repeat": Style(color="#CF222E"),
"exception": Style(color="#CF222E"),
"include": Style(color="#CF222E"),
"number": Style(color="#d73a49"),
"float": Style(color="#d73a49"),
"parameter": Style(color="#24292e"),
"class": Style(color="#963800"),
"variable": Style(color="#e36209"),
"function": Style(color="#6639BB"),
"method": Style(color="#6639BB"),
"boolean": Style(color="#7DAF9C"),
"constant.builtin": Style(color="#7DAF9C"),
"tag": Style(color="#6639BB"),
"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.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"),
},
)
_CSS_THEME = TextAreaTheme(name="css", syntax_styles=_DARK_VS.syntax_styles)
_BUILTIN_THEMES = {
"css": _CSS_THEME,
"monokai": _MONOKAI,
"dracula": _DRACULA,
"vscode_dark": _DARK_VS,
"github_light": _GITHUB_LIGHT,
}