diff --git a/Cargo.lock b/Cargo.lock index 493be7fa..ea7e6236 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -124,6 +124,7 @@ dependencies = [ "reqwest", "serde", "serde_json", + "strip-ansi-escapes", "thiserror", "tokio", "toml", @@ -1848,6 +1849,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strip-ansi-escapes" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "011cbb39cf7c1f62871aea3cc46e5817b0937b49e9447370c93cacbe93a766d8" +dependencies = [ + "vte", +] + [[package]] name = "strsim" version = "0.10.0" @@ -2234,6 +2244,12 @@ dependencies = [ "serde", ] +[[package]] +name = "utf8parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372" + [[package]] name = "vcpkg" version = "0.2.15" @@ -2252,6 +2268,27 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "vte" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cbce692ab4ca2f1f3047fcf732430249c0e971bfdd2b234cf2c47ad93af5983" +dependencies = [ + "arrayvec", + "utf8parse", + "vte_generate_state_changes", +] + +[[package]] +name = "vte_generate_state_changes" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "wait-timeout" version = "0.2.0" diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index 044d238a..93e6285b 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -125,28 +125,24 @@ impl telemetry::EventListener for Terminal { let (max_mem, max_cpu) = find_max_execution_units(&tests); for (module, infos) in &group_by_module(&tests) { - let first = fmt_test(infos.first().unwrap(), max_mem, max_cpu, false).len(); + let title = module.bold().blue().to_string(); + + let tests = infos + .iter() + .map(|eval_info| fmt_test(eval_info, max_mem, max_cpu, true)) + .collect::>() + .join("\n"); + + let summary = fmt_test_summary(infos, true); + println!( - "{} {} {}", - " ┌──".bright_black(), - module.bold().blue(), - pretty::pad_left("".to_string(), first - module.len() - 3, "─") - .bright_black() - ); - for eval_info in infos { - println!( - " {} {}", - "│".bright_black(), - fmt_test(eval_info, max_mem, max_cpu, true) + "{}\n", + pretty::indent( + &pretty::open_box(&title, &tests, &summary, |border| border + .bright_black() + .to_string()), + 4 ) - } - let last = fmt_test(infos.last().unwrap(), max_mem, max_cpu, false).len(); - let summary = fmt_test_summary(infos, false).len(); - println!( - "{} {}\n", - pretty::pad_right(" └".to_string(), last - summary + 5, "─") - .bright_black(), - fmt_test_summary(infos, true), ); } } diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 5890f65c..03bcba85 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -26,6 +26,7 @@ regex = "1.6.0" reqwest = "0.11.13" serde = { version = "1.0.144", features = ["derive"] } serde_json = { version = "1.0.85", features = ["preserve_order"] } +strip-ansi-escapes = "0.1.1" thiserror = "1.0.37" tokio = { version = "1.23.0", features = ["full"] } toml = "0.5.9" diff --git a/crates/project/src/error.rs b/crates/project/src/error.rs index 8bdae4ed..213a7e73 100644 --- a/crates/project/src/error.rs +++ b/crates/project/src/error.rs @@ -286,11 +286,11 @@ impl Diagnostic for Error { None => None, Some(hint) => { let budget = ExBudget { mem: i64::MAX, cpu: i64::MAX, }; - let left = pretty::boxed("left", match hint.left.eval(budget) { + let left = pretty::boxed("left", &match hint.left.eval(budget) { (Ok(term), _, _) => format!("{term}"), (Err(err), _, _) => format!("{err}"), }); - let right = pretty::boxed("right", match hint.right.eval(budget) { + let right = pretty::boxed("right", &match hint.right.eval(budget) { (Ok(term), _, _) => format!("{term}"), (Err(err), _, _) => format!("{err}"), }); diff --git a/crates/project/src/pretty.rs b/crates/project/src/pretty.rs index 7761062a..a9b6182d 100644 --- a/crates/project/src/pretty.rs +++ b/crates/project/src/pretty.rs @@ -1,26 +1,100 @@ -pub fn boxed(title: &str, content: String) -> String { - let n = content.lines().fold(0, |max, l| { - let n = l.len(); +pub fn ansi_len(s: &str) -> usize { + String::from_utf8(strip_ansi_escapes::strip(s).unwrap()) + .unwrap() + .chars() + .count() +} + +pub fn len_longest_line(s: &str) -> usize { + s.lines().fold(0, |max, l| { + let n = ansi_len(l); if n > max { n } else { max } - }); + }) +} + +pub fn boxed(title: &str, content: &str) -> String { + boxed_with(title, content, |s| s.to_string()) +} + +pub fn boxed_with(title: &str, content: &str, border_style: fn(&str) -> String) -> String { + let n = len_longest_line(content); let content = content .lines() - .map(|line| format!("│ {} │", pad_right(line.to_string(), n, " "))) + .map(|line| { + format!( + "{} {} {}", + border_style("│"), + pad_right(line.to_string(), n, " "), + border_style("│"), + ) + }) .collect::>() .join("\n"); - let top = format!("┍━ {}┑", pad_right(format!("{title} "), n, "━")); - let bottom = format!("┕{}┙", pad_right(String::new(), n + 2, "━")); + let top = format!( + "{} {}{}", + border_style("┍━"), + pad_right(format!("{title} "), n, &border_style("━")), + border_style("┑"), + ); + + let bottom = format!( + "{}{}{}", + border_style("┕"), + pad_right(String::new(), n + 2, &border_style("━")), + border_style("┙") + ); + format!("{top}\n{content}\n{bottom}") } +pub fn open_box( + title: &str, + content: &str, + footer: &str, + border_style: fn(&str) -> String, +) -> String { + let i = ansi_len(content.lines().collect::>().first().unwrap()); + let j = len_longest_line(content); + let k = ansi_len(footer); + + let content = content + .lines() + .map(|line| format!("{} {line}", border_style("│"),)) + .collect::>() + .join("\n"); + + let top = format!( + "{} {}", + border_style("┍━"), + pad_right(format!("{title} "), i - 1, &border_style("━")), + ); + + let bottom = format!( + "{} {}", + pad_right(border_style("┕"), j - k + 1, &border_style("━")), + footer + ); + + format!("{top}\n{content}\n{bottom}") +} + +pub fn indent(lines: &str, n: usize) -> String { + let tab = pad_left(String::new(), n, " "); + lines + .lines() + .map(|line| format!("{tab}{line}")) + .collect::>() + .join("\n") +} + pub fn pad_left(mut text: String, n: usize, delimiter: &str) -> String { - let diff = n as i32 - text.len() as i32; + let diff = n as i32 - ansi_len(&text) as i32; if diff.is_positive() { for _ in 0..diff { text.insert_str(0, delimiter); @@ -30,7 +104,7 @@ pub fn pad_left(mut text: String, n: usize, delimiter: &str) -> String { } pub fn pad_right(mut text: String, n: usize, delimiter: &str) -> String { - let diff = n as i32 - text.len() as i32; + let diff = n as i32 - ansi_len(&text) as i32; if diff.is_positive() { for _ in 0..diff { text.push_str(delimiter);