wcwidth 0.8.2__tar.gz → 0.8.3__tar.gz

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 (151) hide show
  1. {wcwidth-0.8.2 → wcwidth-0.8.3}/PKG-INFO +15 -17
  2. {wcwidth-0.8.2 → wcwidth-0.8.3}/docs/intro.rst +13 -15
  3. {wcwidth-0.8.2 → wcwidth-0.8.3}/docs/specs.rst +8 -5
  4. {wcwidth-0.8.2 → wcwidth-0.8.3}/pyproject.toml +1 -1
  5. {wcwidth-0.8.2 → wcwidth-0.8.3}/tests/test_clip.py +81 -1
  6. {wcwidth-0.8.2 → wcwidth-0.8.3}/tests/test_textwrap.py +20 -0
  7. {wcwidth-0.8.2 → wcwidth-0.8.3}/tox.ini +19 -6
  8. {wcwidth-0.8.2 → wcwidth-0.8.3}/ucs-detect/data/AbsoluteTelnetSSH.yaml +251 -304
  9. {wcwidth-0.8.2 → wcwidth-0.8.3}/ucs-detect/data/alacritty.yaml +22 -21
  10. {wcwidth-0.8.2 → wcwidth-0.8.3}/ucs-detect/data/bobcat.yaml +336 -61
  11. {wcwidth-0.8.2 → wcwidth-0.8.3}/ucs-detect/data/conemu.exe.yaml +5 -4
  12. {wcwidth-0.8.2 → wcwidth-0.8.3}/ucs-detect/data/contour.yaml +383 -187
  13. {wcwidth-0.8.2 → wcwidth-0.8.3}/ucs-detect/data/coolretroterm.yaml +170 -91
  14. {wcwidth-0.8.2 → wcwidth-0.8.3}/ucs-detect/data/extraterm.yaml +16 -860
  15. {wcwidth-0.8.2 → wcwidth-0.8.3}/ucs-detect/data/foot.yaml +34 -18
  16. {wcwidth-0.8.2 → wcwidth-0.8.3}/ucs-detect/data/ghostty.yaml +7 -22
  17. wcwidth-0.8.2/ucs-detect/data/lxterminal.yaml → wcwidth-0.8.3/ucs-detect/data/gnometerminal.yaml +1380 -1352
  18. {wcwidth-0.8.2 → wcwidth-0.8.3}/ucs-detect/data/hyper.yaml +872 -835
  19. {wcwidth-0.8.2 → wcwidth-0.8.3}/ucs-detect/data/kitty.yaml +246 -217
  20. {wcwidth-0.8.2 → wcwidth-0.8.3}/ucs-detect/data/konsole.yaml +135 -116
  21. {wcwidth-0.8.2 → wcwidth-0.8.3}/ucs-detect/data/libvterm.yaml +228 -155
  22. wcwidth-0.8.3/ucs-detect/data/lxterminal.yaml +13347 -0
  23. {wcwidth-0.8.2 → wcwidth-0.8.3}/ucs-detect/data/mintty.yaml +6 -5
  24. {wcwidth-0.8.2 → wcwidth-0.8.3}/ucs-detect/data/mlterm.yaml +146 -91
  25. {wcwidth-0.8.2 → wcwidth-0.8.3}/ucs-detect/data/putty.yaml +24 -20
  26. {wcwidth-0.8.2 → wcwidth-0.8.3}/ucs-detect/data/qterminal.yaml +59 -46
  27. {wcwidth-0.8.2 → wcwidth-0.8.3}/ucs-detect/data/rio.yaml +317 -106
  28. {wcwidth-0.8.2 → wcwidth-0.8.3}/ucs-detect/data/rxvtunicode.yaml +29 -16
  29. {wcwidth-0.8.2 → wcwidth-0.8.3}/ucs-detect/data/screen.yaml +1812 -1806
  30. {wcwidth-0.8.2 → wcwidth-0.8.3}/ucs-detect/data/st.yaml +32 -16
  31. {wcwidth-0.8.2 → wcwidth-0.8.3}/ucs-detect/data/tabby.yaml +1312 -612
  32. {wcwidth-0.8.2 → wcwidth-0.8.3}/ucs-detect/data/teraterm.yaml +7 -6
  33. {wcwidth-0.8.2 → wcwidth-0.8.3}/ucs-detect/data/terminalexe.yaml +19 -12
  34. wcwidth-0.8.3/ucs-detect/data/terminator.yaml +13561 -0
  35. {wcwidth-0.8.2 → wcwidth-0.8.3}/ucs-detect/data/terminology.yaml +222 -116
  36. {wcwidth-0.8.2 → wcwidth-0.8.3}/ucs-detect/data/tmux.yaml +195 -140
  37. {wcwidth-0.8.2 → wcwidth-0.8.3}/ucs-detect/data/vscodeterminal.yaml +35 -40
  38. {wcwidth-0.8.2 → wcwidth-0.8.3}/ucs-detect/data/warp.yaml +277 -870
  39. {wcwidth-0.8.2 → wcwidth-0.8.3}/ucs-detect/data/wezterm.yaml +334 -267
  40. {wcwidth-0.8.2 → wcwidth-0.8.3}/ucs-detect/data/xfce4terminal.yaml +1488 -1421
  41. {wcwidth-0.8.2 → wcwidth-0.8.3}/ucs-detect/data/xterm.yaml +18 -21
  42. {wcwidth-0.8.2 → wcwidth-0.8.3}/ucs-detect/data/zellij.yaml +268 -864
  43. {wcwidth-0.8.2 → wcwidth-0.8.3}/ucs-detect/data/zutty.yaml +57 -47
  44. {wcwidth-0.8.2 → wcwidth-0.8.3}/wcwidth/__init__.py +1 -1
  45. {wcwidth-0.8.2 → wcwidth-0.8.3}/wcwidth/_clip.py +65 -32
  46. {wcwidth-0.8.2 → wcwidth-0.8.3}/wcwidth/textwrap.py +15 -9
  47. wcwidth-0.8.3/wcwidth/textwrap.py,cover +664 -0
  48. wcwidth-0.8.2/ucs-detect/data/gnometerminal.yaml +0 -16247
  49. wcwidth-0.8.2/ucs-detect/data/terminator.yaml +0 -15308
  50. {wcwidth-0.8.2 → wcwidth-0.8.3}/.gitignore +0 -0
  51. {wcwidth-0.8.2 → wcwidth-0.8.3}/.pylintrc +0 -0
  52. {wcwidth-0.8.2 → wcwidth-0.8.3}/LICENSE +0 -0
  53. {wcwidth-0.8.2 → wcwidth-0.8.3}/README.rst +0 -0
  54. {wcwidth-0.8.2 → wcwidth-0.8.3}/bin/show-sequences +0 -0
  55. {wcwidth-0.8.2 → wcwidth-0.8.3}/bin/strip-sequences +0 -0
  56. {wcwidth-0.8.2 → wcwidth-0.8.3}/bin/update-tables.py +0 -0
  57. {wcwidth-0.8.2 → wcwidth-0.8.3}/bin/verify-table-integrity.py +0 -0
  58. {wcwidth-0.8.2 → wcwidth-0.8.3}/bin/wcwidth-browser.py +0 -0
  59. {wcwidth-0.8.2 → wcwidth-0.8.3}/bin/wcwidth-libc-comparator.py +0 -0
  60. {wcwidth-0.8.2 → wcwidth-0.8.3}/code_templates/grapheme_override_per_terminal.py.j2 +0 -0
  61. {wcwidth-0.8.2 → wcwidth-0.8.3}/code_templates/grapheme_override_table.py.j2 +0 -0
  62. {wcwidth-0.8.2 → wcwidth-0.8.3}/code_templates/grapheme_registry.py.j2 +0 -0
  63. {wcwidth-0.8.2 → wcwidth-0.8.3}/code_templates/grapheme_table.py.j2 +0 -0
  64. {wcwidth-0.8.2 → wcwidth-0.8.3}/code_templates/python_table.py.j2 +0 -0
  65. {wcwidth-0.8.2 → wcwidth-0.8.3}/code_templates/table_overrides.py.j2 +0 -0
  66. {wcwidth-0.8.2 → wcwidth-0.8.3}/code_templates/term_programs.py.j2 +0 -0
  67. {wcwidth-0.8.2 → wcwidth-0.8.3}/code_templates/unicode_version.rst.j2 +0 -0
  68. {wcwidth-0.8.2 → wcwidth-0.8.3}/code_templates/unicode_versions.py.j2 +0 -0
  69. {wcwidth-0.8.2 → wcwidth-0.8.3}/docs/api.rst +0 -0
  70. {wcwidth-0.8.2 → wcwidth-0.8.3}/docs/conf.py +0 -0
  71. {wcwidth-0.8.2 → wcwidth-0.8.3}/docs/index.rst +0 -0
  72. {wcwidth-0.8.2 → wcwidth-0.8.3}/docs/requirements.txt +0 -0
  73. {wcwidth-0.8.2 → wcwidth-0.8.3}/docs/unicode_version.rst +0 -0
  74. {wcwidth-0.8.2 → wcwidth-0.8.3}/requirements-develop.txt +0 -0
  75. {wcwidth-0.8.2 → wcwidth-0.8.3}/requirements-docs.in +0 -0
  76. {wcwidth-0.8.2 → wcwidth-0.8.3}/requirements-tests38.in +0 -0
  77. {wcwidth-0.8.2 → wcwidth-0.8.3}/requirements-tests38.txt +0 -0
  78. {wcwidth-0.8.2 → wcwidth-0.8.3}/requirements-tests39.in +0 -0
  79. {wcwidth-0.8.2 → wcwidth-0.8.3}/requirements-tests39.txt +0 -0
  80. {wcwidth-0.8.2 → wcwidth-0.8.3}/requirements-update.in +0 -0
  81. {wcwidth-0.8.2 → wcwidth-0.8.3}/requirements-update.txt +0 -0
  82. {wcwidth-0.8.2 → wcwidth-0.8.3}/tests/__init__.py +0 -0
  83. {wcwidth-0.8.2 → wcwidth-0.8.3}/tests/conftest.py +0 -0
  84. {wcwidth-0.8.2 → wcwidth-0.8.3}/tests/test_ambiguous.py +0 -0
  85. {wcwidth-0.8.2 → wcwidth-0.8.3}/tests/test_benchmarks.py +0 -0
  86. {wcwidth-0.8.2 → wcwidth-0.8.3}/tests/test_clip_cjk_emoji.py +0 -0
  87. {wcwidth-0.8.2 → wcwidth-0.8.3}/tests/test_clip_overtyping.py +0 -0
  88. {wcwidth-0.8.2 → wcwidth-0.8.3}/tests/test_core.py +0 -0
  89. {wcwidth-0.8.2 → wcwidth-0.8.3}/tests/test_emojis.py +0 -0
  90. {wcwidth-0.8.2 → wcwidth-0.8.3}/tests/test_grapheme.py +0 -0
  91. {wcwidth-0.8.2 → wcwidth-0.8.3}/tests/test_hyperlink.py +0 -0
  92. {wcwidth-0.8.2 → wcwidth-0.8.3}/tests/test_justify.py +0 -0
  93. {wcwidth-0.8.2 → wcwidth-0.8.3}/tests/test_sgr_state.py +0 -0
  94. {wcwidth-0.8.2 → wcwidth-0.8.3}/tests/test_term_overrides.py +0 -0
  95. {wcwidth-0.8.2 → wcwidth-0.8.3}/tests/test_text_sizing.py +0 -0
  96. {wcwidth-0.8.2 → wcwidth-0.8.3}/tests/test_ucslevel.py +0 -0
  97. {wcwidth-0.8.2 → wcwidth-0.8.3}/tests/test_width.py +0 -0
  98. {wcwidth-0.8.2 → wcwidth-0.8.3}/ucs-detect/data/_linuxfbdev.yaml +0 -0
  99. {wcwidth-0.8.2 → wcwidth-0.8.3}/ucs-detect/data/_syncterm.yaml +0 -0
  100. {wcwidth-0.8.2 → wcwidth-0.8.3}/ucs-detect/data/emoji-test-latest.txt +0 -0
  101. {wcwidth-0.8.2 → wcwidth-0.8.3}/ucs-detect/data/emoji-variation-sequences-latest.txt +0 -0
  102. {wcwidth-0.8.2 → wcwidth-0.8.3}/ucs-detect/data/emoji-zwj-sequences-latest.txt +0 -0
  103. {wcwidth-0.8.2 → wcwidth-0.8.3}/ucs-detect/data/iterm2.yaml +0 -0
  104. {wcwidth-0.8.2 → wcwidth-0.8.3}/ucs-detect/data/securecrt.yaml +0 -0
  105. {wcwidth-0.8.2 → wcwidth-0.8.3}/ucs-detect/data/terminalapp.yaml +0 -0
  106. {wcwidth-0.8.2 → wcwidth-0.8.3}/ucs-detect/data/westonterminal.yaml +0 -0
  107. {wcwidth-0.8.2 → wcwidth-0.8.3}/wcwidth/_constants.py +0 -0
  108. {wcwidth-0.8.2 → wcwidth-0.8.3}/wcwidth/_wcswidth.py +0 -0
  109. {wcwidth-0.8.2 → wcwidth-0.8.3}/wcwidth/_wcwidth.py +0 -0
  110. {wcwidth-0.8.2 → wcwidth-0.8.3}/wcwidth/_width.py +0 -0
  111. {wcwidth-0.8.2 → wcwidth-0.8.3}/wcwidth/align.py +0 -0
  112. {wcwidth-0.8.2 → wcwidth-0.8.3}/wcwidth/bisearch.py +0 -0
  113. {wcwidth-0.8.2 → wcwidth-0.8.3}/wcwidth/control_codes.py +0 -0
  114. {wcwidth-0.8.2 → wcwidth-0.8.3}/wcwidth/escape_sequences.py +0 -0
  115. {wcwidth-0.8.2 → wcwidth-0.8.3}/wcwidth/grapheme.py +0 -0
  116. {wcwidth-0.8.2 → wcwidth-0.8.3}/wcwidth/hyperlink.py +0 -0
  117. {wcwidth-0.8.2 → wcwidth-0.8.3}/wcwidth/py.typed +0 -0
  118. {wcwidth-0.8.2 → wcwidth-0.8.3}/wcwidth/sgr_state.py +0 -0
  119. {wcwidth-0.8.2 → wcwidth-0.8.3}/wcwidth/table_ambiguous.py +0 -0
  120. {wcwidth-0.8.2 → wcwidth-0.8.3}/wcwidth/table_grapheme.py +0 -0
  121. {wcwidth-0.8.2 → wcwidth-0.8.3}/wcwidth/table_grapheme_overrides/__init__.py +0 -0
  122. {wcwidth-0.8.2 → wcwidth-0.8.3}/wcwidth/table_grapheme_overrides/_known_27e0693f.py +0 -0
  123. {wcwidth-0.8.2 → wcwidth-0.8.3}/wcwidth/table_grapheme_overrides/_known_3d4826b8.py +0 -0
  124. {wcwidth-0.8.2 → wcwidth-0.8.3}/wcwidth/table_grapheme_overrides/_known_45d92e98.py +0 -0
  125. {wcwidth-0.8.2 → wcwidth-0.8.3}/wcwidth/table_grapheme_overrides/_known_4cdf59ce.py +0 -0
  126. {wcwidth-0.8.2 → wcwidth-0.8.3}/wcwidth/table_grapheme_overrides/_known_50bf0759.py +0 -0
  127. {wcwidth-0.8.2 → wcwidth-0.8.3}/wcwidth/table_grapheme_overrides/_known_529fbb4a.py +0 -0
  128. {wcwidth-0.8.2 → wcwidth-0.8.3}/wcwidth/table_grapheme_overrides/_known_5bfac390.py +0 -0
  129. {wcwidth-0.8.2 → wcwidth-0.8.3}/wcwidth/table_grapheme_overrides/_known_813fee16.py +0 -0
  130. {wcwidth-0.8.2 → wcwidth-0.8.3}/wcwidth/table_grapheme_overrides/_known_8589765c.py +0 -0
  131. {wcwidth-0.8.2 → wcwidth-0.8.3}/wcwidth/table_grapheme_overrides/_known_8f94b404.py +0 -0
  132. {wcwidth-0.8.2 → wcwidth-0.8.3}/wcwidth/table_grapheme_overrides/_known_970dbe10.py +0 -0
  133. {wcwidth-0.8.2 → wcwidth-0.8.3}/wcwidth/table_grapheme_overrides/_known_c0a2cdbf.py +0 -0
  134. {wcwidth-0.8.2 → wcwidth-0.8.3}/wcwidth/table_grapheme_overrides/_known_c2157f7e.py +0 -0
  135. {wcwidth-0.8.2 → wcwidth-0.8.3}/wcwidth/table_grapheme_overrides/_known_c3db41c0.py +0 -0
  136. {wcwidth-0.8.2 → wcwidth-0.8.3}/wcwidth/table_grapheme_overrides/_known_da9ceb0a.py +0 -0
  137. {wcwidth-0.8.2 → wcwidth-0.8.3}/wcwidth/table_grapheme_overrides/_known_e08bd75e.py +0 -0
  138. {wcwidth-0.8.2 → wcwidth-0.8.3}/wcwidth/table_grapheme_overrides/_known_e22030f3.py +0 -0
  139. {wcwidth-0.8.2 → wcwidth-0.8.3}/wcwidth/table_grapheme_overrides/_known_fcc05a0f.py +0 -0
  140. {wcwidth-0.8.2 → wcwidth-0.8.3}/wcwidth/table_grapheme_overrides/_known_fd9d4c44.py +0 -0
  141. {wcwidth-0.8.2 → wcwidth-0.8.3}/wcwidth/table_grapheme_overrides/_registry.py +0 -0
  142. {wcwidth-0.8.2 → wcwidth-0.8.3}/wcwidth/table_mc.py +0 -0
  143. {wcwidth-0.8.2 → wcwidth-0.8.3}/wcwidth/table_overrides.py +0 -0
  144. {wcwidth-0.8.2 → wcwidth-0.8.3}/wcwidth/table_term_programs.py +0 -0
  145. {wcwidth-0.8.2 → wcwidth-0.8.3}/wcwidth/table_vs15.py +0 -0
  146. {wcwidth-0.8.2 → wcwidth-0.8.3}/wcwidth/table_vs16.py +0 -0
  147. {wcwidth-0.8.2 → wcwidth-0.8.3}/wcwidth/table_wide.py +0 -0
  148. {wcwidth-0.8.2 → wcwidth-0.8.3}/wcwidth/table_zero.py +0 -0
  149. {wcwidth-0.8.2 → wcwidth-0.8.3}/wcwidth/text_sizing.py +0 -0
  150. {wcwidth-0.8.2 → wcwidth-0.8.3}/wcwidth/unicode_versions.py +0 -0
  151. {wcwidth-0.8.2 → wcwidth-0.8.3}/wcwidth/wcwidth.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: wcwidth
