datui_lib/widgets/text_input/
history.rs1use color_eyre::Result;
7
8use crate::cache::CacheManager;
9
10pub fn push_entry(entries: &mut Vec<String>, entry: String) {
13 if entries.last() == Some(&entry) {
14 return;
15 }
16 entries.push(entry);
17}
18
19const LINE_BREAK_ON_DISK: char = '\u{2028}';
23
24fn to_disk(entry: &str) -> String {
25 entry.replace('\n', &LINE_BREAK_ON_DISK.to_string())
26}
27
28fn from_disk(entry: String) -> String {
29 if entry.contains(LINE_BREAK_ON_DISK) {
30 entry.replace(LINE_BREAK_ON_DISK, "\n")
31 } else {
32 entry
33 }
34}
35
36fn trim(entries: &mut Vec<String>, limit: usize) {
38 let excess = entries.len().saturating_sub(limit);
39 entries.drain(..excess);
40}
41
42#[derive(Debug, Clone, Default)]
45pub(super) struct InputHistory {
46 pub id: Option<String>,
47 entries: Vec<String>,
48 index: Option<usize>,
50 stash: Option<String>,
52 pub limit: usize,
53 loaded: bool,
54}
55
56impl InputHistory {
57 pub fn new(limit: usize) -> Self {
58 Self {
59 limit,
60 ..Self::default()
61 }
62 }
63
64 pub fn is_enabled(&self) -> bool {
65 self.id.is_some()
66 }
67
68 #[cfg(test)]
69 pub fn entries(&self) -> &[String] {
70 &self.entries
71 }
72
73 pub fn ensure_loaded(&mut self, cache: &CacheManager) -> Result<()> {
75 if self.loaded {
76 return Ok(());
77 }
78 if let Some(id) = &self.id {
79 self.entries = cache
80 .load_history_file(id)?
81 .into_iter()
82 .map(from_disk)
83 .collect();
84 self.loaded = true;
85 }
86 Ok(())
87 }
88
89 pub fn remember(&mut self, value: &str, cache: &CacheManager) -> Result<()> {
92 let Some(id) = self.id.clone() else {
93 return Ok(());
94 };
95 if value.is_empty() {
96 return Ok(());
97 }
98 push_entry(&mut self.entries, value.to_string());
99 trim(&mut self.entries, self.limit);
100
101 let entry = to_disk(value);
102 let limit = self.limit;
103 cache.update_history_file(&id, move |entries| {
104 push_entry(entries, entry);
105 trim(entries, limit);
106 })?;
107 Ok(())
108 }
109
110 #[cfg(test)]
112 pub fn seed(&mut self, entry: String) {
113 self.loaded = true;
114 push_entry(&mut self.entries, entry);
115 }
116
117 pub fn reset_position(&mut self) {
119 self.index = None;
120 self.stash = None;
121 }
122
123 pub fn older(&mut self, current: &str, cache: Option<&CacheManager>) -> Option<String> {
126 self.id.as_ref()?;
127 if !self.loaded {
128 let cache = cache?;
129 self.ensure_loaded(cache)
130 .inspect_err(|e| log::warn!(target: "datui", "read input history: {e:#}"))
131 .ok()?;
132 }
133 if self.entries.is_empty() {
134 return None;
135 }
136 if self.index.is_none() {
137 self.stash = Some(current.to_string());
138 }
139 let index = match self.index {
140 Some(current) => current.saturating_sub(1),
141 None => self.entries.len() - 1,
142 };
143 self.index = Some(index);
144 self.entries.get(index).cloned()
145 }
146
147 pub fn newer(&mut self) -> Option<String> {
150 self.id.as_ref()?;
151 let index = self.index?;
152 if index + 1 >= self.entries.len() {
153 let stashed = self.stash.take();
154 self.index = None;
155 return stashed;
156 }
157 self.index = Some(index + 1);
158 self.entries.get(index + 1).cloned()
159 }
160}
161
162#[cfg(test)]
163mod tests;