Skip to content
89 changes: 89 additions & 0 deletions tests/test_rich_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,92 @@ def main(bar: str):
result = runner.invoke(app, ["--help"])
assert "Usage" in result.stdout
assert "BAR" in result.stdout


def test_make_rich_text_with_ansi_escape_sequences():
from typer.rich_utils import Text, _make_rich_text

ansi_text = "This is \x1b[4munderlined\x1b[0m text"
result = _make_rich_text(text=ansi_text, markup_mode=None)

assert isinstance(result, Text)
assert "\x1b[" not in result.plain
assert "underlined" in result.plain

mixed_text = "Start \x1b[31mred\x1b[0m middle \x1b[32mgreen\x1b[0m end"
result = _make_rich_text(text=mixed_text, markup_mode=None)
assert isinstance(result, Text)
assert "\x1b[" not in result.plain
assert "red" in result.plain
assert "green" in result.plain

fake_ansi = "This contains \x1b[ but not a complete sequence"
result = _make_rich_text(text=fake_ansi, markup_mode=None)
assert isinstance(result, Text)
assert "\x1b[" not in result.plain
assert "This contains " in result.plain


def test_make_rich_text_with_typer_style_in_help():
app = typer.Typer()

@app.command()
def example(
a: str = typer.Option(help="This is A"),
b: str = typer.Option(help=f"This is {typer.style('B', underline=True)}"),
):
"""Example command with styled help text."""
pass # pragma: no cover

result = runner.invoke(app, ["--help"])

assert result.exit_code == 0
assert "This is A" in result.stdout
assert "This is B" in result.stdout
assert "\x1b[" not in result.stdout


def test_help_table_alignment_with_styled_text():
app = typer.Typer()

@app.command()
def example(
a: str = typer.Option(help="This is A"),
b: str = typer.Option(help=f"This is {typer.style('B', underline=True)}"),
c: str = typer.Option(help="This is C"),
):
"""Example command with styled help text."""
pass # pragma: no cover

result = runner.invoke(app, ["--help"])

assert result.exit_code == 0

lines = result.stdout.split("\n")

option_a_line = None
option_b_line = None
option_c_line = None

for line in lines:
if "--a" in line and "This is A" in line:
option_a_line = line
elif "--b" in line and "This is B" in line:
option_b_line = line
elif "--c" in line and "This is C" in line:
option_c_line = line

assert option_a_line is not None, "Option A line not found"
assert option_b_line is not None, "Option B line not found"
assert option_c_line is not None, "Option C line not found"

def find_right_boundary_pos(line):
return line.rfind("|")

pos_a = find_right_boundary_pos(option_a_line)
pos_b = find_right_boundary_pos(option_b_line)
pos_c = find_right_boundary_pos(option_c_line)

assert pos_a == pos_b == pos_c, (
f"Right boundaries not aligned: A={pos_a}, B={pos_b}, C={pos_c}"
)
11 changes: 10 additions & 1 deletion typer/rich_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
MARKUP_MODE_MARKDOWN = "markdown"
MARKUP_MODE_RICH = "rich"
_RICH_HELP_PANEL_NAME = "rich_help_panel"
ANSI_PREFIX = "\033["

MarkupMode = Literal["markdown", "rich", None]

Expand Down Expand Up @@ -129,6 +130,10 @@ class NegativeOptionHighlighter(RegexHighlighter):
negative_highlighter = NegativeOptionHighlighter()


def _has_ansi_character(text: str) -> bool:
return ANSI_PREFIX in text


def _get_rich_console(stderr: bool = False) -> Console:
return Console(
theme=Theme(
Expand Down Expand Up @@ -167,7 +172,11 @@ def _make_rich_text(
if markup_mode == MARKUP_MODE_RICH:
return highlighter(Text.from_markup(text, style=style))
else:
return highlighter(Text(text, style=style))
# if _ANSI_ESCAPE_SEQUENCE in text:
if _has_ansi_character(text):
return highlighter(Text.from_ansi(text, style=style))
else:
return highlighter(Text(text, style=style))


@group()
Expand Down
Loading