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