cellstr 0.2.1__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 (41) hide show
  1. cellstr-0.2.1/.gitignore +16 -0
  2. cellstr-0.2.1/CHANGELOG.md +53 -0
  3. cellstr-0.2.1/CONTRIBUTING.md +28 -0
  4. cellstr-0.2.1/GUIDE.md +149 -0
  5. cellstr-0.2.1/LICENSE +21 -0
  6. cellstr-0.2.1/LICENSE-UNICODE +39 -0
  7. cellstr-0.2.1/PKG-INFO +144 -0
  8. cellstr-0.2.1/README.md +108 -0
  9. cellstr-0.2.1/data/ucd/MANIFEST.json +65 -0
  10. cellstr-0.2.1/pyproject.toml +80 -0
  11. cellstr-0.2.1/scripts/fetch_upstream.py +40 -0
  12. cellstr-0.2.1/scripts/gen_ucd_tables.py +202 -0
  13. cellstr-0.2.1/src/cellstr/__init__.py +41 -0
  14. cellstr-0.2.1/src/cellstr/__main__.py +8 -0
  15. cellstr-0.2.1/src/cellstr/_ansi.py +62 -0
  16. cellstr-0.2.1/src/cellstr/_cells.py +113 -0
  17. cellstr-0.2.1/src/cellstr/_cli.py +184 -0
  18. cellstr-0.2.1/src/cellstr/_graphemes.py +101 -0
  19. cellstr-0.2.1/src/cellstr/_ops.py +147 -0
  20. cellstr-0.2.1/src/cellstr/_props.py +61 -0
  21. cellstr-0.2.1/src/cellstr/_truncate.py +130 -0
  22. cellstr-0.2.1/src/cellstr/_ucd_data.py +31 -0
  23. cellstr-0.2.1/src/cellstr/_ucd_part1.py +166 -0
  24. cellstr-0.2.1/src/cellstr/_width.py +111 -0
  25. cellstr-0.2.1/src/cellstr/py.typed +0 -0
  26. cellstr-0.2.1/tests/conftest.py +28 -0
  27. cellstr-0.2.1/tests/test_ansi.py +66 -0
  28. cellstr-0.2.1/tests/test_cli.py +90 -0
  29. cellstr-0.2.1/tests/test_contract.py +173 -0
  30. cellstr-0.2.1/tests/test_docs_examples.py +32 -0
  31. cellstr-0.2.1/tests/test_doctests.py +24 -0
  32. cellstr-0.2.1/tests/test_e2e_wheel.py +69 -0
  33. cellstr-0.2.1/tests/test_edges.py +67 -0
  34. cellstr-0.2.1/tests/test_fuzz.py +131 -0
  35. cellstr-0.2.1/tests/test_grapheme_conformance.py +57 -0
  36. cellstr-0.2.1/tests/test_ops.py +126 -0
  37. cellstr-0.2.1/tests/test_oracle_wcwidth.py +140 -0
  38. cellstr-0.2.1/tests/test_truncate.py +89 -0
  39. cellstr-0.2.1/tests/test_ucd_tables.py +79 -0
  40. cellstr-0.2.1/tests/test_width.py +141 -0
  41. cellstr-0.2.1/tests/test_width_contract.py +78 -0
