diff --git a/src/prompt_toolkit/formatted_text/ansi.py b/src/prompt_toolkit/formatted_text/ansi.py index 4761982ab..8cc37a636 100644 --- a/src/prompt_toolkit/formatted_text/ansi.py +++ b/src/prompt_toolkit/formatted_text/ansi.py @@ -37,6 +37,7 @@ def __init__(self, value: str) -> None: self._color: str | None = None self._bgcolor: str | None = None self._bold = False + self._dim = False self._underline = False self._strike = False self._italic = False @@ -153,8 +154,8 @@ def _select_graphic_rendition(self, attrs: list[int]) -> None: self._bgcolor = _bg_colors[attr] elif attr == 1: self._bold = True - # elif attr == 2: - # self._faint = True + elif attr == 2: + self._dim = True elif attr == 3: self._italic = True elif attr == 4: @@ -171,6 +172,7 @@ def _select_graphic_rendition(self, attrs: list[int]) -> None: self._strike = True elif attr == 22: self._bold = False # Normal intensity + self._dim = False elif attr == 23: self._italic = False elif attr == 24: @@ -188,6 +190,7 @@ def _select_graphic_rendition(self, attrs: list[int]) -> None: self._color = None self._bgcolor = None self._bold = False + self._dim = False self._underline = False self._strike = False self._italic = False @@ -232,6 +235,8 @@ def _create_style_string(self) -> str: result.append("bg:" + self._bgcolor) if self._bold: result.append("bold") + if self._dim: + result.append("dim") if self._underline: result.append("underline") if self._strike: diff --git a/src/prompt_toolkit/output/vt100.py b/src/prompt_toolkit/output/vt100.py index 90df21e55..57826b940 100644 --- a/src/prompt_toolkit/output/vt100.py +++ b/src/prompt_toolkit/output/vt100.py @@ -257,7 +257,7 @@ def __missing__(self, value: tuple[int, int, int]) -> int: class _EscapeCodeCache(Dict[Attrs, str]): """ Cache for VT100 escape codes. It maps - (fgcolor, bgcolor, bold, underline, strike, reverse) tuples to VT100 + (fgcolor, bgcolor, bold, underline, strike, italic, blink, reverse, hidden, dim) tuples to VT100 escape sequences. :param true_color: When True, use 24bit colors instead of 256 colors. @@ -277,6 +277,7 @@ def __missing__(self, attrs: Attrs) -> str: blink, reverse, hidden, + dim, ) = attrs parts: list[str] = [] @@ -284,6 +285,8 @@ def __missing__(self, attrs: Attrs) -> str: if bold: parts.append("1") + if dim: + parts.append("2") if italic: parts.append("3") if blink: diff --git a/src/prompt_toolkit/output/win32.py b/src/prompt_toolkit/output/win32.py index 83ccea43f..1c0cc4eec 100644 --- a/src/prompt_toolkit/output/win32.py +++ b/src/prompt_toolkit/output/win32.py @@ -293,6 +293,7 @@ def set_attributes(self, attrs: Attrs, color_depth: ColorDepth) -> None: blink, reverse, hidden, + dim, ) = attrs self._hidden = bool(hidden) diff --git a/src/prompt_toolkit/styles/base.py b/src/prompt_toolkit/styles/base.py index 06572210a..59776cdac 100644 --- a/src/prompt_toolkit/styles/base.py +++ b/src/prompt_toolkit/styles/base.py @@ -29,6 +29,7 @@ class Attrs(NamedTuple): blink: bool | None reverse: bool | None hidden: bool | None + dim: bool | None """ @@ -41,6 +42,7 @@ class Attrs(NamedTuple): :param blink: Boolean :param reverse: Boolean :param hidden: Boolean +:param dim: Boolean """ #: The default `Attrs`. @@ -54,6 +56,7 @@ class Attrs(NamedTuple): blink=False, reverse=False, hidden=False, + dim=False, ) diff --git a/src/prompt_toolkit/styles/style.py b/src/prompt_toolkit/styles/style.py index fc8b31bd0..8aab8e1b2 100644 --- a/src/prompt_toolkit/styles/style.py +++ b/src/prompt_toolkit/styles/style.py @@ -88,6 +88,7 @@ def parse_color(text: str) -> str: blink=None, reverse=None, hidden=None, + dim=None, ) @@ -151,6 +152,10 @@ def _parse_style_str(style_str: str) -> Attrs: attrs = attrs._replace(hidden=True) elif part == "nohidden": attrs = attrs._replace(hidden=False) + elif part == "dim": + attrs = attrs._replace(dim=True) + elif part == "nodim": + attrs = attrs._replace(dim=False) # Pygments properties that we ignore. elif part in ("roman", "sans", "mono"): @@ -345,6 +350,7 @@ def _or(*values: _T) -> _T: blink=_or(False, *[a.blink for a in list_of_attrs]), reverse=_or(False, *[a.reverse for a in list_of_attrs]), hidden=_or(False, *[a.hidden for a in list_of_attrs]), + dim=_or(False, *[a.dim for a in list_of_attrs]), ) diff --git a/tests/test_formatted_text.py b/tests/test_formatted_text.py index 60f9cdf45..a111a7f20 100644 --- a/tests/test_formatted_text.py +++ b/tests/test_formatted_text.py @@ -89,6 +89,46 @@ def test_ansi_formatting(): assert isinstance(to_formatted_text(value), FormattedText) +def test_ansi_dim(): + # Test dim formatting + value = ANSI("\x1b[2mhello\x1b[0m") + + assert to_formatted_text(value) == [ + ("dim", "h"), + ("dim", "e"), + ("dim", "l"), + ("dim", "l"), + ("dim", "o"), + ] + + # Test dim with other attributes + value = ANSI("\x1b[1;2;31mhello\x1b[0m") + + assert to_formatted_text(value) == [ + ("ansired bold dim", "h"), + ("ansired bold dim", "e"), + ("ansired bold dim", "l"), + ("ansired bold dim", "l"), + ("ansired bold dim", "o"), + ] + + # Test dim reset with code 22 + value = ANSI("\x1b[1;2mhello\x1b[22mworld\x1b[0m") + + assert to_formatted_text(value) == [ + ("bold dim", "h"), + ("bold dim", "e"), + ("bold dim", "l"), + ("bold dim", "l"), + ("bold dim", "o"), + ("", "w"), + ("", "o"), + ("", "r"), + ("", "l"), + ("", "d"), + ] + + def test_ansi_256_color(): assert to_formatted_text(ANSI("\x1b[38;5;124mtest")) == [ ("#af0000", "t"), diff --git a/tests/test_print_formatted_text.py b/tests/test_print_formatted_text.py index 7d0e99a33..3c6924401 100644 --- a/tests/test_print_formatted_text.py +++ b/tests/test_print_formatted_text.py @@ -91,3 +91,22 @@ def test_html_with_style(): f.data == "\x1b[0m\x1b[?7h\x1b[0;32mhello\x1b[0m \x1b[0;1mworld\x1b[0m\r\n\x1b[0m" ) + + +@pytest.mark.skipif(is_windows(), reason="Doesn't run on Windows yet.") +def test_print_formatted_text_with_dim(): + """ + Test that dim formatting works correctly. + """ + f = _Capture() + style = Style.from_dict( + { + "dimtext": "dim", + } + ) + tokens = FormattedText([("class:dimtext", "dim text")]) + + pt_print(tokens, style=style, file=f, color_depth=ColorDepth.DEFAULT) + + # Check that the ANSI dim escape code (ESC[2m) is in the output + assert "\x1b[0;2m" in f.data or "\x1b[2m" in f.data diff --git a/tests/test_style.py b/tests/test_style.py index d0a4790b8..ad9b90eab 100644 --- a/tests/test_style.py +++ b/tests/test_style.py @@ -22,6 +22,7 @@ def test_style_from_dict(): blink=False, reverse=False, hidden=False, + dim=False, ) assert style.get_attrs_for_style_str("class:a") == expected @@ -36,6 +37,7 @@ def test_style_from_dict(): blink=True, reverse=True, hidden=False, + dim=False, ) assert style.get_attrs_for_style_str("class:b") == expected @@ -50,6 +52,7 @@ def test_style_from_dict(): blink=False, reverse=False, hidden=False, + dim=False, ) assert style.get_attrs_for_style_str("#ff0000") == expected @@ -64,6 +67,7 @@ def test_style_from_dict(): blink=False, reverse=False, hidden=False, + dim=False, ) assert style.get_attrs_for_style_str("class:a #00ff00") == expected @@ -77,6 +81,7 @@ def test_style_from_dict(): blink=False, reverse=False, hidden=False, + dim=False, ) assert style.get_attrs_for_style_str("#00ff00 class:a") == expected @@ -101,6 +106,7 @@ def test_class_combinations_1(): blink=False, reverse=False, hidden=False, + dim=False, ) assert style.get_attrs_for_style_str("class:a class:b") == expected assert style.get_attrs_for_style_str("class:a,b") == expected @@ -131,6 +137,7 @@ def test_class_combinations_2(): blink=False, reverse=False, hidden=False, + dim=False, ) assert style.get_attrs_for_style_str("class:a class:b") == expected assert style.get_attrs_for_style_str("class:a,b") == expected @@ -147,6 +154,7 @@ def test_class_combinations_2(): blink=False, reverse=False, hidden=False, + dim=False, ) assert style.get_attrs_for_style_str("class:b class:a") == expected assert style.get_attrs_for_style_str("class:b,a") == expected @@ -173,6 +181,7 @@ def test_substyles(): blink=False, reverse=False, hidden=False, + dim=False, ) assert style.get_attrs_for_style_str("class:a") == expected @@ -186,6 +195,7 @@ def test_substyles(): blink=False, reverse=False, hidden=False, + dim=False, ) assert style.get_attrs_for_style_str("class:a.b") == expected assert style.get_attrs_for_style_str("class:a.b.c") == expected @@ -201,6 +211,7 @@ def test_substyles(): blink=False, reverse=False, hidden=False, + dim=False, ) assert style.get_attrs_for_style_str("class:b") == expected assert style.get_attrs_for_style_str("class:b.a") == expected @@ -215,6 +226,7 @@ def test_substyles(): blink=False, reverse=False, hidden=False, + dim=False, ) assert style.get_attrs_for_style_str("class:b.c") == expected assert style.get_attrs_for_style_str("class:b.c.d") == expected @@ -234,6 +246,7 @@ def test_swap_light_and_dark_style_transformation(): blink=False, reverse=False, hidden=False, + dim=False, ) after = Attrs( color="ffbbbb", @@ -245,6 +258,7 @@ def test_swap_light_and_dark_style_transformation(): blink=False, reverse=False, hidden=False, + dim=False, ) assert transformation.transform_attrs(before) == after @@ -260,6 +274,7 @@ def test_swap_light_and_dark_style_transformation(): blink=False, reverse=False, hidden=False, + dim=False, ) after = Attrs( color="ansibrightred", @@ -271,6 +286,7 @@ def test_swap_light_and_dark_style_transformation(): blink=False, reverse=False, hidden=False, + dim=False, ) assert transformation.transform_attrs(before) == after diff --git a/tests/test_style_transformation.py b/tests/test_style_transformation.py index e4eee7c7b..0651e7fa8 100644 --- a/tests/test_style_transformation.py +++ b/tests/test_style_transformation.py @@ -17,6 +17,7 @@ def default_attrs(): blink=False, reverse=False, hidden=False, + dim=False, )