plurimus 0.7.0

A Bevy-native terminal renderer: cameras, widgets, and 2d/3d pipelines drawn to terminal cells.
Documentation
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
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
# Plurimus Architecture

Plurimus is a Bevy-native terminal renderer: a workspace of crates that render
Bevy worlds to terminal cells. The cell model is ratatui's - `Buffer`, `Cell`,
`Rect`, `Style` from ratatui-core - and the rendering model is Bevy's: any
number of systems produce drawable data throughout the frame, and exactly one
presenter writes to the terminal.

Every frame flows through the same pipeline. Terminal-relevant data is extracted
from the main world into a dedicated terminal render sub-app; pipelines
rasterize that data into per-camera buffers (world-space pipelines first, the ui
pass on top); the compositor merges camera buffers into a single frame buffer in
camera order and downsamples it to the terminal's color depth; and the presenter
diffs the composed frame against the previous one, writing only the changed
cells through a ratatui `Backend`. Multiple `TerminalCamera`s with cell-space
viewports split the terminal the way multiple cameras split a window - a map
view, a sidebar, and a minimap are three cameras with three viewports.

Consumers adopt the workspace in tiers. Core alone renders to any `Backend`;
adding input and crossterm gives a live terminal; the ui, widgets, and bevy-ui
tiers add interaction and controls; the 2d and 3d pipelines draw world-space
entities. Each tier is a feature on the facade crate and a crate of its own.

```mermaid
flowchart TB
    world["main world<br/>cameras, widgets, nodes, 2d/3d entities"]

    subgraph subapp["SubApp"]
        rasterize["Rasterize<br/>world pass / ui pass"]
        composite["Composite<br/>merge / post-process / downsample"]
        present["Present<br/>diff against the previous frame"]
    end

    backend["ratatui Backend"]
    term["terminal"]

    world --> subapp
    rasterize -->|CameraBuffers| composite
    composite -->|FrameBuffer| present
    present -->|changed cells| backend
    backend --> term
```

## Crate Organization

### plurimus

The facade crate. Feature-gated re-exports of the member crates and nothing
else: `plurimus_core` is unconditional, and each feature enables one member
crate and its module (`crossterm` implies `term`; `widgets` and `bevy-ui` imply
`ui`). The default feature set is `crossterm` - core, term, and a live terminal.
The facade crate also hosts the runnable examples.

### plurimus_core

