use regex::{Regex, RegexSet};
use std::sync::LazyLock;
pub(super) struct Profile {
pub(super) match_command: Regex,
pub(super) strip_lines: Option<RegexSet>,
pub(super) keep_stderr: Option<RegexSet>,
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) on_fail_msg: 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)
.unwrap_or_else(|e| panic!("bad match_command regex ({e}): {match_command:?}")),
strip_lines: None,
keep_stderr: None,
max_line_len: None,
head_lines: None,
tail_lines: None,
max_lines: None,
on_empty: None,
on_fail_msg: None,
output_transform: None,
standalone_only: false,
}
}
fn strip(mut self, patterns: &[&str]) -> Self {
self.strip_lines = Some(
RegexSet::new(patterns)
.unwrap_or_else(|e| panic!("bad strip_lines regex ({e}): {patterns:?}")),
);
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)
.unwrap_or_else(|e| panic!("bad keep_stderr regex ({e}): {patterns:?}")),
);
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
}
const fn on_fail(mut self, msg: &'static str) -> Self {
self.on_fail_msg = 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))),
)
.unwrap_or_else(|e| {
panic!("bad cargo compile strip regex ({e}): derived from {CARGO_COMPILE_PREFIXES:?}")
})
});
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,
fail_msg: Option<&'static str>,
) -> Profile {
let mut p = Profile::new(matcher)
.strip_set(&CARGO_COMPILE_STRIP)
.keep_stderr(CARGO_COMPILE_KEEP_STDERR)
.max(max_lines)
.on_empty(ok_msg);
if let Some(msg) = fail_msg {
p = p.on_fail(msg);
}
p
}
fn lint_tool(matcher: &str, max_lines: usize, ok_msg: &'static str) -> Profile {
Profile::new(matcher)
.strip(&[BLANK_LINE])
.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+"])
.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",
])
.max(40)
.on_empty("[docker: ok]"),
Profile::new(r"^gh\b")
.strip(&[BLANK_LINE, r"^\s*-\s", r"warning:.*(?:gh|GitHub)"])
.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").on_empty("[git diff: no changes]"),
cargo_tool(
r"^cargo\s+(build|check)\b",
50,
"[cargo: ok]",
Some("[cargo: failed]"),
),
cargo_tool(
r"^cargo\s+clippy\b",
50,
"[cargo clippy: ok]",
Some("[cargo clippy: failed]"),
),
Profile::new(r"^cargo\s+fmt\b")
.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",
])
.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!"])
.tail(15)
.max(40),
small_util(r"^(?: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),
lint_tool(r"^tsc\b", 50, "[tsc: ok]"),
Profile::new(r"^vitest\b")
.strip(&[r"^\s*(stdout|PASS|SKIP)\s"])
.tail(20)
.max(60),
lint_tool(r"^eslint\b", 50, "[eslint: ok]"),
Profile::new(r"^prettier\b").strip(&[BLANK_LINE]).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",
])
.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",
])
.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"])
.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)
.standalone_only(),
Profile::new(r"^ls\b")
.output_transform(super::compact_ls)
.standalone_only(),
]
});