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