diff --git a/Cargo.toml b/Cargo.toml index 818d5e2..c44163a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,4 +17,5 @@ version = "0.44.0" features = [ "Win32_Foundation", "Win32_System_Console", + "Win32_UI_Input_KeyboardAndMouse" ] diff --git a/src/edit.rs b/src/edit.rs index 27e22d7..260fe1c 100644 --- a/src/edit.rs +++ b/src/edit.rs @@ -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, - /// 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 { + Ok(Self { + input: input::Reader::new()?, + buffer: Buffer::new(), + display: output::Writer::stdout()?, + prompt: Prompt::new(), + }) + } + + pub fn read_command(&mut self) -> anyhow::Result> { + 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 { + 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 { + 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 { + 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 { + 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(()) + } +} diff --git a/src/error.rs b/src/error.rs index adecc3f..e256a31 100644 --- a/src/error.rs +++ b/src/error.rs @@ -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>(msg: S) -> Self { - Error::InputError(msg.into()) - } } impl From for std::io::Error { diff --git a/src/input.rs b/src/input.rs index 9ad86c4..cce0a5f 100644 --- a/src/input.rs +++ b/src/input.rs @@ -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 { + 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 { + 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 { - 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 { + 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, + + /// whether or not the control key is pressed, i think? ctrl: bool, + + pub(crate) escapes: EscapeCursor, } impl Reader { pub fn new() -> Result { + 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 { - 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 { - 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 { + 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 { - 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 { - 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 { + 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"), + Control(c) => write!(f, "Event"), + Escape(e) => write!(f, "Event"), + Focus(true) => write!(f, "Event"), + Focus(false) => write!(f, "Event"), + Menu(n) => write!(f, "Event"), + Mouse { x, y } => write!(f, "Event"), + Size => write!(f, "Event"), + } + } +} impl From for Event { fn from(rec: Console::INPUT_RECORD) -> Self { @@ -353,24 +304,7 @@ impl From 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 for Event { } } +#[derive(Debug)] +pub struct EscapeCursor { + target: Rc, + root: Rc, +} + +#[derive(Debug)] +pub enum EscapeNode { + Root { + children: RefCell>>, + }, + Nonterminal { + c: char, + children: RefCell>>, + }, + 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>> { + match self { + EscapeNode::Root { children } | EscapeNode::Nonterminal { children, .. } => { + children.borrow_mut() + } + _ => panic!(), + } + } + + fn child(&self, c: char) -> Option> { + for child in self.children().iter_mut() { + if child.char() == c { + return Some(Rc::clone(child)); + } + } + None + } + + fn child_nonterminal(&self, c: char) -> Rc { + 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 { + 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 for ControlCharacter { + type Error = InputError; + + fn try_from(v: Console::KEY_EVENT_RECORD) -> Result { + 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)), + } + } + } } diff --git a/src/interactive.rs b/src/interactive.rs index b723a07..fbcf223 100644 --- a/src/interactive.rs +++ b/src/interactive.rs @@ -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 { 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 { - 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 { + // 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(&mut self, e: E) -> io::Result<()> { self.render_error_helper(e, 0)?; @@ -167,9 +149,9 @@ impl Session { fn render_error_helper(&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)?; diff --git a/src/key.rs b/src/key.rs index 704d78a..2261900 100644 --- a/src/key.rs +++ b/src/key.rs @@ -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 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 -*/ diff --git a/src/lex.rs b/src/lex.rs index 1db4fd5..d7b5121 100644 --- a/src/lex.rs +++ b/src/lex.rs @@ -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, 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, 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 - -*/ diff --git a/src/log.rs b/src/log.rs index 604ac0c..e5aa230 100644 --- a/src/log.rs +++ b/src/log.rs @@ -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, diff --git a/src/main.rs b/src/main.rs index 324e1b8..34f8fa4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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 => {} } - } + */ } diff --git a/src/output.rs b/src/output.rs index 21d98b1..916c718 100644 --- a/src/output.rs +++ b/src/output.rs @@ -109,13 +109,23 @@ impl Writer { } } - pub fn close(&mut self) -> Result<()> { + pub fn stderr() -> Result { 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(()) diff --git a/src/parse.rs b/src/parse.rs index c601158..df0f8bf 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -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 { - Rc::clone(&self.root) - } - pub fn value(&self) -> &Value { &self.target.value } @@ -154,12 +144,22 @@ impl Cursor { } } - pub fn render_textree(&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 { + Rc::clone(&self.root) + } + + // pub fn render_textree(&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 { - 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 { + 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 -*/ diff --git a/src/runtime.rs b/src/runtime.rs index 6b5e122..a119a71 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -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; } +/// 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>, } diff --git a/src/syntax.rs b/src/syntax.rs index 401f7ce..7f9c644 100644 --- a/src/syntax.rs +++ b/src/syntax.rs @@ -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 { 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(()) } }