3
- Version: 0.8.2
3
+ Version: 0.8.3
4
4
  Summary: Measures the displayed width of unicode strings in a terminal
5
5
  Project-URL: Homepage, https://github.com/jquast/wcwidth
6
6
  Author-email: Jeff Quast <contact@jeffquast.com>
@@ -176,7 +176,7 @@ width()
176
176
  -------
177
177
 
178
178
  Use function `width()`_ to measure a string with improved handling of ``control_codes`` and
179
- measurement Corrections_ through ``term_program``:.
179
+ measurement Corrections_ through ``term_program``:
180
180
 
181
181
  .. code-block:: python
182
182
 
@@ -511,8 +511,7 @@ recognized names:
511
511
  .. END_LIST_TERM_PROGRAMS
512
512
 
513
513
  ``term_program=False`` (the default for `width()`_, `ljust()`_, `rjust()`_, `center()`_,
514
- `wrap()`_, and `clip()`_) disables terminal corrections. `wcstwidth()`_ defaults to
515
- ``term_program=True`` for auto-detection.
514
+ `wrap()`_, and `clip()`_) disables terminal corrections.
516
515
 
517
516
  For automatic tests and other purposes that require cross-environment consistency, set static values
518
517
  or unset ``TERM`` and ``TERM_PROGRAM`` environment values, such as in ``conftest.py`` with pytest:
