From 15e3ca93cc3a6bd759575f075323aafe48009f73 Mon Sep 17 00:00:00 2001 From: Jordan Orelli Date: Sun, 5 Mar 2023 23:20:02 -0600 Subject: [PATCH] cleaning up shell state this sure is tedious --- src/input.rs | 73 ++++++++++++++++++----------------- src/line.rs | 41 +++++++++++++++++--- src/main.rs | 105 +++++++++++++++++++++++--------------------------- src/output.rs | 43 ++++++++++----------- src/shell.rs | 60 +++++++++++++++++++++++++++-- 5 files changed, 198 insertions(+), 124 deletions(-) diff --git a/src/input.rs b/src/input.rs index 92e7950..d46fe1e 100644 --- a/src/input.rs +++ b/src/input.rs @@ -94,41 +94,10 @@ fn stdin_handle() -> Result { } } -fn setup_stdin() -> Result<()> { - let mut mode = Console::CONSOLE_MODE(0); - unsafe { - Console::SetConsoleCP(65001); - - let handle = stdin_handle()?; - Error::check(Console::GetConsoleMode(handle, &mut mode))?; - - // allow terminal input characters - mode |= Console::ENABLE_VIRTUAL_TERMINAL_INPUT; - - // disable automatic processing of CTRL+C, we'll handle it ourselves - mode &= !Console::ENABLE_PROCESSED_INPUT; - - // disable line mode to get every input as its pressed - mode &= !Console::ENABLE_LINE_INPUT; - - // disable automatic echoing of inputs - mode &= !Console::ENABLE_ECHO_INPUT; - - // 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); - } - Ok(()) -} - 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; 100], + 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 @@ -137,13 +106,45 @@ pub struct Reader { impl Reader { pub fn new() -> Result { - setup_stdin()?; - Ok(Self { - buf: [Console::INPUT_RECORD::default(); 100], + let v = Self { + buf: [Console::INPUT_RECORD::default(); 32], buf_len: 0, buf_idx: 0, input: stdin_handle()?, - }) + }; + v.reset()?; + Ok(v) + } + + pub fn reset(&self) -> Result<()> { + let mut mode = Console::CONSOLE_MODE(0); + unsafe { + Console::SetConsoleCP(65001); + + let handle = stdin_handle()?; + Error::check(Console::GetConsoleMode(handle, &mut mode))?; + + // allow terminal input characters + mode |= Console::ENABLE_VIRTUAL_TERMINAL_INPUT; + + // disable automatic processing of CTRL+C, we'll handle it ourselves + mode &= !Console::ENABLE_PROCESSED_INPUT; + + // disable line mode to get every input as its pressed + mode &= !Console::ENABLE_LINE_INPUT; + + // disable automatic echoing of inputs + mode &= !Console::ENABLE_ECHO_INPUT; + + // 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); + } + Ok(()) } pub fn next(&mut self) -> Result { diff --git a/src/line.rs b/src/line.rs index 4a73397..b93b367 100644 --- a/src/line.rs +++ b/src/line.rs @@ -1,4 +1,5 @@ use crate::log::*; +use std::cmp::min; pub struct Line { /// the current contents of the line @@ -33,9 +34,13 @@ impl Line { self.chars.iter().collect() } - pub fn back(&mut self) -> bool { + pub fn back(&mut self, n: usize) -> bool { if self.cursor > 0 { - self.cursor -= 1; + if n > self.cursor { + self.cursor = n; + } else { + self.cursor -= n; + } self.show_debug(); true } else { @@ -83,9 +88,9 @@ impl Line { n } - pub fn forward(&mut self) -> bool { + pub fn forward(&mut self, n: usize) -> bool { if self.cursor < self.chars.len() { - self.cursor += 1; + self.cursor = min(self.chars.len(), self.cursor + n); self.show_debug(); true } else { @@ -109,7 +114,33 @@ impl Line { chars.iter().collect() } + pub fn pos(&self) -> usize { + self.cursor + } + + pub fn len(&self) -> usize { + self.chars.len() + } + fn show_debug(&self) { - debug!(" {: <5} {: >5} {}", self.cursor, self.chars.len(), self.show()); + let before = if self.chars.len() > 0 { + self.chars[0..self.cursor].to_vec().iter().collect() + } else { + String::new() + }; + + let after = if self.chars.len() > 0 { + self.chars[self.cursor..].to_vec().iter().collect() + } else { + String::new() + }; + + debug!( + " {: <5} {: >5} {}·{}", + self.cursor, + self.chars.len(), + before, + after + ); } } diff --git a/src/main.rs b/src/main.rs index 2ca1d0b..6337bff 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,6 @@ mod output; mod shell; mod prompt; -use line::Line; use prompt::Prompt; use std::io::Write; @@ -27,16 +26,13 @@ fn main() -> Result<()> { } } - let mut line = Line::new(); let prompt = Prompt::new(); - let mut input = input::Reader::new()?; - let mut output = output::Writer::new()?; - let shell = Shell::new(); + let mut shell = Shell::new()?; - prompt.print(&mut output)?; + prompt.print(&mut shell.output)?; info!("» enter"); loop { - match input.next()? { + match shell.input.next()? { input::Event::Key(event) => { if event.down { if event.code.val == 0 { @@ -48,9 +44,19 @@ fn main() -> Result<()> { } info!(" {}", event); + if event.code == key::LEFT { + shell.back(1)?; + continue; + } + + if event.code == key::RIGHT { + shell.forward(1)?; + continue; + } + if event.code == key::ENTER { - output.newline()?; - let s = line.pop(); + shell.output.newline()?; + let s = shell.line.pop(); let parts: Vec<&str> = s.split_whitespace().collect(); if parts.len() > 0 { let cmd = parts[0].to_string(); @@ -60,7 +66,7 @@ fn main() -> Result<()> { vec![] }; debug!("◇ {} {}", cmd.clone(), args.join(" ")); - match shell.eval(&mut output, cmd.clone(), args.clone()) { + match shell.eval(cmd.clone(), args.clone()) { Ok(true) => info!("▷ {} {}", cmd, args.join(" ")), Ok(false) => warn!("▷ {} {}", cmd, args.join(" ")), Err(e) => { @@ -68,8 +74,11 @@ fn main() -> Result<()> { println!("error: {}", e); } } + // Some commands don't leave the terminal in a clean state, so we use reset + // to ensure that our input and output modes are what we expect them to be. + shell.reset()?; } - prompt.print(&mut output)?; + prompt.print(&mut shell.output)?; continue; } @@ -78,22 +87,22 @@ fn main() -> Result<()> { } if event.code == key::BACKSPACE { - if line.backspace() { + if shell.line.backspace() { // move cursor back two spaces - output.back(2)?; - let tail = format!("{} ", line.tail()); + shell.output.back(2)?; + let tail = format!("{} ", shell.line.tail()); let n = tail.chars().count(); - output.write(tail.as_bytes())?; + shell.output.write(tail.as_bytes())?; // after writing out the tail, rewind by the number of characters in // the tail if n > 1 { // let text = format!("\x1b[{}D", n - 1); - output.back(n-1)?; + shell.output.back(n-1)?; // output.write(text.as_bytes())?; } else { // honestly I can't remember how I figured this out - output.write(b" \x1b[1D")?; + shell.output.write(b" \x1b[1D")?; } } continue; @@ -102,7 +111,7 @@ fn main() -> Result<()> { // CTRL-D to exit if event.ctrl && event.code == key::D { info!("» exit"); - output.close()?; + shell.output.close()?; return Ok(()); } @@ -110,98 +119,80 @@ fn main() -> Result<()> { if event.ctrl && event.code == key::J { debug!("⎈ j: dot"); // red bullet - output.write(String::from("\x1b[31m\u{2022}\x1b[0m").as_bytes())?; + shell.output.write(String::from("\x1b[31m\u{2022}\x1b[0m").as_bytes())?; continue; } // CTRL-L to clear the screen if event.ctrl && event.code == key::L { info!("» clear"); - output.clear()?; - prompt.print(&mut output)?; - output.write(line.show().as_bytes())?; + shell.output.clear()?; + prompt.print(&mut shell.output)?; + shell.output.write(shell.line.show().as_bytes())?; + shell.output.back(shell.line.len() - shell.line.pos())?; + shell.reset()?; continue; } // CTRL-U to erase to the beginning of the line if event.ctrl && event.code == key::U { info!("» clear left"); - let n = line.clear_left(); + let n = shell.line.clear_left(); if n > 0 { // move left by the number of elements removed - output.back(n)?; + shell.output.back(n)?; // draw the elements remaining, followed by a space for each removed // element - let kept = line.show(); + let kept = shell.line.show(); let text = format!("{}{:width$}", kept, "", width = n); - output.write(text.as_bytes())?; - output.back(n + kept.chars().count())?; + shell.output.write(text.as_bytes())?; + shell.output.back(n + kept.chars().count())?; } continue; } // CTRL-A to move to the beginning of the line if event.ctrl && event.code == key::A { - info!("» seek left"); - let n = line.seek_left(); - if n > 0 { - // move left by the distance seeked - output.back(n)?; - } + shell.seek_left()?; continue; } // CTRL-E to move to the end of the line if event.ctrl && event.code == key::E { - info!("» seek right"); - let n = line.seek_right(); - if n > 0 { - // move right by the distance seeked - output.forward(n)?; - } + shell.seek_right()?; continue; } // TODO: something better here, this is crappy. I should be checking characters // based on their unicode categories, not this garbo if !event.char.is_control() { - line.insert(event.char); + shell.line.insert(event.char); - let tail = line.tail(); + let tail = shell.line.tail(); let n = tail.chars().count(); // write everything from the current line cursor out to the output buffer. - output.write(tail.as_bytes())?; + shell.output.write(tail.as_bytes())?; if n > 1 { // if we wrote more than one character, because we weren't at the end, we // need to rewind the terminal cursor to where it was. - output.back(n-1)?; + shell.output.back(n-1)?; } continue; } warn!("‽ {}", event); } - input::Event::Left => { - debug!("⛬ ←"); - if line.back() { - output.back(1)?; - } - } - input::Event::Right => { - debug!("⛬ →"); - if line.forward() { - output.forward(1)?; - } - } + input::Event::Left => { shell.back(1)? } + input::Event::Right => { shell.forward(1)? } input::Event::Up => { debug!("⛬ ↑"); } input::Event::Down => { debug!("⛬ ↓"); } - input::Event::Home => {} - input::Event::End => {} + input::Event::Home => { shell.seek_left()? } + input::Event::End => { shell.seek_right()? } input::Event::Focus(true) => {} input::Event::Focus(false) => {} input::Event::Menu(_command_id) => {} diff --git a/src/output.rs b/src/output.rs index 546bb24..55dfeb1 100644 --- a/src/output.rs +++ b/src/output.rs @@ -94,7 +94,7 @@ fn log_output_mode(mode: Console::CONSOLE_MODE) { } } -pub fn stdout_handle() -> Result { +fn stdout_handle() -> Result { unsafe { let handle = Console::GetStdHandle(Console::STD_OUTPUT_HANDLE) .context("unable to get stdin handle")?; @@ -102,34 +102,17 @@ pub fn stdout_handle() -> Result { } } -fn setup_stdout() -> Result<()> { - unsafe { - Console::SetConsoleOutputCP(65001); - } - - let mut mode = Console::CONSOLE_MODE(0); - unsafe { - let handle = stdout_handle()?; - if Console::GetConsoleMode(handle, &mut mode).as_bool() { - // debug!("Stdout details:"); - // log_output_mode(mode); - } else { - return Err(Error::last_error().into()); - } - } - Ok(()) -} - pub struct Writer { output: HANDLE, } impl Writer { pub fn new() -> Result { - setup_stdout()?; - Ok(Self { + let mut v = Self { output: stdout_handle()?, - }) + }; + v.reset()?; + Ok(v) } pub fn close(&mut self) -> Result<()> { @@ -139,6 +122,22 @@ impl Writer { Ok(()) } + pub fn reset(&mut self) -> Result<()> { + unsafe { + Console::SetConsoleOutputCP(65001); + } + // let mut mode = Console::CONSOLE_MODE(0); + // unsafe { + // if Console::GetConsoleMode(self.output, &mut mode).as_bool() { + // // debug!("Stdout details:"); + // // log_output_mode(mode); + // } else { + // return Err(Error::last_error().into()); + // } + // } + Ok(()) + } + pub fn newline(&mut self) -> Result<()> { self.write(b"\r\n")?; Ok(()) diff --git a/src/shell.rs b/src/shell.rs index b2c1ad7..a6a71b5 100644 --- a/src/shell.rs +++ b/src/shell.rs @@ -1,18 +1,70 @@ use std::fs::File; use std::io::{Read, Seek, SeekFrom, Write}; use crate::output; +use crate::input; +use crate::line::Line; +use crate::log::*; use anyhow::Result; pub struct Shell { + pub input: input::Reader, + pub output: output::Writer, + pub line: Line, } impl Shell { - pub fn new() -> Self { - Self {} + pub fn new() -> Result { + Ok(Self { + input: input::Reader::new()?, + output: output::Writer::new()?, + line: Line::new(), + }) } - pub fn eval(&self, output: &mut output::Writer, cmd: String, args: Vec<&str>) -> Result { + pub fn back(&mut self, n: usize) -> Result<()> { + debug!("⛬ ←"); + if self.line.back(n) { + self.output.back(n)?; + } + Ok(()) + } + + pub fn forward(&mut self, n: usize) -> Result<()> { + debug!("⛬ →"); + if self.line.forward(n) { + self.output.forward(n)?; + } + Ok(()) + } + + pub fn reset(&mut self) -> Result<()> { + self.input.reset()?; + self.output.reset()?; + Ok(()) + } + + pub fn seek_right(&mut self) -> Result<()> { + info!("» seek right"); + let n = self.line.seek_right(); + if n > 0 { + // move right by the distance seeked + self.output.forward(n)?; + } + Ok(()) + } + + pub fn seek_left(&mut self) -> Result<()> { + info!("» seek left"); + let n = self.line.seek_left(); + if n > 0 { + // move left by the distance seeked + self.output.back(n)?; + } + Ok(()) + } + + pub fn eval(&mut self, cmd: String, args: Vec<&str>) -> Result { match cmd.as_str() { "pwd" => { let pb = std::env::current_dir()?; @@ -78,7 +130,7 @@ impl Shell { if n == 1 { buf.push(one_byte[0]); if let Ok(s) = std::str::from_utf8(&buf) { - output.write(s.as_bytes())?; + self.output.write(s.as_bytes())?; buf.clear(); } }