1pub use create_note_dialog::CreateNoteDialog;
2pub use delete_dialog::DeleteConfirmDialog;
3pub use file_ops_menu::FileOpsMenuDialog;
4pub use help_dialog::HelpDialog;
5pub use move_dialog::MoveDialog;
6pub use quick_note_modal::QuickNoteModal;
7pub use rename_dialog::RenameDialog;
8pub use save_search_dialog::SaveSearchDialog;
9pub use sort_dialog::SortDialog;
10pub use theme_picker::ThemePickerDialog;
11pub use update_dialog::UpdateAvailableDialog;
12pub use workspace_switcher::WorkspaceSwitcherModal;
13
14use std::sync::Arc;
15
16use kimun_core::NoteVault;
17use ratatui::Frame;
18use ratatui::layout::{Constraint, Direction, Layout, Rect};
19use ratatui::style::{Color, Modifier, Style};
20use ratatui::widgets::{Block, Borders, Paragraph, Widget};
21
22use crate::components::Component;
23use crate::components::event_state::EventState;
24use crate::components::events::{AppEvent, AppTx, InputEvent, OverlayData, SaveSource, SortTarget};
25use crate::components::file_list::{SortField, SortOrder};
26use crate::components::overlay::{Overlay, OverlayKind, OverlayMsg};
27use crate::settings::themes::Theme;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum ValidationState {
36 Idle,
38 Pending,
40 Available,
42 Taken,
44}
45
46pub mod create_note_dialog;
47pub mod delete_dialog;
48pub mod file_ops_menu;
49pub mod help_dialog;
50pub mod move_dialog;
51pub mod quick_note_modal;
52pub mod rename_dialog;
53pub mod save_search_dialog;
54pub mod sort_dialog;
55pub mod theme_picker;
56pub mod update_dialog;
57pub mod workspace_switcher;
58
59pub enum ActiveDialog {
60 Menu(FileOpsMenuDialog),
61 Delete(DeleteConfirmDialog),
62 Rename(RenameDialog),
63 Move(MoveDialog),
64 CreateNote(CreateNoteDialog),
65 Help(HelpDialog),
66 QuickNote(QuickNoteModal),
67 WorkspaceSwitcher(WorkspaceSwitcherModal),
68 SaveSearch(SaveSearchDialog),
69 Sort(SortDialog),
70 ThemePicker(ThemePickerDialog),
71 UpdateAvailable(UpdateAvailableDialog),
72}
73
74impl ActiveDialog {
75 pub fn set_error(&mut self, msg: String) {
76 match self {
77 ActiveDialog::Menu(_) => {} ActiveDialog::Delete(d) => d.error = Some(msg),
79 ActiveDialog::Rename(d) => d.error = Some(msg),
80 ActiveDialog::Move(d) => d.error = Some(msg),
81 ActiveDialog::CreateNote(d) => d.error = Some(msg),
82 ActiveDialog::Help(_) => {}
83 ActiveDialog::QuickNote(d) => d.error = Some(msg),
84 ActiveDialog::WorkspaceSwitcher(_) => {} ActiveDialog::SaveSearch(_) => {} ActiveDialog::Sort(_) => {} ActiveDialog::ThemePicker(_) => {} ActiveDialog::UpdateAvailable(_) => {} }
90 }
91
92 pub fn help(key_bindings: &crate::keys::KeyBindings) -> Self {
94 ActiveDialog::Help(HelpDialog::new(key_bindings))
95 }
96
97 pub fn cheatsheet(settings: &crate::settings::AppSettings) -> Self {
99 ActiveDialog::Help(HelpDialog::cheatsheet(settings))
100 }
101
102 pub fn query_syntax() -> Self {
104 ActiveDialog::Help(HelpDialog::query_syntax())
105 }
106
107 pub fn theme_picker(settings: &crate::settings::AppSettings) -> Self {
109 ActiveDialog::ThemePicker(ThemePickerDialog::new(settings))
110 }
111
112 pub fn update(status: &crate::update::UpdateStatus) -> Self {
114 ActiveDialog::UpdateAvailable(UpdateAvailableDialog::new(status))
115 }
116
117 pub fn quick_note(vault: Arc<NoteVault>) -> Self {
118 ActiveDialog::QuickNote(QuickNoteModal::new(vault))
119 }
120
121 pub fn workspace_switcher(settings: &crate::settings::AppSettings) -> Self {
122 ActiveDialog::WorkspaceSwitcher(WorkspaceSwitcherModal::new(settings))
123 }
124
125 pub fn create_note(
126 path: kimun_core::nfs::VaultPath,
127 vault: Arc<NoteVault>,
128 content: Option<String>,
129 ) -> Self {
130 ActiveDialog::CreateNote(CreateNoteDialog::new(path, vault, content))
131 }
132
133 pub fn save_search(
138 query: String,
139 provenance: Option<String>,
140 source: SaveSource,
141 vault: Arc<NoteVault>,
142 tx: &AppTx,
143 ) -> Self {
144 let tx = tx.clone();
145 tokio::spawn(async move {
146 if let Ok(searches) = vault.list_saved_searches().await {
147 let names = searches.into_iter().map(|s| s.name).collect();
148 tx.send(AppEvent::OverlayData(OverlayData::SavedSearchNamesLoaded(
149 names,
150 )))
151 .ok();
152 }
153 });
154 ActiveDialog::SaveSearch(SaveSearchDialog::new(query, provenance, source))
155 }
156
157 pub fn sort(
158 target: SortTarget,
159 field: SortField,
160 order: SortOrder,
161 group_directories: bool,
162 ) -> Self {
163 ActiveDialog::Sort(SortDialog::new(target, field, order, group_directories))
164 }
165
166 pub fn file_ops_menu(path: kimun_core::nfs::VaultPath) -> Self {
167 ActiveDialog::Menu(FileOpsMenuDialog::new(path))
168 }
169
170 pub fn delete(path: kimun_core::nfs::VaultPath, vault: Arc<NoteVault>) -> Self {
171 ActiveDialog::Delete(DeleteConfirmDialog::new(path, vault))
172 }
173
174 pub fn rename(path: kimun_core::nfs::VaultPath, vault: Arc<NoteVault>) -> Self {
175 ActiveDialog::Rename(RenameDialog::new(path, vault))
176 }
177
178 pub fn move_to(path: kimun_core::nfs::VaultPath, vault: Arc<NoteVault>, tx: &AppTx) -> Self {
179 ActiveDialog::Move(MoveDialog::new(path, vault, tx))
180 }
181}
182
183impl Overlay for ActiveDialog {
184 fn kind(&self) -> OverlayKind {
185 OverlayKind::Dialog
186 }
187
188 fn handle_input(&mut self, event: &InputEvent, tx: &AppTx) -> EventState {
189 <Self as Component>::handle_input(self, event, tx)
190 }
191
192 fn handle_data(
193 &mut self,
194 data: &OverlayData,
195 _vault: &Arc<NoteVault>,
196 tx: &AppTx,
197 ) -> OverlayMsg {
198 match data {
199 OverlayData::RenameValidation { available } => {
200 if let ActiveDialog::Rename(d) = self {
201 d.validation_state = if *available {
202 ValidationState::Available
203 } else {
204 ValidationState::Taken
205 };
206 d.validation_task = None;
207 }
208 OverlayMsg::Consumed
209 }
210 OverlayData::MoveDirectoriesLoaded(paths) => {
211 if let ActiveDialog::Move(d) = self {
212 d.all_dirs = paths.clone();
213 d.filtered = None;
214 d.load_task = None;
215 if d.list_state.selected().is_none() && !d.results().is_empty() {
216 d.list_state.select(Some(0));
217 }
218 d.spawn_validation(tx);
219 }
220 OverlayMsg::Consumed
221 }
222 OverlayData::MoveFilterResults(paths) => {
223 if let ActiveDialog::Move(d) = self {
224 d.filter_task = None;
225 d.filtered = Some(paths.clone());
226 if !d.results().is_empty() {
227 d.list_state.select(Some(0));
228 } else {
229 d.list_state.select(None);
230 }
231 d.spawn_validation(tx);
232 }
233 OverlayMsg::Consumed
234 }
235 OverlayData::MoveDestValidation { available } => {
236 if let ActiveDialog::Move(d) = self {
237 d.dest_validation = if *available {
238 ValidationState::Available
239 } else {
240 ValidationState::Taken
241 };
242 d.validation_task = None;
243 }
244 OverlayMsg::Consumed
245 }
246 OverlayData::SavedSearchNamesLoaded(names) => {
247 if let ActiveDialog::SaveSearch(d) = self {
248 d.set_existing_names(names.clone());
249 }
250 OverlayMsg::Consumed
251 }
252 OverlayData::Error(text) => {
253 self.set_error(text.clone());
254 OverlayMsg::Consumed
255 }
256 }
257 }
258
259 fn render(&mut self, f: &mut Frame, area: Rect, theme: &Theme) {
260 <Self as Component>::render(self, f, area, theme, true);
261 }
262}
263
264impl Component for ActiveDialog {
265 fn handle_input(&mut self, event: &InputEvent, tx: &AppTx) -> EventState {
266 let InputEvent::Key(key) = event else {
267 return EventState::NotConsumed;
268 };
269 match self {
270 ActiveDialog::Menu(d) => d.handle_key(*key, tx),
271 ActiveDialog::Delete(d) => d.handle_key(*key, tx),
272 ActiveDialog::Rename(d) => d.handle_key(*key, tx),
273 ActiveDialog::Move(d) => d.handle_key(*key, tx),
274 ActiveDialog::CreateNote(d) => d.handle_key(*key, tx),
275 ActiveDialog::Help(d) => d.handle_key(*key, tx),
276 ActiveDialog::QuickNote(d) => d.handle_key(*key, tx),
277 ActiveDialog::WorkspaceSwitcher(d) => d.handle_key(*key, tx),
278 ActiveDialog::SaveSearch(d) => d.handle_input(event, tx),
279 ActiveDialog::Sort(d) => d.handle_input(event, tx),
280 ActiveDialog::ThemePicker(d) => d.handle_key(*key, tx),
281 ActiveDialog::UpdateAvailable(d) => d.handle_key(*key, tx),
282 }
283 }
284
285 fn render(&mut self, f: &mut Frame, rect: Rect, theme: &Theme, focused: bool) {
286 match self {
287 ActiveDialog::Menu(d) => d.render(f, rect, theme, focused),
288 ActiveDialog::Delete(d) => d.render(f, rect, theme, focused),
289 ActiveDialog::Rename(d) => d.render(f, rect, theme, focused),
290 ActiveDialog::Move(d) => d.render(f, rect, theme, focused),
291 ActiveDialog::CreateNote(d) => d.render(f, rect, theme, focused),
292 ActiveDialog::Help(d) => d.render(f, rect, theme, focused),
293 ActiveDialog::QuickNote(d) => d.render(f, rect, theme, focused),
294 ActiveDialog::WorkspaceSwitcher(d) => d.render(f, rect, theme, focused),
295 ActiveDialog::SaveSearch(d) => d.render(f, rect, theme, focused),
296 ActiveDialog::Sort(d) => d.render(f, rect, theme, focused),
297 ActiveDialog::ThemePicker(d) => d.render(f, rect, theme, focused),
298 ActiveDialog::UpdateAvailable(d) => d.render(f, rect, theme, focused),
299 }
300 }
301}
302
303pub(super) fn render_path_row(f: &mut Frame, rect: Rect, path: &str, fg: Color, bg: Color) {
309 f.render_widget(
310 Paragraph::new(path).style(Style::default().fg(fg).bg(bg)),
311 rect,
312 );
313}
314
315pub(super) fn render_separator(f: &mut Frame, rect: Rect, gray: Color, bg: Color) {
317 Block::default()
318 .borders(Borders::TOP)
319 .border_style(Style::default().fg(gray))
320 .style(Style::default().bg(bg))
321 .render(rect, f.buffer_mut());
322}
323
324pub(super) fn render_error_row(f: &mut Frame, rect: Rect, msg: &str, theme: &Theme) {
326 f.render_widget(
327 Paragraph::new(format!(" Error: {msg}")).style(
328 Style::default()
329 .fg(theme.red.to_ratatui())
330 .bg(theme.bg_panel.to_ratatui()),
331 ),
332 rect,
333 );
334}
335
336pub(super) fn render_confirm_hint(
339 f: &mut Frame,
340 rect: Rect,
341 enter_text: &str,
342 enter_active: bool,
343 fg: Color,
344 gray: Color,
345 bg: Color,
346) {
347 let enter_style = if enter_active {
348 Style::default().fg(fg).bg(bg)
349 } else {
350 Style::default().fg(gray).bg(bg).add_modifier(Modifier::DIM)
351 };
352 let chunks = Layout::default()
353 .direction(Direction::Horizontal)
354 .constraints([
355 Constraint::Length(enter_text.len() as u16 + 1),
356 Constraint::Min(1),
357 ])
358 .split(rect);
359 f.render_widget(Paragraph::new(enter_text).style(enter_style), chunks[0]);
360 f.render_widget(
361 Paragraph::new(" [Esc] Cancel").style(Style::default().fg(gray).bg(bg)),
362 chunks[1],
363 );
364}
365
366pub(super) use crate::components::fixed_centered_rect;
372
373#[cfg(test)]
374mod tests {
375 use super::*;
376 use crate::keys::KeyBindings;
377
378 #[test]
379 fn active_dialog_help_variant_compiles() {
380 let dialog = HelpDialog::new(&KeyBindings::empty());
381 let _active: ActiveDialog = ActiveDialog::Help(dialog);
382 }
383
384 #[test]
385 fn active_dialog_sort_variant_compiles() {
386 use crate::components::events::SortTarget;
387 use crate::components::file_list::{SortField, SortOrder};
388 let _active: ActiveDialog = ActiveDialog::sort(
389 SortTarget::Sidebar,
390 SortField::Name,
391 SortOrder::Ascending,
392 false,
393 );
394 }
395}