@@ -588,16 +587,6 @@ To upgrade requirements for building documentation, run::
588
587
 
589
588
  tox -e update_requirements_docs
590
589
 
591
- Utilities
592
- ---------
593
-
594
- Supplementary tools for browsing and testing terminals for wide unicode
595
- characters are found in the `bin/`_ of this project's source code. Just ensure
596
- to first ``pip install -r requirements-develop.txt`` from this projects main
597
- folder. For example, an interactive browser for testing::
598
-
599
- python ./bin/wcwidth-browser.py
600
-
601
590
  ====
602
591
  Uses
603
592
  ====
@@ -685,9 +674,15 @@ languages:
685
674
  History
686
675
  =======
687
676
 
677
+ 0.8.3 *2026-08-28*
678
+ * **Bugfix** Do not hang on `wrap()`_ calls of width 1 with text containing OSC8 hyperlinks and
679
+ wide characters, `PR #231`_.
680
+ * **Bugfix** `clip()`_ with ``propagate_sgr=True``, should match behavior of `propagate_sgr()`_,
681
+ `PR #235`_.
682
+
688
683
  0.8.2 *2026-06-29*
689
- * **Bugfix** Do not raise IndexError when given legacy POSIX ``n`` argument to `wcswidth()`_ or
690
- `wcstwidth()`_ exceed string length without raising IndexError
684
+ * **Bugfix** Do not raise IndexError when legacy POSIX ``n`` argument to `wcswidth()`_ or
685
+ `wcstwidth()`_ exceeds the given string length. `PR #230`_
691
686
 
