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
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
# SLT Cookbook

Copy-paste recipes for common TUI apps. Each recipe is also shipped as a standalone runnable example at `examples/cookbook_<name>.rs`, so you can run and tweak it directly.

Recipes assume familiarity with the core mental model from [QUICK_START.md](QUICK_START.md). If a widget surprises you, cross-check [WIDGETS.md](WIDGETS.md) for the full API table and [PATTERNS.md](PATTERNS.md) for composition advice.

## Index

- [Login Form with Validation]#login-form-with-validation`cargo run --example cookbook_login`
- [Data Table with Search and Sort]#data-table-with-search-and-sort`cargo run --example cookbook_table`
- [Modal Confirmation with Toast]#modal-confirmation-with-toast`cargo run --example cookbook_modal_toast`
- [Real-time Dashboard with Charts]#real-time-dashboard-with-charts`cargo run --example cookbook_dashboard`
- [File Picker with Preview]#file-picker-with-preview`cargo run --example cookbook_file_picker`
- [Components with Shared State]#components-with-shared-state`provide` / `use_context` (v0.19.0)
- [Common pitfalls]#common-pitfalls

## Prerequisites

Add SLT to a fresh project:

```toml
# Cargo.toml
[dependencies]
superlighttui = "0.19"
```

Every recipe follows the same outer shape:

```rust
fn main() -> std::io::Result<()> {
    // 1. Create state once, outside the closure.
    let mut state = /* ... */;

    // 2. Your closure runs each frame.
    slt::run(|ui: &mut slt::Context| {
        // Global shortcuts first.
        if ui.key('q') || ui.key_code(slt::KeyCode::Esc) {
            ui.quit();
        }

        // Describe your UI.
        ui.text("hello");
    })
}
```

The closure is the whole app. Plain Rust variables hold state; `slt::Context` mutates on interaction. No framework object, no stores.

---

## Login Form with Validation

### What it shows

- Two `TextInputState` fields with `add_validator(...)` chains that run every frame.
- Password masking via `TextInputState::masked`.
- Automatic Tab / Shift+Tab focus cycling — no manual focus handling required.
- Submit button that is only enabled when both fields pass validation.

### Full code

```rust
use slt::{AlertLevel, Border, Color, Context, KeyCode, TextInputState};

fn main() -> std::io::Result<()> {
    let mut email = TextInputState::with_placeholder("you@example.com");
    email.add_validator(|v| {
        if v.is_empty() {
            Err("Email is required".into())
        } else if !v.contains('@') || !v.contains('.') {
            Err("Invalid email format".into())
        } else {
            Ok(())
        }
    });

    let mut password = TextInputState::with_placeholder("At least 8 characters");
    password.masked = true;
    password.add_validator(|v| {
        if v.len() < 8 {
            Err("Password must be at least 8 characters".into())
        } else {
            Ok(())
        }
    });

    let mut submitted: Option<String> = None;

    slt::run(|ui: &mut Context| {
        if ui.key_mod('q', slt::KeyModifiers::CONTROL) || ui.key_code(KeyCode::Esc) {
            ui.quit();
        }

        // Run validators every frame; cheap because inputs are tiny.
        email.run_validators();
        password.run_validators();
        let valid = email.errors().is_empty() && password.errors().is_empty();

        let _ = ui
            .bordered(Border::Rounded)
            .title("Sign In")
            .pad(2)
            .gap(1)
            .col(|ui| {
                ui.text("Welcome back").bold().fg(Color::Cyan);
                ui.text("Use Tab / Shift+Tab to move between fields.").dim();
                ui.separator();

                ui.text("Email").bold();
                let _ = ui.text_input(&mut email);
                for err in email.errors() {
                    ui.text(format!("  {err}")).fg(Color::Red);
                }

                ui.text("Password").bold();
                let _ = ui.text_input(&mut password);
                for err in password.errors() {
                    ui.text(format!("  {err}")).fg(Color::Red);
                }

                ui.separator();
                let _ = ui.row(|ui| {
                    // Disable the button when invalid by swapping the label.
                    let label = if valid { "Submit" } else { "Submit (fix errors)" };
                    if ui.button(label).clicked && valid {
                        submitted = Some(email.value.clone());
                    }
                    ui.spacer();
                    if let Some(ref user) = submitted {
                        let _ = ui.alert(
                            &format!("Signed in as {user}"),
                            AlertLevel::Success,
                        );
                    }
                });
            });
    })
}
```

