ResearchShared TUI Extraction

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 catalog

Ratatui 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 termrock contains 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-lookbook enables 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 featuresAdded by crossterm
all widgets and widget buildersCrossterm event-to-logical-input conversion
component interaction states, logical actions, and outcomeskey, mouse, resize, focus, and paste adapters where supported
semantic themes and component style overridesCrosstermBackend construction/re-export through the selected Ratatui integration
display-width, layout, hit-test, scroll, and text helperspartial-initialization-safe managed terminal session/builder
Widget/StatefulWidget rendering into Ratatui buffersraw mode, alternate screen, cursor, and mouse-capture setup/restoration options
direct Buffer and TestBackend testingconcise 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 crossterm must 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/TryFrom implementations, 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 = false remains 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

ModuleOwnsMust not own
textdisplay width, clipping, terminal-control sanitization, bounded linespaths, URLs, command output policy
inputlogical keys, modifiers, chords, bindings, action dispatchproduct commands or hardcoded action wording
interactionfocus, hover, selection, modal outcomesapplication navigation or root model
layoutresponsive dialog specifications, hit-test geometry, bottom regionsrole/auth/mount/database-specific rectangles
scrolloffsets, axes, viewport math, cursor following, scrollbar modelsproduct content construction
stylesemantic roles, Theme, builders, Tailrocks phosphor presetglobal product constants or capsule/status meanings
widgetsborrowed render models and explicit stateful widgetsI/O, effects, domain records, global process state
osctyped hyperlink/pointer/clipboard request values and pure OSC 8/22/52 encodersemission timing, stdout writes, clipboard policy, URL-opening decisions
runtimeexecutor-neutral Dirty/UpdateResult/Subscription/Component/View contracts plus the drive_frame/drive_render frame drivers; base module optional by useTokio, spawning, effect execution, panic/signal policy, any requirement that widgets use it
crosstermoptional event adapters, backend convenience, and scoped session guardwidgets, 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:

  1. Rendering: standard Ratatui Widget/StatefulWidget implementations.
  2. Interaction: component-specific handle_action methods over logical actions, returning a small outcome such as Ignored, Changed, Submitted(Id), or Cancelled.
  3. 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_thumb

Do 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 Drop only as an idempotent best-effort fallback because Drop cannot 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 areaRequired actionTarget
theme.rs and root RGB constantsreplace product constants with private semantic theme + phosphor presetstyle
geometry.rssplit display/text/tab math from agent_display_nametext, layout; agent mapping stays local
jackin_core::shorten_home re-exportremove from the donor facade; retain the environment-aware helper in jackin-core for TUI and non-TUI callersjackin-core
keymap.rsretain logical chord/action mapping; move action labels to consumer datainput
scroll.rs, scrollable-panel mathconsolidate one scroll model and one display-width source, retaining the tui-scrollbar metrics dependency as the single thumb-math sourcescroll, widgets
runtime.rsextract 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 helpersextract as typed requests plus pure encoders; emission stays consumer-ownedosc
lookbook SVG color tablederive from the shared theme values; assert committed SVGs unchangedtermrock-lookbook
terminal_modes.rsrefactor into partial-init-safe optional Crossterm sessioncrossterm
ownership.rsdo not extract product-global terminal/diagnostics policyjackin❯
host_colors.rsretain locally until a second consumer needs OSC 10/11 discoveryconsumer adapter
ansi.rssplit pure parsing/sanitization from banners, version/help, and direct outputtext; product output stays local
animation.rs, output.rsdo not extract launch/container behavior or direct outputjackin❯
BrandHeaderkeep jackin❯ wordmark composition local; extract a neutral header bar only through separately scoped future demandconsumer first
ContainerInfo / DebugInforeplace generic row rendering with DetailTable; keep container/version builder localwidgets + consumer facade
confirm dialogextract generic state/widget; move exit wording and data-loss constructors localwidgets
modal rectanglesreplace role/auth/source/mount modes with min/preferred/max dialog specificationlayout
status footerredesign as data-driven left/right slots with stable IDs; keep usage/container/run policy localwidgets + consumer facade
select listseparate rows from state, use stable IDs, pluggable filtering, display-width measurement, configurable hintswidgets
text inputuse grapheme/display-column cursor rules, external validation, configurable actionswidgets
diff viewmake render/update boundary explicit; no hidden content mutation in a free render functionwidgets
URL and OSC overlaysreturn typed regions/requests through osc; consumer decides whether policy permits emissionosc 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-checks against 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 osc requests 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 crossterm render fixtures and component API inventories so enabling the feature cannot change or hide a widget;
  • use cargo package --list to 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.

On this page