Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Other

Ratatui

What is Ratatui?

  • TUI = Terminal User Interface
  • Ratatui

Ratatui Showcases

From Ratatui Apps

  • atuin - Atuin replaces your existing shell history with a SQLite database, and records additional context for your commands.
  • bandwhich - A CLI utility for displaying current network utilization by process, connection and remote IP/hostname.
  • bottom - A customizable cross-platform graphical process/system monitor for the terminal.
  • csvlens - A command line CSV file viewer. It is like less but made for CSV.
  • dua - a disk space analysis tool designed for speed, leveraging parallel processing to quickly provide detailed disk usage information and allowing for faster deletion of unnecessary data compared to the standard ‘rm’ command.
  • joshuto - Ranger-like terminal file manager written in Rust.
  • fzf-make - A command line tool that executes make target using fuzzy finder with preview window.
  • gitui - TUI for git written in rust.
  • gpg-tui - gpg-tui is a Terminal User Interface for GnuPG.
  • material - A material design color palette for the terminal.
  • minesweep-rs - A mine sweeping game written in Rust.
  • oatmeal - Oatmeal is a terminal UI chat application that speaks with LLMs.
  • oha - oha sends some load to a web application and show realtime tui.
  • rucola - An application to manage markdown notes from your terminal and compile them to HTML.
  • scope-tui - A simple oscilloscope/vectorscope/spectroscope for your terminal.
  • slumber - Terminal HTTP/REST client.
  • taskwarrior-tui - A terminal user interface for taskwarrior.
  • xplr - A hackable, minimal, fast TUI file explorer.
  • yazi - Blazing fast terminal file manager written in Rust, based on async I/O.
  • openapi-tui - Unlock the power of APIs with simplicity and speed, right from your terminal. View OpenAPI documentations in your terminal.

Ratatui - Hello World

  • DefaultTerminal
  • render_widget
  • KeyEventKind}
  • KeyCode
  • Paragraph
  • Event

Taken from the Hello world tutorial

  • This example turns the terminal blue and writes some text on it.
  • It waits till the user presses q and then quits.
[package]
name = "hello-world"
version = "0.1.0"
edition = "2021"

[dependencies]
crossterm = "0.28.1"
ratatui = "0.29.0"
use std::io;

use ratatui::{
    crossterm::event::{self, KeyCode, KeyEventKind},
    style::Stylize,
    widgets::Paragraph,
    DefaultTerminal,
};

fn main() -> io::Result<()> {
    let mut terminal = ratatui::init();
    terminal.clear()?;
    let app_result = run(terminal);
    ratatui::restore();
    app_result
}

fn run(mut terminal: DefaultTerminal) -> io::Result<()> {
    loop {
        terminal.draw(|frame| {
            let greeting = Paragraph::new("Hello Ratatui! (press 'q' to quit)")
                .white()
                .on_blue();
            frame.render_widget(greeting, frame.area());
        })?;

        if let event::Event::Key(key) = event::read()? {
            if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
                return Ok(());
            }
        }
    }
}

Ratatui - handle keyboard events

  • KeyEventKind

  • KeyModifiers

  • CONTROL

  • SHIFT

  • ESC

  • We move the text to be displayed to a variable so we can change it.

  • We accept every key on the keyboard.

  • Pressing ESCape or Ctrl-C will terminate the application.

  • Any other key or key-combination will be displayed on the screen. (e.g. Left, Right)

[package]
name = "hello-world"
version = "0.1.0"
edition = "2021"

[dependencies]
crossterm = "0.28.1"
ratatui = "0.29.0"
use std::io;

use crossterm::event::KeyModifiers;
use ratatui::{
    crossterm::event::{self, KeyCode, KeyEventKind},
    style::Stylize,
    widgets::Paragraph,
    DefaultTerminal,
};

fn main() -> io::Result<()> {
    let mut terminal = ratatui::init();
    terminal.clear()?;
    let app_result = run(terminal);
    ratatui::restore();
    app_result
}

fn run(mut terminal: DefaultTerminal) -> io::Result<()> {
    let mut text = String::new();
    loop {
        terminal.draw(|frame| {
            let greeting = Paragraph::new(text.clone()).white().on_blue();
            frame.render_widget(greeting, frame.area());
        })?;

        if let event::Event::Key(key) = event::read()? {
            if key.kind == KeyEventKind::Press {
                // if key.code == KeyCode::Char('q') {
                //     return Ok(());
                // }

                //text.push_str("a");

                if key.code == KeyCode::Esc {
                    return Ok(());
                }
                if key.modifiers == KeyModifiers::CONTROL && key.code == KeyCode::Char('c') {
                    return Ok(());
                }

                // // println!("{}", key.modifiers);
                // if key.modifiers == KeyModifiers::SHIFT {
                //     text.push_str("Shift+");
                //     text.push_str(&key.code.to_string());
                // } else {
                //     text.push_str(&key.code.to_string());
                // }

                //text.push_str(&key.code.to_string());
                text = format!("{} - {}", key.modifiers, key.code);
            }
        }
    }
}

Ratatui - a more structured way to handle state

  • In this example we a struct, arbitrarily named App to repreent the state of the program. It will hold the text to be displayed and a flag indicating if we need to exit the program.
  • We also separated the draw and the handle_key_event methods and moved the run method into the struct.
  • We also implemented the Widget. Later we'll use a lot more widgets to improve the look of the application.
[package]
name = "hello-world"
version = "0.1.0"
edition = "2021"

[dependencies]
crossterm = "0.28.1"
ratatui = "0.29.0"
use std::io;

use crossterm::event::KeyModifiers;
use ratatui::{
    buffer::Buffer,
    crossterm::event::{self, KeyCode, KeyEventKind},
    layout::Rect,
    style::Stylize,
    widgets::{Paragraph, Widget},
    DefaultTerminal, Frame,
};

#[derive(Debug, Default)]
struct App {
    text: String,
    exit: bool,
}

impl App {
    fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> {
        while !self.exit {
            terminal.draw(|frame| self.draw(frame))?;

            if let event::Event::Key(key) = event::read()? {
                self.handle_key_event(key);
            }
        }
        Ok(())
    }

    fn draw(&self, frame: &mut Frame<'_>) {
        frame.render_widget(self, frame.area());
    }

    fn handle_key_event(&mut self, key: event::KeyEvent) {
        if key.kind == KeyEventKind::Press {
            match key.code {
                KeyCode::Esc => self.exit = true,
                KeyCode::Char('c') if key.modifiers == KeyModifiers::CONTROL => self.exit = true,
                _ => self.text = format!("{} - {}", key.modifiers, key.code),
            }
        }
    }
}

impl Widget for &App {
    fn render(self, area: Rect, buf: &mut Buffer) {
        Paragraph::new(self.text.clone())
            .white()
            .on_blue()
            .render(area, buf);
    }
}

fn main() -> io::Result<()> {
    let mut terminal = ratatui::init();
    terminal.clear()?;
    let app_result = App::default().run(&mut terminal);
    ratatui::restore();
    app_result
}

Ratatui - counting keyboard events

  • We have another field in the struct. A number that will be increased on ever press of the keyboard.
use std::io;

use crossterm::event::KeyModifiers;
use ratatui::{
    buffer::Buffer,
    crossterm::event::{self, KeyCode, KeyEventKind},
    layout::Rect,
    style::Stylize,
    widgets::{Paragraph, Widget},
    DefaultTerminal, Frame,
};

#[derive(Debug, Default)]
struct App {
    counter: u32,
    text: String,
    exit: bool,
}

impl App {
    fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> {
        while !self.exit {
            terminal.draw(|frame| self.draw(frame))?;

            if let event::Event::Key(key) = event::read()? {
                self.handle_key_event(key);
            }
        }
        Ok(())
    }

    fn draw(&self, frame: &mut Frame<'_>) {
        frame.render_widget(self, frame.area());
    }

    fn handle_key_event(&mut self, key: event::KeyEvent) {
        if key.kind == KeyEventKind::Press {
            match key.code {
                KeyCode::Esc => self.exit = true,
                KeyCode::Char('c') if key.modifiers == KeyModifiers::CONTROL => self.exit = true,
                _ => {
                    self.text = format!("{} - {}", key.modifiers, key.code);
                    self.counter += 1;
                }
            }
        }
    }
}

impl Widget for &App {
    fn render(self, area: Rect, buf: &mut Buffer) {
        Paragraph::new(format!("{}: {}", self.counter, self.text))
            .white()
            .on_blue()
            .render(area, buf);
    }
}

fn main() -> io::Result<()> {
    let mut terminal = ratatui::init();
    terminal.clear()?;
    let app_result = App::default().run(&mut terminal);
    ratatui::restore();
    app_result
}

Ratatui - a stopwatch or automatic counter - blocking vs. polling events

  • poll

  • At first it still counts every keyboard even as the reading from the keyboard is a blocking call.

  • We can use event::poll to check if there is anything to read at all. The poll has a timeout.

  • This way we can do work (count) in-between checking for events.

use std::io;

use crossterm::event::KeyModifiers;
use ratatui::{
    buffer::Buffer,
    crossterm::event::{self, KeyCode, KeyEventKind},
    layout::Rect,
    style::Stylize,
    widgets::{Paragraph, Widget},
    DefaultTerminal, Frame,
};

#[derive(Debug, Default)]
struct App {
    counter: u32,
    text: String,
    exit: bool,
}

impl App {
    fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> {
        while !self.exit {
            terminal.draw(|frame| self.draw(frame))?;

            // Blocking read
            if let event::Event::Key(key) = event::read()? {
                self.handle_key_event(key);
            }

            // Poll before blocking read
            // if event::poll(std::time::Duration::from_millis(100))? {
            //     if let event::Event::Key(key) = event::read()? {
            //         self.handle_key_event(key);
            //     }
            // }

            // Short poll
            // if event::poll(std::time::Duration::from_millis(1))? {
            //     if let event::Event::Key(key) = event::read()? {
            //         self.handle_key_event(key);
            //     }
            // }
            // std::thread::sleep(std::time::Duration::from_millis(100));

            self.counter += 1;
        }
        Ok(())
    }

    fn draw(&self, frame: &mut Frame<'_>) {
        frame.render_widget(self, frame.area());
    }

    fn handle_key_event(&mut self, key: event::KeyEvent) {
        if key.kind == KeyEventKind::Press {
            match key.code {
                KeyCode::Esc => self.exit = true,
                KeyCode::Char('c') if key.modifiers == KeyModifiers::CONTROL => self.exit = true,
                _ => self.text = format!("{} - {}", key.modifiers, key.code),
            }
        }
    }
}

impl Widget for &App {
    fn render(self, area: Rect, buf: &mut Buffer) {
        Paragraph::new(format!("{}: {}", self.counter, self.text))
            .white()
            .on_blue()
            .render(area, buf);
    }
}

fn main() -> io::Result<()> {
    let mut terminal = ratatui::init();
    terminal.clear()?;
    let app_result = App::default().run(&mut terminal);
    ratatui::restore();
    app_result
}

Ratatui - counter

  • A copy of the counter app

  • Thre is a crash if the counter overflows or underflows!

[package]
name = "counter"
version = "0.1.0"
edition = "2021"

[dependencies]
crossterm = "0.28.1"
ratatui = "0.29.0"
use std::io;

use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind};
use ratatui::{
    buffer::Buffer,
    layout::Rect,
    style::Stylize,
    symbols::border,
    text::{Line, Text},
    widgets::{Block, Paragraph, Widget},
    DefaultTerminal, Frame,
};

#[derive(Debug, Default)]
pub struct App {
    counter: u8,
    exit: bool,
}

impl App {
    pub fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> {
        while !self.exit {
            terminal.draw(|frame| self.draw(frame))?;
            self.handle_events()?;
        }
        Ok(())
    }

    fn draw(&self, frame: &mut Frame) {
        frame.render_widget(self, frame.area());
    }

    fn handle_events(&mut self) -> io::Result<()> {
        match event::read()? {
            // it's important to check that the event is a key press event as
            // crossterm also emits key release and repeat events on Windows.
            Event::Key(key_event) if key_event.kind == KeyEventKind::Press => {
                self.handle_key_event(key_event)
            }
            _ => {}
        };
        Ok(())
    }

    fn handle_key_event(&mut self, key_event: KeyEvent) {
        match key_event.code {
            KeyCode::Char('q') => self.exit(),
            KeyCode::Left => self.decrement_counter(),
            KeyCode::Right => self.increment_counter(),
            _ => {}
        }
    }

    fn exit(&mut self) {
        self.exit = true;
    }

    fn increment_counter(&mut self) {
        self.counter += 1;
    }

    fn decrement_counter(&mut self) {
        self.counter -= 1;
    }
}

impl Widget for &App {
    fn render(self, area: Rect, buf: &mut Buffer) {
        let title = Line::from(" Counter App Tutorial ".bold());
        let instructions = Line::from(vec![
            " Decrement ".into(),
            "<Left>".blue().bold(),
            " Increment ".into(),
            "<Right>".blue().bold(),
            " Quit ".into(),
            "<Q> ".blue().bold(),
        ]);
        let block = Block::bordered()
            .title(title.centered())
            .title_bottom(instructions.centered())
            .border_set(border::THICK);

        let counter_text = Text::from(vec![Line::from(vec![
            "Value: ".into(),
            self.counter.to_string().yellow(),
        ])]);

        Paragraph::new(counter_text)
            .centered()
            .block(block)
            .render(area, buf);
    }
}

fn main() -> io::Result<()> {
    let mut terminal = ratatui::init();
    let app_result = App::default().run(&mut terminal);
    ratatui::restore();
    app_result
}

#[cfg(test)]
mod tests {
    use super::*;
    use ratatui::style::Style;

    #[test]
    fn render() {
        let app = App::default();
        let mut buf = Buffer::empty(Rect::new(0, 0, 50, 4));

        app.render(buf.area, &mut buf);

        let mut expected = Buffer::with_lines(vec![
            "┏━━━━━━━━━━━━━ Counter App Tutorial ━━━━━━━━━━━━━┓",
            "┃                    Value: 0                    ┃",
            "┃                                                ┃",
            "┗━ Decrement <Left> Increment <Right> Quit <Q> ━━┛",
        ]);
        let title_style = Style::new().bold();
        let counter_style = Style::new().yellow();
        let key_style = Style::new().blue().bold();
        expected.set_style(Rect::new(14, 0, 22, 1), title_style);
        expected.set_style(Rect::new(28, 1, 1, 1), counter_style);
        expected.set_style(Rect::new(13, 3, 6, 1), key_style);
        expected.set_style(Rect::new(30, 3, 7, 1), key_style);
        expected.set_style(Rect::new(43, 3, 4, 1), key_style);

        assert_eq!(buf, expected);
    }

    #[test]
    fn handle_key_event() -> io::Result<()> {
        let mut app = App::default();
        app.handle_key_event(KeyCode::Right.into());
        assert_eq!(app.counter, 1);

        app.handle_key_event(KeyCode::Left.into());
        assert_eq!(app.counter, 0);

        let mut app = App::default();
        app.handle_key_event(KeyCode::Char('q').into());
        assert!(app.exit);

        Ok(())
    }
}

TODO Ratatui - confirmation popup to verify intent to exit

TODO Ratatui - widgets

Show various widgets and how to work with them.

use std::io;

use crossterm::event::KeyModifiers;
use ratatui::{
    buffer::Buffer,
    crossterm::event::{self, KeyCode, KeyEventKind},
    layout::Rect,
    style::{Style, Stylize},
    symbols::border,
    text::{Line, Text},
    widgets::{Block, List, ListDirection, ListState, Paragraph, StatefulWidget, Widget},
    DefaultTerminal, Frame,
};

#[derive(Debug, Default)]
struct App {
    counter: u8,
    exit: bool,
}

impl App {
    fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> {
        while !self.exit {
            terminal.draw(|frame| self.draw(frame))?;

            if let event::Event::Key(key) = event::read()? {
                self.handle_key_event(key);
            }
        }
        Ok(())
    }

    fn draw(&self, frame: &mut Frame<'_>) {
        frame.render_widget(self, frame.area());
    }

    fn handle_key_event(&mut self, key: event::KeyEvent) {
        if key.kind == KeyEventKind::Press {
            match key.code {
                KeyCode::Esc => self.exit = true,
                KeyCode::Char('c') if key.modifiers == KeyModifiers::CONTROL => self.exit = true,
                KeyCode::Right => self.counter = self.counter.saturating_add(1),
                KeyCode::Left => self.counter = self.counter.saturating_sub(1),
                _ => {}
            }
        }
    }
}

impl Widget for &App {
    fn render(self, area: Rect, buf: &mut Buffer) {
        match self.counter {
            1 => Paragraph::new(format!("{}", self.counter))
                .white()
                .on_blue()
                .render(area, buf),
            2 => Paragraph::new(format!("{}", self.counter))
                .black()
                .on_green()
                .render(area, buf),
            3 => {
                let text = Text::from(vec![Line::from(vec![
                    "Centered Value: ".to_string().black().on_white(),
                    self.counter.to_string().red(),
                ])]);

                Paragraph::new(text)
                    .centered()
                    .on_light_blue()
                    .render(area, buf);
            },

            4 => {
                let title = Line::from(" Counter App Tutorial ".bold());
                let instructions = Line::from(vec![
                    " Decrement ".into(),
                    "<Left>".blue().bold(),
                    " Increment ".into(),
                    "<Right>".blue().bold(),
                    " Quit ".into(),
                    "<Q> ".blue().bold(),
                ]);
                let block = Block::bordered()
                    .title(title.centered())
                    .title_bottom(instructions.centered())
                    .border_set(border::THICK);

                let counter_text = Text::from(vec![Line::from(vec![
                    "Value: ".into(),
                    self.counter.to_string().yellow(),
                ])]);

                Paragraph::new(counter_text)
                    .centered()
                    .block(block)
                    .render(area, buf);
            },

            5 => {
                let items = ["Item 1", "Item 2", "Item 3"];
                let list = List::new(items)
                    .block(Block::bordered().title("List"))
                    .style(Style::new().black())
                    .highlight_style(Style::new().italic())
                    .highlight_symbol(">>")
                    .repeat_highlight_symbol(true)
                    .direction(ListDirection::BottomToTop);
                Widget::render(list, area, buf);
            },

            0 => {
                let mut state = ListState::default();
let items = ["Item 1", "Item 2", "Item 3"];
let list = List::new(items)
    .block(Block::bordered().title("List"))
    .highlight_style(Style::new().reversed())
    .highlight_symbol(">>")
    .repeat_highlight_symbol(true);

    StatefulWidget::render(list, area, buf, &mut state)
            }

            _ => Paragraph::new(format!("Not implemented: {} Press the left and right arrows to cycle throught the examples.", self.counter))
                .white()
                .on_blue()
                .render(area, buf),
        }
    }
}

fn main() -> io::Result<()> {
    let mut terminal = ratatui::init();
    terminal.clear()?;
    let app_result = App::default().run(&mut terminal);
    ratatui::restore();
    app_result
}

Tera

Tera Template system

Tera Hello World

  • Context
  • render
[package]
name = "hello-world"
version = "0.1.0"
edition = "2021"

[dependencies]
tera = "1.20.0"
use tera::Context;
use tera::Tera;

fn main() {
    let tera = Tera::new("templates/*.html").unwrap();

    let mut context = Context::new();
    context.insert("name", "World");

    let result = tera.render("hello.html", &context).unwrap();
    assert_eq!(result, "Hello, World!");
    println!("{result}");
}
Hello, {{name}}!
$ tree
.
├── Cargo.lock
├── Cargo.toml
├── src
│   └── main.rs
└── templates
    └── hello.html

Tera - list templates

I am not sure if this is ever needed in an application, but it helped me when first I could not figure out the expected directroy layout. It might be used for debugging to see if Tera can really find your templates.

$ tree
.
├── Cargo.lock
├── Cargo.toml
├── out.out
├── src
│   └── main.rs
└── templates
    ├── hello.html
    └── incl
        └── header.html
use tera::Tera;

fn main() {
    let tera = Tera::new("templates/*.html").unwrap();

    tera.get_template_names().for_each(|x| println!("{}", x));
}
incl/header.html
hello.html
[package]
name = "hello-world"
version = "0.1.0"
edition = "2021"

[dependencies]
tera = "1.20.0"
Hello, {{name}}!
<html>
    <head>
    </head>

Tera - use built-in filters on strings

use tera::Context;
use tera::Tera;