692
687
  0.8.1 *2026-06-08*
693
688
  * **Improved** `wcstwidth()`_ with new ``zeroer``, ``narrow_wider``, and ``narrow_zeroer``
@@ -942,6 +937,9 @@ https://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c::
942
937
  .. _`PR #223`: https://github.com/jquast/wcwidth/pull/223
943
938
  .. _`PR #224`: https://github.com/jquast/wcwidth/pull/224
944
939
  .. _`PR #226`: https://github.com/jquast/wcwidth/pull/226
940
+ .. _`PR #230`: https://github.com/jquast/wcwidth/pull/230
941
+ .. _`PR #231`: https://github.com/jquast/wcwidth/pull/231
942
+ .. _`PR #235`: https://github.com/jquast/wcwidth/pull/235
945
943
  .. _`Issue #101`: https://github.com/jquast/wcwidth/issues/101
946
944
  .. _`Issue #155`: https://github.com/jquast/wcwidth/issues/155
947
945
  .. _`Issue #190`: https://github.com/jquast/wcwidth/issues/190
@@ -145,7 +145,7 @@ width()
145
145
  -------
146
146
 
147
147
  Use function `width()`_ to measure a string with improved handling of ``control_codes`` and
148
- measurement Corrections_ through ``term_program``:.
148
+ measurement Corrections_ through ``term_program``:
149
149
 
