@ossiq/cli-linux-x64 0.1.13 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/bin/ossiq/_internal/_cffi_backend.cpython-313-x86_64-linux-gnu.so +0 -0
  2. package/bin/ossiq/_internal/base_library.zip +0 -0
  3. package/bin/ossiq/_internal/cryptography/hazmat/bindings/_rust.abi3.so +0 -0
  4. package/bin/ossiq/_internal/cryptography-50.0.1.dist-info/METADATA +110 -0
  5. package/bin/ossiq/_internal/cryptography-50.0.1.dist-info/RECORD +122 -0
  6. package/bin/ossiq/_internal/cryptography-50.0.1.dist-info/WHEEL +5 -0
  7. package/bin/ossiq/_internal/cryptography-50.0.1.dist-info/licenses/LICENSE +3 -0
  8. package/bin/ossiq/_internal/cryptography-50.0.1.dist-info/licenses/LICENSE.APACHE +202 -0
  9. package/bin/ossiq/_internal/{semantic_version-2.10.0.dist-info/LICENSE → cryptography-50.0.1.dist-info/licenses/LICENSE.BSD} +14 -9
  10. package/bin/ossiq/_internal/cryptography-50.0.1.dist-info/sboms/cryptography-rust.cyclonedx.json +1365 -0
  11. package/bin/ossiq/_internal/cryptography-50.0.1.dist-info/sboms/sbom.json +43 -0
  12. package/bin/ossiq/_internal/keyring-25.7.0.dist-info/METADATA +554 -0
  13. package/bin/ossiq/_internal/keyring-25.7.0.dist-info/RECORD +40 -0
  14. package/bin/ossiq/_internal/{semantic_version-2.10.0.dist-info → keyring-25.7.0.dist-info}/WHEEL +1 -2
  15. package/bin/ossiq/_internal/keyring-25.7.0.dist-info/entry_points.txt +13 -0
  16. package/bin/ossiq/_internal/keyring-25.7.0.dist-info/licenses/LICENSE +18 -0
  17. package/bin/ossiq/_internal/keyring-25.7.0.dist-info/top_level.txt +1 -0
  18. package/bin/ossiq/_internal/lark/grammars/common.lark +59 -0
  19. package/bin/ossiq/_internal/lark/grammars/lark.lark +62 -0
  20. package/bin/ossiq/_internal/lark/grammars/python.lark +302 -0
  21. package/bin/ossiq/_internal/lark/grammars/unicode.lark +7 -0
  22. package/bin/ossiq/_internal/lark/py.typed +0 -0
  23. package/bin/ossiq/_internal/libpython3.13.so.1.0 +0 -0
  24. package/bin/ossiq/_internal/ossiq/data/SKILL.md +42 -16
  25. package/bin/ossiq/_internal/ossiq/domain/README.md +30 -4
  26. package/bin/ossiq/_internal/ossiq/solver/README.md +10 -3
  27. package/bin/ossiq/_internal/ossiq/strategy/README.md +20 -2
  28. package/bin/ossiq/_internal/ossiq/ui/html_templates/spa_app.html +1828 -1518
  29. package/bin/ossiq/_internal/ossiq/ui/renderers/export/README.md +103 -40
  30. package/bin/ossiq/_internal/ossiq/ui/renderers/export/schemas/export_schema_v1.6.json +1805 -0
  31. package/bin/ossiq/_internal/ossiq/ui/renderers/export/schemas/export_schema_v1.6_standard.json +1328 -0
  32. package/bin/ossiq/_internal/ossiq-0.1.14.dist-info/INSTALLER +1 -0
  33. package/bin/ossiq/_internal/ossiq-0.1.14.dist-info/METADATA +207 -0
  34. package/bin/ossiq/_internal/ossiq-0.1.14.dist-info/RECORD +13 -0
  35. package/bin/ossiq/_internal/ossiq-0.1.14.dist-info/REQUESTED +0 -0
  36. package/bin/ossiq/_internal/ossiq-0.1.14.dist-info/uv_cache.json +1 -0
  37. package/bin/ossiq/_internal/pysolvers.cpython-313-x86_64-linux-gnu.so +0 -0
  38. package/bin/ossiq/_internal/setuptools/_vendor/importlib_metadata-8.7.1.dist-info/INSTALLER +1 -0
  39. package/bin/ossiq/_internal/setuptools/_vendor/importlib_metadata-8.7.1.dist-info/METADATA +133 -0
  40. package/bin/ossiq/_internal/setuptools/_vendor/importlib_metadata-8.7.1.dist-info/RECORD +21 -0
  41. package/bin/ossiq/_internal/setuptools/_vendor/importlib_metadata-8.7.1.dist-info/REQUESTED +0 -0
  42. package/bin/ossiq/_internal/setuptools/_vendor/importlib_metadata-8.7.1.dist-info/WHEEL +5 -0
  43. package/bin/ossiq/_internal/setuptools/_vendor/importlib_metadata-8.7.1.dist-info/licenses/LICENSE +73 -0
  44. package/bin/ossiq/_internal/setuptools/_vendor/importlib_metadata-8.7.1.dist-info/top_level.txt +1 -0
  45. package/bin/ossiq/ossiq +0 -0
  46. package/package.json +1 -1
  47. package/bin/ossiq/_internal/ossiq-0.1.13.dist-info/METADATA +0 -385
  48. package/bin/ossiq/_internal/ossiq-0.1.13.dist-info/RECORD +0 -13
  49. package/bin/ossiq/_internal/ossiq-0.1.13.dist-info/uv_cache.json +0 -1
  50. package/bin/ossiq/_internal/semantic_version-2.10.0.dist-info/METADATA +0 -324
  51. package/bin/ossiq/_internal/semantic_version-2.10.0.dist-info/RECORD +0 -10
  52. package/bin/ossiq/_internal/semantic_version-2.10.0.dist-info/top_level.txt +0 -1
  53. package/bin/ossiq/_internal/wcwidth/textwrap.py,cover +0 -664
  54. /package/bin/ossiq/_internal/{ossiq-0.1.13.dist-info → cryptography-50.0.1.dist-info}/INSTALLER +0 -0
  55. /package/bin/ossiq/_internal/{ossiq-0.1.13.dist-info → cryptography-50.0.1.dist-info}/REQUESTED +0 -0
  56. /package/bin/ossiq/_internal/{semantic_version-2.10.0.dist-info → keyring-25.7.0.dist-info}/INSTALLER +0 -0
  57. /package/bin/ossiq/_internal/{semantic_version-2.10.0.dist-info → keyring-25.7.0.dist-info}/REQUESTED +0 -0
  58. /package/bin/ossiq/_internal/{ossiq-0.1.13.dist-info → ossiq-0.1.14.dist-info}/WHEEL +0 -0
  59. /package/bin/ossiq/_internal/{ossiq-0.1.13.dist-info → ossiq-0.1.14.dist-info}/direct_url.json +0 -0
  60. /package/bin/ossiq/_internal/{ossiq-0.1.13.dist-info → ossiq-0.1.14.dist-info}/entry_points.txt +0 -0
  61. /package/bin/ossiq/_internal/{ossiq-0.1.13.dist-info → ossiq-0.1.14.dist-info}/licenses/AUTHORS +0 -0
  62. /package/bin/ossiq/_internal/{ossiq-0.1.13.dist-info → ossiq-0.1.14.dist-info}/licenses/LICENSE +0 -0
  63. /package/bin/ossiq/_internal/{ossiq-0.1.13.dist-info → ossiq-0.1.14.dist-info}/uv_build.json +0 -0
