fm/modes/menu/
marks.rs

1use std::collections::BTreeSet;
2use std::io::{self, BufWriter, Write};
3use std::path::{Path, PathBuf};
4
5use anyhow::{anyhow, 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 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}
20
21impl Marks {
22    /// True if there's no marks yet
23    #[must_use]
24    pub fn is_empty(&self) -> bool {
25        self.content.is_empty()
26    }
27
28    /// The number of saved marks
29    #[must_use]
30    pub fn len(&self) -> usize {
31        self.content.len()
32    }
33
34    /// Reads the marks stored in the config file (~/.config/fm/marks.cfg).
35    /// If an invalid marks is read, only the valid ones are kept
36    /// and the file is saved again.
37    pub fn setup(&mut self) {
38        self.save_path = PathBuf::from(tilde(MARKS_FILEPATH).as_ref());
39        self.content = vec![];
40        self.used_chars = BTreeSet::new();
41        let mut must_save = false;
42        if let Ok(lines) = read_lines(&self.save_path) {
43            for line in lines {
44                if let Ok((ch, path)) = Self::parse_line(line) {
45                    if !self.used_chars.contains(&ch) {
46                        self.content.push((ch, path));
47                        self.used_chars.insert(ch);
48                    }
49                } else {
50                    must_save = true;
51                }
52            }
53        }
54        self.content.sort();
55        self.index = 0;
56        if must_save {
57            log_info!("Wrong marks found, will save it again");
58            let _ = self.save_marks();
59        }
60    }
61
62    /// Returns an optional marks associated to a char bind.
63    #[must_use]
64    pub fn get(&self, key: char) -> Option<PathBuf> {
65        for (ch, dest) in &self.content {
66            if &key == ch {
67                return Some(dest.clone());
68            }
69        }
70        None
71    }
72
73    fn parse_line(line: Result<String, io::Error>) -> Result<(char, PathBuf)> {
74        let line = line?;
75        let sp: Vec<&str> = line.split(':').collect();
76        if sp.len() != 2 {
77            return Err(anyhow!("marks: parse_line: Invalid mark line: {line}"));
78        }
79        sp[0].chars().next().map_or_else(
80            || {
81                Err(anyhow!(
82                    "marks: parse line
83                 Invalid first character in: {line}"
84                ))
85            },
86            |ch| {
87                let path = PathBuf::from(sp[1]);
88                Ok((ch, path))
89            },
90        )
91    }
92
93    /// Store a new mark in the config file.
94    /// If an update is done, the marks are saved again.
95    ///
96    /// # Errors
97    ///
98    /// It may fail if writing to the marks file fails.
99    pub fn new_mark(&mut self, ch: char, path: &Path) -> Result<()> {
100        if ch == ':' {
101            log_line!("new mark - ':' can't be used as a mark");
102            return Ok(());
103        }
104        if self.used_chars.contains(&ch) {
105            self.update_mark(ch, path);
106        } else {
107            self.content.push((ch, path.to_path_buf()));
108        }
109
110        self.save_marks()?;
111        log_line!("Saved mark {ch} -> {p}", p = path.display());
112        Ok(())
113    }
114
115    fn update_mark(&mut self, ch: char, path: &Path) {
116        let mut found_index = None;
117        for (index, (k, _)) in self.content.iter().enumerate() {
118            if *k == ch {
119                found_index = Some(index);
120                break;
121            }
122        }
123        if let Some(found_index) = found_index {
124            self.content[found_index] = (ch, path.to_path_buf());
125        }
126    }
127
128    pub fn remove_selected(&mut self) -> Result<()> {
129        if self.is_empty() {
130            return Ok(());
131        }
132        let (ch, path) = self.selected().context("no marks saved")?;
133        log_line!("Removed marks {ch} -> {path}", path = path.display());
134        self.content.remove(self.index);
135        self.prev();
136        self.save_marks()
137    }
138
139    fn save_marks(&mut self) -> Result<()> {
140        let file = std::fs::File::create(&self.save_path)?;
141        let mut buf = BufWriter::new(file);
142        self.content.sort();
143        for (ch, path) in &self.content {
144            writeln!(buf, "{}:{}", ch, Self::path_as_string(path)?)?;
145        }
146        Ok(())
147    }
148
149    fn path_as_string(path: &Path) -> Result<String> {
150        Ok(path
151            .to_str()
152            .context("path_as_string: unreadable path")?
153            .to_owned())
154    }
155
156    /// Returns a vector of strings like "d: /dev" for every mark.
157    #[must_use]
158    pub fn as_strings(&self) -> Vec<String> {
159        self.content
160            .iter()
161            .map(|(ch, path)| Self::format_mark(*ch, path))
162            .collect()
163    }
164
165    fn format_mark(ch: char, path: &Path) -> String {
166        format!("{ch}    {path}", path = path.display())
167    }
168}
169
170type Pair = (char, PathBuf);
171impl_selectable!(Marks);
172impl_content!(Marks, Pair);
173
174impl DrawMenu<Pair> for Marks {}