Skip to main content

ghoti_shell/
repl.rs

1use std::ops::ControlFlow;
2
3use ghoti_exec::{ExecContext, Status};
4use owo_colors::OwoColorize;
5use rustyline::completion::Completer;
6use rustyline::error::ReadlineError;
7use rustyline::hint::Hinter;
8use rustyline::history::MemHistory;
9use rustyline::{Editor, Helper, Highlighter, Validator};
10
11#[derive(Helper, Validator, Highlighter)]
12struct ShellHelper<'a, 'ctx> {
13    ctx: &'a mut ExecContext<'ctx>,
14}
15
16impl Hinter for ShellHelper<'_, '_> {
17    type Hint = String;
18
19    fn hint(&self, line: &str, pos: usize, _ctx: &rustyline::Context<'_>) -> Option<Self::Hint> {
20        let sp_pos = line.find(' ').unwrap_or(line.len());
21        if line.is_empty() || pos > sp_pos {
22            return None;
23        }
24
25        self.ctx
26            .list_funcs_without_autoload(|name, _cmd| {
27                if let Some(rest) = name.strip_prefix(line) {
28                    return ControlFlow::Break(rest.bright_black().to_string());
29                }
30                ControlFlow::Continue(())
31            })
32            .break_value()
33    }
34}
35
36impl Completer for ShellHelper<'_, '_> {
37    type Candidate = String;
38
39    fn complete(
40        &self,
41        line: &str,
42        pos: usize,
43        _ctx: &rustyline::Context<'_>,
44    ) -> rustyline::Result<(usize, Vec<Self::Candidate>)> {
45        let sp_pos = line.find(' ').unwrap_or(line.len());
46        if line.is_empty() || pos > sp_pos {
47            return Ok((0, Vec::new()));
48        }
49
50        let mut candidates = Vec::new();
51        self.ctx.list_funcs_without_autoload::<()>(|name, _cmd| {
52            if let Some(rest) = name.strip_prefix(line) {
53                candidates.push(rest.to_string());
54            }
55            ControlFlow::Continue(())
56        });
57
58        Ok((line.len(), candidates))
59    }
60}
61
62pub fn run_repl(
63    rt: &tokio::runtime::Runtime,
64    ctx: &mut ExecContext<'_>,
65) -> Result<(), Box<dyn std::error::Error>> {
66    let mut rl: Editor<ShellHelper, MemHistory> =
67        rustyline::Editor::with_history(rustyline::Config::default(), MemHistory::new())?;
68    rl.set_helper(Some(ShellHelper { ctx }));
69
70    loop {
71        let prompt = {
72            let ctx = &mut *rl.helper_mut().unwrap().ctx;
73            // For completion
74            rt.block_on(ctx.populate_autoload_func_cache());
75
76            let st = ctx.last_status();
77            if st.is_success() {
78                "ghoti-shell> ".to_owned()
79            } else {
80                format!("ghoti-shell[{st}]> ")
81            }
82        };
83
84        let input = match rl.readline(&prompt) {
85            Ok(input) => input,
86            Err(ReadlineError::Eof) => {
87                ctx.set_last_status(Status::SUCCESS);
88                return Ok(());
89            }
90            Err(ReadlineError::Interrupted) => continue,
91            Err(err) => return Err(err.into()),
92        };
93        rl.add_history_entry(input.clone())?;
94        let ctx = &mut *rl.helper_mut().unwrap().ctx;
95
96        rt.block_on(ctx.exec_source(None, input));
97        // Prevent next prompt from clobbering the output if it contains no newline.
98        println!();
99    }
100}