use std::env;
use super::Color;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum ColorMode {
#[default]
TrueColor,
Color256,
Basic16,
}
impl ColorMode {
pub fn detect() -> Self {
if let Ok(mode) = env::var("PHOTON_COLOR_MODE") {
match mode.as_str() {
| "truecolor" | "24bit" | "rgb" => return ColorMode::TrueColor,
| "256" | "256color" => return ColorMode::Color256,
| "16" | "basic" => return ColorMode::Basic16,
| _ => {},
}
}
if let Ok(ct) = env::var("COLORTERM") &&
(ct == "truecolor" || ct == "24bit")
{
return ColorMode::TrueColor;
}
if let Ok(term) = env::var("TERM") &&
term.contains("256color")
{
return ColorMode::Color256;
}
ColorMode::TrueColor
}
}
pub fn fg(color: Color, mode: ColorMode) -> String {
match mode {
| ColorMode::TrueColor => format!("\x1b[38;2;{};{};{}m", color.0, color.1, color.2),
| ColorMode::Color256 => format!("\x1b[38;5;{}m", rgb_to_256(color)),
| ColorMode::Basic16 => format!("\x1b[{}m", rgb_to_16_fg(color)),
}
}
pub fn bg(color: Color, mode: ColorMode) -> String {
match mode {
| ColorMode::TrueColor => format!("\x1b[48;2;{};{};{}m", color.0, color.1, color.2),
| ColorMode::Color256 => format!("\x1b[48;5;{}m", rgb_to_256(color)),
| ColorMode::Basic16 => format!("\x1b[{}m", rgb_to_16_bg(color)),
}
}
pub const RESET: &str = "\x1b[0m";
fn rgb_to_256(color: Color) -> u8 {
let Color(r, g, b) = color;
if r == g && g == b {
if r < 8 {
return 16;
}
if r > 248 {
return 231;
}
return 232 + ((r - 8) / 10);
}
let r = closest_cube_level(r);
let g = closest_cube_level(g);
let b = closest_cube_level(b);
16 + 36 * r + 6 * g + b
}
fn closest_cube_level(v: u8) -> u8 {
if v < 48 {
0
} else if v < 115 {
1
} else if v < 155 {
2
} else if v < 195 {
3
} else if v < 235 {
4
} else {
5
}
}
fn rgb_to_16_fg(color: Color) -> u8 {
30 + rgb_to_16_index(color)
}
fn rgb_to_16_bg(color: Color) -> u8 {
40 + rgb_to_16_index(color)
}
fn rgb_to_16_index(color: Color) -> u8 {
let Color(r, g, b) = color;
let intensity = (r as u16 + g as u16 + b as u16) / 3;
let is_bright = intensity > 128;
let idx = if r > 128 && g < 128 && b < 128 {
1 } else if r < 128 && g > 128 && b < 128 {
2 } else if r > 128 && g > 128 && b < 128 {
3 } else if r < 128 && g < 128 && b > 128 {
4 } else if r > 128 && g < 128 && b > 128 {
5 } else if r < 128 && g > 128 && b > 128 {
6 } else {
0 };
if is_bright && idx == 0 {
7 } else if is_bright {
idx + 60 } else {
idx
}
}
#[cfg(test)]
mod tests {
use std::{
env,
sync::Mutex,
};
use super::*;
static ENV_LOCK: Mutex<()> = Mutex::new(());
fn with_envs<F>(vars: &[(&str, Option<&str>)], f: F)
where
F: FnOnce(), {
let _guard = ENV_LOCK.lock().unwrap();
let originals: Vec<(String, Option<String>)> = vars
.iter()
.map(|(key, _)| ((*key).to_string(), env::var(key).ok()))
.collect();
for (key, value) in vars {
match value {
| Some(v) => unsafe { env::set_var(key, v) },
| None => unsafe { env::remove_var(key) },
}
}
f();
for (key, original) in originals {
match original {
| Some(v) => unsafe { env::set_var(&key, v) },
| None => unsafe { env::remove_var(&key) },
}
}
}
#[test]
fn truecolor_fg() {
let s = fg(Color::SUNBEAM_ORANGE, ColorMode::TrueColor);
assert_eq!(s, "\x1b[38;2;250;82;15m");
}
#[test]
fn truecolor_bg() {
let s = bg(Color::WARM_IVORY, ColorMode::TrueColor);
assert_eq!(s, "\x1b[48;2;255;250;237m");
}
#[test]
fn color256_produces_valid_codes() {
let s = fg(Color::SUNBEAM_ORANGE, ColorMode::Color256);
assert!(s.starts_with("\x1b[38;5;"));
assert!(s.ends_with('m'));
}
#[test]
fn color256_bg_produces_valid_codes() {
let s = bg(Color::SUNBEAM_ORANGE, ColorMode::Color256);
assert!(s.starts_with("\x1b[48;5;"));
assert!(s.ends_with('m'));
}
#[test]
fn basic16_produces_valid_codes() {
let s = fg(Color::WHITE, ColorMode::Basic16);
assert!(s.starts_with("\x1b["));
assert!(s.ends_with('m'));
}
#[test]
fn basic16_bg_produces_valid_codes() {
let s = bg(Color::WHITE, ColorMode::Basic16);
assert!(s.starts_with("\x1b["));
assert!(s.ends_with('m'));
}
#[test]
fn detect_respects_photon_color_mode() {
with_envs(&[("PHOTON_COLOR_MODE", Some("256"))], || {
assert_eq!(ColorMode::detect(), ColorMode::Color256);
});
with_envs(&[("PHOTON_COLOR_MODE", Some("basic"))], || {
assert_eq!(ColorMode::detect(), ColorMode::Basic16);
});
with_envs(&[("PHOTON_COLOR_MODE", Some("truecolor"))], || {
assert_eq!(ColorMode::detect(), ColorMode::TrueColor);
});
with_envs(
&[
("PHOTON_COLOR_MODE", Some("unknown")),
("COLORTERM", None),
("TERM", None),
],
|| {
assert_eq!(ColorMode::detect(), ColorMode::TrueColor);
},
);
}
#[test]
fn detect_respects_colorterm() {
with_envs(
&[
("PHOTON_COLOR_MODE", None),
("COLORTERM", Some("truecolor")),
("TERM", None),
],
|| {
assert_eq!(ColorMode::detect(), ColorMode::TrueColor);
},
);
with_envs(
&[
("PHOTON_COLOR_MODE", None),
("COLORTERM", Some("24bit")),
("TERM", None),
],
|| {
assert_eq!(ColorMode::detect(), ColorMode::TrueColor);
},
);
}
#[test]
fn detect_respects_term() {
with_envs(
&[
("PHOTON_COLOR_MODE", None),
("COLORTERM", None),
("TERM", Some("xterm-256color")),
],
|| {
assert_eq!(ColorMode::detect(), ColorMode::Color256);
},
);
with_envs(
&[
("PHOTON_COLOR_MODE", None),
("COLORTERM", None),
("TERM", Some("xterm")),
],
|| {
assert_eq!(ColorMode::detect(), ColorMode::TrueColor);
},
);
}
#[test]
fn rgb_to_256_grayscale() {
assert_eq!(rgb_to_256(Color(0, 0, 0)), 16);
assert_eq!(rgb_to_256(Color(255, 255, 255)), 231);
assert_eq!(rgb_to_256(Color(128, 128, 128)), 232 + ((128 - 8) / 10));
}
#[test]
fn rgb_to_256_color_cube() {
let idx = rgb_to_256(Color::SUNBEAM_ORANGE);
assert!((16..=231).contains(&idx));
}
#[test]
fn closest_cube_level_branches() {
assert_eq!(closest_cube_level(0), 0);
assert_eq!(closest_cube_level(47), 0);
assert_eq!(closest_cube_level(48), 1);
assert_eq!(closest_cube_level(114), 1);
assert_eq!(closest_cube_level(115), 2);
assert_eq!(closest_cube_level(154), 2);
assert_eq!(closest_cube_level(155), 3);
assert_eq!(closest_cube_level(194), 3);
assert_eq!(closest_cube_level(195), 4);
assert_eq!(closest_cube_level(234), 4);
assert_eq!(closest_cube_level(235), 5);
assert_eq!(closest_cube_level(255), 5);
}
#[test]
fn rgb_to_16_fg_and_bg() {
assert_eq!(rgb_to_16_fg(Color::WHITE), 37);
assert_eq!(rgb_to_16_bg(Color::WHITE), 47);
}
#[test]
fn rgb_to_16_index_color_branches() {
assert_eq!(rgb_to_16_index(Color(255, 0, 0)), 1);
assert_eq!(rgb_to_16_index(Color(0, 255, 0)), 2);
assert_eq!(rgb_to_16_index(Color(255, 255, 0)), 63);
assert_eq!(rgb_to_16_index(Color(0, 0, 255)), 4);
assert_eq!(rgb_to_16_index(Color(255, 0, 255)), 65);
assert_eq!(rgb_to_16_index(Color(0, 255, 255)), 66);
assert_eq!(rgb_to_16_index(Color(0, 0, 0)), 0);
assert_eq!(rgb_to_16_index(Color(255, 255, 255)), 7);
assert_eq!(rgb_to_16_index(Color(192, 0, 0)), 1);
}
}