use forge::signal::compactor;
use once_cell::sync::Lazy;
use regex::Regex;
static PULL_LAYER_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?m)^[a-f0-9]+: (Pulling from|Pull complete|Already exists|Waiting|Downloading|Extracting|Verifying Checksum)[^\n]*\n?").unwrap()
});
static BUILD_STEP_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?m)^Step \d+/\d+ : [^\n]+\n?(?: ---> [a-f0-9]+\n?)?").unwrap());
static BUILD_CONTEXT_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?m)^Sending build context to Docker daemon[^\n]+\n?").unwrap());
static COMPOSE_PULLING_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?m)^\[.+\] Pulling [^\n]+\n?").unwrap());
pub fn compress_pull(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let s = PULL_LAYER_RE.replace_all(&cleaned, "");
compactor::collapse_blanks(&s)
}
pub fn compress_build(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let s = BUILD_CONTEXT_RE.replace_all(&cleaned, "");
let s = BUILD_STEP_RE.replace_all(&s, "");
let s = PULL_LAYER_RE.replace_all(&s, "");
compactor::collapse_blanks(&s)
}
pub fn compress_ps(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let lines: Vec<&str> = cleaned.lines().collect();
if lines.len() <= 20 {
return cleaned;
}
format!(
"{}\n... [{} more containers] ...",
lines[..20].join("\n"),
lines.len() - 20
)
}
pub fn compress_compose(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let s = COMPOSE_PULLING_RE.replace_all(&cleaned, "");
let s = PULL_LAYER_RE.replace_all(&s, "");
compactor::collapse_blanks(&s)
}
pub fn compress_logs(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
super::super::sys::log_dedup(&cleaned)
}
pub fn compress_inspect(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let noisy: &[&str] = &[
"\"GraphDriver\"",
"\"MountLabel\"",
"\"ProcessLabel\"",
"\"AppArmorProfile\"",
"\"ExecIDs\"",
"\"HostsPath\"",
"\"ResolvConfPath\"",
"\"LogPath\"",
"\"MountPoint\"",
];
let out: Vec<&str> = cleaned
.lines()
.filter(|l| noisy.iter().all(|n| !l.contains(n)))
.collect();
let lines = out;
if lines.len() <= 60 {
return lines.join("\n");
}
format!(
"{}\n... [{} more lines] ...",
lines[..60].join("\n"),
lines.len() - 60
)
}
pub fn compress_stats(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let lines: Vec<&str> = cleaned.lines().collect();
if lines.len() <= 25 {
return cleaned;
}
format!(
"{}\n... [{} more containers] ...",
lines[..25].join("\n"),
lines.len() - 25
)
}
pub fn compress_network(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let noisy: &[&str] = &[
"\"Options\":",
"\"Labels\":",
"\"Internal\":",
"\"Attachable\":",
"\"Ingress\":",
"\"ConfigFrom\":",
"\"ConfigOnly\":",
"\"Peers\":",
"\"Services\":",
];
let out: Vec<&str> = cleaned
.lines()
.filter(|l| noisy.iter().all(|n| !l.trim().starts_with(n)))
.collect();
out.join("\n")
}
pub fn compress_docker(subcmd: &str, raw: &str) -> String {
let sub = subcmd.trim();
if sub.starts_with("pull") {
return compress_pull(raw);
}
if sub.starts_with("build") {
return compress_build(raw);
}
if sub.starts_with("ps") || sub.starts_with("images") {
return compress_ps(raw);
}
if sub.starts_with("compose") || sub.starts_with("up") || sub.starts_with("down") {
return compress_compose(raw);
}
if sub.starts_with("logs") {
return compress_logs(raw);
}
if sub.starts_with("inspect") {
return compress_inspect(raw);
}
if sub.starts_with("stats") {
return compress_stats(raw);
}
if sub.starts_with("network") {
return compress_network(raw);
}
if sub.starts_with("system") {
return compress_system(raw);
}
compactor::normalise(raw)
}
pub fn compress_system(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let lines: Vec<&str> = cleaned.lines().filter(|l| !l.trim().is_empty()).collect();
let has_summary = lines
.iter()
.any(|l| l.contains("reclaimed") || l.contains("Total"));
if has_summary && lines.len() > 10 {
let summary: Vec<&str> = lines
.iter()
.copied()
.filter(|l| l.contains("reclaimed") || l.contains("Total") || l.starts_with("TYPE"))
.collect();
let deleted_count = lines
.iter()
.filter(|l| l.len() == 12 || l.len() == 64)
.count();
if deleted_count > 0 {
return format!("[{deleted_count} IDs deleted]\n{}", summary.join("\n"));
}
return summary.join("\n");
}
if lines.len() <= 20 {
return lines.join("\n");
}
format!(
"{}\n... [{} more lines] ...",
lines[..20].join("\n"),
lines.len() - 20
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pull_strips_layer_progress() {
let raw = "abc123: Pulling from library/ubuntu\nabc123: Pull complete\nStatus: Downloaded newer image\n";
let out = compress_pull(raw);
assert!(!out.contains("Pull complete"), "{out}");
assert!(out.contains("Downloaded newer image"));
}
#[test]
fn build_strips_step_noise() {
let raw = "Sending build context to Docker daemon 5.12kB\nStep 1/3 : FROM ubuntu:22.04\n ---> abc123\nSuccessfully built def456\n";
let out = compress_build(raw);
assert!(!out.contains("Sending build context"), "{out}");
assert!(!out.contains("Step 1/3"), "{out}");
assert!(out.contains("Successfully built"));
}
#[test]
fn ps_truncates_many_containers() {
let header = "CONTAINER ID IMAGE COMMAND STATUS\n";
let rows: String = (0..25)
.map(|i| format!("{:012x} img{i} cmd Up\n", i))
.collect();
let out = compress_ps(&format!("{header}{rows}"));
assert!(out.contains("more containers"), "{out}");
}
#[test]
fn inspect_strips_noisy_fields() {
let raw = "[\n {\n \"Id\": \"abc123\",\n \"GraphDriver\": { \"Data\": null },\n \"MountLabel\": \"\",\n \"State\": { \"Status\": \"running\" }\n }\n]\n";
let out = compress_inspect(raw);
assert!(!out.contains("GraphDriver"), "{out}");
assert!(out.contains("running"), "{out}");
}
#[test]
fn network_strips_options_labels() {
let raw = "[\n {\n \"Name\": \"bridge\",\n \"Driver\": \"bridge\",\n \"Options\": {},\n \"Labels\": {},\n \"Containers\": {}\n }\n]\n";
let out = compress_network(raw);
assert!(!out.contains("\"Options\":"), "{out}");
assert!(out.contains("\"Name\""), "{out}");
}
}