1use std::io::{self, Write};
6
7pub struct InputComposer {
10 history: Vec<String>,
12 history_pos: Option<usize>,
14 commands: Vec<String>,
16 #[allow(dead_code)]
18 buffer: Vec<String>,
19 #[allow(dead_code)]
21 cursor_row: usize,
22 #[allow(dead_code)]
23 cursor_col: usize,
24 prompt: String,
26 cont_prompt: String,
28}
29
30impl InputComposer {
31 pub fn new() -> Self {
33 Self {
34 history: Vec::new(),
35 history_pos: None,
36 commands: vec![
37 "/help".into(),
38 "/plan".into(),
39 "/run".into(),
40 "/chat".into(),
41 "/clear".into(),
42 "/history".into(),
43 "/save".into(),
44 "/load".into(),
45 "/exit".into(),
46 "/quit".into(),
47 ],
48 buffer: vec![String::new()],
49 cursor_row: 0,
50 cursor_col: 0,
51 prompt: "> ".into(),
52 cont_prompt: ". ".into(),
53 }
54 }
55
56 pub fn add_commands(&mut self, cmds: &[&str]) {
58 for cmd in cmds {
59 self.commands.push(cmd.to_string());
60 }
61 }
62
63 pub fn add_history(&mut self, line: &str) {
65 if !line.trim().is_empty() {
66 self.history.insert(0, line.to_string());
67 if self.history.len() > 1000 {
68 self.history.pop();
69 }
70 }
71 self.history_pos = None;
72 }
73
74 pub fn read_input(&mut self) -> anyhow::Result<String> {
76 let mut lines: Vec<String> = Vec::new();
77 let mut first = true;
78
79 loop {
80 let prompt = if first {
81 &self.prompt
82 } else {
83 &self.cont_prompt
84 };
85 print!("{prompt}");
86 io::stdout().flush()?;
87
88 let mut line = String::new();
89 let bytes = io::stdin().read_line(&mut line)?;
90
91 if bytes == 0 {
92 break;
94 }
95
96 let trimmed = line.trim_end_matches('\n').trim_end_matches('\r');
97
98 if first && trimmed.is_empty() {
99 return Ok(String::new());
101 }
102
103 if !first && trimmed.is_empty() {
104 break;
106 }
107
108 let text = trimmed.to_string();
109 if first {
110 self.add_history(&text);
111 }
112 lines.push(text);
113 first = false;
114 }
115
116 Ok(lines.join("\n"))
117 }
118
119 pub fn autocomplete(&self, partial: &str) -> Vec<String> {
121 if !partial.starts_with('/') {
122 return Vec::new();
123 }
124
125 let lower = partial.to_lowercase();
126 self.commands
127 .iter()
128 .filter(|cmd| cmd.to_lowercase().starts_with(&lower))
129 .cloned()
130 .collect()
131 }
132
133 pub fn history_up(&mut self) -> Option<&str> {
135 if self.history.is_empty() {
136 return None;
137 }
138 let pos = self
139 .history_pos
140 .map_or(0, |p| (p + 1).min(self.history.len() - 1));
141 self.history_pos = Some(pos);
142 Some(&self.history[pos])
143 }
144
145 pub fn history_down(&mut self) -> Option<&str> {
147 match self.history_pos {
148 Some(0) | None => {
149 self.history_pos = None;
150 None
151 }
152 Some(p) => {
153 self.history_pos = Some(p - 1);
154 Some(&self.history[p - 1])
155 }
156 }
157 }
158
159 pub fn get_history(&self) -> &[String] {
161 &self.history
162 }
163}
164
165impl Default for InputComposer {
166 fn default() -> Self {
167 Self::new()
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 #[test]
176 fn test_autocomplete() {
177 let composer = InputComposer::new();
178 let matches = composer.autocomplete("/he");
179 assert!(matches.contains(&"/help".to_string()));
180 }
181
182 #[test]
183 fn test_history() {
184 let mut composer = InputComposer::new();
185 composer.add_history("hello");
186 composer.add_history("world");
187
188 let up = composer.history_up();
190 assert_eq!(up, Some("world"));
191
192 let up2 = composer.history_up();
194 assert_eq!(up2, Some("hello"));
195
196 let down = composer.history_down();
198 assert_eq!(down, Some("world"));
199
200 let none = composer.history_down();
202 assert_eq!(none, None);
203 }
204}