Skip to content
kd
16 May 2024
Back to blog

The Basic Building blocks of Ratatui - Part 3

4 min read (727 words)

Table of contents

Ratatui is a crate for building terminal user interfaces in Rust.

In this post, we'll explore the text primitives of ratatui.

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("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")
.replace("'", "&#39;")
}
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);
}

Text primitives

In Ratatui, there are 3 fundamental text primitives that you should be aware of.

Span

The first is a Span.

use ratatui::text::Span;
let span = Span::raw("hello world");
span
Span { content: "hello world", style: Style { fg: None, bg: None, underline_color: None, add_modifier: NONE, sub_modifier: NONE } }

A Span contains two fields.

span.content
"hello world"
span.style
Style { fg: None, bg: None, underline_color: None, add_modifier: NONE, sub_modifier: NONE }

A Style object contains foreground color, background color, and modifiers for whether the style being applied is bold, italics, etc

There are a number of constructors for Span that you may use, but ratatui exposes a Stylize trait that makes it easy to style content which I find very useful.

use ratatui::style::Stylize; // required trait to use style methods
"hello world".bold()
Span { content: "hello world", style: Style { fg: None, bg: None, underline_color: None, add_modifier: BOLD, sub_modifier: NONE } }

You can even chain these trait methods to add more styles:

"hello world".bold().yellow().on_black()
Span { content: "hello world", style: Style { fg: Some(Yellow), bg: Some(Black), underline_color: None, add_modifier: BOLD, sub_modifier: NONE } }
show_html(span_to_html("hello world".bold()))
hello world
show_html(span_to_html("hello world".yellow().bold().on_black()))
hello world

With ratatui-macros, you can even use a format! style macro to create a Span

use ratatui_macros::span;
let world = "world";
span!("hello {}", world)
Span { content: "hello world", style: Style { fg: None, bg: None, underline_color: None, add_modifier: NONE, sub_modifier: NONE } }

Line

The second primitive to be aware of is a Line.

A line consists of one or more spans.

use ratatui::text::Line;
let line = Line::raw("hello world");
line
Line { spans: [Span { content: "hello world", style: Style { fg: None, bg: None, underline_color: None, add_modifier: NONE, sub_modifier: NONE } }], style: Style { fg: None, bg: None, underline_color: None, add_modifier: NONE, sub_modifier: NONE }, alignment: None }
line.spans
[Span { content: "hello world", style: Style { fg: None, bg: None, underline_color: None, add_modifier: NONE, sub_modifier: NONE } }]
line.spans.len()
1

A unique feature of lines is that new lines are removed but the content is split into multiple spans.

let line = Line::raw("hello world\ngoodbye world");
line.spans[0]
Span { content: "hello world", style: Style { fg: None, bg: None, underline_color: None, add_modifier: NONE, sub_modifier: NONE } }
line.spans[1]
Span { content: "goodbye world", style: Style { fg: None, bg: None, underline_color: None, add_modifier: NONE, sub_modifier: NONE } }

A line can also be styled with methods from the Stylize trait:

Line::raw("hello world").bold()
Line { spans: [Span { content: "hello world", style: Style { fg: None, bg: None, underline_color: None, add_modifier: NONE, sub_modifier: NONE } }], style: Style { fg: None, bg: None, underline_color: None, add_modifier: BOLD, sub_modifier: NONE }, alignment: None }

In this case, the individual span's styles are left untouched but the Line's style is updated.

Another unique feature about Line is that they can be aligned.

let centered_line = line.centered();
centered_line
Line { spans: [Span { content: "hello world", style: Style { fg: None, bg: None, underline_color: None, add_modifier: NONE, sub_modifier: NONE } }, Span { content: "goodbye world", style: Style { fg: None, bg: None, underline_color: None, add_modifier: NONE, sub_modifier: NONE } }], style: Style { fg: None, bg: None, underline_color: None, add_modifier: NONE, sub_modifier: NONE }, alignment: Some(Center) }

With ratatui-macros, you can create a Line using the line! macro using a vec!-like syntax.

use ratatui_macros::line;
line!["hello", " ", "world"].yellow().bold().centered();

Every element in the line! macro is converted to a Span.

Text

Finally there is Text, which is a collection of Lines.

use ratatui::text::Text;
Text::from(vec![Line::raw("hello world"), Line::raw("goodbye world")]);

With ratatui-macros, you can create a Text using text! macro using a vec!-like syntax.

use ratatui_macros::text;
text!["hello world", "goodbye world"];

Here, every element in the text! macro is converted to a Line.

Like Line, Text can also be aligned. In this case, the alignment occurs on every Line inside the Text.

let t = text!["hello world", "goodbye world"].right_aligned();
t.alignment
Some(Right)
use ratatui::widgets::Widget;
let (x, y, width, height) = (0, 0, 50, 5);
let area = ratatui::layout::Rect::new(x, y, width, height);
let mut buf = ratatui::buffer::Buffer::empty(area);
text![
"left aligned bold text".bold(),
"center aligned italic text".italic().into_centered_line(),
"right aligned with yellow on black".yellow().on_black().into_right_aligned_line(),
].render(area, &mut buf);
show_html(buffer_to_html(&buf))
left aligned bold text                            
            center aligned italic text            
                right aligned with yellow on black
                                                  
                                                  

Conclusion

In the next post, we'll examine the a few commonly used widgets.


Citation

@online{krishnamurthy2024thebasicbuildingblocksofratatuipart3,
  author = {Dheepak Krishnamurthy},
  title = {The Basic Building blocks of Ratatui - Part 3},
  year = {2024},
  date = {2024-05-16},
  url = {https://kdheepak.com/blog/the-basic-building-blocks-of-ratatui-part-3/},
  langid = {en},
}

For attribution, please cite this work as:

Dheepak Krishnamurthy, "The Basic Building blocks of Ratatui - Part 3", May 16, 2024 https://kdheepak.com/blog/the-basic-building-blocks-of-ratatui-part-3/


The Basic Building blocks of Ratatui - Part 4
The Basic Building blocks of Ratatui - Part 2