Skip to content

Commit

Permalink
implement dim fix (Textualize#2326)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

* Update src/textual/constants.py

Co-authored-by: Rodrigo Girão Serrão <[email protected]>

---------

Co-authored-by: Rodrigo Girão Serrão <[email protected]>
  • Loading branch information
willmcgugan and rodrigogiraoserrao authored Apr 19, 2023
1 parent 7c5203a commit 81882fd
Show file tree
Hide file tree
Showing 7 changed files with 116 additions and 22 deletions.
13 changes: 7 additions & 6 deletions src/textual/_styles_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

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
Expand Down Expand Up @@ -139,7 +139,7 @@ def render_widget(self, widget: Widget, crop: Region) -> list[Strip]:
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
Expand Down Expand Up @@ -170,7 +170,7 @@ def render(
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.
Expand All @@ -186,7 +186,7 @@ def render(
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.
Expand Down Expand Up @@ -225,8 +225,9 @@ def render(
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)

Expand Down
14 changes: 10 additions & 4 deletions src/textual/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
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,
Expand All @@ -53,6 +52,7 @@

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
Expand Down Expand Up @@ -81,7 +81,7 @@
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,
Expand Down Expand Up @@ -260,11 +260,17 @@ def __init__(
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(),
Expand Down
3 changes: 3 additions & 0 deletions src/textual/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ def get_environ_int(name: str, default: int) -> int:

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

Expand Down
90 changes: 82 additions & 8 deletions src/textual/filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
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

Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -126,11 +142,12 @@ def __init__(self, dim_factor: float = 0.5) -> None:
"""
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.
Expand All @@ -143,11 +160,68 @@ def apply(self, segments: list[Segment]) -> list[Segment]:
(
_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
else segment
)
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
]
12 changes: 10 additions & 2 deletions src/textual/strip.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

from ._cache import FIFOCache
from ._segment_tools import index_to_cell_position
from .color import Color
from .constants import DEBUG
from .filter import LineFilter

Expand Down Expand Up @@ -67,6 +68,7 @@ class Strip:
"_divide_cache",
"_crop_cache",
"_style_cache",
"_filter_cache",
"_render_cache",
"_link_ids",
]
Expand All @@ -79,6 +81,7 @@ def __init__(
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

Expand Down Expand Up @@ -265,7 +268,7 @@ def simplify(self) -> 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:
Expand All @@ -274,7 +277,12 @@ def apply_filter(self, filter: LineFilter) -> 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.
Expand Down
3 changes: 2 additions & 1 deletion tests/test_line_filter.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from rich.segment import Segment
from rich.style import Style

from textual.color import Color
from textual.filter import DimFilter


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

Expand Down
3 changes: 2 additions & 1 deletion tests/test_strip.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
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

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

0 comments on commit 81882fd

Please sign in to comment.