use nix::sys::signal::{kill, Signal}; use nix::unistd::Pid; use std::env::current_dir; use std::fs; use std::io::{self, Read, Write}; use std::path::PathBuf; use std::sync::{Arc, Mutex}; // STDIN is file descriptor (fd) 0 on Linux and other UN*X-likes pub const STDIN: i32 = 0; // Key input types from the user #[derive(PartialEq)] enum Key { Up, Down, Right, Left, Tab, ShiftTab, Ctrlc, Else(u8), Ignored, } /// Retrieve a single byte of input /// /// Requires some setup beforehand (see beginning of repl()) fn getchar() -> Key { let mut b = [0; 1]; io::stdout().lock().flush().unwrap(); io::stdin().read_exact(&mut b).unwrap(); // Might me an ASNI escape sequence 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, } } return Key::Ignored; } // Tab 9 => return Key::Tab, // ctrlc 3 => return Key::Ctrlc, // Everything else _ => Key::Else(b[0]), } } /// Handles autocomplete functionality for file paths /// /// 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 /// key. fn autocomplete( buffer: &mut Arc>>, index: usize, pwd: &PathBuf, ) -> Result<(String, usize), Box> { let buffer = buffer.lock().unwrap(); let word = match buffer.last() { Some(c) if *c == b' ' => "".to_string(), None => "".to_string(), _ => { let mut word: Vec = vec![]; for c in buffer.iter().rev() { if *c == b' ' || *c == b'/' { break; } word.push(*c); } word.reverse(); String::from_utf8_lossy(&mut word).to_string() } }; // Get a file listing let paths = fs::read_dir(&pwd)?; let paths = if word.is_empty() { paths .into_iter() .filter(|path| { !path .as_ref() .unwrap() .file_name() .to_string_lossy() .starts_with(".") }) .collect::>() } else { paths .into_iter() .filter(|path| { path.as_ref() .unwrap() .file_name() .to_string_lossy() .starts_with(&word) }) .collect::>() }; // Return nothing is paths is empty if paths.is_empty() { return Ok(("".to_string(), 0)); } // Collect path into DirEntries let mut paths = paths .iter() .map(|path| path.as_ref().unwrap()) .collect::>(); // Sort the paths 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(); 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() }; let path = if word.is_empty() { path } else { path[word.len()..].to_string() }; print!("{}", path); Ok((path, paths.len())) } /// Handle user input at the repl prompt /// /// This is required instead of io::stdin().read_line(), because certain /// keys like `` and `` 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>>, pos: &mut Arc>) -> usize { // Keep track of the last key let mut last: Option = 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 loop { let c = getchar(); match c { Key::Up => { continue; } Key::Down => { continue; } Key::Right => { if *pos.lock().unwrap() >= buffer.lock().unwrap().len() { continue; } print!("\x1b[1C"); *pos.lock().unwrap() += 1; } Key::Left => { if *pos.lock().unwrap() == 0 { continue; } print!("\u{8}"); *pos.lock().unwrap() -= 1; } Key::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; } } Key::ShiftTab => { if last == Some(Key::Tab) { if auindex.checked_sub(2) == None { auindex = length - 1; } else { auindex -= 2; } } 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 = if auindex == 0 { len - 1 } else { auindex - 1 }; } Key::Ctrlc => { kill(Pid::from_raw(0 as i32), Signal::SIGINT).unwrap(); } Key::Ignored => { continue; } Key::Else(c) => match c { // enter/return b'\n' => break, // tab b'\t' => { *pos.lock().unwrap() += 1; print!(" "); buffer.lock().unwrap().push(b' '); } // ctrl-d 4 => return 0, // backspace 127 => { if *pos.lock().unwrap() == 0 { continue; } *pos.lock().unwrap() -= 1; if *pos.lock().unwrap() == buffer.lock().unwrap().len() { buffer.lock().unwrap().pop(); print!("\u{8} \u{8}"); } else { buffer.lock().unwrap().remove(*pos.lock().unwrap()); print!( "\u{8}{} ", String::from_utf8_lossy( &buffer.lock().unwrap()[*pos.lock().unwrap()..] ) ); for _ in *pos.lock().unwrap()..buffer.lock().unwrap().len() + 1 { print!("\u{8}"); } } // Reset autocomplete variables auindex = 0; aulen = 0; } // everything else _ => { let mut buffer = buffer.lock().unwrap(); // 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; } 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}"); } } // Update directory for autocomplete let word = match buffer.last() { Some(c) if *c == b' ' => "".to_string(), None => "".to_string(), _ => { let mut word: Vec = vec![]; for c in buffer.iter().rev() { if *c == b' ' { 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; } } } 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); } *pos.lock().unwrap() = 0; println!(); buffer.lock().unwrap().push(b'\n'); buffer.lock().unwrap().len() }