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};
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((_, created)) => tx_clone.announce_and_open(path, created),
47 Err(e) => {
48 tx_clone.send(AppEvent::DialogError(e.to_string())).ok();
49 }
50 }
51 });
52 EventState::Consumed
53 }
54 KeyCode::Esc => {
55 tx.send(AppEvent::CloseOverlay).ok();
56 EventState::Consumed
57 }
58 _ => EventState::NotConsumed,
59 }
60 }
61}
62
63impl Component for CreateNoteDialog {
64 fn render(&mut self, f: &mut Frame, rect: Rect, theme: &Theme, _focused: bool) {
65 let height = if self.error.is_some() { 10 } else { 9 };
66 let popup_area = super::fixed_centered_rect(52, height, rect);
67
68 let gray = theme.gray.to_ratatui();
69 let fg = theme.fg.to_ratatui();
70 let bg = theme.bg_panel.to_ratatui();
71
72 let inner = modal_chrome(
73 f,
74 popup_area,
75 theme,
76 ModalSpec {
77 title: Some(" Create note? "),
78 border: Some(Style::default().fg(gray)),
79 ..Default::default()
80 },
81 );
82
83 let rows = Layout::default()
84 .direction(Direction::Vertical)
85 .constraints([
86 Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Min(0), ])
95 .split(inner);
96
97 super::render_path_row(f, rows[1], &self.path_display, fg, bg);
98 super::render_separator(f, rows[2], gray, bg);
99 f.render_widget(
100 Paragraph::new(" Note doesn't exist.").style(Style::default().fg(gray).bg(bg)),
101 rows[3],
102 );
103 f.render_widget(
104 Paragraph::new(" [Enter] Create [Esc] Cancel")
105 .style(Style::default().fg(gray).bg(bg)),
106 rows[5],
107 );
108 if let Some(msg) = &self.error {
109 super::render_error_row(f, rows[6], msg, theme);
110 }
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117 use kimun_core::VaultConfig;
118 use tokio::sync::mpsc;
119
120 #[tokio::test]
130 #[ignore = "requires a real vault directory with kimun.sqlite"]
131 async fn new_with_vault_does_not_panic() {
132 let tmp = std::env::temp_dir().join("kimun_test_vault");
133 std::fs::create_dir_all(&tmp).unwrap();
134
135 let vault = Arc::new(
136 NoteVault::new(VaultConfig::new(tmp))
137 .await
138 .expect("vault creation failed"),
139 );
140 let (_tx, _rx) = mpsc::unbounded_channel::<AppEvent>();
141 let dialog = CreateNoteDialog::new(VaultPath::root(), vault);
142 assert!(dialog.error.is_none());
143 }
144
145 #[test]
146 fn esc_sends_close_dialog() {
147 use ratatui::crossterm::event::{KeyEvent, KeyModifiers};
148
149 let rt = tokio::runtime::Runtime::new().unwrap();
150 rt.block_on(async {
151 let tmp = std::env::temp_dir().join("kimun_create_esc_test");
152 std::fs::create_dir_all(&tmp).unwrap();
153
154 let vault_result = NoteVault::new(VaultConfig::new(tmp)).await;
155 let Ok(vault) = vault_result else { return };
156 let vault = Arc::new(vault);
157
158 let (tx, mut rx) = mpsc::unbounded_channel::<AppEvent>();
159 let mut dialog = CreateNoteDialog::new(VaultPath::root(), vault);
160
161 let key = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
162 let state = dialog.handle_key(key, &tx);
163
164 assert_eq!(state, EventState::Consumed);
165 let event = rx.try_recv().expect("expected AppEvent::CloseOverlay");
166 assert!(matches!(event, AppEvent::CloseOverlay));
167 });
168 }
169
170 #[test]
171 fn enter_returns_consumed() {
172 use ratatui::crossterm::event::{KeyEvent, KeyModifiers};
173
174 let rt = tokio::runtime::Runtime::new().unwrap();
175 rt.block_on(async {
176 let tmp = std::env::temp_dir().join("kimun_create_enter_test");
177 std::fs::create_dir_all(&tmp).unwrap();
178
179 let vault_result = NoteVault::new(VaultConfig::new(tmp)).await;
180 let Ok(vault) = vault_result else { return };
181 let vault = Arc::new(vault);
182
183 let (tx, _rx) = mpsc::unbounded_channel::<AppEvent>();
184 let mut dialog = CreateNoteDialog::new(VaultPath::root(), vault);
189
190 let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE);
191 let state = dialog.handle_key(key, &tx);
192
193 assert_eq!(state, EventState::Consumed);
194 });
195 }
196}