150
150
  .. code-block:: python
151
151
 
@@ -480,8 +480,7 @@ recognized names:
480
480
  .. END_LIST_TERM_PROGRAMS
481
481
 
482
482
  ``term_program=False`` (the default for `width()`_, `ljust()`_, `rjust()`_, `center()`_,
483
- `wrap()`_, and `clip()`_) disables terminal corrections. `wcstwidth()`_ defaults to
484
- ``term_program=True`` for auto-detection.
483
+ `wrap()`_, and `clip()`_) disables terminal corrections.
485
484
 
486
485
  For automatic tests and other purposes that require cross-environment consistency, set static values
487
486
  or unset ``TERM`` and ``TERM_PROGRAM`` environment values, such as in ``conftest.py`` with pytest:
@@ -557,16 +556,6 @@ To upgrade requirements for building documentation, run::
557
556
 
558
557
  tox -e update_requirements_docs
559
558
 
560
- Utilities
561
- ---------
562
-
563
- Supplementary tools for browsing and testing terminals for wide unicode
564
- characters are found in the `bin/`_ of this project's source code. Just ensure
565
- to first ``pip install -r requirements-develop.txt`` from this projects main
566
- folder. For example, an interactive browser for testing::
567
-
568
- python ./bin/wcwidth-browser.py
569
-
570
559
  ====
571
560
  Uses
572
561
  ====
@@ -654,9 +643,15 @@ languages:
654
643
  History
655
644
  =======
656
645
 
646
+ 0.8.3 *2026-08-28*
647
+ * **Bugfix** Do not hang on `wrap()`_ calls of width 1 with text containing OSC8 hyperlinks and
648
+ wide characters, `PR #231`_.
649
+ * **Bugfix** `clip()`_ with ``propagate_sgr=True``, should match behavior of `propagate_sgr()`_,
650
+ `PR #235`_.
651
+
657
652
  0.8.2 *2026-06-29*
