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