Something went wrong. Try again.
atproto git client
Something went wrong. Try again.
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149//! The six styles atgc paints with, and the one function that paints.//!//! # Why this exists//!//! Three modules each grew the same four lines — a stringly-typed SGR//! parameter, a `format!("\x1b[{code}m{text}\x1b[0m")`, and a boolean deciding//! whether to bother://!//! - [`crate::term::say`], for the `warning:` and `note:` prefixes//! - [`crate::cmd::logs::render`]'s `Palette`, for six named colours//! - [`crate::cmd::repo::read`], for one bold heading//!//! Nothing was wrong with any of them, and that is the problem with three//! copies: they agreed by coincidence rather than by construction. `93` is//! `Palette::warn` *and* `say`'s `warning:` prefix because someone typed the//! same two digits twice, and a fourth caller reaching for "the warning//! colour" had nowhere to look it up.//!//! # anstyle rather than our own//!//! [`anstyle`] is the vocabulary crate the Rust CLI ecosystem settled on for//! exactly this, and it was already compiled into this binary — clap builds//! its help on it — so adopting it costs a line in `Cargo.toml` and no//! dependency at all. What it buys is that a colour is a value with a type//! instead of a string that happens to parse: `AnsiColor::BrightYellow` can//! be wrong about which colour is wanted, but it cannot be `"39m"` or `"1;93"`//! or an unterminated escape.//!//! The bytes are unchanged. `anstyle` renders `BrightYellow` as `\x1b[93m`//! and its reset as `\x1b[0m`, which is what all three call sites emitted, and//! `the_rendered_bytes_are_the_ones_we_used_to_write` below pins each of the//! six against the literal escape it replaces.//!//! # What is not here//!//! `anstream`, the companion crate that strips escapes from a stream that//! cannot render them. atgc already decides that once, in//! [`crate::term::hyperlink::escapes_wanted`], from three facts it can state —//! and having *two* mechanisms for "should this be coloured" is worse than//! having one, whichever is cleverer.//!//! The canvas in [`crate::cmd::about`] is also not here. Its cell colour is//! not only a terminal style — the HTML rendering of the same canvas matches//! on it to pick a CSS class — so it is a key shared by two renderers that//! happens to be spelled as an SGR parameter. Giving it a proper `Ink` enum is//! the right fix and a different one; see TODO.md.
use anstyle::{AnsiColor, Style};
/// A heading. The only style here that is not a colour, and the only one that/// stays legible on a terminal whose palette has been rethemed.pub const BOLD: Style = Style::new().bold();
/// Secondary text: labels, timestamps, anything that should recede.pub const DIM: Style = fg(AnsiColor::BrightBlack);
/// A failure — a refused write, an error outcome in a log.pub const BAD: Style = fg(AnsiColor::BrightRed);
/// A success, and the one that has to stay distinguishable from [`BAD`] for/// a reader who cannot tell red from green: nothing in atgc uses colour as/// the *only* carrier of an outcome, which is why every painted word here is/// also a word.pub const GOOD: Style = fg(AnsiColor::BrightGreen);
/// Something is wrong or incomplete but the result still stands.pub const WARN: Style = fg(AnsiColor::BrightYellow);
/// A caveat about a result that is otherwise fine.pub const NOTE: Style = fg(AnsiColor::BrightCyan);
/// `const` so the six above can be, which is what lets them be constants/// rather than functions.const fn fg(color: AnsiColor) -> Style { Style::new().fg_color(Some(anstyle::Color::Ansi(color)))}
/// `text` in `style`, or `text` alone when `on` is false.////// The boolean is passed in rather than read here on purpose. Whether escapes/// are wanted is one decision made in one place, and it is not the same/// question on both streams — stdout answers it through/// [`crate::term::hyperlink::stdout_escapes_wanted`], which `--json` also/// vetoes, while stderr answers it without that veto. A `paint` that read the/// environment itself would have to pick one of those and be wrong for the/// other caller.pub fn paint(on: bool, style: Style, text: &str) -> String { if on { format!("{}{text}{}", style.render(), style.render_reset()) } else { text.to_string() }}
#[cfg(test)]mod tests { use super::*;
/// The whole claim of this change: the escapes are the same escapes. /// /// Pinned against literals rather than against `anstyle`'s own rendering, /// so this is a test of what reaches a terminal and not a tautology — if /// a future anstyle rendered `BrightYellow` as `\x1b[38;5;11m`, that is /// still a valid encoding of the same colour and this test would still be /// right to fail, because the output would no longer be byte-for-byte /// what every prior release wrote. #[test] fn the_rendered_bytes_are_the_ones_we_used_to_write() { for (style, code) in [ (BOLD, "1"), (DIM, "90"), (BAD, "91"), (GOOD, "92"), (WARN, "93"), (NOTE, "96"), ] { assert_eq!( paint(true, style, "x"), format!("\x1b[{code}mx\x1b[0m"), "{code} no longer renders the way the hand-written escape did" ); } }
/// Off is plain text, not an empty escape — a pipe, a redirect or /// `NO_COLOR` gets bytes it can grep. #[test] fn painting_off_emits_no_escapes() { for style in [BOLD, DIM, BAD, GOOD, WARN, NOTE] { let painted = paint(false, style, "x"); assert_eq!(painted, "x"); assert!(!painted.contains('\x1b')); } }
/// Six distinct styles. Two that rendered the same would make one of the /// call sites silently lose a distinction it thinks it is drawing. #[test] fn no_two_styles_render_alike() { let mut rendered: Vec<String> = [BOLD, DIM, BAD, GOOD, WARN, NOTE] .into_iter() .map(|s| paint(true, s, "x")) .collect(); rendered.sort_unstable(); rendered.dedup(); assert_eq!(rendered.len(), 6, "two styles render identically"); }}