Compare commits

..

50 Commits

Author SHA1 Message Date
Jordan Orelli 17d51617e0 ohhhhhhhh right backslashes on windows, shit 9 months ago
Jordan Orelli fd730452ca expand ~ on windows 9 months ago
Jordan Orelli 8ed0868e56 remove unused imports 9 months ago
Jordan Orelli b48e0b65d0 mkdir 9 months ago
Jordan Orelli af181b372c ls should use file names, not abs paths 9 months ago
Jordan Orelli e71c3f89f7 ls and pwd 9 months ago
Jordan Orelli d25382ee10 ok this escape sequence business is bad 10 months ago
Jordan Orelli 3d71f25528 unrecognized escape sequences no longer crash the shell 10 months ago
Jordan Orelli 08b2b3be52 document the semicolon
w00t
10 months ago
Jordan Orelli cdfa848f46 format examples 10 months ago
Jordan Orelli 90f8a8c8f2 examples are ok 10 months ago
Jordan Orelli 11a3911f28 adding a builtin rm 10 months ago
Jordan Orelli 70e40cb7ae don't add empty text to history 10 months ago
Jordan Orelli 1153c2dc49 add history tests 10 months ago
Jordan Orelli 437afd9d76 non-persistent history 10 months ago
Jordan Orelli 447dd3edc1 double-quoted string literals 10 months ago
Jordan Orelli 965cb45437 all lex errors have a position now 10 months ago
Jordan Orelli 1d504ff36d ok fix escape key handling for real this time 10 months ago
Jordan Orelli 0a821851a3 remove that obnoxious readme 10 months ago
Jordan Orelli 731bd69477 pressing escape no longer crashes the shell 10 months ago
Jordan Orelli 5676c6ddcc tweaks 11 months ago
Jordan Orelli 11088138af ok by now i can get some git hooks or CI going 11 months ago
Jordan Orelli 3d231156e3 removing some dead code 11 months ago
Jordan Orelli a847b56b05 cleaning upp 11 months ago
Jordan Orelli 5505cb48c6 you can now use a semicolon to have TWO statements 11 months ago
Jordan Orelli be5bebf25b adding cd 11 months ago
Jordan Orelli 678b5f7932 more builtin refactoring 11 months ago
Jordan Orelli 9c7d64e512 updating builtin definitions 11 months ago
Jordan Orelli 621f64d3f1 refactoring builtins 11 months ago
Jordan Orelli 71f148139e refactored some stuff 11 months ago
Jordan Orelli 948326df4f moving stuff from syntax to runtime 11 months ago
Jordan Orelli 680adc5882 refactoring 11 months ago
Jordan Orelli f39a22d408 renaming Shell to Session 11 months ago
Jordan Orelli 68909d22a8 builtin echo returns 11 months ago
Jordan Orelli 27ebbf7c97 error printing is less bad 11 months ago
Jordan Orelli 5108e4457f code organization 11 months ago
Jordan Orelli 97602bf42e parse2 is now parse 11 months ago
Jordan Orelli f0dc5f2e64 delete the old parser 11 months ago
Jordan Orelli aa6bcedcf0 the new parser is doing parsing now 11 months ago
Jordan Orelli 7004de4e82 gettin somewhere 11 months ago
Jordan Orelli 56bfdcc9fc dead code removal 11 months ago
Jordan Orelli cb53fb9195 lex tests actually test the tokens now 11 months ago
Jordan Orelli cd51f4cce1 i'm redoing all the parsing lol 11 months ago
Jordan Orelli d152c4092a I'm redoing the lexing and parsing 1 year ago
Jordan Orelli 4a0db72d4e logging is in a separate method now 1 year ago
Jordan Orelli 522668be28 maybe i should figure out how to glob 2 years ago
Jordan Orelli 05793bd42c awkward but sorta works 2 years ago
Jordan Orelli 1715ba037f this doesn't work but I wanna switch computers 2 years ago
Jordan Orelli 845679eebc more readme info 2 years ago
Jordan Orelli 5de73c2c06 add a readme 2 years ago

@ -10,11 +10,12 @@ thiserror = "1.0"
macros = { path = "macros" } macros = { path = "macros" }
dirs = "4" dirs = "4"
log = "0.4" log = { version = "0.4", features = [ "max_level_debug", "release_max_level_off" ] }
[dependencies.windows] [dependencies.windows]
version = "0.44.0" version = "0.44.0"
features = [ features = [
"Win32_Foundation", "Win32_Foundation",
"Win32_System_Console", "Win32_System_Console",
"Win32_UI_Input_KeyboardAndMouse"
] ]

@ -0,0 +1,44 @@
# clyde
A command-line shell for Windows.
## examples
In all examples, the Clyde prompt is written as follows:
```
```
### Run a built-in command
Built-in commands are invoked by executing them with a bare string
```
c:\dev\clyde ▷ cd src
c:\dev\clyde\src ▷
```
### Run an external command
External commands are invoked in the same way:
```
c:\dev\clyde ▷ git status
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
```
### Run commands back to back
To run two commands back to back, separate them with a semicolon. Note that
this interaction is entirely unconditional: the second command executes
regardless of whether the first command succeeded or not.
```
c:\dev\clyde ▷ echo one; echo two
one
two
```

@ -0,0 +1,82 @@
/// changes directory
mod cd;
mod ls;
/// prints stuff
mod echo;
/// prints environment variables
mod printenv;
mod pwd;
/// removes files
mod rm;
mod mkdir;
/// the butt of a file
mod tail;
/// locates a binary on our PATH
mod which;
use crate::{
error::ExecError,
run::{Call, Context, Value},
};
use std::collections::HashMap;
/// All of the builtin commands that exist
#[derive(Clone, Copy)]
pub enum Builtin {
Changedir,
Echo,
Ls,
Mkdir,
Printenv,
Pwd,
Rm,
Tail,
Which,
}
impl Builtin {
fn resolve(&self) -> &dyn Call {
use Builtin::*;
match self {
Changedir => &cd::Changedir,
Echo => &echo::Echo,
Ls => &ls::Ls,
Mkdir => &mkdir::Mkdir,
Printenv => &printenv::Printenv,
Pwd => &pwd::Pwd,
Rm => &rm::Rm,
Tail => &tail::Tail,
Which => &which::Which,
}
}
}
impl Call for Builtin {
fn call(&self, ctx: &mut Context, args: &[Value]) -> Result<Value, ExecError> {
self.resolve().call(ctx, args)
}
}
/// A mapping of all of the builtin command values to their names
pub fn all() -> HashMap<&'static str, Builtin> {
use Builtin::*;
HashMap::from([
("cd", Changedir),
("echo", Echo),
("ls", Ls),
("mkdir", Mkdir),
("printenv", Printenv),
("pwd", Pwd),
("rm", Rm),
("tail", Tail),
("which", Which),
])
}

@ -0,0 +1,25 @@
use crate::{
error::ExecError,
run::{Call, Context, Value},
};
use std::env;
pub struct Changedir;
impl Call for Changedir {
fn call(&self, _ctx: &mut Context, args: &[Value]) -> Result<Value, ExecError> {
match args.len() {
0 => {
todo!()
}
1 => {
let name = args[0].try_as_str()?;
env::set_current_dir(name).map_err(|e| ExecError::Misc(e.to_string()))?;
}
_ => {
todo!()
}
}
Ok(Value::None)
}
}

@ -0,0 +1,18 @@
use crate::{
error::ExecError,
run::{Call, Context, Value},
};
use std::io::Write;
pub struct Echo;
impl Call for Echo {
fn call(&self, ctx: &mut Context, args: &[Value]) -> Result<Value, ExecError> {
let args = args
.into_iter()
.map(|v| v.try_as_str())
.collect::<Result<Vec<_>, _>>()?;
_ = write!(ctx.stdout, "{}\n", args.join(" "));
Ok(Value::None)
}
}

@ -0,0 +1,24 @@
use crate::{
error::ExecError,
run::{Call, Context, Value},
};
use std::{env, fs, io::Write};
pub struct Ls;
impl Call for Ls {
fn call(&self, ctx: &mut Context, _args: &[Value]) -> Result<Value, ExecError> {
let cwd = env::current_dir()?;
let dir = fs::read_dir(&cwd)?;
for child in dir {
let child_path = child?.path();
let fname = child_path
.file_name()
.ok_or_else(|| ExecError::new("no file name"))?
.to_str()
.ok_or_else(|| ExecError::new("non-unicode file name"))?;
_ = write!(ctx.stdout, "{}\n", fname);
}
Ok(Value::None)
}
}

@ -0,0 +1,25 @@
use crate::{
error::ExecError,
run::{Call, Context, Value},
};
use std::fs;
pub struct Mkdir;
impl Call for Mkdir {
fn call(&self, _ctx: &mut Context, args: &[Value]) -> Result<Value, ExecError> {
match args.len() {
0 => {
todo!()
}
1 => {
let path = args[0].try_as_str()?;
fs::create_dir(path)?;
}
_ => {
todo!()
}
}
Ok(Value::None)
}
}

@ -0,0 +1,26 @@
use crate::{
error::ExecError,
run::{Call, Context, Value},
};
use std::{env, io::Write};
pub struct Printenv;
impl Call for Printenv {
fn call(&self, ctx: &mut Context, args: &[Value]) -> Result<Value, ExecError> {
if args.len() > 0 {
for arg in args {
let name = arg.try_as_str()?;
_ = match env::var(name) {
Ok(val) => writeln!(ctx.stdout, "{}", val),
Err(e) => writeln!(ctx.stderr, "ERROR {}", e),
};
}
} else {
for (name, val) in env::vars() {
_ = writeln!(ctx.stdout, "{name}={val}");
}
}
Ok(Value::None)
}
}

@ -0,0 +1,15 @@
use crate::{
error::ExecError,
run::{Call, Context, Value},
};
use std::{env, io::Write};
pub struct Pwd;
impl Call for Pwd {
fn call(&self, ctx: &mut Context, args: &[Value]) -> Result<Value, ExecError> {
let cwd = env::current_dir()?;
_ = write!(ctx.stdout, "{}\n", cwd.display());
Ok(Value::None)
}
}

@ -0,0 +1,25 @@
use crate::{
error::ExecError,
run::{Call, Context, Value},
};
use std::fs;
pub struct Rm;
impl Call for Rm {
fn call(&self, _ctx: &mut Context, args: &[Value]) -> Result<Value, ExecError> {
match args.len() {
0 => {
todo!()
}
1 => {
let path = args[0].try_as_str()?;
fs::remove_file(path).map_err(|err| ExecError::Misc(err.to_string()))?;
}
_ => {
todo!()
}
}
Ok(Value::None)
}
}

@ -1,30 +1,18 @@
use crate::{ use crate::{
ext::Command, error::ExecError,
output, run::{Call, Context, Value},
}; };
use std::{ use std::{
fs::File, fs::File,
io::{Seek, SeekFrom, Read, Write}, io::{Read, Seek, SeekFrom, Write},
}; };
use anyhow::Result; pub struct Tail;
pub struct Tail { }
impl Command for Tail {
fn name() -> String {
String::from("tail")
}
fn create() -> Self {
Self{ }
}
fn exec(&mut self, args: Vec<&str>) -> Result<bool> { impl Call for Tail {
let mut stdout = output::Writer::stdout()?; fn call(&self, ctx: &mut Context, args: &[Value]) -> Result<Value, ExecError> {
if args.len() > 0 { if args.len() > 0 {
let fname = args[0]; let fname = args[0].try_as_str()?;
match File::options().read(true).open(fname) { match File::options().read(true).open(fname) {
Ok(mut f) => { Ok(mut f) => {
_ = f.seek(SeekFrom::End(0)); _ = f.seek(SeekFrom::End(0));
@ -36,7 +24,7 @@ impl Command for Tail {
if n == 1 { if n == 1 {
buf.push(one_byte[0]); buf.push(one_byte[0]);
if let Ok(s) = std::str::from_utf8(&buf) { if let Ok(s) = std::str::from_utf8(&buf) {
stdout.write(s.as_bytes())?; _ = ctx.stdout.write(s.as_bytes());
buf.clear(); buf.clear();
} }
} }
@ -46,13 +34,10 @@ impl Command for Tail {
} }
} }
Err(e) => { Err(e) => {
println!("failed to open file: {}", e); _ = write!(ctx.stderr, "failed to open file: {}", e);
return Err(e.into());
} }
} }
} else {
println!("need a file name");
return Ok(false);
} }
Ok(Value::None)
} }
} }

@ -0,0 +1,26 @@
use crate::{
error::ExecError,
run::{Call, Context, Value},
};
use std::{env, io::Write};
pub struct Which;
impl Call for Which {
fn call(&self, ctx: &mut Context, args: &[Value]) -> Result<Value, ExecError> {
let path = env::var("path").unwrap_or_default();
let dirs: Vec<&str> = path.split(";").collect();
for arg in args {
let name = arg.try_as_str()?;
for d in dirs.iter() {
let dir = std::path::Path::new(d);
let fname = dir.join(name).with_extension("exe");
if fname.exists() && fname.is_file() {
_ = writeln!(ctx.stdout, "{}", fname.to_str().unwrap());
}
}
}
Ok(Value::None)
}
}

