#![doc = include_str!("../README.md")]
#![deny(missing_docs)]
#![warn(clippy::all, clippy::nursery, clippy::pedantic, clippy::cargo)]
mod errors;
mod types;
pub use errors::{CompletersError, ShellCodeError};
use std::{env, fmt::Display, process::exit};
pub use types::CompletionType;
pub fn handle_completion<F, I>(handler: F)
where
F: FnOnce(Completion) -> I,
I: IntoIterator,
I::Item: Display,
{
match Completion::init() {
Ok(Some(completion)) => {
let candidates = handler(completion);
Completion::complete(candidates);
}
Ok(None) => {
}
Err(e) => {
eprintln!("Error: {e}");
exit(1);
}
}
}
#[derive(Debug, Clone)]
pub struct Completion {
pub words: Vec<String>,
pub word_index: usize,
pub line: String,
pub cursor_index: usize,
pub completion_type: CompletionType,
pub key: char,
}
impl Completion {
pub fn init() -> Result<Option<Self>, CompletersError> {
let Ok(complete) = env::var("COMPLETE") else {
return Ok(None);
};
match complete.as_str() {
"" | "0" => Ok(None),
"1" => Ok(Some(Self::from_args(env::args().skip(1).collect())?)),
"bash" => {
println!("{}", Self::generate_bash()?);
exit(0);
}
"nu" | "nushell" => {
println!("{}", Self::generate_nu()?);
exit(0);
}
_ => Err(CompletersError::UnrecognizedEnvVar(complete)),
}
}
pub fn from_args(mut args: Vec<String>) -> Result<Self, CompletersError> {
use CompletersError::InvalidValue;
if args.len() < 5 {
return Err(CompletersError::MissingField);
}
let words = args.split_off(5);
let positional: [String; 5] = args.try_into().map_err(|_| CompletersError::MissingField)?; let [word_index, line, cursor_index, completion_type, key] = positional;
let word_index = word_index.parse::<usize>().map_err(|e| InvalidValue {
field: "word_index".to_string(),
value: word_index,
what: e.to_string(),
})?;
let cursor_index = cursor_index.parse::<usize>().map_err(|e| InvalidValue {
field: "cursor_index".to_string(),
value: cursor_index,
what: e.to_string(),
})?;
let completion_type = completion_type.parse::<u8>().map_err(|e| InvalidValue {
field: "completion_type".to_string(),
value: completion_type,
what: e.to_string(),
})?;
let completion_type = completion_type.try_into().map_err(|()| InvalidValue {
field: "completion_type".to_string(),
value: completion_type.to_string(),
what: "Cannot interpret completion type".to_string(),
})?;
let key = key
.parse::<u8>()
.map_err(|e| InvalidValue {
field: "key".to_string(),
value: key,
what: e.to_string(),
})?
.into();
Ok(Self {
words,
word_index,
line,
cursor_index,
completion_type,
key,
})
}
pub fn complete<I>(candidates: I)
where
I: IntoIterator,
I::Item: Display,
{
println!("COMPLETERS_COMPLETE");
for candidate in candidates {
println!("{candidate}");
}
exit(0);
}
pub fn delegate(self) {
println!("COMPLETERS_DELEGATE");
let Self {
word_index,
line,
cursor_index,
completion_type,
key,
words,
} = self;
println!("{word_index}");
println!("{line}");
println!("{cursor_index}");
println!("{completion_type}");
println!("{key}");
for word in words {
println!("{word}");
}
exit(0);
}
pub fn empty() {
Self::complete::<[&str; 0]>([]);
}
fn get_name_path() -> Result<(String, String), ShellCodeError> {
let path = env::current_exe()?;
let name = path
.file_name()
.and_then(|name| name.to_str())
.ok_or_else(|| ShellCodeError::Encoding("Failed to decode program name".to_string()))?;
if !is_safe(name) {
return Err(ShellCodeError::Encoding(
"Program name contains unsafe characters".to_string(),
));
}
let path = path
.to_str()
.ok_or_else(|| ShellCodeError::Encoding("Failed to decode program path".to_string()))?;
if !is_safe(path) {
return Err(ShellCodeError::Encoding(
"Program path contains unsafe characters".to_string(),
));
}
Ok((name.to_string(), path.to_string()))
}
pub fn generate_bash() -> Result<String, ShellCodeError> {
let (name, path) = Self::get_name_path()?;
Ok(format!(
include_str!("./templates/bash.tmpl"),
name = name,
path = path
))
}
pub fn generate_nu() -> Result<String, ShellCodeError> {
let (name, path) = Self::get_name_path()?;
Ok(format!(
include_str!("./templates/nu.tmpl"),
name = name,
path = path
))
}
}
fn is_safe(s: &str) -> bool {
!s.is_empty()
&& s.chars()
.all(|c| c.is_alphanumeric() || "_-./\\".contains(c))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_completion_from_args() {
let args = vec![
"1".to_string(), "my_command s".to_string(), "12".to_string(), "33".to_string(), "9".to_string(), "my_command".to_string(), "s".to_string(), ];
let completion = Completion::from_args(args).unwrap();
assert_eq!(completion.words, vec!["my_command", "s"]);
assert_eq!(completion.word_index, 1);
assert_eq!(completion.line, "my_command s");
assert_eq!(completion.cursor_index, 12);
assert_eq!(completion.completion_type, CompletionType::ListAlternatives);
assert_eq!(completion.key, '\t');
}
}