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:
Will McGugan
2023-04-19 13:24:31 +01:00
committed by GitHub
parent 7c5203aa1b
commit 81882fdf7d
7 changed files with 116 additions and 22 deletions

View File

@@ -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)

View File

@@ -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(),

View File

@@ -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."""

View File

@@ -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
]

View File

@@ -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.

View File

@@ -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"))]

View File

@@ -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():