From a51b4b42ba6219f5b31cead8a34ebb7535828631 Mon Sep 17 00:00:00 2001 From: "P. McD0nald" Date: Fri, 24 Apr 2026 10:11:51 +0000 Subject: [PATCH] fix(ansi): handle CRLF line endings in Text.from_ansi Fixes #4090 The AnsiDecoder.decode() method splits input on \n and then calls decode_line() which strips \n and uses rsplit('\r', 1)[-1] to handle carriage returns. When the input contains CRLF (\r\n), the split preserves the \r in the line content, and then rsplit('\r', 1)[-1] takes the empty string after the final \r, discarding all text. This fix normalizes \r\n to \n before splitting, ensuring that CRLF sequences are treated as plain line endings rather than terminal carriage returns that overwrite content. Added test_decode_crlf to cover CRLF, mixed line endings, and consecutive CRLF sequences. --- rich/ansi.py | 1 + tests/test_ansi.py | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/rich/ansi.py b/rich/ansi.py index 7bbcb0bfc..db51c0f00 100644 --- a/rich/ansi.py +++ b/rich/ansi.py @@ -132,6 +132,7 @@ def decode(self, terminal_text: str) -> Iterable[Text]: Yields: Text: Marked up Text. """ + terminal_text = terminal_text.replace("\r\n", "\n") for line in re.split(r"(?<=\n)", terminal_text): yield self.decode_line(line.rstrip("\n")) diff --git a/tests/test_ansi.py b/tests/test_ansi.py index 21e20b440..56c9d9968 100644 --- a/tests/test_ansi.py +++ b/tests/test_ansi.py @@ -102,3 +102,16 @@ def test_decode_newlines(): assert Text.from_ansi("Hello\nWorld\n").plain == "Hello\nWorld\n" assert Text.from_ansi("Hello\nWorld\n\n").plain == "Hello\nWorld\n\n" assert Text.from_ansi("\nHello\nWorld\n\n").plain == "\nHello\nWorld\n\n" + + +def test_decode_crlf(): + """Test CRLF line endings are handled correctly. + Regression test for https://github.com/Textualize/rich/issues/4090 + """ + assert Text.from_ansi("Hello\r\nWorld\r\n").plain == "Hello\nWorld\n" + assert Text.from_ansi("Hello\r\n").plain == "Hello\n" + assert Text.from_ansi("\r\nHello\r\n").plain == "\nHello\n" + assert Text.from_ansi("Hello\r\n\r\nWorld").plain == "Hello\n\nWorld" + # Mixed line endings + assert Text.from_ansi("Hello\nWorld\r\n").plain == "Hello\nWorld\n" + assert Text.from_ansi("Hello\r\nWorld\n").plain == "Hello\nWorld\n"