### Key patterns

- `add_validator(|v| -> Result<(), String>)` is additive — call it once per rule. `run_validators()` populates `errors()`, which is the source of truth for the UI.
- Never guard `ui.text_input(...)` behind an `if`. Call it every frame; otherwise focus and key consumption state drifts.
- Focus cycling is handled by SLT. Register widgets in the order you want them traversed.
- The submit button stays rendered even when invalid — this keeps hook order stable. We gate the effect (`submitted = ...`) instead.

---

## Data Table with Search and Sort

### What it shows

- `TableState` with built-in search, pagination, and column sort.
- A `TextInputState` driving the filter each frame.
- Header-click sort (`toggle_sort`) via keyboard shortcuts `H` / `L`.
- Zebra rows + row-size footer with live row count.

### Full code

```rust
use slt::{Border, Color, Context, KeyCode, TableState, TextInputState};

fn main() -> std::io::Result<()> {
    let mut table = TableState::new(
        vec!["Name", "Role", "Stars"],
        vec![
            vec!["slt",       "TUI library",  "500"],
            vec!["ratatui",   "TUI library",  "12000"],
            vec!["bubbletea", "TUI framework", "30000"],
            vec!["ink",       "React for CLI", "28000"],
            vec!["textual",   "Python TUI",    "26000"],
            vec!["cursive",   "Rust TUI",      "4200"],
            vec!["termion",   "terminal lib",  "1900"],
            vec!["notcurses", "rendering lib", "2300"],
            vec!["blessed",   "JS TUI",        "10700"],
            vec!["prompt-kit","form lib",      "890"],
        ],
    );
    table.page_size = 5;
    table.zebra = true;

    let mut filter = TextInputState::with_placeholder("Filter across all columns...");

    slt::run(|ui: &mut Context| {
        if ui.key_mod('q', slt::KeyModifiers::CONTROL) || ui.key_code(KeyCode::Esc) {
            ui.quit();
        }
        // Column-sort shortcuts (1..=3).
        for i in 1..=3u8 {
            if ui.key((b'0' + i) as char) {
                table.toggle_sort((i - 1) as usize);
            }
        }
        if ui.key_code(KeyCode::Right) {
            table.next_page();
        }
        if ui.key_code(KeyCode::Left) {
            table.prev_page();
        }

        let _ = ui
            .bordered(Border::Rounded)
            .title("Projects")
            .pad(1)
            .gap(1)
            .col(|ui| {
                ui.text("Press 1/2/3 to sort, Left/Right to page").dim();
                let _ = ui.text_input(&mut filter);
                // Pipe the text input straight into the table filter each frame.
                table.set_filter(&filter.value);

                let _ = ui.table(&mut table);

                let _ = ui.row(|ui| {
                    ui.text(format!(
                        "Showing {}/{} rows",
                        table.visible_indices().len(),
                        table.rows.len(),
                    ))
                    .dim();
                    ui.spacer();
                    if let Some(col) = table.sort_column {
                        let arrow = if table.sort_ascending { "asc" } else { "desc" };
                        ui.text(format!("Sort: {} {arrow}", table.headers[col]))
                            .fg(Color::Cyan);
                    }
                    ui.spacer();
                    ui.text(format!(
                        "page {}/{}",
                        table.page + 1,
                        table.total_pages()
                    ))
                    .dim();
                });

                if let Some(row) = table.selected_row() {
                    ui.separator();
                    ui.text(format!("Selected: {}", row.join(" | "))).bold();
                }
            });
    })
}
```

### Key patterns

- `TableState::new` accepts headers and string rows. `Stars` contains numbers inside strings — SLT's sort detects numeric columns automatically by trying `str::parse::<f64>()`.
- `set_filter` runs a cached, lowercase, token-AND search across all cells. Calling it with the same value is a no-op.
- `toggle_sort(col)` flips asc/desc on the second press; `sort_by(col)` always sets ascending.
- `page_size` of 0 disables pagination. When set, `visible_indices()` is still the full filtered set; SLT slices it internally for the current page.

