diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py index c99ca82a2..bdf94da46 100644 --- a/cmd2/argparse_custom.py +++ b/cmd2/argparse_custom.py @@ -398,7 +398,7 @@ def __init__(self, value: object, descriptive_data: Sequence[Any], *args: Any) - # Make sure all objects are renderable by a Rich table. renderable_data = [obj if is_renderable(obj) else str(obj) for obj in descriptive_data] - # Convert objects with ANSI styles to Rich Text for correct display width. + # Convert strings containing ANSI style sequences to Rich Text objects for correct display width. self.descriptive_data = ru.prepare_objects_for_rendering(*renderable_data) # Save the original value to support CompletionItems as argparse choices. diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index d60b752bb..2eceae2f8 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -1191,12 +1191,12 @@ def _completion_supported(self) -> bool: @property def visible_prompt(self) -> str: - """Read-only property to get the visible prompt with any ANSI style escape codes stripped. + """Read-only property to get the visible prompt with any ANSI style sequences stripped. - Used by transcript testing to make it easier and more reliable when users are doing things like coloring the - prompt using ANSI color codes. + Used by transcript testing to make it easier and more reliable when users are doing things like + coloring the prompt. - :return: prompt stripped of any ANSI escape codes + :return: the stripped prompt """ return su.strip_style(self.prompt) @@ -4214,7 +4214,7 @@ def _print_documented_command_topics(self, header: str, cmds: list[str], verbose def render_columns(self, str_list: list[str] | None, display_width: int = 80) -> str: """Render a list of single-line strings as a compact set of columns. - This method correctly handles strings containing ANSI escape codes and + This method correctly handles strings containing ANSI style sequences and full-width characters (like those used in CJK languages). Each column is only as wide as necessary and columns are separated by two spaces. diff --git a/cmd2/rich_utils.py b/cmd2/rich_utils.py index 20001df13..dfef892f7 100644 --- a/cmd2/rich_utils.py +++ b/cmd2/rich_utils.py @@ -1,5 +1,6 @@ """Provides common utilities to support Rich in cmd2-based applications.""" +import re from collections.abc import Mapping from enum import Enum from typing import ( @@ -28,13 +29,16 @@ from .styles import DEFAULT_CMD2_STYLES +# A compiled regular expression to detect ANSI style sequences. +ANSI_STYLE_SEQUENCE_RE = re.compile(r"\x1b\[[0-9;?]*m") + class AllowStyle(Enum): """Values for ``cmd2.rich_utils.ALLOW_STYLE``.""" - ALWAYS = 'Always' # Always output ANSI style sequences - NEVER = 'Never' # Remove ANSI style sequences from all output - TERMINAL = 'Terminal' # Remove ANSI style sequences if the output is not going to the terminal + ALWAYS = "Always" # Always output ANSI style sequences + NEVER = "Never" # Remove ANSI style sequences from all output + TERMINAL = "Terminal" # Remove ANSI style sequences if the output is not going to the terminal def __str__(self) -> str: """Return value instead of enum name for printing in cmd2's set command.""" @@ -234,7 +238,7 @@ def rich_text_to_string(text: Text) -> str: """Convert a Rich Text object to a string. This function's purpose is to render a Rich Text object, including any styles (e.g., color, bold), - to a plain Python string with ANSI escape codes. It differs from `text.plain`, which strips + to a plain Python string with ANSI style sequences. It differs from `text.plain`, which strips all formatting. :param text: the text object to convert @@ -259,7 +263,7 @@ def rich_text_to_string(text: Text) -> str: def string_to_rich_text(text: str) -> Text: - r"""Create a Text object from a string which can contain ANSI escape codes. + r"""Create a Rich Text object from a string which can contain ANSI style sequences. This wraps rich.Text.from_ansi() to handle an issue where it removes the trailing line break from a string (e.g. "Hello\n" becomes "Hello"). @@ -323,9 +327,9 @@ def prepare_objects_for_rendering(*objects: Any) -> tuple[Any, ...]: """Prepare a tuple of objects for printing by Rich's Console.print(). This function converts any non-Rich object whose string representation contains - ANSI style codes into a rich.Text object. This ensures correct display width - calculation, as Rich can then properly parse and account for the non-printing - ANSI codes. All other objects are left untouched, allowing Rich's native + ANSI style sequences into a Rich Text object. This ensures correct display width + calculation, as Rich can then properly parse and account for these non-printing + codes. All other objects are left untouched, allowing Rich's native renderers to handle them. :param objects: objects to prepare @@ -342,12 +346,10 @@ def prepare_objects_for_rendering(*objects: Any) -> tuple[Any, ...]: if isinstance(renderable, ConsoleRenderable): continue - # Check if the object's string representation contains ANSI styles, and if so, - # replace it with a Rich Text object for correct width calculation. renderable_as_str = str(renderable) - renderable_as_text = string_to_rich_text(renderable_as_str) - if renderable_as_text.plain != renderable_as_str: - object_list[i] = renderable_as_text + # Check for any ANSI style sequences in the string. + if ANSI_STYLE_SEQUENCE_RE.search(renderable_as_str): + object_list[i] = string_to_rich_text(renderable_as_str) return tuple(object_list) diff --git a/cmd2/string_utils.py b/cmd2/string_utils.py index a77eb5f6b..9b9d590c7 100644 --- a/cmd2/string_utils.py +++ b/cmd2/string_utils.py @@ -1,7 +1,7 @@ """Provides string utility functions. This module offers a collection of string utility functions built on the Rich library. -These utilities are designed to correctly handle strings with ANSI escape codes and +These utilities are designed to correctly handle strings with ANSI style sequences and full-width characters (like those used in CJK languages). """ @@ -94,13 +94,12 @@ def stylize(val: str, style: StyleType) -> str: def strip_style(val: str) -> str: - """Strip all ANSI styles from a string. + """Strip all ANSI style sequences from a string. :param val: string to be stripped :return: the stripped string """ - text = ru.string_to_rich_text(val) - return text.plain + return ru.ANSI_STYLE_SEQUENCE_RE.sub("", val) def str_width(val: str) -> int: @@ -163,4 +162,4 @@ def norm_fold(val: str) -> str: """ import unicodedata - return unicodedata.normalize('NFC', val).casefold() + return unicodedata.normalize("NFC", val).casefold()