@@ -0,0 +1,16 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ venv/
5
+ build/
6
+ dist/
7
+ *.egg-info/
8
+ .pytest_cache/
9
+ .mypy_cache/
10
+ .ruff_cache/
11
+ .coverage
12
+ htmlcov/
13
+
14
+ # Unicode data files, fetched locally by scripts/fetch_upstream.py; only the manifest is tracked.
15
+ data/ucd/*
16
+ !data/ucd/MANIFEST.json
@@ -0,0 +1,53 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
5
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.2.1]
10
+
11
+ ### Changed
12
+
13
+ - The Unicode Character Database files are no longer committed. `data/ucd/MANIFEST.json`
14
+ pins them to Unicode 18.0.0, plus the 15.1.0 EastAsianWidth.txt the oracle test uses
15
+ (URL, size, sha256, licence), and `scripts/fetch_upstream.py`
16
+ fetches them; the generator check and the conformance tests skip until then. The
17
+ generated tables stay in the package and the Unicode licence notice ships in the wheel.
18
+ - The sdist ships only the sources, tests, scripts and docs.
19
+
20
+ ## [0.2.0]
21
+
22
+ ### Added
23
+
24
+ - Grapheme-aware width: emoji ZWJ sequences, skin tone and tag sequences, keycaps and
25
+ regional indicator flags measure as one two-cell glyph; VS16 makes an emoji wide and
26
+ VS15 makes it narrow. New `iter_graphemes()` (UAX #29) and `grapheme_width()`.
27
+ - `ambiguous=1 | 2` on every function for East Asian Ambiguous characters.
28
+ - `tabsize=` on every function, and `expand_tabs()` that counts display cells.
29
+ - `truncate(..., position="end" | "start" | "middle")`, `truncate_detailed()` returning
30
+ a `Truncation`, and `fit()` to truncate or pad to an exact width.
31
+ - `iter_cells()` yielding typed `Cell` records (text, width, column, escape flag).
32
+ - Recognition of DCS, SOS, PM and APC strings, charset designations and the remaining
33
+ single-byte escapes, alongside CSI and OSC (BEL or ST terminated).
34
+ - The `cellstr` command: `measure`, `truncate`, `pad`, `fit` and `slice` over stdin lines.
35
+ - Width, grapheme and emoji tables generated from a committed Unicode 18.0.0 UCD snapshot
36
+ by `scripts/gen_ucd_tables.py`, with the Unicode License in `LICENSE-UNICODE`.
37
+
38
+ ### Changed
39
+
40
+ - `cell_len`, `cell_slice`, `truncate` and the padding functions measure grapheme
41
+ clusters, so emoji sequences change width: the family emoji was 6 cells and is now 2,
42
+ a flag was 2 and stays 2, an emoji with VS15 is now 1.
43
+ - `truncate` rejects a suffix that contains a tab or a line break.
44
+ - Width tables move from a hand-vendored Unicode 15.1 extract to generated Unicode
45
+ 18.0.0 tables. Spacing marks (Mc) are one cell, as in Kuhn's wcwidth and glibc.
46
+ - `cell_slice` never splits a grapheme cluster, so accents stay on their letter.
47
+ - `ljust`, `rjust` and `center` accept any one-cell grapheme cluster as the fill.
48
+
49
+ ## [0.1.0]
50
+
51
+ - First release: `cell_len`, `char_width`, `cell_slice`, `ljust`, `rjust`,
52
+ `center`, `truncate` and `strip_ansi`, with a vendored Unicode width table and
53
+ full type hints.
@@ -0,0 +1,28 @@
1
+ # Contributing
2
+
3
+ Thanks for taking the time. This project keeps a small, predictable shape.
4
+
5
+ ## Setup
6
+
7
+ ```
8
+ pip install -r requirements-dev.txt
9
+ python scripts/fetch_upstream.py
10
+ ```
11
+
12
+ The fetch step is optional: it downloads the Unicode data files listed in
13
+ `data/ucd/MANIFEST.json` (checked against their sha256) into `data/ucd/`, which
14
+ git ignores. The generator check and the tests that need them skip until they are there.
15
+
16
+ ## Before you open a pull request
17
+
18
+ - Keep public API in `src/cellstr/__init__.py`; internals live in `_`-prefixed modules.
19
+ - Add a test for the behaviour you change. Tests inject time, disk and network; they
20
+ never touch the real thing.
21
+ - Never edit `src/cellstr/_ucd_*.py` by hand: they are generated from the fetched `data/ucd/` files
22
+ by `python scripts/gen_ucd_tables.py` (see `data/ucd/MANIFEST.json`).
23
+ - Run `scripts/check.ps1` (format, lint, types, table freshness, tests) and make sure it
24
+ passes. The wheel end-to-end test needs network access for the build backend;
25
+ offline, run `pytest -m "not e2e"`.
26
+ - One logical change per commit, imperative subject line.
27
+
28
+ Open an issue first if the change is large or shifts the public API.
cellstr-0.2.1/GUIDE.md ADDED
@@ -0,0 +1,149 @@
1
+ # cellstr guide
2
+
3
+ This guide explains how `cellstr` decides how wide text is, where terminals disagree, and
4
+ how to pick the policy that matches yours.
5
+
6
+ ## The rules
7
+
8
+ Width is decided per extended grapheme cluster (UAX #29), the unit a reader sees as one
9
+ character. Escape sequences are never part of a cluster: they count as zero cells, and an
10
+ escape sequence between two code points ends the cluster before it.
11
+
12
+ **Single code points** follow Markus Kuhn's reference `wcwidth`
13
+ (https://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c), applied to Unicode 18.0.0 data:
14
+
15
+ | Code points | Cells |
16
+ | --- | --- |
17
+ | U+0000, C0 and C1 controls, DEL | 0 (Kuhn returns -1 for controls; 0 is easier to add up) |
18
+ | Nonspacing and enclosing marks (General_Category Mn, Me) | 0 |
19
+ | Format characters (Cf), such as ZERO WIDTH SPACE and ZERO WIDTH JOINER | 0 |
20
+ | SOFT HYPHEN U+00AD | 1, or `ambiguous` since it is East Asian Ambiguous (it is visible) |
21
+ | Hangul medial vowels and final consonants U+1160..U+11FF, U+D7B0..U+D7FF | 0 |
22
+ | East_Asian_Width W (wide) and F (fullwidth) | 2 |
23
+ | East_Asian_Width A (ambiguous) | `ambiguous`, 1 by default |
24
+ | Everything else, including spacing marks (Mc) | 1 |
25
+
26
+ Unassigned code points follow the defaults in the header of EastAsianWidth.txt: wide in
27
+ the CJK ideograph blocks and in Planes 2 and 3, narrow elsewhere.
28
+
29
+ **Clusters of several code points** add up their code points, which keeps accents at zero
30
+ and a Hangul syllable built from jamo at two, except for emoji sequences, which draw as a
31
+ single glyph:
32
+
33
+ | Cluster | Example | Cells |
34
+ | --- | --- | --- |
35
+ | Emoji + VS16 (U+FE0F, emoji presentation) | ❤️ | 2 |
36
+ | Emoji + VS15 (U+FE0E, text presentation) | ⌚︎ | 1 |
37
+ | Emoji modifier sequence (skin tone) | 👍🏽 | 2 |
38
+ | Emoji ZWJ sequence | 👨‍👩‍👧 | 2 |
39
+ | Emoji tag sequence (subdivision flag) | 🏴󠁧󠁢󠁳󠁣󠁴󠁿 | 2 |
40
+ | Pair of regional indicators (flag) | 🇯🇵 | 2 |
41
+ | Keycap sequence | 1️⃣ | 2 |
42
+
43
+ ```python
44
+ from cellstr import char_width, grapheme_width
45
+
46
+ char_width("❤") # -> 1
47
+ grapheme_width("❤️") # -> 2
48
+ grapheme_width("⌚︎") # -> 1
49
+ grapheme_width("\U0001f44d\U0001f3fd") # -> 2
50
+ grapheme_width("각") # -> 2
51
+ char_width("é"), char_width("́") # -> (1, 0)
52
+ ```
53
+
54
+ ## Where terminals disagree
55
+
56
+ No standard says how many cells a character takes, so terminals make different choices.
57
+ These are the cases that matter in practice.
58
+
59
+ **East Asian Ambiguous characters.** Greek and Cyrillic letters, box drawing, circled
60
+ digits and many symbols are one cell in Western use and two in legacy CJK use. Most
61
+ terminals let the user choose: xterm has the `cjkWidth` resource, GNOME Terminal and
62
+ other VTE terminals have an "Ambiguous-width characters" preference, and iTerm2 can treat
63
+ ambiguous characters as double width. Pass `ambiguous=2` (or `--ambiguous 2` on the
64
+ command line) when your users run such a setting, typically with a CJK locale.
65
+
66
+ ```python
67
+ from cellstr import cell_len, ljust
68
+
69
+ cell_len("── α") # -> 4
70
+ cell_len("── α", ambiguous=2) # -> 7
71
+ ljust("ж", 3, ambiguous=2) # -> "ж "
72
+ ```
73
+
74
+ **Emoji ZWJ sequences.** A terminal that measures code point by code point, as glibc's
75
+ `wcwidth` does, advances the cursor six cells for the family emoji (2 + 0 + 2 + 0 + 2)
76
+ and often draws three separate faces. Terminals that segment grapheme clusters draw one
77
+ glyph in two cells. `cellstr` measures two. If your
78
+ terminal works per code point, measure with the recipe below.
79
+
80
+ **Variation selectors.** VS16 asks for emoji presentation. Terminals that ignore it keep
81
+ a text-default symbol such as U+2764 at one cell and let the colour glyph overflow into
82
+ the next cell; `cellstr` measures two. VS15 asks for text presentation: `cellstr` measures
83
+ one cell, while terminals that ignore it, and the `wcwidth` package, keep the emoji at two.
84
+
85
+ **Spacing marks.** Vowel signs in Indic scripts such as U+093F are spacing marks (Mc).
86
+ Kuhn's reference gives them one cell (only Mn, Me and Cf are zero there), the `wcwidth`
87
+ package gives them zero. `cellstr` gives one, so कि is two cells here.
88
+
89
+ **Code point by code point recipe.** For a terminal without grapheme support, add up
90
+ single code points yourself:
91
+
92
+ ```python
93
+ from cellstr import char_width, strip_ansi
94
+
95
+ text = "\U0001f468‍\U0001f469‍\U0001f467"
96
+ sum(char_width(ch) for ch in strip_ansi(text)) # -> 6
97
+ ```
98
+
99
+ ## Slicing, padding and truncation
100
+
101
+ - `cell_slice(text, start, stop)` keeps the clusters that sit fully inside the half-open
102
+ column range. A wide cluster cut by either edge is dropped whole, so the result can be
103
+ one cell short; `fit()` pads that cell back. Every escape sequence is kept, so a
104
+ hyperlink or colour that opens before the range still closes after it.
105
+ - `ljust`, `rjust` and `center` pad with a one-cell fill character and never shorten text.
106
+ - `truncate(..., position=...)` cuts at the end (keep the start), at the start (keep the
107
+ end, useful for log tails) or in the middle (keep both ends, useful for paths).
108
+ `truncate_detailed` returns a `Truncation` with the result, its width, the original
109
+ width and whether anything was cut.
110
+ - With `tabsize` set, tabs advance to the next tab stop, counted from the start of the
111
+ line, and become spaces in slices and padded output so the layout does not shift.
112
+
113
+ ```python
114
+ from cellstr import cell_slice, fit, truncate
115
+
116
+ link = "\x1b]8;;https://example.com\x1b\\docs\x1b]8;;\x1b\\"
117
+ cell_slice(link, 0, 2) # -> "\x1b]8;;https://example.com\x1b\\do\x1b]8;;\x1b\\"
118
+ log = "12:00:01 worker-7 job finished"
119
+ truncate(log, 16, position="start") # -> "... job finished"
120
+ fit("\x1b[1mok\x1b[0m", 4, align="center") # -> " \x1b[1mok\x1b[0m "
121
+ ```
122
+
123
+ ## Differences from the wcwidth package
124
+
125
+ `tests/test_oracle_wcwidth.py` compares every code point with `wcwidth` 0.2.13 (whose
126
+ newest tables are Unicode 15.1.0). The package is only a test oracle, never a runtime
127
+ dependency. Result: 1,112,200 of 1,114,112 code points agree (99.83 %). Every one of the
128
+ 1,912 differences has a recorded reason, and the test fails on any new one:
129
+
130
+ | Reason | Code points |
131
+ | --- | --- |
132
+ | Assigned after Unicode 15.1 | 1,263 |
133
+ | East_Asian_Width changed after 15.1 (for example the trigrams U+2630..U+2637 became W) | 188 |
134
+ | General_Category changed after 15.1 (U+1171E became Mc) | 1 |
135
+ | Spacing marks (Mc): 1 here, as in Kuhn's reference, 0 in `wcwidth` | 452 |
136
+ | SOFT HYPHEN, LINE SEPARATOR, PARAGRAPH SEPARATOR: 1 here (Kuhn), 0 in `wcwidth` | 3 |
137
+ | Lone skin tone modifier: 2 here (it is East Asian Wide), 0 in `wcwidth` | 5 |
138
+
139
+ For strings, `cellstr` and `wcswidth` agree on ZWJ sequences, flags, skin tones, VS16
140
+ and keycaps. They differ on VS15 and spacing marks, as described above.
141
+
142
+ ## Unicode data
143
+
144
+ The tables are built from the Unicode 18.0.0 data files, which are not committed:
145
+ `data/ucd/MANIFEST.json` pins their URLs, sizes and SHA-256 sums, and
146
+ `python scripts/fetch_upstream.py` fetches them into `data/ucd/` (the tests that read them
147
+ skip until then). To move to a new Unicode version, update the manifest, fetch, run
148
+ `python scripts/gen_ucd_tables.py` and the test suite. `tests/test_ucd_tables.py` fails if
149
+ the committed tables differ from a fresh run of the generator.
cellstr-0.2.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Javier Lianes
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,39 @@
1
+ UNICODE LICENSE V3
2
+
3
+ COPYRIGHT AND PERMISSION NOTICE
4
+
5
+ Copyright © 1991-2026 Unicode, Inc.
6
+
7
+ NOTICE TO USER: Carefully read the following legal agreement. BY
8
+ DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR
9
+ SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE
10
+ TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT
11
+ DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE.
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a
14
+ copy of data files and any associated documentation (the "Data Files") or
15
+ software and any associated documentation (the "Software") to deal in the
16
+ Data Files or Software without restriction, including without limitation
17
+ the rights to use, copy, modify, merge, publish, distribute, and/or sell
18
+ copies of the Data Files or Software, and to permit persons to whom the
19
+ Data Files or Software are furnished to do so, provided that either (a)
20
+ this copyright and permission notice appear with all copies of the Data
21
+ Files or Software, or (b) this copyright and permission notice appear in
22
+ associated Documentation.
23
+
24
+ THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
25
+ KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
26
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
27
+ THIRD PARTY RIGHTS.
28
+
29
+ IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE
30
+ BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES,
31
+ OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
32
+ WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
33
+ ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA
34
+ FILES OR SOFTWARE.
35
+
36
+ Except as contained in this notice, the name of a copyright holder shall
37
+ not be used in advertising or otherwise to promote the sale, use or other
38
+ dealings in these Data Files or Software without prior written
39
+ authorization of the copyright holder.
cellstr-0.2.1/PKG-INFO ADDED
@@ -0,0 +1,144 @@
1
+ Metadata-Version: 2.5
2
+ Name: cellstr
3
+ Version: 0.2.1
4
+ Summary: Display-width aware len, slice, pad and truncate for terminal text: wide CJK, emoji sequences, combining marks and ANSI codes
5
+ Project-URL: Homepage, https://lianes8studio.com
6
+ Project-URL: Repository, https://github.com/JaviLianes8/cellstr
7
+ Project-URL: Issues, https://github.com/JaviLianes8/cellstr/issues
8
+ Project-URL: Changelog, https://github.com/JaviLianes8/cellstr/blob/main/CHANGELOG.md
9
+ Project-URL: LinkedIn, https://www.linkedin.com/in/jlianes/
10
+ Project-URL: Funding, https://buymeacoffee.com/jlianesglrs
11
+ Author-email: Javier Lianes <javier@lianes8studio.com>
12
+ License-Expression: MIT
13
+ License-File: LICENSE
14
+ License-File: LICENSE-UNICODE
15
+ Keywords: ansi,cli,east-asian-width,emoji,grapheme,terminal,truncate,unicode,wcwidth,width
16
+ Classifier: Development Status :: 4 - Beta
17
+ Classifier: Environment :: Console
18
+ Classifier: Intended Audience :: Developers
19
+ Classifier: License :: OSI Approved :: MIT License
20
+ Classifier: Operating System :: OS Independent
21
+ Classifier: Programming Language :: Python :: 3
22
+ Classifier: Programming Language :: Python :: 3 :: Only
23
+ Classifier: Topic :: Software Development :: Libraries
24
+ Classifier: Topic :: Terminals
25
+ Classifier: Topic :: Text Processing
26
+ Classifier: Typing :: Typed
27
+ Requires-Python: >=3.10
28
+ Provides-Extra: dev
29
+ Requires-Dist: build; extra == 'dev'
30
+ Requires-Dist: mypy; extra == 'dev'
31
+ Requires-Dist: pytest; extra == 'dev'
32
+ Requires-Dist: ruff; extra == 'dev'
33
+ Requires-Dist: twine; extra == 'dev'
34
+ Requires-Dist: wcwidth==0.2.13; extra == 'dev'
35
+ Description-Content-Type: text/markdown
36
+
37
+ # cellstr
38
+
39
+ Display-width aware `len`, slicing, padding and truncation for terminal text: East Asian
40
+ wide characters, emoji sequences, combining marks, zero-width characters and ANSI escape
41
+ sequences, measured the way a terminal draws them. Typed, zero dependencies, Unicode 18.0.0.
42
+
43
+ ## Install
44
+
45
+ ```
46
+ pip install cellstr
47
+ ```
48
+
49
+ ## Usage
50
+
51
+ `len`, slicing and `str.ljust` count code points, so they misalign any text that carries
52
+ colour codes, CJK characters, emoji or accents. `cellstr` counts display cells.
53
+
54
+ ```python
55
+ from cellstr import cell_len, cell_slice, fit, ljust, truncate
56
+
57
+ warn = "\x1b[31mWARN\x1b[0m" # red "WARN"
58
+ cell_len(warn) # -> 4
59
+ cell_len("你好") # -> 4
60
+ cell_len("👨‍👩‍👧") # -> 2
61
+ cell_len("🇯🇵") # -> 2
62
+ cell_len("é") # -> 1
63
+
64
+ cell_slice(warn, 0, 2) # -> "\x1b[31mWA\x1b[0m"
65
+ cell_slice("a你b", 0, 2) # -> "a"
66
+ cell_slice("x👨‍👩‍👧y", 0, 2) # -> "x"
67
+
68
+ ljust("你", 4) # -> "你 "
69
+ truncate("hello world", 8) # -> "hello..."
70
+ truncate("/usr/local/lib/python", 12, position="middle") # -> "/usr/...thon"
71
+ fit("张伟张伟", 5, suffix="") # -> "张伟 "
72
+ ```
73
+
74
+ What it gets right:
75
+
76
+ - **Grapheme clusters.** A ZWJ family, a skin tone sequence, a flag, a keycap or a letter
77
+ with accents is one unit: measured once and never split by slicing or truncation.
78
+ - **Escape sequences.** SGR colours, OSC 8 hyperlinks, cursor movement and the other
79
+ ECMA-48 families count as nothing and are kept in place, so styled text stays styled.
80
+ - **Ambiguous width.** Greek, Cyrillic, box drawing and circled digits are one cell in
81
+ Western terminals and two in CJK ones. Pass `ambiguous=2` to any function.
82
+ - **Tabs.** Pass `tabsize=8` to measure tab stops; slicing and padding turn tabs into the
83
+ spaces they cover. `expand_tabs()` does it on its own.
84
+ - **Real Unicode data.** Tables are generated by a committed script from a committed
85
+ Unicode Character Database snapshot, and the full grapheme break test suite runs.
86
+
87
+ ```python
88
+ from cellstr import cell_len, iter_cells, truncate_detailed
89
+
90
+ cell_len("αβ─", ambiguous=2) # -> 6
91
+ cell_len("a\tb", tabsize=8) # -> 9
92
+ [c.width for c in iter_cells("a你🇯🇵")] # -> [1, 2, 2]
93
+ truncate_detailed("hello world", 8).truncated # -> True
94
+ ```
95
+
96
+ Public API: `cell_len`, `char_width`, `grapheme_width`, `iter_graphemes`, `iter_cells`
97
+ (`Cell`), `cell_slice`, `ljust`, `rjust`, `center`, `fit`, `truncate`,
98
+ `truncate_detailed` (`Truncation`), `expand_tabs`, `strip_ansi`, `UNICODE_VERSION`, and
99
+ the `Align` and `Position` literal types. Every function is fully typed and documented.
100
+
101
+ ## Command line
102
+
103
+ ```
104
+ $ printf 'abc\n你好\n' | cellstr measure
105
+ 3
106
+ 4
107
+ $ printf '/usr/local/lib/python3/site-packages\n' | cellstr truncate 16 --position middle
108
+ /usr/lo...ckages
109
+ $ printf 'id\n12345\n' | cellstr pad 8 --align right
110
+ id
111
+ 12345
112
+ ```
113
+
114
+ Commands: `measure` (`--max`), `truncate N`, `pad N`, `fit N` and `slice START [STOP]`,
115
+ all with `--ambiguous` and `--tabsize`. `cellstr --help` lists runnable examples.
116
+
117
+ ## Terminals disagree
118
+
119
+ There is no standard for how many cells a character takes, and terminals differ on
120
+ ambiguous-width characters, emoji ZWJ sequences and variation selectors. `cellstr`
121
+ follows Markus Kuhn's reference `wcwidth` for single code points and
122
+ measures emoji sequences as one glyph, as terminals with grapheme support draw them.
123
+ [GUIDE.md](GUIDE.md) has the exact rules, concrete examples of where terminals differ, a
124
+ recipe for code point by code point measuring, and every difference from the `wcwidth`
125
+ package.
126
+
127
+ ## Why
128
+
129
+ `wcwidth` measures plain-text width well, but it does not give you a typed, cohesive set
130
+ of string operations: slicing by column that keeps escapes and graphemes intact, padding,
131
+ fitting a table cell and width-aware truncation with a suffix at either end or in the
132
+ middle. `strip-ansi` style tools remove escapes but know nothing about width. `cellstr`
133
+ does all of it in one small, fully typed, zero-dependency package.
134
+
135
+ ## License
136
+
137
+ MIT. See [LICENSE](LICENSE). The property tables in `src/cellstr/_ucd_*.py` are generated
138
+ from the Unicode Character Database and are covered by the Unicode License v3, see [LICENSE-UNICODE](LICENSE-UNICODE).
139
+
140
+ ## Links
141
+
142
+ - Website: https://lianes8studio.com
143
+ - GitHub: https://github.com/JaviLianes8
144
+ - LinkedIn: https://www.linkedin.com/in/jlianes/
@@ -0,0 +1,108 @@
1
+ # cellstr
2
+
3
+ Display-width aware `len`, slicing, padding and truncation for terminal text: East Asian
4
+ wide characters, emoji sequences, combining marks, zero-width characters and ANSI escape
5
+ sequences, measured the way a terminal draws them. Typed, zero dependencies, Unicode 18.0.0.
6
+
7
+ ## Install
8
+
9
+ ```
10
+ pip install cellstr
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ `len`, slicing and `str.ljust` count code points, so they misalign any text that carries
16
+ colour codes, CJK characters, emoji or accents. `cellstr` counts display cells.
17
+
18
+ ```python
19
+ from cellstr import cell_len, cell_slice, fit, ljust, truncate
20
+
21
+ warn = "\x1b[31mWARN\x1b[0m" # red "WARN"
22
+ cell_len(warn) # -> 4
23
+ cell_len("你好") # -> 4
24
+ cell_len("👨‍👩‍👧") # -> 2
25
+ cell_len("🇯🇵") # -> 2
26
+ cell_len("é") # -> 1
27
+
28
+ cell_slice(warn, 0, 2) # -> "\x1b[31mWA\x1b[0m"
29
+ cell_slice("a你b", 0, 2) # -> "a"
30
+ cell_slice("x👨‍👩‍👧y", 0, 2) # -> "x"
31
+
32
+ ljust("你", 4) # -> "你 "
33
+ truncate("hello world", 8) # -> "hello..."
34
+ truncate("/usr/local/lib/python", 12, position="middle") # -> "/usr/...thon"
35
+ fit("张伟张伟", 5, suffix="") # -> "张伟 "
36
+ ```
37
+
38
+ What it gets right:
39
+
40
+ - **Grapheme clusters.** A ZWJ family, a skin tone sequence, a flag, a keycap or a letter
41
+ with accents is one unit: measured once and never split by slicing or truncation.
42
+ - **Escape sequences.** SGR colours, OSC 8 hyperlinks, cursor movement and the other
43
+ ECMA-48 families count as nothing and are kept in place, so styled text stays styled.
44
+ - **Ambiguous width.** Greek, Cyrillic, box drawing and circled digits are one cell in
45
+ Western terminals and two in CJK ones. Pass `ambiguous=2` to any function.
46
+ - **Tabs.** Pass `tabsize=8` to measure tab stops; slicing and padding turn tabs into the
47
+ spaces they cover. `expand_tabs()` does it on its own.
48
+ - **Real Unicode data.** Tables are generated by a committed script from a committed
49
+ Unicode Character Database snapshot, and the full grapheme break test suite runs.
50
+
51
+ ```python
52
+ from cellstr import cell_len, iter_cells, truncate_detailed
53
+
54
+ cell_len("αβ─", ambiguous=2) # -> 6
55
+ cell_len("a\tb", tabsize=8) # -> 9
56
+ [c.width for c in iter_cells("a你🇯🇵")] # -> [1, 2, 2]
57
+ truncate_detailed("hello world", 8).truncated # -> True
58
+ ```
59
+
60
+ Public API: `cell_len`, `char_width`, `grapheme_width`, `iter_graphemes`, `iter_cells`
61
+ (`Cell`), `cell_slice`, `ljust`, `rjust`, `center`, `fit`, `truncate`,
62
+ `truncate_detailed` (`Truncation`), `expand_tabs`, `strip_ansi`, `UNICODE_VERSION`, and
63
+ the `Align` and `Position` literal types. Every function is fully typed and documented.
64
+
65
+ ## Command line
66
+
67
+ ```
68
+ $ printf 'abc\n你好\n' | cellstr measure
69
+ 3
70
+ 4
71
+ $ printf '/usr/local/lib/python3/site-packages\n' | cellstr truncate 16 --position middle
72
+ /usr/lo...ckages
73
+ $ printf 'id\n12345\n' | cellstr pad 8 --align right
74
+ id
75
+ 12345
76
+ ```
77
+
78
+ Commands: `measure` (`--max`), `truncate N`, `pad N`, `fit N` and `slice START [STOP]`,
79
+ all with `--ambiguous` and `--tabsize`. `cellstr --help` lists runnable examples.
80
+
81
+ ## Terminals disagree
82
+
83
+ There is no standard for how many cells a character takes, and terminals differ on
84
+ ambiguous-width characters, emoji ZWJ sequences and variation selectors. `cellstr`
85
+ follows Markus Kuhn's reference `wcwidth` for single code points and
86
+ measures emoji sequences as one glyph, as terminals with grapheme support draw them.
87
+ [GUIDE.md](GUIDE.md) has the exact rules, concrete examples of where terminals differ, a
88
+ recipe for code point by code point measuring, and every difference from the `wcwidth`
89
+ package.
90
+
91
+ ## Why
92
+
93
+ `wcwidth` measures plain-text width well, but it does not give you a typed, cohesive set
94
+ of string operations: slicing by column that keeps escapes and graphemes intact, padding,
95
+ fitting a table cell and width-aware truncation with a suffix at either end or in the
96
+ middle. `strip-ansi` style tools remove escapes but know nothing about width. `cellstr`
97
+ does all of it in one small, fully typed, zero-dependency package.
98
+
99
+ ## License
100
+
101
+ MIT. See [LICENSE](LICENSE). The property tables in `src/cellstr/_ucd_*.py` are generated
102
+ from the Unicode Character Database and are covered by the Unicode License v3, see [LICENSE-UNICODE](LICENSE-UNICODE).
103
+
104
+ ## Links
105
+
106
+ - Website: https://lianes8studio.com
107
+ - GitHub: https://github.com/JaviLianes8
108
+ - LinkedIn: https://www.linkedin.com/in/jlianes/
@@ -0,0 +1,65 @@
1
+ {
2
+ "DerivedCoreProperties.txt": {
3
+ "title": "Unicode Character Database 18.0.0, DerivedCoreProperties.txt",
4
+ "url": "https://www.unicode.org/Public/18.0.0/ucd/DerivedCoreProperties.txt",
5
+ "revision": "Unicode 18.0.0",
6
+ "fetched": "2026-10-13",
7
+ "size": 1159889,
8
+ "sha256": "09c928886a178fcafd93c29e4bd59073a058e5a100b716d425cb563ab50f68c9",
9
+ "licence": "Unicode License v3 (https://www.unicode.org/license.txt), see LICENSE-UNICODE"
10
+ },
11
+ "DerivedGeneralCategory.txt": {
12
+ "title": "Unicode Character Database 18.0.0, DerivedGeneralCategory.txt",
13
+ "url": "https://www.unicode.org/Public/18.0.0/ucd/extracted/DerivedGeneralCategory.txt",
14
+ "revision": "Unicode 18.0.0",
15
+ "fetched": "2026-10-13",
16
+ "size": 282143,
17
+ "sha256": "d6b151d2d40ee9b1876d26f417980f45ffae47b6055ccf7203cb31f07a030f94",
18
+ "licence": "Unicode License v3 (https://www.unicode.org/license.txt), see LICENSE-UNICODE"
19
+ },
20
+ "EastAsianWidth.txt": {
21
+ "title": "Unicode Character Database 18.0.0, EastAsianWidth.txt",
22
+ "url": "https://www.unicode.org/Public/18.0.0/ucd/EastAsianWidth.txt",
23
+ "revision": "Unicode 18.0.0",
24
+ "fetched": "2026-10-13",
25
+ "size": 204705,
26
+ "sha256": "a0cf29eacd00cfcaec4381c6b7c281685f18dbb4e7ff82b4076ccb342ca839aa",
27
+ "licence": "Unicode License v3 (https://www.unicode.org/license.txt), see LICENSE-UNICODE"
28
+ },
29
+ "GraphemeBreakProperty.txt": {
30
+ "title": "Unicode Character Database 18.0.0, GraphemeBreakProperty.txt",
31
+ "url": "https://www.unicode.org/Public/18.0.0/ucd/auxiliary/GraphemeBreakProperty.txt",
32
+ "revision": "Unicode 18.0.0",
33
+ "fetched": "2026-10-13",
34
+ "size": 100009,
35
+ "sha256": "0839dcb79e4ac639ecd538b1abf7c9d22e3f9dd265b7e182d33627aa4d75b45a",
36
+ "licence": "Unicode License v3 (https://www.unicode.org/license.txt), see LICENSE-UNICODE"
37
+ },
38
+ "GraphemeBreakTest.txt": {
39
+ "title": "Unicode Character Database 18.0.0, GraphemeBreakTest.txt",
40
+ "url": "https://www.unicode.org/Public/18.0.0/ucd/auxiliary/GraphemeBreakTest.txt",
41
+ "revision": "Unicode 18.0.0",
42
+ "fetched": "2026-10-13",
43
+ "size": 137090,
44
+ "sha256": "b0cf047ee94485bbdc846de2b902f5f8a815f6b674f9d04223cddadd91c9df31",
45
+ "licence": "Unicode License v3 (https://www.unicode.org/license.txt), see LICENSE-UNICODE"
46
+ },
47
+ "emoji-data.txt": {
48
+ "title": "Unicode Character Database 18.0.0, emoji-data.txt",
49
+ "url": "https://www.unicode.org/Public/18.0.0/ucd/emoji/emoji-data.txt",
50
+ "revision": "Unicode 18.0.0",
51
+ "fetched": "2026-10-13",
52
+ "size": 108719,
53
+ "sha256": "80d00f8e616a0ef27fd6b8de3b758c06383b5d917e2977709578e68baf733bf1",
54
+ "licence": "Unicode License v3 (https://www.unicode.org/license.txt), see LICENSE-UNICODE"
55
+ },
56
+ "EastAsianWidth-15.1.0.txt": {
57
+ "title": "Unicode Character Database 15.1.0, EastAsianWidth.txt",
58
+ "url": "https://www.unicode.org/Public/15.1.0/ucd/EastAsianWidth.txt",
59
+ "revision": "Unicode 15.1.0",
60
+ "fetched": "2026-10-13",
61
+ "size": 194188,
62
+ "sha256": "b08191401dc125f4e84ef262a95754faae6b737c79538e17ea9664a63434e94e",
63
+ "licence": "Unicode License v3 (https://www.unicode.org/license.txt), see LICENSE-UNICODE"
64
+ }
65
+ }