superlighttui 0.20.1

Super Light TUI - A lightweight, ergonomic terminal UI library
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
# Common Patterns

This page is for the moment when “hello world” is no longer enough and a real app starts to form.

The goal is not to teach every API again.
It is to show how to keep SLT code readable as screens, widgets, and state grow.

## The default shape of a real SLT app

For most medium-sized apps, the most readable shape is:

- one plain Rust `App` struct for app state
- one top-level `run(...)` or `run_with(...)` closure
- a few `render_*` helper functions for big panels or screens
- occasional hooks for truly local persistent state

That keeps the public grammar small without turning the closure into a 400-line blob.

## Application state lives in normal Rust

```rust
use slt::{Context, KeyCode};

struct App {
    count: i32,
    dark: bool,
}

fn main() -> std::io::Result<()> {
    let mut app = App {
        count: 0,
        dark: false,
    };

    slt::run(|ui: &mut Context| {
        if ui.key('q') || ui.key_code(KeyCode::Esc) {
            ui.quit();
        }
        if ui.button("+1").clicked {
            app.count += 1;
        }

        ui.checkbox("Dark mode", &mut app.dark);
        ui.text(format!("count: {}", app.count));
    })
}
```

Use plain structs when the state belongs to your app, your domain, or your screens.

## Local persistent state with hooks

```rust
let count = ui.use_state(|| 0i32);
ui.text(format!("{}", count.get(ui)));
if ui.button("+1").clicked {
    *count.get_mut(ui) += 1;
}
```

Use hooks when:

- the state is local to one render subtree
- introducing a top-level field would add more noise than clarity
- you want to prototype quickly

Keep hook call order stable across frames.

## When to use app state vs hooks

Use app state when:

- multiple screens need the value
- the value matters outside rendering
- you want explicit ownership and easier refactoring

Use hooks when:

- the value is local to one widget subtree
- the lifetime is purely UI-local
- you want a lightweight scratchpad for a small interactive fragment

The mistake is not using hooks.
The mistake is using hooks for everything until the screen becomes impossible to reason about.

## Derived state with `use_memo`

```rust
let filtered = ui.use_memo(&(query.clone(), items.len()), |(query, _)| {
    items
        .iter()
        .filter(|item| item.contains(query))
        .cloned()
        .collect::<Vec<_>>()
});
```

Use `use_memo` when the computation is deterministic and should only rerun when the dependency tuple changes.
Like `use_state`, call order must stay stable across frames.

## Reduce repeated builder chains with helpers

Large SLT screens start to look noisy when the same builder chain repeats everywhere.
The fix is usually not a new framework.
It is a helper function.

```rust
use slt::{Border, Context};

fn panel(ui: &mut Context, title: &str, f: impl FnOnce(&mut Context)) {
    let _ = ui
        .bordered(Border::Rounded)
        .title(title)
        .p(1)
        .grow(1)
        .col(f);
}
```

This keeps the chaining style but moves the repetition out of the screen body.

## Components

A "component" in SLT is not a framework concept. There is no `Component` trait, no virtual DOM, no lifecycle hooks beyond the few state primitives you already know. A component is whatever helper function you find yourself extracting when the same shape repeats.

This section walks the four building blocks that make components ergonomic:

1. Functions for the shape itself
2. `use_state_named` for state that survives across frames inside the component
3. `provide` / `use_context` for values that propagate through deep trees
4. `.with` / `.with_if` for conditional styling

The order matters. Reach for the simpler tool first.

### Components as Functions (the canonical pattern)

The simplest reusable component is a free function that takes `&mut Context` and any explicit state it needs. No framework magic, no registration step.

```rust
use slt::{Border, Context, Trend};

fn metric_card(ui: &mut Context, label: &str, value: f64, trend: Trend) {
    let _ = ui.bordered(Border::Single).pad(1).col(|ui| {
        ui.text(label).dim();
        ui.text(format!("{:.1}", value)).bold();
        let arrow = match trend {
            Trend::Up => "▲",
            Trend::Down => "▼",
            Trend::Flat => "—",
        };
        ui.text(arrow);
    });
}
```

Usage from a screen:

```rust
fn render_dashboard(ui: &mut Context, app: &App) {
    ui.row(|ui| {
        metric_card(ui, "Revenue", app.revenue, Trend::Up);
        metric_card(ui, "Latency p99", app.p99_ms, Trend::Down);
        metric_card(ui, "Active users", app.users, Trend::Flat);
    });
}
```

Trade-offs:

