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
|
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>;
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user