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