diff --git a/CHANGELOG b/CHANGELOG index e8afc07cb..6ae404c6f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,12 @@ CHANGELOG ========= +Unreleased +---------- + +Fixes: +- Escape a C1 CSI in ``ANSI.format`` so an argument cannot set a color (#2113). + 3.0.53: 2026-07-26 ------------------ diff --git a/src/prompt_toolkit/formatted_text/ansi.py b/src/prompt_toolkit/formatted_text/ansi.py index 2f355c2b7..98d9fde35 100644 --- a/src/prompt_toolkit/formatted_text/ansi.py +++ b/src/prompt_toolkit/formatted_text/ansi.py @@ -291,7 +291,8 @@ def ansi_escape(text: object) -> str: """ Replace characters with a special meaning. """ - return str(text).replace("\x1b", "?").replace("\b", "?") + # \x9b is the C1 form of ESC [. Leaving it lets format() set a color. + return str(text).replace("\x1b", "?").replace("\x9b", "?").replace("\b", "?") class ANSIFormatter(Formatter): diff --git a/tests/test_formatted_text.py b/tests/test_formatted_text.py index a111a7f20..2e926bd2f 100644 --- a/tests/test_formatted_text.py +++ b/tests/test_formatted_text.py @@ -210,6 +210,38 @@ def test_ansi_interpolation(): ] +def test_ansi_c1_interpolation(): + # A C1 CSI in an argument must not set a color. The template still can. + value = ANSI("\x9b32mOK") + assert to_formatted_text(value) == [ + ("ansigreen", "O"), + ("ansigreen", "K"), + ] + + value = ANSI("x{}").format("\x9b31mRED") + assert to_formatted_text(value) == [ + ("", "x"), + ("", "?"), + ("", "3"), + ("", "1"), + ("", "m"), + ("", "R"), + ("", "E"), + ("", "D"), + ] + + value = ANSI("%s") % "\x9b31mRED" + assert to_formatted_text(value) == [ + ("", "?"), + ("", "3"), + ("", "1"), + ("", "m"), + ("", "R"), + ("", "E"), + ("", "D"), + ] + + def test_interpolation(): value = Template(" {} ").format(HTML("hello"))