fn main() {
    let tera = Tera::new("templates/*.html").unwrap();

    let mut context: Context = Context::new();
    context.insert("text", " Hello, World! How are you? ");

    let result = tera.render("hello.html", &context).unwrap();
    println!("{result}");
}
text:       '{{text}}'
reverse:    '{{text | reverse}}'
upper:      '{{text | upper}}'
lower:      '{{text | lower}}'
capitalize: '{{text | capitalize}}'
wordcount:  '{{text | wordcount}}'
length:     '{{text | length}}'
slugify:    '{{text | slugify}}'
title:      '{{text | title}}'
trim:       '{{text | trim}}'
trim_start: '{{text | trim_start}}'
trim_end:   '{{text | trim_end}}'
text:       ' Hello, World! How are you? '
reverse:    ' ?uoy era woH !dlroW ,olleH '
upper:      ' HELLO, WORLD! HOW ARE YOU? '
lower:      ' hello, world! how are you? '
capitalize: ' hello, world! how are you? '
wordcount:  '5'
length:     '28'
slugify:    'hello-world-how-are-you'
title:      ' Hello, World! How Are You? '
trim:       'Hello, World! How are you?'
trim_start: 'Hello, World! How are you? '
trim_end:   ' Hello, World! How are you?'

Tera - create your own filter

  • register_filter
  • Result
  • Tera
  • Value
  • to_value
use std::collections::HashMap;

use tera::{to_value, Context, Result, Tera, Value};

fn fixed_filter(_val: &Value, _map: &HashMap<String, Value>) -> Result<Value> {
    Ok(to_value("some fixed value").unwrap())
}

fn same_value_filter(val: &Value, _map: &HashMap<String, Value>) -> Result<Value> {
    Ok(to_value(val).unwrap())
}

fn my_len(val: &Value, _map: &HashMap<String, Value>) -> Result<Value> {
    let s = val.as_str().unwrap();
    Ok(to_value(s.len()).unwrap())
}

fn filter_with_params(_val: &Value, map: &HashMap<String, Value>) -> Result<Value> {
    //println!("map: {:?}", map);
    let param = map.get("param").unwrap().as_str().unwrap();
    let attr = map.get("attr").unwrap().as_u64().unwrap();

    Ok(to_value(format!("param={param} attr={attr}")).unwrap())
}

fn main() {
    let mut tera = Tera::new("templates/*.html").unwrap();
    tera.register_filter("fixed_filter", fixed_filter);
    tera.register_filter("same_value_filter", same_value_filter);
    tera.register_filter("my_len", my_len);
    tera.register_filter("filter_with_params", filter_with_params);

    let mut context = Context::new();
    context.insert("text", "Hello, World!");

    let result = tera.render("hello.html", &context).unwrap();
    println!("{result}");
}
text:               '{{text}}'
fixed_filter:       '{{text | fixed_filter}}'
same_value_filter:  '{{text | same_value_filter}}'
my_len:             '{{text | my_len}}'
filter_with_params: '{{text | filter_with_params(attr=1, param="hello")}}'
text:               'Hello, World!'
fixed_filter:       'some fixed value'
same_value_filter:  'Hello, World!'
my_len:             '13'
filter_with_params: 'param=hello attr=1'

Other

Variable shadowing

  • let

  • We can declare the same variable multiple time using the let keyword.

  • The 2nd and subsequent declarations hide (shadow) the previous ones.

  • If this happened inside a block then when the shadowing version of the variable goes out of scope the previous value becomes visible again. (Observe, the last line being 6.)

fn main() {
    let x = 5;
    println!("x={x}");

    let x = x + 1;
    println!("x={x}");

    {
        println!("x={x}");
        let x = x * 2;
        println!("x={x}");
    }

    println!("x={x}");
}
x=5
x=6
x=6
x=12
x=6

String formatting

  • format
  • sprintf
fn main() {
    let name = "Foo";
    let text1 = format!("Hello {}, how are you?", name);
    let text2 = format!("Hello {name}, how are you?");
    println!("{}", text1);
    println!("{}", text2);
}
fn main() {
    let text = "Hello";
    println!("{}", text);
    let reversed = reverse(text);
    println!("{}", text);
    println!("{}", reversed);
}

fn reverse(text: &str) -> String {
    let reversed: String = text.chars().rev().collect();
    reversed
}
Hello
Hello
olleH
fn main() {
    let characters = ('a', 'b');
    println!("{:?}", characters);

    let strings = ("a", "b");
    println!("{:?}", strings);
    // println!("{:?}", letters.collect());
    println!("Hello");
    // println!('Hello'); // error: character literal may only contain one codepoint
}

Factorial functions returning Result

  • Restult
  • Ok
  • Err
fn main() {
    let numbers = vec![0, 1, 2, 3, 4, -1];
    for number in numbers {
        let res = factorial(number);
        println!("{}! = {:?}", number, res);
    }
    println!();

    //for number in numbers {
    //    match factorial(number) {
    //        Ok(res) => {
    //            println!("{}! = {}", number, res);
    //        },
    //        Err(err) => {
    //            println!("Error {}", err);
    //        }
    //    }
    //}
}

fn factorial(number: i32) -> Result<i32, &'static str> {
    if number < 0 {
        return Err("Factorial requires a non-negative integer");
    }
    let mut num = 1;
    for n in 1..=number {
        num *= n;
    }
    Ok(num)
}
0! = Ok(1)
1! = Ok(1)
2! = Ok(2)
3! = Ok(6)
4! = Ok(24)
-1! = Err("Factorial requires a non-negative integer")

Split function into files

pub mod helper;

fn main() {
    println!("main");
    same_file();
    helper::public_in_helper();
    //helper::private_in_helper();
}

fn same_file() {
    println!("same file");
}
#![allow(unused)]
fn main() {
pub fn public_in_helper() {
    println!("public_in_helper");
    private_in_helper();
}

fn private_in_helper() {
    println!("in_helper");
}
}
[package]
name = "project"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

Variable Scope in Rust

  • Every block creates a scope
fn main() {
    let fname = "Foo";
    let lname = "Bar";
    println!("{} {}", fname, lname);
    {
        let fname = "Peti";
        println!("{} {}", fname, lname);
    }
    println!("{} {}", fname, lname);
}
Foo Bar
Peti Bar
Foo Bar

Declare empty variable and assign to it later

fn main() {
    let r;

    {
        r = "one";
    }
    // r = "two"; // cannot assign twice to immutable variable `r`

    println!("r: {}", r);
}

Declare empty variable - requires type

#[allow(clippy::needless_late_init)]
fn main() {
    let name;
    name = "Foo";
    dbg!(name);
}
[examples/intro/empty_string.rs:4] name = "Foo"

SystemTime now

//use std::time::Instant;
use std::time::SystemTime;

fn main() {
    //let now = Instant::now();
    match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) {
        Ok(n) => println!("1970-01-01 00:00:00 UTC was {} seconds ago!", n.as_secs()),
        Err(_) => panic!("SystemTime before UNIX EPOCH!"),
    }
    //let now = SystemTime::now();
    //println!("{:?}", now);
}

Exit

use std::process;

fn main() {
    let early_exit = false;

    if early_exit {
        println!("Early exit");
        process::exit(3);
    }
    println!("Still running");
}
echo $?
echo %ERROR_LEVEL%

Define multiple variables

fn main() {
    let (a, b) = (2, "hello");
    dbg!(a);
    dbg!(b);
}

wc

[package]
name = "rust-wc"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
use std::env;

fn main() {
    // get the path from the command line
    // if it is a file work on it
    // if it is a folder ...
    // get command line parameters

    let files: Vec<String> = env::args().collect();
    for file in &files[1..files.len()] {
        dbg!(file);
    }
}

copy vs clone

TODO

Type alias

  • type

  • We can use the type keyword to create aliases to existing types. This can help us in reading the code, but Rust does not do any enforcement.

  • As you can see in the following example we can pass arguments to a "different" type as long as it is an alias to the same type.

  • type

type Meters = u32;
type Kilograms = u32;

fn main() {
    let m: Meters = 3;
    let k: Kilograms = 4;
    calc(m);
    calc(k);
}

fn calc(y: Meters) {
    println!("{y}");
}
3
4

Solution: Age limit

use std::io;
use std::io::Write;

fn main() {
    let mut age = String::new();

    print!("How old are you? ");
    io::stdout().flush().expect("Oups");
    io::stdin()
        .read_line(&mut age)
        .expect("Failed to get input");

    let age: f32 = age
        .trim()
        .parse()
        .unwrap_or_else(|_| panic!("Could not convert '{age}' to floating point number"));
    if age < 18.0 {
        println!("You are under 18. You cannot legally drink alcohol!");
    } else {
        println!("Congratulations, you can legally drink alcohol!");
    }
}
use std::io;
use std::io::Write;

fn main() {
    let mut age = String::new();

    print!("How old are you? ");
    io::stdout().flush().expect("Oups");
    io::stdin()
        .read_line(&mut age)
        .expect("Failed to get input");

    let age: f32 = age
        .trim()
        .parse()
        .unwrap_or_else(|_| panic!("Could not convert '{age}' to floating point number"));
    if age < 18.0 {
        println!("You are under 18. You cannot legally drink alcohol!");
    } else if age >= 21.0 {
        println!("Congratulations, you can legally drink alcohol! Even in the USA.");
    } else {
        println!("You can drink alcohol, unless you are in the USA.");
    }
}
use std::collections::HashMap;
use std::io;
use std::io::Write;

fn main() {
    let mut age = String::new();
    print!("How old are you? ");
    io::stdout().flush().expect("Oups");
    io::stdin()
        .read_line(&mut age)
        .expect("Failed to get input");
    let age: f32 = age
        .trim()
        .parse()
        .unwrap_or_else(|_| panic!("Could not convert '{age}' to floating point number"));

    let mut country = String::new();
    print!("Which country are you in? ");
    io::stdout().flush().expect("Oups");
    io::stdin()
        .read_line(&mut country)
        .expect("Failed to get input");
    let country = country.trim();

    let age_limit = HashMap::from([
        ("Australia", 18.0),
        ("Austria", 16.0), // Actually it depends on the type of alcohol
        ("Hungary", 18.0),
        ("Israel", 18.0),
        ("Pakistan", 21.0), // Prohibited for muslims, 21 for others
        ("Paraguay", 20.0),
        ("USA", 21.0),
    ]);

    if age_limit.contains_key(&country) {
        if age_limit[&country] <= age {
            println!("Congratulations, you can legally drink alcohol in {country}.");
        } else {
            println!(
                "Sorry. You are under {}. You cannot legally drink alcohol in {}!",
                age_limit[country], country
            );
        }
    } else {
        println!("You typed in '{country}'. Unfortunately we don't have that name in our database");
    }
}

Multi-counter in manually handled CSV file

use std::collections::HashMap;
use std::env;
use std::fs::File;
use std::io::{BufRead, BufReader, Write};
use std::process::exit;

type Counter = HashMap<String, u32>;
const FILENAME: &str = "counter.csv";

