use color_eyre::Result;
use crate::cache::CacheManager;
pub fn push_entry(entries: &mut Vec<String>, entry: String) {
if entries.last() == Some(&entry) {
return;
}
entries.push(entry);
}
const LINE_BREAK_ON_DISK: char = '\u{2028}';
fn to_disk(entry: &str) -> String {
entry.replace('\n', &LINE_BREAK_ON_DISK.to_string())
}
fn from_disk(entry: String) -> String {
if entry.contains(LINE_BREAK_ON_DISK) {
entry.replace(LINE_BREAK_ON_DISK, "\n")
} else {
entry
}
}
fn trim(entries: &mut Vec<String>, limit: usize) {
let excess = entries.len().saturating_sub(limit);
entries.drain(..excess);
}
#[derive(Debug, Clone, Default)]
pub(super) struct InputHistory {
pub id: Option<String>,
entries: Vec<String>,
index: Option<usize>,
stash: Option<String>,
pub limit: usize,
loaded: bool,
}
impl InputHistory {
pub fn new(limit: usize) -> Self {
Self {
limit,
..Self::default()
}
}
pub fn is_enabled(&self) -> bool {
self.id.is_some()
}
#[cfg(test)]
pub fn entries(&self) -> &[String] {
&self.entries
}
pub fn ensure_loaded(&mut self, cache: &CacheManager) -> Result<()> {
if self.loaded {
return Ok(());
}
if let Some(id) = &self.id {
self.entries = cache
.load_history_file(id)?
.into_iter()
.map(from_disk)
.collect();
self.loaded = true;
}
Ok(())
}
pub fn remember(&mut self, value: &str, cache: &CacheManager) -> Result<()> {
let Some(id) = self.id.clone() else {
return Ok(());
};
if value.is_empty() {
return Ok(());
}
push_entry(&mut self.entries, value.to_string());
trim(&mut self.entries, self.limit);
let entry = to_disk(value);
let limit = self.limit;
cache.update_history_file(&id, move |entries| {
push_entry(entries, entry);
trim(entries, limit);
})?;
Ok(())
}
#[cfg(test)]
pub fn seed(&mut self, entry: String) {
self.loaded = true;
push_entry(&mut self.entries, entry);
}
pub fn reset_position(&mut self) {
self.index = None;
self.stash = None;
}
pub fn older(&mut self, current: &str, cache: Option<&CacheManager>) -> Option<String> {
self.id.as_ref()?;
if !self.loaded {
let cache = cache?;
self.ensure_loaded(cache)
.inspect_err(|e| log::warn!(target: "datui", "read input history: {e:#}"))
.ok()?;
}
if self.entries.is_empty() {
return None;
}
if self.index.is_none() {
self.stash = Some(current.to_string());
}
let index = match self.index {
Some(current) => current.saturating_sub(1),
None => self.entries.len() - 1,
};
self.index = Some(index);
self.entries.get(index).cloned()
}
pub fn newer(&mut self) -> Option<String> {
self.id.as_ref()?;
let index = self.index?;
if index + 1 >= self.entries.len() {
let stashed = self.stash.take();
self.index = None;
return stashed;
}
self.index = Some(index + 1);
self.entries.get(index + 1).cloned()
}
}
#[cfg(test)]
mod tests;