Skip to content

Commit af18d21

Browse files
authored
Merge pull request #57 from ynqa/widgets
feat: separate crates into core/widgets
2 parents 309f8c7 + 079467c commit af18d21

File tree

105 files changed

+2647
-2386
lines changed

Some content is hidden

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

105 files changed

+2647
-2386
lines changed

.github/workflows/ci.yml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,12 @@ jobs:
3030
with:
3131
command: test
3232
args: -- --nocapture --format pretty
33-
- uses: actions-rs/cargo@v1
34-
with:
35-
command: build
36-
args: --examples
33+
- name: Build examples
34+
run: |
35+
find examples \
36+
-name Cargo.toml \
37+
-exec cargo build \
38+
--manifest-path {} \;
3739
- uses: actions-rs/cargo@v1
3840
with:
3941
command: build

Cargo.toml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,19 @@
11
[workspace]
22
resolver = "2"
33
members = [
4+
"examples/*",
45
"promkit",
6+
"promkit-core",
57
"promkit-derive",
8+
"promkit-widgets",
69
]
10+
11+
[workspace.dependencies]
12+
anyhow = "1.0.97"
13+
# See https://github.com/crossterm-rs/crossterm/issues/935
14+
crossterm = { version = "0.28.1", features = ["use-dev-tty", "event-stream", "libc", "serde"] }
15+
radix_trie = "0.2.1"
16+
rayon = "1.10.0"
17+
serde = "1.0.219"
18+
serde_json = { version = "1.0.140", features = ["preserve_order"] }
19+
unicode-width = "0.2.0"

