use common_macros::hash_set;
use std::collections::HashSet;
use crate::libs::get_lib_completions;
#[cfg(unix)]
use std::ffi::OsStr;
#[cfg(unix)]
use std::path::Path;
#[cfg(unix)]
use std::path::PathBuf;
use std::path::is_separator;
use std::sync::OnceLock;
static PATH_COMMANDS: OnceLock<HashSet<String>> = OnceLock::new();
static LM_CMDS: OnceLock<HashSet<&'static str>> = OnceLock::new();
fn get_path_commands() -> &'static HashSet<String> {
PATH_COMMANDS.get_or_init(init_path_cmds)
}
fn get_lm_commands() -> &'static HashSet<&'static str> {
LM_CMDS.get_or_init(init_lm_cmds)
}
fn init_lm_cmds() -> HashSet<&'static str> {
let cmds: HashSet<&'static str> = hash_set! {
"let",
"fn",
"if",
"else {",
"match",
"while (",
"for i in",
"loop {\n",
"break",
"return",
"history",
"del",
"use",
};
cmds
}
pub fn is_valid_command(cmd: &str) -> bool {
get_path_commands().contains(cmd)
}
pub fn collect_command_with_prefix(prefix: &str) -> Vec<&str> {
if prefix.is_empty() || !prefix.is_ascii() {
return Vec::new();
}
let c1 = get_lm_commands()
.iter()
.filter(|x| x.starts_with(prefix))
.copied()
.collect::<Vec<_>>();
if c1.is_empty() {
match get_lib_completions(prefix) {
Some(lib) => return lib,
_ => {
return get_path_commands()
.iter()
.filter(|x| x.starts_with(prefix))
.map(|x| x.as_ref())
.collect::<Vec<_>>();
}
}
}
c1
}
#[cfg(unix)]
fn is_executable(path: &Path) -> bool {
use std::os::unix::fs::PermissionsExt;
path.metadata()
.map(|m| m.permissions().mode() & 0o111 != 0)
.unwrap_or(false)
}
#[cfg(unix)]
fn init_path_cmds() -> HashSet<String> {
let path_var = std::env::var("PATH").unwrap_or_default();
let path_separator = if cfg!(windows) { ";" } else { ":" };
path_var
.split(path_separator)
.flat_map(|dir| {
let dir_path = PathBuf::from(dir);
scan_path_cmds(&dir_path)
})
.collect()
}
#[cfg(windows)]
fn init_path_cmds() -> HashSet<String> {
HashSet::new()
}
#[cfg(unix)]
fn scan_path_cmds(dir: &Path) -> Vec<String> {
let mut commands = Vec::new();
if let Ok(entries) = dir.read_dir() {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
commands.extend(scan_path_cmds(&path));
} else if is_executable(&path)
&& let Some(stem) = path.file_stem().and_then(OsStr::to_str)
{
commands.push(stem.to_string());
}
}
}
commands
}
pub fn should_trigger_path_completion(line: &str, pos: usize) -> bool {
if let Some(_) = line[..pos].rfind(' ') {
return false;
}
if let Some(_) = line[..pos].find(is_separator) {
return true;
}
false
}
#[derive(Debug, PartialEq)]
pub enum LumeCompletionType {
Path,
Command,
Param,
None,
}
pub fn detect_completion_type(
line: &str,
pos: usize,
) -> (LumeCompletionType, usize) {
if line.is_empty() || pos == 0 {
return (LumeCompletionType::None, pos);
}
let prefix = &line[..pos];
if should_trigger_path_completion(line, pos) {
return (LumeCompletionType::Path, pos);
}
let command_pos = find_command_pos(prefix);
let command_section = &prefix[command_pos..];
if is_typing_command(command_section) {
return (LumeCompletionType::Command, command_pos);
}
if is_after_command_word(command_section) {
return check_privileged_command(command_section, "doas ", command_pos).unwrap_or(
check_privileged_command(command_section, "sudo ", command_pos).unwrap_or(
(LumeCompletionType::Param, command_pos),
),
);
}
(LumeCompletionType::None, pos)
}
fn check_privileged_command(
command_section: &str,
prefix: &str,
command_pos: usize,
) -> Option<(LumeCompletionType, usize)> {
if let Some(stripped_cmd) = command_section.strip_prefix(prefix) {
if is_after_command_word(stripped_cmd) {
return Some((LumeCompletionType::Param, command_pos + prefix.len()));
} else {
return Some((LumeCompletionType::Command, command_pos + prefix.len()));
}
}
None
}
pub fn find_command_pos(prefix: &str) -> usize {
let pos = prefix
.rfind([':', '>', '|', '&', '(', ';', '\n', '+'])
.map(|i| i + 1)
.unwrap_or(0);
prefix[pos..]
.find(|x: char| !char::is_ascii_whitespace(&x))
.map(|i| i + pos)
.unwrap_or(0)
}
fn is_typing_command(command_section: &str) -> bool {
!command_section.contains(' ')
}
fn is_after_command_word(command_section: &str) -> bool {
if let Some(space_pos) = command_section.find(' ') {
let after_space = &command_section[space_pos + 1..];
after_space.is_empty()
|| !after_space.contains(|c: char| matches!(&c, '|' | '&' | ')' | ';' | '\n'))
} else {
false
}
}