use std::borrow::Cow;
use std::path::Path;
use serde_json::Value;
use shell_words::split as shell_split;
use super::streams::strip_ansi_codes;
pub(super) fn parse_command_tokens(payload: &Value) -> Option<Vec<String>> {
if let Some(array) = payload.get("command").and_then(Value::as_array) {
let mut tokens = Vec::new();
for value in array {
if let Some(segment) = value.as_str()
&& !segment.is_empty()
{
tokens.push(segment.to_string());
}
}
if !tokens.is_empty() {
return Some(tokens);
}
}
if let Some(command_str) = payload.get("command").and_then(Value::as_str) {
if command_str.trim().is_empty() {
return None;
}
if let Ok(segments) = shell_split(command_str)
&& !segments.is_empty()
{
return Some(segments);
}
}
None
}
fn normalized_command_name(tokens: &[String]) -> Option<String> {
tokens
.first()
.and_then(|cmd| Path::new(cmd).file_name())
.and_then(|name| name.to_str())
.map(|name| name.to_ascii_lowercase())
}
fn command_is_multicol_listing(tokens: &[String]) -> bool {
normalized_command_name(tokens)
.map(|name| {
matches!(
name.as_str(),
"ls" | "dir" | "vdir" | "gls" | "colorls" | "exa" | "eza"
)
})
.unwrap_or(false)
}
fn listing_has_single_column_flag(tokens: &[String]) -> bool {
tokens.iter().any(|arg| {
matches!(
arg.as_str(),
"-1" | "--format=single-column"
| "--long"
| "-l"
| "--tree"
| "--grid=never"
| "--no-grid"
)
})
}
pub(super) fn preprocess_terminal_stdout<'a>(
tokens: Option<&[String]>,
stdout: &'a str,
) -> Cow<'a, str> {
if stdout.trim().is_empty() {
return Cow::Borrowed(stdout);
}
let filtered_text =
if stdout.contains("MallocStackLogging:") || stdout.contains("malloc: enabling abort()") {
let mut filtered = String::with_capacity(stdout.len());
let mut removed_any = false;
for line in stdout.lines() {
if line.contains("MallocStackLogging:")
|| line.contains("malloc: enabling abort()")
|| line.contains("can't turn off malloc stack logging")
{
removed_any = true;
continue;
}
if !filtered.is_empty() {
filtered.push('\n');
}
filtered.push_str(line);
}
removed_any.then_some(filtered)
} else {
None
};
let normalized = if let Some(filtered) = filtered_text {
let stripped = strip_ansi_codes(&filtered);
match stripped {
Cow::Borrowed(text) => normalize_carriage_returns(text).into_owned().into(),
Cow::Owned(text) => normalize_carriage_returns(&text).into_owned().into(),
}
} else {
let stripped = strip_ansi_codes(stdout);
match stripped {
Cow::Borrowed(text) => normalize_carriage_returns(text),
Cow::Owned(text) => normalize_carriage_returns(&text).into_owned().into(),
}
};
let should_strip_numbers = tokens
.map(command_can_emit_rust_diagnostics)
.unwrap_or(false)
&& looks_like_rust_diagnostic(normalized.as_ref());
if should_strip_numbers {
return strip_rust_diagnostic_columns(normalized);
}
if let Some(parts) = tokens
&& command_is_multicol_listing(parts)
&& !listing_has_single_column_flag(parts)
{
let plain = strip_ansi_codes(normalized.as_ref());
let mut rows = String::with_capacity(plain.len());
for entry in plain.split_whitespace() {
if entry.is_empty() {
continue;
}
rows.push_str(entry);
rows.push('\n');
}
return Cow::Owned(rows);
}
normalized
}
fn command_can_emit_rust_diagnostics(tokens: &[String]) -> bool {
tokens
.first()
.map(|cmd| matches!(cmd.as_str(), "cargo" | "rustc"))
.unwrap_or(false)
}
fn looks_like_rust_diagnostic(text: &str) -> bool {
if text.is_empty() {
return false;
}
let mut snippet_lines = 0usize;
let mut pointer_lines = 0usize;
let mut has_location_marker = false;
for line in text.lines().take(200) {
let trimmed = line.trim_start();
if trimmed.starts_with("--> ") {
has_location_marker = true;
}
if trimmed.starts_with('|') {
pointer_lines += 1;
}
if let Some((prefix, _)) = trimmed.split_once('|') {
let prefix_trimmed = prefix.trim();
if !prefix_trimmed.is_empty() && prefix_trimmed.chars().all(|ch| ch.is_ascii_digit()) {
snippet_lines += 1;
}
}
if snippet_lines >= 1 && pointer_lines >= 1 {
return true;
}
if snippet_lines >= 2 && has_location_marker {
return true;
}
}
false
}
fn strip_rust_diagnostic_columns<'a>(content: Cow<'a, str>) -> Cow<'a, str> {
match content {
Cow::Borrowed(text) => strip_rust_diagnostic_columns_from_str(text)
.map(Cow::Owned)
.unwrap_or_else(|| Cow::Borrowed(text)),
Cow::Owned(text) => {
if let Some(stripped) = strip_rust_diagnostic_columns_from_str(&text) {
Cow::Owned(stripped)
} else {
Cow::Owned(text)
}
}
}
}
fn strip_rust_diagnostic_columns_from_str(input: &str) -> Option<String> {
if input.is_empty() {
return None;
}
let mut output = String::with_capacity(input.len());
let mut changed = false;
for chunk in input.split_inclusive('\n') {
let (line, had_newline) = chunk
.strip_suffix('\n')
.map(|line| (line, true))
.unwrap_or((chunk, false));
if let Some(prefix_end) = rust_diagnostic_prefix_end(line) {
changed = true;
output.push_str(&line[prefix_end..]);
} else {
output.push_str(line);
}
if had_newline {
output.push('\n');
}
}
if changed { Some(output) } else { None }
}
fn rust_diagnostic_prefix_end(line: &str) -> Option<usize> {
let bytes = line.as_bytes();
let len = bytes.len();
let mut idx = 0usize;
while idx < len && bytes[idx].is_ascii_whitespace() {
idx += 1;
}
if idx >= len {
return None;
}
if bytes[idx].is_ascii_digit() {
let mut cursor = idx;
while cursor < len && bytes[cursor].is_ascii_digit() {
cursor += 1;
}
if cursor == idx {
return None;
}
while cursor < len && bytes[cursor].is_ascii_whitespace() {
cursor += 1;
}
if cursor < len && bytes[cursor] == b'|' {
cursor += 1;
if cursor < len && bytes[cursor] == b' ' {
cursor += 1;
}
return Some(cursor);
}
return None;
}
if bytes[idx] == b'|' {
let mut cursor = idx + 1;
if cursor < len && bytes[cursor] == b' ' {
cursor += 1;
}
return Some(cursor);
}
None
}
fn normalize_carriage_returns(input: &str) -> Cow<'_, str> {
if !input.contains('\r') {
return Cow::Borrowed(input);
}
let mut output = String::with_capacity(input.len());
let mut current_line = String::new();
let mut chars = input.chars().peekable();
while let Some(ch) = chars.next() {
match ch {
'\r' => {
if matches!(chars.peek(), Some('\n')) {
chars.next();
output.push_str(¤t_line);
output.push('\n');
current_line.clear();
} else {
current_line.clear();
}
}
'\n' => {
output.push_str(¤t_line);
output.push('\n');
current_line.clear();
}
_ => current_line.push(ch),
}
}
if !current_line.is_empty() {
output.push_str(¤t_line);
}
Cow::Owned(output)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn preprocess_strips_rust_line_numbers_for_cargo_output() {
let tokens = vec!["cargo".to_string(), "check".to_string()];
let input = "\
warning: this is a warning
--> src/main.rs:12:5
|
12 | let x = 5;
| ----- value defined here
|
= note: additional context
";
let processed = preprocess_terminal_stdout(Some(&tokens), input);
let output = processed.to_string();
assert!(!output.contains("12 |"));
assert!(output.contains("let x = 5;"));
assert!(output.contains("----- value defined here"));
}
#[test]
fn detects_rust_diagnostic_shape() {
let sample = "\
warning: something
--> src/lib.rs:7:9
|
7 | println!(\"hi\");
| ^^^^^^^^^^^^^^^
";
assert!(
looks_like_rust_diagnostic(sample),
"should detect diagnostic structure"
);
}
#[test]
fn rust_prefix_end_handles_pointer_lines() {
let line = " | ^ expected struct `Foo`, found enum `Bar`";
let idx = rust_diagnostic_prefix_end(line).expect("prefix");
assert_eq!(
&line[idx..],
" ^ expected struct `Foo`, found enum `Bar`"
);
}
#[test]
fn strip_rust_columns_returns_none_when_unmodified() {
let sample = "no diagnostics here";
assert!(strip_rust_diagnostic_columns_from_str(sample).is_none());
}
#[test]
fn detects_nested_json_command_output() {
let nested_json = r#"{"stdout": "test output", "stderr": "test error", "returncode": 0}"#;
if let Ok(parsed) = serde_json::from_str::<Value>(nested_json) {
assert!(parsed.get("stdout").is_some());
assert!(parsed.get("stderr").is_some());
assert!(parsed.get("returncode").is_some());
} else {
panic!("Failed to parse nested JSON");
}
}
#[test]
fn parse_command_tokens_handles_array_format() {
let payload = serde_json::json!({
"command": ["cargo", "fmt"]
});
let tokens = parse_command_tokens(&payload);
assert_eq!(tokens, Some(vec!["cargo".to_string(), "fmt".to_string()]));
}
#[test]
fn parse_command_tokens_handles_string_format() {
let payload = serde_json::json!({
"command": "cargo fmt"
});
let tokens = parse_command_tokens(&payload);
assert_eq!(tokens, Some(vec!["cargo".to_string(), "fmt".to_string()]));
}
#[test]
fn parse_command_tokens_returns_none_for_empty_array() {
let payload = serde_json::json!({
"command": []
});
let tokens = parse_command_tokens(&payload);
assert_eq!(tokens, None);
}
}