tabiew 0.13.1

A lightweight TUI application to view and query tabular data files, such as CSV, TSV, and parquet.
use std::ops::Div;

use crossterm::event::{KeyCode, KeyModifiers};
use ratatui::{
    layout::{Constraint, Rect},
    widgets::{Cell, Clear, Row, StatefulWidget, Table, TableState, Widget},
};
use unicode_width::UnicodeWidthStr;

use crate::{
    handler::message::Message,
    misc::config::theme,
    tui::{component::Component, widgets::block::Block},
};

#[derive(Debug)]
pub struct TabSwitcher {
    tabs: Vec<String>,
    title: String,
    list_state: TableState,
    rollback: usize,
}

impl TabSwitcher {
    pub fn new(title: impl Into<String>, tabs: Vec<String>, idx: usize) -> TabSwitcher {
        Self {
            list_state: TableState::default().with_selected(idx),
            tabs,
            title: title.into(),
            rollback: idx,
        }
    }

    pub fn selected(&self) -> Option<usize> {
        self.list_state.selected()
    }

    pub fn select(&mut self, idx: impl Into<Option<usize>>) {
        self.list_state.select(idx.into());
    }

    pub fn select_prev(&mut self) {
        self.list_state.select_previous();
    }

    pub fn select_next(&mut self) {
        let idx = self
            .list_state
            .selected()
            .unwrap_or_default()
            .saturating_add(1)
            .min(self.tabs.len().saturating_sub(1));
        self.list_state.select(Some(idx));
    }

    pub fn select_first(&mut self) {
        self.list_state.select(Some(0));
    }
    pub fn select_last(&mut self) {
        self.list_state
            .select(Some(self.tabs.len().saturating_sub(1)));
    }
}

impl Component for TabSwitcher {
    fn render(
        &mut self,
        area: Rect,
        buf: &mut ratatui::prelude::Buffer,
        focus_state: super::component::FocusState,
    ) {
        let num_width = (self.tabs.len().checked_ilog10().unwrap_or_default() + 1) as u16;
        let text_width = self
            .tabs
            .iter()
            .map(|s| s.width() as u16)
            .max()
            .unwrap_or_default()
            .max(34)
            .min(area.width.div(2));
        let width = num_width + text_width + 3;
        let area = Rect::new(
            area.x.saturating_add(area.width).saturating_sub(width),
            area.y,
            width,
            area.height,
        );

        Widget::render(Clear, area, buf);

        let rows = self.tabs.iter().enumerate().map(|(i, s)| {
            Row::new([
                Cell::new(format!(" {:>width$}", i + 1, width = num_width as usize))
                    .style(theme().subtext()),
                Cell::new(s.as_str()).style(theme().text()),
            ])
        });
        let table = Table::default()
            .rows(rows)
            .style(theme().text())
            .row_highlight_style(theme().row_highlighted())
            .widths([
                Constraint::Length(num_width + 1),
                Constraint::Length(text_width),
            ])
            .column_spacing(1)
            .block(Block::default().title(self.title.as_str()).into_widget());
        if focus_state.is_focused() {
            StatefulWidget::render(table, area, buf, &mut self.list_state);
        } else {
            StatefulWidget::render(table, area, buf, &mut self.list_state.with_selected(None));
        }
    }

    fn handle(&mut self, event: crossterm::event::KeyEvent) -> bool {
        match (event.code, event.modifiers) {
            (KeyCode::Up, KeyModifiers::NONE)
            | (KeyCode::Char('k'), KeyModifiers::NONE)
            | (KeyCode::Char('p'), KeyModifiers::CONTROL) => {
                self.select_prev();
                if let Some(select) = self.list_state.selected() {
                    Message::TabsSelect(select).enqueue();
                }
                true
            }
            (KeyCode::Down, KeyModifiers::NONE)
            | (KeyCode::Char('j'), KeyModifiers::NONE)
            | (KeyCode::Char('n'), KeyModifiers::CONTROL) => {
                self.select_next();
                if let Some(select) = self.list_state.selected() {
                    Message::TabsSelect(select).enqueue();
                }
                true
            }
            (KeyCode::Home, KeyModifiers::NONE) | (KeyCode::Char('g'), KeyModifiers::NONE) => {
                self.select_first();
                if let Some(select) = self.list_state.selected() {
                    Message::TabsSelect(select).enqueue();
                }
                true
            }
            (KeyCode::End, KeyModifiers::NONE) | (KeyCode::Char('G'), KeyModifiers::SHIFT) => {
                self.select_last();
                if let Some(select) = self.list_state.selected() {
                    Message::TabsSelect(select).enqueue();
                }
                true
            }
            (KeyCode::Enter, KeyModifiers::NONE) => {
                Message::TabsDismissSwitcher.enqueue();
                true
            }
            (KeyCode::Esc, KeyModifiers::NONE)
            | (KeyCode::Char('q'), KeyModifiers::NONE)
            | (KeyCode::Char('t'), KeyModifiers::NONE) => {
                Message::TabsDismissSwitcher.enqueue();
                Message::TabsSelect(self.rollback).enqueue();
                true
            }
            _ => false,
        }
    }
}