···11+use std::sync::OnceLock;
22+33+static NO_COLOR: OnceLock<bool> = OnceLock::new();
44+55+/// Initialize the color configuration based on CLI flag and environment.
66+/// Should be called once at startup.
77+pub fn init(cli_no_color: bool) {
88+ let no_color = cli_no_color || should_disable_color_from_env();
99+ NO_COLOR.set(no_color).ok();
1010+}
1111+1212+/// Returns true if colors should be disabled.
1313+pub fn no_color() -> bool {
1414+ *NO_COLOR.get().unwrap_or(&false)
1515+}
1616+1717+/// Returns true if colors are enabled.
1818+pub fn color_enabled() -> bool {
1919+ !no_color()
2020+}
2121+2222+/// Check environment variables to determine if color should be disabled.
2323+fn should_disable_color_from_env() -> bool {
2424+ // NO_COLOR standard: https://no-color.org/
2525+ // If NO_COLOR exists (with any value), disable color
2626+ if std::env::var("NO_COLOR").is_ok() {
2727+ return true;
2828+ }
2929+3030+ // Generic CI detection
3131+ if std::env::var("CI").is_ok_and(|v| v == "true") {
3232+ return true;
3333+ }
3434+3535+ // GitHub actions
3636+ if std::env::var("GITHUB_ACTIONS").is_ok_and(|v| v == "true") {
3737+ return true;
3838+ }
3939+4040+ // GitLab CI
4141+ if std::env::var("GITLAB_CI").is_ok() {
4242+ return true;
4343+ }
4444+4545+ // Travis CI
4646+ if std::env::var("TRAVIS").is_ok_and(|v| v == "true") {
4747+ return true;
4848+ }
4949+5050+ // Jenkins
5151+ if std::env::var("JENKINS_URL").is_ok() {
5252+ return true;
5353+ }
5454+5555+ // Check if output is not a TTY (piped)
5656+ // This is a common convention for disabling colors
5757+ if std::env::var("TERM").is_ok_and(|v| v == "dumb") {
5858+ return true;
5959+ }
6060+6161+ false
6262+}
+51-24
src/diff.rs
···33use terminal_light::luma;
4455use super::{
66+ color,
67 package::{DiffType, Package, SizeDelta},
78 parser::DiffRoot,
89};
···9394 }
94959596 fn display_by_category(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
9696- let text_color = if luma().is_ok_and(|luma| luma > 0.6) {
9797- Color::DarkGray
9898- } else {
9999- Color::LightGray
100100- };
101101- let title_style = nu_ansi_term::Style::new().underline().bold().fg(text_color);
9797+ let name_width = self.longest_name + 2;
10298103103- let name_width = self.longest_name + 2;
9999+ if color::color_enabled() {
100100+ let text_color = if luma().is_ok_and(|luma| luma > 0.6) {
101101+ Color::DarkGray
102102+ } else {
103103+ Color::LightGray
104104+ };
105105+ let title_style = nu_ansi_term::Style::new().underline().bold().fg(text_color);
104106105105- if !self.changed.is_empty() {
106106- writeln!(f, "{}", &title_style.paint("Changed"))?;
107107- for (name, package) in &self.changed {
108108- writeln!(f, "[{}] {name:name_width$}{package}", Yellow.paint("C"))?;
107107+ if !self.changed.is_empty() {
108108+ writeln!(f, "{}", &title_style.paint("Changed"))?;
109109+ for (name, package) in &self.changed {
110110+ writeln!(f, "[{}] {name:name_width$}{package}", Yellow.paint("C"))?;
111111+ }
112112+ writeln!(f)?;
109113 }
110110- writeln!(f)?;
111111- }
112114113113- if !self.added.is_empty() {
114114- writeln!(f, "{}", &title_style.paint("Added"))?;
115115- for (name, package) in &self.added {
116116- write!(f, "[{}] {name:name_width$}{package}", Green.paint("A"))?;
115115+ if !self.added.is_empty() {
116116+ writeln!(f, "{}", &title_style.paint("Added"))?;
117117+ for (name, package) in &self.added {
118118+ write!(f, "[{}] {name:name_width$}{package}", Green.paint("A"))?;
119119+ writeln!(f)?;
120120+ }
117121 writeln!(f)?;
118122 }
119119- writeln!(f)?;
120120- }
123123+124124+ if !self.removed.is_empty() {
125125+ writeln!(f, "{}", &title_style.paint("Removed"))?;
126126+ for (name, package) in &self.removed {
127127+ writeln!(f, "[{}] {name:name_width$}{package}", Red.paint("R"))?;
128128+ }
129129+ writeln!(f)?;
130130+ }
131131+ } else {
132132+ if !self.changed.is_empty() {
133133+ writeln!(f, "Changed")?;
134134+ for (name, package) in &self.changed {
135135+ writeln!(f, "[C] {name:name_width$}{package}")?;
136136+ }
137137+ writeln!(f)?;
138138+ }
139139+140140+ if !self.added.is_empty() {
141141+ writeln!(f, "Added")?;
142142+ for (name, package) in &self.added {
143143+ writeln!(f, "[A] {name:name_width$}{package}")?;
144144+ }
145145+ writeln!(f)?;
146146+ }
121147122122- if !self.removed.is_empty() {
123123- writeln!(f, "{}", &title_style.paint("Removed"))?;
124124- for (name, package) in &self.removed {
125125- writeln!(f, "[{}] {name:name_width$}{package}", Red.paint("R"))?;
148148+ if !self.removed.is_empty() {
149149+ writeln!(f, "Removed")?;
150150+ for (name, package) in &self.removed {
151151+ writeln!(f, "[R] {name:name_width$}{package}")?;
152152+ }
153153+ writeln!(f)?;
126154 }
127127- writeln!(f)?;
128155 }
129156130157 Ok(())
+22-9
src/main.rs
···66use nu_ansi_term::{Color, Style};
77use terminal_light::luma;
8899+mod color;
910mod diff;
1011mod package;
1112mod parser;
···3031 /// sort by size difference
3132 #[arg(short, long)]
3233 size: bool,
3434+3535+ /// disable colored output (also respects NO_COLOR env var and CI environments)
3636+ #[arg(long)]
3737+ no_color: bool,
3338}
34393540fn main() -> Result<()> {
3641 let args = Args::parse();
4242+4343+ // Initialize color configuration
4444+ color::init(args.no_color);
4545+3746 let before = args.before;
3847 let after = args.after;
3948 let lix_bin = args.lix_bin;
···6170 let mut packages: PackageListDiff = PackageListDiff::new();
6271 packages.by_size = args.size;
6372 packages.from_diff_root(packages_diff);
6464-6565- let text_color = if luma().is_ok_and(|luma| luma > 0.6) {
6666- Color::DarkGray
6767- } else {
6868- Color::LightGray
6969- };
7070- let arrow_style = Style::new().bold().fg(text_color);
71737274 let before_text = format!("<<< {}", before.display());
7375 let after_text = format!(">>> {}", after.display());
74767575- println!("{}", arrow_style.paint(before_text));
7676- println!("{}\n", arrow_style.paint(after_text));
7777+ if color::color_enabled() {
7878+ let text_color = if luma().is_ok_and(|luma| luma > 0.6) {
7979+ Color::DarkGray
8080+ } else {
8181+ Color::LightGray
8282+ };
8383+ let arrow_style = Style::new().bold().fg(text_color);
8484+ println!("{}", arrow_style.paint(&before_text));
8585+ println!("{}\n", arrow_style.paint(&after_text));
8686+ } else {
8787+ println!("{before_text}");
8888+ println!("{after_text}\n");
8989+ }
77907891 println!("{packages}");
7992
+11-5
src/versioning.rs
···11use nu_ansi_term::Color::{Green, Red, Yellow};
22use std::{cmp::Ordering, fmt::Display};
3344+use super::color;
55+46#[derive(Debug)]
57pub struct VersionComponent(String, Ordering);
68···3436 let val = &component.0;
3537 let cmp = component.1;
36383737- let text = if cmp == Ordering::Less {
3838- format!("{}", Red.paint(val))
3939- } else if cmp == Ordering::Greater {
4040- format!("{}", Green.paint(val))
3939+ let text = if color::color_enabled() {
4040+ if cmp == Ordering::Less {
4141+ format!("{}", Red.paint(val))
4242+ } else if cmp == Ordering::Greater {
4343+ format!("{}", Green.paint(val))
4444+ } else {
4545+ format!("{}", Yellow.paint(val))
4646+ }
4147 } else {
4242- format!("{}", Yellow.paint(val))
4848+ val.clone()
4349 };
44504551 out.push_str(&text);