Skip to main content

mj_controller/
hel_readline.rs

1//! Readline-backed input for standalone terminal prompts.
2
3use std::io::{self, IsTerminal, Write};
4
5use anyhow::{Context, Result};
6use reedline::{DefaultPrompt, DefaultPromptSegment, Reedline, Signal};
7
8pub struct LineReader {
9    editor: Reedline,
10}
11
12impl Default for LineReader {
13    fn default() -> Self {
14        Self {
15            editor: Reedline::create(),
16        }
17    }
18}
19
20impl LineReader {
21    /// Read an editable line. `None` represents Ctrl-D; Ctrl-C returns an
22    /// empty answer so callers retain their existing default/cancel behavior.
23    pub fn read_line(&mut self, label: &str) -> Result<Option<String>> {
24        if !io::stdin().is_terminal() || !io::stdout().is_terminal() {
25            print!("{label}");
26            io::stdout().flush()?;
27            let mut answer = String::new();
28            let read = io::stdin()
29                .read_line(&mut answer)
30                .context("read terminal response")?;
31            return Ok((read > 0).then(|| answer.trim().to_owned()));
32        }
33
34        let prompt = DefaultPrompt::new(
35            DefaultPromptSegment::Basic(label.to_owned()),
36            DefaultPromptSegment::Empty,
37        );
38        match self
39            .editor
40            .read_line(&prompt)
41            .context("read terminal response")?
42        {
43            Signal::Success(answer) => Ok(Some(answer.trim().to_owned())),
44            Signal::CtrlD => Ok(None),
45            Signal::CtrlC => Ok(Some(String::new())),
46            Signal::ExternalBreak(answer) | Signal::HostCommand(answer) => {
47                Ok(Some(answer.trim().to_owned()))
48            }
49            _ => Ok(None),
50        }
51    }
52}