---

## Modal Confirmation with Toast

### What it shows

- A destructive "Delete" action guarded by `ui.modal(...)`.
- `ToastState` for transient notifications with severity levels.
- `raw_key_code(KeyCode::Esc)` to close overlays regardless of modal state.
- Split logic: the button in the main UI opens the modal; the modal itself handles Confirm/Cancel.

### Full code

```rust
use slt::{Border, ButtonVariant, Color, Context, KeyCode, ToastState};

fn main() -> std::io::Result<()> {
    let mut items: Vec<String> = (1..=6).map(|i| format!("Document {i}.txt")).collect();
    let mut selected: usize = 0;
    let mut show_modal = false;
    let mut toasts = ToastState::new();

    slt::run(|ui: &mut Context| {
        let tick = ui.tick();

        if !show_modal && ui.key('q') {
            ui.quit();
        }
        if ui.key_code(KeyCode::Up) && selected > 0 {
            selected -= 1;
        }
        if ui.key_code(KeyCode::Down) && selected + 1 < items.len() {
            selected += 1;
        }

        let _ = ui
            .bordered(Border::Rounded)
            .title("Files")
            .pad(1)
            .gap(1)
            .col(|ui| {
                ui.text("Select a file, then press Delete.").dim();
                for (i, name) in items.iter().enumerate() {
                    let prefix = if i == selected { "> " } else { "  " };
                    let color = if i == selected { Color::Cyan } else { Color::White };
                    ui.text(format!("{prefix}{name}")).fg(color);
                }

                ui.separator();
                let _ = ui.row(|ui| {
                    if ui.button("Refresh").clicked {
                        toasts.info("List refreshed", tick);
                    }
                    ui.spacer();
                    if ui.button_with("Delete", ButtonVariant::Danger).clicked
                        && !items.is_empty()
                    {
                        show_modal = true;
                    }
                });
            });

        if show_modal {
            // Esc closes the modal no matter what is below it.
            if ui.raw_key_code(KeyCode::Esc) {
                show_modal = false;
            }

            let _ = ui.modal(|ui| {
                let _ = ui
                    .bordered(Border::Double)
                    .title("Confirm delete")
                    .pad(2)
                    .gap(1)
                    .col(|ui| {
                        let target = items
                            .get(selected)
                            .cloned()
                            .unwrap_or_else(|| "<nothing>".into());
                        ui.text("You are about to delete:").bold();
                        ui.text(format!("  {target}")).fg(Color::Yellow);
                        ui.text("This cannot be undone.").dim();

                        let _ = ui.row(|ui| {
                            if ui.button("Cancel").clicked {
                                show_modal = false;
                                toasts.info("Canceled", tick);
                            }
                            ui.spacer();
                            if ui
                                .button_with("Delete", ButtonVariant::Danger)
                                .clicked
                            {
                                let name = items.remove(selected);
                                if selected >= items.len() && selected > 0 {
                                    selected -= 1;
                                }
                                toasts.success(format!("Deleted {name}"), tick);
                                show_modal = false;
                            }
                        });
                    });
            });
        }

        // Toast rendering must happen every frame so `cleanup()` can expire old messages.
        ui.toast(&mut toasts);
    })
}
```

### Key patterns

- `ui.modal(|ui| { ... })` dims the background and traps focus inside the closure. The UI behind it still renders, but interaction routes to the modal.
- `ui.raw_key_code(...)` bypasses the modal's input consumption — ideal for global keys like Esc. Plain `ui.key_code(...)` inside the modal is for modal-local keys.
- `ToastState::info/success/warning/error` take `(message, tick)`. `ui.toast(&mut toasts)` both renders and cleans up expired entries — always call it unconditionally every frame.
- Gate global shortcuts (like `q`) behind `!show_modal` so they don't fire while the modal is open.

---

## Real-time Dashboard with Charts

### What it shows

