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