|
|
@ -11,6 +11,9 @@ mod ext;
|
|
|
|
/// handles input from terminals
|
|
|
|
/// handles input from terminals
|
|
|
|
mod input;
|
|
|
|
mod input;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// defines the interactive mode
|
|
|
|
|
|
|
|
mod interactive;
|
|
|
|
|
|
|
|
|
|
|
|
/// key presses, independent from input sources
|
|
|
|
/// key presses, independent from input sources
|
|
|
|
mod key;
|
|
|
|
mod key;
|
|
|
|
|
|
|
|
|
|
|
@ -18,7 +21,7 @@ mod key;
|
|
|
|
mod lex;
|
|
|
|
mod lex;
|
|
|
|
|
|
|
|
|
|
|
|
/// a real primitive line editor
|
|
|
|
/// a real primitive line editor
|
|
|
|
mod line;
|
|
|
|
mod edit;
|
|
|
|
|
|
|
|
|
|
|
|
mod log;
|
|
|
|
mod log;
|
|
|
|
mod output;
|
|
|
|
mod output;
|
|
|
@ -27,7 +30,6 @@ mod output;
|
|
|
|
mod parse;
|
|
|
|
mod parse;
|
|
|
|
|
|
|
|
|
|
|
|
mod prompt;
|
|
|
|
mod prompt;
|
|
|
|
mod shell;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// syntax and semantic analysis
|
|
|
|
/// syntax and semantic analysis
|
|
|
|
mod syntax;
|
|
|
|
mod syntax;
|
|
|
@ -35,25 +37,22 @@ mod syntax;
|
|
|
|
/// topoglyph is a real word, i promise
|
|
|
|
/// topoglyph is a real word, i promise
|
|
|
|
mod topo;
|
|
|
|
mod topo;
|
|
|
|
|
|
|
|
|
|
|
|
use crate::log::*;
|
|
|
|
use crate::{interactive::Session, log::*, prompt::Prompt, syntax::Eval};
|
|
|
|
use prompt::Prompt;
|
|
|
|
|
|
|
|
use shell::Shell;
|
|
|
|
|
|
|
|
use syntax::Eval;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
use std::io::Write;
|
|
|
|
use std::io::Write;
|
|
|
|
|
|
|
|
|
|
|
|
use anyhow::Result;
|
|
|
|
use anyhow::Result;
|
|
|
|
|
|
|
|
|
|
|
|
fn main() -> Result<()> {
|
|
|
|
fn main() -> Result<()> {
|
|
|
|
let mut shell = Shell::new()?;
|
|
|
|
let mut session = Session::new()?;
|
|
|
|
shell.enable_logging("~/clyde.log");
|
|
|
|
session.enable_logging("~/clyde.log");
|
|
|
|
|
|
|
|
|
|
|
|
let prompt = Prompt::new();
|
|
|
|
let prompt = Prompt::new();
|
|
|
|
|
|
|
|
|
|
|
|
prompt.print(&mut shell.output)?;
|
|
|
|
prompt.print(&mut session.output)?;
|
|
|
|
info!("» shell session start --------");
|
|
|
|
info!("» shell session start --------");
|
|
|
|
loop {
|
|
|
|
loop {
|
|
|
|
match shell.input.next()? {
|
|
|
|
match session.input.next()? {
|
|
|
|
input::Event::Key(event) => {
|
|
|
|
input::Event::Key(event) => {
|
|
|
|
if event.down {
|
|
|
|
if event.down {
|
|
|
|
if event.code.val == 0 {
|
|
|
|
if event.code.val == 0 {
|
|
|
@ -66,18 +65,18 @@ fn main() -> Result<()> {
|
|
|
|
info!(" {}", event);
|
|
|
|
info!(" {}", event);
|
|
|
|
|
|
|
|
|
|
|
|
if event.code == key::LEFT {
|
|
|
|
if event.code == key::LEFT {
|
|
|
|
shell.back(1)?;
|
|
|
|
session.back(1)?;
|
|
|
|
continue;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if event.code == key::RIGHT {
|
|
|
|
if event.code == key::RIGHT {
|
|
|
|
shell.forward(1)?;
|
|
|
|
session.forward(1)?;
|
|
|
|
continue;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if event.code == key::ENTER {
|
|
|
|
if event.code == key::ENTER {
|
|
|
|
shell.output.newline()?;
|
|
|
|
session.output.newline()?;
|
|
|
|
let s = shell.line.pop();
|
|
|
|
let s = session.editor.pop();
|
|
|
|
info!("◇ {}", s);
|
|
|
|
info!("◇ {}", s);
|
|
|
|
if let Ok(tokens) = lex::lex(&s) {
|
|
|
|
if let Ok(tokens) = lex::lex(&s) {
|
|
|
|
for t in tokens {
|
|
|
|
for t in tokens {
|
|
|
@ -90,19 +89,19 @@ fn main() -> Result<()> {
|
|
|
|
let mut state = syntax::State::new();
|
|
|
|
let mut state = syntax::State::new();
|
|
|
|
if let Err(e) = tree.eval(&mut state) {
|
|
|
|
if let Err(e) = tree.eval(&mut state) {
|
|
|
|
error!("{e:?}");
|
|
|
|
error!("{e:?}");
|
|
|
|
shell.render_error(e);
|
|
|
|
_ = session.render_error(e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Err(e) => {
|
|
|
|
Err(e) => {
|
|
|
|
error!("{e:?}");
|
|
|
|
error!("{e:?}");
|
|
|
|
shell.render_error(e);
|
|
|
|
_ = session.render_error(e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// shell.exec(tree.into())?;
|
|
|
|
// shell.exec(tree.into())?;
|
|
|
|
// Some commands don't leave the terminal in a clean state, so we use reset
|
|
|
|
// Some commands don't leave the terminal in a clean state, so we use reset
|
|
|
|
// to ensure that our input and output modes are what we expect them to be.
|
|
|
|
// to ensure that our input and output modes are what we expect them to be.
|
|
|
|
shell.reset()?;
|
|
|
|
session.reset()?;
|
|
|
|
prompt.print(&mut shell.output)?;
|
|
|
|
prompt.print(&mut session.output)?;
|
|
|
|
continue;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
@ -111,22 +110,22 @@ fn main() -> Result<()> {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if event.code == key::BACKSPACE {
|
|
|
|
if event.code == key::BACKSPACE {
|
|
|
|
if shell.line.backspace() {
|
|
|
|
if session.editor.backspace() {
|
|
|
|
// move cursor back two spaces
|
|
|
|
// move cursor back two spaces
|
|
|
|
shell.output.back(2)?;
|
|
|
|
session.output.back(2)?;
|
|
|
|
let tail = format!("{} ", shell.line.tail());
|
|
|
|
let tail = format!("{} ", session.editor.tail());
|
|
|
|
let n = tail.chars().count();
|
|
|
|
let n = tail.chars().count();
|
|
|
|
shell.output.write(tail.as_bytes())?;
|
|
|
|
session.output.write(tail.as_bytes())?;
|
|
|
|
|
|
|
|
|
|
|
|
// after writing out the tail, rewind by the number of characters in
|
|
|
|
// after writing out the tail, rewind by the number of characters in
|
|
|
|
// the tail
|
|
|
|
// the tail
|
|
|
|
if n > 1 {
|
|
|
|
if n > 1 {
|
|
|
|
// let text = format!("\x1b[{}D", n - 1);
|
|
|
|
// let text = format!("\x1b[{}D", n - 1);
|
|
|
|
shell.output.back(n - 1)?;
|
|
|
|
session.output.back(n - 1)?;
|
|
|
|
// output.write(text.as_bytes())?;
|
|
|
|
// output.write(text.as_bytes())?;
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
// honestly I can't remember how I figured this out
|
|
|
|
// honestly I can't remember how I figured this out
|
|
|
|
shell.output.write(b" \x1b[1D")?;
|
|
|
|
session.output.write(b" \x1b[1D")?;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
continue;
|
|
|
|
continue;
|
|
|
@ -135,7 +134,7 @@ fn main() -> Result<()> {
|
|
|
|
// CTRL-D to exit
|
|
|
|
// CTRL-D to exit
|
|
|
|
if event.ctrl && event.code == key::D {
|
|
|
|
if event.ctrl && event.code == key::D {
|
|
|
|
info!("» exit");
|
|
|
|
info!("» exit");
|
|
|
|
shell.output.close()?;
|
|
|
|
session.output.close()?;
|
|
|
|
return Ok(());
|
|
|
|
return Ok(());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
@ -143,7 +142,7 @@ fn main() -> Result<()> {
|
|
|
|
if event.ctrl && event.code == key::J {
|
|
|
|
if event.ctrl && event.code == key::J {
|
|
|
|
debug!("⎈ j: dot");
|
|
|
|
debug!("⎈ j: dot");
|
|
|
|
// red bullet
|
|
|
|
// red bullet
|
|
|
|
shell
|
|
|
|
session
|
|
|
|
.output
|
|
|
|
.output
|
|
|
|
.write(String::from("\x1b[31m\u{2022}\x1b[0m").as_bytes())?;
|
|
|
|
.write(String::from("\x1b[31m\u{2022}\x1b[0m").as_bytes())?;
|
|
|
|
continue;
|
|
|
|
continue;
|
|
|
@ -152,69 +151,71 @@ fn main() -> Result<()> {
|
|
|
|
// CTRL-L to clear the screen
|
|
|
|
// CTRL-L to clear the screen
|
|
|
|
if event.ctrl && event.code == key::L {
|
|
|
|
if event.ctrl && event.code == key::L {
|
|
|
|
info!("» clear");
|
|
|
|
info!("» clear");
|
|
|
|
shell.output.clear()?;
|
|
|
|
session.output.clear()?;
|
|
|
|
prompt.print(&mut shell.output)?;
|
|
|
|
prompt.print(&mut session.output)?;
|
|
|
|
shell.output.write(shell.line.show().as_bytes())?;
|
|
|
|
session.output.write(session.editor.show().as_bytes())?;
|
|
|
|
shell.output.back(shell.line.len() - shell.line.pos())?;
|
|
|
|
session
|
|
|
|
shell.reset()?;
|
|
|
|
.output
|
|
|
|
|
|
|
|
.back(session.editor.len() - session.editor.pos())?;
|
|
|
|
|
|
|
|
session.reset()?;
|
|
|
|
continue;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// CTRL-U to erase to the beginning of the line
|
|
|
|
// CTRL-U to erase to the beginning of the line
|
|
|
|
if event.ctrl && event.code == key::U {
|
|
|
|
if event.ctrl && event.code == key::U {
|
|
|
|
info!("» clear left");
|
|
|
|
info!("» clear left");
|
|
|
|
let n = shell.line.clear_left();
|
|
|
|
let n = session.editor.clear_left();
|
|
|
|
if n > 0 {
|
|
|
|
if n > 0 {
|
|
|
|
// move left by the number of elements removed
|
|
|
|
// move left by the number of elements removed
|
|
|
|
shell.output.back(n)?;
|
|
|
|
session.output.back(n)?;
|
|
|
|
// draw the elements remaining, followed by a space for each removed
|
|
|
|
// draw the elements remaining, followed by a space for each removed
|
|
|
|
// element
|
|
|
|
// element
|
|
|
|
let kept = shell.line.show();
|
|
|
|
let kept = session.editor.show();
|
|
|
|
let text = format!("{}{:width$}", kept, "", width = n);
|
|
|
|
let text = format!("{}{:width$}", kept, "", width = n);
|
|
|
|
shell.output.write(text.as_bytes())?;
|
|
|
|
session.output.write(text.as_bytes())?;
|
|
|
|
shell.output.back(n + kept.chars().count())?;
|
|
|
|
session.output.back(n + kept.chars().count())?;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
continue;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// CTRL-A to move to the beginning of the line
|
|
|
|
// CTRL-A to move to the beginning of the line
|
|
|
|
if event.ctrl && event.code == key::A {
|
|
|
|
if event.ctrl && event.code == key::A {
|
|
|
|
shell.seek_left()?;
|
|
|
|
session.seek_left()?;
|
|
|
|
continue;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// CTRL-E to move to the end of the line
|
|
|
|
// CTRL-E to move to the end of the line
|
|
|
|
if event.ctrl && event.code == key::E {
|
|
|
|
if event.ctrl && event.code == key::E {
|
|
|
|
shell.seek_right()?;
|
|
|
|
session.seek_right()?;
|
|
|
|
continue;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TODO: something better here, this is crappy. I should be checking characters
|
|
|
|
// TODO: something better here, this is crappy. I should be checking characters
|
|
|
|
// based on their unicode categories, not this garbo
|
|
|
|
// based on their unicode categories, not this garbo
|
|
|
|
if !event.char.is_control() {
|
|
|
|
if !event.char.is_control() {
|
|
|
|
shell.line.insert(event.char);
|
|
|
|
session.editor.insert(event.char);
|
|
|
|
|
|
|
|
|
|
|
|
let tail = shell.line.tail();
|
|
|
|
let tail = session.editor.tail();
|
|
|
|
let n = tail.chars().count();
|
|
|
|
let n = tail.chars().count();
|
|
|
|
|
|
|
|
|
|
|
|
// write everything from the current line cursor out to the output buffer.
|
|
|
|
// write everything from the current line cursor out to the output buffer.
|
|
|
|
shell.output.write(tail.as_bytes())?;
|
|
|
|
session.output.write(tail.as_bytes())?;
|
|
|
|
if n > 1 {
|
|
|
|
if n > 1 {
|
|
|
|
// if we wrote more than one character, because we weren't at the end, we
|
|
|
|
// if we wrote more than one character, because we weren't at the end, we
|
|
|
|
// need to rewind the terminal cursor to where it was.
|
|
|
|
// need to rewind the terminal cursor to where it was.
|
|
|
|
shell.output.back(n - 1)?;
|
|
|
|
session.output.back(n - 1)?;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
continue;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
warn!("‽ {}", event);
|
|
|
|
warn!("‽ {}", event);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
input::Event::Left => shell.back(1)?,
|
|
|
|
input::Event::Left => session.back(1)?,
|
|
|
|
input::Event::Right => shell.forward(1)?,
|
|
|
|
input::Event::Right => session.forward(1)?,
|
|
|
|
input::Event::Up => debug!("⛬ ↑"),
|
|
|
|
input::Event::Up => debug!("⛬ ↑"),
|
|
|
|
input::Event::Down => debug!("⛬ ↓"),
|
|
|
|
input::Event::Down => debug!("⛬ ↓"),
|
|
|
|
input::Event::Home => shell.seek_left()?,
|
|
|
|
input::Event::Home => session.seek_left()?,
|
|
|
|
input::Event::End => shell.seek_right()?,
|
|
|
|
input::Event::End => session.seek_right()?,
|
|
|
|
input::Event::Focus(true) => {}
|
|
|
|
input::Event::Focus(true) => {}
|
|
|
|
input::Event::Focus(false) => {}
|
|
|
|
input::Event::Focus(false) => {}
|
|
|
|
input::Event::Menu(_command_id) => {}
|
|
|
|
input::Event::Menu(_command_id) => {}
|
|
|
|