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