fm/io/
input_history.rs

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
14/// The whole input history, read and written from and to a file.
15/// It's filtered by content.
16/// If the flag "log_are_enabled" is set to false, it will not be updated in the logs.
17pub 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    /// If logs are disabled, nothing is saved on disk, only during current session
106    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    /// True iff the mode is logged.
123    /// It's almost always the case, only password mode isn't saved.
124    /// This method is usefull to check if an input should be replaced when the user want to.
125    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/// Different kind of histories, depending of the menu_mode.
148/// It has a few methods to record and filter methods from text input.
149#[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/// Simple struct to record what kind of history is related to an input.
217/// Since we record most user inputs, they are messed up.
218/// Navigating in those elements can be confusing if we don't filter them by kind.
219#[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}