Skip to main content

fm/modes/menu/
marks.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::io::{self, BufWriter, Write};
3use std::path::{Path, PathBuf};
4
5use anyhow::{anyhow, bail, Context, Result};
6
7use crate::common::{read_lines, tilde, MARKS_FILEPATH};
8use crate::io::DrawMenu;
9use crate::{impl_content, impl_selectable, log_info, log_line};
10
11/// Holds the marks created by the user.
12/// It's an ordered map between any char (except `:`, ` ` and control char) and a `PathBuf`.
13#[derive(Clone, Default)]
14pub struct Marks {
15    save_path: PathBuf,
16    content: Vec<(char, PathBuf)>,
17    pub index: usize,
18    used_chars: BTreeSet<char>,
19    paths_to_mark: BTreeMap<PathBuf, char>,
20}
21
22impl Marks {
23    /// True if there's no marks yet
24    #[must_use]
25    pub fn is_empty(&self) -> bool {
26        self.content.is_empty()
27    }
28
29    /// The number of saved marks
30    #[must_use]
31    pub fn len(&self) -> usize {
32        self.content.len()
33    }
34
35    /// Reads the marks stored in the config file (~/.config/fm/marks.cfg).
36    /// If an invalid marks is read, only the valid ones are kept
37    /// and the file is saved again.
38    pub fn setup(&mut self) {
39        self.save_path = PathBuf::from(tilde(MARKS_FILEPATH).as_ref());
40        self.content = vec![];
41        self.used_chars = BTreeSet::new();
42        let mut must_save = false;
43        if let Ok(lines) = read_lines(&self.save_path) {
44            for line in lines {
45                if let Ok((ch, path)) = Self::parse_line(line) {
46                    if !self.used_chars.contains(&ch) {
47                        self.content.push((ch, path));
48                        self.used_chars.insert(ch);
49                    }
50                } else {
51                    must_save = true;
52                }
53            }
54        }
55        self.content.sort();
56        self.index = 0;
57        if must_save {
58            log_info!("Wrong marks found, will save it again");
59            let _ = self.save_marks();
60        }
61        self.save_paths_to_mark();
62    }
63
64    fn save_paths_to_mark(&mut self) {
65        self.paths_to_mark.clear();
66        for (c, p) in self.content.iter() {
67            self.paths_to_mark.insert(p.to_path_buf(), *c);
68        }
69    }
70
71    /// Returns an optional marks associated to a char bind.
72    #[must_use]
73    pub fn get(&self, key: char) -> Option<PathBuf> {
74        for (ch, dest) in &self.content {
75            if &key == ch {
76                return Some(dest.clone());
77            }
78        }
79        None
80    }
81
82    fn parse_line(line: Result<String, io::Error>) -> Result<(char, PathBuf)> {
83        let line = line?;
84        let sp: Vec<&str> = line.split(':').collect();
85        if sp.len() != 2 {
86            return Err(anyhow!("marks: parse_line: Invalid mark line: {line}"));
87        }
88        sp[0].chars().next().map_or_else(
89            || {
90                bail!(
91                    "marks: parse line
92Invalid first character in: {line}"
93                )
94            },
95            |ch| {
96                if ch == ':' || ch == ' ' || ch.is_control() {
97                    bail!(
98                        "marks: parse line
99Invalid first characer in: {line}"
100                    )
101                }
102                let path = PathBuf::from(sp[1]);
103                Ok((ch, path))
104            },
105        )
106    }
107
108    /// Store a new mark in the config file.
109    /// If an update is done, the marks are saved again.
110    ///
111    /// # Errors
112    ///
113    /// It may fail if writing to the marks file fails.
114    pub fn new_mark(&mut self, ch: char, path: &Path) -> Result<()> {
115        if ch.is_control() {
116            log_line!("new mark - please use a printable symbol for mark");
117            return Ok(());
118        }
119        if ch == ':' || ch == ' ' {
120            log_line!("new mark - '{ch}' can't be used as a mark");
121            return Ok(());
122        }
123        self.remove_path(path)?;
124        if self.used_chars.contains(&ch) {
125            self.update_mark(ch, path);
126        } else {
127            self.content.push((ch, path.to_path_buf()));
128            self.used_chars.insert(ch);
129        }
130
131        self.save_marks()?;
132        log_line!("Saved mark {ch} -> {p}", p = path.display());
133        Ok(())
134    }
135
136    fn update_mark(&mut self, ch: char, path: &Path) {
137        let mut found_index = None;
138        for (index, (k, _)) in self.content.iter().enumerate() {
139            if *k == ch {
140                found_index = Some(index);
141                break;
142            }
143        }
144        if let Some(found_index) = found_index {
145            self.content[found_index] = (ch, path.to_path_buf());
146        }
147    }
148
149    pub fn remove_selected(&mut self) -> Result<()> {
150        if self.is_empty() {
151            return Ok(());
152        }
153        if self.index >= self.content.len() {
154            bail!(
155                "index of mark is {index} and len is {len}",
156                index = self.index,
157                len = self.content.len()
158            );
159        }
160
161        let (ch, path) = &self.content[self.index];
162        self.used_chars.remove(ch);
163        log_line!("Removed marks {ch} -> {path}", path = path.display());
164        self.content.remove(self.index);
165        self.prev();
166        self.save_marks()?;
167        Ok(())
168    }
169
170    fn save_marks(&mut self) -> Result<()> {
171        let file = std::fs::File::create(&self.save_path)?;
172        let mut buf = BufWriter::new(file);
173        self.content.sort();
174        self.save_paths_to_mark();
175        for (ch, path) in &self.content {
176            writeln!(buf, "{}:{}", ch, Self::path_as_string(path)?)?;
177        }
178        Ok(())
179    }
180
181    fn path_as_string(path: &Path) -> Result<String> {
182        Ok(path
183            .to_str()
184            .context("path_as_string: unreadable path")?
185            .to_owned())
186    }
187
188    /// Returns a vector of strings like "d: /dev" for every mark.
189    #[must_use]
190    pub fn as_strings(&self) -> Vec<String> {
191        self.content
192            .iter()
193            .map(|(ch, path)| Self::format_mark(*ch, path))
194            .collect()
195    }
196
197    fn format_mark(ch: char, path: &Path) -> String {
198        format!("{ch}    {path}", path = path.display())
199    }
200
201    /// Returns the char for associated to a path.
202    /// For marked path, it's their mark,
203    /// Otherwise it's ' '
204    pub fn char_for(&self, path: &Path) -> &char {
205        self.paths_to_mark.get(path).unwrap_or(&' ')
206    }
207
208    /// Change mark path from `old_path` to `new_path` and write the marks to disk
209    pub fn move_path(&mut self, old_path: &Path, new_path: &Path) -> Result<()> {
210        let ch = *self.char_for(old_path);
211        if ch == ' ' {
212            return Ok(());
213        }
214        self.update_mark(ch, new_path);
215        self.save_marks()
216    }
217
218    /// Remove a path from marks. Does nothing if the path isn't marked.
219    /// If the path is marked, it's removed everywhere (paths_to_mark, content and used_ch).
220    /// The marks are then saved.
221    pub fn remove_path(&mut self, old_path: &Path) -> Result<()> {
222        let Some(ch) = self.paths_to_mark.remove(old_path) else {
223            return Ok(());
224        };
225        self.used_chars.remove(&ch);
226        let mut found = false;
227        for index in 0..self.content.len() {
228            let (used_ch, stored_path) = &self.content[index];
229            if used_ch == &ch && old_path == stored_path {
230                self.content.remove(index);
231                found = true;
232                if index <= self.index {
233                    self.index = self.index.saturating_sub(1);
234                }
235                break;
236            }
237        }
238        if !found {
239            bail!(
240                "Couldn't find {old_path} in marks",
241                old_path = old_path.display()
242            )
243        }
244        self.save_marks()
245    }
246}
247
248type Pair = (char, PathBuf);
249impl_content!(Marks, Pair);
250
251impl DrawMenu<Pair> for Marks {}