kimun_notes/components/
dialog_manager.rs1use std::sync::Arc;
2
3use kimun_core::NoteVault;
4use kimun_core::nfs::VaultPath;
5use ratatui::Frame;
6use ratatui::layout::Rect;
7
8use crate::components::Component;
9use crate::components::dialogs::{
10 ActiveDialog, CreateNoteDialog, DeleteConfirmDialog, FileOpsMenuDialog, HelpDialog, MoveDialog,
11 QuickNoteModal, RenameDialog, ValidationState, WorkspaceSwitcherModal,
12};
13use crate::components::event_state::EventState;
14use crate::components::events::{AppEvent, AppTx, InputEvent};
15use crate::keys::KeyBindings;
16use crate::settings::AppSettings;
17use crate::settings::themes::Theme;
18
19pub struct DialogManager {
25 active: Option<ActiveDialog>,
26 saved_focus: Option<u8>,
29}
30
31impl Default for DialogManager {
32 fn default() -> Self {
33 Self::new()
34 }
35}
36
37impl DialogManager {
38 pub fn new() -> Self {
39 Self {
40 active: None,
41 saved_focus: None,
42 }
43 }
44
45 pub fn is_open(&self) -> bool {
46 self.active.is_some()
47 }
48
49 pub fn open(&mut self, dialog: ActiveDialog, current_focus: u8) {
52 if self.saved_focus.is_none() {
53 self.saved_focus = Some(current_focus);
54 }
55 self.active = Some(dialog);
56 }
57
58 pub fn close(&mut self) -> Option<u8> {
60 self.active = None;
61 self.saved_focus.take()
62 }
63
64 pub fn handle_input(&mut self, event: &InputEvent, tx: &AppTx) -> EventState {
65 if let Some(dialog) = &mut self.active {
66 dialog.handle_input(event, tx)
67 } else {
68 EventState::NotConsumed
69 }
70 }
71
72 pub fn render(&mut self, f: &mut Frame, area: Rect, theme: &Theme) {
73 if let Some(dialog) = &mut self.active {
74 dialog.render(f, area, theme, true);
75 }
76 }
77
78 pub fn handle_app_message(
81 &mut self,
82 msg: &AppEvent,
83 vault: &Arc<NoteVault>,
84 tx: &AppTx,
85 current_focus: u8,
86 ) -> bool {
87 match msg {
88 AppEvent::ShowFileOpsMenu(path) => {
89 self.open(
90 ActiveDialog::Menu(FileOpsMenuDialog::new(path.clone())),
91 current_focus,
92 );
93 true
94 }
95 AppEvent::ShowDeleteDialog(path) => {
96 self.open(
97 ActiveDialog::Delete(DeleteConfirmDialog::new(path.clone(), vault.clone())),
98 current_focus,
99 );
100 true
101 }
102 AppEvent::ShowRenameDialog(path) => {
103 self.open(
104 ActiveDialog::Rename(RenameDialog::new(path.clone(), vault.clone())),
105 current_focus,
106 );
107 true
108 }
109 AppEvent::ShowMoveDialog(path) => {
110 self.open(
111 ActiveDialog::Move(MoveDialog::new(path.clone(), vault.clone(), tx)),
112 current_focus,
113 );
114 true
115 }
116 AppEvent::RenameValidation { available } => {
117 if let Some(ActiveDialog::Rename(d)) = &mut self.active {
118 d.validation_state = if *available {
119 ValidationState::Available
120 } else {
121 ValidationState::Taken
122 };
123 d.validation_task = None;
124 }
125 true
126 }
127 AppEvent::MoveDirectoriesLoaded(paths) => {
128 if let Some(ActiveDialog::Move(d)) = &mut self.active {
129 d.all_dirs = paths.clone();
130 d.filtered = None;
131 d.load_task = None;
132 if d.list_state.selected().is_none() && !d.results().is_empty() {
133 d.list_state.select(Some(0));
134 }
135 d.spawn_validation(tx);
136 }
137 true
138 }
139 AppEvent::MoveFilterResults(paths) => {
140 if let Some(ActiveDialog::Move(d)) = &mut self.active {
141 d.filter_task = None;
142 d.filtered = Some(paths.clone());
143 if !d.results().is_empty() {
144 d.list_state.select(Some(0));
145 } else {
146 d.list_state.select(None);
147 }
148 d.spawn_validation(tx);
149 }
150 true
151 }
152 AppEvent::MoveDestValidation { available } => {
153 if let Some(ActiveDialog::Move(d)) = &mut self.active {
154 d.dest_validation = if *available {
155 ValidationState::Available
156 } else {
157 ValidationState::Taken
158 };
159 d.validation_task = None;
160 }
161 true
162 }
163 AppEvent::DialogError(msg) => {
164 if let Some(dialog) = &mut self.active {
165 dialog.set_error(msg.clone());
166 }
167 true
168 }
169 AppEvent::CloseDialog => {
170 self.close();
171 true
172 }
173 _ => false,
174 }
175 }
176
177 pub fn open_help(&mut self, key_bindings: &KeyBindings, current_focus: u8) {
179 self.open(
180 ActiveDialog::Help(HelpDialog::new(key_bindings)),
181 current_focus,
182 );
183 }
184
185 pub fn open_quick_note(&mut self, vault: Arc<NoteVault>, current_focus: u8) {
186 self.open(
187 ActiveDialog::QuickNote(QuickNoteModal::new(vault)),
188 current_focus,
189 );
190 }
191
192 pub fn open_workspace_switcher(&mut self, settings: &AppSettings, current_focus: u8) {
193 self.open(
194 ActiveDialog::WorkspaceSwitcher(WorkspaceSwitcherModal::new(settings)),
195 current_focus,
196 );
197 }
198
199 pub fn open_create_note(&mut self, path: VaultPath, vault: Arc<NoteVault>, current_focus: u8) {
201 self.open(
202 ActiveDialog::CreateNote(CreateNoteDialog::new(path, vault)),
203 current_focus,
204 );
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211 use kimun_core::VaultConfig;
212
213 #[test]
214 fn new_is_not_open() {
215 let dm = DialogManager::new();
216 assert!(!dm.is_open());
217 }
218
219 #[test]
220 fn open_then_close_returns_focus() {
221 let mut dm = DialogManager::new();
222 let kb = KeyBindings::empty();
223 dm.open_help(&kb, 42);
224 assert!(dm.is_open());
225 let restored = dm.close();
226 assert_eq!(restored, Some(42));
227 assert!(!dm.is_open());
228 }
229
230 #[tokio::test]
231 async fn chained_dialogs_preserve_original_focus() {
232 let mut dm = DialogManager::new();
233 let path = VaultPath::new("test");
234 dm.open(ActiveDialog::Menu(FileOpsMenuDialog::new(path.clone())), 1);
235 let dir = std::env::temp_dir().join("kimun_dm_test");
237 std::fs::create_dir_all(&dir).ok();
238 let vault = Arc::new(NoteVault::new(VaultConfig::new(&dir)).await.unwrap());
239 dm.open(
240 ActiveDialog::Delete(DeleteConfirmDialog::new(path, vault)),
241 99, );
243 let restored = dm.close();
244 assert_eq!(restored, Some(1)); }
246
247 #[test]
248 fn close_when_empty_returns_none() {
249 let mut dm = DialogManager::new();
250 assert_eq!(dm.close(), None);
251 }
252}