658
- * **Bugfix** Do not raise IndexError when given legacy POSIX ``n`` argument to `wcswidth()`_ or
659
- `wcstwidth()`_ exceed string length without raising IndexError
653
+ * **Bugfix** Do not raise IndexError when legacy POSIX ``n`` argument to `wcswidth()`_ or
654
+ `wcstwidth()`_ exceeds the given string length. `PR #230`_
660
655
 
661
656
  0.8.1 *2026-06-08*
662
657
  * **Improved** `wcstwidth()`_ with new ``zeroer``, ``narrow_wider``, and ``narrow_zeroer``
@@ -911,6 +906,9 @@ https://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c::
911
906
  .. _`PR #223`: https://github.com/jquast/wcwidth/pull/223
912
907
  .. _`PR #224`: https://github.com/jquast/wcwidth/pull/224
913
908
  .. _`PR #226`: https://github.com/jquast/wcwidth/pull/226
909
+ .. _`PR #230`: https://github.com/jquast/wcwidth/pull/230
910
+ .. _`PR #231`: https://github.com/jquast/wcwidth/pull/231
911
+ .. _`PR #235`: https://github.com/jquast/wcwidth/pull/235
914
912
  .. _`Issue #101`: https://github.com/jquast/wcwidth/issues/101
915
913
  .. _`Issue #155`: https://github.com/jquast/wcwidth/issues/155
916
914
  .. _`Issue #190`: https://github.com/jquast/wcwidth/issues/190
@@ -81,6 +81,14 @@ Width of 1
81
81
  String characters are measured width of 1 when they are not
82
82
  measured as `Width of 0`_ or `Width of 2`_.
83
83
 
84
+ Any character with `U+FE0E`_ (Variation Selector 15) defined as ``text style``
85
+ in `emoji-variation-sequences.txt`_, per `UTS #51`_ and `Unicode Standard
86
+ Section 23.4`_: VS15 subtracts 1 cell from the wide character it directly
87
+ follows, making the pair width 1. Narrow characters are unchanged.
88
+
89
+ .. note:: Variation Selector 15 width specification is contested, see
90
+ https://github.com/jquast/wcwidth/issues/211
91
+
84
92
  Width of 2
85
93
  ----------
86
94
 
@@ -105,11 +113,6 @@ in `emoji-variation-sequences.txt`_, per `UTS #51`_ and `Unicode Standard
105
113
  Section 23.4`_: VS16 adds 1 cell to the narrow character it directly follows,
106
114
  making the pair width 2. Wide characters are unchanged.
107
115
 
108
- Any character with `U+FE0E`_ (Variation Selector 15) defined as ``text style``
109
- in `emoji-variation-sequences.txt`_, per `UTS #51`_ and `Unicode Standard
110
- Section 23.4`_: VS15 subtracts 1 cell from the wide character it directly
111
- follows, making the pair width 1. Narrow characters are unchanged.
112
-
113
116
  Any character of non-zero width followed by an ``Mc`` (`Spacing Combining Mark`_)
114
117
  character when measured in sequence by :func:`wcwidth.wcswidth` or
115
118
  :func:`wcwidth.width`. The ``Mc`` character caps the cluster width at 2,
@@ -4,7 +4,7 @@ requires = [ "hatchling" ]
4
4
 
5
5
  [project]
6
6
  name = "wcwidth"
7
- version = "0.8.2" # don't forget to also update wcwidth/__init__.py:__version__
7
+ version = "0.8.3" # don't forget to also update wcwidth/__init__.py:__version__
8
8
  description = "Measures the displayed width of unicode strings in a terminal"
9
9
  readme = "README.rst"
