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