slt
Skill199 starsChanged 15 days agoMITView on GitHub
Build Rust TUI apps with SuperLightTUI v0.26 (immediate-mode terminal UI). Use this skill when the user asks to create, modify, or debug terminal UI code in this repo, or asks "how do I X in SLT / TUI / terminal", or types Korean triggers like "터미널 UI", "TUI 만들어줘", "SLT로", "ratatui 대신". Read REFERENCES.md for feature flags and doc pointers; grep `src/context/` and `src/widgets/` before inventing any API.
- Commits and pushes
We split this skill into the rules it gives an agent
15 rules. About 0 are common, 2 are distinctive, and 13 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
13
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 52
≤3 positional args: When 4+ args appear, use an opts struct (<Widget>Opts) or a builder.
- Very specific: line 59
Stateful widgets take
&mut <Widget>State: Never &mut String, &mut Vec<T>, &mut usize. - Very specific: line 113
When a method belongs to two layers (ui.bordered(B) shortcut vs ui.container().border(B) explicit), prefer the explicit form in skill …
- Distinctive: line 117
When unsure about a widget signature, find its family and match.
- Very specific: line 186
Stop threading &theme, &tick, &mut toasts through every render fn.
- Very specific: line 262
Check examples/ for the closest pattern (see Reference Examples below).
- Very specific: line 264
Keep Cargo.toml features minimal — see REFERENCES.md.
- Distinctive: line 265
Run the quality gate before saying "done".
- Very specific: line 299
Verify gh release view, crates.io, docs.rs
- Very specific: line 306
Inventing method names: Always grep src/context/ and src/widgets/ first.
- Very specific: line 309
Building your own disabled / hover / context-menu machinery: Use ui.disabled(cond, |ui| ..), .hover_bg(..) / .focus_bg(..) / .active_bg(..) …
- Very specific: line 313
use_state()insideif/match/for: Use use_state_keyed(id, init) (&'static str or runtime String id). - Very specific: line 314
Deprecated 0.26 forms: Do not write button_with / button_colored, badge_colored, stat_colored, separator_colored, progress_bar_colored, …
- Very specific: line 317
'staticon deferred raw draw: Use draw_with for owned data, or draw_precomputed when the closure must borrow local data and fixed source … - Very specific: line 328
Only Cargo-listed targets are runnable (cargo run --example <name>.
≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
SuperLightTUI (SLT) Authoring Skill
Targets SLT v0.26.0. Verify every API against src/ before using it.
Two runtime rules agents get wrong
- The mouse is on by default; hover is opt-in. Since v0.26,
RunConfig::default()leavesmouse_mode: None, which resolves toMouseMode::Draglocally andMouseMode::Clickover SSH/mosh (src/runtime/config.rs). Plainslt::run(...)therefore deliversResponse.clicked,right_clicked,double_clicked,scroll_delta, scrollbar and split-pane drags, and the built-in text selection. Pointer moves without a button held are only reported inMouseMode::Hover, so any app that relies onhovered,group_hover_*,on_hoverorui.tooltipmust start with<!-- slt-check: {"name":"skill_hover_mode","features":["crossterm"],"context":"module"} -->fn main() -> std::io::Result<()> { slt::run_with(slt::RunConfig::default().mouse_mode(slt::MouseMode::Hover), |ui| { ui.text("hover me"); }) }MouseMode::Off(or.mouse(false)) disables the mouse and keeps the terminal's native selection;.mouse(true)is a shorthand forHover. HeadlessTestBackendtests inject mouse events directly regardless of the mode. - Focused text fields own their keys. While
text_input/textarea/ a form field / an open command palette has focus,ui.key('q'),ui.key_code(..),ui.key_mod(..)andui.shortcut(..)skip the keys it edits with (printable keys without Ctrl/Super/Meta, Backspace, Delete, arrows, Home, End; Enter/Up/Down fortextarea; also Tab, Shift+Tab, PageUp/PageDown forcode_editor), in any call order. Chords, Esc, Tab (outsidecode_editor) and F-keys still reach shortcuts;ui.raw_key(c)sees every press.<!-- slt-check: {"name":"skill_shortcut_before_input","features":[],"context":"ui","setup":"let mut search = TextInputState::new();"} -->if ui.shortcut(KeyCode::Char('q'), KeyModifiers::NONE) { ui.quit(); } // safe before the input ui.text_input(&mut search); // typing 'q' inserts it
Mental model
SLT is immediate-mode. Your app is one closure: slt::run(|ui: &mut Context| { ... }). The closure runs every frame. State lives in plain Rust variables outside the closure — no App trait, no Model/View/Update, no retained tree. SLT handles flexbox layout, ANSI diff, and stdout flush.
Response.rect reflects the previous frame because layout runs after the closure returns. Frame 1 returns a zero Rect. Guard measurement-dependent logic with if ui.tick() > 0 { ... }. See docs/guide/02-state.md#the-previous-frame-rule.
For larger apps write components as functions: fn render_card(ui: &mut Context, data: &Card). Share read-mostly state with ui.provide(value, |ui| ...) + ui.use_context::<T>() instead of threading &theme through every helper.
Imports: use slt::prelude::*; is the one-line app import (run entry points, Context, Response, Button / ButtonVariant, WidgetColors, State / StateHandle, style types, KeyCode / KeyModifiers, common widget states). It omits the crossterm re-export; import anything else by path.
The API rules (predictability anchors)
These are non-negotiable since v0.20. When generating new code, every public widget must match all of them. Known exceptions are listed in docs/internals/design-principles.md#api-review-rules; do not copy them into new APIs.
- Builder for optional config. Methods on
Contextreturn a builder when ≥1 option exists. Builders are consuming (mut self -> Self), render onDrop, and expose.show()(consumes the builder) to capture a*Response.<!-- slt-check: {"name":"skill_builder_rule","features":[],"context":"ui","setup":"let segs = [\"home\", \"docs\"];"} -->
Removed in v0.20:ui.gauge(0.6).label("60%").width(24).color(Color::Cyan); // good let r = ui.breadcrumb(&segs).separator(" › ").show(); // capture response if let Some(i) = r.clicked_segment { ui.text(format!("jump to {i}")); }gauge_w,gauge_colored,line_gauge_with,breadcrumb_sep,LineGaugeOpts,HighlightRange::single,label_owned. Do not write these — AI training data may suggest them. - Floats are
f64. Public surface never takes/returnsf32.0.5isf64natively, soui.gauge(0.5)just works. - ≤3 positional args. When 4+ args appear, use an opts struct (
<Widget>Opts) or a builder.<!-- slt-check: {"name":"skill_opts_struct","features":[],"context":"ui","setup":"let (total, viewport) = (100, 20); let mut scroll = ScrollState::new();"} -->let opts = GutterOpts::line_numbers(total, viewport); // Opts struct let _ = ui.scrollable_with_gutter(&mut scroll, opts, |ui, abs| { ui.text(format!("line {abs}")); }); - Stateful widgets take
&mut <Widget>State. Never&mut String,&mut Vec<T>,&mut usize. Trivial-value exceptions:slider(&mut f64),checkbox(&mut bool),toggle(&mut bool).<!-- slt-check: {"name":"skill_state_rule","features":[],"context":"ui","setup":"let mut search = TextInputState::new(); let mut tabs = TabsState::new(vec![\"Files\", \"Settings\"]);"} -->ui.text_input(&mut search); // a TextInputState, not &mut String ui.tabs(&mut tabs); // a TabsState, not &mut usize - Responses. Single-rect widgets return
Response. Compound widgets return<Widget>Response: Deref<Target = Response>with#[must_use]. Never tuples.<!-- slt-check: {"name":"skill_response_rule","features":[],"context":"ui","setup":"let segs = [\"home\", \"docs\"];"} -->if ui.button("Save").clicked { /* save */ } // Response if let Some(i) = ui.breadcrumb(&segs).show().clicked_segment { ui.text(format!("jump to {i}")); } // BreadcrumbResponse Return-type pattern. Methods on
Contextreturn one of three shapes — picking the wrong one is the most common AI-generated compile error.Text<'_>— scoped builder for one text element (v0.26). Returned bytext,link,styled,timer_display. Chain:bold,dim,italic,fg,bg,gradient,wrap,truncate,align,text_center,m,mx,w,h,grow,with_if,with;.show()returns itsResponse. Only text options follow, soui.text("x").bold().button("y")does not compile — start a new statement. TheContext-levelui.bold()/ui.fg(..)"last text" modifiers are deprecated.&mut Self—spacer,line_wrap,toast,form_field.Response— interaction result of an independently-rendered widget. Use for every stateful interactive widget (checkbox,toggle,table,tabs,select,radio,multi_select,text_input,list,tree,file_picker,calendar,command_palette,rich_log).- Response builder (v0.26) —
button→Button,badge→Badge,stat→Stat,help→Help,separator→Separator,progress/progress_bar→ProgressBar,slider→Slider,line_chart/area_chart→LineChart. Options chain (.variant(v),.colors(&c),.color(c),.step(s),.key_color(c)), the builder derefs toResponse,.show()returns an ownedResponse. Builder args cannot borrowui:let c = ui.theme().success;first. - Container helpers:
col/row/line/modal→Response.line_wrap→&mut Self.screen(name, &mut ScreenState, f)returns().
<!-- slt-check-skip: contrasts a deliberate compile error (Button has no .bold()) with the correct forms -->ui.button("Save").bold(); // ❌ Button has no .bold() — compile error if ui.button("Save").clicked { … } // ✅ Response field via Deref if ui.button("Delete").variant(ButtonVariant::Danger).colors(&c).clicked { … } // ✅ options compose let save = ui.button("Save").show(); // ✅ owned Response; `ui` is free again ui.text("Saved").bold().fg(green); // ✅ Text builder chain
Naming categories (naming micro tier)
Method names encode their category. When picking a name, match the category shape of nearby methods.
| Category | Shape | Examples |
| Verbs (actions, side effects) | <verb> or <verb>_<object> | quit, notify, register_focusable, focus_by_name, consume_event, set_ratio |
| Nouns (getters, no side effects) | <noun> or <noun>_<modifier> | theme, width, events, focused_name, state.cursor. Never get_X. |
| Adjectives (Layer 2 builder modifiers) | short, ≤2 syllables | bordered, bg, fg, p, m, w, h, gap, grow, fill, bold, dim |
| Constructors | Type::default() / Type::new(args) / Type::with_X(arg) | TextInputState::default(), SplitPaneState::new(0.5), TextInputState::with_placeholder("…") |
Allowed universal abbreviations: bg fg id idx len min max pos pct w h x y r g b a.
Forbidden: ctx btn lbl dbg cfg req res srv db in public API. Closure params over &mut Context are always ui, never ctx.
Layer model (5 layers, predictability anchor)
| Layer | Object | Examples | ||
| 1 — Context | &mut Context (the ui parameter) | ui.text(...), ui.button(...), ui.row(...), ui.theme(), ui.use_state(...) | ||
| 2 — ContainerBuilder | returned by ui.container(), ui.bordered(...) | chained adjectives: `.p(2).bg(c).gap(1).col( | ui | ...)` |
| 3 — Widget | impl Widget for MyType { type Response; fn ui(...) } | custom widget extension point | ||
| 4 — State | pub struct <Widget>State in src/widgets/*.rs | TextInputState, TableState, ScrollState | ||
| 5 — Response | Response or <Widget>Response: Deref<Response> | Response { clicked, right_clicked, double_clicked, hovered, changed, focused, gained_focus, lost_focus, submitted, scroll_delta, rect } |
When a method belongs to two layers (ui.bordered(B) shortcut vs ui.container().border(B) explicit), prefer the explicit form in skill output.
Sibling widget shapes (memorize)
When unsure about a widget signature, find its family and match.
<!-- slt-check-skip: signature cheat sheet; type names and `...` stand in for argument values -->// Stateful interactive widgets — &mut <Widget>State, returns Response
ui.list(&mut ListState); ui.tabs(&mut TabsState);
ui.table(&mut TableState); ui.tree(&mut TreeState);
ui.select(&mut SelectState); ui.radio(&mut RadioState);
ui.multi_select(&mut MultiSelectState);
ui.file_picker(&mut FilePickerState); ui.calendar(&mut CalendarState);
ui.text_input(&mut TextInputState); ui.textarea(&mut TextareaState, rows); // textarea takes rows
ui.code_editor(&mut CodeEditorState).show(); // builder; highlight needs syntax-*
ui.rich_log(&mut RichLogState); ui.command_palette(&mut CommandPaletteState);
ui.toast(&mut ToastState); ui.spinner(&SpinnerState); // toast returns &mut Self; spinner is &, not &mut
// Trivial-value siblings — exception to Rule 4
let saved = ui.button("Save").clicked; // Button builder → Response
ui.checkbox("Done", &mut done); // Response
ui.toggle("Enabled", &mut on); // Response
ui.slider("Vol", &mut value, 0.0..=100.0).step(5.0); // Slider builder → Response
// Display builders — option methods instead of *_colored variants
ui.badge("beta").color(c); ui.stat("CPU", "42%").color(c); ui.separator().color(c);
ui.progress(0.4).color(c); ui.line_chart(&data, 40, 8).color(c);
ui.help(&[("q", "quit")]).key_color(c).text_color(dim);
ui.code_block(src).lang("rust").numbered(); // renders on Drop
ui.grid_columns(&[GridColumn::Fixed(8), GridColumn::Grow(1)], |ui| { ... });
// Builder widgets — chain optional config, render on Drop
ui.gauge(0.6).label("60%").width(24).color(Color::Cyan);
ui.line_gauge(0.6).filled('━').empty('─').width(24).label("60%");
let r = ui.breadcrumb(&segs).separator(" › ").color(Color::Cyan).show();
// Compound responses — Deref<Response> + extra fields
let r = ui.gauge(cpu).label("CPU").show(); // GaugeResponse { ratio: f64 }
let r = ui.breadcrumb(&segs).show(); // BreadcrumbResponse { clicked_segment: Option<usize> }
let r = ui.split_pane(&mut split, l, r); // SplitPaneResponse { ratio: f64 }
let r = ui.scrollable_with_gutter(&mut scroll, opts, body); // GutterResponse { current_highlight: Option<usize> }
let r = ui.form_field_response(&mut field); // FormFieldResponse { response, input, layout_rect } (v0.25)Hook ordering — three variants
Hooks must be called in the same order every frame unless they are id-keyed.
| Hook | Key | Safe in if/match? | Use when |
ui.use_state(|| init) | call order | No | Top-level state, no conditional placement |
ui.use_state_keyed("id", || init) | &'static str or String | Yes | Conditional/branching state, or per-row state with a key from data (format!("row-{i}")) |
ui.use_state_keyed_default::<T>("id") | &'static str or String | Yes | Same, using T::default() |
ui.use_memo(&deps, |d| compute(d)) | call order + deps | No | Cached compute, deps change → recompute |
ui.use_effect(|d| { ... }, &deps) | call order + deps | No | Side effect on deps change |
<!-- slt-check-skip: WRONG/RIGHT contrast; the first line is the hook-order anti-pattern -->// WRONG — order-based hook in conditional drifts call order between frames
if expanded { let count = ui.use_state(|| 0); }
// RIGHT — id-keyed variant is safe inside conditionals
if expanded { let count = ui.use_state_keyed_default::<i32>("sidebar.count"); }
// Per-list-item runtime keys
for i in 0..items.len() {
let count = ui.use_state_keyed_default::<i32>(format!("counter-{i}"));
}Context injection (provide / use_context)
Stop threading &theme, &tick, &mut toasts through every render fn. provide injects a typed value scoped to a closure; nested code reads it back with use_context.
<!-- slt-check: {"name":"skill_provide_context","features":["crossterm"],"context":"module"} -->#[derive(Clone, Copy)]
struct AppCtx { theme: slt::Theme, tick: u64, user: &'static str }
fn main() -> std::io::Result<()> {
slt::run(|ui| {
let ctx = AppCtx { theme: *ui.theme(), tick: ui.tick(), user: "subin" };
ui.provide(ctx, |ui| {
render_card(ui);
});
})
}
fn render_card(ui: &mut slt::Context) {
let ctx = *ui.use_context::<AppCtx>(); // panics if missing; copy before the next `ui` call
// let maybe = ui.try_use_context::<AppCtx>(); // returns Option<&T>
ui.text(format!("hi {} (tick {})", ctx.user, ctx.tick));
}Reserve explicit parameters for writes (&mut MyDocState). Bound is T: 'static — use &'static str for literals, String for runtime values.
Conditional styling (with_if / with)
with_if(cond, modifier) and with(modifier) collapse conditional branches into a single chain. Available on Text and ContainerBuilder. Beware: the Text closure receives a builder for the same element and returns (), ContainerBuilder's closure maps Self -> Self.
<!-- slt-check: {"name":"skill_with_if","features":[],"context":"ui","setup":"let (is_error, is_focused) = (true, false); let theme = *ui.theme();"} -->// Text — closure receives a Text for the same element
ui.text("Status").with_if(is_error, |t| { t.bold().fg(Color::Red); });
// ContainerBuilder — closure receives Self by value
ui.container().with_if(is_focused, |c| c.bg(theme.surface_hover)).col(|ui| { ui.text("body"); });Custom widget pattern (Layer 3)
When to use which pattern:
- Function (
fn render_card(ui: &mut Context, data: &CardData)): 90% of cases. Use for screens, sections, reusable layouts. Cheaper to write, no trait bounds, easier to test. Built-in widgets follow this shape internally (impl Contextdirect methods). impl Widget: when the component (a) has its own state struct that the caller owns, and (b) you wantui.widget(&mut w)ergonomics matching built-ins. Required for third-party crates that export widgets through trait-bound APIs.
<!-- slt-check: {"name":"skill_custom_widget","features":[],"context":"module"} -->struct Label<'a> { text: &'a str }
impl<'a> slt::Widget for Label<'a> {
type Response = slt::Response;
fn ui(&mut self, ui: &mut slt::Context) -> Self::Response {
ui.register_focusable();
ui.text(self.text).bold();
slt::Response::default()
}
}
fn render(ui: &mut slt::Context) {
let mut label = Label { text: "hello" };
ui.widget(&mut label); // or call `label.ui(ui)` with `Widget` in scope
}For mouse hit-testing call ui.interaction() (no arguments) immediately before the element that should own the hit rect; it returns a Response. For keyboard, let focused = ui.register_focusable();, iterate ui.key_presses_when(focused) (yields (index, &KeyEvent)), and mark handled keys with ui.consume_event(index) so later widgets and shortcuts ignore them.
Focus, scrolling and geometry (v0.24 / v0.25)
- Scroll containers follow focus.
ui.scrollable(&mut scroll)reveals the newly focused child (Tab into an off-screen field scrolls it into view).ScrollState::follow_focusis a publicbool, defaulttrue; setscroll.follow_focus = falsefor application-controlled scrolling. - Forms with geometry.
ui.form_field(&mut field)stays chainable (&mut Self).ui.form_field_response(&mut field) -> FormFieldResponsereturnsresponse(field-level flags, also viaDeref),input(the input's own response) andlayout_rect: Option<Rect>(previous-frame full border box). - Full vs clipped rects.
Response.rectis the clipped hit rect.ui.focused_layout_rect()andui.measured_layout_rect("name")return the previous frame's full layout box (before scroll offset and clipping),Noneon frame 1 or after resize. - Headless time.
TestBackendticks advance per frame; usetb.advance_time(Duration::ZERO)for a fixed clock.has_pending_input()reports input queued behind a Tab group (custom loops onAppStatetoo). - Bounded canvas.
ui.try_canvas(w, h, |cv| ..) -> Result<Response, CanvasError>;ui.canvas_with(&mut canvas, |cv| ..)reuses storage across frames.
Authoring workflow
- Confirm the goal — what app? Data table? Dashboard? Form? Game?
- Check
examples/for the closest pattern (see Reference Examples below). Start from that file if it fits. - Grep
src/context/widgets/andsrc/widgets/for the actual signature before writingui.foo(...). Do NOT invent APIs. - Keep
Cargo.tomlfeatures minimal — see REFERENCES.md. - Run the quality gate before saying "done".
Quality gate (mandatory before saying "done")
Core — every commit:
cargo fmt -- --check
cargo check --all-features
cargo clippy --all-features -- -D warnings
cargo test --all-features
cargo check --examples --all-featuresExtended — before PR or release:
typos
bash scripts/api_audit.sh --strict
cargo check -p superlighttui --no-default-features
cargo check -p slt-wasm --target wasm32-unknown-unknown
cargo hack check -p superlighttui --each-feature --no-dev-deps
cargo audit
cargo deny checkRelease workflow (mandatory — do not skip any step)
AGENTS.md has the full 8-step checklist. Short version:
- Local PRE-CI (Core + Extended both green)
- Bump
Cargo.toml, updateCHANGELOG.md - Branch
release/vX.Y.Z, single atomic commit, push gh pr create, wait for CI green- Merge (squash), pull main
- Tag, push tag, wait for
release.ymlgreen - Verify
gh release view, crates.io, docs.rs - Only now announce
Red flags that mean STOP: "Probably fine", "Just a docs change", "CI will catch it", "I'll tag now and fix later". Run the gate locally first.
Common pitfalls (AI-generated SLT code)
- Inventing method names. Always grep
src/context/andsrc/widgets/first. - Expecting hover without
MouseMode::Hover. Clicks, wheel and drags work by default, but plain pointer moves are not reported; see "Two runtime rules" above. Do not add.mouse(true)just to "turn the mouse on". - Shortcut checked before a text input renders. Typing
qquits the app. Render inputs first, then checkui.key(...). - Building your own disabled / hover / context-menu machinery. Use
ui.disabled(cond, |ui| ..),.hover_bg(..)/.focus_bg(..)/.active_bg(..)on containers,ui.popover(..),ui.context_menu(..)andui.menu_bar(..).ui.modaltraps focus since 0.26 (ModalOpts::new().tab_trap(false)opts out). - Calling
ui.interaction(rect)oravailable_key_presses().interaction()takes no arguments;available_key_pressesis crate-private. Usekey_presses_when(focused)+consume_event(i). - Stale removed APIs.
gauge_w,gauge_colored,line_gauge_with,breadcrumb_sep,LineGaugeOpts,HighlightRange::single,label_ownedare GONE in v0.20. Use the builder forms. Response.recton frame 1. ZeroRect. Guard withui.tick() > 0.use_state()insideif/match/for. Useuse_state_keyed(id, init)(&'static stror runtimeStringid).use_state_namedis deprecated (same slot).- Deprecated 0.26 forms. Do not write
button_with/button_colored,badge_colored,stat_colored,separator_colored,progress_bar_colored,line_chart_colored,help_colored,slider_with,grid_with,use_state_named; use the builder methods above. Removed outright:.pad,.min_width-style container setters,ui.row_gap(n, f)/ui.col_gap(n, f)(usecontainer().gap(n)),key_seq,code_block_lang/code_block_numbered*, f32 color methods. Seedocs/reference/migration.md(v0.25 → v0.26). - Holding a builder across
uicalls.let r = ui.button("x"); ui.text(".."); r.clickeddoes not compile; writeui.button("x").show(). - Forgetting
.show()on builders that return a response. Drop renders and discards the response. Capture withlet r = ui.gauge(...).show();. 'staticon deferred raw draw. Usedraw_withfor owned data, ordraw_precomputedwhen the closure must borrow local data and fixed source dimensions are acceptable.- Mixing crossterm raw events with
ui.*helpers. Preferui.key(),ui.key_code(),ui.key_mod(). For modal-aware shortcuts useui.raw_key_*. - Hard-coding
Color::Rgb(...)instead ofui.theme()— themes can't swap. RichLogState::new()for unbounded. New caps at 10000; useRichLogState::new_unbounded()if you really want unlimited.- First-frame hover/click tests. Render once to warm the prev-frame hit map, then send the event in a second
tb.render(...)call. - Binding only Ctrl-C as quit. macOS terminals intercept Ctrl-C as Copy. Always pair
q,Esc, andCtrl-Q. unsafeblocks.#![forbid(unsafe_code)]. Hard compile error.- Printing to stdout/stderr from a widget. A library must not write to stdout. Lints catch this.
Reference examples (skill should reference these by file:line)
Only Cargo-listed targets are runnable (cargo run --example <name>; 29 targets, auto-discovery is off). Every other file under examples/ is compiled as a module of a tour target or test (#[path = ...] mod), so all of them are type-checked by cargo check --all-targets --all-features; tests/docs_contract.rs enforces that no example source is left uncompiled.
| Domain | Reference file | Compiled via | Key shape |
| Hello / minimal | examples/hello.rs (21 lines) | target hello | slt::run, bordered.title.col, quit triple |
| Counter (state in closure) | examples/counter.rs | target counter | move-closure state pattern |
| Inline mode | examples/inline.rs (23 lines) | target inline | run_inline(rows, ...) |
| Tabbed tour | examples/cookbook_tour.rs | target cookbook_tour | TabsState + child pub fn render(ui, &mut DemoState) |
| Form / validators | examples/cookbook_login.rs | cookbook_tour | TextInputState::with_placeholder, masked password, validation |
| Searchable+sortable table | examples/cookbook_table.rs | cookbook_tour | TableState::set_filter, toggle_sort, consume_key |
| Modal + Toast | examples/cookbook_modal_toast.rs | cookbook_tour | ButtonVariant::Danger, raw_key_code(Esc) for modal-aware quit |
| Modal focus trap | examples/modal_trap.rs | widgets_tour | ModalOpts::new() |
| Menus, popovers, disabled, state styles | examples/menus.rs | menus | menu_bar, context_menu, popover, disabled, hover_bg |
| File picker | examples/cookbook_file_picker.rs | cookbook_tour | FilePickerState::selected_file() |
| Dashboard (chart+sparkline) | examples/cookbook_dashboard.rs | cookbook_tour | ui.chart(|c| c.line(...).color(..), w, h), rolling VecDeque<f64> |
| Animation primitives | examples/canvas_tour.rs (Anim tab) | target canvas_tour | Tween/Spring/Keyframes/Sequence/Stagger; standalone version examples/anim.rs (target anim) |
use_state_keyed | examples/use_state_keyed.rs | widgets_tour | per-row counter via format!("counter-{i}") |
use_effect | examples/use_effect.rs | widgets_tour | three dep shapes (&(), &i32, &bool) |
provide/use_context | tests/context_provider.rs, examples/demo_website.rs:139-152 | test / showcase_tour | injection + try_use_context |
| Theme subtree | examples/theme_subtree.rs | widgets_tour | container().theme(theme) per-subtree override |
| Theme density | examples/spacing_scale.rs | widgets_tour | Theme::compact() / comfortable() / spacious() |
| Gauge builder | examples/gauge.rs:104-107 | widgets_tour | ui.gauge(value).label(...).width(24) |
| Line gauge | examples/gauge.rs:74-92 | widgets_tour | ui.line_gauge(0.45).filled('#').empty('.').width(24) |
| Breadcrumb response | examples/breadcrumb_response.rs:72-77 | widgets_tour | ui.breadcrumb(&segs).separator(" › ").show() |
| Scrollable + gutter | examples/gutter_highlights.rs:150-165 | widgets_tour | GutterOpts::line_numbers, HighlightRange::line |
| Split pane | examples/split_pane.rs | widgets_tour | split_pane, vsplit_pane, drag handle |
| WidthSpec variants | examples/widthspec.rs | widgets_tour | Constraints::default().w_pct(50), .w_ratio(1,3), .w_minmax(10,30) |
| Named focus | examples/named_focus.rs:187 | widgets_tour | register_focusable_named + focus_by_name |
| Keymap help overlay | examples/keymap_help.rs | widgets_tour | WidgetKeyHelp, publish_keymap, keymap_help_overlay(open) |
| DX shortcuts | examples/dx_shortcuts.rs | widgets_tour | on_hover, animate_bool, fill(), Rect::center_in |
| Static log / scrollback | examples/static_log.rs | widgets_tour | slt::run_static, ui.static_log(...) |
| Async | examples/async_demo.rs | target async_demo (needs --features async) | slt::run_async with tokio, .sender(), .join() |
| All-in-one tour | examples/widgets_tour.rs | target widgets_tour | tabs over every single-feature demo; examples/feature_showcase.rs (target feature_showcase) composes them on one screen |
| Recent widgets | examples/widgets_extra.rs, examples/interaction_tour.rs | targets widgets_extra, interaction_tour | paginator / number_input / scheduler / inspector; Response interaction signals, KeyMap, focus traversal |
| Test utilities | tests/example_snapshots/testing_report.rs | test | record_frames, sequence().tick(..).key(..).type_string(..) |
Testing pattern (headless)
<!-- slt-check: {"name":"skill_testing_pattern","features":[],"context":"module"} -->use slt::{Context, EventBuilder, KeyCode, TestBackend, TextInputState};
#[test]
fn my_widget_renders() {
let mut tb = TestBackend::new(80, 24);
tb.render(|ui| { ui.text("hello"); });
tb.assert_contains("hello");
}
// Multi-frame with events (warm prev-frame hit map first)
#[test]
fn click_triggers() {
let mut tb = TestBackend::new(40, 10);
tb.render(|ui| { let _ = ui.button("Save"); }); // warm frame
tb.run_with_events(EventBuilder::new().click(2, 0).build(), // event frame
|ui| { if ui.button("Save").clicked { /* assert */ } });
}
// Multi-step (default, v0.26): one FnMut over plain &mut state, no RefCell.
// step/press/type_text/run_with_events all keep focus between frames.
#[test]
fn tab_then_type() {
let mut tb = TestBackend::new(30, 4);
let (mut a, mut b) = (TextInputState::new(), TextInputState::new());
let mut app = |ui: &mut Context| { ui.text_input(&mut a); ui.text_input(&mut b); };
tb.step(vec![], &mut app)
.press(KeyCode::Tab, &mut app)
.type_text("x", &mut app);
assert_eq!(b.value, "x"); // after the closure's last use
}
// Hover: EventBuilder::new().hover(x, y). Force a focus slot:
// tb.render_with_events(events, 0, 0, |ui| ..).See tests/example_snapshots/testing_report.rs for record_frames, assert_not_contains, assert_style_at.
File layout cheat sheet
| Area | Primary files |
| Public API | src/lib.rs (re-exports) |
| Run loop / backend | src/terminal.rs, src/lib.rs (run, run_with, run_inline, run_async, run_static, frame, frame_owned) |
| Context core | src/context/{core,runtime,container,helpers,state}.rs |
| Widget impls | src/context/widgets/<family>.rs (+ <family>/), same family names as the state files |
| Layer 4 state types | src/widgets/<family>.rs |
| Compound responses | next to their family's state: BreadcrumbResponse (display.rs), GaugeResponse (feedback.rs), SplitPaneResponse / GutterResponse (layout.rs), FormFieldResponse (text_input.rs) |
| Layout kernels | src/layout/{tree,flexbox,collect,render,command}.rs |
| Style / theme | src/style/{color,theme}.rs, src/style.rs |
| Animation | src/anim.rs |
| Charts | src/chart.rs, src/chart/*.rs, src/context/widgets/viz/*.rs |
| Testing helpers | src/test_utils.rs |
| Skill references | REFERENCES.md (feature flags, doc pointers) |
Korean conventions
- "ㄱㄱ" = "go go" → proceed immediately, no clarifying questions
- "켜줘" / "열어줘" → open the file in Cursor (NOT
catto terminal) - "고쳐줘" / "수정해줘" → fix with minimal change, run quality gate
- "리뷰해줘" → audit only, do not modify code unless explicitly asked
In this repository5 other files it gives its agents.
- AGENTS.mdAGENTS.md
- docs/llms.txtllms.txt
- slt-migrationSkill
- sltSkill
- slt-migrationSkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

