Skip to main content

agentic_vision/cli/
repl_complete.rs

1//! Tab completion and hints for the avis REPL.
2
3use 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}