tabiew 0.15.1

A lightweight TUI application to view and query tabular data files, such as CSV, TSV, and parquet.
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use ratatui::{
    layout::Rect,
    text::{Line, Span},
    widgets::{Block, Paragraph, Widget},
};
use unicode_width::UnicodeWidthStr;

use crate::{
    handler::message::Message,
    misc::{buffer_ext::BufferExt, config::theme},
    tui::{
        app_default::{AppDefault, AppTitle},
        component::Component,
        icons,
        widgets::tabiew_logo::TabiewLogo,
    },
};

const PADDING_X: u16 = 2;
const PADDING_Y: u16 = 1;
const FRAME: u16 = 2;
const GAP: u16 = 1;
const REPOSITORY: &str = env!("CARGO_PKG_REPOSITORY");

#[derive(Debug, Default)]
pub struct About;

impl Component for About {
    fn render(
        &mut self,
        _area: ratatui::prelude::Rect,
        buf: &mut ratatui::prelude::Buffer,
        _focus_state: crate::tui::component::FocusState,
    ) {
        let area = buf.area;
        let label_width = entries()
            .iter()
            .map(|(label, _)| label.width())
            .max()
            .unwrap_or(0);
        let chrome_x = FRAME + PADDING_X * 2;
        let chrome_y = FRAME + PADDING_Y * 2;

        let line_width = |value: &str| label_width + 2 + value.width();
        let room = area.width.saturating_sub(chrome_x) as usize;
        let entries = entries()
            .into_iter()
            .filter(|(_, value)| line_width(value) <= room)
            .collect::<Vec<_>>();
        let info_width = entries
            .iter()
            .map(|(_, value)| line_width(value))
            .max()
            .unwrap_or(0) as u16;
        let info_height = entries.len() as u16;
        let gap = if entries.is_empty() { 0 } else { GAP };

        let available = Rect {
            width: area.width.saturating_sub(chrome_x),
            height: area.height.saturating_sub(chrome_y + info_height + gap),
            ..area
        };
        let Some((logo_width, logo_height)) = TabiewLogo::size(available) else {
            return;
        };
        let content_width = logo_width.max(info_width);
        let content_height = logo_height + gap + info_height;
        let width = content_width + chrome_x;
        let height = content_height + chrome_y;
        if width > area.width || height > area.height {
            return;
        }

        let area = Rect {
            x: area.x + (area.width - width) / 2,
            y: area.y + (area.height - height) / 2,
            width,
            height,
        };
        buf.clear(area);
        let block = Block::app_default().app_title(icons::INFO.title("About"));
        let inner = block.inner(area);
        block.render(area, buf);

        let content = Rect {
            x: inner.x + PADDING_X,
            y: inner.y + PADDING_Y,
            width: content_width,
            height: content_height,
        };
        TabiewLogo.render(
            Rect {
                height: logo_height,
                ..content
            },
            buf,
        );
        Paragraph::new(
            entries
                .into_iter()
                .map(|(label, value)| {
                    Line::from(vec![
                        Span::styled(format!("{label:<label_width$}  "), theme().subtext()),
                        Span::styled(value, theme().text()),
                    ])
                })
                .collect::<Vec<_>>(),
        )
        .style(theme().text())
        .render(
            Rect {
                x: content.x + (content_width - info_width) / 2,
                y: content.y + logo_height + gap,
                width: info_width,
                height: info_height,
            },
            buf,
        );
    }

    fn handle(&mut self, event: KeyEvent) -> bool {
        match (event.code, event.modifiers) {
            (KeyCode::Esc, KeyModifiers::NONE) | (KeyCode::Char('q'), KeyModifiers::NONE) => {
                Message::AppDismissOverlay.enqueue();
                true
            }
            _ => false,
        }
    }
}

fn entries() -> Vec<(String, String)> {
    vec![
        (
            icons::TAG.title("Version"),
            env!("CARGO_PKG_VERSION").to_owned(),
        ),
        (icons::GITHUB.title("GitHub"), REPOSITORY.to_owned()),
        (icons::BOOK.title("Wiki"), format!("{REPOSITORY}/wiki")),
    ]
}