- Fully explicit. Rust's ownership tells you exactly who reads and who mutates what.
- Type-safe. The compiler enforces parameter shapes at every call site.
- No hidden state. The function only renders; it does not remember anything between frames.

The cost is parameter count. When a component grows past four or five arguments, group related values into a small struct and pass `&Props` instead of repeating five-arg signatures across the codebase.

```rust
struct MetricCardProps<'a> {
    label: &'a str,
    value: f64,
    trend: Trend,
    accent: Color,
}

fn metric_card(ui: &mut Context, p: &MetricCardProps<'_>) {
    // ...
}
```

This is still "just a function." No ceremony.

### Component-local State with `use_state_named`

Sometimes a component needs state that must survive across frames but does not belong to the caller. A collapsible panel knows whether it is expanded. A pagination control knows the current page. Threading those values up through every call site pollutes the caller's API.

`use_state_named` is the answer. It is `use_state`, but keyed by an explicit `&'static str` instead of by hook call order:

```rust
use slt::{Border, Context};

fn expandable_card(
    ui: &mut Context,
    id: &'static str,
    title: &str,
    body: impl FnOnce(&mut Context),
) {
    let expanded = ui.use_state_named::<bool>(id);
    let _ = ui.bordered(Border::Single).col(|ui| {
        let label = if *expanded.get(ui) { "▼" } else { "▶" };
        if ui.button(label).clicked {
            let v = *expanded.get(ui);
            *expanded.get_mut(ui) = !v;
        }
        ui.text(title).bold();
        if *expanded.get(ui) {
            body(ui);
        }
    });
}
```

Usage:

```rust
expandable_card(ui, "card.networking", "Networking", |ui| {
    ui.text("eth0  192.168.1.42");
    ui.text("wlan0 10.0.0.7");
});

expandable_card(ui, "card.disks", "Disks", |ui| {
    ui.text("nvme0n1  931 GiB");
});
```

When you need a default different from `Default::default()`, use `use_state_named_with`:

```rust
let page = ui.use_state_named_with::<usize>("pager", || 1);
```

Rules of the road:

- IDs are `&'static str`. Pick something descriptive — `"card.networking"`, `"pager.users"`, not `"x"`.
- Two calls with the same id at the same scope share state. This is sometimes what you want (siblings agreeing on a value) and sometimes a bug (two unrelated cards collapsing together). When in doubt, suffix with the data key: `"card.disks"` vs `"card.networking"`.
- Unlike positional `use_state`, named state does not depend on call order. You can branch around a named-state read without losing the value next frame.
- For the full method surface on the returned `State<T>`, see `STATE_APIS.md`.

### Context Injection with `ui.provide` / `ui.use_context`

Some values want to propagate through a deep tree without being passed to every function. The classic examples are theme, the current user, and feature flags. Threading them through ten render helpers is parameter-drilling, and it makes refactoring painful.

`provide` makes a value available inside a closure. Anything inside that closure can read it via `use_context`:

```rust
use slt::{Color, Context};

struct AppContext {
    username: String,
    show_debug: bool,
}

fn main() -> std::io::Result<()> {
    slt::run(|ui| {
        let app = AppContext {
            username: "sb".into(),
            show_debug: true,
        };
        ui.provide(app, |ui| {
            render_home(ui);
        });
    })
}

fn render_home(ui: &mut Context) {
    let app = ui.use_context::<AppContext>();
    ui.text(format!("Hello, {}", app.username));
    if app.show_debug {
        ui.text("debug mode on").dim();
    }
}
```

How it behaves:

- **Scoped.** The value is alive only inside the body closure passed to `provide`. Outside that closure, `use_context::<AppContext>()` panics — there is no value.
- **Shadowing.** Nested `provide` calls of the same type shadow the outer one for the duration of the inner closure. Think of it as a LIFO stack, one stack per `TypeId`.
- **Optional reads.** `try_use_context::<T>() -> Option<&T>` returns `None` instead of panicking. Use it when a component should work both inside and outside the provider.

```rust
fn render_footer(ui: &mut Context) {
    if let Some(app) = ui.try_use_context::<AppContext>() {
        ui.text(format!("logged in as {}", app.username)).dim();
    } else {
        ui.text("anonymous").dim();
    }
}
```

When *not* to use context:

- The value lives in your top-level app struct and you already pass `&mut App` around. Do not duplicate it into context.
- The value is needed by exactly one render helper that is two scope levels away. A parameter is clearer.

A good heuristic: reach for `provide` when the same value is needed by three or more render helpers across two or more scope levels.

### Conditional Styling with `.with_if`

