Skip to content

Commit 339b73d

Browse files
committed
Added rich_to_pt_color conversion helper function
Created a new converter function to safely extract the correct string representation for prompt-toolkit: - For `ColorSystem.STANDARD` colors (0-15), it maps directly to `prompt-toolkit`'s `ansicolor` names (e.g. `ansired`, `ansibrightred`). This ensures that `prompt-toolkit` delegates to the terminal's theme for the basic 16 colors. - For `ColorSystem.EIGHT_BIT` and `ColorSystem.TRUECOLOR`, it falls back to parsing the RGB hex code (e.g., `#ff0000`), which `prompt-toolkit` supports natively. Also: - Renamed `to_pt_style` to `rich_to_pt_style` to make its intent more obvious - Added tests for the new `rich_to_pt_color` conversion function
1 parent a6927c3 commit 339b73d

4 files changed

Lines changed: 110 additions & 37 deletions

File tree

cmd2/cmd2.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ def __init__(self, msg: str = "") -> None:
192192
Cmd2History,
193193
Cmd2Lexer,
194194
pt_filter_style,
195-
to_pt_style,
195+
rich_to_pt_style,
196196
)
197197
from .utils import (
198198
Settable,
@@ -731,8 +731,8 @@ def _get_pt_style(self) -> "PtStyle":
731731
if self._cached_pt_style is not None and self._cached_pt_style_params == current_params:
732732
return self._cached_pt_style
733733

734-
item_style = to_pt_style(rich_item_style)
735-
meta_style = to_pt_style(rich_meta_style)
734+
item_style = rich_to_pt_style(rich_item_style)
735+
meta_style = rich_to_pt_style(rich_meta_style)
736736

737737
self._cached_pt_style_params = current_params
738738
self._cached_pt_style = PtStyle.from_dict(

cmd2/pt_utils.py

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
from . import string_utils as su
3131

3232
if TYPE_CHECKING: # pragma: no cover
33+
from rich.color import Color
34+
3335
from .cmd2 import Cmd
3436

3537

@@ -51,22 +53,51 @@ def pt_filter_style(text: str | ANSI) -> str | ANSI:
5153
return text if isinstance(text, ANSI) else ANSI(text)
5254

5355

54-
def to_pt_style(rich_style: Style | None) -> str:
56+
def rich_to_pt_color(color: "Color | None") -> str:
57+
"""Convert a rich Color object to a prompt_toolkit color string."""
58+
if not color or color.is_default:
59+
return "default"
60+
61+
# Use prompt_toolkit's 16 standard ansi color names if applicable.
62+
# This prevents overriding terminal themes with absolute RGB values.
63+
if color.number is not None and 0 <= color.number <= 15:
64+
# prompt_toolkit accepts these standard names directly
65+
ansi_names = [
66+
"ansiblack",
67+
"ansired",
68+
"ansigreen",
69+
"ansiyellow",
70+
"ansiblue",
71+
"ansimagenta",
72+
"ansicyan",
73+
"ansiwhite",
74+
"ansibrightblack",
75+
"ansibrightred",
76+
"ansibrightgreen",
77+
"ansibrightyellow",
78+
"ansibrightblue",
79+
"ansibrightmagenta",
80+
"ansibrightcyan",
81+
"ansibrightwhite",
82+
]
83+
return ansi_names[color.number]
84+
85+
# For 8-bit and truecolor, we fallback to hex RGB strings which prompt-toolkit supports natively
86+
c = color.get_truecolor()
87+
return f"#{c.red:02x}{c.green:02x}{c.blue:02x}"
88+
89+
90+
def rich_to_pt_style(rich_style: Style | None) -> str:
5591
"""Convert a rich Style object to a prompt_toolkit style string."""
5692
if not rich_style:
5793
return ""
5894
parts = ["noreverse"]
59-
if rich_style.color and not rich_style.color.is_default:
60-
c = rich_style.color.get_truecolor()
61-
parts.append(f"fg:#{c.red:02x}{c.green:02x}{c.blue:02x}")
62-
else:
63-
parts.append("fg:default")
64-
65-
if rich_style.bgcolor and not rich_style.bgcolor.is_default:
66-
c = rich_style.bgcolor.get_truecolor()
67-
parts.append(f"bg:#{c.red:02x}{c.green:02x}{c.blue:02x}")
68-
else:
69-
parts.append("bg:default")
95+
96+
fg_color = rich_to_pt_color(rich_style.color)
97+
parts.append(f"fg:{fg_color}")
98+
99+
bg_color = rich_to_pt_color(rich_style.bgcolor)
100+
parts.append(f"bg:{bg_color}")
70101

71102
if rich_style.bold is not None:
72103
parts.append("bold" if rich_style.bold else "nobold")

tests/test_cmd2.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1922,7 +1922,13 @@ def test_get_pt_style_caching(base_app) -> None:
19221922
attrs = style3.class_names_and_attrs
19231923
found = False
19241924
for classes, attr in attrs:
1925-
if "completion-menu.completion.current" in classes and attr.color in ("800000", "darkred", "ff0000", "#800000"):
1925+
if "completion-menu.completion.current" in classes and attr.color in (
1926+
"800000",
1927+
"darkred",
1928+
"ff0000",
1929+
"#800000",
1930+
"ansired",
1931+
):
19261932
found = True
19271933
break
19281934
assert found, "Color change not found in cached style"

tests/test_pt_utils.py

Lines changed: 57 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -602,74 +602,110 @@ def test_clear(self):
602602
assert not history.get_strings()
603603

604604

605-
class TestToPtStyle:
606-
def test_to_pt_style_none(self):
607-
assert pt_utils.to_pt_style(None) == ""
605+
class TestRichToPtColor:
606+
def test_rich_to_pt_color_none(self):
607+
assert pt_utils.rich_to_pt_color(None) == "default"
608608

609-
def test_to_pt_style_color(self):
609+
def test_rich_to_pt_color_default(self):
610+
from rich.color import Color
611+
612+
c = Color.parse("default")
613+
assert pt_utils.rich_to_pt_color(c) == "default"
614+
615+
def test_rich_to_pt_color_standard(self):
616+
from rich.color import Color
617+
618+
c = Color.parse("red")
619+
assert pt_utils.rich_to_pt_color(c) == "ansired"
620+
c = Color.parse("bright_red")
621+
assert pt_utils.rich_to_pt_color(c) == "ansibrightred"
622+
# Test a standard color initialized by number
623+
c = Color.from_ansi(2)
624+
assert pt_utils.rich_to_pt_color(c) == "ansigreen"
625+
626+
def test_rich_to_pt_color_eight_bit(self):
627+
from rich.color import Color
628+
629+
# 155 is an 8-bit color
630+
c = Color.from_ansi(155)
631+
# Should convert to truecolor hex equivalent #afff5f
632+
assert pt_utils.rich_to_pt_color(c) == "#afff5f"
633+
634+
def test_rich_to_pt_color_truecolor(self):
635+
from rich.color import Color
636+
637+
c = Color.parse("#123456")
638+
assert pt_utils.rich_to_pt_color(c) == "#123456"
639+
640+
641+
class TestRichToPtStyle:
642+
def test_rich_to_pt_style_none(self):
643+
assert pt_utils.rich_to_pt_style(None) == ""
644+
645+
def test_rich_to_pt_style_color(self):
610646
from rich.style import Style
611647

612648
style = Style(color="#123456")
613-
pt_style = pt_utils.to_pt_style(style)
649+
pt_style = pt_utils.rich_to_pt_style(style)
614650
assert "fg:#123456" in pt_style
615651
assert "bg:default" in pt_style
616652
assert "noreverse" in pt_style
617653

618-
def test_to_pt_style_bgcolor(self):
654+
def test_rich_to_pt_style_bgcolor(self):
619655
from rich.style import Style
620656

621657
style = Style(bgcolor="#654321")
622-
pt_style = pt_utils.to_pt_style(style)
658+
pt_style = pt_utils.rich_to_pt_style(style)
623659
assert "fg:default" in pt_style
624660
assert "bg:#654321" in pt_style
625661

626-
def test_to_pt_style_default_color(self):
662+
def test_rich_to_pt_style_default_color(self):
627663
from rich.style import Style
628664

629665
style = Style(color="default", bgcolor="default")
630-
pt_style = pt_utils.to_pt_style(style)
666+
pt_style = pt_utils.rich_to_pt_style(style)
631667
assert "fg:default" in pt_style
632668
assert "bg:default" in pt_style
633669

634-
def test_to_pt_style_bold(self):
670+
def test_rich_to_pt_style_bold(self):
635671
from rich.style import Style
636672

637673
style = Style(bold=True)
638-
pt_style = pt_utils.to_pt_style(style)
674+
pt_style = pt_utils.rich_to_pt_style(style)
639675
assert "bold" in pt_style
640676
assert "nobold" not in pt_style
641677

642-
def test_to_pt_style_nobold(self):
678+
def test_rich_to_pt_style_nobold(self):
643679
from rich.style import Style
644680

645681
style = Style(bold=False)
646-
pt_style = pt_utils.to_pt_style(style)
682+
pt_style = pt_utils.rich_to_pt_style(style)
647683
assert "nobold" in pt_style
648684

649-
def test_to_pt_style_italic(self):
685+
def test_rich_to_pt_style_italic(self):
650686
from rich.style import Style
651687

652688
style = Style(italic=True)
653-
pt_style = pt_utils.to_pt_style(style)
689+
pt_style = pt_utils.rich_to_pt_style(style)
654690
assert "italic" in pt_style
655691

656-
def test_to_pt_style_noitalic(self):
692+
def test_rich_to_pt_style_noitalic(self):
657693
from rich.style import Style
658694

659695
style = Style(italic=False)
660-
pt_style = pt_utils.to_pt_style(style)
696+
pt_style = pt_utils.rich_to_pt_style(style)
661697
assert "noitalic" in pt_style
662698

663-
def test_to_pt_style_underline(self):
699+
def test_rich_to_pt_style_underline(self):
664700
from rich.style import Style
665701

666702
style = Style(underline=True)
667-
pt_style = pt_utils.to_pt_style(style)
703+
pt_style = pt_utils.rich_to_pt_style(style)
668704
assert "underline" in pt_style
669705

670-
def test_to_pt_style_nounderline(self):
706+
def test_rich_to_pt_style_nounderline(self):
671707
from rich.style import Style
672708

673709
style = Style(underline=False)
674-
pt_style = pt_utils.to_pt_style(style)
710+
pt_style = pt_utils.rich_to_pt_style(style)
675711
assert "nounderline" in pt_style

0 commit comments

Comments
 (0)