use crate::seed::{self, TerminalCommandVocabulary};
pub(super) fn shell_command_for_task(prompt: &str) -> Option<String> {
let vocab = seed::terminal_command_vocabulary();
named_shell_command(prompt, &vocab)
.or_else(|| asks_for_directory_listing(prompt).then(|| String::from("ls")))
}
fn named_shell_command(prompt: &str, vocab: &TerminalCommandVocabulary) -> Option<String> {
let lower = prompt.to_ascii_lowercase();
let has_phrase = vocab.terminal_phrases.iter().any(|p| lower.contains(p));
let has_cjk_verb = vocab.cjk_run_verbs.iter().any(|v| lower.contains(v));
let words: Vec<&str> = prompt
.split(|c: char| c.is_whitespace())
.filter(|w| !w.is_empty())
.collect();
let is_run_verb = |word: &str| {
let normalized = normalize_command_word(word);
vocab.run_verbs.iter().any(|v| v == &normalized)
};
let is_shell_token = |word: &str| {
let normalized = normalize_command_word(word);
!normalized.is_empty() && vocab.shell_tokens.iter().any(|t| t == &normalized)
};
let has_verb = words.iter().any(|w| is_run_verb(w)) || has_cjk_verb;
for (index, word) in words.iter().enumerate() {
if index == 0 || !is_shell_token(word) {
continue;
}
if is_run_verb(words[index - 1]) {
return Some(collect_command(&words[index..]));
}
}
if has_verb || has_phrase {
if let Some(word) = words.iter().find(|w| is_shell_token(w)) {
return Some(normalize_command_word(word));
}
}
None
}
fn collect_command(words: &[&str]) -> String {
let mut parts = vec![normalize_command_word(words[0])];
for word in &words[1..] {
if is_prose_word(word) {
break;
}
let trimmed = word.trim_matches(|c: char| c == '`' || c == ',' || c == '.');
if trimmed.is_empty() {
break;
}
parts.push(trimmed.to_owned());
}
parts.join(" ")
}
fn normalize_command_word(word: &str) -> String {
word.trim_matches('`')
.chars()
.take_while(|c| c.is_ascii_alphanumeric() || *c == '_' || *c == '-')
.collect::<String>()
.to_ascii_lowercase()
}
fn is_prose_word(word: &str) -> bool {
const PROSE_WORDS: &[&str] = &[
"command",
"commands",
"to",
"in",
"into",
"on",
"the",
"a",
"an",
"and",
"then",
"please",
"for",
"of",
"that",
"which",
"so",
"this",
"these",
"those",
"here",
"there",
"me",
"us",
"you",
"it",
"from",
"at",
"with",
"will",
"would",
"can",
"could",
"should",
"using",
"via",
"inside",
"within",
"output",
"result",
"results",
"contents",
"content",
"directory",
"folder",
"folders",
"file",
"files",
"currently",
"again",
"also",
"just",
"now",
];
let normalized = word
.trim_matches(|c: char| !c.is_ascii_alphanumeric())
.to_ascii_lowercase();
PROSE_WORDS.contains(&normalized.as_str())
}
fn asks_for_directory_listing(prompt: &str) -> bool {
let lower = prompt.to_ascii_lowercase();
let asks_for_listing = contains_any(
&lower,
&[
"list files",
"list the files",
"list all files",
"list local files",
"list of files",
"list of all files",
"list directory",
"list the directory",
"list the contents",
"listing of files",
"directory listing",
"directory contents",
"folder contents",
"contents of this directory",
"contents of the current directory",
"contents of this folder",
"contents of the current folder",
"show files",
"show the files",
"show me the files",
"see the files",
],
);
let asks_which_files = contains_any(
&lower,
&[
"what files",
"which files",
"files are in",
"files in the",
"files in this",
"files in current",
"files in the current",
"files exist",
"files are here",
"files are there",
],
);
let local_scope = contains_any(
&lower,
&[
"here",
"current directory",
"working directory",
"current working directory",
"this directory",
"the directory",
"current folder",
"this folder",
"the folder",
"local files",
],
);
(asks_for_listing || asks_which_files) && local_scope
}
fn contains_any(text: &str, phrases: &[&str]) -> bool {
phrases.iter().any(|phrase| text.contains(phrase))
}