diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0ee7b9a673..209b7940fd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
@@ -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
diff --git a/Makefile b/Makefile
index 9d71e69d73..e06381a19a 100644
--- a/Makefile
+++ b/Makefile
@@ -1,3 +1,10 @@
+make:
+ python -m build --wheel --skip-dependency-check --no-isolation
+install: uninstall
+ python -m installer dist/*.whl
+uninstall:
+ rm -rf /usr/bin/textual
+ rm -i -rf /usr/lib/python3.*/site-packages/textual*
test:
pytest --cov-report term-missing --cov=textual tests/ -vv
unit-test:
diff --git a/docs/examples/styles/background.css b/docs/examples/styles/background.css
index 27f8649d29..9f7472758f 100644
--- a/docs/examples/styles/background.css
+++ b/docs/examples/styles/background.css
@@ -12,3 +12,6 @@ Static {
#static3 {
background: hsl(240, 100%, 50%);
}
+#static4 {
+ background: ansi_default;
+}
diff --git a/docs/examples/styles/background.py b/docs/examples/styles/background.py
index cef306ddcc..8a8ce88223 100644
--- a/docs/examples/styles/background.py
+++ b/docs/examples/styles/background.py
@@ -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")
diff --git a/docs/examples/styles/color.css b/docs/examples/styles/color.css
index b5552495ae..65d0ff0cae 100644
--- a/docs/examples/styles/color.css
+++ b/docs/examples/styles/color.css
@@ -11,3 +11,6 @@ Static {
#static3 {
color: hsl(240, 100%, 50%)
}
+#static4 {
+ color: ansi_magenta;
+}
diff --git a/docs/examples/styles/color.py b/docs/examples/styles/color.py
index 26543b0a04..e571ee0b02 100644
--- a/docs/examples/styles/color.py
+++ b/docs/examples/styles/color.py
@@ -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")
diff --git a/docs/styles/background.md b/docs/styles/background.md
index 6c41885c77..0a67c773ea 100644
--- a/docs/styles/background.md
+++ b/docs/styles/background.md
@@ -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;
diff --git a/docs/styles/color.md b/docs/styles/color.md
index 0fae0a0b0f..c56efa5604 100644
--- a/docs/styles/color.md
+++ b/docs/styles/color.md
@@ -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;
diff --git a/src/textual/_border.py b/src/textual/_border.py
index 4307281bcd..8591e11065 100644
--- a/src/textual/_border.py
+++ b/src/textual/_border.py
@@ -32,8 +32,8 @@
"outer": ("▛▀▜", "▌ ▐", "▙▄▟"),
"hkey": ("▔▔▔", " ", "▁▁▁"),
"vkey": ("▏ ▕", "▏ ▕", "▏ ▕"),
- "tall": ("▊▔▎", "▊ ▎", "▊▁▎"),
- "wide": ("▁▁▁", "▎ ▋", "▔▔▔"),
+ "tall": ("▐▔▌", "▐ ▌", "▐▁▌"),
+ "wide": ("▁▁▁", "▌ ▐", "▔▔▔"),
}
# Some of the borders are on the widget background and some are on the background of the parent
@@ -55,8 +55,8 @@
"outer": ((0, 0, 0), (0, 0, 0), (0, 0, 0)),
"hkey": ((0, 0, 0), (0, 0, 0), (0, 0, 0)),
"vkey": ((0, 0, 0), (0, 0, 0), (0, 0, 0)),
- "tall": ((2, 0, 1), (2, 0, 1), (2, 0, 1)),
- "wide": ((1, 1, 1), (0, 1, 3), (1, 1, 1)),
+ "tall": ((1, 0, 1), (1, 0, 1), (1, 0, 1)),
+ "wide": ((1, 1, 1), (0, 1, 0), (1, 1, 1)),
}
INVISIBLE_EDGE_TYPES = cast("frozenset[EdgeType]", frozenset(("", "none", "hidden")))
@@ -108,8 +108,6 @@ def get_box(
styles = (
inner,
outer,
- Style.from_color(outer.bgcolor, inner.color),
- Style.from_color(inner.bgcolor, outer.color),
)
return (
diff --git a/src/textual/_color_constants.py b/src/textual/_color_constants.py
index cedab56736..7a82c60a20 100644
--- a/src/textual/_color_constants.py
+++ b/src/textual/_color_constants.py
@@ -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),
diff --git a/src/textual/color.py b/src/textual/color.py
index a53a0abafc..5e54b0f6c3 100644
--- a/src/textual/color.py
+++ b/src/textual/color.py
@@ -54,6 +54,8 @@
from ._color_constants import COLOR_NAME_TO_RGB
from .geometry import clamp
+import math
+
_TRUECOLOR = ColorType.TRUECOLOR
@@ -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:
@@ -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)
return cls(r, g, b)
@classmethod
@@ -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]:
@@ -348,7 +363,7 @@ 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:
@@ -356,7 +371,7 @@ def blend(
"""
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
diff --git a/src/textual/geometry.py b/src/textual/geometry.py
index e29790e304..09eecf9efe 100644
--- a/src/textual/geometry.py
+++ b/src/textual/geometry.py
@@ -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
@@ -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
diff --git a/src/textual/scrollbar.py b/src/textual/scrollbar.py
index 917e36a69e..3851c087f8 100644
--- a/src/textual/scrollbar.py
+++ b/src/textual/scrollbar.py
@@ -93,20 +93,24 @@ def render_bar(
) -> Segments:
if vertical:
- bars = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", " "]
+ startbars = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"]
+ endbars = ["🮆", "🮅", "🮄", "▀", "🮃", "🮂", "▔", " "]
else:
- bars = ["▉", "▊", "▋", "▌", "▍", "▎", "▏", " "]
+ startbars = ["▕", "🮇", "🮈", "▐", "🮉", "🮊", "🮋", "█"]
+ endbars = ["▉", "▊", "▋", "▌", "▍", "▎", "▏", " "]
back = back_color
bar = bar_color
- len_bars = len(bars)
+ # TODO: allow variable len_bars for start/end
+ len_bars = len(startbars)
width_thickness = thickness if vertical else 1
_Segment = Segment
_Style = Style
- blank = " " * width_thickness
+ blank = "█" * width_thickness
+ space = " " * width_thickness
foreground_meta = {"@mouse.up": "release", "@mouse.down": "grab"}
if window_size and size and virtual_size and size != virtual_size:
@@ -121,38 +125,32 @@ def render_bar(
upper = {"@mouse.up": "scroll_up"}
lower = {"@mouse.up": "scroll_down"}
- upper_back_segment = Segment(blank, _Style(bgcolor=back, meta=upper))
- lower_back_segment = Segment(blank, _Style(bgcolor=back, meta=lower))
+ upper_back_segment = Segment(space, _Style(bgcolor=back, meta=upper))
+ lower_back_segment = Segment(space, _Style(bgcolor=back, meta=lower))
segments = [upper_back_segment] * int(size)
segments[end_index:] = [lower_back_segment] * (size - end_index)
segments[start_index:end_index] = [
- _Segment(blank, _Style(bgcolor=bar, meta=foreground_meta))
+ _Segment(blank, _Style(color=bar, meta=foreground_meta))
] * (end_index - start_index)
# Apply the smaller bar characters to head and tail of scrollbar for more "granularity"
if start_index < len(segments):
- bar_character = bars[len_bars - 1 - start_bar]
- if bar_character != " ":
- segments[start_index] = _Segment(
- bar_character * width_thickness,
- _Style(bgcolor=back, color=bar, meta=foreground_meta)
- if vertical
- else _Style(bgcolor=bar, color=back, meta=foreground_meta),
- )
+ bar_character = startbars[len_bars - 1 - start_bar]
+ segments[start_index] = _Segment(
+ bar_character * width_thickness,
+ _Style(bgcolor=back, color=bar, meta=foreground_meta)
+ )
if end_index < len(segments):
- bar_character = bars[len_bars - 1 - end_bar]
- if bar_character != " ":
- segments[end_index] = _Segment(
- bar_character * width_thickness,
- _Style(bgcolor=bar, color=back, meta=foreground_meta)
- if vertical
- else _Style(bgcolor=back, color=bar, meta=foreground_meta),
- )
+ bar_character = endbars[len_bars - 1 - end_bar]
+ segments[end_index] = _Segment(
+ bar_character * width_thickness,
+ _Style(bgcolor=back, color=bar, meta=foreground_meta)
+ )
else:
style = _Style(bgcolor=back)
- segments = [_Segment(blank, style=style)] * int(size)
+ segments = [_Segment(space, style=style)] * int(size)
if vertical:
return Segments(segments, new_lines=True)
else:
diff --git a/tests/css/test_stylesheet.py b/tests/css/test_stylesheet.py
index be54fe936a..60dd61e8ee 100644
--- a/tests/css/test_stylesheet.py
+++ b/tests/css/test_stylesheet.py
@@ -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
@@ -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)],
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
index 4a9464a814..fbf9ae01bd 100644
--- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
@@ -682,131 +682,131 @@
font-weight: 700;
}
- .terminal-470351640-matrix {
+ .terminal-3110661383-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
- .terminal-470351640-title {
+ .terminal-3110661383-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
- .terminal-470351640-r1 { fill: #c5c8c6 }
- .terminal-470351640-r2 { fill: #ffffff }
+ .terminal-3110661383-r1 { fill: #c5c8c6 }
+ .terminal-3110661383-r2 { fill: #ffffff }
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
- BackgroundApp
+ BackgroundApp
-
-
-
-
-
-
- Widget 1
-
-
-
-
-
-
-
- Widget 2
-
-
-
-
-
-
-
- Widget 3
-
-
-
+
+
+
+
+
+ Widget 1
+
+
+
+
+
+ Widget 2
+
+
+
+
+
+ Widget 3
+
+
+
+
+
+ Widget 4
+
+
@@ -1151,133 +1151,134 @@
font-weight: 700;
}
- .terminal-830407627-matrix {
+ .terminal-2794611268-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
- .terminal-830407627-title {
+ .terminal-2794611268-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
- .terminal-830407627-r1 { fill: #c5c8c6 }
- .terminal-830407627-r2 { fill: #ff0000 }
- .terminal-830407627-r3 { fill: #00ff00 }
- .terminal-830407627-r4 { fill: #0000ff }
+ .terminal-2794611268-r1 { fill: #c5c8c6 }
+ .terminal-2794611268-r2 { fill: #ff0000 }
+ .terminal-2794611268-r3 { fill: #00ff00 }
+ .terminal-2794611268-r4 { fill: #0000ff }
+ .terminal-2794611268-r5 { fill: #98729f }
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
- ColorApp
+ ColorApp
-
-
-
-
-
-
- I'm red!
-
-
-
-
-
-
-
- I'm rgb(0, 255, 0)!
-
-
-
-
-
-
-
- I'm hsl(240, 100%, 50%)!
-
-
-
+
+
+
+
+
+ I'm red!
+
+
+
+
+
+ I'm rgb(0, 255, 0)!
+
+
+
+
+
+ I'm hsl(240, 100%, 50%)!
+
+
+
+
+
+ I'm ansi_magenta!
+
+
diff --git a/tests/test_color.py b/tests/test_color.py
index 10340d3dc9..25332e0ca4 100644
--- a/tests/test_color.py
+++ b/tests/test_color.py
@@ -4,13 +4,22 @@
from textual.color import Color, Lab, lab_to_rgb, rgb_to_lab
-
def test_rich_color():
"""Check conversion to Rich color."""
assert Color(10, 20, 30, 1.0).rich_color == RichColor.from_rgb(10, 20, 30)
assert Color.from_rich_color(RichColor.from_rgb(10, 20, 30)) == Color(
10, 20, 30, 1.0
)
+ assert Color.parse("ansi_default").rich_color == RichColor.default()
+ assert Color.parse("ansi_black").rich_color.number == 0
+ assert Color.parse("ansi_red").rich_color.number == 1
+ assert Color.parse("ansi_white").rich_color.number == 7
+ assert Color.parse("ansi_bright_black").rich_color.number == 8
+ assert Color.parse("ansi_bright_red").rich_color.number == 9
+ assert Color.parse("ansi_bright_white").rich_color.number == 15
+ # Round-trip
+ assert Color.from_rich_color(RichColor.default()).rich_color == RichColor.default()
+ assert Color.from_rich_color(RichColor.from_ansi(3)).rich_color.number == 3
def test_rich_color_rich_output():
diff --git a/tests/test_strip.py b/tests/test_strip.py
index 891af9845e..24a970ebb9 100644
--- a/tests/test_strip.py
+++ b/tests/test_strip.py
@@ -90,7 +90,7 @@ def test_simplify():
def test_apply_filter():
- strip = Strip([Segment("foo", Style.parse("red"))])
+ strip = Strip([Segment("foo", Style.parse("#800000"))])
expected = Strip([Segment("foo", Style.parse("#1b1b1b"))])
print(repr(strip))
print(repr(expected))