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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
//! Consumer event surface (#12): point-in-time notifications the engine
//! accumulates while parsing, for the consumer to drain.
//!
//! Pull, not push — the engine queues events during `feed` and the consumer
//! takes them with `drain_events`, mirroring the rest of the pull cadence
//! (`damage` / `frame` / `reset_damage`). No callback is injected across the
//! boundary, so the engine stays decoupled from the consumer's event loop
//! (unlike alacritty's `EventListener`, whose push model would couple them).
//!
//! OSC 8 hyperlinks are deliberately absent — a hyperlink is per-cell state
//! (which cells are links), not a point-in-time event, so it is modelled like
//! graphemes in its own slice (#26), not here.
use crate;
/// Which selection an `OSC 52` clipboard request names (#828).
///
/// A *value*, never the protocol byte, so a consumer never parses the sequence —
/// the same reason [`TermEvent::SetPaletteColor`] carries a `u8` index rather
/// than the field it was written in.
///
/// **Three members, and `p` is kept apart from `s` deliberately.** The
/// sequence's target field admits `c`, `p`, `q`, `s` and the eight cut buffers
/// (`ctlseqs.txt:2156`); justerm models the three a consumer can act on and
/// ignores the rest rather than folding them onto a neighbour, since mapping `q`
/// onto a selection would be the engine inventing an equivalence the application
/// did not ask for. ghostty folds every unrecognised kind onto the clipboard
/// (`src/termio/stream_handler.zig:1013`); alacritty ignores them
/// (`alacritty_terminal/src/term/mod.rs:1714`), and so does this. Read
/// ghostty's from the `switch` and not from the comment four lines above it,
/// which says *"we ignore the 'kind' field and always use the standard
/// clipboard"* and is contradicted by the code under it — only the `else` arm
/// goes to `.standard`.
///
/// **The first draft collapsed `p` and `s` into one member, and that was wrong
/// on the wire.** alacritty collapses them
/// (`alacritty_terminal/src/term/mod.rs:1713`) but replies with the byte the
/// application sent (`:1744`), so the collapse never reaches a client. This
/// engine hands the consumer a value and gets it back at `report_clipboard`, so
/// a collapse here would answer `ESC ] 52 ; s ; ?` naming `p` — a selector the
/// application never wrote, in the one field a client could pair a reply on. The
/// spec lists the two separately (`ctlseqs.txt:2157`), xterm binds them to
/// different atoms (`misc.c:3327`) and echoes the recognised list back
/// (`misc.c:3384`), and ghostty keeps three locations apart in both directions
/// (`src/Surface.zig:5954`, `src/terminal/c/terminal.zig:2942`). Splitting the
/// member is what lets the value round-trip without the engine remembering
/// anything.
///
/// **`#[non_exhaustive]` (#843).** The set is open by the paragraph above: `q` and
/// the eight cut buffers are in the sequence and unmodelled here, so a later slice
/// may name one. A consumer meeting a member it does not know can decline the
/// request, which is already how it refuses any of them.
///
/// **ghostty reaches the same shape independently**, which #843 had recorded as
/// impossible — its issue says Zig "has no such construct", and Zig does: a
/// trailing `_` marks a non-exhaustive enum, used at 23 sites in that tree. The
/// one that matters here is `src/terminal/clipboard.zig:2`, whose `Location` is
/// `{ standard, selection, primary, _ }` — the same three members this type
/// carries, and open for the same reason. Convergence on both the partition and
/// the openness is the non-arbitrariness signal; it was invisible while the
/// corpus was recorded as having no vote.
/// A consumer-facing event emitted while parsing the VT stream.
///
/// **`#[non_exhaustive]`, so a consumer must carry a `_` arm and a new variant
/// never breaks one.** Decided 2026-09-02, by the maintainer, while #828 was
/// adding two — and what decided it was neither this slice nor any consumer we
/// can see.
///
/// **What decided it is `CLAUDE.md`'s own identity statement**: *"`justerm-core`
/// is not penterm-only — it is a reusable, independent crate."* That sentence
/// says there are consumers we cannot edit, which is precisely what this
/// attribute defends; a crate whose identity were "internal, used by penterm"
/// would want the opposite, because there a broken build is the compiler doing
/// us a favour. So this follows from a call already made rather than from a
/// preference, and it reverses only if that identity does.
///
/// Three measurements, so the next reader does not have to retake them:
///
/// - **crates.io reverse dependencies: zero** (the single row the API returns is
/// this crate itself), across 248 downloads split over 11 versions — i.e. no
/// external consumer exists *today*. That is why the identity statement had to
/// decide it: there was nothing to observe.
/// - **Cost in this workspace: zero.** `cargo test --workspace` (87 suites) and
/// `clippy -D warnings` both stay green. A same-crate `match` may still be
/// exhaustive, `justerm-wasm-decode` and `justerm-renderer` never name
/// `TermEvent`, and `justerm-web`'s `events.ts` is a deliberately narrower
/// union (title / bell / cwd).
/// - **The window closes at `1.0.0`.** Adding this is free while the crate is
/// `0.x` and is *itself* a breaking change afterwards, while an enum without
/// it turns every future variant into a major bump. Conformance here is
/// cumulative by design (#47 is a perpetual tail) and the two slices before
/// this one added three variants and two, so that rate is measured rather than
/// assumed.
///
/// **The argument that lost, recorded because it is a real cost.** An exhaustive
/// match is a *feature* for a consumer: the compiler tells them a new event
/// exists and makes them decide about it. penterm's `route_event` is the worked
/// example — its `ColumnMode` and `ColorSchemeQuery` arms carry a comment
/// explaining why each is dropped, written by someone the compiler had just
/// informed. That signal is given up here, and it now has to come from release
/// notes. It loses because this is a *notification* channel where ignoring an
/// unknown event is documented as safe, so the guarantee belongs in prose rather
/// than in the type — but a consumer who wanted the old behaviour is not
/// imagining the loss.
///
/// (penterm was the evidence that an outside exhaustive matcher can exist — its
/// five-variant `match` predates nine minor versions of this enum — and not the
/// reason. It is being reimplemented, which is exactly why it could not be.)