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, 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(path: kimun_core::nfs::VaultPath, vault: Arc<NoteVault>) -> Self {
126 ActiveDialog::CreateNote(CreateNoteDialog::new(path, vault))
127 }
128
129 pub fn save_search(
134 query: String,
135 provenance: Option<String>,
136 source: SaveSource,
137 vault: Arc<NoteVault>,
138 tx: &AppTx,
139 ) -> Self {
140 let tx = tx.clone();
141 tokio::spawn(async move {
142 if let Ok(searches) = vault.list_saved_searches().await {
143 let names = searches.into_iter().map(|s| s.name).collect();
144 tx.send(AppEvent::SavedSearchNamesLoaded(names)).ok();
145 }
146 });
147 ActiveDialog::SaveSearch(SaveSearchDialog::new(query, provenance, source))
148 }
149
150 pub fn sort(
151 target: SortTarget,
152 field: SortField,
153 order: SortOrder,
154 group_directories: bool,
155 ) -> Self {
156 ActiveDialog::Sort(SortDialog::new(target, field, order, group_directories))
157 }
158
159 pub fn file_ops_menu(path: kimun_core::nfs::VaultPath) -> Self {
160 ActiveDialog::Menu(FileOpsMenuDialog::new(path))
161 }
162
163 pub fn delete(path: kimun_core::nfs::VaultPath, vault: Arc<NoteVault>) -> Self {
164 ActiveDialog::Delete(DeleteConfirmDialog::new(path, vault))
165 }
166
167 pub fn rename(path: kimun_core::nfs::VaultPath, vault: Arc<NoteVault>) -> Self {
168 ActiveDialog::Rename(RenameDialog::new(path, vault))
169 }
170
171 pub fn move_to(path: kimun_core::nfs::VaultPath, vault: Arc<NoteVault>, tx: &AppTx) -> Self {
172 ActiveDialog::Move(MoveDialog::new(path, vault, tx))
173 }
174}
175
176impl Overlay for ActiveDialog {
177 fn kind(&self) -> OverlayKind {
178 OverlayKind::Dialog
179 }
180
181 fn handle_input(&mut self, event: &InputEvent, tx: &AppTx) -> EventState {
182 <Self as Component>::handle_input(self, event, tx)
183 }
184
185 fn handle_app_message(
186 &mut self,
187 msg: &AppEvent,
188 _vault: &Arc<NoteVault>,
189 tx: &AppTx,
190 ) -> OverlayMsg {
191 match msg {
192 AppEvent::RenameValidation { available } => {
193 if let ActiveDialog::Rename(d) = self {
194 d.validation_state = if *available {
195 ValidationState::Available
196 } else {
197 ValidationState::Taken
198 };
199 d.validation_task = None;
200 }
201 OverlayMsg::Consumed
202 }
203 AppEvent::MoveDirectoriesLoaded(paths) => {
204 if let ActiveDialog::Move(d) = self {
205 d.all_dirs = paths.clone();
206 d.filtered = None;
207 d.load_task = None;
208 if d.list_state.selected().is_none() && !d.results().is_empty() {
209 d.list_state.select(Some(0));
210 }
211 d.spawn_validation(tx);
212 }
213 OverlayMsg::Consumed
214 }
215 AppEvent::MoveFilterResults(paths) => {
216 if let ActiveDialog::Move(d) = self {
217 d.filter_task = None;
218 d.filtered = Some(paths.clone());
219 if !d.results().is_empty() {
220 d.list_state.select(Some(0));
221 } else {
222 d.list_state.select(None);
223 }
224 d.spawn_validation(tx);
225 }
226 OverlayMsg::Consumed
227 }
228 AppEvent::MoveDestValidation { available } => {
229 if let ActiveDialog::Move(d) = self {
230 d.dest_validation = if *available {
231 ValidationState::Available
232 } else {
233 ValidationState::Taken
234 };
235 d.validation_task = None;
236 }
237 OverlayMsg::Consumed
238 }
239 AppEvent::SavedSearchNamesLoaded(names) => {
240 if let ActiveDialog::SaveSearch(d) = self {
241 d.set_existing_names(names.clone());
242 }
243 OverlayMsg::Consumed
244 }
245 AppEvent::DialogError(text) => {
246 self.set_error(text.clone());
247 OverlayMsg::Consumed
248 }
249 _ => OverlayMsg::NotConsumed,
250 }
251 }
252
253 fn render(&mut self, f: &mut Frame, area: Rect, theme: &Theme) {
254 <Self as Component>::render(self, f, area, theme, true);
255 }
256}
257
258impl Component for ActiveDialog {
259 fn handle_input(&mut self, event: &InputEvent, tx: &AppTx) -> EventState {
260 let InputEvent::Key(key) = event else {
261 return EventState::NotConsumed;
262 };
263 match self {
264 ActiveDialog::Menu(d) => d.handle_key(*key, tx),
265 ActiveDialog::Delete(d) => d.handle_key(*key, tx),
266 ActiveDialog::Rename(d) => d.handle_key(*key, tx),
267 ActiveDialog::Move(d) => d.handle_key(*key, tx),
268 ActiveDialog::CreateNote(d) => d.handle_key(*key, tx),
269 ActiveDialog::Help(d) => d.handle_key(*key, tx),
270 ActiveDialog::QuickNote(d) => d.handle_key(*key, tx),
271 ActiveDialog::WorkspaceSwitcher(d) => d.handle_key(*key, tx),
272 ActiveDialog::SaveSearch(d) => d.handle_input(event, tx),
273 ActiveDialog::Sort(d) => d.handle_input(event, tx),
274 ActiveDialog::ThemePicker(d) => d.handle_key(*key, tx),
275 ActiveDialog::UpdateAvailable(d) => d.handle_key(*key, tx),
276 }
277 }
278
279 fn render(&mut self, f: &mut Frame, rect: Rect, theme: &Theme, focused: bool) {
280 match self {
281 ActiveDialog::Menu(d) => d.render(f, rect, theme, focused),
282 ActiveDialog::Delete(d) => d.render(f, rect, theme, focused),
283 ActiveDialog::Rename(d) => d.render(f, rect, theme, focused),
284 ActiveDialog::Move(d) => d.render(f, rect, theme, focused),
285 ActiveDialog::CreateNote(d) => d.render(f, rect, theme, focused),
286 ActiveDialog::Help(d) => d.render(f, rect, theme, focused),
287 ActiveDialog::QuickNote(d) => d.render(f, rect, theme, focused),
288 ActiveDialog::WorkspaceSwitcher(d) => d.render(f, rect, theme, focused),
289 ActiveDialog::SaveSearch(d) => d.render(f, rect, theme, focused),
290 ActiveDialog::Sort(d) => d.render(f, rect, theme, focused),
291 ActiveDialog::ThemePicker(d) => d.render(f, rect, theme, focused),
292 ActiveDialog::UpdateAvailable(d) => d.render(f, rect, theme, focused),
293 }
294 }
295}
296
297pub(super) fn render_path_row(f: &mut Frame, rect: Rect, path: &str, fg: Color, bg: Color) {
303 f.render_widget(
304 Paragraph::new(path).style(Style::default().fg(fg).bg(bg)),
305 rect,
306 );
307}
308
309pub(super) fn render_separator(f: &mut Frame, rect: Rect, gray: Color, bg: Color) {
311 Block::default()
312 .borders(Borders::TOP)
313 .border_style(Style::default().fg(gray))
314 .style(Style::default().bg(bg))
315 .render(rect, f.buffer_mut());
316}
317
318pub(super) fn render_error_row(f: &mut Frame, rect: Rect, msg: &str, theme: &Theme) {
320 f.render_widget(
321 Paragraph::new(format!(" Error: {msg}")).style(
322 Style::default()
323 .fg(theme.red.to_ratatui())
324 .bg(theme.bg_panel.to_ratatui()),
325 ),
326 rect,
327 );
328}
329
330pub(super) fn render_confirm_hint(
333 f: &mut Frame,
334 rect: Rect,
335 enter_text: &str,
336 enter_active: bool,
337 fg: Color,
338 gray: Color,
339 bg: Color,
340) {
341 let enter_style = if enter_active {
342 Style::default().fg(fg).bg(bg)
343 } else {
344 Style::default().fg(gray).bg(bg).add_modifier(Modifier::DIM)
345 };
346 let chunks = Layout::default()
347 .direction(Direction::Horizontal)
348 .constraints([
349 Constraint::Length(enter_text.len() as u16 + 1),
350 Constraint::Min(1),
351 ])
352 .split(rect);
353 f.render_widget(Paragraph::new(enter_text).style(enter_style), chunks[0]);
354 f.render_widget(
355 Paragraph::new(" [Esc] Cancel").style(Style::default().fg(gray).bg(bg)),
356 chunks[1],
357 );
358}
359
360pub(super) use crate::components::fixed_centered_rect;
366
367#[cfg(test)]
368mod tests {
369 use super::*;
370 use crate::keys::KeyBindings;
371
372 #[test]
373 fn active_dialog_help_variant_compiles() {
374 let dialog = HelpDialog::new(&KeyBindings::empty());
375 let _active: ActiveDialog = ActiveDialog::Help(dialog);
376 }
377
378 #[test]
379 fn active_dialog_sort_variant_compiles() {
380 use crate::components::events::SortTarget;
381 use crate::components::file_list::{SortField, SortOrder};
382 let _active: ActiveDialog = ActiveDialog::sort(
383 SortTarget::Sidebar,
384 SortField::Name,
385 SortOrder::Ascending,
386 false,
387 );
388 }
389}