From 61f7096dfa37c763b50a9c60088a068c420d599f Mon Sep 17 00:00:00 2001 From: Lucas Colombo Date: Wed, 18 Sep 2024 19:25:52 -0300 Subject: [PATCH] =?UTF-8?q?docs(cli):=20=F0=9F=93=9D=20documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/cli/tui/README.md | 149 ++++++++++++++++++++++++++++++++++- lib/cli/tui/framework/tui.rs | 112 ++++++++++++++++---------- 2 files changed, 219 insertions(+), 42 deletions(-) diff --git a/lib/cli/tui/README.md b/lib/cli/tui/README.md index 74dbea7..ea69d58 100644 --- a/lib/cli/tui/README.md +++ b/lib/cli/tui/README.md @@ -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. \ No newline at end of file +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 + } +} +``` \ No newline at end of file diff --git a/lib/cli/tui/framework/tui.rs b/lib/cli/tui/framework/tui.rs index 38c969a..1ae4205 100644 --- a/lib/cli/tui/framework/tui.rs +++ b/lib/cli/tui/framework/tui.rs @@ -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>, 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); @@ -100,52 +125,53 @@ impl Tui { let render_delay = render_interval.tick(); let crossterm_event = reader.next().fuse(); tokio::select! { - _ = _cancellation_token.cancelled() => { - break; - } - maybe_event = crossterm_event => { - match maybe_event { - Some(Ok(evt)) => { - match evt { - CrosstermEvent::Key(key) => { - if key.kind == KeyEventKind::Press { - _event_tx.send(Event::Key(key)).unwrap(); - } - }, - CrosstermEvent::Mouse(mouse) => { - _event_tx.send(Event::Mouse(mouse)).unwrap(); - }, - CrosstermEvent::Resize(x, y) => { - _event_tx.send(Event::Resize(x, y)).unwrap(); - }, - CrosstermEvent::FocusLost => { - _event_tx.send(Event::FocusLost).unwrap(); - }, - CrosstermEvent::FocusGained => { - _event_tx.send(Event::FocusGained).unwrap(); - }, - CrosstermEvent::Paste(s) => { - _event_tx.send(Event::Paste(s)).unwrap(); - }, - } - } - Some(Err(_)) => { - _event_tx.send(Event::Error).unwrap(); - } - None => {}, + _ = _cancellation_token.cancelled() => { + break; } - }, - _ = tick_delay => { - _event_tx.send(Event::Tick).unwrap(); - }, - _ = render_delay => { - _event_tx.send(Event::Render).unwrap(); - }, + maybe_event = crossterm_event => { + match maybe_event { + Some(Ok(evt)) => { + match evt { + CrosstermEvent::Key(key) => { + if key.kind == KeyEventKind::Press { + _event_tx.send(Event::Key(key)).unwrap(); + } + }, + CrosstermEvent::Mouse(mouse) => { + _event_tx.send(Event::Mouse(mouse)).unwrap(); + }, + CrosstermEvent::Resize(x, y) => { + _event_tx.send(Event::Resize(x, y)).unwrap(); + }, + CrosstermEvent::FocusLost => { + _event_tx.send(Event::FocusLost).unwrap(); + }, + CrosstermEvent::FocusGained => { + _event_tx.send(Event::FocusGained).unwrap(); + }, + CrosstermEvent::Paste(s) => { + _event_tx.send(Event::Paste(s)).unwrap(); + }, + } + } + Some(Err(_)) => { + _event_tx.send(Event::Error).unwrap(); + } + None => {}, + } + }, + _ = tick_delay => { + _event_tx.send(Event::Tick).unwrap(); + }, + _ = render_delay => { + _event_tx.send(Event::Render).unwrap(); + }, } } }); } + /// 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 { self.event_rx.recv().await } @@ -214,18 +243,21 @@ impl Deref for Tui { type Target = ratatui::Terminal>; 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(); } }