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, 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(query: String) -> Self {
102 ActiveDialog::SaveSearch(SaveSearchDialog::new(query))
103 }
104
105 pub fn sort(
106 target: SortTarget,
107 field: SortField,
108 order: SortOrder,
109 group_directories: bool,
110 ) -> Self {
111 ActiveDialog::Sort(SortDialog::new(target, field, order, group_directories))
112 }
113
114 pub fn file_ops_menu(path: kimun_core::nfs::VaultPath) -> Self {
115 ActiveDialog::Menu(FileOpsMenuDialog::new(path))
116 }
117
118 pub fn delete(path: kimun_core::nfs::VaultPath, vault: Arc<NoteVault>) -> Self {
119 ActiveDialog::Delete(DeleteConfirmDialog::new(path, vault))
120 }
121
122 pub fn rename(path: kimun_core::nfs::VaultPath, vault: Arc<NoteVault>) -> Self {
123 ActiveDialog::Rename(RenameDialog::new(path, vault))
124 }
125
126 pub fn move_to(path: kimun_core::nfs::VaultPath, vault: Arc<NoteVault>, tx: &AppTx) -> Self {
127 ActiveDialog::Move(MoveDialog::new(path, vault, tx))
128 }
129}
130
131impl Overlay for ActiveDialog {
132 fn kind(&self) -> OverlayKind {
133 OverlayKind::Dialog
134 }
135
136 fn handle_input(&mut self, event: &InputEvent, tx: &AppTx) -> EventState {
137 <Self as Component>::handle_input(self, event, tx)
138 }
139
140 fn handle_app_message(
141 &mut self,
142 msg: &AppEvent,
143 _vault: &Arc<NoteVault>,
144 tx: &AppTx,
145 ) -> OverlayMsg {
146 match msg {
147 AppEvent::RenameValidation { available } => {
148 if let ActiveDialog::Rename(d) = self {
149 d.validation_state = if *available {
150 ValidationState::Available
151 } else {
152 ValidationState::Taken
153 };
154 d.validation_task = None;
155 }
156 OverlayMsg::Consumed
157 }
158 AppEvent::MoveDirectoriesLoaded(paths) => {
159 if let ActiveDialog::Move(d) = self {
160 d.all_dirs = paths.clone();
161 d.filtered = None;
162 d.load_task = None;
163 if d.list_state.selected().is_none() && !d.results().is_empty() {
164 d.list_state.select(Some(0));
165 }
166 d.spawn_validation(tx);
167 }
168 OverlayMsg::Consumed
169 }
170 AppEvent::MoveFilterResults(paths) => {
171 if let ActiveDialog::Move(d) = self {
172 d.filter_task = None;
173 d.filtered = Some(paths.clone());
174 if !d.results().is_empty() {
175 d.list_state.select(Some(0));
176 } else {
177 d.list_state.select(None);
178 }
179 d.spawn_validation(tx);
180 }
181 OverlayMsg::Consumed
182 }
183 AppEvent::MoveDestValidation { available } => {
184 if let ActiveDialog::Move(d) = self {
185 d.dest_validation = if *available {
186 ValidationState::Available
187 } else {
188 ValidationState::Taken
189 };
190 d.validation_task = None;
191 }
192 OverlayMsg::Consumed
193 }
194 AppEvent::DialogError(text) => {
195 self.set_error(text.clone());
196 OverlayMsg::Consumed
197 }
198 _ => OverlayMsg::NotConsumed,
199 }
200 }
201
202 fn render(&mut self, f: &mut Frame, area: Rect, theme: &Theme) {
203 <Self as Component>::render(self, f, area, theme, true);
204 }
205}
206
207impl Component for ActiveDialog {
208 fn handle_input(&mut self, event: &InputEvent, tx: &AppTx) -> EventState {
209 let InputEvent::Key(key) = event else {
210 return EventState::NotConsumed;
211 };
212 match self {
213 ActiveDialog::Menu(d) => d.handle_key(*key, tx),
214 ActiveDialog::Delete(d) => d.handle_key(*key, tx),
215 ActiveDialog::Rename(d) => d.handle_key(*key, tx),
216 ActiveDialog::Move(d) => d.handle_key(*key, tx),
217 ActiveDialog::CreateNote(d) => d.handle_key(*key, tx),
218 ActiveDialog::Help(d) => d.handle_key(*key, tx),
219 ActiveDialog::QuickNote(d) => d.handle_key(*key, tx),
220 ActiveDialog::WorkspaceSwitcher(d) => d.handle_key(*key, tx),
221 ActiveDialog::SaveSearch(d) => d.handle_input(event, tx),
222 ActiveDialog::Sort(d) => d.handle_input(event, tx),
223 }
224 }
225
226 fn render(&mut self, f: &mut Frame, rect: Rect, theme: &Theme, focused: bool) {
227 match self {
228 ActiveDialog::Menu(d) => d.render(f, rect, theme, focused),
229 ActiveDialog::Delete(d) => d.render(f, rect, theme, focused),
230 ActiveDialog::Rename(d) => d.render(f, rect, theme, focused),
231 ActiveDialog::Move(d) => d.render(f, rect, theme, focused),
232 ActiveDialog::CreateNote(d) => d.render(f, rect, theme, focused),
233 ActiveDialog::Help(d) => d.render(f, rect, theme, focused),
234 ActiveDialog::QuickNote(d) => d.render(f, rect, theme, focused),
235 ActiveDialog::WorkspaceSwitcher(d) => d.render(f, rect, theme, focused),
236 ActiveDialog::SaveSearch(d) => d.render(f, rect, theme, focused),
237 ActiveDialog::Sort(d) => d.render(f, rect, theme, focused),
238 }
239 }
240}
241
242pub(super) fn render_path_row(f: &mut Frame, rect: Rect, path: &str, fg: Color, bg: Color) {
248 f.render_widget(
249 Paragraph::new(path).style(Style::default().fg(fg).bg(bg)),
250 rect,
251 );
252}
253
254pub(super) fn render_separator(f: &mut Frame, rect: Rect, fg_muted: Color, bg: Color) {
256 Block::default()
257 .borders(Borders::TOP)
258 .border_style(Style::default().fg(fg_muted))
259 .style(Style::default().bg(bg))
260 .render(rect, f.buffer_mut());
261}
262
263pub(super) fn render_error_row(f: &mut Frame, rect: Rect, msg: &str, bg: Color) {
265 f.render_widget(
266 Paragraph::new(format!(" Error: {msg}")).style(Style::default().fg(Color::Red).bg(bg)),
267 rect,
268 );
269}
270
271pub(super) fn render_confirm_hint(
274 f: &mut Frame,
275 rect: Rect,
276 enter_text: &str,
277 enter_active: bool,
278 fg: Color,
279 fg_muted: Color,
280 bg: Color,
281) {
282 let enter_style = if enter_active {
283 Style::default().fg(fg).bg(bg)
284 } else {
285 Style::default()
286 .fg(fg_muted)
287 .bg(bg)
288 .add_modifier(Modifier::DIM)
289 };
290 let chunks = Layout::default()
291 .direction(Direction::Horizontal)
292 .constraints([
293 Constraint::Length(enter_text.len() as u16 + 1),
294 Constraint::Min(1),
295 ])
296 .split(rect);
297 f.render_widget(Paragraph::new(enter_text).style(enter_style), chunks[0]);
298 f.render_widget(
299 Paragraph::new(" [Esc] Cancel").style(Style::default().fg(fg_muted).bg(bg)),
300 chunks[1],
301 );
302}
303
304pub(super) fn fixed_centered_rect(
310 width: u16,
311 height: u16,
312 area: ratatui::layout::Rect,
313) -> ratatui::layout::Rect {
314 let w = width.min(area.width);
315 let h = height.min(area.height);
316 ratatui::layout::Rect {
317 x: area.x + (area.width.saturating_sub(w)) / 2,
318 y: area.y + (area.height.saturating_sub(h)) / 2,
319 width: w,
320 height: h,
321 }
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327 use crate::keys::KeyBindings;
328
329 #[test]
330 fn active_dialog_help_variant_compiles() {
331 let dialog = HelpDialog::new(&KeyBindings::empty());
332 let _active: ActiveDialog = ActiveDialog::Help(dialog);
333 }
334
335 #[test]
336 fn active_dialog_sort_variant_compiles() {
337 use crate::components::events::SortTarget;
338 use crate::components::file_list::{SortField, SortOrder};
339 let _active: ActiveDialog = ActiveDialog::sort(
340 SortTarget::Sidebar,
341 SortField::Name,
342 SortOrder::Ascending,
343 false,
344 );
345 }
346}