diff --git a/Lib/_pyrepl/utils.py b/Lib/_pyrepl/utils.py index 1a697ad61c8e89..716ac7985854ae 100644 --- a/Lib/_pyrepl/utils.py +++ b/Lib/_pyrepl/utils.py @@ -16,7 +16,21 @@ from .types import CharBuffer, CharWidths from .trace import trace -ANSI_ESCAPE_SEQUENCE = re.compile(r"\x1b\[[ -@]*[A-~]") +ANSI_ESCAPE_SEQUENCE = re.compile( + r""" + \x1b + (?: + # CSI: ESC [ followed by parameter/intermediate bytes and a final byte. + # For example, ESC [ 31 m ("\x1b[31m") selects red foreground text. + \[ [ -@]* [A-~] + | + # OSC: ESC ] followed by a payload terminated by BEL or ST (ESC \). + # For example, ESC ] 0 ; title BEL ("\x1b]0;title\x07") sets the title. + \] [^\x1b\x07]* (?: \x07 | \x1b\\ ) + ) + """, + re.X, +) ZERO_WIDTH_BRACKET = re.compile(r"\x01.*?\x02") ZERO_WIDTH_TRANS = str.maketrans({"\x01": "", "\x02": ""}) IDENTIFIERS_AFTER = {"def", "class"} diff --git a/Lib/test/test_pyrepl/test_utils.py b/Lib/test/test_pyrepl/test_utils.py index b621071f1249c3..e91b82cfc6d21b 100644 --- a/Lib/test/test_pyrepl/test_utils.py +++ b/Lib/test/test_pyrepl/test_utils.py @@ -47,6 +47,14 @@ def test_wlen(self): self.assertEqual(wlen('e\N{COMBINING ACUTE ACCENT}'), 1) self.assertEqual(wlen('a\N{ZERO WIDTH JOINER}b'), 2) + def test_wlen_with_osc_sequence(self): + prompt = ">>> " + self.assertEqual(wlen("\x1b]633;A\x07" + prompt), len(prompt)) + + def test_wlen_with_unterminated_osc_sequence_as_individual_characters(self): + # \x1b is treated as a single character (ESC) + self.assertEqual(wlen("\x1b]633;A"), len("\x1b]633;A")) + def test_prev_next_window(self): def gen_normal(): yield 1 diff --git a/Misc/NEWS.d/next/Library/2026-10-29-23-24-59.gh-issue-157928.vjwtXc.rst b/Misc/NEWS.d/next/Library/2026-10-29-23-24-59.gh-issue-157928.vjwtXc.rst new file mode 100644 index 00000000000000..dfa7a122a9e22e --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-29-23-24-59.gh-issue-157928.vjwtXc.rst @@ -0,0 +1 @@ +Correctly detect and handle OSC sequences in PyREPL.