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