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, AppTxExt, OverlayData};
14use crate::components::panel::{ModalSpec, modal_chrome};
15use crate::settings::themes::Theme;
16
17pub struct CreateNoteDialog {
18 pub path: VaultPath,
24 pub vault: Arc<NoteVault>,
25 pub path_display: String,
27 pub error: Option<String>,
28 pub content: Option<String>,
32}
33
34impl CreateNoteDialog {
35 pub fn new(path: VaultPath, vault: Arc<NoteVault>, content: Option<String>) -> Self {
36 let path_display = format!(" {}", path);
37 Self {
38 path,
39 vault,
40 path_display,
41 error: None,
42 content,
43 }
44 }
45
46 pub fn handle_key(&mut self, key: KeyEvent, tx: &AppTx) -> EventState {
49 match key.code {
50 KeyCode::Enter => {
51 let path = self.path.clone();
52 let vault = Arc::clone(&self.vault);
53 let content = self.content.clone();
54 let tx_clone = tx.clone();
55 tokio::spawn(async move {
56 let result = match &content {
61 Some(text) => vault
62 .create_note_avoiding_conflicts(&path, text)
63 .await
64 .map(|actual_path| (actual_path, true)),
65 None => vault
66 .load_or_create_note(&path, content)
67 .await
68 .map(|(_, created)| (path.clone(), created)),
69 };
70 match result {
71 Ok((actual_path, created)) => {
72 tx_clone.announce_and_open(actual_path, created)
73 }
74 Err(e) => {
75 tx_clone
76 .send(AppEvent::OverlayData(OverlayData::Error(e.to_string())))
77 .ok();
78 }
79 }
80 });
81 EventState::Consumed
82 }
83 KeyCode::Esc => {
84 tx.send(AppEvent::CloseOverlay).ok();
85 EventState::Consumed
86 }
87 _ => EventState::NotConsumed,
88 }
89 }
90}
91
92impl Component for CreateNoteDialog {
93 fn render(&mut self, f: &mut Frame, rect: Rect, theme: &Theme, _focused: bool) {
94 let height = if self.error.is_some() { 10 } else { 9 };
95 let popup_area = super::fixed_centered_rect(52, height, rect);
96
97 let gray = theme.gray.to_ratatui();
98 let fg = theme.fg.to_ratatui();
99 let bg = theme.bg_panel.to_ratatui();
100
101 let inner = modal_chrome(
102 f,
103 popup_area,
104 theme,
105 ModalSpec {
106 title: Some(" Create note? "),
107 border: Some(Style::default().fg(gray)),
108 ..Default::default()
109 },
110 );
111
112 let rows = Layout::default()
113 .direction(Direction::Vertical)
114 .constraints([
115 Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Min(0), ])
124 .split(inner);
125
126 super::render_path_row(f, rows[1], &self.path_display, fg, bg);
127 super::render_separator(f, rows[2], gray, bg);
128 f.render_widget(
129 Paragraph::new(" Note doesn't exist.").style(Style::default().fg(gray).bg(bg)),
130 rows[3],
131 );
132 f.render_widget(
133 Paragraph::new(" [Enter] Create [Esc] Cancel")
134 .style(Style::default().fg(gray).bg(bg)),
135 rows[5],
136 );
137 if let Some(msg) = &self.error {
138 super::render_error_row(f, rows[6], msg, theme);
139 }
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use kimun_core::VaultConfig;
147 use tokio::sync::mpsc;
148
149 #[tokio::test]
159 #[ignore = "requires a real vault directory with kimun.sqlite"]
160 async fn new_with_vault_does_not_panic() {
161 let tmp = std::env::temp_dir().join("kimun_test_vault");
162 std::fs::create_dir_all(&tmp).unwrap();
163
164 let vault = Arc::new(
165 NoteVault::new(VaultConfig::new(crate::test_support::sys(tmp)))
166 .await
167 .expect("vault creation failed"),
168 );
169 let (_tx, _rx) = mpsc::unbounded_channel::<AppEvent>();
170 let dialog = CreateNoteDialog::new(VaultPath::root(), vault, None);
171 assert!(dialog.error.is_none());
172 }
173
174 #[test]
175 fn esc_sends_close_dialog() {
176 use ratatui::crossterm::event::{KeyEvent, KeyModifiers};
177
178 let rt = tokio::runtime::Runtime::new().unwrap();
179 rt.block_on(async {
180 let tmp = std::env::temp_dir().join("kimun_create_esc_test");
181 std::fs::create_dir_all(&tmp).unwrap();
182
183 let vault_result =
184 NoteVault::new(VaultConfig::new(crate::test_support::sys(tmp))).await;
185 let Ok(vault) = vault_result else { return };
186 let vault = Arc::new(vault);
187
188 let (tx, mut rx) = mpsc::unbounded_channel::<AppEvent>();
189 let mut dialog = CreateNoteDialog::new(VaultPath::root(), vault, None);
190
191 let key = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
192 let state = dialog.handle_key(key, &tx);
193
194 assert_eq!(state, EventState::Consumed);
195 let event = rx.try_recv().expect("expected AppEvent::CloseOverlay");
196 assert!(matches!(event, AppEvent::CloseOverlay));
197 });
198 }
199
200 #[test]
201 fn enter_returns_consumed() {
202 use ratatui::crossterm::event::{KeyEvent, KeyModifiers};
203
204 let rt = tokio::runtime::Runtime::new().unwrap();
205 rt.block_on(async {
206 let tmp = std::env::temp_dir().join("kimun_create_enter_test");
207 std::fs::create_dir_all(&tmp).unwrap();
208
209 let vault_result =
210 NoteVault::new(VaultConfig::new(crate::test_support::sys(tmp))).await;
211 let Ok(vault) = vault_result else { return };
212 let vault = Arc::new(vault);
213
214 let (tx, _rx) = mpsc::unbounded_channel::<AppEvent>();
215 let mut dialog = CreateNoteDialog::new(VaultPath::root(), vault, None);
220
221 let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE);
222 let state = dialog.handle_key(key, &tx);
223
224 assert_eq!(state, EventState::Consumed);
225 });
226 }
227}