Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
56d8fac
wip; tests using vterm
ynqa Mar 9, 2026
7ca6b62
wip; tests for resizing
ynqa Mar 9, 2026
9441b1e
wip; tests for middle_insert
ynqa Mar 9, 2026
a8587a2
wip;
ynqa Mar 9, 2026
2b0d29d
wip; define screen
ynqa Mar 9, 2026
b142ee0
chore: remove examples
ynqa Mar 9, 2026
51ca0a1
feat: create termharness proj
ynqa Mar 9, 2026
2de4a0c
chore: use termharness to test terminal screen after operations for r…
ynqa Mar 9, 2026
272e26b
chore: def error for Screen::line
ynqa Mar 9, 2026
62f1c38
tests: for Screen::*
ynqa Mar 9, 2026
86e88cb
chore: mk formatting.rs
ynqa Mar 9, 2026
f3e046f
chore: formatting.rs => screen_diff.rs
ynqa Mar 9, 2026
c7eefb2
chore: merge into screen_assert
ynqa Mar 9, 2026
afc8d38
tests: for screen_assert
ynqa Mar 9, 2026
121d19f
chore: create session and terminal
ynqa Mar 9, 2026
49d128a
chore: create Session::spawn to start pseudo-terminal
ynqa Mar 9, 2026
b40f845
tests: for Session::spawn
ynqa Mar 9, 2026
893af1b
chore: use Session::spawn instead
ynqa Mar 9, 2026
84527e0
fix: zsh reference
ynqa Mar 9, 2026
596228c
fix: zsh reference
ynqa Mar 9, 2026
388c6f8
chore: use alacritty_terminal instead
ynqa Mar 10, 2026
d56ecc6
tests: move left sometimes before experiments
ynqa Mar 10, 2026
c3d66ba
docs: why move left sometimes?
ynqa Mar 10, 2026
b5e4674
fix: move zsh_reference_capture to top
ynqa Mar 10, 2026
5d80468
fix: main => zsh_resize_wrap
ynqa Mar 10, 2026
df62da0
chore: mk zsh_middle_insert_wrap
ynqa Mar 10, 2026
f6874a7
fix: commodity for testing
ynqa Mar 10, 2026
e947d91
chore: move portable-pty to top
ynqa Mar 10, 2026
b0e5e90
chore: remove screen from termharness
ynqa Mar 10, 2026
ad94b26
tests: remove tests in readline once
ynqa Mar 10, 2026
435173e
chore: create zsh_pretend but no command execution
ynqa Mar 10, 2026
4533a2c
chore: define spawn session
ynqa Mar 10, 2026
ee674d2
on-bug: not work middle_insert_wrap execution
ynqa Mar 10, 2026
00ae1ad
fix: output middle_insert_wrap for zsh pretend
ynqa Mar 10, 2026
e564f27
feat: zsherio for comparing zsh to zsh-pretend
ynqa Mar 10, 2026
de490a2
chore: into zsherio
ynqa Mar 10, 2026
e180007
docs: about capture
ynqa Mar 10, 2026
46be559
chore: mv zsh_reference_capture/zsh_pretend => zsh_pretend
ynqa Mar 10, 2026
07a54e4
fix: clear and move_to
ynqa Mar 11, 2026
8f07c22
fix: shorter duration
ynqa Mar 11, 2026
ad0cffc
tests: zsh vs zsh pretend
ynqa Mar 11, 2026
1714dcc
tests: save as artifacts
ynqa Mar 11, 2026
dc75c6d
tests: resize wrap testing
ynqa Mar 11, 2026
9e7b75b
tests: improve visibilities
ynqa Mar 11, 2026
e9541de
fix: enable to capture cursor position on
ynqa Mar 11, 2026
488868a
chore: more understandable
ynqa Mar 11, 2026
b5d53ec
tests: start at current_position
ynqa Mar 11, 2026
7b66438
tests: zsh_pretend_matches_zsh_for_small_terminal_overflow
ynqa Mar 11, 2026
22df6e8
tests: zsh_pretend_matches_zsh_for_small_terminal_overflow (workaround)
ynqa Mar 11, 2026
1736806
chore: remove Deref/DerefMut for StyledGraphemes
ynqa Mar 11, 2026
3b41dee
chore: quote for zsh-pretend result
ynqa Mar 11, 2026
eca1ccc
tests: strip_outer_quotes
ynqa Mar 11, 2026
5cc1e20
tests: ignore zsh_pretend_matches_zsh_for_resize_wrap
ynqa Mar 11, 2026
8ebf240
chore: remove Pane and matrixify
ynqa Mar 11, 2026
2eb8f73
chore: GraphemeFactory => Widget
ynqa Mar 11, 2026
dc01145
fix: calc wrap_lines on text_editor create_graphemes
ynqa Mar 11, 2026
435161f
chore(workaround): nothing on resize
ynqa Mar 12, 2026
7c4a5a5
tests: remove assert_eq from termharness
ynqa Mar 12, 2026
681b79d
chore: zsh_pretend => zsh-render-parity
ynqa Mar 12, 2026
21bde8e
chore: common for testing
ynqa Mar 12, 2026
621b970
chore: tidy up functions for testing
ynqa Mar 12, 2026
5e7a2bc
chore: tidy up functions for testing
ynqa Mar 12, 2026
ef3d197
chore: tidy up functions for testing
ynqa Mar 12, 2026
8b68cc0
on-err: tidy up functions for testing
ynqa Mar 12, 2026
95bbbab
on-err: tidy up functions for testing
ynqa Mar 12, 2026
8ea943a
on-err: tidy up zsherio for testing
ynqa Mar 12, 2026
a6d2dec
chore: tidy up functions for testing
ynqa Mar 12, 2026
2ec3ccc
chore: rename tests for more strictly and properly
ynqa Mar 12, 2026
4a26769
docs: Concept.md
ynqa Mar 12, 2026
ac93102
docs: README.md
ynqa Mar 12, 2026
41acc9e
chore: update tapes
ynqa Mar 12, 2026
9fbfb05
chore: create script for tape => gif
ynqa Mar 12, 2026
075914e
fix: tree dir
ynqa Mar 12, 2026
35e9e14
cargo-fmt
ynqa Mar 12, 2026
a495b45
fix: no-enter on tapes
ynqa Mar 12, 2026
6c435c5
chore: remove event-dbg
ynqa Mar 12, 2026
06e354b
docs: renewal demo
ynqa Mar 12, 2026
0be3697
docs: use filepath instead of github links
ynqa Mar 12, 2026
4f3e439
fix: typo Consideration
ynqa Mar 13, 2026
cb553f8
fix: typo scenario
ynqa Mar 13, 2026
3a65cd8
Potential fix for pull request finding
ynqa Mar 13, 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@ 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/
6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
[workspace]
resolver = "2"
members = [
"event-dbg",
"examples/*",
"promkit",
"promkit-core",
"promkit-derive",
"promkit-widgets",
"termharness",
"zsh-render-parity",
"zsherio",
]

