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