diff --git a/src/textual/_styles_cache.py b/src/textual/_styles_cache.py index a14877ac7..ae795a12e 100644 --- a/src/textual/_styles_cache.py +++ b/src/textual/_styles_cache.py @@ -7,6 +7,7 @@ from rich.segment import Segment from rich.style import Style from ._border import get_box, render_row +from ._filter import LineFilter from ._opacity import _apply_opacity from ._segment_tools import line_crop, line_pad, line_trim from ._types import Lines @@ -134,6 +135,7 @@ class StylesCache: content_size=widget.content_region.size, padding=styles.padding, crop=crop, + filter=widget.app._filter, ) if widget.auto_links: _style_links = style_links @@ -163,6 +165,7 @@ class StylesCache: content_size: Size | None = None, padding: Spacing | None = None, crop: Region | None = None, + filter: LineFilter | None = None, ) -> Lines: """Render a widget content plus CSS styles. @@ -212,6 +215,8 @@ class StylesCache: self._cache[y] = line else: line = self._cache[y] + if filter: + line = filter.filter(line) add_line(line) self._dirty_lines.difference_update(crop.line_range) diff --git a/src/textual/app.py b/src/textual/app.py index 9dcb85839..12c60e3b1 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -35,6 +35,7 @@ from ._animator import Animator, DEFAULT_EASING, Animatable, EasingFunction from ._callback import invoke from ._context import active_app from ._event_broker import NoHandler, extract_handler_actions +from ._filter import LineFilter, Monochrome from .binding import Bindings, NoBinding from .css.query import NoMatchingNodesError from .css.stylesheet import Stylesheet @@ -163,12 +164,18 @@ class App(Generic[ReturnType], DOMNode): super().__init__() self.features: frozenset[FeatureFlag] = parse_features(os.getenv("TEXTUAL", "")) + self._filter: LineFilter | None = None + environ = dict(os.environ) + no_color = environ.pop("NO_COLOR", None) + if no_color is not None: + self._filter = Monochrome() self.console = Console( file=(_NullFile() if self.is_headless else sys.__stdout__), markup=False, highlight=False, emoji=False, legacy_windows=False, + _environ=environ, ) self.error_console = Console(markup=False, stderr=True) self.driver_class = driver_class or self.get_driver_class() diff --git a/src/textual/color.py b/src/textual/color.py index ea2c9c768..d2645b8b4 100644 --- a/src/textual/color.py +++ b/src/textual/color.py @@ -311,6 +311,17 @@ class Color(NamedTuple): r, g, b, a = self return f"rgb({r},{g},{b})" if a == 1 else f"rgba({r},{g},{b},{a})" + @property + def monochrome(self) -> Color: + """Get a monochrome version of this color. + + Returns: + Color: A new monochrome color. + """ + r, g, b, a = self + gray = round(r * 0.2126 + g * 0.7152 + b * 0.0722) + return Color(gray, gray, gray, a) + def __rich_repr__(self) -> rich.repr.Result: r, g, b, a = self yield r diff --git a/tests/test_color.py b/tests/test_color.py index 105eb8e94..10340d3dc 100644 --- a/tests/test_color.py +++ b/tests/test_color.py @@ -31,6 +31,13 @@ def test_css(): assert Color(10, 20, 30, 0.5).css == "rgba(10,20,30,0.5)" +def test_monochrome(): + assert Color(10, 20, 30).monochrome == Color(19, 19, 19) + assert Color(10, 20, 30, 0.5).monochrome == Color(19, 19, 19, 0.5) + assert Color(255, 255, 255).monochrome == Color(255, 255, 255) + assert Color(0, 0, 0).monochrome == Color(0, 0, 0) + + def test_rgb(): assert Color(10, 20, 30, 0.55).rgb == (10, 20, 30)