diff options
-rw-r--r-- | Cargo.lock | 7 | ||||
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | src/buffer.rs | 768 | ||||
-rw-r--r-- | src/main.rs | 58 | ||||
-rw-r--r-- | src/poem/read.rs | 21 |
5 files changed, 538 insertions, 317 deletions
@@ -28,6 +28,7 @@ dependencies = [ "nix", "signal-hook", "termios", + "unicode-width", ] [[package]] @@ -75,3 +76,9 @@ checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" dependencies = [ "libc", ] + +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" @@ -17,3 +17,4 @@ libc = "0.2.153" nix = { version = "0.29.0", features = ["signal"] } signal-hook = "0.3.17" termios = "0.3.3" +unicode-width = "0.2.0" diff --git a/src/buffer.rs b/src/buffer.rs index 7ece258..bd5ab60 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -5,100 +5,209 @@ use std::fs; use std::io::{self, Read, Write}; use std::path::PathBuf; use std::sync::{Arc, Mutex}; +use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; -// STDIN is file descriptor (fd) 0 on Linux and other UN*X-likes +/// Typical file descriptor for STDIN on Linux on other U**X-likes pub const STDIN: i32 = 0; -// Key input types from the user -#[derive(PartialEq)] -enum Key { +/// Keys recognized by [getchar] +#[derive(Clone, PartialEq)] +pub enum Key { + /// Up arrow key Up, + + /// Down arrow key Down, + + /// Right arrow key Right, + + /// Left arrow key Left, + + /// Tab key Tab, + + /// Shift + Tab key combo ShiftTab, + + /// Backspace key + Backspace, + + /// Return (or Enter) key + Newline, + + /// Ctrl + c key combo Ctrlc, - Else(u8), + + /// Ctrl + d key combo + Ctrld, + + /// An ASCII or UTF-8 character + Else(char), + + /// Key not recognized by [getchar] Ignored, } -/// Retrieve a single byte of input +/// Flush STDIN +macro_rules! flush { + () => { + io::stdout().lock().flush().unwrap(); + }; +} + +/// Read the next (single) byte of input from STDIN +macro_rules! getc { + ($buffer:expr) => { + match io::stdin().read_exact($buffer) { + Ok(_) => {} + Err(_) => return Key::Ignored, + } + }; +} + +/// Try to convert a UTF-8 byte sequence into a [std::char] +macro_rules! char { + ($extension:expr) => { + match String::from_utf8_lossy($extension).chars().next() { + Some(c) => Key::Else(c), + None => Key::Ignored, + } + }; +} + +/// Retrieve the next character from STDIN +/// +/// A UTF-8 compatible function used to get the next character from STDIN. A UTF-8 character may be +/// anywhere from 1 byte to 4 bytes. The function does not (yet) account for every single key (or +/// key combination) that a user might input. For any key (and/or combo) that is not recognized, +/// this returns [Key::Ignored], and is functionally treated as a no op. /// -/// Requires some setup beforehand (see beginning of repl()) +/// Please see [Key] for a list of keys and combos that are recognized. 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 - match b[0] { - // Escape sequences - 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, - - // Shift tab - 90 => return Key::ShiftTab, - - // Everything else - _ => return Key::Ignored, + flush!(); + + let mut header = [0; 1]; + let mut extension = Vec::new(); + getc!(&mut header); + extension.push(header[0]); + + if header[0] <= 0x7f { + match header[0] { + 3 => return Key::Ctrlc, + 4 => return Key::Ctrld, + 9 => return Key::Tab, + b'\n' => return Key::Newline, + 127 => return Key::Backspace, + _ => {} + } + + if header[0] == 27 { + getc!(&mut header); + match header[0] { + 91 => { + getc!(&mut header); + match header[0] { + 65 => return Key::Up, + 66 => return Key::Down, + 67 => return Key::Right, + 68 => return Key::Left, + 90 => return Key::ShiftTab, + _ => return Key::Ignored, + } } + _ => return Key::Ignored, } - - return Key::Ignored; } - // Tab - 9 => return Key::Tab, - - // ctrlc - 3 => return Key::Ctrlc, - - // Everything else - _ => Key::Else(b[0]), + char!(&extension) + } else if header[0] >= 0xc0 && header[0] <= 0xdf { + let mut continuation = [0; 1]; + getc!(&mut continuation); + extension.extend(continuation); + char!(&extension) + } else if header[0] >= 0xe0 && header[0] <= 0xef { + let mut continuation = [0; 2]; + getc!(&mut continuation); + extension.extend(continuation); + char!(&extension) + } else if header[0] >= 0xf0 && header[0] <= 0xf7 { + let mut continuation = [0; 3]; + getc!(&mut continuation); + extension.extend(continuation); + char!(&extension) + } else { + Key::Ignored } } -/// Handles autocomplete functionality for file paths +/// Typical (tab drivem) completion +/// +/// This function provides a primitive form of shell completion (tab autocomplete). Currently, it +/// only works with file paths, there is NO completion for commands, for instance. Also, completion +/// is only enabled if the cursor is at the end of the buffer. Otherwise, pressing Tab will have no +/// effect. /// -/// Currently, dwvsh does not implement zsh's full autocomplete -/// ecosystem (though there are plans to). For now, this simply adds a -/// builtin way to get autocomplete suggestions for file paths via the -/// <tab> key. -fn autocomplete( - buffer: &mut Arc<Mutex<Vec<u8>>>, - index: usize, - pwd: &PathBuf, -) -> Result<(String, usize), Box<dyn std::error::Error>> { - let buffer = buffer.lock().unwrap(); - let word = match buffer.last() { - Some(c) if *c == b' ' => "".to_string(), - None => "".to_string(), - _ => { - let mut word: Vec<u8> = vec![]; - for c in buffer.iter().rev() { - if *c == b' ' || *c == b'/' { +/// This function cycles through possible completion paths, keeping track of the position, present +/// working directory, etc. +fn comp( + buffer: &mut Vec<char>, + bpos: &mut usize, + pos: &mut usize, + len: &mut usize, + pwd: &mut PathBuf, + last_key: Key, + reverse: bool, +) -> String { + // Reset the buffer position + if *bpos >= *len { + *bpos -= *len; + } + + // Remove the last autocomplete value from the buffer + while *len > 0 { + buffer.pop(); + print!("\u{8} \u{8}"); + *len -= 1; + } + + // Reverse the buffer, which will make our while loop further down much easier + let mut rev = buffer.iter().rev(); + + // Holds the last word (i.e. anything after the last space (' ') or forward slash ('/')) + let mut word = Vec::new(); + while let Some(c) = rev.next() { + match rev.clone().peekable().peek() { + Some(next) if **next == '\\' => { + word.push(*c); + } + Some(_) => { + if *c == ' ' || *c == '/' { break; } word.push(*c); } - word.reverse(); - String::from_utf8_lossy(&mut word).to_string() + None => { + word.push(*c); + break; + } } + } + + // Collect the word into a String + let word = word + .iter() + .rev() + .filter(|c| **c != '\\') + .collect::<String>(); + + // Get a file listing, filtering for the word, if it is not empty + let paths = match fs::read_dir(pwd) { + Ok(paths) => paths, + Err(_) => return String::new(), }; - // Get a file listing - let paths = fs::read_dir(&pwd)?; let paths = if word.is_empty() { paths .into_iter() @@ -124,292 +233,365 @@ fn autocomplete( .collect::<Vec<_>>() }; - // Return nothing is paths is empty + // Return nothing if there are not matches if paths.is_empty() { - return Ok(("".to_string(), 0)); + return String::new(); } - // Collect path into DirEntries + // Collect path into DirEntry(s) let mut paths = paths .iter() .map(|path| path.as_ref().unwrap()) .collect::<Vec<_>>(); - // Sort the paths + // Sort the entries in alphabetical order paths.sort_by(|a, b| { a.file_name() .to_ascii_lowercase() .cmp(&b.file_name().to_ascii_lowercase()) }); - // Output the file listing at index on the prompt - // let path = paths[index].path(); - let path = paths[index].path(); + // Do extra maths on the position if switching autocomplete directions + if last_key == Key::Tab && reverse { + let ori_pos = *pos; + if *pos == 0 || *pos == 1 { + *pos = paths.len(); + } - let path = if path.is_dir() { - path.file_name().unwrap().to_str().unwrap().to_string() + "/" - } else { - path.file_name().unwrap().to_str().unwrap().to_string() - }; + if ori_pos == 1 { + *pos -= 1; + } else { + *pos -= 2; + } + } + + // Do extra maths on the position if switching autocomplete directions + if last_key == Key::ShiftTab && !reverse { + if *pos == 0 { + *pos = paths.len(); + } + *pos += 2; + } + + // Reset the position if it's larger than the number of entries + if *pos >= paths.len() { + *pos = *pos - paths.len(); - let mut path = if word.is_empty() { - path + // Need to double-check the newly computed pos + if *pos >= paths.len() { + *pos = 0; + } + } + + // Get the path (or only part of the path if we matched with word) + let path = paths[*pos].path(); + let mut path = if paths[*pos].path().is_dir() { + (path.file_name().unwrap().to_string_lossy()[word.len()..].to_string() + "/").to_string() } else { - path[word.len()..].to_string() + path.file_name().unwrap().to_string_lossy()[word.len()..].to_string() }; - let pause_positions = path - .chars() - .enumerate() - .filter(|(_, c)| *c == ' ') - .map(|(i, _)| i) - .collect::<Vec<_>>(); - for pos in pause_positions { - path.insert(pos, '\\'); + // Reset from previous autocomplete + (0..*len).for_each(|_| print!("\u{8}")); + (0..*len).for_each(|_| print!(" ")); + (0..*len).for_each(|_| print!("\u{8}")); + + let mut j = 0; + let mut chars = path.chars().collect::<Vec<char>>(); + for (i, c) in chars.clone().iter().enumerate() { + if *c == ' ' { + chars.insert(i + j, '\\'); + j += 1; + } } + let path = chars.iter().collect::<String>(); + + // Print out the path print!("{}", path); - Ok((path, paths.len())) + // Update the buffer + buffer.append(&mut path.chars().collect::<Vec<_>>()); + + // Math the position + if reverse { + if *pos == 0 { + *pos = paths.len(); + } + *pos -= 1; + } else { + *pos += 1; + } + + // Update the length of the last comp + *len = UnicodeWidthStr::width(path.as_str()); + + // Update the buffer position + *bpos += *len; + + path } -/// 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 { - // Keep track of the last key - let mut last: Option<Key> = None; - - // Keep track of index for autocomplete - let mut pwd = current_dir().unwrap_or(PathBuf::from(env!("HOME"))); - let mut auindex = 0; - let mut aulen = 0; - - // Keep track of the length of the last buffer from autcomplete() - let mut length = 0; - - // Loop over characters until there is a newline +pub fn getline( + buffer: &mut Arc<Mutex<Vec<char>>>, + pos: &mut Arc<Mutex<usize>>, + comp_pos: &mut Arc<Mutex<usize>>, + comp_len: &mut Arc<Mutex<usize>>, + last_key: &mut Arc<Mutex<Key>>, +) -> usize { + // Position in the buffer. Subject to change based on input from the user. Typing a character + // increments the position, while backspacing will decrement it. The user may also move it + // manually using the arrow keys to insert or delete at an arbitrary location in the buffer. + // let mut pos = 0; + + // Present working directory. Keeps track of the user's PWD for autocomplete. + let mut pwd = current_dir().unwrap_or(PathBuf::from(".")); + + // Position in the autocomplete list. This value gets reset if a new autocomplete list is + // generated (for instance, if the user presses '/' to start autocomplete in a new directory). + // let mut comp_pos = 0; + + // Keep track of the length of the last buffer from comp(). + // let mut comp_len = 0; + + // Keep track of the last key for autocomplete, as we may need to add or sub additionally from + // comp_pos before calling comp(). + // let mut last_key = Key::Ignored; + + // Always clear our state variables before proceeding + buffer.lock().unwrap().clear(); + *pos.lock().unwrap() = 0; + *comp_pos.lock().unwrap() = 0; + *comp_len.lock().unwrap() = 0; + *last_key.lock().unwrap() = Key::Ignored; + + // Receive input loop { - let c = getchar(); - match c { - Key::Up => { - continue; - } - Key::Down => { - continue; + match getchar() { + Key::Tab => { + if *pos.lock().unwrap() != buffer.lock().unwrap().len() { + continue; + } + + comp( + &mut buffer.lock().unwrap(), + &mut pos.lock().unwrap(), + &mut comp_pos.lock().unwrap(), + &mut comp_len.lock().unwrap(), + &mut pwd, + last_key.lock().unwrap().clone(), + false, + ); + *last_key.lock().unwrap() = Key::Tab; } - Key::Right => { - if *pos.lock().unwrap() >= buffer.lock().unwrap().len() { + Key::ShiftTab => { + if *pos.lock().unwrap() != buffer.lock().unwrap().len() { continue; } - print!("\x1b[1C"); - *pos.lock().unwrap() += 1; + + comp( + &mut buffer.lock().unwrap(), + &mut pos.lock().unwrap(), + &mut comp_pos.lock().unwrap(), + &mut comp_len.lock().unwrap(), + &mut pwd, + last_key.lock().unwrap().clone(), + true, + ); + *last_key.lock().unwrap() = Key::ShiftTab; } - Key::Left => { + Key::Ctrlc => kill(Pid::from_raw(0 as i32), Signal::SIGINT).unwrap(), + Key::Ctrld => return 0, + Key::Backspace => { if *pos.lock().unwrap() == 0 { continue; } - print!("\u{8}"); + *pos.lock().unwrap() -= 1; - } - Key::Tab => { - if last == Some(Key::ShiftTab) { - auindex += 2; - if auindex >= length { - auindex = 0; - } - } - while aulen > 0 { - buffer.lock().unwrap().pop(); - print!("\u{8} \u{8}"); - *pos.lock().unwrap() -= 1; - aulen -= 1; - } - let (path, len) = - autocomplete(buffer, auindex, &pwd).unwrap_or(("".to_string(), 0)); - length = len; - for c in path.into_bytes().iter() { - buffer.lock().unwrap().insert(*pos.lock().unwrap(), *c); - *pos.lock().unwrap() += 1; - aulen += 1; - } - auindex += 1; - if auindex >= len { - auindex = 0; + let trunc = &buffer.lock().unwrap()[*pos.lock().unwrap()..] + .iter() + .collect::<String>(); + let trunc_width = UnicodeWidthStr::width(trunc.as_str()); + let c = buffer.lock().unwrap().remove(*pos.lock().unwrap()); + let width = UnicodeWidthChar::width(c).unwrap_or(1); + + if *pos.lock().unwrap() == buffer.lock().unwrap().len() { + (0..width).for_each(|_| print!("\u{8}")); + print!(" \u{8}"); + } else { + (0..trunc_width).for_each(|_| print!(" ")); + (0..trunc_width + width).for_each(|_| print!("\u{8}")); + buffer.lock().unwrap()[*pos.lock().unwrap()..] + .iter() + .for_each(|c| print!("{}", c)); + (0..trunc_width - width).for_each(|_| print!("\u{8}")); } - } - Key::ShiftTab => { - if last == Some(Key::Tab) { - if auindex.checked_sub(2) == None { - auindex = length - 1; - } else { - auindex -= 2; + + // Update directory for autocomplete + let buffer = buffer.lock().unwrap(); + let comp_path = match buffer.last() { + Some(c) if *c == ' ' => String::new(), + None => String::new(), + _ => { + let mut path = Vec::new(); + for c in buffer.iter().rev() { + if *c == ' ' { + break; + } + path.push(*c); + } + + let mut path = path.iter().rev().collect::<String>(); + if path.starts_with("..") && path.chars().filter(|c| *c == '/').count() == 1 + { + path = String::from(".."); + } + + loop { + match path.chars().last() { + Some(c) if c == '/' || c == '.' || c == '~' => { + break; + } + Some(_) => { + path.pop(); + } + None => { + break; + } + } + } + + path } + }; + + // Reset comp variables + pwd = if comp_path.is_empty() { + current_dir().unwrap_or(PathBuf::from(".")) + } else if comp_path.starts_with("~") { + PathBuf::from(format!("{}{}", env!("HOME"), &comp_path[1..]).to_string()) + } else { + PathBuf::from(comp_path) + }; + *comp_pos.lock().unwrap() = 0; + let mut comp_len = comp_len.lock().unwrap(); + if *comp_len > 0 { + *comp_len -= 1; } - while aulen > 0 { - buffer.lock().unwrap().pop(); - print!("\u{8} \u{8}"); - *pos.lock().unwrap() -= 1; - aulen -= 1; + } + Key::Up => continue, + Key::Down => continue, + Key::Right => { + if *pos.lock().unwrap() >= buffer.lock().unwrap().len() { + continue; } - let (path, len) = - autocomplete(buffer, auindex, &pwd).unwrap_or(("".to_string(), 0)); - length = len; - for c in path.into_bytes().iter() { - buffer.lock().unwrap().insert(*pos.lock().unwrap(), *c); - *pos.lock().unwrap() += 1; - aulen += 1; + + let width = UnicodeWidthChar::width(buffer.lock().unwrap()[*pos.lock().unwrap()]) + .unwrap_or(1); + for _ in 0..width { + print!("\x1b[1C"); } - auindex = if auindex == 0 { len - 1 } else { auindex - 1 }; - } - Key::Ctrlc => { - kill(Pid::from_raw(0 as i32), Signal::SIGINT).unwrap(); - } - Key::Ignored => { - continue; + + *pos.lock().unwrap() += 1; } - Key::Else(c) => match c { - // enter/return - b'\n' => break, - - // tab - b'\t' => { - *pos.lock().unwrap() += 1; - print!(" "); - buffer.lock().unwrap().push(b' '); + Key::Left => { + if *pos.lock().unwrap() == 0 { + continue; } - // 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}"); - } + *pos.lock().unwrap() -= 1; + let width = UnicodeWidthChar::width(buffer.lock().unwrap()[*pos.lock().unwrap()]) + .unwrap_or(1); + for _ in 0..width { + print!("\u{8}"); + } + } + Key::Ignored => continue, + Key::Newline => break, + Key::Else(c) => { + let mut buffer = buffer.lock().unwrap(); + match buffer.last() { + Some(last) if *last == '/' && c == '/' => { + buffer.pop(); + *pos.lock().unwrap() -= 1; + print!("\u{8} \u{8}") } - - // Reset autocomplete variables - auindex = 0; - aulen = 0; + _ => {} } - // everything else - _ => { - let mut buffer = buffer.lock().unwrap(); + let trunc = &buffer[*pos.lock().unwrap()..].iter().collect::<String>(); + let trunc_width = UnicodeWidthStr::width(trunc.as_str()); + buffer.insert(*pos.lock().unwrap(), c); + *pos.lock().unwrap() += 1; + if *pos.lock().unwrap() == buffer.len() { + print!("{}", c); + } else { + (0..trunc_width).for_each(|_| print!(" ")); + (0..trunc_width).for_each(|_| print!("\u{8}")); + buffer[*pos.lock().unwrap() - 1..] + .iter() + .for_each(|c| print!("{}", c)); + (0..trunc_width).for_each(|_| print!("\u{8}")); + } - // Print out the character as the user is typing - match buffer.last() { - Some(last) if *last == b'/' && c == b'/' => { - buffer.pop(); - *pos.lock().unwrap() -= 1; + // Update directory for autocomplete + let comp_path = match buffer.last() { + Some(c) if *c == ' ' => String::new(), + None => String::new(), + _ => { + let mut path = Vec::new(); + for c in buffer.iter().rev() { + if *c == ' ' { + break; + } + path.push(*c); } - Some(_) => print!("{}", c as char), - None => print!("{}", c as char), - } - // Insert the character onto the buffer at whatever *pos.lock().unwrap()ition - // the cursor is at - buffer.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.len() { - print!( - "{}", - String::from_utf8_lossy(&buffer[*pos.lock().unwrap()..]) - ); - for _ in *pos.lock().unwrap()..buffer.len() { - print!("\u{8}"); + let mut path = path.iter().rev().collect::<String>(); + if path.starts_with("..") && path.chars().filter(|c| *c == '/').count() == 1 + { + path = String::from(".."); } - } - // Update directory for autocomplete - let word = match buffer.last() { - Some(c) if *c == b' ' => "".to_string(), - None => "".to_string(), - _ => { - let mut word: Vec<u8> = vec![]; - for c in buffer.iter().rev() { - if *c == b' ' { + loop { + match path.chars().last() { + Some(c) if c == '/' || c == '.' || c == '~' => { break; } - word.push(*c); - } - word.reverse(); - if word.starts_with(b"..") - && word.iter().filter(|c| *c == &b'/').count() == 1 - { - word = vec![b'.', b'.'] - } - loop { - match word.last() { - Some(c) if *c == b'/' || *c == b'.' || *c == b'~' => { - break; - } - Some(_) => { - word.pop(); - } - None => { - break; - } + Some(_) => { + path.pop(); + } + None => { + break; } } - String::from_utf8_lossy(&mut word).to_string() } - }; - - // Check for the ~ character (used to represent $HOME) - let word = if word.is_empty() { - current_dir() - .unwrap_or(PathBuf::from(env!("HOME"))) - .to_string_lossy() - .to_string() - } else if word.starts_with("~") { - let home = env!("HOME"); - format!("{}{}", home, &word[1..]).to_string() - } else { - word - }; - - // Reset autocomplete variables - pwd = PathBuf::from(word); - auindex = 0; - aulen = 0; - } - }, - } - // Update the last key - last = Some(c); + path + } + }; + + // Reset comp variables + pwd = if comp_path.is_empty() { + current_dir().unwrap_or(PathBuf::from(".")) + } else if comp_path.starts_with("~") { + PathBuf::from(format!("{}{}", env!("HOME"), &comp_path[1..]).to_string()) + } else { + PathBuf::from(comp_path) + }; + *comp_pos.lock().unwrap() = 0; + *comp_len.lock().unwrap() = 0; + } + }; } - *pos.lock().unwrap() = 0; println!(); - buffer.lock().unwrap().push(b'\n'); - buffer.lock().unwrap().len() + + let mut bytes = 0; + buffer + .lock() + .unwrap() + .iter() + .for_each(|c| bytes += c.len_utf8()); + bytes } diff --git a/src/main.rs b/src/main.rs index 8cfd680..a73c2c4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,7 @@ mod path; mod poem; use poem::{read::Readable, recite::Reciteable, Poem}; mod compose; -use buffer::{getline, STDIN}; +use buffer::{getline, Key, STDIN}; use compose::Environment; use termios::{tcsetattr, Termios, ECHO, ECHOE, ICANON, TCSANOW}; @@ -27,8 +27,11 @@ use termios::{tcsetattr, Termios, ECHO, ECHOE, ICANON, TCSANOW}; /// ``` fn repl( away: &mut Arc<Mutex<bool>>, - buffer: &mut Arc<Mutex<Vec<u8>>>, + buffer: &mut Arc<Mutex<Vec<char>>>, pos: &mut Arc<Mutex<usize>>, + comp_pos: &mut Arc<Mutex<usize>>, + comp_len: &mut Arc<Mutex<usize>>, + last_key: &mut Arc<Mutex<Key>>, env: &mut Environment, ) { // Setup termios flags @@ -39,9 +42,6 @@ fn repl( // Main shell loop loop { - // Clear the buffer - buffer.lock().unwrap().clear(); - // Get the prompt let prompt = match env::var("PS1") { Ok(val) => val, @@ -60,7 +60,7 @@ fn repl( tcsetattr(STDIN, TCSANOW, &mut termios).unwrap(); // Wait for user input - let bytes = getline(buffer, pos); + let bytes = getline(buffer, pos, comp_pos, comp_len, last_key); // Check if we've reached EOF (i.e. <C-d>) if bytes == 0 { @@ -69,9 +69,7 @@ fn repl( } // Convert buffer to a string and trim it - let poetry = String::from_utf8_lossy(&buffer.lock().unwrap()) - .trim() - .to_string(); + let poetry = buffer.lock().unwrap().iter().collect::<String>(); // Skip parsing if there is no poetry if poetry.is_empty() { @@ -86,7 +84,7 @@ fn repl( *away.lock().unwrap() = true; // Parse the poem - let poem = Poem::read(poetry, env); + let poem = Poem::read(poetry.to_string(), env); let poem = match poem { Ok(poem) => poem, Err(e) => { @@ -151,17 +149,43 @@ fn main() { // Compose the environment for dwvsh let mut env = compose::env(); - // Handle signals + // Set when we are not on the buffer let mut away = Arc::new(Mutex::new(true)); - let mut buffer: Arc<Mutex<Vec<u8>>> = Arc::new(Mutex::new(vec![])); + + // Any text in the current buffer + let mut buffer: Arc<Mutex<Vec<char>>> = Arc::new(Mutex::new(Vec::new())); + + // Position in the buffer. Subject to change based on input from the user. Typing a character + // increments the position, while backspacing will decrement it. The user may also move it + // manually using the arrow keys to insert or delete at an arbitrary location in the buffer. let mut pos: Arc<Mutex<usize>> = Arc::new(Mutex::new(0)); + + // Position in the autocomplete list. This value gets reset if a new autocomplete list is + // generated (for instance, if the user preses '/' to start autocomplete in a new directory). + let mut comp_pos: Arc<Mutex<usize>> = Arc::new(Mutex::new(0)); + + // Keep track of the length of the last buffer from [crate::buffer::comp]. + let mut comp_len: Arc<Mutex<usize>> = Arc::new(Mutex::new(0)); + + // Keep track of the last key for autocomplete, as we may need to add or sub additionally from + // [comp_pos] before calling [crate::buffer::comp] (i.e. swapping directions (tab vs. shift + + // tab)). + let mut last_key: Arc<Mutex<Key>> = Arc::new(Mutex::new(Key::Ignored)); + + // Handle signals unsafe { let away = Arc::clone(&away); let buffer = Arc::clone(&buffer); let pos = Arc::clone(&pos); + let comp_pos = Arc::clone(&comp_pos); + let comp_len = Arc::clone(&comp_len); + let last_key = Arc::clone(&last_key); signal_hook::low_level::register(signal_hook::consts::SIGINT, move || { buffer.lock().unwrap().clear(); *pos.lock().unwrap() = 0; + *comp_pos.lock().unwrap() = 0; + *comp_len.lock().unwrap() = 0; + *last_key.lock().unwrap() = Key::Ignored; if *away.lock().unwrap() { println!(); } else { @@ -180,5 +204,13 @@ fn main() { options(&mut env); // Begin evaluating commands - repl(&mut away, &mut buffer, &mut pos, &mut env); + repl( + &mut away, + &mut buffer, + &mut pos, + &mut comp_pos, + &mut comp_len, + &mut last_key, + &mut env, + ); } diff --git a/src/poem/read.rs b/src/poem/read.rs index baf6a1d..8b83e58 100644 --- a/src/poem/read.rs +++ b/src/poem/read.rs @@ -357,7 +357,16 @@ impl Readable for Poem { channel = Some(Rune::Notes); } verse.add(&mut word, channel); - channel = Some(rune); + if last != Rune::Read + && last != Rune::Write + && last != Rune::Write2 + && last != Rune::WriteAll + && last != Rune::Addendum + && last != Rune::Addendum2 + && last != Rune::AddendumAll + { + channel = Some(rune); + } } Rune::Special => { @@ -383,15 +392,6 @@ impl Readable for Poem { poem!(chars, j, i, c, verse, word, env); } - // Indicates an environment variable to fork with, - // if the verse's stanza is empty so far - // Rune::Environment => { - // word.push(c); - // if verse.is_empty() { - // channel = Some(rune); - // } - // } - // Indicates a file operation (<, >, or >>) Rune::Read | Rune::Write @@ -402,7 +402,6 @@ impl Readable for Poem { | Rune::AddendumAll => { channel = Some(rune); verse.add(&mut word, channel); - // channel = Some(rune); verse.io.push(rune); } |