Skip to content

Preserve terminal styles when dropping whitespace-only wrapped lines - #310

Closed
manav4u wants to merge 5 commits into
jquast:masterfrom
manav4u:master
Closed

manav4u wants to merge 5 commits into
jquast:masterfrom
manav4u:master

Conversation

@manav4u

@manav4u manav4u commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

At width 1, leading whitespace combined with SGR sequences can leave a visually empty first line or drop the opening style:

from wcwidth import wrap

wrap('\x1b[31m a\x1b[0m', 1)
# before: ['a\x1b[0m']
# after:  ['\x1b[31ma\x1b[0m']

wrap('\x1b[31m a-b\x1b[0m', 1)
# before: a sequence-only first line, then a, -, b
# after: three individually red lines, a, -, b

This extends the whitespace-only-line behavior from #292 to styled text. Transfer sequences from a discarded whitespace chunk to the next chunk when there is no current line to receive them. Check visible whitespace before appending a line, retaining sequences for the next chunk instead of emitting a whitespace-only line.

Only perform the additional visible-content scan for lines containing escape sequences, leaving plain-text whitespace on the existing path.

Apply the corresponding change to libwcwidth. Its wrapper intentionally does not propagate SGR per line, so the C tests check its existing raw-sequence behavior rather than Python's default per-line resets.

Tests cover both SGR propagation settings, preserving whitespace when requested, and initial indentation. Six Python regressions fail before the fix; the C regression fails for all three inputs before the fix.

Validation on Linux/Python 3.12.15:

  • Python suite with compiled extension: 1,790 passed, 12 skipped.
  • Pure-Python suite: 1,754 passed, 48 skipped.
  • Native C suite with AddressSanitizer and UndefinedBehaviorSanitizer: 12 executables, 91 tests, zero failures.
  • Strict source MyPy, changed-file Flake8 and isort, pydocstyle, Pylint 10/10 for textwrap.py, and diff whitespace checks pass.

Other Python versions, documentation builds and upstream CI have not been run. No performance improvement is claimed.

Prepared with AI assistance.

@jquast

jquast commented Nov 7, 2026

Copy link
Copy Markdown
Owner

This is only a problem for width 1, no other width?

@manav4u

manav4u commented Nov 7, 2026

Copy link
Copy Markdown
Contributor Author

No, it also affects wider widths when the leading whitespace fills the available line. I used width 1 to keep the original examples small.

For example, at width 2:

wrap('\x1b[31m  abc\x1b[0m', 2)
# before: ['\x1b[31m\x1b[0m', '\x1b[31mab\x1b[0m', '\x1b[31mc\x1b[0m']
# after:  ['\x1b[31mab\x1b[0m', '\x1b[31mc\x1b[0m']

And at width 5:

wrap('\x1b[31m     hello world\x1b[0m', 5)
# before: ['hello', 'world\x1b[0m']
# after:  ['\x1b[31mhello\x1b[0m', '\x1b[31mworld\x1b[0m']

The first emits a sequence-only line; the second loses the opening color. I also checked widths 3, 10 and 20. The current patch fixes these without a source change. The regression tests in the PR should cover wider widths too.

jquast added a commit that referenced this pull request Nov 9, 2026
…perf (#316)

Follow-up of #310 with small change about unrolling inner 'if' clause,

> At width 1, leading whitespace combined with SGR sequences can leave a visually empty first line or drop the opening style:

---------

Co-authored-by: Manav <manavpatilmp19@gmail.com>
@jquast jquast closed this in #316 Nov 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants