Rust and TUI: A Romantic Affair

Tapas Das
6 min readMar 31, 2023

--

Pic Courtesy: https://www.talentopia.com/news/the-rust-programming-language-its-history-and-why/

Rust: A brief history

For decades, coders wrote critical systems in C and C++. Now they turn to Rust.

In 2006, Hoare was a 29-year-old computer programmer working for Mozilla, the open-source browser company. An out of order elevator (owing to software crash), and the pain of climbing 21 floors to his apartment, resulted in Hoare designing a new computer language, one that he hoped would make it possible to write small, fast code without memory bugs.

He named it Rust, after a group of remarkably hardy fungi that are, he says, “over-engineered for survival.”

Seventeen years later, Rust has become one of the hottest new languages on the planet — maybe the hottest. There are 2.8 million coders writing in Rust, and companies from Microsoft to Amazon regard it as key to their future. The chat platform Discord used Rust to speed up its system, Dropbox uses it to sync files to your computer, and Cloudflare uses it to process more than 20% of all internet traffic.

Idea Origin

For a long time, I was loooking for a hobby project to build using Rust. Finally I decided to build a terminal-centric RSS feed reader, which should be user-friendly and blazingly fast.

The first challenge was to design and build the terminal interface that users will interact with. Below is the rough sketch I came up with for the UI design.

At the top, the application heading will be displayed. Below that the menu options will be shown like Add, Update, Remove, Help, Quit, etc.

Below the menu bar, list of RSS feeds, corresponding articles list and descriptions will be shown. And, the bottom section will show license and maintainer information.

Let’s get into coding.

Dependency Rust Packages

I’m using the below rust packages to help render the UI screen as per the design shown above.

  1. Crossterm:

It provides clearing, event (input) handling, styling, cursor movement, and terminal actions for both Windows and UNIX systems. Crossterm aims to be simple and easy to call in code. Through the simplicity of Crossterm, you do not have to worry about the platform you are working with.

2. TUI:

This is a Rust library used to build rich terminal users interfaces and dashboards.

I’ll add both these packages into the dependency section in Cargo.toml file.

Clearing the terminal for the app to run

enable_raw_mode().expect("can run in raw mode");
let mut terminal = Terminal::new(CrosstermBackend::new(io::stdout()))?;
terminal.clear()?;

The 1st line sets the terminal to raw (or noncanonical) mode, which eliminates the need to wait for an Enter by the user to react to the input.

The next 2 lines define a CrosstermBackend using stdout and uses it in a TUI Terminal, clearing it initially and implicitly checking that everything works.

Rendering widgets in TUI

Let’s start by implementing a render loop, which calls terminal.draw() on every iteration.

