cosmic_files/
dialog.rs

1// Copyright 2023 System76 <info@system76.com>
2// SPDX-License-Identifier: GPL-3.0-only
3
4use 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        //TODO: only do this once somehow?
255        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/// Messages that are used specifically by our [`App`].
449#[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
543/// The [`App`] stores application-specific state.
544struct 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            //TODO: easier way to create buttons with rich text
657            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            /*TODO: a11y feature: .label(&self.accept_label.text)*/
679        );
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                            // Only show one property view to avoid issues like hangs when generating
702                            // preview images on thousands of files
703                            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                            // At least two selected items
715                            (Some(_), Some(_)) => Some(self.tab.multi_preview_view(None)),
716                            // Exactly one selected item
717                            (Some(item), None) => Some(item.preview_view(None, military_time)),
718                            // No selected items
719                            _ => 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        // Collect all mounter items
940        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        // Sort by name lexically
945        nav_items.sort_unstable_by(|a, b| LANGUAGE_SORTER.compare(&a.1.name(), &b.1.name()));
946        // Add items to nav model
947        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            // Unwatch paths no longer used
984            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            // Watch new paths
998            for path in &new_paths {
999                if !old_paths.contains(path) {
1000                    //TODO: should this be recursive?
1001                    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        //TODO: should any of this run in a command?
1016        Task::none()
1017    }
1018}
1019
1020/// Implement [`Application`] to integrate with COSMIC.
1021impl Application for App {
1022    /// Default async executor to use with the app.
1023    type Executor = executor::Default;
1024
1025    /// Argument received
1026    type Flags = Flags;
1027
1028    /// Message type specific to our [`App`].
1029    type Message = Message;
1030
1031    /// The unique application ID to supply to the window manager.
1032    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    /// Creates the application, and optionally emits command on initialize.
1043    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        //TODO: should gallery view just be a dialog?
1145        if self.tab.gallery {
1146            return Some(
1147                widget::column::with_children([
1148                    self.tab.gallery_view().map(Message::TabMessage),
1149                    // Draw button row as part of the overlay
1150                    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                    //TODO: selected state is not appearing different
1249                    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        //TODO .on_close(|entity| cosmic::cosmic::action::app(Message::NavBarClose(entity)))
1304        .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            // Close gallery if open
1355            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            // Close location editing if enabled
1370            self.tab.edit_location = None;
1371            return Task::none();
1372        }
1373
1374        if self.search_get().is_some() {
1375            // Close search if open
1376            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                // Unfocus if there was a focused button
1383                return widget::button::focus(widget::Id::unique());
1384            }
1385            return Task::none();
1386        }
1387
1388        // Close the dialog if the focused widget is the dialog's main text input instead of
1389        // unfocussing the widget.
1390        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    /// Handle application events here.
1400    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                    // Don't overwrite military time
1427                    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                                    // cd to directory
1444                                    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                // Select based on filename
1468                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                // Check key binds from accept label
1490                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                // Uncaptured keys with only shift modifiers go to the search or location box
1501                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                            // Try to add text to end of location
1518                            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                            // Reset buffer if timeout elapsed
1527                            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                            // Accumulate character and select
1534                            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                // Check for unmounted folders
1556                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                // Go back to home in any tabs that were unmounted
1580                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                // Insert new items
1591                self.mounter_items.insert(mounter_key, mounter_items);
1592
1593                // Update nav bar
1594                //TODO: this could change favorites IDs while they are in use
1595                self.update_nav_model();
1596
1597                return Task::batch(commands);
1598            }
1599            Message::ClientItems(client_key, client_items) => {
1600                // Check for unmounted folders
1601                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                // Go back to home in any tabs that were not_connected
1622                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                // Insert new items
1633                self.client_items.insert(client_key, client_items);
1634
1635                // Update nav bar
1636                //TODO: this could change favorites IDs while they are in use
1637                self.update_nav_model();
1638
1639                return Task::batch(commands);
1640            }
1641            Message::Mouse(window_id, _button) => {
1642                // Close context menu when clicking outside.
1643                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 metadata or data changed, find the matching item and reload it
1670                                    //TODO: this could be further optimized by looking at what exactly changed
1671                                    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                                                //TODO: reload more, like mime types?
1675                                                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                                                //TODO item.thumbnail_opt =
1693                                            }
1694                                        }
1695                                    }
1696                                } else {
1697                                    // Any other events reload the whole tab
1698                                    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                // Ensure selection is allowed
1740                //TODO: improve tab logic so this doesn't block the open button so often
1741                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                            // If the only selected item is a directory and we are selecting files, cd to it
1746                            let message = Message::TabMessage(tab::Message::Location(
1747                                Location::Path(path.clone()),
1748                            ));
1749                            return self.update(message);
1750                        }
1751
1752                        // Otherwise, this is not a legal selection
1753                        return Task::none();
1754                    }
1755                }
1756
1757                // If there are proper matching items, return them
1758                if !paths.is_empty() {
1759                    self.result_opt = Some(DialogResult::Open(paths));
1760                    return window::close(self.flags.window_id);
1761                }
1762
1763                // If we are in directory mode, return the current directory
1764                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                        // cd to directory
1785                        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                // Update filename box when anything is selected
1829                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                                        // Open context menu
1857                                        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, // Paste not used in dialogs
1902                                                            &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                            // converting an f32 to an i16 here by multiplying by 10 and casting to i16
1950                            // further resolution isn't necessary
1951                            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                    // Filter
1967                    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                            // Directories are always shown
1993                            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                                // Check for glob match (last because it is slower)
2002                                    || parsed_globs.iter().any(|glob| glob.matches(&item.name))
2003                        });
2004                    }
2005
2006                    // Select based on filename
2007                    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                    // Reset focus on location change
2024                    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    /// Creates a view after each update.
2085    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                                                    // Data not mutated
2186                                                    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                                                    // Data not mutated nor modify time changed
2195                                                    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}