cyndikator 0.2.2

A cli rss reader
use crate::db::Database;
use crossterm::{
    event::{read, Event},
    terminal,
};
use std::io::{stdout, Write};
use std::panic;

use draw::*;
use inter::Mode;
use raw::Raw;
use record::Cache;
use stat::State;

mod draw;
mod inter;
mod raw;
mod record;
mod stat;

pub struct View {
    db: Database,
}

pub trait Inducable<T> {
    fn induce(&mut self, elem: &T);
}

pub enum ScrollUnit {
    Line,
    Half,
    Page,
}

pub enum Position {
    Abs(u32),
    Last,
}

pub struct Indexes(Vec<u32>);

pub enum Action {
    RelUp(u16, ScrollUnit),
    RelDown(u16, ScrollUnit),
    Goto(Position),
    Delete,
    Undo,
    Open,
    Noop,
    Quit,

    StartSearch,
    SearchPreview(String),
    SetSearch(String),
    Next,
    Prev,
}

impl View {
    pub fn new(db: Database) -> View {
        View { db }
    }

    fn render(self, out: &mut impl Write) -> eyre::Result<()> {
        let mut state = State::new(terminal::size()?.1);
        let mut cache = Cache::new(self.db);
        let mut inter = inter::Inter::default();

        loop {
            let selected = state.offset();
            let entries = cache.window(state.base(), state.height() as u32)?;
            Full {
                selected,
                entries,
                status: inter.status(),
            }
            .draw(out)?;
            out.flush()?;

            let e = read()?;
            state.induce(&e);

            let action = match e {
                Event::Key(e) => inter.handle(&e)?,
                _ => {
                    state.recalc(cache.total());
                    continue;
                }
            };
            state.induce(&action);

            match action {
                Action::Open => {
                    if let Some(url) = cache.hot_load(state.abs()).and_then(|e| e.url.as_ref()) {
                        let _ = open::that_in_background(url);
                    }
                }

                Action::Delete => {
                    cache.delete(state.abs());
                }

                Action::Undo => {
                    cache.undo();
                }

                Action::Quit => break,

                Action::SetSearch(ref search) => {
                    let idx = &cache.search_indexes(search)?;
                    state.goto_next(idx);
                }

                Action::Next => {
                    if let Some(search) = state.search() {
                        let idx = &cache.search_indexes(search)?;
                        state.goto_next(idx);
                    }
                }

                Action::Prev => {
                    if let Some(search) = state.search() {
                        let idx = &cache.search_indexes(search)?;
                        state.goto_prev(idx);
                    }
                }

                _ => (),
            };

            inter.induce(&action);

            state.recalc(cache.total());
        }

        Ok(())
    }

    pub fn interact(self) -> eyre::Result<()> {
        let mut out = stdout();

        let raw = Raw::default();

        AltScreen::Enter.draw(&mut out)?;
        Clear.draw(&mut out)?;
        ShowCur(false).draw(&mut out)?;

        panic::set_hook(Box::new(|info| {
            let mut out = stdout();

            let _ = Clear.draw(&mut out);
            let _ = ShowCur(true).draw(&mut out);
            let _ = AltScreen::Exit.draw(&mut out);
            let _ = out.flush();
            let _ = terminal::disable_raw_mode();

            println!("{}", info);
        }));

        let res = self.render(&mut out);

        let _ = Clear.draw(&mut out);
        let _ = ShowCur(true).draw(&mut out);
        let _ = AltScreen::Exit.draw(&mut out)?;

        drop(raw);
        out.flush()?;

        let _ = panic::take_hook();

        res
    }
}

impl Indexes {
    fn next(&self, idx: u32) -> Option<u32> {
        for pos in &self.0 {
            if *pos > idx {
                return Some(*pos);
            }
        }

        None
    }

    fn prev(&self, idx: u32) -> Option<u32> {
        let mut res = None;
        for pos in &self.0 {
            if *pos > idx {
                return res;
            }

            if *pos < idx {
                res = Some(*pos);
            }
        }

        res
    }
}