- Simulated per-tick metrics feeding a `sparkline` and `bar_chart`.
- `ui.chart(|c| ...)` multi-series builder with a `line` and `area` overlay.
- `stat_trend` and `stat_colored` for KPI tiles.
- A ring buffer of recent values — the classic "rolling window" pattern for TUIs.

> Dashboards often want hover-driven highlight or drag-to-pan. Both use previous-frame layout data, so the first frame after layout change can look odd. See [DEBUGGING.md]DEBUGGING.md for why — it is not a bug, it is immediate mode's trade-off.

### Full code

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

fn main() -> std::io::Result<()> {
    let spinner = SpinnerState::dots();
    let mut cpu_hist: Vec<f64> = vec![0.0; 60];
    let mut req_hist: Vec<f64> = vec![0.0; 60];

    slt::run(|ui: &mut Context| {
        if ui.key_mod('q', slt::KeyModifiers::CONTROL) || ui.key_code(KeyCode::Esc) {
            ui.quit();
        }

        let tick = ui.tick() as f64;
        let cpu = 45.0 + 30.0 * (tick * 0.08).sin() + 10.0 * (tick * 0.3).cos();
        let reqs = (120.0 + 80.0 * (tick * 0.05).sin()).max(0.0);

        // Ring-buffer update: drop oldest, append newest.
        cpu_hist.rotate_left(1);
        if let Some(last) = cpu_hist.last_mut() {
            *last = cpu;
        }
        req_hist.rotate_left(1);
        if let Some(last) = req_hist.last_mut() {
            *last = reqs;
        }

        let _ = ui
            .bordered(Border::Rounded)
            .title("Live Dashboard")
            .pad(1)
            .gap(1)
            .grow(1)
            .col(|ui| {
                let _ = ui.row(|ui| {
                    ui.spinner(&spinner);
                    ui.text(" LIVE").bold().fg(Color::Green);
                    ui.spacer();
                    ui.text(format!("tick {tick:.0}")).dim();
                });

                let _ = ui.row(|ui| {
                    let _ = ui.bordered(Border::Single).pad(1).grow(1).col(|ui| {
                        let _ = ui.stat_trend("Requests / min", &format!("{reqs:.0}"), Trend::Up);
                    });
                    let _ = ui.bordered(Border::Single).pad(1).grow(1).col(|ui| {
                        let color = if cpu > 80.0 { Color::Red } else { Color::Cyan };
                        let _ = ui.stat_colored("CPU", &format!("{cpu:.1}%"), color);
                    });
                    let _ = ui.bordered(Border::Single).pad(1).grow(1).col(|ui| {
                        let _ = ui.stat_colored("P99", "42ms", Color::Yellow);
                    });
                });

                let _ = ui
                    .bordered(Border::Single)
                    .title("CPU (last 60 ticks)")
                    .pad(1)
                    .col(|ui| {
                        let _ = ui.sparkline(&cpu_hist, 60);
                    });

                let _ = ui
                    .bordered(Border::Single)
                    .title("Requests / min")
                    .pad(1)
                    .col(|ui| {
                        let _ = ui.chart(
                            |c| {
                                c.area(&req_hist).label("rpm").color(Color::Cyan);
                                c.grid(true);
                            },
                            60,
                            10,
                        );
                    });

                let top_routes: Vec<(&str, f64)> = vec![
                    ("/api/users", 420.0),
                    ("/api/items", 312.0),
                    ("/auth", 280.0),
                    ("/health", 95.0),
                    ("/metrics", 60.0),
                ];
                let _ = ui
                    .bordered(Border::Single)
                    .title("Top Routes")
                    .pad(1)
                    .col(|ui| {
                        let _ = ui.bar_chart(&top_routes, 30);
                    });

                let _ = ui.help(&[("Ctrl+Q", "quit"), ("Esc", "quit")]);
            });
    })
}
```

### Key patterns

- Store history in a fixed-size `Vec<f64>` and rotate it. This is cheaper than `VecDeque` for tiny buffers and keeps data contiguous for `sparkline(&history, width)`.
- `ui.chart(|c| { ... }, width, height)` takes an explicit size. For grow-to-fit behavior, read `ui.width()` / `ui.height()` and compute manually.
- `stat_trend` pairs a value with an up/down arrow via `Trend::Up` / `Trend::Down`. `stat_colored` lets you encode severity.
- Spinners animate off the frame tick automatically — just create one `SpinnerState::dots()` at startup and hand the reference in each frame.

---

## File Picker with Preview

### What it shows

- `FilePickerState` on the left, text preview on the right in a two-pane layout.
- Extension filtering (`.extensions(&["rs", "toml", "md"])`).
- Safe file reading with a size cap — avoids locking the UI on multi-MB files.
- `notify` toasts for error reporting without growing the main UI.

### Full code

```rust
use slt::{Border, Color, Context, FilePickerState, KeyCode, ScrollState, ToastState};
use std::path::PathBuf;

