06 - Public API And Refactoring
06 - Public API And Refactoring
The extraction must produce a deliberately small library API, not preserve every current pub item. The donor root and component facade currently contain 39 public module declarations, 29 public re-export statements, and 112 public declaration/re-export lines in total. This count was produced on 2026-07-15 with rg over crates/jackin-tui/src/lib.rs and crates/jackin-tui/src/components.rs. It measures declaration lines rather than unique Rust API items, but it demonstrates that the existing root is already a large compatibility surface.
At donor revision 33896a504e19ef13adb8692550c1845cb86a9504, the two donor source trees contain 77 Rust files and 17,033 lines, measured with find ... -name '*.rs' and wc -l. There are 246 #[test] or #[tokio::test] attributes in jackin-tui, 15 in the lookbook, and 195 Rust consumer files outside the donor crates that reference jackin_tui. The migration therefore needs an API inventory and compatibility program, even though both projects are pre-release.
Target dependency layers
consumer domain models and effects
|
v
termrock::crossterm optional integration
crossterm event conversion + terminal session ownership
|
v
termrock components
widgets + interaction state + semantic theme
|
v
termrock foundations
text + input + focus + geometry + scrolling
|
v
ratatui-core
termrock-lookbook -> termrock + crossterm adapter + SVG renderer
docs -> termrock-lookbook generated catalogRatatui 0.30 split its workspace so widget-library authors can depend on ratatui-core for fewer dependencies and a narrower API surface, while applications may continue to use the umbrella ratatui crate. The Ratatui documentation explicitly recommends ratatui-core for widget-library authors. termrock should follow that boundary: the base crate renders into Ratatui buffers and does not initialize a terminal.
The initial manifest should expose only one optional integration feature:
[features]
default = []
crossterm = ["dep:crossterm", "dep:ratatui-crossterm"]The dependency split is fixed. The initial tested cell comes from the donor lock: ratatui-core 0.1.2 in termrock, ratatui-crossterm 0.1.2 plus crossterm 0.29.0 behind the feature, and umbrella ratatui 0.30.2 only where the lookbook/application examples need it. Manifest requirements may use compatible ranges, but Cargo.lock and compatibility.toml record that exact cell, and the graph must contain no duplicate Ratatui core or Crossterm version. Other focused dependency patch versions remain bounded Stage 2 implementation finalization under Decision 17. The fixed policy is:
- base
termrockcontains no Tokio, async runtime, stdout/stderr, filesystem, network, process, telemetry, or secret dependency; - Crossterm types do not appear in the default API; conversion from Crossterm events and terminal-session ownership are feature-gated adapters;
termrock-lookbookenables the adapter because its interactive CLI owns a real terminal;- add no
serde,tokio, or testing feature until a concrete second consumer requires the same contract.
Cargo features unify across a dependency graph and should be additive. Optional dependencies should be named through dep: so dependency names do not accidentally become permanent public feature names. Removing or changing a feature can be a compatibility break, so default = [] keeps the foundation predictable. See the Cargo feature reference and Cargo SemVer guidance.
Terminal capability baseline
termrock targets modern truecolor terminals (Ghostty-class). Assumed capabilities: 24-bit SGR color, alternate screen, SGR mouse reporting, OSC 8 hyperlinks, OSC 22 pointer shapes, and OSC 52 clipboard write. Components emit truecolor styles unconditionally; the library contains no 256/16-color fallback, NO_COLOR handling, light-background adaptation, RTL/BiDi shaping, or capability-detection code. A consumer that needs degradation implements it above the library. The supported-terminal statement ships in the README and the component catalog. Terminal size is explicitly not a capability assumption: tiny and empty rectangles remain valid, panic-free inputs, and the minimum/normal/wide render gates stay required.
Typed terminal requests
The modern-terminal baseline makes OSC 8 hyperlinks, OSC 22 pointer shapes, and OSC 52 clipboard writes first-class library concerns, but rendering stays side-effect free. The donor's current shape — the error and container-info dialogs returning raw overlay byte vectors for the host loop to write after the frame — is replaced by one typed surface in the osc module:
- widgets return typed link regions (stable ID, rect, URL) derived from the same layout function that painted them;
- pointer-shape and clipboard-write requests are plain data values;
- pure encoders turn a request into escape bytes without performing any I/O;
- the consumer decides whether, when, and where to emit — after the Ratatui frame, through the Capsule socket protocol, or not at all, according to its own URL/clipboard policy.
Because the encoders are pure and the regions come from the shared layout function, both are unit-testable and parity-checkable even though buffer and SVG fixtures cannot capture post-frame bytes.
Base API and Crossterm convenience API
The crossterm feature is first-class support, not a separate widget system. It
adds a convenient integration layer over the complete base API:
| Available without features | Added by crossterm |
|---|---|
| all widgets and widget builders | Crossterm event-to-logical-input conversion |
| component interaction states, logical actions, and outcomes | key, mouse, resize, focus, and paste adapters where supported |
| semantic themes and component style overrides | CrosstermBackend construction/re-export through the selected Ratatui integration |
| display-width, layout, hit-test, scroll, and text helpers | partial-initialization-safe managed terminal session/builder |
Widget/StatefulWidget rendering into Ratatui buffers | raw mode, alternate screen, cursor, and mouse-capture setup/restoration options |
direct Buffer and TestBackend testing | concise reference helpers for a normal Crossterm application |
Conceptually, consumers choose between these paths:
// No TermRock feature: use every component with Ratatui buffers or a backend
// selected and managed entirely by the application.
use termrock::{style::Theme, widgets::Tabs};
// With `features = ["crossterm"]`: the same components plus adapters and a
// managed session that removes repetitive terminal setup/cleanup.
use termrock::crossterm::{CrosstermSession, SessionOptions, adapt_event};
use termrock::{style::Theme, widgets::Tabs};The names are illustrative until the API review, but the layering is fixed:
- enabling
crosstermmust not change a component type, default style, rendering result, action, outcome, or module path; - no widget, theme, layout primitive, or logical input type may be guarded by
#[cfg(feature = "crossterm")]; - the feature may add
From/TryFromimplementations, adapter functions, backend/session types, and convenience constructors only; - a consumer may enable the feature solely for event conversion while managing its terminal independently;
- the managed session is optional even when the feature is enabled;
- no convenience API installs a panic hook, signal handler, logger, Tokio runtime, or application event loop;
default-features = falseremains a fully documented and tested way to use every TermRock component with Ratatui.
This follows the separation already present in Ratatui: the
ratatui-crossterm integration
implements Ratatui's backend contract, while widgets render independently of
that backend.
Proposed public modules
| Module | Owns | Must not own |
|---|---|---|
text | display width, clipping, terminal-control sanitization, bounded lines | paths, URLs, command output policy |
input | logical keys, modifiers, chords, bindings, action dispatch | product commands or hardcoded action wording |
interaction | focus, hover, selection, modal outcomes | application navigation or root model |
layout | responsive dialog specifications, hit-test geometry, bottom regions | role/auth/mount/database-specific rectangles |
scroll | offsets, axes, viewport math, cursor following, scrollbar models | product content construction |
style | semantic roles, Theme, builders, Tailrocks phosphor preset | global product constants or capsule/status meanings |
widgets | borrowed render models and explicit stateful widgets | I/O, effects, domain records, global process state |
osc | typed hyperlink/pointer/clipboard request values and pure OSC 8/22/52 encoders | emission timing, stdout writes, clipboard policy, URL-opening decisions |
runtime | executor-neutral Dirty/UpdateResult/Subscription/Component/View contracts plus the drive_frame/drive_render frame drivers; base module optional by use | Tokio, spawning, effect execution, panic/signal policy, any requirement that widgets use it |
crossterm | optional event adapters, backend convenience, and scoped session guard | widgets, Tokio, signal policy, panic-hook installation, logging initialization |
Do not add a broad prelude or glob re-export in the first release. Callers should import from the owning module so the library can distinguish foundational API from component internals. Private render helpers remain private. A root re-export is allowed only for the few entry types consumers use on nearly every screen, such as Theme.
Component contract
Ratatui recommends implementing widgets by reference for reuse. New components should implement Widget for &Component for stateless rendering and StatefulWidget for &Component when Ratatui-managed interaction state is genuinely required. Do not expose the current mixture of free render_* functions, Frame-specific wrappers, and consuming widgets as three equivalent public paths. See Ratatui's custom widget guidance.
Use this shape:
pub struct Tabs<'a, Id> {
items: &'a [Tab<'a, Id>],
theme: &'a Theme,
}
pub struct TabsState<Id> {
selected: Option<Id>,
hovered: Option<Id>,
focused: bool,
}
impl<Id> StatefulWidget for &Tabs<'_, Id> {
type State = TabsState<Id>;
fn render(self, area: Rect, buffer: &mut Buffer, state: &mut Self::State) {
// Paint from borrowed items and explicit interaction state.
}
}The code is illustrative, not a frozen signature. The required properties are:
- render data is borrowed where practical rather than cloned into a component every frame;
- stable consumer IDs cross the interaction boundary instead of array indices;
- interaction state is separate from domain data;
- rendering has no side effects and never changes domain state;
- builders keep fields private so a field can be added without breaking every struct literal;
- public enums that may grow are
#[non_exhaustive], with consumers required to handle unknown variants; - hit-test geometry is derived by the same layout function used to paint the component;
- a component does not retain callbacks, views, executors, or product objects.
WidgetRef and StatefulWidgetRef remain unstable Ratatui features in the researched release. termrock should use the stable Widget for &T / StatefulWidget for &T pattern and avoid making an unstable Ratatui feature part of its contract.
State, input, and effects
The donor's Component<Ev, Msg>, View<Model>, Dirty, ModalOutcome<T>, and typed UpdateResult<E> concepts are useful, but they should not become mandatory widget machinery.
Ratatui's Elm Architecture guidance separates model, message-driven updates, and a side-effect-free view. Its Component Architecture guidance describes a valid application-level alternative that co-locates event handling, update, and rendering. TermRock must remain compatible with both without choosing either for the consumer: it provides component interaction state, logical actions, pure updates, typed outcomes, and standard widgets, while the consumer owns the root model/message loop and effect execution. The complete per-component application of this decision is in 09 - Component redesign catalog.
The initial contract has three independent levels:
- Rendering: standard Ratatui
Widget/StatefulWidgetimplementations. - Interaction: component-specific
handle_actionmethods over logical actions, returning a small outcome such asIgnored,Changed,Submitted(Id), orCancelled. - Application effects: consumer-owned update code converts outcomes into product messages and executes I/O.
Raw terminal events are adapted before component dispatch:
Crossterm KeyEvent ----+
+--> termrock::input::KeyChord --> consumer keymap --> Action
capsule byte decoder --+This keeps the same widgets usable in jackin❯ host surfaces and the in-container multiplexer without forcing every surface to receive Crossterm events. Key bindings own chords and actions; visible hint labels are consumer-provided data derived from that binding table. The library must not hardcode English labels such as select, save, or cancel inside state objects.
The executor-neutral runtime contracts — Dirty, UpdateResult<E>, SubscriptionPoll, the Subscription trait with std-channel and closure adapters, Component<Ev, Msg>, and View<Model> — plus the persistent drive_frame and one-shot closure-based drive_render drivers extract into the ordinary base runtime module. It is optional by use, not a Cargo feature: the initial manifest still has only the crossterm feature, no widget references runtime, and no architecture example requires it. The contracts and both draw drivers are pure Ratatui; drive_render adapts a short-lived renderer onto drive_frame rather than introducing a competing loop. jackin❯'s canonical TUI rules already mandate the one shared frame driver across its console, Capsule, and launch surfaces, and deleting the donor would otherwise strand these drivers in a new product-local crate. The module stays convenience, not framework, and future consumers are not required to adopt it. The Tokio receiver implementations, spawn helpers, and fallback runtime in crates/jackin-tui/src/runtime.rs stay in jackin❯ with jackin-console as the target home; Rust coherence prevents a consumer crate from implementing the extracted Subscription trait directly for Tokio receiver types, so the shared module ships a subscription closure adapter and consumers may wrap receivers in local newtypes. No fallback Tokio runtime is created inside the component library.
Theme API
The current palette exposes product constants for rain animation, Capsule menu states, debug chips, status tabs, and links from crates/jackin-tui/src/lib.rs. That is a product stylesheet, not a reusable theme API.
Theme should have private fields and a builder/default path. For the initial revision line, both Theme::default() and a new builder start from Theme::tailrocks_phosphor(); consumers opt into another palette or override tokens explicitly. Components request semantic roles rather than palette names:
canvas / surface / elevated / backdrop
text / text_muted / text_disabled
border / border_focused / selection / focus
accent / success / warning / danger / info
link / link_hover
input / input_invalid
scroll_track / scroll_thumbDo not expose a trait with one required method per token: adding a token would break implementors. A private representation with Theme::style(Role) and a ThemeBuilder can add a role with a documented fallback. Theme::tailrocks_phosphor() preserves the established look without making PHOSPHOR_GREEN the contract of every widget. Changing a default token is a behavioral API change that requires regenerated renders and a migration note.
Component-specific style overrides are data passed to that component, not new global constants. All states also require a non-color signal: glyph, border, label, modifier, or position. Color cannot be the only indication of focus, selection, error, or progress. The donor violates this today (panel focus is a color-only border change); the fix is a recorded quality-tier item that lands after jackin❯ parity, not during extraction.
Parity constrains the refactor in three specific ways: Theme::tailrocks_phosphor() must reproduce the donor's exact RGB values so migrated fixtures stay byte-identical; the faded alpha helper keeps its exact scaling math; and the lookbook SVG writer derives its color table from the same theme values instead of the duplicated hex table it hardcodes today, with a test asserting the committed SVGs are unchanged by that consolidation. Product tokens — CAPSULE_MENU_*, BRAND_BLOCK, DEBUG_AMBER, TAB_BG_*, and the rain colors — never enter Theme; jackin❯ keeps them as local constants layered on top of the semantic tokens.
Terminal ownership
Crossterm's raw mode, alternate screen, and mouse capture are separate operations, and its APIs require explicit disable/leave operations. See the Crossterm terminal module and event module.
The optional crossterm integration should provide a scoped session with these rules:
- record each mode only after it was successfully enabled;
- if setup fails halfway, restore the already-enabled modes before returning the error;
- expose an explicit fallible
restore()used on the normal path; - use
Droponly as an idempotent best-effort fallback becauseDropcannot report restoration failure; - restore modes in reverse ownership order and always attempt every cleanup step;
- support alternate-screen and inline modes through explicit options;
- never install a global panic hook, signal handler, logger, or diagnostics route from the library;
- never use product-global atomics such as the current rich-surface/host-screen flags in
crates/jackin-tui/src/ownership.rs.
The application root owns panic/signal integration and calls the scoped restore operation. termrock-lookbook provides a complete reference integration and PTY tests. Crossterm event conversion is pure; event polling strategy remains consumer-owned.
Donor refactoring ledger
| Donor area | Required action | Target |
|---|---|---|
theme.rs and root RGB constants | replace product constants with private semantic theme + phosphor preset | style |
geometry.rs | split display/text/tab math from agent_display_name | text, layout; agent mapping stays local |
jackin_core::shorten_home re-export | remove from the donor facade; retain the environment-aware helper in jackin-core for TUI and non-TUI callers | jackin-core |
keymap.rs | retain logical chord/action mapping; move action labels to consumer data | input |
scroll.rs, scrollable-panel math | consolidate one scroll model and one display-width source, retaining the tui-scrollbar metrics dependency as the single thumb-math source | scroll, widgets |
runtime.rs | extract executor-neutral contracts and the pure Ratatui drive_frame/drive_render drivers into the base module, optional by use; Tokio adapters stay in jackin❯ | runtime; Tokio helpers stay local |
root PointerShape, OSC 22/52 helpers | extract as typed requests plus pure encoders; emission stays consumer-owned | osc |
| lookbook SVG color table | derive from the shared theme values; assert committed SVGs unchanged | termrock-lookbook |
terminal_modes.rs | refactor into partial-init-safe optional Crossterm session | crossterm |
ownership.rs | do not extract product-global terminal/diagnostics policy | jackin❯ |
host_colors.rs | retain locally until a second consumer needs OSC 10/11 discovery | consumer adapter |
ansi.rs | split pure parsing/sanitization from banners, version/help, and direct output | text; product output stays local |
animation.rs, output.rs | do not extract launch/container behavior or direct output | jackin❯ |
BrandHeader | keep jackin❯ wordmark composition local; extract a neutral header bar only through separately scoped future demand | consumer first |
ContainerInfo / DebugInfo | replace generic row rendering with DetailTable; keep container/version builder local | widgets + consumer facade |
| confirm dialog | extract generic state/widget; move exit wording and data-loss constructors local | widgets |
| modal rectangles | replace role/auth/source/mount modes with min/preferred/max dialog specification | layout |
| status footer | redesign as data-driven left/right slots with stable IDs; keep usage/container/run policy local | widgets + consumer facade |
| select list | separate rows from state, use stable IDs, pluggable filtering, display-width measurement, configurable hints | widgets |
| text input | use grapheme/display-column cursor rules, external validation, configurable actions | widgets |
| diff view | make render/update boundary explicit; no hidden content mutation in a free render function | widgets |
| URL and OSC overlays | return typed regions/requests through osc; consumer decides whether policy permits emission | osc types; emission stays a consumer adapter |
The existing SelectListState in crates/jackin-tui/src/components/select_list.rs demonstrates why refactoring precedes publication: it owns Vec<String>, returns indices, performs fixed ASCII substring filtering, measures labels with chars().count(), and embeds action labels. The existing TextField in crates/jackin-tui/src/components/text_input.rs combines editing with product validation and action wording. Those behaviors are valuable donor implementations, but their current signatures are not the reusable API.
Chapter 09 expands this ledger across every current component implementation file and the supporting geometry, input, theme, runtime, terminal, ANSI, URL, output, and animation modules. Its classification is the required source for the generated extraction ledger; the shorter table above is only the dependency-level summary.
Public API review gate
Before the first immutable consumer revision:
- generate and review a public-item inventory for
termrock; - deny missing documentation and broken intra-doc links;
- run
cargo-semver-checksagainst the previous tag after the first tagged baseline; - require a migration note for public renames, moves, changed defaults, feature changes, and MSRV changes;
- prohibit public consumer dependencies, glob exports, mutable globals, hidden I/O, and executor-specific types;
- verify no widget returns raw escape bytes; the typed
oscrequests and pure encoders are the only escape-producing surface; - verify the default-feature crate builds without Crossterm or Tokio in
cargo tree; - compare base and
crosstermrender fixtures and component API inventories so enabling the feature cannot change or hide a widget; - use
cargo package --listto review exactly what a registry package would contain, even while Git revisions remain the distribution path.
cargo-semver-checks detects many Rust API changes, but it cannot prove behavioral, keyboard, rendering, or feature-default compatibility. The deterministic lookbook and conformance suites remain independent required gates. Cargo's own SemVer reference is the policy baseline.