1use cosmic::app::cosmic::Cosmic;
5use cosmic::app::{Core, Task, context_drawer};
6use cosmic::iced::core::SmolStr;
7use cosmic::iced::core::widget::operation;
8use cosmic::iced::futures::{self, SinkExt};
9use cosmic::iced::keyboard::key::{Named, Physical};
10use cosmic::iced::keyboard::{Event as KeyEvent, Key, Modifiers};
11#[cfg(feature = "wayland")]
12use cosmic::iced::platform_specific::shell::{self as iced_winit, SurfaceIdWrapper};
13use cosmic::iced::widget::scrollable;
14use cosmic::iced::widget::scrollable::AbsoluteOffset;
15use cosmic::iced::{
16 self, Alignment, Event, Length, Size, Subscription, event, mouse, stream, window,
17};
18use cosmic::widget::menu::key_bind::Modifier;
19use cosmic::widget::menu::{Action as MenuAction, KeyBind};
20use cosmic::widget::{self, Operation, segmented_button};
21use cosmic::{Application, ApplicationExt, Element, cosmic_config, cosmic_theme, executor, theme};
22use mime_guess::{Mime, mime};
23use notify_debouncer_full::notify::{self, RecommendedWatcher};
24use notify_debouncer_full::{DebouncedEvent, Debouncer, RecommendedCache, new_debouncer};
25use recently_used_xbel::update_recently_used;
26use rustc_hash::{FxHashMap, FxHashSet};
27use std::any::TypeId;
28use std::collections::{HashMap, VecDeque};
29use std::path::PathBuf;
30use std::time::{self, Instant};
31use std::{env, fmt, fs};
32
33use crate::app::{
34 Action, ContextPage, Message as AppMessage, PreviewItem, PreviewKind, REPLACE_BUTTON_ID,
35};
36use crate::config::{
37 Config, DialogConfig, Favorite, TIME_CONFIG_ID, ThumbCfg, TimeConfig, TypeToSearch,
38};
39use crate::key_bind::key_binds;
40use crate::localize::LANGUAGE_SORTER;
41use crate::mounter::{MOUNTERS, MounterItem, MounterItems, MounterKey, MounterMessage};
42use crate::russh::{CLIENTS, ClientItem, ClientItems, ClientKey, ClientMessage};
43use crate::tab::{self, ItemMetadata, Location, SearchLocation, Tab};
44use crate::zoom::{zoom_in_view, zoom_out_view, zoom_to_default};
45use crate::{fl, home_dir, menu};
46
47#[derive(Clone, Debug)]
48pub struct DialogMessage(cosmic::Action<Message>);
49
50#[derive(Clone, Debug)]
51pub enum DialogResult {
52 Cancel,
53 Open(Vec<PathBuf>),
54}
55
56#[derive(Clone, Debug)]
57pub enum DialogKind {
58 OpenFile,
59 OpenFolder,
60 OpenMultipleFiles,
61 OpenMultipleFolders,
62 SaveFile { filename: String },
63}
64
65impl DialogKind {
66 pub fn title(&self) -> String {
67 match self {
68 Self::OpenFile => fl!("open-file"),
69 Self::OpenFolder => fl!("open-folder"),
70 Self::OpenMultipleFiles => fl!("open-multiple-files"),
71 Self::OpenMultipleFolders => fl!("open-multiple-folders"),
72 Self::SaveFile { .. } => fl!("save-file"),
73 }
74 }
75
76 pub fn accept_label(&self) -> String {
77 match self {
78 Self::SaveFile { .. } => fl!("save"),
79 _ => fl!("open"),
80 }
81 }
82
83 pub const fn is_dir(&self) -> bool {
84 matches!(self, Self::OpenFolder | Self::OpenMultipleFolders)
85 }
86
87 pub const fn multiple(&self) -> bool {
88 matches!(self, Self::OpenMultipleFiles | Self::OpenMultipleFolders)
89 }
90
91 pub const fn save(&self) -> bool {
92 matches!(self, Self::SaveFile { .. })
93 }
94}
95
96#[derive(Clone, Debug)]
97pub struct DialogChoiceOption {
98 pub id: String,
99 pub label: String,
100}
101
102impl AsRef<str> for DialogChoiceOption {
103 fn as_ref(&self) -> &str {
104 &self.label
105 }
106}
107
108#[derive(Clone, Debug)]
109pub enum DialogChoice {
110 CheckBox {
111 id: String,
112 label: String,
113 value: bool,
114 },
115 ComboBox {
116 id: String,
117 label: String,
118 options: Vec<DialogChoiceOption>,
119 selected: Option<usize>,
120 },
121}
122
123#[derive(Clone, Debug)]
124pub enum DialogFilterPattern {
125 Glob(String),
126 Mime(String),
127}
128
129#[derive(Clone, Debug)]
130pub struct DialogFilter {
131 pub label: String,
132 pub patterns: Vec<DialogFilterPattern>,
133}
134
135impl AsRef<str> for DialogFilter {
136 fn as_ref(&self) -> &str {
137 &self.label
138 }
139}
140
141#[derive(Clone, Debug)]
142pub struct DialogLabelSpan {
143 pub text: String,
144 pub underline: bool,
145}
146
147#[derive(Clone, Debug)]
148pub struct DialogLabel {
149 pub spans: Vec<DialogLabelSpan>,
150 pub key_bind_opt: Option<KeyBind>,
151}
152
153impl<T: AsRef<str>> From<T> for DialogLabel {
154 fn from(text: T) -> Self {
155 let mut spans = Vec::<DialogLabelSpan>::new();
156 let mut key_bind_opt = None;
157 let mut next_underline = false;
158 for c in text.as_ref().chars() {
159 let underline = next_underline;
160 next_underline = false;
161
162 if c == '_' && !underline {
163 next_underline = true;
164 continue;
165 }
166
167 if underline && key_bind_opt.is_none() {
168 key_bind_opt = Some(KeyBind {
169 modifiers: vec![Modifier::Alt],
170 key: Key::Character(c.to_lowercase().to_string().into()),
171 });
172 }
173
174 if let Some(span) = spans.last_mut()
175 && underline == span.underline
176 {
177 span.text.push(c);
178 continue;
179 }
180
181 spans.push(DialogLabelSpan {
182 text: String::from(c),
183 underline,
184 });
185 }
186
187 Self {
188 spans,
189 key_bind_opt,
190 }
191 }
192}
193
194impl<'a, M: Clone + 'static> From<&'a DialogLabel> for Element<'a, M> {
195 fn from(label: &'a DialogLabel) -> Self {
196 let mut iced_spans: Vec<cosmic::iced::core::text::Span<'_, ()>> =
197 Vec::with_capacity(label.spans.len());
198 for span in &label.spans {
199 iced_spans.push(cosmic::iced::widget::span(&span.text).underline(span.underline));
200 }
201 cosmic::iced::widget::rich_text(iced_spans).into()
202 }
203}
204
205pub struct DialogSettings {
206 app_id: String,
207 kind: DialogKind,
208 path_opt: Option<PathBuf>,
209}
210
211impl DialogSettings {
212 pub fn new() -> Self {
213 Default::default()
214 }
215
216 pub fn app_id(mut self, app_id: String) -> Self {
217 self.app_id = app_id;
218 self
219 }
220
221 pub fn kind(mut self, kind: DialogKind) -> Self {
222 self.kind = kind;
223 self
224 }
225
226 pub fn path(mut self, path: PathBuf) -> Self {
227 self.path_opt = Some(path);
228 self
229 }
230}
231
232impl Default for DialogSettings {
233 fn default() -> Self {
234 Self {
235 app_id: App::APP_ID.to_string(),
236 kind: DialogKind::OpenFile,
237 path_opt: None,
238 }
239 }
240}
241
242pub struct Dialog<M> {
243 cosmic: Cosmic<App>,
244 mapper: fn(DialogMessage) -> M,
245 on_result: Box<dyn Fn(DialogResult) -> M>,
246}
247
248impl<M: Send + 'static> Dialog<M> {
249 pub fn new(
250 dialog_settings: DialogSettings,
251 mapper: fn(DialogMessage) -> M,
252 on_result: impl Fn(DialogResult) -> M + 'static,
253 ) -> (Self, Task<M>) {
254 crate::localize::localize();
256
257 let (config_handler, config) = Config::load();
258
259 let settings = window::Settings {
260 decorations: false,
261 exit_on_close_request: false,
262 min_size: Some(Size::new(360.0, 180.0)),
263 resizable: true,
264 size: Size::new(1024.0, 640.0),
265 transparent: true,
266 ..Default::default()
267 };
268
269 #[cfg(target_os = "linux")]
270 {
271 settings.platform_specific.application_id = dialog_settings.app_id;
272 }
273
274 let (window_id, window_command) = window::open(settings);
275
276 let mut core = Core::default();
277 core.set_main_window_id(Some(window_id));
278 let flags = Flags {
279 kind: dialog_settings.kind,
280 path_opt: dialog_settings.path_opt.as_ref().and_then(|path| {
281 match fs::canonicalize(path) {
282 Ok(ok) => Some(ok),
283 Err(err) => {
284 log::warn!("failed to canonicalize {}: {}", path.display(), err);
285 None
286 }
287 }
288 }),
289 window_id,
290 config_handler,
291 config,
292 };
293
294 let (cosmic, cosmic_command) = Cosmic::<App>::init((core, flags));
295 (
296 Self {
297 cosmic,
298 mapper,
299 on_result: Box::new(on_result),
300 },
301 Task::batch([
302 window_command.map(|_id| cosmic::action::none()),
303 cosmic_command
304 .map(DialogMessage)
305 .map(move |message| cosmic::action::app(mapper(message))),
306 ]),
307 )
308 }
309
310 pub fn set_title(&mut self, title: impl Into<String>) -> Task<M> {
311 let mapper = self.mapper;
312 self.cosmic.app.title = title.into();
313 self.cosmic
314 .app
315 .update_title()
316 .map(DialogMessage)
317 .map(move |message| cosmic::action::app(mapper(message)))
318 }
319
320 pub fn set_accept_label(&mut self, accept_label: impl AsRef<str>) {
321 self.cosmic.app.accept_label = DialogLabel::from(accept_label);
322 }
323
324 pub fn choices(&self) -> &[DialogChoice] {
325 &self.cosmic.app.choices
326 }
327
328 pub fn set_choices(&mut self, choices: impl Into<Vec<DialogChoice>>) {
329 self.cosmic.app.choices = choices.into();
330 }
331
332 pub fn filters(&self) -> (&[DialogFilter], Option<usize>) {
333 (&self.cosmic.app.filters, self.cosmic.app.filter_selected)
334 }
335
336 pub fn set_filters(
337 &mut self,
338 filters: impl Into<Vec<DialogFilter>>,
339 filter_selected: Option<usize>,
340 ) -> Task<M> {
341 let mapper = self.mapper;
342 self.cosmic.app.filters = filters.into();
343 self.cosmic.app.filter_selected = filter_selected;
344 self.cosmic
345 .app
346 .rescan_tab(None)
347 .map(DialogMessage)
348 .map(move |message| cosmic::action::app(mapper(message)))
349 }
350
351 pub fn subscription(&self) -> Subscription<M> {
352 self.cosmic
353 .subscription()
354 .map(DialogMessage)
355 .with(self.mapper)
356 .map(|(mapper, message)| mapper(message))
357 }
358
359 pub fn update(&mut self, message: DialogMessage) -> Task<M> {
360 let mapper = self.mapper;
361 let command = self
362 .cosmic
363 .update(message.0)
364 .map(DialogMessage)
365 .map(move |message| cosmic::action::app(mapper(message)));
366 if let Some(result) = self.cosmic.app.result_opt.take() {
367 #[cfg(feature = "wayland")]
368 if !self.cosmic.surface_views.is_empty() {
369 log::debug!("waiting for surfaces to close...");
370 let mut tasks = Vec::new();
371 for id in self.cosmic.surface_views.iter() {
372 match id.1.1 {
373 SurfaceIdWrapper::Window(id) => {
374 tasks.push(window::close::<M>(id).discard());
375 }
376 SurfaceIdWrapper::LayerSurface(id) => {
377 tasks.push(iced_winit::wayland::commands::layer_surface::destroy_layer_surface::<M>(id).discard());
378 }
379 SurfaceIdWrapper::Popup(id) => {
380 tasks.push(
381 iced_winit::wayland::commands::popup::destroy_popup::<M>(id)
382 .discard(),
383 );
384 }
385 SurfaceIdWrapper::Subsurface(id) => {
386 tasks.push(
387 iced_winit::wayland::commands::subsurface::destroy_subsurface::<M>(
388 id,
389 )
390 .discard(),
391 );
392 }
393 _ => {}
394 }
395 }
396 let on_result_message = (self.on_result)(result);
397
398 tasks.push(Task::future(async move {
399 cosmic::action::app(on_result_message)
400 }));
401 tasks.push(command);
402 return Task::batch(tasks);
403 }
404 let on_result_message = (self.on_result)(result);
405
406 Task::batch([
407 command,
408 Task::future(async move { cosmic::action::app(on_result_message) }),
409 ])
410 } else {
411 command
412 }
413 }
414
415 pub fn view(&self, window_id: window::Id) -> Element<'_, M> {
416 self.cosmic
417 .view(window_id)
418 .map(DialogMessage)
419 .map(self.mapper)
420 }
421
422 pub const fn window_id(&self) -> window::Id {
423 self.cosmic.app.flags.window_id
424 }
425
426 #[cfg(feature = "wayland")]
427 pub fn contains_surface(&self, id: &window::Id) -> bool {
428 self.cosmic.surface_views.contains_key(id)
429 }
430}
431
432#[derive(Clone, Debug)]
433enum DialogPage {
434 NewFolder { parent: PathBuf, name: String },
435 Replace { filename: String },
436}
437
438#[derive(Clone, Debug)]
439struct Flags {
440 kind: DialogKind,
441 path_opt: Option<PathBuf>,
442 window_id: window::Id,
443 #[allow(dead_code)]
444 config_handler: Option<cosmic_config::Config>,
445 config: Config,
446}
447
448#[derive(Clone, Debug)]
450enum Message {
451 None,
452 Cancel,
453 Choice(usize, usize),
454 Config(Config),
455 DialogCancel,
456 DialogComplete,
457 DialogUpdate(DialogPage),
458 Escape,
459 Filename(String),
460 Filter(usize),
461 Key(Modifiers, Key, Physical, Option<SmolStr>),
462 ModifiersChanged(Modifiers),
463 MounterItems(MounterKey, MounterItems),
464 ClientItems(ClientKey, ClientItems),
465 Mouse(window::Id, mouse::Button),
466 NewFolder,
467 NotifyEvents(Vec<DebouncedEvent>),
468 NotifyWatcher(WatcherWrapper),
469 Open,
470 Preview,
471 Save(bool),
472 ScrollTab(i16),
473 SearchActivate,
474 SearchClear,
475 SearchInput(String),
476 Surface(cosmic::surface::Action),
477 #[allow(clippy::enum_variant_names)]
478 TabMessage(tab::Message),
479 TabRescan(
480 Location,
481 Option<Box<tab::Item>>,
482 Vec<tab::Item>,
483 Option<Vec<PathBuf>>,
484 ),
485 TabView(tab::View),
486 TimeConfigChange(TimeConfig),
487 ToggleFoldersFirst,
488 ToggleShowHidden,
489 ZoomDefault,
490 ZoomIn,
491 ZoomOut,
492}
493
494impl From<AppMessage> for Message {
495 fn from(app_message: AppMessage) -> Self {
496 match app_message {
497 AppMessage::None => Self::None,
498 AppMessage::Preview(_entity_opt) => Self::Preview,
499 AppMessage::SearchActivate => Self::SearchActivate,
500 AppMessage::ScrollTab(scroll_speed) => Self::ScrollTab(scroll_speed),
501 AppMessage::TabMessage(_entity_opt, tab_message) => Self::TabMessage(tab_message),
502 AppMessage::TabView(_entity_opt, view) => Self::TabView(view),
503 AppMessage::ToggleFoldersFirst => Self::ToggleFoldersFirst,
504 AppMessage::ToggleShowHidden => Self::ToggleShowHidden,
505 AppMessage::ZoomDefault(_entity_opt) => Self::ZoomDefault,
506 AppMessage::ZoomIn(_entity_opt) => Self::ZoomIn,
507 AppMessage::ZoomOut(_entity_opt) => Self::ZoomOut,
508 AppMessage::NewItem(_entity_opt, true) => Self::NewFolder,
509 AppMessage::Surface(action) => Self::Surface(action),
510 unsupported => {
511 log::warn!("{unsupported:?} not supported in dialog mode");
512 Self::None
513 }
514 }
515 }
516}
517
518pub struct MounterData(MounterKey, MounterItem);
519pub struct ClientData(ClientKey, ClientItem);
520
521struct WatcherWrapper {
522 watcher_opt: Option<Debouncer<RecommendedWatcher, RecommendedCache>>,
523}
524
525impl Clone for WatcherWrapper {
526 fn clone(&self) -> Self {
527 Self { watcher_opt: None }
528 }
529}
530
531impl fmt::Debug for WatcherWrapper {
532 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
533 f.debug_struct("WatcherWrapper").finish()
534 }
535}
536
537impl PartialEq for WatcherWrapper {
538 fn eq(&self, _other: &Self) -> bool {
539 false
540 }
541}
542
543struct App {
545 core: Core,
546 flags: Flags,
547 title: String,
548 accept_label: DialogLabel,
549 choices: Vec<DialogChoice>,
550 #[allow(dead_code)]
551 context_menu_window: Option<window::Id>,
552 context_page: ContextPage,
553 dialog_pages: VecDeque<DialogPage>,
554 dialog_text_input: widget::Id,
555 filters: Vec<DialogFilter>,
556 filter_selected: Option<usize>,
557 filename_id: widget::Id,
558 modifiers: Modifiers,
559 mounter_items: FxHashMap<MounterKey, MounterItems>,
560 client_items: FxHashMap<ClientKey, ClientItems>,
561 nav_model: segmented_button::SingleSelectModel,
562 result_opt: Option<DialogResult>,
563 search_id: widget::Id,
564 tab: Tab,
565 key_binds: HashMap<KeyBind, Action>,
566 watcher_opt: Option<(
567 Debouncer<RecommendedWatcher, RecommendedCache>,
568 FxHashSet<PathBuf>,
569 )>,
570 auto_scroll_speed: Option<i16>,
571 type_select_prefix: String,
572 type_select_last_key: Option<Instant>,
573}
574
575impl App {
576 fn button_view(&self) -> Element<'_, Message> {
577 let cosmic_theme::Spacing {
578 space_xxxs,
579 space_xxs,
580 space_xs,
581 space_s,
582 space_l,
583 ..
584 } = theme::spacing();
585 let is_condensed = self.core().is_condensed();
586
587 let mut col = widget::column::with_capacity(2).spacing(space_xxs);
588 if let DialogKind::SaveFile { filename } = &self.flags.kind {
589 col = col.push(
590 widget::text_input("", filename)
591 .id(self.filename_id.clone())
592 .double_click_select_delimiter('.')
593 .on_input(Message::Filename)
594 .on_submit(|_| Message::Save(false)),
595 );
596 }
597
598 let mut row = widget::row::with_capacity(
599 usize::from(!self.filters.is_empty())
600 + self.choices.len() * 2
601 + if is_condensed { 0 } else { 3 },
602 )
603 .align_y(Alignment::Center)
604 .spacing(space_xxs);
605 if !self.filters.is_empty() {
606 row = row.push(widget::dropdown(
607 &self.filters,
608 self.filter_selected,
609 Message::Filter,
610 ));
611 }
612 for (choice_i, choice) in self.choices.iter().enumerate() {
613 match choice {
614 DialogChoice::CheckBox { label, value, .. } => {
615 row = row.push(
616 widget::checkbox(*value)
617 .label(label)
618 .on_toggle(move |checked| {
619 Message::Choice(choice_i, usize::from(checked))
620 }),
621 );
622 }
623 DialogChoice::ComboBox {
624 label,
625 options,
626 selected,
627 ..
628 } => {
629 row = row.push(widget::text::heading(label));
630 row = row.push(widget::dropdown(options, *selected, move |option_i| {
631 Message::Choice(choice_i, option_i)
632 }));
633 }
634 }
635 }
636
637 if is_condensed {
638 col = col.push(row);
639 row = widget::row::with_capacity(3)
640 .align_y(Alignment::Center)
641 .spacing(space_xxs);
642 }
643 row = row.push(widget::space::horizontal());
644 row = row.push(widget::button::standard(fl!("cancel")).on_press(Message::Cancel));
645
646 let mut has_selected = false;
647 if let Some(items) = self.tab.items_opt() {
648 for item in items {
649 if item.selected {
650 has_selected = true;
651 break;
652 }
653 }
654 }
655 row = row.push(
656 widget::button::custom(
658 widget::row::with_children([Element::from(&self.accept_label)])
659 .padding([0, space_s])
660 .width(Length::Shrink)
661 .height(space_l)
662 .spacing(space_xxxs)
663 .align_y(Alignment::Center)
664 )
665 .padding(0)
666 .on_press_maybe(if self.flags.kind.save() {
667 if let DialogKind::SaveFile { filename } = &self.flags.kind {
668 (!filename.is_empty()).then_some(Message::Save(false))
669 } else {
670 None
671 }
672 } else if has_selected || self.flags.kind.is_dir() {
673 Some(Message::Open)
674 } else {
675 None
676 })
677 .class(widget::button::ButtonClass::Suggested)
678 );
680
681 col = col.push(row);
682
683 widget::layer_container(col)
684 .layer(cosmic_theme::Layer::Primary)
685 .padding([8, space_xs])
686 .into()
687 }
688
689 fn preview<'a>(&'a self, kind: &'a PreviewKind) -> Element<'a, tab::Message> {
690 let military_time = self.tab.config.military_time;
691 let mut children = Vec::with_capacity(1);
692 match kind {
693 PreviewKind::Custom(PreviewItem(item)) => {
694 children.push(item.preview_view(None, military_time));
695 }
696 PreviewKind::Location(location) => {
697 if let Some(items) = self.tab.items_opt() {
698 for item in items {
699 if item.location_opt.as_ref() == Some(location) {
700 children.push(item.preview_view(None, military_time));
701 break;
704 }
705 }
706 }
707 }
708 PreviewKind::Selected => {
709 if let Some(items) = self.tab.items_opt() {
710 let preview_opt = {
711 let mut selected = items.iter().filter(|item| item.selected);
712
713 match (selected.next(), selected.next()) {
714 (Some(_), Some(_)) => Some(self.tab.multi_preview_view(None)),
716 (Some(item), None) => Some(item.preview_view(None, military_time)),
718 _ => None,
720 }
721 };
722
723 if let Some(preview) = preview_opt {
724 children.push(preview);
725 }
726
727 if children.is_empty()
728 && let Some(item) = &self.tab.parent_item_opt
729 {
730 children.push(item.preview_view(None, military_time));
731 }
732 }
733 }
734 }
735 widget::column::with_children(children).into()
736 }
737
738 fn rescan_tab(&self, selection_paths: Option<Vec<PathBuf>>) -> Task<Message> {
739 let location = self.tab.location.clone();
740 let icon_sizes = self.tab.config.icon_sizes;
741 let _mounter_items = self.mounter_items.clone();
742 let client_items = self.client_items.clone();
743 Task::future(async move {
744 let location2 = location.clone();
745 match tokio::task::spawn_blocking(move || location2.scan(icon_sizes)).await {
746 Ok((parent_item_opt, mut items)) => {
747 #[cfg(feature = "gvfs")]
748 {
749 let mounter_paths: Box<[_]> = mounter_items
750 .values()
751 .flatten()
752 .filter_map(MounterItem::path)
753 .collect();
754 if !mounter_paths.is_empty() {
755 for item in &mut items {
756 item.is_mount_point =
757 item.path_opt().is_some_and(|p| mounter_paths.contains(p));
758 }
759 }
760 }
761 #[cfg(feature = "russh")]
762 {
763 let client_paths: Box<[_]> = client_items
764 .values()
765 .flatten()
766 .filter_map(ClientItem::path)
767 .collect();
768 if !client_paths.is_empty() {
769 for item in &mut items {
770 item.is_client_point =
771 item.path_opt().is_some_and(|p| client_paths.contains(p));
772 }
773 }
774 }
775 cosmic::action::app(Message::TabRescan(
776 location,
777 parent_item_opt,
778 items,
779 selection_paths,
780 ))
781 }
782 Err(err) => {
783 log::warn!("failed to rescan: {err}");
784 cosmic::action::none()
785 }
786 }
787 })
788 }
789
790 fn search_get(&self) -> Option<&str> {
791 match &self.tab.location {
792 Location::Search(_, term, ..) => Some(term),
793 _ => None,
794 }
795 }
796
797 fn search_set(&mut self, term_opt: Option<String>) -> Task<Message> {
798 let location_opt = match term_opt {
799 Some(term) => {
800 let search_location = if let Some(path) = self.tab.location.path_opt() {
801 Some(SearchLocation::Path(path.clone()))
802 } else if self.tab.location.is_recents() {
803 Some(SearchLocation::Recents)
804 } else if self.tab.location.is_trash() {
805 Some(SearchLocation::Trash)
806 } else {
807 None
808 };
809
810 search_location.map(|search_location| {
811 (
812 Location::Search(
813 search_location,
814 term,
815 self.tab.config.show_hidden,
816 Instant::now(),
817 ),
818 true,
819 )
820 })
821 }
822 None => match &self.tab.location {
823 Location::Search(search_location, ..) => match search_location {
824 SearchLocation::Path(path) => Some((Location::Path(path.clone()), false)),
825 SearchLocation::Recents => Some((Location::Recents, false)),
826 SearchLocation::Trash => Some((Location::Trash, false)),
827 },
828 _ => None,
829 },
830 };
831 if let Some((location, focus_search)) = location_opt {
832 self.tab.change_location(&location, None);
833 return Task::batch([
834 self.update_title(),
835 self.update_watcher(),
836 self.rescan_tab(None),
837 if focus_search {
838 widget::text_input::focus(self.search_id.clone())
839 } else {
840 Task::none()
841 },
842 ]);
843 }
844 Task::none()
845 }
846
847 fn update_config(&mut self) -> Task<Message> {
848 self.core.window.show_context = self.flags.config.dialog.show_details;
849 let config = self.flags.config.dialog_tab();
850 self.tab.config.view = config.view;
851 self.update_nav_model();
852 self.update(Message::TabMessage(tab::Message::Config(config)))
853 }
854
855 fn with_dialog_config<F: Fn(&mut DialogConfig)>(&mut self, f: F) -> Task<Message> {
856 let mut dialog = self.flags.config.dialog;
857 f(&mut dialog);
858 if dialog == self.flags.config.dialog {
859 Task::none()
860 } else {
861 if let Some(config_handler) = &self.flags.config_handler {
862 match self.flags.config.set_dialog(config_handler, dialog) {
863 Ok(_) => {}
864 Err(err) => {
865 log::warn!("failed to save config \"dialog\": {err}");
866 }
867 }
868 } else {
869 self.flags.config.dialog = dialog;
870 log::warn!("failed to save config \"dialog\": no config handler",);
871 }
872 self.update_config()
873 }
874 }
875
876 fn activate_nav_model_location(&mut self, location: &Location) {
877 let nav_bar_id = self.nav_model.iter().find(|&id| {
878 self.nav_model
879 .data::<Location>(id)
880 .is_some_and(|l| l == location)
881 });
882
883 if let Some(id) = nav_bar_id {
884 self.nav_model.activate(id);
885 } else {
886 let active = self.nav_model.active();
887 segmented_button::Selectable::deactivate(&mut self.nav_model, active);
888 }
889 }
890
891 fn close_context_menus(&mut self) -> Task<Message> {
892 self.tab.location_context_menu_index = None;
893 if self.tab.context_menu.is_some() {
894 return self.update(Message::TabMessage(tab::Message::ContextMenu(None, None)));
895 }
896
897 Task::none()
898 }
899
900 fn update_nav_model(&mut self) {
901 let mut nav_model = segmented_button::ModelBuilder::default();
902
903 if self.flags.config.show_recents {
904 nav_model = nav_model.insert(|b| {
905 b.text(fl!("recents"))
906 .icon(widget::icon::from_name("document-open-recent-symbolic"))
907 .data(Location::Recents)
908 });
909 }
910
911 for favorite in &self.flags.config.favorites {
912 if let Some(path) = favorite.path_opt() {
913 let name = if matches!(favorite, Favorite::Home) {
914 fl!("home")
915 } else if let Favorite::Network { name, .. } = favorite {
916 name.clone()
917 } else if let Some(file_name) = path.file_name().and_then(|x| x.to_str()) {
918 file_name.to_string()
919 } else {
920 continue;
921 };
922 nav_model = nav_model.insert(move |b| {
923 b.text(name.clone())
924 .icon(
925 widget::icon::icon(if path.is_dir() {
926 tab::folder_icon_symbolic(&path, 16)
927 } else {
928 widget::icon::from_name("text-x-generic-symbolic")
929 .size(16)
930 .handle()
931 })
932 .size(16),
933 )
934 .data(Location::Path(path.clone()))
935 });
936 }
937 }
938
939 let mut nav_items = Vec::new();
941 for (key, items) in &self.mounter_items {
942 nav_items.extend(items.iter().map(|item| (*key, item)));
943 }
944 nav_items.sort_unstable_by(|a, b| LANGUAGE_SORTER.compare(&a.1.name(), &b.1.name()));
946 for (i, (key, item)) in nav_items.into_iter().enumerate() {
948 nav_model = nav_model.insert(|mut b| {
949 b = b.text(item.name()).data(MounterData(key, item.clone()));
950 if let Some(path) = item.path() {
951 b = b.data(Location::Path(path));
952 }
953 if let Some(icon) = item.icon(true) {
954 b = b.icon(widget::icon::icon(icon).size(16));
955 }
956 if item.is_mounted() {
957 b = b.closable();
958 }
959 if i == 0 {
960 b = b.divider_above();
961 }
962 b
963 });
964 }
965
966 self.nav_model = nav_model.build();
967
968 self.activate_nav_model_location(&self.tab.location.clone());
969 }
970
971 fn update_title(&mut self) -> Task<Message> {
972 self.set_header_title(self.title.clone());
973 self.set_window_title(self.title.clone(), self.flags.window_id)
974 }
975
976 fn update_watcher(&mut self) -> Task<Message> {
977 if let Some((mut watcher, old_paths)) = self.watcher_opt.take() {
978 let mut new_paths = FxHashSet::default();
979 if let Some(path) = &self.tab.location.path_opt() {
980 new_paths.insert((*path).clone());
981 }
982
983 for path in &old_paths {
985 if !new_paths.contains(path) {
986 match watcher.unwatch(path) {
987 Ok(()) => {
988 log::debug!("unwatching {}", path.display());
989 }
990 Err(err) => {
991 log::debug!("failed to unwatch {}: {}", path.display(), err);
992 }
993 }
994 }
995 }
996
997 for path in &new_paths {
999 if !old_paths.contains(path) {
1000 match watcher.watch(path, notify::RecursiveMode::NonRecursive) {
1002 Ok(()) => {
1003 log::debug!("watching {}", path.display());
1004 }
1005 Err(err) => {
1006 log::debug!("failed to watch {}: {}", path.display(), err);
1007 }
1008 }
1009 }
1010 }
1011
1012 self.watcher_opt = Some((watcher, new_paths));
1013 }
1014
1015 Task::none()
1017 }
1018}
1019
1020impl Application for App {
1022 type Executor = executor::Default;
1024
1025 type Flags = Flags;
1027
1028 type Message = Message;
1030
1031 const APP_ID: &'static str = "com.system76.CosmicFilesDialog";
1033
1034 fn core(&self) -> &Core {
1035 &self.core
1036 }
1037
1038 fn core_mut(&mut self) -> &mut Core {
1039 &mut self.core
1040 }
1041
1042 fn init(mut core: Core, flags: Self::Flags) -> (Self, Task<Message>) {
1044 core.window.context_is_overlay = false;
1045 core.window.show_close = false;
1046 core.window.show_maximize = false;
1047 core.window.show_minimize = false;
1048
1049 let title = flags.kind.title();
1050 let accept_label = flags.kind.accept_label();
1051
1052 let location = Location::Path(match &flags.path_opt {
1053 Some(path) => path.clone(),
1054 None => match env::current_dir() {
1055 Ok(path) => path,
1056 Err(_) => home_dir(),
1057 },
1058 });
1059
1060 let mut tab = Tab::new(
1061 location,
1062 flags.config.dialog_tab(),
1063 flags.config.tb_config(),
1064 ThumbCfg::default(),
1065 None,
1066 widget::Id::unique(),
1067 None,
1068 );
1069 tab.mode = tab::Mode::Dialog(flags.kind.clone());
1070 tab.sort_name = tab::HeadingOptions::Modified;
1071 tab.sort_direction = false;
1072
1073 let key_binds = key_binds(&tab.mode);
1074
1075 let mut app = Self {
1076 core,
1077 flags,
1078 title,
1079 accept_label: DialogLabel::from(accept_label),
1080 choices: Vec::new(),
1081 context_menu_window: None,
1082 context_page: ContextPage::Preview(None, PreviewKind::Selected),
1083 dialog_pages: VecDeque::new(),
1084 dialog_text_input: widget::Id::new("Dialog Text Input"),
1085 filters: Vec::new(),
1086 filter_selected: None,
1087 filename_id: widget::Id::new("Dialog Filename"),
1088 modifiers: Modifiers::empty(),
1089 mounter_items: FxHashMap::default(),
1090 client_items: FxHashMap::default(),
1091 nav_model: segmented_button::ModelBuilder::default().build(),
1092 result_opt: None,
1093 search_id: widget::Id::new("Dialog File Search"),
1094 tab,
1095 key_binds,
1096 watcher_opt: None,
1097 auto_scroll_speed: None,
1098 type_select_prefix: String::new(),
1099 type_select_last_key: None,
1100 };
1101
1102 let commands = Task::batch([
1103 app.update_config(),
1104 app.update_title(),
1105 app.update_watcher(),
1106 app.rescan_tab(None),
1107 ]);
1108
1109 (app, commands)
1110 }
1111
1112 fn context_drawer(&self) -> Option<context_drawer::ContextDrawer<'_, Message>> {
1113 if !self.core.window.show_context {
1114 return None;
1115 }
1116
1117 match &self.context_page {
1118 ContextPage::Preview(_, kind) => {
1119 let actions = self
1120 .tab
1121 .items_opt()
1122 .and_then(|items| {
1123 items
1124 .iter()
1125 .find(|item| item.selected)
1126 .map(|item| item.preview_actions().map(Message::TabMessage))
1127 })
1128 .unwrap_or_else(|| widget::space::horizontal().into());
1129 Some(
1130 context_drawer::context_drawer(
1131 self.preview(kind).map(Message::TabMessage),
1132 Message::Preview,
1133 )
1134 .actions(actions),
1135 )
1136 }
1137 _ => None,
1138 }
1139 }
1140
1141 fn dialog(&self) -> Option<Element<'_, Message>> {
1142 let cosmic_theme::Spacing { space_xxs, .. } = theme::spacing();
1143
1144 if self.tab.gallery {
1146 return Some(
1147 widget::column::with_children([
1148 self.tab.gallery_view().map(Message::TabMessage),
1149 widget::container(self.button_view())
1151 .width(Length::Fill)
1152 .padding(space_xxs)
1153 .class(theme::Container::WindowBackground)
1154 .into(),
1155 ])
1156 .into(),
1157 );
1158 }
1159
1160 let dialog_page = self.dialog_pages.front()?;
1161
1162 let dialog = match dialog_page {
1163 DialogPage::NewFolder { parent, name } => {
1164 let mut dialog = widget::dialog().title(fl!("create-new-folder"));
1165
1166 let complete_maybe = if name.is_empty() {
1167 None
1168 } else if name == "." || name == ".." {
1169 dialog = dialog.tertiary_action(widget::text::body(fl!(
1170 "name-invalid",
1171 filename = name.as_str()
1172 )));
1173 None
1174 } else if name.contains('/') {
1175 dialog = dialog.tertiary_action(widget::text::body(fl!("name-no-slashes")));
1176 None
1177 } else {
1178 let path = parent.join(name);
1179 if path.exists() {
1180 if path.is_dir() {
1181 dialog = dialog
1182 .tertiary_action(widget::text::body(fl!("folder-already-exists")));
1183 } else {
1184 dialog = dialog
1185 .tertiary_action(widget::text::body(fl!("file-already-exists")));
1186 }
1187 None
1188 } else {
1189 if name.starts_with('.') {
1190 dialog = dialog.tertiary_action(widget::text::body(fl!("name-hidden")));
1191 }
1192 Some(Message::DialogComplete)
1193 }
1194 };
1195
1196 dialog
1197 .primary_action(
1198 widget::button::suggested(fl!("save"))
1199 .on_press_maybe(complete_maybe.clone()),
1200 )
1201 .secondary_action(
1202 widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel),
1203 )
1204 .control(
1205 widget::column::with_children([
1206 widget::text::body(fl!("folder-name")).into(),
1207 widget::text_input("", name.as_str())
1208 .id(self.dialog_text_input.clone())
1209 .on_input(move |name| {
1210 Message::DialogUpdate(DialogPage::NewFolder {
1211 parent: parent.clone(),
1212 name,
1213 })
1214 })
1215 .on_submit_maybe(complete_maybe.map(|maybe| move |_| maybe.clone()))
1216 .into(),
1217 ])
1218 .spacing(space_xxs),
1219 )
1220 }
1221 DialogPage::Replace { filename } => widget::dialog()
1222 .title(fl!("replace-title", filename = filename.as_str()))
1223 .icon(widget::icon::from_name("dialog-question").size(64))
1224 .body(fl!("replace-warning"))
1225 .primary_action(
1226 widget::button::suggested(fl!("replace"))
1227 .on_press(Message::DialogComplete)
1228 .id(REPLACE_BUTTON_ID.clone()),
1229 )
1230 .secondary_action(
1231 widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel),
1232 ),
1233 };
1234
1235 Some(dialog.into())
1236 }
1237
1238 fn footer(&self) -> Option<Element<'_, Message>> {
1239 Some(self.button_view())
1240 }
1241
1242 fn header_end(&self) -> Vec<Element<'_, Message>> {
1243 let mut elements = Vec::with_capacity(3);
1244
1245 if let Some(term) = self.search_get() {
1246 if self.core.is_condensed() {
1247 elements.push(
1248 widget::button::icon(widget::icon::from_name("system-search-symbolic"))
1250 .on_press(Message::SearchClear)
1251 .padding(8)
1252 .selected(true)
1253 .into(),
1254 );
1255 } else {
1256 elements.push(
1257 widget::text_input::search_input("", term)
1258 .width(Length::Fixed(240.0))
1259 .id(self.search_id.clone())
1260 .on_clear(Message::SearchClear)
1261 .on_input(Message::SearchInput)
1262 .into(),
1263 );
1264 }
1265 } else {
1266 elements.push(
1267 widget::button::icon(widget::icon::from_name("system-search-symbolic"))
1268 .on_press(Message::SearchActivate)
1269 .padding(8)
1270 .into(),
1271 );
1272 }
1273
1274 if self.flags.kind.save() {
1275 elements.push(
1276 widget::button::icon(widget::icon::from_name("folder-new-symbolic"))
1277 .on_press(Message::NewFolder)
1278 .padding(8)
1279 .into(),
1280 );
1281 }
1282
1283 let show_details = match self.context_page {
1284 ContextPage::Preview(..) => self.core.window.show_context,
1285 _ => false,
1286 };
1287 elements
1288 .push(menu::dialog_menu(&self.tab, &self.key_binds, show_details).map(Message::from));
1289
1290 elements
1291 }
1292
1293 fn nav_bar(&self) -> Option<Element<'_, cosmic::Action<Self::Message>>> {
1294 if !self.core().nav_bar_active() {
1295 return None;
1296 }
1297
1298 let nav_model = self.nav_model()?;
1299
1300 let mut nav = cosmic::widget::nav_bar(nav_model, |entity| {
1301 cosmic::action::cosmic(cosmic::app::Action::NavBar(entity))
1302 })
1303 .close_icon(
1305 widget::icon::from_name("media-eject-symbolic")
1306 .size(16)
1307 .icon(),
1308 )
1309 .into_container();
1310
1311 if !self.core().is_condensed() {
1312 nav = nav.max_width(280);
1313 }
1314
1315 Some(Element::from(
1316 nav.width(Length::Shrink).height(Length::Fill),
1317 ))
1318 }
1319
1320 fn nav_model(&self) -> Option<&segmented_button::SingleSelectModel> {
1321 Some(&self.nav_model)
1322 }
1323
1324 fn on_app_exit(&mut self) -> Option<Message> {
1325 self.result_opt = Some(DialogResult::Cancel);
1326 None
1327 }
1328
1329 fn on_nav_select(&mut self, entity: segmented_button::Entity) -> Task<Message> {
1330 self.nav_model.activate(entity);
1331 if let Some(location) = self.nav_model.data::<Location>(entity) {
1332 let message = Message::TabMessage(tab::Message::Location(location.clone()));
1333 return self.update(message);
1334 }
1335
1336 if let Some(data) = self.nav_model.data::<MounterData>(entity)
1337 && let Some(mounter) = MOUNTERS.get(&data.0)
1338 {
1339 return mounter
1340 .mount(data.1.clone())
1341 .map(|()| cosmic::action::none());
1342 }
1343 if let Some(data) = self.nav_model.data::<ClientData>(entity)
1344 && let Some(client) = CLIENTS.get(&data.0) {
1345 return client
1346 .connect(data.1.clone())
1347 .map(|()| cosmic::action::none());
1348 }
1349 Task::none()
1350 }
1351
1352 fn on_escape(&mut self) -> Task<Message> {
1353 if self.tab.gallery {
1354 self.tab.gallery = false;
1356 return Task::none();
1357 }
1358
1359 if self.tab.location_context_menu_index.is_some() {
1360 self.tab.location_context_menu_index = None;
1361 return Task::none();
1362 }
1363
1364 if self.tab.context_menu.is_some() {
1365 return self.update(Message::TabMessage(tab::Message::ContextMenu(None, None)));
1366 }
1367
1368 if self.tab.edit_location.is_some() {
1369 self.tab.edit_location = None;
1371 return Task::none();
1372 }
1373
1374 if self.search_get().is_some() {
1375 return self.search_set(None);
1377 }
1378
1379 let had_focused_button = self.tab.select_focus_id().is_some();
1380 if self.tab.select_none() {
1381 if had_focused_button {
1382 return widget::button::focus(widget::Id::unique());
1384 }
1385 return Task::none();
1386 }
1387
1388 if let operation::Outcome::Some(focused) = operation::focusable::find_focused().finish()
1391 && self.dialog_text_input == focused
1392 {
1393 return self.update(Message::Cancel);
1394 }
1395
1396 self.update(Message::Cancel)
1397 }
1398
1399 fn update(&mut self, message: Message) -> Task<Message> {
1401 match message {
1402 Message::None => {}
1403 Message::Cancel => {
1404 self.result_opt = Some(DialogResult::Cancel);
1405 return window::close(self.flags.window_id);
1406 }
1407 Message::Choice(choice_i, option_i) => {
1408 if let Some(choice) = self.choices.get_mut(choice_i) {
1409 match choice {
1410 DialogChoice::CheckBox { value, .. } => *value = option_i > 0,
1411 DialogChoice::ComboBox {
1412 options, selected, ..
1413 } => {
1414 if option_i < options.len() {
1415 *selected = Some(option_i);
1416 } else {
1417 *selected = None;
1418 }
1419 }
1420 }
1421 }
1422 }
1423 Message::Config(config) => {
1424 if config != self.flags.config {
1425 log::info!("update config");
1426 let military_time = self.flags.config.tab.military_time;
1428 self.flags.config = config;
1429 self.flags.config.tab.military_time = military_time;
1430 return self.update_config();
1431 }
1432 }
1433 Message::DialogCancel => {
1434 self.dialog_pages.pop_front();
1435 }
1436 Message::DialogComplete => {
1437 if let Some(dialog_page) = self.dialog_pages.pop_front() {
1438 match dialog_page {
1439 DialogPage::NewFolder { parent, name } => {
1440 let path = parent.join(name);
1441 match fs::create_dir(&path) {
1442 Ok(()) => {
1443 let message = Message::TabMessage(tab::Message::Location(
1445 Location::Path(path),
1446 ));
1447 return self.update(message);
1448 }
1449 Err(err) => {
1450 log::warn!("failed to create {}: {}", path.display(), err);
1451 }
1452 }
1453 }
1454 DialogPage::Replace { .. } => {
1455 return self.update(Message::Save(true));
1456 }
1457 }
1458 }
1459 }
1460 Message::DialogUpdate(dialog_page) => {
1461 if !self.dialog_pages.is_empty() {
1462 self.dialog_pages[0] = dialog_page;
1463 }
1464 }
1465 Message::Escape => return self.on_escape(),
1466 Message::Filename(new_filename) => {
1467 self.tab.select_name(&new_filename);
1469
1470 if let DialogKind::SaveFile { filename } = &mut self.flags.kind {
1471 *filename = new_filename;
1472 }
1473 }
1474 Message::Filter(filter_i) => {
1475 if filter_i < self.filters.len() {
1476 self.filter_selected = Some(filter_i);
1477 } else {
1478 self.filter_selected = None;
1479 }
1480 return self.rescan_tab(None);
1481 }
1482 Message::Key(modifiers, key, physical_key, text) => {
1483 for (key_bind, action) in &self.key_binds {
1484 if key_bind.matches(modifiers, &key, Some(&physical_key)) {
1485 return self.update(Message::from(action.message()));
1486 }
1487 }
1488
1489 if let Some(key_bind) = &self.accept_label.key_bind_opt
1491 && key_bind.matches(modifiers, &key, Some(&physical_key))
1492 {
1493 return self.update(if self.flags.kind.save() {
1494 Message::Save(false)
1495 } else {
1496 Message::Open
1497 });
1498 }
1499
1500 if !modifiers.logo()
1502 && !modifiers.control()
1503 && !modifiers.alt()
1504 && matches!(key, Key::Character(_))
1505 && let Some(text) = text
1506 {
1507 match self.flags.config.type_to_search {
1508 TypeToSearch::Recursive => {
1509 let mut term = self.search_get().unwrap_or_default().to_string();
1510 term.push_str(&text);
1511 return self.search_set(Some(term));
1512 }
1513 TypeToSearch::EnterPath => {
1514 let location = (self.tab.edit_location)
1515 .as_ref()
1516 .map_or_else(|| &self.tab.location, |x| &x.location);
1517 if let Some(path) = location.path_opt() {
1519 let mut path_string = path.to_string_lossy().to_string();
1520 path_string.push_str(&text);
1521 self.tab.edit_location =
1522 Some(location.with_path(PathBuf::from(path_string)).into());
1523 }
1524 }
1525 TypeToSearch::SelectByPrefix => {
1526 if let Some(last_key) = self.type_select_last_key
1528 && last_key.elapsed() >= tab::TYPE_SELECT_TIMEOUT
1529 {
1530 self.type_select_prefix.clear();
1531 }
1532
1533 self.type_select_prefix.push_str(&text.to_lowercase());
1535 self.type_select_last_key = Some(Instant::now());
1536
1537 self.tab.select_by_prefix(&self.type_select_prefix);
1538 if let Some(offset) = self.tab.select_focus_scroll() {
1539 return scrollable::scroll_to(
1540 self.tab.scrollable_id.clone(),
1541 AbsoluteOffset {
1542 x: Some(offset.x),
1543 y: Some(offset.y),
1544 },
1545 );
1546 }
1547 }
1548 }
1549 }
1550 }
1551 Message::ModifiersChanged(modifiers) => {
1552 self.modifiers = modifiers;
1553 }
1554 Message::MounterItems(mounter_key, mounter_items) => {
1555 let mut unmounted = Vec::new();
1557 if let Some(old_items) = self.mounter_items.get(&mounter_key) {
1558 for old_item in old_items {
1559 if let Some(old_path) = old_item.path()
1560 && old_item.is_mounted()
1561 {
1562 let mut still_mounted = false;
1563 for item in &mounter_items {
1564 if let Some(path) = item.path()
1565 && path == old_path
1566 && item.is_mounted()
1567 {
1568 still_mounted = true;
1569 break;
1570 }
1571 }
1572 if !still_mounted {
1573 unmounted.push(Location::Path(old_path));
1574 }
1575 }
1576 }
1577 }
1578
1579 let mut commands = Vec::new();
1581 {
1582 let home_location = Location::Path(home_dir());
1583 if unmounted.contains(&self.tab.location) {
1584 self.tab.change_location(&home_location, None);
1585 commands.push(self.update_watcher());
1586 commands.push(self.rescan_tab(None));
1587 }
1588 }
1589
1590 self.mounter_items.insert(mounter_key, mounter_items);
1592
1593 self.update_nav_model();
1596
1597 return Task::batch(commands);
1598 }
1599 Message::ClientItems(client_key, client_items) => {
1600 let mut not_connected = Vec::new();
1602 if let Some(old_items) = self.client_items.get(&client_key) {
1603 for old_item in old_items {
1604 if let Some(old_path) = old_item.path()
1605 && old_item.is_connected() {
1606 let mut still_connected = false;
1607 for item in &client_items {
1608 if let Some(path) = item.path()
1609 && path == old_path && item.is_connected() {
1610 still_connected = true;
1611 break;
1612 }
1613 }
1614 if !still_connected {
1615 not_connected.push(Location::Path(old_path));
1616 }
1617 }
1618 }
1619 }
1620
1621 let mut commands = Vec::new();
1623 {
1624 let home_location = Location::Path(home_dir());
1625 if not_connected.contains(&self.tab.location) {
1626 self.tab.change_location(&home_location, None);
1627 commands.push(self.update_watcher());
1628 commands.push(self.rescan_tab(None));
1629 }
1630 }
1631
1632 self.client_items.insert(client_key, client_items);
1634
1635 self.update_nav_model();
1638
1639 return Task::batch(commands);
1640 }
1641 Message::Mouse(window_id, _button) => {
1642 if self.core.main_window_id() == Some(window_id) {
1644 return self.close_context_menus();
1645 }
1646 }
1647 Message::NewFolder => {
1648 if let Some(path) = self.tab.location.path_opt() {
1649 self.dialog_pages.push_back(DialogPage::NewFolder {
1650 parent: path.clone(),
1651 name: String::new(),
1652 });
1653 return widget::text_input::focus(self.dialog_text_input.clone());
1654 }
1655 }
1656 Message::NotifyEvents(events) => {
1657 log::debug!("{events:?}");
1658
1659 if let Some(path) = self.tab.location.path_opt() {
1660 let mut contains_change = false;
1661 for event in &events {
1662 for event_path in &event.paths {
1663 if event_path.starts_with(path) {
1664 if let notify::EventKind::Modify(
1665 notify::event::ModifyKind::Metadata(_)
1666 | notify::event::ModifyKind::Data(_),
1667 ) = event.kind
1668 {
1669 if let Some(items) = &mut self.tab.items_opt {
1672 for item in items.iter_mut() {
1673 if item.path_opt() == Some(event_path) {
1674 match fs::metadata(event_path) {
1676 Ok(new_metadata) => {
1677 if let ItemMetadata::Path {
1678 metadata, ..
1679 } = &mut item.metadata
1680 {
1681 *metadata = new_metadata;
1682 }
1683 }
1684 Err(err) => {
1685 log::warn!(
1686 "failed to reload metadata for {}: {}",
1687 path.display(),
1688 err
1689 );
1690 }
1691 }
1692 }
1694 }
1695 }
1696 } else {
1697 contains_change = true;
1699 break;
1700 }
1701 }
1702 }
1703 }
1704 if contains_change {
1705 return self.rescan_tab(None);
1706 }
1707 }
1708 }
1709 Message::NotifyWatcher(mut watcher_wrapper) => match watcher_wrapper.watcher_opt.take()
1710 {
1711 Some(watcher) => {
1712 self.watcher_opt = Some((watcher, FxHashSet::default()));
1713 return self.update_watcher();
1714 }
1715 None => {
1716 log::warn!("message did not contain notify watcher");
1717 }
1718 },
1719 Message::Open => {
1720 let mut paths = Vec::new();
1721 if let Some(items) = self.tab.items_opt() {
1722 for item in items {
1723 if item.selected
1724 && let Some(path) = item.path_opt()
1725 {
1726 paths.push(path.clone());
1727 if self.flags.config.show_recents {
1728 let _ = update_recently_used(
1729 path,
1730 Self::APP_ID.to_string(),
1731 "cosmic-files".to_string(),
1732 None,
1733 );
1734 }
1735 }
1736 }
1737 }
1738
1739 for path in &paths {
1742 let path_is_dir = path.is_dir();
1743 if path_is_dir != self.flags.kind.is_dir() {
1744 if path_is_dir && paths.len() == 1 {
1745 let message = Message::TabMessage(tab::Message::Location(
1747 Location::Path(path.clone()),
1748 ));
1749 return self.update(message);
1750 }
1751
1752 return Task::none();
1754 }
1755 }
1756
1757 if !paths.is_empty() {
1759 self.result_opt = Some(DialogResult::Open(paths));
1760 return window::close(self.flags.window_id);
1761 }
1762
1763 if self.flags.kind.is_dir()
1765 && let Location::Path(tab_path) = &self.tab.location
1766 {
1767 self.result_opt = Some(DialogResult::Open(vec![tab_path.clone()]));
1768 return window::close(self.flags.window_id);
1769 }
1770 }
1771 Message::Preview => {
1772 self.context_page = ContextPage::Preview(None, PreviewKind::Selected);
1773 return self.with_dialog_config(|config| {
1774 config.show_details = !config.show_details;
1775 });
1776 }
1777 Message::Save(replace) => {
1778 if let DialogKind::SaveFile { filename } = &self.flags.kind
1779 && !filename.is_empty()
1780 && let Some(tab_path) = self.tab.location.path_opt()
1781 {
1782 let path = tab_path.join(filename);
1783 if path.is_dir() {
1784 let message =
1786 Message::TabMessage(tab::Message::Location(Location::Path(path)));
1787 return self.update(message);
1788 } else if !replace && path.exists() {
1789 self.dialog_pages.push_back(DialogPage::Replace {
1790 filename: filename.clone(),
1791 });
1792 return widget::button::focus(REPLACE_BUTTON_ID.clone());
1793 }
1794 self.result_opt = Some(DialogResult::Open(vec![path]));
1795 return window::close(self.flags.window_id);
1796 }
1797 }
1798 Message::ScrollTab(scroll_speed) => {
1799 return self.update(Message::TabMessage(tab::Message::ScrollTab(
1800 f32::from(scroll_speed) / 10.0,
1801 )));
1802 }
1803 Message::SearchActivate => {
1804 let mut tasks = vec![self.close_context_menus()];
1805
1806 if self.search_get().is_none() {
1807 tasks.push(self.search_set(Some(String::new())));
1808 } else {
1809 tasks.push(widget::text_input::focus(self.search_id.clone()));
1810 }
1811
1812 return Task::batch(tasks);
1813 }
1814 Message::SearchClear => {
1815 return Task::batch([self.close_context_menus(), self.search_set(None)]);
1816 }
1817 Message::SearchInput(input) => {
1818 return self.search_set(Some(input));
1819 }
1820 Message::TabMessage(tab_message) => {
1821 let click_i_opt = match tab_message {
1822 tab::Message::Click(click_i_opt) => click_i_opt,
1823 _ => None,
1824 };
1825
1826 let tab_commands = self.tab.update(tab_message, self.modifiers);
1827
1828 if let DialogKind::SaveFile { filename } = &mut self.flags.kind
1830 && let Some(click_i) = click_i_opt
1831 && let Some(items) = self.tab.items_opt()
1832 && let Some(item) = items.get(click_i)
1833 && item.selected
1834 && !item.metadata.is_dir()
1835 {
1836 filename.clone_from(&item.name);
1837 }
1838
1839 let mut commands = Vec::new();
1840 for tab_command in tab_commands {
1841 match tab_command {
1842 tab::Command::Action(action) => {
1843 commands.push(self.update(Message::from(action.message())));
1844 }
1845 tab::Command::ChangeLocation(_tab_title, _tab_path, selection_paths) => {
1846 commands.push(Task::batch([
1847 self.update_watcher(),
1848 self.rescan_tab(selection_paths),
1849 ]));
1850 }
1851 tab::Command::ContextMenu(_point_opt, _parent_id) => {
1852 #[cfg(feature = "wayland")]
1853 match point_opt {
1854 Some(point) => {
1855 if crate::is_wayland() {
1856 use cctk::wayland_protocols::xdg::shell::client::xdg_positioner::{
1858 Anchor, Gravity,
1859 };
1860 use cosmic::iced::runtime::platform_specific::wayland::popup::{
1861 SctkPopupSettings, SctkPositioner,
1862 };
1863 use cosmic::iced::Rectangle;
1864 let window_id = window::Id::unique();
1865 self.context_menu_window = Some(window_id);
1866 let autosize_id = widget::Id::unique();
1867 commands.push(self.update(Message::Surface(
1868 cosmic::surface::action::app_popup(
1869 move |app: &mut Self| -> SctkPopupSettings {
1870 let anchor_rect = Rectangle {
1871 x: point.x as i32,
1872 y: point.y as i32,
1873 width: 1,
1874 height: 1,
1875 };
1876 let positioner = SctkPositioner {
1877 size: None,
1878 anchor_rect,
1879 anchor: Anchor::None,
1880 gravity: Gravity::BottomRight,
1881 reactive: true,
1882 ..Default::default()
1883 };
1884 SctkPopupSettings {
1885 parent: parent_id
1886 .unwrap_or(app.flags.window_id),
1887 id: window_id,
1888 positioner,
1889 parent_size: None,
1890 grab: true,
1891 close_with_children: false,
1892 input_zone: None,
1893 }
1894 },
1895 Some(Box::new(move |app: &Self| {
1896 widget::autosize::autosize(
1897 menu::context_menu(
1898 &app.tab,
1899 &app.key_binds,
1900 &app.modifiers,
1901 false, &app.config.tb_config,
1903 &app.flags.config.context_actions,
1904 )
1905 .map(Message::TabMessage)
1906 .map(cosmic::Action::App),
1907 autosize_id.clone(),
1908 )
1909 .into()
1910 })),
1911 ),
1912 )));
1913 }
1914 }
1915 None => {
1916 if let Some(window_id) = self.context_menu_window.take() {
1917 commands.push(self.update(Message::Surface(
1918 cosmic::surface::action::destroy_popup(window_id),
1919 )));
1920 }
1921 }
1922 }
1923 }
1924 tab::Command::Iced(iced_command) => {
1925 commands.push(iced_command.0.map(|tab_message| {
1926 cosmic::action::app(Message::TabMessage(tab_message))
1927 }));
1928 }
1929 tab::Command::OpenFile(_item_path) => {
1930 if self.flags.kind.save() {
1931 commands.push(self.update(Message::Save(false)));
1932 } else {
1933 commands.push(self.update(Message::Open));
1934 }
1935 }
1936 tab::Command::Preview(kind) => {
1937 self.context_page = ContextPage::Preview(None, kind);
1938 commands.push(self.with_dialog_config(|config| {
1939 config.show_details = true;
1940 }));
1941 }
1942 tab::Command::WindowDrag => {
1943 commands.push(window::drag(self.flags.window_id));
1944 }
1945 tab::Command::WindowToggleMaximize => {
1946 commands.push(window::toggle_maximize(self.flags.window_id));
1947 }
1948 tab::Command::AutoScroll(scroll_speed) => {
1949 if let Some(scroll_speed_float) = scroll_speed {
1952 self.auto_scroll_speed = Some((scroll_speed_float * 10.0) as i16);
1953 } else {
1954 self.auto_scroll_speed = None;
1955 }
1956 }
1957 unsupported => {
1958 log::warn!("{unsupported:?} not supported in dialog mode");
1959 }
1960 }
1961 }
1962 return Task::batch(commands);
1963 }
1964 Message::TabRescan(location, parent_item_opt, mut items, selection_paths) => {
1965 if location == self.tab.location {
1966 if let Some(filter_i) = self.filter_selected
1968 && let Some(filter) = self.filters.get(filter_i)
1969 {
1970 let mut parsed_globs = Vec::new();
1971 let mut mimes = Vec::new();
1972 for pattern in &filter.patterns {
1973 match pattern {
1974 DialogFilterPattern::Glob(value) => {
1975 match glob::Pattern::new(value) {
1976 Ok(glob) => parsed_globs.push(glob),
1977 Err(err) => {
1978 log::warn!("failed to parse glob {value:?}: {err}");
1979 }
1980 }
1981 }
1982 DialogFilterPattern::Mime(value) => match value.parse::<Mime>() {
1983 Ok(parsed) => mimes.push(parsed),
1984 Err(err) => {
1985 log::warn!("failed to parse mime {value:?}: {err}");
1986 }
1987 },
1988 }
1989 }
1990
1991 items.retain(|item| {
1992 item.metadata.is_dir()
1994 || mimes.iter().any(|filter_mime| {
1995 if filter_mime.subtype() == mime::STAR {
1996 filter_mime.type_() == item.mime.type_()
1997 } else {
1998 *filter_mime == item.mime
1999 }
2000 })
2001 || parsed_globs.iter().any(|glob| glob.matches(&item.name))
2003 });
2004 }
2005
2006 if let DialogKind::SaveFile { filename } = &self.flags.kind {
2008 for item in &mut items {
2009 item.selected = &item.name == filename;
2010 }
2011 }
2012
2013 self.tab.parent_item_opt = parent_item_opt;
2014 self.tab.set_items(items);
2015
2016 if let Some(mut selection_paths) = selection_paths {
2017 if !self.flags.kind.multiple() {
2018 selection_paths.truncate(1);
2019 }
2020 self.tab.select_paths(selection_paths);
2021 }
2022
2023 if self.search_get().is_some() {
2025 return widget::text_input::focus(self.search_id.clone());
2026 }
2027 if let DialogKind::SaveFile { filename } = &self.flags.kind {
2028 return Task::batch([
2029 widget::text_input::focus(self.filename_id.clone()),
2030 widget::text_input::select_until_last(
2031 self.filename_id.clone(),
2032 filename,
2033 '.',
2034 ),
2035 ]);
2036 }
2037 return widget::text_input::focus(self.filename_id.clone());
2038 }
2039 }
2040 Message::TabView(view) => {
2041 return self.with_dialog_config(|config| {
2042 config.view = view;
2043 });
2044 }
2045 Message::TimeConfigChange(time_config) => {
2046 self.flags.config.tab.military_time = time_config.military_time;
2047 return self.update_config();
2048 }
2049 Message::ToggleFoldersFirst => {
2050 return self.with_dialog_config(|config| {
2051 config.folders_first = !config.folders_first;
2052 });
2053 }
2054 Message::ToggleShowHidden => {
2055 return self.with_dialog_config(|config| {
2056 config.show_hidden = !config.show_hidden;
2057 });
2058 }
2059 Message::ZoomDefault => {
2060 return self.with_dialog_config(|config| {
2061 zoom_to_default(config.view, &mut config.icon_sizes);
2062 });
2063 }
2064 Message::ZoomIn => {
2065 return self.with_dialog_config(|config| {
2066 zoom_in_view(config.view, &mut config.icon_sizes);
2067 });
2068 }
2069 Message::ZoomOut => {
2070 return self.with_dialog_config(|config| {
2071 zoom_out_view(config.view, &mut config.icon_sizes);
2072 });
2073 }
2074 Message::Surface(action) => {
2075 return cosmic::task::message(cosmic::Action::Cosmic(
2076 cosmic::app::Action::Surface(action),
2077 ));
2078 }
2079 }
2080
2081 Task::none()
2082 }
2083
2084 fn view(&self) -> Element<'_, Message> {
2086 let cosmic_theme::Spacing { space_xxs, .. } = theme::spacing();
2087
2088 let mut col = widget::column::with_capacity(2);
2089
2090 if self.core.is_condensed()
2091 && let Some(term) = self.search_get()
2092 {
2093 col = col.push(
2094 widget::container(
2095 widget::text_input::search_input("", term)
2096 .width(Length::Fill)
2097 .id(self.search_id.clone())
2098 .on_clear(Message::SearchClear)
2099 .on_input(Message::SearchInput),
2100 )
2101 .padding(space_xxs),
2102 );
2103 }
2104
2105 col = col.push(
2106 self.tab
2107 .view(&self.key_binds, &self.modifiers, false, &[])
2108 .map(Message::TabMessage),
2109 );
2110
2111 col.into()
2112 }
2113
2114 fn subscription(&self) -> Subscription<Message> {
2115 struct WatcherSubscription;
2116 struct TimeSubscription;
2117 let mut subscriptions = vec![
2118 event::listen_with(|event, status, window_id| match event {
2119 Event::Mouse(mouse::Event::ButtonPressed(button)) => match status {
2120 event::Status::Ignored => Some(Message::Mouse(window_id, button)),
2121 event::Status::Captured => None,
2122 },
2123 Event::Keyboard(KeyEvent::KeyPressed {
2124 key,
2125 physical_key,
2126 modifiers,
2127 text,
2128 ..
2129 }) => match status {
2130 event::Status::Ignored => {
2131 Some(Message::Key(modifiers, key, physical_key, text))
2132 }
2133 event::Status::Captured => {
2134 if key == Key::Named(Named::Escape) {
2135 Some(Message::Escape)
2136 } else {
2137 None
2138 }
2139 }
2140 },
2141 Event::Keyboard(KeyEvent::ModifiersChanged(modifiers)) => {
2142 Some(Message::ModifiersChanged(modifiers))
2143 }
2144 _ => None,
2145 }),
2146 Config::subscription().map(|update| {
2147 if !update.errors.is_empty() {
2148 log::info!(
2149 "errors loading config {:?}: {:?}",
2150 update.keys,
2151 update.errors
2152 );
2153 }
2154 Message::Config(update.config)
2155 }),
2156 cosmic_config::config_subscription::<_, TimeConfig>(
2157 TypeId::of::<TimeSubscription>(),
2158 TIME_CONFIG_ID.into(),
2159 1,
2160 )
2161 .map(|update| {
2162 if !update.errors.is_empty() {
2163 log::info!(
2164 "errors loading time config {:?}: {:?}",
2165 update.keys,
2166 update.errors
2167 );
2168 }
2169 Message::TimeConfigChange(update.config)
2170 }),
2171 Subscription::run_with(TypeId::of::<WatcherSubscription>(), |_| {
2172 stream::channel(100, {
2173 |mut output: futures::channel::mpsc::Sender<_>| async move {
2174 let watcher_res = {
2175 let mut output = output.clone();
2176 new_debouncer(
2177 time::Duration::from_millis(250),
2178 Some(time::Duration::from_millis(250)),
2179 move |events_res: notify_debouncer_full::DebounceEventResult| {
2180 match events_res {
2181 Ok(mut events) => {
2182 events.retain(|event| {
2183 match &event.kind {
2184 notify::EventKind::Access(_) => {
2185 false
2187 }
2188 notify::EventKind::Modify(
2189 notify::event::ModifyKind::Metadata(e),
2190 ) if (*e != notify::event::MetadataKind::Any
2191 && *e
2192 != notify::event::MetadataKind::WriteTime) =>
2193 {
2194 false
2196 }
2197 _ => true
2198 }
2199 });
2200
2201 if !events.is_empty() {
2202 match futures::executor::block_on(async {
2203 output.send(Message::NotifyEvents(events)).await
2204 }) {
2205 Ok(()) => {}
2206 Err(err) => {
2207 log::warn!(
2208 "failed to send notify events: {err:?}"
2209 );
2210 }
2211 }
2212 }
2213 }
2214 Err(err) => {
2215 log::warn!("failed to watch files: {err:?}");
2216 }
2217 }
2218 },
2219 )
2220 };
2221
2222 match watcher_res {
2223 Ok(watcher) => {
2224 match output
2225 .send(Message::NotifyWatcher(WatcherWrapper {
2226 watcher_opt: Some(watcher),
2227 }))
2228 .await
2229 {
2230 Ok(()) => {}
2231 Err(err) => {
2232 log::warn!("failed to send notify watcher: {err:?}");
2233 }
2234 }
2235 }
2236 Err(err) => {
2237 log::warn!("failed to create file watcher: {err:?}");
2238 }
2239 }
2240
2241 std::future::pending().await
2242 }
2243 })
2244 }),
2245 self.tab
2246 .subscription(
2247 self.core.window.show_context
2248 && matches!(
2249 self.context_page,
2250 ContextPage::Preview(_, PreviewKind::Selected)
2251 ),
2252 )
2253 .map(Message::TabMessage),
2254 ];
2255
2256 if let Some(scroll_speed) = self.auto_scroll_speed {
2257 subscriptions.push(
2258 iced::time::every(time::Duration::from_millis(10))
2259 .with(scroll_speed)
2260 .map(|(scroll_speed, _)| Message::ScrollTab(scroll_speed)),
2261 );
2262 }
2263
2264 subscriptions.extend(MOUNTERS.iter().map(|(key, mounter)| {
2265 mounter
2266 .subscription()
2267 .with(*key)
2268 .map(|(key, mounter_message)| {
2269 if let MounterMessage::Items(items) = mounter_message {
2270 Message::MounterItems(key, items)
2271 } else {
2272 log::warn!("{mounter_message:?} not supported in dialog mode");
2273 Message::None
2274 }
2275 })
2276 }));
2277 subscriptions.extend(CLIENTS.iter().map(|(key, client)| {
2278 client
2279 .subscription()
2280 .with(*key)
2281 .map(|(key, client_message)| {
2282 if let ClientMessage::Items(items) = client_message {
2283 Message::ClientItems(key, items)
2284 } else {
2285 log::warn!("{client_message:?} not supported in dialog mode");
2286 Message::None
2287 }
2288 })
2289 }));
2290
2291 Subscription::batch(subscriptions)
2292 }
2293}