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#[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 #[must_use]
24 pub fn is_empty(&self) -> bool {
25 self.content.is_empty()
26 }
27
28 #[must_use]
30 pub fn len(&self) -> usize {
31 self.content.len()
32 }
33
34 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 #[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 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 #[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 {}