Conditional styling is everywhere — a row that highlights when selected, a label that turns red when invalid, a button that dims when disabled. Without help, this clutters the call site:

```rust
let mut t = ui.text("Status");
if is_error {
    t = t.bold().fg(Color::Red);
}
if is_selected {
    t = t.bg(Color::DarkGray);
}
```

`.with_if(cond, modifier)` compresses that into a chain that reads top-to-bottom:

```rust
ui.text("Status")
    .with_if(is_error, |t| {
        t.bold().fg(Color::Red);
    })
    .with_if(is_selected, |t| {
        t.bg(Color::DarkGray);
    });
```

The closure runs only when the condition is true. The modifier receives a mutable handle to the same builder, so you can chain any styling method inside.

For unconditional grouping — factoring shared modifier blocks out of multiple call sites — use `.with(modifier)`:

```rust
fn dim_label(t: &mut TextBuilder<'_>) {
    t.dim().italic();
}

ui.text("uptime: 3d 4h").with(dim_label);
ui.text("region: us-west-2").with(dim_label);
```

Both `.with` and `.with_if` are available on text and on container builders, so the same idiom works for `bordered`, `row`, `col`, etc.:

```rust
ui.bordered(Border::Single)
    .with_if(panel_focused, |c| {
        c.title("(focused)").pad(2);
    })
    .col(|ui| {
        // ...
    });
```

### When to use which

| Pattern | Use when | Example scenarios |
|---|---|---|
| Function + explicit args | Small components, fewer than 3-4 params | `metric_card`, `header_row`, `kv_pair` |
| `use_state_named` | Component has LOCAL state that should survive across frames | collapsible panel, pagination cursor, sort direction |
| `provide` / `use_context` | Values cross 3+ scope levels | theme, logged-in user, feature flags, request id |
| `.with_if` | Styling depends on a runtime condition | selected row, error text, disabled state, focused panel |
| `.with` | Factor shared style blocks out of multiple call sites | "dim label", "code-style text", "subtle border" |

### Anti-patterns

These look tempting but make code harder, not easier:

- **Reaching for context when a parameter is clearer.** If a value is used in one helper, just pass it. Context is for values that propagate, not for "I do not feel like typing the parameter."
- **Using `use_state_named` for app-level state.** Page-level state, selected tab, current user — these belong in your top-level `App` struct so that tests, persistence, and refactors stay sane. Named state is for state that is genuinely local to a component instance.
- **Reusing the same id by accident.** `expandable_card(ui, "card", ...)` called five times shares one boolean across all five cards. Pick ids that name the *instance*, not the *kind*.
- **Nesting `provide` purely to "override defaults."** If you find yourself wrapping every render helper in a fresh `provide`, the value should probably be a parameter or a method argument instead.

For full working apps that combine these patterns, see `COOKBOOK.md`. For the per-method reference on `State<T>` and friends, see `STATE_APIS.md`. For the single-file AI-oriented reference, see `COMPLETE_REFERENCE.md`.

## Split big screens into render helpers

```rust
fn render_sidebar(ui: &mut Context, app: &mut App) {
    ui.text("Navigation").bold();
    let _ = ui.list(&mut app.sidebar);
}

fn render_content(ui: &mut Context, app: &mut App) {
    ui.text(format!("Selected: {}", app.current_title()));
}

fn render_app(ui: &mut Context, app: &mut App) {
    ui.row(|ui| {
        panel(ui, "Sidebar", |ui| render_sidebar(ui, app));
        panel(ui, "Content", |ui| render_content(ui, app));
    });
}
```

If a closure becomes hard to scan, extract render helpers before inventing a new abstraction layer.

## Forms and validation

```rust
let mut email = TextInputState::with_placeholder("you@example.com");
ui.text_input(&mut email);
email.validate(|value| {
    if value.contains('@') {
        Ok(())
    } else {
        Err("Invalid email".into())
    }
});
```

For larger forms, reach for `FormField` and `FormState`.

## Focus and keyboard shortcuts

```rust
if ui.key('q') {
    ui.quit();
}
if ui.key_code(KeyCode::Enter) {
    submit();
}
if ui.raw_key_code(KeyCode::Esc) {
    close_overlay();
}
```

Use `raw_*` shortcuts for keys that must work regardless of modal or overlay state.

## Screen helpers and navigation

```rust
ui.screen("home", &mut screens, |ui| {
    ui.text("Home").bold();
});

ui.screen("settings", &mut screens, |ui| {
    ui.text("Settings");
});
```

