docs(cli): 📝 documentation

This commit is contained in:
Lucas Colombo 2024-09-18 19:25:52 -03:00
parent cf3983af3a
commit 61f7096dfa
Signed by: lucas
GPG Key ID: EF34786CFEFFAE35
2 changed files with 219 additions and 42 deletions

View File

@ -19,6 +19,151 @@ This crate is for internal use. It's only published privately.
cargo add lool --registry=lugit --features cli cli.tui 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
}
}
```

View File

@ -28,6 +28,14 @@ fn io() -> IO {
} }
pub type Frame<'a> = ratatui::Frame<'a>; 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 struct Tui {
pub terminal: ratatui::Terminal<Backend<IO>>, pub terminal: ratatui::Terminal<Backend<IO>>,
pub task: JoinHandle<()>, 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 { pub fn tick_rate(mut self, tick_rate: f64) -> Self {
self.tick_rate = tick_rate; self.tick_rate = tick_rate;
self 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 { pub fn frame_rate(mut self, frame_rate: f64) -> Self {
self.frame_rate = frame_rate; self.frame_rate = frame_rate;
self self
} }
/// Sets whether the Tui should capture mouse events. The default is false.
pub fn mouse(mut self, mouse: bool) -> Self { pub fn mouse(mut self, mouse: bool) -> Self {
self.mouse = mouse; self.mouse = mouse;
self self
} }
/// Sets whether the Tui should capture paste events. The default is false.
pub fn paste(mut self, paste: bool) -> Self { pub fn paste(mut self, paste: bool) -> Self {
self.paste = paste; self.paste = paste;
self self
} }
/// Starts the Tui event loop.
pub fn start(&mut self) { pub fn start(&mut self) {
let tick_delay = std::time::Duration::from_secs_f64(1.0 / self.tick_rate); 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); 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<()> { pub fn stop(&self) -> Result<()> {
self.cancel(); self.cancel();
let mut counter = 0; let mut counter = 0;
@ -163,6 +189,7 @@ impl Tui {
Ok(()) Ok(())
} }
/// Enables cross-term raw mode and enters the alternate screen.
pub fn enter(&mut self) -> Result<()> { pub fn enter(&mut self) -> Result<()> {
crossterm::terminal::enable_raw_mode()?; crossterm::terminal::enable_raw_mode()?;
crossterm::execute!(io(), EnterAlternateScreen, cursor::Hide)?; crossterm::execute!(io(), EnterAlternateScreen, cursor::Hide)?;
@ -176,6 +203,7 @@ impl Tui {
Ok(()) Ok(())
} }
/// Disables cross-term raw mode and exits the alternate screen.
pub fn exit(&mut self) -> Result<()> { pub fn exit(&mut self) -> Result<()> {
self.stop()?; self.stop()?;
if crossterm::terminal::is_raw_mode_enabled()? { if crossterm::terminal::is_raw_mode_enabled()? {
@ -205,6 +233,7 @@ impl Tui {
Ok(()) Ok(())
} }
/// Returns the next event from the event channel.
pub async fn next(&mut self) -> Option<Event> { pub async fn next(&mut self) -> Option<Event> {
self.event_rx.recv().await self.event_rx.recv().await
} }
@ -214,18 +243,21 @@ impl Deref for Tui {
type Target = ratatui::Terminal<Backend<IO>>; type Target = ratatui::Terminal<Backend<IO>>;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
// deref Tui as Terminal
&self.terminal &self.terminal
} }
} }
impl DerefMut for Tui { impl DerefMut for Tui {
fn deref_mut(&mut self) -> &mut Self::Target { fn deref_mut(&mut self) -> &mut Self::Target {
// deref Tui as Terminal mutably
&mut self.terminal &mut self.terminal
} }
} }
impl Drop for Tui { impl Drop for Tui {
fn drop(&mut self) { fn drop(&mut self) {
// Ensure that the terminal is cleaned up when the Tui is dropped
self.exit().unwrap(); self.exit().unwrap();
} }
} }