refactored some stuff

main
Jordan Orelli 7 months ago
parent 948326df4f
commit 71f148139e

@ -17,4 +17,5 @@ version = "0.44.0"
features = [
"Win32_Foundation",
"Win32_System_Console",
"Win32_UI_Input_KeyboardAndMouse"
]

@ -1,13 +1,18 @@
use crate::log::*;
use std::cmp::min;
use crate::{
input::{self, ControlCharacter, Escape},
log::*,
output,
prompt::Prompt,
};
/// An interactive text editor. The Editor is in control of all line editing functionality. It is a
/// type of text editor, albeit an extremely primitive one.
use std::{cmp::min, io::Write};
/// The text buffer of a text editor.
pub struct Buffer {
/// the current contents of the line
/// the current contents of the buffer. This is the least efficient way possible to do this.
chars: Vec<char>,
/// the cursor position of our head within the vector of characters that we store as chars
/// the position of our edit cursor within [Self::chars]
cursor: usize,
}
@ -76,7 +81,8 @@ impl Buffer {
}
}
/// moves the cursor to the start of the edit line, returning the old cursor value
/// moves the edit cursor to the beginning of the buffer. The returned value is the index of
/// the edit cursor before the move.
pub fn seek_left(&mut self) -> usize {
let n = self.cursor;
self.cursor = 0;
@ -84,6 +90,8 @@ impl Buffer {
n
}
/// moves the edit cursor to the end of the buffer. The returned value is the index of th eedit
/// cursor before the move.
pub fn seek_right(&mut self) -> usize {
let n = self.chars.len() - self.cursor;
self.cursor = self.chars.len();
@ -91,6 +99,7 @@ impl Buffer {
n
}
/// moves the edit cursor forward by n positions
pub fn forward(&mut self, n: usize) -> bool {
if self.cursor < self.chars.len() {
self.cursor = min(self.chars.len(), self.cursor + n);
@ -147,3 +156,244 @@ impl Buffer {
);
}
}
enum Status {
Ongoing,
Submit(String),
Done,
}
/// A primitive terminal-based text editor
pub struct Editor {
/// the incoming terminal events
pub(crate) 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,
prompt: Prompt,
}
impl Editor {
pub fn new() -> anyhow::Result<Self> {
Ok(Self {
input: input::Reader::new()?,
buffer: Buffer::new(),
display: output::Writer::stdout()?,
prompt: Prompt::new(),
})
}
pub fn read_command(&mut self) -> anyhow::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)?,
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) => return Ok(Some(e)),
}
}
}
fn handle_key_press(&mut self, event: crate::key::Event) -> anyhow::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) -> anyhow::Result<Status> {
use Escape::*;
match esc {
Left => self.back(1)?,
Right => self.forward(1)?,
Home => self.seek_left()?,
End => self.seek_right()?,
esc => debug!(" Ignored escape: {esc:?}"),
}
Ok(Status::Ongoing)
}
fn handle_control(&mut self, cc: ControlCharacter) -> anyhow::Result<Status> {
use ControlCharacter::*;
match cc {
StartOfHeading => self.seek_left()?,
EndOfTransmission => return Ok(Status::Done),
Enquiry => self.seek_right()?,
FormFeed => self.clear_screen()?,
NegativeAcknowledge => self.clear_left()?,
cc => debug!("ignored control character: {cc:?}"),
}
Ok(Status::Ongoing)
}
fn submit(&mut self) -> anyhow::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) -> anyhow::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) -> anyhow::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) -> anyhow::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) -> anyhow::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) -> anyhow::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) -> anyhow::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) -> anyhow::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) -> anyhow::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) -> anyhow::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) -> anyhow::Result<()> {
self.buffer.clear();
self.display.reset()?;
Ok(())
}
}

