tabiew 0.13.1

A lightweight TUI application to view and query tabular data files, such as CSV, TSV, and parquet.
use crossterm::event::{KeyCode, KeyModifiers};
use ratatui::{
    layout::{Constraint, Flex, Layout},
    symbols::{
        border::{ROUNDED, Set},
        line::{VERTICAL_LEFT, VERTICAL_RIGHT},
    },
    widgets::{Borders, Clear, List, ListItem, ListState, StatefulWidget, Widget},
};

use crate::{
    misc::config::theme,
    tui::{
        component::Component,
        widgets::{block::Block, input::Input},
    },
};

#[derive(Debug, Default)]
pub struct TextPickerWithSuggestion<P: Provider> {
    title: String,
    input: Input,
    list: ListState,
    args: (String, usize),
    items: Vec<P::Suggestion>,
    provider: P,
}

impl<P> TextPickerWithSuggestion<P>
where
    P: Provider,
{
    pub fn new(title: impl Into<String>, provider: P) -> Self {
        Self {
            title: title.into(),
            input: Default::default(),
            list: ListState::default().with_selected(0.into()),
            args: (String::default(), 0),
            items: provider.suggestions("", 0),
            provider,
        }
    }

    pub fn value(&self) -> &str {
        self.input.value()
    }

    pub fn has_suggestions(&self) -> bool {
        !self.items.is_empty()
    }

    pub fn apply_selected(&mut self) {
        if let Some(suggestion) = self.list.selected().and_then(|idx| self.items.get(idx)) {
            suggestion.apply_to(&mut self.input);
        }
    }
}

impl<P> Component for TextPickerWithSuggestion<P>
where
    P: Provider,
{
    fn render(
        &mut self,
        _area: ratatui::prelude::Rect,
        buf: &mut ratatui::prelude::Buffer,
        focus_state: crate::tui::component::FocusState,
    ) {
        if self.args.0 != self.input.value() || self.args.1 != self.input.cursor() {
            self.args = (self.input.value().to_owned(), self.input.cursor());
            self.items = self
                .provider
                .suggestions(self.input.value(), self.input.cursor());
        }

        let list = List::default()
            .style(theme().text())
            .highlight_style(theme().row_highlighted())
            .block(
                Block::default()
                    .border_set(Set {
                        top_left: VERTICAL_RIGHT,
                        top_right: VERTICAL_LEFT,
                        ..ROUNDED
                    })
                    .into_widget(),
            )
            .items(
                self.items
                    .iter()
                    .map(|suggestion| ListItem::new(suggestion.title())),
            );

        let width = 80;
        let height = list.len().saturating_add(4).min(25) as u16;

        let [area] = Layout::horizontal([Constraint::Length(width)])
            .flex(Flex::Center)
            .areas(buf.area);
        let [_, area] =
            Layout::vertical([Constraint::Length(3), Constraint::Length(height)]).areas(area);

        Widget::render(Clear, area, buf);
        let [input_area, list_area] =
            Layout::vertical([Constraint::Length(2), Constraint::Fill(1)]).areas(area);

        let input_area = {
            let block = Block::default()
                .borders(Borders::LEFT | Borders::RIGHT | Borders::TOP)
                .title(self.title.as_str());
            let input_inner = block.inner(input_area);
            Widget::render(block, area, buf);
            input_inner
        };
        self.input.render(input_area, buf, focus_state);
        StatefulWidget::render(list, list_area, buf, &mut self.list);
    }

    fn handle(&mut self, event: crossterm::event::KeyEvent) -> bool {
        self.input.handle(event)
            || match (event.code, event.modifiers) {
                (KeyCode::Up, KeyModifiers::NONE) | (KeyCode::Char('p'), KeyModifiers::CONTROL) => {
                    if self.list.selected() != Some(0) {
                        self.list.select_previous();
                    } else {
                        self.list.select(Some(self.items.len().saturating_sub(1)));
                    }
                    true
                }
                (KeyCode::Down, KeyModifiers::NONE)
                | (KeyCode::Char('n'), KeyModifiers::CONTROL) => {
                    if self.list.selected() != Some(self.items.len().saturating_sub(1)) {
                        self.list.select_next();
                    } else {
                        self.list.select_first();
                    }
                    true
                }
                _ => false,
            }
    }
}

pub trait Suggestion {
    fn title(&self) -> &str;
    fn apply_to(&self, input: &mut Input);
}

pub trait Provider {
    type Suggestion: Suggestion;
    fn suggestions(&self, query: &str, cursor: usize) -> Vec<Self::Suggestion>;
}