tabiew 0.16.0

A lightweight TUI application to view and query tabular data files, such as CSV, TSV, and parquet.
use std::{
    cmp::Reverse,
    collections::{BTreeSet, HashMap},
    fmt::Debug,
    sync::{
        Arc, Mutex,
        atomic::{AtomicBool, Ordering},
        mpsc::{Receiver, Sender, TryRecvError, channel},
    },
    time::{Duration, Instant},
};

use fuzzy_matcher::{FuzzyMatcher, skim::SkimMatcherV2};
use polars::{frame::DataFrame, prelude::IdxCa};

use rayon::prelude::*;

use crate::{
    misc::unwrap_or_graceful_shutdown::UnwrapOrGracefulShutdown,
    tui::misc::any_value_formatter::AnyValueFormatter,
};

type RowIndex = u32;
type SimScore = i64;

#[derive(Debug)]
pub struct Searcher {
    pat: String,
    sync_df: SyncDataFrame,
    alive: Arc<AtomicBool>,
}

impl Searcher {
    pub fn exact(df: DataFrame, pat: String) -> Self {
        Self::new::<Exact>(df, pat)
    }

    pub fn fuzzy(df: DataFrame, pat: String) -> Self {
        Self::new::<Skim>(df, pat)
    }

    fn new<S>(df: DataFrame, pat: String) -> Self
    where
        S: Score + Default + Sync + Send + 'static,
    {
        let sync_df = SyncDataFrame::new();
        let alive = Arc::new(AtomicBool::new(true));
        if pat.is_empty() {
            sync_df.insert(df);
        } else {
            sync_df.insert(df.clear());
            let (tx, rx) = channel();
            spawn_search_thread::<S>(df.clone(), pat.to_owned(), alive.clone(), tx);
            spawn_collector_thread(df, rx, sync_df.clone());
        }
        Self {
            sync_df,
            alive,
            pat,
        }
    }

    pub fn latest(&self) -> Option<DataFrame> {
        self.sync_df.take()
    }

    pub fn pattern(&self) -> &str {
        &self.pat
    }
}

impl Drop for Searcher {
    fn drop(&mut self) {
        self.alive.store(false, Ordering::Relaxed);
    }
}

trait Score {
    fn score(&self, a: &str, b: &str) -> Option<i64>;
}

#[derive(Default)]
struct Skim {
    matcher: SkimMatcherV2,
}

impl Score for Skim {
    fn score(&self, a: &str, b: &str) -> Option<i64> {
        self.matcher.fuzzy_match(a, b)
    }
}

#[derive(Default)]
struct Exact;

impl Score for Exact {
    fn score(&self, a: &str, b: &str) -> Option<i64> {
        a.contains(b).then_some(1)
    }
}

#[derive(Debug, Clone)]
struct SyncDataFrame(Arc<Mutex<Option<DataFrame>>>);

impl SyncDataFrame {
    fn new() -> Self {
        Self(Arc::new(Mutex::new(None)))
    }

    fn insert(&self, df: DataFrame) {
        if let Ok(mut mut_grd) = self.0.lock() {
            *mut_grd = Some(df);
        }
    }

    fn take(&self) -> Option<DataFrame> {
        self.0.lock().ok().and_then(|mut mut_grd| mut_grd.take())
    }
}

#[derive(Debug)]
struct ConnectionAware<T> {
    connected: bool,
    recv: Receiver<T>,
}

impl<T> ConnectionAware<T> {
    fn new(recv: Receiver<T>) -> Self {
        ConnectionAware {
            connected: true,
            recv,
        }
    }

    fn connected(&self) -> bool {
        self.connected
    }
}

impl<T> Iterator for ConnectionAware<T> {
    type Item = T;

    fn next(&mut self) -> Option<Self::Item> {
        match self.recv.try_recv() {
            Ok(v) => Some(v),
            Err(TryRecvError::Empty) => None,
            Err(TryRecvError::Disconnected) => {
                self.connected = false;
                None
            }
        }
    }
}

#[derive(Debug)]
struct Interval {
    tick_rate: Duration,
    last_tick: Instant,
}

impl Interval {
    pub fn new(tick_rate: Duration) -> Self {
        Self {
            tick_rate,
            last_tick: Instant::now(),
        }
    }

    pub fn sleep(&mut self) {
        std::thread::sleep(self.tick_rate.saturating_sub(self.last_tick.elapsed()));
        self.last_tick = Instant::now();
    }
}

#[derive(Debug, Default)]
struct Scores {
    map: HashMap<RowIndex, SimScore>,
    bts: BTreeSet<(Reverse<SimScore>, RowIndex)>,
}

impl Scores {
    fn insert(&mut self, row: RowIndex, score: SimScore) {
        self.map
            .entry(row)
            .and_modify(|old_score| {
                if *old_score < score {
                    self.bts.remove(&(Reverse(*old_score), row));
                    *old_score = score;
                    self.bts.insert((Reverse(score), row));
                }
            })
            .or_insert_with(|| {
                self.bts.insert((Reverse(score), row));
                score
            });
    }

    fn indices(&self) -> impl Iterator<Item = RowIndex> {
        self.bts.iter().map(|(_, idx)| *idx)
    }
}

fn spawn_search_thread<S>(
    df: DataFrame,
    pat: String,
    alive: Arc<AtomicBool>,
    tx: Sender<(RowIndex, SimScore)>,
) where
    S: Score + Default + Sync,
{
    std::thread::spawn(move || {
        let matcher = S::default();
        let _ = df
            .columns()
            .iter()
            .flat_map(|column| column.as_materialized_series().iter().enumerate())
            .par_bridge()
            .take_any_while(|_| alive.load(Ordering::Relaxed))
            .filter_map(|(idx, value)| {
                let value = AnyValueFormatter::new(None).into_multi_line(value);
                if value == pat {
                    Some((idx, i64::MAX))
                } else {
                    matcher.score(&value, &pat).map(|score| (idx, score))
                }
            })
            .try_for_each(|(idx, score)| tx.send((idx as u32, score)));
    });
}

fn spawn_collector_thread(
    df: DataFrame,
    rx: Receiver<(RowIndex, SimScore)>,
    sync_df: SyncDataFrame,
) {
    std::thread::spawn(move || {
        let mut interval = Interval::new(Duration::from_millis(100));
        let mut scores = Scores::default();
        let mut recv = ConnectionAware::new(rx);
        let mut updated = false;
        while recv.connected() {
            let mut should_update = false;
            for (idx, new_score) in recv.by_ref() {
                should_update = true;
                scores.insert(idx, new_score);
            }

            if should_update {
                sync_df.insert(
                    df.take(&IdxCa::new_vec("name".into(), scores.indices().collect()))
                        .unwrap_or_default(),
                );
                updated = true;
            }
            interval.sleep();
        }
        if !updated {
            sync_df.insert(
                df.take(&IdxCa::new_vec("name".into(), scores.indices().collect()))
                    .unwrap_or_graceful_shutdown(),
            );
        }
    });
}