Skip to content

Commit 4b26c0c

Browse files
authored
Merge pull request #74 from ynqa/nopanematrixify
Test and refactor `core` module
2 parents 04bca2b + 3a65cd8 commit 4b26c0c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+1828
-1259
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,6 @@ Cargo.lock
1515

1616
# Ignore GIF files in the tapes directory
1717
tapes/*.gif
18+
19+
# Ignore test artifacts emitted by zsh-render-parity integration tests
20+
zsh-render-parity/.artifacts/

Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
[workspace]
22
resolver = "2"
33
members = [
4-
"event-dbg",
54
"examples/*",
65
"promkit",
76
"promkit-core",
87
"promkit-derive",
98
"promkit-widgets",
9+
"termharness",
10+
"zsh-render-parity",
11+
"zsherio",
1012
]
1113

1214
[workspace.dependencies]
@@ -15,11 +17,13 @@ async-trait = "0.1.89"
1517
crossbeam-skiplist = "0.1.3"
1618
crossterm = { version = "0.29.0", features = ["use-dev-tty", "event-stream", "serde"] }
1719
futures = "0.3.32"
20+
portable-pty = "0.9.0"
1821
radix_trie = "0.3.0"
1922
rayon = "1.11.0"
2023
scopeguard = "1.2.0"
2124
serde = "1.0.228"
2225
serde_json = { version = "1.0.149", features = ["preserve_order"] }
2326
termcfg = { version = "0.2.0", features = ["crossterm_0_29_0"] }
2427
tokio = { version = "1.49.0", features = ["full"] }
28+
thiserror = "2.0.18"
2529
unicode-width = "0.2.2"

Concept.md

Lines changed: 122 additions & 154 deletions
Original file line numberDiff line numberDiff line change
@@ -1,201 +1,169 @@
11
# Concept
22

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

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:
76

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.
1214

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.
1821

1922
> [!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):
4442

4543
```rust
46-
// Initialize the prompt state
4744
self.initialize().await?;
4845

49-
// Start the event loop
5046
while let Some(event) = EVENT_STREAM.lock().await.next().await {
5147
match event {
5248
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+
5454
if self.evaluate(&event).await? == Signal::Quit {
5555
break;
5656
}
5757
}
58-
Err(e) => {
59-
eprintln!("Error reading event: {}", e);
60-
break;
61-
}
58+
Err(_) => break,
6259
}
6360
}
6461

65-
// Finalize the prompt and return the result
6662
self.finalize()
6763
```
6864

6965
As a diagram:
7066

7167
```mermaid
7268
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
9375
end
9476
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
9783
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+
```
10387

10488
## Customizability
10589

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
10993

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

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
115104

116105
```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+
}
150144
```
151145

152-
By combining these configuration options, you can significantly customize existing presets.
153-
154-
### Advanced Customization
146+
### 2. Build your own prompt
155147

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

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
163153

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.
167157

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
171159

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:
182162

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)
187166

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

Comments
 (0)