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}