use nix::sys::stat::Mode; use radix_trie::{Trie, TrieCommon}; use rustyline::hint::{Hint, Hinter}; use rustyline::Context; use rustyline_derive::{Completer, Helper, Highlighter, Validator}; use std::ffi::OsString; use std::os::unix::fs::PermissionsExt; static EXE_MASK: Mode = Mode::from_bits_truncate(0o111); #[test] fn test_exe_mask() { let a = Mode::from_bits_truncate(0o100755); assert!(a.contains(EXE_MASK)); } #[derive(Completer, Helper, Validator, Highlighter)] pub struct CommandHinter { cmd_hints: Trie, path_hints: Trie, } #[derive(Hash, Debug, PartialEq, Eq)] pub struct CompleterHint { display: String, complete_up_to: usize, } impl Hint for CompleterHint { fn display(&self) -> &str { &self.display } fn completion(&self) -> Option<&str> { if self.complete_up_to > 0 { Some(&self.display[..self.complete_up_to]) } else { None } } } impl CompleterHint { fn new(text: &str, complete_up_to: &str) -> CompleterHint { assert!(text.starts_with(complete_up_to)); CompleterHint { display: text.into(), complete_up_to: complete_up_to.len(), } } fn from_path(text: OsString) -> CompleterHint { CompleterHint { complete_up_to: text.len(), display: text.into_string().unwrap(), } } fn suffix(&self, strip_chars: usize) -> CompleterHint { CompleterHint { display: self.display[strip_chars..].to_owned(), complete_up_to: self.complete_up_to.saturating_sub(strip_chars), } } } impl CommandHinter { pub fn new() -> Result { let mut cmd_hints = Trie::new(); let path_hints = Trie::new(); for path in std::env::var("PATH").unwrap_or_default().split(":") { #[cfg(debug_assertions)] println!("+index {}", path); if let Ok(entries) = std::fs::read_dir(path) { for x in entries { if let Ok(entry) = x { let mode = Mode::from_bits_truncate(entry.metadata()?.permissions().mode()); if mode.contains(EXE_MASK) { cmd_hints.insert( entry.file_name().into_string().unwrap(), CompleterHint::from_path(entry.file_name()), ); } } } } } Ok(CommandHinter { cmd_hints, path_hints, }) } pub fn set_cwd(&mut self, path: &std::path::PathBuf) { self.path_hints = Trie::new(); if let Ok(entries) = std::fs::read_dir(path) { for x in entries { if let Ok(entry) = x { self.path_hints.insert( entry.file_name().into_string().unwrap(), CompleterHint::from_path(entry.file_name()), ); } } } } } fn hints( trie: &Trie, line: &str, pos: usize, ) -> Option> { match trie.get_raw_descendant(line) { Some(subtrie) => Some( subtrie .values() .filter_map(|hint| { if pos > 0 && hint.display.starts_with(&line[..pos]) { Some(hint.suffix(pos)) } else { None } }) .collect(), ), None => None, } } impl Hinter for CommandHinter { type Hint = CompleterHint; /// Takes the currently edited `line` with the cursor `pos`ition and /// returns the string that should be displayed or `None` /// if no hint is available for the text the user currently typed. fn hint(&self, line: &str, pos: usize, _ctx: &Context<'_>) -> Option { if line.is_empty() { return None; } if pos < line.len() { return None; } if let Some(mut idx) = line.rfind(' ') { idx += 1; if pos <= idx { return None; } return hints(&self.path_hints, &line[idx..], pos - idx) .and_then(|mut l| l.pop_front()); } hints(&self.cmd_hints, line, pos).and_then(|mut l| l.pop_front()) } }