fn main() {
    let name = get_name();
    //println!("{}", name);

    let mut counters: Counter = read_the_csv_file();
    if counters.contains_key(&name) {
        counters.insert(name.clone(), counters[&name] + 1);
    } else {
        counters.insert(name.clone(), 1);
    }
    // the above 5 lines could be replaced with this one:
    //*counters.entry(name.clone()).or_insert(0) += 1;

    println!("{}: {}", &name, counters[&name]);
    save_the_csv_file(counters)
}

/// Get name from command line
fn get_name() -> String {
    let args: Vec<String> = env::args().collect();
    if args.len() != 2 {
        eprintln!("Usage: {} NAME", args[0]);
        exit(1);
    }
    args[1].clone()
}

fn read_the_csv_file() -> Counter {
    let mut counters: Counter = HashMap::new();
    match File::open(FILENAME) {
        Ok(file) => {
            let reader = BufReader::new(file);
            for line in reader.lines() {
                let line = line.unwrap();
                let line = line.trim();
                let parts: Vec<&str> = line.split('=').collect();
                let name = parts[0];
                let count: u32 = parts[1].parse().unwrap();
                counters.insert(name.to_string(), count);
            }
        }
        Err(error) => {
            println!("Error opening file {}: {}", FILENAME, error);
        }
    }
    counters
}

fn save_the_csv_file(counters: Counter) {
    let mut file = File::create(FILENAME).unwrap();
    for (name, count) in counters.iter() {
        writeln!(&mut file, "{}={}", name, count).unwrap();
    }
}

Get path to current executable

  • current_exe
use std::env;

fn main() {
    println!("{}", env::current_exe().unwrap().display());

    match env::current_exe() {
        Ok(exe_path) => println!("Path: {}", exe_path.display()),
        Err(err) => println!("Failed to get current exe path: {err}"),
    };
}

cache dependencies with sccache

Commafy

[package]
name = "commafy"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
#![allow(unused)]
fn main() {
pub fn commafy<Integer: Into<i64> + Copy + std::fmt::Debug + std::fmt::Display>(
    num: Integer,
) -> String {
    //pub fn commafy(num: i32) -> String {
    let num = format!("{num}");
    let mut ix = 0;
    let num = num
        .chars()
        .rev()
        .map(|chr| {
            ix += 1;
            if ix % 3 == 1 && ix > 1 {
                format!(",{chr}")
            } else {
                format!("{chr}")
            }
        })
        .collect::<String>();
    num.chars().rev().collect::<String>()
}

#[cfg(test)]
mod tests {
    use super::commafy;

    #[test]
    fn test_commafy() {
        assert_eq!("1", commafy(1));
        assert_eq!("12", commafy(12));
        assert_eq!("123", commafy(123));
        assert_eq!("1,234", commafy(1234));
        assert_eq!("12,345", commafy(12345));
        assert_eq!("123,456", commafy(123456));
        assert_eq!("1,234,567", commafy(1234567));

        assert_eq!("1,234", commafy(1234i16));
        assert_eq!("254", commafy(254u8));
        assert_eq!("1,254", commafy(1254u16));
    }
}
}

Commafys

fn main() {
    assert_eq!(commafy("56789012"), "56,789,012");
    assert_eq!(commafy("789012"), "789,012");
    assert_eq!(commafy("123"), "123");
    assert_eq!(commafy("1234"), "1,234");
}

fn commafy(text: &str) -> String {
    let mut chars: Vec<_> = text.chars().collect();
    let len = text.len();
    for i in (3..text.len()).step_by(3) {
        chars.insert(len - i, ',');
    }
    let commafied: String = chars.iter().collect();
    commafied
}

Use statements

// Use one-by-one

//#[allow(unused_imports)]
//use std::io::Read;
//#[allow(unused_imports)]
//use std::io::Write;
#[allow(unused_imports)]
use std::fs::File;
#[allow(unused_imports)]
use std::io;

// Combine two
//#[allow(unused_imports)]
//use std::io::{Read, Write};

// Combine crate and function
//#[allow(unused_imports)]
//use std::io::{self, Write};

// Combine several, unrelated
//#[allow(unused_imports)]
//use {
//    std::io::Read,
//    std::io::Write,
//    std::fs::File,
//    std::io,
//};

fn main() {
    println!("Hello, world!");
}

Take version number from Cargo.toml

  • VERSION
  • CARGO_PKG_VERSION
const VERSION: &str = env!("CARGO_PKG_VERSION");

fn main() {
    println!("Running version {VERSION}");
}

Ansi colors

[package]
name = "ansi-colors"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
ansi_term = "0.12.1"
use ansi_term::Colour::{Blue, Green, Red};

fn main() {
    println!("Hello, world!");
    println!("{}", Blue.bold().paint("blue"));
    println!("{}", Green.dimmed().paint("green"));

    println!(
        "Hello {} sky and {} grass!",
        Blue.bold().paint("blue"),
        Green.bold().paint("green")
    );
    println!("{}", Red.bold().paint("red"));
}

What I learned from learning Rust

  • Gabor Szabo

  • https://szabgab.com/

  • https://github.com/szabgab

  • https://rustatic.code-maven.com/

  • https://rust-digger.code-maven.com/

  • https://rust.code-maven.com/

Temperature converter

[package]
name = "temperature-converter"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

