Skip to main content

qframe/widgets/file_manager/
state.rs

1//! What a [`FileManager`](super::FileManager) knows about the folder it shows, and what happens to
2//! it.
3//!
4//! Reading a folder is I/O, so it never happens while drawing: the state asks for a folder when it
5//! is opened and keeps the answer, and the view is built from what is already known.
6
7use std::collections::{BTreeMap, BTreeSet};
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10
11use crate::runtime::{Command, Confirm, Task, TaskEvent, TaskId, TaskOutcome};
12use crate::widgets::{Toast, TreeDrop};
13
14use super::details::{FileDetails, PAGE};
15use super::ops::{self, FileChange, FileError, NameProblem, is_within, name_of, parent_key};
16use super::watch::Live;
17
18/// The key of the folder the manager is rooted at. Keys below it are paths relative to the root,
19/// written with `/` whatever the platform, because they are identities rather than paths.
20pub const ROOT: &str = "";
21
22/// The key of the entry `name` inside the folder `parent`.
23#[must_use]
24pub fn child_key(parent: &str, name: &str) -> String {
25    if parent.is_empty() { name.to_owned() } else { format!("{parent}/{name}") }
26}
27
28/// One entry of a folder, as a file manager reads it: a name, whether it can be opened and whether
29/// it is a program.
30///
31/// A tree row shows nothing else, so nothing else is read. Size, date and permissions each mean
32/// another call to the system for every entry, which a folder of ten thousand entries cannot
33/// afford, so they belong to the views that show them.
34#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
35pub struct FolderEntry {
36    /// Its name, without any folder before it.
37    pub name: String,
38    /// Whether it is a folder, and so can be opened. A symbolic link is never a folder: opening
39    /// one would leave the tree it is drawn in.
40    pub folder: bool,
41    /// Whether it is a file that may be run, which gives it a program's icon.
42    ///
43    /// Only asked of a file whose name says nothing of its kind, since the name wins where it
44    /// does (see [`file_kind`](crate::icons::file_kind)): a folder of ten thousand photos asks the
45    /// system nothing more, and a folder of programs asks once for each of them. Always `false`
46    /// where the system has no such permission.
47    pub executable: bool,
48}
49
50impl FolderEntry {
51    /// Reads one folder the way [`read_folder`](Self::read_folder) does, keeping why it could not
52    /// be read rather than what the system said about it, so the words are chosen where the
53    /// person's language is known.
54    pub(super) fn list(path: &Path) -> Result<Vec<Self>, FileError> {
55        let mut entries = Vec::new();
56        for entry in std::fs::read_dir(path).map_err(ops::read_error)? {
57            let entry = entry.map_err(ops::read_error)?;
58            let name = entry.file_name().to_string_lossy().into_owned();
59            let folder = entry.file_type().is_ok_and(|kind| kind.is_dir());
60            let executable = !folder && runs(&entry, &name);
61            entries.push(Self { name, folder, executable });
62        }
63        entries.sort_by(|a, b| b.folder.cmp(&a.folder).then_with(|| a.name.cmp(&b.name)));
64        Ok(entries)
65    }
66
67    /// Whether the entry is one the platform hides: on every system the framework runs on, a name
68    /// that starts with a dot.
69    #[must_use]
70    pub fn is_hidden(&self) -> bool {
71        self.name.starts_with('.')
72    }
73
74    /// Reads one folder, folders first and then files, each group in name order.
75    ///
76    /// This touches the disk, so it belongs on a background thread; [`FileManagerState`] asks for
77    /// it with [`Command::perform`] and never while drawing.
78    ///
79    /// # Errors
80    ///
81    /// What the operating system said, when the folder cannot be read. The manager's own reads say
82    /// it in the person's language instead; see [`FileManagerMsg::Listed`].
83    pub fn read_folder(path: &Path) -> Result<Vec<Self>, String> {
84        let mut entries = Vec::new();
85        for entry in std::fs::read_dir(path).map_err(|error| error.to_string())? {
86            let entry = entry.map_err(|error| error.to_string())?;
87            // A name the platform does not spell as text is still an entry; showing it lossily is
88            // better than pretending the folder holds less than it does.
89            let name = entry.file_name().to_string_lossy().into_owned();
90            // `file_type` does not follow a link, so a link to a folder is an entry and not a way
91            // out of the tree.
92            let folder = entry.file_type().is_ok_and(|kind| kind.is_dir());
93            let executable = !folder && runs(&entry, &name);
94            entries.push(Self { name, folder, executable });
95        }
96        entries.sort_by(|a, b| b.folder.cmp(&a.folder).then_with(|| a.name.cmp(&b.name)));
97        Ok(entries)
98    }
99}
100
101/// Whether the file `entry`, called `name`, is a program: one whose name says nothing of its kind
102/// and that may be run. A link answers for what it points to, since its own permissions are
103/// always all of them.
104fn runs(entry: &std::fs::DirEntry, name: &str) -> bool {
105    if crate::icons::file_kind(name, false, false).family() != crate::icons::KindFamily::File {
106        return false;
107    }
108    may_run(&entry.path())
109}
110
111/// Whether the file at `path` may be run by someone.
112#[cfg(unix)]
113fn may_run(path: &Path) -> bool {
114    use std::os::unix::fs::PermissionsExt;
115    std::fs::metadata(path).is_ok_and(|data| data.is_file() && data.permissions().mode() & 0o111 != 0)
116}
117
118/// Whether the file at `path` may be run: never known here, so never said.
119#[cfg(not(unix))]
120fn may_run(_path: &Path) -> bool {
121    false
122}
123
124/// What the name the dialog asks for is for.
125#[derive(Debug, Clone, PartialEq, Eq)]
126pub enum NameFor {
127    /// A new, empty file.
128    File,
129    /// A new, empty folder.
130    Folder,
131    /// The entry of this key, which keeps its place and takes the new name.
132    Rename(String),
133}
134
135/// The dialog that asks for a name, while it is open.
136#[derive(Debug, Clone, PartialEq, Eq)]
137pub struct Naming {
138    /// What the name is for.
139    pub purpose: NameFor,
140    /// The key of the folder the name is to be used in.
141    pub folder: String,
142    /// What has been typed.
143    pub value: String,
144    /// Whether the person tried to confirm; an empty name is only pointed out after that, so a
145    /// dialog that has just opened does not start by scolding.
146    pub tried: bool,
147}
148
149/// Something that happened in a [`FileManager`](super::FileManager).
150///
151/// Hand every one of these to [`FileManagerState::update`]; the application's own messages come
152/// from [`FileManager::on_open`](super::FileManager::on_open) and the items it adds to the menu,
153/// never from here.
154///
155/// More things may happen as the manager grows, so match with a `_` arm; an application normally
156/// hands every one of these straight over without looking.
157#[derive(Debug, Clone)]
158#[non_exhaustive]
159pub enum FileManagerMsg {
160    /// The cursor moved to this key.
161    Select(String),
162    /// The entries of these keys became the selection.
163    Choose(Vec<String>),
164    /// The folder of this key was opened, or closed when `false`.
165    Expand(String, bool),
166    /// The folder of this key was read in the background, or could not be, by the application's
167    /// own reading. The text is the system's own; hand it over as it comes.
168    Read(String, Result<Vec<FolderEntry>, String>),
169    /// The folder of this key was read by the manager itself.
170    ///
171    /// Why a read failed is kept as a [`FileError`] rather than as words, because the thread that
172    /// read the folder does not know the person's language; the words are chosen here.
173    Listed(String, Result<Vec<FolderEntry>, FileError>),
174    /// A new file was asked for in the folder of this key.
175    NewFile(String),
176    /// A new folder was asked for in the folder of this key.
177    NewFolder(String),
178    /// The entry of this key was asked to take another name.
179    Rename(String),
180    /// The entry of this key was cut, with the rest of the selection when it is part of it, to be
181    /// pasted into another folder.
182    Cut(String),
183    /// The entry of this key was copied, with the rest of the selection when it is part of it, to
184    /// be pasted into another folder. What was copied stays where it is.
185    Copy(String),
186    /// What was cut was asked to go into the folder of this key.
187    Paste(String),
188    /// What was cut is to stay where it is after all.
189    DropCut,
190    /// Entries were dragged onto a folder, or onto the free space that stands for the root.
191    Drop(TreeDrop),
192    /// Entries were dragged onto a folder with Ctrl held at the release, to be copied there. What
193    /// was copied stays where it is.
194    DropCopy(TreeDrop),
195    /// The entry of this key was asked to be deleted, with the rest of the selection when it is
196    /// part of it; the person is asked first.
197    Delete(String),
198    /// The person said yes to deleting the entries of these keys.
199    DeleteConfirmed(Vec<String>),
200    /// The entry of this key was asked to go to the trash, with the rest of the selection when it
201    /// is part of it. Nothing is asked: the trash can be looked in again.
202    Trash(String),
203    /// Hidden entries are shown from now on, or hidden again when `false`.
204    ShowHidden(bool),
205    /// The long operation running now started, came further along or ended.
206    Work(TaskEvent),
207    /// The long operation running now was asked to stop.
208    Stop,
209    /// Every open folder was asked to be read again.
210    Refresh,
211    /// The name in the dialog changed.
212    Name(String),
213    /// The name in the dialog was confirmed.
214    Submit,
215    /// The dialog was closed without a name.
216    CloseNaming,
217    /// Operations finished or were refused: for each entry its key and what came of it.
218    Done(Vec<(String, Result<FileChange, FileError>)>),
219    /// A batch of outside changes of the watch `u64` arrived; empty once that watch was let go.
220    Changed(u64, Vec<crate::storage::FolderChange>),
221    /// A [bounded](FileManagerState::following_within) wait of the watch `u64` ended with nothing
222    /// changed; the watch waits again.
223    Quiet(u64),
224    /// The details of the entries of these keys were asked for: their size, when they changed and
225    /// their permissions. Keys that are already known, or already on their way, cost nothing.
226    Detail(Vec<String>),
227    /// The details of these keys were read, `None` for an entry the system said nothing about.
228    Detailed(Vec<(String, Option<FileDetails>)>),
229    /// A flat view stepped into the folder of this key; it is read if it has not been.
230    Enter(String),
231    /// A flat view stepped out of the folder it shows, into the one above it.
232    Leave,
233}
234
235/// A long operation the manager is running in the background right now.
236///
237/// Copying is the one operation that can take a while: a rename is instant whatever it moves, but
238/// a folder of photographs is read and written byte by byte. So a copy runs as a
239/// [`Task`](crate::runtime::Task), tells how far it has come and can be stopped; everything else
240/// still runs straight through.
241#[derive(Debug, Clone)]
242pub struct FileWork {
243    id: TaskId,
244    entries: usize,
245    done: f32,
246    note: String,
247}
248
249impl FileWork {
250    /// The task doing the work, so an application can show it in a
251    /// [`Tasks`](crate::runtime::Tasks) model of its own beside its other background work, or stop
252    /// it without going through the manager's own button.
253    #[must_use]
254    pub fn id(&self) -> TaskId {
255        self.id
256    }
257
258    /// The share done, from 0 to 1.
259    #[must_use]
260    pub fn done(&self) -> f32 {
261        self.done
262    }
263
264    /// The name of the entry being copied right now, empty before the first one starts.
265    #[must_use]
266    pub fn note(&self) -> &str {
267        &self.note
268    }
269
270    /// How many entries the operation was given.
271    #[must_use]
272    pub fn entries(&self) -> usize {
273        self.entries
274    }
275}
276
277/// Where a deleted entry goes.
278#[derive(Debug, Default)]
279enum Trash {
280    /// Nowhere: deleting takes an entry away for good, which is all a manager did before.
281    #[default]
282    Off,
283    /// The person's own trash, where the freedesktop specification puts it.
284    Home,
285    /// This folder, for an application with a trash of its own.
286    In(PathBuf),
287}
288
289impl Trash {
290    /// The folder entries go to, when there is one.
291    fn folder(&self) -> Option<PathBuf> {
292        match self {
293            Self::Off => None,
294            Self::Home => super::trash::home_trash(),
295            Self::In(folder) => Some(folder.clone()),
296        }
297    }
298}
299
300/// Turns a manager's messages into the application's own.
301pub(super) type Wrap<Msg> = Arc<dyn Fn(FileManagerMsg) -> Msg + Send + Sync>;
302
303/// What a file manager has read of its root folder, what is open in it, and what is selected.
304///
305/// The application owns one of these per manager on screen, hands it every [`FileManagerMsg`] and
306/// draws it with [`FileManager`](super::FileManager). It reads folders in the background, keeps no
307/// settings and writes nothing of its own to disk.
308///
309/// ```
310/// use qframe::prelude::*;
311/// use qframe::widgets::{FileManager, FileManagerMsg, FileManagerState};
312///
313/// struct Files {
314///     manager: FileManagerState,
315///     opened: Option<std::path::PathBuf>,
316/// }
317///
318/// #[derive(Clone)]
319/// enum Msg {
320///     Manager(FileManagerMsg),
321///     Open(std::path::PathBuf),
322/// }
323///
324/// impl App for Files {
325///     type Msg = Msg;
326///     fn init(&mut self) -> Command<Msg> {
327///         self.manager.load(Msg::Manager)
328///     }
329///     fn update(&mut self, msg: Msg) -> Command<Msg> {
330///         match msg {
331///             Msg::Manager(message) => self.manager.update(message, Msg::Manager),
332///             Msg::Open(path) => {
333///                 self.opened = Some(path);
334///                 Command::none()
335///             }
336///         }
337///     }
338///     fn view(&self, ui: &mut View<'_, Msg>) {
339///         FileManager::new(&self.manager, Msg::Manager)
340///             .on_open(|path| Msg::Open(path.to_path_buf()))
341///             .show(ui)
342///             .fill();
343///     }
344/// }
345/// ```
346#[derive(Debug)]
347pub struct FileManagerState {
348    root: PathBuf,
349    confined: bool,
350    following: bool,
351    /// The folder a flat view shows; the root until one is stepped into.
352    shown: String,
353    children: BTreeMap<String, Vec<FolderEntry>>,
354    /// What is known about an entry besides its name, for the entries it was asked for. Never a
355    /// whole folder: a folder of ten thousand entries must not become ten thousand calls.
356    details: BTreeMap<String, Option<FileDetails>>,
357    /// The keys whose details were asked for and have not come back yet, so one ask is not made
358    /// twice.
359    reading: BTreeSet<String>,
360    open: BTreeSet<String>,
361    loading: BTreeSet<String>,
362    selected: Option<String>,
363    chosen: Vec<String>,
364    /// Why a folder could not be read, by key, for every folder whose last read failed. The root
365    /// is in here like any other: one place holds the reason, so a folder that failed can never
366    /// be drawn as one that is simply empty.
367    errors: BTreeMap<String, String>,
368    cut: Vec<String>,
369    /// Whether what waits to be pasted is to be copied rather than moved.
370    copying: bool,
371    trash: Trash,
372    hidden: bool,
373    work: Option<FileWork>,
374    naming: Option<Naming>,
375    pub(super) live: Live,
376    /// Counts the watches started, so a batch of one that was let go is recognised.
377    pub(super) runs: u64,
378    /// Whether a view that shows details asked for them, so a folder read again on the
379    /// application's word asks for its page again without waiting for a key.
380    details_wanted: bool,
381    /// How long one wait for outside changes may last; `None` waits until something changes.
382    pub(super) patience: Option<std::time::Duration>,
383}
384
385impl FileManagerState {
386    /// The key of the root folder: the manager's top row, and the folder every other key is
387    /// written relative to.
388    pub const ROOT: &'static str = ROOT;
389
390    /// A manager of the folder at `root`, with nothing read yet.
391    ///
392    /// The root is the manager's top row and starts open: its entries are what the manager is for.
393    #[must_use]
394    pub fn new(root: impl Into<PathBuf>) -> Self {
395        Self {
396            root: root.into(),
397            confined: false,
398            following: false,
399            shown: ROOT.to_owned(),
400            children: BTreeMap::new(),
401            details: BTreeMap::new(),
402            reading: BTreeSet::new(),
403            open: BTreeSet::from([ROOT.to_owned()]),
404            loading: BTreeSet::new(),
405            selected: None,
406            chosen: Vec::new(),
407            errors: BTreeMap::new(),
408            cut: Vec::new(),
409            copying: false,
410            trash: Trash::Off,
411            hidden: false,
412            work: None,
413            naming: None,
414            live: Live::Off,
415            runs: 0,
416            details_wanted: false,
417            patience: None,
418        }
419    }
420
421    /// Keeps every operation inside the root: a key that climbs out of it is refused, and so is
422    /// one that goes through a symbolic link, because a link can point anywhere.
423    ///
424    /// An application that shows a folder the person must not leave (a project, a sandbox) asks
425    /// for this; one that shows the whole file system does not. It is a rule about what may be
426    /// changed on disk, so it belongs to the state the operations run from and not to the view,
427    /// which is built afresh every frame.
428    #[must_use]
429    pub fn confined(mut self) -> Self {
430        self.confined = true;
431        self
432    }
433
434    /// Whether operations are kept inside the root.
435    #[must_use]
436    pub fn is_confined(&self) -> bool {
437        self.confined
438    }
439
440    /// Deleting puts an entry in the person's own trash instead of taking it away for good, the
441    /// way the freedesktop trash specification says: `$XDG_DATA_HOME/Trash`, or
442    /// `~/.local/share/Trash` when that variable says nothing.
443    ///
444    /// An entry that cannot be renamed into that folder — one on another file system, or a run with
445    /// no home folder at all — is not deleted quietly instead: the manager says there is no trash
446    /// for it and asks whether to delete it for good, in the danger colour, as its own question.
447    ///
448    /// Windows and macOS have a trash of their own that this specification does not describe, so
449    /// there this asks the same question rather than inventing a folder.
450    #[must_use]
451    pub fn trashing(mut self) -> Self {
452        self.trash = Trash::Home;
453        self
454    }
455
456    /// Deleting puts an entry in the trash folder `folder` instead of the person's own, for an
457    /// application that keeps a trash of its own and for a test, which must never touch the
458    /// person's.
459    ///
460    /// The folder is made when the first entry goes into it, with the `files` and `info` folders
461    /// the specification asks for.
462    #[must_use]
463    pub fn trashing_in(mut self, folder: impl Into<PathBuf>) -> Self {
464        self.trash = Trash::In(folder.into());
465        self
466    }
467
468    /// Whether deleting puts entries in a trash.
469    #[must_use]
470    pub fn is_trashing(&self) -> bool {
471        !matches!(self.trash, Trash::Off)
472    }
473
474    /// Shows the entries the platform hides: the ones whose name starts with a dot.
475    ///
476    /// Off by default, the way a folder is usually looked at. The entries are read either way —
477    /// one read of a folder is one read — so turning this on shows them without going to the disk,
478    /// and a new entry's name is checked against a hidden one that is already there whether they
479    /// are shown or not.
480    #[must_use]
481    pub fn showing_hidden(mut self, showing: bool) -> Self {
482        self.hidden = showing;
483        self
484    }
485
486    /// Shows or hides the hidden entries; see [`showing_hidden`](Self::showing_hidden).
487    pub fn set_showing_hidden(&mut self, showing: bool) {
488        self.hidden = showing;
489    }
490
491    /// Whether the entries the platform hides are shown.
492    #[must_use]
493    pub fn shows_hidden(&self) -> bool {
494        self.hidden
495    }
496
497    /// Follows the folders on screen with a [`FolderWatch`](crate::storage::FolderWatch), so what
498    /// another program changes in them is read again without being asked.
499    ///
500    /// Off by default: the watch waits on a background thread, which a screen that is not shown
501    /// has no reason to keep, and a test drives the batches itself. Turn it on for a manager the
502    /// person is looking at and off again when it leaves the screen.
503    #[must_use]
504    pub fn following(mut self, following: bool) -> Self {
505        self.following = following;
506        self
507    }
508
509    /// Follows outside changes like [`following(true)`](Self::following), with each wait for them
510    /// lasting at most `bound`; a wait that ends with nothing changed simply waits again.
511    ///
512    /// Made for screen tests: a [`Harness`](crate::runtime::Harness) runs the wait on the spot, and
513    /// an unbounded one would never come back, so a test that wants to see another program's
514    /// change arrive gives a short bound and steps the harness. A running application keeps
515    /// `following(true)`.
516    #[must_use]
517    pub fn following_within(mut self, bound: std::time::Duration) -> Self {
518        self.following = true;
519        self.patience = Some(bound);
520        self
521    }
522
523    /// Turns following outside changes on or off; see [`following`](Self::following).
524    pub fn set_following(&mut self, following: bool) {
525        self.following = following;
526        if !following {
527            self.live = Live::Off;
528        }
529    }
530
531    /// Whether outside changes are followed.
532    #[must_use]
533    pub fn follows_changes(&self) -> bool {
534        self.following
535    }
536
537    /// The folder the manager shows.
538    #[must_use]
539    pub fn root(&self) -> &Path {
540        &self.root
541    }
542
543    /// The entries of the folder `key`, when they have been read.
544    #[must_use]
545    pub fn children(&self, key: &str) -> Option<&[FolderEntry]> {
546        self.children.get(key).map(Vec::as_slice)
547    }
548
549    /// The folder a flat view shows, the root until one is stepped into.
550    ///
551    /// The tree shows the whole root and takes no notice of this; the list and the icons show this
552    /// one folder, and [`FileManagerMsg::Enter`] and [`FileManagerMsg::Leave`] move through it.
553    #[must_use]
554    pub fn folder(&self) -> &str {
555        &self.shown
556    }
557
558    /// What is known about the entry `key` besides its name: its size, when it changed last and
559    /// its permissions.
560    ///
561    /// `None` while nothing has been asked for that entry, and `Some(None)` for one the system
562    /// said nothing about — it went away, or may not be looked at. Ask for details with
563    /// [`detail`](Self::detail) or [`FileManagerMsg::Detail`]; a tree row never asks.
564    #[must_use]
565    pub fn details(&self, key: &str) -> Option<Option<&FileDetails>> {
566        self.details.get(key).map(Option::as_ref)
567    }
568
569    /// Whether the details of `key` have been asked for, whether or not the answer has come.
570    #[must_use]
571    pub fn has_details(&self, key: &str) -> bool {
572        self.details.contains_key(key) || self.reading.contains(key)
573    }
574
575    /// Whether the folder `key` is open.
576    #[must_use]
577    pub fn is_open(&self, key: &str) -> bool {
578        self.open.contains(key)
579    }
580
581    /// Whether the folder `key` is being read right now.
582    #[must_use]
583    pub fn is_loading(&self, key: &str) -> bool {
584        self.loading.contains(key)
585    }
586
587    /// The key of the entry the cursor is on: the row with the pillar, which the keys move from.
588    #[must_use]
589    pub fn selected(&self) -> Option<&str> {
590        self.selected.as_deref()
591    }
592
593    /// The keys of every selected entry, the cursor's among them unless it was taken out.
594    #[must_use]
595    pub fn chosen(&self) -> &[String] {
596        &self.chosen
597    }
598
599    /// Why the root folder could not be read, when it could not be.
600    #[must_use]
601    pub fn error(&self) -> Option<&str> {
602        self.folder_error(ROOT)
603    }
604
605    /// Why the folder `key` could not be read, when its last read failed.
606    ///
607    /// A folder the system refused keeps its row and says so; before this the manager kept only
608    /// the root's reason and drew every other refused folder as an empty one, which told the
609    /// person a folder they may not look into holds nothing. `key` may be
610    /// [`ROOT`](Self::ROOT), which is what [`error`](Self::error) asks for.
611    #[must_use]
612    pub fn folder_error(&self, key: &str) -> Option<&str> {
613        self.errors.get(key).map(String::as_str)
614    }
615
616    /// The keys of the entries that were cut and wait to be pasted; empty while what waits is to
617    /// be copied instead.
618    #[must_use]
619    pub fn cut(&self) -> &[String] {
620        if self.copying { &[] } else { &self.cut }
621    }
622
623    /// The keys of the entries that were copied and wait to be pasted.
624    #[must_use]
625    pub fn copied(&self) -> &[String] {
626        if self.copying { &self.cut } else { &[] }
627    }
628
629    /// The keys of the entries that wait to be pasted, whether pasting will move or copy them.
630    /// Only one of the two waits at a time: copying something lets go of what was cut.
631    #[must_use]
632    pub fn pending(&self) -> &[String] {
633        &self.cut
634    }
635
636    /// Whether pasting what waits will copy it rather than move it.
637    #[must_use]
638    pub fn is_copying(&self) -> bool {
639        self.copying
640    }
641
642    /// Whether the entry `key` was cut, or is inside a folder that was. A copied entry is not: it
643    /// stays where it is, so nothing about it is faint.
644    #[must_use]
645    pub fn is_cut(&self, key: &str) -> bool {
646        !self.copying && self.cut.iter().any(|cut| is_within(key, cut))
647    }
648
649    /// The entries of the folder `key` that are drawn: all of them, or the ones the platform does
650    /// not hide while [`shows_hidden`](Self::shows_hidden) is off.
651    #[must_use]
652    pub fn shown_children(&self, key: &str) -> Option<Vec<&FolderEntry>> {
653        let entries = self.children(key)?;
654        Some(entries.iter().filter(|entry| self.hidden || !entry.is_hidden()).collect())
655    }
656
657    /// The long operation running in the background, while one is running. See [`FileWork`].
658    #[must_use]
659    pub fn work(&self) -> Option<&FileWork> {
660        self.work.as_ref()
661    }
662
663    /// The dialog asking for a name, while it is open.
664    #[must_use]
665    pub fn naming(&self) -> Option<&Naming> {
666        self.naming.as_ref()
667    }
668
669    /// Whether the entry `key` is a folder, as far as the manager has read.
670    #[must_use]
671    pub fn is_folder(&self, key: &str) -> bool {
672        let name = name_of(key);
673        self.children(parent_key(key)).is_some_and(|entries| entries.iter().any(|e| e.folder && e.name == name))
674    }
675
676    /// The keys of every folder the manager has read so far, the root excepted.
677    #[must_use]
678    pub fn folder_keys(&self) -> BTreeSet<String> {
679        self.children
680            .iter()
681            .flat_map(|(parent, entries)| {
682                entries.iter().filter(|entry| entry.folder).map(move |entry| child_key(parent, &entry.name))
683            })
684            .collect()
685    }
686
687    /// The path on disk of the entry `key`, the root itself for [`Self::ROOT`].
688    #[must_use]
689    pub fn path(&self, key: &str) -> PathBuf {
690        key.split('/').filter(|part| !part.is_empty()).fold(self.root.clone(), |path, part| path.join(part))
691    }
692
693    /// Makes `key` the one selected entry, with the cursor on it.
694    pub fn select(&mut self, key: &str) {
695        self.selected = Some(key.to_owned());
696        self.chosen = vec![key.to_owned()];
697    }
698
699    /// What an action asked for on the row `key` acts on: the whole selection when the row is one
700    /// of several selected, and the row alone otherwise, the way a right click on a row is meant.
701    ///
702    /// An entry inside a folder that is also taken is left out, because it goes wherever the
703    /// folder goes; the root itself is never taken.
704    #[must_use]
705    pub fn targets(&self, key: &str) -> Vec<String> {
706        targets_of(&self.chosen, key)
707    }
708
709    /// What is wrong with the name typed so far, if anything; an empty name only once the person
710    /// has tried to confirm it.
711    #[must_use]
712    pub fn naming_problem(&self) -> Option<NameProblem> {
713        let naming = self.naming.as_ref()?;
714        let siblings = self.children(&naming.folder).unwrap_or_default().iter().map(|entry| entry.name.as_str());
715        let current = match &naming.purpose {
716            NameFor::Rename(key) => Some(name_of(key)),
717            NameFor::File | NameFor::Folder => None,
718        };
719        let problem = ops::check_name(&naming.value, siblings, current).err()?;
720        (problem != NameProblem::Empty || naming.tried).then_some(problem)
721    }
722
723    /// The folders whose rows are on screen: every open folder whose folders above it are all open
724    /// too, the root first when it is. A folder left open inside a closed one is remembered, not
725    /// shown.
726    #[must_use]
727    pub fn visible_folders(&self) -> Vec<String> {
728        self.open
729            .iter()
730            .filter(|key| {
731                let mut above = key.as_str();
732                while above != ROOT {
733                    above = parent_key(above);
734                    if !self.open.contains(above) {
735                        return false;
736                    }
737                }
738                true
739            })
740            .cloned()
741            .collect()
742    }
743
744    /// The folders whose entries are shown: the root and every open folder that has been read.
745    fn shown_folders(&self) -> Vec<String> {
746        std::iter::once(ROOT.to_owned())
747            .chain(self.open.iter().filter(|key| *key != ROOT && self.children.contains_key(*key)).cloned())
748            .collect()
749    }
750
751    /// Opens or closes the folder `key`, and answers whether its entries still have to be read.
752    ///
753    /// A folder is read once; opening it again shows what is already known and reads nothing, so
754    /// clicking a chevron twice does not go to the disk twice.
755    fn expand(&mut self, key: &str, open: bool) -> bool {
756        if !open {
757            self.open.remove(key);
758            return false;
759        }
760        self.open.insert(key.to_owned());
761        if self.children.contains_key(key) || self.loading.contains(key) {
762            return false;
763        }
764        self.loading.insert(key.to_owned());
765        true
766    }
767
768    /// Takes the answer for the folder `key`.
769    ///
770    /// A folder that could not be read keeps its place and the root says what happened, so the
771    /// person sees the reason rather than a gap.
772    fn take_read(&mut self, key: &str, entries: Result<Vec<FolderEntry>, String>) {
773        self.loading.remove(key);
774        // A folder closed or gone while it was being read keeps nothing of the answer, so opening
775        // it again, or a new folder of its name, reads afresh.
776        if key != ROOT && !self.open.contains(key) {
777            return;
778        }
779        match entries {
780            Ok(entries) => {
781                self.errors.remove(key);
782                self.children.insert(key.to_owned(), entries);
783                // A folder read again may hold entries that changed since, so what was known about
784                // them is let go and asked for afresh rather than shown out of date.
785                self.details.retain(|entry, _| parent_key(entry) != key);
786                self.reading.retain(|entry| parent_key(entry) != key);
787                self.prune(key);
788            }
789            Err(problem) => {
790                // The reason is kept whichever folder it was. The root draws it in place of the
791                // tree; any other folder keeps its row, holds no entries and says on the row that
792                // it could not be read, so an empty folder and a refused one never look alike.
793                self.errors.insert(key.to_owned(), problem);
794                if key != ROOT {
795                    self.children.insert(key.to_owned(), Vec::new());
796                }
797            }
798        }
799    }
800
801    /// Forgets what the manager held below the folder `key` that its entries, just read, no longer
802    /// have: an entry removed or moved away by another program leaves no open folder, cut or
803    /// selection behind that would act on a name that is not there.
804    fn prune(&mut self, key: &str) {
805        let Some(entries) = self.children.get(key) else { return };
806        let names: BTreeSet<&str> = entries.iter().map(|entry| entry.name.as_str()).collect();
807        let held = self
808            .open
809            .iter()
810            .chain(self.children.keys())
811            .chain(&self.loading)
812            .chain(&self.cut)
813            .chain(&self.chosen)
814            .chain(&self.selected);
815        let gone: BTreeSet<String> = held
816            .filter(|held| *held != key && (key == ROOT || is_within(held, key)))
817            .map(|held| {
818                let below = if key == ROOT { held.as_str() } else { &held[key.len() + 1..] };
819                below.split('/').next().unwrap_or(below)
820            })
821            .filter(|name| !names.contains(name))
822            .map(|name| child_key(key, name))
823            .collect();
824        for child in gone {
825            self.forget(&child);
826        }
827    }
828
829    /// Gives every key at or below `from` the place `to` instead, after a rename or a move, so an
830    /// open folder stays open and the selection stays on what moved.
831    fn rekey(&mut self, from: &str, to: &str) {
832        let moved = |key: &str| is_within(key, from).then(|| format!("{to}{}", &key[from.len()..]));
833        self.open = self.open.iter().map(|key| moved(key).unwrap_or_else(|| key.clone())).collect();
834        self.loading.retain(|key| !is_within(key, from));
835        self.children = std::mem::take(&mut self.children)
836            .into_iter()
837            .map(|(key, entries)| (moved(&key).unwrap_or(key), entries))
838            .collect();
839        self.details = std::mem::take(&mut self.details)
840            .into_iter()
841            .map(|(key, details)| (moved(&key).unwrap_or(key), details))
842            .collect();
843        self.errors = std::mem::take(&mut self.errors)
844            .into_iter()
845            .map(|(key, problem)| (moved(&key).unwrap_or(key), problem))
846            .collect();
847        self.reading.retain(|key| !is_within(key, from));
848        if let Some(new) = moved(&self.shown) {
849            self.shown = new;
850        }
851        for key in self.selected.iter_mut().chain(&mut self.cut).chain(&mut self.chosen) {
852            if let Some(new) = moved(key) {
853                *key = new;
854            }
855        }
856    }
857
858    /// Forgets everything at or below `key`, after it was deleted. The cursor goes to the folder it
859    /// was in, the nearest thing still there.
860    fn forget(&mut self, key: &str) {
861        // A folder a flat view shows that is taken away leaves the view in the one above it.
862        if is_within(&self.shown, key) {
863            self.shown = parent_key(key).to_owned();
864        }
865        self.details.retain(|entry, _| !is_within(entry, key));
866        self.reading.retain(|entry| !is_within(entry, key));
867        self.open.retain(|open| !is_within(open, key));
868        self.loading.retain(|loading| !is_within(loading, key));
869        self.children.retain(|folder, _| !is_within(folder, key));
870        self.errors.retain(|folder, _| !is_within(folder, key));
871        self.cut.retain(|cut| !is_within(cut, key));
872        self.chosen.retain(|chosen| !is_within(chosen, key));
873        if self.selected.as_deref().is_some_and(|selected| is_within(selected, key)) {
874            let parent = parent_key(key);
875            self.selected = (parent != ROOT).then(|| parent.to_owned());
876        }
877    }
878}
879
880/// What an action on the row `key` acts on while `chosen` is selected; see
881/// [`FileManagerState::targets`]. The menu is built after the view, without the state, so it takes
882/// the selection along.
883pub(super) fn targets_of(chosen: &[String], key: &str) -> Vec<String> {
884    let keys = if chosen.len() > 1 && chosen.iter().any(|selected| selected == key) {
885        chosen.to_vec()
886    } else {
887        vec![key.to_owned()]
888    };
889    outermost(keys)
890}
891
892/// `keys` without any key inside a folder that is also among them, and without the root, in their
893/// order.
894pub(super) fn outermost(keys: Vec<String>) -> Vec<String> {
895    let all = keys.clone();
896    keys.into_iter()
897        .filter(|key| key != ROOT && !all.iter().any(|other| other != key && is_within(key, other)))
898        .collect()
899}
900
901/// How many names a question lists before it only counts the rest.
902const NAMES_SHOWN: usize = 5;
903
904impl FileManagerState {
905    /// Reads the root the first time, and every folder on screen again after that: the files may
906    /// have changed while the manager was away.
907    ///
908    /// Call it when the manager comes on screen. `wrap` turns the manager's messages into the
909    /// application's own, as in [`update`](Self::update).
910    pub fn load<Msg: Clone + Send + 'static>(
911        &mut self,
912        wrap: impl Fn(FileManagerMsg) -> Msg + Send + Sync + 'static,
913    ) -> Command<Msg> {
914        let wrap: Wrap<Msg> = Arc::new(wrap);
915        self.with_wrap(&wrap, Self::load_with)
916    }
917
918    /// Applies `message` and answers with the work it asks for: folders are read and file
919    /// operations run on background threads, never while drawing.
920    ///
921    /// `wrap` is a function such as `Msg::Manager`, or a closure that captures what it needs, such
922    /// as a screen's own conversion. It is used for every message the work sends back, so it must
923    /// be safe to move to another thread.
924    pub fn update<Msg: Clone + Send + 'static>(
925        &mut self,
926        message: FileManagerMsg,
927        wrap: impl Fn(FileManagerMsg) -> Msg + Send + Sync + 'static,
928    ) -> Command<Msg> {
929        let wrap: Wrap<Msg> = Arc::new(wrap);
930        self.with_wrap(&wrap, |state, wrap| state.apply(message, wrap))
931    }
932
933    /// Runs `work` and then keeps the watch on exactly the folders on screen, whatever the work
934    /// changed about them.
935    fn with_wrap<Msg: Clone + Send + 'static>(
936        &mut self,
937        wrap: &Wrap<Msg>,
938        work: impl FnOnce(&mut Self, &Wrap<Msg>) -> Command<Msg>,
939    ) -> Command<Msg> {
940        let command = work(self, wrap);
941        let followed = super::watch::follow(self, wrap);
942        Command::batch([command, followed])
943    }
944
945    fn load_with<Msg: Clone + Send + 'static>(&mut self, wrap: &Wrap<Msg>) -> Command<Msg> {
946        if self.children.contains_key(ROOT) {
947            let shown = self.shown_folders();
948            return self.reread(shown, wrap);
949        }
950        if !self.loading.insert(ROOT.to_owned()) {
951            return Command::none();
952        }
953        self.read_folder(ROOT, wrap)
954    }
955
956    /// Asks for the details of the entries `keys`: their size, when they changed last and their
957    /// permissions.
958    ///
959    /// Only the keys nothing is known about yet are read, so asking for the same page twice costs
960    /// nothing. Use this when the application knows exactly which rows it draws; a view that shows
961    /// details asks for a page around the cursor by itself. Never hand it a whole folder: every
962    /// key is one more call to the system, which over a remote file system is what a large folder
963    /// cannot afford.
964    ///
965    /// The answers come back as [`FileManagerMsg::Detailed`] and are read with
966    /// [`details`](Self::details).
967    pub fn detail<Msg: Clone + Send + 'static>(
968        &mut self,
969        keys: Vec<String>,
970        wrap: impl Fn(FileManagerMsg) -> Msg + Send + Sync + 'static,
971    ) -> Command<Msg> {
972        let wrap: Wrap<Msg> = Arc::new(wrap);
973        self.read_details(keys, &wrap)
974    }
975
976    /// Asks for the details of a page of entries of the folder `folder`, around the cursor.
977    ///
978    /// A page of two hundred entries is always more than a screen holds and far less than a large
979    /// folder, so a person scrolling rarely waits and a folder of ten thousand entries never turns
980    /// into ten thousand calls to the system. The cursor decides where the page sits; a cursor
981    /// somewhere else, or nowhere, starts it at the top of the folder.
982    ///
983    /// The views that show details use this by themselves; an application that knows exactly which
984    /// rows it draws asks for those with [`detail`](Self::detail) instead.
985    pub fn detail_page<Msg: Clone + Send + 'static>(
986        &mut self,
987        folder: &str,
988        wrap: impl Fn(FileManagerMsg) -> Msg + Send + Sync + 'static,
989    ) -> Command<Msg> {
990        let wrap: Wrap<Msg> = Arc::new(wrap);
991        self.read_page(folder, &wrap)
992    }
993
994    /// Shows the folder `key` in the flat views, reading it if it has not been read.
995    ///
996    /// The cursor starts at the folder's own row, so the keys go on from the top of what is now
997    /// shown rather than from a row of the folder that was left.
998    fn enter<Msg: Clone + Send + 'static>(&mut self, key: &str, wrap: &Wrap<Msg>) -> Command<Msg> {
999        if key != ROOT && !self.is_folder(key) {
1000            return Command::none();
1001        }
1002        self.shown = key.to_owned();
1003        self.selected = None;
1004        self.chosen.clear();
1005        // The folder is opened as well as shown, so the tree and the flat views agree about where
1006        // the person is when the shape is changed.
1007        let read = self.expand(key, true);
1008        if read { self.read_folder(key, wrap) } else { Command::none() }
1009    }
1010
1011    /// The entries of a page around the cursor in the folder `folder` that nothing is known about
1012    /// yet and that nothing is on its way for.
1013    ///
1014    /// This is what a view showing details asks for while it draws: it is empty once the page is
1015    /// known or already being read, so the view asks once and then stops asking.
1016    #[must_use]
1017    pub fn detail_gaps(&self, folder: &str) -> Vec<String> {
1018        let Some(entries) = self.shown_children(folder) else { return Vec::new() };
1019        let keys: Vec<String> = entries.iter().map(|entry| child_key(folder, &entry.name)).collect();
1020        let at = self.selected.as_deref().and_then(|cursor| keys.iter().position(|key| key == cursor)).unwrap_or(0);
1021        // The page is put around the cursor, so moving on in either direction stays inside it.
1022        let start = at.saturating_sub(PAGE / 2);
1023        keys.into_iter().skip(start).take(PAGE).filter(|key| !self.has_details(key)).collect()
1024    }
1025
1026    /// A folder that was just read, when it is the one shown and a view asked for details before,
1027    /// asks for its page at once. The view asks when the person is quiet, which a read the
1028    /// application started does not end, so without this the rows would stay bare until a key.
1029    fn details_again<Msg: Clone + Send + 'static>(&mut self, key: &str, wrap: &Wrap<Msg>) -> Command<Msg> {
1030        if self.details_wanted && key == self.shown { self.read_page(key, wrap) } else { Command::none() }
1031    }
1032
1033    /// The page of [`detail_page`](Self::detail_page), for the manager's own asking.
1034    pub(super) fn read_page<Msg: Clone + Send + 'static>(&mut self, folder: &str, wrap: &Wrap<Msg>) -> Command<Msg> {
1035        let page = self.detail_gaps(folder);
1036        self.read_details(page, wrap)
1037    }
1038
1039    /// Reads the details of `keys` on a background thread, leaving out what is known or on its way.
1040    pub(super) fn read_details<Msg: Clone + Send + 'static>(
1041        &mut self,
1042        keys: Vec<String>,
1043        wrap: &Wrap<Msg>,
1044    ) -> Command<Msg> {
1045        let wanted: Vec<String> = keys.into_iter().filter(|key| key != ROOT && !self.has_details(key)).collect();
1046        if wanted.is_empty() {
1047            return Command::none();
1048        }
1049        for key in &wanted {
1050            self.reading.insert(key.clone());
1051        }
1052        let (root, wrap) = (self.root.clone(), Arc::clone(wrap));
1053        Command::perform(move || {
1054            let read = wanted
1055                .iter()
1056                .map(|key| {
1057                    let path =
1058                        key.split('/').filter(|part| !part.is_empty()).fold(root.clone(), |at, part| at.join(part));
1059                    (key.clone(), FileDetails::read(&path))
1060                })
1061                .collect();
1062            wrap(FileManagerMsg::Detailed(read))
1063        })
1064    }
1065
1066    /// Reads one folder on a background thread.
1067    fn read_folder<Msg: Clone + Send + 'static>(&self, key: &str, wrap: &Wrap<Msg>) -> Command<Msg> {
1068        let (key, path, wrap) = (key.to_owned(), self.path(key), Arc::clone(wrap));
1069        Command::perform(move || wrap(FileManagerMsg::Listed(key.clone(), FolderEntry::list(&path))))
1070    }
1071
1072    /// Reads the folders `keys` again, each while it is still shown.
1073    pub(super) fn reread<Msg: Clone + Send + 'static>(&mut self, keys: Vec<String>, wrap: &Wrap<Msg>) -> Command<Msg> {
1074        let keys: Vec<String> = keys.into_iter().filter(|key| key == ROOT || self.is_open(key)).collect();
1075        let commands: Vec<Command<Msg>> = keys
1076            .into_iter()
1077            .map(|key| {
1078                self.loading.insert(key.clone());
1079                self.read_folder(&key, wrap)
1080            })
1081            .collect();
1082        Command::batch(commands)
1083    }
1084
1085    /// Reads again every folder on screen: when the person asks, when the manager is returned to,
1086    /// and when a watch says anything may have changed.
1087    pub(super) fn refresh<Msg: Clone + Send + 'static>(&mut self, wrap: &Wrap<Msg>) -> Command<Msg> {
1088        let shown = self.shown_folders();
1089        self.reread(shown, wrap)
1090    }
1091
1092    fn apply<Msg: Clone + Send + 'static>(&mut self, message: FileManagerMsg, wrap: &Wrap<Msg>) -> Command<Msg> {
1093        match message {
1094            FileManagerMsg::Select(key) => {
1095                self.selected = Some(key);
1096                Command::none()
1097            }
1098            FileManagerMsg::Choose(keys) => {
1099                self.chosen = keys;
1100                Command::none()
1101            }
1102            FileManagerMsg::Expand(key, open) => {
1103                if !self.expand(&key, open) {
1104                    return Command::none();
1105                }
1106                self.read_folder(&key, wrap)
1107            }
1108            FileManagerMsg::Read(key, entries) => {
1109                self.take_read(&key, entries);
1110                self.details_again(&key, wrap)
1111            }
1112            FileManagerMsg::Listed(key, entries) => {
1113                self.take_read(&key, entries.map_err(|problem| problem.message()));
1114                self.details_again(&key, wrap)
1115            }
1116            FileManagerMsg::NewFile(folder) => self.ask_name(NameFor::File, folder, String::new(), wrap),
1117            FileManagerMsg::NewFolder(folder) => self.ask_name(NameFor::Folder, folder, String::new(), wrap),
1118            FileManagerMsg::Rename(key) => {
1119                let (folder, name) = (parent_key(&key).to_owned(), name_of(&key).to_owned());
1120                self.ask_name(NameFor::Rename(key), folder, name, wrap)
1121            }
1122            FileManagerMsg::Cut(key) => {
1123                self.cut = self.targets(&key);
1124                self.copying = false;
1125                Command::none()
1126            }
1127            FileManagerMsg::Copy(key) => {
1128                self.cut = self.targets(&key);
1129                self.copying = true;
1130                Command::none()
1131            }
1132            FileManagerMsg::DropCut => {
1133                self.cut.clear();
1134                self.copying = false;
1135                Command::none()
1136            }
1137            FileManagerMsg::Paste(into) => {
1138                let keys = self.cut.clone();
1139                if self.copying {
1140                    return self.copy_all(keys, into, wrap);
1141                }
1142                self.move_all(keys, into, wrap)
1143            }
1144            FileManagerMsg::Drop(TreeDrop { keys, into }) => {
1145                self.move_all(outermost(keys), into.unwrap_or_default(), wrap)
1146            }
1147            FileManagerMsg::DropCopy(TreeDrop { keys, into }) => {
1148                self.copy_all(outermost(keys), into.unwrap_or_default(), wrap)
1149            }
1150            FileManagerMsg::Delete(key) => self.ask_delete(self.targets(&key), wrap),
1151            FileManagerMsg::DeleteConfirmed(keys) => {
1152                let confined = self.confined;
1153                self.run_each(keys, wrap, move |root, key| (key.to_owned(), ops::delete(root, key, confined)))
1154            }
1155            FileManagerMsg::Trash(key) => {
1156                let keys = self.targets(&key);
1157                self.trash_all(keys, wrap)
1158            }
1159            FileManagerMsg::ShowHidden(showing) => {
1160                self.hidden = showing;
1161                Command::none()
1162            }
1163            FileManagerMsg::Work(event) => self.took_event(event, wrap),
1164            FileManagerMsg::Stop => match &self.work {
1165                Some(work) => Command::cancel_task(work.id),
1166                None => Command::none(),
1167            },
1168            FileManagerMsg::Refresh => self.refresh(wrap),
1169            FileManagerMsg::Name(value) => {
1170                if let Some(naming) = &mut self.naming {
1171                    naming.value = value;
1172                }
1173                Command::none()
1174            }
1175            FileManagerMsg::Submit => self.submit(wrap),
1176            FileManagerMsg::CloseNaming => {
1177                self.naming = None;
1178                Command::none()
1179            }
1180            FileManagerMsg::Done(results) => self.done(results, wrap),
1181            FileManagerMsg::Changed(run, batch) => super::watch::changed(self, run, batch, wrap),
1182            FileManagerMsg::Quiet(run) => super::watch::quiet(self, run, wrap),
1183            FileManagerMsg::Enter(key) => self.enter(&key, wrap),
1184            FileManagerMsg::Leave => {
1185                if self.shown == ROOT {
1186                    return Command::none();
1187                }
1188                let left = self.shown.clone();
1189                let up = parent_key(&left).to_owned();
1190                let command = self.enter(&up, wrap);
1191                // The cursor lands on the folder that was left, which is where the eye already is.
1192                self.selected = Some(left);
1193                command
1194            }
1195            FileManagerMsg::Detail(keys) => {
1196                self.details_wanted = true;
1197                self.read_details(keys, wrap)
1198            }
1199            FileManagerMsg::Detailed(read) => {
1200                for (key, details) in read {
1201                    self.reading.remove(&key);
1202                    self.details.insert(key, details);
1203                }
1204                Command::none()
1205            }
1206        }
1207    }
1208
1209    /// Opens the dialog asking for a name in `folder`, opening the folder too: the new entry will
1210    /// be shown there, and the names already in it are what the name is checked against.
1211    fn ask_name<Msg: Clone + Send + 'static>(
1212        &mut self,
1213        purpose: NameFor,
1214        folder: String,
1215        value: String,
1216        wrap: &Wrap<Msg>,
1217    ) -> Command<Msg> {
1218        let read = folder != ROOT && self.expand(&folder, true);
1219        let command = if read { self.read_folder(&folder, wrap) } else { Command::none() };
1220        self.naming = Some(Naming { purpose, folder, value, tried: false });
1221        command
1222    }
1223
1224    /// Takes the name in the dialog, or points out what is wrong with it.
1225    fn submit<Msg: Clone + Send + 'static>(&mut self, wrap: &Wrap<Msg>) -> Command<Msg> {
1226        let Some(naming) = self.naming.as_mut() else { return Command::none() };
1227        naming.tried = true;
1228        if self.naming_problem().is_some() {
1229            return Command::none();
1230        }
1231        let Some(Naming { purpose, folder, value: name, .. }) = self.naming.take() else { return Command::none() };
1232        let confined = self.confined;
1233        match purpose {
1234            NameFor::File => self.run_each(vec![folder], wrap, move |root, folder| {
1235                (child_key(folder, &name), ops::create_file(root, folder, &name, confined))
1236            }),
1237            NameFor::Folder => self.run_each(vec![folder], wrap, move |root, folder| {
1238                (child_key(folder, &name), ops::create_folder(root, folder, &name, confined))
1239            }),
1240            // The same name again is nothing to do, not a clash with itself.
1241            NameFor::Rename(key) if name_of(&key) == name => Command::none(),
1242            NameFor::Rename(key) => self
1243                .run_each(vec![key], wrap, move |root, key| (key.to_owned(), ops::rename(root, key, &name, confined))),
1244        }
1245    }
1246
1247    /// Moves the entries `keys` into the folder `into`, each on its own: one that cannot go does
1248    /// not keep the others from going, and the answer says which stayed and why.
1249    fn move_all<Msg: Clone + Send + 'static>(
1250        &mut self,
1251        keys: Vec<String>,
1252        into: String,
1253        wrap: &Wrap<Msg>,
1254    ) -> Command<Msg> {
1255        if keys.is_empty() {
1256            return Command::none();
1257        }
1258        let confined = self.confined;
1259        self.run_each(keys, wrap, move |root, key| (key.to_owned(), ops::move_into(root, key, &into, confined)))
1260    }
1261
1262    /// Copies the entries `keys` into the folder `into`, each on its own, as a background task
1263    /// that says how far it has come and can be stopped.
1264    ///
1265    /// A copy is the one operation with no upper bound: a rename is instant whatever it moves,
1266    /// while a folder of photographs is read and written byte by byte. So this does not hold a
1267    /// thread until it is over and hope it is quick; it is a task like a build or a download, with
1268    /// a share done and a way to say stop.
1269    ///
1270    /// One copy runs at a time: starting another while one is going would make two progress bars
1271    /// for one row of the manager, so a copy asked for while one runs is refused by doing nothing.
1272    fn copy_all<Msg: Clone + Send + 'static>(
1273        &mut self,
1274        keys: Vec<String>,
1275        into: String,
1276        wrap: &Wrap<Msg>,
1277    ) -> Command<Msg> {
1278        if keys.is_empty() || self.work.is_some() {
1279            return Command::none();
1280        }
1281        let (confined, root, count) = (self.confined, self.root.clone(), keys.len());
1282        let label = crate::t!("quvyta.file-manager.copying", n = count);
1283        let finish = Arc::clone(wrap);
1284        let events = Arc::clone(wrap);
1285        let task = Task::new(label, move |cx| {
1286            let mut results = Vec::new();
1287            let step = 1.0 / count as f32;
1288            for (index, key) in keys.iter().enumerate() {
1289                cx.note(name_of(key).to_owned());
1290                let base = index as f32 * step;
1291                let progress = |done: u64, total: u64| {
1292                    let share = if total == 0 { 1.0 } else { done as f32 / total as f32 };
1293                    cx.progress(base + share * step);
1294                };
1295                let stopped = || cx.is_cancelled();
1296                let watch = ops::Watch { progress: &progress, stopped: &stopped };
1297                results.push((key.clone(), ops::copy_watched(&root, key, &into, confined, &watch)));
1298                if cx.is_cancelled() {
1299                    // What was copied before the person said stop stays; the half-written one was
1300                    // taken away again by the copy itself.
1301                    break;
1302                }
1303            }
1304            Ok(finish(FileManagerMsg::Done(results)))
1305        })
1306        .on_event(move |event| events(FileManagerMsg::Work(event)));
1307        self.work = Some(FileWork { id: task.id(), entries: count, done: 0.0, note: String::new() });
1308        Command::task(task)
1309    }
1310
1311    /// Takes what the running operation said about itself.
1312    ///
1313    /// A task that was stopped never delivers its result, so the folders it touched are read again
1314    /// here: what it had already copied is on disk and belongs on the screen.
1315    fn took_event<Msg: Clone + Send + 'static>(&mut self, event: TaskEvent, wrap: &Wrap<Msg>) -> Command<Msg> {
1316        let Some(work) = &mut self.work else { return Command::none() };
1317        if event.id() != work.id {
1318            return Command::none();
1319        }
1320        match event {
1321            TaskEvent::Started { .. } => Command::none(),
1322            TaskEvent::Progress { fraction, note, .. } => {
1323                if let Some(fraction) = fraction {
1324                    work.done = fraction;
1325                }
1326                if let Some(note) = note {
1327                    work.note = note;
1328                }
1329                Command::none()
1330            }
1331            TaskEvent::Finished { outcome, .. } => {
1332                self.work = None;
1333                match outcome {
1334                    TaskOutcome::Cancelled => self.refresh(wrap),
1335                    // A task that ended by itself delivered its `Done` first, which read the
1336                    // folders it touched already.
1337                    TaskOutcome::Done | TaskOutcome::Failed(_) => Command::none(),
1338                }
1339            }
1340        }
1341    }
1342
1343    /// Puts the entries `keys` in the trash, each on its own.
1344    ///
1345    /// Nothing is asked first: the trash can be looked in again, and a question about something
1346    /// that can be undone is a question not worth asking. An entry the trash cannot take is not
1347    /// deleted quietly: [`done`](Self::done) asks about that one on its own.
1348    fn trash_all<Msg: Clone + Send + 'static>(&mut self, keys: Vec<String>, wrap: &Wrap<Msg>) -> Command<Msg> {
1349        let Some(trash) = self.trash.folder() else {
1350            // No trash at all is the same answer for every entry, and the question comes at once.
1351            return self.ask_delete_forever(keys, wrap);
1352        };
1353        if keys.is_empty() {
1354            return Command::none();
1355        }
1356        let confined = self.confined;
1357        self.run_each(keys, wrap, move |root, key| (key.to_owned(), ops::to_trash(&trash, root, key, confined)))
1358    }
1359
1360    /// Asks before deleting, which cannot be undone; a folder says it takes everything in it along.
1361    fn ask_delete<Msg: Clone + Send + 'static>(&self, keys: Vec<String>, wrap: &Wrap<Msg>) -> Command<Msg> {
1362        let folders = keys.iter().any(|key| self.is_folder(key));
1363        let (title, message) = match keys.as_slice() {
1364            [] => return Command::none(),
1365            [key] => {
1366                let name = name_of(key);
1367                let text = if folders {
1368                    "quvyta.file-manager.delete-folder-text"
1369                } else {
1370                    "quvyta.file-manager.delete-file-text"
1371                };
1372                (crate::t!("quvyta.file-manager.delete-title", name = name), crate::t!(text, name = name))
1373            }
1374            many => {
1375                let mut names = many.iter().take(NAMES_SHOWN).map(|key| name_of(key)).collect::<Vec<_>>().join(", ");
1376                if many.len() > NAMES_SHOWN {
1377                    names =
1378                        format!("{names} {}", crate::t!("quvyta.file-manager.and-more", n = many.len() - NAMES_SHOWN));
1379                }
1380                let text = if folders {
1381                    "quvyta.file-manager.delete-many-folders-text"
1382                } else {
1383                    "quvyta.file-manager.delete-many-text"
1384                };
1385                (
1386                    crate::t!("quvyta.file-manager.delete-many-title", n = many.len()),
1387                    crate::t!(text, names = names.as_str()),
1388                )
1389            }
1390        };
1391        let label = if keys.len() > 1 {
1392            crate::t!("quvyta.file-manager.delete-many", n = keys.len())
1393        } else {
1394            crate::t!("quvyta.file-manager.delete")
1395        };
1396        let confirmed = wrap(FileManagerMsg::DeleteConfirmed(keys));
1397        Command::confirm(Confirm::new(title, confirmed).message(message).confirm_label(label).danger())
1398    }
1399
1400    /// Asks whether to delete for good what the trash could not take, saying why the trash was no
1401    /// use: on another file system, or there is none.
1402    fn ask_delete_forever<Msg: Clone + Send + 'static>(&self, keys: Vec<String>, wrap: &Wrap<Msg>) -> Command<Msg> {
1403        let (title, message) = match keys.as_slice() {
1404            [] => return Command::none(),
1405            [key] => {
1406                let name = name_of(key);
1407                (
1408                    crate::t!("quvyta.file-manager.no-trash-title", name = name),
1409                    crate::t!("quvyta.file-manager.no-trash-text", name = name),
1410                )
1411            }
1412            many => (
1413                crate::t!("quvyta.file-manager.no-trash-many-title", n = many.len()),
1414                crate::t!("quvyta.file-manager.no-trash-many-text"),
1415            ),
1416        };
1417        let confirmed = wrap(FileManagerMsg::DeleteConfirmed(keys));
1418        let label = crate::t!("quvyta.file-manager.delete-forever");
1419        Command::confirm(Confirm::new(title, confirmed).message(message).confirm_label(label).danger())
1420    }
1421
1422    /// Runs a file operation for each of `items` on a background thread, one after the other: a
1423    /// move of several entries is done in the order they were given, so a clash between two of
1424    /// them is decided the same way every time.
1425    fn run_each<Msg: Clone + Send + 'static>(
1426        &self,
1427        items: Vec<String>,
1428        wrap: &Wrap<Msg>,
1429        work: impl Fn(&Path, &str) -> (String, Result<FileChange, FileError>) + Send + 'static,
1430    ) -> Command<Msg> {
1431        let (root, wrap) = (self.root.clone(), Arc::clone(wrap));
1432        Command::perform(move || wrap(FileManagerMsg::Done(items.iter().map(|item| work(&root, item)).collect())))
1433    }
1434
1435    /// Takes what the operations changed: the manager follows it, the folders they touched are
1436    /// read again and the selection goes to what was made or moved. What was refused is said,
1437    /// entry by entry when there were several.
1438    fn done<Msg: Clone + Send + 'static>(
1439        &mut self,
1440        results: Vec<(String, Result<FileChange, FileError>)>,
1441        wrap: &Wrap<Msg>,
1442    ) -> Command<Msg> {
1443        let total = results.len();
1444        let mut touched = Vec::new();
1445        let mut arrived = Vec::new();
1446        let mut refused = Vec::new();
1447        let mut without_trash = Vec::new();
1448        for (key, result) in results {
1449            match result {
1450                // The trash could not take it, so the person is asked about that one entry instead
1451                // of being told off: it is a limit of the trash, not a mistake of theirs.
1452                Err(FileError::NoTrash) => without_trash.push(key),
1453                Err(error) => refused.push((key, error)),
1454                Ok(FileChange::Copied(key)) => {
1455                    touched.push(parent_key(&key).to_owned());
1456                    arrived.push(key);
1457                }
1458                Ok(FileChange::Trashed(key)) => {
1459                    self.forget(&key);
1460                    touched.push(parent_key(&key).to_owned());
1461                }
1462                Ok(FileChange::Created(key)) => {
1463                    touched.push(parent_key(&key).to_owned());
1464                    arrived.push(key);
1465                }
1466                Ok(FileChange::Moved(from, to)) => {
1467                    // A cut entry that went somewhere is used up; one that was refused stays cut,
1468                    // to be tried elsewhere.
1469                    self.cut.retain(|cut| *cut != from);
1470                    if from == to {
1471                        continue;
1472                    }
1473                    self.rekey(&from, &to);
1474                    let parent = parent_key(&to).to_owned();
1475                    if parent != ROOT {
1476                        self.open.insert(parent.clone());
1477                    }
1478                    touched.extend([parent_key(&from).to_owned(), parent]);
1479                    arrived.push(to);
1480                }
1481                Ok(FileChange::Deleted(key)) => {
1482                    self.forget(&key);
1483                    touched.push(parent_key(&key).to_owned());
1484                }
1485            }
1486        }
1487        if let Some(first) = arrived.first() {
1488            self.selected = Some(first.clone());
1489            self.chosen = arrived;
1490        }
1491        touched.sort();
1492        touched.dedup();
1493        // An entry the trash could not take is asked about, not counted among the refusals.
1494        let total = total - without_trash.len();
1495        let asked = self.ask_delete_forever(without_trash, wrap);
1496        Command::batch([refusal(total, &refused), asked, self.reread(touched, wrap)])
1497    }
1498}
1499
1500/// Says what was refused: the reason alone when there was one entry, and which entries stayed with
1501/// each one's reason when there were several.
1502fn refusal<Msg: Clone + Send + 'static>(total: usize, refused: &[(String, FileError)]) -> Command<Msg> {
1503    match refused {
1504        [] => Command::none(),
1505        [(_, error)] if total == 1 => {
1506            Command::toast(Toast::danger(crate::t!("quvyta.file-manager.failed")).body(error.message()))
1507        }
1508        _ => {
1509            let lines: Vec<String> =
1510                refused.iter().map(|(key, error)| format!("{}: {}", name_of(key), error.message())).collect();
1511            let title = crate::t!("quvyta.file-manager.failed-some", failed = refused.len(), total = total);
1512            Command::toast(Toast::danger(title).body(lines.join("\n")))
1513        }
1514    }
1515}