summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock7
-rw-r--r--Cargo.toml1
-rw-r--r--src/buffer.rs768
-rw-r--r--src/main.rs58
-rw-r--r--src/poem/read.rs21
5 files changed, 538 insertions, 317 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 5f2722c..c4ff2f6 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"
diff --git a/Cargo.toml b/Cargo.toml
index 3aed5a3..b5abfb9 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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);
}