Ratatui is a crate for building terminal user interfaces in Rust.
In this post we’ll show how to build custom widgets.
Code
:dep ratatui = "0.26.2"
:dep ratatui-macros = "0.4.0"
fn span_to_html(s: ratatui::text::Span) -> String{
let mut html = String::new();
html.push_str("<span style=\"");
// Set foreground color
if let Some(color) = &s.style.fg {
html.push_str(&format!("color: {};", color));
}
// Set background color
if let Some(color) = &s.style.bg {
html.push_str(&format!("background-color: {};", color));
}
// Add modifiers
match s.style.add_modifier {
ratatui::style::Modifier::BOLD => html.push_str("font-weight: bold;"),
ratatui::style::Modifier::ITALIC => html.push_str("font-style: italic;"),
ratatui::style::Modifier::UNDERLINED => html.push_str("text-decoration: underline;"),
_ => {}
}
html.push_str("\">");
html.push_str(&s.content);
html.push_str("</span>");
html
}
fn buffer_to_html(buf: &ratatui::buffer::Buffer) -> String {
fn escape_special_html_characters(text: &str) -> String {
text.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace("\"", """)
.replace("'", "'")
}
let mut html = String::from("<pre><code>");
let w = buf.area.width;
let h = buf.area.height;
for y in 0..h {
for x in 0..w {
let s = buf.get(x, y).symbol();
let escaped = escape_special_html_characters(s);
let style = buf.get(x, y).style();
let span = ratatui::text::Span::styled(s, style);
html.push_str(&span_to_html(span));
}
html.push('\n');
}
html.push_str("</code></pre>");
html
}
fn show_html<D>(content: D) where D: std::fmt::Display {
println!(r#"EVCXR_BEGIN_CONTENT text/html
<div style="display: flex; justify-content:start; gap: 1em; margin: 1em">
{}
</div>
EVCXR_END_CONTENT"#, content);
}Buffer
We saw from an earlier part that frame has a buffer_mut() method:
let backend = ratatui::backend::TestBackend::new(80, 5);
let mut terminal = ratatui::terminal::Terminal::new(backend).unwrap();
terminal.draw(|frame| {
let mut buffer = frame.buffer_mut();
dbg!(buffer);
});[src/lib.rs:169:5] buffer = Buffer {
area: Rect { x: 0, y: 0, width: 80, height: 5 },
content: [
" ",
" ",
" ",
" ",
" ",
],
styles: [
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
]
}We saw how to draw to the Buffer using predefined widgets.
use ratatui::widgets::Widget;
let backend = ratatui::backend::TestBackend::new(80, 5);
let mut terminal = ratatui::terminal::Terminal::new(backend).unwrap();
terminal.draw(|frame| {
let area = frame.size();
let mut buffer = frame.buffer_mut();
ratatui::widgets::Block::bordered().render(area, &mut buffer);
dbg!(buffer);
});[src/lib.rs:172:5] buffer = Buffer {
area: Rect { x: 0, y: 0, width: 80, height: 5 },
content: [
"┌──────────────────────────────────────────────────────────────────────────────┐",
"│ │",
"│ │",
"│ │",
"└──────────────────────────────────────────────────────────────────────────────┘",
],
styles: [
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
]
}But it is also possible to draw to the Buffer manually:
let backend = ratatui::backend::TestBackend::new(80, 5);
let mut terminal = ratatui::terminal::Terminal::new(backend).unwrap();
terminal.draw(|frame| {
let area = frame.size();
let mut buffer = frame.buffer_mut();
ratatui::widgets::Block::bordered().render(area, &mut buffer);
let x = 15;
let y = 2;
let string = "The quick brown fox jumps over the lazy dog.";
let style = ratatui::style::Style::default();
buffer.set_string(x, y, string, style);
show_html(buffer_to_html(buffer));
});┌──────────────────────────────────────────────────────────────────────────────┐
│ │
│ The quick brown fox jumps over the lazy dog. │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
Widget
The Widget trait in ratatui allows you to define what you want to draw
to a Buffer:
struct MyCustomWidget {
counter: usize
}
impl Widget for MyCustomWidget {
fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) {
let x = 15;
let y = 2;
let string = format!("The quick brown fox jumps over the lazy dog - {}", self.counter);
let style = ratatui::style::Style::default();
buf.set_string(x, y, string, style);
}
}let backend = ratatui::backend::TestBackend::new(80, 5);
let mut terminal = ratatui::terminal::Terminal::new(backend).unwrap();
terminal.draw(|frame| {
let area = frame.size();
let mut buffer = frame.buffer_mut();
ratatui::widgets::Block::bordered().render(area, &mut buffer);
MyCustomWidget{ counter: 2 }.render(area, &mut buffer);
show_html(buffer_to_html(buffer));
});┌──────────────────────────────────────────────────────────────────────────────┐
│ │
│ The quick brown fox jumps over the lazy dog - 2 │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
Here’s an example of filling the entire buffer with the symbol ▒:
struct Hatcher;
impl Widget for Hatcher {
fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) {
for x in area.left()..area.right() {
for y in area.top()..area.bottom() {
buf.set_string(x, y, "▒", ratatui::style::Style::default());
}
};
}
}Here’s the output of a Hatcher being rendered:
let backend = ratatui::backend::TestBackend::new(80, 5);
let mut terminal = ratatui::terminal::Terminal::new(backend).unwrap();
terminal.draw(|frame| {
let area = frame.size();
let mut buffer = frame.buffer_mut();
Hatcher.render(area, &mut buffer);
show_html(buffer_to_html(buffer));
});▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
Rendering multiple widgets renders to the buffer in the order they are
called, and hence latter render calls can overwrite earlier renders.
let backend = ratatui::backend::TestBackend::new(80, 5);
let mut terminal = ratatui::terminal::Terminal::new(backend).unwrap();
terminal.draw(|frame| {
let area = frame.size();
let mut buffer = frame.buffer_mut();
Hatcher.render(area, &mut buffer);
MyCustomWidget{ counter: 2 }.render(area, &mut buffer);
show_html(buffer_to_html(buffer));
});▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒The quick brown fox jumps over the lazy dog - 2▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
StatefulWidget
Sometimes you want to store information that is only known during render
for later use. For this, you can use a StatefulWidget:
use ratatui::widgets::StatefulWidget;
#[derive(Default, Debug)]
struct Data {
area: (usize, usize)
}
struct MyCustomWidgetWithState;
impl StatefulWidget for MyCustomWidgetWithState {
type State = Data;
fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer, state: &mut Self::State) {
state.area = (area.width as usize, area.height as usize);
}
}let backend = ratatui::backend::TestBackend::new(80, 5);
let mut terminal = ratatui::terminal::Terminal::new(backend).unwrap();
let mut data = Data::default();
println!("before: {:?}", &data);
terminal.draw(|frame| {
let area = frame.size();
let mut buffer = frame.buffer_mut();
MyCustomWidgetWithState.render(area, &mut buffer, &mut data);
});
println!("after: {:?}", &data);before: Data { area: (0, 0) }
after: Data { area: (80, 5) }Conclusion
In these series of posts, we examined how Ratatui works under the hood. For more information, check out the official tutorials and documentation.