agentic_vision/cli/
repl_complete.rs1use rustyline::completion::{Completer, Pair};
4use rustyline::highlight::Highlighter;
5use rustyline::hint::{Hinter, HistoryHinter};
6use rustyline::validate::Validator;
7use rustyline::{Cmd, ConditionalEventHandler, Context, Event, EventHandler, Helper, KeyEvent};
8
9pub const COMMANDS: &[(&str, &str)] = &[
10 ("create", "Create a new .avis file"),
11 ("load", "Load an .avis file"),
12 ("info", "Display info about the loaded file"),
13 ("capture", "Capture an image from a file path"),
14 ("query", "Search observations"),
15 ("similar", "Find visually similar captures"),
16 ("compare", "Compare two captures by embedding"),
17 ("diff", "Pixel-level diff between two captures"),
18 ("health", "Quality and staleness report"),
19 ("link", "Link a capture to a memory node"),
20 ("stats", "Aggregate statistics"),
21 ("export", "Export observations as JSON"),
22 ("clear", "Clear screen"),
23 ("help", "Show available commands"),
24 ("exit", "Quit the REPL"),
25];
26
27pub struct AvisHelper {
28 hinter: HistoryHinter,
29}
30
31impl Default for AvisHelper {
32 fn default() -> Self {
33 Self {
34 hinter: HistoryHinter::new(),
35 }
36 }
37}
38
39impl AvisHelper {
40 pub fn new() -> Self {
41 Self::default()
42 }
43}
44
45impl Completer for AvisHelper {
46 type Candidate = Pair;
47
48 fn complete(
49 &self,
50 line: &str,
51 pos: usize,
52 _ctx: &Context<'_>,
53 ) -> rustyline::Result<(usize, Vec<Pair>)> {
54 let trimmed = line[..pos].trim_start();
55 if !trimmed.starts_with('/') && !trimmed.is_empty() {
56 return Ok((0, vec![]));
57 }
58 let prefix = trimmed.strip_prefix('/').unwrap_or_default();
59 let matches: Vec<Pair> = COMMANDS
60 .iter()
61 .filter(|(name, _)| name.starts_with(prefix))
62 .map(|(name, _desc)| Pair {
63 display: format!("/{name}"),
64 replacement: format!("/{name} "),
65 })
66 .collect();
67 let start = pos - prefix.len() - if trimmed.starts_with('/') { 1 } else { 0 };
68 Ok((start, matches))
69 }
70}
71
72impl Hinter for AvisHelper {
73 type Hint = String;
74 fn hint(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Option<String> {
75 self.hinter.hint(line, pos, ctx)
76 }
77}
78
79impl Highlighter for AvisHelper {}
80impl Validator for AvisHelper {}
81impl Helper for AvisHelper {}
82
83pub struct TabCompleteOrAcceptHint;
84
85use rustyline::EventContext;
86
87impl ConditionalEventHandler for TabCompleteOrAcceptHint {
88 fn handle(
89 &self,
90 _evt: &Event,
91 _n: rustyline::RepeatCount,
92 _positive: bool,
93 _ctx: &EventContext,
94 ) -> Option<Cmd> {
95 Some(Cmd::Complete)
96 }
97}
98
99pub fn bind_keys(editor: &mut rustyline::Editor<AvisHelper, rustyline::history::DefaultHistory>) {
100 editor.bind_sequence(
101 KeyEvent::from('\t'),
102 EventHandler::Conditional(Box::new(TabCompleteOrAcceptHint)),
103 );
104}