From 4f67f9858e1c4e726bf2bcaac16e823d42a0c989 Mon Sep 17 00:00:00 2001 From: Teakowa <27560638+Teakowa@users.noreply.github.com> Date: Sat, 22 Aug 2026 22:09:34 +0800 Subject: [PATCH 1/2] feat(cli): refine interactive terminal presentation Fixes #213 --- crates/wright-cli/src/present.rs | 56 ++++++++++++++++++++++++-------- crates/wright-cli/tests/cli.rs | 18 ++++++++++ 2 files changed, 61 insertions(+), 13 deletions(-) diff --git a/crates/wright-cli/src/present.rs b/crates/wright-cli/src/present.rs index 5a8cef7..17da6e7 100644 --- a/crates/wright-cli/src/present.rs +++ b/crates/wright-cli/src/present.rs @@ -72,12 +72,17 @@ impl Drop for Activity { let _ = handle.join(); } if self.visible.load(Ordering::Acquire) { - eprint!("\r \r"); - let _ = std::io::stderr().flush(); + clear_activity_line(&mut std::io::stderr()); } } } +fn clear_activity_line(writer: &mut impl Write) { + let _ = write!(writer, "\r\x1b[2K\r"); + let _ = writeln!(writer); + let _ = writer.flush(); +} + #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum Renderer { Terminal, @@ -245,29 +250,35 @@ fn render_verdict( } else { status.to_string() }; - let summary = match envelope.command.as_str() { - "check" => format!( - "{label} check — {} diagnostic(s)", - envelope.diagnostics.len() - ), + println!("{label} {}", envelope.command); + let metadata = match envelope.command.as_str() { + "check" => format!("{} diagnostic(s)", envelope.diagnostics.len()), "lint" => format!( - "{label} lint — {} finding(s) across {} rule(s)", + "{} finding(s) across {} rule(s)", array_len(value, "/result/findings"), array_len(value, "/result/rules"), ), "analyze" => format!( - "{label} analyze — {} symbol(s), {} rule measurement(s)", + "{} symbol(s), {} rule measurement(s)", array_len(value, "/result/facts/symbols"), array_len(value, "/result/facts/rules"), ), "inspect" => format!( - "{label} inspect — {} rule(s), {} symbol(s)", + "{} rule(s), {} symbol(s)", array_len(value, "/result/rules"), array_len(value, "/result/symbols"), ), - other => format!("{label} {other}"), + _ => return, }; - println!("{summary}"); + println!(" {}", dim(&metadata, color)); +} + +fn dim(value: &str, color: bool) -> String { + if color { + format!("\x1b[2m{value}\x1b[0m") + } else { + value.to_string() + } } fn array_len(value: &serde_json::Value, pointer: &str) -> usize { @@ -527,6 +538,7 @@ fn render_analyze(envelope: &Envelope) { .and_then(serde_json::Value::as_array) .cloned() .unwrap_or_default(); + println!("\nAnalysis details"); for symbol in &symbols { let kind = symbol .get("kind") @@ -588,6 +600,10 @@ fn render_lint(envelope: &Envelope) { .and_then(serde_json::Value::as_array) .cloned() .unwrap_or_default(); + println!("\nLint findings"); + if findings.is_empty() { + println!(" none"); + } for finding in &findings { let code = finding .get("code") @@ -640,7 +656,8 @@ fn render_inspect(envelope: &Envelope) { .get("rules") .and_then(serde_json::Value::as_u64) .unwrap_or(0); - println!("inspect: {count} rule(s), {} symbol(s)", symbols.len()); + println!("\nProgram structure"); + println!(" {count} rule(s), {} symbol(s)", symbols.len()); for rule in &rules { let id = rule .get("id") @@ -924,6 +941,19 @@ mod tests { assert!(activity.visible.load(Ordering::Acquire)); } + #[test] + fn activity_cleanup_clears_the_line_and_terminates_it() { + let mut output = Vec::new(); + clear_activity_line(&mut output); + assert_eq!(output, b"\r\x1b[2K\r\n"); + } + + #[test] + fn metadata_is_dimmed_only_for_terminal_color_output() { + assert_eq!(dim("2 symbols", false), "2 symbols"); + assert_eq!(dim("2 symbols", true), "\x1b[2m2 symbols\x1b[0m"); + } + #[test] fn workflow_command_escaping_is_split_by_context() { assert_eq!(escape_workflow_property("a,b:c%\n"), "a%2Cb%3Ac%25%0A"); diff --git a/crates/wright-cli/tests/cli.rs b/crates/wright-cli/tests/cli.rs index 8adad6e..f1d9295 100644 --- a/crates/wright-cli/tests/cli.rs +++ b/crates/wright-cli/tests/cli.rs @@ -162,6 +162,24 @@ fn check_over_clean_input_exits_zero() { let _ = std::fs::remove_dir_all(path.parent().unwrap()); } +#[test] +fn terminal_renderer_uses_command_specific_hierarchy() { + let path = temp_file("flow.txt", &corpus_workshop("synthetic/control-flow")); + for (command, heading, detail) in [ + ("check", "PASS check", "diagnostic(s)"), + ("lint", "WARN lint", "Lint findings"), + ("analyze", "PASS analyze", "Analysis details"), + ("inspect", "PASS inspect", "Program structure"), + ] { + let output = run(&[command, path.to_str().unwrap(), "--renderer", "terminal"]); + assert!(output.status.code().is_some(), "{command} exited by signal"); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains(heading), "{command}: {stdout}"); + assert!(stdout.contains(detail), "{command}: {stdout}"); + } + let _ = std::fs::remove_dir_all(path.parent().unwrap()); +} + #[test] fn check_over_malformed_input_exits_one_with_structured_diagnostics() { // Enough locale evidence to pass detection, then a syntax error. From 92eb077dcb9ffe00fd3699d69f40d01c7176e729 Mon Sep 17 00:00:00 2001 From: Teakowa <27560638+Teakowa@users.noreply.github.com> Date: Sat, 22 Aug 2026 22:19:15 +0800 Subject: [PATCH 2/2] test(cli): make terminal hierarchy assertion color independent --- crates/wright-cli/tests/cli.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/wright-cli/tests/cli.rs b/crates/wright-cli/tests/cli.rs index f1d9295..317a2c7 100644 --- a/crates/wright-cli/tests/cli.rs +++ b/crates/wright-cli/tests/cli.rs @@ -171,7 +171,14 @@ fn terminal_renderer_uses_command_specific_hierarchy() { ("analyze", "PASS analyze", "Analysis details"), ("inspect", "PASS inspect", "Program structure"), ] { - let output = run(&[command, path.to_str().unwrap(), "--renderer", "terminal"]); + let output = run(&[ + command, + path.to_str().unwrap(), + "--renderer", + "terminal", + "--color", + "never", + ]); assert!(output.status.code().is_some(), "{command} exited by signal"); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains(heading), "{command}: {stdout}");