vtcode_commons/
ansi_capabilities.rs1use anstyle_query::{clicolor, clicolor_force, no_color, term_supports_color};
4use once_cell::sync::Lazy;
5use std::sync::atomic::{AtomicU8, Ordering};
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
9pub enum ColorDepth {
10 None = 0,
12 Basic16 = 1,
14 Color256 = 2,
16 TrueColor = 3,
18}
19
20impl ColorDepth {
21 pub fn name(self) -> &'static str {
23 match self {
24 ColorDepth::None => "none",
25 ColorDepth::Basic16 => "16-color",
26 ColorDepth::Color256 => "256-color",
27 ColorDepth::TrueColor => "true-color",
28 }
29 }
30
31 pub fn supports_color(self) -> bool {
33 self != ColorDepth::None
34 }
35
36 pub fn supports_256(self) -> bool {
38 self >= ColorDepth::Color256
39 }
40
41 pub fn supports_true_color(self) -> bool {
43 self == ColorDepth::TrueColor
44 }
45}
46
47#[derive(Clone, Copy, Debug)]
49pub struct AnsiCapabilities {
50 pub color_depth: ColorDepth,
52 pub unicode_support: bool,
54 pub force_color: bool,
56 pub no_color: bool,
58}
59
60impl AnsiCapabilities {
61 pub fn detect() -> Self {
63 Self {
64 color_depth: detect_color_depth(),
65 unicode_support: detect_unicode_support(),
66 force_color: clicolor_force(),
67 no_color: no_color(),
68 }
69 }
70
71 pub fn supports_color(&self) -> bool {
73 !self.no_color && (self.force_color || self.color_depth.supports_color())
74 }
75
76 pub fn supports_256_colors(&self) -> bool {
78 self.supports_color() && self.color_depth.supports_256()
79 }
80
81 pub fn supports_true_color(&self) -> bool {
83 self.supports_color() && self.color_depth.supports_true_color()
84 }
85
86 pub fn should_use_unicode_boxes(&self) -> bool {
88 self.unicode_support && self.supports_color()
89 }
90}
91
92#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
94pub enum ColorScheme {
95 Light,
97 #[default]
99 Dark,
100 Unknown,
102}
103
104impl ColorScheme {
105 pub fn is_light(self) -> bool {
107 matches!(self, ColorScheme::Light)
108 }
109
110 pub fn is_dark(self) -> bool {
112 matches!(self, ColorScheme::Dark | ColorScheme::Unknown)
113 }
114
115 pub fn name(self) -> &'static str {
117 match self {
118 ColorScheme::Light => "light",
119 ColorScheme::Dark => "dark",
120 ColorScheme::Unknown => "unknown",
121 }
122 }
123}
124
125pub fn detect_color_scheme() -> ColorScheme {
127 static CACHED: Lazy<ColorScheme> = Lazy::new(detect_color_scheme_uncached);
129 *CACHED
130}
131
132fn detect_color_scheme_uncached() -> ColorScheme {
133 if let Ok(colorfgbg) = std::env::var("COLORFGBG") {
134 let parts: Vec<&str> = colorfgbg.split(';').collect();
135 if let Some(bg_str) = parts.last()
136 && let Ok(bg) = bg_str.parse::<u8>()
137 {
138 return if bg == 7 || bg == 15 {
139 ColorScheme::Light
140 } else if bg == 0 || bg == 8 {
141 ColorScheme::Dark
142 } else if bg > 230 {
143 ColorScheme::Light
144 } else {
145 ColorScheme::Dark
146 };
147 }
148 }
149
150 if let Ok(term_program) = std::env::var("TERM_PROGRAM") {
151 let term_lower = term_program.to_lowercase();
152 if term_lower.contains("iterm")
153 || term_lower.contains("ghostty")
154 || term_lower.contains("warp")
155 || term_lower.contains("alacritty")
156 {
157 return ColorScheme::Dark;
158 }
159 }
160
161 if cfg!(target_os = "macos")
162 && let Ok(term_program) = std::env::var("TERM_PROGRAM")
163 && term_program == "Apple_Terminal"
164 {
165 return ColorScheme::Light;
166 }
167
168 ColorScheme::Unknown
169}
170
171static COLOR_DEPTH_CACHE: AtomicU8 = AtomicU8::new(255); fn detect_color_depth() -> ColorDepth {
176 let cached = COLOR_DEPTH_CACHE.load(Ordering::Relaxed);
177 if cached != 255 {
178 return match cached {
179 0 => ColorDepth::None,
180 1 => ColorDepth::Basic16,
181 2 => ColorDepth::Color256,
182 3 => ColorDepth::TrueColor,
183 _ => ColorDepth::None,
184 };
185 }
186
187 let depth = if no_color() {
188 ColorDepth::None
189 } else if clicolor_force() {
190 ColorDepth::TrueColor
191 } else if !clicolor().unwrap_or_else(term_supports_color) {
192 ColorDepth::None
193 } else {
194 std::env::var("COLORTERM")
195 .ok()
196 .and_then(|val| {
197 let lower = val.to_lowercase();
198 if lower.contains("truecolor") || lower.contains("24bit") {
199 Some(ColorDepth::TrueColor)
200 } else {
201 None
202 }
203 })
204 .unwrap_or(ColorDepth::Color256)
205 };
206
207 COLOR_DEPTH_CACHE.store(
208 match depth {
209 ColorDepth::None => 0,
210 ColorDepth::Basic16 => 1,
211 ColorDepth::Color256 => 2,
212 ColorDepth::TrueColor => 3,
213 },
214 Ordering::Relaxed,
215 );
216
217 depth
218}
219
220fn detect_unicode_support() -> bool {
222 std::env::var("LANG")
223 .ok()
224 .map(|lang| lang.to_lowercase().contains("utf"))
225 .or_else(|| {
226 std::env::var("LC_ALL")
227 .ok()
228 .map(|lc| lc.to_lowercase().contains("utf"))
229 })
230 .unwrap_or(true)
231}
232
233pub static CAPABILITIES: Lazy<AnsiCapabilities> = Lazy::new(AnsiCapabilities::detect);
235
236pub fn is_no_color() -> bool {
238 no_color()
239}
240
241pub fn is_clicolor_force() -> bool {
243 clicolor_force()
244}