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