use anstyle::{Ansi256Color, Color, RgbColor, Style};
pub const RUST_DEEP: RgbColor = RgbColor(0x6e, 0x26, 0x0e);
pub const AMBER: RgbColor = RgbColor(0xb5, 0x6a, 0x12);
pub const TAN: RgbColor = RgbColor(0x8a, 0x6d, 0x3b);
pub const TIGER_ORANGE: RgbColor = RgbColor(0xe8, 0x7a, 0x1e);
pub const CREAM: RgbColor = RgbColor(0xf5, 0xe6, 0xc8);
pub const BRIGHT_YEL: RgbColor = RgbColor(0xff, 0xe0, 0x3a);
pub const ALPHA_HOT: RgbColor = RgbColor(0xcc, 0x22, 0x00);
pub const ALPHA_COOL: RgbColor = RgbColor(0x00, 0x00, 0x00);
pub fn is_resto_char(c: char) -> bool {
matches!(c, 's' | 'S' | 'R')
}
pub fn resto_background_rgb(c: char) -> Option<RgbColor> {
match c {
's' => Some(TAN),
'S' => Some(AMBER),
'R' => Some(RUST_DEEP),
_ => None,
}
}
pub fn resto_kind_str(c: char) -> &'static str {
match c {
's' => "soft_stay",
'S' => "soft_exit",
'R' => "hard",
_ => "none",
}
}
fn lerp_u8(a: u8, b: u8, t: f64) -> u8 {
let v = a as f64 + (b as f64 - a as f64) * t;
v.round().clamp(0.0, 255.0) as u8
}
pub fn alpha_gradient_rgb(alpha: f64, in_resto: bool) -> RgbColor {
let alpha = if alpha.is_finite() {
alpha.clamp(0.0, 1.0)
} else {
1.0
};
let t = 1.0 - alpha;
let (cool, hot) = if in_resto {
(CREAM, BRIGHT_YEL)
} else {
(ALPHA_COOL, ALPHA_HOT)
};
RgbColor(
lerp_u8(cool.0, hot.0, t),
lerp_u8(cool.1, hot.1, t),
lerp_u8(cool.2, hot.2, t),
)
}
fn cube_level(v: u8) -> u8 {
const STEPS: [u8; 6] = [0, 95, 135, 175, 215, 255];
let mut best = 0u8;
let mut best_d = u16::MAX;
for (i, &s) in STEPS.iter().enumerate() {
let d = (v as i16 - s as i16).unsigned_abs();
if d < best_d {
best_d = d;
best = i as u8;
}
}
best
}
pub fn nearest_ansi256(c: RgbColor) -> Ansi256Color {
let r = cube_level(c.0);
let g = cube_level(c.1);
let b = cube_level(c.2);
Ansi256Color(16 + 36 * r + 6 * g + b)
}
pub fn downgrade(c: RgbColor, truecolor: bool) -> Color {
if truecolor {
Color::Rgb(c)
} else {
Color::Ansi256(nearest_ansi256(c))
}
}
pub fn iteration_row_style(alpha_primal: f64, alpha_char: char) -> Style {
iteration_row_style_with(alpha_primal, alpha_char, truecolor_enabled())
}
pub fn iteration_row_style_with(alpha_primal: f64, alpha_char: char, truecolor: bool) -> Style {
let in_resto = is_resto_char(alpha_char);
let fg = downgrade(alpha_gradient_rgb(alpha_primal, in_resto), truecolor);
let mut style = Style::new().fg_color(Some(fg));
if let Some(bg) = resto_background_rgb(alpha_char) {
style = style.bg_color(Some(downgrade(bg, truecolor)));
}
style
}
pub fn truecolor_enabled() -> bool {
anstyle_query::truecolor()
}
pub fn color_enabled_stdout() -> bool {
use std::io::IsTerminal;
if anstyle_query::clicolor_force() {
return true;
}
if anstyle_query::no_color() {
return false;
}
std::io::stdout().is_terminal()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resto_background_maps_three_kinds() {
assert_eq!(resto_background_rgb('s'), Some(TAN));
assert_eq!(resto_background_rgb('S'), Some(AMBER));
assert_eq!(resto_background_rgb('R'), Some(RUST_DEEP));
for c in [' ', 'f', 'h', 'w', 'W', 't', 'T'] {
assert_eq!(resto_background_rgb(c), None, "char {c:?}");
}
}
#[test]
fn is_resto_char_only_for_s_caps_r() {
for c in ['s', 'S', 'R'] {
assert!(is_resto_char(c), "char {c:?}");
}
for c in [' ', 'f', 'h', 'w', 'W', 't', 'T'] {
assert!(!is_resto_char(c), "char {c:?}");
}
}
#[test]
fn alpha_gradient_normal_endpoints() {
assert_eq!(alpha_gradient_rgb(1.0, false), ALPHA_COOL);
assert_eq!(alpha_gradient_rgb(0.0, false), ALPHA_HOT);
}
#[test]
fn alpha_gradient_resto_endpoints() {
assert_eq!(alpha_gradient_rgb(1.0, true), CREAM);
assert_eq!(alpha_gradient_rgb(0.0, true), BRIGHT_YEL);
}
#[test]
fn alpha_gradient_is_monotonic_toward_hot() {
let mut prev = alpha_gradient_rgb(1.0, false).0;
for step in 1..=10 {
let a = 1.0 - step as f64 / 10.0;
let r = alpha_gradient_rgb(a, false).0;
assert!(r >= prev, "alpha={a} red went backwards {prev}->{r}");
prev = r;
}
assert_eq!(prev, ALPHA_HOT.0);
}
#[test]
fn alpha_gradient_clamps_and_handles_nonfinite() {
assert_eq!(alpha_gradient_rgb(2.0, false), ALPHA_COOL);
assert_eq!(alpha_gradient_rgb(-1.0, false), ALPHA_HOT);
assert_eq!(alpha_gradient_rgb(f64::NAN, false), ALPHA_COOL);
}
#[test]
fn downgrade_picks_rgb_or_256() {
assert_eq!(downgrade(RUST_DEEP, true), Color::Rgb(RUST_DEEP));
match downgrade(RUST_DEEP, false) {
Color::Ansi256(_) => {}
other => panic!("expected Ansi256, got {other:?}"),
}
}
#[test]
fn nearest_ansi256_snaps_pure_colors() {
assert_eq!(
nearest_ansi256(RgbColor(0xff, 0xff, 0xff)),
Ansi256Color(231)
);
assert_eq!(
nearest_ansi256(RgbColor(0x00, 0x00, 0x00)),
Ansi256Color(16)
);
}
#[test]
fn iteration_row_style_composes_fg_and_bg() {
let s = iteration_row_style_with(0.5, 'R', true);
assert!(s.get_fg_color().is_some());
assert_eq!(s.get_bg_color(), Some(Color::Rgb(RUST_DEEP)));
let n = iteration_row_style_with(1.0, ' ', true);
assert_eq!(n.get_fg_color(), Some(Color::Rgb(ALPHA_COOL)));
assert_eq!(n.get_bg_color(), None);
}
}