@ -0,0 +1,474 @@
use crate::{
history::History,
input::{self, ControlCharacter, Escape},
log::*,
output,
prompt::Prompt,
};
use anyhow::Result;
use std::{cmp::min, io::Write};
/// The text buffer of a text editor.
pub struct Buffer {
/// the current text of the edit buffer
chars: Vec<char>,
/// the position of our edit cursor within [Self::chars]
cursor: usize,
}
impl Buffer {
/// an empty buffer
pub fn new() -> Self {
Self {
chars: Vec::new(),
cursor: 0,
}
}
/// clears our edit buffer
pub fn clear(&mut self) {
self.cursor = 0;
self.chars.clear();
self.show_debug();
}
/// takes the text out of the editor, leaving an empty buffer in its place
fn pop(&mut self) -> String {
let s: String = self.chars.iter().collect();
self.clear();
self.show_debug();
s
}
fn show(&self) -> String {
self.chars.iter().collect()
}
/// moves the edit cursor back by a fixed number of characters without modifying the contents
/// of the buffer
fn back(&mut self, n: usize) -> bool {
if self.cursor > 0 {
if n > self.cursor {
self.cursor = n;
} else {
self.cursor -= n;
}
self.show_debug();
true
} else {
false
}
}
/// drops a character from the text buffer at the cursor's location
fn backspace(&mut self) -> bool {
if self.chars.len() > 0 {
if self.cursor > 0 {
self.cursor -= 1;
}
self.chars.remove(self.cursor);
self.show_debug();
true
} else {
false
}
}
/// removes the elements left of the cursor, returning the count of removed elements
fn clear_left(&mut self) -> usize {
if self.cursor > 0 {
let n = self.chars.drain(..self.cursor).count();
self.cursor = 0;
self.show_debug();
n
} else {
0
}
}
/// moves the edit cursor to the beginning of the buffer. The returned value is the index of
/// the edit cursor before the move.
fn seek_left(&mut self) -> usize {
let n = self.cursor;
self.cursor = 0;
self.show_debug();
n
}
/// moves the edit cursor to the end of the buffer. The returned value is the index of th eedit
/// cursor before the move.
fn seek_right(&mut self) -> usize {
let n = self.chars.len() - self.cursor;
self.cursor = self.chars.len();
self.show_debug();
n
}
/// moves the edit cursor forward by n positions
fn forward(&mut self, n: usize) -> bool {
if self.cursor < self.chars.len() {
self.cursor = min(self.chars.len(), self.cursor + n);
self.show_debug();
true
} else {
false
}
}
/// inserts a character into the edit buffer at the cursor's current location
fn insert(&mut self, c: char) {
self.chars.insert(self.cursor, c);
self.cursor += 1;
self.show_debug();
}
/// the portion of the text buffer from the edit cursor to the end of the buffer
fn tail(&self) -> String {
let mut start = self.cursor;
if start > 0 {
start -= 1;
}
let chars = &self.chars[start..];
chars.iter().collect()
}
/// the current position of the edit cursor
fn pos(&self) -> usize {
self.cursor
}
/// the length of the text buffer
fn len(&self) -> usize {
self.chars.len()
}
fn show_debug(&self) {
let before = if self.chars.len() > 0 {
self.chars[0..self.cursor].to_vec().iter().collect()
} else {
String::new()
};
let after = if self.chars.len() > 0 {
self.chars[self.cursor..].to_vec().iter().collect()
} else {
String::new()
};
debug!(
" {: <5} {: >5} {}·{}",
self.cursor,
self.chars.len(),
before,
after
);
}
}
/// Describes how we handle an action from the editor
enum Status {
/// Editing is still in progress
Ongoing,
/// A command has been entered in the editor and should be sent to the interpreter
Submit(String),
/// The edit session has ended and the shell should shut down
Done,
}
/// A primitive terminal-based text editor
pub struct Editor {
/// the incoming terminal events
input: input::Reader,
/// the in-memory representation of the command that we're currently editing
buffer: Buffer,
/// our outgoing terminal events, which is used as the display portion of the editor
display: output::Writer,
/// this thing draws our prompt
prompt: Prompt,
/// Our current position when scrolling through the history. A hist_pos of 0 means that we're
/// not looking at the history. hist_pos of 1 means that we're looking at the first most-recent
/// command entered.
hist_pos: usize,
}
impl Editor {
pub fn new() -> Result<Self> {
Ok(Self {
input: input::Reader::new()?,
buffer: Buffer::new(),
display: output::Writer::stdout()?,
prompt: Prompt::new(),
hist_pos: 0,
})
}
/// reads one command from the editor. This function blocks until the user has entered an
/// entire command.
pub fn read_command(&mut self, history: &mut History) -> Result<Option<String>> {
use input::Event::*;
loop {
let event = self.input.next()?;
let status = match event {
Key(e) => self.handle_key_press(e)?,
Focus(true) => {
self.focus_start();
Status::Ongoing
}
Focus(false) => {
self.focus_end();
Status::Ongoing
}
Control(cc) => self.handle_control(cc)?,
Escape(escape) => self.handle_escape(escape, history)?,
Size => {
debug!("ignoring size event");
Status::Ongoing
}
Menu(_) => {
debug!("ignoring menu event");
Status::Ongoing
}
Mouse { x, y } => {
debug!("ignoring mouse event {x}, {y}");
Status::Ongoing
}
};
match status {
Status::Ongoing => {}
Status::Done => return Ok(None),
Status::Submit(e) => {
self.hist_pos = 0;
return Ok(Some(e));
}
}
}
}
fn handle_key_press(&mut self, event: crate::key::Event) -> Result<Status> {
use crate::key::codes::*;
match event {
_ if event.code == ENTER || event.char == '\r' => return self.submit(),
_ if event.code == TAB || event.char == '\t' => {}
_ if event.char == '\u{7f}' => self.backspace()?,
_ if event.ctrl && event.code == A => self.seek_left()?,
_ if event.ctrl && event.code == D => return Ok(Status::Done),
_ if event.ctrl && event.code == E => self.seek_right()?,
_ if event.ctrl && event.code == U => self.clear_left()?,
_ if event.down && !event.char.is_control() => self.insert(event.char)?,
_ => debug!("ignored key press: {event:?}"),
}
Ok(Status::Ongoing)
}
fn handle_escape(&mut self, esc: Escape, history: &History) -> Result<Status> {
use Escape::*;
match esc {
Left => self.back(1)?,
Right => self.forward(1)?,
Up => self.prev_history(history)?,
Down => self.next_history(history)?,
Home => self.seek_left()?,
End => self.seek_right()?,
esc => debug!(" Ignored escape: {esc:?}"),
}
Ok(Status::Ongoing)
}
fn handle_control(&mut self, cc: ControlCharacter) -> Result<Status> {
use ControlCharacter::*;
match cc {
StartOfHeading => self.seek_left()?,
EndOfTransmission => return Ok(Status::Done),
Enquiry => self.seek_right()?,
Formfeed => self.clear_screen()?,
Linefeed => self.insert_dot()?,
NegativeAcknowledge => self.clear_left()?,
cc => debug!("ignored control character: {cc:?}"),
}
Ok(Status::Ongoing)
}
fn insert_dot(&mut self) -> Result<()> {
self.insert('\u{2022}')?;
Ok(())
}
fn submit(&mut self) -> Result<Status> {
self.display.newline()?;
let text = self.buffer.pop();
info!("◇ {}", text);
Ok(Status::Submit(text))
}
fn focus_start(&mut self) {}
fn focus_end(&mut self) {}
/// moves the edit cursor one character to the left
pub fn back(&mut self, n: usize) -> Result<()> {
debug!("⛬ ←");
if self.buffer.back(n) {
self.display.back(n)?;
}
Ok(())
}
/// moves the edit cursor one character to the right
pub fn forward(&mut self, n: usize) -> Result<()> {
debug!("⛬ →");
if self.buffer.forward(n) {
self.display.forward(n)?;
}
Ok(())
}
/// moves the cursor position to the end of the line
pub fn seek_right(&mut self) -> Result<()> {
info!("»");
let n = self.buffer.seek_right();
if n > 0 {
// move right by the distance seeked
self.display.forward(n)?;
}
Ok(())
}
/// moves the cursor position to the beginning of the line
pub fn seek_left(&mut self) -> Result<()> {
info!("«");
let n = self.buffer.seek_left();
if n > 0 {
// move left by the distance seeked
self.display.back(n)?;
}
Ok(())
}
/// clears the line from the current cursor position to the beginning of the line
pub fn clear_left(&mut self) -> Result<()> {
info!("» clear left");
let n = self.buffer.clear_left();
if n > 0 {
// move left by the number of elements removed
self.display.back(n)?;
// draw the elements remaining, followed by a space for each removed
// element
let kept = self.buffer.show();
let text = format!("{}{:width$}", kept, "", width = n);
self.display.write(text.as_bytes())?;
self.display.back(n + kept.chars().count())?;
}
Ok(())
}
/// clears the scrollback buffer, moving the current edit line to the top of the screen, but
/// leaving the edit cursor in place
pub fn clear_screen(&mut self) -> Result<()> {
info!("» clear");
self.display.clear()?;
self.prompt.print(&mut self.display)?;
self.display.write(self.buffer.show().as_bytes())?;
self.display.back(self.buffer.len() - self.buffer.pos())?;
self.reset()?;
Ok(())
}
pub fn show_prompt(&mut self) -> Result<()> {
self.reset()?;
self.prompt.print(&mut self.display)?;
Ok(())
}
/// inserts a character at edit cursor's current position
pub fn insert(&mut self, c: char) -> Result<()> {
self.buffer.insert(c);
let tail = self.buffer.tail();
let n = tail.chars().count();
// write everything from the current line cursor out to the output buffer.
self.display.write(tail.as_bytes())?;
if n > 1 {
// 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.
self.display.back(n - 1)?;
}
Ok(())
}
pub fn backspace(&mut self) -> Result<()> {
if !self.buffer.backspace() {
return Ok(());
}
// move cursor back two spaces
self.display.back(2)?;
let tail = format!("{} ", self.buffer.tail());
let n = tail.chars().count();
self.display.write(tail.as_bytes())?;
// after writing out the tail, rewind by the number of characters in
// the tail
if n > 1 {
self.display.back(n - 1)?;
} else {
// honestly I can't remember how I figured this out
self.display.write(b" \x1b[1D")?;
}
Ok(())
}
pub fn reset(&mut self) -> Result<()> {
self.buffer.clear();
self.display.reset()?;
Ok(())
}
fn prev_history(&mut self, history: &History) -> Result<()> {
let text = match history.look_back(self.hist_pos + 1) {
Some(s) => {
self.hist_pos += 1;
s
}
None => return Ok(()),
};
self.seek_right()?;
self.clear_left()?;
for c in text.chars() {
self.insert(c)?;
}
Ok(())
}
fn next_history(&mut self, history: &History) -> Result<()> {
if self.hist_pos <= 1 {
self.seek_right()?;
self.clear_left()?;
self.hist_pos = 0;
return Ok(());
}
let text = match history.look_back(self.hist_pos - 1) {
Some(s) => {
self.hist_pos -= 1;
s
}
None => return Ok(()),
};
self.seek_right()?;
self.clear_left()?;
for c in text.chars() {
self.insert(c)?;
}
Ok(())
}
}

