aven 0.1.12

Local-first task manager CLI and sync server
Documentation
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};

use crate::tui::event::{Action, ShortcutLookup, key_label, resolve_shortcut, shortcut_label};
use crate::tui::navigation::{DetailShortcut, detail_shortcut};

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum NormalShortcutResolution {
    Action(Action),
    Prefix,
    Missing(String),
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum DetailShortcutResolution {
    Action(Action),
    Prefix,
    MissingAfterPrefix(String),
    PassThrough,
}

#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub(crate) struct ShortcutBuffer {
    codes: Vec<KeyCode>,
}

impl ShortcutBuffer {
    pub(crate) fn is_empty(&self) -> bool {
        self.codes.is_empty()
    }

    pub(crate) fn clear(&mut self) {
        self.codes.clear();
    }

    pub(crate) fn cancel(&mut self) -> bool {
        let had_pending = !self.codes.is_empty();
        self.clear();
        had_pending
    }

    pub(crate) fn labels(&self) -> Vec<String> {
        self.codes.iter().map(|code| key_label(*code)).collect()
    }

    pub(crate) fn resolve_normal(&mut self, code: KeyCode) -> NormalShortcutResolution {
        let had_pending = !self.codes.is_empty();
        let sequence = self.with_code(code);
        match resolve_shortcut(&sequence) {
            ShortcutLookup::Found(action) => {
                self.clear();
                NormalShortcutResolution::Action(action)
            }
            ShortcutLookup::Ambiguous(_) if !had_pending => {
                self.codes = sequence;
                NormalShortcutResolution::Prefix
            }
            ShortcutLookup::Ambiguous(action) => {
                self.clear();
                NormalShortcutResolution::Action(action)
            }
            ShortcutLookup::Prefix => {
                self.codes = sequence;
                NormalShortcutResolution::Prefix
            }
            ShortcutLookup::Missing => {
                let label = shortcut_label(&sequence);
                self.clear();
                NormalShortcutResolution::Missing(label)
            }
        }
    }

    pub(crate) fn resolve_detail(&mut self, key: KeyEvent) -> DetailShortcutResolution {
        if !key.modifiers.is_empty() && key.modifiers != KeyModifiers::SHIFT {
            return DetailShortcutResolution::PassThrough;
        }

        let had_pending = !self.codes.is_empty();
        let sequence = self.with_code(key.code);
        match detail_shortcut(&sequence) {
            DetailShortcut::Action(action) => {
                self.clear();
                DetailShortcutResolution::Action(action)
            }
            DetailShortcut::Prefix => {
                self.codes = sequence;
                DetailShortcutResolution::Prefix
            }
            DetailShortcut::Missing(label) if had_pending => {
                self.clear();
                DetailShortcutResolution::MissingAfterPrefix(label)
            }
            DetailShortcut::Missing(_) => DetailShortcutResolution::PassThrough,
        }
    }

    pub(crate) fn begin_editor_prefix(&mut self) {
        self.codes.clear();
        self.codes.push(KeyCode::Char('x'));
    }

    pub(crate) fn begin_add_task_status_prefix(&mut self) {
        self.codes.clear();
        self.codes.push(KeyCode::Char('t'));
    }

    pub(crate) fn has_add_task_status_prefix(&self) -> bool {
        self.codes == [KeyCode::Char('t')]
    }

    pub(crate) fn take_add_task_status_request(&mut self, key: KeyEvent) -> Option<&'static str> {
        if self.codes != [KeyCode::Char('t')] || !key.modifiers.is_empty() {
            return None;
        }