{% embed include file="src/examples/other/temperature-converter/input.json)

// // Accept the input filename as a parameter
// // Read JSON file into a struct
// // Convert to andother struct
// // Print

// enum Scale {
//     Centigrade,
//     Farenheit,
//     Kelvin,
// }

// struct TemperatureInput {
//     input_temperature: f32,
//     input_scale: Scale,
//     output_scale: Scale,
// }
// fn main() {
//     calc();
// }

// fn calc() -> Result<String, String> {
//     let filename = String::from("input.json");
//     let fh = std::fs::File::open(filename)?;
//     println!("Hello, world!");
//     ()
// }

fn main() {
    //fibonacci(10);
}

//fn fibo() {
//}

Check slides

use std::collections::HashMap;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::iter::FromIterator;
use std::path::Path;
use std::path::PathBuf;
use std::process::exit;
use std::process::Command;
use std::sync::mpsc;

use chrono::{DateTime, Utc};
use clap::Parser;
use threadpool::ThreadPool;

// use regex::Regex;

// from all the md files extract the list of included files
// from the examples/ directory list all the files
// make sure each file is included and is only included once

// Run every rs file, if there is an out file compare the results.
//
const ROOT: &str = "../../..";

const ACTIONS: [&str; 6] = ["update", "fmt", "fmt_check", "clippy", "test", "run"];

#[derive(Parser)]
struct Cli {
    #[arg(long, help = "Print debug information")]
    verbose: bool,

    #[arg(long, help = "Cleanup the target directory")]
    cleanup: bool,

    #[arg(long, help = "Check if all the examples are used in the md files")]
    use_examples: bool,

    #[arg(long)]
    update: bool,

    #[arg(long)]
    fmt: bool,

    #[arg(long)]
    fmt_check: bool,

    #[arg(long)]
    clippy: bool,

    #[arg(long)]
    test: bool,

    #[arg(long)]
    run: bool,

    #[arg(long, help = "Run all the checks on the selected examples")]
    check: bool,

    #[arg(help = "List of examples to run. e.g examples/other/check-slides/")]
    examples: Vec<String>,
}

fn get_actions(args: &Cli) -> Vec<&str> {
    let mut actions: Vec<&str> = vec![];
    if args.update {
        actions.push("update");
    }
    if args.fmt {
        actions.push("fmt")
    }
    if args.check || args.fmt_check {
        actions.push("fmt_check")
    }
    if args.check || args.clippy {
        actions.push("clippy")
    }
    if args.check || args.test {
        actions.push("test")
    }
    if args.check || args.run {
        actions.push("run")
    }

    actions
}

fn main() {
    let start: DateTime<Utc> = Utc::now();

    let args = Cli::parse();
    let log_level = if args.verbose {
        log::Level::Info
    } else {
        log::Level::Warn
    };
    simple_logger::init_with_level(log_level).unwrap();
    log::info!("verbose: {}", args.verbose);

    std::env::set_current_dir(ROOT).unwrap();

    let examples = if args.examples.is_empty() {
        get_crates(Path::new("examples"))
    } else {
        args.examples.iter().map(PathBuf::from).collect()
    };
    log::info!("Number of examples: {}", examples.len());

    let unused_examples = check_use_of_example_files(args.use_examples);

    let mut success: HashMap<String, i32> = HashMap::new();
    let mut failures: HashMap<String, Vec<PathBuf>> = HashMap::new();
    let actions = get_actions(&args);

    cargo_on_all(
        &mut success,
        &mut failures,
        &examples,
        actions.iter().map(|x| x.to_string()).collect(),
        args.cleanup,
    );

    let mut failures_total = 0;
    for action in ACTIONS {
        if success.contains_key(action) && failures.contains_key(action) {
            log::info!(
                "{action} success: {}, failure: {}",
                success[action],
                failures[action].len()
            );
            failures_total += failures[action].len();
        }
    }

    println!("------- Report -------");
    let end: DateTime<Utc> = Utc::now();
    println!("Elapsed: {}", end.timestamp() - start.timestamp());

    if !unused_examples.is_empty() {
        println!("There are {} unused examples", unused_examples.len());
        for example in &unused_examples {
            println!("  {example:?}",);
        }
    }

    for action in ACTIONS {
        if failures.contains_key(action) {
            report_errors(action, &failures[action]);
        }
    }

    if !unused_examples.is_empty() || failures_total > 0 {
        exit(1);
    }
}

fn report_errors(name: &str, failures: &[PathBuf]) {
    if !failures.is_empty() {
        println!("There are {} examples with {name} errors.", failures.len());
        for failure in failures {
            println!("  {failure:?}",);
        }
    }
}

fn check_use_of_example_files(use_examples: bool) -> Vec<String> {
    let mut unused_examples = vec![];
    if !use_examples {
        return unused_examples;
    }
    log::info!("check_use_of_example_files");
    let md_files = get_md_files();
    let imported_files = get_imported_files(md_files);
    let examples = get_all_the_examples();

    for filename in examples {
        if filename.ends_with("swp") {
            continue;
        }
        if filename.ends_with("counter.db") {
            continue;
        }
        if !imported_files.contains(&filename) {
            if filename.starts_with("examples/rocket/people-and-groups/templates/") {
                continue;
            }
            if filename.starts_with("examples/surrealdb/embedded-rocksdb/tempdb/") {
                continue;
            }
            if filename.starts_with("examples/surrealdb/cli-multi-counter/db/") {
                continue;
            }

            let files = [
                "examples/threads/count-characters/aadef.txt",
                "examples/threads/count-characters/abc.txt",
                "examples/rocket/static-files/static/favicon.ico",
                "examples/files/count-digits/digits.txt",
                "examples/files/read-whole-file/data.txt",
                "examples/ownership/concatenate-content-of-files/dog.txt",
                "examples/ownership/concatenate-content-of-files/cat.txt",
                "examples/ownership/read-file-and-trim-newline/cat.txt",
                "examples/other/check-slides/skips.csv",
            ]
            .into_iter()
            .map(|name| name.to_owned())
            .collect::<Vec<String>>();
            if files.contains(&filename) {
                continue;
            }

            log::error!("Unused file: `{filename}`");
            unused_examples.push(filename);
        }
    }
    unused_examples
}

fn cargo_on_all(
    success: &mut HashMap<String, i32>,
    failures: &mut HashMap<String, Vec<PathBuf>>,
    crates: &[PathBuf],
    actions: Vec<String>,
    cleanup: bool,
) {
    log::info!("cargo_on_all {actions:?} START");
    let start: DateTime<Utc> = Utc::now();
    let number_of_crates = crates.len();

    let (tx, rx) = mpsc::channel();
    let max_threads = 2;
    let pool = ThreadPool::new(max_threads);

    for (ix, crate_folder) in crates.iter().cloned().enumerate() {
        log::info!("crate: {}/{number_of_crates}, {crate_folder:?}", ix + 1);
        let mytx = tx.clone();
        let actions = actions.clone();

        pool.execute(move || {
            let res =
                cargo_actions_on_single(&crate_folder, &actions, cleanup, ix + 1, number_of_crates);
            log::debug!("sending res {res:?}");

            mytx.send((res, crate_folder)).unwrap();
        });
    }
    drop(tx); // close this channel to allow the last thread to finish the receiving loop

    for received in rx {
        for action in &actions {
            if received.0[action] {
                log::debug!("success {action}");
                *success.entry(action.clone()).or_insert(0) += 1;
            } else {
                log::debug!("received failures {:?}", received.1.clone());
                failures
                    .entry(action.clone())
                    .or_default()
                    .push(received.1.clone());
                log::debug!("all failures {:?}", failures);
            }
        }
    }

    let end: DateTime<Utc> = Utc::now();
    log::info!(
        "cargo_on_all {actions:?} DONE Elapsed: {}",
        end.timestamp() - start.timestamp()
    );
}

fn cargo_actions_on_single(
    crate_folder: &PathBuf,
    actions: &[String],
    cleanup: bool,
    ix: usize,
    total: usize,
) -> HashMap<String, bool> {
    log::info!("Actions on {crate_folder:?} START {ix}/{total}");
    let start = Utc::now();
    let mut res = HashMap::new();
    for action in actions {
        res.insert(action.clone(), cargo_on_single(crate_folder, action));
    }
    if cleanup {
        std::fs::remove_dir_all(crate_folder.join("target")).unwrap();
    }
    let end = Utc::now();
    log::info!(
        "Actions on {crate_folder:?} DONE {ix}/{total} Elapsed: {}",
        end.timestamp() - start.timestamp()
    );
    res
}

fn cargo_on_single(crate_path: &PathBuf, action: &str) -> bool {
    log::info!("{action} on {crate_path:?} START");

    let args = get_args(action);
    let skip = skip(action);

    let folder = crate_path.clone().into_os_string().into_string().unwrap();
    let folders = skip.iter().map(|x| x.to_string()).collect::<String>();
    if folders.contains(&folder) {
        log::info!("{action} on {crate_path:?} SKIPPED");
        return true;
    }

    let error = format!("failed to execute 'cargo {args:?} --check' process");
    let mut cmd = Command::new("cargo");
    for arg in args {
        cmd.arg(arg);
    }

    let result = cmd.current_dir(crate_path).output().expect(&error);

    log::info!("{action} on {crate_path:?} DONE");
    if !result.status.success() {
        let code = result.status.code().unwrap();
        log::error!("Cannot execute {args:?} on crate: {crate_path:?} exit code: {code} stdout: {} stderr: {}", std::str::from_utf8(&result.stdout).unwrap(), std::str::from_utf8(&result.stderr).unwrap());
        return false;
    }
    true
}

fn get_crates(path: &Path) -> Vec<PathBuf> {
    log::info!("get_crates");
    let crates = get_crates_recoursive(path);
    log::info!("get_crates done\n");
    crates
}

fn get_crates_recoursive(path: &Path) -> Vec<PathBuf> {
    let mut crates: Vec<PathBuf> = vec![];
    for entry in path.read_dir().expect("read_dir call failed").flatten() {
        if entry.path().ends_with("target") {
            continue;
        }
        //println!("{:?}", entry);
        if entry.path().ends_with("Cargo.toml") {
            //println!("cargo: {:?}", entry.path().parent());
            crates.push(entry.path().parent().unwrap().to_path_buf());
        }
        if entry.path().is_dir() {
            crates.extend(get_crates_recoursive(entry.path().as_path()));
        }
    }

    crates
}

// TODO: go deeper than 2 levels to also handle examples/*/src/main.rs
// TODO: but exclude examples/*/target/
// TODO: move the exclude lists to external files
fn get_all_the_examples() -> Vec<String> {
    log::info!("get_all_the_examples");

    let exclude: Vec<String> = [
        "examples/image/create-image/image.png",
        "examples/other/multi_counter_with_manual_csv/counter.csv",
        "examples/other/send-mail-with-sendgrid/config.txt",
    ]
    .iter()
    .map(|path| path.to_string())
    .collect();
    let pathes = get_examples(Path::new("examples"));
    let pathes: Vec<String> = pathes
        .iter()
        .filter(|path| !exclude.contains(path))
        .cloned()
        .collect();

    log::info!("get_all_the_examples done\n");
    pathes
}

fn get_examples(path: &Path) -> Vec<String> {
    let mut examples: Vec<String> = vec![];
    for entry in path.read_dir().expect("read_dir call failed").flatten() {
        if entry.path().ends_with("Cargo.lock") {
            continue;
        }
        if entry.path().ends_with("Cargo.toml") {
            continue;
        }

        if entry.path().is_dir() {
            if entry.path().ends_with("target") {
                continue;
            }
            examples.extend(get_examples(entry.path().as_path()));
            continue;
        }
        //dbg!(&entry);

        if entry.path().is_file() {
            examples.push(entry.path().into_os_string().into_string().unwrap());
            continue;
        }
    }
    examples
    //return Vec::from_iter( examples.iter().map(|s| s.clone().into_os_string().into_string().expect("Bad") ) );
}

fn get_imported_files(md_files: Vec<PathBuf>) -> Vec<String> {
    log::info!("get_imported_files");
    // println!("{:?}", md_files);
    // ![](examples/arrays/update_hash.rs)
    // let re = Regex::new(r"^!\[\]]\((.*)\)\s*$").unwrap();
    let mut imported_files = vec![];
    for filename in md_files {
        //println!("{:?}", filename);
        match File::open(filename.as_path()) {
            Ok(file) => {
                let reader = BufReader::new(file);
                for line in reader.lines() {
                    let line = line.unwrap();
                    if line.starts_with("![](") && line.ends_with(')') {
                        //println!("{}", &line[4..line.len()-1])
                        imported_files.push((line[4..line.len() - 1]).to_string());
                    }
                }
            }
            Err(error) => {
                log::error!("Failed opening file {filename:?}: {error}");
            }
        }
    }
    log::info!("get_imported_files done\n");
    Vec::from_iter(imported_files.iter().map(|s| s.to_string()))
}

fn get_md_files() -> Vec<PathBuf> {
    log::info!("get_md_files");
    let mut md_files = vec![];
    let path = Path::new(".");
    for entry in path.read_dir().expect("read_dir call failed").flatten() {
        let filename = entry.path();
        //println!("{:?}", filename); //.as_path());
        let extension = filename.extension();
        if let Some(value) = extension {
            if value == "md" {
                // println!("{:?}", filename);
                //println!("{}", filename);
                md_files.push(filename);
            }
        }
        //println!("{:?}", extension.unwrap())
    }

    log::info!("get_md_files done\n");
    md_files
}

#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct Skip {
    example: String,
    update: String,
    test: String,
    clippy: String,
    run: String,
    comment: String,
}

// TODO read this file only once
// TODO compute the path to the file in a simpler way
// TODO change the lookup to be an O(1) operation (instead of returning a vector return a hashmap)
// TODO verify that every example listed in the csv file is still present in the examples directory
// TODO mark examples that should fail compilation and then check that they do fail
fn read_skips() -> HashMap<String, Vec<String>> {
    let path = std::env::current_exe().unwrap();
    let path = path
        .parent()
        .unwrap()
        .parent()
        .unwrap()
        .parent()
        .unwrap()
        .join("skips.csv");
    let csv_text = std::fs::read_to_string(path).unwrap();
    let mut skips: HashMap<String, Vec<String>> = HashMap::from([
        (String::from("update"), vec![]),
        (String::from("test"), vec![]),
        (String::from("clippy"), vec![]),
        (String::from("run"), vec![]),
    ]);
    let mut rdr = csv::ReaderBuilder::new()
        .has_headers(true)
        .trim(csv::Trim::All)
        .from_reader(csv_text.as_bytes());
    for result in rdr.deserialize::<Skip>() {
        match result {
            Ok(record) => {
                if record.update == "true" {
                    skips
                        .get_mut("update")
                        .unwrap()
                        .push(record.example.clone());
                }
                if record.test == "true" {
                    skips.get_mut("test").unwrap().push(record.example.clone());
                }
                if record.clippy == "true" {
                    skips
                        .get_mut("clippy")
                        .unwrap()
                        .push(record.example.clone());
                }
                if record.run == "true" {
                    skips.get_mut("run").unwrap().push(record.example);
                }
            }
            Err(err) => panic!("Error parsing csv {err}"),
        }
    }

    skips
}

fn skip(name: &str) -> Vec<String> {
    let skips = read_skips();

    if name == "update" {
        return skips[&String::from("update")].clone();
    }
    if name == "clippy" {
        return skips[&String::from("clippy")].clone();
    }
    if name == "run" {
        return skips[&String::from("run")].clone();
    }
    if name == "test" {
        return skips[&String::from("test")].clone();
    }

    vec![]
}

fn get_args(action: &str) -> &'static [&'static str] {
    if action == "clippy" {
        return &["clippy", "--", "--deny", "warnings"];
    }
    if action == "update" {
        return &["update"];
    }
    if action == "fmt" {
        return &["fmt"];
    }
    if action == "fmt_check" {
        return &["fmt", "--check"];
    }
    if action == "test" {
        return &["test"];
    }
    if action == "run" {
        return &["run"];
    }

    panic!("Unknown action: {action}");
}

