diff options
-rw-r--r-- | Cargo.lock | 18 | ||||
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | src/buffer.rs | 157 | ||||
-rw-r--r-- | src/main.rs | 39 |
4 files changed, 203 insertions, 12 deletions
@@ -8,13 +8,14 @@ version = "0.0.1" dependencies = [ "libc", "signal-hook", + "termios", ] [[package]] name = "libc" -version = "0.2.153" +version = "0.2.158" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" [[package]] name = "signal-hook" @@ -28,9 +29,18 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "termios" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" dependencies = [ "libc", ] @@ -15,3 +15,4 @@ path = "src/main.rs" [dependencies] libc = "0.2.153" signal-hook = "0.3.17" +termios = "0.3.3" diff --git a/src/buffer.rs b/src/buffer.rs new file mode 100644 index 0000000..8668249 --- /dev/null +++ b/src/buffer.rs @@ -0,0 +1,157 @@ +use std::io::{self, Read, Write}; +use std::sync::{Arc, Mutex}; + +// STDIN is file descriptor (fd) 0 on Linux and other UN*X-likes +pub const STDIN: i32 = 0; + +// Key input types from the user +enum Key { + Up, + Down, + Right, + Left, + Else(u8), + Ignored, +} + +/// Retrieve a single byte of input +/// +/// Requires some setup beforehand (see beginning of repl()) +fn getchar() -> Key { + let mut b = [0; 1]; + io::stdout().lock().flush().unwrap(); + io::stdin().read_exact(&mut b).unwrap(); + + // Might me an ASNI escape sequence + if b[0] == 27 { + io::stdin().read_exact(&mut b).unwrap(); + + if b[0] == 91 { + io::stdin().read_exact(&mut b).unwrap(); + + match b[0] { + // Arrow keys + 65 => return Key::Up, + 66 => return Key::Down, + 67 => return Key::Right, + 68 => return Key::Left, + + // Everything else + _ => return Key::Ignored, + } + } + + return Key::Ignored; + } + + Key::Else(b[0]) +} + +/// Handle user input at the repl prompt +/// +/// This is required instead of io::stdin().read_line(), because certain +/// keys like `<tab>` and `<up>` have special functions (cycle through +/// autocomplete options, and history, respectively). It leverages +/// [getchar] to read each character as the user inputs it. This also +/// means special cases for handling backspace, newlines, etc. Assumes +/// that (ICANON and ECHO) are off. See the beginning of [crate::repl] +/// for more details. +pub fn getline(buffer: &mut Arc<Mutex<Vec<u8>>>, pos: &mut Arc<Mutex<usize>>) -> usize { + // Loop over characters until there is a newline + loop { + let c = getchar(); + match c { + Key::Up => { + continue; + } + Key::Down => { + continue; + } + Key::Right => { + if *pos.lock().unwrap() >= buffer.lock().unwrap().len() { + continue; + } + print!("\x1b[1C"); + *pos.lock().unwrap() += 1; + } + Key::Left => { + if *pos.lock().unwrap() == 0 { + continue; + } + print!("\u{8}"); + *pos.lock().unwrap() -= 1; + } + Key::Ignored => { + continue; + } + Key::Else(c) => match c { + // enter/return + b'\n' => break, + + // tab + b'\t' => { + *pos.lock().unwrap() += 1; + print!(" "); + buffer.lock().unwrap().push(b' '); + } + + // ctrl-d + 4 => return 0, + + // backspace + 127 => { + if *pos.lock().unwrap() == 0 { + continue; + } + *pos.lock().unwrap() -= 1; + + if *pos.lock().unwrap() == buffer.lock().unwrap().len() { + buffer.lock().unwrap().pop(); + print!("\u{8} \u{8}"); + } else { + buffer.lock().unwrap().remove(*pos.lock().unwrap()); + print!( + "\u{8}{} ", + String::from_utf8_lossy( + &buffer.lock().unwrap()[*pos.lock().unwrap()..] + ) + ); + for _ in *pos.lock().unwrap()..buffer.lock().unwrap().len() + 1 { + print!("\u{8}"); + } + } + } + + // everything else + _ => { + // Print out the character as the user is typing + print!("{}", c as char); + + // Insert the character onto the buffer at whatever *pos.lock().unwrap()ition the cursor is at + buffer.lock().unwrap().insert(*pos.lock().unwrap(), c); + + // Increment our *pos.lock().unwrap()ition + *pos.lock().unwrap() += 1; + + // Reprint the end of the buffer if inserting at the front or middle + if *pos.lock().unwrap() != buffer.lock().unwrap().len() { + print!( + "{}", + String::from_utf8_lossy( + &buffer.lock().unwrap()[*pos.lock().unwrap()..] + ) + ); + for _ in *pos.lock().unwrap()..buffer.lock().unwrap().len() { + print!("\u{8}"); + } + } + } + }, + } + } + + *pos.lock().unwrap() = 0; + println!(); + buffer.lock().unwrap().push(b'\n'); + buffer.lock().unwrap().len() +} diff --git a/src/main.rs b/src/main.rs index 98b49ed..527fb8c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,14 @@ use std::env; use std::io::{self, Write}; use std::sync::{Arc, Mutex}; +mod buffer; mod path; mod poem; use poem::{read::Readable, recite::Reciteable, Poem}; mod compose; +use buffer::{getline, STDIN}; use compose::Environment; +use termios::{tcsetattr, Termios, ECHO, ICANON, TCSANOW}; /// Starts the main shell loop /// @@ -22,12 +25,27 @@ use compose::Environment; /// repl(&mut away, &mut env); /// } /// ``` -fn repl(away: &mut Arc<Mutex<bool>>, env: &mut Environment) { +fn repl( + away: &mut Arc<Mutex<bool>>, + buffer: &mut Arc<Mutex<Vec<u8>>>, + pos: &mut Arc<Mutex<usize>>, + env: &mut Environment, +) { + // Setup termios flags + let mut termios = Termios::from_fd(STDIN).unwrap(); + termios.c_lflag &= !(ICANON | ECHO); + // Initial path refresh on startup env.bins = path::refresh(); // Main shell loop loop { + // Reset terminal using proper termios flags + tcsetattr(STDIN, TCSANOW, &mut termios).unwrap(); + + // Clear the buffer + buffer.lock().unwrap().clear(); + // Get the prompt let prompt = match env::var("PS1") { Ok(val) => val, @@ -42,10 +60,7 @@ fn repl(away: &mut Arc<Mutex<bool>>, env: &mut Environment) { *away.lock().unwrap() = false; // Wait for user input - let mut poetry = String::new(); - let bytes = io::stdin() - .read_line(&mut poetry) - .expect("dwvsh: error: unable to evaluate the input string"); + let bytes = getline(buffer, pos); // Check if we've reached EOF (i.e. <C-d>) if bytes == 0 { @@ -53,8 +68,10 @@ fn repl(away: &mut Arc<Mutex<bool>>, env: &mut Environment) { break; } - // Trim the input - let poetry = String::from(poetry.trim()); + // Convert buffer to a string and trim it + let poetry = String::from_utf8_lossy(&buffer.lock().unwrap()) + .trim() + .to_string(); // Skip parsing if there is no poetry if poetry.is_empty() { @@ -132,9 +149,15 @@ fn main() { // Handle signals let mut away = Arc::new(Mutex::new(true)); + let mut buffer: Arc<Mutex<Vec<u8>>> = Arc::new(Mutex::new(vec![])); + let mut pos: Arc<Mutex<usize>> = Arc::new(Mutex::new(0)); unsafe { let away = Arc::clone(&away); + let buffer = Arc::clone(&buffer); + let pos = Arc::clone(&pos); signal_hook::low_level::register(signal_hook::consts::SIGINT, move || { + buffer.lock().unwrap().clear(); + *pos.lock().unwrap() = 0; if *away.lock().unwrap() { println!(); } else { @@ -153,5 +176,5 @@ fn main() { options(&mut env); // Begin evaluating commands - repl(&mut away, &mut env); + repl(&mut away, &mut buffer, &mut pos, &mut env); } |