kimun_notes/components/settings/
vault_section.rs1use ratatui::Frame;
2use ratatui::layout::Rect;
3use ratatui::widgets::{Block, Borders, Paragraph};
4use std::path::PathBuf;
5
6use crate::components::Component;
7use crate::components::event_state::EventState;
8use crate::components::events::{AppEvent, AppTx, InputEvent};
9use crate::settings::themes::Theme;
10
11pub struct VaultSection {
12 current_path: Option<PathBuf>,
13}
14
15impl VaultSection {
16 pub fn new(current_path: Option<PathBuf>) -> Self {
17 Self { current_path }
18 }
19
20 pub fn set_path(&mut self, path: Option<PathBuf>) {
21 self.current_path = path;
22 }
23}
24
25impl Component for VaultSection {
26 fn handle_input(&mut self, event: &InputEvent, tx: &AppTx) -> EventState {
27 let InputEvent::Key(key) = event else {
28 return EventState::NotConsumed;
29 };
30 match key.code {
31 ratatui::crossterm::event::KeyCode::Enter
32 | ratatui::crossterm::event::KeyCode::Char('b') => {
33 tx.send(AppEvent::OpenFileBrowser).ok();
34 EventState::Consumed
35 }
36 _ => EventState::NotConsumed,
37 }
38 }
39
40 fn render(&mut self, f: &mut Frame, rect: Rect, theme: &Theme, focused: bool) {
41 let border_style = theme.border_style(focused);
42 let path_str = self
43 .current_path
44 .as_ref()
45 .map(|p| p.to_string_lossy().into_owned())
46 .unwrap_or_else(|| "(no vault set)".to_string());
47 let text = format!("{} [Enter: Browse]", path_str);
48 let block = Block::default()
49 .title("Vault Path")
50 .borders(Borders::ALL)
51 .border_style(border_style)
52 .style(theme.base_style());
53 let para = Paragraph::new(text).block(block).style(theme.base_style());
54 f.render_widget(para, rect);
55 }
56}
57
58#[cfg(test)]
59mod tests {
60 use super::*;
61
62 #[test]
63 fn renders_no_vault_set_when_none() {
64 let section = VaultSection::new(None);
65 assert!(section.current_path.is_none());
66 }
67
68 #[test]
69 fn renders_path_when_some() {
70 let path = PathBuf::from("/Users/me/notes");
71 let section = VaultSection::new(Some(path.clone()));
72 assert_eq!(section.current_path.as_ref().unwrap(), &path);
73 }
74
75 #[test]
76 fn set_path_updates_current() {
77 let mut section = VaultSection::new(None);
78 let path = PathBuf::from("/Users/me/notes");
79 section.set_path(Some(path.clone()));
80 assert_eq!(section.current_path.as_ref().unwrap(), &path);
81 }
82
83 #[test]
84 fn enter_sends_open_file_browser() {
85 use crate::components::events::AppEvent;
86 use ratatui::crossterm::event::{
87 KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers,
88 };
89 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
90 let mut section = VaultSection::new(None);
91 let key = InputEvent::Key(KeyEvent {
92 code: KeyCode::Enter,
93 modifiers: KeyModifiers::NONE,
94 kind: KeyEventKind::Press,
95 state: KeyEventState::NONE,
96 });
97 let result = section.handle_input(&key, &tx);
98 assert!(matches!(
99 result,
100 crate::components::event_state::EventState::Consumed
101 ));
102 let msg = rx.try_recv().expect("message should be sent");
103 assert!(matches!(msg, AppEvent::OpenFileBrowser));
104 }
105
106 #[test]
107 fn b_key_sends_open_file_browser() {
108 use crate::components::events::AppEvent;
109 use ratatui::crossterm::event::{
110 KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers,
111 };
112 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
113 let mut section = VaultSection::new(None);
114 let key = InputEvent::Key(KeyEvent {
115 code: KeyCode::Char('b'),
116 modifiers: KeyModifiers::NONE,
117 kind: KeyEventKind::Press,
118 state: KeyEventState::NONE,
119 });
120 let result = section.handle_input(&key, &tx);
121 assert!(matches!(
122 result,
123 crate::components::event_state::EventState::Consumed
124 ));
125 let msg = rx.try_recv().expect("message should be sent");
126 assert!(matches!(msg, AppEvent::OpenFileBrowser));
127 }
128
129 #[test]
130 fn renders_no_vault_set_text() {
131 use ratatui::Terminal;
132 use ratatui::backend::TestBackend;
133 let backend = TestBackend::new(60, 5);
134 let mut terminal = Terminal::new(backend).unwrap();
135 let mut section = VaultSection::new(None);
136 let theme = crate::settings::themes::Theme::gruvbox_dark();
137 terminal
138 .draw(|f| {
139 section.render(f, f.area(), &theme, false);
140 })
141 .unwrap();
142 let buffer = terminal.backend().buffer().clone();
143 let flat: String = buffer.content.iter().map(|c| c.symbol()).collect();
144 assert!(
145 flat.contains("(no vault set)"),
146 "Expected '(no vault set)' in rendered output"
147 );
148 }
149
150 #[test]
151 fn renders_vault_path_text() {
152 use ratatui::Terminal;
153 use ratatui::backend::TestBackend;
154 let backend = TestBackend::new(60, 5);
155 let mut terminal = Terminal::new(backend).unwrap();
156 let path = PathBuf::from("/Users/me/notes");
157 let mut section = VaultSection::new(Some(path));
158 let theme = crate::settings::themes::Theme::gruvbox_dark();
159 terminal
160 .draw(|f| {
161 section.render(f, f.area(), &theme, false);
162 })
163 .unwrap();
164 let buffer = terminal.backend().buffer().clone();
165 let flat: String = buffer.content.iter().map(|c| c.symbol()).collect();
166 assert!(
167 flat.contains("notes"),
168 "Expected vault path in rendered output"
169 );
170 }
171}