Expressions vs statements

  • Expressions have a return value do NOT need a trailing semi-colon
  • Statements do not have values and need a semi-colon
fn main() {
    let x = 2;
    println!("{x}");

    let y = {
        let a = 2;
        let b = 3;
        a + b // no semi-colon here!
    }; // there is a semi-colon here!
    println!("{y}");
}

Send email via SendGrid

use sendgrid::SGClient;
use sendgrid::{Destination, Mail};
use std::fs::File;
use std::io::{BufRead, BufReader};

fn main() {
    let sendgrid_api_key = get_key();
    let to_address = String::from("szabgab@gmail.com");
    let to_name = String::from("Gabor Szabo");
    let subject = String::from("Test mail");

    sendgrid(&sendgrid_api_key, &to_name, &to_address, &subject);
}

fn get_key() -> String {
    let filename = "config.txt";
    match File::open(filename) {
        Ok(file) => {
            let reader = BufReader::new(file);
            for line in reader.lines() {
                let line = line.unwrap();
                let parts = line.split('=');
                let parts: Vec<&str> = parts.collect();
                if parts[0] == "SENDGRID_API_KEY" {
                    return parts[1].to_owned();
                }
            }
            panic!("Could not find line");
        }
        Err(error) => {
            panic!("Error opening file {}: {}", filename, error);
        }
    }
}

fn sendgrid(api_key: &str, to_name: &str, to_address: &str, subject: &str) {
    let sg = SGClient::new(api_key);

    let mut x_smtpapi = String::new();
    x_smtpapi.push_str(r#"{"unique_args":{"test":7}}"#);

    let mail_info = Mail::new()
        .add_to(Destination {
            address: to_address,
            name: to_name,
        })
        .add_from("gabor@szabgab.com")
        .add_from_name("Original Sender")
        .add_subject(subject)
        .add_html("<h1>Hello from SendGrid!</h1>")
        .add_header(String::from("x-cool"), "indeed")
        .add_x_smtpapi(&x_smtpapi);

    match sg.send(mail_info) {
        Err(err) => println!("Error: {}", err),
        Ok(body) => println!("Response: {:?}", body),
    };
}
[package]
name = "send-mail-with-sendgrid"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
sendgrid = "0.19"

With a file called config.txt in the same directy that has:

SENDGRID_API_KEY=SG....

Equality of Some - values

  • Some
#[allow(clippy::unnecessary_literal_unwrap)]
fn main() {
    let x = Some(3);
    let y = Some(3);
    println!("{:?}", x); // `Option<{integer}>` cannot be formatted with the default formatter
    println!("{}", x == y);

    let z = Some(&3);
    println!("{:?}", z); // `Option<&{integer}>` cannot be formatted with the default formatter
    println!("{}", &x.unwrap() == z.unwrap());
}
Some(3)
true
Some(3)
true

Fork

[package]
name = "show-forking"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
fork = "0.1.22"
use fork::{fork, Fork};

fn main() {
    println!("Before {}", std::process::id());

    match fork() {
        Ok(Fork::Parent(child)) => {
            println!(
                "In parent process {}, new child has pid: {}",
                std::process::id(),
                child
            );
        }
        Ok(Fork::Child) => {
            println!("In child process {}", std::process::id());
            std::process::exit(0);
        }
        Err(_) => println!("Fork failed"),
    }
    println!("Launched {}", std::process::id());

    println!("After  {}", std::process::id());
}

TODO: wait, waitpid?

sysinfo - Which Operating System are we running on?

  • systinfo

  • kernel_version

  • os_version

  • sysinfo

[package]
name = "system-info"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
sysinfo = "0.29"
use sysinfo::{System, SystemExt};

fn main() {
    let sys = System::new_all();

    println!("System name:             {}", sys.name().unwrap());
    println!("System kernel version:   {}", sys.kernel_version().unwrap());
    println!("System OS version:       {}", sys.os_version().unwrap());
    println!("System host name:        {}", sys.host_name().unwrap());
    println!();

    println!("NB CPUs:                 {}", sys.cpus().len());
    println!();

    println!("total memory: {} bytes", sys.total_memory());
    println!("used memory : {} bytes", sys.used_memory());
    println!("total swap  : {} bytes", sys.total_swap());
    println!("used swap   : {} bytes", sys.used_swap());
}
System name:             Ubuntu
System kernel version:   6.5.0-10-generic
System OS version:       23.10
System host name:        code-maven

NB CPUs:                 16

total memory: 29166940160 bytes
used memory : 10617876480 bytes
total swap  : 8589930496 bytes
used swap   : 0 bytes

Operating system information with os_info

  • os_info

  • os_type

  • architecture

  • os_info

[package]
name = "os-information"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
os_info = "3.7.0"
fn main() {
    let info = os_info::get();

    // Print full information:
    println!("OS information: {info}");

    // Print information separately:
    println!("Type: {}", info.os_type());
    println!("Version: {}", info.version());
    println!("Bitness: {}", info.bitness());
    println!("Architecture: {}", info.architecture().unwrap());
}
OS information: Ubuntu 23.10 (mantic) [64-bit]
Type: Ubuntu
Version: 23.10
Bitness: 64-bit
Architecture: x86_64

Parse string to Rust expression using syn

  • syn
  • parse_str
use syn::{Expr, Result};

fn main() {
    match run() {
        Ok(_val) => println!("ok"),
        Err(err) => println!("err: {}", err),
    }

    cases();
}

fn run() -> Result<()> {
    let code = "..";
    let expr = syn::parse_str::<Expr>(code)?;
    let _res = match expr {
        syn::Expr::Range(_expr) => true, // _expr is the same expression that cannot be printed.
        _ => false,
    };
    //println!("{}", res);

    Ok(())
}

fn cases() {
    assert!(matches!(
        syn::parse_str::<Expr>("..").unwrap(),
        syn::Expr::Range(_expr)
    ));

    //let res = syn::parse_str::<Expr>("2 == 3").unwrap();
    //assert!(matches!(res, syn::Expr::Range(_expr)));

    //assert!(matches!(expr, syn::Expr::If(_expr)));
    println!("done");
}
ok
done

Parse HTML

  • Html

  • parse

  • parse_document

  • CSS

  • Selector

  • attr

  • inner_html

  • scraper

use scraper::{Html, Selector};

fn main() {
    let html = r#"
        <!DOCTYPE html>
        <meta charset="utf-8">
        <title>Hello, world!</title>
        <h1 class="foo">Hello, <i>world!</i></h1>
    "#;

    let document = Html::parse_document(html);
    let selector = Selector::parse("h1").unwrap();
    for element in document.select(&selector) {
        assert_eq!(element.value().name(), "h1");
        assert_eq!(element.attr("class").unwrap(), "foo");
        assert_eq!(element.inner_html(), "Hello, <i>world!</i>");
        assert!(element.has_children());
        let x = element.has_children();
        //let x = element.inner_html();
        println!("{:?}", x);

        // The text content of an elemnet after removing all the markup
        let text = element.text().collect::<Vec<_>>().join("");
        assert_eq!(text, "Hello, world!");
    }

    let selector = Selector::parse("i").unwrap();
    for element in document.select(&selector) {
        assert_eq!(element.value().name(), "i");
        assert_eq!(element.inner_html(), "world!");
        assert!(element.has_children());
        let x = element.has_children();
        //let x = element.inner_html();
        println!("{:?}", x);
    }
}

Fix URL parameter

  • trim_end_matches

  • url

  • The user can provide a URL, but I would like to be flexible and accept both with and without a trailing slash:

  • https://rust.code-maven.com

  • https://rust.code-maven.com/

At first I tried some over-engineered solutions, till I got the recommendation to use trim_end_matches.

fn main() {
    for url in [
        String::from("https://rust.code-maven.com"),
        String::from("https://rust.code-maven.com/"),
    ] {
        process_rec(&url);
        process_mut(&url);
        pre_process(&url);
        trim_if_needed(&url);
    }
}

fn trim_if_needed(url: &str) {
    process(url.trim_end_matches('/'));
}

fn process_rec(url: &str) {
    if url.ends_with('/') {
        return process_rec(&url[0..url.len() - 1]);
    }
    process(url);
}

fn process_mut(mut url: &str) {
    if url.ends_with('/') {
        url = &url[0..url.len() - 1];
    }
    process(url);
}

fn pre_process(url: &str) {
    if url.ends_with('/') {
        return process(&url[0..url.len() - 1]);
    }
    process(url)
}

fn process(url: &str) {
    fetch(url);
    fetch(&format!("{}/page", url));
}

fn fetch(url: &str) {
    println!("Process '{}'", url);
}

However using the url crate might be the best solution in this case:

[package]
name = "fix-url"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
url = "2.5.0"
fn main() {
    for site in [
        String::from("https://rust.code-maven.com"),
        String::from("https://rust.code-maven.com/"),
    ] {
        process(url::Url::parse(&site).unwrap());
    }
}

fn process(site: url::Url) {
    fetch(&site);
    fetch(&site.join("page").unwrap());
}

fn fetch(site: &url::Url) {
    println!("Process '{}'", site);
}

My little Rust runner

  • This is especially useful for the slides so I can create individual Rust example files and run them stand alone.
#!/usr/bin/bash -e

if [ "$1" == "" ];
then
    echo "Usage: $0 path/to/file.rs"
    exit 1
fi

# rustc $1
# name=$(basename $1 .rs)
# ./$name

rustc $1 -o myexe
shift
if [ -f myexe ];
then
    ./myexe $*
    \rm myexe
fi
./rust.sh examples/intro/hello.rs

Ordered floats

Linked list using struct and Box

  • Box
#[derive(Debug)]
#[allow(dead_code)]
struct Thing {
    data: String,
    next: Option<Box<Thing>>,
}

fn main() {
    let a = Thing {
        data: String::from("First"),
        next: None,
    };
    println!("{:#?}", a);

    let b = Thing {
        data: String::from("Second"),
        next: Some(Box::new(a)),
    };
    println!("{:#?}", b);

    let c = Thing {
        data: String::from("Third"),
        next: Some(Box::new(b)),
    };
    println!("{:#?}", c);
}
Thing {
    data: "First",
    next: None,
}
Thing {
    data: "Second",
    next: Some(
        Thing {
            data: "First",
            next: None,
        },
    ),
}
Thing {
    data: "Third",
    next: Some(
        Thing {
            data: "Second",
            next: Some(
                Thing {
                    data: "First",
                    next: None,
                },
            ),
        },
    ),
}

Undirected graph

#![allow(clippy::vec_init_then_push)]

#[allow(dead_code)]
#[derive(Debug)]
struct Node {
    // Vertex, Point
    name: String,
}

#[allow(dead_code)]
#[derive(Debug)]
struct Edge<'a> {
    // Arcs
    a: &'a Node,
    b: &'a Node,
}

