From dfedeba916d517ad735ab889bd5569d6d765ad72 Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Fri, 24 Apr 2026 19:17:41 -0400 Subject: [PATCH] Add styles to theme for completion item and meta foreground and background color This is an attempt to provide higher-contrast color options and to make them configurable as part of a theme. --- CHANGELOG.md | 2 ++ cmd2/cmd2.py | 45 +++++++++++++++++++++++++++++++++++++ cmd2/styles.py | 4 ++++ docs/features/completion.md | 7 ++++++ docs/features/theme.md | 14 ++++++++++++ docs/upgrades.md | 10 +++++++++ 6 files changed, 82 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b5e563151..f4f52e6f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -135,6 +135,8 @@ prompt is displayed. full type hints and IDE autocompletion for `self._cmd` without needing to override and cast the property. - Added `traceback_kwargs` attribute to allow customization of Rich-based tracebacks. + - Added ability to customize `prompt-toolkit` completion menu colors by overriding + `Cmd2Style.COMPLETION_MENU_ITEM` and `Cmd2Style.COMPLETION_MENU_META` in the `cmd2` theme. ## 3.5.1 (April 24, 2026) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index f2ab98944..dba825041 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -78,6 +78,8 @@ from prompt_toolkit.output import DummyOutput, create_output 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, @@ -714,6 +716,46 @@ def _should_continue_multiline(self) -> bool: # No macro found or already processed. The statement is complete. return False + def _get_pt_style(self) -> "PtStyle": + """Return the prompt_toolkit style for the completion menu.""" + + def to_pt_style(rich_style: Style | None) -> str: + """Convert a rich Style object to a prompt_toolkit style string.""" + if not rich_style: + return "" + parts = ["noreverse"] + if rich_style.color: + c = rich_style.color.get_truecolor() + parts.append(f"fg:#{c.red:02x}{c.green:02x}{c.blue:02x}") + else: + parts.append("fg:default") + + if rich_style.bgcolor: + c = rich_style.bgcolor.get_truecolor() + parts.append(f"bg:#{c.red:02x}{c.green:02x}{c.blue:02x}") + else: + parts.append("bg:default") + + 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") + return " ".join(parts) + + theme = ru.get_theme() + item_style = to_pt_style(theme.styles.get(Cmd2Style.COMPLETION_MENU_ITEM)) + meta_style = to_pt_style(theme.styles.get(Cmd2Style.COMPLETION_MENU_META)) + + return PtStyle.from_dict( + { + "completion-menu.completion.current": item_style, + "completion-menu.meta.completion.current": meta_style, + "completion-menu.multi-column-meta": meta_style, + } + ) + def _create_main_session(self, auto_suggest: bool, completekey: str) -> PromptSession[str]: """Create and return the main PromptSession for the application. @@ -755,6 +797,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), } if self.stdin.isatty() and self.stdout.isatty(): @@ -3578,6 +3621,7 @@ def read_input( key_bindings=self.main_session.key_bindings, input=self.main_session.input, output=self.main_session.output, + style=DynamicStyle(self._get_pt_style), ) return self._read_raw_input(prompt, temp_session) @@ -3596,6 +3640,7 @@ def read_secret( temp_session: PromptSession[str] = PromptSession( input=self.main_session.input, output=self.main_session.output, + style=DynamicStyle(self._get_pt_style), ) return self._read_raw_input(prompt, temp_session, is_password=True) diff --git a/cmd2/styles.py b/cmd2/styles.py index 15489d46e..4d015d30b 100644 --- a/cmd2/styles.py +++ b/cmd2/styles.py @@ -51,6 +51,8 @@ class Cmd2Style(StrEnum): """ COMMAND_LINE = "cmd2.example" # Command line examples in help text + COMPLETION_MENU_ITEM = "cmd2.completion_menu.item" # Selected completion item + COMPLETION_MENU_META = "cmd2.completion_menu.meta" # Selected completion help/meta text ERROR = "cmd2.error" # Error text (used by perror()) HELP_HEADER = "cmd2.help.header" # Help table header text HELP_LEADER = "cmd2.help.leader" # Text right before the help tables are listed @@ -63,6 +65,8 @@ class Cmd2Style(StrEnum): # Tightly coupled with the Cmd2Style enum. DEFAULT_CMD2_STYLES: dict[str, StyleType] = { Cmd2Style.COMMAND_LINE: Style(color=Color.CYAN, bold=True), + Cmd2Style.COMPLETION_MENU_ITEM: Style(color=Color.BLACK, bgcolor=Color.GREEN), + Cmd2Style.COMPLETION_MENU_META: Style(color=Color.BLACK, bgcolor=Color.LIGHT_GREEN), Cmd2Style.ERROR: Style(color=Color.BRIGHT_RED), Cmd2Style.HELP_HEADER: Style(color=Color.BRIGHT_GREEN), Cmd2Style.HELP_LEADER: Style(color=Color.CYAN), diff --git a/docs/features/completion.md b/docs/features/completion.md index 85143650b..1def16f23 100644 --- a/docs/features/completion.md +++ b/docs/features/completion.md @@ -116,6 +116,13 @@ demonstration of how this is used. [read_input](https://github.com/python-cmd2/cmd2/blob/main/examples/read_input.py) example for a demonstration. +## Custom Completion Menu Colors + +`cmd2` provides the ability to customize the foreground and background colors of the completion menu +items and their associated help text. See +[Customizing Completion Menu Colors](./theme.md#customizing-completion-menu-colors) in the Theme +documentation for more details. + ## For More Information See [cmd2's argparse_utils API](../api/argparse_utils.md) for a more detailed discussion of argparse diff --git a/docs/features/theme.md b/docs/features/theme.md index 064178fa7..cd348f061 100644 --- a/docs/features/theme.md +++ b/docs/features/theme.md @@ -6,5 +6,19 @@ information. You can use this to brand your application and set an overall consistent look and feel that is appealing to your user base. +## Customizing Completion Menu Colors + +`cmd2` leverages `prompt-toolkit` for its tab completion menu. You can customize the colors of the +completion menu by overriding the following styles in your `cmd2` theme: + +- `Cmd2Style.COMPLETION_MENU_ITEM`: The background and foreground color of the selected completion + item. +- `Cmd2Style.COMPLETION_MENU_META`: The background and foreground color of the selected completion + item's help/meta text. + +By default, these are styled with black text on a green background to provide contrast. + +## Example + See [rich_theme.py](https://github.com/python-cmd2/cmd2/blob/main/examples/rich_theme.py) for a simple example of configuring a custom theme for your `cmd2` application. diff --git a/docs/upgrades.md b/docs/upgrades.md index a89e248f2..2d10a7a44 100644 --- a/docs/upgrades.md +++ b/docs/upgrades.md @@ -46,6 +46,16 @@ See the example for a demonstration of how to implement a background thread that refreshes the toolbar periodically. +### Custom Completion Menu Colors + +`cmd2` now leverages `prompt-toolkit` for its tab completion menu and provides the ability to +customize its appearance using the `cmd2` theme. + +- **Customization**: Override the `Cmd2Style.COMPLETION_MENU_ITEM` and + `Cmd2Style.COMPLETION_MENU_META` styles using `cmd2.rich_utils.set_theme()`. See + [Customizing Completion Menu Colors](features/theme.md#customizing-completion-menu-colors) for + more details. + ### Deleted Modules Removed `rl_utils.py` and `terminal_utils.py` since `prompt-toolkit` provides this functionality.