void-focus 0.3.0-alpha.4

A feature-rich terminal focus timer with task tracking
Documentation
use super::*;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};

impl App {
    pub fn handle_key(&mut self, key: KeyEvent) {
        if self.searching {
            self.handle_search_key(key);
            return;
        }
        if self.popup.is_some() {
            self.handle_popup_key(key);
            return;
        }
        let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
        match key.code {
            KeyCode::Char('q') => self.should_quit = true,
            KeyCode::Esc => self.should_quit = true,
            KeyCode::Char('c') if ctrl => self.should_quit = true,
            KeyCode::Char('s') if ctrl => self.export_backup(),
            KeyCode::Char('1') => self.tab = FocusTab::Dashboard,
            KeyCode::Char('2') => self.tab = FocusTab::Tasks,
            KeyCode::Char('3') => self.tab = FocusTab::Stats,
            KeyCode::Char('4') => self.tab = FocusTab::Settings,
            KeyCode::Char('5') | KeyCode::Char('h') => self.tab = FocusTab::Help,
            KeyCode::Tab => self.next_tab(),
            KeyCode::BackTab => self.prev_tab(),
            _ => match self.tab {
                FocusTab::Dashboard => self.handle_dashboard_key(key),
                FocusTab::Tasks => self.handle_tasks_key(key),
                FocusTab::Stats => self.handle_stats_key(key),
                FocusTab::Settings => self.handle_settings_key(key),
                FocusTab::Help => {}
            },
        }
    }

    pub(crate) fn next_tab(&mut self) {
        let cur = FocusTab::all()
            .iter()
            .position(|t| *t == self.tab)
            .unwrap_or(0);
        self.tab = FocusTab::all()[(cur + 1) % FocusTab::all().len()];
    }

    pub(crate) fn prev_tab(&mut self) {
        let cur = FocusTab::all()
            .iter()
            .position(|t| *t == self.tab)
            .unwrap_or(0);
        let n = FocusTab::all().len();
        self.tab = FocusTab::all()[(cur + n - 1) % n];
    }

    pub(crate) fn handle_dashboard_key(&mut self, key: KeyEvent) {
        match key.code {
            KeyCode::Char('s') | KeyCode::Char(' ') => self.toggle_timer(),
            KeyCode::Char('p') => self.pause_timer(),
            KeyCode::Char('r') => self.reset_timer(),
            KeyCode::Char('n') => {
                self.timer.skip();
                self.on_timer_finished(true);
            }
            KeyCode::Char('m') => self.cycle_mode(),
            KeyCode::Char('+') | KeyCode::Char('=') => self.adjust_minutes(1),
            KeyCode::Char('-') | KeyCode::Char('_') => self.adjust_minutes(-1),
            KeyCode::Char('a') => self.open_add_task(),
            KeyCode::Char('f') => {
                if let Some(id) = self.dashboard_selected_task_id() {
                    self.set_active_task(Some(id));
                    self.set_status("Task set as active.", false);
                }
            }
            KeyCode::Char('g') => {
                self.cycle_task_filter();
            }
            KeyCode::Char('t') => {
                self.cycle_tag_filter();
            }
            KeyCode::Char('z') => {
                self.zen_mode = !self.zen_mode;
                self.set_status(
                    format!("Zen mode {}.", if self.zen_mode { "on" } else { "off" }),
                    false,
                );
            }
            KeyCode::Down | KeyCode::Char('j') => self.move_dashboard_task_selection(1),
            KeyCode::Up | KeyCode::Char('k') => self.move_dashboard_task_selection(-1),
            KeyCode::Enter => {
                if let Some(id) = self.dashboard_selected_task_id() {
                    self.set_active_task(Some(id));
                    self.persist_data(|db, data| storage::cycle_task_status(db, data, id));
                    let status = self
                        .data
                        .tasks
                        .iter()
                        .find(|t| t.id == id)
                        .map(|t| t.status);
                    if let Some(status) = status {
                        if status == crate::model::TaskStatus::Done {
                            self.active_task = None;
                            self.data.active_task_id = None;
                            self.persist(|db| db.persist_active_task(None));
                            self.maybe_advance_task();
                            self.check_queue_empty();
                        }
                        self.bump_data();
                        self.clamp_dashboard_task_selection();
                        self.set_status(format!("Task status: {}", status.label()), false);
                    }
                } else {
                    self.cycle_active_task_status();
                }
            }
            KeyCode::Char('x') => {
                if let Some(id) = self.dashboard_selected_task_id() {
                    self.persist_data(|db, data| storage::mark_task_done(db, data, id));
                    if self.active_task == Some(id) {
                        self.active_task = None;
                        self.data.active_task_id = None;
                        self.persist(|db| db.persist_active_task(None));
                        self.maybe_advance_task();
                    }
                    self.bump_data();
                    self.clamp_dashboard_task_selection();
                    self.check_queue_empty();
                    self.set_status("Task marked done.", false);
                } else {
                    self.mark_active_task_done();
                }
            }
            KeyCode::Char('e') | KeyCode::Char('E') => self.end_session(),
            _ => {}
        }
    }

