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