The render pipeline, and nothing else. Everything here means something against
any ratatui `Backend` - a test harness, a GPU surface, a file - so nothing
needing a real terminal lives here and no crate that does is depended on; the
`headless` example holds that line by building with the terminal tiers off.
`CorePlugin` installs the terminal render sub-app: an extract schedule copies
data out of the main world, then the `TerminalRender` schedule runs its three
phases - `Rasterize` (pipelines write cells into `CameraBuffer`s, world-space
passes beneath the ui pass), `Composite` (camera buffers merge into the
`FrameBuffer` in camera order, apps post-process the composed frame in
`CompositeSystems::PostProcess` while colors are still what the widgets chose,
then colors downsample to the terminal's `ColorDepth`), and `Present`. Core owns
`TerminalCamera` and viewport resolution against `TerminalSize`, the subcell
raster primitives in `raster` (halfblock and braille grids, blits, color
averaging, `ColorDepth` downsampling), and the widget primitive: a `UiWidget`
placed by `UiArea`/`UiCamera`/`UiOrder` is extracted and drawn in one z-sorted
pass with no other crate involved. Its `Default` draws nothing, which is what a
widget holds before its first restyle replaces it, so no widget library needs a
blank of its own. Which camera a widget draws on, the crates agree on rather
than each deriving: `ComputedUiCamera` is resolved once a frame in
`CameraSystems::PropagateCameras` as the widget's own `UiCamera`, else the
nearest ancestor's, else the default - so a child sits on its parent's camera
without being told, and forgetting to say stops being a silent misplacement.
`UiArea` requires it, local to what being half of where; `CameraViewports`
carries a camera to its viewport with the same default fallback, and
`local_area` is `resolve_area`'s inverse, for a crate holding a screen rect that
has to be stored camera-locally. `PresenterPlugin<B>` diffs the composed frame
and writes changed cells through any ratatui-core `Backend`, and applies
`TerminalCursor` - the terminal's own caret, which a screen reader follows and
an input method anchors to - outside that diff, because a caret crossing a cell
changes no cell's content and the diff skips a frame where nothing differs.
Position and visibility go through `Backend`; the shape is a backend's to serve.
Re-exports `ratatui_core`.

### plurimus_term

The terminal contract, both directions: everything that needs a real terminal to
mean anything. Inbound it defines the message types a backend emits into the
main world - `KeyMessage`, `MouseMessage` (cell coordinates), `PasteMessage`,
`FocusMessage`, `TerminalResized` - and the state derived from them: polled
`ButtonInput<KeyCode>` and `ButtonInput<MouseButton>`, plus `CursorCell`. The
two views of the keyboard are keyed differently and deliberately: a message
reports the character the terminal produced, while what is held is keyed on
`KeyCode::held_as`, the lowercase fold a character key is held under. A terminal
reports the character rather than the key, so shifting a hold ends it with a `W`
that has to cancel a `w` or the press is never released; `held_as` is public
because an app keeping its own held set needs the same rule, and only a case
difference survives that round trip - a shifted symbol carries no trace of the
key it came from. Outbound it defines `TerminalRequest`, the one-shot side
effects an app asks of whichever backend is installed - copy to a clipboard
selection, set the window title - and `TerminalCursorStyle`, the shape it asks
the caret to take, which no ratatui `Backend` method can express. That stream is
one-way, so `LastCopied` is the state derived from it: a stock system in
`RequestSystems::Echo` records the last copy bound for the ordinary clipboard,
giving a paste key one answer across every widget that asks. It echoes what was
requested rather than what the terminal holds, and runs in `Last` because a
backend consumes requests by draining them during extraction, after every
main-world schedule. `TermPlugin` requires `CorePlugin` first, since applying a
resize writes core's `TerminalSize`. `InputCapabilities` records what the active
backend can report (real key releases, modifier key events - the kitty keyboard
protocol tier); where a capability is absent, release synthesis fills the gap on
a `ReleaseTimeout`. Losing focus is the gap no capability covers, since a
terminal reports nothing at all while unfocused: every held key is released when
a `FocusMessage` says focus went away, keys only, because a synthetic key
release corrects held state while a pointer release would complete a click
nobody made. Both paths drain one held-key registry, which is why recording into
it is a system of its own rather than the first half of the timeout's - what is
held has to be known on every tier, and only the expiry is a capability's to
turn off. Both run before polled state is derived rather than reading it back,
so a hold the terminal stopped reporting clears in the frame it ended. A release
either writes carries no modifiers and names the key as held, which is what a
terminal does too - an event reports the state it leaves behind, and a shifted
character is exactly what nothing being held can no longer produce. A click
count is the other fact no terminal reports, and the one no capability governs,
since no tier reports it - but only the run itself lives elsewhere, in the crate
whose router knows what a press reached. What stays here is `MultiClickWindow`,
how soon after a press another has to land to run with it, sitting beside
`ReleaseTimeout` as the second knob an app sets once for every widget and read
against the same `Time<Real>`. The `bevy_compat` module forwards messages into
`bevy_input` event types for crates built on them, such as the focus stack, and
`HeldModifiers` is the way back for what that seam drops: bevy's `KeyboardInput`
carries no modifiers, so a key observer polls the ones held through it.

### plurimus_crossterm

The real terminal. `CrosstermPlugin` takes over the terminal on build - raw
mode, alternate screen, mouse capture, bracketed paste, the kitty keyboard
protocol when the terminal supports it, focus reporting - and restores all of it
on exit or panic, the cursor shape included. It detects color support from the
environment, pumps crossterm events into input messages and `TerminalResized`,
and hands a `CrosstermBackend` (via ratatui-crossterm) to core's presenter. The
pump is where a terminal's own encoding is normalized away, because a message
cannot be un-written once emitted and only the writer still has the whole
drained batch: a held key reported as a release followed by its own press
becomes one `KeyKind::Repeat`, and a shifted letter keeps the shift bit the
kitty protocol drops in favour of the shifted character. Which encoding a
terminal uses is learned from what it sends rather than probed, since no
crossterm query distinguishes them. Going the other way it serves
`TerminalRequest` during extraction - which runs inside the sub-app world with
the main world lent in, so one system reaches both the messages and the writer -
and sets the cursor shape, which no `Backend` method reaches. Both flush
themselves, since the presenter skips its flush on a frame where no cell
differs. The writer is generic: stdout by default, or the controlling terminal
directly via `CrosstermPlugin::tty()`.

### plurimus_ui

Interaction over anything with an area. `UiPlugin` computes
`ComputedWidgetArea`s, resolves hover from the cursor with z-order hit testing,
and routes pointer press/drag/release, clicks, and wheel input in three ordered
phases (`Areas`, `Hover`, `Route`). What a press may do is a three-marker
vocabulary: an `InteractionDisabled` widget is inert rather than invisible - it
wins arbitration and absorbs the press whole, and one disabled mid-gesture
releases without clicking - while wheel ticks still fall through it, consuming
being that router's own opt-in; `PressPassThrough` is press transparency, the
widget keeping its area for everything but the press; and `PressFocusDisabled`
suppresses only the focus a press would move, so a toolbar control is
tab-reachable without a click on it taking the keyboard - focus otherwise
follows a press to any `TabIndex` carrier. Every press also carries how many
have run together on it, `PointerPress`, the `Pressed` it leaves on the widget,
and the `Click` completing it all reporting the same number. The run behind it
is this crate's own and private, because every fact keying it is this router's:
which widget the press reached, and whether it reached one at all - a press that
dismissed an overlay or that a disabled widget absorbed ends the run rather than
counting, so the next press starts over rather than arriving as a second. Only
`plurimus_term`'s `MultiClickWindow` bounds it from outside. The count rests on
`Pressed` for the length of the gesture because a gesture outlives the message
that started it: a same-batch release reads it from the router's own list, since
a component inserted by command has not landed yet, and a `PointerDrag` observer
reads it off the entity, which is the one gesture no event carries a count for.
It installs focus via `bevy_input_focus` and pins the dispatch into that
sequence - after `bevy_input`'s own update and between `Areas` and `Hover` - so
a focused-input observer reads this frame's areas and settled key state rather
than whatever the schedule happened to resolve; work that must see what one did
is ordered after the dispatch, which is what `plurimus_widgets` does with
`WidgetSystems::Layout`. It also builds the directional navigation map, and
provides scrolling (`ScrollArea`, `ScrollOffset`, `ScrollIntoView`) with cached
extraction of scrolled content, plus the generic modal-overlay primitives
(`ModalOpen`, `ModalDismiss`) that menus and popovers are built from. What
"inside a modal" means is the overlay's own rect, for the pointer and the wheel
alike: a position an open overlay covers admits that overlay's subtree and
nothing else, so an overlay confines input rather than depending on every child
of it being marked, and a position outside every open overlay dismisses them -
except on a `ModalityToggle`, the marker that survives where geometry cannot
answer, since an opener sits outside the menu it closes. Taking the union of the
overlays covering a position is what admits a submenu inside its parent without
ordering modal roots against each other. Every scroll converges on one event: a
wheel tick, arbitrated by z-order among the `WheelReceptive` widgets under the
cursor whose `WheelAxes` can still use that axis and which an open overlay
admits, and a key bound through `ScrollKeys` on whichever widget holds focus
both become a `ScrollBy`, which whoever stores the scroll consumes - this
crate's `ScrollOffset`, a bevy_ui node's own position, a text editor's engine
viewport - each clamping the step against its own extent. `ScrollKeys` is the
whole opt-in for the keyboard, carrying the `TabIndex` without which nothing can
be sent a key, and it is one of the `(KeyBinding, Action)` bindings components
sharing `first_bound`, the scan this crate owns so a widget family written
elsewhere states "first match wins" by calling it rather than by copying it -
six of them across the workspace, one per widget that takes keys. A `KeyBinding`
is a `Key` and the `KeyModifiers` it must be pressed under, and
`KeyBinding::matches` is the one rule: every modifier but shift exactly, and
shift exactly for a named key but only when asked for on a character, since a
shifted symbol carries the bit on some terminals and not others. The modifiers
it is checked against are polled through `plurimus_term`'s `HeldModifiers`,
bevy's `KeyboardInput` carrying none of its own. `content_cell` is where a
pointer cell becomes a content cell for any of it, clamping into the area so a
captured drag past an edge keeps addressing the nearest one; `screen_cell` is
the way back, refusing rather than clamping, and it is what places the focused
widget's `WidgetCursor` on the terminal - a cursor whose cell is `None` names
none, which is how a widget with nowhere to put its caret says so without
discarding the shape an app gave it. Both take that offset as a bare `Position`,
and `ScrollOffset::resolve` is where a caller holding the component gets one: a
widget carrying no `ScrollOffset` is scrolled to the origin, which is the
crate's rule to state rather than every caller's to know.

It also owns the styling contract entire, so a widget library reaches it without
depending on another widget library. `UiPlugin` initializes the `UiTheme`
resource, `UiTheme::resolve` turns an `InteractionState` into the one `Style`
its documented precedence gives - disabled over pressed over hovered over
normal, focused patched over the winner - and `UiStyle` and `StylistDisabled`
are the two escapes from it. `UiTheme::caret` is the one term `resolve` does not
answer, a caret a widget draws into its own cells being a thing inside the
widget rather than a state the widget is in; whoever draws one patches it over
the character it covers. Beside that vocabulary sits the engine that consumes
it: `StylistCache` records what a widget last drew and `StylistCache::redraws`
is the compare-and-swap every stylist gates on, so a theme swap, an edited
label, or a dirtied container repaints and an idle frame costs a comparison.
Handing an entity back from `StylistDisabled` repaints it too, by a removal hook
that resets its cache - the entity sat outside every stylist query and so missed
whatever landed meanwhile. `observed` reads an entity's state through
`StateQuery` and `StylistCache::with_value` carries the hash a widget's own
value contributes, for a stylist that resolves its state rather than reading it;
`restyle` runs the whole loop for the label-driven case, and a `UiLabel` is a
ratatui `Line`, so a label carries per-span style of its own. Re-exports
`tui_scrollview`, and `bevy_input`'s `Key`, the type its bindings are written
in.

### plurimus_widgets

The widget library, mirroring bevy_ui_widgets where upstream has a counterpart:
its component vocabulary and event contract over terminal-native engines.
Buttons, checkboxes, radio groups, sliders, scrollbars, list boxes, panes,
menus, popovers, a single-line `EditableText`, and a multi-line `TextEditor`
built on ratatui-textarea; `Table` is past the parity list, upstream having no
table to mirror. The editor is the one widget that talks to the clipboard, being
the one with a selection to copy: ctrl+c and ctrl+x offer the text to the
terminal as well as to the engine, and ctrl+v inserts `plurimus_term`'s
`LastCopied`, read at the press so the engine's own kill ring stays whatever
ctrl+k last put there. The single-line field is the one whose engine is
published instead: `TextInput` owns its value and a cursor resting on
grapheme-cluster boundaries, and `TextInput::handle` and `TextInput::paste`
apply a key or pasted text to it, so a host routing its own keys drives a field
it never focuses rather than rewriting the cluster stepping that is the hard
part. What the stock observers add around those two calls is dispatch and
policy, which is why the core leaves Enter untaken: a press emits the final
`ValueChange` and a `Submit` carrying the value, a repeat emits neither since
one intent commits once, and focus loss still emits that final `ValueChange`
alone - which is the whole of what tells committing an entry from abandoning
one. Its caret is drawn only while it holds focus, so a screenful of fields
shows the one the keys reach. Most widgets are stateless controllers emitting
entity events (`Activate`, `ValueChange`); apps apply them, or attach the stock
`*_self_update` observers for uncontrolled behavior. Which keys activate one is
the app's: `Button`, `Checkbox` and `RadioButton` require `ActivateKeys`,
defaulting to Enter and space, and a key the widget is not bound to activates
nothing and propagates - so binding space alone is what lets the form around a
checkbox keep Enter for its submit, and an empty list turns the keyboard path
off while leaving the click. Consuming the key is what activating does, which is
why a disabled widget passes its bound keys on too. It holds bare `KeyBinding`s
rather than the `(KeyBinding, Action)` pairs a list box or table binds,
activation having one action to name; a repeat never activates, one intent
committing once. A menu binds its own Enter and space through `MenuKeys`, which
sits on the popup rather than on every row of it: one table per menu, agreeing
with `ActivateKeys`'s default and independent of it deliberately, since a menu's
Enter is not a form's.

A `Popover` is placed against its anchor's resolved area every frame, so it
follows a moving anchor without being told, and two fields say what that means:
`cell` narrows the anchor to one cell of its content - mapped through the
anchor's own `ScrollOffset` by `plurimus_ui`'s `screen_cell`, so an editor names
its caret once, in the component it already publishes it in - and `camera` names
the camera the popover draws on and is bounded by when that should not be its
anchor's. They compose because the anchor's rect resolves in screen space before
any camera is consulted, and each is one input to the same placement rather than
a path of its own: the side, the mirror when it will not fit, the alignment and
the clamp are the code that was already there, applied to a one-cell rect or
against a different viewport. Both say "nowhere" the same way, with
`Rect::ZERO`, for a cell scrolled out of its anchor's window, an anchor drawing
nothing, and a camera with no viewport this frame alike, because attaching to
something invisible is what a popover has no answer for. `camera` is a field
rather than a user-set `UiCamera` because placement writes a real `UiCamera`
itself, to reach the popover's children, and so could not tell an app's from its
own; the anchor still gates adoption, so a popover with no anchor to be placed
against takes no camera either. Every side attaches to an outer edge, which is
why `PopoverSide` has four variants and not five: a box drawn _inside_ its
anchor has no side to mirror and no edge to align to, and is a child holding a
`UiArea::Fixed` placed through `local_area`.

A stylist rebuilds a widget's `UiWidget` from `plurimus_ui`'s `UiTheme` when the
state it last drew differs from the current one, or when its label changed, not
every frame, and they run in the `WidgetSystems::Style` set an app orders its
own against. Only the stylists themselves are the crate's: the cache they gate
on, the state they read, the label they draw, and the theme vocabulary the app
speaks all belong to `plurimus_ui`, which is what lets a widget family outside
this workspace be written against the same engine. `StylistDisabled` exempts an
entity so an app takes its look while keeping its behavior, and `UiStyle`
patches over the style an entity would otherwise resolve to, on a widget or on
one list or table row.

The two widgets drawn from row children - the list box and the table - share
everything a container cannot work out for itself, which is why `rows` holds it
and depends on neither: the cursor (`ActiveDescendant`), the row decorations
(`ListItemText`, `ListItemTrailing`, `Marked`), and four generic passes in
`WidgetSystems::Layout`. Their rows are child entities, and a child's change
never marks its parent, so one pass forwards a row's edit, restyle, check, mark,
or uncheck to the container before any stylist runs, and a second sums its rows'
heights into the scroll extent, reading that same signal so a row's edit resizes
the content in the frame it happens. A third keeps the cursor pointing at a live
row - filtering a list is despawning its rows, and a cursor naming a dead one
highlights nothing and moves from nowhere, so it re-points to the first
survivor, or to none when none survives, leaving a deliberately empty cursor
alone. A fourth scrolls whichever row the cursor names into view, which belongs
to the cursor rather than to the key that moved it: a click, a repair, and an
app driving the list from a search field beside it all reveal, where once only
the container's own key handler did. A stylist reads it too, rather than hashing
every row to find out, which is what keeps a settled list of any length free on
an idle frame. A row is one terminal row tall unless it carries `ListItemText`,
which only a list box draws: that row is as tall as its text has lines, and the
extent, the row a click lands in, and the reveal that keeps the cursor visible
all measure by height rather than by count. A row's marker gutter lights for
`Checked` or for `Marked`, two channels because `Checked` is the selection the
stock updater writes and an app marking a row for a reason of its own must not
have that redefined under it; `ListItemTrailing` is right-aligned by the list
against a width only the list has, a row being built before the box is placed.
Whichever container holds the cursor, the row it names is styled as focused even
when focus sits elsewhere - being driven is what `ActiveDescendant` is for, and
a cursor nobody can see is that pattern contradicting itself. Both containers
also take their movement keys from a component of `(KeyBinding, Action)`
bindings - `ListBoxKeys`, `TableKeys` - which is how an app remaps a list to vim
keys, `Ctrl+D` included, without reimplementing movement beside the widget. Only
the bindings are the crate's: the scan itself is `plurimus_ui`'s `first_bound`,
the same one a focused scroll area's keys go through. Every widget here takes
its keys that way now: `SliderKeys`, `MenuKeys` and the single-line field's
`TextInputKeys` were the last inline matchers. The field's table is the one that
binds editing rather than movement, so an unbound unchorded character still
types itself, and `TextInputAction::Submit` is the one action
`TextInput::handle` refuses to apply - what committing means is the
dispatcher's, so `handle` leaves it and whoever routes the key acts on it. The
multi-line editor is the exception and stays inline, its keymap belonging to the
ratatui-textarea engine rather than to this crate.

A `Table`'s rows are child entities holding their own cells, banded by
`TableHeader` and `TableFooter` and striped by `TableStripe`. Interaction is
opt-in: `TableSelection` makes the table a tab stop and chooses row, column, or
cell granularity. Selection lands on the release rather than the press, `Click`
carrying the cell it ended on, because selecting usually closes what was clicked
and closing on the way down despawns the entity the pointer router is still owed
a release for; a header click reports its column on the same edge so the app can
sort - the crate supplies the geometry and never the ordering. A list moves its
cursor on press and drag, so the highlight follows the pointer; a table does
not, its cursor gutter existing only while a row is current, so moving the
cursor mid-gesture would shift the columns the release resolves against. Because
a scroll area windows a widget whole, a scrolled table's bands scroll with its
body. A cell is text in its row rather than an entity, so nothing can be a child
of one and `TableGeometry::cell_rect` publishes where a cell is instead - a host
floating a field over that rect is how a cell is edited in place. It and the
click router resolve against one column solve, taken from the table's current
state rather than from what was last drawn, so the two agree whoever moved the
cursor; and it is why an unstated `TableColumns` is divided by this crate rather
than by ratatui, whose identical rule would answer to its own render area and so
could drift from the geometry that has been published.

Re-exports `ratatui_widgets`, `ratatui_textarea`, and `bevy_input`'s `Key`.

### plurimus_bui

The bevy_ui bridge. `BuiPlugin` runs bevy_ui's real layout stack - `Node` trees
computed by taffy - against terminal cameras at one pixel per cell. Only layout
runs: bevy_ui's text, focus, picking, and asset systems stay out, and text is
measured by grapheme width instead of fonts. Computed nodes rasterize in the
terminal sub-app beneath all widgets, and node areas and wheel targets bridge
into plurimus_ui's routers so bevy_ui trees are hoverable, clickable, and
scrollable like any widget.

### plurimus_2d

The software 2d pipeline. `Glyph`, `GlyphBlock`, `Pixel`, and `PixelBlock`
entities positioned by `Transform`s are projected per camera through
`Projection2d` and rasterized into camera buffers in the world-space pass.
Glyphs and pixels each draw in transform `z` order, pixels beneath all glyphs;
`PixelBlock` stamps a palette-indexed bitmap one pixel per subcell, so pixel art
composes as one entity. `SubcellMode` selects halfblock or braille resolution,
and `RenderLayers` masks which cameras see which entities.

### plurimus_3d

The GPU readback pipeline, and the only crate that pulls in bevy_render.
`Render3dPlugins` assembles a headless bevy render stack; a real 3d camera
renders to an image, `ReadbackFrame` carries the pixels back, and a `Strategy3d`
converts them to cells - halfblock colors, luminance ramps (ASCII, blocks,
braille, shading), depth ramps. Depth readback feeds `DepthOcclusion` for
cross-camera occlusion and `EdgeOverlay` for outline characters. The render
stack stops before materials: the app adds its own material system (`PbrPlugin`)
and asset loading such as `bevy_gltf`.

### plurimus_test

Dev-only test support; a dev-dependency everywhere, never shipped. Input
injection (`press_key`, `click`, and friends) writes messages as if a backend
had translated them, `clipboard_writes` takes back the copies an app asked for
the way a backend would, and `composed_frame`/`composed_styled_frame` snapshot
the composed `FrameBuffer` straight out of the sub-app - so a test drives a full
app headlessly with no terminal and no presenter attached. `widget_content`
hands back the drawable an entity currently holds, which is how a test tells a
redraw from a skipped one.

## External Crates

- **Bevy** (0.19) - the ECS and app foundation, consumed as granular crates and
  never the `bevy` umbrella: `bevy_app` and `bevy_ecs` everywhere; `bevy_input`
  and `bevy_time` for the input contract; `bevy_input_focus` and `bevy_window`
  for focus; `bevy_ui`, `bevy_text`, and `bevy_camera` for the layout bridge;
  `bevy_transform` and `bevy_math` for 2d; the render stack (`bevy_render`,
  `bevy_core_pipeline`, `bevy_image`, `bevy_mesh`, `bevy_light`, and friends)
  only in `plurimus_3d`. Apps add whatever further bevy crates their scenes need
  at the same version, and cargo unifies them.
- **Ratatui** - the cell model and widget ecosystem: `ratatui-core` (0.1)
  supplies `Buffer`, `Cell`, `Rect`, `Style`, and the `Backend` seam;
  `ratatui-widgets` (0.3) the stock widget set; `ratatui-textarea` (0.9) the
  multi-line editor engine; `ratatui-crossterm` (0.1) the backend adapter the
  presenter drives.
- **crossterm** (0.29) - terminal control and the event source
  `plurimus_crossterm` translates from.
- **tui-scrollview** (0.6) - scroll-area windowing underneath `plurimus_ui`'s
  scrolling.
- **unicode-segmentation** / **unicode-width** - grapheme segmentation and width
  measurement for text handling in cells.

The minimum supported Rust version is 1.95, declared once in `workspace.package`
and verified in CI.

## Public API

The crates are published, so a public type is a commitment about what may change
under it. A type carries `#[non_exhaustive]` when the thing it models has an
open vocabulary - one terminals define, or pipeline phases, or an app's own
needs rather than a closed domain - so that gaining a field or a variant is a
minor release rather than a breaking one. Every sealed type keeps some way to be
built from outside - `Default` and public fields where that suffices, a
constructor where it does not, and a `const fn with_*` per field where the type
is built in a `const` context, which `KeyModifiers` and `InputCapabilities` are
the pattern for. An attribute that leaves a type unbuildable from outside is not
forward compatibility but a wall.

A type is deliberately left open when an app has to handle every case to be
correct. `KeyKind` is the clearest: press, repeat and release are the whole key
lifecycle, and a consumer's exhaustive match failing to compile is the point - a
`_` arm would swallow a kind it needs to decide about. `ClipboardTarget`,
`TableSelection`, `UiArea`, `PopoverSide`, `PopoverAlign` and `Edge` are open
for the same reason, each saying so where it is declared. Two categories are out
of scope by construction, since neither can grow: unit markers, and tuple
newtypes, where a second field is a redesign rather than an addition.

Traits are sealed only where a downstream implementation would be a mistake.
`TerminalRenderAppExt` is sealed, being an extension trait whose one sensible
implementor is bevy's `App`; sealing it is what lets registration methods appear
as sub-app phases land. `TerminalWidget` is deliberately open, because
implementing it is how a widget outside ratatui's `Widget` convention joins the
pipeline - the `headless` example does exactly that, and the blanket impl over
`Widget for &Self` covers everything else.

## Testing

Tests drive full apps headlessly. Because the presenter is `Backend`-generic and
`plurimus_test` reads the composed `FrameBuffer` directly from the render
sub-app, a test builds a real `App`, injects input as if a backend had
translated it, advances frames, and asserts on frame snapshots - no terminal
involved. Unit tests live in each crate; integration tests in `tests/` cover
cross-crate plugin composition; and every example is compiled as a test
(`test = true`), so the example suites are part of the workspace test run.

CI gates every change: `cargo fmt --all -- --check`,
`cargo clippy --workspace --all-targets --all-features -- -D warnings`,
`cargo test --workspace --all-features`, and
`RUSTDOCFLAGS="-D warnings" cargo doc --workspace --all-features --no-deps`,
plus `cargo hack check --each-feature` on the facade, a `cargo check` on the
MSRV toolchain, prettier and markdownlint over the markdown, typos, cargo-deny,
and cargo-semver-checks. The GPU smoke tests are `#[ignore]`d because they need
a wgpu adapter; run `cargo test --workspace --all-features -- --ignored` when
touching the 3d stack - they are the only coverage of the headless render
stack's plugin composition.

Because clippy runs with `-D warnings`, the lint configuration is a gate rather
than advice. `[workspace.lints]`, inherited by every crate, warns `missing_docs`
and clippy's `pedantic` group alongside `style`, `complexity`, `perf`, and
`suspicious`, denies `correctness`, and selects `missing_const_for_fn` and
`redundant_clone` out of `nursery`. The lints a terminal renderer cannot honor
are allowed at the workspace with the reason inline: the four narrowing-cast
lints, `needless_pass_by_value`, `type_complexity`, `float_cmp`, and
`match_bool`. `clippy.toml` carries the hard tier of the project's size limits -
50 lines per function, 5 parameters, 5 levels of nesting - so a breach fails the
build; file length has no lint and is enforced by review.