[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[PATCH] Rewrite of the buffer code + proper UTF-8 support


This patch rewrites much of the code in src/buffer.rs to (utlimately),
be less buggy. It also changed getchar() to have proper support for
UTF-8 characters. The autocomplete function was also enhanced to support
completions with filenames that have spaces in their paths. It handles
these by placing a backslash ('\') before each space in the filename.
There is not yet any completion support with quote ('), nor double-quote
characters ("). The buffer is still navigable with arrow keys, so
arbitrary deletions and insertions are still possible. Deletions and
insertions with multi-width UTF-8 characters work as expected.
---
 Cargo.lock       |   7 +
 Cargo.toml       |   1 +
 src/buffer.rs    | 768 +++++++++++++++++++++++++++++------------------
 src/main.rs      |  58 +++-
 src/poem/read.rs |  21 +-
 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);
                 }
 
-- 
cheers!~
Rory




Archive administrator: postmaster AT dwarvish DOT org