@@ -1,664 +0,0 @@
1
- > """
2
- > Sequence-aware text wrapping functions.
3
-
4
- > This module provides functions for wrapping text that may contain terminal escape sequences, with
5
- > proper handling of Unicode grapheme clusters and character display widths.
6
- > """
7
-
8
- > from __future__ import annotations
9
-
10
- # std imports
11
- > import secrets
12
- > import textwrap
13
-
14
- > from typing import TYPE_CHECKING, Optional
15
-
16
- # local
17
- > from ._width import width as wcwidth_width
18
- > from .grapheme import iter_graphemes
19
- > from .hyperlink import HyperlinkParams
20
- > from .sgr_state import propagate_sgr as _propagate_sgr
21
- > from .escape_sequences import ZERO_WIDTH_PATTERN, iter_sequences
22
-
23
- - if TYPE_CHECKING: # pragma: no cover
24
- - from typing import Any, Literal
25
-
26
-
27
- > class SequenceTextWrapper(textwrap.TextWrapper):
28
- > """
29
- > Sequence-aware text wrapper extending :class:`textwrap.TextWrapper`.
30
-
31
- > This wrapper properly handles terminal escape sequences and Unicode grapheme clusters when
32
- > calculating text width for wrapping.
33
-
34
- > This implementation is based on the SequenceTextWrapper from the 'blessed' library, with
35
- > contributions from Avram Lubkin and grayjk.
36
-
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.
40
-
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
- > """
46
-
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.
55
-
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``.
65
-
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
74
-
75
- > @staticmethod
76
- > def _next_hyperlink_id() -> str:
77
- > """Generate unique hyperlink id as 8-character hex string."""
78
- > return secrets.token_hex(4)
79
-
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)
85
-
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)
93
-
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)
101
-
102
- > def _split(self, text: str) -> list[str]: # pylint: disable=too-many-locals
103
- > r"""
104
- > Sequence-aware variant of :meth:`textwrap.TextWrapper._split`.
105
-
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.
110
-
111
- > OSC hyperlink sequences are treated as word boundaries::
112
-
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']
115
-
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 (not preceding text). This ensures sequences
123
- # aren't lost when whitespace is dropped.
124
- #
125
- # char_end[i] = position in original text right after the i-th stripped char
126
- > char_end: list[int] = []
127
- > stripped_text = ''
128
- > original_pos = 0
129
- > prev_was_hyperlink_close = False
130
-
131
- > for segment, is_seq in iter_sequences(text):
132
- > if not is_seq:
133
- # Conditionally insert space after hyperlink close to force word boundary
134
- > if prev_was_hyperlink_close and segment and not segment[0].isspace():
135
- > stripped_text += ' '
136
- > char_end.append(original_pos)
137
- > for char in segment:
138
- > original_pos += 1
139
- > char_end.append(original_pos)
140
- > stripped_text += char
141
- > prev_was_hyperlink_close = False
142
- > else:
143
- > is_hyperlink_close = segment.startswith(('\x1b]8;;\x1b\\', '\x1b]8;;\x07'))
144
-
145
- # Conditionally insert space before OSC sequences to artificially create word
146
- # boundary, but *not* before hyperlink close sequences, to ensure hyperlink is
147
- # terminated on the same line.
148
- > if (segment.startswith('\x1b]') and stripped_text and not
149
- > stripped_text[-1].isspace()):
150
- > if not is_hyperlink_close:
151
- > stripped_text += ' '
152
- > char_end.append(original_pos)
153
-
154
- # Escape sequences advance position but don't add to stripped text
155
- > original_pos += len(segment)
156
- > prev_was_hyperlink_close = is_hyperlink_close
157
-
158
- # Add sentinel for final position
159
- > char_end.append(original_pos)
160
-
161
- # Use parent's _split on the stripped text
162
- # pylint: disable-next=protected-access
163
- > stripped_chunks = textwrap.TextWrapper._split(self, stripped_text)
164
-
165
- # Handle text that contains only sequences (no visible characters).
166
- # Return the sequences as a single chunk to preserve them.
167
- > if not stripped_chunks and text:
168
- > return [text]
169
-
170
- # Map the chunks back to the original text with sequences
171
- > result: list[str] = []
172
- > stripped_pos = 0
173
- > num_chunks = len(stripped_chunks)
174
-
175
- > for idx, chunk in enumerate(stripped_chunks):
176
- > chunk_len = len(chunk)
177
-
178
- # Start is where previous character ended (or 0 for first chunk)
179
- > start_orig = 0 if stripped_pos == 0 else char_end[stripped_pos - 1]
180
-
181
- # End is where next character starts. For last chunk, use sentinel
182
- # to include any trailing sequences.
183
- > if idx == num_chunks - 1:
184
- > end_orig = char_end[-1] # sentinel includes trailing sequences
185
- > else:
186
- > end_orig = char_end[stripped_pos + chunk_len - 1]
187
-
188
- # Extract the corresponding portion from the original text
189
- # Skip empty chunks (from virtual spaces inserted at OSC boundaries)
190
- > if start_orig != end_orig:
191
- > result.append(text[start_orig:end_orig])
192
- > stripped_pos += chunk_len
193
-
194
- > return result
195
-
196
- > def _wrap_chunks(self, chunks: list[str]) -> list[str]: # pylint: disable=too-many-branches
197
- > """
198
- > Wrap chunks into lines using sequence-aware width.
199
-
200
- > Override TextWrapper._wrap_chunks to use _width instead of len. Follows stdlib's algorithm:
201
- > greedily fill lines, handle long words. Also handle OSC hyperlink processing. When
202
- > hyperlinks span multiple lines, each line gets complete open/close sequences with matching
203
- > id parameters for hover underlining continuity per OSC 8 spec.
204
- > """
205
- # pylint: disable=too-many-branches,too-many-statements,too-complex,too-many-locals
206
- # pylint: disable=too-many-nested-blocks
207
- # the hyperlink code in particular really pushes the complexity rating of this method.
208
- # preferring to keep it "all in one method" because of so much local state and manipulation.
209
- > if not chunks:
210
- > return []
211
-
212
- > if self.max_lines is not None:
213
- > if self.max_lines > 1:
214
- > indent = self.subsequent_indent
215
- > else:
216
- > indent = self.initial_indent
217
- > if (self._width(indent)
218
- > + self._width(self.placeholder.lstrip())
219
- > > self.width):
220
- > raise ValueError("placeholder too large for max width")
221
-
222
- > lines: list[str] = []
223
- > is_first_line = True
224
-
225
- > hyperlink_state: Optional[HyperlinkParams] = None
226
- # Track the id we're using for the current hyperlink continuation
227
- > current_hyperlink_id: Optional[str] = None
228
-
229
- # Arrange in reverse order so items can be efficiently popped
230
- > chunks = list(reversed(chunks))
231
-
232
- > while chunks:
233
- > current_line: list[str] = []
234
- > current_width = 0
235
-
236
- # Get the indent and available width for current line
237
- > indent = self.initial_indent if is_first_line else self.subsequent_indent
238
- > line_width = self.width - self._width(indent)
239
-
240
- # If continuing a hyperlink from previous line, prepend open sequence
241
- > if hyperlink_state is not None:
242
- > open_seq = HyperlinkParams(
243
- > url=hyperlink_state.url,
244
- > params=hyperlink_state.params,
245
- > terminator=hyperlink_state.terminator,
246
- > ).make_open()
247
- > chunks[-1] = open_seq + chunks[-1]
248
-
249
- # Drop leading whitespace (except at very start)
250
- # When dropping, transfer any sequences to the next chunk.
251
- # Only drop if there's actual whitespace text, not if it's only sequences.
252
- > stripped = self._strip_sequences(chunks[-1])
253
- > if self.drop_whitespace and lines and stripped and not stripped.strip():
254
- > sequences = self._extract_sequences(chunks[-1])
255
- > del chunks[-1]
256
- > if sequences and chunks:
257
- > chunks[-1] = sequences + chunks[-1]
258
-
259
- # Greedily add chunks that fit
260
- > while chunks:
261
- > chunk = chunks[-1]
262
- > chunk_width = self._width(chunk)
263
-
264
- > if current_width + chunk_width <= line_width:
265
- > current_line.append(chunks.pop())
266
- > current_width += chunk_width
267
- > else:
268
- > break
269
-
270
- # Handle chunk that's too long for any line
271
- > if chunks and self._width(chunks[-1]) > line_width:
272
- > self._handle_long_word(
273
- > chunks, current_line, current_width, line_width
274
- > )
275
- > current_width = self._width(''.join(current_line))
276
- # Remove any empty chunks left by _handle_long_word
277
- > while chunks and not chunks[-1]:
278
- > del chunks[-1]
279
-
280
- # Drop trailing whitespace
281
- # When dropping, transfer any sequences to the previous chunk.
282
- # Only drop if there's actual whitespace text, not if it's only sequences.
283
- > stripped_last = self._strip_sequences(current_line[-1]) if current_line else ''
284
- > if (self.drop_whitespace and current_line and
285
- > stripped_last and not stripped_last.strip()):
286
- > sequences = self._extract_sequences(current_line[-1])
287
- > current_width -= self._width(current_line[-1])
288
- > del current_line[-1]
289
- > if sequences and current_line:
290
- > current_line[-1] = current_line[-1] + sequences
291
-
292
- > if current_line:
293
- # Check whether this is a normal append or max_lines
294
- # truncation. Matches stdlib textwrap precedence:
295
- # normal if max_lines not set, not yet reached, or no
296
- # remaining visible content that would need truncation.
297
- > no_more_content = (
298
- > not chunks or
299
- > self.drop_whitespace and
300
- > len(chunks) == 1 and
301
- > not self._strip_sequences(chunks[0]).strip()
302
- > )
303
- > if (self.max_lines is None or
304
- > len(lines) + 1 < self.max_lines or
305
- > no_more_content
306
- > and current_width <= line_width):
307
- > line_content = ''.join(current_line)
308
-
309
- # Track hyperlink state through this line's content
310
- > new_state = self._track_hyperlink_state(line_content, hyperlink_state)
311
-
312
- # If we end inside a hyperlink, append close sequence
313
- > if new_state is not None:
314
- # Ensure we have an id for continuation
315
- > if current_hyperlink_id is None:
316
- > if 'id=' in new_state.params:
317
- > current_hyperlink_id = new_state.params
318
- > elif new_state.params:
319
- # Prepend id to existing params. Per OSC 8 spec, params can have
320
- # multiple key=value pairs separated by ':'.
321
- > current_hyperlink_id = (
322
- > f'id={self._next_hyperlink_id()}:{new_state.params}')
323
- > else:
324
- > current_hyperlink_id = f'id={self._next_hyperlink_id()}'
325
- > line_content += HyperlinkParams(
326
- > terminator=new_state.terminator, url='').make_close()
327
-
328
- # Also need to inject the id into the opening
329
- # sequence if it didn't have one
330
- > if 'id=' not in new_state.params:
331
- # Find and replace the original open sequence with one that has id
332
- > old_open = HyperlinkParams(
333
- > url=new_state.url,
334
- > params=new_state.params,
335
- > terminator=new_state.terminator,
336
- > ).make_open()
337
- > new_open = HyperlinkParams(
338
- > url=new_state.url,
339
- > params=current_hyperlink_id,
340
- > terminator=new_state.terminator,
341
- > ).make_open()
342
- > line_content = line_content.replace(old_open, new_open, 1)
343
-
344
- # Update state for next line, using computed id
345
- > hyperlink_state = HyperlinkParams(
346
- > new_state.url, current_hyperlink_id, new_state.terminator)
347
- > else:
348
- > hyperlink_state = None
349
- > current_hyperlink_id = None # Reset id when hyperlink closes
350
-
351
- # Strip trailing whitespace when drop_whitespace is enabled
352
- # (matches CPython #140627 fix behavior)
353
- > if self.drop_whitespace:
354
- > line_content = line_content.rstrip()
355
- > lines.append(indent + line_content)
356
- > is_first_line = False
357
- > else:
358
- # max_lines reached with remaining content.
359
- # pop chunks until placeholder fits, then break.
360
- > placeholder_w = self._width(self.placeholder)
361
- > while current_line:
362
- > last_text = self._strip_sequences(current_line[-1])
363
- > if (last_text.strip()
364
- > and current_width + placeholder_w <= line_width):
365
- > line_content = ''.join(current_line)
366
- > new_state = self._track_hyperlink_state(
367
- > line_content, hyperlink_state)
368
- > if new_state is not None:
369
- > line_content += HyperlinkParams(
370
- > terminator=new_state.terminator, url='').make_close()
371
- > lines.append(indent + line_content + self.placeholder)
372
- > break
373
- > current_width -= self._width(current_line[-1])
374
- > del current_line[-1]
375
- > else:
376
- > if lines:
377
- > prev_line = self._rstrip_visible(lines[-1])
378
- > if (self._width(prev_line) + placeholder_w
379
- > <= self.width):
380
- > lines[-1] = prev_line + self.placeholder
381
- > break
382
- > lines.append(indent + self.placeholder.lstrip())
383
- > break
384
-
385
- > return lines
386
-
387
- > def _track_hyperlink_state(
388
- > self, text: str,
389
- > state: Optional[HyperlinkParams]) -> Optional[HyperlinkParams]:
390
- > """
391
- > Track hyperlink state through text.
392
-
393
- > :param text: Text to scan for hyperlink sequences.
394
- > :param state: Current state or None if outside hyperlink.
395
- > :returns: Updated state after processing text.
396
- > """
397
- > for segment, is_seq in iter_sequences(text):
398
- > if is_seq:
399
- > parsed_link = HyperlinkParams.parse(segment)
400
- > if parsed_link is not None and parsed_link.url: # has URL = open
401
- > state = parsed_link
402
- > elif segment.startswith(('\x1b]8;;\x1b\\', '\x1b]8;;\x07')): # close
403
- > state = None
404
- > return state
405
-
406
- > def _handle_long_word(self, reversed_chunks: list[str],
407
- > cur_line: list[str], cur_len: int,
408
- > width: int) -> None:
409
- > """
410
- > Sequence-aware :meth:`textwrap.TextWrapper._handle_long_word`.
411
-
412
- > This method ensures that word boundaries are not broken mid-sequence, and respects grapheme
413
- > cluster boundaries when breaking long words.
414
- > """
415
- > if width < 1:
416
- > space_left = 1
417
- > else:
418
- > space_left = width - cur_len
419
-
420
- > chunk = reversed_chunks[-1]
421
-
422
- > if self.break_long_words:
423
- > break_at_hyphen = False
424
- > hyphen_end = 0
425
-
426
- # Handle break_on_hyphens: find last hyphen within space_left
427
- > if self.break_on_hyphens:
428
- # Strip sequences to find hyphen in logical text
429
- > stripped = self._strip_sequences(chunk)
430
- > if len(stripped) > space_left:
431
- # Find last hyphen in the portion that fits
432
- > hyphen_pos = stripped.rfind('-', 0, space_left)
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
437
-
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 = self._find_break_position(chunk, space_left)
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:]
451
-
452
- > elif not cur_line:
453
- > cur_line.append(reversed_chunks.pop())
454
-
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
459
-
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)
469
-
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
473
-
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
478
-
479
- > while idx < len(text):
480
- > char = text[idx]
481
-
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
488
-
489
- # Get grapheme (use start= to avoid slice allocation)
490
- > grapheme = next(iter_graphemes(text, start=idx))
491
-
492
- > grapheme_width = self._width(grapheme)
493
- > if width_so_far + grapheme_width > max_width:
494
- > return idx # Found break point
495
-
496
- > width_so_far += grapheme_width
497
- > idx += len(grapheme)
498
-
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
502
-
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
- > if idx >= len(text):
512
- ! return len(text)
513
- > return idx + len(next(iter_graphemes(text, start=idx)))
514
-
515
- > def _rstrip_visible(self, text: str) -> str:
516
- > """Strip trailing visible whitespace, preserving trailing sequences."""
517
- > segments = list(iter_sequences(text))
518
- > last_vis = -1
519
- > for i, (segment, is_seq) in enumerate(segments):
520
- > if not is_seq and segment.rstrip():
521
- > last_vis = i
522
- > if last_vis == -1:
523
- > return ''
524
- > result = []
525
- > for i, (segment, is_seq) in enumerate(segments):
526
- > if i < last_vis:
527
- > result.append(segment)
528
- > elif i == last_vis:
529
- > result.append(segment.rstrip())
530
- > elif is_seq:
531
- > result.append(segment)
532
- > return ''.join(result)
533
-
534
-
535
- > def wrap(text: str, width: int = 70, *,
536
- > control_codes: Literal['parse', 'strict', 'ignore'] = 'parse',
537
- > tabsize: int = 8,
538
- > expand_tabs: bool = True,
539
- > replace_whitespace: bool = True,
540
- > ambiguous_width: int = 1,
541
- > term_program: bool | str = False,
542
- > initial_indent: str = '',
543
- > subsequent_indent: str = '',
544
- > fix_sentence_endings: bool = False,
545
- > break_long_words: bool = True,
546
- > break_on_hyphens: bool = True,
547
- > drop_whitespace: bool = True,
548
- > max_lines: Optional[int] = None,
549
- > placeholder: str = ' [...]',
550
- > propagate_sgr: bool = True) -> list[str]:
551
- > r"""
552
- > Wrap text to fit within given width, returning a list of wrapped lines.
553
-
554
- > Like :func:`textwrap.wrap`, but measures width in display cells rather than
555
- > characters, correctly handling wide characters, combining marks, and terminal
556
- > escape sequences.
557
-
558
- > :param text: Text to wrap, may contain terminal sequences.
559
- > :param width: Maximum line width in display cells.
560
- > :param control_codes: How to handle terminal sequences (see :func:`~.width`).
561
- > :param tabsize: Tab stop width for tab expansion.
562
- > :param expand_tabs: If True (default), tab characters are expanded
563
- > to spaces using ``tabsize``.
564
- > :param replace_whitespace: If True (default), each whitespace character
565
- > is replaced with a single space after tab expansion. When False,
566
- > control whitespace like ``\n`` has zero display width (unlike
567
- > :func:`textwrap.wrap` which counts ``len()``), so wrap points
568
- > may differ from stdlib for non-space whitespace characters.
569
- > :param ambiguous_width: Width to use for East Asian Ambiguous (A)
570
- > characters. Default is ``1`` (narrow). Set to ``2`` for CJK contexts.
571
- > :param term_program: Terminal software identifier for table correction.
572
- > ``False`` (default) disables override lookup. ``True`` reads the
573
- > ``TERM_PROGRAM`` or ``TERM`` environment variable for auto-detection.
574
- > Accepts a canonical terminal name matching :func:`list_term_programs`,
575
- > such as from XTVERSION_, ENQ_, or ``TERM_PROGRAM``.
576
-
577
- > .. versionadded:: 0.8.0
578
- > :param initial_indent: String prepended to first line.
579
- > :param subsequent_indent: String prepended to subsequent lines.
580
- > :param fix_sentence_endings: If True, ensure sentences are always
581
- > separated by exactly two spaces.
582
- > :param break_long_words: If True, break words longer than width.
583
- > :param break_on_hyphens: If True, allow breaking at hyphens.
584
- > :param drop_whitespace: If True (default), whitespace at the beginning
585
- > and end of each line (after wrapping but before indenting) is dropped.
586
- > Set to False to preserve whitespace.
587
- > :param max_lines: If set, output contains at most this many lines, with
588
- > ``placeholder`` appended to the last line if the text was truncated.
589
- > :param placeholder: String appended to the last line when text is
590
- > truncated by ``max_lines``. Default is ``' [...]'``.
591
- > :param propagate_sgr: If True (default), SGR (terminal styling) sequences
592
- > are propagated across wrapped lines. Each line ends with a reset
593
- > sequence and the next line begins with the active style restored.
594
- > :returns: List of wrapped lines without trailing newlines.
595
-
596
- > SGR (terminal styling) sequences are propagated across wrapped lines
597
- > by default. Each line ends with a reset sequence and the next line
598
- > begins with the active style restored::
599
-
600
- > >>> wrap('\x1b[1;34mHello world\x1b[0m', width=6)
601
- > ['\x1b[1;34mHello\x1b[0m', '\x1b[1;34mworld\x1b[0m']
602
-
603
- > Set ``propagate_sgr=False`` to disable this behavior.
604
-
605
- > Like :func:`textwrap.wrap`, newlines in the input text are treated as
606
- > whitespace and collapsed. To preserve paragraph breaks, wrap each
607
- > paragraph separately::
608
-
609
- > >>> text = 'First line.\nSecond line.'
610
- > >>> wrap(text, 40) # newline collapsed to space
611
- > ['First line. Second line.']
612
- > >>> [line for para in text.split('\n')
613
- > ... for line in (wrap(para, 40) if para else [''])]
614
- > ['First line.', 'Second line.']
615
-
616
- > .. seealso::
617
-
618
- > :func:`textwrap.wrap`, :class:`textwrap.TextWrapper`
619
- > Standard library text wrapping (character-based).
620
-
621
- > :class:`.SequenceTextWrapper`
622
- > Class interface for advanced wrapping options.
623
-
624
- > .. versionadded:: 0.3.0
625
-
626
- > .. versionchanged:: 0.5.0
627
- > Added ``propagate_sgr`` parameter (default True).
628
-
629
- > .. versionchanged:: 0.6.0
630
- > Added ``expand_tabs``, ``replace_whitespace``, ``fix_sentence_endings``,
631
- > ``drop_whitespace``, ``max_lines``, and ``placeholder`` parameters.
632
-
633
- > Example::
634
-
635
- > >>> from wcwidth import wrap
636
- > >>> wrap('hello world', 5)
637
- > ['hello', 'world']
638
- > >>> wrap('中文字符', 4) # CJK characters (2 cells each)
639
- > ['中文', '字符']
640
- > """
641
- # pylint: disable=too-many-arguments,too-many-locals
642
- > wrapper = SequenceTextWrapper(
643
- > width=width,
644
- > control_codes=control_codes,
645
- > tabsize=tabsize,
646
- > expand_tabs=expand_tabs,
647
- > replace_whitespace=replace_whitespace,
648
- > ambiguous_width=ambiguous_width,
649
- > term_program=term_program,
650
- > initial_indent=initial_indent,
651
- > subsequent_indent=subsequent_indent,
652
- > fix_sentence_endings=fix_sentence_endings,
653
- > break_long_words=break_long_words,
654
- > break_on_hyphens=break_on_hyphens,
655
- > drop_whitespace=drop_whitespace,
656
- > max_lines=max_lines,
657
- > placeholder=placeholder,
658
- > )
659
- > lines = wrapper.wrap(text)
660
-
661
- > if propagate_sgr:
662
- > lines = _propagate_sgr(lines)
663
-
664
- > return lines