fn main() {
    let kneiphof = Node {
        name: String::from("Kneiphof"),
    };
    let lomse = Node {
        name: String::from("Lomse"),
    };
    let left = Node {
        name: String::from("Left bank"),
    };
    let _right = Node {
        name: String::from("Right bank"),
    };

    let mut edges: Vec<Edge> = vec![];
    edges.push(Edge {
        a: &kneiphof,
        b: &lomse,
    });

    edges.push(Edge {
        a: &left,
        b: &lomse,
    });

    edges.push(Edge {
        a: &left,
        b: &lomse,
    });

    edges.push(Edge {
        a: &left,
        b: &kneiphof,
    });

    println!("{:#?}", edges);
}

Memory leak

In this example we try to implement a memory leak as we demonstrate in Python showing how Rust makes us avoid it. So far this code has compilation errors.

#![allow(unused_variables)]

#[allow(dead_code)]
struct Thing {
    data: String,
    other: Option<Box<Thing>>,
}

fn main() {
    alloc();
}

fn alloc() {
    let mut a = Thing {
        data: String::from("abc"),
        other: None,
    };

    let mut b = Thing {
        data: String::from("abc"),
        other: None,
    };

    a.other = Some(Box::new(b));
    b.other = Some(Box::new(a));
}

Debug assertions

fn main() {
    println!("Hello, world!");
    if cfg!(debug_assertions) {
        println!("debug_assertions");
    }
}
cargo run

cargo run --release

Passing string to function

  • TODO
// Experiment to use a function accepting strings without explaining the references
// to be used at the beginning of the course

fn main() {
    let args = std::env::args().collect::<Vec<String>>();
    println!("{}", add(args[1].clone(), args[2].clone()));
}

fn add(x: String, y: String) -> u32 {
    let a: u32 = x.parse().expect("number");
    let b: u32 = y.parse().expect("number");
    a + b
}

Num traits

map with threads with Mutex

  • Mutex
  • TODO
use std::sync::{Condvar, Mutex};
use std::thread;

fn main() {
    let numbers: Vec<i32> = (1..=10).collect();
    println!("{:?}", numbers);

    let doubles = map_thread(&numbers, double_function, 3);
    println!("{:?}", doubles);
}

fn double_function(num: &i32) -> i32 {
    //println!("{:?}", std::thread::current().id());
    2 * num
}

fn map_thread<Tin: Sync, Tout: Send>(
    params: &[Tin],
    func: fn(&Tin) -> Tout,
    max_threads: i32,
) -> Vec<Tout> {
    let thread_count = Mutex::new(0);
    let notify_finish = Condvar::new();

    // Use scoped threads so that we can pass references around
    thread::scope(|scope| {
        let mut handles = vec![];
        for xref in params {
            // Wait for earlier threads to finish, if necessary
            let mut guard = thread_count.lock().unwrap();
            while *guard >= max_threads {
                guard = notify_finish.wait(guard).unwrap();
            }

            // Increment running thread count
            *guard += 1;

            handles.push(scope.spawn(|| {
                // Run calculation
                let res = func(xref);

                // Report success, decrement running thread count
                *(thread_count.lock().unwrap()) -= 1;
                notify_finish.notify_one();

                // Return calculation result
                res
            }));
        }

        // Read the return values of each thread, in order
        handles.into_iter().map(|h| h.join().unwrap()).collect()
    })
}

Accessing envrionment variables

  • std::env
  • std::env::var

We can access the environment variables from Rust using the std::env::var function. It returns a Result.

use std::env;

fn main() {
    for name in ["PATH", "RUST"] {
        println!("Checking {name}");
        match env::var(name) {
            Ok(val) => println!("{name}={val}"),
            Err(err) => println!("Environment variable {name} does not exist.\n{err}"),
        }
    }
}

Run as

cargo run
RUST=42 cargo run

List environment variables

  • std::env
  • std::env::vars
use std::env;

fn main() {
    for (name, _value) in env::vars() {
        println!("Variable: {name}");
        // match env::var(name) {
        //     Ok(val) => println!("{name}={val}"),
        //     Err(err) => println!("Environment variable {name} does not exist.\n{err}"),
        // }
    }
}

Closures

fn main() {
    do_something();
    do_something_more();
    change_something();
}

fn do_something() {
    let age = 42;

    let display = || println!("The age is {age}");

    display();
}

fn do_something_more() {
    let age = 42;

    let display = || {
        println!("Before");
        println!("More age {age}");
        println!("After");
    };

    display();
}

fn change_something() {
    let mut age = 42;

    let mut change = || {
        age = 43;
    };

    //println!("Age before {age}");
    change();
    println!("Age after {age}");
}

Reference to a number

  • TODO
fn main() {
    copy_and_change();
    reference_is_borrow();
    reference_in_scope_is_borrow();
}

fn copy_and_change() {
    let mut a = 23;
    let b = 23;

    a += 1;

    println!("{a} {:p}", &a);
    println!("{b} {:p}", &b);
}

fn reference_is_borrow() {
    let mut a = 23;
    a += 1;

    let b = &a;

    //a += 1; // cannot assign to `a` because it is borrowed

    println!("{a} {:p}", &a);
    println!("{b} {:p}", &b);
}

fn reference_in_scope_is_borrow() {
    let mut a = 23;
    a += 1;

    let b = &a;
    println!("{b} {:p}", &b); // the lifetime of the reference ends here

    a += 1; // cannot assign to `a` because it is borrowed

    println!("{a} {:p}", &a);
}

Out of memory

This program will create an ever growing string. When it reaches the total size of the (free) memory in the computer it crashes.

fn main() {
    let n = 100;

    let mut text = String::from("x");
    for i in 0..n {
        text.push_str(&text.clone());
        println!("len: {} {}", i, text.len());
    }
}

Two leveles of modules

#![allow(dead_code)]

fn main() {
    println!("in main");
    //house::live();
    //crate::house::live();

    // kitchen is private
    //house::kitchen::cook();
    //crate::house::kitchen::cook();

    //crate::house::bathroom::shower();
    //house::bathroom::shower();

    house::bathroom::shaving();
}

mod house {
    pub fn live() {
        println!("live");
        kitchen::cook();
        bathroom::shower();
    }

    mod kitchen {
        pub fn cook() {
            println!("cook");
            peal_potatoes();
        }
        fn peal_potatoes() {
            println!("peal_potatoes");
        }

        pub fn doing_dishes() {
            println!("doing_dishes");
        }
    }

    pub mod bathroom {
        pub fn shower() {
            println!("shower");
            use_soap()
        }

        fn use_soap() {
            println!("use_soap");
        }

        pub fn shaving() {
            println!("shaving");
            crate::house::kitchen::cook();
            super::kitchen::cook();
        }
    }
}

Try packages

fn main() {
    println!("main");
    in_main_rs();
    // colors::red(); // private function

    colors::red();
    crate::colors::red();

    colors::deep::in_deep();

    // colors::blue();
    // // colors::blue_helper(); // error[E0603]: function `blue_helper` is private
    // colors::dark::blue();

    // use colors::dark;
    // dark::green();
}

fn in_main_rs() {
    println!("in_main_rs");
}

mod colors {
    // fn red() {
    //     println!("red");
    // }

    pub fn red() {
        println!("public red");
        deep::in_deep();
    }

    // pub fn blue() {
    //     println!("blue");
    //     blue_helper(); // can be called from here
    // }

    pub mod deep {
        pub fn in_deep() {
            println!("in_deep");
        }
    }

    // fn blue_helper() {
    //     println!("blue_helper");
    //     crate::in_main_rs();
    //     super::in_main_rs();
    // }

    // pub mod dark {
    //     pub fn blue() {
    //         println!("dark_blue");
    //         crate::in_main_rs();        // absolute path
    //         super::super::in_main_rs(); // relative path, probably not very good idea
    //         super::red();
    //     }
    //     pub fn green() {
    //         println!("dark_green");
    //     }
    // }
}

release.toml

  • More than 500 crates have a file called release.toml in their git repository and 2 have .release.toml.
  • These are configuration files of the cargo-release crate to make releases easier.

thousands crate for struct with Display (commafy)

  • thousands
  • commafy
use thousands::Separable;

struct Point {
    x: i32,
    y: i32,
}

impl std::fmt::Display for Point {
    fn fmt(&self, format: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(format, "x{} {}x", self.x, self.y)
    }
}

fn main() {
    assert_eq!(12345.separate_with_commas(), "12,345");

    assert_eq!("12345".separate_with_commas(), "12,345");

    let p = Point { x: 1234, y: 4567 };
    println!("{p}");
    println!("{}", p.separate_with_commas());
}
x1234 4567x
x1,234 4567x

Early return on None Option

#![allow(unused)]
#![allow(dead_code)]

fn main() {
fn compute(input: bool) -> Option<String> {
    if input {
        Some(String::from("text"))
    } else {
        None
    }
}

fn manual(input: bool) -> String {
    let result = compute(input);

    if result.is_none() {
        return String::from("Missing");
    }
    let data = result.unwrap();

    // process data:
    format!("Process {data}")
}

fn with_match(input: bool) -> String {
    let result = compute(input);

    match result {
        None => String::from("Missing"),
        Some(data) => {
            // process data:
            format!("Process {data}")
        }
    }
}

fn with_match_and(input: bool) -> String {
    let result = compute(input);

    let data = match result {
        None => return String::from("Missing"),
        Some(data) => data,
    };

    // process data:
    format!("Process {data}")
}

macro_rules! ok_or_return {
    ($cond: expr, $result: expr) => {
        match $cond {
            None => return $result,
            Some(data) => data,
        }
    };
}

fn with_macro(input: bool) -> String {
    let result = compute(input);

    let data = ok_or_return!(result, String::from("Missing"));

    // process data:
    format!("Process {data}")
}

fn let_else(input: bool) -> String {
    let result = compute(input);

    let Some(data) = result else {
        return String::from("Missing");
    };

    // process data:
    format!("Process {data}")
}

#[test]
fn test_compute() {
    assert_eq!(compute(true), Some(String::from("text")));
    assert_eq!(compute(false), None);
}

#[test]
fn test_manual() {
    assert_eq!(manual(true), String::from("Process text"));
    assert_eq!(manual(false), String::from("Missing"));
}

#[test]
fn test_with_match() {
    assert_eq!(with_match(true), String::from("Process text"));
    assert_eq!(with_match(false), String::from("Missing"));
}

#[test]
fn test_with_match_and() {
    assert_eq!(with_match_and(true), String::from("Process text"));
    assert_eq!(with_match_and(false), String::from("Missing"));
}

#[test]
fn test_with_macro() {
    assert_eq!(with_match_and(true), String::from("Process text"));
    assert_eq!(with_match_and(false), String::from("Missing"));
}

#[test]
fn test_let_else() {
    assert_eq!(let_else(true), String::from("Process text"));
    assert_eq!(let_else(false), String::from("Missing"));
}
}

