Concept
March 12, 2026 ยท View on GitHub
Responsibility Boundaries and Data Flow
promkit is organized around three responsibilities with clear boundaries:
-
Event orchestration (
promkit)Promptdefines lifecycle hooks:initialize -> evaluate -> finalizePrompt::runmanages terminal setup/teardown (raw mode, cursor visibility) and drives input events from a singletonEVENT_STREAM.- Events are processed sequentially.
-
State management and UI materialization (
promkit-widgets+promkit-core)- Each widget state implements
Widget. Widget::create_graphemes(width, height)returnsStyledGraphemes, which is the render-ready text unit including style and line breaks.- Widget states focus on state and projection only.
- Each widget state implements
Important
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.
- Rendering (
promkit-core)Renderer<K>stores ordered grapheme chunks inSkipMap<K, StyledGraphemes>.update/removemodify chunks by index key.renderdelegates drawing toTerminal.Terminal::drawperforms 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:
self.initialize().await?;
while let Some(event) = EVENT_STREAM.lock().await.next().await {
match event {
Ok(event) => {
// Current behavior: skip resize events in run loop.
if event.is_resize() {
continue;
}
if self.evaluate(&event).await? == Signal::Quit {
break;
}
}
Err(_) => break,
}
}
self.finalize()
As a diagram:
flowchart LR
Init[Initialize] --> Observe
subgraph Runtime["promkit: Prompt::run"]
Observe[Read crossterm event] --> Eval[Prompt::evaluate]
Eval --> Continue{Signal}
Continue -->|Continue| Observe
end
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
Draw --> Continue
Continue -->|Quit| Finalize[Finalize]
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
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(())
}
2. Build your own prompt
For advanced use cases, combine your own state + evaluator + renderer.
- Implement
Widgetfor custom state projection - Implement
Promptfor lifecycle and event handling - Use
Renderer::update(...).render().awaitwhenever UI should change
This is the same pattern used in examples/byop,
including async background updates (e.g. spinner/task monitor) that push
grapheme updates directly to the shared renderer.
Quality Strategy for Rendering Behavior
Ensuring consistent rendering behavior across terminal environments is a key focus. To achieve this, promkit includes a suite of test tools:
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.