@ -1,3 +1,8 @@
use crate::{
lex::Token,
syntax::ExpandError,
topo::{Glyph, Position},
};
use std::io; use std::io;
use thiserror::Error; use thiserror::Error;
use windows::Win32::Foundation::{GetLastError, BOOL}; use windows::Win32::Foundation::{GetLastError, BOOL};
@ -9,9 +14,126 @@ pub enum Error {
#[error("i/o error: {0}")] #[error("i/o error: {0}")]
IOError(#[from] io::Error), IOError(#[from] io::Error),
}
#[derive(Error, Debug)]
pub enum InputError {
#[error("unrecognized control character: {0}")]
UnrecognizedControlCharacter(u8),
#[error("input record cannot convert to control character because it is an up event")]
ControlCharactersOnlyGoDownNotUp,
#[error("unrecognized escape sequence: {0}")]
BadEscapeSequence(String),
#[error("fart")]
UnexpectedInputRecordDuringEscapeParting,
}
#[derive(Debug, Error)]
pub enum LexError {
#[error("unexpected character {g} at {pos:?}", g = .0.glyph, pos = .0.position)]
UnexpectedCharacter(Glyph),
#[error("unexpected character {g} at {pos:?} while lexing a bare string", g = .0.glyph, pos = .0.position)]
UnexpectedCharacterInBareString(Glyph),
#[error("unexpected character {g} at {pos:?} while lexing a glob", g = .0.glyph, pos = .0.position)]
UnexpectedCharacterInGlob(Glyph),
#[error("unexpected eof")]
UnexpectedEOF(Position),
#[error("input error: {0}")] #[error("not yet supported: {1}")]
InputError(String), NotYetSupported(Position, String),
}
impl LexError {
pub fn not_yet(p: Position, msg: &str) -> Self {
LexError::NotYetSupported(p, msg.to_string())
}
fn position(&self) -> Position {
use LexError::*;
match self {
UnexpectedCharacter(g) => g.position,
UnexpectedCharacterInBareString(g) => g.position,
UnexpectedCharacterInGlob(g) => g.position,
UnexpectedEOF(p) => *p,
NotYetSupported(p, _) => *p,
}
}
}
#[derive(Debug, Error)]
pub enum ParseError {
#[error("lex error at line {line}, column {col}", line=.0.position().line, col=.0.position().column)]
LexError(#[from] LexError),
#[error("Unexpected Token: {0:?}")]
UnexpectedToken(Token),
#[error("Illegal attempt to climb parse tree while already at root")]
AtRootAlready,
#[error("Illegal attempt to climb barse tree when target parent has already been dropped")]
ParentIsGone,
#[error("Illegal attempt to double-borrow a node")]
BorrowError(#[from] std::cell::BorrowMutError),
#[error("Illegal attempt to push a value as a child to a terminal value")]
PushOntoTerminal,
#[error("Statement node has no children")]
StatementIsEmpty,
#[error("dangling")]
DanglingElements,
#[error("shell expansion error")]
ExpansionError(#[from] ExpandError),
#[error("you wouldn't parse a semicolon")]
WhyParseSemicolon,
}
#[derive(Debug, Error)]
pub enum ExecError {
#[error("failed waiting on child process '{name}' ({pid})")]
ProcessWaitError {
name: String,
pid: u32,
#[source]
source: io::Error,
},
#[error("failed to spawn '{name}' as a child process")]
ProcessSpawnError {
name: String,
#[source]
source: io::Error,
},
#[error("i/o error")]
IOError(#[from] std::io::Error),
#[error("value type error: {0}")]
ValueTypeError(String),
#[error("error: {0}")]
Misc(String),
}
impl ExecError {
pub fn type_error<S: Into<String>>(msg: S) -> Self {
Self::ValueTypeError(msg.into())
}
pub fn new<S: Into<String>>(msg: S) -> Self {
Self::Misc(msg.into())
}
} }
impl Error { impl Error {
@ -26,10 +148,6 @@ impl Error {
Err(Error::last_error()) Err(Error::last_error())
} }
} }
pub fn input_error<S: Into<String>>(msg: S) -> Self {
Error::InputError(msg.into())
}
} }
impl From<Error> for std::io::Error { impl From<Error> for std::io::Error {

@ -1,66 +0,0 @@
mod command;
mod tail;
mod which;
mod printenv;
mod echo;
pub use command::Command;
pub use echo::Echo;
pub use tail::Tail;
pub use which::Which;
pub use printenv::Printenv;
/*
Posix Shell builtins:
alias bg cd command false fc fg getopts hash jobs kill
newgrp pwd read true type ulimit umask unalias wait
Bourne Shell builtins:
: - do nothing except expand arguments and perform redirections
. - read and execute a file in the current shell context
break - exit from a loop
cd - change the current working directory to directory
continue - resume the next iteration of a loop
eval - evaluate the arguments as a single command
exec - replace the shell without creating a new process
exit - exits the process with a status
export - exports environment variables
getopts - used by shell scripts to parse arguments
hash - displays the hash table used by the shell to map command names to files
pwd - prints the absolute path of the current directory
readonly - mark each name as readonly so that they cannot be reassigned
return - causes a shell function to stop execution and return a value
shift - shifts the position parameters to the left by n
test - evaluate a conditional expression and return a status of 0 or 1
times - print out the user and system times used by the shell and its children. sorry, what the fuck is this
trap - execute commands when a given signal is received
umask - set the shell process's file creation mask
unset - clears variable or function names
Bash Builtins:
alias - creates an alias for a command
bind - adds key bindings
builtin - access a builtin function even if it has been masked by another function
caller - tells you what line number you're executing?
command - runs a command with a given name, ignoring shell functions
declare - declare variables and give them attributes
echo - echos the outputs
enable - enable and disable builtin shell commands
help - display help text about builtin commands
let - perform arithmetic on shell variables
local - create local variables
logout - exits a login shell with a status
mapfile - read lines from stdin or a file into an array
printf - formats arguments using a format string
read - reads one line from stdin or from a file
readarray - read lines from stdin into an array or something
source - executes the script in the current shell
type - tells you if a thing is an alias, function, builtin, or file command
typeset - synonym for declare, included for korn shell compatibility
ulimit - provides control over the resources available to the processes started by the shell
unalias - clears an alias
*/

@ -1,7 +0,0 @@
use anyhow::Result;
pub trait Command {
fn name() -> String;
fn create() -> Self;
fn exec(&mut self, args: Vec<&str>) -> Result<bool>;
}

@ -1,19 +0,0 @@
use crate::ext::Command;
use anyhow::Result;
pub struct Echo {}
impl Command for Echo {
fn name() -> String {
String::from("echo")
}
fn create() -> Self {
Self {}
}
fn exec(&mut self, args: Vec<&str>) -> Result<bool> {
println!("{}", args.join(" "));
Ok(true)
}
}

@ -1,33 +0,0 @@
use crate::ext::Command;
use anyhow::Result;
pub struct Printenv {}
impl Command for Printenv {
fn name() -> String {
String::from("printenv")
}
fn create() -> Self {
Self {}
}
fn exec(&mut self, args: Vec<&str>) -> Result<bool> {
if args.len() > 0 {
let name = args[0];
match std::env::var(name) {
Ok(val) => {
println!("{}", val);
Ok(true)
}
Err(e) => {
println!("ERROR {}", e);
Ok(false)
}
}
} else {
println!("which variable you fucking dork");
Ok(false)
}
}
}

@ -1,34 +0,0 @@
use crate::ext::Command;
use anyhow::Result;
pub struct Which {}
impl Command for Which {
fn name() -> String {
String::from("which")
}
fn create() -> Self {
Self {}
}
fn exec(&mut self, args: Vec<&str>) -> Result<bool> {
if args.len() > 0 {
let path = std::env::var("path").unwrap();
let dirs: Vec<&str> = path.split(";").collect();
for d in dirs {
let dir = std::path::Path::new(d);
let fname = dir.join(args[0]).with_extension("exe");
if fname.exists() && fname.is_file() {
println!("{}", fname.to_str().unwrap());
return Ok(true);
}
}
println!("not found: {}", args[0]);
Ok(false)
} else {
println!("what do you want to look for?");
Ok(false)
}
}
}

@ -0,0 +1,45 @@
pub struct History {
events: Vec<String>,
}
impl History {
pub fn new() -> Self {
Self { events: Vec::new() }
}
pub fn add<S: Into<String>>(&mut self, event: S) {
let text = event.into();
if text.is_empty() {
return;
}
self.events.push(text);
}
pub fn look_back(&self, idx: usize) -> Option<&str> {
if idx == 0 || idx > self.events.len() {
return None;
}
Some(&self.events[self.events.len() - idx])
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_history() {
let mut history = History::new();
assert_eq!(history.look_back(1), None);
assert_eq!(history.look_back(0), None);
history.add("Hello");
assert_eq!(history.look_back(1), Some("Hello"));
assert_eq!(history.look_back(0), None);
history.add("Hello again");
assert_eq!(history.look_back(2), Some("Hello"));
assert_eq!(history.look_back(1), Some("Hello again"));
assert_eq!(history.look_back(0), None);
}
}

@ -1,132 +1,132 @@
use crate::{error::Error, key, log::*}; use crate::{
error::{Error, InputError},
key,
log::*,
};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use windows::Win32::Foundation::HANDLE; use log::error;
use windows::Win32::System::Console; use std::{
use macros::escapes; cell::{RefCell, RefMut},
collections::VecDeque,
#[allow(dead_code)] fmt,
fn log_input_mode(mode: Console::CONSOLE_MODE) { rc::{Rc, Weak},
// Characters read by the ReadFile or ReadConsole function are written to the active screen };
// buffer as they are typed into the console. This mode can be used only if the use windows::Win32::{Foundation::HANDLE, System::Console};
// ENABLE_LINE_INPUT mode is also enabled.
if (mode & Console::ENABLE_ECHO_INPUT).0 > 0 { /// retrieves a Windows Console handle using the Win32 apis
debug!("Echo Input: Enabled"); fn stdin_handle() -> Result<HANDLE> {
} else { unsafe {
debug!("Echo Input: Disabled"); let handle = Console::GetStdHandle(Console::STD_INPUT_HANDLE)
} .context("unable to get stdin handle")?;
Ok(handle)
// When enabled, text entered in a console window will be inserted at the current cursor
// location and all text following that location will not be overwritten. When disabled, all
// following text will be overwritten.
if (mode & Console::ENABLE_INSERT_MODE).0 > 0 {
debug!("Insert Mode: Enabled");
} else {
debug!("Insert Mode: Disabled");
} }
}
// The ReadFile or ReadConsole function returns only when a carriage return character is read. /// checks to see whether the raw underling console input record is the beginning of an escape
// If this mode is disabled, the functions return when one or more characters are available. /// sequence
if (mode & Console::ENABLE_LINE_INPUT).0 > 0 { fn is_escape_start(record: &Console::INPUT_RECORD) -> bool {
debug!("Line Input Mode: Enabled"); if record.EventType as u32 == Console::KEY_EVENT {
} else { unsafe {
debug!("Line Input Mode: Disabled"); let event = record.Event.KeyEvent;
event.wVirtualKeyCode == 0 && event.uChar.UnicodeChar == 27 && event.bKeyDown.as_bool()
} }
// If the mouse pointer is within the borders of the console window and the window has the
// keyboard focus, mouse events generated by mouse movement and button presses are placed in
// the input buffer. These events are discarded by ReadFile or ReadConsole, even when this mode
// is enabled. The ReadConsoleInput function can be used to read MOUSE_EVENT input records from
// the input buffer.
if (mode & Console::ENABLE_MOUSE_INPUT).0 > 0 {
debug!("Mouse Input: Enabled");
} else { } else {
debug!("Mouse Input: Disabled"); false
} }
}
// CTRL+C is processed by the system and is not placed in the input buffer. If the input buffer /// checks to see if a record is indicating the end of an escape sequence
// is being read by ReadFile or ReadConsole, other control keys are processed by the system and fn is_escape_done(record: &Console::INPUT_RECORD) -> bool {
// are not returned in the ReadFile or ReadConsole buffer. If the ENABLE_LINE_INPUT mode is if record.EventType as u32 == Console::KEY_EVENT {
// also enabled, backspace, carriage return, and line feed characters are handled by the unsafe {
// system. let event = record.Event.KeyEvent;
if (mode & Console::ENABLE_PROCESSED_INPUT).0 > 0 { // This is a key up event for the physical escape key
debug!("Processed Input: Enabled"); event.wVirtualKeyCode == 27
} else { && event.uChar.UnicodeChar == 27
debug!("Processed Input: Disabled"); && !event.bKeyDown.as_bool()
} }
// This flag enables the user to use the mouse to select and edit text. To enable this mode,
// use ENABLE_QUICK_EDIT_MODE | ENABLE_EXTENDED_FLAGS. To disable this mode, use
// ENABLE_EXTENDED_FLAGS without this flag.
if (mode & Console::ENABLE_QUICK_EDIT_MODE).0 > 0 {
debug!("Quick Edit Mode: Enabled");
} else { } else {
debug!("Quick Edit Mode: Disabled"); false
} }
}
// User interactions that change the size of the console screen buffer are reported in the /// attempts to convert a record into a character that would appear in an escape sequence
// console's input buffer. Information about these events can be read from the input buffer by fn as_escape_character(record: &Console::INPUT_RECORD) -> Option<char> {
// applications using the ReadConsoleInput function, but not by those using ReadFile or if record.EventType as u32 != Console::KEY_EVENT {
// ReadConsole. None
if (mode & Console::ENABLE_WINDOW_INPUT).0 > 0 {
debug!("Window Input: Enabled");
} else { } else {
debug!("Window Input: Disabled"); unsafe {
let n = record.Event.KeyEvent.uChar.UnicodeChar as u32;
let c = char::from_u32(n);
c
} }
// Setting this flag directs the Virtual Terminal processing engine to convert user input
// received by the console window into Console Virtual Terminal Sequences that can be retrieved
// by a supporting application through ReadFile or ReadConsole functions.
//
// The typical usage of this flag is intended in conjunction with
// ENABLE_VIRTUAL_TERMINAL_PROCESSING on the output handle to connect to an application that
// communicates exclusively via virtual terminal sequences.
if (mode & Console::ENABLE_VIRTUAL_TERMINAL_INPUT).0 > 0 {
debug!("Virtual Terminal Input: Enabled");
} else {
debug!("Virtual Terminal Input: Disabled");
} }
} }
fn stdin_handle() -> Result<HANDLE> { /// checks to see if a console record is indicating that the control key has been pressed or
/// depressed. Note that this has no analogue in the vt100 specification, this is purely a Windows
/// thing.
fn as_ctrl_toggle(record: &Console::INPUT_RECORD) -> Option<bool> {
if record.EventType as u32 == Console::KEY_EVENT {
unsafe { unsafe {
let handle = Console::GetStdHandle(Console::STD_INPUT_HANDLE) let event = record.Event.KeyEvent;
.context("unable to get stdin handle")?; if event.wVirtualKeyCode == 17 {
Ok(handle) return Some(event.bKeyDown.as_bool());
}
} }
}
None
} }
/// a handle to a terminal's stream of input events. This is analagous to the input portion of a
/// vt100 tty terminal.
///
/// This implementation specifically is implementing the Windows API for Console input.
///
/// In the Windows Console API, console inputs are communicated using struct values having type
/// [INPUT_RECORD](https://learn.microsoft.com/en-us/windows/console/input-record-str).
pub struct Reader { pub struct Reader {
input: HANDLE, input: HANDLE,
// this area in memory is where the Windows API will write events read off of the keyboard.
buf: [Console::INPUT_RECORD; 32],
// the number of valid input record items in the buf since last read
buf_len: u32,
// the position of our current indexer into the input record buffer
buf_idx: usize,
/// this scratch buffer is used as a shared memory location for communicating with the kernel.
/// When we request input records, the windows kernel writes them into this buffer.
scratch: [Console::INPUT_RECORD; 32],
/// A lookahead buffer of unprocessed events. If we request input events from Windows and there
/// are more than one event, we put the unprocessed events into this lookahead buffer to reduce
/// the number of syscalls we're making.
lookahead: VecDeque<Console::INPUT_RECORD>,
/// whether or not the control key is pressed, i think?
ctrl: bool, ctrl: bool,
pub(crate) escapes: EscapeCursor,
} }
impl Reader { impl Reader {
pub fn new() -> Result<Self> { pub fn new() -> Result<Self> {
let escapes = build_prefix_tree();
let v = Self { let v = Self {
buf: [Console::INPUT_RECORD::default(); 32], scratch: [Console::INPUT_RECORD::default(); 32],
buf_len: 0, lookahead: VecDeque::new(),
buf_idx: 0,
input: stdin_handle()?, input: stdin_handle()?,
ctrl: false, ctrl: false,
escapes,
}; };
v.reset()?; v.reset()?;
Ok(v) Ok(v)
} }
pub fn reset(&self) -> Result<()> { pub fn reset(&self) -> Result<()> {
// https://learn.microsoft.com/en-us/windows/console/setconsolemode
let mut mode = Console::CONSOLE_MODE(0); let mut mode = Console::CONSOLE_MODE(0);
unsafe { unsafe {
Console::SetConsoleCP(65001); // set the console's code page to 65001, which is the code page for utf-8
Error::check(Console::SetConsoleCP(65001))?;
let handle = stdin_handle()?; // retrieve the current console mode
Error::check(Console::GetConsoleMode(handle, &mut mode))?; Error::check(Console::GetConsoleMode(self.input, &mut mode))?;
// allow terminal input characters // allow terminal input characters
mode |= Console::ENABLE_VIRTUAL_TERMINAL_INPUT; mode |= Console::ENABLE_VIRTUAL_TERMINAL_INPUT;
@ -143,188 +143,165 @@ impl Reader {
// enable mouse input // enable mouse input
mode |= Console::ENABLE_MOUSE_INPUT; mode |= Console::ENABLE_MOUSE_INPUT;
Error::check(Console::SetConsoleMode(handle, mode))?; // enable reporting of window resize events
Error::check(Console::GetConsoleMode(handle, &mut mode))?; mode |= Console::ENABLE_WINDOW_INPUT;
// debug!("Stdin details:");
// log_input_mode(mode); Error::check(Console::SetConsoleMode(self.input, mode))?;
} }
Ok(()) Ok(())
} }
pub fn next(&mut self) -> Result<Event> { pub fn next(&mut self) -> Result<Event> {
let rec = self.next_rec()?; let record = self.next_record()?;
if rec.EventType as u32 == Console::KEY_EVENT {
if let Some(ctrl_toggle) = as_ctrl_toggle(&record) {
self.ctrl = ctrl_toggle;
debug!("ctrl {on}", on = if self.ctrl { "ON" } else { "OFF" });
return self.next();
}
// This is a little weird but on a vt100 terminal, when you held down control you could
// send ascii values directly. So ctrl-a is actually the ascii value of 1, which is the
// "start of heading" character, and ctrl-d is actually the ascii value of 4, which is the
// "end of transmission" character.
//
// Now the way that the Windows Console api works is that it sends events for both key down
// and key up. Here's the rub: the vt100 did not send any key up events. The vt100 didn't
// send anything at all if you tapped and released control, but the Windows Console api
// would send a ctrl key down and ctrl key up event.
if self.ctrl && record.EventType as u32 == Console::KEY_EVENT {
unsafe { unsafe {
let event = rec.Event.KeyEvent; let key_event = record.Event.KeyEvent;
if event.wVirtualKeyCode == 0 && event.uChar.UnicodeChar == 27 { if key_event.bKeyDown.as_bool() {
return Ok(self.next_escape_sequence()?); match ControlCharacter::try_from(key_event) {
Ok(c) => return Ok(Event::Control(c)),
Err(e) => warn!("{:?}", e),
}
} }
if event.wVirtualKeyCode == 17 {
self.ctrl = event.bKeyDown.as_bool();
debug!("ctrl {}", event.bKeyDown.as_bool());
} }
} }
if is_escape_start(&record) {
match self.next_escape_sequence() {
Ok(escape) => return Ok(Event::Escape(escape)),
Err(e) => {
error!("{e}");
return self.next();
}
} }
Ok(rec.into())
} }
fn log_recs(&mut self) { let event: Event = record.into();
debug!(" +---------------------+"); if matches!(event, Event::Key(key::Event { down: false, .. })) {
for i in 0..self.buf_len { return self.next();
let rec = self.buf[i as usize];
let e: Event = rec.into();
match e {
Event::Key(k) => debug!(" | {} |", k),
_ => debug!(" | {:<14?} |", &e),
} }
Ok(event)
} }
debug!(" +---------------------+");
/// reads the next INPUT_RECORD value from the underlying Windows Console file descriptor.
fn next_record(&mut self) -> Result<Console::INPUT_RECORD> {
if let Some(record) = self.lookahead.pop_front() {
return Ok(record);
} }
fn next_rec(&mut self) -> Result<Console::INPUT_RECORD> { let mut num_read: u32 = 0;
if self.buf_idx as u32 >= self.buf_len {
// request records from windows
unsafe { unsafe {
Error::check(Console::ReadConsoleInputA( Error::check(Console::ReadConsoleInputA(
self.input, self.input,
&mut self.buf, &mut self.scratch,
&mut self.buf_len, &mut num_read,
))?; ))?;
} }
debug!("• {}", self.buf_len);
self.log_recs();
self.buf_idx = 0;
}
let rec = self.buf[self.buf_idx];
self.buf_idx += 1;
return Ok(rec);
}
fn next_escape_sequence(&mut self) -> Result<Event> { let records = &self.scratch[0..num_read as usize];
match self.next_escape_char()? { debug!("{num_read} records:");
'[' => match self.next_escape_char()? { for record in records {
'A' => Ok(Event::Up), self.lookahead.push_back(*record);
'B' => Ok(Event::Down), if record.EventType as u32 == Console::KEY_EVENT {
'C' => Ok(Event::Right), unsafe {
'D' => Ok(Event::Left), let key: key::Event = record.Event.KeyEvent.clone().into();
'H' => Ok(Event::Home), debug!(" {key}");
'F' => Ok(Event::End),
'1' => match self.next_escape_char()? {
'3' => match self.next_escape_char()? {
';' => match self.next_escape_char()? {
'5' => match self.next_escape_char()? {
'u' => Ok(Event::Drop(String::from("13;5u"))),
e => Err(Error::input_error(format!("[13;5 unexpected escape char: {}", e)).into()),
}
e => Err(Error::input_error(format!("[13; unexpected escape char: {}", e)).into()),
}
e => Err(Error::input_error(format!("[13 unexpected escape char: {}", e)).into()),
}
'5' => match self.next_escape_char()? {
'~' => Ok(Event::Drop(String::from("[15~ - F5"))),
e => Err(Error::input_error(format!("[15 unexpected escape char: {}", e)).into()),
}
'7' => match self.next_escape_char()? {
'~' => Ok(Event::Drop(String::from("[17~ - F6"))),
e => Err(Error::input_error(format!("[17 unexpected escape char: {}", e)).into()),
}
'8' => match self.next_escape_char()? {
'~' => Ok(Event::Drop(String::from("[18~ - F7"))),
e => Err(Error::input_error(format!("[18 unexpected escape char: {}", e)).into()),
} }
'9' => match self.next_escape_char()? { } else {
'~' => Ok(Event::Drop(String::from("[19~ - F8"))), debug!(" -");
e => Err(Error::input_error(format!("[19 unexpected escape char: {}", e)).into()),
} }
e => Err(Error::input_error(format!("[1 unexpected escape char: {}", e)).into()),
},
'2' => match self.next_escape_char()? {
'0' => match self.next_escape_char()? {
'~' => Ok(Event::Drop(String::from("[20~ - F9"))),
e => Err(Error::input_error(format!("[20 unexpected escape char: {}", e)).into()),
} }
'1' => match self.next_escape_char()? { Ok(self.lookahead.pop_front().unwrap())
'~' => Ok(Event::Drop(String::from("[21~ - F10"))),
e => Err(Error::input_error(format!("[20 unexpected escape char: {}", e)).into()),
} }
'3' => match self.next_escape_char()? {
'~' => Ok(Event::Drop(String::from("[23~ - F11"))), fn next_escape_sequence(&mut self) -> Result<Escape> {
e => Err(Error::input_error(format!("[23 unexpected escape char: {}", e)).into()), self.escapes.reset();
loop {
let record = self.next_record()?;
if is_escape_start(&record) {
continue;
} }
'4' => match self.next_escape_char()? { if is_escape_done(&record) {
'~' => Ok(Event::Drop(String::from("[24~ - F12"))), if self.escapes.is_at_root() {
e => Err(Error::input_error(format!("[24 unexpected escape char: {}", e)).into()), return Ok(Escape::Empty);
} else {
panic!();
} }
e => Err(Error::input_error(format!("[2 unexpected escape char: {}", e)).into()),
} }
e => Err(Error::input_error(format!("[ unexpected escape char: {}", e)).into()), let c = as_escape_character(&record)
}, .ok_or(InputError::UnexpectedInputRecordDuringEscapeParting)?;
'O' => match self.next_escape_char()? { match self.escapes.step(c) {
'P' => Ok(Event::Drop(String::from("OP - F1"))), EscapeStep::Continue => {}
'Q' => Ok(Event::Drop(String::from("OQ - F2"))), EscapeStep::End(escape) => return Ok(escape),
'R' => Ok(Event::Drop(String::from("OR - F3"))), EscapeStep::Abort(s) => return Err(InputError::BadEscapeSequence(s).into()),
'S' => Ok(Event::Drop(String::from("OS - F4"))),
e => Err(Error::input_error(format!("O unexpected escape char: {}", e)).into()),
}
e => Err(Error::input_error(format!("unexpected escape char: {}", e)).into()),
}
}
// fn take_bracket(&mut self) -> Result<()> {
// let rec = self.next_rec()?;
// if rec.EventType as u32 != Console::KEY_EVENT {
// Err(Error::input_error("failed to read escape sequence: not a key event").into())
// } else {
// unsafe {
// let event = rec.Event.KeyEvent;
// if event.wVirtualKeyCode == 0 && event.uChar.UnicodeChar == 91 {
// Ok(())
// } else {
// Err(Error::input_error("failed to read escape sequence: not a [").into())
// }
// }
// }
// }
fn next_escape_char(&mut self) -> Result<char> {
let rec = self.next_rec()?;
if rec.EventType as u32 != Console::KEY_EVENT {
Err(
Error::input_error("failed to read char in escape sequence: not a key event")
.into(),
)
} else {
unsafe {
let n = rec.Event.KeyEvent.uChar.UnicodeChar as u32;
let c = char::from_u32(n);
c.ok_or_else(|| {
let msg = format!("escape key value is not a valid unicode character: {}", n);
Error::input_error(msg).into()
})
} }
} }
} }
} }
/// Event represents all of the events that may be seen as input by the shell process. Note that
/// the set of events described here includes both in-band and out of band events, which is because
/// that's how it works in the vt100 spec. It's a little funky but that's because it's reflecting a
/// spec from the 70's.
#[derive(Debug)] #[derive(Debug)]
pub enum Event { pub enum Event {
/// The process has received focus
Focus(bool), Focus(bool),
/// This is a windows Menu event. This might be skippable?
Menu(u32), Menu(u32),
/// A Key press. Key events associate to keys on the keyboard that are typically in-band, that
/// is, they're associated with specific characters
Key(key::Event), Key(key::Event),
/// an event from the user's mouse, such as a mouse movement or click
Mouse { x: i16, y: i16 }, Mouse { x: i16, y: i16 },
/// a resize event to inform us that the visual window of our process has changed
Size, Size,
Left,
Right, /// A decoded ANSI Escape Sequence. At the Terminal level, an Escape sequence is defined by a
Up, /// string of characters instead of just one character. Escape Sequences are used to
Down, /// communicate a variety of edit-related functionality such as navigation.
Home, Escape(Escape),
End,
Drop(String), /// An ASCII Controll Character.
Control(ControlCharacter),
} }
const ALT_KEYS: u32 = 0x0002 | 0x0001; impl fmt::Display for Event {
const CTRL_KEYS: u32 = 0x0008 | 0x0004; fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
const SHIFT_PRESSED: u32 = 0x0010; use Event::*;
match self {
Key(k) => write!(f, "Event<Key {k}>"),
Control(c) => write!(f, "Event<Control {c:?}>"),
Escape(e) => write!(f, "Event<Escape {e:?}>"),
Focus(true) => write!(f, "Event<focus>"),
Focus(false) => write!(f, "Event<unfocus>"),
Menu(n) => write!(f, "Event<Menu {n}>"),
Mouse { x, y } => write!(f, "Event<Mouse {x},{y}>"),
Size => write!(f, "Event<Size>"),
}
}
}
impl From<Console::INPUT_RECORD> for Event { impl From<Console::INPUT_RECORD> for Event {
fn from(rec: Console::INPUT_RECORD) -> Self { fn from(rec: Console::INPUT_RECORD) -> Self {
@ -339,24 +316,7 @@ impl From<Console::INPUT_RECORD> for Event {
let event = rec.Event.MenuEvent; let event = rec.Event.MenuEvent;
Event::Menu(event.dwCommandId) Event::Menu(event.dwCommandId)
}, },
Console::KEY_EVENT => { Console::KEY_EVENT => unsafe { Event::Key(rec.Event.KeyEvent.clone().into()) },
// https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
unsafe {
let event = rec.Event.KeyEvent;
let mstate = event.dwControlKeyState;
let c = char::from_u32(event.uChar.UnicodeChar as u32).unwrap_or('💀');
Event::Key(key::Event {
down: event.bKeyDown.as_bool(),
repeats: event.wRepeatCount,
code: key::CODES[event.wVirtualKeyCode as usize],
alt: mstate & ALT_KEYS > 0,
ctrl: mstate & CTRL_KEYS > 0,
shift: mstate & SHIFT_PRESSED > 0,
char: c,
})
}
}
Console::MOUSE_EVENT => { Console::MOUSE_EVENT => {
// OK I think it's safe to ignore these events since we're using the terminal // OK I think it's safe to ignore these events since we're using the terminal
// escape sequences. I think we never see them because terminal.exe is going to // escape sequences. I think we never see them because terminal.exe is going to
@ -381,6 +341,206 @@ impl From<Console::INPUT_RECORD> for Event {
} }
} }
/// A reference to a target node in a prefix tree, used for manipulating the prefix tree
#[derive(Debug, Clone)]
pub struct EscapeCursor {
target: Rc<EscapeNode>,
root: Rc<EscapeNode>,
}
/// A node in a prefix tree. This prefix tree is used to parse strings into escape sequences. This
/// prefix tree is special in that there are no values in the intermediate nodes of the tree.
#[derive(Debug)]
pub enum EscapeNode {
Root {
children: RefCell<Vec<Rc<EscapeNode>>>,
},
Nonterminal {
c: char,
parent: Weak<EscapeNode>,
children: RefCell<Vec<Rc<EscapeNode>>>,
},
Terminal {
c: char,
parent: Weak<EscapeNode>,
v: Escape,
},
}
impl EscapeNode {
/// Creates a new prefix tree mapping character sequences to structured escape codes
pub fn new() -> EscapeCursor {
let root = Rc::new(EscapeNode::Root {
children: RefCell::new(Vec::new()),
});
EscapeCursor {
target: Rc::clone(&root),
root,
}
}
fn char(&self) -> char {
match self {
EscapeNode::Nonterminal { c, .. } | EscapeNode::Terminal { c, .. } => *c,
_ => panic!(),
}
}
fn children(&self) -> RefMut<Vec<Rc<EscapeNode>>> {
match self {
EscapeNode::Root { children } | EscapeNode::Nonterminal { children, .. } => {
children.borrow_mut()
}
_ => panic!(),
}
}
fn parent(&self) -> Option<Rc<EscapeNode>> {
match self {
EscapeNode::Root { .. } => None,
EscapeNode::Nonterminal { parent, .. } => parent.upgrade(),
EscapeNode::Terminal { parent, .. } => parent.upgrade(),
}
}
fn child(&self, c: char) -> Option<Rc<EscapeNode>> {
for child in self.children().iter_mut() {
if child.char() == c {
return Some(Rc::clone(child));
}
}
None
}
}
impl EscapeCursor {
/// advances our cursor by one step. We look at the current position and descend the tree into
/// the node described by the target character. If that node is a terminal node, we return that
/// node's value.
fn step(&mut self, c: char) -> EscapeStep {
let child = match self.target.child(c) {
Some(c) => c,
None => {
let path = self.path();
return EscapeStep::Abort(format!("{path}{c}"));
}
};
match child.as_ref() {
EscapeNode::Terminal { v, .. } => {
self.reset();
EscapeStep::End(*v)
}
_ => {
self.target = child;
EscapeStep::Continue
}
}
}
fn add_step(&mut self, c: char) {
match self.target.child(c) {
Some(child) => self.target = child,
None => {
for child in self.target.clone().children().iter() {
if child.char() == c {
self.target = Rc::clone(child);
return;
}
}
let child = Rc::new(EscapeNode::Nonterminal {
c,
parent: Rc::downgrade(&self.target),
children: RefCell::new(Vec::new()),
});
self.target.children().push(Rc::clone(&child));
self.target = child;
}
}
}
/// adds a terminal node into the tree as a child of the current node
fn add_terminal(&mut self, c: char, v: Escape) {
for child in self.target.children().iter_mut() {
if child.char() == c {
panic!();
}
}
let child = Rc::new(EscapeNode::Terminal {
c,
parent: Rc::downgrade(&self.target),
v,
});
self.target.children().push(child);
}
/// resets the cursor back to the top of the tree
fn reset(&mut self) {
self.target = Rc::clone(&self.root);
}
fn is_at_root(&self) -> bool {
Rc::ptr_eq(&self.target, &self.root)
}
/// inserts a sequence -> Escape mapping into the prefix tree
fn insert(&mut self, sequence: &str, v: Escape) {
self.reset();
let mut chars = sequence.chars().peekable();
loop {
let c = chars.next().unwrap();
if chars.peek().is_none() {
self.add_terminal(c, v);
return;
}
self.add_step(c);
}
}
/// renders as a string the path that takes us to the node to which we are currently pointed
fn path(&self) -> String {
let mut cursor = self.clone();
let mut chars = Vec::new();
while !cursor.is_at_root() {
chars.push(cursor.target.char());
cursor.target = cursor.target.parent().unwrap();
}
chars.iter().rev().collect()
}
}
enum EscapeStep {
Continue,
Abort(String),
End(Escape),
}
/// generates a prefix tree used for parsing escape sequences
macro_rules! escapes {
($($sequence:literal $variant:tt)*) => {
/// a parsed escape sequence
#[derive(Debug, Clone, Copy)]
pub enum Escape {
Empty,
$(
$variant,
)*
}
/// assembles our prefix tree. Generally only needs to be called once. I think this could
/// actually be done in a proc macro that builds the prefix tree at compile time instead of
/// at run time.
fn build_prefix_tree() -> EscapeCursor {
let mut tree = EscapeNode::new();
$(
let v = Escape::$variant;
tree.insert($sequence, v);
)*
tree.reset();
tree
}
};
}
escapes! { escapes! {
"[A" Up "[A" Up
"[B" Down "[B" Down
@ -392,29 +552,119 @@ escapes! {
"[5~" PageUp "[5~" PageUp
"[6~" PageDown "[6~" PageDown
"[13;2u" Shift_Enter "[13;2u" ShiftEnter
"[13;5u" Ctrl_Enter "[13;3u" AltEnter
"[13;6u" Ctrl_Shift_Enter "[13;5u" CtrlEnter
"[13;6u" CtrlShiftEnter
"[1;2P" Shift_F1
"[1;2Q" Shift_F2
"[1;5P" Ctrl_F1
"[1;6P" Ctrl_Shift_F1
"[1;3P" Alt_F1
"[1;4P" Shift_Alt_F1
"OP" F1 "OP" F1
"[1;2P" ShiftF1
"[1;3P" AltF1
"[1;4P" ShiftAltF1
"[1;5P" CtrlF1
"[1;6P" CtrlShiftF1
"[1;8P" CtrlAltShiftF1
"OQ" F2 "OQ" F2
"[1;2Q" ShiftF2
"OR" F3 "OR" F3
"OS" F4 "OS" F4
"[15~" F5 "[15~" F5
"[15;2~" Shift_F5 "[15;2~" ShiftF5
"[17~" F6 "[17~" F6
"[18~" F7 "[18~" F7
"[19~" F8 "[19~" F8
"[20~" F9
"[21~" F10
"[23~" F11
"[24~" F12 "[24~" F12
"[53;5u" Ctrl_Shift_5 "[53;5u" CtrlShift5
}
/// a control character. Control characters are single individual ascii characters that represent
/// out of band signalling.
#[derive(Debug)]
pub enum ControlCharacter {
Null,
StartOfHeading,
StartOfText,
EndOfText,
EndOfTransmission,
Enquiry,
Acknowledge,
Bell,
Backspace,
Tab,
Linefeed,
VTab,
Formfeed,
CarriageReturn,
ShiftOut,
ShiftIn,
DataLinkEscape,
DeviceControl1,
DeviceControl2,
DeviceControl3,
DeviceControl4,
NegativeAcknowledge,
SynchronousIdle,
EndOfTransmissionBlock,
Cancel,
EndOfMedium,
Substitute,
Esc,
FileSeparator,
GroupSeparator,
RecordSeparator,
UnitSeparator,
}
impl TryFrom<Console::KEY_EVENT_RECORD> for ControlCharacter {
type Error = InputError;
fn try_from(v: Console::KEY_EVENT_RECORD) -> Result<Self, Self::Error> {
if !v.bKeyDown.as_bool() {
return Err(InputError::ControlCharactersOnlyGoDownNotUp);
}
use ControlCharacter::*;
unsafe {
match v.uChar.AsciiChar.0 {
0 => Ok(Null),
1 => Ok(StartOfHeading),
2 => Ok(StartOfText),
3 => Ok(EndOfText),
4 => Ok(EndOfTransmission),
5 => Ok(Enquiry),
6 => Ok(Acknowledge),
7 => Ok(Bell),
8 => Ok(Backspace),
9 => Ok(Tab),
10 => Ok(Linefeed),
11 => Ok(VTab),
12 => Ok(Formfeed),
13 => Ok(CarriageReturn),
14 => Ok(ShiftOut),
15 => Ok(ShiftIn),
16 => Ok(DataLinkEscape),
17 => Ok(DeviceControl1),
18 => Ok(DeviceControl2),
19 => Ok(DeviceControl3),
20 => Ok(DeviceControl4),
21 => Ok(NegativeAcknowledge),
22 => Ok(SynchronousIdle),
23 => Ok(EndOfTransmissionBlock),
24 => Ok(Cancel),
25 => Ok(EndOfMedium),
26 => Ok(Substitute),
27 => Ok(Esc),
28 => Ok(FileSeparator),
29 => Ok(GroupSeparator),
30 => Ok(RecordSeparator),
31 => Ok(UnitSeparator),
n => Err(InputError::UnrecognizedControlCharacter(n)),
}
}
}
} }
/*
*/

@ -0,0 +1,128 @@
use crate::{
edit,
history::History,
log::*,
output,
run::{self, Eval},
syntax::parse,
};
use std::{
error::Error,
io::{self, Write},
path::{Path, PathBuf},
};
use anyhow::Result;
use dirs;
/// An interactive session. The Session object is the top-level object used to control an
/// interactive terminal session.
pub struct Session {
pub editor: edit::Editor,
pub stdout: output::Writer,
pub stderr: output::Writer,
}
impl Session {
pub fn new() -> Result<Self> {
Ok(Self {
editor: edit::Editor::new()?,
stdout: output::Writer::stdout()?,
stderr: output::Writer::stderr()?,
})
}
pub fn run(mut self) -> Result<()> {
let mut state = run::State::new();
let mut ctx = run::Context {
stdout: self.stdout.clone(),
stderr: self.stderr.clone(),
state: &mut state,
};
let mut history = History::new();
info!("» shell session start --------");
loop {
self.editor.show_prompt()?;
let text = match self.editor.read_command(&mut history)? {
Some(text) => text,
None => break,
};
history.add(text.clone());
let command = match parse(&text) {
Ok(ast) => ast,
Err(e) => {
self.render_error(e)?;
continue;
}
};
if let Err(e) = command.eval(&mut ctx) {
self.render_error(e)?;
}
}
info!("» exit");
self.stdout.newline()?;
Ok(())
}
pub fn enable_logging<P>(&self, path: P)
where
P: AsRef<Path>,
{
let log_path = self.expand_path(path);
match Log::file(log_path) {
Ok(f) => {
let target = Box::leak(Box::new(f));
_ = set_logger(target).map(|()| set_max_level(LevelFilter::Debug));
}
Err(e) => {
println!("did not open log file: {}", e);
}
}
}
pub fn expand_path<P: AsRef<Path>>(&self, p: P) -> PathBuf {
let p = p.as_ref();
match p.to_str() {
Some(s) => {
if s.len() == 0 || !s.starts_with('~') {
return p.to_path_buf();
}
}
None => {
return p.to_path_buf();
}
}
let mut parts = p.components();
let first = parts.next().unwrap().as_os_str().to_str().unwrap();
if first == "~" {
dirs::home_dir().unwrap().join(parts)
} else {
let my_home = dirs::home_dir().unwrap();
let home_root = my_home.parent().unwrap();
let first = &first[1..];
let other_home = home_root.join(first);
other_home.join(parts)
}
}
pub fn render_error<E: Error>(&mut self, e: E) -> io::Result<()> {
self.render_error_helper(e, 0)?;
Ok(())
}
fn render_error_helper<E: Error>(&mut self, e: E, depth: u8) -> io::Result<()> {
if depth > 0 {
writeln!(self.stdout, " {e}")?;
} else {
writeln!(self.stdout, "{e}:")?;
}
if let Some(cause) = e.source() {
self.render_error_helper(cause, depth + 1)?;
}
Ok(())
}
}

@ -1,14 +1,18 @@
use std::fmt; use std::fmt;
use windows::Win32::System::Console;
#[derive(Debug, PartialEq, Clone, Copy)] /// an abstract keycode
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub struct Code { pub struct Code {
/// The integer value of the keycode /// The integer value of the keycode, as defined by the Windows api. These associate to windows
/// virtual key codes.
pub val: u16, pub val: u16,
/// The unicode symbol that represents the keycode, if any /// The unicode symbol that represents the keycode, if any
pub sym: Option<char>, pub sym: Option<char>,
} }
/// An individual keypress event
#[derive(Debug)] #[derive(Debug)]
pub struct Event { pub struct Event {
/// True for key press events, false for key release events /// True for key press events, false for key release events
@ -17,10 +21,26 @@ pub struct Event {
/// The number of times this event has happened in sequence /// The number of times this event has happened in sequence
pub repeats: u16, pub repeats: u16,
/// The virtual key code for the keyboard event /// The virtual keycode for the keyboard event. This represents the associated character
/// pressed on a keyboard, not the physical button. For example, on an AZERTY keyboard,
/// pressing the key whose legend says A but is in the position of the Q key on a QWERTY
/// keyboard,
///
/// For more info, see here:
/// https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
pub code: Code, pub code: Code,
/// The unicode character, or 💀 if not applicable /// The hardware-provided scancode. This is the location of the physical button, with no
/// relationship to the user's chosen layout.
pub scancode: u16,
/// the untranslated windows virtual keycode
pub keycode: u16,
/// The unicode character of the Event. Note this these correspond to virtual terminal
/// sequences. For example, when you press F1, you get an escape sequence of [OP, you'll see
/// the chars O and P but not related to their keys, because they're virtual keys. Hey wait a
/// second, that means I have to be folding the escape sequences into events.
pub char: char, pub char: char,
/// Whether or not one of the CTRL keys was held when the event was triggered /// Whether or not one of the CTRL keys was held when the event was triggered
@ -33,27 +53,74 @@ pub struct Event {
pub shift: bool, pub shift: bool,
} }
// HEY where did these come from who left these here
const ALT_KEYS: u32 = 0x0002 | 0x0001;
const CTRL_KEYS: u32 = 0x0008 | 0x0004;
const SHIFT_PRESSED: u32 = 0x0010;
impl From<Console::KEY_EVENT_RECORD> for Event {
fn from(record: Console::KEY_EVENT_RECORD) -> Self {
unsafe {
let mstate = record.dwControlKeyState;
let c = char::from_u32(record.uChar.UnicodeChar as u32).unwrap_or('💀');
let keycode = codes::lookup(record.wVirtualKeyCode as usize);
Self {
down: record.bKeyDown.as_bool(),
repeats: record.wRepeatCount,
code: keycode,
scancode: record.wVirtualScanCode,
keycode: record.wVirtualKeyCode,
alt: mstate & ALT_KEYS > 0,
ctrl: mstate & CTRL_KEYS > 0,
shift: mstate & SHIFT_PRESSED > 0,
char: c,
}
}
}
}
impl fmt::Display for Event { impl fmt::Display for Event {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let down = if self.down { '↓' } else { '↑' }; let down = if self.down { '↓' } else { '↑' };
let sym = match self.code.sym { let sym = match self.code.sym {
Some(c) => c, Some(c) => c,
None => '∅', None => '·',
}; };
let ctrl = if self.ctrl { '⎈' } else { '·' }; let ctrl = if self.ctrl { 'C' } else { '·' };
let alt = if self.alt { '⎇' } else { '·' }; let alt = if self.alt { 'A' } else { '·' };
let shift = if self.shift { '⇧'} else { '·' }; let shift = if self.shift { 'S' } else { '·' };
let c = if self.char.is_control() { '·' } else { self.char }; let glyph = if !self.char.is_control() {
write!(f, "{} {} {} {} {: >3} {} {: >1} {: >3}", down, ctrl, alt, shift, self.code.val, sym, c, self.char as u16) self.char
} else if self.char as u32 == 27 {
'⁝'
} else {
'·'
};
write!(
f,
"{down} {ctrl} {alt} {shift} {glyph} {charcode: <3} {sym} {code: <3} {keycode: <3} {scancode: <3}",
charcode = self.char as u32,
code = self.code.val,
keycode = self.keycode,
scancode = self.scancode,
)
} }
} }
/// CODES contains a lookup table for key codes. Note that this is a sparse array and not all macro_rules! codes {
/// values associate to valid key codes.
pub static CODES: [Code; 256] = gen_codes();
macro_rules! keycodes {
($($val:literal $name:ident $sym:literal)*) => { ($($val:literal $name:ident $sym:literal)*) => {
/// stores our key code constants
pub mod codes {
use super::Code;
$(
#[allow(unused)]
pub const $name: Code = Code{val: $val, sym: Some($sym)};
)*
/// generates a table of key codes
/// https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
const fn gen_codes() -> [Code; 256] { const fn gen_codes() -> [Code; 256] {
let mut codes = [Code{val: 0, sym: None}; 256]; let mut codes = [Code{val: 0, sym: None}; 256];
let mut i = 0 as usize; let mut i = 0 as usize;
@ -68,39 +135,53 @@ macro_rules! keycodes {
codes codes
} }
$( /// Lookup table to retrieve keycodes by their numeric value
#[allow(dead_code)] const INDEX: [Code; 256] = gen_codes();
pub const $name: Code = Code{val: $val, sym: Some($sym)};
)* /// Translates the numeric value of a win32 VirtualKeyCode into a key::Code value
pub const fn lookup(vk_code: usize) -> Code {
INDEX[vk_code]
}
}
} }
} }
keycodes! { codes! {
0x07 NOTBACKSPACE '⌫' 0x01 MOUSE_LEFT ' '
0x02 MOUSE_RIGHT ' '
0x03 CTRL_BREAK ' ' // control-break processing
0x04 MIDDLE_MOUSE ' '
0x05 X1_MOUSE ' '
0x06 X2_MOUSE ' ' // 0x07 is reserved
0x08 BACKSPACE '⌫' 0x08 BACKSPACE '⌫'
0x09 TAB '↹' 0x09 TAB '↹' // 0x0A-0B Reserved
// 0x0A-0B Reserved 0x0C CLEAR ' '
// 0x0C CLEAR 0x0D ENTER '↩' // 0x0E-0F Unassigned
0x0D ENTER '↩'
// 0x0E-0F Undefined
0x10 SHIFT '⇧' 0x10 SHIFT '⇧'
0x11 CTRL '⎈' 0x11 CTRL '⎈'
0x12 ALT '⎇' 0x12 ALT '⎇'
0x13 BREAK '⎉' 0x13 PAUSE '⎉'
0x14 CAPS_LOCK '⇪' 0x14 CAPS_LOCK '⇪'
// 0x15 IME Kana mode 0x15 KANA_MODE ' '
// 0x15 IME Hanguel mode (maintained for compatibility; use VK_HANGUL)
// 0x15 IME Hangul mode
// 0x16 IME On
// 0x17 IME Junja mode
// 0x18 IME final mode
// 0x19 IME Hanja mode
// 0x19 IME Kanji mode
// 0x1A IME Off
// 0x1C IME convert
0x1B ESC '⎋' 0x1B ESC '⎋'
0x20 SPACE '␣' 0x20 SPACE '␣'
0x21 PAGE_UP '↟'
0x22 PAGE_DOWN '↡'
0x23 END '⇲' 0x23 END '⇲'
0x25 LEFT '←'
0x26 UP '↑'
0x27 RIGHT '→'
0x28 DOWN '↓'
0x30 NUM_0 '0'
0x31 NUM_1 '1'
0x32 NUM_2 '2'
0x33 NUM_3 '3'
0x34 NUM_4 '4'
0x35 NUM_5 '5'
0x36 NUM_6 '6'
0x37 NUM_7 '7'
0x38 NUM_8 '8'
0x39 NUM_9 '9'
0x41 A 'a' 0x41 A 'a'
0x42 B 'b' 0x42 B 'b'
0x43 C 'c' 0x43 C 'c'
@ -127,144 +208,8 @@ keycodes! {
0x58 X 'x' 0x58 X 'x'
0x59 Y 'y' 0x59 Y 'y'
0x5A Z 'z' 0x5A Z 'z'
0x30 NUM_0 '0'
0x31 NUM_1 '1'
0x32 NUM_2 '2'
0x33 NUM_3 '3'
0x34 NUM_4 '4'
0x35 NUM_5 '5'
0x36 NUM_6 '6'
0x37 NUM_7 '7'
0x38 NUM_8 '8'
0x39 NUM_9 '9'
// 0x70 F1
// 0x71 F2
// 0x72 F3
// 0x73 F4
// 0x74 F5
// 0x75 F6
// 0x76 F7
// 0x77 F8
// 0x78 F9
// 0x79 F10
// 0x7A F11
// 0x7B F12
// 0x7C F13
// 0x7D F14
// 0x7E F15
// 0x7F F16
// 0x80 F17
// 0x81 F18
// 0x82 F19
// 0x83 F20
// 0x84 F21
// 0x85 F22
// 0x86 F23
// 0x87 F24
0x21 PAGE_UP '↟'
0x22 PAGE_DOWN '↡'
0x25 LEFT '←'
0x26 UP '↑'
0x27 RIGHT '→'
0x28 DOWN '↓'
0xA0 LEFT_SHIFT '⇧' 0xA0 LEFT_SHIFT '⇧'
0xA1 RIGHT_SHIFT '⇧' 0xA1 RIGHT_SHIFT '⇧'
0xBE PERIOD '.' 0xBE PERIOD '.'
0xDE QUOTE '\'' 0xDE QUOTE '\''
} }
/*
VK_NONCONVERT 0x1D IME nonconvert
VK_ACCEPT 0x1E IME accept
VK_MODECHANGE 0x1F IME mode change request
VK_HOME 0x24 HOME key
VK_SELECT 0x29 SELECT key
VK_PRINT 0x2A PRINT key
VK_EXECUTE 0x2B EXECUTE key
VK_SNAPSHOT 0x2C PRINT SCREEN key
VK_INSERT 0x2D INS key
VK_DELETE 0x2E DEL key
VK_HELP 0x2F HELP key
- 0x3A-40 Undefined
VK_LWIN 0x5B Left Windows key (Natural keyboard)
VK_RWIN 0x5C Right Windows key (Natural keyboard)
VK_APPS 0x5D Applications key (Natural keyboard)
- 0x5E Reserved
VK_SLEEP 0x5F Computer Sleep key
VK_NUMPAD0 0x60 Numeric keypad 0 key
VK_NUMPAD1 0x61 Numeric keypad 1 key
VK_NUMPAD2 0x62 Numeric keypad 2 key
VK_NUMPAD3 0x63 Numeric keypad 3 key
VK_NUMPAD4 0x64 Numeric keypad 4 key
VK_NUMPAD5 0x65 Numeric keypad 5 key
VK_NUMPAD6 0x66 Numeric keypad 6 key
VK_NUMPAD7 0x67 Numeric keypad 7 key
VK_NUMPAD8 0x68 Numeric keypad 8 key
VK_NUMPAD9 0x69 Numeric keypad 9 key
VK_MULTIPLY 0x6A Multiply key
VK_ADD 0x6B Add key
VK_SEPARATOR 0x6C Separator key
VK_SUBTRACT 0x6D Subtract key
VK_DECIMAL 0x6E Decimal key
VK_DIVIDE 0x6F Divide key
- 0x88-8F Unassigned
VK_NUMLOCK 0x90 NUM LOCK key
VK_SCROLL 0x91 SCROLL LOCK key
0x92-96 OEM specific
- 0x97-9F Unassigned
VK_LCONTROL 0xA2 Left CONTROL key
VK_RCONTROL 0xA3 Right CONTROL key
VK_LMENU 0xA4 Left ALT key
VK_RMENU 0xA5 Right ALT key
VK_BROWSER_BACK 0xA6 Browser Back key
VK_BROWSER_FORWARD 0xA7 Browser Forward key
VK_BROWSER_REFRESH 0xA8 Browser Refresh key
VK_BROWSER_STOP 0xA9 Browser Stop key
VK_BROWSER_SEARCH 0xAA Browser Search key
VK_BROWSER_FAVORITES 0xAB Browser Favorites key
VK_BROWSER_HOME 0xAC Browser Start and Home key
VK_VOLUME_MUTE 0xAD Volume Mute key
VK_VOLUME_DOWN 0xAE Volume Down key
VK_VOLUME_UP 0xAF Volume Up key
VK_MEDIA_NEXT_TRACK 0xB0 Next Track key
VK_MEDIA_PREV_TRACK 0xB1 Previous Track key
VK_MEDIA_STOP 0xB2 Stop Media key
VK_MEDIA_PLAY_PAUSE 0xB3 Play/Pause Media key
VK_LAUNCH_MAIL 0xB4 Start Mail key
VK_LAUNCH_MEDIA_SELECT 0xB5 Select Media key
VK_LAUNCH_APP1 0xB6 Start Application 1 key
VK_LAUNCH_APP2 0xB7 Start Application 2 key
- 0xB8-B9 Reserved
VK_OEM_1 0xBA Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the ';:' key
VK_OEM_PLUS 0xBB For any country/region, the '+' key
VK_OEM_COMMA 0xBC For any country/region, the ',' key
VK_OEM_MINUS 0xBD For any country/region, the '-' key
VK_OEM_PERIOD 0xBE For any country/region, the '.' key
VK_OEM_2 0xBF Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the '/?' key
VK_OEM_3 0xC0 Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the '`~' key
- 0xC1-D7 Reserved
- 0xD8-DA Unassigned
VK_OEM_4 0xDB Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the '[{' key
VK_OEM_5 0xDC Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the '\|' key
VK_OEM_6 0xDD Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the ']}' key
VK_OEM_7 0xDE Used for miscellaneous characters; it can vary by keyboard. For the US standard keyboard, the 'single-quote/double-quote' key
VK_OEM_8 0xDF Used for miscellaneous characters; it can vary by keyboard.
- 0xE0 Reserved
0xE1 OEM specific
VK_OEM_102 0xE2 The <> keys on the US standard keyboard, or the \\| key on the non-US 102-key keyboard
0xE3-E4 OEM specific
VK_PROCESSKEY 0xE5 IME PROCESS key
0xE6 OEM specific
VK_PACKET 0xE7 Used to pass Unicode characters as if they were keystrokes. The VK_PACKET key is the low word of a 32-bit Virtual Key value used for non-keyboard input methods. For more information, see Remark in KEYBDINPUT, SendInput, WM_KEYDOWN, and WM_KEYUP
- 0xE8 Unassigned
0xE9-F5 OEM specific
VK_ATTN 0xF6 Attn key
VK_CRSEL 0xF7 CrSel key
VK_EXSEL 0xF8 ExSel key
VK_EREOF 0xF9 Erase EOF key
VK_PLAY 0xFA Play key
VK_ZOOM 0xFB Zoom key
VK_NONAME 0xFC Reserved
VK_PA1 0xFD PA1 key
VK_OEM_CLEAR 0xFE Clear key
*/

@ -0,0 +1,380 @@
use crate::{
error::LexError,
topo::{Glyph, Glyphs, Position},
};
use std::{collections::VecDeque, fmt, ops::Range};
/// A Lexeme is the text of a given [Token]. The lexeme contains no information about the Token's
/// meaning or type; it is just a fancy string with position information.
#[derive(PartialEq, Clone)]
pub struct Lexeme {
elems: Vec<Glyph>,
}
impl Lexeme {
/// The area of the input text covered by this lexeme. This might return none of the Lexeme is
/// an empty string.
fn span(&self) -> Option<Range<Position>> {
if self.elems.is_empty() {
return None;
}
Some(Range {
start: self.elems[0].position,
end: self.elems[self.elems.len() - 1].position,
})
}
fn text(&self) -> String {
self.elems.as_slice().iter().map(|tg| tg.glyph).collect()
}
}
impl fmt::Debug for Lexeme {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
let span = match self.span() {
Some(span) => span,
None => return write!(f, "<empty Lexeme>"),
};
write!(
f,
"<{text} @{start_line}:{start_column}-{end_line}:{end_column}>",
start_line = span.start.line,
start_column = span.start.column,
end_line = span.end.line,
end_column = span.end.column,
text = self.text(),
)
}
}
impl fmt::Display for Lexeme {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(f, "{}", self.text())
}
}
impl From<Vec<Glyph>> for Lexeme {
fn from(v: Vec<Glyph>) -> Self {
Self { elems: v }
}
}
/// The smallest lexical unit of our language. A token represents a single element of program
/// source text, such as an identifier or a piece of punctuation.
#[allow(dead_code)]
#[derive(Debug, PartialEq, Clone)]
pub enum Token {
/// A bare word: a sequence of characters without any quotes. A bare word is always not a glob.
Word(Lexeme),
/// A bare word containing 1 or more of the special characters ? or *
Glob(Lexeme),
Semi(Glyph),
Pipe(Glyph),
}
impl Token {
/// Checks whether or not another token has the same contents as this Token. The eq
/// implementation will also consider positional information, whereas the `same` function does
/// not.
#[allow(unused)]
fn same(&self, other: &Self) -> bool {
use Token::*;
println!(
"check same self: {self:?} other: {other:?}",
self = self.text(),
other = other.text()
);
match (self, other) {
(Word(a), Word(b)) => a.text() == b.text(),
(Glob(a), Glob(b)) => a.text() == b.text(),
_ => false,
}
}
fn text(&self) -> String {
use Token::*;
match self {
Word(lexeme) | Glob(lexeme) => lexeme.text(),
Pipe(glyph) | Semi(glyph) => String::from(glyph.glyph),
}
}
}
/// Tokenizer splits some input [Glyphs] into [Token] values. The Tokenize has no lookahead or
/// buffering.
struct Tokenizer<'text> {
source: Glyphs<'text>,
}
impl<'text> Tokenizer<'text> {
/// creates a new tokenizer for some given input source text
pub fn new(text: &'text str) -> Self {
Self {
source: Glyphs::new(text),
}
}
/// produces the next token in our input source text. If we've hit EOF, this will return None.
fn next_token(&mut self) -> Option<Result<Token, LexError>> {
self.source.yeet_whitespace();
let next = self.source.next()?;
match next.glyph {
_ if next.is_word() => Some(self.lex_bare_string(vec![next])),
_ if next.is_glob() => Some(self.lex_glob(vec![next])),
'|' => Some(Ok(Token::Pipe(next))),
'@' => Some(self.lex_var(vec![next])),
'\'' => Some(self.lex_raw_string(vec![next])),
'"' => Some(self.lex_string_literal(vec![])),
';' => Some(Ok(Token::Semi(next))),
_ => Some(Err(LexError::UnexpectedCharacter(next))),
}
}
fn lex_bare_string(&mut self, mut progress: Vec<Glyph>) -> Result<Token, LexError> {
while let Some(next) = self.source.peek() {
match next.glyph {
_ if next.glyph.is_whitespace() => break,
_ if next.is_word() => progress.push(self.source.pop()?),
_ if next.is_glob() => {
progress.push(self.source.pop()?);
return self.lex_glob(progress);
}
';' | '|' => break,
_ => {
return Err(LexError::UnexpectedCharacterInBareString(
self.source.pop()?,
))
}
}
}
if progress.is_empty() {
Err(LexError::UnexpectedEOF(self.source.next_position))
} else {
Ok(Token::Word(progress.into()))
}
}
fn lex_glob(&mut self, mut progress: Vec<Glyph>) -> Result<Token, LexError> {
while let Some(next) = self.source.peek() {
match next.glyph {
';' => break,
_ if next.glyph.is_whitespace() => break,
_ if next.is_glob() => progress.push(self.source.pop()?),
// '\\' => {
// self.source.pop()?;
// progress.push(self.source.pop()?);
// }
_ => return Err(LexError::UnexpectedCharacterInGlob(self.source.pop()?)),
}
}
if progress.is_empty() {
Err(LexError::UnexpectedEOF(self.source.next_position))
} else {
Ok(Token::Glob(progress.into()))
}
}
fn lex_raw_string(&mut self, _progress: Vec<Glyph>) -> Result<Token, LexError> {
Err(LexError::not_yet(
self.source.next_position,
"raw strings not done yet",
))
}
fn lex_string_literal(&mut self, mut progress: Vec<Glyph>) -> Result<Token, LexError> {
while let Some(next) = self.source.peek() {
match next.glyph {
_ if next.glyph.is_whitespace() => progress.push(self.source.pop()?),
_ if next.is_word() => progress.push(self.source.pop()?),
_ if next.is_glob() => progress.push(self.source.pop()?),
'"' => {
self.source.pop()?;
break;
}
_ => todo!(),
}
}
if progress.is_empty() {
Err(LexError::UnexpectedEOF(self.source.next_position))
} else {
Ok(Token::Word(progress.into()))
}
}
fn lex_var(&mut self, _progress: Vec<Glyph>) -> Result<Token, LexError> {
Err(LexError::not_yet(
self.source.next_position,
"variables are not done yet",
))
}
}
impl<'text> Iterator for Tokenizer<'text> {
type Item = Result<Token, LexError>;
fn next(&mut self) -> Option<Self::Item> {
self.next_token()
}
}
/// A Lexer is responsible for converting a piece of source text into a sequence of tokens.
pub struct Lexer<'text> {
source: Tokenizer<'text>,
lookahead: VecDeque<Token>,
}
impl<'text> Lexer<'text> {
pub fn new(source: &'text str) -> Self {
Self {
source: Tokenizer::new(source),
lookahead: VecDeque::new(),
}
}
fn fill_lookahead(&mut self, n: usize) -> Result<bool, LexError> {
while self.lookahead.len() < n {
let token = match self.source.next() {
Some(res) => res?,
None => return Ok(false),
};
self.lookahead.push_back(token);
}
Ok(true)
}
fn peek_at(&mut self, idx: usize) -> Result<Option<&Token>, LexError> {
self.fill_lookahead(idx + 1)?;
Ok(self.lookahead.get(idx))
}
pub fn peek(&mut self) -> Result<Option<&Token>, LexError> {
self.peek_at(0)
}
}
impl<'text> Iterator for Lexer<'text> {
type Item = Result<Token, LexError>;
fn next(&mut self) -> Option<Self::Item> {
match self.lookahead.pop_front() {
Some(token) => Some(Ok(token)),
None => self.source.next(),
}
}
}
/// splits a corpus into Tokens.
#[cfg(test)]
pub fn lex(source: &str) -> Result<Vec<Token>, LexError> {
Lexer::new(source).collect()
}
#[cfg(test)]
mod tests {
use super::*;
use std::iter::zip;
fn lexeme(txt: &str) -> Lexeme {
let x: Vec<Glyph> = txt
.chars()
.map(|c| Glyph {
glyph: c,
position: Position::origin(),
bytes: 0..0,
})
.collect();
x.into()
}
fn glob(txt: &str) -> Token {
Token::Glob(lexeme(txt))
}
fn word(txt: &str) -> Token {
Token::Word(lexeme(txt))
}
macro_rules! accept {
( $($test_name:ident $input_text:literal [ $( $token:expr )* ])+) => {$(
#[test]
fn $test_name() -> Result<(), LexError> {
#[allow(unused_mut)]
let mut expected: Vec<Token> = Vec::new();
$( expected.push($token); )*
let lexed = lex($input_text)?;
assert_eq!(expected.len(), lexed.len());
for pair in zip(expected, lexed) {
println!("pair: {pair:?}");
assert!(pair.0.same(&pair.1));
}
Ok(())
}
)*};
}
macro_rules! reject {
($($test_name:ident: $input_text:literal;)+) => {$(
#[test]
fn $test_name() {
match lex($input_text) {
Ok(tokens) => {
println!("output tokens: {tokens:?}");
panic!("Did not encounter an expected lex error");
}
Err(e) => println!("output error: {e:?}"),
}
}
)*};
}
reject! {
// Vars aren't done yet
var: "@name";
// Single-quoted strings arne't done yet
strings: r"echo 'one' two";
}
accept! {
empty "" []
spaces " " []
identifier "a" [ word("a") ]
identifier_2 " a" [ word("a") ]
identifier_3 "a " [ word("a") ]
identifier_4 " a " [ word("a") ]
double_quoted_strings r#"echo "one" two"# [
word("echo")
word("one")
word("two")
]
file_name "poop.exe" [ word("poop.exe") ]
multi_idents "one two three four " [
word("one")
word("two")
word("three")
word("four")
]
glob_1 "*" [ glob("*") ]
glob_2 " * " [ glob("*") ]
glob_3 "x*" [ glob("x*") ]
glob_4 "*x" [ glob("*x") ]
glob_5 "*.py" [ glob("*.py") ]
mixed_1 "ls *.py" [ word("ls") glob("*.py") ]
abs "cd c:\\one\\two" [ word("cd") word("c:\\one\\two")]
home "cd ~\\stuff" [ word("cd") word("~\\stuff") ]
}
}

