Skip to content

Commit 74803c1

Browse files
authored
Added prompt-toolkit version of the app theme which remains synchronized when theme is updated. (#1661)
1 parent 40ca446 commit 74803c1

13 files changed

Lines changed: 689 additions & 343 deletions

CHANGELOG.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,8 @@ prompt is displayed.
9595
- `RawTextCmd2HelpFormatter`
9696
- `TextGroup`
9797
- Replaced the global `APP_THEME` constant in `rich_utils.py` with `get_theme()` and
98-
`set_theme()` functions to support lazy initialization and safer in-place updates of the
99-
theme.
98+
`set_theme()` functions in `theme.py` to support lazy initialization and safer in-place
99+
updates of the theme.
100100
- Renamed `Cmd._command_parsers` to `Cmd.command_parsers`.
101101
- Removed `RichPrintKwargs` `TypedDict` in favor of using `Mapping[str, Any]`, allowing for
102102
greater flexibility in passing keyword arguments to `console.print()` calls.
@@ -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: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,13 @@
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_theme,
59+
set_theme,
60+
)
5961
from .utils import (
6062
CustomCompletionSettings,
6163
Settable,
@@ -100,16 +102,17 @@
100102
# Rich Utils
101103
"ArgumentDefaultsCmd2HelpFormatter",
102104
"Cmd2HelpFormatter",
103-
"get_theme",
104105
"MetavarTypeCmd2HelpFormatter",
105106
"RawDescriptionCmd2HelpFormatter",
106107
"RawTextCmd2HelpFormatter",
107-
"set_theme",
108108
"TextGroup",
109109
# String Utils
110110
"stylize",
111-
# Styles,
111+
# Styles
112112
"Cmd2Style",
113+
# Theme
114+
"get_theme",
115+
"set_theme",
113116
# Utilities
114117
"categorize",
115118
"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))

0 commit comments

Comments
 (0)