Skip to content
6 changes: 2 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ prompt is displayed.
- `RawTextCmd2HelpFormatter`
- `TextGroup`
- Replaced the global `APP_THEME` constant in `rich_utils.py` with `get_theme()` and
`set_theme()` functions to support lazy initialization and safer in-place updates of the
theme.
`set_theme()` functions in `theme.py` to support lazy initialization and safer in-place
updates of the theme.
- Renamed `Cmd._command_parsers` to `Cmd.command_parsers`.
- Removed `RichPrintKwargs` `TypedDict` in favor of using `Mapping[str, Any]`, allowing for
greater flexibility in passing keyword arguments to `console.print()` calls.
Expand Down Expand Up @@ -155,8 +155,6 @@ prompt is displayed.
- Added `Cmd2ArgumentParser.output_to()` context manager to temporarily set the output stream
during `argparse` operations. This is helpful for directing output for functions like
`parse_args()`, which default to `sys.stdout` and lack a `file` argument.
- Added `cmd2.rich_utils.register_theme_update_callback` function to register callback functions
Comment thread
tleonhardt marked this conversation as resolved.
to get called whenever `cmd2.rich_utils.set_theme` is called
- Added ability to customize `prompt-toolkit` completion menu colors by overriding the following
fields in the `cmd2` theme:
- `Cmd2Style.COMPLETION_MENU` - Base style for the entire completion menu container (sets
Expand Down
13 changes: 8 additions & 5 deletions cmd2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,13 @@
RawDescriptionCmd2HelpFormatter,
RawTextCmd2HelpFormatter,
TextGroup,
get_theme,
set_theme,
Comment thread
kmvanbrunt marked this conversation as resolved.
)
from .string_utils import stylize
from .styles import Cmd2Style
from .theme import (
get_theme,
set_theme,
Comment thread
kmvanbrunt marked this conversation as resolved.
)
from .utils import (
CustomCompletionSettings,
Settable,
Expand Down Expand Up @@ -100,16 +102,17 @@
# Rich Utils
"ArgumentDefaultsCmd2HelpFormatter",
"Cmd2HelpFormatter",
"get_theme",
"MetavarTypeCmd2HelpFormatter",
"RawDescriptionCmd2HelpFormatter",
"RawTextCmd2HelpFormatter",
"set_theme",
"TextGroup",
# String Utils
"stylize",
# Styles,
# Styles
"Cmd2Style",
# Theme
"get_theme",
"set_theme",
# Utilities
"categorize",
"CustomCompletionSettings",
Expand Down
40 changes: 2 additions & 38 deletions cmd2/cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@
from prompt_toolkit.patch_stdout import patch_stdout
from prompt_toolkit.shortcuts import CompleteStyle, PromptSession, choice, set_title
from prompt_toolkit.styles import DynamicStyle
from prompt_toolkit.styles import Style as PtStyle
from rich.console import (
Group,
JustifyMethod,
Expand Down Expand Up @@ -162,6 +161,7 @@
TextGroup,
)
from .styles import Cmd2Style
from .theme import get_pt_theme
from .types import (
BoundCommandFunc,
BoundCompleter,
Expand Down Expand Up @@ -195,7 +195,6 @@ def __init__(self, msg: str = "") -> None:
Cmd2History,
Cmd2Lexer,
pt_filter_style,
rich_to_pt_style,
)
from .utils import (
Settable,
Expand Down Expand Up @@ -526,11 +525,6 @@ def __init__(
self._persistent_history_length = persistent_history_length
self._initialize_history(persistent_history_file)

# Cache for prompt_toolkit completion menu styles
self.pt_style: PtStyle
self.update_pt_style()
ru.register_theme_update_callback(self.update_pt_style)

# Create the main PromptSession
self.bottom_toolbar = bottom_toolbar
self.main_session = self._create_main_session(auto_suggest, completekey)
Expand Down Expand Up @@ -724,36 +718,6 @@ def _should_continue_multiline(self) -> bool:
# No macro found or already processed. The statement is complete.
return False

def update_pt_style(self) -> None:
"""Update the cached prompt_toolkit style."""
theme = ru.get_theme()
rich_menu_style = theme.styles.get(Cmd2Style.COMPLETION_MENU, Style.null())
rich_completion_style = theme.styles.get(Cmd2Style.COMPLETION_MENU_COMPLETION, Style.null())
rich_current_style = theme.styles.get(Cmd2Style.COMPLETION_MENU_CURRENT, Style.null())
rich_meta_style = theme.styles.get(Cmd2Style.COMPLETION_MENU_META, Style.null())
rich_meta_current_style = theme.styles.get(Cmd2Style.COMPLETION_MENU_META_CURRENT, Style.null())

menu_style = rich_to_pt_style(rich_menu_style)
completion_style = rich_to_pt_style(rich_completion_style)
current_style = rich_to_pt_style(rich_current_style)
meta_style = rich_to_pt_style(rich_meta_style)
meta_current_style = rich_to_pt_style(rich_meta_current_style)

self.pt_style = PtStyle.from_dict(
{
"completion-menu": menu_style,
"completion-menu.completion": completion_style,
"completion-menu.completion.current": current_style,
"completion-menu.meta.completion": meta_style,
"completion-menu.meta.completion.current": meta_current_style,
"completion-menu.multi-column-meta": meta_current_style,
}
)

def _get_pt_style(self) -> "PtStyle":
"""Return the cached prompt_toolkit style."""
return self.pt_style

def _create_main_session(self, auto_suggest: bool, completekey: str) -> PromptSession[str]:
"""Create and return the main PromptSession for the application.

Expand Down Expand Up @@ -796,7 +760,7 @@ def _(event: Any) -> None: # pragma: no cover
"multiline": filters.Condition(self._should_continue_multiline),
"prompt_continuation": self.continuation_prompt,
"rprompt": self.get_rprompt,
"style": DynamicStyle(self._get_pt_style),
"style": DynamicStyle(get_pt_theme),
}

if self.stdin.isatty() and self.stdout.isatty():
Expand Down
55 changes: 20 additions & 35 deletions cmd2/pt_utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""Utilities for integrating prompt_toolkit with cmd2."""

import re
import weakref
from collections.abc import (
Callable,
Iterable,
Expand Down Expand Up @@ -111,16 +110,20 @@ def rich_to_pt_style(rich_style: StyleType) -> str:

if rich_style.bold is not None:
parts.append("bold" if rich_style.bold else "nobold")
if rich_style.italic is not None:
parts.append("italic" if rich_style.italic else "noitalic")
if rich_style.underline is not None:
parts.append("underline" if rich_style.underline else "nounderline")
if rich_style.strike is not None:
parts.append("strike" if rich_style.strike else "nostrike")
if rich_style.italic is not None:
parts.append("italic" if rich_style.italic else "noitalic")
if rich_style.blink is not None:
parts.append("blink" if rich_style.blink else "noblink")
if rich_style.reverse is not None:
parts.append("reverse" if rich_style.reverse else "noreverse")
if rich_style.conceal is not None:
parts.append("hidden" if rich_style.conceal else "nohidden")
if rich_style.dim is not None:
parts.append("dim" if rich_style.dim else "nodim")
return " ".join(parts)


Expand Down Expand Up @@ -264,21 +267,16 @@ def clear(self) -> None:
self._loaded_strings.clear()


_lexers: "weakref.WeakSet[Cmd2Lexer]" = weakref.WeakSet()


def _update_lexer_colors() -> None:
"""Update colors for all active lexers."""
for lexer in _lexers:
lexer.set_colors()


ru.register_theme_update_callback(_update_lexer_colors)


class Cmd2Lexer(Lexer):
"""Lexer that highlights cmd2 command names, aliases, and macros."""

# Use the 'class:' prefix to look up styles in the prompt-toolkit theme
COMMAND_STYLE = f"class:{Cmd2Style.LEXER_COMMAND}"
ALIAS_STYLE = f"class:{Cmd2Style.LEXER_ALIAS}"
MACRO_STYLE = f"class:{Cmd2Style.LEXER_MACRO}"
FLAG_STYLE = f"class:{Cmd2Style.LEXER_FLAG}"
ARGUMENT_STYLE = f"class:{Cmd2Style.LEXER_ARGUMENT}"

def __init__(
self,
cmd_app: "Cmd",
Expand All @@ -290,19 +288,6 @@ def __init__(
super().__init__()
self._cmd_app = cmd_app

_lexers.add(self)
self.set_colors()

def set_colors(self) -> None:
"""Update colors from the current rich theme."""
# Retrieve styles dynamically from the current theme
theme = ru.get_theme()
self.command_color = rich_to_pt_style(theme.styles.get(Cmd2Style.LEXER_COMMAND, Style.null()))
self.alias_color = rich_to_pt_style(theme.styles.get(Cmd2Style.LEXER_ALIAS, Style.null()))
self.macro_color = rich_to_pt_style(theme.styles.get(Cmd2Style.LEXER_MACRO, Style.null()))
self.flag_color = rich_to_pt_style(theme.styles.get(Cmd2Style.LEXER_FLAG, Style.null()))
self.argument_color = rich_to_pt_style(theme.styles.get(Cmd2Style.LEXER_ARGUMENT, Style.null()))

def lex_document(self, document: Document) -> Callable[[int], Any]:
"""Lex the document."""
# Get redirection tokens and terminators to avoid highlighting them as values
Expand All @@ -319,9 +304,9 @@ def highlight_args(text: str, tokens: list[tuple[str, str]]) -> None:
if space:
tokens.append(("", match_text))
elif flag:
tokens.append((self.flag_color, match_text))
tokens.append((self.FLAG_STYLE, match_text))
elif (quoted or word) and match_text not in exclude_tokens:
tokens.append((self.argument_color, match_text))
tokens.append((self.ARGUMENT_STYLE, match_text))
else:
tokens.append(("", match_text))

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

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

shortcut_found = True
break

if not shortcut_found:
style = ""
if command in self._cmd_app.get_all_commands():
style = self.command_color
style = self.COMMAND_STYLE
elif command in self._cmd_app.aliases:
style = self.alias_color
style = self.ALIAS_STYLE
elif command in self._cmd_app.macros:
style = self.macro_color
style = self.MACRO_STYLE

# Add the command with the determined style
tokens.append((style, command))
Expand Down
Loading
Loading