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 if let Ok(bg) = bg_str.parse::<u8>() {
137 return if bg == 7 || bg == 15 {
138 ColorScheme::Light
139 } else if bg == 0 || bg == 8 {
140 ColorScheme::Dark
141 } else if bg > 230 {
142 ColorScheme::Light
143 } else {
144 ColorScheme::Dark
145 };
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 if let Ok(term_program) = std::env::var("TERM_PROGRAM") {
163 if term_program == "Apple_Terminal" {
164 return ColorScheme::Light;
165 }
166 }
167 }
168
169 ColorScheme::Unknown
170}
171
172static COLOR_DEPTH_CACHE: AtomicU8 = AtomicU8::new(255); fn detect_color_depth() -> ColorDepth {
177 let cached = COLOR_DEPTH_CACHE.load(Ordering::Relaxed);
178 if cached != 255 {
179 return match cached {
180 0 => ColorDepth::None,
181 1 => ColorDepth::Basic16,
182 2 => ColorDepth::Color256,
183 3 => ColorDepth::TrueColor,
184 _ => ColorDepth::None,
185 };
186 }
187
188 let depth = if no_color() {
189 ColorDepth::None
190 } else if clicolor_force() {
191 ColorDepth::TrueColor
192 } else if !clicolor().unwrap_or_else(term_supports_color) {
193 ColorDepth::None
194 } else {
195 std::env::var("COLORTERM")
196 .ok()
197 .and_then(|val| {
198 let lower = val.to_lowercase();
199 if lower.contains("truecolor") || lower.contains("24bit") {
200 Some(ColorDepth::TrueColor)
201 } else {
202 None
203 }
204 })
205 .unwrap_or(ColorDepth::Color256)
206 };
207
208 COLOR_DEPTH_CACHE.store(
209 match depth {
210 ColorDepth::None => 0,
211 ColorDepth::Basic16 => 1,
212 ColorDepth::Color256 => 2,
213 ColorDepth::TrueColor => 3,
214 },
215 Ordering::Relaxed,
216 );
217
218 depth
219}
220
221fn detect_unicode_support() -> bool {
223 std::env::var("LANG")
224 .ok()
225 .map(|lang| lang.to_lowercase().contains("utf"))
226 .or_else(|| {
227 std::env::var("LC_ALL")
228 .ok()
229 .map(|lc| lc.to_lowercase().contains("utf"))
230 })
231 .unwrap_or(true)
232}
233
234pub static CAPABILITIES: Lazy<AnsiCapabilities> = Lazy::new(AnsiCapabilities::detect);
236
237pub fn is_no_color() -> bool {
239 no_color()
240}
241
242pub fn is_clicolor_force() -> bool {
244 clicolor_force()
245}