1use crate::color::{ColorDepth, ColorMode};
4use crate::stream::Probe;
5#[cfg(feature = "detect")]
6use crate::stream::Stream;
7
8pub const DEFAULT_COLUMN_BUFFER: u16 = 2;
11
12pub const DEFAULT_MINIMUM_WIDTH: u16 = 20;
15
16pub const DEFAULT_FALLBACK_WIDTH: u16 = 80;
19
20#[derive(Clone, Copy, Debug, Eq, PartialEq)]
42pub struct DetectOptions {
43 width: Option<u16>,
44 column_buffer: Option<u16>,
45 column_buffer_envs: &'static [&'static str],
46 minimum_width: u16,
47 fallback_width: Option<u16>,
48}
49
50impl DetectOptions {
51 #[must_use]
55 pub const fn new() -> Self {
56 Self {
57 width: None,
58 column_buffer: None,
59 column_buffer_envs: &[],
60 minimum_width: DEFAULT_MINIMUM_WIDTH,
61 fallback_width: Some(DEFAULT_FALLBACK_WIDTH),
62 }
63 }
64
65 #[must_use]
68 pub const fn width(mut self, cells: u16) -> Self {
69 self.width = Some(cells);
70 self
71 }
72
73 #[must_use]
77 pub const fn column_buffer(mut self, cells: u16) -> Self {
78 self.column_buffer = Some(cells);
79 self
80 }
81
82 #[must_use]
85 pub const fn column_buffer_envs(mut self, names: &'static [&'static str]) -> Self {
86 self.column_buffer_envs = names;
87 self
88 }
89
90 #[must_use]
93 pub const fn minimum_width(mut self, cells: u16) -> Self {
94 self.minimum_width = cells;
95 self
96 }
97
98 #[must_use]
102 pub const fn fallback_width(mut self, cells: Option<u16>) -> Self {
103 self.fallback_width = cells;
104 self
105 }
106}
107
108impl Default for DetectOptions {
109 fn default() -> Self {
110 Self::new()
111 }
112}
113
114#[derive(Clone, Copy, Debug, Eq, PartialEq)]
126pub struct Terminal {
127 pub(crate) attached: bool,
128 pub(crate) color: Option<ColorDepth>,
129 pub(crate) width: Option<u16>,
130}
131
132impl Terminal {
133 #[cfg(feature = "detect")]
141 #[must_use]
142 pub fn detect(
143 stream: Stream,
144 mode: ColorMode,
145 options: DetectOptions,
146 env: &dyn Fn(&str) -> Option<String>,
147 ) -> Self {
148 Self::from_probe(Probe::of(stream), mode, options, env)
149 }
150
151 #[cfg(feature = "detect")]
155 #[must_use]
156 pub fn from_process(stream: Stream, mode: ColorMode, options: DetectOptions) -> Self {
157 Self::detect(stream, mode, options, &process_env)
158 }
159
160 #[must_use]
165 pub fn from_probe(
166 probe: Probe,
167 mode: ColorMode,
168 options: DetectOptions,
169 env: &dyn Fn(&str) -> Option<String>,
170 ) -> Self {
171 let lookup = Lookup(env);
172 let color = color_is_on(probe, mode, &lookup).then(|| detect_depth(&lookup));
173 Self {
174 attached: probe.is_terminal(),
175 color,
176 width: detect_width(probe, options, &lookup),
177 }
178 }
179
180 #[must_use]
186 pub fn at_depth(self, depth: ColorDepth) -> Self {
187 Self {
188 color: self.color.map(|_| depth),
189 ..self
190 }
191 }
192
193 #[must_use]
196 pub fn is_attached(self) -> bool {
197 self.attached
198 }
199
200 #[must_use]
202 pub fn color(self) -> Option<ColorDepth> {
203 self.color
204 }
205
206 #[must_use]
210 pub fn width(self) -> Option<u16> {
211 self.width
212 }
213}
214
215#[cfg(feature = "detect")]
218fn process_env(name: &str) -> Option<String> {
219 std::env::var_os(name).map(|value| value.to_string_lossy().into_owned())
220}
221
222struct Lookup<'a>(&'a dyn Fn(&str) -> Option<String>);
224
225impl Lookup<'_> {
226 fn get(&self, name: &str) -> Option<String> {
227 (self.0)(name).filter(|value| !value.is_empty())
228 }
229
230 fn is(&self, name: &str, expected: &str) -> bool {
231 self.get(name).is_some_and(|value| value == expected)
232 }
233}
234
235fn color_is_on(probe: Probe, mode: ColorMode, env: &Lookup<'_>) -> bool {
236 match mode {
237 ColorMode::Always => true,
238 ColorMode::Never => false,
239 ColorMode::Auto => auto_color(probe, env),
240 }
241}
242
243fn auto_color(probe: Probe, env: &Lookup<'_>) -> bool {
244 if env.get("NO_COLOR").is_some() {
245 return false;
246 }
247 if env.get("CLICOLOR_FORCE").is_some_and(|value| value != "0") {
248 return true;
249 }
250 if env
251 .get("FORCE_COLOR")
252 .is_some_and(|value| value != "0" && value != "false")
253 {
254 return true;
255 }
256 if env.is("CLICOLOR", "0") || env.is("TERM", "dumb") {
257 return false;
258 }
259 probe.is_terminal()
260}
261
262fn detect_depth(env: &Lookup<'_>) -> ColorDepth {
263 let truecolor = env.is("COLORTERM", "truecolor")
264 || env.is("COLORTERM", "24bit")
265 || env.is("FORCE_COLOR", "3");
266 let palette = env
267 .get("TERM")
268 .is_some_and(|term| term.contains("256color"))
269 || env.is("FORCE_COLOR", "2");
270 if truecolor {
271 ColorDepth::TrueColor
272 } else if palette {
273 ColorDepth::Ansi256
274 } else {
275 ColorDepth::Ansi16
276 }
277}
278
279fn detect_width(probe: Probe, options: DetectOptions, env: &Lookup<'_>) -> Option<u16> {
280 let columns = options.width.or_else(|| {
281 let detected = probe.columns().or_else(|| {
282 env.get("COLUMNS")?
283 .parse::<u16>()
284 .ok()
285 .filter(|columns| *columns >= 1)
286 });
287 match detected {
288 Some(columns) => Some(effective_width(
289 columns,
290 column_buffer(options, env),
291 options.minimum_width,
292 )),
293 None => options.fallback_width,
294 }
295 });
296 columns.map(|columns| columns.max(1))
297}
298
299fn column_buffer(options: DetectOptions, env: &Lookup<'_>) -> u16 {
300 options
301 .column_buffer
302 .or_else(|| {
303 options
304 .column_buffer_envs
305 .iter()
306 .filter_map(|name| env.get(name))
307 .find_map(|raw| raw.parse().ok())
308 })
309 .unwrap_or(DEFAULT_COLUMN_BUFFER)
310}
311
312fn effective_width(width: u16, buffer: u16, minimum: u16) -> u16 {
313 width.saturating_sub(buffer).max(minimum.min(width))
314}