init
This commit is contained in:
170
src/completer.rs
Normal file
170
src/completer.rs
Normal file
@@ -0,0 +1,170 @@
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use rustyline::completion::{Completer, Pair};
|
||||
use rustyline::highlight::Highlighter;
|
||||
use rustyline::hint::{Hinter, HistoryHinter};
|
||||
use rustyline::validate::Validator;
|
||||
use rustyline::Helper;
|
||||
use rustyline::Context;
|
||||
|
||||
pub struct ShellHelper {
|
||||
hinter: HistoryHinter,
|
||||
}
|
||||
|
||||
impl ShellHelper {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
hinter: HistoryHinter::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Helper for ShellHelper {}
|
||||
impl Validator for ShellHelper {}
|
||||
|
||||
impl Highlighter for ShellHelper {
|
||||
fn highlight_hint<'h>(&self, hint: &'h str) -> std::borrow::Cow<'h, str> {
|
||||
// Gray color for hints
|
||||
std::borrow::Cow::Owned(format!("\x1b[90m{}\x1b[0m", hint))
|
||||
}
|
||||
}
|
||||
|
||||
impl Hinter for ShellHelper {
|
||||
type Hint = String;
|
||||
|
||||
fn hint(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Option<String> {
|
||||
self.hinter.hint(line, pos, ctx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Completer for ShellHelper {
|
||||
type Candidate = Pair;
|
||||
|
||||
fn complete(
|
||||
&self,
|
||||
line: &str,
|
||||
pos: usize,
|
||||
_ctx: &Context<'_>,
|
||||
) -> rustyline::Result<(usize, Vec<Pair>)> {
|
||||
let (start, word) = extract_word(line, pos);
|
||||
|
||||
// If first word → command completion
|
||||
// Otherwise → file path completion
|
||||
let is_first_word = line[..start].trim().is_empty();
|
||||
|
||||
let candidates = if is_first_word {
|
||||
let mut results = complete_commands(word);
|
||||
// Also include files in current dir for ./script style
|
||||
results.extend(complete_path(word));
|
||||
results
|
||||
} else {
|
||||
complete_path(word)
|
||||
};
|
||||
|
||||
Ok((start, candidates))
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract the current word being completed
|
||||
fn extract_word(line: &str, pos: usize) -> (usize, &str) {
|
||||
let bytes = &line.as_bytes()[..pos];
|
||||
let start = bytes.iter().rposition(|&b| b == b' ').map(|i| i + 1).unwrap_or(0);
|
||||
(start, &line[start..pos])
|
||||
}
|
||||
|
||||
/// Complete commands from PATH
|
||||
fn complete_commands(prefix: &str) -> Vec<Pair> {
|
||||
if prefix.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let path_var = match env::var("PATH") {
|
||||
Ok(p) => p,
|
||||
Err(_) => return Vec::new(),
|
||||
};
|
||||
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
let mut results = Vec::new();
|
||||
|
||||
for dir in path_var.split(':') {
|
||||
let entries = match fs::read_dir(dir) {
|
||||
Ok(e) => e,
|
||||
Err(_) => continue,
|
||||
};
|
||||
for entry in entries.flatten() {
|
||||
let name = entry.file_name();
|
||||
let name = name.to_string_lossy();
|
||||
if name.starts_with(prefix) && !seen.contains(name.as_ref()) {
|
||||
seen.insert(name.to_string());
|
||||
results.push(Pair {
|
||||
display: name.to_string(),
|
||||
replacement: name.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results.sort_by(|a, b| a.display.cmp(&b.display));
|
||||
results
|
||||
}
|
||||
|
||||
/// Complete file/directory paths
|
||||
fn complete_path(prefix: &str) -> Vec<Pair> {
|
||||
let (dir, file_prefix) = if prefix.contains('/') {
|
||||
let p = Path::new(prefix);
|
||||
let dir = p.parent().unwrap_or(Path::new("."));
|
||||
let file = p.file_name().map(|f| f.to_string_lossy().to_string()).unwrap_or_default();
|
||||
// Expand ~
|
||||
let dir_str = dir.to_string_lossy();
|
||||
let expanded = if dir_str.starts_with('~') {
|
||||
if let Ok(home) = env::var("HOME") {
|
||||
Path::new(&dir_str.replacen('~', &home, 1)).to_path_buf()
|
||||
} else {
|
||||
dir.to_path_buf()
|
||||
}
|
||||
} else {
|
||||
dir.to_path_buf()
|
||||
};
|
||||
(expanded, file)
|
||||
} else {
|
||||
(std::env::current_dir().unwrap_or_default(), prefix.to_string())
|
||||
};
|
||||
|
||||
let entries = match fs::read_dir(&dir) {
|
||||
Ok(e) => e,
|
||||
Err(_) => return Vec::new(),
|
||||
};
|
||||
|
||||
let mut results = Vec::new();
|
||||
for entry in entries.flatten() {
|
||||
let name = entry.file_name();
|
||||
let name = name.to_string_lossy().to_string();
|
||||
if name.starts_with(&file_prefix) {
|
||||
let is_dir = entry.file_type().map(|t| t.is_dir()).unwrap_or(false);
|
||||
let replacement = if prefix.contains('/') {
|
||||
let parent = Path::new(prefix).parent().unwrap_or(Path::new(""));
|
||||
let mut r = format!("{}/{}", parent.display(), name);
|
||||
if is_dir {
|
||||
r.push('/');
|
||||
}
|
||||
r
|
||||
} else {
|
||||
let mut r = name.clone();
|
||||
if is_dir {
|
||||
r.push('/');
|
||||
}
|
||||
r
|
||||
};
|
||||
let display = if is_dir {
|
||||
format!("{}/", name)
|
||||
} else {
|
||||
name
|
||||
};
|
||||
results.push(Pair { display, replacement });
|
||||
}
|
||||
}
|
||||
|
||||
results.sort_by(|a, b| a.display.cmp(&b.display));
|
||||
results
|
||||
}
|
||||
Reference in New Issue
Block a user