docs(cli): 📝 documentation
This commit is contained in:
parent
cf3983af3a
commit
61f7096dfa
@ -19,6 +19,151 @@ This crate is for internal use. It's only published privately.
|
||||
cargo add lool --registry=lugit --features cli cli.tui
|
||||
```
|
||||
|
||||
# Usage
|
||||
# `lool::cli::tui` Framework
|
||||
|
||||
Pending.
|
||||
This module provides a small framework for building async terminal user interfaces (TUIs) using a
|
||||
component-based architecture, using the `ratatui` library.
|
||||
|
||||
This module defines two primary elements:
|
||||
- the `App` struct,
|
||||
- and the `Component` trait.
|
||||
|
||||
Together, these elements facilitate the creation of modular and interactive terminal applications.
|
||||
|
||||
## Overview
|
||||
|
||||
### `App` Struct
|
||||
|
||||
The `App` struct represents the main application and is responsible of (among other things):
|
||||
|
||||
- **Tick Rate and Frame Rate**: Controls the update frequency of the application.
|
||||
- **Component Management**: Manages a collection of components that make up the user interface.
|
||||
- **Event Handling**: Processes user inputs and dispatches actions to the appropriate components.
|
||||
- **Lifecycle Management**: Handles the start, suspension, and termination of the application.
|
||||
|
||||
### `Component` Trait
|
||||
|
||||
The `Component` trait represents a visual and interactive element of the user interface.
|
||||
|
||||
Components can be nested, allowing for a hierarchical structure where each component can have child
|
||||
components. This trait provides several methods for handling events, updating state, and rendering:
|
||||
|
||||
- **Event Handling**: Methods like `handle_frame_event` and `handle_paste_event` allow components
|
||||
to respond to different types of events.
|
||||
- **State Management**: Methods like `update` and `receive_message` enable components to update
|
||||
their state based on actions or messages.
|
||||
- **Initialization**: The `init` method allows components to perform setup tasks when they are first
|
||||
created.
|
||||
- **Rendering**: The `draw` method is responsible for rendering the component within a specified
|
||||
area. All components must implement this method to display their content.
|
||||
|
||||
## How It Works
|
||||
|
||||
### Component-Based Architecture
|
||||
|
||||
The framework uses a component-based architecture, where the user interface is composed of multiple
|
||||
components. Each component can have child components, forming a tree-like structure. This design
|
||||
promotes modularity and reusability, making it easier to manage complex user interfaces in a
|
||||
structured and standardized way.
|
||||
|
||||
### Interaction Between `App` and `Component`
|
||||
|
||||
- **Initialization**: The `App` initializes all components and sets up the necessary event channels.
|
||||
- **Event Dispatching**: The `App` listens for user inputs and dispatches actions to the relevant
|
||||
components.
|
||||
- **State Updates**: Components update their state based on the actions they receive and can
|
||||
propagate these updates to their child components.
|
||||
- **Rendering**: Components handle their own rendering logic, allowing for a flexible and
|
||||
customizable user interface.
|
||||
|
||||
|
||||
Usually, the `App` is provided with a root component that represents the main component of the
|
||||
application.
|
||||
|
||||
From the Main/Root component, the application can be built by nesting child components as needed in
|
||||
a tree-like structure. Example:
|
||||
|
||||
```txt
|
||||
App
|
||||
└── RootComponent
|
||||
└── Router
|
||||
├── Home
|
||||
│ ├── Header
|
||||
│ └── Content
|
||||
├── About
|
||||
│ ├── Header
|
||||
│ └── Content
|
||||
└── Contact
|
||||
├── Header
|
||||
└── ContactForm
|
||||
```
|
||||
|
||||
In this example, the `RootComponent` is the main component of the application and contains a
|
||||
`Router`, which is another component that manages the routing logic. The `Router` component has
|
||||
three child components: `Home`, `About`, and `Contact` and will render the appropriate component
|
||||
depending on the current route.
|
||||
|
||||
Then, heach "route" component (`Home`, `About`, `Contact`) can have its own child components, such
|
||||
as `Header`, `Content`, and `ContactForm` and use them to build the final user interface.
|
||||
|
||||
The `RootComponent` will call the `draw` method of the `Router` component, which will in turn call
|
||||
the `draw` method of the current route component (`Home`, `About`, or `Contact`), and so on.
|
||||
|
||||
The `draw` chain will propagate down the component tree, allowing each component to render its
|
||||
content. The `App` starts the draw chain a few times per second. The amount of draw calls per second
|
||||
is controlled by the `frame_rate` of the `App`:
|
||||
|
||||
```rust
|
||||
let mut app = App::new(...).frame_rate(24); // 24 frames per second
|
||||
```
|
||||
|
||||
Some tasks might be too expensive to be performed on every frame. In these cases, the `App` alsp
|
||||
defines a `tick_rate` that controls how often the `handle_tick_event` method of the components is
|
||||
called.
|
||||
|
||||
The tick event is often used to update the state of the components, while the frame event is used to
|
||||
render the components in the terminal.
|
||||
|
||||
For example, a tick rate of 1 means that the `handle_tick_event` method of the components will be
|
||||
called once per second. And a component might use this event to update its state, run background
|
||||
tasks, or perform other operations that don't need to be done on every frame.
|
||||
|
||||
```rust
|
||||
let mut app = App::new(...).tick_rate(10); // 10 ticks per second
|
||||
```
|
||||
|
||||
### Component Communication
|
||||
|
||||
Components can communicate with each other using messages. The `Component` trait defines the
|
||||
following methods:
|
||||
|
||||
- `register_action_handler`: registers a mpsc sender, to send messages to the bus.
|
||||
- `receive_message`: receives a message from the bus.
|
||||
|
||||
At the start of the application, the `App` will call the `register_action_handler` method of each
|
||||
component, and they can store the sender to send messages to the bus.
|
||||
|
||||
When a component wants to send a message to another component, it can use the sender it received
|
||||
during the registration process.
|
||||
|
||||
```rust
|
||||
self.bus.send("an:action".to_string());
|
||||
```
|
||||
|
||||
For simplicity, the messages are just strings, but one can serialize more complex data structures
|
||||
into strings if needed in any format like JSON, TOML or even a custom format that just suits the
|
||||
our needs:
|
||||
|
||||
```rust
|
||||
self.bus.send(format!("task:{}:state='{}',date='{}'", task_id, state, date));
|
||||
```
|
||||
|
||||
Then we can receive the message in the `receive_message` method of another component:
|
||||
|
||||
```rust
|
||||
fn receive_message(&mut self, message: String) {
|
||||
if message.starts_with("task:") {
|
||||
// Parse the message and do whatever is needed with the data
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -28,6 +28,14 @@ fn io() -> IO {
|
||||
}
|
||||
pub type Frame<'a> = ratatui::Frame<'a>;
|
||||
|
||||
/// The Tui struct represents a terminal user interface.
|
||||
///
|
||||
/// It encapsulates [ratatui::Terminal] adding extra functionality:
|
||||
/// - [Tui::start] and [Tui::stop] to start and stop the event loop
|
||||
/// - [Tui::enter] and [Tui::exit] to enter and exit the crossterm terminal
|
||||
/// [raw mode](https://docs.rs/crossterm/0.28.1/crossterm/terminal/index.html#raw-mode)
|
||||
/// - Mapping of crossterm events to [Event]s
|
||||
/// - Emits [Event::Tick] and [Event::Render] events at a specified rate
|
||||
pub struct Tui {
|
||||
pub terminal: ratatui::Terminal<Backend<IO>>,
|
||||
pub task: JoinHandle<()>,
|
||||
@ -63,26 +71,43 @@ impl Tui {
|
||||
})
|
||||
}
|
||||
|
||||
/// Sets the tick rate for the Tui. The tick rate is the number of times per second that the
|
||||
/// Tui will emit a [Event::Tick] event. The default tick rate is 4 ticks per second.
|
||||
///
|
||||
/// The tick is different from the render rate, which is the number of times per second that
|
||||
/// the application will be drawn to the screen. The tick rate is useful for updating the
|
||||
/// application state, performing calculations, run background tasks, and other operations that
|
||||
/// do not require a per-frame operation.
|
||||
///
|
||||
/// Tick rate will usually be lower than the frame rate.
|
||||
pub fn tick_rate(mut self, tick_rate: f64) -> Self {
|
||||
self.tick_rate = tick_rate;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the frame rate for the Tui. The frame rate is the number of times per second that the
|
||||
/// Tui will emit a [Event::Render] event. The default frame rate is 60 frames per second.
|
||||
///
|
||||
/// The frame rate is the rate at which the application will be drawn to the screen (by calling
|
||||
/// the `draw` method of each component).
|
||||
pub fn frame_rate(mut self, frame_rate: f64) -> Self {
|
||||
self.frame_rate = frame_rate;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets whether the Tui should capture mouse events. The default is false.
|
||||
pub fn mouse(mut self, mouse: bool) -> Self {
|
||||
self.mouse = mouse;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets whether the Tui should capture paste events. The default is false.
|
||||
pub fn paste(mut self, paste: bool) -> Self {
|
||||
self.paste = paste;
|
||||
self
|
||||
}
|
||||
|
||||
/// Starts the Tui event loop.
|
||||
pub fn start(&mut self) {
|
||||
let tick_delay = std::time::Duration::from_secs_f64(1.0 / self.tick_rate);
|
||||
let render_delay = std::time::Duration::from_secs_f64(1.0 / self.frame_rate);
|
||||
@ -146,6 +171,7 @@ impl Tui {
|
||||
});
|
||||
}
|
||||
|
||||
/// Stops the Tui event loop.
|
||||
pub fn stop(&self) -> Result<()> {
|
||||
self.cancel();
|
||||
let mut counter = 0;
|
||||
@ -163,6 +189,7 @@ impl Tui {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Enables cross-term raw mode and enters the alternate screen.
|
||||
pub fn enter(&mut self) -> Result<()> {
|
||||
crossterm::terminal::enable_raw_mode()?;
|
||||
crossterm::execute!(io(), EnterAlternateScreen, cursor::Hide)?;
|
||||
@ -176,6 +203,7 @@ impl Tui {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Disables cross-term raw mode and exits the alternate screen.
|
||||
pub fn exit(&mut self) -> Result<()> {
|
||||
self.stop()?;
|
||||
if crossterm::terminal::is_raw_mode_enabled()? {
|
||||
@ -205,6 +233,7 @@ impl Tui {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the next event from the event channel.
|
||||
pub async fn next(&mut self) -> Option<Event> {
|
||||
self.event_rx.recv().await
|
||||
}
|
||||
@ -214,18 +243,21 @@ impl Deref for Tui {
|
||||
type Target = ratatui::Terminal<Backend<IO>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
// deref Tui as Terminal
|
||||
&self.terminal
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for Tui {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
// deref Tui as Terminal mutably
|
||||
&mut self.terminal
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Tui {
|
||||
fn drop(&mut self) {
|
||||
// Ensure that the terminal is cleaned up when the Tui is dropped
|
||||
self.exit().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user