atomcode_core/
input_history.rs1use std::io::Write;
12use std::path::PathBuf;
13
14const MAX_ENTRIES: usize = 1000;
15
16pub struct InputHistory;
17
18impl InputHistory {
19 pub fn path() -> PathBuf {
20 crate::config::Config::config_dir().join("input_history.txt")
21 }
22
23 pub fn load() -> Vec<String> {
25 let data = match std::fs::read_to_string(Self::path()) {
26 Ok(d) => d,
27 Err(_) => return Vec::new(),
28 };
29 data.lines()
30 .filter(|l| !l.is_empty())
31 .map(decode_line)
32 .collect()
33 }
34
35 pub fn append(entry: &str) {
37 if entry.trim().is_empty() {
38 return;
39 }
40 let path = Self::path();
41 if let Some(parent) = path.parent() {
42 let _ = std::fs::create_dir_all(parent);
43 }
44
45 let mut line = encode_line(entry);
46 line.push('\n');
47
48 let append_ok = std::fs::OpenOptions::new()
49 .create(true)
50 .append(true)
51 .open(&path)
52 .and_then(|mut f| f.write_all(line.as_bytes()))
53 .is_ok();
54 if !append_ok {
55 return;
56 }
57
58 if let Ok(content) = std::fs::read_to_string(&path) {
60 let lines: Vec<&str> = content.lines().collect();
61 if lines.len() > MAX_ENTRIES {
62 let keep = &lines[lines.len() - MAX_ENTRIES..];
63 let mut new_content = keep.join("\n");
64 new_content.push('\n');
65 let tmp = path.with_extension("txt.tmp");
66 if std::fs::write(&tmp, new_content).is_ok() {
67 let _ = std::fs::rename(&tmp, &path);
68 }
69 }
70 }
71 }
72}
73
74fn encode_line(s: &str) -> String {
75 let mut out = String::with_capacity(s.len());
76 for c in s.chars() {
77 match c {
78 '\\' => out.push_str("\\\\"),
79 '\n' => out.push_str("\\n"),
80 '\r' => {}
81 _ => out.push(c),
82 }
83 }
84 out
85}
86
87fn decode_line(s: &str) -> String {
88 let mut out = String::with_capacity(s.len());
89 let mut chars = s.chars();
90 while let Some(c) = chars.next() {
91 if c == '\\' {
92 match chars.next() {
93 Some('n') => out.push('\n'),
94 Some('\\') => out.push('\\'),
95 Some(other) => {
96 out.push('\\');
97 out.push(other);
98 }
99 None => out.push('\\'),
100 }
101 } else {
102 out.push(c);
103 }
104 }
105 out
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 #[test]
113 fn roundtrip_simple() {
114 assert_eq!(decode_line(&encode_line("hello")), "hello");
115 }
116
117 #[test]
118 fn roundtrip_multiline() {
119 let s = "line1\nline2\nline3";
120 assert_eq!(decode_line(&encode_line(s)), s);
121 }
122
123 #[test]
124 fn roundtrip_with_backslashes() {
125 let s = "path\\to\\file and a \n newline";
126 assert_eq!(decode_line(&encode_line(s)), s);
127 }
128
129 #[test]
130 fn encode_strips_cr() {
131 assert_eq!(encode_line("a\r\nb"), "a\\nb");
132 }
133}