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 use_default_roots(&mut self, roots: bool) {
673 self.use_default_roots = roots;
674 }
675
676 pub fn add_root(&mut self, name: impl AsRef<str>, path: impl Into<PathBuf>) {
678 self.roots
679 .push((OsString::from(name.as_ref()), path.into()))
680 }
681
682 pub fn clear_roots(&mut self) {
684 self.roots.clear();
685 }
686
687 pub fn default_roots(&mut self, start: &Path, last: &Path) {
689 if last.exists() {
690 self.roots.push((
691 OsString::from("Last"), last.into(),
693 ));
694 }
695 self.roots.push((
696 OsString::from("Start"), start.into(),
698 ));
699
700 #[cfg(feature = "user_directories")]
701 {
702 if let Some(home) = home_dir() {
703 self.roots.push((OsString::from("Home"), home));
704 }
705 if let Some(documents) = document_dir() {
706 self.roots.push((OsString::from("Documents"), documents));
707 }
708 }
709
710 #[cfg(feature = "user_directories")]
711 {
712 let disks = Disks::new_with_refreshed_list();
713 for d in disks.list() {
714 self.roots
715 .push((d.name().to_os_string(), d.mount_point().to_path_buf()));
716 }
717 }
718
719 self.root_state.select(Some(0));
720 }
721
722 pub fn directory_dialog(&mut self, path: impl AsRef<Path>) -> Result<(), io::Error> {
724 let path = path.as_ref();
725 let old_path = self.path.clone();
726
727 self.active = true;
728 self.mode = Mode::Dir;
729 self.save_name = None;
730 self.save_ext = None;
731 self.dirs.clear();
732 self.files.clear();
733 self.path = Default::default();
734 if self.use_default_roots {
735 self.clear_roots();
736 self.default_roots(path, &old_path);
737 if old_path.exists() {
738 self.set_path(&old_path)?;
739 } else {
740 self.set_path(path)?;
741 }
742 } else {
743 self.set_path(path)?;
744 }
745 self.build_focus().focus(&self.dir_state);
746 Ok(())
747 }
748
749 pub fn open_dialog(&mut self, path: impl AsRef<Path>) -> Result<(), io::Error> {
751 let path = path.as_ref();
752 let old_path = self.path.clone();
753
754 self.active = true;
755 self.mode = Mode::Open;
756 self.save_name = None;
757 self.save_ext = None;
758 self.dirs.clear();
759 self.files.clear();
760 self.path = Default::default();
761 if self.use_default_roots {
762 self.clear_roots();
763 self.default_roots(path, &old_path);
764 if old_path.exists() {
765 self.set_path(&old_path)?;
766 } else {
767 self.set_path(path)?;
768 }
769 } else {
770 self.set_path(path)?;
771 }
772 self.build_focus().focus(&self.file_state);
773 Ok(())
774 }
775
776 pub fn save_dialog(
778 &mut self,
779 path: impl AsRef<Path>,
780 name: impl AsRef<str>,
781 ) -> Result<(), io::Error> {
782 self.save_dialog_ext(path, name, "")
783 }
784
785 pub fn save_dialog_ext(
787 &mut self,
788 path: impl AsRef<Path>,
789 name: impl AsRef<str>,
790 ext: impl AsRef<str>,
791 ) -> Result<(), io::Error> {
792 let path = path.as_ref();
793 let old_path = self.path.clone();
794
795 self.active = true;
796 self.mode = Mode::Save;
797 self.save_name = Some(OsString::from(name.as_ref()));
798 self.save_ext = Some(OsString::from(ext.as_ref()));
799 self.dirs.clear();
800 self.files.clear();
801 self.path = Default::default();
802 if self.use_default_roots {
803 self.clear_roots();
804 self.default_roots(path, &old_path);
805 if old_path.exists() {
806 self.set_path(&old_path)?;
807 } else {
808 self.set_path(path)?;
809 }
810 } else {
811 self.set_path(path)?;
812 }
813 self.build_focus().focus(&self.save_name_state);
814 Ok(())
815 }
816
817 fn find_parent(&self, path: &Path) -> Option<PathBuf> {
818 if path == Path::new(".") || path.file_name().is_none() {
819 let parent = path.join("..");
820 let canon_parent = parent.canonicalize().ok();
821 let canon_path = path.canonicalize().ok();
822 if canon_parent == canon_path {
823 None
824 } else if parent.exists() && parent.is_dir() {
825 Some(parent)
826 } else {
827 None
828 }
829 } else if let Some(parent) = path.parent() {
830 if parent.exists() && parent.is_dir() {
831 Some(parent.to_path_buf())
832 } else {
833 None
834 }
835 } else {
836 None
837 }
838 }
839
840 fn set_path(&mut self, path: &Path) -> Result<FileOutcome, io::Error> {
842 let old = self.path.clone();
843 let path = path.to_path_buf();
844
845 if old != path {
846 let mut dirs = Vec::new();
847 let mut files = Vec::new();
848
849 if self.find_parent(&path).is_some() {
850 dirs.push(OsString::from(".."));
851 }
852
853 for r in path.read_dir()? {
854 let Ok(r) = r else {
855 continue;
856 };
857
858 if let Ok(meta) = r.metadata() {
859 if meta.is_dir() {
860 dirs.push(r.file_name());
861 } else if meta.is_file() {
862 if let Some(filter) = self.filter.as_ref() {
863 if filter(&r.path()) {
864 files.push(r.file_name());
865 }
866 } else {
867 files.push(r.file_name());
868 }
869 }
870 }
871 }
872
873 self.path = path;
874 self.dirs = dirs;
875 self.files = files;
876
877 self.path_state.set_text(self.path.to_string_lossy());
878 self.path_state.move_to_line_end(false);
879
880 self.dir_state.cancel();
881 if !self.dirs.is_empty() {
882 self.dir_state.list.select(Some(0));
883 } else {
884 self.dir_state.list.select(None);
885 }
886 self.dir_state.list.set_offset(0);
887 if !self.files.is_empty() {
888 self.file_state.select(Some(0));
889 if let Some(name) = &self.save_name {
890 self.save_name_state.set_text(name.to_string_lossy());
891 } else {
892 self.save_name_state
893 .set_text(self.files[0].to_string_lossy());
894 }
895 } else {
896 self.file_state.select(None);
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.set_text("");
901 }
902 }
903 self.file_state.set_offset(0);
904
905 Ok(FileOutcome::Changed)
906 } else {
907 Ok(FileOutcome::Unchanged)
908 }
909 }
910
911 fn use_path_input(&mut self) -> Result<FileOutcome, io::Error> {
912 let path = PathBuf::from(self.path_state.text());
913 if !path.exists() || !path.is_dir() {
914 self.path_state.invalid = true;
915 } else {
916 self.path_state.invalid = false;
917 self.set_path(&path)?;
918 }
919
920 Ok(FileOutcome::Changed)
921 }
922
923 fn chdir(&mut self, dir: &OsString) -> Result<FileOutcome, io::Error> {
924 if dir == &OsString::from("..") {
925 if let Some(parent) = self.find_parent(&self.path) {
926 self.set_path(&parent)
927 } else {
928 Ok(FileOutcome::Unchanged)
929 }
930 } else {
931 self.set_path(&self.path.join(dir))
932 }
933 }
934
935 fn chroot_selected(&mut self) -> Result<FileOutcome, io::Error> {
936 if let Some(select) = self.root_state.selected() {
937 if let Some(d) = self.roots.get(select).cloned() {
938 self.set_path(&d.1)?;
939 return Ok(FileOutcome::Changed);
940 }
941 }
942 Ok(FileOutcome::Unchanged)
943 }
944
945 fn chdir_selected(&mut self) -> Result<FileOutcome, io::Error> {
946 if let Some(select) = self.dir_state.list.selected() {
947 if let Some(dir) = self.dirs.get(select).cloned() {
948 self.chdir(&dir)?;
949 return Ok(FileOutcome::Changed);
950 }
951 }
952 Ok(FileOutcome::Unchanged)
953 }
954
955 fn name_selected(&mut self) -> Result<FileOutcome, io::Error> {
957 if let Some(select) = self.file_state.selected() {
958 if let Some(file) = self.files.get(select).cloned() {
959 let name = file.to_string_lossy();
960 self.save_name_state.set_text(name);
961 return Ok(FileOutcome::Changed);
962 }
963 }
964 Ok(FileOutcome::Unchanged)
965 }
966
967 fn start_edit_dir(&mut self) -> FileOutcome {
969 if !self.dir_state.is_editing() {
970 self.build_focus().focus(&self.dir_state);
971
972 self.dirs.push(OsString::from(""));
973 self.dir_state.editor.edit_dir.set_text("");
974 self.dir_state.edit_new(self.dirs.len() - 1);
975
976 FileOutcome::Changed
977 } else {
978 FileOutcome::Continue
979 }
980 }
981
982 fn cancel_edit_dir(&mut self) -> FileOutcome {
983 if self.dir_state.is_editing() {
984 self.dir_state.cancel();
985 self.dirs.remove(self.dirs.len() - 1);
986 FileOutcome::Changed
987 } else {
988 FileOutcome::Continue
989 }
990 }
991
992 fn commit_edit_dir(&mut self) -> Result<FileOutcome, io::Error> {
993 if self.dir_state.is_editing() {
994 let name = self.dir_state.editor.edit_dir.text().trim();
995 let path = self.path.join(name);
996 if fs::create_dir(&path).is_err() {
997 self.dir_state.editor.edit_dir.invalid = true;
998 Ok(FileOutcome::Changed)
999 } else {
1000 self.dir_state.commit();
1001 if self.mode == Mode::Save {
1002 self.build_focus().focus_no_lost(&self.save_name_state);
1003 }
1004 self.set_path(&path)
1005 }
1006 } else {
1007 Ok(FileOutcome::Unchanged)
1008 }
1009 }
1010
1011 fn close_cancel(&mut self) -> FileOutcome {
1013 self.active = false;
1014 self.dir_state.cancel();
1015 FileOutcome::Cancel
1016 }
1017
1018 fn choose_selected(&mut self) -> FileOutcome {
1020 if self.mode == Mode::Open {
1021 if let Some(select) = self.file_state.selected() {
1022 if let Some(file) = self.files.get(select).cloned() {
1023 self.active = false;
1024 return FileOutcome::Ok(self.path.join(file));
1025 }
1026 }
1027 } else if self.mode == Mode::Save {
1028 let mut path = self.path.join(self.save_name_state.text().trim());
1029 if let Some(ext) = &self.save_ext {
1030 if !ext.is_empty() {
1031 path.set_extension(ext);
1032 }
1033 }
1034 self.active = false;
1035 return FileOutcome::Ok(path);
1036 } else if self.mode == Mode::Dir {
1037 if let Some(select) = self.dir_state.list.selected() {
1038 if let Some(dir) = self.dirs.get(select).cloned() {
1039 self.active = false;
1040 if dir != ".." {
1041 return FileOutcome::Ok(self.path.join(dir));
1042 } else {
1043 return FileOutcome::Ok(self.path.clone());
1044 }
1045 }
1046 }
1047 }
1048 FileOutcome::Unchanged
1049 }
1050}
1051
1052impl HasScreenCursor for FileDialogState {
1053 fn screen_cursor(&self) -> Option<(u16, u16)> {
1054 if self.active {
1055 self.path_state
1056 .screen_cursor()
1057 .or_else(|| self.save_name_state.screen_cursor())
1058 .or_else(|| self.dir_state.screen_cursor())
1059 } else {
1060 None
1061 }
1062 }
1063}
1064
1065impl FileDialogState {
1066 fn build_focus(&self) -> Focus {
1067 let mut fb = FocusBuilder::default();
1068 fb.widget(&self.dir_state);
1069 if self.mode == Mode::Save || self.mode == Mode::Open {
1070 fb.widget(&self.file_state);
1071 }
1072 if self.mode == Mode::Save {
1073 fb.widget(&self.save_name_state);
1074 }
1075 fb.widget(&self.ok_state);
1076 fb.widget(&self.cancel_state);
1077 fb.widget(&self.new_state);
1078 fb.widget(&self.root_state);
1079 fb.widget(&self.path_state);
1080 fb.build()
1081 }
1082}
1083
1084impl HandleEvent<crossterm::event::Event, Dialog, Result<FileOutcome, io::Error>>
1085 for FileDialogState
1086{
1087 fn handle(
1088 &mut self,
1089 event: &crossterm::event::Event,
1090 _qualifier: Dialog,
1091 ) -> Result<FileOutcome, io::Error> {
1092 if !self.active {
1093 return Ok(FileOutcome::Continue);
1094 }
1095
1096 let mut focus = self.build_focus();
1097
1098 let mut f: FileOutcome = focus.handle(event, Regular).into();
1099 let mut r = FileOutcome::Continue;
1100
1101 f = f.or_else(|| match event {
1102 ct_event!(keycode press F(1)) => {
1103 if !self.root_state.is_focused() {
1104 focus.focus(&self.root_state);
1105 FileOutcome::Changed
1106 } else {
1107 FileOutcome::Continue
1108 }
1109 }
1110 ct_event!(keycode press F(2)) => {
1111 if !self.dir_state.is_focused() {
1112 focus.focus(&self.dir_state);
1113 FileOutcome::Changed
1114 } else {
1115 FileOutcome::Continue
1116 }
1117 }
1118 ct_event!(keycode press F(3)) => {
1119 if !self.file_state.is_focused() {
1120 focus.focus(&self.file_state);
1121 FileOutcome::Changed
1122 } else {
1123 FileOutcome::Continue
1124 }
1125 }
1126 ct_event!(keycode press F(4)) => {
1127 if !self.path_state.is_focused() {
1128 focus.focus(&self.path_state);
1129 FileOutcome::Changed
1130 } else {
1131 FileOutcome::Continue
1132 }
1133 }
1134 ct_event!(keycode press F(5)) => {
1135 if !self.save_name_state.is_focused() {
1136 focus.focus(&self.save_name_state);
1137 FileOutcome::Changed
1138 } else {
1139 FileOutcome::Continue
1140 }
1141 }
1142 _ => FileOutcome::Continue,
1143 });
1144
1145 r = r.or_else_try(|| {
1146 handle_path(self, event)?
1147 .or_else_try(|| {
1148 if self.mode == Mode::Save {
1149 handle_name(self, event)
1150 } else {
1151 Ok(FileOutcome::Continue)
1152 }
1153 })?
1154 .or_else_try(|| handle_files(self, event))?
1155 .or_else_try(|| handle_dirs(self, event))?
1156 .or_else_try(|| handle_roots(self, event))?
1157 .or_else_try(|| handle_new(self, event))?
1158 .or_else_try(|| handle_cancel(self, event))?
1159 .or_else_try(|| handle_ok(self, event))
1160 })?;
1161
1162 Ok(max(max(f, r), FileOutcome::Unchanged))
1163 }
1164}
1165
1166fn handle_new(
1167 state: &mut FileDialogState,
1168 event: &crossterm::event::Event,
1169) -> Result<FileOutcome, io::Error> {
1170 try_flow!(match state.new_state.handle(event, Regular) {
1171 ButtonOutcome::Pressed => {
1172 state.start_edit_dir()
1173 }
1174 r => Outcome::from(r).into(),
1175 });
1176 try_flow!(match event {
1177 ct_event!(key press CONTROL-'n') => {
1178 state.start_edit_dir()
1179 }
1180 _ => FileOutcome::Continue,
1181 });
1182 Ok(FileOutcome::Continue)
1183}
1184
1185fn handle_ok(
1186 state: &mut FileDialogState,
1187 event: &crossterm::event::Event,
1188) -> Result<FileOutcome, io::Error> {
1189 try_flow!(match state.ok_state.handle(event, Regular) {
1190 ButtonOutcome::Pressed => state.choose_selected(),
1191 r => Outcome::from(r).into(),
1192 });
1193 Ok(FileOutcome::Continue)
1194}
1195
1196fn handle_cancel(
1197 state: &mut FileDialogState,
1198 event: &crossterm::event::Event,
1199) -> Result<FileOutcome, io::Error> {
1200 try_flow!(match state.cancel_state.handle(event, Regular) {
1201 ButtonOutcome::Pressed => {
1202 state.close_cancel()
1203 }
1204 r => Outcome::from(r).into(),
1205 });
1206 try_flow!(match event {
1207 ct_event!(keycode press Esc) => {
1208 state.close_cancel()
1209 }
1210 _ => FileOutcome::Continue,
1211 });
1212 Ok(FileOutcome::Continue)
1213}
1214
1215fn handle_name(
1216 state: &mut FileDialogState,
1217 event: &crossterm::event::Event,
1218) -> Result<FileOutcome, io::Error> {
1219 try_flow!(Outcome::from(state.save_name_state.handle(event, Regular)));
1220 if state.save_name_state.is_focused() {
1221 try_flow!(match event {
1222 ct_event!(keycode press Enter) => {
1223 state.choose_selected()
1224 }
1225 _ => FileOutcome::Continue,
1226 });
1227 }
1228 Ok(FileOutcome::Continue)
1229}
1230
1231fn handle_path(
1232 state: &mut FileDialogState,
1233 event: &crossterm::event::Event,
1234) -> Result<FileOutcome, io::Error> {
1235 try_flow!(Outcome::from(state.path_state.handle(event, Regular)));
1236 if state.path_state.is_focused() {
1237 try_flow!(match event {
1238 ct_event!(keycode press Enter) => {
1239 state.use_path_input()?;
1240 state.build_focus().focus_no_lost(&state.dir_state.list);
1241 FileOutcome::Changed
1242 }
1243 _ => FileOutcome::Continue,
1244 });
1245 }
1246 on_lost!(
1247 state.path_state => {
1248 state.use_path_input()?
1249 }
1250 );
1251 Ok(FileOutcome::Continue)
1252}
1253
1254fn handle_roots(
1255 state: &mut FileDialogState,
1256 event: &crossterm::event::Event,
1257) -> Result<FileOutcome, io::Error> {
1258 try_flow!(match state.root_state.handle(event, Regular) {
1259 Outcome::Changed => {
1260 state.chroot_selected()?
1261 }
1262 r => r.into(),
1263 });
1264 Ok(FileOutcome::Continue)
1265}
1266
1267fn handle_dirs(
1268 state: &mut FileDialogState,
1269 event: &crossterm::event::Event,
1270) -> Result<FileOutcome, io::Error> {
1271 if matches!(event, ct_event!(keycode press F(2))) {
1273 return Ok(FileOutcome::Continue);
1274 }
1275
1276 try_flow!(match state.dir_state.handle(event, Regular) {
1277 EditOutcome::Edit => {
1278 state.chdir_selected()?
1279 }
1280 EditOutcome::Cancel => {
1281 state.cancel_edit_dir()
1282 }
1283 EditOutcome::Commit | EditOutcome::CommitAndAppend | EditOutcome::CommitAndEdit => {
1284 state.commit_edit_dir()?
1285 }
1286 r => {
1287 Outcome::from(r).into()
1288 }
1289 });
1290 if state.dir_state.list.is_focused() {
1291 try_flow!(handle_nav(&mut state.dir_state.list, &state.dirs, event));
1292 }
1293 Ok(FileOutcome::Continue)
1294}
1295
1296fn handle_files(
1297 state: &mut FileDialogState,
1298 event: &crossterm::event::Event,
1299) -> Result<FileOutcome, io::Error> {
1300 if state.file_state.is_focused() {
1301 try_flow!(match event {
1302 ct_event!(mouse any for m)
1303 if state
1304 .file_state
1305 .mouse
1306 .doubleclick(state.file_state.inner, m) =>
1307 {
1308 state.choose_selected()
1309 }
1310 ct_event!(keycode press Enter) => {
1311 state.choose_selected()
1312 }
1313 _ => FileOutcome::Continue,
1314 });
1315 try_flow!(
1316 match handle_nav(&mut state.file_state, &state.files, event) {
1317 FileOutcome::Changed => {
1318 if state.mode == Mode::Save {
1319 state.name_selected()?
1320 } else {
1321 FileOutcome::Changed
1322 }
1323 }
1324 r => r,
1325 }
1326 );
1327 }
1328 try_flow!(match state.file_state.handle(event, Regular).into() {
1329 FileOutcome::Changed => {
1330 if state.mode == Mode::Save {
1331 state.name_selected()?
1332 } else {
1333 FileOutcome::Changed
1334 }
1335 }
1336 r => r,
1337 });
1338 Ok(FileOutcome::Continue)
1339}
1340
1341fn handle_nav(
1342 list: &mut ListState<RowSelection>,
1343 nav: &[OsString],
1344 event: &crossterm::event::Event,
1345) -> FileOutcome {
1346 flow!(match event {
1347 ct_event!(key press c) => {
1348 let next = find_next_by_key(*c, list.selected().unwrap_or(0), nav);
1349 if let Some(next) = next {
1350 list.move_to(next).into()
1351 } else {
1352 FileOutcome::Unchanged
1353 }
1354 }
1355 _ => FileOutcome::Continue,
1356 });
1357 FileOutcome::Continue
1358}
1359
1360#[allow(clippy::question_mark)]
1361fn find_next_by_key(c: char, start: usize, names: &[OsString]) -> Option<usize> {
1362 let Some(c) = c.to_lowercase().next() else {
1363 return None;
1364 };
1365
1366 let mut idx = start;
1367 let mut selected = None;
1368 loop {
1369 idx += 1;
1370 if idx >= names.len() {
1371 idx = 0;
1372 }
1373 if idx == start {
1374 break;
1375 }
1376
1377 let nav = names[idx].to_string_lossy();
1378
1379 let initials = nav
1380 .split([' ', '_', '-'])
1381 .flat_map(|v| v.chars().next())
1382 .flat_map(|c| c.to_lowercase().next())
1383 .collect::<Vec<_>>();
1384 if initials.contains(&c) {
1385 selected = Some(idx);
1386 break;
1387 }
1388 }
1389
1390 selected
1391}