repline: Promote to its own crate!
cl-repl: Major refactor based on the design of typeck.rs
This commit is contained in:
parent
01ffdb67a6
commit
2a62a1c714
@ -8,6 +8,7 @@ members = [
|
|||||||
"cl-ast",
|
"cl-ast",
|
||||||
"cl-parser",
|
"cl-parser",
|
||||||
"cl-lexer",
|
"cl-lexer",
|
||||||
|
"repline",
|
||||||
]
|
]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
|
@ -15,9 +15,8 @@ cl-lexer = { path = "../cl-lexer" }
|
|||||||
cl-token = { path = "../cl-token" }
|
cl-token = { path = "../cl-token" }
|
||||||
cl-parser = { path = "../cl-parser" }
|
cl-parser = { path = "../cl-parser" }
|
||||||
cl-interpret = { path = "../cl-interpret" }
|
cl-interpret = { path = "../cl-interpret" }
|
||||||
crossterm = "0.27.0"
|
repline = { path = "../repline" }
|
||||||
argh = "0.1.12"
|
argh = "0.1.12"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
cl-structures = { path = "../cl-structures" }
|
|
||||||
cl-typeck = { path = "../cl-typeck" }
|
cl-typeck = { path = "../cl-typeck" }
|
||||||
|
@ -4,10 +4,10 @@ use cl_ast::{
|
|||||||
};
|
};
|
||||||
use cl_lexer::Lexer;
|
use cl_lexer::Lexer;
|
||||||
use cl_parser::Parser;
|
use cl_parser::Parser;
|
||||||
use cl_repl::repline::{error::Error as RlError, Repline};
|
|
||||||
use cl_typeck::{
|
use cl_typeck::{
|
||||||
definition::Def, name_collector::NameCollectable, project::Project, type_resolver::resolve,
|
definition::Def, name_collector::NameCollectable, project::Project, type_resolver::resolve,
|
||||||
};
|
};
|
||||||
|
use repline::{error::Error as RlError, prebaked::*};
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
|
||||||
// Path to display in standard library errors
|
// Path to display in standard library errors
|
||||||
@ -40,53 +40,21 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
|
|
||||||
unsafe { TREES.push(code) }.collect_in_root(&mut prj)?;
|
unsafe { TREES.push(code) }.collect_in_root(&mut prj)?;
|
||||||
|
|
||||||
main_menu(&mut prj)
|
main_menu(&mut prj)?;
|
||||||
}
|
|
||||||
|
|
||||||
pub enum Response {
|
|
||||||
Accept,
|
|
||||||
Deny,
|
|
||||||
Break,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_and(
|
|
||||||
color: &str,
|
|
||||||
begin: &str,
|
|
||||||
mut f: impl FnMut(&str) -> Result<Response, Box<dyn Error>>,
|
|
||||||
) -> Result<(), Box<dyn Error>> {
|
|
||||||
let mut rl = Repline::new(color, begin, "? >");
|
|
||||||
loop {
|
|
||||||
let line = match rl.read() {
|
|
||||||
Err(RlError::CtrlC(_)) => break,
|
|
||||||
Err(RlError::CtrlD(line)) => {
|
|
||||||
rl.deny();
|
|
||||||
line
|
|
||||||
}
|
|
||||||
Ok(line) => line,
|
|
||||||
Err(e) => Err(e)?,
|
|
||||||
};
|
|
||||||
print!("\x1b[G\x1b[J");
|
|
||||||
match f(&line) {
|
|
||||||
Ok(Response::Accept) => rl.accept(),
|
|
||||||
Ok(Response::Deny) => rl.deny(),
|
|
||||||
Ok(Response::Break) => break,
|
|
||||||
Err(e) => print!("\x1b[40G\x1bJ\x1b[91m{e}\x1b[0m"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main_menu(prj: &mut Project) -> Result<(), Box<dyn Error>> {
|
fn main_menu(prj: &mut Project) -> Result<(), RlError> {
|
||||||
banner();
|
banner();
|
||||||
read_and(C_MAIN, "mu>", |line| {
|
read_and(C_MAIN, "mu>", "? >", |line| {
|
||||||
match line.trim() {
|
match line.trim() {
|
||||||
"c" | "code" => enter_code(prj),
|
"c" | "code" => enter_code(prj)?,
|
||||||
"clear" => clear(),
|
"clear" => clear()?,
|
||||||
"e" | "exit" => return Ok(Response::Break),
|
"e" | "exit" => return Ok(Response::Break),
|
||||||
"l" | "list" => list_types(prj),
|
"l" | "list" => list_types(prj),
|
||||||
"q" | "query" => query_type_expression(prj),
|
"q" | "query" => query_type_expression(prj)?,
|
||||||
"r" | "resolve" => resolve_all(prj),
|
"r" | "resolve" => resolve_all(prj)?,
|
||||||
"d" | "desugar" => live_desugar(),
|
"d" | "desugar" => live_desugar()?,
|
||||||
"h" | "help" => {
|
"h" | "help" => {
|
||||||
println!(
|
println!(
|
||||||
"Valid commands are:
|
"Valid commands are:
|
||||||
@ -102,12 +70,12 @@ fn main_menu(prj: &mut Project) -> Result<(), Box<dyn Error>> {
|
|||||||
}
|
}
|
||||||
_ => Err(r#"Invalid command. Type "help" to see the list of valid commands."#)?,
|
_ => Err(r#"Invalid command. Type "help" to see the list of valid commands."#)?,
|
||||||
}
|
}
|
||||||
.map(|_| Response::Accept)
|
Ok(Response::Accept)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn enter_code(prj: &mut Project) -> Result<(), Box<dyn Error>> {
|
fn enter_code(prj: &mut Project) -> Result<(), RlError> {
|
||||||
read_and(C_CODE, "cl>", |line| {
|
read_and(C_CODE, "cl>", "? >", |line| {
|
||||||
if line.trim().is_empty() {
|
if line.trim().is_empty() {
|
||||||
return Ok(Response::Break);
|
return Ok(Response::Break);
|
||||||
}
|
}
|
||||||
@ -120,8 +88,8 @@ fn enter_code(prj: &mut Project) -> Result<(), Box<dyn Error>> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn live_desugar() -> Result<(), Box<dyn Error>> {
|
fn live_desugar() -> Result<(), RlError> {
|
||||||
read_and(C_RESV, "se>", |line| {
|
read_and(C_RESV, "se>", "? >", |line| {
|
||||||
let code = Parser::new(Lexer::new(line)).stmt()?;
|
let code = Parser::new(Lexer::new(line)).stmt()?;
|
||||||
println!("Raw, as parsed:\n{C_LISTING}{code}\x1b[0m");
|
println!("Raw, as parsed:\n{C_LISTING}{code}\x1b[0m");
|
||||||
|
|
||||||
@ -135,8 +103,8 @@ fn live_desugar() -> Result<(), Box<dyn Error>> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn query_type_expression(prj: &mut Project) -> Result<(), Box<dyn Error>> {
|
fn query_type_expression(prj: &mut Project) -> Result<(), RlError> {
|
||||||
read_and(C_RESV, "ty>", |line| {
|
read_and(C_RESV, "ty>", "? >", |line| {
|
||||||
if line.trim().is_empty() {
|
if line.trim().is_empty() {
|
||||||
return Ok(Response::Break);
|
return Ok(Response::Break);
|
||||||
}
|
}
|
||||||
@ -156,7 +124,7 @@ fn resolve_all(prj: &mut Project) -> Result<(), Box<dyn Error>> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn list_types(prj: &mut Project) -> Result<(), Box<dyn Error>> {
|
fn list_types(prj: &mut Project) {
|
||||||
println!(" name\x1b[30G type");
|
println!(" name\x1b[30G type");
|
||||||
for (idx, Def { name, vis, kind, .. }) in prj.pool.iter().enumerate() {
|
for (idx, Def { name, vis, kind, .. }) in prj.pool.iter().enumerate() {
|
||||||
print!("{idx:3}: {vis}");
|
print!("{idx:3}: {vis}");
|
||||||
@ -165,7 +133,6 @@ fn list_types(prj: &mut Project) -> Result<(), Box<dyn Error>> {
|
|||||||
}
|
}
|
||||||
println!("{name}\x1b[30G| {kind}");
|
println!("{name}\x1b[30G| {kind}");
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pretty_def(def: &Def, id: impl Into<usize>) {
|
fn pretty_def(def: &Def, id: impl Into<usize>) {
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
use cl_lexer::Lexer;
|
use cl_lexer::Lexer;
|
||||||
use cl_parser::Parser;
|
use cl_parser::Parser;
|
||||||
use cl_repl::repline::{error::Error as RlError, Repline};
|
use repline::{error::Error as RlError, Repline};
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn Error>> {
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
14
cl-repl/src/ansi.rs
Normal file
14
cl-repl/src/ansi.rs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
//! ANSI escape sequences
|
||||||
|
|
||||||
|
pub const RED: &str = "\x1b[31m";
|
||||||
|
pub const GREEN: &str = "\x1b[32m"; // the color of type checker mode
|
||||||
|
pub const CYAN: &str = "\x1b[36m";
|
||||||
|
pub const BRIGHT_GREEN: &str = "\x1b[92m";
|
||||||
|
pub const BRIGHT_BLUE: &str = "\x1b[94m";
|
||||||
|
pub const BRIGHT_MAGENTA: &str = "\x1b[95m";
|
||||||
|
pub const BRIGHT_CYAN: &str = "\x1b[96m";
|
||||||
|
pub const RESET: &str = "\x1b[0m";
|
||||||
|
pub const OUTPUT: &str = "\x1b[38;5;117m";
|
||||||
|
|
||||||
|
pub const CLEAR_LINES: &str = "\x1b[G\x1b[J";
|
||||||
|
pub const CLEAR_ALL: &str = "\x1b[H\x1b[2J";
|
51
cl-repl/src/args.rs
Normal file
51
cl-repl/src/args.rs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
//! Handles argument parsing (currently using the [argh] crate)
|
||||||
|
|
||||||
|
use argh::FromArgs;
|
||||||
|
use std::{io::IsTerminal, path::PathBuf, str::FromStr};
|
||||||
|
|
||||||
|
/// The Conlang prototype debug interface
|
||||||
|
#[derive(Clone, Debug, FromArgs, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct Args {
|
||||||
|
/// the main source file
|
||||||
|
#[argh(positional)]
|
||||||
|
pub file: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// files to include
|
||||||
|
#[argh(option, short = 'I')]
|
||||||
|
pub include: Vec<PathBuf>,
|
||||||
|
|
||||||
|
/// the CLI operating mode (`f`mt | `l`ex | `r`un)
|
||||||
|
#[argh(option, short = 'm', default = "Default::default()")]
|
||||||
|
pub mode: Mode,
|
||||||
|
|
||||||
|
/// whether to start the repl (`true` or `false`)
|
||||||
|
#[argh(option, short = 'r', default = "is_terminal()")]
|
||||||
|
pub repl: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// gets whether stdin AND stdout are a terminal, for pipelining
|
||||||
|
pub fn is_terminal() -> bool {
|
||||||
|
std::io::stdin().is_terminal() && std::io::stdout().is_terminal()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The CLI's operating mode
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub enum Mode {
|
||||||
|
#[default]
|
||||||
|
Menu,
|
||||||
|
Lex,
|
||||||
|
Fmt,
|
||||||
|
Run,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Mode {
|
||||||
|
type Err = &'static str;
|
||||||
|
fn from_str(s: &str) -> Result<Self, &'static str> {
|
||||||
|
Ok(match s {
|
||||||
|
"f" | "fmt" | "p" | "pretty" => Mode::Fmt,
|
||||||
|
"l" | "lex" | "tokenize" | "token" => Mode::Lex,
|
||||||
|
"r" | "run" => Mode::Run,
|
||||||
|
_ => Err("Recognized modes are: 'r' \"run\", 'f' \"fmt\", 'l' \"lex\"")?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -1,13 +1,5 @@
|
|||||||
use cl_repl::{cli::run, tools::is_terminal};
|
use cl_repl::cli::run;
|
||||||
use std::error::Error;
|
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
if is_terminal() {
|
|
||||||
println!(
|
|
||||||
"--- {} v{} 💪🦈 ---",
|
|
||||||
env!("CARGO_BIN_NAME"),
|
|
||||||
env!("CARGO_PKG_VERSION"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
run(argh::from_env())
|
run(argh::from_env())
|
||||||
}
|
}
|
||||||
|
89
cl-repl/src/cli.rs
Normal file
89
cl-repl/src/cli.rs
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
//! Implement's the command line interface
|
||||||
|
use crate::{
|
||||||
|
args::{Args, Mode},
|
||||||
|
ctx::Context,
|
||||||
|
menu,
|
||||||
|
tools::print_token,
|
||||||
|
};
|
||||||
|
use cl_interpret::{env::Environment, interpret::Interpret, temp_type_impl::ConValue};
|
||||||
|
use cl_lexer::Lexer;
|
||||||
|
use cl_parser::Parser;
|
||||||
|
use std::{error::Error, path::Path};
|
||||||
|
|
||||||
|
/// Run the command line interface
|
||||||
|
pub fn run(args: Args) -> Result<(), Box<dyn Error>> {
|
||||||
|
let Args { file, include, mode, repl } = args;
|
||||||
|
|
||||||
|
let mut env = Environment::new();
|
||||||
|
for path in include {
|
||||||
|
load_file(&mut env, path)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if repl {
|
||||||
|
if let Some(file) = file {
|
||||||
|
load_file(&mut env, file)?;
|
||||||
|
}
|
||||||
|
let mut ctx = Context::with_env(env);
|
||||||
|
match mode {
|
||||||
|
Mode::Menu => menu::main_menu(&mut ctx)?,
|
||||||
|
Mode::Lex => menu::lex(&mut ctx)?,
|
||||||
|
Mode::Fmt => menu::fmt(&mut ctx)?,
|
||||||
|
Mode::Run => menu::run(&mut ctx)?,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let code = match &file {
|
||||||
|
Some(file) => std::fs::read_to_string(file)?,
|
||||||
|
None => std::io::read_to_string(std::io::stdin())?,
|
||||||
|
};
|
||||||
|
|
||||||
|
match mode {
|
||||||
|
Mode::Lex => lex_code(&code, file),
|
||||||
|
Mode::Fmt => fmt_code(&code),
|
||||||
|
Mode::Run | Mode::Menu => run_code(&code, &mut env),
|
||||||
|
}?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_file(
|
||||||
|
env: &mut Environment,
|
||||||
|
path: impl AsRef<Path>,
|
||||||
|
) -> Result<ConValue, Box<dyn Error>> {
|
||||||
|
let file = std::fs::read_to_string(path)?;
|
||||||
|
let code = Parser::new(Lexer::new(&file)).file()?;
|
||||||
|
Ok(env.eval(&code)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lex_code(code: &str, path: Option<impl AsRef<Path>>) -> Result<(), Box<dyn Error>> {
|
||||||
|
for token in Lexer::new(code) {
|
||||||
|
if let Some(path) = &path {
|
||||||
|
print!("{}:", path.as_ref().display());
|
||||||
|
}
|
||||||
|
match token {
|
||||||
|
Ok(token) => print_token(&token),
|
||||||
|
Err(e) => println!("{e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fmt_code(code: &str) -> Result<(), Box<dyn Error>> {
|
||||||
|
let code = Parser::new(Lexer::new(code)).file()?;
|
||||||
|
println!("{code}");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_code(code: &str, env: &mut Environment) -> Result<(), Box<dyn Error>> {
|
||||||
|
let code = Parser::new(Lexer::new(code)).file()?;
|
||||||
|
match code.interpret(env)? {
|
||||||
|
ConValue::Empty => {}
|
||||||
|
ret => println!("{ret}"),
|
||||||
|
}
|
||||||
|
if env.get("main").is_ok() {
|
||||||
|
match env.call("main", &[])? {
|
||||||
|
ConValue::Empty => {}
|
||||||
|
ret => println!("{ret}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
26
cl-repl/src/ctx.rs
Normal file
26
cl-repl/src/ctx.rs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
use cl_interpret::{
|
||||||
|
env::Environment, error::IResult, interpret::Interpret, temp_type_impl::ConValue,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Context {
|
||||||
|
pub env: Environment,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Context {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { env: Environment::new() }
|
||||||
|
}
|
||||||
|
pub fn with_env(env: Environment) -> Self {
|
||||||
|
Self { env }
|
||||||
|
}
|
||||||
|
pub fn run(&mut self, code: &impl Interpret) -> IResult<ConValue> {
|
||||||
|
code.interpret(&mut self.env)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Context {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
@ -1,446 +1,11 @@
|
|||||||
//! Utilities for cl-frontend
|
//! The Conlang REPL, based on [repline]
|
||||||
//!
|
//!
|
||||||
//! # TODO
|
//! Uses [argh] for argument parsing.
|
||||||
//! - [ ] Readline-like line editing
|
|
||||||
//! - [ ] Raw mode?
|
|
||||||
#![warn(clippy::all)]
|
#![warn(clippy::all)]
|
||||||
|
|
||||||
pub mod ansi {
|
pub mod ansi;
|
||||||
// ANSI color escape sequences
|
pub mod args;
|
||||||
pub const ANSI_RED: &str = "\x1b[31m";
|
pub mod cli;
|
||||||
pub const ANSI_GREEN: &str = "\x1b[32m"; // the color of type checker mode
|
pub mod ctx;
|
||||||
pub const ANSI_CYAN: &str = "\x1b[36m";
|
pub mod menu;
|
||||||
// pub const ANSI_BRIGHT_GREEN: &str = "\x1b[92m";
|
pub mod tools;
|
||||||
pub const ANSI_BRIGHT_BLUE: &str = "\x1b[94m";
|
|
||||||
pub const ANSI_BRIGHT_MAGENTA: &str = "\x1b[95m";
|
|
||||||
// const ANSI_BRIGHT_CYAN: &str = "\x1b[96m";
|
|
||||||
pub const ANSI_RESET: &str = "\x1b[0m";
|
|
||||||
pub const ANSI_OUTPUT: &str = "\x1b[38;5;117m";
|
|
||||||
|
|
||||||
pub const ANSI_CLEAR_LINES: &str = "\x1b[G\x1b[J";
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod args {
|
|
||||||
use crate::tools::is_terminal;
|
|
||||||
use argh::FromArgs;
|
|
||||||
use std::{path::PathBuf, str::FromStr};
|
|
||||||
|
|
||||||
/// The Conlang prototype debug interface
|
|
||||||
#[derive(Clone, Debug, FromArgs, PartialEq, Eq, PartialOrd, Ord)]
|
|
||||||
pub struct Args {
|
|
||||||
/// the main source file
|
|
||||||
#[argh(positional)]
|
|
||||||
pub file: Option<PathBuf>,
|
|
||||||
|
|
||||||
/// files to include
|
|
||||||
#[argh(option, short = 'I')]
|
|
||||||
pub include: Vec<PathBuf>,
|
|
||||||
|
|
||||||
/// the Repl mode to start in
|
|
||||||
#[argh(option, short = 'm', default = "Default::default()")]
|
|
||||||
pub mode: Mode,
|
|
||||||
|
|
||||||
/// whether to start the repl (`true` or `false`)
|
|
||||||
#[argh(option, short = 'r', default = "is_terminal()")]
|
|
||||||
pub repl: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The CLI's operating mode
|
|
||||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
|
|
||||||
pub enum Mode {
|
|
||||||
Tokenize,
|
|
||||||
Beautify,
|
|
||||||
#[default]
|
|
||||||
Interpret,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Mode {
|
|
||||||
pub fn ansi_color(self) -> &'static str {
|
|
||||||
use super::ansi::*;
|
|
||||||
match self {
|
|
||||||
Mode::Tokenize => ANSI_BRIGHT_BLUE,
|
|
||||||
Mode::Beautify => ANSI_BRIGHT_MAGENTA,
|
|
||||||
Mode::Interpret => ANSI_CYAN,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for Mode {
|
|
||||||
type Err = &'static str;
|
|
||||||
fn from_str(s: &str) -> Result<Self, &'static str> {
|
|
||||||
Ok(match s {
|
|
||||||
"i" | "interpret" | "r" | "run" => Mode::Interpret,
|
|
||||||
"b" | "beautify" | "p" | "pretty" => Mode::Beautify,
|
|
||||||
"t" | "tokenize" | "token" => Mode::Tokenize,
|
|
||||||
_ => Err("Recognized modes are: 'r' \"run\", 'p' \"pretty\", 't' \"token\"")?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod program {
|
|
||||||
use cl_ast::ast;
|
|
||||||
use cl_interpret::{
|
|
||||||
env::Environment, error::IResult, interpret::Interpret, temp_type_impl::ConValue,
|
|
||||||
};
|
|
||||||
use cl_lexer::Lexer;
|
|
||||||
use cl_parser::{error::PResult, Parser};
|
|
||||||
use std::fmt::Display;
|
|
||||||
|
|
||||||
pub struct Parsable;
|
|
||||||
|
|
||||||
pub enum Parsed {
|
|
||||||
File(ast::File),
|
|
||||||
Stmt(ast::Stmt),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Program<'t, Variant> {
|
|
||||||
text: &'t str,
|
|
||||||
data: Variant,
|
|
||||||
}
|
|
||||||
impl<'t, V> Program<'t, V> {
|
|
||||||
pub fn lex(&self) -> Lexer {
|
|
||||||
Lexer::new(self.text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'t> Program<'t, Parsable> {
|
|
||||||
pub fn new(text: &'t str) -> Self {
|
|
||||||
Self { text, data: Parsable }
|
|
||||||
}
|
|
||||||
pub fn parse(self) -> PResult<Program<'t, Parsed>> {
|
|
||||||
self.parse_file().or_else(|_| self.parse_stmt())
|
|
||||||
}
|
|
||||||
pub fn parse_stmt(&self) -> PResult<Program<'t, Parsed>> {
|
|
||||||
let stmt = Parser::new(self.lex()).stmt()?;
|
|
||||||
// let stmt = WhileElseDesugar.fold_stmt(stmt);
|
|
||||||
|
|
||||||
Ok(Program { data: Parsed::Stmt(stmt), text: self.text })
|
|
||||||
}
|
|
||||||
pub fn parse_file(&self) -> PResult<Program<'t, Parsed>> {
|
|
||||||
let file = Parser::new(self.lex()).file()?;
|
|
||||||
// let file = WhileElseDesugar.fold_file(file);
|
|
||||||
|
|
||||||
Ok(Program { data: Parsed::File(file), text: self.text })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'t> Program<'t, Parsed> {
|
|
||||||
pub fn debug(&self) {
|
|
||||||
match &self.data {
|
|
||||||
Parsed::File(v) => eprintln!("{v:?}"),
|
|
||||||
Parsed::Stmt(v) => eprintln!("{v:?}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn print(&self) {
|
|
||||||
match &self.data {
|
|
||||||
Parsed::File(v) => println!("{v}"),
|
|
||||||
Parsed::Stmt(v) => println!("{v}"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run(&self, env: &mut Environment) -> IResult<ConValue> {
|
|
||||||
match &self.data {
|
|
||||||
Parsed::File(v) => v.interpret(env),
|
|
||||||
Parsed::Stmt(v) => v.interpret(env),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'t> Display for Program<'t, Parsed> {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match &self.data {
|
|
||||||
Parsed::File(v) => write!(f, "{v}"),
|
|
||||||
Parsed::Stmt(v) => write!(f, "{v}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod cli {
|
|
||||||
//! Implement's the command line interface
|
|
||||||
use crate::{
|
|
||||||
args::{Args, Mode},
|
|
||||||
program::{Parsable, Program},
|
|
||||||
repl::Repl,
|
|
||||||
tools::print_token,
|
|
||||||
};
|
|
||||||
use cl_interpret::{env::Environment, temp_type_impl::ConValue};
|
|
||||||
use cl_lexer::Lexer;
|
|
||||||
use cl_parser::Parser;
|
|
||||||
use std::{error::Error, path::Path};
|
|
||||||
|
|
||||||
/// Run the command line interface
|
|
||||||
pub fn run(args: Args) -> Result<(), Box<dyn Error>> {
|
|
||||||
let Args { file, include, mode, repl } = args;
|
|
||||||
|
|
||||||
let mut env = Environment::new();
|
|
||||||
for path in include {
|
|
||||||
load_file(&mut env, path)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if repl {
|
|
||||||
if let Some(file) = file {
|
|
||||||
load_file(&mut env, file)?;
|
|
||||||
}
|
|
||||||
Repl::with_env(mode, env).repl()
|
|
||||||
} else {
|
|
||||||
let code = match &file {
|
|
||||||
Some(file) => std::fs::read_to_string(file)?,
|
|
||||||
None => std::io::read_to_string(std::io::stdin())?,
|
|
||||||
};
|
|
||||||
let code = Program::new(&code);
|
|
||||||
|
|
||||||
match mode {
|
|
||||||
Mode::Tokenize => tokenize(code, file),
|
|
||||||
Mode::Beautify => beautify(code),
|
|
||||||
Mode::Interpret => interpret(code, &mut env),
|
|
||||||
}?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_file(
|
|
||||||
env: &mut Environment,
|
|
||||||
path: impl AsRef<Path>,
|
|
||||||
) -> Result<ConValue, Box<dyn Error>> {
|
|
||||||
let file = std::fs::read_to_string(path)?;
|
|
||||||
let code = Parser::new(Lexer::new(&file)).file()?;
|
|
||||||
env.eval(&code).map_err(Into::into)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn tokenize(
|
|
||||||
code: Program<Parsable>,
|
|
||||||
path: Option<impl AsRef<Path>>,
|
|
||||||
) -> Result<(), Box<dyn Error>> {
|
|
||||||
for token in code.lex() {
|
|
||||||
if let Some(ref path) = path {
|
|
||||||
print!("{}:", path.as_ref().display());
|
|
||||||
}
|
|
||||||
match token {
|
|
||||||
Ok(token) => print_token(&token),
|
|
||||||
Err(e) => println!("{e}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn beautify(code: Program<Parsable>) -> Result<(), Box<dyn Error>> {
|
|
||||||
code.parse()?.print();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn interpret(code: Program<Parsable>, env: &mut Environment) -> Result<(), Box<dyn Error>> {
|
|
||||||
match code.parse()?.run(env)? {
|
|
||||||
ConValue::Empty => {}
|
|
||||||
ret => println!("{ret}"),
|
|
||||||
}
|
|
||||||
if env.get("main").is_ok() {
|
|
||||||
match env.call("main", &[])? {
|
|
||||||
ConValue::Empty => {}
|
|
||||||
ret => println!("{ret}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod repl {
|
|
||||||
use crate::{
|
|
||||||
ansi::*,
|
|
||||||
args::Mode,
|
|
||||||
program::{Parsable, Parsed, Program},
|
|
||||||
tools::print_token,
|
|
||||||
};
|
|
||||||
use cl_interpret::{env::Environment, temp_type_impl::ConValue};
|
|
||||||
use std::fmt::Display;
|
|
||||||
|
|
||||||
/// Implements the interactive interpreter
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct Repl {
|
|
||||||
prompt_again: &'static str, // " ?>"
|
|
||||||
prompt_begin: &'static str, // "cl>"
|
|
||||||
prompt_error: &'static str, // "! >"
|
|
||||||
prompt_succs: &'static str, // " ->"
|
|
||||||
env: Environment,
|
|
||||||
mode: Mode,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Repl {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
prompt_begin: "cl>",
|
|
||||||
prompt_again: " ?>",
|
|
||||||
prompt_error: "! >",
|
|
||||||
prompt_succs: " =>",
|
|
||||||
env: Default::default(),
|
|
||||||
mode: Default::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Prompt functions
|
|
||||||
impl Repl {
|
|
||||||
pub fn prompt_result<T: Display, E: Display>(&self, res: Result<T, E>) {
|
|
||||||
match &res {
|
|
||||||
Ok(v) => self.prompt_succs(v),
|
|
||||||
Err(e) => self.prompt_error(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn prompt_error(&self, err: &impl Display) {
|
|
||||||
let Self { prompt_error: prompt, .. } = self;
|
|
||||||
println!("{ANSI_CLEAR_LINES}{ANSI_RED}{prompt} {err}{ANSI_RESET}")
|
|
||||||
}
|
|
||||||
pub fn prompt_succs(&self, value: &impl Display) {
|
|
||||||
let Self { prompt_succs: _prompt, .. } = self;
|
|
||||||
println!("{ANSI_GREEN}{value}{ANSI_RESET}")
|
|
||||||
}
|
|
||||||
/// Resets the cursor to the start of the line, clears the terminal,
|
|
||||||
/// and sets the output color
|
|
||||||
pub fn begin_output(&self) {
|
|
||||||
print!("{ANSI_CLEAR_LINES}{ANSI_OUTPUT}")
|
|
||||||
}
|
|
||||||
pub fn clear_line(&self) {}
|
|
||||||
}
|
|
||||||
/// The actual REPL
|
|
||||||
impl Repl {
|
|
||||||
/// Constructs a new [Repl] with the provided [Mode]
|
|
||||||
pub fn new(mode: Mode) -> Self {
|
|
||||||
Self { mode, ..Default::default() }
|
|
||||||
}
|
|
||||||
/// Constructs a new [Repl] with the provided [Mode] and [Environment]
|
|
||||||
pub fn with_env(mode: Mode, env: Environment) -> Self {
|
|
||||||
Self { mode, env, ..Default::default() }
|
|
||||||
}
|
|
||||||
/// Runs the main REPL loop
|
|
||||||
pub fn repl(&mut self) {
|
|
||||||
use crate::repline::{error::Error, Repline};
|
|
||||||
|
|
||||||
let mut rl = Repline::new(self.mode.ansi_color(), self.prompt_begin, self.prompt_again);
|
|
||||||
fn clear_line() {
|
|
||||||
print!("\x1b[G\x1b[J");
|
|
||||||
}
|
|
||||||
loop {
|
|
||||||
let buf = match rl.read() {
|
|
||||||
Ok(buf) => buf,
|
|
||||||
// Ctrl-C: break if current line is empty
|
|
||||||
Err(Error::CtrlC(buf)) => {
|
|
||||||
if buf.is_empty() || buf.ends_with('\n') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
rl.accept();
|
|
||||||
println!("Cancelled. (Press Ctrl+C again to quit.)");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Ctrl-D: reset input, and parse it for errors
|
|
||||||
Err(Error::CtrlD(buf)) => {
|
|
||||||
rl.deny();
|
|
||||||
if let Err(e) = Program::new(&buf).parse() {
|
|
||||||
clear_line();
|
|
||||||
self.prompt_error(&e);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
self.prompt_error(&e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
self.begin_output();
|
|
||||||
if self.command(&buf) {
|
|
||||||
rl.deny();
|
|
||||||
rl.set_color(self.mode.ansi_color());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let code = Program::new(&buf);
|
|
||||||
if self.mode == Mode::Tokenize {
|
|
||||||
self.tokenize(&code);
|
|
||||||
rl.deny();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
match code.lex().into_iter().find(|l| l.is_err()) {
|
|
||||||
None => {}
|
|
||||||
Some(Ok(_)) => unreachable!(),
|
|
||||||
Some(Err(error)) => {
|
|
||||||
rl.deny();
|
|
||||||
self.prompt_error(&error);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Ok(mut code) = code.parse() {
|
|
||||||
rl.accept();
|
|
||||||
self.dispatch(&mut code);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn help(&self) {
|
|
||||||
println!(
|
|
||||||
"Commands:\n- $tokens\n Tokenize Mode:\n Outputs information derived by the Lexer\n- $pretty\n Beautify Mode:\n Pretty-prints the input\n- $run\n Interpret Mode:\n Interprets the input using Conlang\'s work-in-progress interpreter\n- $mode\n Prints the current mode\n- $help\n Prints this help message"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
fn command(&mut self, line: &str) -> bool {
|
|
||||||
let Some(line) = line.trim().strip_prefix('$') else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
if let Ok(mode) = line.parse() {
|
|
||||||
self.mode = mode;
|
|
||||||
} else {
|
|
||||||
match line {
|
|
||||||
"$run" => self.mode = Mode::Interpret,
|
|
||||||
"mode" => println!("{:?} Mode", self.mode),
|
|
||||||
"help" => self.help(),
|
|
||||||
_ => return false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
/// Dispatches calls to repl functions based on the program
|
|
||||||
fn dispatch(&mut self, code: &mut Program<Parsed>) {
|
|
||||||
match self.mode {
|
|
||||||
Mode::Tokenize => {}
|
|
||||||
Mode::Beautify => self.beautify(code),
|
|
||||||
Mode::Interpret => self.interpret(code),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn tokenize(&mut self, code: &Program<Parsable>) {
|
|
||||||
for token in code.lex() {
|
|
||||||
match token {
|
|
||||||
Ok(token) => print_token(&token),
|
|
||||||
Err(e) => println!("{e}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn interpret(&mut self, code: &Program<Parsed>) {
|
|
||||||
match code.run(&mut self.env) {
|
|
||||||
Ok(ConValue::Empty) => {}
|
|
||||||
res => self.prompt_result(res),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn beautify(&mut self, code: &Program<Parsed>) {
|
|
||||||
code.print()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod tools {
|
|
||||||
use cl_token::Token;
|
|
||||||
use std::io::IsTerminal;
|
|
||||||
/// Prints a token in the particular way cl-repl does
|
|
||||||
pub fn print_token(t: &Token) {
|
|
||||||
println!(
|
|
||||||
"{:02}:{:02}: {:#19} │{}│",
|
|
||||||
t.line(),
|
|
||||||
t.col(),
|
|
||||||
t.ty(),
|
|
||||||
t.data(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/// gets whether stdin AND stdout are a terminal, for pipelining
|
|
||||||
pub fn is_terminal() -> bool {
|
|
||||||
std::io::stdin().is_terminal() && std::io::stdout().is_terminal()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod repline;
|
|
||||||
|
76
cl-repl/src/menu.rs
Normal file
76
cl-repl/src/menu.rs
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
use crate::{ansi, ctx};
|
||||||
|
use cl_lexer::Lexer;
|
||||||
|
use cl_parser::Parser;
|
||||||
|
use repline::{error::ReplResult, prebaked::*};
|
||||||
|
|
||||||
|
fn clear() {
|
||||||
|
println!("{}", ansi::CLEAR_ALL);
|
||||||
|
banner()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn banner() {
|
||||||
|
println!("--- conlang v{} 💪🦈 ---\n", env!("CARGO_PKG_VERSION"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Presents a selection interface to the user
|
||||||
|
pub fn main_menu(ctx: &mut ctx::Context) -> ReplResult<()> {
|
||||||
|
banner();
|
||||||
|
read_and(ansi::GREEN, "mu>", " ?>", |line| {
|
||||||
|
match line.trim() {
|
||||||
|
"clear" => clear(),
|
||||||
|
"l" | "lex" => lex(ctx)?,
|
||||||
|
"f" | "fmt" => fmt(ctx)?,
|
||||||
|
"r" | "run" => run(ctx)?,
|
||||||
|
"q" | "quit" => return Ok(Response::Break),
|
||||||
|
"h" | "help" => println!(
|
||||||
|
"Valid commands
|
||||||
|
lex (l): Spin up a lexer, and lex some lines
|
||||||
|
fmt (f): Format the input
|
||||||
|
run (r): Enter the REPL, and evaluate some statements
|
||||||
|
help (h): Print this list
|
||||||
|
quit (q): Exit the program"
|
||||||
|
),
|
||||||
|
_ => Err("Unknown command. Type \"help\" for help")?,
|
||||||
|
}
|
||||||
|
Ok(Response::Accept)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(ctx: &mut ctx::Context) -> ReplResult<()> {
|
||||||
|
read_and(ansi::CYAN, "cl>", " ?>", |line| {
|
||||||
|
let code = Parser::new(Lexer::new(line)).stmt()?;
|
||||||
|
|
||||||
|
print!("{}", ansi::OUTPUT);
|
||||||
|
match ctx.run(&code) {
|
||||||
|
Ok(v) => println!("{}{v}", ansi::RESET),
|
||||||
|
Err(e) => println!("{}! > {e}{}", ansi::RED, ansi::RESET),
|
||||||
|
}
|
||||||
|
Ok(Response::Accept)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn lex(_ctx: &mut ctx::Context) -> ReplResult<()> {
|
||||||
|
read_and(ansi::BRIGHT_BLUE, "lx>", " ?>", |line| {
|
||||||
|
for token in Lexer::new(line) {
|
||||||
|
match token {
|
||||||
|
Ok(token) => crate::tools::print_token(&token),
|
||||||
|
Err(e) => eprintln!("! > {}{e}{}", ansi::RED, ansi::RESET),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Response::Accept)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fmt(_ctx: &mut ctx::Context) -> ReplResult<()> {
|
||||||
|
read_and(ansi::BRIGHT_MAGENTA, "cl>", " ?>", |line| {
|
||||||
|
let mut p = Parser::new(Lexer::new(line));
|
||||||
|
|
||||||
|
match p.stmt() {
|
||||||
|
Ok(code) => println!("{}{code}{}", ansi::OUTPUT, ansi::RESET),
|
||||||
|
Err(e) => Err(e)?,
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Response::Accept)
|
||||||
|
})
|
||||||
|
}
|
@ -1,607 +0,0 @@
|
|||||||
//! A small pseudo-multiline editing library
|
|
||||||
// #![allow(unused)]
|
|
||||||
|
|
||||||
pub mod error {
|
|
||||||
/// Result type for Repline
|
|
||||||
pub type ReplResult<T> = std::result::Result<T, Error>;
|
|
||||||
/// Borrowed error (does not implement [Error](std::error::Error)!)
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
/// User broke with Ctrl+C
|
|
||||||
CtrlC(String),
|
|
||||||
/// User broke with Ctrl+D
|
|
||||||
CtrlD(String),
|
|
||||||
/// Invalid unicode codepoint
|
|
||||||
BadUnicode(u32),
|
|
||||||
/// Error came from [std::io]
|
|
||||||
IoFailure(std::io::Error),
|
|
||||||
/// End of input
|
|
||||||
EndOfInput,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::error::Error for Error {}
|
|
||||||
impl std::fmt::Display for Error {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
Error::CtrlC(_) => write!(f, "Ctrl+C"),
|
|
||||||
Error::CtrlD(_) => write!(f, "Ctrl+D"),
|
|
||||||
Error::BadUnicode(u) => write!(f, "0x{u:x} is not a valid unicode codepoint"),
|
|
||||||
Error::IoFailure(s) => write!(f, "{s}"),
|
|
||||||
Error::EndOfInput => write!(f, "End of input"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl From<std::io::Error> for Error {
|
|
||||||
fn from(value: std::io::Error) -> Self {
|
|
||||||
Self::IoFailure(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod chars {
|
|
||||||
//! Converts an <code>[Iterator]<Item = [u8]></code> into an
|
|
||||||
//! <code>[Iterator]<Item = [char]></code>
|
|
||||||
|
|
||||||
use super::error::*;
|
|
||||||
|
|
||||||
/// Converts an <code>[Iterator]<Item = [u8]></code> into an
|
|
||||||
/// <code>[Iterator]<Item = [char]></code>
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct Chars<I: Iterator<Item = u8>>(pub I);
|
|
||||||
impl<I: Iterator<Item = u8>> Chars<I> {
|
|
||||||
pub fn new(bytes: I) -> Self {
|
|
||||||
Self(bytes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<I: Iterator<Item = u8>> Iterator for Chars<I> {
|
|
||||||
type Item = ReplResult<char>;
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
|
||||||
let Self(bytes) = self;
|
|
||||||
let start = bytes.next()? as u32;
|
|
||||||
let (mut out, count) = match start {
|
|
||||||
start if start & 0x80 == 0x00 => (start, 0), // ASCII valid range
|
|
||||||
start if start & 0xe0 == 0xc0 => (start & 0x1f, 1), // 1 continuation byte
|
|
||||||
start if start & 0xf0 == 0xe0 => (start & 0x0f, 2), // 2 continuation bytes
|
|
||||||
start if start & 0xf8 == 0xf0 => (start & 0x07, 3), // 3 continuation bytes
|
|
||||||
_ => return None,
|
|
||||||
};
|
|
||||||
for _ in 0..count {
|
|
||||||
let cont = bytes.next()? as u32;
|
|
||||||
if cont & 0xc0 != 0x80 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
out = out << 6 | (cont & 0x3f);
|
|
||||||
}
|
|
||||||
Some(char::from_u32(out).ok_or(Error::BadUnicode(out)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod flatten {
|
|
||||||
//! Flattens an [Iterator] returning [`Result<T, E>`](Result) or [`Option<T>`](Option)
|
|
||||||
//! into a *non-[FusedIterator](std::iter::FusedIterator)* over `T`
|
|
||||||
|
|
||||||
/// Flattens an [Iterator] returning [`Result<T, E>`](Result) or [`Option<T>`](Option)
|
|
||||||
/// into a *non-[FusedIterator](std::iter::FusedIterator)* over `T`
|
|
||||||
pub struct Flatten<T, I: Iterator<Item = T>>(pub I);
|
|
||||||
impl<T, E, I: Iterator<Item = Result<T, E>>> Iterator for Flatten<Result<T, E>, I> {
|
|
||||||
type Item = T;
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
|
||||||
self.0.next()?.ok()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<T, I: Iterator<Item = Option<T>>> Iterator for Flatten<Option<T>, I> {
|
|
||||||
type Item = T;
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
|
||||||
self.0.next()?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod raw {
|
|
||||||
//! Sets the terminal to [`raw`] mode for the duration of the returned object's lifetime.
|
|
||||||
|
|
||||||
/// Sets the terminal to raw mode for the duration of the returned object's lifetime.
|
|
||||||
pub fn raw() -> impl Drop {
|
|
||||||
Raw::default()
|
|
||||||
}
|
|
||||||
struct Raw();
|
|
||||||
impl Default for Raw {
|
|
||||||
fn default() -> Self {
|
|
||||||
std::thread::yield_now();
|
|
||||||
crossterm::terminal::enable_raw_mode()
|
|
||||||
.expect("should be able to transition into raw mode");
|
|
||||||
Raw()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Drop for Raw {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
crossterm::terminal::disable_raw_mode()
|
|
||||||
.expect("should be able to transition out of raw mode");
|
|
||||||
// std::thread::yield_now();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mod out {
|
|
||||||
#![allow(unused)]
|
|
||||||
use std::io::{Result, Write};
|
|
||||||
|
|
||||||
/// A [Writer](Write) that flushes after every wipe
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub(super) struct EagerWriter<W: Write> {
|
|
||||||
out: W,
|
|
||||||
}
|
|
||||||
impl<W: Write> EagerWriter<W> {
|
|
||||||
pub fn new(writer: W) -> Self {
|
|
||||||
Self { out: writer }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<W: Write> Write for EagerWriter<W> {
|
|
||||||
fn write(&mut self, buf: &[u8]) -> Result<usize> {
|
|
||||||
let out = self.out.write(buf)?;
|
|
||||||
self.out.flush()?;
|
|
||||||
Ok(out)
|
|
||||||
}
|
|
||||||
fn flush(&mut self) -> Result<()> {
|
|
||||||
self.out.flush()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
use self::{chars::Chars, editor::Editor, error::*, flatten::Flatten, raw::raw};
|
|
||||||
use std::{
|
|
||||||
collections::VecDeque,
|
|
||||||
io::{stdout, Bytes, Read, Result, Write},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct Repline<'a, R: Read> {
|
|
||||||
input: Chars<Flatten<Result<u8>, Bytes<R>>>,
|
|
||||||
|
|
||||||
history: VecDeque<String>, // previous lines
|
|
||||||
hindex: usize, // current index into the history buffer
|
|
||||||
|
|
||||||
ed: Editor<'a>, // the current line buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, R: Read> Repline<'a, R> {
|
|
||||||
/// Constructs a [Repline] with the given [Reader](Read), color, begin, and again prompts.
|
|
||||||
pub fn with_input(input: R, color: &'a str, begin: &'a str, again: &'a str) -> Self {
|
|
||||||
Self {
|
|
||||||
input: Chars(Flatten(input.bytes())),
|
|
||||||
history: Default::default(),
|
|
||||||
hindex: 0,
|
|
||||||
ed: Editor::new(color, begin, again),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Set the terminal prompt color
|
|
||||||
pub fn set_color(&mut self, color: &'a str) {
|
|
||||||
self.ed.color = color
|
|
||||||
}
|
|
||||||
/// Reads in a line, and returns it for validation
|
|
||||||
pub fn read(&mut self) -> ReplResult<String> {
|
|
||||||
const INDENT: &str = " ";
|
|
||||||
let mut stdout = stdout().lock();
|
|
||||||
let stdout = &mut stdout;
|
|
||||||
let _make_raw = raw();
|
|
||||||
// self.ed.begin_frame(stdout)?;
|
|
||||||
// self.ed.redraw_frame(stdout)?;
|
|
||||||
self.ed.print_head(stdout)?;
|
|
||||||
loop {
|
|
||||||
stdout.flush()?;
|
|
||||||
match self.input.next().ok_or(Error::EndOfInput)?? {
|
|
||||||
// Ctrl+C: End of Text. Immediately exits.
|
|
||||||
// Ctrl+D: End of Transmission. Ends the current line.
|
|
||||||
'\x03' => {
|
|
||||||
drop(_make_raw);
|
|
||||||
writeln!(stdout)?;
|
|
||||||
return Err(Error::CtrlC(self.ed.to_string()));
|
|
||||||
}
|
|
||||||
'\x04' => {
|
|
||||||
drop(_make_raw);
|
|
||||||
writeln!(stdout)?;
|
|
||||||
return Err(Error::CtrlD(self.ed.to_string()));
|
|
||||||
}
|
|
||||||
// Tab: extend line by 4 spaces
|
|
||||||
'\t' => {
|
|
||||||
self.ed.extend(INDENT.chars(), stdout)?;
|
|
||||||
}
|
|
||||||
// ignore newlines, process line feeds. Not sure how cross-platform this is.
|
|
||||||
'\n' => {}
|
|
||||||
'\r' => {
|
|
||||||
self.ed.push('\n', stdout)?;
|
|
||||||
return Ok(self.ed.to_string());
|
|
||||||
}
|
|
||||||
// Ctrl+Backspace in my terminal
|
|
||||||
'\x17' => {
|
|
||||||
self.ed.erase_word(stdout)?;
|
|
||||||
}
|
|
||||||
// Escape sequence
|
|
||||||
'\x1b' => self.escape(stdout)?,
|
|
||||||
// backspace
|
|
||||||
'\x08' | '\x7f' => {
|
|
||||||
let ed = &mut self.ed;
|
|
||||||
if ed.ends_with(INDENT.chars()) {
|
|
||||||
for _ in 0..INDENT.len() {
|
|
||||||
ed.pop(stdout)?;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ed.pop(stdout)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
c if c.is_ascii_control() => {
|
|
||||||
if cfg!(debug_assertions) {
|
|
||||||
self.ed.extend(c.escape_debug(), stdout)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
c => {
|
|
||||||
self.ed.push(c, stdout)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Handle ANSI Escape
|
|
||||||
fn escape<W: Write>(&mut self, w: &mut W) -> ReplResult<()> {
|
|
||||||
match self.input.next().ok_or(Error::EndOfInput)?? {
|
|
||||||
'[' => self.csi(w)?,
|
|
||||||
'O' => todo!("Process alternate character mode"),
|
|
||||||
other => self.ed.extend(other.escape_debug(), w)?,
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
/// Handle ANSI Control Sequence Introducer
|
|
||||||
fn csi<W: Write>(&mut self, w: &mut W) -> ReplResult<()> {
|
|
||||||
match self.input.next().ok_or(Error::EndOfInput)?? {
|
|
||||||
'A' => {
|
|
||||||
self.hindex = self.hindex.saturating_sub(1);
|
|
||||||
self.restore_history(w)?
|
|
||||||
}
|
|
||||||
'B' => {
|
|
||||||
self.hindex = self.hindex.saturating_add(1).min(self.history.len());
|
|
||||||
self.restore_history(w)?
|
|
||||||
}
|
|
||||||
'C' => self.ed.cursor_forward(1, w)?,
|
|
||||||
'D' => self.ed.cursor_back(1, w)?,
|
|
||||||
'H' => self.ed.home(w)?,
|
|
||||||
'F' => self.ed.end(w)?,
|
|
||||||
'3' => {
|
|
||||||
if let '~' = self.input.next().ok_or(Error::EndOfInput)?? {
|
|
||||||
let _ = self.ed.delete(w);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
other => {
|
|
||||||
if cfg!(debug_assertions) {
|
|
||||||
self.ed.extend(other.escape_debug(), w)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
/// Restores the currently selected history
|
|
||||||
pub fn restore_history<W: Write>(&mut self, w: &mut W) -> ReplResult<()> {
|
|
||||||
let Self { history, hindex, ed, .. } = self;
|
|
||||||
ed.undraw(w)?;
|
|
||||||
ed.clear();
|
|
||||||
ed.print_head(w)?;
|
|
||||||
if let Some(history) = history.get(*hindex) {
|
|
||||||
ed.extend(history.chars(), w)?
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Append line to history and clear it
|
|
||||||
pub fn accept(&mut self) {
|
|
||||||
self.history_append(self.ed.iter().collect());
|
|
||||||
self.ed.clear();
|
|
||||||
self.hindex = self.history.len();
|
|
||||||
}
|
|
||||||
/// Append line to history
|
|
||||||
pub fn history_append(&mut self, mut buf: String) {
|
|
||||||
while buf.ends_with(char::is_whitespace) {
|
|
||||||
buf.pop();
|
|
||||||
}
|
|
||||||
if !self.history.contains(&buf) {
|
|
||||||
self.history.push_back(buf)
|
|
||||||
}
|
|
||||||
while self.history.len() > 20 {
|
|
||||||
self.history.pop_front();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Clear the line
|
|
||||||
pub fn deny(&mut self) {
|
|
||||||
self.ed.clear()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Repline<'a, std::io::Stdin> {
|
|
||||||
pub fn new(color: &'a str, begin: &'a str, again: &'a str) -> Self {
|
|
||||||
Self::with_input(std::io::stdin(), color, begin, again)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod editor {
|
|
||||||
use crossterm::{cursor::*, execute, queue, style::*, terminal::*};
|
|
||||||
use std::{collections::VecDeque, fmt::Display, io::Write};
|
|
||||||
|
|
||||||
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)]
|
|
||||||
pub struct Editor<'a> {
|
|
||||||
head: VecDeque<char>,
|
|
||||||
tail: VecDeque<char>,
|
|
||||||
|
|
||||||
pub color: &'a str,
|
|
||||||
begin: &'a str,
|
|
||||||
again: &'a str,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Editor<'a> {
|
|
||||||
pub fn new(color: &'a str, begin: &'a str, again: &'a str) -> Self {
|
|
||||||
Self { head: Default::default(), tail: Default::default(), color, begin, again }
|
|
||||||
}
|
|
||||||
pub fn iter(&self) -> impl Iterator<Item = &char> {
|
|
||||||
self.head.iter()
|
|
||||||
}
|
|
||||||
pub fn undraw<W: Write>(&self, w: &mut W) -> ReplResult<()> {
|
|
||||||
let Self { head, .. } = self;
|
|
||||||
match head.iter().copied().filter(is_newline).count() {
|
|
||||||
0 => write!(w, "\x1b[0G"),
|
|
||||||
lines => write!(w, "\x1b[{}F", lines),
|
|
||||||
}?;
|
|
||||||
queue!(w, Clear(ClearType::FromCursorDown))?;
|
|
||||||
// write!(w, "\x1b[0J")?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
pub fn redraw<W: Write>(&self, w: &mut W) -> ReplResult<()> {
|
|
||||||
let Self { head, tail, color, begin, again } = self;
|
|
||||||
write!(w, "{color}{begin}\x1b[0m ")?;
|
|
||||||
// draw head
|
|
||||||
for c in head {
|
|
||||||
match c {
|
|
||||||
'\n' => write!(w, "\r\n{color}{again}\x1b[0m "),
|
|
||||||
_ => w.write_all({ *c as u32 }.to_le_bytes().as_slice()),
|
|
||||||
}?
|
|
||||||
}
|
|
||||||
// save cursor
|
|
||||||
execute!(w, SavePosition)?;
|
|
||||||
// draw tail
|
|
||||||
for c in tail {
|
|
||||||
match c {
|
|
||||||
'\n' => write!(w, "\r\n{color}{again}\x1b[0m "),
|
|
||||||
_ => write!(w, "{c}"),
|
|
||||||
}?
|
|
||||||
}
|
|
||||||
// restore cursor
|
|
||||||
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) {
|
|
||||||
self.clear();
|
|
||||||
self.head.extend(s.chars())
|
|
||||||
}
|
|
||||||
pub fn clear(&mut self) {
|
|
||||||
self.head.clear();
|
|
||||||
self.tail.clear();
|
|
||||||
}
|
|
||||||
pub fn delete<W: Write>(&mut self, w: &mut W) -> ReplResult<char> {
|
|
||||||
match self.tail.front() {
|
|
||||||
Some('\n') => {
|
|
||||||
self.undraw(w)?;
|
|
||||||
let out = self.tail.pop_front();
|
|
||||||
self.redraw(w)?;
|
|
||||||
out
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
let out = self.tail.pop_front();
|
|
||||||
self.print_tail(w)?;
|
|
||||||
out
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.ok_or(Error::EndOfInput)
|
|
||||||
}
|
|
||||||
pub fn erase_word<W: Write>(&mut self, w: &mut W) -> ReplResult<()> {
|
|
||||||
while self.pop(w)?.filter(|c| !c.is_whitespace()).is_some() {}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
pub fn len(&self) -> usize {
|
|
||||||
self.head.len() + self.tail.len()
|
|
||||||
}
|
|
||||||
pub fn is_empty(&self) -> bool {
|
|
||||||
self.head.is_empty() && self.tail.is_empty()
|
|
||||||
}
|
|
||||||
pub fn ends_with(&self, iter: impl DoubleEndedIterator<Item = char>) -> bool {
|
|
||||||
let mut iter = iter.rev();
|
|
||||||
let mut head = self.head.iter().rev();
|
|
||||||
loop {
|
|
||||||
match (iter.next(), head.next()) {
|
|
||||||
(None, _) => break true,
|
|
||||||
(Some(_), None) => break false,
|
|
||||||
(Some(a), Some(b)) if a != *b => break false,
|
|
||||||
(Some(_), Some(_)) => continue,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Moves the cursor back `steps` steps
|
|
||||||
pub fn cursor_back<W: Write>(&mut self, steps: usize, w: &mut W) -> ReplResult<()> {
|
|
||||||
for _ in 0..steps {
|
|
||||||
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))?,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
/// Moves the cursor forward `steps` steps
|
|
||||||
pub fn cursor_forward<W: Write>(&mut self, steps: usize, w: &mut W) -> ReplResult<()> {
|
|
||||||
for _ in 0..steps {
|
|
||||||
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))?,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
/// Goes to the beginning of the current line
|
|
||||||
pub fn home<W: Write>(&mut self, w: &mut W) -> ReplResult<()> {
|
|
||||||
loop {
|
|
||||||
match self.head.back() {
|
|
||||||
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)?,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, 'e> IntoIterator for &'e Editor<'a> {
|
|
||||||
type Item = &'e char;
|
|
||||||
type IntoIter = std::iter::Chain<
|
|
||||||
std::collections::vec_deque::Iter<'e, char>,
|
|
||||||
std::collections::vec_deque::Iter<'e, char>,
|
|
||||||
>;
|
|
||||||
fn into_iter(self) -> Self::IntoIter {
|
|
||||||
self.head.iter().chain(self.tail.iter())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<'a> Display for Editor<'a> {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
use std::fmt::Write;
|
|
||||||
let Self { head, tail, .. } = self;
|
|
||||||
for c in head {
|
|
||||||
f.write_char(*c)?;
|
|
||||||
}
|
|
||||||
for c in tail {
|
|
||||||
f.write_char(*c)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
11
cl-repl/src/tools.rs
Normal file
11
cl-repl/src/tools.rs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
use cl_token::Token;
|
||||||
|
/// Prints a token in the particular way [cl-repl](crate) does
|
||||||
|
pub fn print_token(t: &Token) {
|
||||||
|
println!(
|
||||||
|
"{:02}:{:02}: {:#19} │{}│",
|
||||||
|
t.line(),
|
||||||
|
t.col(),
|
||||||
|
t.ty(),
|
||||||
|
t.data(),
|
||||||
|
)
|
||||||
|
}
|
11
repline/Cargo.toml
Normal file
11
repline/Cargo.toml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "repline"
|
||||||
|
repository.workspace = true
|
||||||
|
version.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
crossterm = "0.27.0"
|
283
repline/src/editor.rs
Normal file
283
repline/src/editor.rs
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
//! The [Editor]
|
||||||
|
|
||||||
|
use crossterm::{cursor::*, execute, queue, style::*, terminal::*};
|
||||||
|
use std::{collections::VecDeque, fmt::Display, io::Write};
|
||||||
|
|
||||||
|
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(Clone, Debug)]
|
||||||
|
pub struct Editor<'a> {
|
||||||
|
head: VecDeque<char>,
|
||||||
|
tail: VecDeque<char>,
|
||||||
|
|
||||||
|
pub color: &'a str,
|
||||||
|
begin: &'a str,
|
||||||
|
again: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Editor<'a> {
|
||||||
|
pub fn new(color: &'a str, begin: &'a str, again: &'a str) -> Self {
|
||||||
|
Self { head: Default::default(), tail: Default::default(), color, begin, again }
|
||||||
|
}
|
||||||
|
pub fn iter(&self) -> impl Iterator<Item = &char> {
|
||||||
|
let Self { head, tail, .. } = self;
|
||||||
|
head.iter().chain(tail.iter())
|
||||||
|
}
|
||||||
|
pub fn undraw<W: Write>(&self, w: &mut W) -> ReplResult<()> {
|
||||||
|
let Self { head, .. } = self;
|
||||||
|
match head.iter().copied().filter(is_newline).count() {
|
||||||
|
0 => write!(w, "\x1b[0G"),
|
||||||
|
lines => write!(w, "\x1b[{}F", lines),
|
||||||
|
}?;
|
||||||
|
queue!(w, Clear(ClearType::FromCursorDown))?;
|
||||||
|
// write!(w, "\x1b[0J")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub fn redraw<W: Write>(&self, w: &mut W) -> ReplResult<()> {
|
||||||
|
let Self { head, tail, color, begin, again } = self;
|
||||||
|
write!(w, "{color}{begin}\x1b[0m ")?;
|
||||||
|
// draw head
|
||||||
|
for c in head {
|
||||||
|
match c {
|
||||||
|
'\n' => write!(w, "\r\n{color}{again}\x1b[0m "),
|
||||||
|
_ => w.write_all({ *c as u32 }.to_le_bytes().as_slice()),
|
||||||
|
}?
|
||||||
|
}
|
||||||
|
// save cursor
|
||||||
|
execute!(w, SavePosition)?;
|
||||||
|
// draw tail
|
||||||
|
for c in tail {
|
||||||
|
match c {
|
||||||
|
'\n' => write!(w, "\r\n{color}{again}\x1b[0m "),
|
||||||
|
_ => write!(w, "{c}"),
|
||||||
|
}?
|
||||||
|
}
|
||||||
|
// restore cursor
|
||||||
|
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) {
|
||||||
|
self.clear();
|
||||||
|
self.head.extend(s.chars())
|
||||||
|
}
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.head.clear();
|
||||||
|
self.tail.clear();
|
||||||
|
}
|
||||||
|
pub fn delete<W: Write>(&mut self, w: &mut W) -> ReplResult<char> {
|
||||||
|
match self.tail.front() {
|
||||||
|
Some('\n') => {
|
||||||
|
self.undraw(w)?;
|
||||||
|
let out = self.tail.pop_front();
|
||||||
|
self.redraw(w)?;
|
||||||
|
out
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let out = self.tail.pop_front();
|
||||||
|
self.print_tail(w)?;
|
||||||
|
out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ok_or(Error::EndOfInput)
|
||||||
|
}
|
||||||
|
pub fn erase_word<W: Write>(&mut self, w: &mut W) -> ReplResult<()> {
|
||||||
|
while self.pop(w)?.filter(|c| !c.is_whitespace()).is_some() {}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.head.len() + self.tail.len()
|
||||||
|
}
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.head.is_empty() && self.tail.is_empty()
|
||||||
|
}
|
||||||
|
pub fn ends_with(&self, iter: impl DoubleEndedIterator<Item = char>) -> bool {
|
||||||
|
let mut iter = iter.rev();
|
||||||
|
let mut head = self.head.iter().rev();
|
||||||
|
loop {
|
||||||
|
match (iter.next(), head.next()) {
|
||||||
|
(None, _) => break true,
|
||||||
|
(Some(_), None) => break false,
|
||||||
|
(Some(a), Some(b)) if a != *b => break false,
|
||||||
|
(Some(_), Some(_)) => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Moves the cursor back `steps` steps
|
||||||
|
pub fn cursor_back<W: Write>(&mut self, steps: usize, w: &mut W) -> ReplResult<()> {
|
||||||
|
for _ in 0..steps {
|
||||||
|
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))?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
/// Moves the cursor forward `steps` steps
|
||||||
|
pub fn cursor_forward<W: Write>(&mut self, steps: usize, w: &mut W) -> ReplResult<()> {
|
||||||
|
for _ in 0..steps {
|
||||||
|
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))?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
/// Goes to the beginning of the current line
|
||||||
|
pub fn home<W: Write>(&mut self, w: &mut W) -> ReplResult<()> {
|
||||||
|
loop {
|
||||||
|
match self.head.back() {
|
||||||
|
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)?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'e> IntoIterator for &'e Editor<'a> {
|
||||||
|
type Item = &'e char;
|
||||||
|
type IntoIter = std::iter::Chain<
|
||||||
|
std::collections::vec_deque::Iter<'e, char>,
|
||||||
|
std::collections::vec_deque::Iter<'e, char>,
|
||||||
|
>;
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
self.head.iter().chain(self.tail.iter())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<'a> Display for Editor<'a> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
use std::fmt::Write;
|
||||||
|
for c in self.iter() {
|
||||||
|
f.write_char(*c)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
42
repline/src/error.rs
Normal file
42
repline/src/error.rs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
use crate::iter::chars::BadUnicode;
|
||||||
|
|
||||||
|
/// Result type for Repline
|
||||||
|
pub type ReplResult<T> = std::result::Result<T, Error>;
|
||||||
|
/// Borrowed error (does not implement [Error](std::error::Error)!)
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
/// User broke with Ctrl+C
|
||||||
|
CtrlC(String),
|
||||||
|
/// User broke with Ctrl+D
|
||||||
|
CtrlD(String),
|
||||||
|
/// Invalid unicode codepoint
|
||||||
|
BadUnicode(u32),
|
||||||
|
/// Error came from [std::io]
|
||||||
|
IoFailure(std::io::Error),
|
||||||
|
/// End of input
|
||||||
|
EndOfInput,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for Error {}
|
||||||
|
impl std::fmt::Display for Error {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Error::CtrlC(_) => write!(f, "Ctrl+C"),
|
||||||
|
Error::CtrlD(_) => write!(f, "Ctrl+D"),
|
||||||
|
Error::BadUnicode(u) => write!(f, "\\u{{{u:x}}} is not a valid unicode codepoint"),
|
||||||
|
Error::IoFailure(s) => write!(f, "{s}"),
|
||||||
|
Error::EndOfInput => write!(f, "End of input"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<std::io::Error> for Error {
|
||||||
|
fn from(value: std::io::Error) -> Self {
|
||||||
|
Self::IoFailure(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<BadUnicode> for Error {
|
||||||
|
fn from(value: BadUnicode) -> Self {
|
||||||
|
let BadUnicode(code) = value;
|
||||||
|
Self::BadUnicode(code)
|
||||||
|
}
|
||||||
|
}
|
68
repline/src/iter.rs
Normal file
68
repline/src/iter.rs
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
//! Shmancy iterator adapters
|
||||||
|
|
||||||
|
pub use chars::Chars;
|
||||||
|
pub use flatten::Flatten;
|
||||||
|
|
||||||
|
pub mod chars {
|
||||||
|
//! Converts an <code>[Iterator]<Item = [u8]></code> into an
|
||||||
|
//! <code>[Iterator]<Item = [Result]<[char], [BadUnicode]>></code>
|
||||||
|
|
||||||
|
/// Invalid unicode codepoint found when iterating over [Chars]
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||||
|
pub struct BadUnicode(pub u32);
|
||||||
|
impl std::error::Error for BadUnicode {}
|
||||||
|
impl std::fmt::Display for BadUnicode {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let Self(code) = self;
|
||||||
|
write!(f, "Bad unicode: {code}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts an <code>[Iterator]<Item = [u8]></code> into an
|
||||||
|
/// <code>[Iterator]<Item = [char]></code>
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Chars<I: Iterator<Item = u8>>(pub I);
|
||||||
|
impl<I: Iterator<Item = u8>> Iterator for Chars<I> {
|
||||||
|
type Item = Result<char, BadUnicode>;
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
let Self(bytes) = self;
|
||||||
|
let start = bytes.next()? as u32;
|
||||||
|
let (mut out, count) = match start {
|
||||||
|
start if start & 0x80 == 0x00 => (start, 0), // ASCII valid range
|
||||||
|
start if start & 0xe0 == 0xc0 => (start & 0x1f, 1), // 1 continuation byte
|
||||||
|
start if start & 0xf0 == 0xe0 => (start & 0x0f, 2), // 2 continuation bytes
|
||||||
|
start if start & 0xf8 == 0xf0 => (start & 0x07, 3), // 3 continuation bytes
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
for _ in 0..count {
|
||||||
|
let cont = bytes.next()? as u32;
|
||||||
|
if cont & 0xc0 != 0x80 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
out = out << 6 | (cont & 0x3f);
|
||||||
|
}
|
||||||
|
Some(char::from_u32(out).ok_or(BadUnicode(out)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub mod flatten {
|
||||||
|
//! Flattens an [Iterator] returning [`Result<T, E>`](Result) or [`Option<T>`](Option)
|
||||||
|
//! into a *non-[FusedIterator](std::iter::FusedIterator)* over `T`
|
||||||
|
|
||||||
|
/// Flattens an [Iterator] returning [`Result<T, E>`](Result) or [`Option<T>`](Option)
|
||||||
|
/// into a *non-[FusedIterator](std::iter::FusedIterator)* over `T`
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Flatten<T, I: Iterator<Item = T>>(pub I);
|
||||||
|
impl<T, E, I: Iterator<Item = Result<T, E>>> Iterator for Flatten<Result<T, E>, I> {
|
||||||
|
type Item = T;
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
self.0.next()?.ok()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<T, I: Iterator<Item = Option<T>>> Iterator for Flatten<Option<T>, I> {
|
||||||
|
type Item = T;
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
self.0.next()?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
repline/src/lib.rs
Normal file
13
repline/src/lib.rs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
//! A small pseudo-multiline editing library
|
||||||
|
|
||||||
|
mod editor;
|
||||||
|
mod iter;
|
||||||
|
mod raw;
|
||||||
|
|
||||||
|
pub mod error;
|
||||||
|
pub mod prebaked;
|
||||||
|
pub mod repline;
|
||||||
|
|
||||||
|
pub use error::Error;
|
||||||
|
pub use prebaked::{read_and, Response};
|
||||||
|
pub use repline::Repline;
|
56
repline/src/prebaked.rs
Normal file
56
repline/src/prebaked.rs
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
//! Here's a menu I prepared earlier!
|
||||||
|
//!
|
||||||
|
//! Constructs a [Repline] and repeatedly runs the provided closure on the input strings,
|
||||||
|
//! obeying the closure's [Response].
|
||||||
|
|
||||||
|
use std::error::Error;
|
||||||
|
|
||||||
|
use crate::{error::Error as RlError, repline::Repline};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||||
|
/// Control codes for the [prebaked menu](read_and)
|
||||||
|
pub enum Response {
|
||||||
|
/// Accept the line, and save it to history
|
||||||
|
Accept,
|
||||||
|
/// Reject the line, and clear the buffer
|
||||||
|
Deny,
|
||||||
|
/// End the loop
|
||||||
|
Break,
|
||||||
|
/// Gather more input and try again
|
||||||
|
Continue,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implements a basic menu loop using an embedded [Repline].
|
||||||
|
///
|
||||||
|
/// Repeatedly runs the provided closure on the input strings,
|
||||||
|
/// obeying the closure's [Response].
|
||||||
|
///
|
||||||
|
/// Captures and displays all user [Error]s.
|
||||||
|
///
|
||||||
|
/// # Keybinds
|
||||||
|
/// - `Ctrl+C` exits the loop
|
||||||
|
/// - `Ctrl+D` clears the input, but *runs the closure* with the old input
|
||||||
|
pub fn read_and<F>(color: &str, begin: &str, again: &str, mut f: F) -> Result<(), RlError>
|
||||||
|
where F: FnMut(&str) -> Result<Response, Box<dyn Error>> {
|
||||||
|
let mut rl = Repline::new(color, begin, again);
|
||||||
|
loop {
|
||||||
|
let line = match rl.read() {
|
||||||
|
Err(RlError::CtrlC(_)) => break,
|
||||||
|
Err(RlError::CtrlD(line)) => {
|
||||||
|
rl.deny();
|
||||||
|
line
|
||||||
|
}
|
||||||
|
Ok(line) => line,
|
||||||
|
Err(e) => Err(e)?,
|
||||||
|
};
|
||||||
|
print!("\x1b[G\x1b[J");
|
||||||
|
match f(&line) {
|
||||||
|
Ok(Response::Accept) => rl.accept(),
|
||||||
|
Ok(Response::Deny) => rl.deny(),
|
||||||
|
Ok(Response::Break) => break,
|
||||||
|
Ok(Response::Continue) => continue,
|
||||||
|
Err(e) => print!("\x1b[40G\x1b[A\x1bJ\x1b[91m{e}\x1b[0m\x1b[B"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
21
repline/src/raw.rs
Normal file
21
repline/src/raw.rs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
//! Sets the terminal to [`raw`] mode for the duration of the returned object's lifetime.
|
||||||
|
|
||||||
|
/// Sets the terminal to raw mode for the duration of the returned object's lifetime.
|
||||||
|
pub fn raw() -> impl Drop {
|
||||||
|
Raw::default()
|
||||||
|
}
|
||||||
|
struct Raw();
|
||||||
|
impl Default for Raw {
|
||||||
|
fn default() -> Self {
|
||||||
|
std::thread::yield_now();
|
||||||
|
crossterm::terminal::enable_raw_mode()
|
||||||
|
.expect("should be able to transition into raw mode");
|
||||||
|
Raw()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Drop for Raw {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
crossterm::terminal::disable_raw_mode()
|
||||||
|
.expect("should be able to transition out of raw mode");
|
||||||
|
}
|
||||||
|
}
|
175
repline/src/repline.rs
Normal file
175
repline/src/repline.rs
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
//! Prompts the user, reads the lines. Not much more to it than that.
|
||||||
|
//!
|
||||||
|
//! This module is in charge of parsing keyboard input and interpreting it for the line editor.
|
||||||
|
|
||||||
|
use crate::{editor::Editor, error::*, iter::*, raw::raw};
|
||||||
|
use std::{
|
||||||
|
collections::VecDeque,
|
||||||
|
io::{stdout, Bytes, Read, Result, Write},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Prompts the user, reads the lines. Not much more to it than that.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Repline<'a, R: Read> {
|
||||||
|
input: Chars<Flatten<Result<u8>, Bytes<R>>>,
|
||||||
|
|
||||||
|
history: VecDeque<String>, // previous lines
|
||||||
|
hindex: usize, // current index into the history buffer
|
||||||
|
|
||||||
|
ed: Editor<'a>, // the current line buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Repline<'a, std::io::Stdin> {
|
||||||
|
pub fn new(color: &'a str, begin: &'a str, again: &'a str) -> Self {
|
||||||
|
Self::with_input(std::io::stdin(), color, begin, again)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, R: Read> Repline<'a, R> {
|
||||||
|
/// Constructs a [Repline] with the given [Reader](Read), color, begin, and again prompts.
|
||||||
|
pub fn with_input(input: R, color: &'a str, begin: &'a str, again: &'a str) -> Self {
|
||||||
|
Self {
|
||||||
|
input: Chars(Flatten(input.bytes())),
|
||||||
|
history: Default::default(),
|
||||||
|
hindex: 0,
|
||||||
|
ed: Editor::new(color, begin, again),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Set the terminal prompt color
|
||||||
|
pub fn set_color(&mut self, color: &'a str) {
|
||||||
|
self.ed.color = color
|
||||||
|
}
|
||||||
|
/// Append line to history and clear it
|
||||||
|
pub fn accept(&mut self) {
|
||||||
|
self.history_append(self.ed.to_string());
|
||||||
|
self.ed.clear();
|
||||||
|
self.hindex = self.history.len();
|
||||||
|
}
|
||||||
|
/// Clear the line
|
||||||
|
pub fn deny(&mut self) {
|
||||||
|
self.ed.clear()
|
||||||
|
}
|
||||||
|
/// Reads in a line, and returns it for validation
|
||||||
|
pub fn read(&mut self) -> ReplResult<String> {
|
||||||
|
const INDENT: &str = " ";
|
||||||
|
let mut stdout = stdout().lock();
|
||||||
|
let stdout = &mut stdout;
|
||||||
|
let _make_raw = raw();
|
||||||
|
// self.ed.begin_frame(stdout)?;
|
||||||
|
// self.ed.redraw_frame(stdout)?;
|
||||||
|
self.ed.print_head(stdout)?;
|
||||||
|
loop {
|
||||||
|
stdout.flush()?;
|
||||||
|
match self.input.next().ok_or(Error::EndOfInput)?? {
|
||||||
|
// Ctrl+C: End of Text. Immediately exits.
|
||||||
|
'\x03' => {
|
||||||
|
drop(_make_raw);
|
||||||
|
writeln!(stdout)?;
|
||||||
|
return Err(Error::CtrlC(self.ed.to_string()));
|
||||||
|
}
|
||||||
|
// Ctrl+D: End of Transmission. Ends the current line.
|
||||||
|
'\x04' => {
|
||||||
|
drop(_make_raw);
|
||||||
|
writeln!(stdout)?;
|
||||||
|
return Err(Error::CtrlD(self.ed.to_string()));
|
||||||
|
}
|
||||||
|
// Tab: extend line by 4 spaces
|
||||||
|
'\t' => {
|
||||||
|
self.ed.extend(INDENT.chars(), stdout)?;
|
||||||
|
}
|
||||||
|
// ignore newlines, process line feeds. Not sure how cross-platform this is.
|
||||||
|
'\n' => {}
|
||||||
|
'\r' => {
|
||||||
|
self.ed.push('\n', stdout)?;
|
||||||
|
return Ok(self.ed.to_string());
|
||||||
|
}
|
||||||
|
// Ctrl+Backspace in my terminal
|
||||||
|
'\x17' => {
|
||||||
|
self.ed.erase_word(stdout)?;
|
||||||
|
}
|
||||||
|
// Escape sequence
|
||||||
|
'\x1b' => self.escape(stdout)?,
|
||||||
|
// backspace
|
||||||
|
'\x08' | '\x7f' => {
|
||||||
|
let ed = &mut self.ed;
|
||||||
|
if ed.ends_with(INDENT.chars()) {
|
||||||
|
for _ in 0..INDENT.len() {
|
||||||
|
ed.pop(stdout)?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ed.pop(stdout)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c if c.is_ascii_control() => {
|
||||||
|
if cfg!(debug_assertions) {
|
||||||
|
self.ed.extend(c.escape_debug(), stdout)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c => {
|
||||||
|
self.ed.push(c, stdout)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Handle ANSI Escape
|
||||||
|
fn escape<W: Write>(&mut self, w: &mut W) -> ReplResult<()> {
|
||||||
|
match self.input.next().ok_or(Error::EndOfInput)?? {
|
||||||
|
'[' => self.csi(w)?,
|
||||||
|
'O' => todo!("Process alternate character mode"),
|
||||||
|
other => self.ed.extend(other.escape_debug(), w)?,
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
/// Handle ANSI Control Sequence Introducer
|
||||||
|
fn csi<W: Write>(&mut self, w: &mut W) -> ReplResult<()> {
|
||||||
|
match self.input.next().ok_or(Error::EndOfInput)?? {
|
||||||
|
'A' => {
|
||||||
|
self.hindex = self.hindex.saturating_sub(1);
|
||||||
|
self.restore_history(w)?
|
||||||
|
}
|
||||||
|
'B' => {
|
||||||
|
self.hindex = self.hindex.saturating_add(1).min(self.history.len());
|
||||||
|
self.restore_history(w)?
|
||||||
|
}
|
||||||
|
'C' => self.ed.cursor_forward(1, w)?,
|
||||||
|
'D' => self.ed.cursor_back(1, w)?,
|
||||||
|
'H' => self.ed.home(w)?,
|
||||||
|
'F' => self.ed.end(w)?,
|
||||||
|
'3' => {
|
||||||
|
if let '~' = self.input.next().ok_or(Error::EndOfInput)?? {
|
||||||
|
let _ = self.ed.delete(w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
other => {
|
||||||
|
if cfg!(debug_assertions) {
|
||||||
|
self.ed.extend(other.escape_debug(), w)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
/// Restores the currently selected history
|
||||||
|
fn restore_history<W: Write>(&mut self, w: &mut W) -> ReplResult<()> {
|
||||||
|
let Self { history, hindex, ed, .. } = self;
|
||||||
|
ed.undraw(w)?;
|
||||||
|
ed.clear();
|
||||||
|
ed.print_head(w)?;
|
||||||
|
if let Some(history) = history.get(*hindex) {
|
||||||
|
ed.extend(history.chars(), w)?
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Append line to history
|
||||||
|
fn history_append(&mut self, mut buf: String) {
|
||||||
|
while buf.ends_with(char::is_whitespace) {
|
||||||
|
buf.pop();
|
||||||
|
}
|
||||||
|
if !self.history.contains(&buf) {
|
||||||
|
self.history.push_back(buf)
|
||||||
|
}
|
||||||
|
while self.history.len() > 20 {
|
||||||
|
self.history.pop_front();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user