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')) => {
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)?;
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;
}
}