Skip to content

Commit d786ae3

Browse files
committed
Added prompt-toolkit version of the app theme which remains synchronized when theme is updated.
1 parent 26786be commit d786ae3

10 files changed

Lines changed: 470 additions & 333 deletions

File tree

CHANGELOG.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,6 @@ prompt is displayed.
155155
- Added `Cmd2ArgumentParser.output_to()` context manager to temporarily set the output stream
156156
during `argparse` operations. This is helpful for directing output for functions like
157157
`parse_args()`, which default to `sys.stdout` and lack a `file` argument.
158-
- Added `cmd2.rich_utils.register_theme_update_callback` function to register callback functions
159-
to get called whenever `cmd2.rich_utils.set_theme` is called
160158
- Added ability to customize `prompt-toolkit` completion menu colors by overriding the following
161159
fields in the `cmd2` theme:
162160
- `Cmd2Style.COMPLETION_MENU` - Base style for the entire completion menu container (sets

cmd2/__init__.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,16 @@
5151
RawDescriptionCmd2HelpFormatter,
5252
RawTextCmd2HelpFormatter,
5353
TextGroup,
54-
get_theme,
55-
set_theme,
5654
)
5755
from .string_utils import stylize
5856
from .styles import Cmd2Style
57+
from .theme import (
58+
get_pt_theme,
59+
get_theme,
60+
register_pt_mapping,
61+
register_synchronized_prefix,
62+
set_theme,
63+
)
5964
from .utils import (
6065
CustomCompletionSettings,
6166
Settable,
@@ -100,16 +105,20 @@
100105
# Rich Utils
101106
"ArgumentDefaultsCmd2HelpFormatter",
102107
"Cmd2HelpFormatter",
103-
"get_theme",
104108
"MetavarTypeCmd2HelpFormatter",
105109
"RawDescriptionCmd2HelpFormatter",
106110
"RawTextCmd2HelpFormatter",
107-
"set_theme",
108111
"TextGroup",
109112
# String Utils
110113
"stylize",
111-
# Styles,
114+
# Styles
112115
"Cmd2Style",
116+
# Theme
117+
"get_pt_theme",
118+
"get_theme",
119+
"register_pt_mapping",
120+
"register_synchronized_prefix",
121+
"set_theme",
113122
# Utilities
114123
"categorize",
115124
"CustomCompletionSettings",

cmd2/cmd2.py

Lines changed: 2 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@
8282
from prompt_toolkit.patch_stdout import patch_stdout
8383
from prompt_toolkit.shortcuts import CompleteStyle, PromptSession, choice, set_title
8484
from prompt_toolkit.styles import DynamicStyle
85-
from prompt_toolkit.styles import Style as PtStyle
8685
from rich.console import (
8786
Group,
8887
JustifyMethod,
@@ -162,6 +161,7 @@
162161
TextGroup,
163162
)
164163
from .styles import Cmd2Style
164+
from .theme import get_pt_theme
165165
from .types import (
166166
BoundCommandFunc,
167167
BoundCompleter,
@@ -195,7 +195,6 @@ def __init__(self, msg: str = "") -> None:
195195
Cmd2History,
196196
Cmd2Lexer,
197197
pt_filter_style,
198-
rich_to_pt_style,
199198
)
200199
from .utils import (
201200
Settable,
@@ -526,11 +525,6 @@ def __init__(
526525
self._persistent_history_length = persistent_history_length
527526
self._initialize_history(persistent_history_file)
528527

529-
# Cache for prompt_toolkit completion menu styles
530-
self.pt_style: PtStyle
531-
self.update_pt_style()
532-
ru.register_theme_update_callback(self.update_pt_style)
533-
534528
# Create the main PromptSession
535529
self.bottom_toolbar = bottom_toolbar
536530
self.main_session = self._create_main_session(auto_suggest, completekey)
@@ -724,36 +718,6 @@ def _should_continue_multiline(self) -> bool:
724718
# No macro found or already processed. The statement is complete.
725719
return False
726720

727-
def update_pt_style(self) -> None:
728-
"""Update the cached prompt_toolkit style."""
729-
theme = ru.get_theme()
730-
rich_menu_style = theme.styles.get(Cmd2Style.COMPLETION_MENU, Style.null())
731-
rich_completion_style = theme.styles.get(Cmd2Style.COMPLETION_MENU_COMPLETION, Style.null())
732-
rich_current_style = theme.styles.get(Cmd2Style.COMPLETION_MENU_CURRENT, Style.null())
733-
rich_meta_style = theme.styles.get(Cmd2Style.COMPLETION_MENU_META, Style.null())
734-
rich_meta_current_style = theme.styles.get(Cmd2Style.COMPLETION_MENU_META_CURRENT, Style.null())
735-
736-
menu_style = rich_to_pt_style(rich_menu_style)
737-
completion_style = rich_to_pt_style(rich_completion_style)
738-
current_style = rich_to_pt_style(rich_current_style)
739-
meta_style = rich_to_pt_style(rich_meta_style)
740-
meta_current_style = rich_to_pt_style(rich_meta_current_style)
741-
742-
self.pt_style = PtStyle.from_dict(
743-
{
744-
"completion-menu": menu_style,
745-
"completion-menu.completion": completion_style,
746-
"completion-menu.completion.current": current_style,
747-
"completion-menu.meta.completion": meta_style,
748-
"completion-menu.meta.completion.current": meta_current_style,
749-
"completion-menu.multi-column-meta": meta_current_style,
750-
}
751-
)
752-
753-
def _get_pt_style(self) -> "PtStyle":
754-
"""Return the cached prompt_toolkit style."""
755-
return self.pt_style
756-
757721
def _create_main_session(self, auto_suggest: bool, completekey: str) -> PromptSession[str]:
758722
"""Create and return the main PromptSession for the application.
759723
@@ -796,7 +760,7 @@ def _(event: Any) -> None: # pragma: no cover
796760
"multiline": filters.Condition(self._should_continue_multiline),
797761
"prompt_continuation": self.continuation_prompt,
798762
"rprompt": self.get_rprompt,
799-
"style": DynamicStyle(self._get_pt_style),
763+
"style": DynamicStyle(get_pt_theme),
800764
}
801765

802766
if self.stdin.isatty() and self.stdout.isatty():

cmd2/pt_utils.py

Lines changed: 20 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""Utilities for integrating prompt_toolkit with cmd2."""
22

33
import re
4-
import weakref
54
from collections.abc import (
65
Callable,
76
Iterable,
@@ -111,16 +110,20 @@ def rich_to_pt_style(rich_style: StyleType) -> str:
111110

112111
if rich_style.bold is not None:
113112
parts.append("bold" if rich_style.bold else "nobold")
114-
if rich_style.italic is not None:
115-
parts.append("italic" if rich_style.italic else "noitalic")
116113
if rich_style.underline is not None:
117114
parts.append("underline" if rich_style.underline else "nounderline")
115+
if rich_style.strike is not None:
116+
parts.append("strike" if rich_style.strike else "nostrike")
117+
if rich_style.italic is not None:
118+
parts.append("italic" if rich_style.italic else "noitalic")
118119
if rich_style.blink is not None:
119120
parts.append("blink" if rich_style.blink else "noblink")
120121
if rich_style.reverse is not None:
121122
parts.append("reverse" if rich_style.reverse else "noreverse")
122123
if rich_style.conceal is not None:
123124
parts.append("hidden" if rich_style.conceal else "nohidden")
125+
if rich_style.dim is not None:
126+
parts.append("dim" if rich_style.dim else "nodim")
124127
return " ".join(parts)
125128

126129

@@ -264,21 +267,16 @@ def clear(self) -> None:
264267
self._loaded_strings.clear()
265268

266269

267-
_lexers: "weakref.WeakSet[Cmd2Lexer]" = weakref.WeakSet()
268-
269-
270-
def _update_lexer_colors() -> None:
271-
"""Update colors for all active lexers."""
272-
for lexer in _lexers:
273-
lexer.set_colors()
274-
275-
276-
ru.register_theme_update_callback(_update_lexer_colors)
277-
278-
279270
class Cmd2Lexer(Lexer):
280271
"""Lexer that highlights cmd2 command names, aliases, and macros."""
281272

273+
# Use the 'class:' prefix to look up styles in the prompt-toolkit theme
274+
COMMAND_STYLE = f"class:{Cmd2Style.LEXER_COMMAND}"
275+
ALIAS_STYLE = f"class:{Cmd2Style.LEXER_ALIAS}"
276+
MACRO_STYLE = f"class:{Cmd2Style.LEXER_MACRO}"
277+
FLAG_STYLE = f"class:{Cmd2Style.LEXER_FLAG}"
278+
ARGUMENT_STYLE = f"class:{Cmd2Style.LEXER_ARGUMENT}"
279+
282280
def __init__(
283281
self,
284282
cmd_app: "Cmd",
@@ -290,19 +288,6 @@ def __init__(
290288
super().__init__()
291289
self._cmd_app = cmd_app
292290

293-
_lexers.add(self)
294-
self.set_colors()
295-
296-
def set_colors(self) -> None:
297-
"""Update colors from the current rich theme."""
298-
# Retrieve styles dynamically from the current theme
299-
theme = ru.get_theme()
300-
self.command_color = rich_to_pt_style(theme.styles.get(Cmd2Style.LEXER_COMMAND, Style.null()))
301-
self.alias_color = rich_to_pt_style(theme.styles.get(Cmd2Style.LEXER_ALIAS, Style.null()))
302-
self.macro_color = rich_to_pt_style(theme.styles.get(Cmd2Style.LEXER_MACRO, Style.null()))
303-
self.flag_color = rich_to_pt_style(theme.styles.get(Cmd2Style.LEXER_FLAG, Style.null()))
304-
self.argument_color = rich_to_pt_style(theme.styles.get(Cmd2Style.LEXER_ARGUMENT, Style.null()))
305-
306291
def lex_document(self, document: Document) -> Callable[[int], Any]:
307292
"""Lex the document."""
308293
# Get redirection tokens and terminators to avoid highlighting them as values
@@ -319,9 +304,9 @@ def highlight_args(text: str, tokens: list[tuple[str, str]]) -> None:
319304
if space:
320305
tokens.append(("", match_text))
321306
elif flag:
322-
tokens.append((self.flag_color, match_text))
307+
tokens.append((self.FLAG_STYLE, match_text))
323308
elif (quoted or word) and match_text not in exclude_tokens:
324-
tokens.append((self.argument_color, match_text))
309+
tokens.append((self.ARGUMENT_STYLE, match_text))
325310
else:
326311
tokens.append(("", match_text))
327312

@@ -355,23 +340,23 @@ def get_line(lineno: int) -> list[tuple[str, str]]:
355340
for shortcut, _ in self._cmd_app.statement_parser.shortcuts:
356341
if command.startswith(shortcut):
357342
# Add the shortcut with the command style
358-
tokens.append((self.command_color, shortcut))
343+
tokens.append((self.COMMAND_STYLE, shortcut))
359344

360345
# If there's more in the command word, it's an argument
361346
if len(command) > len(shortcut):
362-
tokens.append((self.argument_color, command[len(shortcut) :]))
347+
tokens.append((self.ARGUMENT_STYLE, command[len(shortcut) :]))
363348

364349
shortcut_found = True
365350
break
366351

367352
if not shortcut_found:
368353
style = ""
369354
if command in self._cmd_app.get_all_commands():
370-
style = self.command_color
355+
style = self.COMMAND_STYLE
371356
elif command in self._cmd_app.aliases:
372-
style = self.alias_color
357+
style = self.ALIAS_STYLE
373358
elif command in self._cmd_app.macros:
374-
style = self.macro_color
359+
style = self.MACRO_STYLE
375360

376361
# Add the command with the determined style
377362
tokens.append((style, command))

cmd2/rich_utils.py

Lines changed: 11 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,7 @@
33
import argparse
44
import re
55
import sys
6-
from collections.abc import (
7-
Callable,
8-
Iterator,
9-
Mapping,
10-
)
6+
from collections.abc import Iterator
117
from enum import Enum
128
from typing import (
139
IO,
@@ -48,7 +44,6 @@
4844
from . import constants
4945
from .styles import (
5046
DEFAULT_ARGPARSE_STYLES,
51-
DEFAULT_CMD2_STYLES,
5247
Cmd2Style,
5348
)
5449

@@ -59,6 +54,13 @@
5954
ANSI_STYLE_SEQUENCE_RE = re.compile(r"\x1b\[[0-9;]*m")
6055

6156

57+
def _get_theme() -> Theme:
58+
"""Retrieve the global Rich theme while avoiding circular imports."""
59+
from .theme import get_theme
60+
61+
return get_theme()
62+
63+
6264
@runtime_checkable
6365
class HelpFormatterRenderable(Protocol):
6466
"""Protocol for objects that require a Cmd2HelpFormatter to render."""
@@ -307,75 +309,6 @@ def __cmd2_argparse_help__(self, formatter: Cmd2HelpFormatter) -> Group:
307309
return Group(styled_title, indented_text)
308310

309311

310-
# The application-wide theme. Use get_theme() and set_theme() to access it.
311-
_APP_THEME: Theme | None = None
312-
313-
# Callbacks to be executed when the theme is updated
314-
_theme_update_callbacks: list[Callable[[], None]] = []
315-
316-
317-
def register_theme_update_callback(callback: Callable[[], None]) -> None:
318-
"""Register a callback to be executed when the theme is updated."""
319-
if callback not in _theme_update_callbacks:
320-
_theme_update_callbacks.append(callback)
321-
322-
323-
def get_theme() -> Theme:
324-
"""Get the application-wide theme. Initializes it on the first call."""
325-
global _APP_THEME # noqa: PLW0603
326-
if _APP_THEME is None:
327-
_APP_THEME = _create_default_theme()
328-
return _APP_THEME
329-
330-
331-
def set_theme(styles: Mapping[str, StyleType] | None = None) -> None:
332-
"""Set the Rich theme used by cmd2.
333-
334-
This function performs an in-place update of the existing theme's
335-
styles. This ensures that any Console objects already using the theme
336-
will reflect the changes immediately without needing to be recreated.
337-
338-
Call set_theme() with no arguments to reset to the default theme.
339-
This will clear any custom styles that were previously applied.
340-
341-
:param styles: optional mapping of style names to styles
342-
"""
343-
theme = get_theme()
344-
345-
# Start with a fresh copy of the default styles.
346-
unparsed_styles: dict[str, StyleType] = {}
347-
unparsed_styles.update(_create_default_theme().styles)
348-
349-
# Add the custom styles, which may contain unparsed strings
350-
if styles is not None:
351-
unparsed_styles.update(styles)
352-
353-
# Use Rich's Theme class to perform the parsing
354-
parsed_styles = Theme(unparsed_styles).styles
355-
356-
# Perform the in-place update with the results
357-
theme.styles.clear()
358-
theme.styles.update(parsed_styles)
359-
360-
# Synchronize rich-argparse styles with the main application theme.
361-
for name in Cmd2HelpFormatter.styles.keys() & theme.styles.keys():
362-
Cmd2HelpFormatter.styles[name] = theme.styles[name]
363-
364-
# Notify callbacks that the theme has been updated
365-
for callback in _theme_update_callbacks:
366-
callback()
367-
368-
369-
def _create_default_theme() -> Theme:
370-
"""Create a default theme for the application.
371-
372-
This theme combines the default styles from cmd2, rich-argparse, and Rich.
373-
"""
374-
app_styles = DEFAULT_CMD2_STYLES.copy()
375-
app_styles.update(DEFAULT_ARGPARSE_STYLES)
376-
return Theme(app_styles, inherit=True)
377-
378-
379312
class Cmd2BaseConsole(Console):
380313
"""Base class for all cmd2 Rich consoles.
381314
@@ -439,7 +372,7 @@ def __init__(
439372
color_system="truecolor" if allow_style else None,
440373
force_terminal=force_terminal,
441374
force_interactive=force_interactive,
442-
theme=get_theme(),
375+
theme=_get_theme(),
443376
**kwargs,
444377
)
445378

@@ -460,7 +393,7 @@ def _build_config_key(
460393
return (
461394
id(file),
462395
ALLOW_STYLE,
463-
id(get_theme()),
396+
id(_get_theme()),
464397
tuple(sorted(kwargs.items())),
465398
)
466399

@@ -677,7 +610,7 @@ def rich_text_to_string(text: Text) -> str:
677610
color_system="truecolor",
678611
soft_wrap=True,
679612
no_color=False,
680-
theme=get_theme(),
613+
theme=_get_theme(),
681614
)
682615
with console.capture() as capture:
683616
console.print(text, end="")

0 commit comments

Comments
 (0)