@ -1,146 +0,0 @@
use crate::log::*;
use std::cmp::min;
pub struct Line {
/// the current contents of the line
chars: Vec<char>,
/// the cursor position of our dit head within the vector of characters that we store as chars
cursor: usize,
}
impl Line {
pub fn new() -> Self {
Self {
chars: Vec::new(),
cursor: 0,
}
}
pub fn clear(&mut self) {
self.cursor = 0;
self.chars.clear();
self.show_debug();
}
pub fn pop(&mut self) -> String {
let s: String = self.chars.iter().collect();
self.clear();
self.show_debug();
s
}
pub fn show(&self) -> String {
self.chars.iter().collect()
}
pub fn back(&mut self, n: usize) -> bool {
if self.cursor > 0 {
if n > self.cursor {
self.cursor = n;
} else {
self.cursor -= n;
}
self.show_debug();
true
} else {
false
}
}
pub fn backspace(&mut self) -> bool {
if self.chars.len() > 0 {
if self.cursor > 0 {
self.cursor -= 1;
}
self.chars.remove(self.cursor);
self.show_debug();
true
} else {
false
}
}
/// removes the elements left of the cursor, returning the count of removed elements
pub fn clear_left(&mut self) -> usize {
if self.cursor > 0 {
let n = self.chars.drain(..self.cursor).count();
self.cursor = 0;
self.show_debug();
n
} else {
0
}
}
/// moves the cursor to the start of the edit line, returning the old cursor value
pub fn seek_left(&mut self) -> usize {
let n = self.cursor;
self.cursor = 0;
self.show_debug();
n
}
pub fn seek_right(&mut self) -> usize {
let n = self.chars.len() - self.cursor;
self.cursor = self.chars.len();
self.show_debug();
n
}
pub fn forward(&mut self, n: usize) -> bool {
if self.cursor < self.chars.len() {
self.cursor = min(self.chars.len(), self.cursor + n);
self.show_debug();
true
} else {
false
}
}
pub fn insert(&mut self, c: char) {
self.chars.insert(self.cursor, c);
self.cursor += 1;
self.show_debug();
}
pub fn tail(&self) -> String {
let mut start = self.cursor;
if start > 0 {
start -= 1;
}
let chars = &self.chars[start..];
chars.iter().collect()
}
pub fn pos(&self) -> usize {
self.cursor
}
pub fn len(&self) -> usize {
self.chars.len()
}
fn show_debug(&self) {
let before = if self.chars.len() > 0 {
self.chars[0..self.cursor].to_vec().iter().collect()
} else {
String::new()
};
let after = if self.chars.len() > 0 {
self.chars[self.cursor..].to_vec().iter().collect()
} else {
String::new()
};
debug!(
" {: <5} {: >5} {}·{}",
self.cursor,
self.chars.len(),
before,
after
);
}
}

