09 - Component Redesign Catalog
09 - Component Redesign Catalog
TermRock must be a reusable Ratatui toolkit, not a relocated copy of the
jackin❯ application framework. A consumer should be able to render one
TermRock button, list, input, or panel without adopting an agent model, root
message enum, Tokio runtime, terminal loop, Tailrocks theme, or any other
component. This chapter turns that constraint into a file-by-file redesign
plan for the 24 current donor component implementation files and their shared
support modules.
Ratatui architecture evidence
Ratatui documents several valid application patterns rather than prescribing one framework:
- The Elm Architecture separates a model, messages, update logic, and a view. Events are mapped to messages, updates perform state transitions, and the view should remain side-effect free for a given state. Ratatui notes that Rust applications may mutate a model in place where appropriate and that stateful widgets sometimes require mutable presentation state while rendering.
- Component Architecture co-locates component initialization, event handling, update, and rendering. The published example uses application-specific event and action types. That is useful inside an application, but it is not an appropriate required trait for a cross-application widget library.
- Flux Architecture uses actions, a dispatcher, stores, and views/widgets in a unidirectional flow. TermRock widgets can render borrowed store projections and return typed outcomes without importing or implementing a dispatcher/store API.
- Ratatui recommends implementing new reusable widgets by reference, using
stable
Widget for &TorStatefulWidget for &Timplementations. TheWidgetRefandStatefulWidgetReftraits are still unstable in Ratatui 0.30.2. See the widget authoring guidance. - Ratatui's Builder Lite pattern
consumes and returns
selffor render configuration. TermRock should follow this familiar construction style while keeping public fields private. - Ratatui recommends direct
Bufferunit tests for focused widget behavior andTestBackendfor application integration. SeeTestBackend.
The implication is not "put TEA into every widget." The implication is to preserve TEA-compatible boundaries without forcing one application architecture.
TermRock architecture decision
TermRock exposes composable rendering and interaction building blocks. It does
not expose a mandatory root Application, Model, Message, Component,
Subscription, or EffectExecutor trait. The executor-neutral base runtime
module (dirty/update contracts, subscriptions with std-channel and closure
adapters, component/view traits, and the drive_frame/drive_render frame drivers) is
optional by use rather than feature-gated; it exists for consumers that want
the donor's TEA conventions, no widget or other module references it, and
nothing requires adopting it.
backend event
|
v
consumer adapter -> logical chord/pointer/resize
|
v
consumer keymap -> component action
|
v
pure component update(state, action) -> component outcome
|
v
consumer maps outcome -> product message/effect/domain mutation
borrowed render data + interaction state + theme
|
v
Widget / StatefulWidget -> BufferThis design supports both common application styles:
- a direct immediate-mode consumer constructs borrowed widgets in its draw function and updates its own state in its event loop;
- a TEA consumer maps component outcomes into its root message enum and executes effects after its root update;
- a component-oriented consumer stores TermRock interaction state in its own component and calls the same update/render functions;
- a Flux consumer maps outcomes into actions handled by its own dispatcher and stores, then projects store state back into the same widgets.
Neither consumer is visible in the public TermRock API. The library never imports their event, action, model, error, telemetry, or runtime types.
Required architecture examples
The new repository must demonstrate architectural neutrality with executable, compile-checked examples rather than a prose claim:
examples/
|- direct.rs
|- tea.rs
|- component.rs
|- flux.rs
|- buffer_only.rs
|- crossterm_manual.rs
`- crossterm_managed.rsAll examples should present the same small neutral screen using the same public tabs, list, input, action, outcome, and theme types. Only application state ownership and message routing differ:
| Example | Demonstrates | Must not imply |
|---|---|---|
direct.rs | ordinary event loop plus immediate widget composition | a framework is required |
tea.rs | application model, root message enum, update, view, effect boundary | TermRock owns the root model or executor |
component.rs | consumer-defined component holding local state and routing events | TermRock's internal trait is mandatory |
flux.rs | consumer actions/store/dispatcher projecting into widgets | TermRock supplies a global store |
buffer_only.rs | no Crossterm/default features; render directly to a Ratatui buffer | a real terminal backend is required |
crossterm_manual.rs | same components with application-owned Crossterm backend/session setup | the convenience layer is mandatory |
crossterm_managed.rs | event adapters and managed-session convenience from the optional feature | TermRock owns the application loop or panic policy |
Examples depend only on public API and contain no #[cfg(test)] access to
internals. CI checks the architecture/buffer examples without features and the
two Crossterm examples with the declared feature. The Fumadocs site
adds an "Application patterns" section that explains these integrations and
links each component page to small direct/TEA snippets where interaction is
relevant. Examples are compatibility tests, not a third TermRock runtime crate.
Standard component anatomy
Not every widget needs every type, but an interactive component should normally be decomposed into these roles:
| Role | Responsibility | Examples |
|---|---|---|
| render data | borrowed, consumer-owned content | Tabs<'a, Id>, ListRows<'a, Id> |
| interaction state | small persistent UI state only | selected ID, focus, scroll offset, cursor |
| action | semantic operation understood by the component | next, previous, insert, delete, submit |
| outcome | typed fact for the consumer to interpret | activated ID, changed, submitted, cancelled |
| layout | pure geometry used by paint and hit testing | TabsLayout<Id>, DialogAreas |
| widget | side-effect-free buffer rendering | Widget for &Tabs, StatefulWidget for &List |
| adapter | optional backend conversion | Crossterm key/mouse to logical input |
The contracts follow these rules:
- Domain records remain in the consumer. Render models contain only the text, styles, IDs, capabilities, and state required to paint and interact.
- Stable caller-provided IDs cross selection, hover, activation, and hit-test boundaries. Collection indices stay internal and ephemeral.
- Widget constructors borrow labels, rows, and theme data where practical. They do not clone a full product collection every frame.
- Configuration uses private fields and Builder Lite methods. State mutation uses named methods or pure update functions, not public fields.
- Rendering does not dispatch callbacks, emit terminal sequences, access the clipboard, open URLs, log, spawn, poll, sleep, or mutate domain state.
- The function that calculates layout also returns the interactive regions. Mouse routing never reconstructs geometry independently from rendering.
- Raw Crossterm events appear only in the optional integration module. Components accept logical actions so byte-decoded and test inputs use the same path.
- Visible hints are consumer-provided or derived from the same keymap that
dispatches actions. A widget does not hardcode
save,agent,run, or other product wording. - Empty and tiny rectangles are valid inputs and must not panic. Content is clipped by display width, not byte length or scalar-value count.
- Public components are useful independently. Rendering
Tabsmust not require a TermRock status bar, terminal session, or root runtime. - Enabling
crosstermadds adapters and convenience only. It does not select different component implementations or change base rendering behavior.
Target module tree
termrock/src/
|- input/
| |- action.rs
| |- binding.rs
| |- chord.rs
| `- pointer.rs
|- interaction/
| |- focus.rs
| |- hover.rs
| |- outcome.rs
| `- overlay_stack.rs
|- layout/
| |- dialog.rs
| |- hit_region.rs
| `- slots.rs
|- scroll/
| |- state.rs
| |- geometry.rs
| `- viewport.rs
|- style/
| |- role.rs
| |- theme.rs
| `- tailrocks_phosphor.rs
|- text/
| |- display_width.rs
| |- sanitize.rs
| `- window.rs
|- osc/
| |- request.rs
| `- encode.rs
|- runtime/ # executor-neutral; optional by use
| |- contract.rs
| |- subscription.rs
| `- frame.rs
|- widgets/
| |- action_bar.rs
| |- detail_table.rs
| |- dialog.rs
| |- diff.rs
| |- hint_bar.rs
| |- list.rs
| |- panel.rs
| |- status_bar.rs
| |- tabs.rs
| |- text_input.rs
| `- toast.rs
`- crossterm/ # optional `crossterm` feature
|- event.rs
|- backend.rs
`- session.rsThis is an ownership map, not a requirement to create a file for every small
type on day one. The important constraints are one canonical owner per
primitive, no components.rs mega-facade, and no module named for a consumer
workflow.
Foundation module decisions
| Donor module | Decision | Required redesign |
|---|---|---|
geometry.rs | split | Move display-width, clipping, fixed-prefix windows, and generic tab geometry; leave agent_display_name in jackin❯. Replace index-returning tab hit tests with stable IDs. |
keymap.rs | parameterize | Keep logical chords and binding dispatch. Feature-gate Crossterm conversion, separate the Capsule raw-byte decoder, make hint text caller data, and remove the global scroll-hint keymap. |
scroll.rs plus scroll helpers in jackin-core | consolidate | Create one canonical offset/axis/thumb/viewport implementation. Retain the maintained tui-scrollbar dependency as the single proportional thumb-metrics source. Use usize for logical offsets, lengths, and indices; clip in logical space and convert through one centralized saturating u16 helper only at the Ratatui geometry edge, never wrapping or silently truncating. Remove parallel public usize/u16 APIs. |
theme.rs plus root RGB constants | reimplement | Replace public product constants with semantic Theme roles, private storage, builders, component overrides, and the optional Tailrocks phosphor preset. Capsule/debug/rain meanings stay local. |
runtime.rs | split | Extract the executor-neutral contracts and pure Ratatui drive_frame/drive_render drivers into the base runtime module, optional by use and referenced by no widget. drive_render remains the one-shot closure adapter over the canonical driver, not a competing loop. jackin❯ mandates one shared frame driver across console, Capsule, and launch, so donor deletion would otherwise strand them. Tokio receiver implementations, spawn helpers, and the fallback runtime stay in jackin❯ (jackin-console), connecting through the subscription closure adapter because coherence forbids direct foreign impls. |
terminal_modes.rs | reimplement | Replace two mouse helper functions with the scoped, partial-initialization-safe session described in chapter 06. Event polling and panic/signal policy remain application-owned. |
ownership.rs | remain | Rich-surface and host-screen atomics, title policy, and alternate-screen reassertion are jackin❯ process policy. None becomes TermRock global state. |
ansi_text.rs | extract | Keep pure ANSI-to-span parsing and stripping with malformed/control-sequence tests. Accept input and return data; perform no output. |
ansi.rs | split | Keep only proven generic encoding/sanitization helpers in a policy-neutral module. Brand banners, help/version output, and direct terminal emission remain local. Raw overlay byte vectors are replaced by the typed osc request/encoder surface. |
root PointerShape, OSC 22 names, OSC 52 helper | extract | Move to osc as typed pointer/clipboard/hyperlink requests plus pure encoders. Emission timing and policy stay in the consumer; no widget returns raw escape bytes. |
url_text.rs | remain | URL opening and log-redaction policy are not widget responsibilities. Promote pure parsing only after a second consumer proves an identical contract. |
host_colors.rs | remain | Host OSC color discovery performs terminal I/O and is not required to render a widget. Keep it in jackin❯; any future extraction requires a separately scoped decision. |
prune_output.rs, output.rs | remain/split | Direct CLI output stays local. Promote only a pure bounded-lines projection if a second consumer needs the same behavior. |
animation.rs | remain | Digital rain, launch/exit timing, output, and host ownership encode product behavior. A future generic animation crate requires separate evidence. |
Component-by-component disposition
Extract after bounded parameterization
| Donor component | Target | Required public contract |
|---|---|---|
bottom_chrome | layout::Slots | Replace the fixed hint/spacer/footer policy with caller-supplied bottom slot heights. Keep saturating geometry and zero-height behavior. jackin❯ can provide its three-row preset locally. |
button_strip | widgets::ActionBar<Id> | Borrow items containing stable ID, label, enabled state, and optional style override. Store focused ID in state, return activated ID, derive hit regions from layout, and remove public line/style/rect helper duplication. |
filter_input | widgets::TextInput composition/preset | Render a consumer label/query with semantic input styles over the shared text-input contract. Do not own filtering or assume a picker. The initial API publishes no separate FilterField; the neutral filter state is a lookbook/docs composition over TextInput. |
hint_bar | widgets::HintBar | Borrow typed hint descriptors containing chord display and caller label. Support wrapping and priority/visibility without owning a keymap or action wording. Keep display-width measurement. Wrapping must cover the Capsule wrapped-hint behavior so its hand-painted rows can converge post-parity. |
hover_tracker | interaction::HoverState<Id> | Track stable IDs over borrowed HitRegion<Id> values. Make pointer update pure and allocation-bounded; no callbacks or component names. |
modal_backdrop | widgets::Backdrop | Accept semantic style/character policy and implement Widget for &Backdrop; never assume black or the Tailrocks palette. |
panel | widgets::Panel | Use semantic focus/emphasis rather than phosphor constants. Accept a Ratatui Block or builder-lite title/border overrides; remove modal_block and unfocused_block policy helpers. |
toast | widgets::Toast | Borrow message, severity, anchor, and style. Geometry is pure; dismissal timer and queue live in the consumer. |
tab_strip | widgets::Tabs<Id> | Borrow stable-ID items with label/active/enabled state. Keep selected/hovered/focused state separate, expose hit regions keyed by ID, and implement by reference rather than consuming the widget. Include per-tab glyph/state slots — the Capsule status bar re-implements tab painting today precisely because the donor widget lacks them. |
Reimplement before extraction
| Donor component | Target | Why the current API cannot move | Replacement |
|---|---|---|---|
select_list | widgets::List<Id> / SelectList<Id> | Owns Vec<String>, selection by index, fixed substring filtering, action labels, and mixed render helpers. | Borrow rows with stable IDs and selectable/separator roles; keep query/selection/scroll in state; accept a consumer-supplied visible projection or filtering strategy; use display-width windows; return activated ID; virtualize visible rows. |
text_input | widgets::TextInput + TextInputState | Mixes Crossterm events, duplicate-name policy, forbidden values, labels, save/cancel wording, cursor bytes, and rendering. | Maintain grapheme-safe editing state and horizontal viewport; accept logical edit actions; take external validation result and message; keep submit/cancel mapping in consumer; expose cursor placement as render metadata. |
scrollable_panel | scroll::ViewportState + list/paragraph widgets | Duplicates scroll math and publishes many low-level render functions with product styles. | Consolidate scrolling first; widgets borrow visible content or a row source, render through StatefulWidget, expose thumb/hit geometry, and avoid materializing large collections per frame. |
dialog_layout | widgets::Dialog + layout::DialogSpec | Combines shell rendering, scrolling, key hints, axes, and Capsule byte-wheel behavior. | Separate responsive geometry, scroll state, shell widget, and logical actions. Caller supplies title/body/footer/action bar; backend adapters translate wheel events. |
modal_rects | layout::DialogSpec | Public variants name role, auth, mount, source, scope, and operation workflows. | Use min/preferred/max width and height, placement, margins, and overflow rules. Product screens construct specs locally. |
confirm_dialog | widgets::ChoiceDialog<Id> | Hardcodes exit/data-loss constructors, yes/no policy, keymap labels, and focused indices. | Borrow title/body/warning/action descriptors; use stable action IDs; expose choice/cancel outcome; keep exit wording and destructive-action policy in jackin❯. |
save_discard_dialog | ChoiceDialog<Id> consumer preset | Encodes a particular dirty-exit workflow and direct Crossterm handling. | Implement Save, Discard, and Cancel as caller action data over the generic choice dialog. No dedicated public TermRock state is published for this workflow. |
error_dialog | widgets::MessageDialog composed with DetailTable | Mixes scrolling, product keymap hints, row copying, hyperlinks, and raw OSC overlay bytes. | Borrow message and neutral detail rows; return typed Copy(Id)/ActivateLink(Id) outcomes and hit regions. Link regions render through the typed osc surface instead of raw overlay bytes. Consumer executes clipboard/OSC/URL policy. |
container_info | widgets::DetailTable<Id> | DebugInfo explicitly builds run, container, role, agent, version, diagnostics, and file-link rows. | Extract only label/value rows, selection, scrolling, wrapping, copy/link capabilities, and typed outcomes. Keep DebugInfo and all product row construction in jackin❯. |
status_footer | widgets::StatusBar<Id> | Names usage, container, run ID, debug chip, and fixed right-side order. | Borrow left/right slots with stable IDs, priority, minimum width, truncation rule, emphasis, and enabled state. Return hit regions/activated ID. jackin❯ decides slot order and meanings. |
status_popup | MessageDialog or Toast composition | Couples status presentation to a dedicated popup state and frame renderer without proving a distinct neutral primitive. | Fold the donor behavior into a generic message surface; do not publish a dedicated TermRock status-popup type. |
diff_view | widgets::DiffView + DiffState | Rendering takes mutable state, mixes projection/layout mutation, hardcoded hints, and theme constants. | Parse/project outside rendering; borrow immutable hunks/lines; keep only selection and scroll in state; use semantic added/removed roles; return no effect from render. |
Keep consumer-local unless reuse is proven
| Donor component | Owner | Reason |
|---|---|---|
brand_header | jackin❯ composition | It renders the literal jackin❯ wordmark and brand colors. Consumers can compose a generic panel/line today; a shared branded header is not yet a neutral primitive. |
focus_owner | split | Replace ButtonFocus and the tab/button-specific owner enum with generic FocusState<Id> or consumer state. Do not publish workflow-shaped focus enums. |
modal_lifecycle | split | Extract only the pure backdrop and inside/outside hit classification. Do not publish a generic stack or parent-chain lifecycle in the initial API; application modal routing remains local, and a future shared stack requires a separately scoped two-consumer decision. |
No component is copied unchanged merely to preserve its current Rust path. The new repository should preserve useful implementation history, but neutralizing commits establish the new ownership and API before any immutable consumer pin is advertised.
Required redesign contracts
Selection and list projection
The list API must not require TermRock to own arbitrary domain values or all rows. A suitable conceptual boundary is:
pub struct ListRow<'a, Id> {
id: Id,
label: Line<'a>,
role: RowRole,
enabled: bool,
}
pub struct ListState<Id> {
selected: Option<Id>,
hovered: Option<Id>,
scroll: ViewportState,
}
pub enum ListOutcome<Id> {
Ignored,
Changed,
Activated(Id),
}The exact signature remains an implementation decision. The invariant is that the consumer can project only the visible/filter-matched domain records into borrowed render rows and recover the original stable ID from an outcome.
Text editing
TextInputState owns only text-editing mechanics: content, cursor boundary,
selection if supported, and horizontal viewport. It does not own a list of
forbidden project names or decide that Enter means "save." The consumer maps
its chord to EditAction::Submit, evaluates its domain validation, and decides
whether a Submitted(String) outcome changes application state or displays a
validation message.
Cursor movement, backspace, delete, selection, and viewport boundaries operate
on Unicode extended grapheme clusters, with cursor positions stored as byte
offsets that are always valid grapheme boundaries. Painting measures
unicode-width display columns over whole grapheme/string slices using the
standard non-CJK width policy. Tests cover combining marks, emoji ZWJ
sequences and modifiers, regional indicators, CJK wide characters, zero-width
characters, and horizontal clipping. The rationale and dependency policy are
closed in 05 - Decision record.
Layout and hit testing
Every interactive widget should expose or internally reuse one pure layout calculation:
layout(area, render data, state) -> visual areas + HitRegion<Id> values
render(layout, data, state, buffer)
pointer(position, regions) -> optional Id/actionThis removes the current pattern where public *_rect, *_hit, render, and
OSC-overlay helpers can drift. Layout results are frame-scoped values; widgets
do not retain terminal coordinates across a resize.
Updates and effects
TermRock component updates may mutate their small interaction state in place, which Ratatui explicitly permits as a practical TEA adaptation. They return data-only outcomes. They do not return boxed futures, Tokio channels, commands that can execute themselves, or consumer messages.
component action -> update interaction state -> outcome
consumer outcome -> root TEA message -> domain update + effect request
consumer runtime -> execute effect -> new root messageThis keeps clipboard writes, URL opening, command execution, terminal output, database calls, agent orchestration, CI operations, and telemetry completely outside TermRock while allowing every consumer to use the same testable interaction machinery.
API rejection checks
The generated public API report and extraction ledger should fail review when any public TermRock signature or documentation contains:
- a Tailrocks product domain noun such as agent, capsule, container, workspace, role, run, database, trace, job, credential, or connection;
- a dependency path from
jackin-*, Holla, Velnor, Parallax, or a future consumer crate; tokio,tracing, filesystem, networking, process, clipboard, secrets, or application configuration in the base crate;- Crossterm types outside the optional integration module;
- any component, state, theme, layout, action, or outcome hidden when the
crosstermfeature is disabled; - public collection indices as durable selection/activation identity;
- raw terminal escape bytes as a widget outcome;
- hardcoded product action labels or key policy;
- rendering that requires
&mutonly because it performs hidden projection, layout caching, event dispatch, or effects; - duplicated free-render and widget APIs without a documented compatibility reason;
- a component that can only be tested or rendered through the lookbook's private application state.
Use a denylist as an audit aid, not as the proof of neutrality. Generic words
can still hide product assumptions, and a legitimate example may use one of
these nouns in consumer documentation. Human API review, neutral stories,
architecture examples, and jackin❯ parity remain required. Another product
proof is explicitly outside this roadmap.
Per-component conformance
Every extracted public component needs, before the first consumer pin (extraction tier):
- direct buffer tests for default, focused, disabled, empty, overflow, tiny, and off-origin rectangles;
- update tests proving ignored actions do not mutate state and handled actions return the documented outcome;
- stable-ID tests across insertion, removal, sorting, and filtering;
- layout/hit-test agreement at minimum, normal, and wide sizes;
- a neutral lookbook story and deterministic SVG;
- rustdoc showing standalone use without the Crossterm integration or root runtime;
- at least one compile-only example using a non-Tailrocks domain ID type.
Before the first tag (quality tier, after jackin❯ parity):
- Unicode/display-width cases appropriate to its content, with the recorded character-count defects fixed;
- a non-color focus/selection/error cue.
The split exists because fixing width math or adding a non-color cue changes rendered bytes: those fixes land as reviewed post-parity changes with regenerated fixtures, per 08 - Migration evidence and gates.
The repository-level direct, TEA, component, Flux, buffer-only, manual Crossterm, and managed Crossterm examples must also compile against every advertised Ratatui/MSRV compatibility cell. Canonical component buffers must be identical with and without the feature.
Composite consumer screens remain covered in their own repositories. This
roadmap proves TermRock primitives are neutral and correct and proves jackin❯
composition remains compatible. Every other product validates its composition
and effects later as part of its own adoption work.