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, ListItem, 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 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 if let Ok(disks) = Disks::new_with_refreshed_list() {
996 for d in disks.iter() {
997 self.roots
998 .push((d.name().to_os_string(), d.mount_point().to_path_buf()));
999 }
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}