Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,9 @@ prompt is displayed.
- `RawDescriptionCmd2HelpFormatter`
- `RawTextCmd2HelpFormatter`
- `TextGroup`
- Replaced the global `APP_THEME` constant in `rich_utils.py` with `get_theme()` and
`set_theme()` functions in `theme.py` to support lazy initialization and safer in-place
updates of the theme.
- Replaced the global `APP_THEME` constant in `rich_utils.py` with `get_theme()`,
`reset_theme()`, and `update_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
6 changes: 4 additions & 2 deletions cmd2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@
from .styles import Cmd2Style
from .theme import (
get_theme,
set_theme,
reset_theme,
update_theme,
)
from .utils import (
CustomCompletionSettings,
Expand Down Expand Up @@ -112,7 +113,8 @@
"Cmd2Style",
# Theme
"get_theme",
"set_theme",
"reset_theme",
"update_theme",
# Utilities
"categorize",
"CustomCompletionSettings",
Expand Down
10 changes: 5 additions & 5 deletions cmd2/rich_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@ def __repr__(self) -> str:
class Cmd2HelpFormatter(RichHelpFormatter):
"""Custom help formatter to configure ordering of help text."""

# Have our own copy of the styles so set_theme() can synchronize them with
# the cmd2 application theme without overwriting RichHelpFormatter's defaults.
# Create our own copy of the styles so cmd2 can synchronize them with
# the application theme without overwriting RichHelpFormatter's defaults.
styles: ClassVar[dict[str, StyleType]] = DEFAULT_ARGPARSE_STYLES.copy()

# Disable automatic highlighting in the help text.
Expand Down Expand Up @@ -341,10 +341,10 @@ def __init__(
"Passing 'force_interactive' is not allowed. Its behavior is controlled by the 'ALLOW_STYLE' setting."
)

# Don't allow a theme to be passed in, as it is controlled by get_theme() and set_theme().
# Use set_theme() to set the global theme or use a temporary theme with console.use_theme().
# Don't allow a theme to be passed in. Use update_theme() to modify the global theme
# or use a temporary theme with console.use_theme().
if "theme" in kwargs:
raise TypeError("Passing 'theme' is not allowed. Its behavior is controlled by get_theme() and set_theme().")
raise TypeError("Passing 'theme' is not allowed. Modify the global theme with update_theme().")

# Store the configuration key used by cmd2 to cache this console.
self._config_key = self._build_config_key(file=file, **kwargs)
Expand Down
2 changes: 1 addition & 1 deletion cmd2/styles.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
their own default styles.

For a complete theming experience, you can create a custom theme that includes
styles from Rich and rich-argparse. The `cmd2.theme.set_theme()` function
styles from Rich and rich-argparse. The `cmd2.theme.update_theme()` function
automatically updates rich-argparse's styles with any custom styles provided in
your theme dictionary, so you don't have to modify them directly.

Expand Down
105 changes: 62 additions & 43 deletions cmd2/theme.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@
from typing import cast

from prompt_toolkit.styles import Style as PtStyle
from rich.style import StyleType
from rich.style import (
Style,
StyleType,
)
from rich.theme import Theme

from .pt_utils import rich_to_pt_style
Expand All @@ -35,16 +38,16 @@
)

# The application-wide theme, defined using Rich's styling system.
# Use get_theme() to access it and set_theme() to modify it.
# Use get_theme() to access it.
# Use reset_theme() and update_theme() to modify it.
_THEME: Theme | None = None

# The prompt-toolkit version of the theme, synchronized from the Rich theme.
# Use get_pt_theme() to access it. This object is automatically updated whenever
# set_theme() is called.
# Use get_pt_theme() to access it.
_PT_THEME: PtStyle | None = None

# Maps style names to internal UI component names used by prompt-toolkit.
# This allows developers to use application-specific style names in set_theme()
# This allows developers to use application-specific style names in update_theme()
# while ensuring the underlying prompt-toolkit UI is styled correctly.
# Use register_pt_mapping() and unregister_pt_mapping() to manage these mappings.
#
Expand Down Expand Up @@ -72,52 +75,73 @@
def get_theme() -> Theme:
"""Get the application-wide Rich theme. Initializes it on the first call."""
if _THEME is None:
set_theme()
reset_theme()
return cast(Theme, _THEME)


def get_pt_theme() -> PtStyle:
"""Get the application-wide prompt-toolkit style. Initializes it on the first call."""
if _PT_THEME is None:
set_theme()
reset_theme()
return cast(PtStyle, _PT_THEME)


def set_theme(styles: Mapping[str, StyleType] | None = None) -> None:
"""Set the application-wide theme.
def reset_theme() -> None:
"""Reset the application-wide theme to its initial state.

This function performs an in-place update of the existing Rich theme's
This function performs an in-place reset of the existing Rich theme's
styles. This ensures that any Console objects already using the theme
will reflect the changes immediately without needing to be recreated.

It also automatically synchronizes the prompt-toolkit theme for any
styles with registered prefixes or mapped UI components.

Call set_theme() with no arguments to reset to the default theme.
This will clear any custom styles that were previously applied.

:param styles: optional mapping of style names to styles
Changes are automatically propagated to all synchronized components.
"""
global _THEME # noqa: PLW0603

# Include default styles from cmd2, rich-argparse, and Rich.
styles = DEFAULT_CMD2_STYLES.copy()
styles.update(DEFAULT_ARGPARSE_STYLES)
default_theme = Theme(styles, inherit=True)

if _THEME is None:
_THEME = Theme()
# Initial assignment
_THEME = default_theme
else:
# Perform in-place reset to preserve existing references
_THEME.styles.clear()
_THEME.styles.update(default_theme.styles)

_sync_all()


def update_theme(styles: Mapping[str, StyleType]) -> None:
"""Update the existing theme.

This function performs an in-place update of the existing Rich theme's
styles. This ensures that any Console objects already using the theme
will reflect the changes immediately without needing to be recreated.

Changes are automatically propagated to all synchronized components.

# Start with a fresh copy of the default styles.
unparsed_styles: dict[str, StyleType] = {}
unparsed_styles.update(_create_default_theme().styles)
:param styles: mapping of style names to styles
"""
# Convert any string styles to Style objects
parsed_styles = {name: style if isinstance(style, Style) else Style.parse(style) for name, style in styles.items()}

# Perform in-place update to preserve existing references
get_theme().styles.update(parsed_styles)

# Add the custom styles, which may contain unparsed strings
if styles is not None:
unparsed_styles.update(styles)
_sync_all()

# Use Rich's Theme class to perform the parsing
parsed_styles = Theme(unparsed_styles).styles

# Perform the in-place update with the results
_THEME.styles.clear()
_THEME.styles.update(parsed_styles)
def _sync_all() -> None:
"""Propagate the global theme to rich-argparse and prompt-toolkit.

If the theme hasn't been initialized yet, this is a no-op.
"""
if _THEME is None:
return

# Synchronize rich-argparse styles with the main application theme.
# Synchronize rich-argparse styles
for name in Cmd2HelpFormatter.styles.keys() & _THEME.styles.keys():
Cmd2HelpFormatter.styles[name] = _THEME.styles[name]

Expand All @@ -126,11 +150,16 @@ def set_theme(styles: Mapping[str, StyleType] | None = None) -> None:


def _sync_pt_theme() -> None:
"""Build a new global PT style object based on the current Rich theme."""
theme = get_theme()
"""Build a new global prompt-toolkit style object based on the current Rich theme.

If the theme hasn't been initialized yet, this is a no-op.
"""
if _THEME is None:
return

style_rules: list[tuple[str, str]] = []

for name, rich_style in theme.styles.items():
for name, rich_style in _THEME.styles.items():
# Only synchronize if it has a registered prefix or mapped UI component.
is_framework_style = any(name.startswith(p) for p in _SYNCHRONIZED_PREFIXES)
is_mapped_style = name in _PT_UI_MAP
Expand All @@ -149,16 +178,6 @@ def _sync_pt_theme() -> None:
_PT_THEME = PtStyle(style_rules)


def _create_default_theme() -> Theme:
"""Create a default theme for the application.

This theme combines the default styles from cmd2, rich-argparse, and Rich.
"""
app_styles = DEFAULT_CMD2_STYLES.copy()
app_styles.update(DEFAULT_ARGPARSE_STYLES)
return Theme(app_styles, inherit=True)


def register_pt_mapping(style_name: str, pt_ui_names: str | Iterable[str]) -> None:
"""Map a Rich theme style name to one or more prompt-toolkit UI components.

Expand Down
2 changes: 1 addition & 1 deletion docs/features/generating_output.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ all colors available to your `cmd2` application.

`cmd2` uses a `rich` [Theme](https://rich.readthedocs.io/en/stable/reference/theme.html) object to
define styles for various UI elements. You can define your own custom theme using
[cmd2.rich_utils.set_theme][]. See the
[cmd2.theme.update_theme][]. See the
[rich_theme.py](https://github.com/python-cmd2/cmd2/blob/main/examples/rich_theme.py) example for
more information.

Expand Down
2 changes: 1 addition & 1 deletion docs/features/theme.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Theme

`cmd2` provides the ability to configure an overall theme for your application using the
[cmd2.rich_utils.set_theme][] function. This is based on the
[cmd2.theme.update_theme][] function. This is based on the
[rich.theme](https://rich.readthedocs.io/en/stable/reference/theme.html) container for style
information. You can use this to brand your application and set an overall consistent look and feel
that is appealing to your user base.
Expand Down
4 changes: 2 additions & 2 deletions docs/upgrades.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ periodically.
customize its appearance using the `cmd2` theme.

- **Customization**: Override the `Cmd2Style.COMPLETION_MENU_CURRENT` and
`Cmd2Style.COMPLETION_MENU_META` styles using `cmd2.theme.set_theme()`. See
`Cmd2Style.COMPLETION_MENU_META` styles using `cmd2.theme.update_theme()`. See
[Customizing Completion Menu Colors](features/theme.md#customizing-completion-menu-colors) for
more details.

Expand Down Expand Up @@ -136,7 +136,7 @@ The new [cmd2.rich_utils][] module provides common utility classes and functions
use of `rich` within `cmd2` applications. Most of what is here is not intended to be user-facing.

The one thing many `cmd2` application developers will likely be interested in using is the
[cmd2.rich_utils.set_theme][] function. See the
[cmd2.theme.update_theme][] function. See the
[rich_theme.py](https://github.com/python-cmd2/cmd2/blob/main/examples/rich_theme.py) example for a
demonstration for how to set a theme (color scheme) for your app.

Expand Down
4 changes: 2 additions & 2 deletions examples/rich_theme.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from rich.style import Style

import cmd2
from cmd2 import Cmd2Style, Color, set_theme
from cmd2 import Cmd2Style, Color, update_theme


class ThemedApp(cmd2.Cmd):
Expand Down Expand Up @@ -41,7 +41,7 @@ def __init__(self, *args, **kwargs):
"traceback.exc_type": Style(color=Color.RED, bgcolor=Color.LIGHT_YELLOW3, bold=True),
"argparse.args": Style(color=Color.AQUAMARINE3, underline=True),
}
set_theme(custom_theme)
update_theme(custom_theme)

@cmd2.with_category("Theme Commands")
def do_theme_show(self, _: cmd2.Statement):
Expand Down
Loading
Loading