Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
30b8c8d
feat(buffer): #231 Buffer::snapshot_format() stable styled snapshot
subinium Apr 28, 2026
f7dea06
feat(test-utils): #229 record_frames, #230 sequence, #232 negative as…
subinium Apr 28, 2026
cc1fffe
docs(test-utils): v0.20 demo + CHANGELOG for #229/#230/#231/#232
subinium Apr 28, 2026
af02256
refactor(style): unified WidthSpec/HeightSpec — Constraints redesign
subinium Apr 28, 2026
30e2df8
perf: v0.20.0 hot-path fixes — FrameState reuse, wrap String alloc, k…
subinium Apr 28, 2026
2683a50
feat(context): v0.20.0 hooks + focus + Response signals
subinium Apr 28, 2026
707f2c0
feat: v0.20.0 widgets (#212, #213, #223, #224, #235)
subinium Apr 28, 2026
070780f
feat: v0.20.0 DX shorthand helpers (#209, #210, #220, #221)
subinium Apr 28, 2026
21f304c
feat: v0.20.0 Agent 6 — theme override + modal tab trap + spacing act…
subinium Apr 28, 2026
cf2e7c4
feat: v0.20.0 lib top-level (issues #233, #236, #238)
subinium Apr 28, 2026
748de95
merge: agent 1 — test-utils foundation (#229, #230, #231, #232)
subinium Apr 28, 2026
e2bea03
merge: agent 2 — WidthSpec/HeightSpec super-issue #237
subinium Apr 28, 2026
0599335
merge: agent 3 — hot-path perf (#204, #205, #206, #228)
subinium Apr 28, 2026
2fb3ffd
merge: agent 4 — context state/hooks/focus (#208, #215, #216, #217, #…
subinium Apr 28, 2026
f7009a2
merge: agent 5 — DX shorthand (#209, #210, #220, #221)
subinium Apr 28, 2026
e81a7c7
chore: ignore .claude/worktrees/ (cleanup gateway artifact)
subinium Apr 28, 2026
58b3e94
merge: agent 6 — theme + modal (#225, #226, #227)
subinium Apr 28, 2026
da62530
merge: agent 7 — widgets (#212, #213, #223, #224, #235)
subinium Apr 28, 2026
635ac21
fix: remove stray conflict marker in CHANGELOG
subinium Apr 28, 2026
cb27394
merge: agent 8 — lib top-level (#233, #236, #238)
subinium Apr 28, 2026
3733c82
fix: remove stray conflict marker after agent 8 merge
subinium Apr 28, 2026
f512e71
fix: meta-review findings (sentinel dedup, RATIO_GROW_SCALE, quit-pat…
subinium Apr 28, 2026
dc90f26
docs: add API_DESIGN.md — five consistency rules for v0.20+
subinium Apr 28, 2026
d9ec729
refactor(api): v0.20.0 consistency pass on new widgets
subinium Apr 28, 2026
b7a196d
merge: API consistency pass — gauge/line_gauge/breadcrumb builders, f…
subinium Apr 28, 2026
494cf6c
merge: docs/API_DESIGN.md — API consistency rules + README link
subinium Apr 28, 2026
e60f968
feat(scripts): add Ghostty demo launcher + v0.20 demo catalog in README
subinium Apr 28, 2026
2eedf93
docs(examples): polish v0.20.0 hooks/focus/theme demos to art-level t…
subinium Apr 28, 2026
3836c7e
docs(examples): polish v0.20.0 widget demos to API_DESIGN template
subinium Apr 28, 2026
0a83b45
refactor(examples): polish v0.20 lib + modal demos to art-level template
subinium Apr 28, 2026
3320a73
docs(examples): polish v0.20.0 test-utils/perf/dx/widthspec demos to …
subinium Apr 28, 2026
fc548fa
merge: demo polish B — hooks/focus/theme demos
subinium Apr 28, 2026
400d7fc
merge: demo polish C — widget demos with new builder APIs
subinium Apr 28, 2026
ea5d296
merge: demo polish D — lib/modal demos
subinium Apr 28, 2026
0f3064c
merge: scripts/ghostty_demos.sh + README v0.20 demo catalog
subinium Apr 28, 2026
4367a9e
merge: demo polish A — test-utils/perf/dx/widthspec demos
subinium Apr 28, 2026
01c44ef
fix: comprehensive review findings (B blocker + C/E nits)
subinium Apr 28, 2026
bc34cb5
refactor(api): remove same-release deprecations + Reviewer A nits
subinium Apr 28, 2026
e04f1ff
merge: remove same-release deprecations + Reviewer A nits (-189 LoC)
subinium Apr 28, 2026
1a9c66b
fix: confirm mouse hit-test ordering + focus_by_name return semantics
subinium Apr 28, 2026
3831a2a
refactor: code cleanup nits — panic helper, gauge dedup, visibility a…
subinium Apr 28, 2026
070ee4a
docs: fix 16 broken intra-doc links + regression_panel render extraction
subinium Apr 28, 2026
5a59824
test: absorb 6 v0.20.1-deferred test gaps
subinium Apr 28, 2026
48b7e84
perf: use_state_keyed single-clone + split_pane key match hoist
subinium Apr 28, 2026
bfc8fba
merge: absorb A — confirm bug + focus_by_name semantics
subinium Apr 28, 2026
e6856a1
merge: absorb D — 6 deferred test gaps
subinium Apr 28, 2026
5dc6b27
merge: absorb E — 16 doc-link fixes + regression_panel render extraction
subinium Apr 28, 2026
60e99cb
fix(examples): activate mouse on 9 v020 demos + ASCII title chars
subinium Apr 28, 2026
0551b05
fix(examples): functional interaction audit on v0.20 widget demos
subinium Apr 28, 2026
22e0c41
fix(examples): functional audit on v0.20 theme + modal demos
subinium Apr 28, 2026
bec3518
fix(examples): functional audit on v0.20 lib demos + ctrl_c macOS-fri…
subinium Apr 28, 2026
080ad2b
fix(examples): functional interaction audit on v0.20 hooks demos
subinium Apr 28, 2026
625d6cf
fix(examples): functional interaction audit on v0.20 integration demos
subinium Apr 28, 2026
a7bc90e
merge: functional audit B — hooks demos
subinium Apr 28, 2026
8c8751a
fix(examples): demo_infoviz unnecessary u32 cast (rust 1.95 clippy)
subinium Apr 28, 2026
439f1b2
fix(examples): perf_regression unnecessary usize cast
subinium Apr 28, 2026
8839ca2
feat: v0.20.0 — final integration (focus eager-attach + chart truncat…
subinium Apr 28, 2026
e77d7ab
refactor(widgets-input): rename selected → selected_file (deprecation…
subinium Apr 28, 2026
3a2469f
feat(widgets-input): textarea undo/redo via Ctrl+Z / Ctrl+Y (#102)
subinium Apr 28, 2026
c90afc0
feat(widgets-display): scrollbar() returns Response for future click/…
subinium Apr 28, 2026
cb1aa6a
refactor(container): hide ContainerBuilder::scroll_offset from rustdo…
subinium Apr 28, 2026
f0616ee
perf(layout): reuse line_segs scratch in wrap_segments (#157)
subinium Apr 28, 2026
e33ab94
fix(widgets-interactive): virtual_list keeps cursor mid-viewport (#192)
subinium Apr 28, 2026
fe1daac
fix(widgets-interactive): calendar h/l now moves day, [/] moves month…
subinium Apr 28, 2026
00eb078
perf(layout): drain commands Vec in build_tree to reuse capacity (#150)
subinium Apr 28, 2026
d3e5958
feat(flexbox): proportional flex-shrink via opt-in .shrink() flag (#161)
subinium Apr 28, 2026
a99cfaf
perf(terminal): per-row hash skip in flush_buffer_diff (#171)
subinium Apr 28, 2026
7535185
feat(layout): DebugLayer enum for F12 overlay opt-in (#201)
subinium Apr 28, 2026
07ea046
chore: post-cherry-pick cleanups for `clippy --examples -D warnings`
subinium Apr 28, 2026
df47348
docs(changelog): expand v0.20.0 entry with the cherry-picked fixes + …
subinium Apr 28, 2026
2795ea5
ci: pin `cargo test` to --test-threads=1 to keep perf-alloc budgets m…
subinium Apr 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,12 @@ jobs:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo test --all-features
# `--test-threads=1` is required because `tests/v020_perf_alloc.rs`
# uses a global allocation counter; CI's higher parallelism than a
# local laptop causes other parallel tests to contaminate the count
# and trip its single-thread budget. Single-threaded execution adds
# ~10s but keeps the perf-budget asserts meaningful.
- run: cargo test --all-features -- --test-threads=1

clippy:
name: Clippy
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ jobs:
components: clippy, rustfmt
- uses: Swatinem/rust-cache@v2
- run: cargo check --all-features
- run: cargo test --all-features
# See ci.yml — perf-alloc tests use a global counter, so CI's higher
# parallelism contaminates measurements. Single-threaded keeps the
# budget asserts honest.
- run: cargo test --all-features -- --test-threads=1
- run: cargo clippy --all-features -- -D warnings
- run: cargo fmt -- --check

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ SESSION-SUMMARY.md
assets/blackpink/
CLAUDE.md
.research/
.claude/worktrees/
164 changes: 163 additions & 1 deletion CHANGELOG.md

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

101 changes: 52 additions & 49 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ members = [".", "crates/slt-wasm"]

[package]
name = "superlighttui"
version = "0.19.3"
version = "0.20.0"
edition = "2021"
description = "Super Light TUI - A lightweight, ergonomic terminal UI library"
license = "MIT"
Expand All @@ -15,6 +15,14 @@ keywords = ["tui", "terminal", "cli", "ui", "immediate-mode"]
categories = ["command-line-interface"]
rust-version = "1.81"
exclude = ["examples/", ".github/", "assets/", "AUDIT-REPORT.md", "CLAUDE.md"]
# Disable cargo's `examples/*.rs` auto-discovery: only the binaries listed
# below as `[[example]]` are exposed via `cargo run --example`. Source
# demos that compose into a tour (v020_*, cookbook_*, most demo_*,
# anim, async_demo, inline, error_boundary_demo) stay in examples/ but
# are reached only via `#[path = ...] mod` includes from the tour
# binaries. See `docs/DEMO_GUIDE.md` for the archetype rules and the
# v0.20 release notes for why these were merged into tours.
autoexamples = false

[lib]
name = "slt"
Expand Down Expand Up @@ -50,6 +58,7 @@ tree-sitter-yaml = { version = "0.7", optional = true }
criterion = { version = "0.5", features = ["html_reports"] }
insta = "1"
proptest = "1"
serde_json = "1"

[features]
crossterm = ["dep:crossterm"]
Expand Down Expand Up @@ -97,90 +106,84 @@ full = ["crossterm", "async", "serde", "image", "qrcode", "kitty-compress"]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]

[[example]]
name = "hello"
path = "examples/hello.rs"
# ── Tour binaries (6) — integrated demos covering v0.20 features and
# domain showcases. Each tour bundles 4–10 source demos via the
# DEMO_GUIDE.md archetype rules. Run these for end-to-end review.

[[example]]
name = "counter"
path = "examples/counter.rs"
name = "v020_tour"
path = "examples/v020_tour.rs"

[[example]]
name = "demo"
path = "examples/demo.rs"
name = "cookbook_tour"
path = "examples/cookbook_tour.rs"

[[example]]
name = "anim"
path = "examples/anim.rs"
name = "showcase_tour"
path = "examples/showcase_tour.rs"

[[example]]
name = "inline"
path = "examples/inline.rs"
name = "canvas_tour"
path = "examples/canvas_tour.rs"

[[example]]
name = "async_demo"
path = "examples/async_demo.rs"
required-features = ["async"]
name = "text_tour"
path = "examples/text_tour.rs"

[[example]]
name = "demo_dashboard"
path = "examples/demo_dashboard.rs"
name = "system_tour"
path = "examples/system_tour.rs"
required-features = ["async"]

[[example]]
name = "demo_cli"
path = "examples/demo_cli.rs"
# ── Standalone — entry / how-to-start (3)

[[example]]
name = "demo_spreadsheet"
path = "examples/demo_spreadsheet.rs"
name = "hello"
path = "examples/hello.rs"

[[example]]
name = "demo_website"
path = "examples/demo_website.rs"
name = "counter"
path = "examples/counter.rs"

[[example]]
name = "demo_infoviz"
path = "examples/demo_infoviz.rs"
name = "demo"
path = "examples/demo.rs"

[[example]]
name = "cookbook_dashboard"
path = "examples/cookbook_dashboard.rs"
# ── Standalone — performance measurement (2)

[[example]]
name = "cookbook_file_picker"
path = "examples/cookbook_file_picker.rs"
name = "perf_interactive"
path = "examples/perf_interactive.rs"

[[example]]
name = "cookbook_login"
path = "examples/cookbook_login.rs"
name = "perf_regression"
path = "examples/perf_regression.rs"

[[example]]
name = "cookbook_modal_toast"
path = "examples/cookbook_modal_toast.rs"
# ── Standalone — development tools (3)

[[example]]
name = "cookbook_table"
path = "examples/cookbook_table.rs"
name = "debug_selection"
path = "examples/debug_selection.rs"

[[example]]
name = "demo_fire"
path = "examples/demo_fire.rs"
name = "test_mouse"
path = "examples/test_mouse.rs"

[[example]]
name = "demo_game"
path = "examples/demo_game.rs"
name = "demo_key_test"
path = "examples/demo_key_test.rs"

[[example]]
name = "demo_kitty_image"
path = "examples/demo_kitty_image.rs"
# ── v0.20 non-interactive reports (2) — kept standalone because they
# print to stdout rather than render a TUI, so they don't compose
# into v020_tour.

[[example]]
name = "demo_cjk"
path = "examples/demo_cjk.rs"
name = "v020_perf_audit"
path = "examples/v020_perf_audit.rs"

[[example]]
name = "demo_overlay_anchor"
path = "examples/demo_overlay_anchor.rs"
name = "v020_test_utils"
path = "examples/v020_test_utils.rs"

[[bench]]
name = "benchmarks"
Expand Down
44 changes: 43 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ The same closure runs across several entry points. Pick one based on UI shape, n

```toml
[dependencies]
superlighttui = { version = "0.19", features = ["async", "image"] }
superlighttui = { version = "0.20", features = ["async", "image"] }
```

| Feature | What it adds |
Expand Down Expand Up @@ -258,6 +258,7 @@ For composition advice, see [Patterns Guide].
| [Animation Guide] | Tween, spring, keyframes, sequence, stagger |
| [Theming Guide] | Theme struct, presets, ThemeBuilder, custom themes |
| [Design Principles] | API constraints and design philosophy |
| [API Design] | Five consistency rules for new widgets and PR review checklist |

## Representative Examples

Expand All @@ -276,6 +277,46 @@ For composition advice, see [Patterns Guide].

The full categorized index lives in [Examples Guide].

### v0.20 Demo Catalog

Run any v020 demo directly:

| Demo | Issue | Showcases |
|---|---|---|
| `v020_showcase` | (integration) | All v0.20 features on one screen |
| `v020_regression_panel` | (integration) | v0.19 + v0.20 cumulative regression check |
| `v020_dx_shortcuts` | #209/#210/#220/#221 | on_hover, animate_bool, fill, center_in |
| `v020_use_state_keyed` | #215 | Dynamic-string-keyed state |
| `v020_use_effect` | #216 | Dependency-tracked effects |
| `v020_named_focus` | #217 | register_focusable_named, focus_by_name |
| `v020_theme_subtree` | #226 | Per-subtree theme override |
| `v020_modal_trap` | #225 | Modal tab_trap focus containment |
| `v020_spacing_scale` | #227 | compact / comfortable / spacious presets |
| `v020_split_pane` | #223 | split_pane / vsplit_pane with drag handle |
| `v020_gauge` | #224 | gauge / line_gauge builder APIs |
| `v020_gutter_highlights` | #235 | scrollable_with_gutter, GutterOpts |
| `v020_breadcrumb_response` | #213 | Builder breadcrumb API |
| `v020_progress_response` | #212 | progress / spinner returning Response |
| `v020_static_log` | #233 | ui.static_log() append-only scrollback |
| `v020_keymap_help` | #236 | WidgetKeyHelp + auto help overlay |
| `v020_ctrl_c_passthrough` | #238 | RunConfig::handle_ctrl_c opt-out |
| `v020_widthspec` | #237 | WidthSpec / HeightSpec sampler |
| `v020_perf_audit` | #204/205/206/228 | Allocation + timing report (stdout) |
| `v020_test_utils` | #229–232 | record_frames / sequence / snapshot_format / negative asserts (stdout) |

Or use the launcher script:

```bash
# Interactive picker
./scripts/ghostty_demos.sh

# All v0.20 demos at once (each in its own Ghostty window)
./scripts/ghostty_demos.sh --features

# Just the integration demos
./scripts/ghostty_demos.sh --showcase
```

## Custom Widgets And Backends

- Implement `Widget` when you want reusable high-level building blocks.
Expand Down Expand Up @@ -316,6 +357,7 @@ The release process expects format, check, clippy, tests, examples, and backend
[Patterns Guide]: docs/PATTERNS.md
[Architecture Guide]: docs/ARCHITECTURE.md
[Design Principles]: docs/DESIGN_PRINCIPLES.md
[API Design]: docs/API_DESIGN.md
[Animation Guide]: docs/ANIMATION.md
[Theming Guide]: docs/THEMING.md
[Features Guide]: docs/FEATURES.md
Expand Down
5 changes: 5 additions & 0 deletions _typos.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
"Pn" = "Pn"
"flate" = "flate"
"flate2" = "flate2"
# Intentional truncation-test prefixes used in tests/widgets.rs to verify
# label-clipping behaviour. They look like typos because they ARE bare
# truncations — that's the point of the assertions.
"Pytho" = "Pytho"
"Memor" = "Memor"

[files]
extend-exclude = [
Expand Down
53 changes: 46 additions & 7 deletions benches/benchmarks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@ fn bench_flush_full_redraw_200x60(c: &mut Criterion) {
let mut group = c.benchmark_group("flush");
group.bench_function("full_redraw_200x60", |b| {
let area = Rect::new(0, 0, 200, 60);
let prev = Buffer::empty(area);
let mut prev = Buffer::empty(area);
let mut curr = Buffer::empty(area);
fill_realistic(&mut curr, 1);
// Sanity: make sure we actually have a non-trivial diff workload.
Expand All @@ -403,10 +403,13 @@ fn bench_flush_full_redraw_200x60(c: &mut Criterion) {
let mut sink: Vec<u8> = Vec::with_capacity(256 * 1024);
b.iter(|| {
sink.clear();
slt::__bench_flush_buffer_diff(
// Issue #171: use the mutable bench entry point so the per-row
// hash refresh is part of the measured cost (matches what
// `Terminal::flush` does in production).
slt::__bench_flush_buffer_diff_mut(
&mut sink,
black_box(&curr),
black_box(&prev),
black_box(&mut curr),
black_box(&mut prev),
ColorDepth::TrueColor,
)
.expect("flush into Vec<u8> cannot fail");
Expand Down Expand Up @@ -437,10 +440,45 @@ fn bench_flush_sparse_change_200x60(c: &mut Criterion) {
let mut sink: Vec<u8> = Vec::with_capacity(64 * 1024);
b.iter(|| {
sink.clear();
slt::__bench_flush_buffer_diff(
slt::__bench_flush_buffer_diff_mut(
&mut sink,
black_box(&mut curr),
black_box(&mut prev),
ColorDepth::TrueColor,
)
.expect("flush into Vec<u8> cannot fail");
black_box(sink.len());
});
});
group.finish();
}

/// 0%-dirty (static) flush baseline for issue #171's GO/NO-GO decision.
///
/// Two identical buffers — `flush_buffer_diff` walks every cell, finds no
/// difference, and emits nothing. This is the worst case for the per-cell
/// scan because every cell pays the comparison cost while no output is
/// produced. If this bench stays under 50 µs on 200×60 we do **not**
/// implement the per-row hash skip (issue #171 NO-GO).
#[cfg(feature = "crossterm")]
fn bench_flush_static_200x60(c: &mut Criterion) {
let mut group = c.benchmark_group("flush");
group.bench_function("static_200x60", |b| {
let area = Rect::new(0, 0, 200, 60);
let mut prev = Buffer::empty(area);
let mut curr = Buffer::empty(area);
fill_realistic(&mut prev, 1);
fill_realistic(&mut curr, 1);
// Sanity: 0% dirty — diff must be empty.
debug_assert!(curr.diff(&prev).is_empty());

let mut sink: Vec<u8> = Vec::with_capacity(1024);
b.iter(|| {
sink.clear();
slt::__bench_flush_buffer_diff_mut(
&mut sink,
black_box(&curr),
black_box(&prev),
black_box(&mut curr),
black_box(&mut prev),
ColorDepth::TrueColor,
)
.expect("flush into Vec<u8> cannot fail");
Expand All @@ -458,6 +496,7 @@ fn bench_flush_group(c: &mut Criterion) {
{
bench_flush_full_redraw_200x60(c);
bench_flush_sparse_change_200x60(c);
bench_flush_static_200x60(c);
}
let _ = c;
}
Expand Down
Loading
Loading