Skip to main content

ez_token/cli/history/
file.rs

1use super::{HistoryKey, HistoryManager};
2use crate::config::cli_config::CliConfig;
3use dialoguer::BasicHistory;
4use dialoguer::History;
5use std::fs::{self, OpenOptions};
6use std::io::{BufRead, BufReader, Write};
7use std::path::PathBuf;
8
9/// A [`HistoryManager`] implementation that persists history to disk.
10///
11/// History files are stored alongside the application's configuration file,
12/// named `.{key}_history` (e.g., `.tenant_history`).
13///
14/// Errors during read or write are silently ignored — history is a
15/// best-effort convenience feature and should never interrupt the CLI flow.
16pub struct FileHistoryManager;
17
18impl HistoryManager for FileHistoryManager {
19    /// Loads history from disk for the given [`HistoryKey`].
20    ///
21    /// Returns an empty [`BasicHistory`] if the file does not exist,
22    /// cannot be opened, or contains no valid entries.
23    fn load(&self, key: HistoryKey) -> BasicHistory {
24        let mut history = BasicHistory::new().max_entries(10).no_duplicates(true);
25
26        let Ok(file) = fs::File::open(get_history_path(key)) else {
27            return history;
28        };
29
30        BufReader::new(file)
31            .lines()
32            .map_while(Result::ok)
33            .filter(|line| !line.trim().is_empty())
34            .for_each(|line| {
35                history.write(&line);
36            });
37
38        history
39    }
40
41    /// Saves the current history entries to disk for the given [`HistoryKey`].
42    ///
43    /// Creates parent directories if they do not exist. The file is
44    /// truncated and rewritten on each save. Entries are written in
45    /// chronological order (oldest first).
46    ///
47    /// Silently returns on any I/O error.
48    fn save(&self, key: HistoryKey, history: &BasicHistory) {
49        let path = get_history_path(key);
50
51        if let Some(parent) = path.parent() {
52            let _ = fs::create_dir_all(parent);
53        }
54
55        let Ok(mut file) = OpenOptions::new()
56            .write(true)
57            .create(true)
58            .truncate(true)
59            .open(path)
60        else {
61            return;
62        };
63
64        for entry in collect_entries(history).iter().rev() {
65            let _ = writeln!(file, "{}", entry);
66        }
67    }
68}
69
70/// Returns the filesystem path for the history file associated with `key`.
71///
72/// The file is placed in the same directory as the application config file,
73/// falling back to the current directory if the config path cannot be determined.
74fn get_history_path(key: HistoryKey) -> PathBuf {
75    CliConfig::get_path()
76        .map(|p| p.parent().unwrap().to_path_buf())
77        .unwrap_or_else(|_| PathBuf::from("."))
78        .join(format!(".{}_history", key.as_str()))
79}
80
81/// Collects all entries from a [`BasicHistory`] into a [`Vec<String>`].
82///
83/// Iterates by index until [`History::read`] returns `None`,
84/// indicating no more entries are available.
85fn collect_entries(history: &BasicHistory) -> Vec<String> {
86    (0..)
87        .map(|i| History::<String>::read(history, i))
88        .take_while(Option::is_some)
89        .flatten()
90        .collect()
91}