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#[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 #[must_use]
25 pub fn is_empty(&self) -> bool {
26 self.content.is_empty()
27 }
28
29 #[must_use]
31 pub fn len(&self) -> usize {
32 self.content.len()
33 }
34
35 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 #[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 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 #[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 pub fn char_for(&self, path: &Path) -> &char {
205 self.paths_to_mark.get(path).unwrap_or(&' ')
206 }
207
208 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 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 {}