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, }