tabiew 0.14.0

A lightweight TUI application to view and query tabular data files, such as CSV, TSV, and parquet.
use std::default::Default;
use std::{borrow::Cow, ops::Add};

use ratatui::{
    layout::{Constraint, Layout},
    style::Modifier,
    text::{Line, Span},
    widgets::Widget,
};
use unicode_width::UnicodeWidthStr;

use crate::{
    misc::config::theme,
    tui::{Pane, pane::TableDescription},
};

pub struct StatusBar<'a> {
    pane: &'a Pane,
    sel_tab: usize,
    tot_tab: usize,
}

impl<'a> StatusBar<'a> {
    pub fn new(pane: &'a Pane, selected_tab: usize, total_tabs: usize) -> Self {
        Self {
            pane,
            sel_tab: selected_tab,
            tot_tab: total_tabs,
        }
    }
}

impl<'a> Widget for StatusBar<'a> {
    fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer)
    where
        Self: Sized,
    {
        let tab_tag = Tag::new(
            "Tab",
            format!(
                "{:>width$} / {}",
                self.sel_tab.add(1),
                self.tot_tab,
                width = self.tot_tab.to_string().len()
            ),
            1,
        );
        let row_tag = Tag::new(
            "Row",
            format!(
                "{:>width$}",
                self.pane.table().selected().unwrap_or_default().add(1),
                width = self.pane.table().data_frame().height().to_string().len(),
            ),
            2,
        );
        let shp_tag = Tag::new(
            "Shape",
            format!(
                "{} x {}",
                self.pane.table().data_frame().height(),
                self.pane.table().data_frame().width()
            ),
            3,
        );
        let [history_area, tab_area, row_area, shp_area] = Layout::horizontal([
            Constraint::Fill(3),
            Constraint::Length(tab_tag.width()),
            Constraint::Length(row_tag.width()),
            Constraint::Length(shp_tag.width()),
        ])
        .spacing(1)
        .areas(area);

        tab_tag.line().render(tab_area, buf);
        row_tag.line().render(row_area, buf);
        shp_tag.line().render(shp_area, buf);

        if let Some(history) = History::new(self.pane.iter_descriptions()) {
            history
                .fitted(history_area.width)
                .line()
                .render(history_area, buf);
        }
    }
}

const SEPARATOR: &str = "\u{25B6}";
const SUMMARIZED: &str = " ... ";

struct History<'a> {
    first: FirstHistoryItem<'a>,
    nexts: Vec<NextHistoryItem<'a>>,
    shrinked: bool,
}

impl<'a> History<'a> {
    fn new(t: impl IntoIterator<Item = &'a TableDescription>) -> Option<Self> {
        let mut iter = t.into_iter();
        let first = iter.next()?;
        Some(Self {
            first: FirstHistoryItem::new(first),
            nexts: iter.map(NextHistoryItem::new).collect(),
            shrinked: false,
        })
    }

    fn required_width(&self) -> u16 {
        let first = self.first.width();
        let nexts = self
            .nexts
            .iter()
            .map(|n| n.width() + SEPARATOR.width() as u16)
            .sum::<u16>();

        if self.shrinked {
            first + nexts + (SUMMARIZED.width() + SEPARATOR.width()) as u16
        } else {
            first + nexts
        }
    }

    fn shrink(&mut self) {
        if self.can_shrink() {
            self.nexts.remove(0);
            self.shrinked = true;
        }
    }

    fn can_shrink(&self) -> bool {
        self.nexts.len() > 1
    }

    fn fitted(mut self, width: u16) -> Self {
        while width < self.required_width() && self.can_shrink() {
            self.shrink();
        }
        self
    }

    fn line(self) -> Line<'a> {
        let mut spans = self.first.spans().collect::<Vec<_>>();
        if self.shrinked {
            spans.extend([
                Span::styled(SEPARATOR, theme().tag(0).reversed()),
                Span::styled(SUMMARIZED, theme().tag(0).reversed()),
            ]);
        }
        for item in self.nexts {
            spans.push(Span::styled(SEPARATOR, theme().tag(0).reversed()));
            spans.extend(item.spans());
        }

        Line::default().spans(spans)
    }
}

struct FirstHistoryItem<'a> {
    td: &'a TableDescription,
}

impl<'a> FirstHistoryItem<'a> {
    fn new(td: &'a TableDescription) -> Self {
        Self { td }
    }

    fn width(&self) -> u16 {
        (4 + self.td.variant().width() + self.td.description().width()) as u16
    }

    fn spans(&self) -> impl Iterator<Item = Span<'a>> {
        [
            Span::styled(" ", theme().tag(0)),
            Span::styled(self.td.variant(), theme().tag(0)),
            Span::styled(" ", theme().tag(0)),
            Span::styled(" ", theme().tag(0).add_modifier(Modifier::REVERSED)),
            Span::styled(
                self.td.description().trim(),
                theme().tag(0).add_modifier(Modifier::REVERSED),
            ),
            Span::styled(" ", theme().tag(0).add_modifier(Modifier::REVERSED)),
        ]
        .into_iter()
    }
}

struct NextHistoryItem<'a> {
    td: &'a TableDescription,
}

impl<'a> NextHistoryItem<'a> {
    fn new(td: &'a TableDescription) -> Self {
        Self { td }
    }

    fn width(&self) -> u16 {
        (4 + self.td.variant().width() + self.td.description().width()) as u16
    }

    fn spans(&self) -> impl Iterator<Item = Span<'a>> {
        [
            Span::styled(" ", theme().tag(0).reversed()),
            Span::styled(self.td.variant(), theme().tag(0).reversed()),
            Span::styled("[", theme().tag(0).reversed()),
            Span::styled(self.td.description().trim(), theme().tag(0).reversed()),
            Span::styled("] ", theme().tag(0).reversed()),
        ]
        .into_iter()
    }
}

struct Tag<'a> {
    key: Cow<'a, str>,
    val: Cow<'a, str>,
    pos: usize,
}

impl<'a> Tag<'a> {
    fn new(key: impl Into<Cow<'a, str>>, val: impl Into<Cow<'a, str>>, pos: usize) -> Self {
        Self {
            key: key.into(),
            val: val.into(),
            pos,
        }
    }

    fn line(&self) -> Line<'_> {
        Line::default().spans([
            Span::styled(" ", theme().tag(self.pos)),
            Span::styled(self.key.as_ref(), theme().tag(self.pos)),
            Span::styled(" ", theme().tag(self.pos)),
            Span::styled(" ", theme().tag(self.pos).add_modifier(Modifier::REVERSED)),
            Span::styled(
                self.val.as_ref(),
                theme().tag(self.pos).add_modifier(Modifier::REVERSED),
            ),
            Span::styled(" ", theme().tag(self.pos).add_modifier(Modifier::REVERSED)),
        ])
    }
    fn width(&self) -> u16 {
        (4 + self.key.width() + self.val.width()) as u16
    }
}