10
10
  keywords = [
@@ -4,7 +4,7 @@
4
4
  import pytest
5
5
 
6
6
  # local
7
- from wcwidth import clip, width, strip_sequences
7
+ from wcwidth import clip, width, propagate_sgr, strip_sequences
8
8
 
9
9
  STRIP_SEQUENCES_CASES = [
10
10
  ('', ''),
@@ -132,6 +132,86 @@ def test_clip_sequences_only():
132
132
  assert repr(clip('\x1b[31m\x1b[0m', 0, 10, propagate_sgr=False)) == repr('\x1b[31m\x1b[0m')
133
133
 
134
134
 
135
+ SGR_PATH_KWARGS = [{}, {'overtyping': True}, {'control_codes': 'ignore'}]
136
+
137
+
138
+ @pytest.mark.parametrize('kwargs', SGR_PATH_KWARGS)
139
+ def test_clip_sgr_inside_window_preserved(kwargs):
140
+ """Style changes within (start, end) are emitted at their original position."""
141
+ # nothing is clipped away, so nothing changes,
142
+ assert (clip('\x1b[1mbold\x1b[m normal', 0, 20, **kwargs) ==
143
+ '\x1b[1mbold\x1b[m normal')
144
+ # and the same holds when the tail is clipped, the trailing reset is
145
+ # synthesized because the final style is still active.
146
+ assert (clip('\x1b[31mred\x1b[32mgreen\x1b[0m', 0, 4, **kwargs) ==
147
+ '\x1b[31mred\x1b[32mg\x1b[0m')
148
+
149
+
150
+ @pytest.mark.parametrize('kwargs', SGR_PATH_KWARGS)
151
+ def test_clip_sgr_before_start_folded_into_prefix(kwargs):
152
+ """Style set before *start* is synthesized as a prefix, not emitted twice."""
153
+ assert clip('\x1b[1;34mHello world\x1b[0m', 6, 11, **kwargs) == '\x1b[1;34mworld\x1b[0m'
154
+ # the style in effect at *start* is the most recent one,
155
+ assert clip('\x1b[31mred\x1b[32mgreen\x1b[0m', 4, 8, **kwargs) == '\x1b[32mreen\x1b[0m'
156
+
157
+
158
+ @pytest.mark.parametrize('kwargs', SGR_PATH_KWARGS)
159
+ def test_clip_sgr_after_end_discarded(kwargs):
160
+ """Style set at or beyond *end* does not appear in the result."""
161
+ assert clip('hello\x1b[31m world\x1b[0m', 0, 5, **kwargs) == 'hello'
162
+
163
+
164
+ @pytest.mark.parametrize('kwargs', SGR_PATH_KWARGS)
165
+ @pytest.mark.parametrize('text', [
166
+ 'plain text',
167
+ '\x1b[31mred',
168
+ '\x1b[31mred\x1b[0m',
169
+ '\x1b[1mbold\x1b[m normal',
170
+ '\x1b[31mred\x1b[32mgreen\x1b[0m',
171
+ 'plain \x1b[4munderline',
172
+ ])
173
+ def test_clip_matches_propagate_sgr(text, kwargs):
174
+ """Clipping nothing away agrees with :func:`propagate_sgr`."""
175
+ assert clip(text, 0, width(text), **kwargs) == propagate_sgr([text])[0]
176
+
177
+
178
+ @pytest.mark.parametrize('kwargs', SGR_PATH_KWARGS)
179
+ @pytest.mark.parametrize('text', [
180
+ '\x1b[1m\x1b[31mbold red\x1b[0m',
181
+ '\x1b[31mred\x1b[32mgreen\x1b[0m',
182
+ '\x1b[?25l\x1b[31mred',
183
+ ])
184
+ def test_clip_sgr_result_is_self_contained(text, kwargs):
185
+ """The result carries its own style: propagating it again is a no-op."""
186
+ result = clip(text, 0, width(text), **kwargs)
187
+ assert propagate_sgr([result])[0] == result
188
+ assert strip_sequences(result) == strip_sequences(text)
189
+
190
+
191
+ @pytest.mark.parametrize('kwargs', SGR_PATH_KWARGS)
192
+ def test_clip_sgr_after_non_sgr_sequence(kwargs):
193
+ """A non-SGR sequence before the first visible cell does not swallow the style."""
194
+ assert clip('\x1b[?25l\x1b[31mred', 0, 10, **kwargs) == '\x1b[31m\x1b[?25lred\x1b[0m'
195
+
196
+
197
+ @pytest.mark.parametrize('kwargs', [{}, {'overtyping': True}])
198
+ def test_clip_sgr_inside_hyperlink_tracked(kwargs):
199
+ """Style changed by hyperlink inner text still terminates with a reset."""
200
+ text = '\x1b]8;;http://x\x07a\x1b[31mb\x1b]8;;\x07c'
201
+ assert clip(text, 0, 10, **kwargs) == text + '\x1b[0m'
202
+
203
+
204
+ @pytest.mark.parametrize('kwargs', [{}, {'overtyping': True}])
205
+ def test_clip_sgr_inside_hyperlink_untracked(kwargs):
206
+ """With ``propagate_sgr=False``, hyperlink inner style is neither tracked nor reset."""
207
+ text = '\x1b]8;;http://x\x07a\x1b[31mb\x1b]8;;\x07c'
208
+ # sequences pass through verbatim, no synthesized prefix or trailing reset,
209
+ assert clip(text, 0, 10, propagate_sgr=False, **kwargs) == text
210
+ # and that holds when 'c', the only cell outside the hyperlink, is clipped away.
211
+ assert (clip(text, 0, 2, propagate_sgr=False, **kwargs) ==
212
+ '\x1b]8;;http://x\x07a\x1b[31mb\x1b]8;;\x07')
213
+
214
+
135
215
  def test_clip_sequences_osc_hyperlink():
136
216
  assert repr(clip('\x1b]8;;https://example.com\x07link\x1b]8;;\x07', 0, 4)) == repr(
137
217
  '\x1b]8;;https://example.com\x07link\x1b]8;;\x07'
@@ -387,6 +387,16 @@ OSC_END_BEL = '\x1b]8;;\x07'
387
387
  '\x1b]8;foo=bar:id=mylink;http://example.com\x1b\\Click\x1b]8;;\x1b\\',
388
388
  '\x1b]8;foo=bar:id=mylink;http://example.com\x1b\\here\x1b]8;;\x1b\\',
389
389
  ],
390
+ ),
391
+ ( # wide grapheme after OSC 8 open at width 1 (must not hang)
392
+ '\x1b]8;;u\x07😀',
393
+ 1,
394
+ ['\x1b]8;id=00000001;u\x07😀\x1b]8;;\x07'],
395
+ ),
396
+ ( # CJK wide char after OSC 8 open at width 1 (must not hang)
397
+ '\x1b]8;;u\x07あ',
398
+ 1,
399
+ ['\x1b]8;id=00000001;u\x07あ\x1b]8;;\x07'],
390
400
  ),])
