use regex::{Regex, RegexSet};
use std::sync::LazyLock;
pub(crate) struct ShortCircuit {
pub(crate) pattern: Regex,
pub(crate) message: &'static str,
pub(crate) unless: Option<Regex>,
}
pub(super) struct Profile {
pub(super) match_command: Regex,
pub(super) strip_lines: Option<RegexSet>,
pub(super) keep_stderr: Option<RegexSet>,
pub(super) short_circuits: Vec<ShortCircuit>,
pub(super) max_line_len: Option<usize>,
pub(super) head_lines: Option<usize>,
pub(super) tail_lines: Option<usize>,
pub(super) max_lines: Option<usize>,
pub(super) on_empty: Option<&'static str>,
pub(super) output_transform: Option<fn(&str, exit_code: i32) -> String>,
pub(super) standalone_only: bool,
}
impl Profile {
pub(super) fn new(match_command: &str) -> Self {
Self {
match_command: Regex::new(match_command).expect("bad match_command regex"),
strip_lines: None,
keep_stderr: None,
short_circuits: Vec::new(),
max_line_len: None,
head_lines: None,
tail_lines: None,
max_lines: None,
on_empty: None,
output_transform: None,
standalone_only: false,
}
}
fn strip(mut self, patterns: &[&str]) -> Self {
self.strip_lines = Some(RegexSet::new(patterns).expect("bad strip_lines regex"));
self
}
fn strip_set(mut self, patterns: &RegexSet) -> Self {
self.strip_lines = Some(patterns.clone());
self
}
fn keep_stderr(mut self, patterns: &[&str]) -> Self {
self.keep_stderr = Some(RegexSet::new(patterns).expect("bad keep_stderr regex"));
self
}
fn short_circuit(mut self, pattern: &str, message: &'static str) -> Self {
self.short_circuits.push(ShortCircuit {
pattern: Regex::new(pattern).expect("bad short_circuit pattern"),
message,
unless: None,
});
self
}
fn short_circuit_unless(mut self, pattern: &str, message: &'static str, unless: &str) -> Self {
self.short_circuits.push(ShortCircuit {
pattern: Regex::new(pattern).expect("bad short_circuit pattern"),
message,
unless: Some(Regex::new(unless).expect("bad unless pattern")),
});
self
}
const fn max_line_len(mut self, n: usize) -> Self {
self.max_line_len = Some(n);
self
}
pub(super) const fn head(mut self, n: usize) -> Self {
self.head_lines = Some(n);
self
}
pub(super) const fn tail(mut self, n: usize) -> Self {
self.tail_lines = Some(n);
self
}
pub(super) const fn max(mut self, n: usize) -> Self {
self.max_lines = Some(n);
self
}
const fn on_empty(mut self, msg: &'static str) -> Self {
self.on_empty = Some(msg);
self
}
fn output_transform(mut self, transform: fn(&str, exit_code: i32) -> String) -> Self {
self.output_transform = Some(transform);
self
}
const fn standalone_only(mut self) -> Self {
self.standalone_only = true;
self
}
}
pub(super) const CARGO_COMPILE_PREFIXES: &[&str] = &[
"Compiling",
"Checking",
"Downloading",
"Downloaded",
"Finished",
"Fresh",
"Blocking",
"Documenting",
"Running",
];
static CARGO_COMPILE_STRIP: LazyLock<RegexSet> = LazyLock::new(|| {
RegexSet::new(
CARGO_COMPILE_PREFIXES
.iter()
.map(|p| format!(r"^\s*{}\s", regex::escape(p))),
)
.expect("bad cargo compile strip regex")
});
const CARGO_COMPILE_KEEP_STDERR: &[&str] = &[r"warning:", r"^error"];
const BLANK_LINE: &str = r"^\s*$";
fn small_util(matcher: &str, max_lines: usize) -> Profile {
Profile::new(matcher)
.strip(&[BLANK_LINE])
.max_line_len(120)
.max(max_lines)
}
fn cargo_tool(matcher: &str, max_lines: usize, ok_msg: &'static str) -> Profile {
Profile::new(matcher)
.strip_set(&CARGO_COMPILE_STRIP)
.keep_stderr(CARGO_COMPILE_KEEP_STDERR)
.max(max_lines)
.on_empty(ok_msg)
}
pub(super) static GEN_FALLBACK: LazyLock<Profile> = LazyLock::new(|| {
Profile::new("")
.max_line_len(500)
.head(10)
.tail(10)
.max(200)
});
pub(super) static PROFILES: LazyLock<Vec<Profile>> = LazyLock::new(|| {
vec![
Profile::new(r"^df\b").max_line_len(80).max(20),
small_util(r"^(?:du|shellcheck|stat|ps|rustc)\b", 50),
Profile::new(r"^make\b")
.strip(&[r"make\[\d+\]:", BLANK_LINE, r"Nothing to be done"])
.max(50)
.on_empty("make: ok"),
Profile::new(r"^ping\b")
.strip(&[r"^\d+ bytes from ", r"^Reply from "])
.tail(4),
Profile::new(r"^rsync\b")
.strip(&[BLANK_LINE, r"sending incremental", r"sent \d+"])
.short_circuit_unless(
r"total size is",
"ok (synced)",
r"error|failed|No such file",
)
.max(20),
Profile::new(r"^ssh\b")
.strip(&[
BLANK_LINE,
r"Warning: Permanently added",
r"^debug1:",
r"Authenticated to",
])
.max(30),
Profile::new(r"^systemctl\s+status\b")
.strip(&[BLANK_LINE])
.head(5)
.tail(10),
Profile::new(r"^docker\b")
.strip(&[
BLANK_LINE,
r"^Step\s+\d+/",
r"^ --->",
r"^ ---> Using cache",
r"^Successfully built",
r"^Successfully tagged",
])
.short_circuit_unless(
r"Successfully built",
"[docker build: ok]",
r"error|Error|failed|FAILED",
)
.max(40)
.on_empty("[docker: ok]"),
Profile::new(r"^gh\b")
.strip(&[BLANK_LINE, r"^\s*-\s", r"warning:.*(?:gh|GitHub)"])
.short_circuit_unless(
r"(\u{2713}|Done|Successfully)",
"[gh: ok]",
r"error|Error|failed|FAILED",
)
.tail(10)
.max(30)
.on_empty("[gh: ok]"),
Profile::new(r"^helm\b")
.strip(&[BLANK_LINE, r"^\s*STATUS:", r"^\s*v\d+\.\d+\.\d+\s"])
.tail(10)
.max(30)
.on_empty("[helm: ok]"),
Profile::new(r"^git\s+log\b")
.strip(&[BLANK_LINE])
.max_line_len(200)
.head(20)
.max(50),
Profile::new(r"^git\s+diff\b").short_circuit(r"^\s*$", "[git diff: no changes]"),
cargo_tool(r"^cargo\s+(build|check)\b", 50, "[cargo: ok]"),
cargo_tool(r"^cargo\s+clippy\b", 50, "[cargo clippy: ok]"),
Profile::new(r"^cargo\s+fmt\b")
.short_circuit(r"^Formatted\s", "[cargo fmt: ok]")
.max(50)
.on_empty("[cargo fmt: ok]"),
Profile::new(r"^cargo\s+install\b")
.strip_set(&CARGO_COMPILE_STRIP)
.max(30),
Profile::new(r"^rustup\b")
.strip(&[r"^info:", r"syncing", r"downloading", r"installing"])
.max(20),
Profile::new(r"^npm\s+install\b")
.strip(&[
r"^\s*added\s",
r"^\s*removed\s",
r"^\s*changed\s",
r"^\s*audited\s",
])
.short_circuit_unless(r"up to date", "npm: up to date", r"ERR|error|failed")
.max(30),
Profile::new(r"^npm\s+audit\b|^pnpm\s+audit\b")
.strip(&[BLANK_LINE])
.head(5)
.tail(10)
.on_empty("[npm audit: clean]"),
Profile::new(r"^npm\s+run\b|^pnpm\s+run\b|^yarn\s+(run\b)")
.strip(&[BLANK_LINE, r"^> .+@", r"^npm ERR!", r"^ERR!"])
.short_circuit_unless(
r"success|Done in",
"[npm run: ok]",
r"error|Error|failed|FAILED|ERR",
)
.tail(15)
.max(40),
small_util(r"^(?:npx|biome|oxlint|ruff)\b", 30),
Profile::new(r"^pnpm\s+install\b")
.strip(&[
r"Already up to date",
r"Progress:",
r"Resolving:",
r"Downloading:",
])
.max(30),
Profile::new(r"^tsc\b")
.strip(&[BLANK_LINE])
.keep_stderr(CARGO_COMPILE_KEEP_STDERR)
.max(50)
.on_empty("[tsc: ok]"),
Profile::new(r"^vitest\b")
.strip(&[r"^\s*(stdout|PASS|SKIP)\s"])
.tail(20)
.max(60),
Profile::new(r"^eslint\b")
.strip(&[BLANK_LINE])
.keep_stderr(CARGO_COMPILE_KEEP_STDERR)
.max(50)
.on_empty("[eslint: ok]"),
Profile::new(r"^prettier\b")
.strip(&[BLANK_LINE])
.short_circuit(r"unchanged", "prettier: ok")
.max(20),
Profile::new(r"^next\s+build\b")
.strip(&[BLANK_LINE, r"^\s*✓", r"^info\s+-"])
.tail(10)
.max(40),
small_util(r"^playwright\b", 40).tail(15),
Profile::new(r"^prisma\b")
.strip(&[BLANK_LINE, r"Environment variables", r"Prisma schema"])
.max(30),
small_util(r"^nx\b", 30)
.strip(&[BLANK_LINE, r"NX\s"])
.on_empty("[nx: ok]"),
Profile::new(r"^turbo\b")
.strip(&[BLANK_LINE, r"^\s*•"])
.tail(10)
.max(40),
Profile::new(r"^jest\b")
.strip(&[r"^\s*PASS\s", r"^\s*Tests:\s"])
.tail(15)
.max(40),
Profile::new(r"^yarn\b")
.strip(&[r"^info\s", r"\[\d+/\d+\]"])
.max(20),
Profile::new(r"^pytest\b")
.strip(&[
BLANK_LINE,
r"^\s*\.+\s*$",
r"^\s*collected\s+\d+",
r"^\s*={3,}\s",
])
.short_circuit_unless(r"passed", "[pytest: passed]", r"failed|error|FAILED|ERROR")
.tail(20)
.max(60)
.on_empty("[pytest: passed]"),
Profile::new(r"^pip\s+install\b")
.strip(&[
BLANK_LINE,
r"^Collecting\s",
r"^\s*Downloading\s",
r"^\s*Installing\s",
r"^Successfully installed\s",
])
.short_circuit(r"already satisfied", "[pip: already satisfied]")
.max(30),
Profile::new(r"^fd\b")
.strip(&[BLANK_LINE])
.head(20)
.tail(10)
.max(50),
Profile::new(r"^rg\b|^grep\b")
.strip(&[BLANK_LINE, r"^Binary\s+file\s+\S+\s+matches"])
.head(20)
.tail(10)
.max(60),
Profile::new(r"^mocha\b")
.strip(&[BLANK_LINE, r"^\s*(✓|✗|√|×)\s"])
.tail(15)
.max(40),
Profile::new(r"^terraform\b")
.strip(&[BLANK_LINE, r"^Initializing", r"^Terraform has been"])
.short_circuit_unless(r"No changes", "[terraform: no changes]", r"error|Error")
.head(5)
.tail(15)
.max(40)
.on_empty("[terraform: ok]"),
Profile::new(r"^cargo\s+(test|nextest)\b")
.output_transform(super::filter_cargo_test_output),
Profile::new(r"^ls\b")
.output_transform(super::compact_ls)
.standalone_only(),
]
});