Skip to content

Commit 1a73c22

Browse files
committed
Moved to_pt_style function to pt_utils.py and added unit tests for it
Also: - Cache prompt-toolkit completion menu styles for efficiency - Fixed color check bug related to default terminal style
1 parent dfedeba commit 1a73c22

3 files changed

Lines changed: 117 additions & 28 deletions

File tree

cmd2/cmd2.py

Lines changed: 17 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ def __init__(self, msg: str = "") -> None:
192192
Cmd2History,
193193
Cmd2Lexer,
194194
pt_filter_style,
195+
to_pt_style,
195196
)
196197
from .utils import (
197198
Settable,
@@ -523,6 +524,10 @@ def __init__(
523524
self._persistent_history_length = persistent_history_length
524525
self._initialize_history(persistent_history_file)
525526

527+
# Cache for prompt_toolkit completion menu styles
528+
self._cached_pt_style: PtStyle | None = None
529+
self._cached_pt_style_params: tuple[Style | None, Style | None] | None = None
530+
526531
# Create the main PromptSession
527532
self.bottom_toolbar = bottom_toolbar
528533
self.main_session = self._create_main_session(auto_suggest, completekey)
@@ -718,44 +723,28 @@ def _should_continue_multiline(self) -> bool:
718723

719724
def _get_pt_style(self) -> "PtStyle":
720725
"""Return the prompt_toolkit style for the completion menu."""
726+
theme = ru.get_theme()
727+
rich_item_style = theme.styles.get(Cmd2Style.COMPLETION_MENU_ITEM)
728+
rich_meta_style = theme.styles.get(Cmd2Style.COMPLETION_MENU_META)
721729

722-
def to_pt_style(rich_style: Style | None) -> str:
723-
"""Convert a rich Style object to a prompt_toolkit style string."""
724-
if not rich_style:
725-
return ""
726-
parts = ["noreverse"]
727-
if rich_style.color:
728-
c = rich_style.color.get_truecolor()
729-
parts.append(f"fg:#{c.red:02x}{c.green:02x}{c.blue:02x}")
730-
else:
731-
parts.append("fg:default")
732-
733-
if rich_style.bgcolor:
734-
c = rich_style.bgcolor.get_truecolor()
735-
parts.append(f"bg:#{c.red:02x}{c.green:02x}{c.blue:02x}")
736-
else:
737-
parts.append("bg:default")
738-
739-
if rich_style.bold is not None:
740-
parts.append("bold" if rich_style.bold else "nobold")
741-
if rich_style.italic is not None:
742-
parts.append("italic" if rich_style.italic else "noitalic")
743-
if rich_style.underline is not None:
744-
parts.append("underline" if rich_style.underline else "nounderline")
745-
return " ".join(parts)
730+
current_params = (rich_item_style, rich_meta_style)
731+
if self._cached_pt_style is not None and self._cached_pt_style_params == current_params:
732+
return self._cached_pt_style
746733

747-
theme = ru.get_theme()
748-
item_style = to_pt_style(theme.styles.get(Cmd2Style.COMPLETION_MENU_ITEM))
749-
meta_style = to_pt_style(theme.styles.get(Cmd2Style.COMPLETION_MENU_META))
734+
item_style = to_pt_style(rich_item_style)
735+
meta_style = to_pt_style(rich_meta_style)
750736

751-
return PtStyle.from_dict(
737+
self._cached_pt_style_params = current_params
738+
self._cached_pt_style = PtStyle.from_dict(
752739
{
753740
"completion-menu.completion.current": item_style,
754741
"completion-menu.meta.completion.current": meta_style,
755742
"completion-menu.multi-column-meta": meta_style,
756743
}
757744
)
758745

746+
return self._cached_pt_style
747+
759748
def _create_main_session(self, auto_suggest: bool, completekey: str) -> PromptSession[str]:
760749
"""Create and return the main PromptSession for the application.
761750

cmd2/pt_utils.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from prompt_toolkit.formatted_text import ANSI
2121
from prompt_toolkit.history import History
2222
from prompt_toolkit.lexers import Lexer
23+
from rich.style import Style
2324

2425
from . import (
2526
constants,
@@ -50,6 +51,32 @@ def pt_filter_style(text: str | ANSI) -> str | ANSI:
5051
return text if isinstance(text, ANSI) else ANSI(text)
5152

5253

54+
def to_pt_style(rich_style: Style | None) -> str:
55+
"""Convert a rich Style object to a prompt_toolkit style string."""
56+
if not rich_style:
57+
return ""
58+
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")
70+
71+
if rich_style.bold is not None:
72+
parts.append("bold" if rich_style.bold else "nobold")
73+
if rich_style.italic is not None:
74+
parts.append("italic" if rich_style.italic else "noitalic")
75+
if rich_style.underline is not None:
76+
parts.append("underline" if rich_style.underline else "nounderline")
77+
return " ".join(parts)
78+
79+
5380
class Cmd2Completer(Completer):
5481
"""Completer that delegates to cmd2's completion logic."""
5582

tests/test_pt_utils.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -624,3 +624,76 @@ def test_clear(self):
624624

625625
history.clear()
626626
assert not history.get_strings()
627+
628+
629+
class TestToPtStyle:
630+
def test_to_pt_style_none(self):
631+
assert pt_utils.to_pt_style(None) == ""
632+
633+
def test_to_pt_style_color(self):
634+
from rich.style import Style
635+
636+
style = Style(color="#123456")
637+
pt_style = pt_utils.to_pt_style(style)
638+
assert "fg:#123456" in pt_style
639+
assert "bg:default" in pt_style
640+
assert "noreverse" in pt_style
641+
642+
def test_to_pt_style_bgcolor(self):
643+
from rich.style import Style
644+
645+
style = Style(bgcolor="#654321")
646+
pt_style = pt_utils.to_pt_style(style)
647+
assert "fg:default" in pt_style
648+
assert "bg:#654321" in pt_style
649+
650+
def test_to_pt_style_default_color(self):
651+
from rich.style import Style
652+
653+
style = Style(color="default", bgcolor="default")
654+
pt_style = pt_utils.to_pt_style(style)
655+
assert "fg:default" in pt_style
656+
assert "bg:default" in pt_style
657+
658+
def test_to_pt_style_bold(self):
659+
from rich.style import Style
660+
661+
style = Style(bold=True)
662+
pt_style = pt_utils.to_pt_style(style)
663+
assert "bold" in pt_style
664+
assert "nobold" not in pt_style
665+
666+
def test_to_pt_style_nobold(self):
667+
from rich.style import Style
668+
669+
style = Style(bold=False)
670+
pt_style = pt_utils.to_pt_style(style)
671+
assert "nobold" in pt_style
672+
673+
def test_to_pt_style_italic(self):
674+
from rich.style import Style
675+
676+
style = Style(italic=True)
677+
pt_style = pt_utils.to_pt_style(style)
678+
assert "italic" in pt_style
679+
680+
def test_to_pt_style_noitalic(self):
681+
from rich.style import Style
682+
683+
style = Style(italic=False)
684+
pt_style = pt_utils.to_pt_style(style)
685+
assert "noitalic" in pt_style
686+
687+
def test_to_pt_style_underline(self):
688+
from rich.style import Style
689+
690+
style = Style(underline=True)
691+
pt_style = pt_utils.to_pt_style(style)
692+
assert "underline" in pt_style
693+
694+
def test_to_pt_style_nounderline(self):
695+
from rich.style import Style
696+
697+
style = Style(underline=False)
698+
pt_style = pt_utils.to_pt_style(style)
699+
assert "nounderline" in pt_style

0 commit comments

Comments
 (0)