@ -10,29 +10,28 @@ pub enum Error {
#[error("i/o error: {0}")]
IOError(#[from] io::Error),
}
#[derive(Error, Debug)]
pub enum InputError {
#[error("unrecognized control character: {0}")]
UnrecognizedControlCharacter(u8),
#[error("input error: {0}")]
InputError(String),
#[error("input record cannot convert to control character because it is an up event")]
ControlCharactersOnlyGoDownNotUp,
#[error("bad escape sequence")]
BadEscapeSequence,
}
#[derive(Debug, Error)]
pub enum LexError {
#[error("a word character was expected but none was encountered")]
ExpectedWordCharacter,
// #[error("unexpected character {0.glyph} at {0.position}")]
#[error("unexpected character {g} at {pos:?}", g = .0.glyph, pos = .0.position)]
UnexpectedCharacter(Glyph),
#[error("unexpected eof")]
UnexpectedEOF,
#[error("invalid trailing carriage return character")]
IllegalTrailingCarriageReturn,
#[error("carriage return without newline is baffling")]
IllegalDanglingCarriageReturn,
#[error("not yet supported: {0}")]
NotYetSupported(String),
}
@ -103,10 +102,6 @@ impl 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 {

@ -1,132 +1,128 @@
use crate::{error::Error, key, log::*};
use crate::{
error::{Error, InputError},
key,
log::*,
};
use anyhow::{Context, Result};
use macros::escapes;
use windows::Win32::Foundation::HANDLE;
use windows::Win32::System::Console;
#[allow(dead_code)]
fn log_input_mode(mode: Console::CONSOLE_MODE) {
// 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
// ENABLE_LINE_INPUT mode is also enabled.
if (mode & Console::ENABLE_ECHO_INPUT).0 > 0 {
debug!("Echo Input: Enabled");
} else {
debug!("Echo Input: Disabled");
}
// 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.
// If this mode is disabled, the functions return when one or more characters are available.
if (mode & Console::ENABLE_LINE_INPUT).0 > 0 {
debug!("Line Input Mode: Enabled");
} else {
debug!("Line Input Mode: Disabled");
}
// 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 {
debug!("Mouse Input: Disabled");
}
// CTRL+C is processed by the system and is not placed in the input buffer. If the input buffer
// is being read by ReadFile or ReadConsole, other control keys are processed by the system and
// are not returned in the ReadFile or ReadConsole buffer. If the ENABLE_LINE_INPUT mode is
// also enabled, backspace, carriage return, and line feed characters are handled by the
// system.
if (mode & Console::ENABLE_PROCESSED_INPUT).0 > 0 {
debug!("Processed Input: Enabled");
} else {
debug!("Processed Input: Disabled");
use std::{
cell::{RefCell, RefMut},
collections::VecDeque,
fmt,
rc::Rc,
};
use windows::Win32::{Foundation::HANDLE, System::Console};
/// retrieves a Windows Console handle using the Win32 apis
fn stdin_handle() -> Result<HANDLE> {
unsafe {
let handle = Console::GetStdHandle(Console::STD_INPUT_HANDLE)
.context("unable to get stdin handle")?;
Ok(handle)
}
}
// 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");
/// checks to see whether the raw underling console input record is the beginning of an escape
/// sequence
fn is_escape_start(record: &Console::INPUT_RECORD) -> bool {
if record.EventType as u32 == Console::KEY_EVENT {
unsafe {
let event = record.Event.KeyEvent;
event.wVirtualKeyCode == 0 && event.uChar.UnicodeChar == 27 && event.bKeyDown.as_bool()
}
} else {
debug!("Quick Edit Mode: Disabled");
false
}
}
// User interactions that change the size of the console screen buffer are reported in the
// console's input buffer. Information about these events can be read from the input buffer by
// applications using the ReadConsoleInput function, but not by those using ReadFile or
// ReadConsole.
if (mode & Console::ENABLE_WINDOW_INPUT).0 > 0 {
debug!("Window Input: Enabled");
/// checks to see if a record is indicating the end of an escape sequence
fn is_escape_done(record: &Console::INPUT_RECORD) -> bool {
if record.EventType as u32 == Console::KEY_EVENT {
unsafe {
let event = record.Event.KeyEvent;
event.wVirtualKeyCode == 0 && event.uChar.UnicodeChar == 27 && !event.bKeyDown.as_bool()
}
} else {
debug!("Window Input: Disabled");
false
}
}
// 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");
/// attempts to convert a record into a character that would appear in an escape sequence
fn as_escape_character(record: &Console::INPUT_RECORD) -> Option<char> {
if record.EventType as u32 != Console::KEY_EVENT {
None
} else {
debug!("Virtual Terminal Input: Disabled");
unsafe {
let n = record.Event.KeyEvent.uChar.UnicodeChar as u32;
let c = char::from_u32(n);
c
}
}
}
fn stdin_handle() -> Result<HANDLE> {
unsafe {
let handle = Console::GetStdHandle(Console::STD_INPUT_HANDLE)
.context("unable to get stdin handle")?;
Ok(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 {
let event = record.Event.KeyEvent;
if event.wVirtualKeyCode == 17 {
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 {
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,
pub(crate) escapes: EscapeCursor,
}
impl Reader {
pub fn new() -> Result<Self> {
let escapes = build_prefix_tree();
let v = Self {
buf: [Console::INPUT_RECORD::default(); 32],
buf_len: 0,
buf_idx: 0,
scratch: [Console::INPUT_RECORD::default(); 32],
lookahead: VecDeque::new(),
input: stdin_handle()?,
ctrl: false,
escapes,
};
v.reset()?;
Ok(v)
}
pub fn reset(&self) -> Result<()> {
// https://learn.microsoft.com/en-us/windows/console/setconsolemode
let mut mode = Console::CONSOLE_MODE(0);
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()?;
Error::check(Console::GetConsoleMode(handle, &mut mode))?;
// retrieve the current console mode
Error::check(Console::GetConsoleMode(self.input, &mut mode))?;
// allow terminal input characters
mode |= Console::ENABLE_VIRTUAL_TERMINAL_INPUT;
@ -143,202 +139,157 @@ impl Reader {
// enable mouse input
mode |= Console::ENABLE_MOUSE_INPUT;
Error::check(Console::SetConsoleMode(handle, mode))?;
Error::check(Console::GetConsoleMode(handle, &mut mode))?;
// debug!("Stdin details:");
// log_input_mode(mode);
// enable reporting of window resize events
mode |= Console::ENABLE_WINDOW_INPUT;
Error::check(Console::SetConsoleMode(self.input, mode))?;
}
Ok(())
}
pub fn next(&mut self) -> Result<Event> {
let rec = self.next_rec()?;
if rec.EventType as u32 == Console::KEY_EVENT {
let record = self.next_record()?;
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 {
let event = rec.Event.KeyEvent;
if event.wVirtualKeyCode == 0 && event.uChar.UnicodeChar == 27 {
return Ok(self.next_escape_sequence()?);
}
if event.wVirtualKeyCode == 17 {
self.ctrl = event.bKeyDown.as_bool();
debug!("ctrl {}", event.bKeyDown.as_bool());
let key_event = record.Event.KeyEvent;
if key_event.bKeyDown.as_bool() {
match ControlCharacter::try_from(key_event) {
Ok(c) => return Ok(Event::Control(c)),
Err(e) => warn!("{:?}", e),
}
}
}
}
Ok(rec.into())
}
fn log_recs(&mut self) {
debug!(" +---------------------+");
for i in 0..self.buf_len {
let rec = self.buf[i as usize];
let e: Event = rec.into();
match e {
Event::Key(k) => debug!(" | {} |", k),
_ => debug!(" | {:<14?} |", &e),
}
if is_escape_start(&record) {
let escape = self.next_escape_sequence()?;
return Ok(Event::Escape(escape));
}
debug!(" +---------------------+");
let event: Event = record.into();
if matches!(event, Event::Key(key::Event { down: false, .. })) {
return self.next();
}
Ok(event)
}
fn next_rec(&mut self) -> Result<Console::INPUT_RECORD> {
if self.buf_idx as u32 >= self.buf_len {
unsafe {
Error::check(Console::ReadConsoleInputA(
self.input,
&mut self.buf,
&mut self.buf_len,
))?;
}
debug!("• {}", self.buf_len);
self.log_recs();
self.buf_idx = 0;
/// 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);
}
let rec = self.buf[self.buf_idx];
self.buf_idx += 1;
return Ok(rec);
}
let mut num_read: u32 = 0;
fn next_escape_sequence(&mut self) -> Result<Event> {
match self.next_escape_char()? {
'[' => match self.next_escape_char()? {
'A' => Ok(Event::Up),
'B' => Ok(Event::Down),
'C' => Ok(Event::Right),
'D' => Ok(Event::Left),
'H' => Ok(Event::Home),
'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()? {
'~' => Ok(Event::Drop(String::from("[19~ - F8"))),
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(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"))),
e => Err(
Error::input_error(format!("[23 unexpected escape char: {}", e)).into(),
),
},
'4' => match self.next_escape_char()? {
'~' => Ok(Event::Drop(String::from("[24~ - F12"))),
e => Err(
Error::input_error(format!("[24 unexpected escape char: {}", e)).into(),
),
},
e => {
Err(Error::input_error(format!("[2 unexpected escape char: {}", e)).into())
}
},
e => Err(Error::input_error(format!("[ unexpected escape char: {}", e)).into()),
},
'O' => match self.next_escape_char()? {
'P' => Ok(Event::Drop(String::from("OP - F1"))),
'Q' => Ok(Event::Drop(String::from("OQ - F2"))),
'R' => Ok(Event::Drop(String::from("OR - F3"))),
'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()),
// request records from windows
unsafe {
Error::check(Console::ReadConsoleInputA(
self.input,
&mut self.scratch,
&mut num_read,
))?;
}
let records = &self.scratch[0..num_read as usize];
debug!("{num_read} records:");
for record in records {
self.lookahead.push_back(*record);
if record.EventType as u32 == Console::KEY_EVENT {
unsafe {
let key: key::Event = record.Event.KeyEvent.clone().into();
debug!(" {key}");
}
} else {
debug!(" -");
}
}
Ok(self.lookahead.pop_front().unwrap())
}
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()
})
fn next_escape_sequence(&mut self) -> Result<Escape> {
self.escapes.reset();
loop {
let record = self.next_record()?;
if is_escape_start(&record) {
continue;
}
if is_escape_done(&record) {
if self.escapes.is_at_root() {
return Ok(Escape::Empty);
} else {
panic!();
}
}
let c = as_escape_character(&record).ok_or(InputError::BadEscapeSequence)?;
if let Some(escape) = self.escapes.step(c) {
return Ok(escape);
}
}
}
}
/// 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)]
pub enum Event {
/// The process has received focus
Focus(bool),
/// This is a windows Menu event. This might be skippable?
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),
/// an event from the user's mouse, such as a mouse movement or click
Mouse { x: i16, y: i16 },
/// a resize event to inform us that the visual window of our process has changed
Size,
Left,
Right,
Up,
Down,
Home,
End,
Drop(String),
/// A decoded ANSI Escape Sequence. At the Terminal level, an Escape sequence is defined by a
/// string of characters instead of just one character. Escape Sequences are used to
/// communicate a variety of edit-related functionality such as navigation.
Escape(Escape),
/// An ASCII Controll Character.
Control(ControlCharacter),
}
const ALT_KEYS: u32 = 0x0002 | 0x0001;
const CTRL_KEYS: u32 = 0x0008 | 0x0004;
const SHIFT_PRESSED: u32 = 0x0010;
impl fmt::Display for Event {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
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 {
fn from(rec: Console::INPUT_RECORD) -> Self {
@ -353,24 +304,7 @@ impl From<Console::INPUT_RECORD> for Event {
let event = rec.Event.MenuEvent;
Event::Menu(event.dwCommandId)
},
Console::KEY_EVENT => {
// 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::KEY_EVENT => unsafe { Event::Key(rec.Event.KeyEvent.clone().into()) },
Console::MOUSE_EVENT => {
// 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
@ -395,6 +329,161 @@ impl From<Console::INPUT_RECORD> for Event {
}
}
#[derive(Debug)]
pub struct EscapeCursor {
target: Rc<EscapeNode>,
root: Rc<EscapeNode>,
}
#[derive(Debug)]
pub enum EscapeNode {
Root {
children: RefCell<Vec<Rc<EscapeNode>>>,
},
Nonterminal {
c: char,
children: RefCell<Vec<Rc<EscapeNode>>>,
},
Terminal {
c: char,
v: Escape,
},
}
impl EscapeNode {
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 child(&self, c: char) -> Option<Rc<EscapeNode>> {
for child in self.children().iter_mut() {
if child.char() == c {
return Some(Rc::clone(child));
}
}
None
}
fn child_nonterminal(&self, c: char) -> Rc<EscapeNode> {
for child in self.children().iter_mut() {
if child.char() == c {
return Rc::clone(child);
}
}
let child = Rc::new(EscapeNode::Nonterminal {
c,
children: RefCell::new(Vec::new()),
});
self.children().push(Rc::clone(&child));
child
}
fn add_child_terminal(&self, c: char, v: Escape) {
for child in self.children().iter_mut() {
if child.char() == c {
panic!();
}
}
let child = Rc::new(EscapeNode::Terminal { c, v });
self.children().push(child);
}
}
impl EscapeCursor {
fn step(&mut self, c: char) -> Option<Escape> {
debug!("step: {c}");
let child = self.target.child(c).unwrap();
match child.as_ref() {
EscapeNode::Terminal { v, .. } => {
self.reset();
Some(*v)
}
_ => {
self.target = child;
None
}
}
}
fn add_step(&mut self, c: char) {
match self.target.child(c) {
Some(child) => self.target = child,
None => {
self.target = self.target.child_nonterminal(c);
}
}
}
fn add_terminal(&mut self, c: char, v: Escape) {
self.target.add_child_terminal(c, v);
}
fn reset(&mut self) {
self.target = Rc::clone(&self.root);
}
fn is_at_root(&self) -> bool {
Rc::ptr_eq(&self.target, &self.root)
}
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);
}
}
}
macro_rules! escapes {
($($sequence:literal $variant:tt)*) => {
#[derive(Debug, Clone, Copy)]
pub enum Escape {
Empty,
$(
$variant,
)*
}
pub fn build_prefix_tree() -> EscapeCursor {
let mut tree = EscapeNode::new();
$(
let v = Escape::$variant;
tree.insert($sequence, v);
)*
tree.reset();
tree
}
};
}
escapes! {
"[A" Up
"[B" Down
@ -406,16 +495,16 @@ escapes! {
"[5~" PageUp
"[6~" PageDown
"[13;2u" Shift_Enter
"[13;5u" Ctrl_Enter
"[13;6u" Ctrl_Shift_Enter
"[13;2u" ShiftEnter
"[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
"[1;2P" ShiftF1
"[1;2Q" ShiftF2
"[1;5P" CtrlF1
"[1;6P" CtrlShiftF1
"[1;3P" AltF1
"[1;4P" ShiftAltF1
"OP" F1
"OQ" F2
@ -423,10 +512,96 @@ escapes! {
"OS" F4
"[15~" F5
"[15;2~" Shift_F5
"[15;2~" ShiftF5
"[17~" F6
"[18~" F7
"[19~" F8
"[20~" F9
"[24~" F12
"[53;5u" Ctrl_Shift_5
"[53;5u" CtrlShift5
}
/// a control character
#[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)),
}
}
}
}

@ -1,9 +1,9 @@
use crate::{
edit,
ext::{Command, Echo, Printenv, Tail, Which},
input,
log::*,
output, runtime, syntax,
output,
runtime::{self, Eval},
syntax::parse,
};
use std::{
@ -18,61 +18,43 @@ use dirs;
/// An interactive session. The Session object is the top-level object used to control an
/// interactive terminal session.
pub struct Session {
pub input: input::Reader,
pub output: output::Writer,
pub editor: edit::Buffer,
pub editor: edit::Editor,
pub stdout: output::Writer,
pub stderr: output::Writer,
pub state: runtime::State,
}
impl Session {
pub fn new() -> Result<Self> {
Ok(Self {
input: input::Reader::new()?,
output: output::Writer::stdout()?,
editor: edit::Buffer::new(),
editor: edit::Editor::new()?,
stdout: output::Writer::stdout()?,
stderr: output::Writer::stderr()?,
state: runtime::State::new(),
})
}
pub fn back(&mut self, n: usize) -> Result<()> {
debug!("⛬ ←");
if self.editor.back(n) {
self.output.back(n)?;
}
Ok(())
}
pub fn forward(&mut self, n: usize) -> Result<()> {
debug!("⛬ →");
if self.editor.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!("»");
let n = self.editor.seek_right();
if n > 0 {
// move right by the distance seeked
self.output.forward(n)?;
}
Ok(())
}
pub fn seek_left(&mut self) -> Result<()> {
info!("«");
let n = self.editor.seek_left();
if n > 0 {
// move left by the distance seeked
self.output.back(n)?;
pub fn run(mut self) -> Result<()> {
info!("» shell session start --------");
loop {
self.editor.show_prompt()?;
let text = match self.editor.read_command()? {
Some(text) => text,
None => break,
};
let command = match parse(&text) {
Ok(ast) => ast,
Err(e) => {
self.render_error(e)?;
continue;
}
};
if let Err(e) = command.eval(&mut self.state) {
self.render_error(e)?;
}
}
info!("» exit");
self.stdout.newline()?;
Ok(())
}
@ -119,46 +101,46 @@ impl Session {
}
}
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);
}
}
}
// 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);
// }
// }
// }
pub fn render_error<E: Error>(&mut self, e: E) -> io::Result<()> {
self.render_error_helper(e, 0)?;
@ -167,9 +149,9 @@ impl Session {
fn render_error_helper<E: Error>(&mut self, e: E, depth: u8) -> io::Result<()> {
if depth > 0 {
writeln!(self.output, " {e}")?;
writeln!(self.stdout, " {e}")?;
} else {
writeln!(self.output, "{e}:")?;
writeln!(self.stdout, "{e}:")?;
}
if let Some(cause) = e.source() {
self.render_error_helper(cause, depth + 1)?;

@ -1,6 +1,7 @@
use std::fmt;
use windows::Win32::System::Console;
#[derive(Debug, PartialEq, Clone, Copy)]
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub struct Code {
/// The integer value of the keycode
pub val: u16,
@ -17,10 +18,25 @@ pub struct Event {
/// The number of times this event has happened in sequence
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,
/// 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,
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,
/// Whether or not one of the CTRL keys was held when the event was triggered
@ -33,74 +49,134 @@ pub struct Event {
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 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let down = if self.down { '↓' } else { '↑' };
let sym = match self.code.sym {
Some(c) => c,
None => '∅',
None => '·',
};
let ctrl = if self.ctrl { '⎈' } else { '·' };
let alt = if self.alt { '⎇' } else { '·' };
let shift = if self.shift { '⇧'} else { '·' };
let c = if self.char.is_control() { '·' } else { self.char };
write!(f, "{} {} {} {} {: >3} {} {: >1} {: >3}", down, ctrl, alt, shift, self.code.val, sym, c, self.char as u16)
let ctrl = if self.ctrl { 'C' } else { '·' };
let alt = if self.alt { 'A' } else { '·' };
let shift = if self.shift { 'S' } else { '·' };
let glyph = if !self.char.is_control() {
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
/// values associate to valid key codes.
pub static CODES: [Code; 256] = gen_codes();
macro_rules! keycodes {
macro_rules! codes {
($($val:literal $name:ident $sym:literal)*) => {
const fn gen_codes() -> [Code; 256] {
let mut codes = [Code{val: 0, sym: None}; 256];
let mut i = 0 as usize;
while i < 256 {
codes[i] = Code{val: i as u16, sym: None};
i = i + 1;
}
pub mod codes {
use super::Code;
$(
codes[$val] = Code{val: $val, sym: Some($sym)};
#[allow(unused)]
pub const $name: Code = Code{val: $val, sym: Some($sym)};
)*
codes
}
$(
#[allow(dead_code)]
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] {
let mut codes = [Code{val: 0, sym: None}; 256];
let mut i = 0 as usize;
while i < 256 {
codes[i] = Code{val: i as u16, sym: None};
i = i + 1;
}
$(
codes[$val] = Code{val: $val, sym: Some($sym)};
)*
codes
}
/// Lookup table to retrieve keycodes by their numeric value
const INDEX: [Code; 256] = gen_codes();
/// 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! {
0x07 NOTBACKSPACE '⌫'
codes! {
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 '⌫'
0x09 TAB '↹'
// 0x0A-0B Reserved
// 0x0C CLEAR
0x0D ENTER '↩'
// 0x0E-0F Undefined
0x09 TAB '↹' // 0x0A-0B Reserved
0x0C CLEAR ' '
0x0D ENTER '↩' // 0x0E-0F Unassigned
0x10 SHIFT '⇧'
0x11 CTRL '⎈'
0x12 ALT '⎇'
0x13 BREAK '⎉'
0x13 PAUSE '⎉'
0x14 CAPS_LOCK '⇪'
// 0x15 IME 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
0x15 KANA_MODE ' '
0x1B ESC '⎋'
0x20 SPACE '␣'
0x21 PAGE_UP '↟'
0x22 PAGE_DOWN '↡'
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'
0x42 B 'b'
0x43 C 'c'
@ -127,144 +203,8 @@ keycodes! {
0x58 X 'x'
0x59 Y 'y'
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 '⇧'
0xA1 RIGHT_SHIFT '⇧'
0xBE PERIOD '.'
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
*/

@ -2,12 +2,7 @@ use crate::{
error::LexError,
topo::{Glyph, Glyphs, Position},
};
use std::{collections::VecDeque, fmt, ops::Range, str::Chars};
/// splits a corpus into Tokens.
pub fn lex(source: &str) -> Result<Vec<Token>, LexError> {
Lexer::new(source).collect()
}
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.
@ -233,6 +228,12 @@ impl<'text> Iterator for Lexer<'text> {
}
}
/// 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::*;
@ -333,15 +334,3 @@ mod tests {
mixed_1 "ls *.py" [ word("ls") glob("*.py") ]
}
}
/*
Run a program or command named a, which is on the PATH
> a
Run a program or command named a, which is in the current directory
> ./a
*/

@ -1,5 +1,5 @@
use crate::error::Error;
pub use log::{debug, error, info, set_logger, set_max_level, warn, LevelFilter};
pub use log::{debug, info, set_logger, set_max_level, warn, LevelFilter};
use std::{
fs::File,

@ -6,6 +6,7 @@ mod builtins;
/// all of the errors for the clyde project live in this module
mod error;
/// this is old and crap delete it please
mod ext;
/// handles input from terminals
@ -29,8 +30,10 @@ mod output;
/// turns our tokens into parse trees
mod parse;
/// why is prompt a whole module what am i doing
mod prompt;
/// the runtime state of the shell process
mod runtime;
/// syntax and semantic analysis
@ -39,191 +42,75 @@ mod syntax;
/// topoglyph is a real word, i promise
mod topo;
use crate::{interactive::Session, log::*, prompt::Prompt, runtime::Eval};
fn main() -> anyhow::Result<()> {
let session = interactive::Session::new()?;
use std::io::Write;
use anyhow::Result;
fn main() -> Result<()> {
let mut session = Session::new()?;
#[cfg(debug_assertions)]
session.enable_logging("~/clyde.log");
let prompt = Prompt::new();
prompt.print(&mut session.output)?;
info!("» shell session start --------");
loop {
match session.input.next()? {
input::Event::Key(event) => {
if event.down {
if event.code.val == 0 {
debug!(" {}", event);
} else {
warn!(" {}", event);
}
continue;
}
info!(" {}", event);
session.run()?;
Ok(())
if event.code == key::LEFT {
session.back(1)?;
continue;
}
if event.code == key::RIGHT {
session.forward(1)?;
continue;
}
if event.code == key::ENTER {
session.output.newline()?;
let s = session.editor.pop();
info!("◇ {}", s);
if let Ok(tokens) = lex::lex(&s) {
for t in tokens {
debug!(" {:?}", t);
/*
loop {
match session.input.next()? {
input::Event::Key(event) => {
if event.down {
if event.code.val == 0 {
debug!(" {}", event);
} else {
warn!(" {}", event);
}
continue;
}
match syntax::parse(&s) {
Ok(tree) => {
debug!(" {:?}", tree);
let mut state = runtime::State::new();
if let Err(e) = tree.eval(&mut state) {
info!(" {}", event);
if event.code == key::ENTER {
session.stdout.newline()?;
let s = session.editor.pop();
info!("◇ {}", s);
if let Ok(tokens) = lex::lex(&s) {
for t in tokens {
debug!(" {:?}", t);
}
}
match syntax::parse(&s) {
Ok(tree) => {
debug!(" {:?}", tree);
let mut state = runtime::State::new();
if let Err(e) = tree.eval(&mut state) {
error!("{e:?}");
_ = session.render_error(e);
}
}
Err(e) => {
error!("{e:?}");
_ = session.render_error(e);
}
}
Err(e) => {
error!("{e:?}");
_ = session.render_error(e);
}
}
// 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.
session.reset()?;
prompt.print(&mut session.output)?;
continue;
}
if event.code == key::TAB {
continue;
}
if event.code == key::BACKSPACE {
if session.editor.backspace() {
// move cursor back two spaces
session.output.back(2)?;
let tail = format!("{} ", session.editor.tail());
let n = tail.chars().count();
session.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);
session.output.back(n - 1)?;
// output.write(text.as_bytes())?;
} else {
// honestly I can't remember how I figured this out
session.output.write(b" \x1b[1D")?;
}
// 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.
session.reset()?;
prompt.print(&mut session.stdout)?;
continue;
}
continue;
}
// CTRL-D to exit
if event.ctrl && event.code == key::D {
info!("» exit");
session.output.close()?;
return Ok(());
}
// CTRL-J to draw a cool little dot
if event.ctrl && event.code == key::J {
debug!("⎈ j: dot");
// red bullet
session
.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");
session.output.clear()?;
prompt.print(&mut session.output)?;
session.output.write(session.editor.show().as_bytes())?;
session
.output
.back(session.editor.len() - session.editor.pos())?;
session.reset()?;
continue;
}
// CTRL-U to erase to the beginning of the line
if event.ctrl && event.code == key::U {
info!("» clear left");
let n = session.editor.clear_left();
if n > 0 {
// move left by the number of elements removed
session.output.back(n)?;
// draw the elements remaining, followed by a space for each removed
// element
let kept = session.editor.show();
let text = format!("{}{:width$}", kept, "", width = n);
session.output.write(text.as_bytes())?;
session.output.back(n + kept.chars().count())?;
// CTRL-J to draw a cool little dot
if event.ctrl && event.code == key::J {
debug!("⎈ j: dot");
// red bullet
session
.stdout
.write(String::from("\x1b[31m\u{2022}\x1b[0m").as_bytes())?;
continue;
}
continue;
}
// CTRL-A to move to the beginning of the line
if event.ctrl && event.code == key::A {
session.seek_left()?;
continue;
}
// CTRL-E to move to the end of the line
if event.ctrl && event.code == key::E {
session.seek_right()?;
continue;
warn!("‽ {}", event);
}
// 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() {
session.editor.insert(event.char);
let tail = session.editor.tail();
let n = tail.chars().count();
// write everything from the current line cursor out to the output buffer.
session.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.
session.output.back(n - 1)?;
}
continue;
}
warn!("‽ {}", event);
input::Event::Up => debug!("⛬ ↑"),
input::Event::Down => debug!("⛬ ↓"),
}
input::Event::Left => session.back(1)?,
input::Event::Right => session.forward(1)?,
input::Event::Up => debug!("⛬ ↑"),
input::Event::Down => debug!("⛬ ↓"),
input::Event::Home => session.seek_left()?,
input::Event::End => session.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 => {}
}
}
*/
}

@ -109,13 +109,23 @@ impl Writer {
}
}
pub fn close(&mut self) -> Result<()> {
pub fn stderr() -> Result<Self> {
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 close(&mut self) -> Result<()> {
// unsafe {
// CloseHandle(self.output);
// }
// Ok(())
// }
pub fn reset(&mut self) -> Result<()> {
unsafe {
Console::SetConsoleOutputCP(65001);
@ -142,6 +152,7 @@ impl Writer {
Ok(())
}
/// clears the output buffer
pub fn clear(&mut self) -> Result<()> {
self.write(b"\x1b[2J\x1b[0;0H")?;
Ok(())

@ -2,8 +2,6 @@ use crate::error::ParseError;
use crate::lex::{Lexer, Token};
use std::{
cell::RefCell,
collections::VecDeque,
io::Write,
rc::{Rc, Weak},
sync::atomic::AtomicUsize,
};
@ -130,18 +128,10 @@ impl Cursor {
Ok(())
}
pub fn is_root(&self) -> bool {
self.target.parent.is_none()
}
pub fn up_to_root(&mut self) {
self.target = Rc::clone(&self.root);
}
pub fn into_root(self) -> Rc<Node> {
Rc::clone(&self.root)
}
pub fn value(&self) -> &Value {
&self.target.value
}
@ -154,12 +144,22 @@ impl Cursor {
}
}
pub fn render_textree<W: Write>(&self, w: &mut W, depth: u32) {
write!(w, "{:?} {pad:?}", self.target.value, pad = depth * 2);
for child in self.iter_children() {
child.render_textree(w, depth + 1);
}
#[cfg(test)]
fn is_root(&self) -> bool {
self.target.parent.is_none()
}
#[cfg(test)]
fn into_root(self) -> Rc<Node> {
Rc::clone(&self.root)
}
// pub fn render_textree<W: Write>(&self, w: &mut W, depth: u32) {
// write!(w, "{:?} {pad:?}", self.target.value, pad = depth * 2);
// for child in self.iter_children() {
// child.render_textree(w, depth + 1);
// }
// }
}
pub struct Parser<'text> {
@ -221,17 +221,17 @@ impl<'text> Parser<'text> {
}
}
fn parse(source: &str) -> Result<Cursor, ParseError> {
let tokens = Lexer::new(source);
let parser = Parser::new(tokens);
parser.parse()
}
#[cfg(test)]
mod test {
use super::*;
use crate::lex::lex;
fn parse(source: &str) -> Result<Cursor, ParseError> {
let tokens = Lexer::new(source);
let parser = Parser::new(tokens);
parser.parse()
}
#[test]
fn root() {
let mut cursor = Node::new();
@ -269,64 +269,3 @@ mod test {
Ok(())
}
}
/*
> ls
start
statement
ls
> ls ;
start
statement
ls
;
> ls ; ls
start
statement
ls
;
statement
ls
> ls one two three
start
statement
ls
one
two
three
> ls > files.txt ; echo files.txt
start
statement
ls
>
files.txt
;
statement
echo
files.txt
> if exists ~/.vimrc : echo you have a vimrc
> if $x == 3: echo hi
start
if
expression
$x
==
3
:
statement
echo
hi
*/

@ -1,14 +1,23 @@
use crate::error::ExecError;
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 State) -> 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),
}
@ -40,7 +49,12 @@ impl Eval for Value {
}
}
/// 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>,
}

@ -2,7 +2,6 @@ use crate::{
builtins::Builtin,
error::{ExecError, ParseError},
lex::{Lexer, Token},
log::debug,
parse,
runtime::{Eval, State, Value},
};
@ -156,7 +155,6 @@ 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()?;
debug!("parse tree: {parse_tree:?}");
let mut builder = TreeBuilder::new();
builder.descend(&mut parse_tree)
}
@ -169,7 +167,6 @@ mod test {
fn hi() -> Result<(), ParseError> {
let e = parse("ls one two three")?;
print!("{:?}", e);
todo!()
//Ok(())
Ok(())
}
}

Loading…
Cancel
Save