@ -1,5 +1,5 @@
use crate::error::Error; use crate::error::Error;
pub use log::{debug, error, info, set_logger, set_max_level, trace, warn, LevelFilter}; pub use log::{debug, info, set_logger, set_max_level, warn, LevelFilter};
use std::{ use std::{
fs::File, fs::File,
@ -49,19 +49,19 @@ where
match record.level() { match record.level() {
log::Level::Error => { log::Level::Error => {
_ = write!(out, "\x1b[31m{}\x1b[0m\n", record.args()); _ = write!(out, "\x1b[31m{}\x1b[0m\n", record.args());
}, }
log::Level::Warn => { log::Level::Warn => {
_ = write!(out, "\x1b[33m{}\x1b[0m\n", record.args()); _ = write!(out, "\x1b[33m{}\x1b[0m\n", record.args());
}, }
log::Level::Info => { log::Level::Info => {
_ = write!(out, "\x1b[37m{}\x1b[0m\n", record.args()); _ = write!(out, "\x1b[37m{}\x1b[0m\n", record.args());
}, }
log::Level::Debug => { log::Level::Debug => {
_ = write!(out, "\x1b[90m{}\x1b[0m\n", record.args()); _ = write!(out, "\x1b[90m{}\x1b[0m\n", record.args());
}, }
log::Level::Trace => { log::Level::Trace => {
_ = write!(out, "\x1b[36m{}\x1b[0m\n", record.args()); _ = write!(out, "\x1b[36m{}\x1b[0m\n", record.args());
}, }
} }
} }
} }

