Skip to main content

atomcode_core/
input_history.rs

1//! Cross-session input history for the TUI input box (↑/↓ recall).
2//!
3//! Stored as one line per submitted user input in `$ATOMCODE_HOME/input_history.txt`,
4//! append-only, capped at `MAX_ENTRIES`. Unlike conversation messages, entries
5//! are plain strings with no role/tool structure — this is purely a UX aid for
6//! recalling past text, not for restoring conversation context.
7//!
8//! Multi-line inputs are encoded by escaping `\\` → `\\\\` and `\n` → `\\n` so
9//! each entry occupies exactly one line on disk.
10
11use 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    /// Load all entries in order (oldest first, newest last).
24    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    /// Append a new entry, trimming the file if it exceeds `MAX_ENTRIES`.
36    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        // Enforce cap: if we've grown past MAX_ENTRIES, rewrite with tail.
59        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}