vtcode_commons/
ansi_capabilities.rs1use crate::color_policy::no_color_env_active;
4use once_cell::sync::Lazy;
5use std::io::IsTerminal;
6use std::sync::atomic::{AtomicU8, Ordering};
7
8fn clicolor() -> Option<bool> {
10 match std::env::var("CLICOLOR") {
11 Ok(val) => Some(!val.is_empty() && val != "0"),
12 Err(_) => None,
13 }
14}
15
16fn clicolor_force() -> bool {
18 std::env::var("CLICOLOR_FORCE").is_ok_and(|val| !val.is_empty() && val != "0")
19}
20
21fn term_supports_color() -> bool {
23 if !std::io::stdout().is_terminal() {
24 return false;
25 }
26 if let Ok(term) = std::env::var("TERM") {
27 if term == "dumb" || term == "emacs" {
28 return false;
29 }
30 }
31 true
32}
33
34#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
36pub enum ColorDepth {
37 None = 0,
39 Basic16 = 1,
41 Color256 = 2,
43 TrueColor = 3,
45}
46
47impl ColorDepth {
48 pub fn name(self) -> &'static str {
50 match self {
51 ColorDepth::None => "none",
52 ColorDepth::Basic16 => "16-color",
53 ColorDepth::Color256 => "256-color",
54 ColorDepth::TrueColor => "true-color",
55 }
56 }
57
58 pub fn supports_color(self) -> bool {
60 self != ColorDepth::None
61 }
62
63 pub fn supports_256(self) -> bool {
65 self >= ColorDepth::Color256
66 }
67
68 pub fn supports_true_color(self) -> bool {
70 self == ColorDepth::TrueColor
71 }
72}
73
74#[derive(Clone, Copy, Debug)]
76pub struct AnsiCapabilities {
77 pub color_depth: ColorDepth,
79 pub unicode_support: bool,
81 pub force_color: bool,
83 pub no_color: bool,
85}
86
87impl AnsiCapabilities {
88 pub fn detect() -> Self {
90 Self {
91 color_depth: detect_color_depth(),
92 unicode_support: detect_unicode_support(),
93 force_color: clicolor_force(),
94 no_color: no_color_env_active(),
95 }
96 }
97
98 pub fn supports_color(&self) -> bool {
100 !self.no_color && (self.force_color || self.color_depth.supports_color())
101 }
102
103 pub fn supports_256_colors(&self) -> bool {
105 self.supports_color() && self.color_depth.supports_256()
106 }
107
108 pub fn supports_true_color(&self) -> bool {
110 self.supports_color() && self.color_depth.supports_true_color()
111 }
112
113 pub fn should_use_unicode_boxes(&self) -> bool {
115 self.unicode_support && self.supports_color()
116 }
117}
118
119#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
121pub enum ColorScheme {
122 Light,
124 #[default]
126 Dark,
127 Unknown,
129}
130
131impl ColorScheme {
132 pub fn is_light(self) -> bool {
134 matches!(self, ColorScheme::Light)
135 }
136
137 pub fn is_dark(self) -> bool {
139 matches!(self, ColorScheme::Dark | ColorScheme::Unknown)
140 }
141
142 pub fn name(self) -> &'static str {
144 match self {
145 ColorScheme::Light => "light",
146 ColorScheme::Dark => "dark",
147 ColorScheme::Unknown => "unknown",
148 }
149 }
150}
151
152const COLOR_SCHEME_LIGHT: u8 = 0;
153const COLOR_SCHEME_DARK: u8 = 1;
154const COLOR_SCHEME_UNKNOWN: u8 = 2;
155const COLOR_SCHEME_UNSET: u8 = 255;
156
157static COLOR_SCHEME_RUNTIME_OVERRIDE: AtomicU8 = AtomicU8::new(COLOR_SCHEME_UNSET);
158
159pub fn detect_color_scheme() -> ColorScheme {
161 if let Some(override_scheme) = color_scheme_runtime_override() {
162 return override_scheme;
163 }
164
165 static CACHED: Lazy<ColorScheme> = Lazy::new(detect_color_scheme_uncached);
167 *CACHED
168}
169
170fn color_scheme_runtime_override() -> Option<ColorScheme> {
171 match COLOR_SCHEME_RUNTIME_OVERRIDE.load(Ordering::Relaxed) {
172 COLOR_SCHEME_LIGHT => Some(ColorScheme::Light),
173 COLOR_SCHEME_DARK => Some(ColorScheme::Dark),
174 COLOR_SCHEME_UNKNOWN => Some(ColorScheme::Unknown),
175 _ => None,
176 }
177}
178
179pub fn set_color_scheme_override(value: Option<ColorScheme>) {
184 let encoded = match value {
185 Some(ColorScheme::Light) => COLOR_SCHEME_LIGHT,
186 Some(ColorScheme::Dark) => COLOR_SCHEME_DARK,
187 Some(ColorScheme::Unknown) => COLOR_SCHEME_UNKNOWN,
188 None => COLOR_SCHEME_UNSET,
189 };
190 COLOR_SCHEME_RUNTIME_OVERRIDE.store(encoded, Ordering::Relaxed);
191}
192
193fn detect_color_scheme_uncached() -> ColorScheme {
194 if let Ok(colorfgbg) = std::env::var("COLORFGBG") {
195 let parts: Vec<&str> = colorfgbg.split(';').collect();
196 if let Some(bg_str) = parts.last()
197 && let Ok(bg) = bg_str.parse::<u8>()
198 {
199 return if bg == 7 || bg == 15 {
200 ColorScheme::Light
201 } else if bg == 0 || bg == 8 {
202 ColorScheme::Dark
203 } else if bg > 230 {
204 ColorScheme::Light
205 } else {
206 ColorScheme::Dark
207 };
208 }
209 }
210
211 if let Ok(term_program) = std::env::var("TERM_PROGRAM") {
212 let term_lower = term_program.to_lowercase();
213 if term_lower.contains("iterm")
214 || term_lower.contains("ghostty")
215 || term_lower.contains("warp")
216 || term_lower.contains("alacritty")
217 {
218 return ColorScheme::Dark;
219 }
220 }
221
222 if cfg!(target_os = "macos")
223 && let Ok(term_program) = std::env::var("TERM_PROGRAM")
224 && term_program == "Apple_Terminal"
225 {
226 return ColorScheme::Light;
227 }
228
229 ColorScheme::Unknown
230}
231
232static COLOR_DEPTH_CACHE: AtomicU8 = AtomicU8::new(255); fn detect_color_depth() -> ColorDepth {
237 let cached = COLOR_DEPTH_CACHE.load(Ordering::Relaxed);
238 if cached != 255 {
239 return match cached {
240 0 => ColorDepth::None,
241 1 => ColorDepth::Basic16,
242 2 => ColorDepth::Color256,
243 3 => ColorDepth::TrueColor,
244 _ => ColorDepth::None,
245 };
246 }
247
248 let depth = if no_color_env_active() {
249 ColorDepth::None
250 } else if clicolor_force() {
251 ColorDepth::TrueColor
252 } else if !clicolor().unwrap_or_else(term_supports_color) {
253 ColorDepth::None
254 } else {
255 std::env::var("COLORTERM")
256 .ok()
257 .and_then(|val| {
258 let lower = val.to_lowercase();
259 if lower.contains("truecolor") || lower.contains("24bit") {
260 Some(ColorDepth::TrueColor)
261 } else {
262 None
263 }
264 })
265 .unwrap_or(ColorDepth::Color256)
266 };
267
268 COLOR_DEPTH_CACHE.store(
269 match depth {
270 ColorDepth::None => 0,
271 ColorDepth::Basic16 => 1,
272 ColorDepth::Color256 => 2,
273 ColorDepth::TrueColor => 3,
274 },
275 Ordering::Relaxed,
276 );
277
278 depth
279}
280
281fn detect_unicode_support() -> bool {
283 std::env::var("LANG")
284 .ok()
285 .map(|lang| lang.to_lowercase().contains("utf"))
286 .or_else(|| std::env::var("LC_ALL").ok().map(|lc| lc.to_lowercase().contains("utf")))
287 .unwrap_or(true)
288}
289
290pub static CAPABILITIES: Lazy<AnsiCapabilities> = Lazy::new(AnsiCapabilities::detect);
292
293pub fn is_no_color() -> bool {
295 no_color_env_active()
296}
297
298pub fn is_clicolor_force() -> bool {
300 clicolor_force()
301}