Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/wcwidth/textwrap.py: 16%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2Sequence-aware text wrapping functions.
4This module provides functions for wrapping text that may contain terminal escape sequences, with
5proper handling of Unicode grapheme clusters and character display widths.
6"""
8from __future__ import annotations
10# std imports
11import secrets
12import textwrap
14from typing import TYPE_CHECKING, Optional
16# local
17from ._width import width as wcwidth_width
18from .grapheme import iter_graphemes
19from .hyperlink import HyperlinkParams
20from .sgr_state import propagate_sgr as _propagate_sgr
21from .escape_sequences import ZERO_WIDTH_PATTERN, iter_sequences
23if TYPE_CHECKING: # pragma: no cover
24 from typing import Any, Literal
27class SequenceTextWrapper(textwrap.TextWrapper):
28 """
29 Sequence-aware text wrapper extending :class:`textwrap.TextWrapper`.
31 This wrapper properly handles terminal escape sequences and Unicode grapheme clusters when
32 calculating text width for wrapping.
34 This implementation is based on the SequenceTextWrapper from the 'blessed' library, with
35 contributions from Avram Lubkin and grayjk.
37 The key difference from the blessed implementation is the addition of grapheme cluster support
38 via :func:`~.iter_graphemes`, providing width calculation for ZWJ emoji sequences, VS-16 emojis
39 and variations, regional indicator flags, and combining characters.
41 OSC 8 hyperlinks are handled specially: when a hyperlink must span multiple lines, each line
42 receives complete open/close sequences with a shared ``id`` parameter, ensuring terminals
43 treat the fragments as a single hyperlink for hover underlining. If the original hyperlink
44 already has an ``id`` parameter, it is preserved; otherwise, one is generated.
45 """
47 def __init__(self, width: int = 70, *,
48 control_codes: Literal['parse', 'strict', 'ignore'] = 'parse',
49 tabsize: int = 8,
50 ambiguous_width: int = 1,
51 term_program: bool | str = False,
52 **kwargs: Any) -> None:
53 """
54 Initialize the wrapper.
56 :param width: Maximum line width in display cells.
57 :param control_codes: How to handle control sequences (see :func:`~.width`).
58 :param tabsize: Tab stop width for tab expansion.
59 :param ambiguous_width: Width to use for East Asian Ambiguous (A) characters.
60 :param term_program: Terminal software identifier for table correction.
61 ``False`` (default) disables override lookup. ``True`` reads the
62 ``TERM_PROGRAM`` or ``TERM`` environment variable for auto-detection.
63 Accepts a canonical terminal name matching :func:`list_term_programs`,
64 such as from XTVERSION_, ENQ_, or ``TERM_PROGRAM``.
66 .. versionadded:: 0.8.0
67 :param kwargs: Additional arguments passed to :class:`textwrap.TextWrapper`.
68 """
69 super().__init__(width=width, **kwargs)
70 self.control_codes = control_codes
71 self.tabsize = tabsize
72 self.ambiguous_width = ambiguous_width
73 self.term_program = term_program
75 @staticmethod
76 def _next_hyperlink_id() -> str:
77 """Generate unique hyperlink id as 8-character hex string."""
78 return secrets.token_hex(4)
80 def _width(self, text: str) -> int:
81 """Measure text width accounting for sequences."""
82 return wcwidth_width(text, control_codes=self.control_codes, tabsize=self.tabsize,
83 ambiguous_width=self.ambiguous_width,
84 term_program=self.term_program)
86 def _strip_sequences(self, text: str) -> str:
87 """Strip all terminal sequences from text."""
88 result = []
89 for segment, is_seq in iter_sequences(text):
90 if not is_seq:
91 result.append(segment)
92 return ''.join(result)
94 def _extract_sequences(self, text: str) -> str:
95 """Extract only terminal sequences from text."""
96 result = []
97 for segment, is_seq in iter_sequences(text):
98 if is_seq:
99 result.append(segment)
100 return ''.join(result)
102 def _split(self, text: str) -> list[str]: # pylint: disable=too-many-locals
103 r"""
104 Sequence-aware variant of :meth:`textwrap.TextWrapper._split`.
106 This method ensures that terminal escape sequences don't interfere with the text splitting
107 logic, particularly for hyphen-based word breaking. It builds a position mapping from
108 stripped text to original text, calls the parent's _split on stripped text, then maps chunks
109 back.
111 OSC hyperlink sequences are treated as word boundaries::
113 >>> wrap('foo \x1b]8;;https://example.com\x07link\x1b]8;;\x07 bar', 6)
114 ['foo', '\x1b]8;;https://example.com\x07link\x1b]8;;\x07', 'bar']
116 Both BEL (``\x07``) and ST (``\x1b\\``) terminators are supported.
117 """
118 # pylint: disable=too-many-locals,too-many-branches
119 # Build a mapping from stripped text positions to original text positions.
120 #
121 # Track where each character ENDS so that sequences between characters
122 # attach to the following text, keeping them when whitespace is dropped.
123 #
124 # char_end[i] = position in original text right after the i-th stripped char
125 char_end: list[int] = []
126 stripped_text = ''
127 original_pos = 0
128 prev_was_hyperlink_close = False
130 for segment, is_seq in iter_sequences(text):
131 if not is_seq:
132 # Conditionally insert space after hyperlink close to force word boundary
133 if prev_was_hyperlink_close and segment and not segment[0].isspace():
134 stripped_text += ' '
135 char_end.append(original_pos)
136 for char in segment:
137 original_pos += 1
138 char_end.append(original_pos)
139 stripped_text += char
140 prev_was_hyperlink_close = False
141 else:
142 is_hyperlink_close = segment.startswith(('\x1b]8;;\x1b\\', '\x1b]8;;\x07'))
144 # Conditionally insert space before OSC sequences to artificially create word
145 # boundary, but *not* before hyperlink close sequences, to ensure hyperlink is
146 # terminated on the same line.
147 if (segment.startswith('\x1b]') and stripped_text and not
148 stripped_text[-1].isspace()):
149 if not is_hyperlink_close:
150 stripped_text += ' '
151 char_end.append(original_pos)
153 # Escape sequences advance position but don't add to stripped text
154 original_pos += len(segment)
155 prev_was_hyperlink_close = is_hyperlink_close
157 # Add sentinel for final position
158 char_end.append(original_pos)
160 # Use parent's _split on the stripped text
161 # pylint: disable-next=protected-access
162 stripped_chunks = textwrap.TextWrapper._split(self, stripped_text)
164 # Handle text that contains only sequences (no visible characters).
165 # Return the sequences as a single chunk to preserve them.
166 if not stripped_chunks and text:
167 return [text]
169 # Map the chunks back to the original text with sequences
170 result: list[str] = []
171 stripped_pos = 0
172 num_chunks = len(stripped_chunks)
174 for idx, chunk in enumerate(stripped_chunks):
175 chunk_len = len(chunk)
177 # Start is where previous character ended (or 0 for first chunk)
178 start_orig = 0 if stripped_pos == 0 else char_end[stripped_pos - 1]
180 # End is where next character starts. For last chunk, use sentinel
181 # to include any trailing sequences.
182 if idx == num_chunks - 1:
183 end_orig = char_end[-1] # sentinel includes trailing sequences
184 else:
185 end_orig = char_end[stripped_pos + chunk_len - 1]
187 # Extract the corresponding portion from the original text
188 # Skip empty chunks (from virtual spaces inserted at OSC boundaries)
189 if start_orig != end_orig:
190 result.append(text[start_orig:end_orig])
191 stripped_pos += chunk_len
193 return result
195 def _wrap_chunks(self, chunks: list[str]) -> list[str]: # pylint: disable=too-many-branches
196 """
197 Wrap chunks into lines using sequence-aware width.
199 Override TextWrapper._wrap_chunks to measure with _width. Follows stdlib's algorithm:
200 greedily fill lines, handle long words. Also handle OSC hyperlink processing. When
201 hyperlinks span multiple lines, each line gets complete open/close sequences with matching
202 id parameters for hover underlining continuity per OSC 8 spec.
203 """
204 # pylint: disable=too-many-branches,too-many-statements,too-complex,too-many-locals
205 # pylint: disable=too-many-nested-blocks
206 # The hyperlink code pushes the complexity rating of this method. It stays in one method
207 # because of the shared local state.
208 if self.width <= 0:
209 raise ValueError('invalid width %r (must be > 0)' % self.width)
210 if not chunks:
211 return []
213 if self.max_lines is not None:
214 if self.max_lines > 1:
215 indent = self.subsequent_indent
216 else:
217 indent = self.initial_indent
218 if (self._width(indent)
219 + self._width(self.placeholder.lstrip())
220 > self.width):
221 raise ValueError("placeholder too large for max width")
223 lines: list[str] = []
224 is_first_line = True
226 hyperlink_state: Optional[HyperlinkParams] = None
227 # Track the id we're using for the current hyperlink continuation
228 current_hyperlink_id: Optional[str] = None
230 # Arrange in reverse order so items can be efficiently popped
231 chunks = list(reversed(chunks))
233 while chunks:
234 current_line: list[str] = []
235 current_width = 0
237 # Get the indent and available width for current line
238 indent = self.initial_indent if is_first_line else self.subsequent_indent
239 line_width = self.width - self._width(indent)
241 # If continuing a hyperlink from previous line, prepend open sequence
242 if hyperlink_state is not None:
243 open_seq = HyperlinkParams(
244 url=hyperlink_state.url,
245 params=hyperlink_state.params,
246 terminator=hyperlink_state.terminator,
247 ).make_open()
248 chunks[-1] = open_seq + chunks[-1]
250 # Drop leading whitespace (except at very start)
251 # When dropping, transfer any sequences to the next chunk.
252 # Only drop when actual whitespace text is present.
253 stripped = self._strip_sequences(chunks[-1])
254 if self.drop_whitespace and lines and stripped and not stripped.strip():
255 sequences = self._extract_sequences(chunks[-1])
256 del chunks[-1]
257 if sequences and chunks:
258 chunks[-1] = sequences + chunks[-1]
260 # Greedily add chunks that fit
261 while chunks:
262 chunk = chunks[-1]
263 chunk_width = self._width(chunk)
265 if current_width + chunk_width <= line_width:
266 current_line.append(chunks.pop())
267 current_width += chunk_width
268 else:
269 break
271 # Handle chunk that's too long for any line
272 if chunks and self._width(chunks[-1]) > line_width:
273 self._handle_long_word(
274 chunks, current_line, current_width, line_width
275 )
276 current_width = self._width(''.join(current_line))
277 # Remove any empty chunks left by _handle_long_word
278 while chunks and not chunks[-1]:
279 del chunks[-1]
281 # Drop trailing whitespace
282 # When dropping, transfer any sequences to the previous chunk.
283 # Only drop when actual whitespace text is present.
284 stripped_last = self._strip_sequences(current_line[-1]) if current_line else ''
285 if (self.drop_whitespace and current_line and
286 stripped_last and not stripped_last.strip()):
287 sequences = self._extract_sequences(current_line[-1])
288 current_width -= self._width(current_line[-1])
289 del current_line[-1]
290 if sequences and current_line:
291 current_line[-1] = current_line[-1] + sequences
293 if current_line:
294 # Check whether this is a normal append or max_lines
295 # truncation. Matches stdlib textwrap precedence:
296 # normal if max_lines not set, not yet reached, or no
297 # remaining visible content that would need truncation.
298 no_more_content = (
299 not chunks or
300 self.drop_whitespace and
301 len(chunks) == 1 and
302 not self._strip_sequences(chunks[0]).strip()
303 )
304 if (self.max_lines is None or
305 len(lines) + 1 < self.max_lines or
306 no_more_content
307 and current_width <= line_width):
308 line_content = ''.join(current_line)
310 # Track hyperlink state through this line's content
311 new_state = self._track_hyperlink_state(line_content, hyperlink_state)
313 # If we end inside a hyperlink, append close sequence
314 if new_state is not None:
315 # Ensure we have an id for continuation
316 if current_hyperlink_id is None:
317 if 'id=' in new_state.params:
318 current_hyperlink_id = new_state.params
319 elif new_state.params:
320 # Prepend id to existing params. Per OSC 8 spec, params can have
321 # multiple key=value pairs separated by ':'.
322 current_hyperlink_id = (
323 f'id={self._next_hyperlink_id()}:{new_state.params}')
324 else:
325 current_hyperlink_id = f'id={self._next_hyperlink_id()}'
326 line_content += HyperlinkParams(
327 terminator=new_state.terminator, url='').make_close()
329 # Also need to inject the id into the opening
330 # sequence if it didn't have one
331 if 'id=' not in new_state.params:
332 # Find and replace the original open sequence with one that has id
333 old_open = HyperlinkParams(
334 url=new_state.url,
335 params=new_state.params,
336 terminator=new_state.terminator,
337 ).make_open()
338 new_open = HyperlinkParams(
339 url=new_state.url,
340 params=current_hyperlink_id,
341 terminator=new_state.terminator,
342 ).make_open()
343 line_content = line_content.replace(old_open, new_open, 1)
345 # Update state for next line, using computed id
346 hyperlink_state = HyperlinkParams(
347 new_state.url, current_hyperlink_id, new_state.terminator)
348 else:
349 hyperlink_state = None
350 current_hyperlink_id = None # Reset id when hyperlink closes
352 # Strip trailing whitespace when drop_whitespace is enabled
353 # (matches CPython #140627 fix behavior)
354 if self.drop_whitespace:
355 line_content = line_content.rstrip()
356 lines.append(indent + line_content)
357 is_first_line = False
358 else:
359 # max_lines reached with remaining content.
360 # pop chunks until placeholder fits, then break.
361 placeholder_w = self._width(self.placeholder)
362 while current_line:
363 last_text = self._strip_sequences(current_line[-1])
364 if (last_text.strip()
365 and current_width + placeholder_w <= line_width):
366 line_content = ''.join(current_line)
367 new_state = self._track_hyperlink_state(
368 line_content, hyperlink_state)
369 if new_state is not None:
370 line_content += HyperlinkParams(
371 terminator=new_state.terminator, url='').make_close()
372 lines.append(indent + line_content + self.placeholder)
373 break
374 current_width -= self._width(current_line[-1])
375 del current_line[-1]
376 else:
377 if lines:
378 prev_line = self._rstrip_visible(lines[-1])
379 if (self._width(prev_line) + placeholder_w
380 <= self.width):
381 lines[-1] = prev_line + self.placeholder
382 break
383 lines.append(indent + self.placeholder.lstrip())
384 break
386 return lines
388 def _track_hyperlink_state(
389 self, text: str,
390 state: Optional[HyperlinkParams]) -> Optional[HyperlinkParams]:
391 """
392 Track hyperlink state through text.
394 :param text: Text to scan for hyperlink sequences.
395 :param state: Current state or None if outside hyperlink.
396 :returns: Updated state after processing text.
397 """
398 for segment, is_seq in iter_sequences(text):
399 if is_seq:
400 parsed_link = HyperlinkParams.parse(segment)
401 if parsed_link is not None and parsed_link.url: # has URL = open
402 state = parsed_link
403 elif segment.startswith(('\x1b]8;;\x1b\\', '\x1b]8;;\x07')): # close
404 state = None
405 return state
407 def _handle_long_word(self, reversed_chunks: list[str],
408 cur_line: list[str], cur_len: int,
409 width: int) -> None:
410 """
411 Sequence-aware :meth:`textwrap.TextWrapper._handle_long_word`.
413 This method ensures that word boundaries are not broken mid-sequence, and respects grapheme
414 cluster boundaries when breaking long words.
415 """
416 if width < 1:
417 space_left = 1
418 else:
419 space_left = width - cur_len
421 chunk = reversed_chunks[-1]
423 if self.break_long_words:
424 break_at_hyphen = False
425 hyphen_end = 0
426 # End of the prefix that fits within space_left by display width.
427 prefix_end = self._find_break_position(chunk, space_left)
429 # Handle break_on_hyphens: find last hyphen in the portion that fits.
430 if self.break_on_hyphens:
431 stripped = self._strip_sequences(chunk[:prefix_end])
432 hyphen_pos = stripped.rfind('-')
433 if hyphen_pos > 0 and any(c != '-' for c in stripped[:hyphen_pos]):
434 # Map back to original position including sequences
435 hyphen_end = self._map_stripped_pos_to_original(chunk, hyphen_pos + 1)
436 break_at_hyphen = True
438 # Break at grapheme boundaries to avoid splitting multi-codepoint characters
439 if break_at_hyphen:
440 actual_end = hyphen_end
441 else:
442 actual_end = prefix_end
443 # Include first visible unit when break would take only leading sequences.
444 if not cur_line and (
445 actual_end == 0
446 or (actual_end < len(chunk)
447 and self._width(chunk[:actual_end]) == 0)):
448 actual_end = self._find_first_visible_break(chunk)
449 cur_line.append(chunk[:actual_end])
450 reversed_chunks[-1] = chunk[actual_end:]
452 elif not cur_line:
453 cur_line.append(reversed_chunks.pop())
455 def _map_stripped_pos_to_original(self, text: str, stripped_pos: int) -> int:
456 """Map a position in stripped text back to original text position."""
457 stripped_idx = 0
458 original_idx = 0
460 for segment, is_seq in iter_sequences(text):
461 if is_seq:
462 original_idx += len(segment)
463 elif stripped_idx + len(segment) > stripped_pos:
464 # Position is within this segment
465 return original_idx + (stripped_pos - stripped_idx)
466 else:
467 stripped_idx += len(segment)
468 original_idx += len(segment)
470 # Caller guarantees stripped_pos < total stripped chars, so we always
471 # return from within the loop. This line satisfies the type checker.
472 return original_idx # pragma: no cover
474 def _find_break_position(self, text: str, max_width: int) -> int:
475 """Find string index in text that fits within max_width cells."""
476 idx = 0
477 width_so_far = 0
479 while idx < len(text):
480 char = text[idx]
482 # Skip escape sequences (they don't add width)
483 if char == '\x1b':
484 match = ZERO_WIDTH_PATTERN.match(text, idx)
485 if match:
486 idx = match.end()
487 continue
489 # Get grapheme (use start= to avoid slice allocation)
490 grapheme = next(iter_graphemes(text, start=idx))
492 grapheme_width = self._width(grapheme)
493 if width_so_far + grapheme_width > max_width:
494 return idx # Found break point
496 width_so_far += grapheme_width
497 idx += len(grapheme)
499 # Caller guarantees chunk_width > max_width, so a grapheme always
500 # exceeds and we return from within the loop. Type checker requires this.
501 return idx # pragma: no cover
503 def _find_first_visible_break(self, text: str) -> int:
504 """End of leading escape sequences plus the first grapheme."""
505 idx = 0
506 while idx < len(text) and text[idx] == '\x1b':
507 match = ZERO_WIDTH_PATTERN.match(text, idx)
508 if match is None:
509 break
510 idx = match.end()
511 return idx + len(next(iter_graphemes(text, start=idx)))
513 def _rstrip_visible(self, text: str) -> str:
514 """Strip trailing visible whitespace, preserving trailing sequences."""
515 segments = list(iter_sequences(text))
516 last_vis = -1
517 for i, (segment, is_seq) in enumerate(segments):
518 if not is_seq and segment.rstrip():
519 last_vis = i
520 if last_vis == -1:
521 return ''
522 result = []
523 for i, (segment, is_seq) in enumerate(segments):
524 if i < last_vis:
525 result.append(segment)
526 elif i == last_vis:
527 result.append(segment.rstrip())
528 elif is_seq:
529 result.append(segment)
530 return ''.join(result)
533def wrap(text: str, width: int = 70, *,
534 control_codes: Literal['parse', 'strict', 'ignore'] = 'parse',
535 tabsize: int = 8,
536 expand_tabs: bool = True,
537 replace_whitespace: bool = True,
538 ambiguous_width: int = 1,
539 term_program: bool | str = False,
540 initial_indent: str = '',
541 subsequent_indent: str = '',
542 fix_sentence_endings: bool = False,
543 break_long_words: bool = True,
544 break_on_hyphens: bool = True,
545 drop_whitespace: bool = True,
546 max_lines: Optional[int] = None,
547 placeholder: str = ' [...]',
548 propagate_sgr: bool = True) -> list[str]:
549 r"""
550 Wrap text to fit within given width, returning a list of wrapped lines.
552 Like :func:`textwrap.wrap`, but measures width in display cells, correctly
553 handling wide characters, combining marks, and terminal
554 escape sequences.
556 :param text: Text to wrap, may contain terminal sequences.
557 :param width: Maximum line width in display cells.
558 :param control_codes: How to handle terminal sequences (see :func:`~.width`).
559 :param tabsize: Tab stop width for tab expansion.
560 :param expand_tabs: If True (default), tab characters are expanded
561 to spaces using ``tabsize``.
562 :param replace_whitespace: If True (default), each whitespace character
563 is replaced with a single space after tab expansion. When False,
564 control whitespace like ``\n`` has zero display width (unlike
565 :func:`textwrap.wrap` which counts ``len()``), so wrap points
566 may differ from stdlib for non-space whitespace characters.
567 :param ambiguous_width: Width to use for East Asian Ambiguous (A)
568 characters. Default is ``1`` (narrow). Set to ``2`` for CJK contexts.
569 :param term_program: Terminal software identifier for table correction.
570 ``False`` (default) disables override lookup. ``True`` reads the
571 ``TERM_PROGRAM`` or ``TERM`` environment variable for auto-detection.
572 Accepts a canonical terminal name matching :func:`list_term_programs`,
573 such as from XTVERSION_, ENQ_, or ``TERM_PROGRAM``.
575 .. versionadded:: 0.8.0
576 :param initial_indent: String prepended to first line.
577 :param subsequent_indent: String prepended to subsequent lines.
578 :param fix_sentence_endings: If True, ensure sentences are always
579 separated by exactly two spaces.
580 :param break_long_words: If True, break words longer than width.
581 :param break_on_hyphens: If True, allow breaking at hyphens.
582 :param drop_whitespace: If True (default), whitespace at the beginning
583 and end of each line (after wrapping but before indenting) is dropped.
584 Set to False to preserve whitespace.
585 :param max_lines: If set, output contains at most this many lines, with
586 ``placeholder`` appended to the last line if the text was truncated.
587 :param placeholder: String appended to the last line when text is
588 truncated by ``max_lines``. Default is ``' [...]'``.
589 :param propagate_sgr: If True (default), SGR (terminal styling) sequences
590 are propagated across wrapped lines. Each line ends with a reset
591 sequence and the next line begins with the active style restored.
592 :returns: List of wrapped lines without trailing newlines.
594 SGR (terminal styling) sequences are propagated across wrapped lines
595 by default. Each line ends with a reset sequence and the next line
596 begins with the active style restored::
598 >>> wrap('\x1b[1;34mHello world\x1b[0m', width=6)
599 ['\x1b[1;34mHello\x1b[0m', '\x1b[1;34mworld\x1b[0m']
601 Set ``propagate_sgr=False`` to disable this behavior.
603 Like :func:`textwrap.wrap`, newlines in the input text are treated as
604 whitespace and collapsed. To preserve paragraph breaks, wrap each
605 paragraph separately::
607 >>> text = 'First line.\nSecond line.'
608 >>> wrap(text, 40) # newline collapsed to space
609 ['First line. Second line.']
610 >>> [line for para in text.split('\n')
611 ... for line in (wrap(para, 40) if para else [''])]
612 ['First line.', 'Second line.']
614 .. seealso::
616 :func:`textwrap.wrap`, :class:`textwrap.TextWrapper`
617 Standard library text wrapping (character-based).
619 :class:`.SequenceTextWrapper`
620 Class interface for advanced wrapping options.
622 .. versionadded:: 0.3.0
624 .. versionchanged:: 0.5.0
625 Added ``propagate_sgr`` parameter (default True).
627 .. versionchanged:: 0.6.0
628 Added ``expand_tabs``, ``replace_whitespace``, ``fix_sentence_endings``,
629 ``drop_whitespace``, ``max_lines``, and ``placeholder`` parameters.
631 Example::
633 >>> from wcwidth import wrap
634 >>> wrap('hello world', 5)
635 ['hello', 'world']
636 >>> wrap('中文字符', 4) # CJK characters (2 cells each)
637 ['中文', '字符']
638 """
639 # pylint: disable=too-many-arguments,too-many-locals
640 wrapper = SequenceTextWrapper(
641 width=width,
642 control_codes=control_codes,
643 tabsize=tabsize,
644 expand_tabs=expand_tabs,
645 replace_whitespace=replace_whitespace,
646 ambiguous_width=ambiguous_width,
647 term_program=term_program,
648 initial_indent=initial_indent,
649 subsequent_indent=subsequent_indent,
650 fix_sentence_endings=fix_sentence_endings,
651 break_long_words=break_long_words,
652 break_on_hyphens=break_on_hyphens,
653 drop_whitespace=drop_whitespace,
654 max_lines=max_lines,
655 placeholder=placeholder,
656 )
657 lines = wrapper.wrap(text)
659 if propagate_sgr:
660 lines = _propagate_sgr(lines)
662 return lines