[workspace.dependencies]
Expand All @@ -15,11 +17,13 @@ async-trait = "0.1.89"
crossbeam-skiplist = "0.1.3"
crossterm = { version = "0.29.0", features = ["use-dev-tty", "event-stream", "serde"] }
futures = "0.3.32"
portable-pty = "0.9.0"
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"] }
termcfg = { version = "0.2.0", features = ["crossterm_0_29_0"] }
tokio = { version = "1.49.0", features = ["full"] }
thiserror = "2.0.18"
unicode-width = "0.2.2"
276 changes: 122 additions & 154 deletions Concept.md
Original file line number Diff line number Diff line change
@@ -1,201 +1,169 @@
# Concept

## Well-defined boundaries for responsibilities and modularization
## Responsibility Boundaries and Data Flow

The core design principle of promkit is the clear separation of the following three functions,
each implemented in dedicated modules:
promkit is organized around three responsibilities with clear boundaries:

- **Event Handlers**: Define behaviors for keyboard inputs (such as when <kbd>Enter</kbd> is pressed)
- **promkit**: Responsible for implementing [Prompt](https://docs.rs/promkit/0.10.0/promkit/trait.Prompt.html) trait, combining widgets and handling corresponding events
- The new async `Prompt` trait provides `initialize`, `evaluate`, and `finalize` methods for complete lifecycle management
- Event processing is now handled through a singleton `EventStream` for asynchronous event handling
1. **Event orchestration (`promkit`)**
- [`Prompt`](./promkit/src/lib.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`.
- Events are processed sequentially.

- **State Updates**: Managing and updating the internal state of widgets
- **promkit-widgets**: Responsible for state management of various widgets and pane generation
- Each widget implements
[PaneFactory](https://docs.rs/promkit-core/0.1.1/promkit_core/trait.PaneFactory.html)
trait to generate panes needed for rendering
2. **State management and UI materialization (`promkit-widgets` + `promkit-core`)**
- 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 states focus on state and projection only.

> [!IMPORTANT]
> The widgets themselves DO NOT contain event handlers
> - This prevents key operation conflicts
> when combining multiple widgets
> - e.g. When combining a listbox and text editor, <kbd>↓</kbd>
> behavior could potentially conflict
> - navigating the list vs. recalling input history

- **Rendering**: Processing to visually display the generated panes
- **promkit-core**: Responsible for basic terminal operations and concurrent rendering
- [SharedRenderer](https://docs.rs/promkit-core/0.2.0/promkit_core/render/type.SharedRenderer.html) (`Arc<Renderer<K>>`) provides thread-safe rendering with `SkipMap` for efficient pane management
- Components now actively trigger rendering (Push-based) rather than being rendered by the event loop
- [Terminal](https://docs.rs/promkit_core/0.1.1/terminal/struct.Terminal.html) handles rendering with `Mutex` for concurrent access
- Currently uses full rendering with plans to implement differential rendering in the future.
- [Pane](https://docs.rs/promkit_core/0.1.1/pane/struct.Pane.html)
defines the data structures for rendering

This separation allows each component to focus on a single responsibility,
making customization and extension easier.

### Event-Loop

These three functions collectively form the core of "event-loop" logic.
Here is the important part of the actual event-loop from the async
[Prompt::run](https://docs.rs/promkit/0.10.0/promkit/trait.Prompt.html#method.run):
> Widgets intentionally do not own event-loop policies.
> Event handling stays in presets or custom `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>`.
- `update` / `remove` modify chunks by index key.
- `render` delegates drawing to [`Terminal`](./promkit-core/src/terminal.rs).
- `Terminal::draw` performs wrapping, clearing, printing, and scrolling.

This keeps responsibilities explicit:
- prompt = control flow
- widgets = state to graphemes
- core renderer = terminal output

## Event Loop

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

```rust
// Initialize the prompt state
self.initialize().await?;

// Start the event loop
while let Some(event) = EVENT_STREAM.lock().await.next().await {
match event {
Ok(event) => {
// Evaluate the event and update state
// Current behavior: skip resize events in run loop.
if event.is_resize() {
continue;
}

if self.evaluate(&event).await? == Signal::Quit {
break;
}
}
Err(e) => {
eprintln!("Error reading event: {}", e);
break;
}
Err(_) => break,
}
}

// Finalize the prompt and return the result
self.finalize()
```

As a diagram:

```mermaid
flowchart LR
Initialize[Initilaize] --> A
subgraph promkit["promkit: event-loop"]
direction LR
A[Observe user input] --> B
B[Interpret as crossterm event] --> C

subgraph presets["promkit: presets"]
direction LR
C[Run operations corresponding to the observed events] --> D[Update state]

subgraph widgets["promkit-widgets"]
direction LR
D[Update state] --> |if needed| Y[Generate panes]
end

Y --> Z[Render widgets]
D --> E{Evaluate}
end

E -->|Continue| A
Init[Initialize] --> Observe

subgraph Runtime["promkit: Prompt::run"]
Observe[Read crossterm event] --> Eval[Prompt::evaluate]
Eval --> Continue{Signal}
Continue -->|Continue| Observe
end

E -->|Quit| Finalize[Finalize]
```
subgraph Preset["promkit presets / custom prompt"]
Eval --> UpdateState[Update widget states]
UpdateState --> Build[Widget::create_graphemes]
Build --> Push[Renderer::update]
Push --> Draw[Renderer::render]
end

In the current implementation of promkit, event handling is centralized and async.
All events are processed sequentially within the async
[Prompt::run](https://docs.rs/promkit/0.10.0/promkit/trait.Prompt.html#method.run)
method and propagated to each implementation through the
[Prompt::evaluate](https://docs.rs/promkit/0.10.0/promkit/trait.Prompt.html#tymethod.evaluate) method.
Draw --> Continue
Continue -->|Quit| Finalize[Finalize]
```

## Customizability

promkit allows customization at various levels.
You can choose the appropriate customization method
according to your use case.
promkit supports customization at two levels.

### 1. Configure existing presets

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

Using high-level APIs, you can easily customize existing preset components. For example, in
[preset::readline::Readline](https://github.com/ynqa/promkit/blob/v0.9.1/promkit/src/preset/readline.rs),
the following customizations are possible:
- title and style
- prefix and cursor styles
- suggestion and history
- masking
- word-break characters
- validator
- text editor visible line count
- evaluator override

```rust
let mut p = Readline::default()
// Set title text
.title("Custom Title")
// Change input prefix
.prefix("$ ")
// Prefix style
.prefix_style(ContentStyle {
foreground_color: Some(Color::DarkRed),
..Default::default()
})
// Active character style
.active_char_style(ContentStyle {
background_color: Some(Color::DarkCyan),
..Default::default()
})
// Inactive character style
.inactive_char_style(ContentStyle::default())
// Enable suggestion feature
.enable_suggest(Suggest::from_iter(["option1", "option2"]))
// Enable history feature
.enable_history()
// Input masking (for password input, etc.)
.mask('*')
// Set word break characters
.word_break_chars(HashSet::from([' ', '-']))
// Input validation feature
.validator(
|text| text.len() > 3,
|text| format!("Please enter more than 3 characters (current: {} characters)", text.len()),
)
// Register custom keymap
.register_keymap("custom", my_custom_keymap)
.prompt()?;
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(())
}
```

By combining these configuration options, you can significantly customize existing presets.

### Advanced Customization
### 2. Build your own prompt

Lower-level customization is also possible:
For advanced use cases, combine your own state + evaluator + renderer.

1. **Creating custom widgets**: You can create your own widgets equivalent to `promkit-widgets`.
By implementing
[PaneFactory](https://docs.rs/promkit-core/0.1.1/promkit_core/trait.PaneFactory.html)
trait for your data structure, you can use it like other standard widgets.
e.g. https://github.com/ynqa/empiriqa/blob/v0.1.0/src/queue.rs
- Implement `Widget` for custom state projection
- Implement `Prompt` for lifecycle and event handling
- Use `Renderer::update(...).render().await` whenever UI should change

2. **Defining custom presets**: By combining multiple widgets and implementing your own event handlers,
you can create completely customized presets. In that case, you need to implement the async
[Prompt](https://docs.rs/promkit/0.10.0/promkit/trait.Prompt.html) trait.
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.

This allows you to leave event-loop logic to promkit (i.e., you can execute the async
[Prompt::run](https://docs.rs/promkit/0.10.0/promkit/trait.Prompt.html#method.run))
while implementing your own rendering logic and event handling with full async support.
## Quality Strategy for Rendering Behavior

```rust
// Example of implementing the new Prompt trait
#[async_trait::async_trait]
impl Prompt for MyCustomPrompt {
type Index = MyIndex;
type Return = MyResult;

fn renderer(&self) -> SharedRenderer<Self::Index> {
self.renderer.clone()
}
Ensuring consistent rendering behavior across terminal environments is a key focus.
To achieve this, promkit includes a suite of test tools:

async fn initialize(&mut self) -> anyhow::Result<()> {
// Initialize your prompt state
self.renderer.render().await
}
- [`termharness`](./termharness)
- [`zsherio`](./zsherio)
- [`zsh-render-parity`](./zsh-render-parity)

async fn evaluate(&mut self, event: &Event) -> anyhow::Result<Signal> {
// Handle events and update state
match event {
// Your event handling logic
_ => Ok(Signal::Continue),
}
}

fn finalize(&mut self) -> anyhow::Result<Self::Return> {
// Produce final result
Ok(self.result.clone())
}
}
```
These tools compare prompt behavior against zsh-oriented scenarios
(e.g. wrapping, resize, and cursor movement), helping keep terminal behavior
predictable while the rendering internals evolve.
Loading
Loading