Use `screen(name, &mut screens, ...)` when you want declarative rendering that only runs for the active screen. Each screen gets isolated hook state and focus.
Use manual `push()` / `pop()` logic on `ScreenState` when you need explicit navigation transitions.

## Modal, overlay, and screen composition

```rust
ui.screen("home", &mut screens, |ui| {
    if ui.button("Settings").clicked {
        screens.push("settings");
    }
});

ui.screen("settings", &mut screens, |ui| {
    if ui.button("Back").clicked {
        screens.pop();
    }
});

if show_modal {
    ui.modal(|ui| {
        ui.text("Confirm?").bold();
    });
}
```

Use screens for view-level navigation and modal/overlay for transient UI layers.

## Error boundaries and recovery

```rust
ui.error_boundary(|ui| {
    ui.text("Protected subtree");
});
```

Use `error_boundary` or `error_boundary_with` when you want one subtree to fail without taking down the whole app.
This is especially useful for experimental widgets, user-generated content, or plugins.

## Custom widgets: focus and interaction

```rust
let focused = ui.register_focusable();
let response = ui.interaction();

if response.hovered {
    ui.tooltip("Hovered");
}
```

Use `register_focusable()` when the widget needs keyboard participation.
Use `interaction()` when the widget needs click/hover without wrapping everything in a container.

## Async background messages

```rust
let tx = slt::run_async(|ui, messages: &mut Vec<String>| {
    for message in messages.drain(..) {
        ui.text(message);
    }
})?;

tx.send("Background work done".into()).await?;
```

Enable with the `async` feature.

## Animation patterns

```rust
// Tween: smooth transition over N ticks
let mut fade = Tween::new(0.0, 1.0, 30);
let opacity = fade.value(ui.tick());

// Spring: physics-based, responds to target changes
let mut spring = Spring::new(0.0, 0.2, 0.85);
if hovered {
    spring.set_target(1.0);
} else {
    spring.set_target(0.0);
}
spring.tick();
let scale = spring.value();

// Stagger: offset animation across list items
let mut stagger = Stagger::new(0.0, 1.0, 20).delay(3).items(items.len());
for (i, item) in items.iter().enumerate() {
    let alpha = stagger.value(ui.tick(), i);
    ui.text(item)
        .fg(Color::Rgb(255, 255, (alpha * 255.0) as u8));
}
```

Animation types are standalone structs that compute values from `ui.tick()`.
Pass computed values to style and layout methods.
See `docs/ANIMATION.md` for the full API.

## Responsive layout

```rust
match ui.breakpoint() {
    Breakpoint::Xs | Breakpoint::Sm => {
        ui.col(|ui| { /* stacked layout */ });
    }
    _ => {
        ui.row(|ui| { /* side-by-side layout */ });
    }
};
```

Use `breakpoint()` for width-dependent layout decisions.
ContainerBuilder also supports responsive methods like `.gap_sm(1).gap_lg(2)`.

## Custom widgets

```rust
use slt::{Color, Context, Style, Widget};

struct Rating {
    value: u8,
    max: u8,
}

impl Widget for Rating {
    type Response = bool;

    fn ui(&mut self, ui: &mut Context) -> bool {
        let focused = ui.register_focusable();
        let mut changed = false;

        if focused {
            if ui.key('+') && self.value < self.max {
                self.value += 1;
                changed = true;
            }
            if ui.key('-') && self.value > 0 {
                self.value -= 1;
                changed = true;
            }
        }

        let stars: String = (0..self.max)
            .map(|i| if i < self.value { '★' } else { '☆' })
            .collect();
        ui.styled(
            stars,
            Style::new().fg(if focused {
                Color::Yellow
            } else {
                Color::White
            }),
        );
        changed
    }
}
```

If you add a new built-in widget to the library itself, also follow the checklist in `CONTRIBUTING.md`.

## Testing and verification

```rust
use slt::TestBackend;

let mut backend = TestBackend::new(40, 10);
backend.render(|ui| {
    ui.text("Hello");
});

backend.assert_contains("Hello");
```

Use `TestBackend` for headless rendering checks and snapshot-style assertions.
For runtime-contract work, add a custom `Backend` + `frame()` test too.

## Heuristics that keep SLT readable

- If a builder chain repeats three times, extract a helper.
- If a closure becomes hard to scan, split it into `render_*` functions.
- If state is shared across screens, move it into your app struct.
- If state is local to one subtree, hooks are fine.
- If behavior depends on previous-frame data, test at least two frames.

SLT stays pleasant when you keep the public grammar small even as the codebase grows.