Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Colors: Add ansi_default and make ansi_* colors actually output their ansi sequences #1460

Closed
wants to merge 10 commits into from
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]

### Added
- Added ansi_default color which returns default terminal color https://github.com/Textualize/textual/pull/1460

### Changed
- ansi_* colors now render simple ANSI sequences https://github.com/Textualize/textual/pull/1460

## [0.9.1] - 2022-12-30

### Added
Expand Down Expand Up @@ -322,6 +330,7 @@ https://textual.textualize.io/blog/2022/11/08/version-040/#version-040
- New handler system for messages that doesn't require inheritance
- Improved traceback handling

[unreleased]: https://github.com/Textualize/textual/compare/v0.9.1...HEAD
[0.9.1]: https://github.com/Textualize/textual/compare/v0.9.0...v0.9.1
[0.9.0]: https://github.com/Textualize/textual/compare/v0.8.2...v0.9.0
[0.8.2]: https://github.com/Textualize/textual/compare/v0.8.1...v0.8.2
Expand Down
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
make:
python -m build --wheel --skip-dependency-check --no-isolation
install:
python -m installer dist/*.whl
test:
pytest --cov-report term-missing --cov=textual tests/ -vv
unit-test:
Expand Down
3 changes: 3 additions & 0 deletions docs/examples/styles/background.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ Static {
#static3 {
background: hsl(240, 100%, 50%);
}
#static4 {
background: ansi_default;
}
1 change: 1 addition & 0 deletions docs/examples/styles/background.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ def compose(self):
yield Static("Widget 1", id="static1")
yield Static("Widget 2", id="static2")
yield Static("Widget 3", id="static3")
yield Static("Widget 4", id="static4")


app = BackgroundApp(css_path="background.css")
3 changes: 3 additions & 0 deletions docs/examples/styles/color.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ Static {
#static3 {
color: hsl(240, 100%, 50%)
}
#static4 {
color: ansi_magenta;
}
1 change: 1 addition & 0 deletions docs/examples/styles/color.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ def compose(self):
yield Static("I'm red!", id="static1")
yield Static("I'm rgb(0, 255, 0)!", id="static2")
yield Static("I'm hsl(240, 100%, 50%)!", id="static3")
yield Static("I'm ansi_magenta!", id="static4")


app = ColorApp(css_path="color.css")
3 changes: 3 additions & 0 deletions docs/styles/background.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ This example creates three widgets and applies a different background to each.
## CSS

```sass
/* Default terminal background */
background: ansi_default;

/* Blue background */
background: blue;

Expand Down
3 changes: 3 additions & 0 deletions docs/styles/color.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ This example sets a different text color to three different widgets.
## CSS

