Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
cb36c7b
feat: implement widget layout and hit testing
ynqa Jul 25, 2026
ed15f35
feat: add click handling to remaining widgets
ynqa Jul 25, 2026
a441a38
feat: add click handling to text editors
ynqa Jul 25, 2026
13607b8
test(core): cover overwide grapheme wrapping
ynqa Jul 25, 2026
1fdc36f
fix(core): preserve overwide graphemes when wrapping
ynqa Jul 25, 2026
dfe3ddd
chore: bump versions for v0.14.0
ynqa Jul 25, 2026
ce70889
Merge pull request #82 from ynqa/v0.14.0/mouse-click
ynqa Jul 25, 2026
14bf2f2
refactor: remove generic cursor abstraction
ynqa Jul 25, 2026
dfb438c
Merge pull request #85 from ynqa/refactor/remove-generic-cursor
ynqa Jul 25, 2026
e836a31
feat: confirm readline suggestions explicitly
ynqa Jul 25, 2026
6fb589b
Merge pull request #84 from ynqa/feat/confirm-readline-suggestion
ynqa Jul 25, 2026
38242d5
fix: bound structured rendering to viewport
ynqa Jul 25, 2026
df598db
bench: add structured widget benchmarks
ynqa Jul 25, 2026
455b660
bench: add structured data fixtures
ynqa Jul 25, 2026
4902f46
docs: update promkit-widgets overview
ynqa Jul 25, 2026
785576a
Merge pull request #86 from ynqa/fix/structured-viewport-performance
ynqa Jul 25, 2026
d4c751a
feat(widgets): add stable structured line numbers
ynqa Jul 25, 2026
a772ba8
fix(widgets): number collapsed YAML roots
ynqa Jul 25, 2026
df50ee1
Merge pull request #87 from ynqa/feat/show-line-number
ynqa Jul 26, 2026
7025960
test: cover stable structured viewports
ynqa Jul 26, 2026
9bbbe6d
feat: preserve structured viewports during navigation
ynqa Jul 26, 2026
c2cafdb
Merge pull request #88 from ynqa/feat/structured-stable-viewport
ynqa Jul 26, 2026
d879a49
refactor(core): extract renderer layout engine
ynqa Jul 26, 2026
9c70901
bench(core): cover renderer layout performance
ynqa Jul 26, 2026
39367db
Merge pull request #89 from ynqa/perf/renderer-layout-benchmark
ynqa Jul 26, 2026
0f28ca6
perf(core): reduce renderer layout cloning
ynqa Jul 26, 2026
d52d91c
Merge pull request #90 from ynqa/perf/reduce-renderer-layout-clones
ynqa Jul 26, 2026
5d0dc4d
test(readline): reproduce stale rows after rapid resize
ynqa Jul 26, 2026
4a7a812
test(readline): preserve preceding terminal output on resize
ynqa Jul 26, 2026
7c30ae2
test(core): specify reversible terminal scrolling
ynqa Jul 26, 2026
8662ff8
fix(core): preserve terminal output across resize
ynqa Jul 26, 2026
c999602
Merge pull request #91 from ynqa/test/readline-rapid-resize-regression
ynqa Jul 26, 2026
36abb5f
test(readline): migrate resize regression to scenario
ynqa Jul 26, 2026
8f6ef06
test(readline): use explicit wait action names
ynqa Jul 26, 2026
77451e8
test(widgets): specify multiline text editor behavior
ynqa Jul 26, 2026
d6f02fd
feat(widgets): support multiline text editing
ynqa Jul 26, 2026
a5f431f
feat(promkit): add multiline text editor preset
ynqa Jul 26, 2026
f2bd9ad
test(text-editor): specify continuation indentation
ynqa Jul 26, 2026
eaa19ca
feat(text-editor): add continuation indentation
ynqa Jul 26, 2026
1868f43
test(text-editor): require blank line to close blocks
ynqa Jul 26, 2026
93817aa
feat(text-editor): submit blocks on blank line
ynqa Jul 26, 2026
365a73e
test(text-editor): add terminal rendering scenarios
ynqa Jul 26, 2026
5316720
refactor(repl): build multiline editor from widgets
ynqa Jul 27, 2026
ceae20f
docs: group advanced examples
ynqa Jul 27, 2026
5b2e519
docs: add advanced example tapes
ynqa Jul 27, 2026
82d9229
docs: refine REPL demo
ynqa Jul 27, 2026
5f337b0
docs: add advanced example demos
ynqa Jul 27, 2026
78521d8
examples: rename BYOP demo to async task
ynqa Jul 27, 2026
50ccf28
Merge pull request #92 from ynqa/feat/multiline-text-editor
ynqa Jul 27, 2026
579451a
tests: split YAML navigation tests by operation
ynqa Jul 27, 2026
c311d81
bench(widgets): add table fixture baseline
ynqa Jul 27, 2026
9e88ada
feat(widgets): add performant CSV table widget
ynqa Jul 27, 2026
b57e5c1
feat(examples): add CSV table viewer
ynqa Jul 27, 2026
7a66108
feat(widgets): scroll tables by display cell
ynqa Jul 27, 2026
bd1bdcc
feat(widgets): make table scroll distance configurable
ynqa Jul 27, 2026
fc548f4
fix(examples): align CSV mouse scrolling with macOS
ynqa Jul 27, 2026
06b51be
Merge pull request #93 from ynqa/feat/csv
ynqa Jul 27, 2026
d97f887
Refactor promkit feature configuration
ynqa Jul 27, 2026
41a6f13
Organize promkit features into sections
ynqa Jul 27, 2026
0bce5a7
Replace presets with composable examples
ynqa Jul 27, 2026
64f0481
Remove readline builder configuration
ynqa Jul 27, 2026
8118338
Restore example behavior parity
ynqa Jul 27, 2026
1627788
Add prefix search widget
ynqa Jul 27, 2026
b07d8a2
Update composition architecture documentation
ynqa Jul 27, 2026
35f2198
Document benchmark validation policy
ynqa Jul 27, 2026
4cc5745
Document project direction since v0.14.0
ynqa Jul 27, 2026
c1b5dd8
Document prompt runtime lifecycle
ynqa Jul 28, 2026
d4915f1
Merge pull request #94 from ynqa/refactor/preset
ynqa Jul 28, 2026
66a4041
Split structured benchmarks by format
ynqa Jul 28, 2026
0b91648
Benchmark structured value loading paths
ynqa Jul 28, 2026
a6e86d5
Parse structured documents directly into rows
ynqa Jul 28, 2026
33d6308
Merge pull request #95 from ynqa/perf/structured-loading
ynqa Jul 28, 2026
6da7afa
Add configurable terminal sessions
ynqa Jul 28, 2026
7690b2e
Apply terminal modes consistently
ynqa Jul 28, 2026
251c218
Use terminal sessions in readline fixtures
ynqa Jul 28, 2026
569095a
Merge pull request #96 from ynqa/feat/terminal-session
ynqa Jul 28, 2026
5ceafc4
docs: streamline promkit-widgets overview
ynqa Jul 28, 2026
8186f35
docs: add CSV and Kubernetes demo tapes
ynqa Jul 28, 2026
663852a
docs: add example index and CSV demo
ynqa Jul 28, 2026
c55c986
docs: refresh v0.14 feature overview
ynqa Jul 28, 2026
43ef6fe
Merge pull request #97 from ynqa/docs
ynqa Jul 28, 2026
3627096
Organize tests by type and function
ynqa Jul 28, 2026
64b6078
Merge pull request #99 from ynqa/refactor/nest-unit-tests-by-function
ynqa Jul 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
6 changes: 4 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,7 @@ Cargo.lock
# Ignore GIF files in the tapes directory
tapes/*.gif

# Ignore test artifacts emitted by zsh-render-parity integration tests
zsh-render-parity/.artifacts/
# Keep tape fixtures under version control
!tapes/*.csv
!tapes/*.json
!tapes/*.yaml
61 changes: 55 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ built on top of [crossterm](https://github.com/crossterm-rs/crossterm).

## Workspace Map (High Level)

- `promkit/`: public prompt presets and `Prompt` lifecycle
- `promkit/`: optional `Prompt` lifecycle runtime, capabilities, and widget facade
- `promkit-core/`: rendering primitives and terminal drawing
- `promkit-widgets/`: reusable widget states
- `promkit-derive/`: proc macros
Expand All @@ -34,29 +34,59 @@ built on top of [crossterm](https://github.com/crossterm-rs/crossterm).

The authoritative model is in [Concept.md](./Concept.md). Keep these boundaries:

1. Event orchestration belongs to `promkit` (`Prompt` lifecycle).
2. `promkit-widgets` is state-to-view projection, without event-loop policy.
3. Rendering concerns belong to `promkit-core`.
1. The `Prompt` lifecycle and terminal event stream belong to `promkit`.
2. Key bindings, focus transitions, and other event policy belong to application
`Prompt` implementations, including the examples.
3. `promkit-widgets` is state-to-view projection, without event-loop policy.
4. Rendering concerns belong to `promkit-core`.

When adding features, preserve these boundaries before optimizing code layout.

## Coding Conventions

### Test Organization

- Name the outer inline test module `tests`.
- Group tests under a module named after the function or method under test.
- When a file defines multiple production types, add a type-level module before
the function-level module.
- Name test functions after the behavior or scenario being verified. Do not use
generic names such as `test` or repeat the function name in a `test_*` prefix.

For example:

```rust
#[cfg(test)]
mod tests {
mod widget_viewport {
mod scroll_to_include {
#[test]
fn does_not_scroll_while_the_position_is_visible() {
// ...
}
}
}
}
```

### Feature Wiring

- Control module exposure via feature flags.
- Evidence: [promkit-widgets/src/lib.rs](./promkit-widgets/src/lib.rs), [promkit/src/preset.rs](./promkit/src/preset.rs)
- Evidence: [promkit-widgets/src/lib.rs](./promkit-widgets/src/lib.rs), [promkit/src/lib.rs](./promkit/src/lib.rs)
- Wire `promkit` features to `promkit-widgets` features.
- Evidence: [promkit/Cargo.toml](./promkit/Cargo.toml), [promkit-widgets/Cargo.toml](./promkit-widgets/Cargo.toml)

## Change Workflow for Agents

When implementing a change:

1. Locate the boundary first (`promkit` vs `promkit-widgets` vs `promkit-core`).
1. Locate the boundary first (application vs `promkit` vs `promkit-widgets` vs
`promkit-core`).
2. Make the smallest coherent edit set.
3. Update tests/examples/docs that demonstrate behavior.
4. Run validation commands locally when possible.
5. For non-trivial implementation work, run the relevant `cargo bench` target
before and after the change and check for performance regressions.

Recommended commands:

Expand All @@ -68,6 +98,25 @@ cargo test -- --nocapture --format pretty

For broader changes, also build example crates.

### Performance Validation

- Benchmark new widgets, data structures, rendering paths, algorithms, and other
substantial implementations.
- Compare against a pre-change result, using Criterion named baselines when the
relevant benchmark supports them.
- Investigate and report meaningful regressions instead of relying only on a
successful benchmark run.
- Documentation-only edits, formatting, and small changes outside
performance-sensitive paths may omit benchmarks. Small changes to hot paths
still require benchmark comparison.

Select the benchmark and feature flags documented by the relevant crate. A
typical invocation is:

```bash
cargo bench -p <crate> --bench <target> --features <features>
```

## What Not to Put Here

- Exhaustive feature tables copied from `Cargo.toml`
Expand Down
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ members = [
[workspace.dependencies]
anyhow = "1.0.102"
async-trait = "0.1.89"
bitflags = "2.9.0"
crossbeam-skiplist = "0.1.3"
crossterm = { version = "0.29.0", features = ["use-dev-tty", "event-stream", "serde"] }
csv = "1.3.1"
futures = "0.3.32"
radix_trie = "0.3.0"
rayon = "1.11.0"
scopeguard = "1.2.0"
serde = "1.0.228"
serde_json = { version = "1.0.149", features = ["preserve_order"] }
serde_yaml = "0.9.34"
Expand Down
171 changes: 92 additions & 79 deletions Concept.md
Original file line number Diff line number Diff line change
@@ -1,44 +1,98 @@
# Concept

## Direction Since v0.14.0

Starting with v0.14.0, promkit is developed around two primary goals:

1. Expand `promkit-widgets` with reusable state and view projections that
applications can combine without adopting a framework-owned event policy.
2. Improve `promkit-core` rendering performance and correctness so wrapping,
resizing, scrolling, viewport movement, and repeated redraws remain stable
without leaving stale or visually corrupted terminal content.

This direction emerged from a feedback loop between the library and applications
built with it, including [jnv](https://github.com/ynqa/jnv) and
[sig](https://github.com/ynqa/sig). Those applications showed that a useful
terminal UI eventually needs an application-specific runtime and event loop.
Modes, key bindings, focus, background work, cancellation, validation, and
domain state transitions cannot be generalized into prompt presets without
constraining the application or adding increasingly application-specific
configuration.

The resulting development loop is:

1. Application development exposes rendering, state, and interaction needs.
2. Reusable state-to-view behavior is extracted into `promkit-widgets`.
3. Rendering correctness and performance improvements are made in
`promkit-core` and verified against regression scenarios and benchmarks.
4. Application-specific orchestration remains in the application, while
examples document useful compositions.

For this reason, the `promkit` crate no longer owns preset implementations.
It provides an optional `Prompt` lifecycle runtime, capabilities, and a widget
facade for applications that find them useful, but it is not intended to replace
an application's event loop. Removing presets is therefore not only a code
reorganization; it establishes application-owned orchestration as the project
direction from v0.14.0 onward.

## Responsibility Boundaries and Data Flow

promkit is organized around three responsibilities with clear boundaries:
promkit is organized around four responsibilities with clear boundaries:

1. **Event orchestration (`promkit`)**
- [`Prompt`](./promkit/src/lib.rs) defines lifecycle hooks:
1. **Prompt lifecycle runtime (`promkit`)**
- [`Prompt`](./promkit/src/runtime.rs) defines lifecycle hooks:
`initialize -> evaluate -> finalize`
- [`Prompt::run`](./promkit/src/lib.rs) manages terminal setup/teardown
(raw mode, cursor visibility) and drives input events from a singleton
`EVENT_STREAM`.
- [`Prompt::run`](./promkit/src/runtime.rs) drives input events from a
singleton `EVENT_STREAM`.
- `TerminalSession` manages opt-in terminal setup/teardown separately from
the prompt lifecycle.
- Events are processed sequentially.

2. **State management and UI materialization (`promkit-widgets` + `promkit-core`)**
2. **Application event policy (`examples` and downstream applications)**
- Applications implement `Prompt` by combining the widget states they need.
- Key bindings, focus transitions, validation flow, and quit conditions stay
in the application.
- The examples are reference compositions, not APIs exported by `promkit`.

3. **State management and UI materialization (`promkit-widgets`)**
- Each widget state implements [`Widget`](./promkit-core/src/lib.rs).
- `Widget::create_graphemes(width, height)` returns
[`StyledGraphemes`](./promkit-core/src/grapheme.rs), which is the render-ready
text unit including style and line breaks.
- `Widget::create_graphemes()` returns `CreatedGraphemes`: width-independent
styled content, layout hints, and an optional logical cursor position.
- Large widgets can override `create_graphemes_in_viewport(width, height)` to
project only content that can be displayed. Prompts that use this path
obtain the current terminal size before updating the renderer.
- Widget states focus on state and projection only.

> [!IMPORTANT]
> Widgets intentionally do not own event-loop policies.
> Event handling stays in presets or custom `Prompt` implementations,
> Event handling stays in application `Prompt` implementations,
> which avoids key-binding conflicts when multiple widgets are combined.

3. **Rendering (`promkit-core`)**
- [`Renderer<K>`](./promkit-core/src/render.rs) stores ordered grapheme chunks in
`SkipMap<K, StyledGraphemes>`.
4. **Rendering (`promkit-core`)**
- [`Renderer<K>`](./promkit-core/src/render.rs) stores ordered
`CreatedGraphemes` chunks.
- [`RendererLayout<K>`](./promkit-core/src/render/layout.rs) performs
terminal-size-dependent wrapping, pane allocation, cursor scrolling, and
viewport clipping without terminal I/O.
- `update` / `remove` modify chunks by index key.
- `render` wraps or truncates content, assigns vertical viewports, scrolls
viewports to include logical cursors, and saves a keyed layout snapshot.
- The layout snapshot supports screen-to-widget hit testing and the inverse
widget-to-screen position mapping.
- `render` delegates drawing to [`Terminal`](./promkit-core/src/terminal.rs).
- `Terminal::draw` performs wrapping, clearing, printing, and scrolling.
- `Terminal::draw_rows` performs clearing, printing, and terminal scrolling
after layout is complete.

This keeps responsibilities explicit:
- prompt = control flow
- runtime = prompt lifecycle and terminal event stream
- terminal session = terminal mode lifecycle
- application prompt = event and focus policy
- widgets = state to graphemes
- core renderer = terminal output

## Event Loop

Current core loop in [`Prompt::run`](./promkit/src/lib.rs):
Current core loop in [`Prompt::run`](./promkit/src/runtime.rs):

```rust
self.initialize().await?;
Expand Down Expand Up @@ -74,7 +128,7 @@ flowchart LR
Continue -->|Continue| Observe
end

subgraph Preset["promkit presets / custom prompt"]
subgraph Application["application prompt / examples"]
Eval --> UpdateState[Update widget states]
UpdateState --> Build[Widget::create_graphemes]
Build --> Push[Renderer::update]
Expand All @@ -87,73 +141,32 @@ flowchart LR

## Customizability

promkit supports customization at two levels.

### 1. Configure existing presets

High-level presets (e.g. `Readline`) expose builder-style options such as:

- title and style
- prefix and cursor styles
- suggestion and history
- masking
- word-break characters
- validator
- text editor visible line count
- evaluator override

```rust
use std::collections::HashSet;

use promkit::{
Prompt,
core::crossterm::style::{Color, ContentStyle},
preset::readline::Readline,
suggest::Suggest,
};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
let result = Readline::default()
.title("Custom Title")
.prefix("$ ")
.prefix_style(ContentStyle {
foreground_color: Some(Color::DarkRed),
..Default::default()
})
.active_char_style(ContentStyle {
background_color: Some(Color::DarkCyan),
..Default::default()
})
.inactive_char_style(ContentStyle::default())
.enable_suggest(Suggest::from_iter(["option1", "option2"]))
.enable_history()
.mask('*')
.word_break_chars(HashSet::from([' ', '-']))
.text_editor_lines(3)
.validator(
|text| text.len() > 3,
|text| format!("Please enter more than 3 characters (current: {})", text.len()),
)
.run()
.await?;

println!("result: {result}");
Ok(())
}
Applications select the capabilities and widgets they need through Cargo
features. The runtime is independent from the widget set:

```toml
promkit = { version = "0.14.0", features = [
"runtime",
"validate",
"prefixsearch",
"text",
"texteditor",
] }
```

### 2. Build your own prompt

For advanced use cases, combine your own state + evaluator + renderer.
The application then combines its own state, event policy, and renderer:

- Implement `Widget` for custom state projection
- Use the widget states required by the application
- Implement `Prompt` for lifecycle and event handling
- Use `Renderer::update(...).render().await` whenever UI should change

This is the same pattern used in [`examples/byop`](./examples/byop/src/byop.rs),
including async background updates (e.g. spinner/task monitor) that push
grapheme updates directly to the shared renderer.
The examples are the reference implementations for these compositions.
[`examples/readline`](./examples/readline/) combines text, text-editor, and
prefix-search widgets with validation. `PrefixSearch` retains its radix trie,
active query, and selection as widget state and projects matching candidates
directly without repackaging them into a listbox.
[`examples/async_task`](./examples/async_task/) demonstrates background updates
that push grapheme changes directly to a shared renderer.

## Quality Strategy for Rendering Behavior

Expand Down
Loading
Loading