kimun_notes/components/dialogs/
create_note_dialog.rs1use std::sync::Arc;
2
3use kimun_core::NoteVault;
4use kimun_core::nfs::VaultPath;
5use ratatui::Frame;
6use ratatui::crossterm::event::{KeyCode, KeyEvent};
7use ratatui::layout::{Constraint, Direction, Layout, Rect};
8use ratatui::style::Style;
9use ratatui::widgets::Paragraph;
10
11use crate::components::Component;
12use crate::components::event_state::EventState;
13use crate::components::events::{AppEvent, AppTx};
14use crate::components::panel::{ModalSpec, modal_chrome};
15use crate::settings::themes::Theme;
16
17pub struct CreateNoteDialog {
18 pub path: VaultPath,
19 pub vault: Arc<NoteVault>,
20 pub path_display: String,
22 pub error: Option<String>,
23}
24
25impl CreateNoteDialog {
26 pub fn new(path: VaultPath, vault: Arc<NoteVault>) -> Self {
27 let path_display = format!(" {}", path);
28 Self {
29 path,
30 vault,
31 path_display,
32 error: None,
33 }
34 }
35
36 pub fn handle_key(&mut self, key: KeyEvent, tx: &AppTx) -> EventState {
39 match key.code {
40 KeyCode::Enter => {
41 let path = self.path.clone();
42 let vault = Arc::clone(&self.vault);
43 let tx_clone = tx.clone();
44 tokio::spawn(async move {
45 match vault.load_or_create_note(&path, None).await {
46 Ok(_) => {
47 tx_clone.send(AppEvent::EntryCreated(path)).ok();
48 }
49 Err(e) => {
50 tx_clone.send(AppEvent::DialogError(e.to_string())).ok();
51 }
52 }
53 });
54 EventState::Consumed
55 }
56 KeyCode::Esc => {
57 tx.send(AppEvent::CloseOverlay).ok();
58 EventState::Consumed
59 }
60 _ => EventState::NotConsumed,
61 }
62 }
63}
64
65impl Component for CreateNoteDialog {
66 fn render(&mut self, f: &mut Frame, rect: Rect, theme: &Theme, _focused: bool) {
67 let height = if self.error.is_some() { 10 } else { 9 };
68 let popup_area = super::fixed_centered_rect(52, height, rect);
69
70 let gray = theme.gray.to_ratatui();
71 let fg = theme.fg.to_ratatui();
72 let bg = theme.bg_panel.to_ratatui();
73
74 let inner = modal_chrome(
75 f,
76 popup_area,
77 theme,
78 ModalSpec {
79 title: Some(" Create note? "),
80 border: Some(Style::default().fg(gray)),
81 ..Default::default()
82 },
83 );
84
85 let rows = Layout::default()
86 .direction(Direction::Vertical)
87 .constraints([
88 Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Min(0), ])
97 .split(inner);
98
99 super::render_path_row(f, rows[1], &self.path_display, fg, bg);
100 super::render_separator(f, rows[2], gray, bg);
101 f.render_widget(
102 Paragraph::new(" Note doesn't exist.").style(Style::default().fg(gray).bg(bg)),
103 rows[3],
104 );
105 f.render_widget(
106 Paragraph::new(" [Enter] Create [Esc] Cancel")
107 .style(Style::default().fg(gray).bg(bg)),
108 rows[5],
109 );
110 if let Some(msg) = &self.error {
111 super::render_error_row(f, rows[6], msg, theme);
112 }
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119 use kimun_core::VaultConfig;
120 use tokio::sync::mpsc;
121
122 #[tokio::test]
132 #[ignore = "requires a real vault directory with kimun.sqlite"]
133 async fn new_with_vault_does_not_panic() {
134 let tmp = std::env::temp_dir().join("kimun_test_vault");
135 std::fs::create_dir_all(&tmp).unwrap();
136
137 let vault = Arc::new(
138 NoteVault::new(VaultConfig::new(tmp))
139 .await
140 .expect("vault creation failed"),
141 );
142 let (_tx, _rx) = mpsc::unbounded_channel::<AppEvent>();
143 let dialog = CreateNoteDialog::new(VaultPath::root(), vault);
144 assert!(dialog.error.is_none());
145 }
146
147 #[test]
148 fn esc_sends_close_dialog() {
149 use ratatui::crossterm::event::{KeyEvent, KeyModifiers};
150
151 let rt = tokio::runtime::Runtime::new().unwrap();
152 rt.block_on(async {
153 let tmp = std::env::temp_dir().join("kimun_create_esc_test");
154 std::fs::create_dir_all(&tmp).unwrap();
155
156 let vault_result = NoteVault::new(VaultConfig::new(tmp)).await;
157 let Ok(vault) = vault_result else { return };
158 let vault = Arc::new(vault);
159
160 let (tx, mut rx) = mpsc::unbounded_channel::<AppEvent>();
161 let mut dialog = CreateNoteDialog::new(VaultPath::root(), vault);
162
163 let key = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
164 let state = dialog.handle_key(key, &tx);
165
166 assert_eq!(state, EventState::Consumed);
167 let event = rx.try_recv().expect("expected AppEvent::CloseOverlay");
168 assert!(matches!(event, AppEvent::CloseOverlay));
169 });
170 }
171
172 #[test]
173 fn enter_returns_consumed() {
174 use ratatui::crossterm::event::{KeyEvent, KeyModifiers};
175
176 let rt = tokio::runtime::Runtime::new().unwrap();
177 rt.block_on(async {
178 let tmp = std::env::temp_dir().join("kimun_create_enter_test");
179 std::fs::create_dir_all(&tmp).unwrap();
180
181 let vault_result = NoteVault::new(VaultConfig::new(tmp)).await;
182 let Ok(vault) = vault_result else { return };
183 let vault = Arc::new(vault);
184
185 let (tx, _rx) = mpsc::unbounded_channel::<AppEvent>();
186 let mut dialog = CreateNoteDialog::new(VaultPath::root(), vault);
190
191 let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE);
192 let state = dialog.handle_key(key, &tx);
193
194 assert_eq!(state, EventState::Consumed);
195 });
196 }
197}