``textwrap.TextWrapper`` variant that measures widths by visible character count. ANSI escape sequences embedded in chunks, indents, or the placeholder are excluded from the width budget. Without this, styled help text (a styled ``Usage:`` prefix, a colorized option name, ...) would
| 36 | |
| 37 | |
| 38 | class TextWrapper(textwrap.TextWrapper): |
| 39 | """``textwrap.TextWrapper`` variant that measures widths by visible |
| 40 | character count. |
| 41 | |
| 42 | ANSI escape sequences embedded in chunks, indents, or the placeholder are |
| 43 | excluded from the width budget. Without this, styled help text (a styled |
| 44 | ``Usage:`` prefix, a colorized option name, ...) would be wrapped earlier |
| 45 | than its visible length warrants and tokens would split mid-word. |
| 46 | """ |
| 47 | |
| 48 | def _handle_long_word( |
| 49 | self, |
| 50 | reversed_chunks: list[str], |
| 51 | cur_line: list[str], |
| 52 | cur_len: int, |
| 53 | width: int, |
| 54 | ) -> None: |
| 55 | space_left = max(width - cur_len, 1) |
| 56 | |
| 57 | if self.break_long_words: |
| 58 | last = reversed_chunks[-1] |
| 59 | cut = _truncate_visible(last, space_left) |
| 60 | res = last[len(cut) :] |
| 61 | cur_line.append(cut) |
| 62 | reversed_chunks[-1] = res |
| 63 | elif not cur_line: |
| 64 | cur_line.append(reversed_chunks.pop()) |
| 65 | |
| 66 | def _wrap_chunks(self, chunks: list[str]) -> list[str]: |
| 67 | """Wrap chunks counting widths in visible characters. |
| 68 | |
| 69 | Mirrors the algorithm of :meth:`textwrap.TextWrapper._wrap_chunks` |
| 70 | with every width measurement routed through |
| 71 | :func:`click._compat.term_len` instead of :func:`len`, so ANSI escape |
| 72 | bytes in chunks, indents, or the placeholder do not inflate the count. |
| 73 | |
| 74 | .. seealso:: |
| 75 | :class:`textwrap.TextWrapper` in the Python standard library documentation: |
| 76 | https://docs.python.org/3/library/textwrap.html#textwrap.TextWrapper |
| 77 | |
| 78 | Reference implementation in CPython: |
| 79 | https://github.com/python/cpython/blob/main/Lib/textwrap.py |
| 80 | """ |
| 81 | lines: list[str] = [] |
| 82 | if self.width <= 0: |
| 83 | raise ValueError(f"invalid width {self.width!r} (must be > 0)") |
| 84 | if self.max_lines is not None: |
| 85 | if self.max_lines > 1: |
| 86 | indent = self.subsequent_indent |
| 87 | else: |
| 88 | indent = self.initial_indent |
| 89 | if term_len(indent) + term_len(self.placeholder.lstrip()) > self.width: |
| 90 | raise ValueError("placeholder too large for max width") |
| 91 | |
| 92 | chunks.reverse() |
| 93 | |
| 94 | while chunks: |
| 95 | cur_line: list[str] = [] |