```sass
/* ANSI yellow */
color: ansi_yellow;

/* Blue text */
color: blue;

Expand Down
36 changes: 20 additions & 16 deletions src/textual/_color_constants.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,29 @@
from __future__ import annotations
from math import nan

COLOR_NAME_TO_RGB: dict[str, tuple[int, int, int] | tuple[int, int, int, float]] = {
# Let's start with a specific pseudo-color::
"transparent": (0, 0, 0, 0),
"ansi_default": (1, 1, 1, nan),
# Then, the 16 common ANSI colors:
"ansi_black": (0, 0, 0),
"ansi_red": (128, 0, 0),
"ansi_green": (0, 128, 0),
"ansi_yellow": (128, 128, 0),
"ansi_blue": (0, 0, 128),
"ansi_magenta": (128, 0, 128),
"ansi_cyan": (0, 128, 128),
"ansi_white": (192, 192, 192),
"ansi_bright_black": (128, 128, 128),
"ansi_bright_red": (255, 0, 0),
"ansi_bright_green": (0, 255, 0),
"ansi_bright_yellow": (255, 255, 0),
"ansi_bright_blue": (0, 0, 255),
"ansi_bright_magenta": (255, 0, 255),
"ansi_bright_cyan": (0, 255, 255),
"ansi_bright_white": (255, 255, 255),
# Values from rich._pallettes.STANDARD_PALETTE
# so RichColor.downgrade will return the same value
"ansi_black": (0, 0, 0, nan),
"ansi_red": (170, 0, 0, nan),
"ansi_green": (0, 170, 0, nan),
"ansi_yellow": (170, 85, 0, nan),
"ansi_blue": (0, 0, 170, nan),
"ansi_magenta": (170, 0, 170, nan),
"ansi_cyan": (0, 170, 170, nan),
"ansi_white": (170, 170, 170, nan),
"ansi_bright_black": (85, 85, 85, nan),
"ansi_bright_red": (255, 85, 85, nan),
"ansi_bright_green": (85, 255, 85, nan),
"ansi_bright_yellow": (255, 255, 85, nan),
"ansi_bright_blue": (85, 85, 255, nan),
"ansi_bright_magenta": (255, 85, 255, nan),
"ansi_bright_cyan": (85, 255, 255, nan),
"ansi_bright_white": (255, 255, 255, nan),
# And then, Web color keywords: (up to CSS Color Module Level 4)
"black": (0, 0, 0),
"silver": (192, 192, 192),
Expand Down
25 changes: 20 additions & 5 deletions src/textual/color.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@
from ._color_constants import COLOR_NAME_TO_RGB
from .geometry import clamp

import math

_TRUECOLOR = ColorType.TRUECOLOR


Expand Down Expand Up @@ -145,7 +147,7 @@ class Color(NamedTuple):
b: int
"""Blue component (0-255)"""
a: float = 1.0
"""Alpha component (0-1)"""
"""Alpha component (0-1) or math.nan to represent ANSI color"""

@classmethod
def from_rich_color(cls, rich_color: RichColor) -> Color:
Expand All @@ -158,6 +160,10 @@ def from_rich_color(cls, rich_color: RichColor) -> Color:
Color: A new Color.
"""
r, g, b = rich_color.get_truecolor()
if rich_color.type is ColorType.STANDARD:
return cls(r, g, b, math.nan)
elif rich_color.type is ColorType.DEFAULT:
return cls(1, 1, 1, math.nan)
Comment on lines +163 to +166
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I think this might be the controversial change from this PR, as a lot of rich color names are made into ansi colors)

return cls(r, g, b)

@classmethod
Expand Down Expand Up @@ -225,10 +231,19 @@ def rich_color(self) -> RichColor:
Returns:
RichColor: A color object as used by Rich.
"""
r, g, b, _a = self
return RichColor(
r, g, b, a = self

rc = RichColor(
f"#{r:02x}{g:02x}{b:02x}", _TRUECOLOR, None, ColorTriplet(r, g, b)
)
if a is math.nan:
# ANSI raw colors
if r == 1 and g == 1 and b == 1:
return RichColor.default() # ansi_default
# "Downgrade" color to closest ansi 16
return RichColor.downgrade(rc, ColorType.STANDARD)

return rc

@property
def normalized(self) -> tuple[float, float, float]:
Expand Down Expand Up @@ -348,15 +363,15 @@ def blend(

Args:
destination (Color): Another color.
factor (float): A blend factor, 0 -> 1.
factor (float): A blend factor, 0 -> 1, or math.nan for 1.
alpha (float | None): New alpha for result. Defaults to None.

Returns:
Color: A new color.
"""
if factor == 0:
return self
elif factor == 1:
elif factor == 1 or factor is math.nan:
return destination
r1, g1, b1, a1 = self
r2, g2, b2, a2 = destination
Expand Down
4 changes: 3 additions & 1 deletion src/textual/geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ def clamp(value: T, minimum: T, maximum: T) -> T:
maximum (T): maximum value.

Returns:
T: New value that is not less than the minimum or greater than the maximum.
T: New value that is not less than the minimum or greater than the maximum,
or the original value if that cannot be satisfied.
"""
if minimum > maximum:
maximum, minimum = minimum, maximum
Expand All @@ -38,6 +39,7 @@ def clamp(value: T, minimum: T, maximum: T) -> T:
elif value > maximum:
return maximum
else:
# math.nan is an example value which is incomparable
return value


Expand Down
5 changes: 3 additions & 2 deletions tests/css/test_stylesheet.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from contextlib import nullcontext as does_not_raise
from typing import Any

import math
import pytest

from textual.color import Color
Expand Down Expand Up @@ -130,8 +131,8 @@ class MyWidget(Widget):
[
# Valid values:
["transparent", does_not_raise(), Color(0, 0, 0, 0)],
["ansi_red", does_not_raise(), Color(128, 0, 0)],
["ansi_bright_magenta", does_not_raise(), Color(255, 0, 255)],
["ansi_red", does_not_raise(), Color(170, 0, 0, math.nan)],
["ansi_bright_magenta", does_not_raise(), Color(255, 85, 255, math.nan)],
["red", does_not_raise(), Color(255, 0, 0)],
["lime", does_not_raise(), Color(0, 255, 0)],
["coral", does_not_raise(), Color(255, 127, 80)],
Expand Down
Loading