vtcode_commons/
ansi_capabilities.rs1use crate::color_policy::no_color_env_active;
4use std::io::IsTerminal;
5use std::sync::LazyLock;
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 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 color_depth: ColorDepth,
79 pub unicode_support: bool,
84 pub force_color: bool,
86 pub no_color: bool,
88}
89
90impl AnsiCapabilities {
91 pub fn detect() -> Self {
93 Self {
94 color_depth: detect_color_depth(),
95 unicode_support: detect_unicode_support(),
96 force_color: clicolor_force(),
97 no_color: no_color_env_active(),
98 }
99 }
100
101 pub fn supports_color(&self) -> bool {
103 !self.no_color && (self.force_color || self.color_depth.supports_color())
104 }
105
106 pub fn supports_256_colors(&self) -> bool {
108 self.supports_color() && self.color_depth.supports_256()
109 }
110
111 pub fn supports_true_color(&self) -> bool {
113 self.supports_color() && self.color_depth.supports_true_color()
114 }
115}
116
117#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
119pub enum ColorScheme {
120 Light,
122 #[default]
124 Dark,
125 Unknown,
127}
128
129impl ColorScheme {
130 pub fn is_light(self) -> bool {
132 matches!(self, ColorScheme::Light)
133 }
134
135 pub fn is_dark(self) -> bool {
137 matches!(self, ColorScheme::Dark | ColorScheme::Unknown)
138 }
139
140 pub fn name(self) -> &'static str {
142 match self {
143 ColorScheme::Light => "light",
144 ColorScheme::Dark => "dark",
145 ColorScheme::Unknown => "unknown",
146 }
147 }
148}
149
150const COLOR_SCHEME_LIGHT: u8 = 0;
151const COLOR_SCHEME_DARK: u8 = 1;
152const COLOR_SCHEME_UNKNOWN: u8 = 2;
153const COLOR_SCHEME_UNSET: u8 = 255;
154
155static COLOR_SCHEME_RUNTIME_OVERRIDE: AtomicU8 = AtomicU8::new(COLOR_SCHEME_UNSET);
156
157pub fn detect_color_scheme() -> ColorScheme {
159 if let Some(override_scheme) = color_scheme_runtime_override() {
160 return override_scheme;
161 }
162
163 static CACHED: LazyLock<ColorScheme> = LazyLock::new(detect_color_scheme_uncached);
165 *CACHED
166}
167
168fn color_scheme_runtime_override() -> Option<ColorScheme> {
169 match COLOR_SCHEME_RUNTIME_OVERRIDE.load(Ordering::Relaxed) {
170 COLOR_SCHEME_LIGHT => Some(ColorScheme::Light),
171 COLOR_SCHEME_DARK => Some(ColorScheme::Dark),
172 COLOR_SCHEME_UNKNOWN => Some(ColorScheme::Unknown),
173 _ => None,
174 }
175}
176
177pub fn set_color_scheme_override(value: Option<ColorScheme>) {
182 let encoded = match value {
183 Some(ColorScheme::Light) => COLOR_SCHEME_LIGHT,
184 Some(ColorScheme::Dark) => COLOR_SCHEME_DARK,
185 Some(ColorScheme::Unknown) => COLOR_SCHEME_UNKNOWN,
186 None => COLOR_SCHEME_UNSET,
187 };
188 COLOR_SCHEME_RUNTIME_OVERRIDE.store(encoded, Ordering::Relaxed);
189}
190
191fn detect_color_scheme_uncached() -> ColorScheme {
192 if let Ok(colorfgbg) = std::env::var("COLORFGBG") {
193 let parts: Vec<&str> = colorfgbg.split(';').collect();
194 if let Some(bg_str) = parts.last()
195 && let Ok(bg) = bg_str.parse::<u8>()
196 {
197 return if bg == 7 || bg == 15 {
198 ColorScheme::Light
199 } else if bg == 0 || bg == 8 {
200 ColorScheme::Dark
201 } else if bg > 230 {
202 ColorScheme::Light
203 } else {
204 ColorScheme::Dark
205 };
206 }
207 }
208
209 if let Ok(term_program) = std::env::var("TERM_PROGRAM") {
210 let term_lower = term_program.to_lowercase();
211 if term_lower.contains("iterm")
212 || term_lower.contains("ghostty")
213 || term_lower.contains("warp")
214 || term_lower.contains("alacritty")
215 {
216 return ColorScheme::Dark;
217 }
218 }
219
220 if cfg!(target_os = "macos")
221 && let Ok(term_program) = std::env::var("TERM_PROGRAM")
222 && term_program == "Apple_Terminal"
223 {
224 return ColorScheme::Light;
225 }
226
227 ColorScheme::Unknown
228}
229
230static COLOR_DEPTH_CACHE: AtomicU8 = AtomicU8::new(255); fn detect_color_depth() -> ColorDepth {
235 let cached = COLOR_DEPTH_CACHE.load(Ordering::Relaxed);
236 if cached != 255 {
237 return match cached {
238 0 => ColorDepth::None,
239 1 => ColorDepth::Basic16,
240 2 => ColorDepth::Color256,
241 3 => ColorDepth::TrueColor,
242 _ => ColorDepth::None,
243 };
244 }
245
246 let depth = if no_color_env_active() {
247 ColorDepth::None
248 } else if clicolor_force() {
249 ColorDepth::TrueColor
250 } else if !clicolor().unwrap_or_else(term_supports_color) {
251 ColorDepth::None
252 } else {
253 std::env::var("COLORTERM")
254 .ok()
255 .and_then(|val| {
256 let lower = val.to_lowercase();
257 if lower.contains("truecolor") || lower.contains("24bit") {
258 Some(ColorDepth::TrueColor)
259 } else {
260 None
261 }
262 })
263 .unwrap_or(ColorDepth::Color256)
264 };
265
266 COLOR_DEPTH_CACHE.store(
267 match depth {
268 ColorDepth::None => 0,
269 ColorDepth::Basic16 => 1,
270 ColorDepth::Color256 => 2,
271 ColorDepth::TrueColor => 3,
272 },
273 Ordering::Relaxed,
274 );
275
276 depth
277}
278
279fn detect_unicode_support() -> bool {
284 crate::terminal_capabilities::box_glyph_tier().supports_unicode()
285}
286
287pub static CAPABILITIES: LazyLock<AnsiCapabilities> = LazyLock::new(AnsiCapabilities::detect);
289
290pub fn is_no_color() -> bool {
292 no_color_env_active()
293}
294
295pub fn is_clicolor_force() -> bool {
297 clicolor_force()
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303
304 #[test]
305 fn unicode_support_follows_the_canonical_capability_probe() {
306 let tier = crate::terminal_capabilities::box_glyph_tier();
309 assert_eq!(AnsiCapabilities::detect().unicode_support, tier.supports_unicode());
310 assert_eq!(CAPABILITIES.unicode_support, tier.supports_unicode());
311 }
312}