From ffaadb687d79775ad28baaecbe7bde11079ba5ce Mon Sep 17 00:00:00 2001 From: Sasha Mitchell Date: Thu, 1 Oct 2026 19:24:18 +0700 Subject: [PATCH] Escape a C1 CSI in ANSI.format. \x9b is the C1 form of ESC [, and an argument could set a color. --- CHANGELOG | 6 +++++ src/prompt_toolkit/formatted_text/ansi.py | 3 ++- tests/test_formatted_text.py | 32 +++++++++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) 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"))