termint 0.9.0

Library for colored printing and Terminal User Interfaces
Documentation
use std::{cell::RefCell, process::ExitCode, rc::Rc};

use fake::Fake;
use termal::eprintcln;
use termint::prelude::*;

const BG: Color = Color::Hex(0x02081e);
const BGL: Color = Color::Hex(0x061038);
const BORDER: Color = Color::Hex(0x535C91);
const FG: Color = Color::Hex(0xc3c1f4);
const SELL: Color = Color::Hex(0xf3a6fc);
const SEL: Color = Color::Hex(0xea4bfc);

fn main() -> ExitCode {
    if let Err(e) = run() {
        eprintcln!("{'r}Error:{'_} {e}");
        return ExitCode::FAILURE;
    }
    ExitCode::SUCCESS
}

fn run() -> Result<(), Error> {
    let mut app = App::default();
    Term::default().setup()?.with_mouse().run(&mut app)
}

#[derive(Clone)]
enum Message {
    CellClicked(usize, usize),
}

struct App {
    table_state: Rc<RefCell<TableState>>,
    employees: Vec<Employee>,
}

impl Application for App {
    type Message = Message;

    fn view(&self, _frame: &Frame) -> Element<Self::Message> {
        let active_cnt = self.employees.iter().filter(|e| e.active).count();

        let table = Table::new(
            get_rows(&self.employees),
            [
                Unit::Length(4),
                Unit::Fill(1),
                Unit::Fill(1),
                Unit::Length(10),
            ],
            self.table_state.clone(),
        )
        .header(vec!["ID", "Name", "Email", "Status"])
        .header_separator(BorderType::Normal)
        .footer(vec![
            "".to_span(),
            format!("Total: {}", self.employees.len()).to_span(),
            "".to_span(),
            format!("{} Active", active_cnt).to_span(),
        ])
        .footer_separator(BorderType::Normal)
        .selected_row_style((BG, SELL))
        .selected_column_style(SELL)
        .selected_cell_style((BGL, SEL))
        .column_spacing(2)
        .on_click(Message::CellClicked)
        .auto_scroll();

        let help =
            "[↑]Move up [↓]Move down [←]Move left [→]Move right [Esc|q]Quit"
                .fg(BORDER);

        let mut block = Block::vertical()
            .title("Employees")
            .border_type(BorderType::Thicker)
            .border_style(Style::new().bg(BG).fg(BORDER))
            .style(Style::new().bg(BG).fg(FG));
        block.push(table, Constraint::Fill(1));
        block.push(help, 1..);
        block.into()
    }

    fn event(&mut self, event: Event) -> Action {
        match event {
            Event::Key(key) => self.key_listener(key),
            _ => Action::NONE,
        }
    }

    fn message(&mut self, message: Self::Message) -> Action {
        match message {
            Message::CellClicked(x, y) => {
                let mut state = self.table_state.borrow_mut();
                state.selected = Some(y);
                state.selected_column = Some(x);
            }
        }
        Action::RERENDER
    }
}

#[derive(Clone)]
struct Employee {
    id: usize,
    name: String,
    email: String,
    active: bool,
}

impl Employee {
    fn generate(count: usize) -> Vec<Self> {
        (0..count)
            .map(|i| Employee {
                id: i + 1,
                name: fake::faker::name::en::Name().fake(),
                email: fake::faker::internet::en::SafeEmail().fake(),
                active: (0..10).fake::<u8>() > 2,
            })
            .collect()
    }
}

impl App {
    fn key_listener(&mut self, key: KeyEvent) -> Action {
        match key.code {
            KeyCode::Down | KeyCode::Char('j') => {
                let mut state = self.table_state.borrow_mut();
                let Some(sel) = state.selected else {
                    return Action::NONE;
                };

                if sel + 1 < self.employees.len() {
                    state.selected = Some(sel + 1);
                }
            }
            KeyCode::Up | KeyCode::Char('k') => {
                let mut state = self.table_state.borrow_mut();
                let Some(sel) = state.selected else {
                    return Action::NONE;
                };

                state.selected = Some(sel.saturating_sub(1));
            }
            KeyCode::Left | KeyCode::Char('h') => {
                let mut state = self.table_state.borrow_mut();
                let Some(sel) = state.selected_column else {
                    return Action::NONE;
                };

                state.selected_column = Some(sel.saturating_sub(1));
            }
            KeyCode::Right | KeyCode::Char('l') => {
                let mut state = self.table_state.borrow_mut();
                let Some(sel) = state.selected_column else {
                    return Action::NONE;
                };

                if sel + 1 < 4 {
                    state.selected_column = Some(sel + 1);
                }
            }
            KeyCode::Esc | KeyCode::Char('q') => return Action::QUIT,
            _ => return Action::NONE,
        }
        Action::RERENDER
    }
}

impl Default for App {
    fn default() -> Self {
        Self {
            table_state: Rc::new(RefCell::new(
                TableState::new(0).selected(0).selected_column(0),
            )),
            employees: Employee::generate(100),
        }
    }
}

fn get_rows<M: Clone>(data: &[Employee]) -> Vec<Row<M>> {
    let rows = data
        .iter()
        .enumerate()
        .map(|(i, e)| {
            let status = if e.active {
                "\nActive\n".green()
            } else {
                "\nOffline\n".gray()
            };

            let mut row = Row::new(vec![
                format!("\n{}\n", e.id).to_span(),
                format!("\n{}\n", e.name).to_span(),
                format!("\n{}\n", e.email).to_span(),
                status.into(),
            ]);
            if i % 2 == 0 {
                row = row.style(Style::new().bg(BGL));
            }
            row
        })
        .collect();
    rows
}