use std::sync::OnceLock;
use memchr::memmem::Finder;
use regex::Regex;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Family {
GitStatus,
GitLog,
GitDiff,
NpmInstall,
CargoBuild,
Pytest,
Ls,
Grep,
Logs,
}
impl Family {
pub fn parse(name: &str) -> Option<Self> {
let n = name.trim().to_ascii_lowercase();
let family = match n.as_str() {
"git_status" | "git-status" | "gitstatus" => Family::GitStatus,
"git_log" | "git-log" | "gitlog" => Family::GitLog,
"git_diff" | "git-diff" | "gitdiff" | "diff" => Family::GitDiff,
"npm_install" | "npm-install" | "npm" | "pip" | "pip_install" | "install" => {
Family::NpmInstall
}
"cargo_build" | "cargo-build" | "cargo" | "build" => Family::CargoBuild,
"pytest" | "test" | "tests" | "jest" => Family::Pytest,
"ls" | "find" | "listing" => Family::Ls,
"grep" | "rg" | "ripgrep" | "search" => Family::Grep,
"logs" | "log" | "generic" => Family::Logs,
_ => return None,
};
Some(family)
}
pub fn as_str(self) -> &'static str {
match self {
Family::GitStatus => "git_status",
Family::GitLog => "git_log",
Family::GitDiff => "git_diff",
Family::NpmInstall => "npm_install",
Family::CargoBuild => "cargo_build",
Family::Pytest => "pytest",
Family::Ls => "ls",
Family::Grep => "grep",
Family::Logs => "logs",
}
}
}
fn finder(needle: &'static str) -> Finder<'static> {
Finder::new(needle.as_bytes())
}
fn grep_line_re() -> &'static Regex {
static RE: OnceLock<Regex> = OnceLock::new();
RE.get_or_init(|| Regex::new(r"^(.+?):(\d+):").expect("grep line re compiles"))
}
pub fn detect(text: &str) -> Family {
static GIT_STATUS_BRANCH: OnceLock<Finder<'static>> = OnceLock::new();
static GIT_STATUS_CLEAN: OnceLock<Finder<'static>> = OnceLock::new();
static GIT_DIFF: OnceLock<Finder<'static>> = OnceLock::new();
static GIT_COMMIT: OnceLock<Finder<'static>> = OnceLock::new();
static CARGO: OnceLock<Finder<'static>> = OnceLock::new();
static NPM_AUDIT: OnceLock<Finder<'static>> = OnceLock::new();
static NPM_ADDED: OnceLock<Finder<'static>> = OnceLock::new();
static PIP_INSTALLED: OnceLock<Finder<'static>> = OnceLock::new();
static PYTEST: OnceLock<Finder<'static>> = OnceLock::new();
let bytes = text.as_bytes();
let branch = GIT_STATUS_BRANCH.get_or_init(|| finder("On branch "));
let clean = GIT_STATUS_CLEAN.get_or_init(|| finder("nothing to commit"));
if branch.find(bytes).is_some() || clean.find(bytes).is_some() {
return Family::GitStatus;
}
let diff = GIT_DIFF.get_or_init(|| finder("diff --git "));
if diff.find(bytes).is_some() {
return Family::GitDiff;
}
let commit = GIT_COMMIT.get_or_init(|| finder("commit "));
if text
.lines()
.any(|l| commit.find(l.as_bytes()) == Some(0) && is_git_log_commit_line(l))
{
return Family::GitLog;
}
let cargo = CARGO.get_or_init(|| finder("Compiling "));
if cargo.find(bytes).is_some() {
return Family::CargoBuild;
}
let added = NPM_ADDED.get_or_init(|| finder("added "));
let audit = NPM_AUDIT.get_or_init(|| finder("audited "));
if audit.find(bytes).is_some() || added.find(bytes).is_some() {
return Family::NpmInstall;
}
let pip = PIP_INSTALLED.get_or_init(|| finder("Successfully installed"));
if pip.find(bytes).is_some() {
return Family::NpmInstall;
}
let pytest = PYTEST.get_or_init(|| finder(" passed"));
if pytest.find(bytes).is_some() && text.contains("===") {
return Family::Pytest;
}
if text.contains("test result:") {
return Family::Pytest;
}
if looks_like_grep(text) {
return Family::Grep;
}
if looks_like_listing(text) {
return Family::Ls;
}
Family::Logs
}
fn is_git_log_commit_line(line: &str) -> bool {
let rest = &line["commit ".len()..];
let sha: &str = rest.split_whitespace().next().unwrap_or("");
sha.len() >= 7 && sha.len() <= 40 && sha.bytes().all(|b| b.is_ascii_hexdigit())
}
fn looks_like_grep(text: &str) -> bool {
let re = grep_line_re();
let mut total = 0usize;
let mut hits = 0usize;
for line in text.lines() {
if line.is_empty() {
continue;
}
total += 1;
if re.is_match(line) {
hits += 1;
}
}
total >= 5 && hits * 10 >= total * 3
}
fn looks_like_listing(text: &str) -> bool {
let mut total = 0usize;
let mut pathish = 0usize;
for line in text.lines() {
let t = line.trim();
if t.is_empty() {
continue;
}
total += 1;
let single_token = !t.contains(char::is_whitespace);
let ls_long = t.len() > 10
&& (t.starts_with('-') || t.starts_with('d') || t.starts_with('l'))
&& t.as_bytes()[1..10]
.iter()
.all(|&b| matches!(b, b'r' | b'w' | b'x' | b's' | b't' | b'-' | b'@' | b'+'));
if single_token || ls_long {
pathish += 1;
}
}
total >= 10 && pathish * 4 >= total * 3
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_aliases() {
assert_eq!(Family::parse("git-status"), Some(Family::GitStatus));
assert_eq!(Family::parse("RG"), Some(Family::Grep));
assert_eq!(Family::parse("nope"), None);
}
#[test]
fn detect_git_status() {
assert_eq!(
detect("On branch main\nnothing to commit"),
Family::GitStatus
);
}
#[test]
fn detect_git_diff() {
assert_eq!(
detect("diff --git a/x b/x\n+++ b/x\n+added"),
Family::GitDiff
);
}
#[test]
fn detect_git_log() {
let log = "commit 1234567890abcdef1234567890abcdef12345678\nAuthor: x\n\n msg\n";
assert_eq!(detect(log), Family::GitLog);
}
#[test]
fn detect_cargo_and_npm() {
assert_eq!(detect(" Compiling foo v0.1.0\n"), Family::CargoBuild);
assert_eq!(detect("added 120 packages in 3s\n"), Family::NpmInstall);
}
#[test]
fn detect_grep_shape() {
let g = "src/a.rs:10:foo\nsrc/a.rs:11:bar\nsrc/b.rs:3:baz\nsrc/b.rs:4:qux\nsrc/c.rs:1:x";
assert_eq!(detect(g), Family::Grep);
}
#[test]
fn detect_falls_back_to_logs() {
assert_eq!(
detect("some arbitrary prose that matches nothing"),
Family::Logs
);
}
}