Introduction: Building Your First TUI Monitoring Dashboard
Welcome to Chapter 15! So far, we’ve explored the foundational elements of Ratatui, from basic widgets and layouts to event handling. Now, it’s time to put all that knowledge into action by building a practical, real-world application: a system monitoring dashboard.
In this chapter, you’ll learn how to create an interactive terminal user interface that displays real-time system metrics like CPU and memory usage. This project will solidify your understanding of Ratatui’s layout system, state management, and event loops, while also introducing you to integrating external Rust crates for system information. By the end, you’ll have a functional TUI dashboard and a deeper appreciation for how all the pieces fit together to create a dynamic terminal application.
Before we dive in, make sure you’re comfortable with:
- Ratatui’s
Layoutsystem (covered in Chapter 7). - Basic widgets like
Block,Paragraph, andGauge(Chapters 5 & 6). - The
crosstermbackend and event handling (Chapter 8). - Managing application state (Chapter 9).
Let’s get cooking!
Core Concepts: Bringing Data to Your TUI
Building a monitoring dashboard involves three main conceptual pillars:
- Gathering System Metrics: How do we get information about the CPU, memory, and other system resources?
- Structuring the UI: How do we arrange multiple pieces of information on the terminal screen?
- Real-time Updates: How do we ensure the data displayed is fresh and responsive?
1. Gathering System Metrics with sysinfo
For obtaining system-level information in Rust, the sysinfo crate is an excellent, cross-platform choice. It provides a straightforward API to query CPU usage, memory consumption, disk I/O, network activity, and more.
Why sysinfo? It abstracts away the complexities of interacting with different operating system APIs (like /proc on Linux or Windows Management Instrumentation) and provides a unified, Rust-friendly interface. This means your monitoring dashboard will work on Linux, macOS, and Windows without needing platform-specific code.
We’ll use sysinfo to get:
- Overall CPU usage percentage.
- Total and used memory in bytes.
2. Structuring the UI: Layouts and Widgets
A monitoring dashboard often presents several pieces of information side-by-side or stacked. This is where Ratatui’s Layout system truly shines. We’ll divide the screen into logical areas using Layout::default().direction(...) and then render specific widgets within each area.
For our dashboard, we’ll aim for a simple layout: a main title, a section for CPU usage, and a section for memory usage. The Block widget will provide borders and titles for each section, while Gauge widgets will visually represent CPU and memory percentages.
3. Real-time Updates: The Event Loop and Timers
A static dashboard isn’t very useful! We need to refresh the displayed data periodically. Our main application loop, which you’re already familiar with, will be modified to:
- Poll for User Input: Check for keyboard events (e.g., ‘q’ to quit).
- Refresh System Data: Call
sysinfo’s refresh methods at regular intervals. - Redraw the UI: Update the Ratatui terminal with the latest data.
We’ll use crossterm::event::poll with a timeout to allow for non-blocking event handling and periodic data refreshes.
Here’s a conceptual flow of our application:
This diagram illustrates the continuous cycle of polling for events, updating our application’s data, and then rendering the UI based on that updated data.
Step-by-Step Implementation: Building the Dashboard
Let’s start coding our monitoring dashboard!
Step 1: Project Setup and Dependencies
First, create a new Rust project:
cargo new ratatui-dashboard
cd ratatui-dashboard
Now, open your Cargo.toml file and add the necessary dependencies. We’ll need ratatui for the UI, crossterm for terminal interaction, and sysinfo for system information.
# In your Cargo.toml
[package]
name = "ratatui-dashboard"
version = "0.1.0"
edition = "2021"
[dependencies]
ratatui = "0.26.0" # Check crates.io for the absolute latest stable version
crossterm = "0.27.0" # Check crates.io for the absolute latest stable version
sysinfo = "0.30.0" # Check crates.io for the absolute latest stable version
Why these versions? As of March 2026, ratatui is a rapidly evolving library, and 0.26.0 (or similar) represents a stable, feature-rich release. crossterm 0.27.0 is its common and robust backend. sysinfo 0.30.0 provides comprehensive system metrics. Always check crates.io for the absolute latest stable versions if you encounter compilation issues with these specific numbers.
Step 2: Basic Application Structure
Let’s start with the boilerplate for a Ratatui application. Open src/main.rs and add the following:
// src/main.rs
use std::{error::Error, io};
use crossterm::{
event::{self, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::CrosstermBackend,
Terminal,
};
use sysinfo::{System, SystemExt, CpuExt, RefreshKind}; // New import for sysinfo
/// Represents the application's state.
/// This struct will hold all the data our TUI needs to display.
struct App {
sys: System,
should_quit: bool,
}
impl App {
/// Constructs a new `App`.
fn new() -> App {
App {
sys: System::new_with_specifics(RefreshKind::new().with_cpu().with_memory()),
should_quit: false,
}
}
/// Handles an incoming event.
fn handle_event(&mut self, event: &Event) {
if let Event::Key(key) = event {
if key.code == KeyCode::Char('q') {
self.should_quit = true;
}
}
}
/// Updates the application's state (e.g., refreshes system info).
fn update(&mut self) {
// We only refresh CPU and memory, as specified in `new()`
self.sys.refresh_cpu();
self.sys.refresh_memory();
}
}
fn main() -> Result<(), Box<dyn Error>> {
// 1. Setup terminal
enable_raw_mode()?;
execute!(io::stdout(), EnterAlternateScreen)?;
let backend = CrosstermBackend::new(io::stdout());
let mut terminal = Terminal::new(backend)?;
// 2. Create app and run it
let mut app = App::new();
let res = run_app(&mut terminal, &mut app);
// 3. Restore terminal
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
// 4. Handle result
if let Err(err) = res {
println!("{:?}", err)
}
Ok(())
}
fn run_app(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>, app: &mut App) -> Result<(), Box<dyn Error>> {
loop {
// Draw the UI
terminal.draw(|f| {
// We'll add drawing logic here later!
// For now, it's empty.
})?;
// Handle events
if event::poll(std::time::Duration::from_millis(250))? {
if let Event::Key(key) = event::read()? {
if key.code == KeyCode::Char('q') {
break; // Exit the loop on 'q'
}
}
}
// Update app state
app.update();
// Check if app wants to quit
if app.should_quit {
break;
}
}
Ok(())
}
Explanation of changes:
use sysinfo::...: We’ve added imports for thesysinfocrate.struct App: Now includes asys: Systemfield to hold our system information.App::new(): Initializessysinfo::System. We useSystem::new_with_specificsandRefreshKind::new().with_cpu().with_memory()to tellsysinfoto only refresh CPU and memory information, which is more efficient than refreshing everything if we don’t need it.App::update(): This method is now responsible for callingself.sys.refresh_cpu()andself.sys.refresh_memory()to get the latest data.run_apploop: Theevent::pollnow has a timeout of 250 milliseconds. This means if no events occur within 250ms, thepollfunction returnsfalse, and the loop continues toapp.update()andterminal.draw(). This ensures our dashboard updates even without user input.
You can try running this now (cargo run), but it will just show an empty screen until you press ‘q’.
Step 3: Drawing the Dashboard Layout
Now, let’s create our UI. We’ll divide the screen into three horizontal sections: a header, a CPU section, and a Memory section.
Modify the terminal.draw closure inside run_app:
// Inside run_app, replace the empty terminal.draw closure:
terminal.draw(|f| {
use ratatui::{
layout::{Constraint, Direction, Layout},
style::{Color, Style},
widgets::{Block, Borders, Paragraph, Gauge},
}; // Added necessary imports inside the closure for clarity
let size = f.size();
// Divide the screen into a header and two main sections
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), // Header (3 lines tall)
Constraint::Min(0), // Main content area
])
.split(size);
// Further divide the main content area for CPU and Memory
let main_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage(50), // CPU section
Constraint::Percentage(50), // Memory section
])
.split(chunks[1]); // Split the second chunk from the first layout
// 1. Header Block
let header_block = Block::default()
.borders(Borders::ALL)
.style(Style::default().fg(Color::LightCyan))
.title(" Ratatui System Monitor ");
f.render_widget(header_block, chunks[0]);
// 2. CPU Block
let cpu_block = Block::default()
.borders(Borders::ALL)
.style(Style::default().fg(Color::Green))
.title(" CPU Usage ");
f.render_widget(cpu_block, main_chunks[0]);
// 3. Memory Block
let mem_block = Block::default()
.borders(Borders::ALL)
.style(Style::default().fg(Color::Yellow))
.title(" Memory Usage ");
f.render_widget(mem_block, main_chunks[1]);
})?; // End of terminal.draw closure
Explanation:
- We bring in
Layout,Constraint,Direction,Block,Borders,Paragraph,Gauge(thoughParagraphandGaugearen’t used yet, they’re common for dashboards). Layout::default().direction(Direction::Vertical): We first split the screen vertically.- The
Constraint::Length(3)makes the top chunk exactly 3 lines tall for our header. Constraint::Min(0)makes the remaining space take up the rest of the screen.
- The
Layout::default().direction(Direction::Horizontal): We then take the second chunk (chunks[1], which is the main content area) and split it horizontally into two equal halves (Constraint::Percentage(50)).- We create
Blockwidgets for the header, CPU, and Memory sections, giving them borders and titles. We also apply different foreground colors for visual distinction. f.render_widget(): This function takes a widget and an area (aRect) and draws the widget within that area.
Run cargo run now. You should see a terminal with three bordered sections: “Ratatui System Monitor”, “CPU Usage”, and “Memory Usage”. Press ‘q’ to quit.
Step 4: Displaying CPU and Memory Usage with Gauge
Now that we have our layout, let’s fill it with actual data! We’ll use the Gauge widget for a visual representation of usage.
Inside the terminal.draw closure, after rendering the cpu_block and mem_block, add the following code:
// ... (previous code for layout and blocks)
// --- CPU Gauge ---
let cpu_usage = app.sys.global_cpu_info().cpu_usage();
let cpu_gauge = Gauge::default()
.block(Block::default().borders(Borders::NONE)) // No extra borders, just content
.gauge_style(Style::default().fg(Color::Green).bg(Color::DarkGray))
.percent(cpu_usage as u16) // Gauge expects u16 percentage
.label(format!("{:.1}%", cpu_usage)); // Display the exact percentage as a label
// Calculate the inner area for the CPU gauge (inside the CPU block)
let cpu_inner_area = main_chunks[0].inner(&ratatui::layout::Margin {
vertical: 1,
horizontal: 1,
});
f.render_widget(cpu_gauge, cpu_inner_area);
// --- Memory Gauge ---
let total_memory = app.sys.total_memory();
let used_memory = app.sys.used_memory();
let mem_percentage = if total_memory > 0 {
(used_memory as f64 / total_memory as f64 * 100.0) as u16
} else {
0
};
let mem_label = format!("Used: {}MB / Total: {}MB ({:.1}%)",
used_memory / 1024 / 1024, // Convert bytes to MB
total_memory / 1024 / 1024,
mem_percentage as f64);
let mem_gauge = Gauge::default()
.block(Block::default().borders(Borders::NONE))
.gauge_style(Style::default().fg(Color::Yellow).bg(Color::DarkGray))
.percent(mem_percentage)
.label(mem_label);
// Calculate the inner area for the Memory gauge
let mem_inner_area = main_chunks[1].inner(&ratatui::layout::Margin {
vertical: 1,
horizontal: 1,
});
f.render_widget(mem_gauge, mem_inner_area);
Explanation:
- CPU Usage:
app.sys.global_cpu_info().cpu_usage(): Retrieves the overall CPU usage percentage (as af32).Gauge::default().percent(cpu_usage as u16): Creates aGaugewidget and sets its fill percentage. We castf32tou16aspercentexpectsu16..label(format!("{:.1}%", cpu_usage)): Adds a text label to the gauge, showing the precise percentage.main_chunks[0].inner(...): This is a crucial trick! We want the gauge to be inside the CPU block, not overlap its borders. Theinner()method withMarginshrinks aRectby the specified amount, giving us a smallerRectto render the gauge into.
- Memory Usage:
app.sys.total_memory()andapp.sys.used_memory(): Get total and used memory in bytes.- We calculate the
mem_percentageand handle the division to avoid errors iftotal_memoryis zero. - The
mem_labelis formatted to show both MB and percentage for more detail. - Similar to CPU,
mem_inner_areaensures the gauge is rendered neatly within its block.
Run cargo run again! You should now see a live, updating dashboard showing your CPU and memory usage. The gauges will animate and the percentages will change in real-time. This is exciting! Press ‘q’ to quit.
Step 5: Enhancing the Header with a Paragraph
Let’s add a bit more information to our header, perhaps the current date and time. We can use a Paragraph widget for this.
Inside the terminal.draw closure, after rendering the header_block, add the following:
// ... (previous code for header_block)
// Header Paragraph (inside the header block)
let now = chrono::Local::now(); // Requires `chrono` crate
let datetime_str = now.format("%Y-%m-%d %H:%M:%S").to_string();
let header_paragraph = Paragraph::new(format!("Current Time: {}", datetime_str))
.style(Style::default().fg(Color::White))
.alignment(ratatui::layout::Alignment::Center);
let header_inner_area = chunks[0].inner(&ratatui::layout::Margin {
vertical: 1,
horizontal: 1,
});
f.render_widget(header_paragraph, header_inner_area);
// ... (rest of the drawing logic)
Wait! The chrono crate is not yet in our Cargo.toml. Let’s add it:
# In your Cargo.toml, under [dependencies]
chrono = "0.4.34" # Check crates.io for the absolute latest stable version, with "local-tz" feature
We need the local-tz feature for chrono::Local::now().
So, it should be:
# In your Cargo.toml, under [dependencies]
chrono = { version = "0.4.34", features = ["local-tz"] } # Check crates.io for the absolute latest stable version
Explanation:
- We use the
chronocrate (a popular date/time library in Rust) to get the current local time. Paragraph::new(...): Creates a text widget..style(...)and.alignment(...): Style the text to be white and centered.- Again,
header_inner_areaensures the paragraph is drawn inside the block’s borders.
Run cargo run again. Now your header will display the current date and time, updating every 250ms along with the system metrics!
Mini-Challenge: Extend the Dashboard
You’ve built a solid foundation for a monitoring dashboard! Now, let’s try to add another piece of information.
Challenge: Add a section to display the hostname of the system.
Hints:
- The
sysinfocrate has aSystemExt::host_name()method. - You’ll need to adjust your
Layoutto accommodate this new section. Perhaps you could make the CPU and Memory sections vertical, and then add the hostname below them, or create a third column. - A
Paragraphwidget will be suitable for displaying the hostname. - Remember to use
inner()withMarginto place yourParagraphinside itsBlock.
What to Observe/Learn: This challenge will reinforce your understanding of Layout manipulation, accessing sysinfo data, and rendering text with Paragraph. Don’t be afraid to experiment with different layout configurations!
Common Pitfalls & Troubleshooting
- “Application doesn’t update, or updates too slowly”:
- Cause: The
event::polltimeout is too long, orapp.update()is not being called frequently enough. - Fix: Ensure
event::pollhas a reasonable timeout (e.g., 100-500ms). Verify thatapp.update()is called in every iteration of the main loop.
- Cause: The
- “UI elements overlap or are not positioned correctly”:
- Cause: Incorrect
Layoutconstraints, or widgets being rendered into the wrongRectareas. - Fix: Carefully review your
Layout::default().constraints(...)setup. Usef.render_widget(widget, area)with the correctarea(e.g.,chunks[0],main_chunks[1]). Remember to usearea.inner(...)to get a sub-rectangle for content inside a bordered block.
- Cause: Incorrect
- “Missing
chronoorsysinfofunctionality”:- Cause: You forgot to add the dependency to
Cargo.toml, or you missed a required feature (likelocal-tzforchrono). - Fix: Double-check your
Cargo.tomlfor all required crates and their features. Runcargo clean && cargo buildto ensure all dependencies are fetched and compiled correctly.
- Cause: You forgot to add the dependency to
- “Terminal not restoring correctly on exit”:
- Cause: The
disable_raw_mode()orLeaveAlternateScreencalls are missed, or an unhandled error prevents them from executing. - Fix: Ensure your
mainfunction’s setup and teardown logic is robust, especially theexecute!calls for terminal restoration. TheResult<(), Box<dyn Error>>return type helps ensure cleanup even ifrun_appfails.
- Cause: The
Summary
Congratulations! You’ve successfully built a real-time system monitoring dashboard using Ratatui and sysinfo.
Here are the key takeaways from this chapter:
- System Metrics Integration: You learned how to use the
sysinfocrate to gather CPU and memory usage information from the underlying operating system. - Dynamic Layouts: You practiced using
LayoutwithDirection::VerticalandDirection::Horizontalto create a structured dashboard interface. - Widget Application: You applied
Blockfor borders and titles,Paragraphfor displaying text, andGaugefor visual progress bars of system usage. - Real-time Updates: You implemented a robust event loop using
crossterm::event::pollwith a timeout to refresh data and redraw the UI periodically, creating a live monitoring experience. - Application State Management: You saw how to manage your application’s data (like
sysinfo::System) within theAppstruct.
This project demonstrates the power and flexibility of Ratatui for building functional and visually appealing terminal applications.
What’s Next?
In the next chapter, we’ll delve into more advanced topics, perhaps exploring how to handle multiple screens or tabs within a single TUI application, or dive deeper into custom widgets and styling for even more dynamic interfaces. Keep building, keep experimenting!
References
- Ratatui Official GitHub Repository
- Crossterm Official GitHub Repository
- Sysinfo Crate Documentation
- Chrono Crate Documentation
- Ratatui Layout Documentation
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.