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