tempest-repl 0.0.1

TempestDB interactive REPL
Documentation
use std::{
    collections::HashMap,
    io::{self, BufRead, Read, Write},
    mem,
};

use termion::{
    event::Key,
    input::TermRead,
    raw::{IntoRawMode, RawTerminal},
};

use crate::stdio::{Stdio, out::RawOut};

pub struct TerminalStdin {
    inner: io::Stdin,
    echo: RawOut<io::Stdout>,
    history: Vec<String>,
    history_pos: Option<usize>,
    stash: HashMap<usize, String>,
    live_stash: String,
}

impl Read for TerminalStdin {
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        self.inner.read(buf)
    }
}

impl BufRead for TerminalStdin {
    fn fill_buf(&mut self) -> io::Result<&[u8]> {
        Ok(&[])
    }

    fn consume(&mut self, _: usize) {}

    fn read_line(&mut self, line: &mut String) -> io::Result<usize> {
        let mut buf = String::new();
        let mut cursor = 0usize;

        self.redraw(&buf, cursor)?;
        let mut keys = self.inner.lock().keys();
        loop {
            match keys.next().transpose()? {
                Some(Key::Alt('\x7f' | '\x08')) => {
                    // skip trailing whitespace, then delete back to the previous whitespace
                    let trimmed = buf[..cursor]
                        .trim_end_matches(|c: char| c.is_whitespace())
                        .len();
                    let word_start = buf[..trimmed]
                        .rfind(|c: char| c.is_whitespace())
                        .map(|i| i + 1)
                        .unwrap_or(0);
                    buf.drain(word_start..cursor);
                    if let Some(i) = self.history_pos {
                        self.stash.insert(i, buf.clone());
                    }
                    cursor = word_start;
                    self.redraw(&buf, cursor)?;
                }
                Some(Key::CtrlLeft | Key::AltLeft) => {
                    let trimmed = buf[..cursor]
                        .trim_end_matches(|c: char| c.is_whitespace())
                        .len();
                    cursor = buf[..trimmed]
                        .rfind(|c: char| c.is_whitespace())
                        .map(|i| i + 1)
                        .unwrap_or(0);
                    self.redraw(&buf, cursor)?;
                }
                Some(Key::CtrlRight | Key::AltRight) => {
                    let after = buf[cursor..].trim_start_matches(|c: char| c.is_whitespace());
                    let skipped = buf.len() - cursor - after.len();
                    cursor = after
                        .find(|c: char| c.is_whitespace())
                        .map(|i| cursor + skipped + i)
                        .unwrap_or(buf.len());
                    self.redraw(&buf, cursor)?;
                }
                Some(Key::Up) => {
                    if !self.history.is_empty() {
                        if self.history_pos.is_none() {
                            self.live_stash = buf.clone();
                        }
                        let new_pos = match self.history_pos {
                            None => self.history.len() - 1,
                            Some(0) => 0,
                            Some(i) => i - 1,
                        };
                        self.history_pos = Some(new_pos);
                        buf = self
                            .stash
                            .get(&new_pos)
                            .cloned()
                            .unwrap_or_else(|| self.history[new_pos].clone());
                        cursor = buf.len();
                        self.redraw(&buf, cursor)?;
                    }
                }
                Some(Key::Down) => match self.history_pos {
                    None => {}
                    Some(i) if i + 1 >= self.history.len() => {
                        self.history_pos = None;
                        buf = mem::take(&mut self.live_stash);
                        cursor = buf.len();
                        self.redraw(&buf, cursor)?;
                    }
                    Some(i) => {
                        self.history_pos = Some(i + 1);
                        buf = self
                            .stash
                            .get(&(i + 1))
                            .cloned()
                            .unwrap_or_else(|| self.history[i + 1].clone());
                        cursor = buf.len();
                        self.redraw(&buf, cursor)?;
                    }
                },
                Some(Key::Char('\n' | '\r')) => {
                    writeln!(self.echo)?;
                    self.echo.flush()?;
                    buf.push('\n');
                    self.stash.clear();
                    self.live_stash.clear();
                    break;
                }
                Some(Key::Char(c)) => {
                    buf.insert(cursor, c);
                    if let Some(i) = self.history_pos {
                        self.stash.insert(i, buf.clone());
                    }
                    cursor += 1;
                    self.redraw(&buf, cursor)?;
                }
                Some(Key::Backspace) => {
                    if cursor > 0 {
                        cursor -= 1;
                        buf.remove(cursor);
                        if let Some(i) = self.history_pos {
                            self.stash.insert(i, buf.clone());
                        }
                        self.redraw(&buf, cursor)?;
                    }
                }
                Some(Key::Left) if cursor > 0 => {
                    cursor -= 1;
                    write!(self.echo, "\x1b[D")?;
                    self.echo.flush()?;
                }
                Some(Key::Right) if cursor < buf.len() => {
                    cursor += 1;
                    write!(self.echo, "\x1b[C")?;
                    self.echo.flush()?;
                }
                Some(Key::Home) if cursor > 0 => {
                    cursor = 0;
                    self.redraw(&buf, cursor)?;
                }
                Some(Key::End) if cursor < buf.len() => {
                    cursor = buf.len();
                    self.redraw(&buf, cursor)?;
                }
                Some(Key::Ctrl('c')) => {
                    if buf.is_empty() {
                        return Err(io::Error::from(io::ErrorKind::Interrupted));
                    } else {
                        buf.clear();
                        cursor = 0;
                        self.redraw(&buf, cursor)?;
                    }
                }
                Some(Key::Ctrl('d')) if buf.is_empty() => return Ok(0),
                _ => {}
            }
        }

        let n = buf.len();
        line.push_str(&buf);
        Ok(n)
    }
}

impl TerminalStdin {
    fn redraw(&mut self, line: &str, cursor: usize) -> io::Result<()> {
        write!(self.echo, "\r\x1b[2K>> {}", line)?;
        // reposition cursor - ">> " is 3 chars
        write!(self.echo, "\r\x1b[{}C", cursor + 3)?;
        self.echo.flush()
    }
}

pub struct TerminalStdio {
    stdin: TerminalStdin,
    stdout: RawOut<RawTerminal<io::Stdout>>,
    stderr: RawOut<io::Stderr>,
}

impl TerminalStdio {
    pub fn new() -> io::Result<Self> {
        let stdin = TerminalStdin {
            inner: io::stdin(),
            echo: RawOut(io::stdout()),
            history: Vec::new(),
            history_pos: None,
            stash: HashMap::new(),
            live_stash: String::new(),
        };
        let stdout = RawOut(io::stdout().into_raw_mode()?);
        let stderr = RawOut(io::stderr());
        Ok(Self {
            stdin,
            stdout,
            stderr,
        })
    }
}

impl Stdio for TerminalStdio {
    fn stdin(&mut self) -> &mut impl BufRead {
        &mut self.stdin
    }

    fn stdout(&mut self) -> &mut impl io::Write {
        &mut self.stdout
    }

    fn stderr(&mut self) -> &mut impl io::Write {
        &mut self.stderr
    }

    fn push_history(&mut self, entry: &str) {
        self.stdin.history.push(entry.to_string());
        self.stdin.history_pos = None;
    }
}