Pattern matching with guards on a number

Rust does not take in account the guard conditions when checking if the arms cover all the possible values. In other words Rust cannot see that our first 3 conditions are exhaustive and that the last condition will never be reached. Thus Rust requires the catch-all _ to be present in order to compile the code

fn main() {
    let age = 65;
    match age {
        x if x < 18 => println!("You are a minor"),
        x if x >= 18 && x <= 65 => println!("You are an adult"),
        x if x>65 => println!("You are a senior citizen"),
        _ => println!("Invalid age")
    }
}

egui

About eframe and egui

eGUI Window

  • CentralPanel
  • NativeOptions
  • run_simple_native
cargo new egui-window
cd egui-window
cargo add eframe

This will make Cargo.toml look like this:

[package]
name = "egui-window"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
eframe = "0.24"

The code is here:

use eframe::egui;

fn main() -> Result<(), eframe::Error> {
    let options = eframe::NativeOptions {
        viewport: egui::ViewportBuilder::default().with_inner_size([320.0, 240.0]),
        ..Default::default()
    };

    eframe::run_simple_native("Rust Maven egui App", options, move |ctx, _frame| {
        egui::CentralPanel::default().show(ctx, |_ui| {});
    })
}

{% embed include file="src/examples/egui/egui-window/screenshot.png)

When you click on the X to close the window, you might get lots of warnings like this:

warning: queue 0x7fa398000ca0 destroyed while proxies still attached:

This was reported here.

eGUI heading

  • heading
use eframe::egui;

fn main() -> Result<(), eframe::Error> {
    let options = eframe::NativeOptions {
        viewport: egui::ViewportBuilder::default().with_inner_size([320.0, 240.0]),
        ..Default::default()
    };

    eframe::run_simple_native("Rust Maven egui App", options, move |ctx, _frame| {
        egui::CentralPanel::default().show(ctx, |ui| {
            ui.heading("Hello World");
        });
    })
}

eGUI label

  • label
use eframe::egui;

fn main() -> Result<(), eframe::Error> {
    let options = eframe::NativeOptions {
        viewport: egui::ViewportBuilder::default().with_inner_size([320.0, 240.0]),
        ..Default::default()
    };

    eframe::run_simple_native("Rust Maven egui App", options, move |ctx, _frame| {
        egui::CentralPanel::default().show(ctx, |ui| {
            ui.heading("Hello World");
            ui.label("A label");
        });
    })
}

eGUI counter with label and button

  • button
  • clicked
  • label
use eframe::egui;

fn main() -> Result<(), eframe::Error> {
    let options = eframe::NativeOptions {
        viewport: egui::ViewportBuilder::default().with_inner_size([320.0, 240.0]),
        ..Default::default()
    };

    let mut counter = 0;

    eframe::run_simple_native("Rust Maven egui App", options, move |ctx, _frame| {
        egui::CentralPanel::default().show(ctx, |ui| {
            if ui.button("Increment").clicked() {
                counter += 1;
            }
            ui.label(format!("Count {counter}"));
        });
    })
}
  • Every time we click on the button, the code in the block gets executed incrementing the counter
  • Then the displayed label is refreshed.

Egui demo app

use eframe::egui;

fn main() {
    let native_options = eframe::NativeOptions::default();
    eframe::run_native(
        "My egui App",
        native_options,
        Box::new(|cc| Box::new(MyEguiApp::new(cc))),
    )
    .unwrap();
}

#[derive(Default)]
struct MyEguiApp {}

impl MyEguiApp {
    fn new(_cc: &eframe::CreationContext<'_>) -> Self {
        // Customize egui here with cc.egui_ctx.set_fonts and cc.egui_ctx.set_visuals.
        // Restore app state using cc.storage (requires the "persistence" feature).
        // Use the cc.gl (a glow::Context) to create graphics shaders and buffers that you can use
        // for e.g. egui::PaintCallback.
        Self::default()
    }
}

impl eframe::App for MyEguiApp {
    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
        egui::CentralPanel::default().show(ctx, |ui| {
            ui.heading("Hello World!");
        });
    }
}

images

Draw some simple images

[package]
name = "crate-image"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
image = "0.24"
fn main() {
    let width = 800;
    let height = 800;

    let mut imgbuf = image::ImageBuffer::new(width, height);

    // gradient based on the example in the sourcde code
    // for (x, y, pixel) in imgbuf.enumerate_pixels_mut() {
    //     let red = (0.3 * x as f32) as u8;
    //     let green = 0;
    //     let blue = (0.3 * y as f32) as u8;

    //     *pixel = image::Rgb([red, green, blue]);
    // }

    // horizontal red gradient
    // for (x, y, pixel) in imgbuf.enumerate_pixels_mut() {
    //     let red = (0.3 * x as f32) as u8;
    //     let green = 0;
    //     let blue = 0;
    //     *pixel = image::Rgb([red, green, blue]);
    // }

    // vertical red gradient
    // for (x, y, pixel) in imgbuf.enumerate_pixels_mut() {
    //     let red = (0.3 * y as f32) as u8;
    //     let green = 0;
    //     let blue = 0;
    //     *pixel = image::Rgb([red, green, blue]);
    // }

    // horizontal red line
    // for (x, y, pixel) in imgbuf.enumerate_pixels_mut() {
    //     let red = 255 as u8;
    //     let green = 0;
    //     let blue = 0;
    //     if y == 23 {
    //         *pixel = image::Rgb([red, green, blue]);
    //     }
    // }

    // draw a horizontal line
    // let red = 100 as u8;
    // let green = 100;
    // let blue = 0;
    // let y = 200;
    // for x in 0..width {
    //     // let pixel = imgbuf.get_pixel_mut(x, y);
    //     // *pixel = image::Rgb([red, green, blue]);
    //     *imgbuf.get_pixel_mut(x, y) = image::Rgb([red, green, blue]);
    // }

    // draw a vertical line
    let red = 100_u8;
    let green = 100;
    let blue = 0;
    let x = 200;
    for y in 0..height {
        // let pixel = imgbuf.get_pixel_mut(x, y);
        // *pixel = image::Rgb([red, green, blue]);
        *imgbuf.get_pixel_mut(x, y) = image::Rgb([red, green, blue]);
    }

    imgbuf.save("image.png").unwrap();
}

Binary

Read binary file into a vector of u8

fn main() {
    let args = std::env::args().collect::<Vec<_>>();
    if args.len() != 2 {
        println!("Usage: {} FILENAME", args[0]);
        std::process::exit(1);
    }
    let filename = &args[1];

    println!("filename {filename}");

    let data: Vec<u8> = std::fs::read(filename).unwrap();
    println!("len: {}", data.len());
}

unsafe

unsafe keywords

unsafe powers

  • TODO

  • Dereference a raw pointer

  • Call an unsafe function or method

  • Access or modify a mutable static variable

  • Implement an unsafe trait

  • Access fields of a union

fn main() {
    let mut text = String::from("abcd");
    println!("{text}");
    let pointer: *mut String = &mut text as *mut String; // mutable raw pointer
    println!("pointer is: {:?}", pointer);
    println!("addr_of: {:?}", std::ptr::addr_of!(text));
    unsafe {
        // pointer.offset(count);
        println!("pointer+1 is: {:?}", pointer.offset(1));
        println!("pointer is: {}", *pointer);

        //println!("ptr is: {}", *ptr.offset(1));
    }

    println!("{text}");

    // let address = 0x012345usize;
    // let r = address as *const i32;

    // println!("{r:?}");

    //safe_increment();
    // unsafe_increment();
}

// fn safe_increment() {

//     let mut num = 5;

//     let r1 = &num;

//     num += 1;

//     println!("{num}");
//     println!("{r1}");
// }

// fn unsafe_increment() {

//     let mut num = 5;

//     let r1 = &num as *const i32; // immutable raw pointer
//     let r2 = &mut num as *mut i32; // mutable raw pointer
//     println!("r1 is: {:?}", r1);
//     println!("r2 is: {:?}", r2);
//     unsafe {
//         println!("r1 is: {}", *r1);
//         println!("r2 is: {}", *r2);
//         *r2 += 1;

//     }
//     println!("{num}");
// }

Unsafe - core dump

  • TODO
fn main() {
    let numbers = vec![3, 5, 7];
    let ptr_1 = numbers.as_ptr() as *mut i32;
    println!("{:?}", numbers);
    //println!("{ptr_1:?}");
    println!("{ptr_1:p}");
    let mut ptr_1 = ptr_1 as usize;
    ptr_1 += 4;
    let nums = unsafe {
        Vec::from_raw_parts(ptr_1 as *mut i32, numbers.len() - 1, numbers.capacity() - 1)
    };
    println!("{:?}", nums);
}

Sea ORM

Counter with Sea-ORM

  • TODO
[package]
name = "counter"
version = "0.1.0"
edition = "2021"

[dependencies]
futures = "0.3.30"
sea-orm = { version = "1.0.1", features = ["sqlx-sqlite", "runtime-async-std-native-tls", "macros"] }
use sea_orm::{ConnectOptions, Database, DbErr};
// cargo install sea-orm-cli

use futures::executor::block_on;

fn main() {
    if let Err(err) = block_on(run()) {
        panic!("{}", err);
    }
}

async fn run() -> Result<(), DbErr> {
    let args = std::env::args().collect::<Vec<_>>();
    if args.len() > 2 {
        println!("Usage: {} [NAME]", args[0]);
        std::process::exit(1);
    }

    let opt = ConnectOptions::new("sqlite:./counter.db?mode=rwc");
    let _db = Database::connect(opt).await?;

    if args.len() == 1 {
        list_counters();
    } else {
        let name = &args[1];
        increment(name);
    }
    Ok(())
}

fn list_counters() {
    println!("list");
}

fn increment(name: &str) {
    println!("{name}:");
}

Embedded

Resaource about Rust on Embedded