Document not found (404)
+This URL is invalid, sorry. Please use the navigation bar or search to continue.
+ +diff --git a/pr-preview/pr-83/.nojekyll b/pr-preview/pr-83/.nojekyll new file mode 100644 index 000000000..f17311098 --- /dev/null +++ b/pr-preview/pr-83/.nojekyll @@ -0,0 +1 @@ +This file makes sure that Github Pages doesn't process mdBook's output. diff --git a/pr-preview/pr-83/404.html b/pr-preview/pr-83/404.html new file mode 100644 index 000000000..0cc1eac62 --- /dev/null +++ b/pr-preview/pr-83/404.html @@ -0,0 +1,200 @@ + + +
+ + +This URL is invalid, sorry. Please use the navigation bar or search to continue.
+ +Copyright (c) 2023 Ratatui Developers
+Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions:
+The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software.
+THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.
+ +If you are interested in a more object oriented approach to organizing TUIs, you can use a
+Component
based approach.
A couple of projects in the wild use this approach
+ +We also have a ratatui-async-template
that has an example of this Component
based approach:
We already covered TEA in the previous section. The Component
+architecture takes a slightly more object oriented trait based approach.
Each component encapsulates its own state, event handlers, and rendering logic.
+Component Initialization (init
) - This is where a component can set up any initial state or
+resources it needs. It’s a separate process from handling events or rendering.
Event Handling (handle_events
, handle_key_events
, handle_mouse_events
) - Each component has
+its own event handlers. This allows for a finer-grained approach to event handling, with each
+component only dealing with the events it’s interested in. This contrasts with Elm’s single
+update function that handles messages for the entire application.
State Update (update
) - Components can have their own local state and can update it in response
+to actions. This state is private to the component, which differs from Elm’s global model.
Rendering (render
) - Each component defines its own rendering logic. It knows how to draw
+itself, given a rendering context. This is similar to Elm’s view function but on a
+component-by-component basis.
Here’s an example of the Component
trait implementation you might use:
use anyhow::Result;
+use crossterm::event::{KeyEvent, MouseEvent};
+use ratatui::layout::Rect;
+
+use crate::{action::Action, event::Event, terminal::Frame};
+
+pub trait Component {
+ fn init(&mut self) -> Result<()> {
+ Ok(())
+ }
+ fn handle_events(&mut self, event: Option<Event>) -> Action {
+ match event {
+ Some(Event::Quit) => Action::Quit,
+ Some(Event::Tick) => Action::Tick,
+ Some(Event::Key(key_event)) => self.handle_key_events(key_event),
+ Some(Event::Mouse(mouse_event)) => self.handle_mouse_events(mouse_event),
+ Some(Event::Resize(x, y)) => Action::Resize(x, y),
+ Some(_) => Action::Noop,
+ None => Action::Noop,
+ }
+ }
+ fn handle_key_events(&mut self, key: KeyEvent) -> Action {
+ Action::Noop
+ }
+ fn handle_mouse_events(&mut self, mouse: MouseEvent) -> Action {
+ Action::Noop
+ }
+ fn update(&mut self, action: Action) -> Action {
+ Action::Noop
+ }
+ fn render(&mut self, f: &mut Frame<'_>, rect: Rect);
+}
+One advantage of this approach is that it incentivizes co-locating the handle_events
, update
and
+render
functions on a component level.
Flux is a design pattern
+introduced by Facebook to address the challenges of building large scale web applications. Though
+originally designed with web applications in mind, the Flux architecture can be applied to any
+client-side project, including terminal applications. Here’s real world example of using the Flux
+architecture with ratatui
: https://github.com/Yengas/rust-chat-server/tree/main/tui.
Flux
for ratatui
?Terminal applications often have to deal with complex user interactions, multiple views, and dynamic
+data sources. Keeping the application predictable and the logic decoupled is crucial. Flux
, with
+its unidirectional data flow, allows ratatui
developers to have a structured way to handle user
+input, process data, and update the views.
Flux
ratatui
OverviewThe dispatcher remains the central hub that manages all data flow in your application. Every action +in the application, whether it’s a user input or a response from a server, will be channeled through +the dispatcher. This ensures a unified way of handling data, and since the dispatcher has no logic +of its own, it simply ensures that all registered callbacks receive the action data.
+struct Dispatcher {
+ store: Store,
+}
+
+impl Dispatcher {
+ fn dispatch(&mut self, action: Action) {
+ self.store.update(action);
+ }
+}
+Stores in Ratatui hold the application’s state and its logic. They could represent things like:
+Stores listen for actions dispatched from the Dispatcher. When a relevant action is dispatched, the +store updates its state and notifies any listening components (or views) that a change has occurred.
+struct Store {
+ counter: i32,
+}
+
+impl Store {
+ fn new() -> Self {
+ Self { counter: 0 }
+ }
+
+ fn update(&mut self, action: Action) {
+ match action {
+ Action::Increment => self.counter += 1,
+ Action::Decrement => self.counter -= 1,
+ }
+ }
+
+ fn get_state(&self) -> i32 {
+ self.counter
+ }
+}
+
+Actions represent any change or event in your application. For instance, when a user presses a key, +selects a menu item, or inputs text, an action is created. This action is dispatched and processed +by the relevant stores, leading to potential changes in application state.
+enum Action {
+ Increment,
+ Decrement,
+}
+ratatui
’s widgets display the application’s UI. They don’t hold or manage the application state,
+but they display it. When a user interacts with a widget, it can create an action that gets
+dispatched, which may lead to a change in a store, which in turn may lead to the widget being
+updated.
This page covers several patterns one can use for their application and acts as a top-level page for +the following articles where these patterns are gone into more in-depth.
+ + +ratatui
When building terminal user interfaces (TUI) with ratatui
, it’s helpful to have a solid structure
+for organizing your application. One proven architecture comes from the Elm language, known simply
+as The Elm Architecture (TEA).
If you are interested in a framework that uses ratatui
that is based on The Elm Architecture,
+you should check out https://github.com/veeso/tui-realm/.
+The documentation on this page is for theoretical understanding and pedagogical purposes only.
In this section, we’ll explore how to apply The Elm Architecture principles to ratatui
TUI apps.
At its core, TEA is split into three main components:
+sequenceDiagram +participant User +participant TUI Application + +User->>TUI Application: Input/Event/Message +TUI Application->>TUI Application: Update (based on Model and Message) +TUI Application->>TUI Application: Render View (from Model) +TUI Application-->>User: Display UI ++
ratatui
Following TEA principles typically involves ensuring that you do the following things:
+In ratatui
, you’ll typically use a struct
to represent your model:
struct Model {
+ //... your application's data goes here
+}
+For a counter app, our model may look like this:
+struct Model {
+ counter: i32,
+ should_quit: bool,
+}
+Updates in TEA are actions triggered by events, such as user inputs. The core idea is to map each of +these actions or events to a message. This can be achieved by creating an enum to keep track of +messages. Based on the received message, the current state of the model is used to determine the +next state.
+Defining a Message
enum
enum Message {
+ //... various inputs or actions that your app cares about
+ // e.g., ButtonPressed, TextEntered, etc.
+}
+For a counter app, our Message
enum may look like this:
enum Message {
+ Increment,
+ Decrement,
+ Reset,
+ Quit,
+}
+update()
function
The update function is at the heart of this process. It takes the current model and a message, and +decides how the model should change in response to that message.
+A key feature of TEA is immutability. Hence, the update function should avoid direct mutation of the +model. Instead, it should produce a new instance of the model reflecting the desired changes.
+fn update(model: &Model, msg: Message) -> Model {
+ match msg {
+ // Match each possible message and decide how the model should change
+ // Return a new model reflecting those changes
+ }
+}
+In TEA, it’s crucial to maintain a clear separation between the data (model) and the logic that +alters it (update). This immutability principle ensures predictability and makes the application +easier to reason about.
+Hence, while immutability is emphasized in TEA, Rust developers can choose the most +suitable approach based on performance and their application’s needs.
+For example, it would be perfectly valid to do the following:
+fn update(model: &mut Model, msg: Message) {
+ match msg {
+ // Match each possible message and decide how the model should change
+ // Modify existing mode reflecting those changes
+ };
+}
+In TEA, the update()
function can not only modify the model based on the Message
, but it can
+also return another Message
. This design can be particularly useful if you want to chain messages
+or have an update lead to another update.
For example, this is what the update()
function may look like for a counter app:
fn update(model: &mut Model, msg: Message) -> Option<Message> {
+ match msg {
+ Message::Increment => {
+ model.counter += 1;
+ if model.counter > 50 {
+ return Some(Message::Reset);
+ }
+ },
+ Message::Decrement => {
+ model.counter -= 1;
+ if model.counter < -50 {
+ return Some(Message::Reset);
+ }
+ },
+ Message::Reset => {
+ model.counter = 0;
+ },
+ Message::Quit => {
+ model.should_quit = true;
+ },
+ _ => {},
+ }
+ None // Default return value if no specific message is to be returned
+}
+Remember that this design choice means that the main
loop will need to handle the
+returned message, calling update()
again based on that returned message.
Returning a Message
from the update()
function allows a developer to reason about their code as
+a “Finite State Machine”. Finite State Machines operate on defined states and transitions, where an
+initial state and an event (in our case, a Message
) lead to a subsequent state. This cascading
+approach ensures that the system remains in a consistent and predictable state after handling a
+series of interconnected events.
Here’s a state transition diagram of the counter example from above:
+stateDiagram-v2 + state Model { + counter : counter = 0 + should_quit : should_quit = false + } + + Model --> Increment + Model --> Decrement + Model --> Reset + Model --> Quit + + Increment --> Model: counter += 1 + Increment --> Reset: if > 50 + + Decrement --> Model: counter -= 1 + Decrement --> Reset: if < -50 + + Reset --> Model: counter = 0 + + Quit --> break: should_quit = true ++
While TEA doesn’t use the Finite State Machine terminology or strictly enforce that paradigm, +thinking of your application’s state as a state machine can allow developers to break down intricate +state transitions into smaller, more manageable steps. This can make designing the application’s +logic clearer and improve code maintainability.
+The view function in the Elm Architecture is tasked with taking the current model and producing a +visual representation for the user. In the case of ratatui, it translates the model into terminal UI +elements. It’s essential that the view function remains a pure function: for a given state of the +model, it should always produce the same UI representation.
+fn view(model: &Model) {
+ //... use `ratatui` functions to draw your UI based on the model's state
+}
+Every time the model is updated, the view function should be capable of reflecting those changes +accurately in the terminal UI.
+In TEA, you are expected to ensure that your view function is side-effect free. The view()
+function shouldn’t modify global state or perform any other actions. Its sole job is to map the
+model to a visual representation.
For a given state of the model, the view function should always produce the same visual output. This +predictability makes your TUI application easier to reason about and debug.
+With immediate mode rendering you may run into an issue: the view
function is only aware of the
+area available to draw in at render time.
This limitation is a recognized constraint of immediate mode GUIs. Overcoming it often involves +trade-offs. One common solution is to store the drawable size and reference it in the subsequent +frame, although this can introduce a frame delay in layout adjustments, leading to potential +flickering during the initial rendering when changes in screen size occur.
+An alternative would be using the Resize
event from crossterm
and to clear the UI and force
+redraw everything during that event.
In ratatui
, there are
+StatefulWidget
s which
+require a mutable reference to state during render.
For this reason, you may choose to forego the view
immutability principle. For example, if you
+were interested in rendering a List
, your view
function may look like this:
fn view(model: &mut Model, f: &mut Frame) {
+ let items = app.items.items.iter().map(|element| ListItem::new(element)).collect();
+ f.render_stateful_widget(List::new(items), f.size(), &mut app.items.state);
+}
+
+fn main() {
+ loop {
+ ...
+ terminal
+ .draw(|f| {
+ view(&mut model, f);
+ })?;
+ ...
+ }
+}
+Another advantage of having access to the Frame
in the view()
function is that you have access
+to setting the cursor position, which is useful for displaying text fields. For example, if you
+wanted to draw an input field using tui-input
, you
+might have a view
that looks like this:
fn view(model: &mut Model, f: &mut Frame) {
+ let area = f.size();
+ let input = Paragraph::new(app.input.value());
+ f.render_widget(input, area);
+ if app.mode == Mode::Insert {
+ f.set_cursor(
+ (area.x + 1 + self.input.cursor() as u16).min(area.x + area.width - 2),
+ area.y + 1
+ )
+ }
+}
+When you put it all together, your main application loop might look something like:
+Message
This cycle repeats, ensuring your TUI is always up-to-date with user interactions.
+As an illustrative example, here’s the Counter App +refactored using TEA.
+The notable difference from before is that we have an Model
struct that captures the app state,
+and a Message
enum that captures the various actions your app can take.
// cargo add anyhow ratatui crossterm
+use anyhow::Result;
+use ratatui::{
+ prelude::{CrosstermBackend, Terminal},
+ widgets::Paragraph,
+};
+
+pub type Frame<'a> = ratatui::Frame<'a, ratatui::backend::CrosstermBackend<std::io::Stderr>>;
+
+// MODEL
+struct Model {
+ counter: i32,
+ should_quit: bool,
+}
+
+// MESSAGES
+#[derive(PartialEq)]
+enum Message {
+ Increment,
+ Decrement,
+ Reset,
+ Quit,
+}
+
+// UPDATE
+fn update(model: &mut Model, msg: Message) -> Option<Message> {
+ match msg {
+ Message::Increment => {
+ model.counter += 1;
+ if model.counter > 50 {
+ return Some(Message::Reset);
+ }
+ },
+ Message::Decrement => {
+ model.counter -= 1;
+ if model.counter < -50 {
+ return Some(Message::Reset);
+ }
+ },
+ Message::Reset => model.counter = 0,
+ Message::Quit => model.should_quit = true, // You can handle cleanup and exit here
+ };
+ None
+}
+
+// VIEW
+fn view(model: &mut Model, f: &mut Frame) {
+ f.render_widget(Paragraph::new(format!("Counter: {}", model.counter)), f.size());
+}
+
+// Convert Event to Message
+// We don't need to pass in a `model` to this function in this example
+// but you might need it as your project evolves
+fn handle_event(_: &Model) -> Result<Option<Message>> {
+ let message = if crossterm::event::poll(std::time::Duration::from_millis(250))? {
+ if let crossterm::event::Event::Key(key) = crossterm::event::read()? {
+ match key.code {
+ crossterm::event::KeyCode::Char('j') => Message::Increment,
+ crossterm::event::KeyCode::Char('k') => Message::Decrement,
+ crossterm::event::KeyCode::Char('q') => Message::Quit,
+ _ => return Ok(None),
+ }
+ } else {
+ return Ok(None);
+ }
+ } else {
+ return Ok(None);
+ };
+ Ok(Some(message))
+}
+
+pub fn initialize_panic_handler() {
+ let original_hook = std::panic::take_hook();
+ std::panic::set_hook(Box::new(move |panic_info| {
+ crossterm::execute!(std::io::stderr(), crossterm::terminal::LeaveAlternateScreen).unwrap();
+ crossterm::terminal::disable_raw_mode().unwrap();
+ original_hook(panic_info);
+ }));
+}
+
+fn main() -> Result<()> {
+ initialize_panic_handler();
+
+ // Startup
+ crossterm::terminal::enable_raw_mode()?;
+ crossterm::execute!(std::io::stderr(), crossterm::terminal::EnterAlternateScreen)?;
+
+ let mut terminal = Terminal::new(CrosstermBackend::new(std::io::stderr()))?;
+ let mut model = Model { counter: 0, should_quit: false };
+
+ loop {
+ // Render the current view
+ terminal.draw(|f| {
+ view(&mut model, f);
+ })?;
+
+ // Handle events and map to a Message
+ let mut current_msg = handle_event(&model)?;
+
+ // Process updates as long as they return a non-None message
+ while current_msg != None {
+ current_msg = update(&mut model, current_msg.unwrap());
+ }
+
+ // Exit loop if quit flag is set
+ if model.should_quit {
+ break;
+ }
+ }
+
+ // Shutdown
+ crossterm::execute!(std::io::stderr(), crossterm::terminal::LeaveAlternateScreen)?;
+ crossterm::terminal::disable_raw_mode()?;
+ Ok(())
+}
+
+ The alternate screen is a separate buffer that some terminals provide, distinct from the main +screen. When activated, the terminal will display the alternate screen, hiding the current content +of the main screen. Applications can write to this screen as if it were the regular terminal +display, but when the application exits, the terminal will switch back to the main screen, and the +contents of the alternate screen will be cleared. This is useful for applications like text editors +or terminal games that want to use the full terminal window without disrupting the command line or +other terminal content.
+This creates a seamless transition between the application and the regular terminal session, as the +content displayed before launching the application will reappear after the application exits.
+Note that not all terminal emulators support the alternate screen, and even those that do may handle +it differently. As a result, the behavior may vary depending on the backend being used. Always +consult the specific backend’s documentation to understand how it implements the alternate screen.
+ +Choose Crossterm for most tasks.
+Ratatui interfaces with the terminal emulator through its “backends”. These are powerful libraries
+that grant ratatui
the ability to capture keypresses, maneuver the cursor, style the text with
+colors and other features. As of now, ratatui
supports three backends:
Selecting a backend does influence your project’s structure, but the core functionalities remain +consistent across all options. Here’s a flowchart that can help you make your decision.
+graph TD; + Q1[Is the TUI only for Wezterm users?] + Q2[Is Windows compatibility important?] + Q3[Are you familiar with Crossterm?] + Q4[Are you familiar with Termion?] + Crossterm + Termwiz + Termion + + Q1 -->|Yes| Termwiz + Q1 -->|No| Q2 + Q2 -->|Yes| Crossterm + Q2 -->|No| Q3 + Q3 -->|Yes| Crossterm + Q3 -->|No| Q4 + Q4 -->|Yes| Termion + Q4 -->|No| Crossterm ++
Though we try to make sure that all backends are fully-supported, the most commonly-used backend is +Crossterm. If you have no particular reason to use Termion or Termwiz, you will find it easiest to +learn Crossterm simply due to its popularity.
+ +Ratatui interfaces with the terminal emulator through a backend. These libraries enable Ratatui via
+the Terminal
type to draw styled text to the screen, manipulate the cursor, and interrogate
+properties of the terminal such as the console or window size. You application will generally also
+use the backend directly to capture keyboard, mouse and window events, and enable raw mode and the
+alternate screen.
Ratatui supports the following backends:
+CrosstermBackend
and the crossterm
(enabled by default).TermionBackend
and the termion
feature.TermwizBackend
and the termion
feature.TestBackend
which can be useful to unit test your application’s UIFor information on how to choose a backend see: Comparison
+Each backend supports Raw Mode (which changes how the terminal handles input and +output processing), an Alternate Screen which allows it to render to a +separate buffer than your shell commands use, and Mouse Capture, which allows +your application to capture mouse events.
+ +Mouse capture is a mode where the terminal captures mouse events such as clicks, scrolls, and +movement, and sends them to the application as special sequences or events. This enables the +application to handle and respond to mouse actions, providing a more interactive and graphical user +experience within the terminal. It’s particularly useful for applications like terminal-based games, +text editors, or other programs that require more direct interaction from the user.
+Each backend handles mouse capture differently, with variations in the types of events that can be +captured and how they are represented. As such, the behavior may vary depending on the backend being +used, and developers should consult the specific backend’s documentation to understand how it +implements mouse capture.
+ +Raw mode is a mode where the terminal does not perform any processing or handling of the input and
+output. This means that features such as echoing input characters, line buffering, and special
+character processing (e.g., CTRL-C
or SIGINT
) are disabled. This is useful for applications that
+want to have complete control over the terminal input and output, processing each keystroke
+themselves.
For example, in raw mode, the terminal will not perform line buffering on the input, so the +application will receive each key press as it is typed, instead of waiting for the user to press +enter. This makes it suitable for real-time applications like text editors, terminal-based games, +and more.
+Each backend handles raw mode differently, so the behavior may vary depending on the backend being +used. Be sure to consult the backend’s specific documentation for exact details on how it implements +raw mode.
+ + +There are many ways to handle events with the ratatui
library. Mostly becuase ratatui
does not
+directly expose any event catching; the programmer will depend on the chosen backend’s library.
However, there are a few ways to think about event handling that may help you. While this is not an +exhaustive list, it covers a few of the more common implementations. But remember, the correct way, +is the one that works for you and your current application.
+This is the simplest way to handle events because it handles all of the events as they appear. It is
+often simply a match on the results of event::read()?
(in crossterm) on the different supported
+keys. Pros: This has the advantage of requiring no message passing, and allows the programmer to
+edit all of the possible keyboard events in one place.
Cons: However, this particular way of handling events simply does not scale well. Because all +events are handled in one place, you will be unable to split different groups of keybinds out into +separate locations.
+This way of handling events involves polling for events in one place, and then sending +messages/calling sub functions with the event that was caught. Pros: This has a similar appeal to +the first method in its simplicity. With this paradigm, you can easily split extensive pattern +matching into sub functions that can go in separate files. This way is also the idea often used in +basic multi-threaded applications because message channels are used to pass multi-threaded safe +messages.
+Cons: This method requires a main loop to be running to consistently poll for events in a +centralized place.
+In this style, control of the Terminal
and the main loop to a sub-module. In this case, the entire
+rendering and event handling responsibilities can be safely passed to the sub-module. In theory, an
+application built like this doesn’t need a centralized event listener. Pros: There is no centralized
+event loop that you need to update whenever a new sub-module is created.
Cons: However, if several sub-modules in your application have similar event handling loops, this +way could lead to a lot of duplicated code.
+ +In this section, we will cover various concepts associated with terminal user interfaces, such as:
+The world of UI development consists mainly of two dominant paradigms: retained mode and immediate
+mode. Most traditional GUI libraries operate under the retained mode paradigm. However, ratatui
+employs the immediate mode rendering approach. for TUI development.
This makes ratatui
is different from GUI frameworks you might use, because it only updates when
+you tell it to.
Immediate mode rendering is a UI paradigm where the UI is recreated every frame. Instead of creating +a fixed set of UI widgets and updating their state, you “draw” your UI from scratch in every frame +based on the current application state.
+In a nutshell:
+In ratatui
, every frame draws the UI anew.
loop {
+ terminal.draw(|f| {
+ if state.condition {
+ f.render_widget(SomeWidget::new(), layout);
+ } else {
+ f.render_widget(AnotherWidget::new(), layout);
+ }
+ })?;
+}
+This article and the accompanying YouTube video is worth your +time if you are new to the immediate mode rendering paradigm.
+ +This 4 minute talk about IMGUI
is also tangentially relevant.
Backend.draw()
. Hence, if the rendering
+thread is inadvertently blocked, the UI will not update until the thread resumes.The ratatui
library in particular only handles how widget would be rendered to a “Backend”, e.g.
+crossterm
. The Backend
in question would use an external crate e.g. crossterm
for actually
+drawing to the terminal.
Event loop orchestration: Along with managing “the render loop”, developers are also
+responsible for handling “the event loop”. This involves deciding on a third-party library for the
+job. crossterm
is a popular crate to handle key inputs and you’ll find plenty of examples in the
+repository and online for how to use it. crossterm
also supports a async
event stream, if you
+are interested in using tokio
.
Architecture design considerations: With ratatui
, out of the box, there’s little to no help
+in organizing large applications. Ultimately, the decision on structure and discipline rests with
+the developer to be principled.
See the contributors graph on GitHub +for more up to date information.
+ +The ratatui-book is written in
+mdbook
.
The book is built as HTML pages as part of a +GitHub Action +and is available to view at https://ratatui-org.github.io/ratatui-book/.
+Feel free to make contributions if you’d like to improve the documentation.
+If you want to set up your local environment, you can run the following:
+cargo install mdbook --version 0.4.30
+cargo install mdbook-admonish --version 1.9.0
+cargo install mdbook-svgbob2 --version 0.3.0
+cargo install mdbook-linkcheck --version 0.7.7
+cargo install mdbook-mermaid --version 0.12.6
+cargo install mdbook-emojicodes --version 0.2.2
+
+These plugins allow additional features.
+mdbook-admonish
The following raw markdown:
+```admonish note
+This is a note
+```
+
+```admonish tip
+This is a tip
+```
+
+```admonish warning
+This is a warning
+```
+
+```admonish info
+This is a info
+```
+
+will render as the following:
+ + + + +mdbook-mermaid
The following raw markdown:
+```mermaid
+graph TD;
+ A-->B;
+ A-->C;
+ B-->D;
+ C-->D;
+```
+
+will render as the following:
+graph TD; + A-->B; + A-->C; + B-->D; + C-->D; ++
mdbook-svgbob2
The following raw markdown:
+```svgbob
+ .---.
+ /-o-/--
+ .-/ / /->
+ ( * \/
+ '-. \
+ \ /
+ '
+```
+
+will render as the following:
+ +mdbook-emojicodes
The following raw markdown:
+I love cats 🐱 and dogs 🐶, I have two, one's gray, like a raccoon 🦝, and the other
+one is black, like the night 🌃.
+
+will render as the following:
+I love cats 🐱 and dogs 🐶, I have two, one’s gray, like a raccoon 🦝, and the other +one is black, like the night 🌃.
+ +Check out the CONTRIBUTING GUIDE +for more information.
+Try to do one pull request per change. The time taken to review a PR grows exponential with the size +of the change. Small focused PRs will generally be much more faster to review. PRs that include both +refactoring (or reformatting) with actual changes are more difficult to review as every line of the +change becomes a place where a bug may have been introduced. Consider splitting refactoring / +reformatting changes into a separate PR from those that make a behavioral change, as the tests help +guarantee that the behavior is unchanged.
+tui-rs
for similar workThe original fork of Ratatui, tui-rs
, has a large amount of
+history of the project. Please search, read, link, and summarize any relevant
+issues,
+discussions and
+pull requests.
We use conventional commits and check for them as +a lint build step. To help adhere to the format, we recommend to install +Commitizen. By using this tool you automatically +follow the configuration defined in .cz.toml. Your commit messages should have enough +information to help someone reading the CHANGELOG understand what is new just from +the title. The summary helps expand on that to provide information that helps provide more context, +describes the nature of the problem that the commit is solving and any unintuitive effects of the +change. It’s rare that code changes can easily communicate intent, so make sure this is clearly +documented.
+The final version of your PR that will be committed to the repository should be rebased and tested +against main. Every commit will end up as a line in the changelog, so please squash commits that are +only formatting or incremental fixes to things brought up as part of the PR review. Aim for a single +commit (unless there is a strong reason to stack the commits). See +Git Best Practices - On Sausage Making +for more on this.
+We’re using cargo-husky to automatically run git hooks,
+which will run cargo make ci
before each push. To initialize the hook run cargo test
. If
+cargo-make
is not installed, it will provide instructions to install it for you. This will ensure
+that your code is formatted, compiles and passes all tests before you push. If you need to skip this
+check, you can use git push --no-verify
.
We use commit signature verification, which will block commits from being merged via the UI unless +they are signed. To set up your machine to sign commits, see +managing commit signature verification +in GitHub docs.
+Clone the repo and build it using cargo-make
+Ratatui is an ordinary Rust project where common tasks are managed with
+cargo-make. It wraps common cargo
commands with sane
+defaults depending on your platform of choice. Building the project should be as easy as running
+cargo make build
.
git clone https://github.com/ratatui-org/ratatui.git
+cd ratatui
+cargo make build
+
+The test coverage of the crate is reasonably good,
+but this can always be improved. Focus on keeping the tests simple and obvious and write unit tests
+for all new or modified code. Beside the usual doc and unit tests, one of the most valuable test you
+can write for Ratatui is a test against the TestBackend
. It allows you to assert the content of
+the output buffer that would have been flushed to the terminal after a given draw call. See
+widgets_block_renders
in tests/widgets_block.rs for an example.
When writing tests, generally prefer to write unit tests and doc tests directly in the code file
+being tested rather than integration tests in the tests/
folder.
If an area that you’re making a change in is not tested, write tests to characterize the existing
+behavior before changing it. This helps ensure that we don’t introduce bugs to existing software
+using Ratatui (and helps make it easy to migrate apps still using tui-rs
).
For coverage, we have two bacon jobs (one for all tests, and one for
+unit tests, keyboard shortcuts v
and u
respectively) that run
+cargo-llvm-cov to report the coverage. Several plugins
+exist to show coverage directly in your editor. E.g.:
We don’t currently use any unsafe code in Ratatui, and would like to keep it that way. However there +may be specific cases that this becomes necessary in order to avoid slowness. Please see +this discussion for more about the +decision.
+ +A lot of examples out there in the wild might use the following code for sending key presses:
+ CrosstermEvent::Key(e) => tx.send(Event::Key(e)),
+However, on Windows, when using Crossterm
, this will send the same Event::Key(e)
twice; one for
+when you press the key, i.e. KeyEventKind::Press
and one for when you release the key, i.e.
+KeyEventKind::Release
. On MacOS
and Linux
only KeyEventKind::Press
kinds of key
event is
+generated.
To make the code work as expected across all platforms, you can do this instead:
+ CrosstermEvent::Key(key) => {
+ if key.kind == KeyEventKind::Press {
+ event_tx.send(Event::Key(key)).unwrap();
+ }
+ },
+
+ ratatui
vs tui-realm
Fundamentally, the difference is that ratatui
is a library but
+tui-realm
is a framework.
The terms library and framework are often used interchangeably in software development, but they +serve different purposes and have distinct characteristics.
+While ratatui
provides tools (widgets) for building terminal UIs, it doesn’t dictate or enforce a
+specific way to structure your application. You need to decide how to best use the library in your
+particular context, giving you more flexibility.
In contrast, tui-realm
might provide more guidelines and enforcements about how your application
+should be structured or how data flows through it. And, for the price of that freedom, you get more
+features out of the box with tui-realm
and potentially lesser code in your application to do the
+same thing that you would with ratatui
.
tokio
and async
/await
?ratatui
isn’t a native async
library. So is it beneficial to use tokio
or async
/await
?
And as a user, there really is only one point of interface with the ratatui
library and that’s the
+terminal.draw(|f| ui(f))
functionality, because the rendering of widgets happens in ui(f)
.
+Everything else in your code is your own to do as you wish.
Should terminal.draw(|f| ui(f))
be async
? Possibly. Rendering to the terminal buffer is
+relatively fast, especially using the double buffer technique that only renders diffs that ratatui
+uses.
Can we make it async
ourselves? Yes, we can. Check out
+https://github.com/ratatui-org/ratatui-async-template for an example.
The only other part related to ratatui
that is beneficial to being async
is reading the key
+event inputs from stdin
, and that can be made async
with crossterm
’s event-stream.
So the real question is what other parts of your app require async
or benefit from being async
?
+If the answer is not much, maybe it is simpler to not use async
and avoiding tokio
.
Another way to think about it is, do you think your app would work better with 1 thread like this?
+ +Or would it work with 3 threads / tokio
tasks like this:
The former can be done without any async
code and the latter is the approach showcased in
+ratatui-async-template
with tokio
.
This project was forked from tui-rs
in February 2023, with
+the blessing of the original author, Florian Dehau
+(@fdehau).
The original repository contains all the issues, PRs and discussion that were raised originally, and +it is useful to refer to when contributing code, documentation, or issues with Ratatui.
+We imported all the PRs from the original repository and implemented many of the smaller ones and +made notes on the leftovers. These are marked as draft PRs and labelled as +imported from tui. +We have documented the current state of those PRs, and anyone is welcome to pick them up and +continue the work on them.
+We have not imported all issues opened on the previous repository. For that reason, anyone wanting +to work on or discuss an issue will have to follow the following workflow:
+Referencing issue #[<issue number>](<original issue link>)
You can then resume the conversation by replying to the new issue you have created.
+ +termwiz
ratatui
supports a new backend called termwiz
which is a “Terminal Wizardry” crate that powers
+wezterm.
To use it, enable the termwiz
feature in Cargo.toml
:
[dependencies.ratatui]
+version = "0.21.0"
+features = ["termwiz"]
+default-features = false
+
+Then you can utilize TermwizBackend
object for creating a terminal. Here is a simple program that
+shows a text on the screen for 5 seconds using ratatui
+ termwiz
:
use ratatui::{backend::TermwizBackend, widgets::Paragraph, Terminal};
+use std::{
+ error::Error,
+ thread,
+ time::{Duration, Instant},
+};
+
+fn main() -> Result<(), Box<dyn Error>> {
+ let backend = TermwizBackend::new()?;
+ let mut terminal = Terminal::new(backend)?;
+ terminal.hide_cursor()?;
+
+ let now = Instant::now();
+ while now.elapsed() < Duration::from_secs(5) {
+ terminal.draw(|f| f.render_widget(Paragraph::new("termwiz example"), f.size()))?;
+ thread::sleep(Duration::from_millis(250));
+ }
+
+ terminal.show_cursor()?;
+ terminal.flush()?;
+ Ok(())
+}
+
+A calendar widget has been added which was originally a part of the +extra-widgets repository.
+Since this new widget depends on time
crate, we gated it behind widget-calendar
feature to avoid
+an extra dependency:
[dependencies.ratatui]
+version = "0.21.0"
+features = ["widget-calendar"]
+
+Here is the example usage:
+Monthly::new(
+ time::Date::from_calendar_date(2023, time::Month::January, 1).unwrap(),
+ CalendarEventStore::default(),
+)
+.show_weekdays_header(Style::default())
+.show_month_header(Style::default())
+.show_surrounding(Style::default()),
+
+Results in:
+ January 2023
+ Su Mo Tu We Th Fr Sa
+ 1 2 3 4 5 6 7
+ 8 9 10 11 12 13 14
+ 15 16 17 18 19 20 21
+ 22 23 24 25 26 27 28
+ 29 30 31 1 2 3 4
+
+Circle
widget has been added with the use-case of showing an accuracy radius on the world map.
Here is an example of how to use it with Canvas
:
Canvas::default()
+ .paint(|ctx| {
+ ctx.draw(&Circle {
+ x: 5.0,
+ y: 2.0,
+ radius: 5.0,
+ color: Color::Reset,
+ });
+ })
+ .marker(Marker::Braille)
+ .x_bounds([-10.0, 10.0])
+ .y_bounds([-10.0, 10.0]),
+
+Results in:
+ ⡠⠤⢤⡀
+⢸⡁ ⡧
+ ⠑⠒⠚⠁
+
+This was a highly requested feature and the original implementation was done by +@fdehau himself. Folks at Atuin completed the +implementation and we are happy to finally have this incorporated in the new release!
+An inline viewport refers to a rectangular section of the terminal window that is set aside for +displaying content.
+In the repository, there is an example that simulates downloading multiple files in parallel: +https://github.com/ratatui-org/ratatui/blob/main/examples/inline.rs
+++Before you could only put the title on the top row of a Block. Now you can put it on the bottom +row! Revolutionary.
+
For example, place the title on the bottom and center:
+Paragraph::new("ratatui")
+ .alignment(Alignment::Center)
+ .block(
+ Block::default()
+ .title(Span::styled("Title", Style::default()))
+ .title_on_bottom()
+ .title_alignment(Alignment::Center)
+ .borders(Borders::ALL),
+ )
+
+Results in:
+┌─────────────────────┐
+│ ratatui │
+│ │
+└────────Title────────┘
+
+If we want to render a widget inside a Block
with a certain distance from its borders, we need to
+create another Layout
element based on the outer Block
, add a margin and render the Widget
+into it. Adding a padding property on the block element skips the creation of this second Layout.
This property works especially when rendering texts, as we can just create a block with padding and +use it as the text wrapper:
+let block = Block::default()
+ .borders(Borders::ALL)
+ .padding(Padding::new(1, 1, 2, 2));
+let paragraph = Paragraph::new("example paragraph").block(block);
+f.render_widget(paragraph, area);
+
+Rendering another widget should be easy too, using the .inner
method:
let block = Block::default().borders(Borders::ALL).padding(Padding {
+ left: todo!(),
+ right: todo!(),
+ top: todo!(),
+ bottom: todo!(),
+});
+let inner_block = Block::default().borders(Borders::ALL);
+let inner_area = block.inner(area);
+
+f.render_widget(block, area);
+f.render_widget(inner_block, inner_area);
+f.render_widget(paragraph, area);
+
+A new type called Masked
is added for text-related types for masking data with a mask character.
+The example usage is as follows:
Line::from(vec![
+ Span::raw("Masked text: "),
+ Span::styled(
+ Masked::new("password", '*'),
+ Style::default().fg(Color::Red),
+ ),
+])
+
+Results in:
+Masked text: ********
+
+border!
macroA border!
macro has been added that takes TOP
, BOTTOM
, LEFT
, RIGHT
, and ALL
and returns
+a Borders
object.
An empty border!()
call returns NONE
.
For example:
+border!(ALL)
+border!(LEFT, RIGHT)
+border!()
+
+This is gated behind a macros
feature flag to ensure short build times. To enable it, update
+Cargo.toml
as follows:
[dependencies.ratatui]
+version = "0.21.0"
+features = ["macros"]
+
+Going forward, we will most likely put the new macros behind macros
feature as well.
String
Have you ever needed this conversion?
+"black" => Color::Black,
+"red" => Color::Red,
+"green" => Color::Green,
+// etc.
+
+Don’t worry, we got you covered:
+Color::from_str("lightblue") // Color::LightBlue
+Color::from_str("10") // Color::Indexed(10)
+Color::from_str("#FF0000") // Color::Rgb(255, 0, 0)
+
+Spans
-> Line
Line
is a significantly better name over Spans
as the plural causes confusion and the type
+really is a representation of a line of text made up of spans.
So, Spans
is renamed as Line
and a deprecation notice has been added.
See +https://github.com/ratatui-org/ratatui/pull/178 +for more discussion.
+List
now has a len()
method for returning the number of itemsSparkline
now has a direction()
method for specifying the render direction (left to right /
+right to left)Table
and List
states now have offset()
and offset_mut()
methodsTestBackend
) with Display
implementationHere is the list of applications that has been added:
+Also, we moved APPS.md
file to the
+Wiki so check it out for more
+applications built with ratatui
!
tui-rs
We put together a migration guide at the Wiki: +Migrating from TUI
+Also, the minimum supported Rust version is 1.65.0
Any contribution is highly appreciated! There are +contribution guidelines for +getting started.
+Feel free to submit issues and throw in +ideas!
+If you are having a problem with ratatui
or want to contribute to the project or just want to
+chit-chat, feel free to join our Discord server!