391
401
  def test_wrap_hyperlink_word_boundary(text, w, expected):
392
402
  """OSC hyperlink sequences should act as word boundaries."""
@@ -491,3 +501,13 @@ def test_wrap_bare_esc():
491
501
  """Bare ESC not part of a recognized sequence is treated as zero-width."""
492
502
  assert wrap('ab\x1bcd ef', 5) == ['ab\x1bcd', 'ef']
493
503
  assert wrap('ab\x1b\x00cdef', 3) == ['ab\x1b\x00c', 'def']
504
+
505
+
506
+ def test_wrap_bare_esc_at_line_start():
507
+ """
508
+ Exercises the bare-ESC safety break in _find_first_visible_break.
509
+
510
+ A bare \\x1b that does not match ZERO_WIDTH_PATTERN triggers a safety 'break' statement but is
511
+ not found in any practical terminal sequence or string (ESC followed by NUL).
512
+ """
513
+ assert wrap('\x1b\x00あ', 1) == ['\x1b', '\x00', 'あ']
@@ -1,5 +1,5 @@
1
1
  [tox]
2
- envlist = update, fetch, compile, autopep8, docformatter, docformatter_check, isort, isort_check, pylint, pylint_tests, flake8, flake8_tests, pydocstyle, mypy, codespell, format, lint, docs, verify_tables, py{38, 39, 310, 311, 312, 313, 314}
2
+ envlist = update, fetch, compile, autopep8, docformatter, docformatter_check, isort, isort_check, pylint, pylint_tests, flake8, flake8_tests, pydocstyle, mypy, codespell, twine_check, format, lint, docs, verify_tables, py{38, 39, 310, 311, 312, 313, 314}
3
3
  skip_missing_interpreters = true
4
4
 
5
5
  [base]
@@ -122,13 +122,13 @@ commands =
122
122
  wcwidth/ bin/ tests/
123
123
 
124
124
  [testenv:isort]
125
- deps = isort
125
+ deps = isort==8.0.1
126
126
  basepython = python3.13
127
- commands = isort --quiet --apply --recursive wcwidth tests bin
127
+ commands = isort --quiet wcwidth tests bin
128
128
 
129
129
  [testenv:pylint]
130
130
  basepython = python3.13
131
- deps = pylint
131
+ deps = pylint==4.0.7
132
132
  commands = pylint --rcfile={toxinidir}/.pylintrc \
133
133
  --ignore=tests,docs,conf.py,build,distutils,.pyenv,.git,.tox \
134
134
  {posargs:{toxinidir}}/wcwidth
@@ -183,7 +183,7 @@ commands = docformatter --check --diff --recursive --pre-summary-newline \
183
183
 
184
184
  [testenv:isort_check]
185
185
  basepython = python3.14
186
- deps = isort
186
+ deps = {[testenv:isort]deps}
187
187
  commands = isort --diff --check-only wcwidth tests bin
188
188
 
189
189
  [testenv:flake8_tests]
@@ -194,7 +194,7 @@ commands = flake8 --ignore=E501,W504,F401 tests/
194
194
  [testenv:pylint_tests]
195
195
  basepython = python3.13
196
196
  deps = pytest
197
- pylint
197
+ {[testenv:pylint]deps}
198
198
  commands = pylint --rcfile={toxinidir}/.pylintrc \
199
199
  --disable=protected-access,unused-argument,missing-function-docstring,line-too-long \
200
200
  tests
@@ -217,6 +217,18 @@ commands = codespell --skip="*.pyc,htmlcov,_build,build,*.egg-info,.tox,data,./t
217
217
  --uri-ignore-words-list '*' \
218
218
  --summary --count
219
219
 
220
+ [testenv:twine_check]
221
+ # Verify the built distributions the way upload.pypi.org does: 'twine check'
222
+ # runs the README through readme_renderer, which halts on any reStructuredText
223
+ # error (such as a reference with no matching target). Neither doc8 (D000 is
224
+ # disabled) nor the python linters inspect README.rst.
225
+ basepython = python3.14
226
+ skip_install = true
227
+ deps = build
228
+ twine
229
+ commands = python -m build --outdir {envtmpdir}/dist
230
+ python -c "import glob, subprocess, sys; subprocess.check_call(['twine', 'check', '--strict'] + sorted(glob.glob(sys.argv[1] + '/dist/*')))" {envtmpdir}
231
+
220
232
  [testenv:format]
221
233
  basepython = python3.13
222
234
  deps = {[testenv:isort]deps}
@@ -234,6 +246,7 @@ deps = {[testenv:flake8]deps}
234
246
  {[testenv:pydocstyle]deps}
235
247
  {[testenv:pylint_tests]deps}
236
248
  {[testenv:codespell]deps}
249
+ {[testenv:twine_check]deps}
237
250
  commands = {[testenv:compile]commands}
238
251
  {[testenv:flake8]commands}
239
252
  {[testenv:mypy]commands}