Concept.md

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
# Concept
2+
3+
## Well-defined boundaries for responsibilities and modularization
4+
5+
The core design principle of promkit is the clear separation of the following three functions,
6+
each implemented in dedicated modules:
7+
8+
- **Event Handlers**: Define behaviors for keyboard inputs (such as when <kbd>Enter</kbd> is pressed)
9+
- **promkit**: Responsible for implementing event handlers, combining widgets and handling corresponding events
10+
- In the future, there is potential to further separate event handlers to improve customizability.
11+
12+
- **State Updates**: Managing and updating the internal state of widgets
13+
- **promkit-widgets**: Responsible for state management of various widgets and pane generation
14+
- Each widget implements
15+
[PaneFactory](https://docs.rs/promkit/0.8.0/promkit/trait.PaneFactory.html)
16+
trait to generate panes needed for rendering
17+
18+
> [!IMPORTANT]
19+
> The widgets themselves DO NOT contain event handlers
20+
> - This prevents key operation conflicts
21+
> when combining multiple widgets
22+
> - e.g. When combining a listbox and text editor, <kbd>↓</kbd>
23+
> behavior could potentially conflict
24+
> - navigating the list vs. recalling input history
25+
26+
- **Rendering**: Processing to visually display the generated panes
27+
- **promkit-core**: Responsible for basic terminal operations
28+
- [Terminal](https://docs.rs/promkit_core/0.1.0/terminal/struct.Terminal.html) handles rendering
29+
- Currently uses full rendering with plans to implement differential rendering in the future.
30+
- [Pane](https://docs.rs/promkit_core/0.1.0/pane/struct.Pane.html)
31+
defines the data structures for rendering
32+
33+
This separation allows each component to focus on a single responsibility,
34+
making customization and extension easier.
35+
36+
### Event-Loop
37+
38+
These three functions collectively form the core of "event-loop" logic.
39+
Here is the important part of the actual event-loop from
40+
[Prompt<T: Renderer>::run](https://docs.rs/promkit/0.8.0/promkit/struct.Prompt.html#method.run):
41+
42+
```rust
43+
// Core part of event-loop
44+
loop {
45+
// Read events
46+
let ev = event::read()?;
47+
48+
// Evaluate events and update state
49+
if self.renderer.evaluate(&ev)? == PromptSignal::Quit {
50+
// Exit processing
51+
break;
52+
}
53+
54+
// Render based on the latest state
55+
let size = crossterm::terminal::size()?;
56+
terminal.draw(&self.renderer.create_panes(size.0, size.1))?;
57+
}
58+
```
59+
60+
As a diagram:
61+
62+
```mermaid
63+
flowchart LR
64+
subgraph promkit["promkit: event-loop"]
65+
direction LR
66+
A[User Input] --> L[Determine behavior that matches key event]
67+
68+
subgraph B_event["promkit: event-handler"]
69+
direction LR
70+
L[Determine behavior that matches key event] --> D[Request: update state]
71+
end
72+
73+
D --> F[Update state]
74+
75+
subgraph update_state["promkit-widgets"]
76+
direction LR
77+
F[Update state]
78+
end
79+
80+
F --> C{Evaluate}
81+
C -->|Continue| G[Request: generate pane for rendering]
82+
G[Request: generate pane for rendering] --> output_pane
83+
84+
subgraph gen_pane["promkit-widgets"]
85+
direction LR
86+
output_pane[Generate pane for rendering]
87+
end
88+
89+
subgraph promkit_core["promkit-core"]
90+
direction LR
91+
output_pane --> H[Rendering]
92+
end
93+
94+
H --> A
95+
end
96+
97+
C -->|Break out of loop| E[Exit]
98+
```
99+
100+
In the current implementation of promkit, event handling is centralized.
101+
That is, all events are processed sequentially within
102+
[Prompt<T: Renderer>::run](https://docs.rs/promkit/0.8.0/promkit/struct.Prompt.html#method.run)
103+
and propagated to each widget through
104+
[Renderer.evaluate](https://docs.rs/promkit/0.8.0/promkit/trait.Renderer.html#tymethod.evaluate).
105+
106+
> [!NOTE]
107+
> The current implementation of
108+
> [Prompt<T: Renderer>::run](https://docs.rs/promkit/0.8.0/promkit/struct.Prompt.html#method.run)
109+
> is provisional. In the future,
110+
> we plan to introduce more flexible event processing and rendering mechanisms.
111+
112+
## Customizability
113+
114+
promkit allows customization at various levels.
115+
You can choose the appropriate customization method
116+
according to your use case.
117+
118+
### Customize as configures
119+
120+
Using high-level APIs, you can easily customize existing preset components. For example, in
121+
[preset::readline::Readline](https://github.com/ynqa/promkit/blob/v0.8.0/promkit/src/preset/readline.rs),
122+
the following customizations are possible:
123+
124+
```rust
125+
let mut p = Readline::default()
126+
// Set title text
127+
.title("Custom Title")
128+
// Change input prefix
129+
.prefix("$ ")
130+
// Prefix style
131+
.prefix_style(StyleBuilder::new().fgc(Color::DarkBlue).build())
132+
// Active character style
133+
.active_char_style(StyleBuilder::new().bgc(Color::DarkMagenta).build())
134+
// Inactive character style
135+
.inactive_char_style(StyleBuilder::new().fgc(Color::DarkGrey).build())
136+
// Enable suggestion feature
137+
.enable_suggest(Suggest::from_iter(["option1", "option2"]))
138+
// Enable history feature
139+
.enable_history()
140+
// Input masking (for password input, etc.)
141+
.mask('*')
142+
// Set word break characters
143+
.word_break_chars(HashSet::from([' ', '-']))
144+
// Input validation feature
145+
.validator(
146+
|text| text.len() > 3,
147+
|text| format!("Please enter more than 3 characters (current: {} characters)", text.len()),
148+
)
149+
// Register custom keymap
150+
.register_keymap("custom", my_custom_keymap)
151+
.prompt()?;
152+
```
153+
154+
By combining these configuration options, you can significantly customize existing presets.
155+
156+
### Advanced Customization
157+
158+
Lower-level customization is also possible:
159+
160+
1. **Creating custom widgets**: You can create your own widgets equivalent to `promkit-widgets`.
161+
By implementing
162+
[PaneFactory](https://docs.rs/promkit/0.8.0/promkit/trait.PaneFactory.html)
163+
trait for your data structure, you can use it like other standard widgets.
164+
e.g. https://github.com/ynqa/empiriqa/blob/v0.1.0/src/queue.rs
165+
166+
2. **Defining custom presets**: By combining multiple widgets and implementing your own event handlers,
167+
you can create completely customized presets. In that case, you need to implement
168+
[Renderer](https://docs.rs/promkit/0.8.0/promkit/trait.Renderer.html) trait.
169+
170+
This allows you to leave event-loop logic to promkit (i.e., you can execute
171+
[Prompt<T: Renderer>::run](https://docs.rs/promkit/0.8.0/promkit/struct.Prompt.html#method.run))
172+
while implementing your own rendering logic and event handling.
173+
174+
3. **Customizing Event-Loop**: One of the important features of promkit is the clear separation
175+
between widgets (UI elements) and renderer (rendering logic). This separation allows users to flexibly
176+
control how the two interact.
177+
178+
For example, [jnv](https://github.com/ynqa/jnv) project implements its own Renderer to allow
179+
widgets to directly control rendering (excerpt below):
180+
181+
```rust
182+
// Implementation example in jnv
183+
pub struct Renderer {
184+
terminal: Terminal,
185+
panes: [Pane; PANE_SIZE],
186+
}
187+
188+
impl Renderer {
189+
pub fn update_and_draw<I: IntoIterator<Item = (PaneIndex, Pane)>>(
190+
&mut self,
191+
iter: I,
192+
) -> anyhow::Result<()> {
193+
for (index, pane) in iter {
194+
self.panes[index as usize] = pane;
195+
}
196+
self.terminal.draw(&self.panes)?;
197+
Ok(())
198+
}
199+
}
200+
```
201+
202+
With this approach, widgets can hold an instance of `Renderer` directly and actively request
203+
rendering by calling `update_and_draw` method when their state is updated.
204+
205+
In the future, we plan to extend promkit to allow users to more intuitively and flexibly compose
206+
their own event-loop logic. This will make it easier to customize according to the characteristics
207+
and requirements of each application, aiming to accommodate a wider range of use cases.

0 commit comments

Comments
 (0)