const PREVIEW_MAX_BYTES: u64 = 64 * 1024; // 64 KiB cap

fn main() -> std::io::Result<()> {
    let mut picker = FilePickerState::new(
        std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
    )
    .extensions(&["rs", "toml", "md", "txt", "json"])
    .show_hidden(false);

    let mut preview: Option<(PathBuf, String)> = None;
    let mut preview_scroll = ScrollState::new();
    let mut toasts = ToastState::new();

    slt::run(|ui: &mut Context| {
        let tick = ui.tick();
        if ui.key_mod('q', slt::KeyModifiers::CONTROL) || ui.key_code(KeyCode::Esc) {
            ui.quit();
        }

        let _ = ui
            .bordered(Border::Rounded)
            .title("File Picker")
            .pad(1)
            .gap(1)
            .grow(1)
            .col(|ui| {
                ui.text(format!("Dir: {}", picker.current_dir.display()))
                    .dim()
                    .wrap();
                ui.text("Enter opens, Backspace goes up, filter: .rs .toml .md .txt .json")
                    .dim();
                ui.separator();

                let _ = ui.row(|ui| {
                    // Left pane: picker.
                    let _ = ui
                        .bordered(Border::Single)
                        .title("Browse")
                        .pad(1)
                        .grow(1)
                        .col(|ui| {
                            if ui.file_picker(&mut picker).changed {
                                if let Some(path) = picker.selected().cloned() {
                                    match read_preview(&path) {
                                        Ok(content) => {
                                            preview = Some((path.clone(), content));
                                            preview_scroll.offset = 0;
                                            let name = path
                                                .file_name()
                                                .and_then(|s| s.to_str())
                                                .unwrap_or("?");
                                            toasts.success(format!("Loaded {name}"), tick);
                                        }
                                        Err(err) => {
                                            toasts.error(err, tick);
                                        }
                                    }
                                }
                            }
                        });

                    // Right pane: preview.
                    let _ = ui
                        .bordered(Border::Single)
                        .title("Preview")
                        .pad(1)
                        .grow(2)
                        .col(|ui| match &preview {
                            None => {
                                ui.text("Select a file to preview.").dim();
                            }
                            Some((path, content)) => {
                                ui.text(path.display().to_string()).fg(Color::Cyan).wrap();
                                ui.separator();
                                let _ = ui.scrollable(&mut preview_scroll).grow(1).col(|ui| {
                                    for line in content.lines() {
                                        ui.text(line);
                                    }
                                });
                            }
                        });
                });
            });

        ui.toast(&mut toasts);
    })
}

