1use crate::_private::NonExhaustive;
6use crate::button::{Button, ButtonState, ButtonStyle};
7use crate::event::{ButtonOutcome, FileOutcome, TextOutcome};
8use crate::layout::{DialogItem, LayoutOuter, layout_as_grid};
9use crate::list::edit::{EditList, EditListState};
10use crate::list::selection::{RowSelection, RowSetSelection};
11use crate::list::{List, ListState, ListStyle};
12use crate::util::{block_padding2, reset_buf_area};
13use crossterm::event::{Event, MouseEvent};
14#[cfg(feature = "user_directories")]
15use dirs::{document_dir, home_dir};
16use log::debug;
17use rat_event::{Dialog, HandleEvent, MouseOnly, Outcome, Regular, ct_event, event_flow};
18use rat_focus::{Focus, FocusBuilder, FocusFlag, HasFocus, Navigation, on_lost};
19use rat_ftable::event::EditOutcome;
20use rat_reloc::RelocatableState;
21use rat_scrolled::Scroll;
22use rat_text::text_input::{TextInput, TextInputState};
23use rat_text::{HasScreenCursor, TextStyle};
24use ratatui::buffer::Buffer;
25use ratatui::layout::{Alignment, Constraint, Direction, Flex, Layout, Position, Rect, Size};
26use ratatui::style::Style;
27use ratatui::text::Text;
28use ratatui::widgets::{Block, ListItem};
29use ratatui::widgets::{StatefulWidget, Widget};
30use std::cmp::max;
31use std::collections::HashSet;
32use std::ffi::OsString;
33use std::fmt::{Debug, Formatter};
34use std::path::{Path, PathBuf};
35use std::{fs, io};
36#[cfg(feature = "user_directories")]
37use sysinfo::Disks;
38
39#[derive(Debug, Clone)]
60pub struct FileDialog<'a> {
61 style: Style,
62 block: Option<Block<'a>>,
63 list_style: Option<ListStyle>,
64 roots_style: Option<ListStyle>,
65 text_style: Option<TextStyle>,
66 button_style: Option<ButtonStyle>,
67
68 layout: LayoutOuter,
69
70 ok_text: &'a str,
71 cancel_text: &'a str,
72}
73
74#[derive(Debug, Clone)]
76pub struct FileDialogStyle {
77 pub style: Style,
78 pub block: Option<Block<'static>>,
80 pub border_style: Option<Style>,
81 pub title_style: Option<Style>,
82 pub layout: Option<LayoutOuter>,
84 pub list: Option<ListStyle>,
86 pub roots: Option<ListStyle>,
88 pub text: Option<TextStyle>,
90 pub button: Option<ButtonStyle>,
92
93 pub non_exhaustive: NonExhaustive,
94}
95
96#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
98#[allow(dead_code)]
99enum Mode {
100 #[default]
101 Open,
102 OpenMany,
103 Save,
104 Dir,
105}
106
107#[derive(Debug, Clone)]
108enum FileStateMode {
109 Open(ListState<RowSelection>),
110 OpenMany(ListState<RowSetSelection>),
111 Save(ListState<RowSelection>),
112 Dir(FocusFlag),
113}
114
115impl Default for FileStateMode {
116 fn default() -> Self {
117 Self::Open(Default::default())
118 }
119}
120
121impl HasFocus for FileStateMode {
122 fn build(&self, builder: &mut FocusBuilder) {
123 match self {
124 FileStateMode::Open(st) => {
125 builder.widget(st);
126 }
127 FileStateMode::OpenMany(st) => {
128 builder.widget(st);
129 }
130 FileStateMode::Save(st) => {
131 builder.widget(st);
132 }
133 FileStateMode::Dir(f) => {
134 builder.widget_navigate(f, Navigation::None);
135 }
136 }
137 }
138
139 fn focus(&self) -> FocusFlag {
140 match self {
141 FileStateMode::Open(st) => st.focus(),
142 FileStateMode::OpenMany(st) => st.focus(),
143 FileStateMode::Save(st) => st.focus(),
144 FileStateMode::Dir(f) => f.clone(),
145 }
146 }
147
148 fn area(&self) -> Rect {
149 match self {
150 FileStateMode::Open(st) => st.area(),
151 FileStateMode::OpenMany(st) => st.area(),
152 FileStateMode::Save(st) => st.area(),
153 FileStateMode::Dir(_) => Rect::default(),
154 }
155 }
156}
157
158impl FileStateMode {
159 pub(crate) fn is_double_click(&self, m: &MouseEvent) -> bool {
160 match self {
161 FileStateMode::Open(st) => st.mouse.doubleclick(st.inner, m),
162 FileStateMode::OpenMany(st) => st.mouse.doubleclick(st.inner, m),
163 FileStateMode::Save(st) => st.mouse.doubleclick(st.inner, m),
164 FileStateMode::Dir(_) => false,
165 }
166 }
167
168 pub(crate) fn set_offset(&mut self, n: usize) {
169 match self {
170 FileStateMode::Open(st) => {
171 st.set_offset(n);
172 }
173 FileStateMode::OpenMany(st) => {
174 st.set_offset(n);
175 }
176 FileStateMode::Save(st) => {
177 st.set_offset(n);
178 }
179 FileStateMode::Dir(_) => {}
180 }
181 }
182
183 pub(crate) fn first_selected(&self) -> Option<usize> {
184 match self {
185 FileStateMode::Open(st) => st.selected(),
186 FileStateMode::OpenMany(st) => st.lead(),
187 FileStateMode::Save(st) => st.selected(),
188 FileStateMode::Dir(_) => None,
189 }
190 }
191
192 pub(crate) fn selected(&self) -> HashSet<usize> {
193 match self {
194 FileStateMode::Open(st) => {
195 let mut sel = HashSet::new();
196 if let Some(v) = st.selected() {
197 sel.insert(v);
198 }
199 sel
200 }
201 FileStateMode::OpenMany(st) => st.selected(),
202 FileStateMode::Save(st) => {
203 let mut sel = HashSet::new();
204 if let Some(v) = st.selected() {
205 sel.insert(v);
206 }
207 sel
208 }
209 FileStateMode::Dir(_) => Default::default(),
210 }
211 }
212
213 pub(crate) fn select(&mut self, select: Option<usize>) {
214 match self {
215 FileStateMode::Open(st) => {
216 st.select(select);
217 }
218 FileStateMode::OpenMany(st) => {
219 st.set_lead(select, false);
220 }
221 FileStateMode::Save(st) => {
222 st.select(select);
223 }
224 FileStateMode::Dir(_) => {}
225 }
226 }
227
228 pub(crate) fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
229 match self {
230 FileStateMode::Open(st) => st.relocate(shift, clip),
231 FileStateMode::OpenMany(st) => st.relocate(shift, clip),
232 FileStateMode::Save(st) => st.relocate(shift, clip),
233 FileStateMode::Dir(_) => {}
234 }
235 }
236}
237
238#[expect(clippy::type_complexity)]
240pub struct FileDialogState {
241 pub area: Rect,
244 pub active: bool,
246
247 mode: Mode,
248
249 path: PathBuf,
250 save_name: Option<OsString>,
251 save_ext: Option<OsString>,
252 dirs: Vec<OsString>,
253 filter: Option<Box<dyn Fn(&Path) -> bool + 'static>>,
254 files: Vec<OsString>,
255 no_default_roots: bool,
256 roots: Vec<(OsString, PathBuf)>,
257
258 path_state: TextInputState,
259 root_state: ListState<RowSelection>,
260 dir_state: EditListState<EditDirNameState>,
261 file_state: FileStateMode,
262 save_name_state: TextInputState,
263 new_state: ButtonState,
264 cancel_state: ButtonState,
265 ok_state: ButtonState,
266}
267
268pub(crate) mod event {
269 use rat_event::{ConsumedEvent, Outcome};
270 use std::path::PathBuf;
271
272 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
274 pub enum FileOutcome {
275 Continue,
277 Unchanged,
280 Changed,
285 Cancel,
287 Ok(PathBuf),
289 OkList(Vec<PathBuf>),
291 }
292
293 impl ConsumedEvent for FileOutcome {
294 fn is_consumed(&self) -> bool {
295 !matches!(self, FileOutcome::Continue)
296 }
297 }
298
299 impl From<FileOutcome> for Outcome {
300 fn from(value: FileOutcome) -> Self {
301 match value {
302 FileOutcome::Continue => Outcome::Continue,
303 FileOutcome::Unchanged => Outcome::Unchanged,
304 FileOutcome::Changed => Outcome::Changed,
305 FileOutcome::Ok(_) => Outcome::Changed,
306 FileOutcome::Cancel => Outcome::Changed,
307 FileOutcome::OkList(_) => Outcome::Changed,
308 }
309 }
310 }
311
312 impl From<Outcome> for FileOutcome {
313 fn from(value: Outcome) -> Self {
314 match value {
315 Outcome::Continue => FileOutcome::Continue,
316 Outcome::Unchanged => FileOutcome::Unchanged,
317 Outcome::Changed => FileOutcome::Changed,
318 }
319 }
320 }
321
322 impl From<bool> for FileOutcome {
324 fn from(value: bool) -> Self {
325 if value {
326 FileOutcome::Changed
327 } else {
328 FileOutcome::Unchanged
329 }
330 }
331 }
332}
333
334impl Clone for FileDialogState {
335 fn clone(&self) -> Self {
336 Self {
337 area: self.area,
338 active: self.active,
339 mode: self.mode,
340 path: self.path.clone(),
341 save_name: self.save_name.clone(),
342 save_ext: self.save_ext.clone(),
343 dirs: self.dirs.clone(),
344 filter: None,
345 files: self.files.clone(),
346 no_default_roots: self.no_default_roots,
347 roots: self.roots.clone(),
348 path_state: self.path_state.clone(),
349 root_state: self.root_state.clone(),
350 dir_state: self.dir_state.clone(),
351 file_state: self.file_state.clone(),
352 save_name_state: self.save_name_state.clone(),
353 new_state: self.new_state.clone(),
354 cancel_state: self.cancel_state.clone(),
355 ok_state: self.ok_state.clone(),
356 }
357 }
358}
359
360impl Debug for FileDialogState {
361 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
362 f.debug_struct("FileOpenState")
363 .field("active", &self.active)
364 .field("mode", &self.mode)
365 .field("path", &self.path)
366 .field("save_name", &self.save_name)
367 .field("dirs", &self.dirs)
368 .field("files", &self.files)
369 .field("no_default_roots", &self.no_default_roots)
370 .field("roots", &self.roots)
371 .field("path_state", &self.path_state)
372 .field("root_state", &self.root_state)
373 .field("dir_state", &self.dir_state)
374 .field("file_state", &self.file_state)
375 .field("name_state", &self.save_name_state)
376 .field("cancel_state", &self.cancel_state)
377 .field("ok_state", &self.ok_state)
378 .finish()
379 }
380}
381
382impl Default for FileDialogStyle {
383 fn default() -> Self {
384 FileDialogStyle {
385 style: Default::default(),
386 layout: Default::default(),
387 list: Default::default(),
388 roots: Default::default(),
389 button: Default::default(),
390 block: Default::default(),
391 border_style: Default::default(),
392 text: Default::default(),
393 non_exhaustive: NonExhaustive,
394 title_style: Default::default(),
395 }
396 }
397}
398
399impl Default for FileDialogState {
400 fn default() -> Self {
401 Self {
402 area: Default::default(),
403 active: Default::default(),
404 mode: Default::default(),
405 path: Default::default(),
406 save_name: Default::default(),
407 save_ext: Default::default(),
408 dirs: Default::default(),
409 filter: Default::default(),
410 files: Default::default(),
411 no_default_roots: Default::default(),
412 roots: Default::default(),
413 path_state: Default::default(),
414 root_state: Default::default(),
415 dir_state: Default::default(),
416 file_state: Default::default(),
417 save_name_state: Default::default(),
418 new_state: Default::default(),
419 cancel_state: Default::default(),
420 ok_state: Default::default(),
421 }
422 }
423}
424
425impl<'a> Default for FileDialog<'a> {
426 fn default() -> Self {
427 Self {
428 block: Default::default(),
429 style: Default::default(),
430 layout: Default::default(),
431 list_style: Default::default(),
432 roots_style: Default::default(),
433 text_style: Default::default(),
434 button_style: Default::default(),
435 ok_text: "Ok",
436 cancel_text: "Cancel",
437 }
438 }
439}
440
441impl<'a> FileDialog<'a> {
442 pub fn new() -> Self {
444 Self::default()
445 }
446
447 pub fn ok_text(mut self, txt: &'a str) -> Self {
449 self.ok_text = txt;
450 self
451 }
452
453 pub fn cancel_text(mut self, txt: &'a str) -> Self {
455 self.cancel_text = txt;
456 self
457 }
458
459 pub fn block(mut self, block: Block<'a>) -> Self {
461 self.block = Some(block);
462 self.block = self.block.map(|v| v.style(self.style));
463 self
464 }
465
466 pub fn style(mut self, style: Style) -> Self {
468 self.style = style;
469 self.block = self.block.map(|v| v.style(style));
470 self
471 }
472
473 pub fn list_style(mut self, style: ListStyle) -> Self {
475 self.list_style = Some(style);
476 self
477 }
478
479 pub fn roots_style(mut self, style: ListStyle) -> Self {
481 self.roots_style = Some(style);
482 self
483 }
484
485 pub fn text_style(mut self, style: TextStyle) -> Self {
487 self.text_style = Some(style);
488 self
489 }
490
491 pub fn button_style(mut self, style: ButtonStyle) -> Self {
493 self.button_style = Some(style);
494 self
495 }
496
497 pub fn left(mut self, left: Constraint) -> Self {
499 self.layout = self.layout.left(left);
500 self
501 }
502
503 pub fn top(mut self, top: Constraint) -> Self {
505 self.layout = self.layout.top(top);
506 self
507 }
508
509 pub fn right(mut self, right: Constraint) -> Self {
511 self.layout = self.layout.right(right);
512 self
513 }
514
515 pub fn bottom(mut self, bottom: Constraint) -> Self {
517 self.layout = self.layout.bottom(bottom);
518 self
519 }
520
521 pub fn position(mut self, pos: Position) -> Self {
523 self.layout = self.layout.position(pos);
524 self
525 }
526
527 pub fn width(mut self, width: Constraint) -> Self {
529 self.layout = self.layout.width(width);
530 self
531 }
532
533 pub fn height(mut self, height: Constraint) -> Self {
535 self.layout = self.layout.height(height);
536 self
537 }
538
539 pub fn size(mut self, size: Size) -> Self {
541 self.layout = self.layout.size(size);
542 self
543 }
544
545 pub fn styles(mut self, styles: FileDialogStyle) -> Self {
547 self.style = styles.style;
548 if styles.block.is_some() {
549 self.block = styles.block;
550 }
551 if let Some(border_style) = styles.border_style {
552 self.block = self.block.map(|v| v.border_style(border_style));
553 }
554 if let Some(title_style) = styles.title_style {
555 self.block = self.block.map(|v| v.title_style(title_style));
556 }
557 self.block = self.block.map(|v| v.style(self.style));
558 if let Some(layout) = styles.layout {
559 self.layout = layout;
560 }
561 if styles.list.is_some() {
562 self.list_style = styles.list;
563 }
564 if styles.roots.is_some() {
565 self.roots_style = styles.roots;
566 }
567 if styles.text.is_some() {
568 self.text_style = styles.text;
569 }
570 if styles.button.is_some() {
571 self.button_style = styles.button;
572 }
573 self
574 }
575}
576
577#[derive(Debug, Default)]
578struct EditDirName<'a> {
579 edit_dir: TextInput<'a>,
580}
581
582#[derive(Debug, Default, Clone)]
583struct EditDirNameState {
584 edit_dir: TextInputState,
585}
586
587impl StatefulWidget for EditDirName<'_> {
588 type State = EditDirNameState;
589
590 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
591 self.edit_dir.render(area, buf, &mut state.edit_dir);
592 }
593}
594
595impl HasScreenCursor for EditDirNameState {
596 fn screen_cursor(&self) -> Option<(u16, u16)> {
597 self.edit_dir.screen_cursor()
598 }
599}
600
601impl RelocatableState for EditDirNameState {
602 fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
603 self.edit_dir.relocate(shift, clip);
604 }
605}
606
607impl HandleEvent<Event, Regular, EditOutcome> for EditDirNameState {
608 fn handle(&mut self, event: &Event, qualifier: Regular) -> EditOutcome {
609 match self.edit_dir.handle(event, qualifier) {
610 TextOutcome::Continue => EditOutcome::Continue,
611 TextOutcome::Unchanged => EditOutcome::Unchanged,
612 TextOutcome::Changed => EditOutcome::Changed,
613 TextOutcome::TextChanged => EditOutcome::Changed,
614 }
615 }
616}
617
618impl HandleEvent<Event, MouseOnly, EditOutcome> for EditDirNameState {
619 fn handle(&mut self, event: &Event, qualifier: MouseOnly) -> EditOutcome {
620 match self.edit_dir.handle(event, qualifier) {
621 TextOutcome::Continue => EditOutcome::Continue,
622 TextOutcome::Unchanged => EditOutcome::Unchanged,
623 TextOutcome::Changed => EditOutcome::Changed,
624 TextOutcome::TextChanged => EditOutcome::Changed,
625 }
626 }
627}
628
629impl HasFocus for EditDirNameState {
630 fn build(&self, builder: &mut FocusBuilder) {
631 builder.leaf_widget(self);
632 }
633
634 fn focus(&self) -> FocusFlag {
635 self.edit_dir.focus()
636 }
637
638 fn area(&self) -> Rect {
639 self.edit_dir.area()
640 }
641}
642
643impl StatefulWidget for FileDialog<'_> {
644 type State = FileDialogState;
645
646 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
647 state.area = area;
648
649 if !state.active {
650 return;
651 }
652
653 let block;
654 let block = if let Some(block) = self.block.as_ref() {
655 block
656 } else {
657 block = Block::bordered()
658 .title(match state.mode {
659 Mode::Open => " Open ",
660 Mode::OpenMany => " Open ",
661 Mode::Save => " Save ",
662 Mode::Dir => " Directory ",
663 })
664 .style(self.style);
665 &block
666 };
667
668 let layout = self.layout.layout_dialog(
669 area,
670 block_padding2(block),
671 [
672 Constraint::Percentage(20),
673 Constraint::Percentage(30),
674 Constraint::Percentage(50),
675 ],
676 0,
677 Flex::Center,
678 );
679 state.area = layout.area();
680
681 reset_buf_area(layout.area(), buf);
682 block.render(area, buf);
683
684 match state.mode {
685 Mode::Open => {
686 render_open(&self, layout.widget_for(DialogItem::Content), buf, state);
687 }
688 Mode::OpenMany => {
689 render_open_many(&self, layout.widget_for(DialogItem::Content), buf, state);
690 }
691 Mode::Save => {
692 render_save(&self, layout.widget_for(DialogItem::Content), buf, state);
693 }
694 Mode::Dir => {
695 render_open_dir(&self, layout.widget_for(DialogItem::Content), buf, state);
696 }
697 }
698
699 let mut l_n = layout.widget_for(DialogItem::Button(1));
700 l_n.width = 10;
701 Button::new(Text::from("New").alignment(Alignment::Center))
702 .styles_opt(self.button_style.clone())
703 .render(l_n, buf, &mut state.new_state);
704
705 let l_oc = Layout::horizontal([Constraint::Length(10), Constraint::Length(10)])
706 .spacing(1)
707 .flex(Flex::End)
708 .split(layout.widget_for(DialogItem::Button(2)));
709
710 Button::new(Text::from(self.cancel_text).alignment(Alignment::Center))
711 .styles_opt(self.button_style.clone())
712 .render(l_oc[0], buf, &mut state.cancel_state);
713
714 Button::new(Text::from(self.ok_text).alignment(Alignment::Center))
715 .styles_opt(self.button_style.clone())
716 .render(l_oc[1], buf, &mut state.ok_state);
717 }
718}
719
720fn render_open_dir(
721 widget: &FileDialog<'_>,
722 area: Rect,
723 buf: &mut Buffer,
724 state: &mut FileDialogState,
725) {
726 let l_grid = layout_as_grid(
727 area,
728 Layout::horizontal([
729 Constraint::Percentage(20), Constraint::Percentage(80),
731 ]),
732 Layout::vertical([
733 Constraint::Length(1), Constraint::Fill(1),
735 ]),
736 );
737
738 let mut l_path = l_grid.widget_for((1, 0));
740 l_path.width = l_path.width.saturating_sub(1);
741 TextInput::new()
742 .styles_opt(widget.text_style.clone())
743 .render(l_path, buf, &mut state.path_state);
744
745 List::default()
746 .items(state.roots.iter().map(|v| {
747 let s = v.0.to_string_lossy();
748 ListItem::from(format!("{}", s))
749 }))
750 .scroll(Scroll::new())
751 .styles_opt(widget.roots_style.clone())
752 .render(l_grid.widget_for((0, 1)), buf, &mut state.root_state);
753
754 EditList::new(
755 List::default()
756 .items(state.dirs.iter().map(|v| {
757 let s = v.to_string_lossy();
758 ListItem::from(s)
759 }))
760 .scroll(Scroll::new())
761 .styles_opt(widget.list_style.clone()),
762 EditDirName {
763 edit_dir: TextInput::new().styles_opt(widget.text_style.clone()),
764 },
765 )
766 .render(l_grid.widget_for((1, 1)), buf, &mut state.dir_state);
767}
768
769fn render_open(widget: &FileDialog<'_>, area: Rect, buf: &mut Buffer, state: &mut FileDialogState) {
770 let l_grid = layout_as_grid(
771 area,
772 Layout::horizontal([
773 Constraint::Percentage(20),
774 Constraint::Percentage(30),
775 Constraint::Percentage(50),
776 ]),
777 Layout::new(
778 Direction::Vertical,
779 [Constraint::Length(1), Constraint::Fill(1)],
780 ),
781 );
782
783 let mut l_path = l_grid.widget_for((1, 0)).union(l_grid.widget_for((2, 0)));
785 l_path.width = l_path.width.saturating_sub(1);
786 TextInput::new()
787 .styles_opt(widget.text_style.clone())
788 .render(l_path, buf, &mut state.path_state);
789
790 List::default()
791 .items(state.roots.iter().map(|v| {
792 let s = v.0.to_string_lossy();
793 ListItem::from(format!("{}", s))
794 }))
795 .scroll(Scroll::new())
796 .styles_opt(widget.roots_style.clone())
797 .render(l_grid.widget_for((0, 1)), buf, &mut state.root_state);
798
799 EditList::new(
800 List::default()
801 .items(state.dirs.iter().map(|v| {
802 let s = v.to_string_lossy();
803 ListItem::from(s)
804 }))
805 .scroll(Scroll::new())
806 .styles_opt(widget.list_style.clone()),
807 EditDirName {
808 edit_dir: TextInput::new().styles_opt(widget.text_style.clone()),
809 },
810 )
811 .render(l_grid.widget_for((1, 1)), buf, &mut state.dir_state);
812
813 let FileStateMode::Open(file_state) = &mut state.file_state else {
814 panic!("invalid mode");
815 };
816 List::default()
817 .items(state.files.iter().map(|v| {
818 let s = v.to_string_lossy();
819 ListItem::from(s)
820 }))
821 .scroll(Scroll::new())
822 .styles_opt(widget.list_style.clone())
823 .render(l_grid.widget_for((2, 1)), buf, file_state);
824}
825
826fn render_open_many(
827 widget: &FileDialog<'_>,
828 area: Rect,
829 buf: &mut Buffer,
830 state: &mut FileDialogState,
831) {
832 let l_grid = layout_as_grid(
833 area,
834 Layout::horizontal([
835 Constraint::Percentage(20),
836 Constraint::Percentage(30),
837 Constraint::Percentage(50),
838 ]),
839 Layout::new(
840 Direction::Vertical,
841 [Constraint::Length(1), Constraint::Fill(1)],
842 ),
843 );
844
845 let mut l_path = l_grid.widget_for((1, 0)).union(l_grid.widget_for((2, 0)));
847 l_path.width = l_path.width.saturating_sub(1);
848 TextInput::new()
849 .styles_opt(widget.text_style.clone())
850 .render(l_path, buf, &mut state.path_state);
851
852 List::default()
853 .items(state.roots.iter().map(|v| {
854 let s = v.0.to_string_lossy();
855 ListItem::from(format!("{}", s))
856 }))
857 .scroll(Scroll::new())
858 .styles_opt(widget.roots_style.clone())
859 .render(l_grid.widget_for((0, 1)), buf, &mut state.root_state);
860
861 EditList::new(
862 List::default()
863 .items(state.dirs.iter().map(|v| {
864 let s = v.to_string_lossy();
865 ListItem::from(s)
866 }))
867 .scroll(Scroll::new())
868 .styles_opt(widget.list_style.clone()),
869 EditDirName {
870 edit_dir: TextInput::new().styles_opt(widget.text_style.clone()),
871 },
872 )
873 .render(l_grid.widget_for((1, 1)), buf, &mut state.dir_state);
874
875 let FileStateMode::OpenMany(file_state) = &mut state.file_state else {
876 panic!("invalid mode");
877 };
878 List::default()
879 .items(state.files.iter().map(|v| {
880 let s = v.to_string_lossy();
881 ListItem::from(s)
882 }))
883 .scroll(Scroll::new())
884 .styles_opt(widget.list_style.clone())
885 .render(l_grid.widget_for((2, 1)), buf, file_state);
886}
887
888fn render_save(widget: &FileDialog<'_>, area: Rect, buf: &mut Buffer, state: &mut FileDialogState) {
889 let l_grid = layout_as_grid(
890 area,
891 Layout::horizontal([
892 Constraint::Percentage(20),
893 Constraint::Percentage(30),
894 Constraint::Percentage(50),
895 ]),
896 Layout::new(
897 Direction::Vertical,
898 [
899 Constraint::Length(1),
900 Constraint::Fill(1),
901 Constraint::Length(1),
902 ],
903 ),
904 );
905
906 let mut l_path = l_grid.widget_for((1, 0)).union(l_grid.widget_for((2, 0)));
908 l_path.width = l_path.width.saturating_sub(1);
909 TextInput::new()
910 .styles_opt(widget.text_style.clone())
911 .render(l_path, buf, &mut state.path_state);
912
913 List::default()
914 .items(state.roots.iter().map(|v| {
915 let s = v.0.to_string_lossy();
916 ListItem::from(format!("{}", s))
917 }))
918 .scroll(Scroll::new())
919 .styles_opt(widget.roots_style.clone())
920 .render(l_grid.widget_for((0, 1)), buf, &mut state.root_state);
921
922 EditList::new(
923 List::default()
924 .items(state.dirs.iter().map(|v| {
925 let s = v.to_string_lossy();
926 ListItem::from(s)
927 }))
928 .scroll(Scroll::new())
929 .styles_opt(widget.list_style.clone()),
930 EditDirName {
931 edit_dir: TextInput::new().styles_opt(widget.text_style.clone()),
932 },
933 )
934 .render(l_grid.widget_for((1, 1)), buf, &mut state.dir_state);
935
936 let FileStateMode::Save(file_state) = &mut state.file_state else {
937 panic!("invalid mode");
938 };
939 List::default()
940 .items(state.files.iter().map(|v| {
941 let s = v.to_string_lossy();
942 ListItem::from(s)
943 }))
944 .scroll(Scroll::new())
945 .styles_opt(widget.list_style.clone())
946 .render(l_grid.widget_for((2, 1)), buf, file_state);
947
948 TextInput::new()
949 .styles_opt(widget.text_style.clone())
950 .render(l_grid.widget_for((2, 2)), buf, &mut state.save_name_state);
951}
952
953impl FileDialogState {
954 pub fn new() -> Self {
955 Self::default()
956 }
957
958 pub fn active(&self) -> bool {
959 self.active
960 }
961
962 pub fn set_filter(&mut self, filter: impl Fn(&Path) -> bool + 'static) {
964 self.filter = Some(Box::new(filter));
965 }
966
967 pub fn set_last_path(&mut self, last: &Path) {
972 self.path = last.into();
973 }
974
975 pub fn use_default_roots(&mut self, roots: bool) {
977 self.no_default_roots = !roots;
978 }
979
980 pub fn no_default_roots(&mut self) {
982 self.no_default_roots = true;
983 }
984
985 pub fn add_root(&mut self, name: impl AsRef<str>, path: impl Into<PathBuf>) {
987 self.roots
988 .push((OsString::from(name.as_ref()), path.into()))
989 }
990
991 pub fn clear_roots(&mut self) {
993 self.roots.clear();
994 }
995
996 pub fn default_roots(&mut self, start: &Path, last: &Path) {
998 if last.exists() {
999 self.roots.push((
1000 OsString::from("Last"), last.into(),
1002 ));
1003 }
1004 self.roots.push((
1005 OsString::from("Start"), start.into(),
1007 ));
1008
1009 #[cfg(feature = "user_directories")]
1010 {
1011 if let Some(home) = home_dir() {
1012 self.roots.push((OsString::from("Home"), home));
1013 }
1014 if let Some(documents) = document_dir() {
1015 self.roots.push((OsString::from("Documents"), documents));
1016 }
1017 }
1018
1019 #[cfg(feature = "user_directories")]
1020 {
1021 let disks = Disks::new_with_refreshed_list();
1022 for d in disks.list() {
1023 self.roots
1024 .push((d.name().to_os_string(), d.mount_point().to_path_buf()));
1025 }
1026 }
1027
1028 self.root_state.select(Some(0));
1029 }
1030
1031 pub fn directory_dialog(&mut self, path: impl AsRef<Path>) -> Result<(), io::Error> {
1033 let path = path.as_ref();
1034 let old_path = self.path.clone();
1035
1036 self.active = true;
1037 self.mode = Mode::Dir;
1038 self.file_state = FileStateMode::Dir(FocusFlag::new());
1039 self.save_name = None;
1040 self.save_ext = None;
1041 self.dirs.clear();
1042 self.files.clear();
1043 self.path = Default::default();
1044 if !self.no_default_roots {
1045 self.clear_roots();
1046 self.default_roots(path, &old_path);
1047 if old_path.exists() {
1048 self.set_path(&old_path)?;
1049 } else {
1050 self.set_path(path)?;
1051 }
1052 } else {
1053 self.set_path(path)?;
1054 }
1055 self.build_focus().focus(&self.dir_state);
1056 Ok(())
1057 }
1058
1059 pub fn open_dialog(&mut self, path: impl AsRef<Path>) -> Result<(), io::Error> {
1061 let path = path.as_ref();
1062 let old_path = self.path.clone();
1063
1064 self.active = true;
1065 self.mode = Mode::Open;
1066 self.file_state = FileStateMode::Open(Default::default());
1067 self.save_name = None;
1068 self.save_ext = None;
1069 self.dirs.clear();
1070 self.files.clear();
1071 self.path = Default::default();
1072 if !self.no_default_roots {
1073 self.clear_roots();
1074 self.default_roots(path, &old_path);
1075 if old_path.exists() {
1076 self.set_path(&old_path)?;
1077 } else {
1078 self.set_path(path)?;
1079 }
1080 } else {
1081 self.set_path(path)?;
1082 }
1083 self.build_focus().focus(&self.file_state);
1084 Ok(())
1085 }
1086
1087 pub fn open_many_dialog(&mut self, path: impl AsRef<Path>) -> Result<(), io::Error> {
1089 let path = path.as_ref();
1090 let old_path = self.path.clone();
1091
1092 self.active = true;
1093 self.mode = Mode::OpenMany;
1094 self.file_state = FileStateMode::OpenMany(Default::default());
1095 self.save_name = None;
1096 self.save_ext = None;
1097 self.dirs.clear();
1098 self.files.clear();
1099 self.path = Default::default();
1100 if !self.no_default_roots {
1101 self.clear_roots();
1102 self.default_roots(path, &old_path);
1103 if old_path.exists() {
1104 self.set_path(&old_path)?;
1105 } else {
1106 self.set_path(path)?;
1107 }
1108 } else {
1109 self.set_path(path)?;
1110 }
1111 self.build_focus().focus(&self.file_state);
1112 Ok(())
1113 }
1114
1115 pub fn save_dialog(
1117 &mut self,
1118 path: impl AsRef<Path>,
1119 name: impl AsRef<str>,
1120 ) -> Result<(), io::Error> {
1121 self.save_dialog_ext(path, name, "")
1122 }
1123
1124 pub fn save_dialog_ext(
1126 &mut self,
1127 path: impl AsRef<Path>,
1128 name: impl AsRef<str>,
1129 ext: impl AsRef<str>,
1130 ) -> Result<(), io::Error> {
1131 let path = path.as_ref();
1132 let old_path = self.path.clone();
1133
1134 self.active = true;
1135 self.mode = Mode::Save;
1136 self.file_state = FileStateMode::Save(Default::default());
1137 self.save_name = Some(OsString::from(name.as_ref()));
1138 self.save_ext = Some(OsString::from(ext.as_ref()));
1139 self.dirs.clear();
1140 self.files.clear();
1141 self.path = Default::default();
1142 if !self.no_default_roots {
1143 self.clear_roots();
1144 self.default_roots(path, &old_path);
1145 if old_path.exists() {
1146 self.set_path(&old_path)?;
1147 } else {
1148 self.set_path(path)?;
1149 }
1150 } else {
1151 self.set_path(path)?;
1152 }
1153 self.build_focus().focus(&self.save_name_state);
1154 Ok(())
1155 }
1156
1157 fn find_parent(&self, path: &Path) -> Option<PathBuf> {
1158 if path == Path::new(".") || path.file_name().is_none() {
1159 let parent = path.join("..");
1160 let canon_parent = parent.canonicalize().ok();
1161 let canon_path = path.canonicalize().ok();
1162 if canon_parent == canon_path {
1163 None
1164 } else if parent.exists() && parent.is_dir() {
1165 Some(parent)
1166 } else {
1167 None
1168 }
1169 } else if let Some(parent) = path.parent() {
1170 if parent.exists() && parent.is_dir() {
1171 Some(parent.to_path_buf())
1172 } else {
1173 None
1174 }
1175 } else {
1176 None
1177 }
1178 }
1179
1180 fn set_path(&mut self, path: &Path) -> Result<FileOutcome, io::Error> {
1182 let old = self.path.clone();
1183 let path = path.to_path_buf();
1184
1185 if old != path {
1186 let mut dirs = Vec::new();
1187 let mut files = Vec::new();
1188
1189 if self.find_parent(&path).is_some() {
1190 dirs.push(OsString::from(".."));
1191 }
1192
1193 for r in path.read_dir()? {
1194 let Ok(r) = r else {
1195 continue;
1196 };
1197
1198 if let Ok(meta) = r.metadata() {
1199 if meta.is_dir() {
1200 dirs.push(r.file_name());
1201 } else if meta.is_file() {
1202 if let Some(filter) = self.filter.as_ref() {
1203 if filter(&r.path()) {
1204 files.push(r.file_name());
1205 }
1206 } else {
1207 files.push(r.file_name());
1208 }
1209 }
1210 }
1211 }
1212
1213 self.path = path;
1214 self.dirs = dirs;
1215 self.files = files;
1216
1217 self.path_state.set_text(self.path.to_string_lossy());
1218 self.path_state.move_to_line_end(false);
1219
1220 self.dir_state.cancel();
1221 if !self.dirs.is_empty() {
1222 self.dir_state.list.select(Some(0));
1223 } else {
1224 self.dir_state.list.select(None);
1225 }
1226 self.dir_state.list.set_offset(0);
1227 if !self.files.is_empty() {
1228 self.file_state.select(Some(0));
1229 if let Some(name) = &self.save_name {
1230 self.save_name_state.set_text(name.to_string_lossy());
1231 } else {
1232 self.save_name_state
1233 .set_text(self.files[0].to_string_lossy());
1234 }
1235 } else {
1236 self.file_state.select(None);
1237 if let Some(name) = &self.save_name {
1238 self.save_name_state.set_text(name.to_string_lossy());
1239 } else {
1240 self.save_name_state.set_text("");
1241 }
1242 }
1243 self.file_state.set_offset(0);
1244
1245 Ok(FileOutcome::Changed)
1246 } else {
1247 Ok(FileOutcome::Unchanged)
1248 }
1249 }
1250
1251 fn use_path_input(&mut self) -> Result<FileOutcome, io::Error> {
1252 let path = PathBuf::from(self.path_state.text());
1253 if !path.exists() || !path.is_dir() {
1254 self.path_state.invalid = true;
1255 } else {
1256 self.path_state.invalid = false;
1257 self.set_path(&path)?;
1258 }
1259
1260 Ok(FileOutcome::Changed)
1261 }
1262
1263 fn chdir(&mut self, dir: &OsString) -> Result<FileOutcome, io::Error> {
1264 if dir == &OsString::from("..") {
1265 if let Some(parent) = self.find_parent(&self.path) {
1266 self.set_path(&parent)
1267 } else {
1268 Ok(FileOutcome::Unchanged)
1269 }
1270 } else {
1271 self.set_path(&self.path.join(dir))
1272 }
1273 }
1274
1275 fn chroot_selected(&mut self) -> Result<FileOutcome, io::Error> {
1276 if let Some(select) = self.root_state.selected() {
1277 if let Some(d) = self.roots.get(select).cloned() {
1278 self.set_path(&d.1)?;
1279 return Ok(FileOutcome::Changed);
1280 }
1281 }
1282 Ok(FileOutcome::Unchanged)
1283 }
1284
1285 fn chdir_selected(&mut self) -> Result<FileOutcome, io::Error> {
1286 if let Some(select) = self.dir_state.list.selected() {
1287 if let Some(dir) = self.dirs.get(select).cloned() {
1288 self.chdir(&dir)?;
1289 return Ok(FileOutcome::Changed);
1290 }
1291 }
1292 Ok(FileOutcome::Unchanged)
1293 }
1294
1295 fn name_selected(&mut self) -> Result<FileOutcome, io::Error> {
1297 if let Some(select) = self.file_state.first_selected() {
1298 if let Some(file) = self.files.get(select).cloned() {
1299 let name = file.to_string_lossy();
1300 self.save_name_state.set_text(name);
1301 return Ok(FileOutcome::Changed);
1302 }
1303 }
1304 Ok(FileOutcome::Continue)
1305 }
1306
1307 fn start_edit_dir(&mut self) -> FileOutcome {
1309 if !self.dir_state.is_editing() {
1310 self.build_focus().focus(&self.dir_state);
1311
1312 self.dirs.push(OsString::from(""));
1313 self.dir_state.editor.edit_dir.set_text("");
1314 self.dir_state.edit_new(self.dirs.len() - 1);
1315
1316 FileOutcome::Changed
1317 } else {
1318 FileOutcome::Continue
1319 }
1320 }
1321
1322 fn cancel_edit_dir(&mut self) -> FileOutcome {
1323 if self.dir_state.is_editing() {
1324 self.dir_state.cancel();
1325 self.dirs.remove(self.dirs.len() - 1);
1326 FileOutcome::Changed
1327 } else {
1328 FileOutcome::Continue
1329 }
1330 }
1331
1332 fn commit_edit_dir(&mut self) -> Result<FileOutcome, io::Error> {
1333 if self.dir_state.is_editing() {
1334 let name = self.dir_state.editor.edit_dir.text().trim();
1335 let path = self.path.join(name);
1336 if fs::create_dir(&path).is_err() {
1337 self.dir_state.editor.edit_dir.invalid = true;
1338 Ok(FileOutcome::Changed)
1339 } else {
1340 self.dir_state.commit();
1341 if self.mode == Mode::Save {
1342 self.build_focus().focus_no_lost(&self.save_name_state);
1343 }
1344 self.set_path(&path)
1345 }
1346 } else {
1347 Ok(FileOutcome::Unchanged)
1348 }
1349 }
1350
1351 fn close_cancel(&mut self) -> FileOutcome {
1353 self.active = false;
1354 self.dir_state.cancel();
1355 FileOutcome::Cancel
1356 }
1357
1358 fn choose_selected(&mut self) -> FileOutcome {
1360 match self.mode {
1361 Mode::Open => {
1362 if let Some(select) = self.file_state.first_selected() {
1363 if let Some(file) = self.files.get(select).cloned() {
1364 self.active = false;
1365 return FileOutcome::Ok(self.path.join(file));
1366 }
1367 }
1368 }
1369 Mode::OpenMany => {
1370 let sel = self
1371 .file_state
1372 .selected()
1373 .iter()
1374 .map(|&idx| self.path.join(self.files.get(idx).expect("file")))
1375 .collect::<Vec<_>>();
1376 self.active = false;
1377 return FileOutcome::OkList(sel);
1378 }
1379 Mode::Save => {
1380 let mut path = self.path.join(self.save_name_state.text().trim());
1381 if let Some(ext) = &self.save_ext {
1382 if !ext.is_empty() {
1383 path.set_extension(ext);
1384 }
1385 }
1386 self.active = false;
1387 return FileOutcome::Ok(path);
1388 }
1389 Mode::Dir => {
1390 if let Some(select) = self.dir_state.list.selected() {
1391 if let Some(dir) = self.dirs.get(select).cloned() {
1392 self.active = false;
1393 if dir != ".." {
1394 return FileOutcome::Ok(self.path.join(dir));
1395 } else {
1396 return FileOutcome::Ok(self.path.clone());
1397 }
1398 }
1399 }
1400 }
1401 }
1402 FileOutcome::Continue
1403 }
1404}
1405
1406impl HasScreenCursor for FileDialogState {
1407 fn screen_cursor(&self) -> Option<(u16, u16)> {
1408 if self.active {
1409 self.path_state
1410 .screen_cursor()
1411 .or_else(|| self.save_name_state.screen_cursor())
1412 .or_else(|| self.dir_state.screen_cursor())
1413 } else {
1414 None
1415 }
1416 }
1417}
1418
1419impl HasFocus for FileDialogState {
1420 fn build(&self, _builder: &mut FocusBuilder) {
1421 }
1423
1424 fn focus(&self) -> FocusFlag {
1425 unimplemented!("not available")
1426 }
1427
1428 fn area(&self) -> Rect {
1429 unimplemented!("not available")
1430 }
1431}
1432
1433impl RelocatableState for FileDialogState {
1434 fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
1435 self.area.relocate(shift, clip);
1436 self.path_state.relocate(shift, clip);
1437 self.root_state.relocate(shift, clip);
1438 self.dir_state.relocate(shift, clip);
1439 self.file_state.relocate(shift, clip);
1440 self.save_name_state.relocate(shift, clip);
1441 self.new_state.relocate(shift, clip);
1442 self.cancel_state.relocate(shift, clip);
1443 self.ok_state.relocate(shift, clip);
1444 }
1445}
1446
1447impl FileDialogState {
1448 fn build_focus(&self) -> Focus {
1449 let mut fb = FocusBuilder::default();
1450 fb.widget(&self.dir_state);
1451 fb.widget(&self.file_state);
1452 if self.mode == Mode::Save {
1453 fb.widget(&self.save_name_state);
1454 }
1455 fb.widget(&self.ok_state);
1456 fb.widget(&self.cancel_state);
1457 fb.widget(&self.new_state);
1458 fb.widget(&self.root_state);
1459 fb.widget(&self.path_state);
1460 fb.build()
1461 }
1462}
1463
1464impl HandleEvent<Event, Dialog, Result<FileOutcome, io::Error>> for FileDialogState {
1465 fn handle(&mut self, event: &Event, _qualifier: Dialog) -> Result<FileOutcome, io::Error> {
1466 if !self.active {
1467 return Ok(FileOutcome::Continue);
1468 }
1469
1470 let mut focus = self.build_focus();
1471 let mut f: FileOutcome = focus.handle(event, Regular).into();
1472 let next_focus: Option<&dyn HasFocus> = match event {
1473 ct_event!(keycode press F(1)) => Some(&self.root_state),
1474 ct_event!(keycode press F(2)) => Some(&self.dir_state),
1475 ct_event!(keycode press F(3)) => Some(&self.file_state),
1476 ct_event!(keycode press F(4)) => Some(&self.path_state),
1477 ct_event!(keycode press F(5)) => Some(&self.save_name_state),
1478 _ => None,
1479 };
1480 if let Some(next_focus) = next_focus {
1481 focus.focus(next_focus);
1482 f = FileOutcome::Changed;
1483 }
1484
1485 let r = 'f: {
1486 event_flow!(break 'f handle_path(self, event)?);
1487 event_flow!(
1488 break 'f if self.mode == Mode::Save {
1489 handle_name(self, event)?
1490 } else {
1491 FileOutcome::Continue
1492 }
1493 );
1494 event_flow!(break 'f handle_files(self, event)?);
1495 event_flow!(break 'f handle_dirs(self, event)?);
1496 event_flow!(break 'f handle_roots(self, event)?);
1497 event_flow!(break 'f handle_new(self, event)?);
1498 event_flow!(break 'f handle_cancel(self, event)?);
1499 event_flow!(break 'f handle_ok(self, event)?);
1500 FileOutcome::Continue
1501 };
1502
1503 event_flow!(max(f, r));
1504 Ok(FileOutcome::Unchanged)
1506 }
1507}
1508
1509fn handle_new(state: &mut FileDialogState, event: &Event) -> Result<FileOutcome, io::Error> {
1510 event_flow!(match state.new_state.handle(event, Regular) {
1511 ButtonOutcome::Pressed => {
1512 state.start_edit_dir()
1513 }
1514 r => Outcome::from(r).into(),
1515 });
1516 event_flow!(match event {
1517 ct_event!(key press CONTROL-'n') => {
1518 state.start_edit_dir()
1519 }
1520 _ => FileOutcome::Continue,
1521 });
1522 Ok(FileOutcome::Continue)
1523}
1524
1525fn handle_ok(state: &mut FileDialogState, event: &Event) -> Result<FileOutcome, io::Error> {
1526 event_flow!(match state.ok_state.handle(event, Regular) {
1527 ButtonOutcome::Pressed => state.choose_selected(),
1528 r => Outcome::from(r).into(),
1529 });
1530 Ok(FileOutcome::Continue)
1531}
1532
1533fn handle_cancel(state: &mut FileDialogState, event: &Event) -> Result<FileOutcome, io::Error> {
1534 event_flow!(match state.cancel_state.handle(event, Regular) {
1535 ButtonOutcome::Pressed => {
1536 state.close_cancel()
1537 }
1538 r => Outcome::from(r).into(),
1539 });
1540 event_flow!(match event {
1541 ct_event!(keycode press Esc) => {
1542 state.close_cancel()
1543 }
1544 _ => FileOutcome::Continue,
1545 });
1546 Ok(FileOutcome::Continue)
1547}
1548
1549fn handle_name(state: &mut FileDialogState, event: &Event) -> Result<FileOutcome, io::Error> {
1550 event_flow!(Outcome::from(state.save_name_state.handle(event, Regular)));
1551 if state.save_name_state.is_focused() {
1552 event_flow!(match event {
1553 ct_event!(keycode press Enter) => {
1554 state.choose_selected()
1555 }
1556 _ => FileOutcome::Continue,
1557 });
1558 }
1559 Ok(FileOutcome::Continue)
1560}
1561
1562fn handle_path(state: &mut FileDialogState, event: &Event) -> Result<FileOutcome, io::Error> {
1563 event_flow!(Outcome::from(state.path_state.handle(event, Regular)));
1564 if state.path_state.is_focused() {
1565 event_flow!(match event {
1566 ct_event!(keycode press Enter) => {
1567 state.use_path_input()?;
1568 state.build_focus().focus_no_lost(&state.dir_state.list);
1569 FileOutcome::Changed
1570 }
1571 _ => FileOutcome::Continue,
1572 });
1573 }
1574 on_lost!(
1575 state.path_state => {
1576 state.use_path_input()?
1577 }
1578 );
1579 Ok(FileOutcome::Continue)
1580}
1581
1582fn handle_roots(state: &mut FileDialogState, event: &Event) -> Result<FileOutcome, io::Error> {
1583 event_flow!(match state.root_state.handle(event, Regular) {
1584 Outcome::Changed => {
1585 state.chroot_selected()?
1586 }
1587 r => r.into(),
1588 });
1589 Ok(FileOutcome::Continue)
1590}
1591
1592fn handle_dirs(state: &mut FileDialogState, event: &Event) -> Result<FileOutcome, io::Error> {
1593 if matches!(event, ct_event!(keycode press F(2))) {
1595 return Ok(FileOutcome::Continue);
1596 }
1597
1598 event_flow!(match state.dir_state.handle(event, Regular) {
1599 EditOutcome::Edit => {
1600 state.chdir_selected()?
1601 }
1602 EditOutcome::Cancel => {
1603 state.cancel_edit_dir()
1604 }
1605 EditOutcome::Commit | EditOutcome::CommitAndAppend | EditOutcome::CommitAndEdit => {
1606 state.commit_edit_dir()?
1607 }
1608 r => {
1609 Outcome::from(r).into()
1610 }
1611 });
1612 if state.dir_state.list.is_focused() {
1613 event_flow!(handle_nav(&mut state.dir_state.list, &state.dirs, event)?);
1614 }
1615 Ok(FileOutcome::Continue)
1616}
1617
1618fn handle_files(state: &mut FileDialogState, event: &Event) -> Result<FileOutcome, io::Error> {
1619 if state.file_state.is_focused() {
1620 event_flow!(log f0: match event {
1621 ct_event!(mouse any for m) if state.file_state.is_double_click(m) => {
1622 state.choose_selected()
1623 }
1624 ct_event!(keycode press Enter) => {
1625 state.choose_selected()
1626 }
1627 _ => FileOutcome::Continue,
1628 });
1629 event_flow!(log f1: {
1630 match &mut state.file_state {
1631 FileStateMode::Open(st) => handle_nav(st, &state.files, event)?,
1632 FileStateMode::OpenMany(st) => handle_nav_many(st, &state.files, event)?,
1633 FileStateMode::Save(st) => match handle_nav(st, &state.files, event)? {
1634 FileOutcome::Changed => state.name_selected()?,
1635 r => r,
1636 },
1637 FileStateMode::Dir(_) => FileOutcome::Continue,
1638 }
1639 });
1640 }
1641 event_flow!(match &mut state.file_state {
1642 FileStateMode::Open(st) => {
1643 st.handle(event, Regular).into()
1644 }
1645 FileStateMode::OpenMany(st) => {
1646 st.handle(event, Regular).into()
1647 }
1648 FileStateMode::Save(st) => {
1649 match st.handle(event, Regular) {
1650 Outcome::Changed => state.name_selected()?.into(),
1651 r => r.into(),
1652 }
1653 }
1654 FileStateMode::Dir(_) => FileOutcome::Continue,
1655 });
1656
1657 Ok(FileOutcome::Continue)
1658}
1659
1660fn handle_nav(
1661 list: &mut ListState<RowSelection>,
1662 nav: &[OsString],
1663 event: &Event,
1664) -> Result<FileOutcome, io::Error> {
1665 event_flow!(match event {
1666 ct_event!(key press c) => {
1667 let next = find_next_by_key(*c, list.selected().unwrap_or(0), nav);
1668 if let Some(next) = next {
1669 list.move_to(next).into()
1670 } else {
1671 FileOutcome::Unchanged
1672 }
1673 }
1674 _ => FileOutcome::Continue,
1675 });
1676 Ok(FileOutcome::Continue)
1677}
1678
1679fn handle_nav_many(
1680 list: &mut ListState<RowSetSelection>,
1681 nav: &[OsString],
1682 event: &Event,
1683) -> Result<FileOutcome, io::Error> {
1684 event_flow!(match event {
1685 ct_event!(key press c) => {
1686 let next = find_next_by_key(*c, list.lead().unwrap_or(0), nav);
1687 if let Some(next) = next {
1688 list.move_to(next, false).into()
1689 } else {
1690 FileOutcome::Unchanged
1691 }
1692 }
1693 _ => FileOutcome::Continue,
1694 });
1695 Ok(FileOutcome::Continue)
1696}
1697
1698#[allow(clippy::question_mark)]
1699fn find_next_by_key(c: char, start: usize, names: &[OsString]) -> Option<usize> {
1700 let Some(c) = c.to_lowercase().next() else {
1701 return None;
1702 };
1703
1704 let mut idx = start;
1705 let mut selected = None;
1706 loop {
1707 idx += 1;
1708 if idx >= names.len() {
1709 idx = 0;
1710 }
1711 if idx == start {
1712 break;
1713 }
1714
1715 let nav = names[idx].to_string_lossy();
1716
1717 let initials = nav
1718 .split([' ', '_', '-'])
1719 .flat_map(|v| v.chars().next())
1720 .flat_map(|c| c.to_lowercase().next())
1721 .collect::<Vec<_>>();
1722 if initials.contains(&c) {
1723 selected = Some(idx);
1724 break;
1725 }
1726 }
1727
1728 selected
1729}
1730
1731pub fn handle_events(
1735 state: &mut FileDialogState,
1736 _focus: bool,
1737 event: &Event,
1738) -> Result<FileOutcome, io::Error> {
1739 HandleEvent::handle(state, event, Dialog)
1740}