본문으로 건너뛰기
cccskills
무료GitHub에 공개

slt

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.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

연동 도구GitHub
설치 방법 보기

포함된 파일(2)

  • SKILL.md30.6 KB
  • REFERENCES.md13.2 KB

SKILL.md(원문)

설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.

SuperLightTUI (SLT) Authoring Skill

Targets SLT v0.26.0. Verify every API against src/ before using it.

Two runtime rules agents get wrong

  1. The mouse is on by default; hover is opt-in. Since v0.26, RunConfig::default() leaves mouse_mode: None, which resolves to MouseMode::Drag locally and MouseMode::Click over SSH/mosh (src/runtime/config.rs). Plain slt::run(...) therefore delivers Response.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 in MouseMode::Hover, so any app that relies on hovered, group_hover_*, on_hover or ui.tooltip must 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 for Hover. Headless TestBackend tests inject mouse events directly regardless of the mode.
  2. 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(..) and ui.shortcut(..) skip the keys it edits with (printable keys without Ctrl/Super/Meta, Backspace, Delete, arrows, Home, End; Enter/Up/Down for textarea; also Tab, Shift+Tab, PageUp/PageDown for code_editor), in any call order. Chords, Esc, Tab (outside code_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.

  1. Builder for optional config. Methods on Context return a builder when ≥1 option exists. Builders are consuming (mut self -> Self), render on Drop, and expose .show() (consumes the builder) to capture a *Response.

    <!-- slt-check: {"name":"skill_builder_rule","features":[],"context":"ui","setup":"let segs = [\"home\", \"docs\"];"} -->
    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}")); }
    

    Removed in v0.20: gauge_w, gauge_colored, line_gauge_with, breadcrumb_sep, LineGaugeOpts, HighlightRange::single, label_owned. Do not write these — AI training data may suggest them.

  2. Floats are f64. Public surface never takes/returns f32. 0.5 is f64 natively, so ui.gauge(0.5) just works.

  3. ≤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}")); });
    
  4. 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
    
  5. 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
    
  6. Return-type pattern. Methods on Context return 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 by text, 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 its Response. Only text options follow, so ui.text("x").bold().button("y") does not compile — start a new statement. The Context-level ui.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 to Response, .show() returns an owned Response. Builder args cannot borrow ui: 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.

CategoryShapeExamples
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 syllablesbordered, bg, fg, p, m, w, h, gap, grow, fill, bold, dim
ConstructorsType::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)

