From dc45593b5cf96d585c0a46fa70c392de0dae35a9 Mon Sep 17 00:00:00 2001 From: neeraj netrakar Date: Tue, 20 Oct 2026 16:56:53 +0530 Subject: [PATCH] Strip 8-bit C1 controls in Vt100_Output.write and set_title --- src/prompt_toolkit/output/vt100.py | 11 ++++++++++- tests/test_vt100_output.py | 25 ++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/prompt_toolkit/output/vt100.py b/src/prompt_toolkit/output/vt100.py index b2712254f..eb19fda8a 100644 --- a/src/prompt_toolkit/output/vt100.py +++ b/src/prompt_toolkit/output/vt100.py @@ -11,6 +11,7 @@ import io import os +import re import sys from collections.abc import Callable, Hashable, Iterable, Sequence from typing import TextIO @@ -254,6 +255,10 @@ def __missing__(self, value: tuple[int, int, int]) -> int: _16_bg_colors = _16ColorCache(bg=True) _256_colors = _256ColorCache() +# 8-bit C1 control characters. Terminals in UTF-8 mode can interpret these +# like their 7-bit "ESC x" equivalents (e.g. "\x9b" is CSI). +_C1_CONTROLS_RE = re.compile("[\x80-\x9f]") + class _EscapeCodeCache(dict[Attrs, str]): """ @@ -520,7 +525,10 @@ def write(self, data: str) -> None: Write text to output. (Removes vt100 escape codes. -- used for safely writing text.) """ - self._buffer.append(data.replace("\x1b", "?")) + data = data.replace("\x1b", "?") + if not data.isascii(): + data = _C1_CONTROLS_RE.sub("?", data) + self._buffer.append(data) def set_title(self, title: str) -> None: """ @@ -530,6 +538,7 @@ def set_title(self, title: str) -> None: "linux", "eterm-color", ): # Not supported by the Linux console. + title = _C1_CONTROLS_RE.sub("", title) self.write_raw( "\x1b]2;{}\x07".format(title.replace("\x1b", "").replace("\x07", "")) ) diff --git a/tests/test_vt100_output.py b/tests/test_vt100_output.py index ca62be54e..2b63c87b2 100644 --- a/tests/test_vt100_output.py +++ b/tests/test_vt100_output.py @@ -1,6 +1,12 @@ from __future__ import annotations -from prompt_toolkit.output.vt100 import _256_colors, _get_closest_ansi_color +import io + +from prompt_toolkit.output.vt100 import ( + Vt100_Output, + _256_colors, + _get_closest_ansi_color, +) def test_get_closest_ansi_color(): @@ -29,3 +35,20 @@ def test_256_colors(): # Grayscale assert _256_colors[(8, 8, 8)] == 232 # First grayscale level assert _256_colors[(238, 238, 238)] == 255 # Last grayscale level + + +def _make_output() -> Vt100_Output: + return Vt100_Output(io.StringIO(), lambda: None, term="xterm") + + +def test_write_strips_c1_controls(): + # 8-bit CSI/OSC/ST must not reach the terminal, same as ESC. + output = _make_output() + output.write("a\x1b[31mb\x9b31mc\x9d0;x\x9cd\u2500") + assert output._buffer == ["a?[31mb?31mc?0;x?d\u2500"] + + +def test_set_title_strips_c1_controls(): + output = _make_output() + output.set_title("title\x9c\x9b2J") + assert output._buffer == ["\x1b]2;title2J\x07"]