use std::io::{self, Write};
pub struct InputComposer {
history: Vec<String>,
history_pos: Option<usize>,
commands: Vec<String>,
buffer: Vec<String>,
cursor_row: usize,
cursor_col: usize,
prompt: String,
cont_prompt: String,
}
impl InputComposer {
pub fn new() -> Self {
Self {
history: Vec::new(),
history_pos: None,
commands: vec![
"/help".into(),
"/plan".into(),
"/run".into(),
"/chat".into(),
"/clear".into(),
"/history".into(),
"/save".into(),
"/load".into(),
"/exit".into(),
"/quit".into(),
],
buffer: vec![String::new()],
cursor_row: 0,
cursor_col: 0,
prompt: "> ".into(),
cont_prompt: ". ".into(),
}
}
pub fn add_commands(&mut self, cmds: &[&str]) {
for cmd in cmds {
self.commands.push(cmd.to_string());
}
}
pub fn add_history(&mut self, line: &str) {
if !line.trim().is_empty() {
self.history.insert(0, line.to_string());
if self.history.len() > 1000 {
self.history.pop();
}
}
self.history_pos = None;
}
pub fn read_input(&mut self) -> anyhow::Result<String> {
let mut lines: Vec<String> = Vec::new();
let mut first = true;
loop {
let prompt = if first { &self.prompt } else { &self.cont_prompt };
print!("{prompt}");
io::stdout().flush()?;
let mut line = String::new();
let bytes = io::stdin().read_line(&mut line)?;
if bytes == 0 {
break;
}
let trimmed = line.trim_end_matches('\n').trim_end_matches('\r');
if first && trimmed.is_empty() {
return Ok(String::new());
}
if !first && trimmed.is_empty() {
break;
}
let text = trimmed.to_string();
if first {
self.add_history(&text);
}
lines.push(text);
first = false;
}
Ok(lines.join("\n"))
}
pub fn autocomplete(&self, partial: &str) -> Vec<String> {
if !partial.starts_with('/') {
return Vec::new();
}
let lower = partial.to_lowercase();
self.commands
.iter()
.filter(|cmd| cmd.to_lowercase().starts_with(&lower))
.cloned()
.collect()
}
pub fn history_up(&mut self) -> Option<&str> {
if self.history.is_empty() {
return None;
}
let pos = self.history_pos.map_or(0, |p| (p + 1).min(self.history.len() - 1));
self.history_pos = Some(pos);
Some(&self.history[pos])
}
pub fn history_down(&mut self) -> Option<&str> {
match self.history_pos {
Some(0) | None => {
self.history_pos = None;
None
}
Some(p) => {
self.history_pos = Some(p - 1);
Some(&self.history[p - 1])
}
}
}
pub fn get_history(&self) -> &[String] {
&self.history
}
}
impl Default for InputComposer {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_autocomplete() {
let composer = InputComposer::new();
let matches = composer.autocomplete("/he");
assert!(matches.contains(&"/help".to_string()));
}
#[test]
fn test_history() {
let mut composer = InputComposer::new();
composer.add_history("hello");
composer.add_history("world");
let up = composer.history_up();
assert_eq!(up, Some("world"));
let down = composer.history_down();
assert_eq!(down, Some("hello"));
}
}