1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
//! Grapheme-cluster segmentation for the streaming parser (#295, DECSET mode 2027).
//!
//! justerm-core processes input one `char` at a time (`Term::print`). Under mode 2027 the parser
//! must decide, per incoming scalar, whether it **extends** the previous cell's grapheme cluster
//! (ride the side-table, no new cell) or **breaks** (start a new cell). This is the incremental
//! form of UAX #29 extended grapheme-cluster segmentation.
//!
//! The break decision is delegated to `unicode-segmentation` (the full UAX #29 rule set:
//! GB9 Extend/ZWJ, GB9a SpacingMark, GB9b Prepend, GB11 emoji-ZWJ, GB12/GB13 regional-indicator
//! pairing) rather than hand-rolled — the rules need large Unicode property tables that would rot.
//!
//! No break-state is persisted across `print` calls (cursor moves / CR-LF would corrupt it, cf.
//! ghostty `Terminal.print`). Instead the caller reconstructs the previous cluster's text from the
//! cell (`base scalar + side-table Vec<char>`) and asks [`grapheme_extends`] fresh each time.
//!
//! ## A variation selector on a non-emoji base is KEPT (#317 §1, decided 2026-08-18)
//!
//! `x` + VS16 is not an emoji sequence — the selector changes nothing about how `x` is drawn or how
//! wide it is. justerm still joins it into the side-table, because UAX #29 puts it there: VS16/VS15
//! are `Extend`, so `grapheme_extends` says yes and the scalar rides along. The only place it is
//! observable is **text extraction** — a copy of that cell yields the extra scalar.
//!
//! ghostty drops it: *"the terminal does not store those selectors in the cell, so callers must also
//! restore their grapheme break state and leave prev unchanged"* (`src/unicode/grapheme.zig:56` @
//! `e6e26e16`). Recorded here because the divergence is **narrower than it looks, and #317's body
//! described it wrongly** as a disagreement about UAX #29. It is not one: ghostty's own
//! `graphemeWidth('x', 0xFE0F)` returns `len = 2` (`:315`), so both implementations agree the
//! selector is *in the cluster*. They differ one layer down, on whether the cell **stores** what the
//! cluster contains — and ghostty's own comment states the cost of its answer, which is that every
//! caller now has to repair a break state the storage layer discarded.
//!
//! justerm keeps it, on the tie-breaker for this layer: VT semantics answer to **the spec**, above
//! any implementation including ours (ADR-0004). Widths are identical either way, so nothing on
//! screen distinguishes them; what a cell hands back is the cluster the spec says it is.
use UnicodeSegmentation;
/// Whether appending `c` to `prev_cluster` keeps it a **single** grapheme cluster — i.e. `c`
/// extends the cluster rather than starting a new one. `prev_cluster` is the full text of the
/// preceding cell's cluster (its base scalar plus any already-joined scalars); it is one grapheme
/// by construction. Returns `false` for an empty `prev_cluster` (nothing to extend).
///
/// Uses extended grapheme-cluster rules (UAX #29). Appending a scalar that does not add a new
/// grapheme boundary (a combining mark, ZWJ, an emoji joined via ZWJ, a skin-tone modifier, a
/// VS16/VS15 selector, the second of a regional-indicator pair) extends; anything that starts a
/// fresh cluster (a new base letter, a CJK ideograph, the *third* regional indicator by GB12/13
/// parity) breaks.
pub