LayerObjectExamples
1 — Context&mut Context (the ui parameter)ui.text(...), ui.button(...), ui.row(...), ui.theme(), ui.use_state(...)
2 — ContainerBuilderreturned by ui.container(), ui.bordered(...)chained adjectives: `.p(2).bg(c).gap(1).col(
3 — Widgetimpl Widget for MyType { type Response; fn ui(...) }custom widget extension point
4 — Statepub struct <Widget>State in src/widgets/*.rsTextInputState, TableState, ScrollState
5 — ResponseResponse 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.

HookKeySafe in if/match?Use when
ui.use_state(|| init)call orderNoTop-level state, no conditional placement
ui.use_state_keyed("id", || init)&'static str or StringYesConditional/branching state, or per-row state with a key from data (format!("row-{i}"))
ui.use_state_keyed_default::<T>("id")&'static str or StringYesSame, using T::default()
ui.use_memo(&deps, |d| compute(d))call order + depsNoCached compute, deps change → recompute
ui.use_effect(|d| { ... }, &deps)call order + depsNoSide 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 Context direct methods).
  • impl Widget: when the component (a) has its own state struct that the caller owns, and (b) you want ui.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_focus is a public bool, default true; set scroll.follow_focus = false for application-controlled scrolling.
  • Forms with geometry. ui.form_field(&mut field) stays chainable (&mut Self). ui.form_field_response(&mut field) -> FormFieldResponse returns response (field-level flags, also via Deref), input (the input's own response) and layout_rect: Option<Rect> (previous-frame full border box).
  • Full vs clipped rects. Response.rect is the clipped hit rect. ui.focused_layout_rect() and ui.measured_layout_rect("name") return the previous frame's full layout box (before scroll offset and clipping), None on frame 1 or after resize.
  • Headless time. TestBackend ticks advance per frame; use tb.advance_time(Duration::ZERO) for a fixed clock. has_pending_input() reports input queued behind a Tab group (custom loops on AppState too).
  • Bounded canvas. ui.try_canvas(w, h, |cv| ..) -> Result<Response, CanvasError>; ui.canvas_with(&mut canvas, |cv| ..) reuses storage across frames.

Authoring workflow

  1. Confirm the goal — what app? Data table? Dashboard? Form? Game?
  2. Check examples/ for the closest pattern (see Reference Examples below). Start from that file if it fits.
  3. Grep src/context/widgets/ and src/widgets/ for the actual signature before writing ui.foo(...). Do NOT invent APIs.
  4. Keep Cargo.toml features minimal — see REFERENCES.md.
  5. 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-features

Extended — 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 check

Release workflow (mandatory — do not skip any step)

AGENTS.md has the full 8-step checklist. Short version:

  1. Local PRE-CI (Core + Extended both green)
  2. Bump Cargo.toml, update CHANGELOG.md
  3. Branch release/vX.Y.Z, single atomic commit, push
  4. gh pr create, wait for CI green
  5. Merge (squash), pull main
  6. Tag, push tag, wait for release.yml green
  7. Verify gh release view, crates.io, docs.rs
  8. 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/ and src/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 q quits the app. Render inputs first, then check ui.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(..) and ui.menu_bar(..). ui.modal traps focus since 0.26 (ModalOpts::new().tab_trap(false) opts out).
  • Calling ui.interaction(rect) or available_key_presses(). interaction() takes no arguments; available_key_presses is crate-private. Use key_presses_when(focused) + consume_event(i).
  • Stale removed APIs. gauge_w, gauge_colored, line_gauge_with, breadcrumb_sep, LineGaugeOpts, HighlightRange::single, label_owned are GONE in v0.20. Use the builder forms.
  • Response.rect on frame 1. Zero Rect. Guard with ui.tick() > 0.
  • use_state() inside if/match/for. Use use_state_keyed(id, init) (&'static str or runtime String id). use_state_named is 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) (use container().gap(n)), key_seq, code_block_lang / code_block_numbered*, f32 color methods. See docs/reference/migration.md (v0.25 → v0.26).
  • Holding a builder across ui calls. let r = ui.button("x"); ui.text(".."); r.clicked does not compile; write ui.button("x").show().
  • Forgetting .show() on builders that return a response. Drop renders and discards the response. Capture with let r = ui.gauge(...).show();.
  • 'static on deferred raw draw. Use draw_with for owned data, or draw_precomputed when the closure must borrow local data and fixed source dimensions are acceptable.
  • Mixing crossterm raw events with ui.* helpers. Prefer ui.key(), ui.key_code(), ui.key_mod(). For modal-aware shortcuts use ui.raw_key_*.
  • Hard-coding Color::Rgb(...) instead of ui.theme() — themes can't swap.
  • RichLogState::new() for unbounded. New caps at 10000; use RichLogState::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, and Ctrl-Q.
  • unsafe blocks. #![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.

DomainReference fileCompiled viaKey shape
Hello / minimalexamples/hello.rs (21 lines)target helloslt::run, bordered.title.col, quit triple
Counter (state in closure)examples/counter.rstarget countermove-closure state pattern
Inline modeexamples/inline.rs (23 lines)target inlinerun_inline(rows, ...)
Tabbed tourexamples/cookbook_tour.rstarget cookbook_tourTabsState + child pub fn render(ui, &mut DemoState)
Form / validatorsexamples/cookbook_login.rscookbook_tourTextInputState::with_placeholder, masked password, validation
Searchable+sortable tableexamples/cookbook_table.rscookbook_tourTableState::set_filter, toggle_sort, consume_key
Modal + Toastexamples/cookbook_modal_toast.rscookbook_tourButtonVariant::Danger, raw_key_code(Esc) for modal-aware quit
Modal focus trapexamples/modal_trap.rswidgets_tourModalOpts::new()
Menus, popovers, disabled, state stylesexamples/menus.rsmenusmenu_bar, context_menu, popover, disabled, hover_bg
File pickerexamples/cookbook_file_picker.rscookbook_tourFilePickerState::selected_file()
Dashboard (chart+sparkline)examples/cookbook_dashboard.rscookbook_tourui.chart(|c| c.line(...).color(..), w, h), rolling VecDeque<f64>
Animation primitivesexamples/canvas_tour.rs (Anim tab)target canvas_tourTween/Spring/Keyframes/Sequence/Stagger; standalone version examples/anim.rs (target anim)
use_state_keyedexamples/use_state_keyed.rswidgets_tourper-row counter via format!("counter-{i}")
use_effectexamples/use_effect.rswidgets_tourthree dep shapes (&(), &i32, &bool)
provide/use_contexttests/context_provider.rs, examples/demo_website.rs:139-152test / showcase_tourinjection + try_use_context
Theme subtreeexamples/theme_subtree.rswidgets_tourcontainer().theme(theme) per-subtree override
Theme densityexamples/spacing_scale.rswidgets_tourTheme::compact() / comfortable() / spacious()
Gauge builderexamples/gauge.rs:104-107widgets_tourui.gauge(value).label(...).width(24)
Line gaugeexamples/gauge.rs:74-92widgets_tourui.line_gauge(0.45).filled('#').empty('.').width(24)
Breadcrumb responseexamples/breadcrumb_response.rs:72-77widgets_tourui.breadcrumb(&segs).separator(" › ").show()
Scrollable + gutterexamples/gutter_highlights.rs:150-165widgets_tourGutterOpts::line_numbers, HighlightRange::line
Split paneexamples/split_pane.rswidgets_toursplit_pane, vsplit_pane, drag handle
WidthSpec variantsexamples/widthspec.rswidgets_tourConstraints::default().w_pct(50), .w_ratio(1,3), .w_minmax(10,30)
Named focusexamples/named_focus.rs:187widgets_tourregister_focusable_named + focus_by_name
Keymap help overlayexamples/keymap_help.rswidgets_tourWidgetKeyHelp, publish_keymap, keymap_help_overlay(open)
DX shortcutsexamples/dx_shortcuts.rswidgets_touron_hover, animate_bool, fill(), Rect::center_in
Static log / scrollbackexamples/static_log.rswidgets_tourslt::run_static, ui.static_log(...)
Asyncexamples/async_demo.rstarget async_demo (needs --features async)slt::run_async with tokio, .sender(), .join()
All-in-one tourexamples/widgets_tour.rstarget widgets_tourtabs over every single-feature demo; examples/feature_showcase.rs (target feature_showcase) composes them on one screen
Recent widgetsexamples/widgets_extra.rs, examples/interaction_tour.rstargets widgets_extra, interaction_tourpaginator / number_input / scheduler / inspector; Response interaction signals, KeyMap, focus traversal
Test utilitiestests/example_snapshots/testing_report.rstestrecord_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

AreaPrimary files
Public APIsrc/lib.rs (re-exports)
Run loop / backendsrc/terminal.rs, src/lib.rs (run, run_with, run_inline, run_async, run_static, frame, frame_owned)
Context coresrc/context/{core,runtime,container,helpers,state}.rs
Widget implssrc/context/widgets/<family>.rs (+ <family>/), same family names as the state files
Layer 4 state typessrc/widgets/<family>.rs
Compound responsesnext to their family's state: BreadcrumbResponse (display.rs), GaugeResponse (feedback.rs), SplitPaneResponse / GutterResponse (layout.rs), FormFieldResponse (text_input.rs)
Layout kernelssrc/layout/{tree,flexbox,collect,render,command}.rs
Style / themesrc/style/{color,theme}.rs, src/style.rs
Animationsrc/anim.rs
Chartssrc/chart.rs, src/chart/*.rs, src/context/widgets/viz/*.rs
Testing helperssrc/test_utils.rs
Skill referencesREFERENCES.md (feature flags, doc pointers)

Korean conventions

  • "ㄱㄱ" = "go go" → proceed immediately, no clarifying questions
  • "켜줘" / "열어줘" → open the file in Cursor (NOT cat to terminal)
  • "고쳐줘" / "수정해줘" → fix with minimal change, run quality gate
  • "리뷰해줘" → audit only, do not modify code unless explicitly asked

리뷰

아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.

댓글

로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. URL은 2개까지입니다.

0 / 1,000자

이 양식은 reCAPTCHA로 보호되며 Google 개인정보처리방침 및 서비스 약관이 적용됩니다.

같은 저장소의 스킬

Migrate Rust TUIs from ratatui (or cursive, Python textual) to SuperLightTUI v0.26. Use when porting an existing TUI codebase to SLT, or when the user asks "how do I do X from ratatui in SLT". Korean triggers "ratatui 마이그레이션", "SLT로 포팅", "이걸 SLT로".

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

subinium/SuperLightTUI1942026년 10월 18일 업데이트

subinium의 스킬 모두 보기

이 스킬의 문제 신고