mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
implement dim fix (#2326)
* implement dim fix * docstrings * foreground fix * cached filters * cache default * fix for filter tests * docstring * optimization * Update src/textual/filter.py Co-authored-by: Rodrigo Girão Serrão <5621605+rodrigogiraoserrao@users.noreply.github.com> * Update src/textual/constants.py Co-authored-by: Rodrigo Girão Serrão <5621605+rodrigogiraoserrao@users.noreply.github.com> --------- Co-authored-by: Rodrigo Girão Serrão <5621605+rodrigogiraoserrao@users.noreply.github.com>
This commit is contained in:
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from functools import lru_cache
|
||||
from sys import intern
|
||||
from typing import TYPE_CHECKING, Callable, Iterable
|
||||
from typing import TYPE_CHECKING, Callable, Iterable, Sequence
|
||||
|
||||
from rich.console import Console
|
||||
from rich.segment import Segment
|
||||
@@ -139,7 +139,7 @@ class StylesCache:
|
||||
content_size=widget.content_region.size,
|
||||
padding=styles.padding,
|
||||
crop=crop,
|
||||
filter=widget.app._filter,
|
||||
filters=widget.app._filters,
|
||||
)
|
||||
if widget.auto_links:
|
||||
hover_style = widget.hover_style
|
||||
@@ -170,7 +170,7 @@ class StylesCache:
|
||||
content_size: Size | None = None,
|
||||
padding: Spacing | None = None,
|
||||
crop: Region | None = None,
|
||||
filter: LineFilter | None = None,
|
||||
filters: Sequence[LineFilter] | None = None,
|
||||
) -> list[Strip]:
|
||||
"""Render a widget content plus CSS styles.
|
||||
|
||||
@@ -186,7 +186,7 @@ class StylesCache:
|
||||
content_size: Size of content or None to assume full size.
|
||||
padding: Override padding from Styles, or None to use styles.padding.
|
||||
crop: Region to crop to.
|
||||
filter: Additional post-processing for the segments.
|
||||
filters: Additional post-processing for the segments.
|
||||
|
||||
Returns:
|
||||
Rendered lines.
|
||||
@@ -225,8 +225,9 @@ class StylesCache:
|
||||
self._cache[y] = strip
|
||||
else:
|
||||
strip = self._cache[y]
|
||||
if filter:
|
||||
strip = strip.apply_filter(filter)
|
||||
if filters:
|
||||
for filter in filters:
|
||||
strip = strip.apply_filter(filter, background)
|
||||
add_strip(strip)
|
||||
self._dirty_lines.difference_update(crop.line_range)
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@ from contextlib import (
|
||||
from datetime import datetime
|
||||
from functools import partial
|
||||
from pathlib import Path, PurePath
|
||||
from queue import Queue
|
||||
from time import perf_counter
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
@@ -53,6 +52,7 @@ from weakref import WeakSet
|
||||
|
||||
import rich
|
||||
import rich.repr
|
||||
from rich import terminal_theme
|
||||
from rich.console import Console, RenderableType
|
||||
from rich.protocol import is_renderable
|
||||
from rich.segment import Segment, Segments
|
||||
@@ -81,7 +81,7 @@ from .driver import Driver
|
||||
from .drivers.headless_driver import HeadlessDriver
|
||||
from .features import FeatureFlag, parse_features
|
||||
from .file_monitor import FileMonitor
|
||||
from .filter import LineFilter, Monochrome
|
||||
from .filter import ANSIToTruecolor, DimFilter, LineFilter, Monochrome
|
||||
from .geometry import Offset, Region, Size
|
||||
from .keys import (
|
||||
REPLACED_KEYS,
|
||||
@@ -260,11 +260,17 @@ class App(Generic[ReturnType], DOMNode):
|
||||
super().__init__()
|
||||
self.features: frozenset[FeatureFlag] = parse_features(os.getenv("TEXTUAL", ""))
|
||||
|
||||
self._filter: LineFilter | None = None
|
||||
self._filters: list[LineFilter] = []
|
||||
environ = dict(os.environ)
|
||||
no_color = environ.pop("NO_COLOR", None)
|
||||
if no_color is not None:
|
||||
self._filter = Monochrome()
|
||||
self._filters.append(Monochrome())
|
||||
|
||||
for filter_name in constants.FILTERS.split(","):
|
||||
filter = filter_name.lower().strip()
|
||||
if filter == "dim":
|
||||
self._filters.append(ANSIToTruecolor(terminal_theme.DIMMED_MONOKAI))
|
||||
self._filters.append(DimFilter())
|
||||
|
||||
self.console = Console(
|
||||
file=_NullFile(),
|
||||
|
||||
@@ -53,6 +53,9 @@ DEBUG: Final[bool] = get_environ_bool("TEXTUAL_DEBUG")
|
||||
|
||||
DRIVER: Final[str | None] = get_environ("TEXTUAL_DRIVER", None)
|
||||
|
||||
FILTERS: Final[str] = get_environ("TEXTUAL_FILTERS", "")
|
||||
"""A list of filters to apply to renderables."""
|
||||
|
||||
LOG_FILE: Final[str | None] = get_environ("TEXTUAL_LOG", None)
|
||||
"""A last resort log file that appends all logs, when devtools isn't working."""
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from functools import lru_cache
|
||||
from rich.color import Color as RichColor
|
||||
from rich.segment import Segment
|
||||
from rich.style import Style
|
||||
from rich.terminal_theme import TerminalTheme
|
||||
|
||||
from .color import Color
|
||||
|
||||
@@ -14,11 +15,12 @@ class LineFilter(ABC):
|
||||
"""Base class for a line filter."""
|
||||
|
||||
@abstractmethod
|
||||
def apply(self, segments: list[Segment]) -> list[Segment]:
|
||||
def apply(self, segments: list[Segment], background: Color) -> list[Segment]:
|
||||
"""Transform a list of segments.
|
||||
|
||||
Args:
|
||||
segments: A list of segments.
|
||||
background: The background color.
|
||||
|
||||
Returns:
|
||||
A new list of segments.
|
||||
@@ -53,11 +55,12 @@ def monochrome_style(style: Style) -> Style:
|
||||
class Monochrome(LineFilter):
|
||||
"""Convert all colors to monochrome."""
|
||||
|
||||
def apply(self, segments: list[Segment]) -> list[Segment]:
|
||||
def apply(self, segments: list[Segment], background: Color) -> list[Segment]:
|
||||
"""Transform a list of segments.
|
||||
|
||||
Args:
|
||||
segments: A list of segments.
|
||||
background: The background color.
|
||||
|
||||
Returns:
|
||||
A new list of segments.
|
||||
@@ -96,8 +99,11 @@ def dim_color(background: RichColor, color: RichColor, factor: float) -> RichCol
|
||||
)
|
||||
|
||||
|
||||
DEFAULT_COLOR = RichColor.default()
|
||||
|
||||
|
||||
@lru_cache(1024)
|
||||
def dim_style(style: Style, factor: float) -> Style:
|
||||
def dim_style(style: Style, background: Color, factor: float) -> Style:
|
||||
"""Replace dim attribute with a dim color.
|
||||
|
||||
Args:
|
||||
@@ -109,9 +115,19 @@ def dim_style(style: Style, factor: float) -> Style:
|
||||
"""
|
||||
return (
|
||||
style
|
||||
+ Style.from_color(dim_color(style.bgcolor, style.color, factor), None)
|
||||
+ NO_DIM
|
||||
)
|
||||
+ Style.from_color(
|
||||
dim_color(
|
||||
(
|
||||
background.rich_color
|
||||
if style.bgcolor.is_default
|
||||
else (style.bgcolor or DEFAULT_COLOR)
|
||||
),
|
||||
style.color or DEFAULT_COLOR,
|
||||
factor,
|
||||
),
|
||||
None,
|
||||
)
|
||||
) + NO_DIM
|
||||
|
||||
|
||||
# Can be used as a workaround for https://github.com/xtermjs/xterm.js/issues/4161
|
||||
@@ -126,11 +142,12 @@ class DimFilter(LineFilter):
|
||||
"""
|
||||
self.dim_factor = dim_factor
|
||||
|
||||
def apply(self, segments: list[Segment]) -> list[Segment]:
|
||||
def apply(self, segments: list[Segment], background: Color) -> list[Segment]:
|
||||
"""Transform a list of segments.
|
||||
|
||||
Args:
|
||||
segments: A list of segments.
|
||||
background: The background color.
|
||||
|
||||
Returns:
|
||||
A new list of segments.
|
||||
@@ -143,7 +160,7 @@ class DimFilter(LineFilter):
|
||||
(
|
||||
_Segment(
|
||||
segment.text,
|
||||
_dim_style(segment.style, factor),
|
||||
_dim_style(segment.style, background, factor),
|
||||
None,
|
||||
)
|
||||
if segment.style is not None and segment.style.dim
|
||||
@@ -151,3 +168,60 @@ class DimFilter(LineFilter):
|
||||
)
|
||||
for segment in segments
|
||||
]
|
||||
|
||||
|
||||
class ANSIToTruecolor(LineFilter):
|
||||
"""Convert ANSI colors to their truecolor equivalents."""
|
||||
|
||||
def __init__(self, terminal_theme: TerminalTheme):
|
||||
"""Initialise filter.
|
||||
|
||||
Args:
|
||||
terminal_theme: A rich terminal theme.
|
||||
"""
|
||||
self.terminal_theme = terminal_theme
|
||||
|
||||
@lru_cache(1024)
|
||||
def truecolor_style(self, style: Style) -> Style:
|
||||
"""Replace system colors with truecolor equivalent.
|
||||
|
||||
Args:
|
||||
style: Style to apply truecolor filter to.
|
||||
|
||||
Returns:
|
||||
New style.
|
||||
"""
|
||||
terminal_theme = self.terminal_theme
|
||||
color = style.color
|
||||
if color is not None and color.is_system_defined:
|
||||
color = RichColor.from_rgb(
|
||||
*color.get_truecolor(terminal_theme, foreground=True)
|
||||
)
|
||||
bgcolor = style.bgcolor
|
||||
if bgcolor is not None and bgcolor.is_system_defined:
|
||||
bgcolor = RichColor.from_rgb(
|
||||
*bgcolor.get_truecolor(terminal_theme, foreground=False)
|
||||
)
|
||||
return style + Style.from_color(color, bgcolor)
|
||||
|
||||
def apply(self, segments: list[Segment], background: Color) -> list[Segment]:
|
||||
"""Transform a list of segments.
|
||||
|
||||
Args:
|
||||
segments: A list of segments.
|
||||
background: The background color.
|
||||
|
||||
Returns:
|
||||
A new list of segments.
|
||||
"""
|
||||
_Segment = Segment
|
||||
truecolor_style = self.truecolor_style
|
||||
|
||||
return [
|
||||
_Segment(
|
||||
text,
|
||||
None if style is None else truecolor_style(style),
|
||||
None,
|
||||
)
|
||||
for text, style, _ in segments
|
||||
]
|
||||
|
||||
@@ -18,6 +18,7 @@ from rich.style import Style, StyleType
|
||||
|
||||
from ._cache import FIFOCache
|
||||
from ._segment_tools import index_to_cell_position
|
||||
from .color import Color
|
||||
from .constants import DEBUG
|
||||
from .filter import LineFilter
|
||||
|
||||
@@ -67,6 +68,7 @@ class Strip:
|
||||
"_divide_cache",
|
||||
"_crop_cache",
|
||||
"_style_cache",
|
||||
"_filter_cache",
|
||||
"_render_cache",
|
||||
"_link_ids",
|
||||
]
|
||||
@@ -79,6 +81,7 @@ class Strip:
|
||||
self._divide_cache: FIFOCache[tuple[int, ...], list[Strip]] = FIFOCache(4)
|
||||
self._crop_cache: FIFOCache[tuple[int, int], Strip] = FIFOCache(16)
|
||||
self._style_cache: FIFOCache[Style, Strip] = FIFOCache(16)
|
||||
self._filter_cache: FIFOCache[tuple[LineFilter, Color], Strip] = FIFOCache(4)
|
||||
self._render_cache: str | None = None
|
||||
self._link_ids: set[str] | None = None
|
||||
|
||||
@@ -265,7 +268,7 @@ class Strip:
|
||||
)
|
||||
return line
|
||||
|
||||
def apply_filter(self, filter: LineFilter) -> Strip:
|
||||
def apply_filter(self, filter: LineFilter, background: Color) -> Strip:
|
||||
"""Apply a filter to all segments in the strip.
|
||||
|
||||
Args:
|
||||
@@ -274,7 +277,12 @@ class Strip:
|
||||
Returns:
|
||||
A new Strip.
|
||||
"""
|
||||
return Strip(filter.apply(self._segments), self._cell_length)
|
||||
cached_strip = self._filter_cache.get((filter, background))
|
||||
if cached_strip is None:
|
||||
cached_strip = Strip(
|
||||
filter.apply(self._segments, background), self._cell_length
|
||||
)
|
||||
return cached_strip
|
||||
|
||||
def style_links(self, link_id: str, link_style: Style) -> Strip:
|
||||
"""Apply a style to Segments with the given link_id.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from rich.segment import Segment
|
||||
from rich.style import Style
|
||||
|
||||
from textual.color import Color
|
||||
from textual.filter import DimFilter
|
||||
|
||||
|
||||
@@ -11,7 +12,7 @@ def test_dim_apply():
|
||||
|
||||
segments = [Segment("Hello, World!", Style.parse("dim #ffffff on #0000ff"))]
|
||||
|
||||
dimmed_segments = dim_filter.apply(segments)
|
||||
dimmed_segments = dim_filter.apply(segments, Color(0, 0, 0))
|
||||
|
||||
expected = [Segment("Hello, World!", Style.parse("not dim #7f7fff on #0000ff"))]
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ from rich.segment import Segment
|
||||
from rich.style import Style
|
||||
|
||||
from textual._segment_tools import NoCellPositionForIndex
|
||||
from textual.color import Color
|
||||
from textual.filter import Monochrome
|
||||
from textual.strip import Strip
|
||||
|
||||
@@ -102,7 +103,7 @@ def test_apply_filter():
|
||||
expected = Strip([Segment("foo", Style.parse("#1b1b1b"))])
|
||||
print(repr(strip))
|
||||
print(repr(expected))
|
||||
assert strip.apply_filter(Monochrome()) == expected
|
||||
assert strip.apply_filter(Monochrome(), Color(0, 0, 0)) == expected
|
||||
|
||||
|
||||
def test_style_links():
|
||||
|
||||
Reference in New Issue
Block a user