1use crate::_private::NonExhaustive;
6use crate::button::{Button, ButtonState, ButtonStyle};
7use crate::event::{ButtonOutcome, FileOutcome, TextOutcome};
8use crate::layout::{DialogItem, layout_as_grid, layout_dialog};
9use crate::list::edit::{EditList, EditListState};
10use crate::list::selection::RowSelection;
11use crate::list::{List, ListState, ListStyle};
12use crate::util::{block_padding2, reset_buf_area};
13#[cfg(feature = "user_directories")]
14use dirs::{document_dir, home_dir};
15use rat_event::{
16 ConsumedEvent, Dialog, HandleEvent, MouseOnly, Outcome, Regular, ct_event, flow, try_flow,
17};
18use rat_focus::{Focus, FocusBuilder, FocusFlag, HasFocus, on_lost};
19use rat_ftable::event::EditOutcome;
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, Rect};
25use ratatui::style::Style;
26use ratatui::text::Text;
27use ratatui::widgets::{Block, ListItem};
28use ratatui::widgets::{StatefulWidget, Widget};
29use std::cmp::max;
30use std::ffi::OsString;
31use std::fmt::{Debug, Formatter};
32use std::path::{Path, PathBuf};
33use std::{fs, io};
34#[cfg(feature = "user_directories")]
35use sysinfo::Disks;
36
37#[derive(Debug, Clone)]
58pub struct FileDialog<'a> {
59 block: Option<Block<'a>>,
60 no_block: bool,
61
62 style: Style,
63 list_style: Option<ListStyle>,
64 roots_style: Option<ListStyle>,
65 text_style: Option<TextStyle>,
66 button_style: Option<ButtonStyle>,
67 ok_text: &'a str,
68 cancel_text: &'a str,
69}
70
71#[derive(Debug)]
73pub struct FileDialogStyle {
74 pub style: Style,
75 pub list: Option<ListStyle>,
77 pub roots: Option<ListStyle>,
79 pub text: Option<TextStyle>,
81 pub button: Option<ButtonStyle>,
83 pub block: Option<Block<'static>>,
85 pub no_block: Option<bool>,
87
88 pub non_exhaustive: NonExhaustive,
89}
90
91#[derive(Debug, PartialEq, Eq)]
93#[allow(dead_code)]
94enum Mode {
95 Open,
96 Save,
97 Dir,
98}
99
100#[allow(clippy::type_complexity)]
102pub struct FileDialogState {
103 pub active: bool,
105
106 mode: Mode,
107
108 path: PathBuf,
109 save_name: Option<OsString>,
110 save_ext: Option<OsString>,
111 dirs: Vec<OsString>,
112 filter: Option<Box<dyn Fn(&Path) -> bool + 'static>>,
113 files: Vec<OsString>,
114 use_default_roots: bool,
115 roots: Vec<(OsString, PathBuf)>,
116
117 path_state: TextInputState,
118 root_state: ListState<RowSelection>,
119 dir_state: EditListState<EditDirNameState>,
120 file_state: ListState<RowSelection>,
121 save_name_state: TextInputState,
122 new_state: ButtonState,
123 cancel_state: ButtonState,
124 ok_state: ButtonState,
125}
126
127pub(crate) mod event {
128 use rat_event::{ConsumedEvent, Outcome};
129 use std::path::PathBuf;
130
131 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
133 pub enum FileOutcome {
134 Continue,
136 Unchanged,
139 Changed,
144 Cancel,
146 Ok(PathBuf),
148 }
149
150 impl ConsumedEvent for FileOutcome {
151 fn is_consumed(&self) -> bool {
152 !matches!(self, FileOutcome::Continue)
153 }
154 }
155
156 impl From<FileOutcome> for Outcome {
157 fn from(value: FileOutcome) -> Self {
158 match value {
159 FileOutcome::Continue => Outcome::Continue,
160 FileOutcome::Unchanged => Outcome::Unchanged,
161 FileOutcome::Changed => Outcome::Changed,
162 FileOutcome::Ok(_) => Outcome::Changed,
163 FileOutcome::Cancel => Outcome::Changed,
164 }
165 }
166 }
167
168 impl From<Outcome> for FileOutcome {
169 fn from(value: Outcome) -> Self {
170 match value {
171 Outcome::Continue => FileOutcome::Continue,
172 Outcome::Unchanged => FileOutcome::Unchanged,
173 Outcome::Changed => FileOutcome::Changed,
174 }
175 }
176 }
177
178 impl From<bool> for FileOutcome {
180 fn from(value: bool) -> Self {
181 if value {
182 FileOutcome::Changed
183 } else {
184 FileOutcome::Unchanged
185 }
186 }
187 }
188}
189
190impl Debug for FileDialogState {
191 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
192 f.debug_struct("FileOpenState")
193 .field("active", &self.active)
194 .field("mode", &self.mode)
195 .field("path", &self.path)
196 .field("save_name", &self.save_name)
197 .field("dirs", &self.dirs)
198 .field("files", &self.files)
199 .field("use_default_roots", &self.use_default_roots)
200 .field("roots", &self.roots)
201 .field("path_state", &self.path_state)
202 .field("root_state", &self.root_state)
203 .field("dir_state", &self.dir_state)
204 .field("file_state", &self.file_state)
205 .field("name_state", &self.save_name_state)
206 .field("cancel_state", &self.cancel_state)
207 .field("ok_state", &self.ok_state)
208 .finish()
209 }
210}
211
212impl Default for FileDialogStyle {
213 fn default() -> Self {
214 FileDialogStyle {
215 style: Default::default(),
216 list: Default::default(),
217 roots: Default::default(),
218 button: Default::default(),
219 block: Default::default(),
220 no_block: Default::default(),
221 text: Default::default(),
222 non_exhaustive: NonExhaustive,
223 }
224 }
225}
226
227impl Default for FileDialogState {
228 fn default() -> Self {
229 let mut s = Self {
230 active: false,
231 mode: Mode::Open,
232 path: Default::default(),
233 save_name: Default::default(),
234 save_ext: Default::default(),
235 dirs: Default::default(),
236 filter: Default::default(),
237 files: Default::default(),
238 use_default_roots: true,
239 roots: Default::default(),
240 path_state: Default::default(),
241 root_state: Default::default(),
242 dir_state: Default::default(),
243 file_state: Default::default(),
244 save_name_state: Default::default(),
245 new_state: Default::default(),
246 cancel_state: Default::default(),
247 ok_state: Default::default(),
248 };
249 s.dir_state.list.set_scroll_selection(true);
250 s.file_state.set_scroll_selection(true);
251 s
252 }
253}
254
255impl<'a> Default for FileDialog<'a> {
256 fn default() -> Self {
257 Self {
258 block: Default::default(),
259 no_block: false,
260 style: Default::default(),
261 list_style: Default::default(),
262 roots_style: Default::default(),
263 text_style: Default::default(),
264 button_style: Default::default(),
265 ok_text: "Ok",
266 cancel_text: "Cancel",
267 }
268 }
269}
270
271impl<'a> FileDialog<'a> {
272 pub fn new() -> Self {
274 Self::default()
275 }
276
277 pub fn ok_text(mut self, txt: &'a str) -> Self {
279 self.ok_text = txt;
280 self
281 }
282
283 pub fn cancel_text(mut self, txt: &'a str) -> Self {
285 self.cancel_text = txt;
286 self
287 }
288
289 pub fn block(mut self, block: Block<'a>) -> Self {
291 self.block = Some(block);
292 self.block = self.block.map(|v| v.style(self.style));
293 self
294 }
295
296 pub fn no_block(mut self) -> Self {
298 self.block = None;
299 self.no_block = true;
300 self
301 }
302
303 pub fn style(mut self, style: Style) -> Self {
305 self.style = style;
306 self
307 }
308
309 pub fn list_style(mut self, style: ListStyle) -> Self {
311 self.list_style = Some(style);
312 self
313 }
314
315 pub fn roots_style(mut self, style: ListStyle) -> Self {
317 self.roots_style = Some(style);
318 self
319 }
320
321 pub fn text_style(mut self, style: TextStyle) -> Self {
323 self.text_style = Some(style);
324 self
325 }
326
327 pub fn button_style(mut self, style: ButtonStyle) -> Self {
329 self.button_style = Some(style);
330 self
331 }
332
333 pub fn styles(mut self, styles: FileDialogStyle) -> Self {
335 self.style = styles.style;
336 if styles.list.is_some() {
337 self.list_style = styles.list;
338 }
339 if styles.roots.is_some() {
340 self.roots_style = styles.roots;
341 }
342 if styles.text.is_some() {
343 self.text_style = styles.text;
344 }
345 if styles.button.is_some() {
346 self.button_style = styles.button;
347 }
348 if let Some(no_block) = styles.no_block {
349 self.no_block = no_block;
350 }
351 if styles.block.is_some() {
352 self.block = styles.block;
353 }
354 self.block = self.block.map(|v| v.style(self.style));
355 self
356 }
357}
358
359#[derive(Debug, Default)]
360struct EditDirName<'a> {
361 edit_dir: TextInput<'a>,
362}
363
364#[derive(Debug, Default)]
365struct EditDirNameState {
366 edit_dir: TextInputState,
367}
368
369impl StatefulWidget for EditDirName<'_> {
370 type State = EditDirNameState;
371
372 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
373 self.edit_dir.render(area, buf, &mut state.edit_dir);
374 }
375}
376
377impl HasScreenCursor for EditDirNameState {
378 fn screen_cursor(&self) -> Option<(u16, u16)> {
379 self.edit_dir.screen_cursor()
380 }
381}
382
383impl HandleEvent<crossterm::event::Event, Regular, EditOutcome> for EditDirNameState {
384 fn handle(&mut self, event: &crossterm::event::Event, qualifier: Regular) -> EditOutcome {
385 match self.edit_dir.handle(event, qualifier) {
386 TextOutcome::Continue => EditOutcome::Continue,
387 TextOutcome::Unchanged => EditOutcome::Unchanged,
388 TextOutcome::Changed => EditOutcome::Changed,
389 TextOutcome::TextChanged => EditOutcome::Changed,
390 }
391 }
392}
393
394impl HandleEvent<crossterm::event::Event, MouseOnly, EditOutcome> for EditDirNameState {
395 fn handle(&mut self, event: &crossterm::event::Event, qualifier: MouseOnly) -> EditOutcome {
396 match self.edit_dir.handle(event, qualifier) {
397 TextOutcome::Continue => EditOutcome::Continue,
398 TextOutcome::Unchanged => EditOutcome::Unchanged,
399 TextOutcome::Changed => EditOutcome::Changed,
400 TextOutcome::TextChanged => EditOutcome::Changed,
401 }
402 }
403}
404
405impl HasFocus for EditDirNameState {
406 fn build(&self, builder: &mut FocusBuilder) {
407 builder.leaf_widget(self);
408 }
409
410 fn focus(&self) -> FocusFlag {
411 self.edit_dir.focus()
412 }
413
414 fn area(&self) -> Rect {
415 self.edit_dir.area()
416 }
417}
418
419impl StatefulWidget for FileDialog<'_> {
420 type State = FileDialogState;
421
422 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
423 if !state.active {
424 return;
425 }
426
427 let block;
428 let block = if let Some(block) = self.block.as_ref() {
429 block
430 } else if !self.no_block {
431 block = Block::bordered()
432 .title(match state.mode {
433 Mode::Open => " Open ",
434 Mode::Save => " Save ",
435 Mode::Dir => " Directory ",
436 })
437 .style(self.style);
438 &block
439 } else {
440 block = Block::new().style(self.style);
441 &block
442 };
443
444 let layout = layout_dialog(
445 area,
446 block_padding2(block),
447 [
448 Constraint::Percentage(20),
449 Constraint::Percentage(30),
450 Constraint::Percentage(50),
451 ],
452 0,
453 Flex::Center,
454 );
455
456 reset_buf_area(layout.area(), buf);
457 block.render(area, buf);
458
459 match state.mode {
460 Mode::Open => {
461 render_open(&self, layout.widget_for(DialogItem::Content), buf, state);
462 }
463 Mode::Save => {
464 render_save(&self, layout.widget_for(DialogItem::Content), buf, state);
465 }
466 Mode::Dir => {
467 render_open_dir(&self, layout.widget_for(DialogItem::Content), buf, state);
468 }
469 }
470
471 let mut l_n = layout.widget_for(DialogItem::Button(1));
472 l_n.width = 10;
473 Button::new(Text::from("New").alignment(Alignment::Center))
474 .styles_opt(self.button_style.clone())
475 .render(l_n, buf, &mut state.new_state);
476
477 let l_oc = Layout::horizontal([Constraint::Length(10), Constraint::Length(10)])
478 .spacing(1)
479 .flex(Flex::End)
480 .split(layout.widget_for(DialogItem::Button(2)));
481
482 Button::new(Text::from(self.cancel_text).alignment(Alignment::Center))
483 .styles_opt(self.button_style.clone())
484 .render(l_oc[0], buf, &mut state.cancel_state);
485
486 Button::new(Text::from(self.ok_text).alignment(Alignment::Center))
487 .styles_opt(self.button_style.clone())
488 .render(l_oc[1], buf, &mut state.ok_state);
489 }
490}
491
492fn render_open_dir(
493 widget: &FileDialog<'_>,
494 area: Rect,
495 buf: &mut Buffer,
496 state: &mut FileDialogState,
497) {
498 let l_grid = layout_as_grid(
499 area,
500 Layout::horizontal([
501 Constraint::Percentage(20), Constraint::Percentage(80),
503 ]),
504 Layout::vertical([
505 Constraint::Length(1), Constraint::Fill(1),
507 ]),
508 );
509
510 let mut l_path = l_grid.widget_for((1, 0));
512 l_path.width = l_path.width.saturating_sub(1);
513 TextInput::new()
514 .styles_opt(widget.text_style.clone())
515 .render(l_path, buf, &mut state.path_state);
516
517 List::default()
518 .items(state.roots.iter().map(|v| {
519 let s = v.0.to_string_lossy();
520 ListItem::from(format!("{}", s))
521 }))
522 .scroll(Scroll::new())
523 .styles_opt(widget.roots_style.clone())
524 .render(l_grid.widget_for((0, 1)), buf, &mut state.root_state);
525
526 EditList::new(
527 List::default()
528 .items(state.dirs.iter().map(|v| {
529 let s = v.to_string_lossy();
530 ListItem::from(s)
531 }))
532 .scroll(Scroll::new())
533 .styles_opt(widget.list_style.clone()),
534 EditDirName {
535 edit_dir: TextInput::new().styles_opt(widget.text_style.clone()),
536 },
537 )
538 .render(l_grid.widget_for((1, 1)), buf, &mut state.dir_state);
539}
540
541fn render_open(widget: &FileDialog<'_>, area: Rect, buf: &mut Buffer, state: &mut FileDialogState) {
542 let l_grid = layout_as_grid(
543 area,
544 Layout::horizontal([
545 Constraint::Percentage(20),
546 Constraint::Percentage(30),
547 Constraint::Percentage(50),
548 ]),
549 Layout::new(
550 Direction::Vertical,
551 [Constraint::Length(1), Constraint::Fill(1)],
552 ),
553 );
554
555 let mut l_path = l_grid.widget_for((1, 0)).union(l_grid.widget_for((2, 0)));
557 l_path.width = l_path.width.saturating_sub(1);
558 TextInput::new()
559 .styles_opt(widget.text_style.clone())
560 .render(l_path, buf, &mut state.path_state);
561
562 List::default()
563 .items(state.roots.iter().map(|v| {
564 let s = v.0.to_string_lossy();
565 ListItem::from(format!("{}", s))
566 }))
567 .scroll(Scroll::new())
568 .styles_opt(widget.roots_style.clone())
569 .render(l_grid.widget_for((0, 1)), buf, &mut state.root_state);
570
571 EditList::new(
572 List::default()
573 .items(state.dirs.iter().map(|v| {
574 let s = v.to_string_lossy();
575 ListItem::from(s)
576 }))
577 .scroll(Scroll::new())
578 .styles_opt(widget.list_style.clone()),
579 EditDirName {
580 edit_dir: TextInput::new().styles_opt(widget.text_style.clone()),
581 },
582 )
583 .render(l_grid.widget_for((1, 1)), buf, &mut state.dir_state);
584
585 List::default()
586 .items(state.files.iter().map(|v| {
587 let s = v.to_string_lossy();
588 ListItem::from(s)
589 }))
590 .scroll(Scroll::new())
591 .styles_opt(widget.list_style.clone())
592 .render(l_grid.widget_for((2, 1)), buf, &mut state.file_state);
593}
594
595fn render_save(widget: &FileDialog<'_>, area: Rect, buf: &mut Buffer, state: &mut FileDialogState) {
596 let l_grid = layout_as_grid(
597 area,
598 Layout::horizontal([
599 Constraint::Percentage(20),
600 Constraint::Percentage(30),
601 Constraint::Percentage(50),
602 ]),
603 Layout::new(
604 Direction::Vertical,
605 [
606 Constraint::Length(1),
607 Constraint::Fill(1),
608 Constraint::Length(1),
609 ],
610 ),
611 );
612
613 let mut l_path = l_grid.widget_for((1, 0)).union(l_grid.widget_for((2, 0)));
615 l_path.width = l_path.width.saturating_sub(1);
616 TextInput::new()
617 .styles_opt(widget.text_style.clone())
618 .render(l_path, buf, &mut state.path_state);
619
620 List::default()
621 .items(state.roots.iter().map(|v| {
622 let s = v.0.to_string_lossy();
623 ListItem::from(format!("{}", s))
624 }))
625 .scroll(Scroll::new())
626 .styles_opt(widget.roots_style.clone())
627 .render(l_grid.widget_for((0, 1)), buf, &mut state.root_state);
628
629 EditList::new(
630 List::default()
631 .items(state.dirs.iter().map(|v| {
632 let s = v.to_string_lossy();
633 ListItem::from(s)
634 }))
635 .scroll(Scroll::new())
636 .styles_opt(widget.list_style.clone()),
637 EditDirName {
638 edit_dir: TextInput::new().styles_opt(widget.text_style.clone()),
639 },
640 )
641 .render(l_grid.widget_for((1, 1)), buf, &mut state.dir_state);
642
643 List::default()
644 .items(state.files.iter().map(|v| {
645 let s = v.to_string_lossy();
646 ListItem::from(s)
647 }))
648 .scroll(Scroll::new())
649 .styles_opt(widget.list_style.clone())
650 .render(l_grid.widget_for((2, 1)), buf, &mut state.file_state);
651
652 TextInput::new()
653 .styles_opt(widget.text_style.clone())
654 .render(l_grid.widget_for((2, 2)), buf, &mut state.save_name_state);
655}
656
657impl FileDialogState {
658 pub fn new() -> Self {
659 Self::default()
660 }
661
662 pub fn active(&self) -> bool {
663 self.active
664 }
665
666 pub fn set_filter(&mut self, filter: impl Fn(&Path) -> bool + 'static) {
668 self.filter = Some(Box::new(filter));
669 }
670
671 pub fn set_last_path(&mut self, last: &Path) {
676 self.path = last.into();
677 }
678
679 pub fn use_default_roots(&mut self, roots: bool) {
681 self.use_default_roots = roots;
682 }
683
684 pub fn add_root(&mut self, name: impl AsRef<str>, path: impl Into<PathBuf>) {
686 self.roots
687 .push((OsString::from(name.as_ref()), path.into()))
688 }
689
690 pub fn clear_roots(&mut self) {
692 self.roots.clear();
693 }
694
695 pub fn default_roots(&mut self, start: &Path, last: &Path) {
697 if last.exists() {
698 self.roots.push((
699 OsString::from("Last"), last.into(),
701 ));
702 }
703 self.roots.push((
704 OsString::from("Start"), start.into(),
706 ));
707
708 #[cfg(feature = "user_directories")]
709 {
710 if let Some(home) = home_dir() {
711 self.roots.push((OsString::from("Home"), home));
712 }
713 if let Some(documents) = document_dir() {
714 self.roots.push((OsString::from("Documents"), documents));
715 }
716 }
717
718 #[cfg(feature = "user_directories")]
719 {
720 let disks = Disks::new_with_refreshed_list();
721 for d in disks.list() {
722 self.roots
723 .push((d.name().to_os_string(), d.mount_point().to_path_buf()));
724 }
725 }
726
727 self.root_state.select(Some(0));
728 }
729
730 pub fn directory_dialog(&mut self, path: impl AsRef<Path>) -> Result<(), io::Error> {
732 let path = path.as_ref();
733 let old_path = self.path.clone();
734
735 self.active = true;
736 self.mode = Mode::Dir;
737 self.save_name = None;
738 self.save_ext = None;
739 self.dirs.clear();
740 self.files.clear();
741 self.path = Default::default();
742 if self.use_default_roots {
743 self.clear_roots();
744 self.default_roots(path, &old_path);
745 if old_path.exists() {
746 self.set_path(&old_path)?;
747 } else {
748 self.set_path(path)?;
749 }
750 } else {
751 self.set_path(path)?;
752 }
753 self.build_focus().focus(&self.dir_state);
754 Ok(())
755 }
756
757 pub fn open_dialog(&mut self, path: impl AsRef<Path>) -> Result<(), io::Error> {
759 let path = path.as_ref();
760 let old_path = self.path.clone();
761
762 self.active = true;
763 self.mode = Mode::Open;
764 self.save_name = None;
765 self.save_ext = None;
766 self.dirs.clear();
767 self.files.clear();
768 self.path = Default::default();
769 if self.use_default_roots {
770 self.clear_roots();
771 self.default_roots(path, &old_path);
772 if old_path.exists() {
773 self.set_path(&old_path)?;
774 } else {
775 self.set_path(path)?;
776 }
777 } else {
778 self.set_path(path)?;
779 }
780 self.build_focus().focus(&self.file_state);
781 Ok(())
782 }
783
784 pub fn save_dialog(
786 &mut self,
787 path: impl AsRef<Path>,
788 name: impl AsRef<str>,
789 ) -> Result<(), io::Error> {
790 self.save_dialog_ext(path, name, "")
791 }
792
793 pub fn save_dialog_ext(
795 &mut self,
796 path: impl AsRef<Path>,
797 name: impl AsRef<str>,
798 ext: impl AsRef<str>,
799 ) -> Result<(), io::Error> {
800 let path = path.as_ref();
801 let old_path = self.path.clone();
802
803 self.active = true;
804 self.mode = Mode::Save;
805 self.save_name = Some(OsString::from(name.as_ref()));
806 self.save_ext = Some(OsString::from(ext.as_ref()));
807 self.dirs.clear();
808 self.files.clear();
809 self.path = Default::default();
810 if self.use_default_roots {
811 self.clear_roots();
812 self.default_roots(path, &old_path);
813 if old_path.exists() {
814 self.set_path(&old_path)?;
815 } else {
816 self.set_path(path)?;
817 }
818 } else {
819 self.set_path(path)?;
820 }
821 self.build_focus().focus(&self.save_name_state);
822 Ok(())
823 }
824
825 fn find_parent(&self, path: &Path) -> Option<PathBuf> {
826 if path == Path::new(".") || path.file_name().is_none() {
827 let parent = path.join("..");
828 let canon_parent = parent.canonicalize().ok();
829 let canon_path = path.canonicalize().ok();
830 if canon_parent == canon_path {
831 None
832 } else if parent.exists() && parent.is_dir() {
833 Some(parent)
834 } else {
835 None
836 }
837 } else if let Some(parent) = path.parent() {
838 if parent.exists() && parent.is_dir() {
839 Some(parent.to_path_buf())
840 } else {
841 None
842 }
843 } else {
844 None
845 }
846 }
847
848 fn set_path(&mut self, path: &Path) -> Result<FileOutcome, io::Error> {
850 let old = self.path.clone();
851 let path = path.to_path_buf();
852
853 if old != path {
854 let mut dirs = Vec::new();
855 let mut files = Vec::new();
856
857 if self.find_parent(&path).is_some() {
858 dirs.push(OsString::from(".."));
859 }
860
861 for r in path.read_dir()? {
862 let Ok(r) = r else {
863 continue;
864 };
865
866 if let Ok(meta) = r.metadata() {
867 if meta.is_dir() {
868 dirs.push(r.file_name());
869 } else if meta.is_file() {
870 if let Some(filter) = self.filter.as_ref() {
871 if filter(&r.path()) {
872 files.push(r.file_name());
873 }
874 } else {
875 files.push(r.file_name());
876 }
877 }
878 }
879 }
880
881 self.path = path;
882 self.dirs = dirs;
883 self.files = files;
884
885 self.path_state.set_text(self.path.to_string_lossy());
886 self.path_state.move_to_line_end(false);
887
888 self.dir_state.cancel();
889 if !self.dirs.is_empty() {
890 self.dir_state.list.select(Some(0));
891 } else {
892 self.dir_state.list.select(None);
893 }
894 self.dir_state.list.set_offset(0);
895 if !self.files.is_empty() {
896 self.file_state.select(Some(0));
897 if let Some(name) = &self.save_name {
898 self.save_name_state.set_text(name.to_string_lossy());
899 } else {
900 self.save_name_state
901 .set_text(self.files[0].to_string_lossy());
902 }
903 } else {
904 self.file_state.select(None);
905 if let Some(name) = &self.save_name {
906 self.save_name_state.set_text(name.to_string_lossy());
907 } else {
908 self.save_name_state.set_text("");
909 }
910 }
911 self.file_state.set_offset(0);
912
913 Ok(FileOutcome::Changed)
914 } else {
915 Ok(FileOutcome::Unchanged)
916 }
917 }
918
919 fn use_path_input(&mut self) -> Result<FileOutcome, io::Error> {
920 let path = PathBuf::from(self.path_state.text());
921 if !path.exists() || !path.is_dir() {
922 self.path_state.invalid = true;
923 } else {
924 self.path_state.invalid = false;
925 self.set_path(&path)?;
926 }
927
928 Ok(FileOutcome::Changed)
929 }
930
931 fn chdir(&mut self, dir: &OsString) -> Result<FileOutcome, io::Error> {
932 if dir == &OsString::from("..") {
933 if let Some(parent) = self.find_parent(&self.path) {
934 self.set_path(&parent)
935 } else {
936 Ok(FileOutcome::Unchanged)
937 }
938 } else {
939 self.set_path(&self.path.join(dir))
940 }
941 }
942
943 fn chroot_selected(&mut self) -> Result<FileOutcome, io::Error> {
944 if let Some(select) = self.root_state.selected() {
945 if let Some(d) = self.roots.get(select).cloned() {
946 self.set_path(&d.1)?;
947 return Ok(FileOutcome::Changed);
948 }
949 }
950 Ok(FileOutcome::Unchanged)
951 }
952
953 fn chdir_selected(&mut self) -> Result<FileOutcome, io::Error> {
954 if let Some(select) = self.dir_state.list.selected() {
955 if let Some(dir) = self.dirs.get(select).cloned() {
956 self.chdir(&dir)?;
957 return Ok(FileOutcome::Changed);
958 }
959 }
960 Ok(FileOutcome::Unchanged)
961 }
962
963 fn name_selected(&mut self) -> Result<FileOutcome, io::Error> {
965 if let Some(select) = self.file_state.selected() {
966 if let Some(file) = self.files.get(select).cloned() {
967 let name = file.to_string_lossy();
968 self.save_name_state.set_text(name);
969 return Ok(FileOutcome::Changed);
970 }
971 }
972 Ok(FileOutcome::Unchanged)
973 }
974
975 fn start_edit_dir(&mut self) -> FileOutcome {
977 if !self.dir_state.is_editing() {
978 self.build_focus().focus(&self.dir_state);
979
980 self.dirs.push(OsString::from(""));
981 self.dir_state.editor.edit_dir.set_text("");
982 self.dir_state.edit_new(self.dirs.len() - 1);
983
984 FileOutcome::Changed
985 } else {
986 FileOutcome::Continue
987 }
988 }
989
990 fn cancel_edit_dir(&mut self) -> FileOutcome {
991 if self.dir_state.is_editing() {
992 self.dir_state.cancel();
993 self.dirs.remove(self.dirs.len() - 1);
994 FileOutcome::Changed
995 } else {
996 FileOutcome::Continue
997 }
998 }
999
1000 fn commit_edit_dir(&mut self) -> Result<FileOutcome, io::Error> {
1001 if self.dir_state.is_editing() {
1002 let name = self.dir_state.editor.edit_dir.text().trim();
1003 let path = self.path.join(name);
1004 if fs::create_dir(&path).is_err() {
1005 self.dir_state.editor.edit_dir.invalid = true;
1006 Ok(FileOutcome::Changed)
1007 } else {
1008 self.dir_state.commit();
1009 if self.mode == Mode::Save {
1010 self.build_focus().focus_no_lost(&self.save_name_state);
1011 }
1012 self.set_path(&path)
1013 }
1014 } else {
1015 Ok(FileOutcome::Unchanged)
1016 }
1017 }
1018
1019 fn close_cancel(&mut self) -> FileOutcome {
1021 self.active = false;
1022 self.dir_state.cancel();
1023 FileOutcome::Cancel
1024 }
1025
1026 fn choose_selected(&mut self) -> FileOutcome {
1028 if self.mode == Mode::Open {
1029 if let Some(select) = self.file_state.selected() {
1030 if let Some(file) = self.files.get(select).cloned() {
1031 self.active = false;
1032 return FileOutcome::Ok(self.path.join(file));
1033 }
1034 }
1035 } else if self.mode == Mode::Save {
1036 let mut path = self.path.join(self.save_name_state.text().trim());
1037 if let Some(ext) = &self.save_ext {
1038 if !ext.is_empty() {
1039 path.set_extension(ext);
1040 }
1041 }
1042 self.active = false;
1043 return FileOutcome::Ok(path);
1044 } else if self.mode == Mode::Dir {
1045 if let Some(select) = self.dir_state.list.selected() {
1046 if let Some(dir) = self.dirs.get(select).cloned() {
1047 self.active = false;
1048 if dir != ".." {
1049 return FileOutcome::Ok(self.path.join(dir));
1050 } else {
1051 return FileOutcome::Ok(self.path.clone());
1052 }
1053 }
1054 }
1055 }
1056 FileOutcome::Unchanged
1057 }
1058}
1059
1060impl HasScreenCursor for FileDialogState {
1061 fn screen_cursor(&self) -> Option<(u16, u16)> {
1062 if self.active {
1063 self.path_state
1064 .screen_cursor()
1065 .or_else(|| self.save_name_state.screen_cursor())
1066 .or_else(|| self.dir_state.screen_cursor())
1067 } else {
1068 None
1069 }
1070 }
1071}
1072
1073impl FileDialogState {
1074 fn build_focus(&self) -> Focus {
1075 let mut fb = FocusBuilder::default();
1076 fb.widget(&self.dir_state);
1077 if self.mode == Mode::Save || self.mode == Mode::Open {
1078 fb.widget(&self.file_state);
1079 }
1080 if self.mode == Mode::Save {
1081 fb.widget(&self.save_name_state);
1082 }
1083 fb.widget(&self.ok_state);
1084 fb.widget(&self.cancel_state);
1085 fb.widget(&self.new_state);
1086 fb.widget(&self.root_state);
1087 fb.widget(&self.path_state);
1088 fb.build()
1089 }
1090}
1091
1092impl HandleEvent<crossterm::event::Event, Dialog, Result<FileOutcome, io::Error>>
1093 for FileDialogState
1094{
1095 fn handle(
1096 &mut self,
1097 event: &crossterm::event::Event,
1098 _qualifier: Dialog,
1099 ) -> Result<FileOutcome, io::Error> {
1100 if !self.active {
1101 return Ok(FileOutcome::Continue);
1102 }
1103
1104 let mut focus = self.build_focus();
1105
1106 let mut f: FileOutcome = focus.handle(event, Regular).into();
1107 let mut r = FileOutcome::Continue;
1108
1109 f = f.or_else(|| match event {
1110 ct_event!(keycode press F(1)) => {
1111 if !self.root_state.is_focused() {
1112 focus.focus(&self.root_state);
1113 FileOutcome::Changed
1114 } else {
1115 FileOutcome::Continue
1116 }
1117 }
1118 ct_event!(keycode press F(2)) => {
1119 if !self.dir_state.is_focused() {
1120 focus.focus(&self.dir_state);
1121 FileOutcome::Changed
1122 } else {
1123 FileOutcome::Continue
1124 }
1125 }
1126 ct_event!(keycode press F(3)) => {
1127 if !self.file_state.is_focused() {
1128 focus.focus(&self.file_state);
1129 FileOutcome::Changed
1130 } else {
1131 FileOutcome::Continue
1132 }
1133 }
1134 ct_event!(keycode press F(4)) => {
1135 if !self.path_state.is_focused() {
1136 focus.focus(&self.path_state);
1137 FileOutcome::Changed
1138 } else {
1139 FileOutcome::Continue
1140 }
1141 }
1142 ct_event!(keycode press F(5)) => {
1143 if !self.save_name_state.is_focused() {
1144 focus.focus(&self.save_name_state);
1145 FileOutcome::Changed
1146 } else {
1147 FileOutcome::Continue
1148 }
1149 }
1150 _ => FileOutcome::Continue,
1151 });
1152
1153 r = r.or_else_try(|| {
1154 handle_path(self, event)?
1155 .or_else_try(|| {
1156 if self.mode == Mode::Save {
1157 handle_name(self, event)
1158 } else {
1159 Ok(FileOutcome::Continue)
1160 }
1161 })?
1162 .or_else_try(|| handle_files(self, event))?
1163 .or_else_try(|| handle_dirs(self, event))?
1164 .or_else_try(|| handle_roots(self, event))?
1165 .or_else_try(|| handle_new(self, event))?
1166 .or_else_try(|| handle_cancel(self, event))?
1167 .or_else_try(|| handle_ok(self, event))
1168 })?;
1169
1170 Ok(max(max(f, r), FileOutcome::Unchanged))
1171 }
1172}
1173
1174fn handle_new(
1175 state: &mut FileDialogState,
1176 event: &crossterm::event::Event,
1177) -> Result<FileOutcome, io::Error> {
1178 try_flow!(match state.new_state.handle(event, Regular) {
1179 ButtonOutcome::Pressed => {
1180 state.start_edit_dir()
1181 }
1182 r => Outcome::from(r).into(),
1183 });
1184 try_flow!(match event {
1185 ct_event!(key press CONTROL-'n') => {
1186 state.start_edit_dir()
1187 }
1188 _ => FileOutcome::Continue,
1189 });
1190 Ok(FileOutcome::Continue)
1191}
1192
1193fn handle_ok(
1194 state: &mut FileDialogState,
1195 event: &crossterm::event::Event,
1196) -> Result<FileOutcome, io::Error> {
1197 try_flow!(match state.ok_state.handle(event, Regular) {
1198 ButtonOutcome::Pressed => state.choose_selected(),
1199 r => Outcome::from(r).into(),
1200 });
1201 Ok(FileOutcome::Continue)
1202}
1203
1204fn handle_cancel(
1205 state: &mut FileDialogState,
1206 event: &crossterm::event::Event,
1207) -> Result<FileOutcome, io::Error> {
1208 try_flow!(match state.cancel_state.handle(event, Regular) {
1209 ButtonOutcome::Pressed => {
1210 state.close_cancel()
1211 }
1212 r => Outcome::from(r).into(),
1213 });
1214 try_flow!(match event {
1215 ct_event!(keycode press Esc) => {
1216 state.close_cancel()
1217 }
1218 _ => FileOutcome::Continue,
1219 });
1220 Ok(FileOutcome::Continue)
1221}
1222
1223fn handle_name(
1224 state: &mut FileDialogState,
1225 event: &crossterm::event::Event,
1226) -> Result<FileOutcome, io::Error> {
1227 try_flow!(Outcome::from(state.save_name_state.handle(event, Regular)));
1228 if state.save_name_state.is_focused() {
1229 try_flow!(match event {
1230 ct_event!(keycode press Enter) => {
1231 state.choose_selected()
1232 }
1233 _ => FileOutcome::Continue,
1234 });
1235 }
1236 Ok(FileOutcome::Continue)
1237}
1238
1239fn handle_path(
1240 state: &mut FileDialogState,
1241 event: &crossterm::event::Event,
1242) -> Result<FileOutcome, io::Error> {
1243 try_flow!(Outcome::from(state.path_state.handle(event, Regular)));
1244 if state.path_state.is_focused() {
1245 try_flow!(match event {
1246 ct_event!(keycode press Enter) => {
1247 state.use_path_input()?;
1248 state.build_focus().focus_no_lost(&state.dir_state.list);
1249 FileOutcome::Changed
1250 }
1251 _ => FileOutcome::Continue,
1252 });
1253 }
1254 on_lost!(
1255 state.path_state => {
1256 state.use_path_input()?
1257 }
1258 );
1259 Ok(FileOutcome::Continue)
1260}
1261
1262fn handle_roots(
1263 state: &mut FileDialogState,
1264 event: &crossterm::event::Event,
1265) -> Result<FileOutcome, io::Error> {
1266 try_flow!(match state.root_state.handle(event, Regular) {
1267 Outcome::Changed => {
1268 state.chroot_selected()?
1269 }
1270 r => r.into(),
1271 });
1272 Ok(FileOutcome::Continue)
1273}
1274
1275fn handle_dirs(
1276 state: &mut FileDialogState,
1277 event: &crossterm::event::Event,
1278) -> Result<FileOutcome, io::Error> {
1279 if matches!(event, ct_event!(keycode press F(2))) {
1281 return Ok(FileOutcome::Continue);
1282 }
1283
1284 try_flow!(match state.dir_state.handle(event, Regular) {
1285 EditOutcome::Edit => {
1286 state.chdir_selected()?
1287 }
1288 EditOutcome::Cancel => {
1289 state.cancel_edit_dir()
1290 }
1291 EditOutcome::Commit | EditOutcome::CommitAndAppend | EditOutcome::CommitAndEdit => {
1292 state.commit_edit_dir()?
1293 }
1294 r => {
1295 Outcome::from(r).into()
1296 }
1297 });
1298 if state.dir_state.list.is_focused() {
1299 try_flow!(handle_nav(&mut state.dir_state.list, &state.dirs, event));
1300 }
1301 Ok(FileOutcome::Continue)
1302}
1303
1304fn handle_files(
1305 state: &mut FileDialogState,
1306 event: &crossterm::event::Event,
1307) -> Result<FileOutcome, io::Error> {
1308 if state.file_state.is_focused() {
1309 try_flow!(match event {
1310 ct_event!(mouse any for m)
1311 if state
1312 .file_state
1313 .mouse
1314 .doubleclick(state.file_state.inner, m) =>
1315 {
1316 state.choose_selected()
1317 }
1318 ct_event!(keycode press Enter) => {
1319 state.choose_selected()
1320 }
1321 _ => FileOutcome::Continue,
1322 });
1323 try_flow!(
1324 match handle_nav(&mut state.file_state, &state.files, event) {
1325 FileOutcome::Changed => {
1326 if state.mode == Mode::Save {
1327 state.name_selected()?
1328 } else {
1329 FileOutcome::Changed
1330 }
1331 }
1332 r => r,
1333 }
1334 );
1335 }
1336 try_flow!(match state.file_state.handle(event, Regular).into() {
1337 FileOutcome::Changed => {
1338 if state.mode == Mode::Save {
1339 state.name_selected()?
1340 } else {
1341 FileOutcome::Changed
1342 }
1343 }
1344 r => r,
1345 });
1346 Ok(FileOutcome::Continue)
1347}
1348
1349fn handle_nav(
1350 list: &mut ListState<RowSelection>,
1351 nav: &[OsString],
1352 event: &crossterm::event::Event,
1353) -> FileOutcome {
1354 flow!(match event {
1355 ct_event!(key press c) => {
1356 let next = find_next_by_key(*c, list.selected().unwrap_or(0), nav);
1357 if let Some(next) = next {
1358 list.move_to(next).into()
1359 } else {
1360 FileOutcome::Unchanged
1361 }
1362 }
1363 _ => FileOutcome::Continue,
1364 });
1365 FileOutcome::Continue
1366}
1367
1368#[allow(clippy::question_mark)]
1369fn find_next_by_key(c: char, start: usize, names: &[OsString]) -> Option<usize> {
1370 let Some(c) = c.to_lowercase().next() else {
1371 return None;
1372 };
1373
1374 let mut idx = start;
1375 let mut selected = None;
1376 loop {
1377 idx += 1;
1378 if idx >= names.len() {
1379 idx = 0;
1380 }
1381 if idx == start {
1382 break;
1383 }
1384
1385 let nav = names[idx].to_string_lossy();
1386
1387 let initials = nav
1388 .split([' ', '_', '-'])
1389 .flat_map(|v| v.chars().next())
1390 .flat_map(|c| c.to_lowercase().next())
1391 .collect::<Vec<_>>();
1392 if initials.contains(&c) {
1393 selected = Some(idx);
1394 break;
1395 }
1396 }
1397
1398 selected
1399}