limit_cli/tui/input/
history.rs1use bincode::{deserialize, serialize};
6use std::fs;
7use std::path::PathBuf;
8
9const MAX_HISTORY_SIZE: usize = 500;
11
12#[derive(Debug, Clone)]
14pub struct InputHistory {
15 entries: Vec<String>,
17 max_size: usize,
19 current_index: Option<usize>,
21 saved_draft: Option<String>,
23}
24
25impl InputHistory {
26 pub fn new() -> Self {
28 Self {
29 entries: Vec::with_capacity(100),
30 max_size: MAX_HISTORY_SIZE,
31 current_index: None,
32 saved_draft: None,
33 }
34 }
35
36 pub fn with_max_size(max_size: usize) -> Self {
38 Self {
39 entries: Vec::with_capacity(100),
40 max_size,
41 current_index: None,
42 saved_draft: None,
43 }
44 }
45
46 pub fn add(&mut self, text: &str) {
52 let text = text.trim();
53
54 if text.is_empty() {
56 return;
57 }
58
59 if self.entries.first().map(|s| s.as_str()) == Some(text) {
61 return;
62 }
63
64 self.entries.insert(0, text.to_string());
66
67 if self.entries.len() > self.max_size {
69 self.entries.truncate(self.max_size);
70 }
71
72 self.current_index = None;
74 self.saved_draft = None;
75 }
76
77 pub fn navigate_up(&mut self, current_draft: &str) -> Option<&str> {
82 if self.entries.is_empty() {
83 return None;
84 }
85
86 if self.current_index.is_none() {
88 self.saved_draft = if current_draft.is_empty() {
89 None
90 } else {
91 Some(current_draft.to_string())
92 };
93 }
94
95 match self.current_index {
97 None => {
98 self.current_index = Some(0);
99 self.entries.first().map(|s| s.as_str())
100 }
101 Some(idx) if idx + 1 < self.entries.len() => {
102 self.current_index = Some(idx + 1);
103 self.entries.get(idx + 1).map(|s| s.as_str())
104 }
105 Some(_) => {
106 self.current()
108 }
109 }
110 }
111
112 pub fn navigate_down(&mut self) -> Option<&str> {
116 match self.current_index {
117 None => {
118 None
120 }
121 Some(0) => {
122 self.current_index = None;
124 None }
126 Some(idx) => {
127 self.current_index = Some(idx - 1);
128 self.current()
129 }
130 }
131 }
132
133 pub fn current(&self) -> Option<&str> {
135 self.current_index
136 .and_then(|idx| self.entries.get(idx).map(|s| s.as_str()))
137 }
138
139 pub fn is_navigating(&self) -> bool {
141 self.current_index.is_some()
142 }
143
144 pub fn saved_draft(&self) -> Option<&str> {
146 self.saved_draft.as_deref()
147 }
148
149 pub fn reset_navigation(&mut self) {
151 self.current_index = None;
152 self.saved_draft = None;
153 }
154
155 pub fn len(&self) -> usize {
157 self.entries.len()
158 }
159
160 pub fn is_empty(&self) -> bool {
162 self.entries.is_empty()
163 }
164
165 pub fn entries(&self) -> &[String] {
167 &self.entries
168 }
169
170 pub fn load(path: &PathBuf) -> Result<Self, String> {
172 if !path.exists() {
173 return Ok(Self::new());
174 }
175
176 let data = fs::read(path).map_err(|e| format!("Failed to read history file: {}", e))?;
177
178 let entries: Vec<String> =
179 deserialize(&data).map_err(|e| format!("Failed to deserialize history: {}", e))?;
180
181 Ok(Self {
182 entries,
183 max_size: MAX_HISTORY_SIZE,
184 current_index: None,
185 saved_draft: None,
186 })
187 }
188
189 pub fn save(&self, path: &PathBuf) -> Result<(), String> {
191 if let Some(parent) = path.parent() {
193 fs::create_dir_all(parent)
194 .map_err(|e| format!("Failed to create history directory: {}", e))?;
195 }
196
197 let serialized =
198 serialize(&self.entries).map_err(|e| format!("Failed to serialize history: {}", e))?;
199
200 fs::write(path, serialized).map_err(|e| format!("Failed to write history file: {}", e))?;
201
202 Ok(())
203 }
204
205 pub fn clear(&mut self) {
207 self.entries.clear();
208 self.current_index = None;
209 self.saved_draft = None;
210 }
211}
212
213impl Default for InputHistory {
214 fn default() -> Self {
215 Self::new()
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222 use tempfile::tempdir;
223
224 #[test]
225 fn test_new_history_is_empty() {
226 let history = InputHistory::new();
227 assert!(history.is_empty());
228 assert_eq!(history.len(), 0);
229 assert!(!history.is_navigating());
230 }
231
232 #[test]
233 fn test_add_entry() {
234 let mut history = InputHistory::new();
235 history.add("hello");
236
237 assert_eq!(history.len(), 1);
238 assert_eq!(history.entries()[0], "hello");
239 }
240
241 #[test]
242 fn test_add_multiple_entries() {
243 let mut history = InputHistory::new();
244 history.add("first");
245 history.add("second");
246 history.add("third");
247
248 assert_eq!(history.len(), 3);
249 assert_eq!(history.entries()[0], "third");
251 assert_eq!(history.entries()[1], "second");
252 assert_eq!(history.entries()[2], "first");
253 }
254
255 #[test]
256 fn test_add_empty_ignored() {
257 let mut history = InputHistory::new();
258 history.add("");
259 history.add(" ");
260
261 assert!(history.is_empty());
262 }
263
264 #[test]
265 fn test_add_duplicate_most_recent_ignored() {
266 let mut history = InputHistory::new();
267 history.add("hello");
268 history.add("hello");
269
270 assert_eq!(history.len(), 1);
271 }
272
273 #[test]
274 fn test_add_duplicate_older_allowed() {
275 let mut history = InputHistory::new();
276 history.add("hello");
277 history.add("world");
278 history.add("hello"); assert_eq!(history.len(), 3);
281 assert_eq!(history.entries()[0], "hello"); assert_eq!(history.entries()[1], "world"); assert_eq!(history.entries()[2], "hello"); }
285
286 #[test]
287 fn test_max_size_truncation() {
288 let mut history = InputHistory::with_max_size(3);
289 history.add("first");
290 history.add("second");
291 history.add("third");
292 history.add("fourth");
293
294 assert_eq!(history.len(), 3);
295 assert_eq!(history.entries()[0], "fourth");
296 assert_eq!(history.entries()[2], "second");
297 }
298
299 #[test]
300 fn test_navigate_up_empty_history() {
301 let mut history = InputHistory::new();
302 let result = history.navigate_up("draft");
303
304 assert!(result.is_none());
305 assert!(!history.is_navigating());
306 }
307
308 #[test]
309 fn test_navigate_up_saves_draft() {
310 let mut history = InputHistory::new();
311 history.add("entry");
312
313 history.navigate_up("my draft");
314
315 assert!(history.is_navigating());
316 assert_eq!(history.saved_draft(), Some("my draft"));
317 }
318
319 #[test]
320 fn test_navigate_up_empty_draft_not_saved() {
321 let mut history = InputHistory::new();
322 history.add("entry");
323
324 history.navigate_up("");
325
326 assert!(history.is_navigating());
327 assert!(history.saved_draft().is_none());
328 }
329
330 #[test]
331 fn test_navigate_up_multiple() {
332 let mut history = InputHistory::new();
333 history.add("oldest");
334 history.add("middle");
335 history.add("newest");
336
337 let first = history.navigate_up("");
338 assert_eq!(first, Some("newest"));
339 assert_eq!(history.current(), Some("newest"));
340
341 let second = history.navigate_up("");
342 assert_eq!(second, Some("middle"));
343
344 let third = history.navigate_up("");
345 assert_eq!(third, Some("oldest"));
346
347 let fourth = history.navigate_up("");
349 assert_eq!(fourth, Some("oldest"));
350 }
351
352 #[test]
353 fn test_navigate_down_from_oldest() {
354 let mut history = InputHistory::new();
355 history.add("oldest");
356 history.add("newest");
357
358 history.navigate_up("");
360 history.navigate_up("");
361 assert_eq!(history.current(), Some("oldest"));
362
363 let result = history.navigate_down();
365 assert_eq!(result, Some("newest"));
366
367 let result = history.navigate_down();
369 assert!(result.is_none());
370 assert!(!history.is_navigating());
371 }
372
373 #[test]
374 fn test_navigate_down_without_navigation() {
375 let mut history = InputHistory::new();
376 history.add("entry");
377
378 let result = history.navigate_down();
380 assert!(result.is_none());
381 assert!(!history.is_navigating());
382 }
383
384 #[test]
385 fn test_reset_navigation() {
386 let mut history = InputHistory::new();
387 history.add("entry");
388
389 history.navigate_up("draft");
390 assert!(history.is_navigating());
391
392 history.reset_navigation();
393 assert!(!history.is_navigating());
394 assert!(history.saved_draft().is_none());
395 }
396
397 #[test]
398 fn test_add_resets_navigation() {
399 let mut history = InputHistory::new();
400 history.add("first");
401
402 history.navigate_up("draft");
403 assert!(history.is_navigating());
404
405 history.add("second");
406 assert!(!history.is_navigating());
407 assert!(history.saved_draft().is_none());
408 }
409
410 #[test]
411 fn test_roundtrip_save_load() {
412 let dir = tempdir().unwrap();
413 let path = dir.path().join("history.bin");
414
415 let mut history = InputHistory::new();
416 history.add("first");
417 history.add("second");
418 history.add("third");
419
420 history.save(&path).unwrap();
421
422 let loaded = InputHistory::load(&path).unwrap();
423 assert_eq!(loaded.len(), 3);
424 assert_eq!(loaded.entries()[0], "third");
425 assert_eq!(loaded.entries()[1], "second");
426 assert_eq!(loaded.entries()[2], "first");
427 }
428
429 #[test]
430 fn test_load_missing_file_returns_empty() {
431 let dir = tempdir().unwrap();
432 let path = dir.path().join("nonexistent.bin");
433
434 let history = InputHistory::load(&path).unwrap();
435 assert!(history.is_empty());
436 }
437
438 #[test]
439 fn test_save_creates_parent_directory() {
440 let dir = tempdir().unwrap();
441 let path = dir.path().join("nested").join("dir").join("history.bin");
442
443 let mut history = InputHistory::new();
444 history.add("test");
445
446 history.save(&path).unwrap();
447 assert!(path.exists());
448 }
449
450 #[test]
451 fn test_clear() {
452 let mut history = InputHistory::new();
453 history.add("entry");
454 history.navigate_up("draft");
455
456 history.clear();
457
458 assert!(history.is_empty());
459 assert!(!history.is_navigating());
460 assert!(history.saved_draft().is_none());
461 }
462
463 #[test]
464 fn test_trim_on_add() {
465 let mut history = InputHistory::new();
466 history.add(" hello world ");
467
468 assert_eq!(history.entries()[0], "hello world");
469 }
470
471 #[test]
472 fn test_navigate_up_down_cycle() {
473 let mut history = InputHistory::new();
474 history.add("oldest");
475 history.add("middle");
476 history.add("newest");
477
478 history.navigate_up("my draft");
480
481 history.navigate_up("");
483 history.navigate_up("");
484 assert_eq!(history.current(), Some("oldest"));
485
486 history.navigate_down();
488 assert_eq!(history.current(), Some("middle"));
489
490 history.navigate_down();
491 assert_eq!(history.current(), Some("newest"));
492
493 let result = history.navigate_down();
495 assert!(result.is_none());
496 assert_eq!(history.saved_draft(), Some("my draft"));
497 }
498}