Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).


## [Unreleased]

### Fixed
- Fixed background color carrying over onto the following line when soft wrapping is enabled. https://github.com/Textualize/rich/issues/3838

## [14.1.0] - 2025-06-25

### Changed
Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,4 @@ The following people have contributed to the development of Rich:
- [Jonathan Helmus](https://github.com/jjhelmus)
- [Brandon Capener](https://github.com/bcapener)
- [Alex Zheng](https://github.com/alexzheng111)
- [Kevin Van Brunt](https://github.com/kmvanbrunt)
5 changes: 3 additions & 2 deletions rich/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -1735,14 +1735,15 @@ def print(
> 1
):
new_segments.insert(0, Segment.line())
buffer_extend = self._buffer.extend
if crop:
buffer_extend = self._buffer.extend
for line in Segment.split_and_crop_lines(
new_segments, self.width, pad=False
):
buffer_extend(line)
else:
self._buffer.extend(new_segments)
for line in Segment.split_lines(new_segments, include_new_lines=True):
buffer_extend(line)

def print_json(
self,
Expand Down
11 changes: 10 additions & 1 deletion rich/segment.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,17 +247,23 @@ def filter_control(
return filterfalse(attrgetter("control"), segments)

@classmethod
def split_lines(cls, segments: Iterable["Segment"]) -> Iterable[List["Segment"]]:
def split_lines(
cls,
segments: Iterable["Segment"],
include_new_lines: bool = False,
) -> Iterable[List["Segment"]]:
"""Split a sequence of segments in to a list of lines.

Args:
segments (Iterable[Segment]): Segments potentially containing line feeds.
include_new_lines (bool): Include newline segments in results. Defaults to False.

Yields:
Iterable[List[Segment]]: Iterable of segment lists, one per line.
"""
line: List[Segment] = []
append = line.append
new_line_segment = cls.line()

for segment in segments:
if "\n" in segment.text and not segment.control:
Expand All @@ -267,6 +273,8 @@ def split_lines(cls, segments: Iterable["Segment"]) -> Iterable[List["Segment"]]
if _text:
append(cls(_text, style))
if new_line:
if include_new_lines:
line.append(new_line_segment)
yield line
line = []
append = line.append
Expand All @@ -292,6 +300,7 @@ def split_and_crop_lines(
length (int): Desired line length.
style (Style, optional): Style to use for any padding.
pad (bool): Enable padding of lines that are less than `length`.
include_new_lines (bool): Include newline segments in results. Defaults to True.

Returns:
Iterable[List[Segment]]: An iterable of lines of segments.
Expand Down
59 changes: 59 additions & 0 deletions tests/test_console.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,65 @@ def test_print_text_multiple() -> None:
assert console.file.getvalue() == "\x1b[1mfoo\x1b[0m bar baz\n"


def test_print_soft_wrap_no_styled_newlines() -> None:
"""Test that style is not applied to newlines when soft wrapping."""

str1 = "line1\nline2\n"
str2 = "line5\nline6\n"
sep = "(sep1)\n(sep2)\n"
end = "(end1)\n(end2)\n"
style = "blue on white"

# All newlines should appear outside of ANSI style sequences.
expected = (
"\x1b[34;47mline1\x1b[0m\n"
"\x1b[34;47mline2\x1b[0m\n"
"\x1b[34;47m(sep1)\x1b[0m\n"
"\x1b[34;47m(sep2)\x1b[0m\n"
"\x1b[34;47mline5\x1b[0m\n"
"\x1b[34;47mline6\x1b[0m\n"
"\x1b[34;47m(end1)\x1b[0m\n"
"\x1b[34;47m(end2)\x1b[0m\n"
)

console = Console(color_system="truecolor")
with console.capture() as capture:
console.print(str1, str2, sep=sep, end=end, style=style, soft_wrap=True)
assert capture.get() == expected


def test_print_word_wrap_no_styled_newlines() -> None:
"""
Test that word wrapping does not insert styled newlines
or apply style to existing newlines.
"""
str1 = "this\nwill word wrap\n"
str2 = "and\nso will this\n"
sep = "(sep1)\n(sep2)\n"
end = "(end1)\n(end2)\n"
style = "blue on white"

# All newlines should appear outside of ANSI style sequences.
expected = (
"\x1b[34;47mthis\x1b[0m\n"
"\x1b[34;47mwill word \x1b[0m\n"
"\x1b[34;47mwrap\x1b[0m\n"
"\x1b[34;47m(sep1)\x1b[0m\n"
"\x1b[34;47m(sep2)\x1b[0m\n"
"\x1b[34;47mand\x1b[0m\n"
"\x1b[34;47mso will \x1b[0m\n"
"\x1b[34;47mthis\x1b[0m\n"
"\x1b[34;47m(end1)\x1b[0m\n"
"\x1b[34;47m(end2)\x1b[0m\n"
)

# Set a width which will cause word wrapping.
console = Console(color_system="truecolor", width=10)
with console.capture() as capture:
console.print(str1, str2, sep=sep, end=end, style=style, soft_wrap=False)
assert capture.get() == expected


def test_print_json() -> None:
console = Console(file=io.StringIO(), color_system="truecolor")
console.print_json('[false, true, null, "foo"]', indent=4)
Expand Down
8 changes: 8 additions & 0 deletions tests/test_segment.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ def test_split_lines():
assert list(Segment.split_lines(lines)) == [[Segment("Hello")], [Segment("World")]]


def test_split_lines_include_newlines():
lines = [Segment("Hello\nWorld")]
assert list(Segment.split_lines(lines, include_new_lines=True)) == [
[Segment("Hello"), Segment("\n", None)],
[Segment("World")],
]


def test_split_and_crop_lines():
assert list(
Segment.split_and_crop_lines([Segment("Hello\nWorld!\n"), Segment("foo")], 4)
Expand Down