kimun-notes 0.11.0

A terminal-based notes application
Documentation
use ratatui::Frame;
use ratatui::layout::Rect;
use ratatui::widgets::{Block, Borders, Paragraph};
use std::path::PathBuf;

use crate::components::Component;
use crate::components::event_state::EventState;
use crate::components::events::{AppEvent, AppTx, InputEvent};
use crate::settings::themes::Theme;

pub struct VaultSection {
    current_path: Option<PathBuf>,
}

impl VaultSection {
    pub fn new(current_path: Option<PathBuf>) -> Self {
        Self { current_path }
    }

    pub fn set_path(&mut self, path: Option<PathBuf>) {
        self.current_path = path;
    }
}

impl Component for VaultSection {
    fn handle_input(&mut self, event: &InputEvent, tx: &AppTx) -> EventState {
        let InputEvent::Key(key) = event else {
            return EventState::NotConsumed;
        };
        match key.code {
            ratatui::crossterm::event::KeyCode::Enter
            | ratatui::crossterm::event::KeyCode::Char('b') => {
                tx.send(AppEvent::OpenFileBrowser).ok();
                EventState::Consumed
            }
            _ => EventState::NotConsumed,
        }
    }

    fn render(&mut self, f: &mut Frame, rect: Rect, theme: &Theme, focused: bool) {
        let border_style = theme.border_style(focused);
        let path_str = self
            .current_path
            .as_ref()
            .map(|p| p.to_string_lossy().into_owned())
            .unwrap_or_else(|| "(no vault set)".to_string());
        let text = format!("{}    [Enter: Browse]", path_str);
        let block = Block::default()
            .title("Vault Path")
            .borders(Borders::ALL)
            .border_style(border_style)
            .style(theme.base_style());
        let para = Paragraph::new(text).block(block).style(theme.base_style());
        f.render_widget(para, rect);
    }
}

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

    #[test]
    fn renders_no_vault_set_when_none() {
        let section = VaultSection::new(None);
        assert!(section.current_path.is_none());
    }

    #[test]
    fn renders_path_when_some() {
        let path = PathBuf::from("/Users/me/notes");
        let section = VaultSection::new(Some(path.clone()));
        assert_eq!(section.current_path.as_ref().unwrap(), &path);
    }

    #[test]
    fn set_path_updates_current() {
        let mut section = VaultSection::new(None);
        let path = PathBuf::from("/Users/me/notes");
        section.set_path(Some(path.clone()));
        assert_eq!(section.current_path.as_ref().unwrap(), &path);
    }

    #[test]
    fn enter_sends_open_file_browser() {
        use crate::components::events::AppEvent;
        use ratatui::crossterm::event::{
            KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers,
        };
        let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
        let mut section = VaultSection::new(None);
        let key = InputEvent::Key(KeyEvent {
            code: KeyCode::Enter,
            modifiers: KeyModifiers::NONE,
            kind: KeyEventKind::Press,
            state: KeyEventState::NONE,
        });
        let result = section.handle_input(&key, &tx);
        assert!(matches!(
            result,
            crate::components::event_state::EventState::Consumed
        ));
        let msg = rx.try_recv().expect("message should be sent");
        assert!(matches!(msg, AppEvent::OpenFileBrowser));
    }

    #[test]
    fn b_key_sends_open_file_browser() {
        use crate::components::events::AppEvent;
        use ratatui::crossterm::event::{
            KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers,
        };
        let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
        let mut section = VaultSection::new(None);
        let key = InputEvent::Key(KeyEvent {
            code: KeyCode::Char('b'),
            modifiers: KeyModifiers::NONE,
            kind: KeyEventKind::Press,
            state: KeyEventState::NONE,
        });
        let result = section.handle_input(&key, &tx);
        assert!(matches!(
            result,
            crate::components::event_state::EventState::Consumed
        ));
        let msg = rx.try_recv().expect("message should be sent");
        assert!(matches!(msg, AppEvent::OpenFileBrowser));
    }

    #[test]
    fn renders_no_vault_set_text() {
        use ratatui::Terminal;
        use ratatui::backend::TestBackend;
        let backend = TestBackend::new(60, 5);
        let mut terminal = Terminal::new(backend).unwrap();
        let mut section = VaultSection::new(None);
        let theme = crate::settings::themes::Theme::gruvbox_dark();
        terminal
            .draw(|f| {
                section.render(f, f.area(), &theme, false);
            })
            .unwrap();
        let buffer = terminal.backend().buffer().clone();
        let flat: String = buffer.content.iter().map(|c| c.symbol()).collect();
        assert!(
            flat.contains("(no vault set)"),
            "Expected '(no vault set)' in rendered output"
        );
    }

    #[test]
    fn renders_vault_path_text() {
        use ratatui::Terminal;
        use ratatui::backend::TestBackend;
        let backend = TestBackend::new(60, 5);
        let mut terminal = Terminal::new(backend).unwrap();
        let path = PathBuf::from("/Users/me/notes");
        let mut section = VaultSection::new(Some(path));
        let theme = crate::settings::themes::Theme::gruvbox_dark();
        terminal
            .draw(|f| {
                section.render(f, f.area(), &theme, false);
            })
            .unwrap();
        let buffer = terminal.backend().buffer().clone();
        let flat: String = buffer.content.iter().map(|c| c.symbol()).collect();
        assert!(
            flat.contains("notes"),
            "Expected vault path in rendered output"
        );
    }
}