use std::path::Path;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PromptHistory {
entries: Vec<String>,
}
impl PromptHistory {
pub fn new() -> Self {
PromptHistory::default()
}
pub fn load_from_file(path: &Path) -> Self {
let Ok(text) = std::fs::read_to_string(path) else {
return PromptHistory::new();
};
let entries = text
.lines()
.filter(|l| !l.is_empty())
.map(unescape_entry)
.collect();
PromptHistory { entries }
}
pub fn save_to_file(&self, path: &Path) {
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let text: String = self
.entries
.iter()
.map(|e| escape_entry(e))
.collect::<Vec<_>>()
.join("\n");
let _ = std::fs::write(path, text);
}
pub fn push(&mut self, entry: impl Into<String>) {
let entry = entry.into();
if entry.is_empty() {
return;
}
if self.entries.last().map(String::as_str) != Some(entry.as_str()) {
self.entries.push(entry);
}
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn search(&self, query: &str) -> Vec<&str> {
let q = query.to_ascii_lowercase();
self.entries
.iter()
.rev()
.filter(|e| q.is_empty() || e.to_ascii_lowercase().contains(&q))
.map(String::as_str)
.collect()
}
}
fn escape_entry(s: &str) -> String {
s.replace('\\', "\\\\").replace('\n', "\\n")
}
fn unescape_entry(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut chars = s.chars();
while let Some(c) = chars.next() {
if c == '\\' {
match chars.next() {
Some('n') => out.push('\n'),
Some('\\') => out.push('\\'),
Some(other) => {
out.push('\\');
out.push(other);
}
None => out.push('\\'),
}
} else {
out.push(c);
}
}
out
}