vtcode_commons/
ansi_capabilities.rs1use crate::color_policy::no_color_env_active;
4use anstyle_query::{clicolor, clicolor_force, term_supports_color};
5use once_cell::sync::Lazy;
6use std::sync::atomic::{AtomicU8, Ordering};
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
10pub enum ColorDepth {
11 None = 0,
13 Basic16 = 1,
15 Color256 = 2,
17 TrueColor = 3,
19}
20
21impl ColorDepth {
22 pub fn name(self) -> &'static str {
24 match self {
25 ColorDepth::None => "none",
26 ColorDepth::Basic16 => "16-color",
27 ColorDepth::Color256 => "256-color",
28 ColorDepth::TrueColor => "true-color",
29 }
30 }
31
32 pub fn supports_color(self) -> bool {
34 self != ColorDepth::None
35 }
36
37 pub fn supports_256(self) -> bool {
39 self >= ColorDepth::Color256
40 }
41
42 pub fn supports_true_color(self) -> bool {
44 self == ColorDepth::TrueColor
45 }
46}
47
48#[derive(Clone, Copy, Debug)]
50pub struct AnsiCapabilities {
51 pub color_depth: ColorDepth,
53 pub unicode_support: bool,
55 pub force_color: bool,
57 pub no_color: bool,
59}
60
61impl AnsiCapabilities {
62 pub fn detect() -> Self {
64 Self {
65 color_depth: detect_color_depth(),
66 unicode_support: detect_unicode_support(),
67 force_color: clicolor_force(),
68 no_color: no_color_env_active(),
69 }
70 }
71
72 pub fn supports_color(&self) -> bool {
74 !self.no_color && (self.force_color || self.color_depth.supports_color())
75 }
76
77 pub fn supports_256_colors(&self) -> bool {
79 self.supports_color() && self.color_depth.supports_256()
80 }
81
82 pub fn supports_true_color(&self) -> bool {
84 self.supports_color() && self.color_depth.supports_true_color()
85 }
86
87 pub fn should_use_unicode_boxes(&self) -> bool {
89 self.unicode_support && self.supports_color()
90 }
91}
92
93#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
95pub enum ColorScheme {
96 Light,
98 #[default]
100 Dark,
101 Unknown,
103}
104
105impl ColorScheme {
106 pub fn is_light(self) -> bool {
108 matches!(self, ColorScheme::Light)
109 }
110
111 pub fn is_dark(self) -> bool {
113 matches!(self, ColorScheme::Dark | ColorScheme::Unknown)
114 }
115
116 pub fn name(self) -> &'static str {
118 match self {
119 ColorScheme::Light => "light",
120 ColorScheme::Dark => "dark",
121 ColorScheme::Unknown => "unknown",
122 }
123 }
124}
125
126const COLOR_SCHEME_LIGHT: u8 = 0;
127const COLOR_SCHEME_DARK: u8 = 1;
128const COLOR_SCHEME_UNKNOWN: u8 = 2;
129const COLOR_SCHEME_UNSET: u8 = 255;
130
131static COLOR_SCHEME_RUNTIME_OVERRIDE: AtomicU8 = AtomicU8::new(COLOR_SCHEME_UNSET);
132
133pub fn detect_color_scheme() -> ColorScheme {
135 if let Some(override_scheme) = color_scheme_runtime_override() {
136 return override_scheme;
137 }
138
139 static CACHED: Lazy<ColorScheme> = Lazy::new(detect_color_scheme_uncached);
141 *CACHED
142}
143
144fn color_scheme_runtime_override() -> Option<ColorScheme> {
145 match COLOR_SCHEME_RUNTIME_OVERRIDE.load(Ordering::Relaxed) {
146 COLOR_SCHEME_LIGHT => Some(ColorScheme::Light),
147 COLOR_SCHEME_DARK => Some(ColorScheme::Dark),
148 COLOR_SCHEME_UNKNOWN => Some(ColorScheme::Unknown),
149 _ => None,
150 }
151}
152
153pub fn set_color_scheme_override(value: Option<ColorScheme>) {
158 let encoded = match value {
159 Some(ColorScheme::Light) => COLOR_SCHEME_LIGHT,
160 Some(ColorScheme::Dark) => COLOR_SCHEME_DARK,
161 Some(ColorScheme::Unknown) => COLOR_SCHEME_UNKNOWN,
162 None => COLOR_SCHEME_UNSET,
163 };
164 COLOR_SCHEME_RUNTIME_OVERRIDE.store(encoded, Ordering::Relaxed);
165}
166
167fn detect_color_scheme_uncached() -> ColorScheme {
168 if let Ok(colorfgbg) = std::env::var("COLORFGBG") {
169 let parts: Vec<&str> = colorfgbg.split(';').collect();
170 if let Some(bg_str) = parts.last()
171 && let Ok(bg) = bg_str.parse::<u8>()
172 {
173 return if bg == 7 || bg == 15 {
174 ColorScheme::Light
175 } else if bg == 0 || bg == 8 {
176 ColorScheme::Dark
177 } else if bg > 230 {
178 ColorScheme::Light
179 } else {
180 ColorScheme::Dark
181 };
182 }
183 }
184
185 if let Ok(term_program) = std::env::var("TERM_PROGRAM") {
186 let term_lower = term_program.to_lowercase();
187 if term_lower.contains("iterm")
188 || term_lower.contains("ghostty")
189 || term_lower.contains("warp")
190 || term_lower.contains("alacritty")
191 {
192 return ColorScheme::Dark;
193 }
194 }
195
196 if cfg!(target_os = "macos")
197 && let Ok(term_program) = std::env::var("TERM_PROGRAM")
198 && term_program == "Apple_Terminal"
199 {
200 return ColorScheme::Light;
201 }
202
203 ColorScheme::Unknown
204}
205
206static COLOR_DEPTH_CACHE: AtomicU8 = AtomicU8::new(255); fn detect_color_depth() -> ColorDepth {
211 let cached = COLOR_DEPTH_CACHE.load(Ordering::Relaxed);
212 if cached != 255 {
213 return match cached {
214 0 => ColorDepth::None,
215 1 => ColorDepth::Basic16,
216 2 => ColorDepth::Color256,
217 3 => ColorDepth::TrueColor,
218 _ => ColorDepth::None,
219 };
220 }
221
222 let depth = if no_color_env_active() {
223 ColorDepth::None
224 } else if clicolor_force() {
225 ColorDepth::TrueColor
226 } else if !clicolor().unwrap_or_else(term_supports_color) {
227 ColorDepth::None
228 } else {
229 std::env::var("COLORTERM")
230 .ok()
231 .and_then(|val| {
232 let lower = val.to_lowercase();
233 if lower.contains("truecolor") || lower.contains("24bit") {
234 Some(ColorDepth::TrueColor)
235 } else {
236 None
237 }
238 })
239 .unwrap_or(ColorDepth::Color256)
240 };
241
242 COLOR_DEPTH_CACHE.store(
243 match depth {
244 ColorDepth::None => 0,
245 ColorDepth::Basic16 => 1,
246 ColorDepth::Color256 => 2,
247 ColorDepth::TrueColor => 3,
248 },
249 Ordering::Relaxed,
250 );
251
252 depth
253}
254
255fn detect_unicode_support() -> bool {
257 std::env::var("LANG")
258 .ok()
259 .map(|lang| lang.to_lowercase().contains("utf"))
260 .or_else(|| {
261 std::env::var("LC_ALL")
262 .ok()
263 .map(|lc| lc.to_lowercase().contains("utf"))
264 })
265 .unwrap_or(true)
266}
267
268pub static CAPABILITIES: Lazy<AnsiCapabilities> = Lazy::new(AnsiCapabilities::detect);
270
271pub fn is_no_color() -> bool {
273 no_color_env_active()
274}
275
276pub fn is_clicolor_force() -> bool {
278 clicolor_force()
279}