|
1 | 1 | # Concept |
2 | 2 |
|
3 | | -## Well-defined boundaries for responsibilities and modularization |
| 3 | +## Responsibility Boundaries and Data Flow |
4 | 4 |
|
5 | | -The core design principle of promkit is the clear separation of the following three functions, |
6 | | -each implemented in dedicated modules: |
| 5 | +promkit is organized around three responsibilities with clear boundaries: |
7 | 6 |
|
8 | | -- **Event Handlers**: Define behaviors for keyboard inputs (such as when <kbd>Enter</kbd> is pressed) |
9 | | - - **promkit**: Responsible for implementing [Prompt](https://docs.rs/promkit/0.10.0/promkit/trait.Prompt.html) trait, combining widgets and handling corresponding events |
10 | | - - The new async `Prompt` trait provides `initialize`, `evaluate`, and `finalize` methods for complete lifecycle management |
11 | | - - Event processing is now handled through a singleton `EventStream` for asynchronous event handling |
| 7 | +1. **Event orchestration (`promkit`)** |
| 8 | + - [`Prompt`](./promkit/src/lib.rs) defines lifecycle hooks: |
| 9 | + `initialize -> evaluate -> finalize` |
| 10 | + - [`Prompt::run`](./promkit/src/lib.rs) manages terminal setup/teardown |
| 11 | + (raw mode, cursor visibility) and drives input events from a singleton |
| 12 | + `EVENT_STREAM`. |
| 13 | + - Events are processed sequentially. |
12 | 14 |
|
13 | | -- **State Updates**: Managing and updating the internal state of widgets |
14 | | - - **promkit-widgets**: Responsible for state management of various widgets and pane generation |
15 | | - - Each widget implements |
16 | | - [PaneFactory](https://docs.rs/promkit-core/0.1.1/promkit_core/trait.PaneFactory.html) |
17 | | - trait to generate panes needed for rendering |
| 15 | +2. **State management and UI materialization (`promkit-widgets` + `promkit-core`)** |
| 16 | + - Each widget state implements [`Widget`](./promkit-core/src/lib.rs). |
| 17 | + - `Widget::create_graphemes(width, height)` returns |
| 18 | + [`StyledGraphemes`](./promkit-core/src/grapheme.rs), which is the render-ready |
| 19 | + text unit including style and line breaks. |
| 20 | + - Widget states focus on state and projection only. |
18 | 21 |
|
19 | 22 | > [!IMPORTANT] |
20 | | -> The widgets themselves DO NOT contain event handlers |
21 | | -> - This prevents key operation conflicts |
22 | | -> when combining multiple widgets |
23 | | -> - e.g. When combining a listbox and text editor, <kbd>↓</kbd> |
24 | | -> behavior could potentially conflict |
25 | | -> - navigating the list vs. recalling input history |
26 | | -
|
27 | | -- **Rendering**: Processing to visually display the generated panes |
28 | | - - **promkit-core**: Responsible for basic terminal operations and concurrent rendering |
29 | | - - [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 |
30 | | - - Components now actively trigger rendering (Push-based) rather than being rendered by the event loop |
31 | | - - [Terminal](https://docs.rs/promkit_core/0.1.1/terminal/struct.Terminal.html) handles rendering with `Mutex` for concurrent access |
32 | | - - Currently uses full rendering with plans to implement differential rendering in the future. |
33 | | - - [Pane](https://docs.rs/promkit_core/0.1.1/pane/struct.Pane.html) |
34 | | - defines the data structures for rendering |
35 | | - |
36 | | -This separation allows each component to focus on a single responsibility, |
37 | | -making customization and extension easier. |
38 | | - |
39 | | -### Event-Loop |
40 | | - |
41 | | -These three functions collectively form the core of "event-loop" logic. |
42 | | -Here is the important part of the actual event-loop from the async |
43 | | -[Prompt::run](https://docs.rs/promkit/0.10.0/promkit/trait.Prompt.html#method.run): |
| 23 | +> Widgets intentionally do not own event-loop policies. |
| 24 | +> Event handling stays in presets or custom `Prompt` implementations, |
| 25 | +> which avoids key-binding conflicts when multiple widgets are combined. |
| 26 | +
|
| 27 | +3. **Rendering (`promkit-core`)** |
| 28 | + - [`Renderer<K>`](./promkit-core/src/render.rs) stores ordered grapheme chunks in |
| 29 | + `SkipMap<K, StyledGraphemes>`. |
| 30 | + - `update` / `remove` modify chunks by index key. |
| 31 | + - `render` delegates drawing to [`Terminal`](./promkit-core/src/terminal.rs). |
| 32 | + - `Terminal::draw` performs wrapping, clearing, printing, and scrolling. |
| 33 | + |
| 34 | +This keeps responsibilities explicit: |
| 35 | +- prompt = control flow |
| 36 | +- widgets = state to graphemes |
| 37 | +- core renderer = terminal output |
| 38 | + |
| 39 | +## Event Loop |
| 40 | + |
| 41 | +Current core loop in [`Prompt::run`](./promkit/src/lib.rs): |
44 | 42 |
|
45 | 43 | ```rust |
46 | | -// Initialize the prompt state |
47 | 44 | self.initialize().await?; |
48 | 45 |
|
49 | | -// Start the event loop |
50 | 46 | while let Some(event) = EVENT_STREAM.lock().await.next().await { |
51 | 47 | match event { |
52 | 48 | Ok(event) => { |
53 | | - // Evaluate the event and update state |
| 49 | + // Current behavior: skip resize events in run loop. |
| 50 | + if event.is_resize() { |
| 51 | + continue; |
| 52 | + } |
| 53 | + |
54 | 54 | if self.evaluate(&event).await? == Signal::Quit { |
55 | 55 | break; |
56 | 56 | } |
57 | 57 | } |
58 | | - Err(e) => { |
59 | | - eprintln!("Error reading event: {}", e); |
60 | | - break; |
61 | | - } |
| 58 | + Err(_) => break, |
62 | 59 | } |
63 | 60 | } |
64 | 61 |
|
65 | | -// Finalize the prompt and return the result |
66 | 62 | self.finalize() |
67 | 63 | ``` |
68 | 64 |
|
69 | 65 | As a diagram: |
70 | 66 |
|
71 | 67 | ```mermaid |
72 | 68 | flowchart LR |
73 | | - Initialize[Initilaize] --> A |
74 | | - subgraph promkit["promkit: event-loop"] |
75 | | - direction LR |
76 | | - A[Observe user input] --> B |
77 | | - B[Interpret as crossterm event] --> C |
78 | | -
|
79 | | - subgraph presets["promkit: presets"] |
80 | | - direction LR |
81 | | - C[Run operations corresponding to the observed events] --> D[Update state] |
82 | | -
|
83 | | - subgraph widgets["promkit-widgets"] |
84 | | - direction LR |
85 | | - D[Update state] --> |if needed| Y[Generate panes] |
86 | | - end |
87 | | -
|
88 | | - Y --> Z[Render widgets] |
89 | | - D --> E{Evaluate} |
90 | | - end |
91 | | -
|
92 | | - E -->|Continue| A |
| 69 | + Init[Initialize] --> Observe |
| 70 | +
|
| 71 | + subgraph Runtime["promkit: Prompt::run"] |
| 72 | + Observe[Read crossterm event] --> Eval[Prompt::evaluate] |
| 73 | + Eval --> Continue{Signal} |
| 74 | + Continue -->|Continue| Observe |
93 | 75 | end |
94 | 76 |
|
95 | | - E -->|Quit| Finalize[Finalize] |
96 | | -``` |
| 77 | + subgraph Preset["promkit presets / custom prompt"] |
| 78 | + Eval --> UpdateState[Update widget states] |
| 79 | + UpdateState --> Build[Widget::create_graphemes] |
| 80 | + Build --> Push[Renderer::update] |
| 81 | + Push --> Draw[Renderer::render] |
| 82 | + end |
97 | 83 |
|
98 | | -In the current implementation of promkit, event handling is centralized and async. |
99 | | -All events are processed sequentially within the async |
100 | | -[Prompt::run](https://docs.rs/promkit/0.10.0/promkit/trait.Prompt.html#method.run) |
101 | | -method and propagated to each implementation through the |
102 | | -[Prompt::evaluate](https://docs.rs/promkit/0.10.0/promkit/trait.Prompt.html#tymethod.evaluate) method. |
| 84 | + Draw --> Continue |
| 85 | + Continue -->|Quit| Finalize[Finalize] |
| 86 | +``` |
103 | 87 |
|
104 | 88 | ## Customizability |
105 | 89 |
|
106 | | -promkit allows customization at various levels. |
107 | | -You can choose the appropriate customization method |
108 | | -according to your use case. |
| 90 | +promkit supports customization at two levels. |
| 91 | + |
| 92 | +### 1. Configure existing presets |
109 | 93 |
|
110 | | -### Customize as configures |
| 94 | +High-level presets (e.g. `Readline`) expose builder-style options such as: |
111 | 95 |
|
112 | | -Using high-level APIs, you can easily customize existing preset components. For example, in |
113 | | -[preset::readline::Readline](https://github.com/ynqa/promkit/blob/v0.9.1/promkit/src/preset/readline.rs), |
114 | | -the following customizations are possible: |
| 96 | +- title and style |
| 97 | +- prefix and cursor styles |
| 98 | +- suggestion and history |
| 99 | +- masking |
| 100 | +- word-break characters |
| 101 | +- validator |
| 102 | +- text editor visible line count |
| 103 | +- evaluator override |
115 | 104 |
|
116 | 105 | ```rust |
117 | | -let mut p = Readline::default() |
118 | | - // Set title text |
119 | | - .title("Custom Title") |
120 | | - // Change input prefix |
121 | | - .prefix("$ ") |
122 | | - // Prefix style |
123 | | - .prefix_style(ContentStyle { |
124 | | - foreground_color: Some(Color::DarkRed), |
125 | | - ..Default::default() |
126 | | - }) |
127 | | - // Active character style |
128 | | - .active_char_style(ContentStyle { |
129 | | - background_color: Some(Color::DarkCyan), |
130 | | - ..Default::default() |
131 | | - }) |
132 | | - // Inactive character style |
133 | | - .inactive_char_style(ContentStyle::default()) |
134 | | - // Enable suggestion feature |
135 | | - .enable_suggest(Suggest::from_iter(["option1", "option2"])) |
136 | | - // Enable history feature |
137 | | - .enable_history() |
138 | | - // Input masking (for password input, etc.) |
139 | | - .mask('*') |
140 | | - // Set word break characters |
141 | | - .word_break_chars(HashSet::from([' ', '-'])) |
142 | | - // Input validation feature |
143 | | - .validator( |
144 | | - |text| text.len() > 3, |
145 | | - |text| format!("Please enter more than 3 characters (current: {} characters)", text.len()), |
146 | | - ) |
147 | | - // Register custom keymap |
148 | | - .register_keymap("custom", my_custom_keymap) |
149 | | - .prompt()?; |
| 106 | +use std::collections::HashSet; |
| 107 | + |
| 108 | +use promkit::{ |
| 109 | + Prompt, |
| 110 | + core::crossterm::style::{Color, ContentStyle}, |
| 111 | + preset::readline::Readline, |
| 112 | + suggest::Suggest, |
| 113 | +}; |
| 114 | + |
| 115 | +#[tokio::main] |
| 116 | +async fn main() -> anyhow::Result<()> { |
| 117 | + let result = Readline::default() |
| 118 | + .title("Custom Title") |
| 119 | + .prefix("$ ") |
| 120 | + .prefix_style(ContentStyle { |
| 121 | + foreground_color: Some(Color::DarkRed), |
| 122 | + ..Default::default() |
| 123 | + }) |
| 124 | + .active_char_style(ContentStyle { |
| 125 | + background_color: Some(Color::DarkCyan), |
| 126 | + ..Default::default() |
| 127 | + }) |
| 128 | + .inactive_char_style(ContentStyle::default()) |
| 129 | + .enable_suggest(Suggest::from_iter(["option1", "option2"])) |
| 130 | + .enable_history() |
| 131 | + .mask('*') |
| 132 | + .word_break_chars(HashSet::from([' ', '-'])) |
| 133 | + .text_editor_lines(3) |
| 134 | + .validator( |
| 135 | + |text| text.len() > 3, |
| 136 | + |text| format!("Please enter more than 3 characters (current: {})", text.len()), |
| 137 | + ) |
| 138 | + .run() |
| 139 | + .await?; |
| 140 | + |
| 141 | + println!("result: {result}"); |
| 142 | + Ok(()) |
| 143 | +} |
150 | 144 | ``` |
151 | 145 |
|
152 | | -By combining these configuration options, you can significantly customize existing presets. |
153 | | - |
154 | | -### Advanced Customization |
| 146 | +### 2. Build your own prompt |
155 | 147 |
|
156 | | -Lower-level customization is also possible: |
| 148 | +For advanced use cases, combine your own state + evaluator + renderer. |
157 | 149 |
|
158 | | -1. **Creating custom widgets**: You can create your own widgets equivalent to `promkit-widgets`. |
159 | | -By implementing |
160 | | -[PaneFactory](https://docs.rs/promkit-core/0.1.1/promkit_core/trait.PaneFactory.html) |
161 | | -trait for your data structure, you can use it like other standard widgets. |
162 | | -e.g. https://github.com/ynqa/empiriqa/blob/v0.1.0/src/queue.rs |
| 150 | +- Implement `Widget` for custom state projection |
| 151 | +- Implement `Prompt` for lifecycle and event handling |
| 152 | +- Use `Renderer::update(...).render().await` whenever UI should change |
163 | 153 |
|
164 | | -2. **Defining custom presets**: By combining multiple widgets and implementing your own event handlers, |
165 | | -you can create completely customized presets. In that case, you need to implement the async |
166 | | -[Prompt](https://docs.rs/promkit/0.10.0/promkit/trait.Prompt.html) trait. |
| 154 | +This is the same pattern used in [`examples/byop`](./examples/byop/src/byop.rs), |
| 155 | +including async background updates (e.g. spinner/task monitor) that push |
| 156 | +grapheme updates directly to the shared renderer. |
167 | 157 |
|
168 | | -This allows you to leave event-loop logic to promkit (i.e., you can execute the async |
169 | | -[Prompt::run](https://docs.rs/promkit/0.10.0/promkit/trait.Prompt.html#method.run)) |
170 | | -while implementing your own rendering logic and event handling with full async support. |
| 158 | +## Quality Strategy for Rendering Behavior |
171 | 159 |
|
172 | | -```rust |
173 | | -// Example of implementing the new Prompt trait |
174 | | -#[async_trait::async_trait] |
175 | | -impl Prompt for MyCustomPrompt { |
176 | | - type Index = MyIndex; |
177 | | - type Return = MyResult; |
178 | | - |
179 | | - fn renderer(&self) -> SharedRenderer<Self::Index> { |
180 | | - self.renderer.clone() |
181 | | - } |
| 160 | +Ensuring consistent rendering behavior across terminal environments is a key focus. |
| 161 | +To achieve this, promkit includes a suite of test tools: |
182 | 162 |
|
183 | | - async fn initialize(&mut self) -> anyhow::Result<()> { |
184 | | - // Initialize your prompt state |
185 | | - self.renderer.render().await |
186 | | - } |
| 163 | +- [`termharness`](./termharness) |
| 164 | +- [`zsherio`](./zsherio) |
| 165 | +- [`zsh-render-parity`](./zsh-render-parity) |
187 | 166 |
|
188 | | - async fn evaluate(&mut self, event: &Event) -> anyhow::Result<Signal> { |
189 | | - // Handle events and update state |
190 | | - match event { |
191 | | - // Your event handling logic |
192 | | - _ => Ok(Signal::Continue), |
193 | | - } |
194 | | - } |
195 | | - |
196 | | - fn finalize(&mut self) -> anyhow::Result<Self::Return> { |
197 | | - // Produce final result |
198 | | - Ok(self.result.clone()) |
199 | | - } |
200 | | -} |
201 | | -``` |
| 167 | +These tools compare prompt behavior against zsh-oriented scenarios |
| 168 | +(e.g. wrapping, resize, and cursor movement), helping keep terminal behavior |
| 169 | +predictable while the rendering internals evolve. |
0 commit comments