tabiew 0.15.1

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::{Color, Modifier, Style},
    text::{Line, Span},
    widgets::Widget,
};
use unicode_width::UnicodeWidthStr;

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

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

impl<'a> StatusBar<'a> {
    pub fn new(pane: &'a Pane) -> Self {
        Self { pane }
    }
}

impl<'a> Widget for StatusBar<'a> {
    fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer)
    where
        Self: Sized,
    {
        let row_tag = (!self.pane.table().gutter_visible()).then(|| {
            Tag::new(
                icons::ROW_NUMBERS.str("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(
            icons::GRID.str("Shape"),
            format!(
                "{} x {}",
                self.pane.table().data_frame().height(),
                self.pane.table().data_frame().width()
            ),
            3,
        );
        let tags = row_tag.into_iter().chain([shp_tag]).collect::<Vec<_>>();
        let areas = Layout::horizontal(
            std::iter::once(Constraint::Fill(3))
                .chain(tags.iter().map(|tag| Constraint::Length(tag.width()))),
        )
        .spacing(1)
        .split(area);

        for (tag, area) in tags.iter().zip(areas.iter().skip(1)) {
            tag.line().render(*area, buf);
        }

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

fn summarized() -> &'static str {
    icons::ELLIPSIS.str("...")
}

fn separator() -> &'static str {
    icons::WEDGE.str("\u{25B6}")
}

struct History<'a> {
    first: HistoryItem<'a>,
    sibls: Vec<HistoryItem<'a>>,
    shrinked: bool,
}

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

    fn required_width(&self) -> u16 {
        let separator = separator().width() as u16;
        let items = std::iter::once(&self.first)
            .chain(self.sibls.iter())
            .map(|item| item.width())
            .sum::<u16>();
        let separators = self.sibls.len() as u16 * separator;

        if self.shrinked {
            items + separators + summarized().width() as u16 + 2 + separator
        } else {
            items + separators
        }
    }

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

    fn can_shrink(&self) -> bool {
        self.sibls.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 prev = self.first.tail_style();
        let mut spans = self.first.spans().collect::<Vec<_>>();

        if self.shrinked {
            let style = theme().tag(1);
            spans.push(chevron(prev, style));
            spans.push(Span::styled(format!(" {} ", summarized()), style));
            prev = style;
        }

        for item in self.sibls {
            spans.push(chevron(prev, item.head_style()));
            prev = item.tail_style();
            spans.extend(item.spans());
        }

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

fn reversed(style: Style) -> Style {
    Style::default()
        .fg(style.bg.unwrap_or(Color::Reset))
        .bg(style.fg.unwrap_or(Color::Reset))
}

fn chevron(left: Style, right: Style) -> Span<'static> {
    Span::styled(
        separator(),
        Style::default()
            .fg(left.bg.unwrap_or(Color::Reset))
            .bg(right.bg.unwrap_or(Color::Reset)),
    )
}

struct HistoryItem<'a> {
    td: &'a TableDescription,
    pos: usize,
}

impl<'a> HistoryItem<'a> {
    fn new(td: &'a TableDescription, pos: usize) -> Self {
        Self { td, pos }
    }

    fn head_style(&self) -> Style {
        theme().tag(self.pos)
    }

    fn tail_style(&self) -> Style {
        if self.td.description().trim().is_empty() {
            self.head_style()
        } else {
            reversed(self.head_style())
        }
    }

    fn label(&self) -> &'static str {
        self.td.icon().str(self.td.variant())
    }

    fn width(&self) -> u16 {
        let description = self.td.description().trim();
        if description.is_empty() {
            (2 + self.label().width()) as u16
        } else {
            (4 + self.label().width() + description.width()) as u16
        }
    }

    fn spans(&self) -> impl Iterator<Item = Span<'a>> {
        let head = self.head_style();
        let tail = self.tail_style();
        let description = self.td.description().trim();
        [
            Some(Span::styled(" ", head)),
            Some(Span::styled(self.label(), head)),
            Some(Span::styled(" ", head)),
            (!description.is_empty()).then(|| Span::styled(" ", tail)),
            (!description.is_empty()).then(|| Span::styled(description, tail)),
            (!description.is_empty()).then(|| Span::styled(" ", tail)),
        ]
        .into_iter()
        .flatten()
    }
}

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
    }
}