1use std::fmt::{Display, Formatter};
2use std::fs::{File, OpenOptions};
3use std::io::{Error as IoError, Write};
4use std::path::{Path, PathBuf};
5
6use clap::Parser;
7
8use anyhow::{bail, Context, Result};
9
10use crate::common::{read_lines, tilde};
11use crate::io::Args;
12use crate::modes::{InputCompleted, InputSimple, Menu};
13
14pub struct InputHistory {
18 file_path: PathBuf,
19 content: Vec<HistoryElement>,
20 filtered: Vec<HistoryElement>,
21 index: Option<usize>,
22 log_are_enabled: bool,
23}
24
25impl InputHistory {
26 pub fn load(path: &str) -> Result<Self> {
27 let file_path = PathBuf::from(tilde(path).to_string());
28 Ok(Self {
29 content: Self::load_content(&file_path)?,
30 file_path,
31 filtered: vec![],
32 index: None,
33 log_are_enabled: Args::parse().log,
34 })
35 }
36
37 fn load_content(path: &Path) -> Result<Vec<HistoryElement>> {
38 if !Path::new(&path).exists() {
39 File::create(path)?;
40 }
41 Ok(read_lines(path)?
42 .map(HistoryElement::from_str)
43 .filter_map(|line| line.ok())
44 .collect())
45 }
46
47 fn write_elem(&self, elem: &HistoryElement) -> Result<()> {
48 let mut hist_file = OpenOptions::new().append(true).open(&self.file_path)?;
49 hist_file.write_all(elem.to_string().as_bytes())?;
50 Ok(())
51 }
52
53 pub fn filter_by_mode(&mut self, menu_mode: Menu) {
54 let Some(kind) = HistoryKind::from_mode(menu_mode) else {
55 return;
56 };
57 self.index = None;
58 self.filtered = self
59 .content
60 .iter()
61 .filter(|elem| elem.kind == kind)
62 .map(|elem| elem.to_owned())
63 .collect();
64 crate::log_info!(
65 "input history filtered for {menu_mode} has {len} elts",
66 len = self.filtered.len()
67 );
68 }
69
70 pub fn prev(&mut self) {
71 if self.filtered.is_empty() {
72 return;
73 }
74 if self.index.is_none() {
75 self.index = Some(0);
76 } else {
77 self.index = self.index.map(|index| (index + 1) % self.filtered.len());
78 }
79 }
80
81 pub fn next(&mut self) {
82 if self.filtered.is_empty() {
83 return;
84 }
85 if self.index.is_none() {
86 self.index = Some(self.filtered.len().saturating_sub(1));
87 } else {
88 self.index = self.index.map(|index| {
89 if index > 0 {
90 index - 1
91 } else {
92 self.filtered.len() - 1
93 }
94 })
95 }
96 }
97
98 pub fn current(&self) -> Option<&HistoryElement> {
99 match self.index {
100 None => None,
101 Some(index) => self.filtered.get(index),
102 }
103 }
104
105 pub fn update(&mut self, mode: Menu, input_string: &str) -> Result<()> {
107 let Some(elem) = HistoryElement::from_mode_input_string(mode, input_string) else {
108 return Ok(());
109 };
110 if let Some(last) = self.filtered.last() {
111 if *last == elem {
112 return Ok(());
113 }
114 }
115 if self.log_are_enabled {
116 self.write_elem(&elem)?;
117 }
118 self.content.push(elem);
119 Ok(())
120 }
121
122 pub fn is_mode_logged(&self, mode: &Menu) -> bool {
126 !matches!(
127 mode,
128 Menu::Navigate(_)
129 | Menu::InputSimple(InputSimple::Password(_, _))
130 | Menu::InputSimple(InputSimple::CloudNewdir)
131 | Menu::NeedConfirmation(_)
132 )
133 }
134
135 pub fn filtered_as_list(&self) -> Vec<String> {
136 self.filtered
137 .iter()
138 .map(|elt| elt.content.clone())
139 .collect()
140 }
141
142 pub fn filtered_is_empty(&self) -> bool {
143 self.filtered.is_empty()
144 }
145}
146
147#[derive(Clone, PartialEq, Eq)]
150pub enum HistoryKind {
151 InputSimple(InputSimple),
152 InputCompleted(InputCompleted),
153}
154
155impl Display for HistoryKind {
156 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157 let menu = match self {
158 Self::InputCompleted(input_completed) => match input_completed {
159 InputCompleted::Cd => "Cd",
160 InputCompleted::Search => "Search",
161 InputCompleted::Exec => "Exec",
162 InputCompleted::Action => "Action",
163 },
164 Self::InputSimple(input_simple) => match input_simple {
165 InputSimple::Rename => "Rename",
166 InputSimple::Chmod => "Chmod",
167 InputSimple::Newfile => "Newfile",
168 InputSimple::Newdir => "Newdir",
169 InputSimple::RegexMatch => "RegexMatch",
170 InputSimple::Sort => "Sort",
171 InputSimple::Filter => "Filter",
172 InputSimple::SetNvimAddr => "SetNvimAddr",
173 InputSimple::ShellCommand => "ShellCommand",
174 InputSimple::Remote => "Remote",
175 InputSimple::CloudNewdir => "xxx",
176 InputSimple::Password(_, _) => "xxx",
177 },
178 };
179 write!(f, "{menu}")
180 }
181}
182
183impl HistoryKind {
184 fn from_string(kind: &String) -> Result<Self> {
185 Ok(match kind.as_ref() {
186 "Cd" => Self::InputCompleted(InputCompleted::Cd),
187 "Search" => Self::InputCompleted(InputCompleted::Search),
188 "Exec" => Self::InputCompleted(InputCompleted::Exec),
189 "Action" => Self::InputCompleted(InputCompleted::Action),
190
191 "Shell" => Self::InputSimple(InputSimple::ShellCommand),
192 "Chmod" => Self::InputSimple(InputSimple::Chmod),
193 "Sort" => Self::InputSimple(InputSimple::Sort),
194 "Rename" => Self::InputSimple(InputSimple::Rename),
195 "Newfile" => Self::InputSimple(InputSimple::Newfile),
196 "Newdir" => Self::InputSimple(InputSimple::Newdir),
197 "RegexMatch" => Self::InputSimple(InputSimple::RegexMatch),
198 "Filter" => Self::InputSimple(InputSimple::Filter),
199 "SetNvimAddr" => Self::InputSimple(InputSimple::SetNvimAddr),
200 "Remote" => Self::InputSimple(InputSimple::Remote),
201
202 _ => bail!("{kind} isn't a valid HistoryKind"),
203 })
204 }
205
206 fn from_mode(menu_mode: Menu) -> Option<Self> {
207 match menu_mode {
208 Menu::InputSimple(InputSimple::Password(_, _) | InputSimple::CloudNewdir) => None,
209 Menu::InputSimple(input_simple) => Some(Self::InputSimple(input_simple)),
210 Menu::InputCompleted(input_completed) => Some(Self::InputCompleted(input_completed)),
211 _ => None,
212 }
213 }
214}
215
216#[derive(Clone, Eq, PartialEq)]
220pub struct HistoryElement {
221 kind: HistoryKind,
222 content: String,
223}
224
225impl Display for HistoryElement {
226 fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
227 writeln!(
228 f,
229 "{kind} - {content}",
230 kind = self.kind,
231 content = self.content
232 )
233 }
234}
235
236impl HistoryElement {
237 fn split_kind_content(line: Result<String, IoError>) -> Result<(String, String)> {
238 let line = line?.to_owned();
239 let (mut kind, mut content) = line
240 .split_once('-')
241 .context("no delimiter '-' found in line")?;
242 kind = kind.trim();
243 content = content.trim();
244 Ok((kind.to_owned(), content.to_owned()))
245 }
246
247 pub fn from_mode_input_string(mode: Menu, input_string: &str) -> Option<Self> {
248 let kind = HistoryKind::from_mode(mode)?;
249 Some(Self {
250 kind,
251 content: input_string.to_owned(),
252 })
253 }
254
255 fn from_str(line: Result<String, IoError>) -> Result<Self> {
256 let (kind, content) = Self::split_kind_content(line)?;
257 if content.is_empty() {
258 bail!("empty line")
259 } else {
260 Ok(Self {
261 kind: HistoryKind::from_string(&kind)?,
262 content: content.to_owned(),
263 })
264 }
265 }
266
267 pub fn content(&self) -> &str {
268 &self.content
269 }
270}