cl-frontend: Embrace the crossterm jank

This commit is contained in:
John 2024-02-28 05:11:06 -06:00
parent e01c71f9a7
commit 78b8d87408
2 changed files with 211 additions and 66 deletions

View File

@ -379,13 +379,7 @@ pub mod cli {
/// Runs the main REPL loop /// Runs the main REPL loop
pub fn repl(&mut self) { pub fn repl(&mut self) {
use crate::repline::{error::Error, Repline}; use crate::repline::{error::Error, Repline};
let mut rl = Repline::new( let mut rl = Repline::new(self.mode.ansi_color(), self.prompt_begin, self.prompt_again);
// std::fs::File::open("/dev/stdin").unwrap(),
self.mode.ansi_color(),
self.prompt_begin,
self.prompt_again,
);
// self.prompt_begin();
fn clear_line() { fn clear_line() {
print!("\x1b[G\x1b[J"); print!("\x1b[G\x1b[J");
} }
@ -397,16 +391,17 @@ pub mod cli {
if buf.is_empty() || buf.ends_with('\n') { if buf.is_empty() || buf.ends_with('\n') {
return; return;
} }
rl.accept();
println!("Cancelled. (Press Ctrl+C again to quit.)");
continue; continue;
} }
// Ctrl-D: reset input, and parse it for errors // Ctrl-D: reset input, and parse it for errors
Err(Error::CtrlD(buf)) => { Err(Error::CtrlD(buf)) => {
rl.deny();
if let Err(e) = Program::new(&buf).parse() { if let Err(e) = Program::new(&buf).parse() {
println!();
clear_line(); clear_line();
self.prompt_error(&e); self.prompt_error(&e);
} }
rl.deny();
continue; continue;
} }
Err(e) => { Err(e) => {
@ -432,7 +427,7 @@ pub mod cli {
Some(Ok(_)) => unreachable!(), Some(Ok(_)) => unreachable!(),
Some(Err(error)) => { Some(Err(error)) => {
rl.deny(); rl.deny();
eprintln!("{error}"); self.prompt_error(&error);
continue; continue;
} }
} }

View File

@ -211,109 +211,112 @@ impl<'a, R: Read> Repline<'a, R> {
pub fn read(&mut self) -> ReplResult<String> { pub fn read(&mut self) -> ReplResult<String> {
const INDENT: &str = " "; const INDENT: &str = " ";
let mut stdout = stdout().lock(); let mut stdout = stdout().lock();
let stdout = &mut stdout;
let _make_raw = raw(); let _make_raw = raw();
self.ed.begin_frame(&mut stdout)?; // self.ed.begin_frame(stdout)?;
self.ed.render(&mut stdout)?; // self.ed.redraw_frame(stdout)?;
self.ed.print_head(stdout)?;
loop { loop {
stdout.flush()?; stdout.flush()?;
self.ed.begin_frame(&mut stdout)?;
match self.input.next().ok_or(Error::EndOfInput)?? { match self.input.next().ok_or(Error::EndOfInput)?? {
// Ctrl+C: End of Text. Immediately exits. // Ctrl+C: End of Text. Immediately exits.
// Ctrl+D: End of Transmission. Ends the current line. // Ctrl+D: End of Transmission. Ends the current line.
'\x03' => { '\x03' => {
self.ed.render(&mut stdout)?;
drop(_make_raw); drop(_make_raw);
writeln!(stdout)?;
return Err(Error::CtrlC(self.ed.to_string())); return Err(Error::CtrlC(self.ed.to_string()));
} }
'\x04' => { '\x04' => {
self.ed.render(&mut stdout)?; // TODO: this, better
drop(_make_raw); drop(_make_raw);
writeln!(stdout)?;
return Err(Error::CtrlD(self.ed.to_string())); return Err(Error::CtrlD(self.ed.to_string()));
} }
// Tab: extend line by 4 spaces // Tab: extend line by 4 spaces
'\t' => { '\t' => {
self.ed.extend(INDENT.chars()); self.ed.extend(INDENT.chars(), stdout)?;
} }
// ignore newlines, process line feeds. Not sure how cross-platform this is. // ignore newlines, process line feeds. Not sure how cross-platform this is.
'\n' => {} '\n' => {}
'\r' => { '\r' => {
self.ed.push('\n').ignore(); self.ed.push('\n', stdout)?;
self.ed.render(&mut stdout)?;
return Ok(self.ed.to_string()); return Ok(self.ed.to_string());
} }
// Escape sequence // Escape sequence
'\x1b' => self.escape()?, '\x1b' => self.escape(stdout)?,
// backspace // backspace
'\x08' | '\x7f' => { '\x08' | '\x7f' => {
let ed = &mut self.ed; let ed = &mut self.ed;
if ed.ends_with(INDENT.chars()) { if ed.ends_with(INDENT.chars()) {
for _ in 0..INDENT.len() { for _ in 0..INDENT.len() {
ed.pop().ignore(); ed.pop(stdout)?;
} }
} else { } else {
self.ed.pop().ignore(); ed.pop(stdout)?;
} }
} }
c if c.is_ascii_control() => { c if c.is_ascii_control() => {
eprint!("\\x{:02x}", c as u32); eprint!("\\x{:02x}", c as u32);
} }
c => { c => {
self.ed.push(c).unwrap(); self.ed.push(c, stdout)?;
} }
} }
self.ed.render(&mut stdout)?;
} }
} }
/// Handle ANSI Escape /// Handle ANSI Escape
fn escape(&mut self) -> ReplResult<()> { fn escape<W: Write>(&mut self, w: &mut W) -> ReplResult<()> {
match self.input.next().ok_or(Error::EndOfInput)?? { match self.input.next().ok_or(Error::EndOfInput)?? {
'[' => self.csi()?, '[' => self.csi(w)?,
'O' => todo!("Process alternate character mode"), 'O' => todo!("Process alternate character mode"),
other => self.ed.extend(['\x1b', other]), other => self.ed.extend(['\x1b', other], w)?,
} }
Ok(()) Ok(())
} }
/// Handle ANSI Control Sequence Introducer /// Handle ANSI Control Sequence Introducer
fn csi(&mut self) -> ReplResult<()> { fn csi<W: Write>(&mut self, w: &mut W) -> ReplResult<()> {
match self.input.next().ok_or(Error::EndOfInput)?? { match self.input.next().ok_or(Error::EndOfInput)?? {
'A' => { 'A' => {
self.hindex = self.hindex.saturating_sub(1); self.hindex = self.hindex.saturating_sub(1);
self.restore_history(); self.restore_history(w)?
} }
'B' => { 'B' => {
self.hindex = self self.hindex = self
.hindex .hindex
.saturating_add(1) .saturating_add(1)
.min(self.history.len().saturating_sub(1)); .min(self.history.len().saturating_sub(1));
self.restore_history(); self.restore_history(w)?
} }
'C' => self.ed.cursor_back(1).ignore(), 'C' => self.ed.cursor_forward(1, w)?,
'D' => self.ed.cursor_forward(1).ignore(), 'D' => self.ed.cursor_back(1, w)?,
'H' => self.ed.home(w)?,
'F' => self.ed.end(w)?,
'3' => { '3' => {
if let '~' = self.input.next().ok_or(Error::EndOfInput)?? { if let '~' = self.input.next().ok_or(Error::EndOfInput)?? {
self.ed.delete().ignore() self.ed.delete(w).ignore()
} }
} }
other => { other => {
eprintln!("{other}"); eprint!("{}", other.escape_unicode());
} }
} }
Ok(()) Ok(())
} }
/// Restores the currently selected history /// Restores the currently selected history
pub fn restore_history(&mut self) { pub fn restore_history<W: Write>(&mut self, w: &mut W) -> ReplResult<()> {
let Self { history, hindex, ed, .. } = self; let Self { history, hindex, ed, .. } = self;
if !(0..history.len()).contains(hindex) { if !(0..history.len()).contains(hindex) {
return; return Ok(());
}; };
ed.undraw(w)?;
ed.clear(); ed.clear();
ed.print_head(w)?;
ed.extend( ed.extend(
history history
.get(*hindex) .get(*hindex)
.expect("history should contain index") .expect("history should contain index")
.trim_end_matches(|c: char| c != '\n' && c.is_whitespace())
.chars(), .chars(),
); w,
)
} }
/// Append line to history and clear it /// Append line to history and clear it
@ -323,7 +326,10 @@ impl<'a, R: Read> Repline<'a, R> {
self.hindex = self.history.len(); self.hindex = self.history.len();
} }
/// Append line to history /// Append line to history
pub fn history_append(&mut self, buf: String) { pub fn history_append(&mut self, mut buf: String) {
while buf.ends_with(char::is_whitespace) {
buf.pop();
}
if !self.history.contains(&buf) { if !self.history.contains(&buf) {
self.history.push_back(buf) self.history.push_back(buf)
} }
@ -344,10 +350,25 @@ impl<'a> Repline<'a, std::io::Stdin> {
} }
pub mod editor { pub mod editor {
use crossterm::{cursor::*, execute, queue, style::*, terminal::*};
use std::{collections::VecDeque, fmt::Display, io::Write}; use std::{collections::VecDeque, fmt::Display, io::Write};
use super::error::{Error, ReplResult}; use super::error::{Error, ReplResult};
fn is_newline(c: &char) -> bool {
*c == '\n'
}
fn write_chars<'a, W: Write>(
c: impl IntoIterator<Item = &'a char>,
w: &mut W,
) -> std::io::Result<()> {
for c in c {
write!(w, "{c}")?;
}
Ok(())
}
#[derive(Debug)] #[derive(Debug)]
pub struct Editor<'a> { pub struct Editor<'a> {
head: VecDeque<char>, head: VecDeque<char>,
@ -365,16 +386,17 @@ pub mod editor {
pub fn iter(&self) -> impl Iterator<Item = &char> { pub fn iter(&self) -> impl Iterator<Item = &char> {
self.head.iter() self.head.iter()
} }
pub fn begin_frame<W: Write>(&self, w: &mut W) -> ReplResult<()> { pub fn undraw<W: Write>(&self, w: &mut W) -> ReplResult<()> {
let Self { head, .. } = self; let Self { head, .. } = self;
match head.iter().filter(|&&c| c == '\n').count() { match head.iter().copied().filter(is_newline).count() {
0 => write!(w, "\x1b[0G"), 0 => write!(w, "\x1b[0G"),
lines => write!(w, "\x1b[{}F", lines), lines => write!(w, "\x1b[{}F", lines),
}?; }?;
write!(w, "\x1b[0J")?; queue!(w, Clear(ClearType::FromCursorDown))?;
// write!(w, "\x1b[0J")?;
Ok(()) Ok(())
} }
pub fn render<W: Write>(&self, w: &mut W) -> ReplResult<()> { pub fn redraw<W: Write>(&self, w: &mut W) -> ReplResult<()> {
let Self { head, tail, color, begin, again } = self; let Self { head, tail, color, begin, again } = self;
write!(w, "{color}{begin}\x1b[0m ")?; write!(w, "{color}{begin}\x1b[0m ")?;
// draw head // draw head
@ -385,7 +407,7 @@ pub mod editor {
}? }?
} }
// save cursor // save cursor
write!(w, "\x1b[s")?; execute!(w, SavePosition)?;
// draw tail // draw tail
for c in tail { for c in tail {
match c { match c {
@ -394,7 +416,99 @@ pub mod editor {
}? }?
} }
// restore cursor // restore cursor
w.write_all(b"\x1b[u").map_err(Into::into) execute!(w, RestorePosition)?;
Ok(())
}
pub fn prompt<W: Write>(&self, w: &mut W) -> ReplResult<()> {
let Self { head, color, begin, again, .. } = self;
queue!(
w,
MoveToColumn(0),
Print(color),
Print(if head.is_empty() { begin } else { again }),
ResetColor,
Print(' '),
)?;
Ok(())
}
pub fn print_head<W: Write>(&self, w: &mut W) -> ReplResult<()> {
self.prompt(w)?;
write_chars(
self.head.iter().skip(
self.head
.iter()
.rposition(is_newline)
.unwrap_or(self.head.len())
+ 1,
),
w,
)?;
Ok(())
}
pub fn print_tail<W: Write>(&self, w: &mut W) -> ReplResult<()> {
let Self { tail, .. } = self;
queue!(w, SavePosition, Clear(ClearType::UntilNewLine))?;
write_chars(tail.iter().take_while(|&c| !is_newline(c)), w)?;
queue!(w, RestorePosition)?;
Ok(())
}
pub fn push<W: Write>(&mut self, c: char, w: &mut W) -> ReplResult<()> {
// Tail optimization: if the tail is empty,
//we don't have to undraw and redraw on newline
if self.tail.is_empty() {
self.head.push_back(c);
match c {
'\n' => {
write!(w, "\r\n")?;
self.print_head(w)?;
}
c => {
queue!(w, Print(c))?;
}
};
return Ok(());
}
if '\n' == c {
self.undraw(w)?;
}
self.head.push_back(c);
match c {
'\n' => self.redraw(w)?,
_ => {
write!(w, "{c}")?;
self.print_tail(w)?;
}
}
Ok(())
}
pub fn pop<W: Write>(&mut self, w: &mut W) -> ReplResult<Option<char>> {
if let Some('\n') = self.head.back() {
self.undraw(w)?;
}
let c = self.head.pop_back();
// if the character was a newline, we need to go back a line
match c {
Some('\n') => self.redraw(w)?,
Some(_) => {
// go back a char
queue!(w, MoveLeft(1), Print(' '), MoveLeft(1))?;
self.print_tail(w)?;
}
None => {}
}
Ok(c)
}
pub fn extend<T: IntoIterator<Item = char>, W: Write>(
&mut self,
iter: T,
w: &mut W,
) -> ReplResult<()> {
for c in iter {
self.push(c, w)?;
}
Ok(())
} }
pub fn restore(&mut self, s: &str) { pub fn restore(&mut self, s: &str) {
self.clear(); self.clear();
@ -404,15 +518,21 @@ pub mod editor {
self.head.clear(); self.head.clear();
self.tail.clear(); self.tail.clear();
} }
pub fn push(&mut self, c: char) -> ReplResult<()> { pub fn delete<W: Write>(&mut self, w: &mut W) -> ReplResult<char> {
self.head.push_back(c); match self.tail.front() {
Ok(()) Some('\n') => {
} self.undraw(w)?;
pub fn pop(&mut self) -> ReplResult<char> { let out = self.tail.pop_front();
self.head.pop_back().ok_or(Error::EndOfInput) self.redraw(w)?;
} out
pub fn delete(&mut self) -> ReplResult<char> { }
self.tail.pop_front().ok_or(Error::EndOfInput) _ => {
let out = self.tail.pop_front();
self.print_tail(w)?;
out
}
}
.ok_or(Error::EndOfInput)
} }
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
self.head.len() + self.tail.len() self.head.len() + self.tail.len()
@ -433,26 +553,56 @@ pub mod editor {
} }
} }
/// Moves the cursor back `steps` steps /// Moves the cursor back `steps` steps
pub fn cursor_forward(&mut self, steps: usize) -> Option<()> { pub fn cursor_back<W: Write>(&mut self, steps: usize, w: &mut W) -> ReplResult<()> {
let Self { head, tail, .. } = self;
for _ in 0..steps { for _ in 0..steps {
tail.push_front(head.pop_back()?); if let Some('\n') = self.head.back() {
self.undraw(w)?;
}
let Some(c) = self.head.pop_back() else {
return Ok(());
};
self.tail.push_front(c);
match c {
'\n' => self.redraw(w)?,
_ => queue!(w, MoveLeft(1))?,
}
} }
Some(()) Ok(())
} }
/// Moves the cursor forward `steps` steps /// Moves the cursor forward `steps` steps
pub fn cursor_back(&mut self, steps: usize) -> Option<()> { pub fn cursor_forward<W: Write>(&mut self, steps: usize, w: &mut W) -> ReplResult<()> {
let Self { head, tail, .. } = self;
for _ in 0..steps { for _ in 0..steps {
head.push_back(tail.pop_front()?); if let Some('\n') = self.tail.front() {
self.undraw(w)?
}
let Some(c) = self.tail.pop_front() else {
return Ok(());
};
self.head.push_back(c);
match c {
'\n' => self.redraw(w)?,
_ => queue!(w, MoveRight(1))?,
}
} }
Some(()) Ok(())
} }
} /// Goes to the beginning of the current line
pub fn home<W: Write>(&mut self, w: &mut W) -> ReplResult<()> {
impl<'a> Extend<char> for Editor<'a> { loop {
fn extend<T: IntoIterator<Item = char>>(&mut self, iter: T) { match self.head.back() {
self.head.extend(iter) Some('\n') | None => break Ok(()),
Some(_) => self.cursor_back(1, w)?,
}
}
}
/// Goes to the end of the current line
pub fn end<W: Write>(&mut self, w: &mut W) -> ReplResult<()> {
loop {
match self.tail.front() {
Some('\n') | None => break Ok(()),
Some(_) => self.cursor_forward(1, w)?,
}
}
} }
} }