Skip to main content

trail/app/
marks.rs

1//! The mark set: entries picked with `Space` (or a visual-mode range) for a
2//! later paste, move, copy or delete.
3//!
4//! Marks are **paths**, not indices — an index names whichever entry occupies a
5//! row now, and the whole point of a mark is that it survives navigation, a
6//! re-sort and a refresh. There is one set for the whole session, shared by
7//! every tab, so entries can be marked in one tab and pasted from another. It
8//! is not persisted: marks end when Trail does.
9//!
10//! Visual mode is another way of filling the set: its range is worked out here
11//! from the anchor and the selection.
12
13use std::collections::BTreeSet;
14use std::path::{Path, PathBuf};
15
16use crate::app::mode::Mode;
17use crate::app::state::AppState;
18
19/// The marked entries, in path order.
20#[derive(Debug, Default, Clone, PartialEq, Eq)]
21pub struct MarkSet {
22    paths: BTreeSet<PathBuf>,
23}
24
25impl MarkSet {
26    /// Marks `path` if it was not marked, unmarks it if it was. Returns whether
27    /// it is marked afterwards.
28    pub fn toggle(&mut self, path: &Path) -> bool {
29        if self.paths.remove(path) {
30            false
31        } else {
32            self.paths.insert(path.to_owned());
33            true
34        }
35    }
36
37    /// Marks every path in `paths`, leaving those already marked as they are.
38    pub fn extend(&mut self, paths: impl IntoIterator<Item = PathBuf>) {
39        self.paths.extend(paths);
40    }
41
42    /// Whether `path` is marked.
43    pub fn contains(&self, path: &Path) -> bool {
44        self.paths.contains(path)
45    }
46
47    /// How many entries are marked.
48    pub fn len(&self) -> usize {
49        self.paths.len()
50    }
51
52    /// Whether nothing is marked.
53    pub fn is_empty(&self) -> bool {
54        self.paths.is_empty()
55    }
56
57    /// Unmarks everything. Returns how many were marked.
58    pub fn clear(&mut self) -> usize {
59        let count = self.paths.len();
60        self.paths.clear();
61        count
62    }
63
64    /// Unmarks `path`.
65    pub fn remove(&mut self, path: &Path) {
66        self.paths.remove(path);
67    }
68
69    /// The marked paths, in path order.
70    pub fn paths(&self) -> impl Iterator<Item = &PathBuf> {
71        self.paths.iter()
72    }
73}
74
75/// The rows, first and last inclusive, that visual mode's range covers in the
76/// list the user is looking at — or `None` outside visual mode.
77///
78/// Counted the way `AppState::selected` is, so the range follows the sort, the
79/// hidden-file setting and any filter. An anchor that has left the listing (a
80/// refresh removed it) collapses the range onto the selection.
81pub fn visual_bounds(state: &AppState) -> Option<(usize, usize)> {
82    let Mode::Visual { anchor } = &state.mode else {
83        return None;
84    };
85    let anchor_row = state
86        .filtered_entries()
87        .position(|(_, e)| &e.path == anchor)
88        .unwrap_or(state.selected);
89    Some((
90        anchor_row.min(state.selected),
91        anchor_row.max(state.selected),
92    ))
93}
94
95/// The paths visual mode's range covers, top to bottom; empty outside it.
96pub fn visual_paths(state: &AppState) -> Vec<PathBuf> {
97    match visual_bounds(state) {
98        Some((first, last)) => state
99            .filtered_entries()
100            .skip(first)
101            .take(last - first + 1)
102            .map(|(_, e)| e.path.clone())
103            .collect(),
104        None => Vec::new(),
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn toggling_twice_unmarks() {
114        let mut marks = MarkSet::default();
115        let path = Path::new("a.txt");
116        assert!(marks.toggle(path));
117        assert!(marks.contains(path));
118        assert!(!marks.toggle(path));
119        assert!(marks.is_empty());
120    }
121}