kbt 2.0.5

Keyboard tester in terminal
use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect};
use ratatui::style::{Color, Modifier, Style};
use ratatui::widgets::{Block, BorderType, Borders, Paragraph};
use ratatui::Frame;

use crate::key::Key;
use crate::model::{KbtError, KeyState, KeyUI, KeyboardLayout, KeyboardSize, VerticalKeyPart};
use crate::{App, KEY_HEIGHT};

pub fn show_to_small_dialog(frame: &mut Frame) {
    let terminal_size = frame.size();

    let message = "window is too small :(";

    let message_height: u16 = 1;
    let message_width: u16 = 25;
    let left_padding: u16 = (terminal_size.width / 2) - (message_width / 2);
    let top_padding: u16 = (terminal_size.height / 2) - (message_height / 2);

    let rect = Rect::new(left_padding, top_padding, message_width, message_height);

    let title = Paragraph::new(message).style(
        Style::default()
            .fg(Color::Yellow)
            .add_modifier(Modifier::ITALIC),
    );

    frame.render_widget(title, rect);
}

pub fn draw(frame: &mut Frame, state: &App) -> Result<(), KbtError> {
    match state.keyboard_size {
        KeyboardSize::Keyboard60 => draw_layout(frame, state, &state.layouts.layout_60),
        KeyboardSize::Keyboard80 => draw_layout(frame, state, &state.layouts.layout_80),
        KeyboardSize::Keyboard100 => draw_layout(frame, state, &state.layouts.layout_100),
    }
}

fn draw_layout(frame: &mut Frame, state: &App, layout: &KeyboardLayout) -> Result<(), KbtError> {
    let terminal_size: Rect = frame.size();

    let left_padding: u16 = (terminal_size.width / 2) - (layout.width / 2);
    let top_padding: u16 = (terminal_size.height / 2) - (layout.height / 2);

    for (idx, row) in layout.rows.iter().enumerate() {
        let idx: u16 = u16::try_from(idx)?;
        let y_offset: u16 = (KEY_HEIGHT * idx) + top_padding;
        let keyboard_rect = Rect::new(left_padding, y_offset, row.width, KEY_HEIGHT);

        draw_row(&row.keys, state, keyboard_rect, frame)
    }

    let less_than_5_pressed = state
        .key_states
        .values()
        .filter(|key_state| matches!(key_state, KeyState::Released | KeyState::Pressed))
        .count()
        < 5;

    if less_than_5_pressed {
        draw_help(top_padding + layout.height + 3, frame);
    }

    Ok(())
}

fn draw_row(row_keys: &[KeyUI], state: &App, keyboard_rect: Rect, frame: &mut Frame) {
    let chunks = Layout::default()
        .direction(Direction::Horizontal)
        .constraints(make_row_constraints(row_keys).as_ref())
        .split(keyboard_rect);

    for (x_pos, ui_key) in row_keys.iter().enumerate() {
        let key_state = state
            .key_states
            .get(&ui_key.key)
            .unwrap_or(&KeyState::Untouched);

        let borders = match (ui_key.key, ui_key.vertical_key_part) {
            (Key::Separator, _) => Borders::NONE,
            (_, Some(VerticalKeyPart::Top)) => Borders::LEFT | Borders::RIGHT | Borders::TOP,
            (_, Some(VerticalKeyPart::Bottom)) => Borders::LEFT | Borders::RIGHT | Borders::BOTTOM,
            _ => Borders::ALL,
        };

        let border_type = match key_state {
            KeyState::Pressed => BorderType::Double,
            KeyState::Released => BorderType::Thick,
            KeyState::Untouched => BorderType::Plain,
        };

        let style = match key_state {
            KeyState::Pressed => Style::default().fg(Color::Yellow),
            KeyState::Released => Style::default()
                .fg(Color::LightGreen)
                .add_modifier(Modifier::BOLD),
            KeyState::Untouched => Style::default(),
        };

        let block = Block::default().borders(borders).border_type(border_type);

        let label = if let Some(VerticalKeyPart::Bottom) = ui_key.vertical_key_part {
            String::new()
        } else {
            ui_key.key.to_string()
        };

        let text = Paragraph::new(label)
            .block(block)
            .style(style)
            .alignment(Alignment::Center);

        frame.render_widget(text, chunks[x_pos])
    }
}

fn make_row_constraints(keys: &[KeyUI]) -> Vec<Constraint> {
    keys.iter()
        .map(|key| {
            Constraint::Length(
                u16::try_from(key.size.static_len() as i16 + key.size_correction.unwrap_or(0))
                    .unwrap_or(0),
            )
        })
        .collect()
}

fn draw_help(y_offset: u16, frame: &mut Frame) {
    let terminal_size = frame.size();
    let fits = frame.size().height > y_offset;

    if fits {
        let message = "ctrl+r to restart, ctrl+q to quit";
        let message_len = 33;
        let message_height = 1;

        let x_offset: u16 = (terminal_size.width / 2) - (message_len / 2);

        let rect = Rect::new(x_offset, y_offset, message_len, message_height);

        let help = Paragraph::new(message)
            .style(Style::default().fg(Color::Gray).add_modifier(Modifier::DIM));

        frame.render_widget(help, rect);
    }
}