fn read_preview(path: &std::path::Path) -> Result<String, String> {
    let metadata = std::fs::metadata(path)
        .map_err(|e| format!("metadata: {e}"))?;
    if metadata.len() > PREVIEW_MAX_BYTES {
        return Err(format!(
            "File too large ({} bytes, limit {} bytes)",
            metadata.len(),
            PREVIEW_MAX_BYTES
        ));
    }
    std::fs::read_to_string(path).map_err(|e| format!("read: {e}"))
}
```

### Key patterns

- `FilePickerState` owns the directory listing. `.extensions(&[...])` filters non-directory entries; directories are always shown so you can navigate in.
- `picker.selected()` returns `Option<&PathBuf>` only when the user picks a file with Enter — directories set `current_dir` instead and emit `changed = true` but no selection.
- Always gate `std::fs::read_to_string` behind a size check. A runaway file read blocks the thread and the UI stops responding.
- Pair a `ScrollState` with every long content region. Reset `offset = 0` when you swap the content — otherwise the previous file's scroll position leaks into the new file.

---

## Components with Shared State

### What it shows

- `ui.provide(value, |ui| ...)` to publish shared app context once at the root.
- `ui.use_context::<T>()` (panics if missing) and `ui.try_use_context::<T>()` (returns `Option<&T>`) to read it back from any nested render fn.
- Replaces threading `&theme`, `&tick`, `&user` parameters through every signature.

The canonical real-world refactor is `examples/demo_website.rs`, which moved from per-call argument threading to one `provide` at the top.

### When to reach for it

- Many nested render helpers all want the same read-only value (theme, tick, current user).
- You catch yourself adding the same parameter to every render fn signature.

Reserve explicit parameters for **writes** (`&mut MyDocState`, `&mut ToastState`). `provide` / `use_context` is a *read*-side ergonomics tool, not a state container.

### Full code

```rust
use slt::{Border, Color, Context, KeyCode, Theme};

// `provide` boxes the value as `dyn Any`, requiring `T: 'static`. `Theme` is
// `Copy`, so deref-copy from `ui.theme()`. `&'static str` works for string
// literals; switch to `String` for runtime values.
struct AppCtx {
    theme: Theme,
    tick: u64,
    user: &'static str,
}

fn main() -> std::io::Result<()> {
    slt::run(|ui: &mut Context| {
        if ui.key('q') || ui.key_code(KeyCode::Esc) {
            ui.quit();
        }

        let ctx = AppCtx {
            theme: *ui.theme(),
            tick: ui.tick(),
            user: "subin",
        };

        ui.provide(ctx, |ui| {
            let _ = ui
                .bordered(Border::Rounded)
                .title("Shared context demo")
                .pad(1)
                .gap(1)
                .col(|ui| {
                    render_header(ui);
                    render_card(ui);
                });
        });
    })
}

fn render_header(ui: &mut Context) {
    let ctx = ui.use_context::<AppCtx>();
    ui.text(format!("hi, {}", ctx.user)).bold().fg(Color::Cyan);
    ui.text(format!("tick {}", ctx.tick)).dim();
}

fn render_card(ui: &mut Context) {
    // Optional read — never panics if no provider is on the stack.
    if let Some(ctx) = ui.try_use_context::<AppCtx>() {
        ui.text(format!("theme bg: {:?}", ctx.theme.bg));
    } else {
        ui.text("no app context").dim();
    }
}
```

### Key patterns

- `provide` is scoped to the closure body. Once that body returns, the value pops off the context stack.
- `use_context::<T>()` finds the nearest provided `T` by type. Two providers of the same type stack — the inner one wins inside its body.
- Use `try_use_context` in helpers that should also work outside the provider (e.g. unit tests, isolated demos).

---

## Common pitfalls

### `RichLogState::new()` silently truncates at 10000 entries

`RichLogState::new()` is bounded at 10000 entries (v0.19.2). Older entries are dropped to keep memory predictable. If you are running a long-lived dashboard or log viewer and entries seem to "disappear" from the top, that is the cap, not a bug.

For unbounded accumulation use `RichLogState::new_unbounded()` and accept the memory cost, or add your own retention policy on top of the bounded variant.

```rust
// Bounded — drops oldest after 10k entries.
let mut log = slt::RichLogState::new();

// Unbounded — grows until the process dies. Use only when you control the input rate.
let mut log = slt::RichLogState::new_unbounded();
```

---

## Where to go next

- [PATTERNS.md]PATTERNS.md — when these recipes are not enough and you need structural advice (render helpers, screens, hooks vs app state).
- [THEMING.md]THEMING.md — all recipes honor `ui.theme()`; swap `Theme::dark()` to `Theme::tokyo_night()` and see everything recolor.
- [TESTING.md]TESTING.md — each recipe's logic can be exercised headlessly with `TestBackend::new(w, h).render(|ui| { ... })`.
- [ANIMATION.md]ANIMATION.md — tween the dashboard tiles or stagger-fade the file picker list.
- [EXAMPLES.md]EXAMPLES.md — full index of runnable examples, including the `cookbook_*` entries paired with this page.