@ -1,205 +1,52 @@
/// builtin functions
mod builtin;
/// all of the errors for the clyde project live in this module
mod error; mod error;
mod ext;
/// handles input from terminals
mod input; mod input;
/// defines the interactive mode
mod interactive;
/// key presses, independent from input sources
mod key; mod key;
mod line;
/// lexical analysis
mod lex;
/// a real primitive line editor
mod edit;
mod history;
/// logging configuration
mod log; mod log;
/// stdout control, output more generally
mod output; mod output;
/// turns our tokens into parse trees
mod parse; mod parse;
mod prompt;
mod shell;
use crate::log::*;
use prompt::Prompt;
use shell::Shell;
use std::io::Write;
use anyhow::Result;
fn main() -> Result<()> {
let mut shell = Shell::new()?;
let log_path = shell.expand_path("~/clyde.log");
match Log::file(log_path) {
Ok(f) => {
let target = Box::leak(Box::new(f));
_ = set_logger(target).map(|()| set_max_level(LevelFilter::Debug));
}
Err(e) => {
println!("did not open log file: {}", e);
}
}
let prompt = Prompt::new();
prompt.print(&mut shell.output)?;
info!("» enter");
loop {
match shell.input.next()? {
input::Event::Key(event) => {
if event.down {
if event.code.val == 0 {
debug!(" {}", event);
} else {
warn!(" {}", event);
}
continue;
}
info!(" {}", event);
if event.code == key::LEFT {
shell.back(1)?;
continue;
}
if event.code == key::RIGHT {
shell.forward(1)?;
continue;
}
if event.code == key::ENTER {
shell.output.newline()?;
let s = shell.line.pop();
info!("◇ {}", s);
let tree = parse::parse(&s)?;
debug!(" {:?}", tree);
shell.exec(tree.into())?;
// 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.
shell.reset()?;
prompt.print(&mut shell.output)?;
continue;
}
if event.code == key::TAB {
continue;
}
if event.code == key::BACKSPACE {
if shell.line.backspace() {
// move cursor back two spaces
shell.output.back(2)?;
let tail = format!("{} ", shell.line.tail());
let n = tail.chars().count();
shell.output.write(tail.as_bytes())?;
// after writing out the tail, rewind by the number of characters in
// the tail
if n > 1 {
// let text = format!("\x1b[{}D", n - 1);
shell.output.back(n - 1)?;
// output.write(text.as_bytes())?;
} else {
// honestly I can't remember how I figured this out
shell.output.write(b" \x1b[1D")?;
}
}
continue;
}
// CTRL-D to exit
if event.ctrl && event.code == key::D {
info!("» exit");
shell.output.close()?;
return Ok(());
}
// CTRL-J to draw a cool little dot
if event.ctrl && event.code == key::J {
debug!("⎈ j: dot");
// red bullet
shell
.output
.write(String::from("\x1b[31m\u{2022}\x1b[0m").as_bytes())?;
continue;
}
// CTRL-L to clear the screen
if event.ctrl && event.code == key::L {
info!("» clear");
shell.output.clear()?;
prompt.print(&mut shell.output)?;
shell.output.write(shell.line.show().as_bytes())?;
shell.output.back(shell.line.len() - shell.line.pos())?;
shell.reset()?;
continue;
}
// CTRL-U to erase to the beginning of the line
if event.ctrl && event.code == key::U {
info!("» clear left");
let n = shell.line.clear_left();
if n > 0 {
// move left by the number of elements removed
shell.output.back(n)?;
// draw the elements remaining, followed by a space for each removed
// element
let kept = shell.line.show();
let text = format!("{}{:width$}", kept, "", width = n);
shell.output.write(text.as_bytes())?;
shell.output.back(n + kept.chars().count())?;
}
continue;
}
// CTRL-A to move to the beginning of the line
if event.ctrl && event.code == key::A {
shell.seek_left()?;
continue;
}
// CTRL-E to move to the end of the line
if event.ctrl && event.code == key::E {
shell.seek_right()?;
continue;
}
// TODO: something better here, this is crappy. I should be checking characters
// based on their unicode categories, not this garbo
if !event.char.is_control() {
shell.line.insert(event.char);
let tail = shell.line.tail();
let n = tail.chars().count();
// write everything from the current line cursor out to the output buffer.
shell.output.write(tail.as_bytes())?;
if n > 1 {
// 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.
shell.output.back(n - 1)?;
}
continue;
}
warn!("‽ {}", event);
}
input::Event::Left => shell.back(1)?,
input::Event::Right => shell.forward(1)?,
input::Event::Up => debug!("⛬ ↑"),
input::Event::Down => debug!("⛬ ↓"),
input::Event::Home => shell.seek_left()?,
input::Event::End => shell.seek_right()?,
input::Event::Focus(true) => {}
input::Event::Focus(false) => {}
input::Event::Menu(_command_id) => {}
input::Event::Drop(seq) => debug!("? {}", seq),
input::Event::Mouse { .. } => {}
input::Event::Size => {}
}
}
}
/* /// why is prompt a whole module what am i doing
mod prompt;
> ls /// the runtime state of the shell process
foo mod run;
bar
whatever
> ls /// syntax and semantic analysis
mod syntax;
/// topoglyph is a real word, i promise
mod topo;
fn main() -> anyhow::Result<()> {
let session = interactive::Session::new()?;
#[cfg(debug_assertions)]
session.enable_logging("~/clyde.log");
*/ session.run()
}

