From 37224f63c595c0dc6dc8503451f7cac17d784a36 Mon Sep 17 00:00:00 2001 From: Jordan Orelli Date: Sun, 5 Mar 2023 19:14:28 -0600 Subject: [PATCH] more refactoring for great beauty --- src/key.rs | 4 +- src/line.rs | 2 +- src/main.rs | 252 ++++++++++---------------------------------------- src/output.rs | 149 ++++++++++++++++++++++++----- src/prompt.rs | 24 ++--- 5 files changed, 187 insertions(+), 244 deletions(-) diff --git a/src/key.rs b/src/key.rs index 8390f37..c9fad9f 100644 --- a/src/key.rs +++ b/src/key.rs @@ -43,7 +43,7 @@ impl fmt::Display for Event { let ctrl = if self.ctrl { '⎈' } else { '·' }; let alt = if self.alt { '⎇' } else { '·' }; let shift = if self.shift { '⇧'} else { '·' }; - write!(f, "{} {} {} {} {: >2} {}", down, ctrl, alt, shift, self.code.val, sym) + write!(f, "{} {} {} {} {: >3} {}", down, ctrl, alt, shift, self.code.val, sym) } } @@ -168,6 +168,8 @@ keycodes! { 0x28 DOWN '↓' 0xA0 LEFT_SHIFT '⇧' 0xA1 RIGHT_SHIFT '⇧' + 0xBE PERIOD '.' + 0xDE QUOTE '\'' } /* diff --git a/src/line.rs b/src/line.rs index d01c6c2..4a73397 100644 --- a/src/line.rs +++ b/src/line.rs @@ -110,6 +110,6 @@ impl Line { } fn show_debug(&self) { - debug!(" {: <4} {: >5} {}", self.cursor, self.chars.len(), self.show()); + debug!(" {: <5} {: >5} {}", self.cursor, self.chars.len(), self.show()); } } diff --git a/src/main.rs b/src/main.rs index 0e73bc9..82ae787 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,151 +1,21 @@ mod error; mod input; -mod output; mod key; mod line; mod log; +mod output; mod prompt; use line::Line; use prompt::Prompt; use std::fs::File; use std::io::{Read, Seek, SeekFrom, Write}; -use windows::Win32::{ - Foundation::HANDLE, - System::Console, -}; - -use anyhow::{Context, Result}; -use crate::{error::Error, log::*}; +use anyhow::Result; -#[allow(dead_code)] -fn log_output_mode(mode: Console::CONSOLE_MODE) { - // Characters written by the WriteFile or WriteConsole function or echoed by the ReadFile or - // ReadConsole function are parsed for ASCII control sequences, and the correct action is - // performed. Backspace, tab, bell, carriage return, and line feed characters are processed. It - // should be enabled when using control sequences or when ENABLE_VIRTUAL_TERMINAL_PROCESSING is - // set. - if (mode & Console::ENABLE_PROCESSED_OUTPUT).0 > 0 { - debug!("Processed Output: Enabled"); - } else { - debug!("Processed Output: Disabled"); - } - - // When writing with WriteFile or WriteConsole or echoing with ReadFile or ReadConsole, the - // cursor moves to the beginning of the next row when it reaches the end of the current row. - // This causes the rows displayed in the console window to scroll up automatically when the - // cursor advances beyond the last row in the window. It also causes the contents of the - // console screen buffer to scroll up (../discarding the top row of the console screen buffer) - // when the cursor advances beyond the last row in the console screen buffer. If this mode is - // disabled, the last character in the row is overwritten with any subsequent characters. - if (mode & Console::ENABLE_WRAP_AT_EOL_OUTPUT).0 > 0 { - debug!("Wrap at EOL: Enabled"); - } else { - debug!("Wrap at EOL: Disabled"); - } +use crate::log::*; - // When writing with WriteFile or WriteConsole, characters are parsed for VT100 and similar - // control character sequences that control cursor movement, color/font mode, and other - // operations that can also be performed via the existing Console APIs. For more information, - // see Console Virtual Terminal Sequences. - // - // Ensure ENABLE_PROCESSED_OUTPUT is set when using this flag. - if (mode & Console::ENABLE_VIRTUAL_TERMINAL_PROCESSING).0 > 0 { - debug!("Terminal Processing: Enabled"); - } else { - debug!("Terminal Processing: Disabled"); - } - - // When writing with WriteFile or WriteConsole, this adds an additional state to end-of-line - // wrapping that can delay the cursor move and buffer scroll operations. - // - // Normally when ENABLE_WRAP_AT_EOL_OUTPUT is set and text reaches the end of the line, the - // cursor will immediately move to the next line and the contents of the buffer will scroll up - // by one line. In contrast with this flag set, the cursor does not move to the next line, and - // the scroll operation is not performed. The written character will be printed in the final - // position on the line and the cursor will remain above this character as if - // ENABLE_WRAP_AT_EOL_OUTPUT was off, but the next printable character will be printed as if - // ENABLE_WRAP_AT_EOL_OUTPUT is on. No overwrite will occur. Specifically, the cursor quickly - // advances down to the following line, a scroll is performed if necessary, the character is - // printed, and the cursor advances one more position. - // - // The typical usage of this flag is intended in conjunction with setting - // ENABLE_VIRTUAL_TERMINAL_PROCESSING to better emulate a terminal emulator where writing the - // final character on the screen (../in the bottom right corner) without triggering an - // immediate scroll is the desired behavior. - if (mode & Console::DISABLE_NEWLINE_AUTO_RETURN).0 > 0 { - debug!("Newline Auto Return: Enabled"); - } else { - debug!("Newline Auto Return: Disabled"); - } - - // The APIs for writing character attributes including WriteConsoleOutput and - // WriteConsoleOutputAttribute allow the usage of flags from character attributes to adjust the - // color of the foreground and background of text. Additionally, a range of DBCS flags was - // specified with the COMMON_LVB prefix. Historically, these flags only functioned in DBCS code - // pages for Chinese, Japanese, and Korean languages. - // - // With exception of the leading byte and trailing byte flags, the remaining flags describing - // line drawing and reverse video (../swap foreground and background colors) can be useful for - // other languages to emphasize portions of output. - // - // Setting this console mode flag will allow these attributes to be used in every code page on - // every language. - // - // It is off by default to maintain compatibility with known applications that have - // historically taken advantage of the console ignoring these flags on non-CJK machines to - // store bits in these fields for their own purposes or by accident. - // - // Note that using the ENABLE_VIRTUAL_TERMINAL_PROCESSING mode can result in LVB grid and - // reverse video flags being set while this flag is still off if the attached application - // requests underlining or inverse video via Console Virtual Terminal Sequences. - if (mode & Console::ENABLE_LVB_GRID_WORLDWIDE).0 > 0 { - debug!("LVB Grid: Enabled"); - } else { - debug!("LVB Grid: Disabled"); - } -} - -pub fn stdout_handle() -> Result { - unsafe { - let handle = Console::GetStdHandle(Console::STD_OUTPUT_HANDLE) - .context("unable to get stdin handle")?; - Ok(handle) - } -} - -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(()) -} - -fn newline(stdout: HANDLE) -> Result<()> { - unsafe { - Error::check(Console::WriteConsoleA( - stdout, - "\r\n".as_bytes(), - None, - None, - ))?; - } - Ok(()) -} - -fn eval(cmd: String, args: Vec<&str>) -> Result { +fn eval(output: &mut output::Writer, cmd: String, args: Vec<&str>) -> Result { match cmd.as_str() { "pwd" => { let pb = std::env::current_dir()?; @@ -200,11 +70,10 @@ fn eval(cmd: String, args: Vec<&str>) -> Result { "tail" => { if args.len() > 0 { let fname = args[0]; - let stdout = stdout_handle()?; match File::options().read(true).open(fname) { Ok(mut f) => { _ = f.seek(SeekFrom::End(0)); - let mut one_byte: [u8; 1] = [0 ; 1]; + let mut one_byte: [u8; 1] = [0; 1]; let mut buf: Vec = vec![]; loop { match f.read(&mut one_byte) { @@ -212,14 +81,12 @@ fn eval(cmd: String, args: Vec<&str>) -> Result { if n == 1 { buf.push(one_byte[0]); if let Ok(s) = std::str::from_utf8(&buf) { - unsafe { - Error::check(Console::WriteConsoleA(stdout, s.as_bytes(), None, None))?; - } + output.write(s.as_bytes())?; buf.clear(); } } - }, - Err(_) => {}, + } + Err(_) => {} } } } @@ -270,15 +137,12 @@ fn main() -> Result<()> { } } - setup_stdout()?; - - let stdout = stdout_handle()?; let mut line = Line::new(); let prompt = Prompt::new(); let mut input = input::Reader::new()?; let mut output = output::Writer::new()?; - prompt.print()?; + prompt.print(&mut output)?; info!("» enter"); loop { match input.next()? { @@ -294,23 +158,27 @@ fn main() -> Result<()> { info!(" {}", event); if event.code == key::ENTER { - newline(stdout)?; + output.newline()?; let s = line.pop(); let parts: Vec<&str> = s.split_whitespace().collect(); if parts.len() > 0 { let cmd = parts[0].to_string(); - let args = if parts.len() > 1 { parts[1..].to_vec() } else { vec![] }; + let args = if parts.len() > 1 { + parts[1..].to_vec() + } else { + vec![] + }; debug!("◇ {} {}", cmd.clone(), args.join(" ")); - match eval(cmd.clone(), args.clone()) { + match eval(&mut output, cmd.clone(), args.clone()) { Ok(true) => info!("▷ {} {}", cmd, args.join(" ")), Ok(false) => warn!("▷ {} {}", cmd, args.join(" ")), Err(e) => { error!("▷ {} {} ● {}", cmd, args.join(" "), e); println!("error: {}", e); - }, + } } } - prompt.print()?; + prompt.print(&mut output)?; continue; } @@ -321,20 +189,20 @@ fn main() -> Result<()> { if event.code == key::BACKSPACE { if line.backspace() { // move cursor back two spaces - output.write("\x1b[2D")?; + output.write(b"\x1b[2D")?; // output.back(2)?; let tail = format!("{} ", line.tail()); let n = tail.chars().count(); - output.write(&tail)?; + 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.write(&text)?; + let text = format!("\x1b[{}D", n - 1); + output.write(text.as_bytes())?; } else { // honestly I can't remember how I figured this out - output.write(" \x1b[1D")?; + output.write(b" \x1b[1D")?; } } continue; @@ -351,16 +219,16 @@ fn main() -> Result<()> { if event.ctrl && event.code == key::J { debug!("⎈ j: dot"); // red bullet - output.write("\x1b[31m\u{2022}\x1b[0m")?; + 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.write("\x1b[2J\x1b[0;0H")?; - prompt.print()?; - output.write(&line.show())?; + output.write(b"\x1b[2J\x1b[0;0H")?; + prompt.print(&mut output)?; + output.write(line.show().as_bytes())?; continue; } @@ -371,20 +239,14 @@ fn main() -> Result<()> { if n > 0 { // move left by the number of elements removed let text = format!("\x1b[{}D", n); - unsafe { - Error::check(Console::WriteConsoleA(stdout, text.as_bytes(), None, None))?; - } + output.write(text.as_bytes())?; // draw the elements remaining, followed by a space for each removed // element let kept = line.show(); let text = format!("{}{:width$}", kept, "", width = n); - unsafe { - Error::check(Console::WriteConsoleA(stdout, text.as_bytes(), None, None))?; - } + output.write(text.as_bytes())?; let text = format!("\x1b[{}D", n + kept.chars().count()); - unsafe { - Error::check(Console::WriteConsoleA(stdout, text.as_bytes(), None, None))?; - } + output.write(text.as_bytes())?; } continue; } @@ -396,9 +258,7 @@ fn main() -> Result<()> { if n > 0 { // move left by the distance seeked let text = format!("\x1b[{}D", n); - unsafe { - Error::check(Console::WriteConsoleA(stdout, text.as_bytes(), None, None))?; - } + output.write(text.as_bytes())?; } continue; } @@ -410,9 +270,7 @@ fn main() -> Result<()> { if n > 0 { // move right by the distance seeked let text = format!("\x1b[{}C", n); - unsafe { - Error::check(Console::WriteConsoleA(stdout, text.as_bytes(), None, None))?; - } + output.write(text.as_bytes())?; } continue; } @@ -426,20 +284,12 @@ fn main() -> Result<()> { let n = tail.chars().count(); // write everything from the current line cursor out to the output buffer. - unsafe { - Error::check(Console::WriteConsoleA( - stdout, - tail.as_bytes(), - None, - None, - ))?; - + 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. - if n > 1 { - let text = format!("\x1b[{}D", n-1); - Error::check(Console::WriteConsoleA(stdout, text.as_bytes(), None, None))?; - } + let text = format!("\x1b[{}D", n - 1); + output.write(text.as_bytes())?; } continue; } @@ -449,34 +299,28 @@ fn main() -> Result<()> { input::Event::Left => { debug!("⛬ ←"); if line.back() { - unsafe { - let text = "\x1b[D"; // lol this sucks - Error::check(Console::WriteConsoleA(stdout, text.as_bytes(), None, None))?; - } + output.write(b"\x1b[D")?; // lol this sucks } - }, + } input::Event::Right => { debug!("⛬ →"); if line.forward() { - unsafe { - let text = "\x1b[C"; // lol this sucks - Error::check(Console::WriteConsoleA(stdout, text.as_bytes(), None, None))?; - } + output.write(b"\x1b[C")?; // lol this sucks } } input::Event::Up => { debug!("⛬ ↑"); - }, + } input::Event::Down => { debug!("⛬ ↓"); - }, - input::Event::Home => {}, - input::Event::End => {}, - input::Event::Focus(true) => {}, - input::Event::Focus(false) => {}, - input::Event::Menu(_command_id) => {}, - input::Event::Mouse{..} => {}, - input::Event::Size => {}, + } + input::Event::Home => {} + input::Event::End => {} + input::Event::Focus(true) => {} + input::Event::Focus(false) => {} + input::Event::Menu(_command_id) => {} + input::Event::Mouse { .. } => {} + input::Event::Size => {} } } } diff --git a/src/output.rs b/src/output.rs index 99d37fb..bde1cd2 100644 --- a/src/output.rs +++ b/src/output.rs @@ -1,8 +1,98 @@ +use crate::{error::Error, log::*}; use anyhow::{Context, Result}; -use crate::error::Error; -use windows::Win32::{Foundation::{CloseHandle, HANDLE}, System::Console}; -// use std::io; +use std::io::{self, Write}; +use windows::Win32::{ + Foundation::{CloseHandle, HANDLE}, + System::Console, +}; +#[allow(dead_code)] +fn log_output_mode(mode: Console::CONSOLE_MODE) { + // Characters written by the WriteFile or WriteConsole function or echoed by the ReadFile or + // ReadConsole function are parsed for ASCII control sequences, and the correct action is + // performed. Backspace, tab, bell, carriage return, and line feed characters are processed. It + // should be enabled when using control sequences or when ENABLE_VIRTUAL_TERMINAL_PROCESSING is + // set. + if (mode & Console::ENABLE_PROCESSED_OUTPUT).0 > 0 { + debug!("Processed Output: Enabled"); + } else { + debug!("Processed Output: Disabled"); + } + + // When writing with WriteFile or WriteConsole or echoing with ReadFile or ReadConsole, the + // cursor moves to the beginning of the next row when it reaches the end of the current row. + // This causes the rows displayed in the console window to scroll up automatically when the + // cursor advances beyond the last row in the window. It also causes the contents of the + // console screen buffer to scroll up (../discarding the top row of the console screen buffer) + // when the cursor advances beyond the last row in the console screen buffer. If this mode is + // disabled, the last character in the row is overwritten with any subsequent characters. + if (mode & Console::ENABLE_WRAP_AT_EOL_OUTPUT).0 > 0 { + debug!("Wrap at EOL: Enabled"); + } else { + debug!("Wrap at EOL: Disabled"); + } + + // When writing with WriteFile or WriteConsole, characters are parsed for VT100 and similar + // control character sequences that control cursor movement, color/font mode, and other + // operations that can also be performed via the existing Console APIs. For more information, + // see Console Virtual Terminal Sequences. + // + // Ensure ENABLE_PROCESSED_OUTPUT is set when using this flag. + if (mode & Console::ENABLE_VIRTUAL_TERMINAL_PROCESSING).0 > 0 { + debug!("Terminal Processing: Enabled"); + } else { + debug!("Terminal Processing: Disabled"); + } + + // When writing with WriteFile or WriteConsole, this adds an additional state to end-of-line + // wrapping that can delay the cursor move and buffer scroll operations. + // + // Normally when ENABLE_WRAP_AT_EOL_OUTPUT is set and text reaches the end of the line, the + // cursor will immediately move to the next line and the contents of the buffer will scroll up + // by one line. In contrast with this flag set, the cursor does not move to the next line, and + // the scroll operation is not performed. The written character will be printed in the final + // position on the line and the cursor will remain above this character as if + // ENABLE_WRAP_AT_EOL_OUTPUT was off, but the next printable character will be printed as if + // ENABLE_WRAP_AT_EOL_OUTPUT is on. No overwrite will occur. Specifically, the cursor quickly + // advances down to the following line, a scroll is performed if necessary, the character is + // printed, and the cursor advances one more position. + // + // The typical usage of this flag is intended in conjunction with setting + // ENABLE_VIRTUAL_TERMINAL_PROCESSING to better emulate a terminal emulator where writing the + // final character on the screen (../in the bottom right corner) without triggering an + // immediate scroll is the desired behavior. + if (mode & Console::DISABLE_NEWLINE_AUTO_RETURN).0 > 0 { + debug!("Newline Auto Return: Enabled"); + } else { + debug!("Newline Auto Return: Disabled"); + } + + // The APIs for writing character attributes including WriteConsoleOutput and + // WriteConsoleOutputAttribute allow the usage of flags from character attributes to adjust the + // color of the foreground and background of text. Additionally, a range of DBCS flags was + // specified with the COMMON_LVB prefix. Historically, these flags only functioned in DBCS code + // pages for Chinese, Japanese, and Korean languages. + // + // With exception of the leading byte and trailing byte flags, the remaining flags describing + // line drawing and reverse video (../swap foreground and background colors) can be useful for + // other languages to emphasize portions of output. + // + // Setting this console mode flag will allow these attributes to be used in every code page on + // every language. + // + // It is off by default to maintain compatibility with known applications that have + // historically taken advantage of the console ignoring these flags on non-CJK machines to + // store bits in these fields for their own purposes or by accident. + // + // Note that using the ENABLE_VIRTUAL_TERMINAL_PROCESSING mode can result in LVB grid and + // reverse video flags being set while this flag is still off if the attached application + // requests underlining or inverse video via Console Virtual Terminal Sequences. + if (mode & Console::ENABLE_LVB_GRID_WORLDWIDE).0 > 0 { + debug!("LVB Grid: Enabled"); + } else { + debug!("LVB Grid: Disabled"); + } +} pub fn stdout_handle() -> Result { unsafe { @@ -12,41 +102,58 @@ 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 { - Ok(Self{ + setup_stdout()?; + Ok(Self { output: stdout_handle()?, }) } - pub fn write(&mut self, s: &str) -> Result<()> { + pub fn close(&mut self) -> Result<()> { unsafe { - Error::check(Console::WriteConsoleA(self.output, s.as_bytes(), None, None))?; + CloseHandle(self.output); } Ok(()) } - pub fn close(&mut self) -> Result<()> { + pub fn newline(&mut self) -> Result<()> { + self.write(b"\r\n")?; + Ok(()) + } +} + +impl Write for Writer { + fn write(&mut self, buf: &[u8]) -> io::Result { unsafe { - CloseHandle(self.output); + Error::check(Console::WriteConsoleA(self.output, buf, None, None))?; } + Ok(0) + } + + fn flush(&mut self) -> io::Result<()> { Ok(()) } } - -// impl std::io::Write for Writer { -// fn write(&mut self, buf: &[u8]) -> io::Result { -// unsafe { -// Error::check(Console::WriteConsoleA(self.output, buf, None, None))?; -// } -// Ok(0) -// } -// -// fn flush(&mut self) -> io::Result<()> { -// Ok(()) -// } -// } diff --git a/src/prompt.rs b/src/prompt.rs index 54c6532..76dbead 100644 --- a/src/prompt.rs +++ b/src/prompt.rs @@ -1,6 +1,6 @@ -use crate::{error::Error, stdout_handle}; +use crate::output; +use std::io::Write; use anyhow::Result; -use windows::Win32::System::Console; pub struct Prompt { s: String, @@ -13,24 +13,14 @@ impl Prompt { } } - pub fn print(&self) -> Result<()> { + pub fn print(&self, output: &mut output::Writer) -> Result<()> { match std::env::current_dir() { - Ok(d) => unsafe { + Ok(d) => { let text = d.to_str().unwrap().to_owned() + " " + &self.s; - Error::check(Console::WriteConsoleA( - stdout_handle()?, - text.as_bytes(), - None, - None, - ))?; + output.write(text.as_bytes())?; } - Err(_) => unsafe { - Error::check(Console::WriteConsoleA( - stdout_handle()?, - self.s.as_bytes(), - None, - None, - ))?; + Err(_) => { + output.write(self.s.as_bytes())?; }, } Ok(())