tui-ui-guidelines
GitHub提供Warp项目无头TUI前端开发的代码规范,明确区分TUI与GUI的实现差异,指导基于cell-grid的TuiElement库进行界面构建、布局测量及事件处理。
Trigger Scenarios
Install
npx skills add warpdotdev/warp --skill tui-ui-guidelines -g -y
SKILL.md
Frontmatter
{
"name": "tui-ui-guidelines",
"description": "Guidelines for writing Warp headless TUI (crates\/warp_tui) UI code with the cell-grid TuiElement library. Read before any TUI UI work."
}
tui-ui-guidelines
Guidelines for writing UI code in Warp's headless TUI front-end. This is the TUI counterpart to gui-ui-guidelines (which covers the pixel-based GUI desktop app). Read this once at the start of any TUI UI task, then keep it in mind while implementing.
The TUI is a distinct front-end from the GUI desktop app. Do not carry over GUI assumptions (pixels, mouse-pixel hit-testing, GPU/WGSL, .app bundles, design-system button pixel themes, launch modals). If a GUI guideline is about pixel layout or GPU rendering, it does not apply here.
Where TUI UI code lives
- Front-end views/screens:
crates/warp_tui— per-channel console binaries (e.g.crates/warp_tui/src/bin). Run/observe the TUI with./script/run-tui. There is no.appbundle, no GPU/WGSL, and no mouse-pixel model. - Element library:
crates/warpui_core/src/elements/tui, behind thetuicargo feature. This is a parallel cell-grid element vocabulary, separate from the GUIElement/Viewlibrary.
Shared with the GUI (do reuse): the Entity/model core in warp_core/warpui — App/Entity/AppContext/ViewContext, the actions system, Appearance/theming, FeatureFlag runtime checks (FeatureFlag::X.is_enabled() works in both front-ends), telemetry, and logging.
Different from the GUI (do NOT use here): the GUI Element/View types, pixel geometry, and GPU/WGSL rendering or pixel-drawn button themes. The TUI has its own crates/warp_tui/Cargo.toml; the compile-time Cargo-feature bridge in app/Cargo.toml + app/src/lib.rs enabled_features() is GUI-app-specific and does not gate TUI code. (The TUI does have hover/click: TuiHoverable and tui_collapsible reuse the shared MouseStateHandle, so own that handle outside render just like the GUI — only pixel-based hit-testing is GUI-only.)
The TuiElement trait
Defined in crates/warpui_core/src/elements/tui/mod.rs. An element measures itself, then paints into a sub-rectangle of a cell buffer:
layout(&mut self, constraint: TuiConstraint, ctx: &mut TuiLayoutContext, app: &AppContext) -> TuiSize— measure against a constraint and return a size within it.appgives shared read access to the core (mirrors the GUI'sElement::layout).render(&self, area: TuiRect, buffer: &mut TuiBuffer, ctx: &mut TuiPaintContext)— paint intoareaofbuffer.area's size is whatlayoutreturned, clamped to what was available.cursor_position(&self, area, ctx) -> Option<(u16, u16)>— where the terminal cursor should sit withinarea, if this element owns it (default:None).present(&mut self, ctx)— participate in the child-view recursion so the presenter records parent/child view relationships (default: nothing; only container/child-view elements override it).dispatch_event(&mut self, event, area, event_ctx, ctx, app) -> bool— offer an event to this element, returning whether it was handled (default:false)..finish()— boxing convenience that returnsBox<dyn TuiElement>, mirroring the GUIElement::finish. Always terminate an element with.finish(); never hand-wrap an element inBox::new. It's what the child-taking APIs (TuiFlex::child/with_child,TuiChildView, etc.) expect, and it keeps element trees consistent and readable.
Composition vocabulary
Re-exported from crates/warpui_core/src/elements/tui/mod.rs:
- Layout containers:
TuiFlex(TuiFlex::row()/TuiFlex::column(), with.child(...),.flex_child(...),.with_cross_axis_alignment(...)),TuiContainer, andTuiConstrainedBox(e.g..with_max_cols(N)). - Content:
TuiText(.with_style(style),.truncate(),TuiText::from_spans([...])). - View/embedding:
TuiChildViewfor embedding another view's rendered element;TuiEventHandler(e.g..on_key("x", |_, _, _| ...)) to attach handlers to a subtree. - Multi-child trait:
TuiParentElementprovideswith_child/with_children/add_child/add_children. - Geometry (integer cells):
TuiSize,TuiRect,TuiConstraint(TuiConstraint::loose(size)/TuiConstraint::tight(size);TuiConstraint::clamp). AlsoTuiPoint.
Styling
Styles are TuiStyle values (Color, Modifier — e.g. Modifier::BOLD, Modifier::DIM) painted into a TuiBuffer of Cells. Terminal cells have no alpha, so styles are solid.
Prefer the semantic style helpers on TuiUiBuilder (crates/warp_tui/src/tui_builder.rs) over hardcoding colors — this mirrors the GUI guideline about reusing themes. Construct it per render with TuiUiBuilder::from_app(app), then ask for semantic styles: primary_text_style(), muted_text_style(), dim_text_style(), error_text_style(), success_glyph_style(), accent_border_style(), input_text_style(), etc. The builder owns the theme→style recipes so views ask for "primary text" / "muted text" instead of deriving colors from the theme by hand. Do not reach for raw ANSI slots (e.g. Color::White) directly — those are tuned for dark backgrounds and wash out on light themes.
Events and keybindings
Crossterm input is converted (in crate::runtime) to TuiEvent and dispatched through the element tree via dispatch_event; text-cursor placement flows through cursor_position.
Keybindings follow the GUI convention: each TUI view module exposes a top-level init(app) that registers its bindings, aggregated in crates/warp_tui/src/keybindings.rs and called once at TUI startup. Fixed/reserved bindings (e.g. ctrl-c) are tagged with the tui group (TUI_BINDING_GROUP); editable, user-remappable bindings are named with a tui: prefix. GUI bindings never fire in the TUI — predicate-scoped bindings never match TUI keymap contexts, and predicate-less ones dispatch action types no TUI view handles — and debug-time validators (register_binding_validators) enforce that any keystroke binding matching a TUI view's context is TUI-owned.
Example: composing a small element tree
A TuiFlex::column() of styled TuiText children, wrapped in a width cap (illustrative):
let builder = TuiUiBuilder::from_app(app);
let title_style = builder.accent_border_style().add_modifier(Modifier::BOLD);
let muted = builder.muted_text_style();
let column = TuiFlex::column()
.child(
TuiText::new("Warp Agent")
.with_style(title_style)
.truncate()
.finish(),
)
.child(TuiText::new(version).with_style(muted).truncate().finish());
TuiConstrainedBox::new(column.finish())
.with_max_cols(48)
.finish()
Verify API names against the element library (crates/warpui_core/src/elements/tui/mod.rs) and TuiUiBuilder (crates/warp_tui/src/tui_builder.rs); don't invent methods. Don't treat existing crates/warp_tui view code as canonical examples — much of it is early prototyping and isn't the pattern to copy going forward.
Reference
- Run/observe the TUI with
./script/run-tui. - Verify a TUI change by building and running it (
./script/run-tui) and observing the output in an interactive terminal; thetui-verify-changeskill covers this end to end. - Write and run TUI tests with the
tui-testingskill.
Version History
- 726c1b6 Current 2026-07-24 20:21


