use libedit::hint::Hint;
use libedit::suggestion::Suggestion;
use libedit::term::supports_color;
use libedit::{
Action, ActionContext, Completer, Completion, EditLine, Error, History, LineContext, Result,
Tokenizer,
};
use std::io::Write;
use std::path::PathBuf;
const COMMANDS: &[(&str, &str)] = &[
("show", "display operational state"),
("set", "configure a parameter"),
("help", "show available commands"),
("history", "show stored history entries"),
("clear", "clear the command history"),
(
"caf\u{00e9}",
"serve a caf\u{00e9} — UTF-8 in commands works",
),
("quit", "exit the CLI"),
("exit", "exit the CLI"),
];
struct CommandCompleter;
impl Completer for CommandCompleter {
fn complete(&mut self, ctx: &LineContext) -> Completion {
if ctx.line()[..ctx.cursor()].contains(char::is_whitespace) {
return Completion::none();
}
let word = ctx.word();
let matches: Vec<String> = COMMANDS
.iter()
.filter(|(name, _)| name.starts_with(word))
.map(|(name, _)| name.to_string())
.collect();
Completion::new(matches)
}
}
fn history_path() -> PathBuf {
let mut p = PathBuf::from(std::env::var("HOME").unwrap_or_else(|_| ".".into()));
p.push(".repl_history");
p
}
fn print_context_help(out: &mut impl Write, prefix: &str) {
let matches: Vec<_> = COMMANDS
.iter()
.filter(|(name, _)| name.starts_with(prefix))
.collect();
if matches.is_empty() {
let _ = writeln!(out, "\n (no completions)");
return;
}
let _ = writeln!(out, "\nPossible completions:");
let max_name = matches.iter().map(|(n, _)| n.len()).max().unwrap_or(0);
for (name, desc) in &matches {
let _ = writeln!(
out,
" \x1b[1;36m{:<width$}\x1b[0m {desc}",
name,
width = max_name
);
}
}
fn main() -> Result<()> {
let mut editor = EditLine::new_with_locale("cli")?;
let mut history = History::with_size(1000);
let mut tokenizer = Tokenizer::new(None)?;
let color = supports_color();
history.set_unique(true);
let hist_path = history_path();
let _ = history.load(&hist_path);
editor.set_history(&mut history)?;
editor.set_completer(CommandCompleter)?;
if color {
editor.set_candidate_styler(|cand: &str, out: &mut String| {
use std::fmt::Write as _;
let _ = write!(out, "\x1b[1;36m{cand}\x1b[0m");
});
}
if color {
editor.set_suggester(|ctx: &LineContext| {
let line = ctx.line();
if line.is_empty() {
return None;
}
let mut iter = COMMANDS
.iter()
.filter(|(name, _)| name.starts_with(line) && name.len() > line.len());
let (name, _) = iter.next()?;
if iter.next().is_some() {
return None;
}
Some(Suggestion::new(&name[line.len()..]))
})?;
editor.set_suggestion_style("\x1b[2m", "\x1b[0m");
}
editor.set_hinter(|ctx: &LineContext| {
let lookup = if ctx.word().is_empty() {
ctx.line().split_ascii_whitespace().next().unwrap_or("")
} else {
ctx.word()
};
if lookup.is_empty() {
return None;
}
let mut iter = COMMANDS.iter().filter(|(name, _)| name.starts_with(lookup));
let first = iter.next()?;
if iter.next().is_some() {
return None;
}
Some(Hint::new(format!("-- {}", first.1)))
});
let help_action = editor.add_action("context-help", |ctx: &ActionContext| {
let mut out = ctx.output();
print_context_help(&mut out, ctx.word());
out.flush().unwrap();
Action::Redisplay
})?;
editor.bind_key("?", &help_action)?;
editor.set_signal_handling(true)?;
if color {
println!(
"\x1b[1;36mcli\x1b[0m -- type \x1b[1m?\x1b[0m for help, Tab to complete, Ctrl-D to exit"
);
} else {
println!("cli -- type ? for help, Tab to complete, Ctrl-D to exit");
}
println!("(ghost text suggests -- accept with Ctrl-F or ->)\n");
let prompt = if color {
"\x1b[1;32mcli>\x1b[0m "
} else {
"cli> "
};
loop {
let line = match editor.readline(prompt) {
Ok(Some(line)) => line,
Ok(None) => break,
Err(Error::Interrupted) => {
println!("^C");
continue;
}
Err(e) => return Err(e),
};
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
history.add(trimmed)?;
tokenizer.reset();
let words = tokenizer.tokenize(trimmed)?;
match words.first().map(String::as_str) {
Some("quit") | Some("exit") => break,
Some("help") => {
let max = COMMANDS.iter().map(|(n, _)| n.len()).max().unwrap_or(0);
for (name, desc) in COMMANDS {
println!(" \x1b[1;36m{:<width$}\x1b[0m {desc}", name, width = max);
}
}
Some("history") => {
if history.is_empty() {
println!("(no history)");
} else {
println!("{} entries", history.len());
if let Ok(latest) = history.first() {
println!("most recent: {latest}");
}
}
}
Some("clear") => {
history.clear();
println!("history cleared");
}
Some("caf\u{00e9}") => println!("(caf\u{00e9}: not implemented in this demo)"),
Some("show") => println!("(show: not implemented in this demo)"),
Some("set") => println!("(set: not implemented in this demo)"),
Some(cmd) => {
println!("\x1b[31munknown command:\x1b[0m `{cmd}` -- press ? for help");
}
None => {}
}
}
if let Err(e) = history.save(&hist_path) {
eprintln!("warning: could not save history: {e}");
}
println!("bye!");
Ok(())
}