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