loop {
terminal.draw(|rect| {
let size = rect.size();
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
.constraints(
[
Constraint::Length(3), // Heading
Constraint::Length(3), // Menu
Constraint::Min(2), // Content
Constraint::Length(3), // Footer
]
.as_ref(),
)
.split(size);

The draw function includes a closure, which receives a Rect. This is simply a layout primitive for a rectangle used in TUI, which is used to define where widgets should be rendered.

The next step is to define the chunks of the layout. I have a traditional vertical layout with 4 boxes:

  1. Heading
  2. Menu
  3. Content
  4. Footer

The content is further sub-divided horizontally into 3 boxes:

  1. RSS feed list
  2. Articles list
  3. Article summary
            let rss_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints(
[
Constraint::Percentage(20), // RSS feed list
Constraint::Percentage(30), // Articles list
Constraint::Percentage(50), // Article summary
]
.as_ref(),
)
.split(chunks[2]); // Content

Defining the application heading

let app_heading = "Application Heading";
...
let heading = Paragraph::new(app_heading)
.style(Style::default().fg(Color::Yellow))
.alignment(Alignment::Center)
.block(
Block::default()
.borders(Borders::ALL)
.style(Style::default().fg(Color::White))
.border_type(BorderType::Plain),
);

rect.render_widget(heading, chunks[0]);

The Paragraph widget is one of the many preexisting widgets in TUI. I’ve defined the paragraph with a hardcoded text, set the style by using a different foreground color using .fg on the default style, set the alignment to center, and then define a block.

The “block” is important because it’s a “base” widget, meaning it can be used by all other widgets to be rendered into. A block defines an area where you can put a title and an optional border around the content you’re rendering inside the box.

Finally, the “heading” widget is rendered in chunks[0], which is reserved for displaying the application heading.

Defining the menu titles

let menu_titles = vec!["Add", "Update", "Delete", "Quit"];
...
let menu = menu_titles
.iter()
.map(|t| {
let (first, rest) = t.split_at(1);
Spans::from(vec![
Span::styled(
first,
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::UNDERLINED),
),
Span::styled(rest, Style::default().fg(Color::White)),
])
})
.collect();

let menu_titles = Tabs::new(menu)
.block(Block::default().title("Menu").borders(Borders::ALL))
.style(Style::default().fg(Color::White))
.highlight_style(Style::default().fg(Color::Yellow))
.divider(Span::raw(" | "));

rect.render_widget(menu_titles, chunks[1]);

TUI comes with a Tabs widget out of the box, that helps in rendering tabs-based menu. To render the menu, I’ll first create a list of Span (like the HTML <span> tag 😉) elements for holding the menu labels and then put them inside the Tabs widget.

I’m iterating over the hardcoded menu labels and for each one, I’m splitting the string at the first character. This is done to style the first character differently, giving it a different color and an underline, to indicate to the user, that this is the character they need to type in order to activate this menu item.

The result of this operation is a Spans element, which is simply a list of Span. This is nothing more than multiple, optionally styled, pieces of text. These spans are then put into a Tabs widget. I’ve also defined a divide and set a basic block with borders and a title to keep the style consistent.

Displaying the content block

Here’s first I’ll be defining the RSS feed section using TUI widgets: Block, List and ListItem.

fn render_rss_feed_list<'a>() -> List<'a> {
let rss_feeds = Block::default()
.borders(Borders::ALL)
.style(Style::default().fg(Color::White))
.title("RSS Feeds")
.border_type(BorderType::Plain);

let items: Vec<_> = vec!["Feed-1", "Feed-2", "Feed-3"]
.into_iter()
.map(|feed| ListItem::new(Spans::from(vec![Span::styled(feed, Style::default())])))
.collect();

let rss_feed_list = List::new(items).block(rss_feeds).highlight_style(
Style::default()
.bg(Color::Yellow)
.fg(Color::Black)
.add_modifier(Modifier::BOLD),
);

rss_feed_list
}

I’m defining few dummy RSS feed elements to be shown, and mapping them into a ListItem vector. This is then passed into List widget with default Style, and background and foreground colors defined.

Same way, I’ll be defining the Articles List and Summary sections using the TUI widgets: Block, List, ListItem and Paragraph.

fn render_rss_articles_list<'a>() -> (List<'a>, Paragraph<'a>) {
let articles = Block::default()
.borders(Borders::ALL)
.style(Style::default().fg(Color::White))
.title("Articles")
.border_type(BorderType::Plain);

let items: Vec<_> = vec!["Article-1", "Article-2"]
.into_iter()
.map(|feed| ListItem::new(Spans::from(vec![Span::styled(feed, Style::default())])))
.collect();

let list = List::new(items).block(articles).highlight_style(
Style::default()
.bg(Color::Yellow)
.fg(Color::Black)
.add_modifier(Modifier::BOLD),
);

let article_summary = Paragraph::new(vec![Spans::from(vec![Span::styled(
"Article Summary",
Style::default().fg(Color::LightBlue),
)])])
.block(
Block::default()
.borders(Borders::ALL)
.style(Style::default().fg(Color::White))
.border_type(BorderType::Plain),
);

(list, article_summary)
}

These widgets are then rendered by placing them into appropriate boxes in theRectTUI object.

        let left = render_rss_feed_list();
let (middle, right) = render_rss_articles_list();
rect.render_widget(left, rss_chunks[0]);
rect.render_widget(middle, rss_chunks[1]);
rect.render_widget(right, rss_chunks[2]);

Defining the License block

Finally, the license block is rendered using pretty-much the same code as application heading code block.

            let license = Paragraph::new("Released and maintained under GPL-3.0 license")
.style(Style::default().fg(Color::LightCyan))
.alignment(Alignment::Center)
.block(
Block::default()
.borders(Borders::ALL)
.style(Style::default().fg(Color::White))
.border_type(BorderType::Plain),
);

rect.render_widget(license, chunks[3]);

A sample license statement is defined in Paragraph widget, with Style, Alignment and Block defined.

Final TUI View

Once the code compilation is completed successfully, below is the TUI screen that’s rendered.

And voila! It’s exact replica of what I had initially designed. 😎

Next Steps

In my next blog post, I’ll showing how to code user-interaction features to navigate the menu titles and content block sections.

There’s something very satisfying about designing a user-interface in 100% Rust. Let me know your thoughts and comments, and if you want to collaborate on a Rust-based project, then drop me a mail at dlaststark@gmail.com.

--

--