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::{Block, Borders, Clear, Paragraph};
10
11use crate::components::Component;
12use crate::components::event_state::EventState;
13use crate::components::events::{AppEvent, AppTx};
14use crate::settings::themes::Theme;
15
16pub struct CreateNoteDialog {
17 pub path: VaultPath,
18 pub vault: Arc<NoteVault>,
19 pub path_display: String,
21 pub error: Option<String>,
22}
23
24impl CreateNoteDialog {
25 pub fn new(path: VaultPath, vault: Arc<NoteVault>) -> Self {
26 let path_display = format!(" {}", path);
27 Self {
28 path,
29 vault,
30 path_display,
31 error: None,
32 }
33 }
34
35 pub fn handle_key(&mut self, key: KeyEvent, tx: &AppTx) -> EventState {
38 match key.code {
39 KeyCode::Enter => {
40 let path = self.path.clone();
41 let vault = Arc::clone(&self.vault);
42 let tx_clone = tx.clone();
43 tokio::spawn(async move {
44 match vault.load_or_create_note(&path, None).await {
45 Ok(_) => {
46 tx_clone.send(AppEvent::EntryCreated(path)).ok();
47 }
48 Err(e) => {
49 tx_clone.send(AppEvent::DialogError(e.to_string())).ok();
50 }
51 }
52 });
53 EventState::Consumed
54 }
55 KeyCode::Esc => {
56 tx.send(AppEvent::CloseOverlay).ok();
57 EventState::Consumed
58 }
59 _ => EventState::NotConsumed,
60 }
61 }
62}
63
64impl Component for CreateNoteDialog {
65 fn render(&mut self, f: &mut Frame, rect: Rect, theme: &Theme, _focused: bool) {
66 let height = if self.error.is_some() { 10 } else { 9 };
67 let popup_area = super::fixed_centered_rect(52, height, rect);
68
69 f.render_widget(Clear, popup_area);
70
71 let fg_muted = theme.fg_muted.to_ratatui();
72 let fg = theme.fg.to_ratatui();
73 let bg = theme.bg_panel.to_ratatui();
74
75 let outer_block = Block::default()
76 .title(" Create note? ")
77 .borders(Borders::ALL)
78 .border_style(Style::default().fg(fg_muted))
79 .style(theme.panel_style());
80 let inner = outer_block.inner(popup_area);
81 f.render_widget(outer_block, popup_area);
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], fg_muted, bg);
99 f.render_widget(
100 Paragraph::new(" Note doesn't exist.").style(Style::default().fg(fg_muted).bg(bg)),
101 rows[3],
102 );
103 f.render_widget(
104 Paragraph::new(" [Enter] Create [Esc] Cancel")
105 .style(Style::default().fg(fg_muted).bg(bg)),
106 rows[5],
107 );
108 if let Some(msg) = &self.error {
109 super::render_error_row(f, rows[6], msg, bg);
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);
188
189 let key = KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE);
190 let state = dialog.handle_key(key, &tx);
191
192 assert_eq!(state, EventState::Consumed);
193 });
194 }
195}