sara-tasks 2.7.9

Sara — folder-aware task manager
use crossterm::event::KeyCode;
use ratatui::Frame;
use rusqlite::Connection;

use super::{Flow, Screen, cycle, step_direction};
use crate::commands::dream::layout::nearest_in_direction;
use crate::commands::dream::load::open;
use crate::commands::dream::render;
use crate::commands::dream::types::{Dir, DreamData, Star, WebData};

const MAX_ZOOM: f64 = 6.0;
const ZOOM_STEP: f64 = 1.25;

pub(in crate::commands::dream) struct WebScreen {
    web: WebData,
    frame: u64,
    selected: usize,
    show_help: bool,
    dream: Option<DreamData>,
    dream_frame: u64,
    dream_selected: usize,
    zoom: f64,
    search_input: Option<String>,
    query: String,
}

impl WebScreen {
    pub(in crate::commands::dream) fn new(web: WebData) -> Self {
        Self {
            web,
            frame: 0,
            selected: 0,
            show_help: false,
            dream: None,
            dream_frame: 0,
            dream_selected: 0,
            zoom: 1.0,
            search_input: None,
            query: String::new(),
        }
    }

    fn jump(&self, back: bool) -> usize {
        jump_to_match(self.selected, &self.query, &self.web.stars, back)
    }

    fn on_search_key(&mut self, code: KeyCode) {
        let Some(buf) = self.search_input.as_mut() else {
            return;
        };
        match code {
            KeyCode::Esc => {
                self.search_input = None;
                self.query.clear();
            }
            KeyCode::Enter => {
                self.query = buf.trim().to_lowercase();
                self.search_input = None;
                self.selected = self.jump(false);
            }
            KeyCode::Backspace => {
                if buf.pop().is_none() {
                    self.search_input = None;
                }
            }
            KeyCode::Char(c) => buf.push(c),
            _ => {}
        }
    }

    fn navigate(&mut self, code: KeyCode, forward: bool) {
        if let Some(d) = &self.dream {
            self.dream_selected = cycle(self.dream_selected, d.neighbors.len(), forward);
            return;
        }
        let stars = &self.web.stars;
        self.selected = match code {
            KeyCode::Right => nearest_in_direction(stars, self.selected, Dir::Right),
            KeyCode::Down => nearest_in_direction(stars, self.selected, Dir::Down),
            KeyCode::Left => nearest_in_direction(stars, self.selected, Dir::Left),
            KeyCode::Up => nearest_in_direction(stars, self.selected, Dir::Up),
            _ => cycle(self.selected, stars.len(), forward),
        };
    }

    fn on_web_key(&mut self, code: KeyCode) -> bool {
        match code {
            KeyCode::Char('/') => self.search_input = Some(String::new()),
            KeyCode::Char('n') => self.selected = self.jump(false),
            KeyCode::Char('N') => self.selected = self.jump(true),
            KeyCode::Char('+') | KeyCode::Char('=') => {
                self.zoom = (self.zoom * ZOOM_STEP).min(MAX_ZOOM)
            }
            KeyCode::Char('-') => self.zoom = (self.zoom / ZOOM_STEP).max(1.0),
            KeyCode::Char('0') => self.zoom = 1.0,
            _ => return false,
        }
        true
    }

    fn back(&mut self) -> Flow {
        if self.show_help {
            self.show_help = false;
        } else if self.dream.is_some() {
            self.dream = None;
        } else if !self.query.is_empty() {
            self.query.clear();
        } else if self.zoom > 1.0 {
            self.zoom = 1.0;
        } else {
            return Flow::Quit;
        }
        Flow::Continue
    }

    fn dive(&mut self, conn: &Connection) {
        let target = match &self.dream {
            Some(d) => d.memory_label_at(self.dream_selected),
            None => Some(self.web.stars[self.selected].label.as_str()),
        };
        if let Some(t) = target
            && let Ok(d) = open(conn, t)
        {
            self.dream = Some(d);
            self.dream_frame = 0;
            self.dream_selected = 0;
        }
    }
}

impl Screen for WebScreen {
    fn draw(&self, f: &mut Frame) {
        match &self.dream {
            Some(d) => render::ui(
                f,
                d,
                self.dream_frame,
                self.dream_selected,
                &[self.web.stars[self.selected].label.clone()],
                self.show_help,
            ),
            None => render::ui_web(
                f,
                &self.web,
                self.frame,
                self.selected,
                self.show_help,
                self.zoom,
                self.search_input.as_deref(),
                &self.query,
            ),
        }
    }

    fn tick(&mut self) {
        self.frame += 1;
        self.dream_frame += 1;
    }

    fn on_key(&mut self, conn: &Connection, code: KeyCode) -> Flow {
        if self.dream.is_none() && self.search_input.is_some() {
            self.on_search_key(code);
            return Flow::Continue;
        }
        if self.dream.is_none() && self.on_web_key(code) {
            return Flow::Continue;
        }
        if let Some(forward) = step_direction(code) {
            self.navigate(code, forward);
            return Flow::Continue;
        }
        match code {
            KeyCode::Char('q') => return Flow::Quit,
            KeyCode::Char('?') => self.show_help = !self.show_help,
            KeyCode::Esc | KeyCode::Backspace => return self.back(),
            KeyCode::Enter => self.dive(conn),
            _ => {}
        }
        Flow::Continue
    }
}

fn jump_to_match(selected: usize, query: &str, stars: &[Star], back: bool) -> usize {
    if query.is_empty() {
        return selected;
    }
    let n = stars.len();
    (1..=n)
        .map(|step| cycle_by(selected, n, step, back))
        .find(|&i| stars[i].matches(query))
        .unwrap_or(selected)
}

fn cycle_by(selected: usize, n: usize, step: usize, back: bool) -> usize {
    if back {
        (selected + n - step) % n
    } else {
        (selected + step) % n
    }
}