Skip to content

Commit 615a6f8

Browse files
authored
feat(tree): Color the output (#15242)
### What does this PR try to resolve? This is an experiment to find ways to make `cargo tree` output easier to grok. I find too many details are hard to spot because everything looks the same. ![image](https://github.com/user-attachments/assets/92055027-c7a0-4357-9870-400ba6c2de9c) The goal is to make it easier to scan the output for relevant details. So far, this approach seems the most viable. Knowing what type of edges led to where you are provides useful context for scrolling through large lists of dependencies. Fixes #10558 ### How should we test and review this PR? I had considered styling normal dependency edges but there isn't a header to clarify what they mean, so I decided to match the package name as that is the header. So this didn't buy as much as I had hoped, especially since dimmed lines aren't obvious with my terminal. Maybe if we used different unicode graph characters. Instead of coloring the "this is elided" `(*)`, I had considered using a unicode character that looks like its pointing from that item up. I only found one that looked close to that but the origin for the arrow was bottom aligned, rather than center aligned and it looked off. Alternatives considered - Once an edge is marked with dev or build dependencies, all future edges inherit it - While this makes it clear what section you are in, so does the outer most line - Feature edges are the color of the edges that led to them - Unsure of value either way - This might get weird with `--invert` - Style local nodes different than non-local - With the format string being user customizable, I'm concerned with styling over or within user styling - Rainbow packages: For every package in the same namespace (`.split_once("::").0`) or prefix (`.split_once(['-','_']).0`), assign a color (maybe only for the top N packages to reduce duplicate colors) - Would help a lot with `gix-*` and similar other cases - If we cap the number of participating packages, would need care to work with `--depth` (of course,. these could be mixed) ### Additional information
2 parents 29f8d03 + 9b6e11e commit 615a6f8

File tree

11 files changed

+428
-16
lines changed

11 files changed

+428
-16
lines changed

src/cargo/ops/tree/mod.rs

+36-13
Original file line numberDiff line numberDiff line change
@@ -300,26 +300,31 @@ fn print_node<'a>(
300300
no_dedupe: bool,
301301
display_depth: DisplayDepth,
302302
visited_deps: &mut HashSet<NodeId>,
303-
levels_continue: &mut Vec<bool>,
303+
levels_continue: &mut Vec<(anstyle::Style, bool)>,
304304
print_stack: &mut Vec<NodeId>,
305305
) {
306306
let new = no_dedupe || visited_deps.insert(node_index);
307307

308308
match prefix {
309309
Prefix::Depth => drop_print!(ws.gctx(), "{}", levels_continue.len()),
310310
Prefix::Indent => {
311-
if let Some((last_continues, rest)) = levels_continue.split_last() {
312-
for continues in rest {
311+
if let Some(((last_style, last_continues), rest)) = levels_continue.split_last() {
312+
for (style, continues) in rest {
313313
let c = if *continues { symbols.down } else { " " };
314-
drop_print!(ws.gctx(), "{} ", c);
314+
drop_print!(ws.gctx(), "{style}{c}{style:#} ");
315315
}
316316

317317
let c = if *last_continues {
318318
symbols.tee
319319
} else {
320320
symbols.ell
321321
};
322-
drop_print!(ws.gctx(), "{0}{1}{1} ", c, symbols.right);
322+
drop_print!(
323+
ws.gctx(),
324+
"{last_style}{0}{1}{1}{last_style:#} ",
325+
c,
326+
symbols.right
327+
);
323328
}
324329
}
325330
Prefix::None => {}
@@ -333,7 +338,7 @@ fn print_node<'a>(
333338
let star = if (new && !in_cycle) || !has_deps {
334339
""
335340
} else {
336-
" (*)"
341+
color_print::cstr!(" <yellow,dim>(*)</>")
337342
};
338343
drop_println!(ws.gctx(), "{}{}", format.display(graph, node_index), star);
339344

@@ -379,7 +384,7 @@ fn print_dependencies<'a>(
379384
no_dedupe: bool,
380385
display_depth: DisplayDepth,
381386
visited_deps: &mut HashSet<NodeId>,
382-
levels_continue: &mut Vec<bool>,
387+
levels_continue: &mut Vec<(anstyle::Style, bool)>,
383388
print_stack: &mut Vec<NodeId>,
384389
kind: &EdgeKind,
385390
) {
@@ -390,19 +395,23 @@ fn print_dependencies<'a>(
390395

391396
let name = match kind {
392397
EdgeKind::Dep(DepKind::Normal) => None,
393-
EdgeKind::Dep(DepKind::Build) => Some("[build-dependencies]"),
394-
EdgeKind::Dep(DepKind::Development) => Some("[dev-dependencies]"),
398+
EdgeKind::Dep(DepKind::Build) => {
399+
Some(color_print::cstr!("<blue,bold>[build-dependencies]</>"))
400+
}
401+
EdgeKind::Dep(DepKind::Development) => {
402+
Some(color_print::cstr!("<cyan,bold>[dev-dependencies]</>"))
403+
}
395404
EdgeKind::Feature => None,
396405
};
397406

398407
if let Prefix::Indent = prefix {
399408
if let Some(name) = name {
400-
for continues in &**levels_continue {
409+
for (style, continues) in &**levels_continue {
401410
let c = if *continues { symbols.down } else { " " };
402-
drop_print!(ws.gctx(), "{} ", c);
411+
drop_print!(ws.gctx(), "{style}{c}{style:#} ");
403412
}
404413

405-
drop_println!(ws.gctx(), "{}", name);
414+
drop_println!(ws.gctx(), "{name}");
406415
}
407416
}
408417

@@ -433,7 +442,8 @@ fn print_dependencies<'a>(
433442
.peekable();
434443

435444
while let Some(dependency) = it.next() {
436-
levels_continue.push(it.peek().is_some());
445+
let style = edge_line_color(dependency.kind());
446+
levels_continue.push((style, it.peek().is_some()));
437447
print_node(
438448
ws,
439449
graph,
@@ -451,3 +461,16 @@ fn print_dependencies<'a>(
451461
levels_continue.pop();
452462
}
453463
}
464+
465+
fn edge_line_color(kind: EdgeKind) -> anstyle::Style {
466+
match kind {
467+
EdgeKind::Dep(DepKind::Normal) => anstyle::Style::new() | anstyle::Effects::DIMMED,
468+
EdgeKind::Dep(DepKind::Build) => {
469+
anstyle::AnsiColor::Blue.on_default() | anstyle::Effects::BOLD
470+
}
471+
EdgeKind::Dep(DepKind::Development) => {
472+
anstyle::AnsiColor::Cyan.on_default() | anstyle::Effects::BOLD
473+
}
474+
EdgeKind::Feature => anstyle::AnsiColor::Magenta.on_default() | anstyle::Effects::DIMMED,
475+
}
476+
}

tests/testsuite/tree.rs renamed to tests/testsuite/cargo_tree/deps.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use cargo_test_support::registry::{Dependency, Package};
66
use cargo_test_support::str;
77
use cargo_test_support::{basic_manifest, git, project, rustc_host, Project};
88

9-
use super::features2::switch_to_resolver_2;
9+
use crate::features2::switch_to_resolver_2;
1010

1111
fn make_simple_proj() -> Project {
1212
Package::new("c", "1.0.0").publish();
+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
use cargo_test_support::file;
2+
use cargo_test_support::prelude::*;
3+
use cargo_test_support::project;
4+
use cargo_test_support::registry::Package;
5+
6+
#[cargo_test]
7+
fn case() {
8+
Package::new("a", "1.0.0").dep("b", "1.0").publish();
9+
Package::new("b", "1.0.0").dep("c", "1.0").publish();
10+
Package::new("c", "1.0.0").publish();
11+
12+
let p = project()
13+
.file(
14+
"Cargo.toml",
15+
r#"
16+
[package]
17+
name = "foo"
18+
version = "0.1.0"
19+
20+
[dependencies]
21+
a = "1.0"
22+
b = "1.0"
23+
"#,
24+
)
25+
.file("src/lib.rs", "")
26+
.file("build.rs", "fn main() {}")
27+
.build();
28+
29+
snapbox::cmd::Command::cargo_ui()
30+
.arg("tree")
31+
.current_dir(p.root())
32+
.assert()
33+
.success()
34+
.stdout_eq(file!["stdout.term.svg"])
35+
.stderr_eq(file!["stderr.term.svg"]);
36+
}
Loading
Loading
+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
use cargo_test_support::file;
2+
use cargo_test_support::prelude::*;
3+
use cargo_test_support::project;
4+
use cargo_test_support::registry::Package;
5+
6+
#[cargo_test]
7+
fn case() {
8+
Package::new("normal_a", "1.0.0")
9+
.dep("normal_b", "1.0")
10+
.publish();
11+
Package::new("normal_b", "1.0.0")
12+
.dep("normal_c", "1.0")
13+
.build_dep("normal_b_build_a", "1.0.0")
14+
.dev_dep("normal_b_dev_a", "1.0.0")
15+
.publish();
16+
Package::new("normal_c", "1.0.0").publish();
17+
Package::new("normal_b_build_a", "1.0.0")
18+
.dep("normal_b_build_a_normal_a", "1.0.0")
19+
.publish();
20+
Package::new("normal_b_build_a_normal_a", "1.0.0").publish();
21+
Package::new("normal_b_dev_a", "1.0.0")
22+
.dep("normal_b_dev_a_normal_a", "1.0.0")
23+
.publish();
24+
Package::new("normal_b_dev_a_normal_a", "1.0.0").publish();
25+
Package::new("normal_d", "1.0.0").publish();
26+
27+
Package::new("build_a", "1.0.0")
28+
.dep("build_b", "1.0")
29+
.publish();
30+
Package::new("build_b", "1.0.0")
31+
.dep("build_c", "1.0")
32+
.build_dep("build_b_build_a", "1.0.0")
33+
.dev_dep("build_b_dev_a", "1.0.0")
34+
.publish();
35+
Package::new("build_c", "1.0.0").publish();
36+
Package::new("build_b_build_a", "1.0.0")
37+
.dep("build_b_build_a_normal_a", "1.0.0")
38+
.publish();
39+
Package::new("build_b_build_a_normal_a", "1.0.0").publish();
40+
Package::new("build_b_dev_a", "1.0.0")
41+
.dep("build_b_dev_a_normal_a", "1.0.0")
42+
.publish();
43+
Package::new("build_b_dev_a_normal_a", "1.0.0").publish();
44+
Package::new("build_d", "1.0.0").publish();
45+
46+
Package::new("dev_a", "1.0.0").dep("dev_b", "1.0").publish();
47+
Package::new("dev_b", "1.0.0")
48+
.dep("dev_c", "1.0")
49+
.build_dep("dev_b_build_a", "1.0.0")
50+
.dev_dep("dev_b_dev_a", "1.0.0")
51+
.publish();
52+
Package::new("dev_c", "1.0.0").publish();
53+
Package::new("dev_b_build_a", "1.0.0")
54+
.dep("dev_b_build_a_normal_a", "1.0.0")
55+
.publish();
56+
Package::new("dev_b_build_a_normal_a", "1.0.0").publish();
57+
Package::new("dev_b_dev_a", "1.0.0")
58+
.dep("dev_b_dev_a_normal_a", "1.0.0")
59+
.publish();
60+
Package::new("dev_b_dev_a_normal_a", "1.0.0").publish();
61+
Package::new("dev_d", "1.0.0").publish();
62+
63+
let p = project()
64+
.file(
65+
"Cargo.toml",
66+
r#"
67+
[package]
68+
name = "foo"
69+
version = "0.1.0"
70+
71+
[features]
72+
default = ["foo"]
73+
foo = ["dep:normal_a"]
74+
75+
[dependencies]
76+
normal_a = { version = "1.0", optional = true }
77+
normal_d = "1.0"
78+
79+
[build-dependencies]
80+
build_a = "1.0"
81+
build_d = "1.0"
82+
83+
[dev-dependencies]
84+
dev_a = "1.0"
85+
dev_d = "1.0"
86+
"#,
87+
)
88+
.file("src/lib.rs", "")
89+
.file("build.rs", "fn main() {}")
90+
.build();
91+
92+
snapbox::cmd::Command::cargo_ui()
93+
.arg("tree")
94+
.arg("--edges=features")
95+
.current_dir(p.root())
96+
.assert()
97+
.success()
98+
.stdout_eq(file!["stdout.term.svg"])
99+
.stderr_eq(file!["stderr.term.svg"]);
100+
}
Loading

0 commit comments

Comments
 (0)