The Basic Building blocks of Ratatui - Part 3

Author

Dheepak Krishnamurthy

Published

May 17, 2024

Keywords

rust, ratatui

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.

Reuse

Citation

BibTeX citation:
@online{krishnamurthy2024,
  author = {Krishnamurthy, Dheepak},
  title = {The {Basic} {Building} Blocks of {Ratatui} - {Part} 3},
  date = {2024-05-17},
  url = {https://kdheepak.com/blog/the-basic-building-blocks-of-ratatui-part-3/},
  langid = {en}
}
For attribution, please cite this work as:
D. Krishnamurthy, “The Basic Building blocks of Ratatui - Part 3,” May 17, 2024. https://kdheepak.com/blog/the-basic-building-blocks-of-ratatui-part-3/.