@ -1,99 +1,9 @@
use crate::{error::Error, log::*}; use crate::error::Error;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use std::io::{self, Write}; use std::io::{self, Write};
use windows::Win32::{ use windows::Win32::{Foundation::HANDLE, System::Console};
Foundation::{CloseHandle, HANDLE},
System::Console,
};
#[allow(dead_code)]
fn log_output_mode(mode: Console::CONSOLE_MODE) {
// Characters written by the WriteFile or WriteConsole function or echoed by the ReadFile or
// ReadConsole function are parsed for ASCII control sequences, and the correct action is
// performed. Backspace, tab, bell, carriage return, and line feed characters are processed. It
// should be enabled when using control sequences or when ENABLE_VIRTUAL_TERMINAL_PROCESSING is
// set.
if (mode & Console::ENABLE_PROCESSED_OUTPUT).0 > 0 {
debug!("Processed Output: Enabled");
} else {
debug!("Processed Output: Disabled");
}
// When writing with WriteFile or WriteConsole or echoing with ReadFile or ReadConsole, the
// cursor moves to the beginning of the next row when it reaches the end of the current row.
// This causes the rows displayed in the console window to scroll up automatically when the
// cursor advances beyond the last row in the window. It also causes the contents of the
// console screen buffer to scroll up (../discarding the top row of the console screen buffer)
// when the cursor advances beyond the last row in the console screen buffer. If this mode is
// disabled, the last character in the row is overwritten with any subsequent characters.
if (mode & Console::ENABLE_WRAP_AT_EOL_OUTPUT).0 > 0 {
debug!("Wrap at EOL: Enabled");
} else {
debug!("Wrap at EOL: Disabled");
}
// When writing with WriteFile or WriteConsole, characters are parsed for VT100 and similar
// control character sequences that control cursor movement, color/font mode, and other
// operations that can also be performed via the existing Console APIs. For more information,
// see Console Virtual Terminal Sequences.
//
// Ensure ENABLE_PROCESSED_OUTPUT is set when using this flag.
if (mode & Console::ENABLE_VIRTUAL_TERMINAL_PROCESSING).0 > 0 {
debug!("Terminal Processing: Enabled");
} else {
debug!("Terminal Processing: Disabled");
}
// When writing with WriteFile or WriteConsole, this adds an additional state to end-of-line
// wrapping that can delay the cursor move and buffer scroll operations.
//
// Normally when ENABLE_WRAP_AT_EOL_OUTPUT is set and text reaches the end of the line, the
// cursor will immediately move to the next line and the contents of the buffer will scroll up
// by one line. In contrast with this flag set, the cursor does not move to the next line, and
// the scroll operation is not performed. The written character will be printed in the final
// position on the line and the cursor will remain above this character as if
// ENABLE_WRAP_AT_EOL_OUTPUT was off, but the next printable character will be printed as if
// ENABLE_WRAP_AT_EOL_OUTPUT is on. No overwrite will occur. Specifically, the cursor quickly
// advances down to the following line, a scroll is performed if necessary, the character is
// printed, and the cursor advances one more position.
//
// The typical usage of this flag is intended in conjunction with setting
// ENABLE_VIRTUAL_TERMINAL_PROCESSING to better emulate a terminal emulator where writing the
// final character on the screen (../in the bottom right corner) without triggering an
// immediate scroll is the desired behavior.
if (mode & Console::DISABLE_NEWLINE_AUTO_RETURN).0 > 0 {
debug!("Newline Auto Return: Enabled");
} else {
debug!("Newline Auto Return: Disabled");
}
// The APIs for writing character attributes including WriteConsoleOutput and
// WriteConsoleOutputAttribute allow the usage of flags from character attributes to adjust the
// color of the foreground and background of text. Additionally, a range of DBCS flags was
// specified with the COMMON_LVB prefix. Historically, these flags only functioned in DBCS code
// pages for Chinese, Japanese, and Korean languages.
//
// With exception of the leading byte and trailing byte flags, the remaining flags describing
// line drawing and reverse video (../swap foreground and background colors) can be useful for
// other languages to emphasize portions of output.
//
// Setting this console mode flag will allow these attributes to be used in every code page on
// every language.
//
// It is off by default to maintain compatibility with known applications that have
// historically taken advantage of the console ignoring these flags on non-CJK machines to
// store bits in these fields for their own purposes or by accident.
//
// Note that using the ENABLE_VIRTUAL_TERMINAL_PROCESSING mode can result in LVB grid and
// reverse video flags being set while this flag is still off if the attached application
// requests underlining or inverse video via Console Virtual Terminal Sequences.
if (mode & Console::ENABLE_LVB_GRID_WORLDWIDE).0 > 0 {
debug!("LVB Grid: Enabled");
} else {
debug!("LVB Grid: Disabled");
}
}
#[derive(Clone)]
pub struct Writer { pub struct Writer {
output: HANDLE, output: HANDLE,
} }
@ -103,17 +13,20 @@ impl Writer {
unsafe { unsafe {
let handle = Console::GetStdHandle(Console::STD_OUTPUT_HANDLE) let handle = Console::GetStdHandle(Console::STD_OUTPUT_HANDLE)
.context("unable to get stdout handle")?; .context("unable to get stdout handle")?;
let mut stdout = Self{output: handle}; let mut stdout = Self { output: handle };
stdout.reset()?; stdout.reset()?;
Ok(stdout) Ok(stdout)
} }
} }
pub fn close(&mut self) -> Result<()> { pub fn stderr() -> Result<Self> {
unsafe { unsafe {
CloseHandle(self.output); let handle = Console::GetStdHandle(Console::STD_ERROR_HANDLE)
.context("unable to get stdout handle")?;
let mut stdout = Self { output: handle };
stdout.reset()?;
Ok(stdout)
} }
Ok(())
} }
pub fn reset(&mut self) -> Result<()> { pub fn reset(&mut self) -> Result<()> {
@ -142,23 +55,25 @@ impl Writer {
Ok(()) Ok(())
} }
/// clears the output buffer
pub fn clear(&mut self) -> Result<()> { pub fn clear(&mut self) -> Result<()> {
self.write(b"\x1b[2J\x1b[0;0H")?; self.write(b"\x1b[2J\x1b[0;0H")?;
Ok(()) Ok(())
} }
// pub fn hide_cursor(&mut self) -> Result<()> {
// self.write(b"\x1b[?25l")?;
// Ok(())
// }
} }
impl Write for Writer { impl Write for Writer {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> { fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let mut written = 0;
unsafe { unsafe {
Error::check(Console::WriteConsoleA(self.output, buf, None, None))?; Error::check(Console::WriteConsoleA(
} self.output,
Ok(0) buf,
Some(&mut written),
None,
))?;
}
Ok(written as usize)
} }
fn flush(&mut self) -> io::Result<()> { fn flush(&mut self) -> io::Result<()> {

@ -1,463 +1,352 @@
use log::debug; use crate::error::ParseError;
use crate::lex::{Lexer, Token};
use std::{ use std::{
fmt,
ops::Deref,
cell::RefCell, cell::RefCell,
rc::Rc, rc::{Rc, Weak},
sync::atomic::AtomicUsize,
}; };
use thiserror::Error;
#[derive(Debug, Error)] /// The contents of a node in our parse tree. The parse tree consists of both terminal and
pub enum Error { /// nonterminal symbols.
#[error("Unexpected Token")]
UnexpectedToken,
}
#[allow(dead_code)]
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub enum Token { pub enum Value {
Ident(String), /// The start symbol of our parse tree. Each parse tree is rooted in a node whose value is the
Pipe, /// start symbol. This is the only node in the tree that should utilize the start symbol.
} Start,
#[allow(dead_code)] /// The children of a statement symbol make up the components of what will become a statement
fn lex<S: AsRef<str>>(text: S) -> Result<Vec<Token>, Error> { /// in our ast
Lexer::new(text.as_ref()).lex() Command,
}
struct Lexer<'text> { /// Each of the tokens from the lex stage becomes a terminal node on our tree
chars: std::str::Chars<'text>, Terminal(Token),
peeked: Option<char>,
} }
impl<'text> Lexer<'text> { impl Value {
fn new(text: &'text str) -> Self { fn is_terminal(&self) -> bool {
Self { matches!(self, Value::Terminal(_))
chars: text.chars(),
peeked: None,
}
} }
}
fn lex(&mut self) -> Result<Vec<Token>, Error> { /// A node in a parse tree.
let mut tokens = vec![]; #[derive(Debug)]
while self.peek().is_some() { pub struct Node {
self.skip_whitespace(); pub id: usize,
match self.peek() {
None => return Ok(tokens),
Some('|') => {
tokens.push(Token::Pipe);
self.skip();
}
Some(_) => {
tokens.push(self.ident());
}
}
}
Ok(tokens)
}
fn skip_whitespace(&mut self) { /// A node may or may not have a parent node. If a node does not have a parent node, that node
loop { /// is the root node of a tree.
match self.peek() { parent: Option<Weak<Node>>,
None => break,
Some(c) => {
if c.is_whitespace() {
self.skip()
} else {
break;
}
}
}
}
}
fn peek(&mut self) -> Option<char> { /// The value of the element at this node
match self.peeked { pub value: Value,
Some(c) => Some(c),
None => match self.chars.next() {
Some(c) => {
self.peeked = Some(c);
Some(c)
}
None => None,
},
}
}
fn skip(&mut self) { /// A node may or may not have children. Since an empty vector is a valid vector, a node
if self.peeked.is_some() { /// without children is represented as having an empty children vector. A node having an empty
self.peeked = None; /// list of children is a leaf node in a tree.
} else { pub children: RefCell<Vec<Rc<Node>>>,
self.chars.next(); }
}
}
fn ident(&mut self) -> Token { impl Node {
let mut kept: Vec<char> = vec![]; fn new() -> Cursor {
loop { let root = Rc::new(Node {
match self.peek() { id: next_id(),
None => break, parent: None,
Some(c) => { value: Value::Start,
if c.is_whitespace() { children: RefCell::new(Vec::new()),
break; });
} Cursor {
kept.push(c); target: Rc::clone(&root),
self.skip(); prev: root.id,
} root,
}
} }
Token::Ident(kept.iter().collect())
} }
}
// struct Parser { pub fn is_semi(&self) -> bool {
// state: Tree, matches!(self.value, Value::Terminal(Token::Semi(_)))
// } }
//
// impl Parser {
// fn new() -> Self {
// Self {
// state: Tree::new(),
// }
// }
//
// fn parse(mut self, tokens: Vec<Token>) -> Result<Node, Error> {
// Ok(Node::empty())
// // for token in tokens {
// // self.take(token);
// // }
// // let t = self.state.replace(Node::empty());
// // Ok(t.root())
// }
// }
#[derive(PartialEq, Clone)]
pub enum Element {
Empty,
Command(String),
Literal(String),
} }
impl fmt::Debug for Element { impl PartialEq for Node {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { fn eq(&self, other: &Self) -> bool {
match self { self.id == other.id
Element::Empty => write!(f, "()"),
Element::Command(cmd) => write!(f, "cmd<{}>", cmd),
Element::Literal(lit) => write!(f, "lit<{}>", lit),
}
} }
} }
pub struct Node { /// Iterates through the children of a cursor node, producing new cursor positions.
pub elem: Element, pub struct ChildIter {
pub parent: Option<Rc<RefCell<Node>>>, target: Rc<Node>,
pub children: Vec<Rc<RefCell<Node>>>, root: Rc<Node>,
idx: usize,
} }
impl Node { impl Iterator for ChildIter {
pub fn new(elem: Element) -> Self { type Item = Cursor;
Self {
elem,
parent: None,
children: Vec::new(),
}
}
pub fn empty() -> Self {
Self::new(Element::Empty)
}
pub fn child_of(parent: Rc<RefCell<Self>>, elem: Element) -> Self {
Self {
elem,
parent: Some(parent),
children: Vec::new(),
}
}
pub fn visit(self) -> Tree { fn next(&mut self) -> Option<Self::Item> {
self.into() let children = self.target.children.borrow();
let v = children.get(self.idx)?;
self.idx += 1;
Some(Cursor {
target: Rc::clone(v),
root: Rc::clone(&self.root),
prev: self.target.id,
})
} }
} }
impl fmt::Debug for Node { const LAST_ID: AtomicUsize = AtomicUsize::new(0);
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { fn next_id() -> usize {
write!(f, "{:?}", self.elem)?; LAST_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
if self.children.len() > 0 {
write!(f, "[")?;
for child in self.children.iter() {
write!(f, "{:?}", child.borrow())?;
}
write!(f, "]")?;
}
Ok(())
}
} }
pub struct Tree { /// Cursor values expose access to a parse tree. A cursor is logically a pointer to a single node
target: Rc<RefCell<Node>>, /// within a parse tree. The cursor helps with the ownership structure: so long as there is a
/// cursor to any node on the tree, the tree remains in memory. Once there are no cursors pointed
/// at the tree, it is dropped.
#[derive(Debug, Clone)]
pub struct Cursor {
pub target: Rc<Node>,
root: Rc<Node>,
prev: usize,
} }
impl Tree { impl Cursor {
fn new() -> Self { /// Climbs one level up a parse tree. The cursor is re-pointed from its current target node to
Node::empty().into() /// the parent of its current target node. This method fails if the cursor is already at the
/// root node of the parse tree.
pub fn up(&mut self) -> Result<(), ParseError> {
match &self.target.parent {
None => Err(ParseError::AtRootAlready),
Some(parent) => match parent.upgrade() {
Some(parent) => {
self.goto(parent);
Ok(())
} }
None => Err(ParseError::ParentIsGone),
/// Adds an element as a node as a child of the current node, then descends the tree to select },
/// that child node. This is similar to how pushing a value changes the top element of a stack.
fn push(self, elem: Element) -> Self {
if self.is_empty() {
self.target.replace(Node::new(elem));
self
} else {
let child = Node::child_of(Rc::clone(&self.target), elem);
Tree{target: Rc::new(RefCell::new(child))}
} }
} }
/// Adds a child node with a given element value to the currently selected node without chaning /// Adds a value to the children of the current target node, then descends to select that
/// our selection. /// child.
fn append(self, elem: Element) -> Self { fn push(&mut self, v: Value) -> Result<(), ParseError> {
if self.is_empty() { if self.target.value.is_terminal() {
self.target.replace(Node::new(elem)); return Err(ParseError::PushOntoTerminal);
} else {
let node = Node::child_of(Rc::clone(&self.target), elem);
let child = Rc::new(RefCell::new(node));
self.target.borrow_mut().children.push(child);
} }
self let node = Node {
id: next_id(),
parent: Some(Rc::downgrade(&self.target)),
value: v,
children: RefCell::new(Vec::new()),
};
let node = Rc::new(node);
self.target
.children
.try_borrow_mut()?
.push(Rc::clone(&node));
self.goto(node);
Ok(())
} }
/// Tells us whether or not the currently selected node is an empty node /// moves the cursor up to the root of the tree in place
fn is_empty(&self) -> bool { pub fn up_to_root(&mut self) {
self.target.borrow().elem == Element::Empty self.goto(Rc::clone(&self.root));
} }
fn parent(&self) -> Option<Self> { pub fn value(&self) -> &Value {
self.target.borrow().parent.as_ref().map(|parent| Self { &self.target.value
target: Rc::clone(parent),
})
} }
fn is_root(&self) -> bool { pub fn iter_children(&self) -> ChildIter {
self.parent().is_none() ChildIter {
target: Rc::clone(&self.target),
root: Rc::clone(&self.root),
idx: 0,
} }
pub fn root(self) -> Self {
match self.parent() {
Some(parent) => parent.root(),
None => self,
} }
#[cfg(test)]
fn is_root(&self) -> bool {
self.target.parent.is_none()
} }
fn peek(&self) -> Element { #[cfg(test)]
self.target.borrow().elem.clone() fn into_root(self) -> Rc<Node> {
Rc::clone(&self.root)
} }
fn into_node(self) -> Node { fn text_tree(&self) -> String {
self.into() let mut p = self.clone();
p.up_to_root();
let mut buf = String::new();
self.text_tree_helper(0, &mut buf);
buf
} }
fn children(&self) -> ChildIter { fn text_tree_helper(&self, depth: u32, mut buf: &mut String) {
ChildIter { let line = format!("{:?}\n", self.value());
parent: Rc::clone(&self.target), *buf = buf.to_owned() + &line;
idx: 0, for child in self.iter_children() {
child.text_tree_helper(depth + 1, &mut buf);
} }
} }
}
impl fmt::Debug for Tree { fn goto(&mut self, next: Rc<Node>) {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { self.prev = self.target.id;
write!(f, "{:?}", self.target.borrow()) self.target = next;
} }
} }
pub struct ChildIter { /// The Parser is responsible for turning a sequence of Tokens into a Parse Tree.
/// pointer to the node in the tree whose children we are looking at. pub struct Parser<'text> {
parent: Rc<RefCell<Node>>, input: Lexer<'text>,
idx: usize, output: Cursor,
} }
impl Iterator for ChildIter { impl<'text> Parser<'text> {
type Item = Tree; pub fn new(source: Lexer<'text>) -> Self {
Self {
fn next(&mut self) -> Option<Self::Item> { input: source,
if self.idx >= self.parent.borrow().children.len() { output: Node::new(),
None
} else {
let child = Rc::clone(&self.parent.borrow().children[self.idx]);
self.idx += 1;
Some(Tree{target: child})
} }
} }
}
impl From<Tree> for Node { pub fn parse(mut self) -> Result<Cursor, ParseError> {
fn from(value: Tree) -> Self { while self.step()? {}
value.target.replace(Node::empty()) self.output.up_to_root();
Ok(self.output)
} }
}
impl From<Node> for Tree { fn step(&mut self) -> Result<bool, ParseError> {
fn from(value: Node) -> Self { match self.output.value() {
Self { Value::Start => self.step_start(),
target: Rc::new(RefCell::new(value)), Value::Command => self.step_statement(),
Value::Terminal(_) => panic!(),
} }
} }
}
// I don't know how to write a parser lol
// ls
// Command{name: "ls"}
// echo one two three
// Command{name: "echo", args: [Lit("one"), Lit("two"), Lit("three")]}
// echo *.rs fn step_start(&mut self) -> Result<bool, ParseError> {
// Command{name: "echo", args: [Glob("*.rs")]} assert!(matches!(self.output.value(), Value::Start));
// Command{name: "echo", args: [Path("main.rs"), Path("parse.rs"), ...]} match self.input.peek()? {
Some(Token::Word(_)) => {
// cat main.rs | wc -l self.output.push(Value::Command)?;
// Pipe{ let token = self.input.next().unwrap()?;
// src: Command{name: "cat", args: [Lit("main.rs")]} self.output.push(Value::Terminal(token))?;
// dest: Command{name: "wc", args: [Lit("-l")]} self.output.up()?;
// } Ok(true)
// echo one ; echo two
// Sequence{
// children: [
// Command{Name:
// ]
// }
pub fn parse<S: AsRef<str>>(text: S) -> Result<Tree, Error> {
let mut tree = Tree::new();
for token in lex(text)? {
match (tree.peek(), token) {
(Element::Empty, Token::Ident(cmd)) => {
tree = tree.push(Element::Command(cmd));
} }
(Element::Command(_), Token::Ident(arg)) => { Some(Token::Glob(_)) => {
tree = tree.append(Element::Literal(arg)); let token = self.input.next().unwrap()?;
Err(ParseError::UnexpectedToken(token))
} }
_ => todo!(), Some(Token::Semi(_)) => {
self.output.push(Value::Command)?;
let token = self.input.next().unwrap()?;
self.output.push(Value::Terminal(token))?;
self.output.up()?;
self.output.up()?;
Ok(true)
} }
Some(Token::Pipe(_)) => {
let token = self.input.next().unwrap()?;
Err(ParseError::UnexpectedToken(token))
}
None => Ok(false),
} }
Ok(tree.root())
}
#[cfg(test)]
mod tests {
use super::*;
use Token::Pipe;
fn ident<S: Into<String>>(s: S) -> Token {
Token::Ident(s.into())
} }
macro_rules! lex { fn step_statement(&mut self) -> Result<bool, ParseError> {
( assert!(matches!(self.output.value(), Value::Command));
$($name:ident: $line:literal $($token:expr)* ;)+ match self.input.peek()? {
) => { Some(Token::Word(_) | Token::Glob(_)) => {
$( let token = self.input.next().unwrap()?;
#[test] self.output.push(Value::Terminal(token))?;
fn $name() -> Result<(), Error> { self.output.up()?;
let mut tokens = lex($line)?; Ok(true)
$(
assert!(tokens.len() > 0);
let front = tokens.remove(0);
assert_eq!($token, front);
)*
Ok(())
} }
)* Some(Token::Pipe(_)) => {
}; todo!()
} }
Some(Token::Semi(_)) => {
let token = self.input.next().unwrap()?;
self.output.push(Value::Terminal(token))?;
self.output.up()?;
self.output.up()?;
Ok(true)
}
None => Ok(false),
}
}
}
lex! { /*
single_token:
"one"
ident("one");
two_tokens:
"ls one two"
ident("ls") ident("one") ident("two");
leading_whitespace:
" one"
ident("one");
trailing_whitespace:
"one "
ident("one");
surrounding_whitespace: a b
" one " command
ident("one"); terminal
a
terminal
b
internal_hyphen: a | b
"one-two" pipeline
ident("one-two"); command
terminal
a
terminal
|
command
terminal
b
pipe:
"|"
Pipe;
pipeline: */
"one | two"
ident("one") Pipe ident("two");
pipeline_2: #[cfg(test)]
"one |two" mod test {
ident("one") Pipe ident("two"); use super::*;
} use crate::lex::lex;
#[test] fn parse(source: &str) -> Result<Cursor, ParseError> {
fn empty_tree() { let tokens = Lexer::new(source);
let root = Tree::new().root(); let parser = Parser::new(tokens);
assert!(root.is_root()); parser.parse()
assert_eq!(root.peek(), Element::Empty);
assert_eq!(root.children().count(), 0);
} }
#[test] #[test]
fn tree_root() { fn root() {
let root = Tree::new() let mut cursor = Node::new();
.push(Element::Command(String::from("ls"))) assert!(cursor.up().is_err());
.root(); assert!(cursor.target.value == Value::Start);
assert!(root.is_root()); assert!(cursor.is_root());
assert_eq!(root.children().count(), 0);
assert_eq!(root.into_node().elem, Element::Command(String::from("ls")));
} }
#[test] #[test]
fn tree_push() { fn single_val() {
let tree = Tree::new() let mut cursor = Node::new();
.push(Element::Command(String::from("ls")));
assert_eq!(tree.peek(), Element::Command(String::from("ls"))); let mut tokens = lex(" ls ").unwrap();
let ls = tokens.pop().unwrap();
let tree = Tree::new() assert!(cursor.push(Value::Command).is_ok());
.push(Element::Command(String::from("ls"))) assert!(cursor.push(Value::Terminal(ls.clone())).is_ok());
.append(Element::Command(String::from("one"))); assert!(cursor.push(Value::Terminal(ls.clone())).is_err());
assert_eq!(tree.peek(), Element::Command(String::from("ls"))); assert!(cursor.target.value == Value::Terminal(ls));
assert!(!cursor.is_root());
assert!(cursor.up().is_ok());
assert!(cursor.up().is_ok());
assert!(cursor.is_root());
assert!(cursor.up().is_err());
assert!(cursor.value() == &Value::Start);
let root = cursor.into_root();
assert!(root.value == Value::Start);
} }
#[test] #[test]
fn parse_args() { fn test_parse() -> Result<(), ParseError> {
let res = parse("ls one two three"); let x = parse("ls one two three")?;
assert!(res.is_ok()); println!("Parse tree: {x:?}");
// parse("*")?;
let tree = res.unwrap(); // parse("x* ls")?;
assert_eq!(tree.peek(), Element::Command(String::from("ls"))); Ok(())
assert_eq!(tree.children().count(), 3);
let mut args = tree.children();
assert_eq!(args.next().unwrap().peek(), Element::Literal(String::from("one")));
assert_eq!(args.next().unwrap().peek(), Element::Literal(String::from("two")));
assert_eq!(args.next().unwrap().peek(), Element::Literal(String::from("three")));
assert!(args.next().is_none());
} }
} }