    pub(crate) fn handle_stats_key(&mut self, key: KeyEvent) {
        if self.recent_sessions.is_empty() {
            if matches!(key.code, KeyCode::Char('e') | KeyCode::Char('E')) {
                self.end_session();
            }
            return;
        }
        let n = self.recent_sessions.len();
        match key.code {
            KeyCode::Down | KeyCode::Char('j') => {
                self.stats_session_selected = (self.stats_session_selected + 1) % n;
            }
            KeyCode::Up | KeyCode::Char('k') => {
                self.stats_session_selected = if self.stats_session_selected == 0 {
                    n - 1
                } else {
                    self.stats_session_selected - 1
                };
            }
            KeyCode::Char('d') => {
                let id = self.recent_sessions[self.stats_session_selected].id;
                self.persist_data(|db, data| storage::delete_session(db, data, id));
                self.bump_data();
                self.set_status("Session deleted.", false);
            }
            KeyCode::Char('+') | KeyCode::Char('=') => {
                let entry = &self.recent_sessions[self.stats_session_selected];
                let new_mins = entry.record.minutes.saturating_add(5);
                let id = entry.id;
                self.persist_data(|db, data| {
                    storage::adjust_session_minutes(db, data, id, new_mins)
                });
                self.bump_data();
            }
            KeyCode::Char('-') => {
                let entry = &self.recent_sessions[self.stats_session_selected];
                let new_mins = entry.record.minutes.saturating_sub(5).max(1);
                let id = entry.id;
                self.persist_data(|db, data| {
                    storage::adjust_session_minutes(db, data, id, new_mins)
                });
                self.bump_data();
            }
            KeyCode::Char('e') | KeyCode::Char('E') => self.end_session(),
            _ => {}
        }
    }

    pub(crate) fn handle_search_key(&mut self, key: KeyEvent) {
        match key.code {
            KeyCode::Esc => {
                self.searching = false;
                self.task_search.clear();
            }
            KeyCode::Enter => {
                self.searching = false;
            }
            KeyCode::Backspace => {
                self.task_search.pop();
            }
            KeyCode::Char(c) => {
                self.task_search.push(c);
            }
            _ => {}
        }
    }

    pub(crate) fn handle_tasks_key(&mut self, key: KeyEvent) {
        let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
        match key.code {
            KeyCode::Char('f') => {
                if let Some(id) = self.selected_task_id() {
                    self.start_focus_on_task(id);
                }
            }
            KeyCode::Char('g') => {
                self.cycle_task_filter();
            }
            KeyCode::Char('/') => {
                self.searching = true;
                self.task_search.clear();
            }
            KeyCode::Char('t') => {
                if let Some(id) = self.selected_task_id() {
                    self.persist_data(|db, data| storage::toggle_today(db, data, id));
                    self.bump_data();
                }
            }
            KeyCode::Char('a') => self.open_add_task(),
            KeyCode::Char('e') => self.open_edit_task(),
            KeyCode::Char('d') => self.open_confirm_delete(),
            KeyCode::Enter => {
                if let Some(id) = self.selected_task_id() {
                    self.persist_data(|db, data| storage::cycle_task_status(db, data, id));
                    self.bump_data();
                }
            }
            KeyCode::Char(' ') => {
                if let Some(id) = self.selected_task_id() {
                    self.set_active_task(Some(id));
                    self.set_status("Task set as active for the timer.", false);
                }
            }
            KeyCode::Char('1') => {
                if let Some(id) = self.selected_task_id() {
                    self.persist_data(|db, data| {
                        storage::set_priority(db, data, id, Priority::Low)
                    });
                    self.bump_data();
                }
            }
            KeyCode::Char('2') => {
                if let Some(id) = self.selected_task_id() {
                    self.persist_data(|db, data| {
                        storage::set_priority(db, data, id, Priority::Medium)
                    });
                    self.bump_data();
                }
            }
            KeyCode::Char('3') => {
                if let Some(id) = self.selected_task_id() {
                    self.persist_data(|db, data| {
                        storage::set_priority(db, data, id, Priority::High)
                    });
                    self.bump_data();
                }
            }
            KeyCode::Down | KeyCode::Char('j') if !ctrl => self.move_task_selection(1),
            KeyCode::Up | KeyCode::Char('k') if !ctrl => self.move_task_selection(-1),
            KeyCode::Down | KeyCode::Char('j') if ctrl => {
                if let Some(id) = self.selected_task_id() {
                    self.persist_data(|db, data| storage::move_task(db, data, id, 1));
                    self.bump_data();
                }
            }
            KeyCode::Up | KeyCode::Char('k') if ctrl => {
                if let Some(id) = self.selected_task_id() {
                    self.persist_data(|db, data| storage::move_task(db, data, id, -1));
                    self.bump_data();
                }
            }
            KeyCode::PageDown => self.move_task_selection(8),
            KeyCode::PageUp => self.move_task_selection(-8),
            KeyCode::Home => {
                let len = self.filtered_task_indices().len();
                if len > 0 {
                    self.task_state.select(Some(0));
                }
            }
            KeyCode::End => {
                let len = self.filtered_task_indices().len();
                if len > 0 {
                    self.task_state.select(Some(len - 1));
                }
            }
            _ => {}
        }
    }

    pub(crate) fn move_task_selection(&mut self, delta: i32) {
        let len = self.filtered_task_indices().len();
        if len == 0 {
            return;
        }
        let cur = self.task_state.selected().unwrap_or(0) as i32;
        let new = (cur + delta).clamp(0, len as i32 - 1) as usize;
        self.task_state.select(Some(new));
    }
}