        let status = match key.code {
            KeyCode::Char('i') => Some("inbox"),
            KeyCode::Char('b') => Some("backlog"),
            KeyCode::Char('t') => Some("todo"),
            KeyCode::Char('a') => Some("active"),
            KeyCode::Char('d') => Some("done"),
            KeyCode::Char('x') => Some("canceled"),
            _ => None,
        };
        if status.is_some() || key.code == KeyCode::Esc {
            self.clear();
        }
        status
    }

    pub(crate) fn begin_add_task_priority_prefix(&mut self) {
        self.codes.clear();
        self.codes.push(KeyCode::Char('r'));
    }

    pub(crate) fn has_add_task_priority_prefix(&self) -> bool {
        self.codes == [KeyCode::Char('r')]
    }

    pub(crate) fn take_add_task_priority_request(&mut self, key: KeyEvent) -> Option<&'static str> {
        if self.codes != [KeyCode::Char('r')] || !key.modifiers.is_empty() {
            return None;
        }

        let priority = match key.code {
            KeyCode::Char('n') => Some("none"),
            KeyCode::Char('l') => Some("low"),
            KeyCode::Char('m') => Some("medium"),
            KeyCode::Char('h') => Some("high"),
            KeyCode::Char('u') => Some("urgent"),
            _ => None,
        };
        if priority.is_some() || key.code == KeyCode::Esc {
            self.clear();
        }
        priority
    }

    pub(crate) fn take_editor_open_request(&mut self, key: KeyEvent) -> bool {
        if self.codes != [KeyCode::Char('x')] {
            return false;
        }

        self.clear();
        key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('e')
    }

    fn with_code(&self, code: KeyCode) -> Vec<KeyCode> {
        let mut sequence = self.codes.clone();
        sequence.push(code);
        sequence
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn normal_prefix_is_stored_and_rendered() {
        let mut buffer = ShortcutBuffer::default();
        assert_eq!(
            buffer.resolve_normal(KeyCode::Char('t')),
            NormalShortcutResolution::Prefix
        );
        assert_eq!(buffer.labels(), vec!["t".to_string()]);
    }

    #[test]
    fn normal_missing_clears_and_reports_full_label() {
        let mut buffer = ShortcutBuffer::default();
        assert_eq!(
            buffer.resolve_normal(KeyCode::Char('t')),
            NormalShortcutResolution::Prefix
        );
        assert_eq!(
            buffer.resolve_normal(KeyCode::Char('z')),
            NormalShortcutResolution::Missing("t z".to_string())
        );
        assert!(buffer.is_empty());
    }

    #[test]
    fn detail_missing_without_prefix_passes_through() {
        let mut buffer = ShortcutBuffer::default();
        assert_eq!(
            buffer.resolve_detail(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE)),
            DetailShortcutResolution::PassThrough
        );
    }

    #[test]
    fn detail_missing_after_prefix_clears_and_warns() {
        let mut buffer = ShortcutBuffer::default();
        assert_eq!(
            buffer.resolve_detail(KeyEvent::new(KeyCode::Char('t'), KeyModifiers::NONE)),
            DetailShortcutResolution::Prefix
        );
        assert_eq!(
            buffer.resolve_detail(KeyEvent::new(KeyCode::Char('z'), KeyModifiers::NONE)),
            DetailShortcutResolution::MissingAfterPrefix("t z".to_string())
        );
        assert!(buffer.is_empty());
    }

    #[test]
    fn editor_prefix_non_open_key_clears_and_returns_false() {
        let mut buffer = ShortcutBuffer::default();
        buffer.begin_editor_prefix();
        assert!(
            !buffer
                .take_editor_open_request(KeyEvent::new(KeyCode::Char('z'), KeyModifiers::NONE,))
        );
        assert!(buffer.is_empty());
    }

    #[test]
    fn editor_prefix_ctrl_e_clears_and_returns_true() {
        let mut buffer = ShortcutBuffer::default();
        buffer.begin_editor_prefix();
        assert!(
            buffer
                .take_editor_open_request(
                    KeyEvent::new(KeyCode::Char('e'), KeyModifiers::CONTROL,)
                )
        );
        assert!(buffer.is_empty());
    }
}