1use crate::capabilities::Report;
11use rich::cells::char_cell_width;
12use rich::protocol::{ConsoleEnvironment, TargetCapabilities};
13use rich::{Console, ConsoleOptions, Renderable, Segment, Style};
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
18#[cfg_attr(feature = "serde", derive(serde::Serialize))]
19#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
20pub enum Fidelity {
21 Ascii,
23 Plain,
25 Styled,
27 Rich,
29 Animated,
31}
32
33impl Fidelity {
34 pub const ALL: [Fidelity; 5] = [
36 Fidelity::Animated,
37 Fidelity::Rich,
38 Fidelity::Styled,
39 Fidelity::Plain,
40 Fidelity::Ascii,
41 ];
42
43 pub fn name(self) -> &'static str {
45 match self {
46 Fidelity::Animated => "animated",
47 Fidelity::Rich => "rich",
48 Fidelity::Styled => "styled",
49 Fidelity::Plain => "plain",
50 Fidelity::Ascii => "ascii",
51 }
52 }
53
54 pub fn select(source: &dyn FidelitySource, policy: &Policy) -> Fidelity {
65 let facts = source.facts();
66 let natural = if !facts.unicode {
67 Fidelity::Ascii
68 } else if !facts.color {
69 if facts.interactive {
70 Fidelity::Styled
71 } else {
72 Fidelity::Plain
73 }
74 } else if facts.animation && facts.interactive && policy.allow_animation {
75 Fidelity::Animated
76 } else {
77 Fidelity::Rich
78 };
79 policy.clamp(natural)
80 }
81
82 pub fn for_console(console: &Console, policy: &Policy) -> Fidelity {
85 match console.render_environment() {
86 Some(env) => Self::select(&env.capabilities(), policy),
87 None => Self::select(&ConsoleFacts(console), policy),
88 }
89 }
90}
91
92#[derive(Clone, Copy, Debug, PartialEq, Eq)]
94pub struct FidelityFacts {
95 pub unicode: bool,
96 pub color: bool,
97 pub interactive: bool,
98 pub animation: bool,
99}
100
101pub trait FidelitySource {
103 fn facts(&self) -> FidelityFacts;
104}
105
106impl FidelitySource for FidelityFacts {
107 fn facts(&self) -> FidelityFacts {
108 *self
109 }
110}
111
112impl FidelitySource for Report {
113 fn facts(&self) -> FidelityFacts {
114 FidelityFacts {
115 unicode: self.unicode.value,
116 color: self.color.value.color_system().is_some(),
117 interactive: self.interactive.value,
118 animation: self.animation.value,
119 }
120 }
121}
122
123impl FidelitySource for TargetCapabilities {
125 fn facts(&self) -> FidelityFacts {
126 FidelityFacts {
127 unicode: self.unicode,
128 color: self.color_system.is_some(),
129 interactive: self.interactive,
130 animation: self.interactive,
131 }
132 }
133}
134
135struct ConsoleFacts<'a>(&'a Console);
136
137impl FidelitySource for ConsoleFacts<'_> {
138 fn facts(&self) -> FidelityFacts {
139 let c = self.0;
140 FidelityFacts {
141 unicode: !c.ascii_only(),
142 color: c.color_system().is_some() && !c.no_color(),
143 interactive: c.is_terminal(),
144 animation: c.is_terminal(),
145 }
146 }
147}
148
149#[derive(Clone, Copy, Debug, PartialEq, Eq)]
151pub struct Policy {
152 pub ceiling: Option<Fidelity>,
154 pub floor: Option<Fidelity>,
156 pub allow_animation: bool,
158}
159
160impl Default for Policy {
161 fn default() -> Self {
162 Self {
163 ceiling: None,
164 floor: None,
165 allow_animation: true,
166 }
167 }
168}
169
170impl Policy {
171 #[must_use]
172 pub fn ceiling(mut self, level: Fidelity) -> Self {
173 self.ceiling = Some(level);
174 self
175 }
176 #[must_use]
177 pub fn floor(mut self, level: Fidelity) -> Self {
178 self.floor = Some(level);
179 self
180 }
181 pub fn clamp(&self, level: Fidelity) -> Fidelity {
183 let level = self.ceiling.map_or(level, |c| level.min(c));
184 self.floor.map_or(level, |f| level.max(f))
185 }
186}
187
188pub trait Degradable {
190 fn levels(&self) -> &[Fidelity];
192 fn render_at(
194 &self,
195 level: Fidelity,
196 console: &Console,
197 options: &ConsoleOptions,
198 ) -> Vec<Segment>;
199}
200
201pub struct Adaptive<T> {
205 inner: T,
206 level: Option<Fidelity>,
207 policy: Policy,
208}
209
210impl<T: Degradable> Adaptive<T> {
211 pub fn new(inner: T) -> Self {
212 Self {
213 inner,
214 level: None,
215 policy: Policy::default(),
216 }
217 }
218 #[must_use]
220 pub fn level(mut self, level: Fidelity) -> Self {
221 self.level = Some(level);
222 self
223 }
224 #[must_use]
226 pub fn policy(mut self, policy: Policy) -> Self {
227 self.policy = policy;
228 self
229 }
230 pub fn resolve(&self, console: &Console) -> (Fidelity, Fidelity) {
232 let selected = self
233 .level
234 .unwrap_or_else(|| Fidelity::for_console(console, &self.policy));
235 let levels = self.inner.levels();
236 let chosen = levels
237 .iter()
238 .copied()
239 .filter(|l| *l <= selected)
240 .max()
241 .or_else(|| levels.iter().copied().min())
242 .unwrap_or(selected);
243 (selected, chosen)
244 }
245}
246
247impl<T: Degradable> Renderable for Adaptive<T> {
248 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
249 let (selected, chosen) = self.resolve(console);
250 let segments = self.inner.render_at(chosen, console, options);
251 if chosen > selected {
252 degrade_segments(segments, selected)
253 } else {
254 segments
255 }
256 }
257}
258
259pub fn degrade_segments(segments: Vec<Segment>, level: Fidelity) -> Vec<Segment> {
261 match level {
262 Fidelity::Animated | Fidelity::Rich => segments,
263 Fidelity::Styled => strip_color(segments),
264 Fidelity::Plain => strip_styles(segments),
265 Fidelity::Ascii => ascii_fallback(strip_styles(segments)),
266 }
267}
268
269pub struct Degrade<R> {
271 inner: R,
272 level: Option<Fidelity>,
273 policy: Policy,
274}
275
276impl<R: Renderable> Degrade<R> {
277 pub fn new(inner: R) -> Self {
278 Self {
279 inner,
280 level: None,
281 policy: Policy::default(),
282 }
283 }
284 #[must_use]
286 pub fn level(mut self, level: Fidelity) -> Self {
287 self.level = Some(level);
288 self
289 }
290 #[must_use]
291 pub fn policy(mut self, policy: Policy) -> Self {
292 self.policy = policy;
293 self
294 }
295}
296
297pub struct Borrowed<'a>(pub &'a dyn Renderable);
299
300impl Renderable for Borrowed<'_> {
301 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
302 self.0.rich_render(console, options)
303 }
304 fn measure(&self, console: &Console, options: &ConsoleOptions) -> rich::measure::Measurement {
305 self.0.measure(console, options)
306 }
307}
308
309impl<'a> Degrade<Borrowed<'a>> {
310 pub fn borrowed(inner: &'a dyn Renderable) -> Self {
312 Self::new(Borrowed(inner))
313 }
314}
315
316impl<R: Renderable> Degradable for Degrade<R> {
317 fn levels(&self) -> &[Fidelity] {
318 &Fidelity::ALL
319 }
320 fn render_at(
321 &self,
322 level: Fidelity,
323 console: &Console,
324 options: &ConsoleOptions,
325 ) -> Vec<Segment> {
326 degrade_segments(self.inner.rich_render(console, options), level)
327 }
328}
329
330impl<R: Renderable> Renderable for Degrade<R> {
331 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
332 let level = self
333 .level
334 .unwrap_or_else(|| Fidelity::for_console(console, &self.policy));
335 self.render_at(level, console, options)
336 }
337 fn measure(&self, console: &Console, options: &ConsoleOptions) -> rich::measure::Measurement {
338 self.inner.measure(console, options)
339 }
340}
341
342pub(crate) const ATTR_NAMES: [&str; 13] = [
344 "bold",
345 "dim",
346 "italic",
347 "underline",
348 "blink",
349 "blink2",
350 "reverse",
351 "conceal",
352 "strike",
353 "underline2",
354 "frame",
355 "encircle",
356 "overline",
357];
358
359pub fn style_without_color(style: &Style) -> Style {
361 let words: Vec<String> = ATTR_NAMES
362 .iter()
363 .enumerate()
364 .filter_map(|(i, name)| match style.attr(i) {
365 Some(true) => Some((*name).to_owned()),
366 Some(false) => Some(format!("not {name}")),
367 None => None,
368 })
369 .collect();
370 let base = if words.is_empty() {
371 Style::new()
372 } else {
373 Style::parse(&words.join(" ")).unwrap_or_default()
374 };
375 base.update_link(style.link().map(str::to_owned))
376}
377
378pub fn strip_color(segments: Vec<Segment>) -> Vec<Segment> {
380 segments
381 .into_iter()
382 .map(|mut s| {
383 s.style = s
384 .style
385 .as_ref()
386 .map(style_without_color)
387 .filter(|st| !st.is_null());
388 s
389 })
390 .collect()
391}
392
393pub fn strip_styles(segments: Vec<Segment>) -> Vec<Segment> {
395 segments
396 .into_iter()
397 .filter(|s| !s.control)
398 .map(|mut s| {
399 s.style = None;
400 s
401 })
402 .collect()
403}
404
405pub fn ascii_fallback(segments: Vec<Segment>) -> Vec<Segment> {
409 segments
410 .into_iter()
411 .map(|mut s| {
412 if !s.control && !s.text.is_ascii() {
413 s.text = ascii_text(&s.text).into();
414 }
415 s
416 })
417 .collect()
418}
419
420pub fn ascii_text(text: &str) -> String {
422 let mut out = String::with_capacity(text.len());
423 for c in text.chars() {
424 if c.is_ascii() {
425 out.push(c);
426 } else if let Some(a) = ascii_char(c) {
427 out.push(a);
428 for _ in 1..char_cell_width(c) {
429 out.push(' ');
430 }
431 } else {
432 for _ in 0..char_cell_width(c) {
434 out.push('?');
435 }
436 }
437 }
438 out
439}
440
441pub fn ascii_char(c: char) -> Option<char> {
443 Some(match c {
444 '─' | '━' | '┄' | '┅' | '┈' | '┉' | '╌' | '╍' | '╴' | '╶' | '╸' | '╺' | '╼' | '╾' => {
446 '-'
447 }
448 '═' => '=',
449 '│' | '┃' | '┆' | '┇' | '┊' | '┋' | '╎' | '╏' | '║' | '╵' | '╷' | '╹' | '╻' | '╽' | '╿' => {
450 '|'
451 }
452 '╱' => '/',
453 '╲' => '\\',
454 '╳' => 'X',
455 '\u{2500}'..='\u{257f}' => '+',
456 '░' => '.',
458 '▒' => ':',
459 '▓' | '█' => '#',
460 '▁' | '▂' => '_',
461 '\u{2580}'..='\u{259f}' => '#',
462 '\u{2800}' => ' ',
464 '\u{2801}'..='\u{28ff}' => '.',
465 '→' | '⇒' | '⟶' | '➜' | '➔' | '▶' | '►' | '▸' | '❯' | '›' | '»' => '>',
467 '←' | '⇐' | '⟵' | '◀' | '◄' | '◂' | '❮' | '‹' | '«' => '<',
468 '↑' | '⇑' | '▲' | '▴' => '^',
469 '↓' | '⇓' | '▼' | '▾' => 'v',
470 '↔' | '⇔' => '-',
471 '↷' | '↻' | '⟳' => '~',
472 '•' | '●' | '◉' | '▪' | '■' | '◆' | '★' | '∙' | '⁃' => '*',
474 '○' | '◦' | '◯' | '□' | '◇' | '☐' => 'o',
475 '·' | '…' | '⋯' => '.',
476 '✓' | '✔' | '☑' | '✅' => 'v',
477 '✗' | '✘' | '✕' | '✖' | '×' | '☒' | '❌' => 'x',
478 '⚠' => '!',
479 'ℹ' => 'i',
480 '‘' | '’' | '′' => '\'',
482 '“' | '”' | '″' => '"',
483 '‐' | '‑' | '‒' | '–' | '—' | '―' | '−' => '-',
484 '\u{a0}' | '\u{2002}'..='\u{200a}' => ' ',
485 _ => return None,
486 })
487}