@ -1,7 +1,8 @@
use crate::output; use crate::output;
use std::io::Write;
use anyhow::Result; use anyhow::Result;
use std::io::Write;
/// draws the prompt that appears before a user enters a command
pub struct Prompt { pub struct Prompt {
s: String, s: String,
} }
@ -14,6 +15,11 @@ impl Prompt {
// | | +-------- clear styles // | | +-------- clear styles
// | | | +- show cursor // | | | +- show cursor
// v v v v // v v v v
#[cfg(debug_assertions)]
s: String::from("\x1b[?25l\x1b[31m ▷ \x1b[0m\x1b[?25h"),
#[allow(dead_code)]
#[cfg(not(debug_assertions))]
s: String::from("\x1b[?25l\x1b[32m ▷ \x1b[0m\x1b[?25h"), s: String::from("\x1b[?25l\x1b[32m ▷ \x1b[0m\x1b[?25h"),
} }
} }
@ -21,12 +27,13 @@ impl Prompt {
pub fn print(&self, output: &mut output::Writer) -> Result<()> { pub fn print(&self, output: &mut output::Writer) -> Result<()> {
match std::env::current_dir() { match std::env::current_dir() {
Ok(d) => { Ok(d) => {
let text = d.to_str().unwrap().to_owned() + &self.s; _ = write!(output, "{cwd}{arrow}", cwd = d.display(), arrow = self.s);
output.write(text.as_bytes())?; // let text = d.to_str().unwrap().to_owned() + &self.s;
// output.write(text.as_bytes())?;
} }
Err(_) => { Err(_) => {
output.write(self.s.as_bytes())?; _ = write!(output, "{}", self.s);
}, }
} }
Ok(()) Ok(())
} }

@ -0,0 +1,78 @@
use crate::{builtin, error::ExecError, output};
use std::{collections::HashMap, process};
/// Eval represents anything that can be evaluated at runtime.
pub trait Eval {
/// Evaluates the receiver, given the current runtime state.
fn eval(&self, ctx: &mut Context) -> Result<Value, ExecError>;
}
/// An individual value at runtime.
#[derive(Debug, Clone)]
pub enum Value {
/// The empty value. This is somewhat analagous to () in Rust or void in C or null in languages
/// that have null.
None,
/// A textual value.
Text(String),
/// The result of having executed some external OS process.
ExitStatus(process::ExitStatus),
}
impl Value {
pub fn try_to_string(self) -> Result<String, ExecError> {
Ok(self.try_as_str()?.to_string())
}
pub fn try_as_str(&self) -> Result<&str, ExecError> {
match self {
Value::None => Err(ExecError::type_error("expected text value, saw None value")),
Value::Text(v) => Ok(&v),
Value::ExitStatus(_) => Err(ExecError::type_error(
"expected text value, saw ExitStatus value",
)),
}
}
}
impl Eval for Value {
fn eval(&self, _: &mut Context) -> Result<Value, ExecError> {
Ok(self.clone())
}
}
/// The state of the runtime. In an environment-passing interpreter, this would be the environment.
/// We avoid the term "environment" because in the context of an OS shell it's confusing whether it
/// means the state of the interpreter itself, or the environment variables of the process. This
/// State type represents the state of the interpreter itself.
pub struct State {
#[allow(unused)]
variables: HashMap<&'static str, Value>,
pub(crate) builtins: HashMap<&'static str, builtin::Builtin>,
}
impl State {
pub fn new() -> Self {
Self {
variables: HashMap::new(),
builtins: builtin::all(),
}
}
pub fn builtin(&self, name: &str) -> Option<builtin::Builtin> {
self.builtins.get(name).copied()
}
}
pub trait Call {
fn call(&self, ctx: &mut Context, args: &[Value]) -> Result<Value, ExecError>;
}
pub struct Context<'s> {
pub(crate) stdout: output::Writer,
pub(crate) stderr: output::Writer,
pub(crate) state: &'s mut State,
}

@ -1,162 +0,0 @@
use crate::{
ext::{Command, Echo, Printenv, Tail, Which},
input,
line::Line,
log::*,
output,
// parse::parse,
parse::{Element, Node},
};
use std::path::{Path, PathBuf};
use std::rc::Rc;
use anyhow::Result;
use dirs;
pub struct Shell {
pub input: input::Reader,
pub output: output::Writer,
pub line: Line,
}
impl Shell {
pub fn new() -> Result<Self> {
Ok(Self {
input: input::Reader::new()?,
output: output::Writer::stdout()?,
line: Line::new(),
})
}
pub fn back(&mut self, n: usize) -> Result<()> {
debug!("⛬ ←");
if self.line.back(n) {
self.output.back(n)?;
}
Ok(())
}
pub fn forward(&mut self, n: usize) -> Result<()> {
debug!("⛬ →");
if self.line.forward(n) {
self.output.forward(n)?;
}
Ok(())
}
pub fn reset(&mut self) -> Result<()> {
self.input.reset()?;
self.output.reset()?;
Ok(())
}
pub fn seek_right(&mut self) -> Result<()> {
info!("» seek right");
let n = self.line.seek_right();
if n > 0 {
// move right by the distance seeked
self.output.forward(n)?;
}
Ok(())
}
pub fn seek_left(&mut self) -> Result<()> {
info!("» seek left");
let n = self.line.seek_left();
if n > 0 {
// move left by the distance seeked
self.output.back(n)?;
}
Ok(())
}
pub fn expand_path<P: AsRef<Path>>(&self, p: P) -> PathBuf {
let p = p.as_ref();
match p.to_str() {
Some(s) => {
if s.len() == 0 || !s.starts_with('~') {
return p.to_path_buf();
}
}
None => {
return p.to_path_buf();
}
}
let mut parts = p.components();
let first = parts.next().unwrap().as_os_str().to_str().unwrap();
if first == "~" {
dirs::home_dir().unwrap().join(parts)
} else {
let my_home = dirs::home_dir().unwrap();
let home_root = my_home.parent().unwrap();
let first = &first[1..];
let other_home = home_root.join(first);
other_home.join(parts)
}
}
pub fn exec(&mut self, root: Node) -> Result<bool> {
match root.elem {
Element::Empty => Ok(true),
Element::Command(cmd) => {
let args: Vec<String> = root
.children
.iter()
.map(|node| match &node.borrow().elem {
Element::Literal(v) => v.clone(),
_ => todo!(),
})
.collect();
self.eval(
cmd.to_string(),
args.iter().map(|arg| arg.as_str()).collect(),
)
}
Element::Literal(_) => todo!(),
}
}
pub fn eval(&mut self, cmd: String, args: Vec<&str>) -> Result<bool> {
match cmd.as_str() {
"pwd" => {
let pb = std::env::current_dir()?;
println!("{}", pb.as_path().as_os_str().to_str().unwrap());
return Ok(true);
}
"cd" => {
let cwd = std::env::current_dir()?;
if args.len() > 0 {
let target = cwd.join(args[0]);
std::env::set_current_dir(target)?;
}
return Ok(true);
}
"printenv" => Printenv::create().exec(args),
"which" => Which::create().exec(args),
"tail" => Tail::create().exec(args),
"echo" => Echo::create().exec(args),
_ => {
let mut proc = std::process::Command::new(cmd);
if args.len() > 0 {
proc.args(args);
}
match proc.spawn() {
Ok(mut child) => {
if let Err(e) = child.wait() {
println!("error: {}", e);
return Err(e.into());
}
}
Err(e) => {
println!("error: {}", e);
return Ok(false);
}
}
return Ok(true);
}
}
}
}

@ -0,0 +1,252 @@
use crate::{
builtin::Builtin,
error::{ExecError, ParseError},
lex::{Lexer, Token},
parse,
run::{Call, Context, Eval, Value},
};
use std::{collections::HashSet, env, ffi::OsString, process};
/// The differnt types of nodes that may appear in our AST
#[derive(Debug)]
pub enum Element {
/// A Block is a list of statements
Block(Block),
/// A Command represents the desire to execute a command
Command(Command),
/// A literal is a ... literal value
Literal(Value),
}
impl Eval for Element {
fn eval(&self, ctx: &mut Context) -> Result<Value, ExecError> {
use Element::*;
match self {
Block(block) => block.eval(ctx),
Command(cmd) => cmd.eval(ctx),
Literal(v) => v.eval(ctx),
}
}
}
#[derive(Debug)]
pub struct Block {
commands: Vec<Command>,
}
impl Block {
pub fn new() -> Self {
Block {
commands: Vec::new(),
}
}
}
impl Eval for Block {
fn eval(&self, ctx: &mut Context) -> Result<Value, ExecError> {
let mut v = Value::None;
for cmd in self.commands.iter() {
v = cmd.eval(ctx)?;
}
Ok(v)
}
}
#[derive(Debug)]
pub struct Command {
name: Box<Element>,
args: Vec<Element>,
}
impl Command {
fn exec_builtin(&self, ctx: &mut Context, builtin: Builtin) -> Result<Value, ExecError> {
let args = self
.args
.iter()
.map(|elem| elem.eval(ctx))
.collect::<Result<Vec<_>, _>>()?;
builtin.call(ctx, &args)
}
fn exec_subprocess(&self, ctx: &mut Context, name: &str) -> Result<Value, ExecError> {
let args = self
.args
.iter()
.map(|elem| elem.eval(ctx)?.try_to_string())
.collect::<Result<Vec<_>, _>>()?;
let mut proc = process::Command::new(&name);
if args.len() > 0 {
proc.args(args);
}
let mut child = proc.spawn().map_err(|e| ExecError::ProcessSpawnError {
name: name.to_string(),
source: e,
})?;
let pid = child.id();
let status = child.wait().map_err(|e| ExecError::ProcessWaitError {
name: name.to_string(),
pid,
source: e,
})?;
Ok(Value::ExitStatus(status))
}
}
impl Eval for Command {
fn eval(&self, ctx: &mut Context) -> Result<Value, ExecError> {
let name = self.name.eval(ctx)?.try_to_string()?;
match ctx.state.builtin(&name) {
Some(builtin) => self.exec_builtin(ctx, builtin),
None => self.exec_subprocess(ctx, &name),
}
}
}
/// the TreeBuilder is responsible for converting a parse tree into an abstract syntax tree. This
/// process leaves us with a tree that can be executed directly
struct TreeBuilder {
visited: HashSet<usize>,
}
impl TreeBuilder {
fn new() -> Self {
Self {
visited: HashSet::new(),
}
}
fn parse(&mut self, source: &mut parse::Cursor) -> Result<Element, ParseError> {
let e = match source.value() {
parse::Value::Start => {
let mut root = Block::new();
let children = source.iter_children();
for mut child in children {
if child.target.is_semi() {
continue;
}
let e = self.parse(&mut child)?;
match e {
Element::Command(cmd) => root.commands.push(cmd),
_ => panic!(),
}
}
Element::Block(root)
}
parse::Value::Command => {
let mut children = source.iter_children();
let mut first = match children.next() {
Some(child) => child,
None => return Err(ParseError::StatementIsEmpty),
};
if first.target.is_semi() {
return Err(ParseError::StatementIsEmpty);
}
let name = self.parse(&mut first)?;
let mut cmd = Command {
name: Box::new(name),
args: Vec::new(),
};
// we set complete to true when we find a semicolon. If there are any parse nodes
// that appear -after- a semicolon, that indicates a bug in the prior stage.
let mut complete = false;
for mut child in children {
if child.target.is_semi() {
complete = true;
continue;
}
if complete {
return Err(ParseError::DanglingElements);
} else {
let e = self.parse(&mut child)?;
cmd.args.push(e);
}
}
Element::Command(cmd)
}
parse::Value::Terminal(Token::Word(word)) => {
let text = word.to_string();
let text = expand(&text)?;
Element::Literal(Value::Text(text))
}
parse::Value::Terminal(Token::Glob(_)) => {
todo!()
}
parse::Value::Terminal(Token::Pipe(_)) => {
todo!()
}
parse::Value::Terminal(Token::Semi(_)) => {
return Err(ParseError::WhyParseSemicolon);
}
};
self.visited.insert(source.target.id);
Ok(e)
}
}
pub fn parse(source: &str) -> Result<Element, ParseError> {
let tokens = Lexer::new(source);
let parser = parse::Parser::new(tokens);
let mut parse_tree = parser.parse()?;
let mut builder = TreeBuilder::new();
let root = builder.parse(&mut parse_tree)?;
Ok(root)
}
#[cfg(target_os = "windows")]
fn home_dir() -> Result<OsString, ExpandError> {
env::var_os("HOMEPATH").ok_or(ExpandError::EmptyEnvironmentVar(String::from("HOMEPATH")))
}
#[derive(thiserror::Error, Debug)]
pub enum ExpandError {
#[error("environment var is not set: {0}")]
EmptyEnvironmentVar(String),
#[error("home dir is not utf-8")]
NotUnicode,
}
fn expand(text: &str) -> Result<String, ExpandError> {
let chars = text.chars();
// let mut escape = false;
let mut s = String::new();
for c in chars {
// if !escape && c == '\\' {
// escape = true;
// continue;
// }
// if escape {
// s.push(c);
// escape = false;
// continue;
// }
if c == '~' {
let p = home_dir()?;
s.push_str(p.to_str().ok_or(ExpandError::NotUnicode)?);
continue;
}
s.push(c);
}
Ok(s)
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn hi() -> Result<(), ParseError> {
let e = parse("ls one two three")?;
print!("{:?}", e);
Ok(())
}
}

@ -0,0 +1,207 @@
use crate::error::LexError;
use std::{collections::VecDeque, fmt, ops::Range, str::Chars};
/// A position a some two dimensional space defined by lines and columns, specifically concerned
/// with positions of characters within text
#[derive(PartialEq, Clone, Copy)]
pub struct Position {
/// The visual line in which this glyph appears in the source text
pub line: u64,
/// The visual column in which this glyph appears in the source text
pub column: u64,
}
impl Position {
/// The 0,0 position: the very first character of a document is at the 0,0 position
pub fn origin() -> Self {
Self { line: 0, column: 0 }
}
/// Increments position by column, going from the current line,column position to the next
/// column on the same line.
pub fn incr(&mut self) -> Position {
let p = *self;
self.column += 1;
p
}
/// Increments the position by line, going from the current line,column position to the
/// beginning of the next line.
pub fn incr_line(&mut self) -> Position {
let p = *self;
self.column = 0;
self.line += 1;
p
}
}
impl fmt::Debug for Position {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(
f,
"({line}, {column})",
line = self.line,
column = self.column
)
}
}
impl fmt::Display for Position {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
(self as &dyn fmt::Debug).fmt(f)
}
}
/// A Glyph represents a character occurring at some specific position in some input text.
#[derive(PartialEq, Clone)]
pub struct Glyph {
/// the unicode code point of the glyph
pub glyph: char,
/// The visual position in which the glyph appears; i.e., the human-comprehensible location
/// of the glyph in the source text
pub position: Position,
/// The byte offsets corresponding to this topoglyph in the source data; i.e., the
/// machine-comprehensible location of the glyph in the source text
pub bytes: Range<u64>,
}
impl Glyph {
/// checks to see whether or not the character for this glyph is a word character
pub fn is_word(&self) -> bool {
match self.glyph {
'-' | '/' | '\\' | '.' | ':' | '~' => true,
_ => self.glyph.is_alphanumeric(),
}
}
/// checks to see whether or not the character for this glyph is a glob character
pub fn is_glob(&self) -> bool {
self.is_word() || self.glyph == '*'
}
}
impl fmt::Debug for Glyph {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(
f,
"[{char} ({pos:?})]",
char = self.glyph,
pos = self.position
)
}
}
/// Glyphs produces [glyphs](Glyph) for a source text; i.e., it is an iterator of [Glyph] values.
/// Glyphs is used to control reading from the source text and keeps a lookahead buffer of glyphs
/// that have not been processed. While a [crate::lex::Lexer] is responsible for the creation and
/// iteration of [tokens](crate::lex::Token), Glyphs is responsible for the creation and iteration
/// of glyphs.
pub struct Glyphs<'text> {
/// the characters of the input text that we're interested in reading
source: Chars<'text>,
/// the position of the next character in the source text
pub(crate) next_position: Position,
/// the number of bytes we've read from our input document so far
bytes_read: u64,
/// lookahead buffer
lookahead: VecDeque<Glyph>,
}
impl<'text> Glyphs<'text> {
pub fn new(source: &'text str) -> Self {
// neat
Self {
source: source.chars(),
next_position: Position::origin(),
bytes_read: 0,
lookahead: VecDeque::new(),
}
}
/// reads the next n characters from the source text into our lookahead buffer
fn fill_lookahead(&mut self, n: usize) -> bool {
while self.lookahead.len() < n {
let c = match self.source.next() {
Some(c) => c,
None => break,
};
let len = c.len_utf8();
let start = self.bytes_read;
self.bytes_read += len as u64;
let position = if c == '\n' {
self.next_position.incr_line()
} else {
self.next_position.incr()
};
self.lookahead.push_back(Glyph {
glyph: c,
position,
bytes: Range {
start,
end: self.bytes_read,
},
})
}
self.lookahead.len() == n
}
/// returns a reference to the next character from the source text, advancing our internal
/// lookahead buffer if necessary. Returns None if we're already at the end of our source text.
pub fn peek(&mut self) -> Option<&Glyph> {
self.peek_at(0)
}
/// takes the next character from our input text
pub fn pop(&mut self) -> Result<Glyph, LexError> {
self.next()
.ok_or(LexError::UnexpectedEOF(self.next_position))
}
/// returns a reference to a character in our lookahead buffer at a given position. This allows
/// us to perform a lookahead read without consuming any tokens, maintaining our current
/// position and keeping our unconsumed characters safe.
fn peek_at(&mut self, idx: usize) -> Option<&Glyph> {
self.fill_lookahead(idx + 1);
self.lookahead.get(idx)
}
/// discards characters from our current position so long as the upcoming characters match some
/// predicate. This is called yeet_while instead of skip_while in order to avoid conflicting
/// with the
/// [skip_while](https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.skip_while)
/// method of the stdlib Iterator trait.
pub fn yeet_while<F>(&mut self, mut pred: F)
where
F: FnMut(&Glyph) -> bool,
{
while let Some(g) = self.peek() {
if pred(&g) {
self.next();
} else {
return;
}
}
}
/// discards all of the upcoming whitespace characters. After calling yeet_whitespace, the next
/// Glyp is guaranteed to be either non-whitespace or None (None would be because we've hit
/// EOF)
pub fn yeet_whitespace(&mut self) {
self.yeet_while(|tg| tg.glyph.is_whitespace());
}
}
impl<'text> Iterator for Glyphs<'text> {
type Item = Glyph;
fn next(&mut self) -> Option<Self::Item> {
self.fill_lookahead(1);
self.lookahead.pop_front()
}
}
Loading…
Cancel
Save