use anstyle::{Ansi256Color, AnsiColor, Color, RgbColor};
use crate::ansi_capabilities::{ColorScheme, detect_color_scheme};
use crate::color256_theme::adjust_index_for_theme;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DiffTheme {
Dark,
Light,
}
impl DiffTheme {
pub fn detect() -> Self {
match detect_color_scheme() {
ColorScheme::Light => Self::Light,
ColorScheme::Dark | ColorScheme::Unknown => Self::Dark,
}
}
pub fn is_light(self) -> bool {
self == Self::Light
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DiffColorLevel {
TrueColor,
Ansi256,
Ansi16,
}
impl DiffColorLevel {
pub fn detect() -> Self {
let colorterm = std::env::var("COLORTERM").unwrap_or_default();
let term = std::env::var("TERM").unwrap_or_default();
let term_program = std::env::var("TERM_PROGRAM").ok();
let has_wt_session = std::env::var_os("WT_SESSION").is_some();
let has_force_color_override = std::env::var_os("FORCE_COLOR").is_some();
diff_color_level_for_terminal(
base_diff_color_level(&colorterm, &term),
term_program.as_deref(),
has_wt_session,
has_force_color_override,
)
}
}
fn base_diff_color_level(colorterm: &str, term: &str) -> DiffColorLevel {
let colorterm = colorterm.to_ascii_lowercase();
let term = term.to_ascii_lowercase();
if colorterm.contains("truecolor") || colorterm.contains("24bit") {
DiffColorLevel::TrueColor
} else if term.contains("256") {
DiffColorLevel::Ansi256
} else {
DiffColorLevel::Ansi16
}
}
fn diff_color_level_for_terminal(
base_level: DiffColorLevel,
term_program: Option<&str>,
has_wt_session: bool,
has_force_color_override: bool,
) -> DiffColorLevel {
if has_force_color_override {
return base_level;
}
if has_wt_session || (base_level == DiffColorLevel::Ansi16 && is_windows_terminal(term_program))
{
return DiffColorLevel::TrueColor;
}
base_level
}
fn is_windows_terminal(term_program: Option<&str>) -> bool {
let Some(program) = term_program else {
return false;
};
let normalized = program.trim().to_ascii_lowercase();
normalized.contains("windows_terminal") || normalized.contains("windows terminal")
}
const DARK_TC_ADD_LINE_BG: (u8, u8, u8) = (25, 45, 35); const DARK_TC_DEL_LINE_BG: (u8, u8, u8) = (90, 40, 40);
const LIGHT_TC_ADD_LINE_BG: (u8, u8, u8) = (215, 240, 215); const LIGHT_TC_DEL_LINE_BG: (u8, u8, u8) = (255, 235, 235); const LIGHT_TC_ADD_NUM_BG: (u8, u8, u8) = (175, 225, 175); const LIGHT_TC_DEL_NUM_BG: (u8, u8, u8) = (250, 210, 210); const LIGHT_TC_GUTTER_FG: (u8, u8, u8) = (25, 25, 25);
const DARK_256_ADD_LINE_BG: u8 = 22; const DARK_256_DEL_LINE_BG: u8 = 52;
const LIGHT_256_ADD_LINE_BG: u8 = 194; const LIGHT_256_DEL_LINE_BG: u8 = 224; const LIGHT_256_ADD_NUM_BG: u8 = 157; const LIGHT_256_DEL_NUM_BG: u8 = 217; const LIGHT_256_GUTTER_FG: u8 = 236;
fn rgb(t: (u8, u8, u8)) -> Color {
Color::Rgb(RgbColor(t.0, t.1, t.2))
}
fn indexed(i: u8, theme: DiffTheme) -> Color {
let adjusted = adjust_index_for_theme(i, theme.is_light());
Color::Ansi256(Ansi256Color(adjusted))
}
pub fn diff_add_bg(theme: DiffTheme, level: DiffColorLevel) -> Color {
match (theme, level) {
(DiffTheme::Dark, DiffColorLevel::TrueColor) => rgb(DARK_TC_ADD_LINE_BG),
(DiffTheme::Dark, DiffColorLevel::Ansi256) => indexed(DARK_256_ADD_LINE_BG, theme),
(DiffTheme::Dark, DiffColorLevel::Ansi16) => Color::Ansi(AnsiColor::Green),
(DiffTheme::Light, DiffColorLevel::TrueColor) => rgb(LIGHT_TC_ADD_LINE_BG),
(DiffTheme::Light, DiffColorLevel::Ansi256) => indexed(LIGHT_256_ADD_LINE_BG, theme),
(DiffTheme::Light, DiffColorLevel::Ansi16) => Color::Ansi(AnsiColor::BrightGreen),
}
}
pub fn diff_del_bg(theme: DiffTheme, level: DiffColorLevel) -> Color {
match (theme, level) {
(DiffTheme::Dark, DiffColorLevel::TrueColor) => rgb(DARK_TC_DEL_LINE_BG),
(DiffTheme::Dark, DiffColorLevel::Ansi256) => indexed(DARK_256_DEL_LINE_BG, theme),
(DiffTheme::Dark, DiffColorLevel::Ansi16) => Color::Ansi(AnsiColor::Red),
(DiffTheme::Light, DiffColorLevel::TrueColor) => rgb(LIGHT_TC_DEL_LINE_BG),
(DiffTheme::Light, DiffColorLevel::Ansi256) => indexed(LIGHT_256_DEL_LINE_BG, theme),
(DiffTheme::Light, DiffColorLevel::Ansi16) => Color::Ansi(AnsiColor::BrightRed),
}
}
pub fn diff_gutter_fg_light(level: DiffColorLevel) -> Color {
match level {
DiffColorLevel::TrueColor => rgb(LIGHT_TC_GUTTER_FG),
DiffColorLevel::Ansi256 => indexed(LIGHT_256_GUTTER_FG, DiffTheme::Light),
DiffColorLevel::Ansi16 => Color::Ansi(AnsiColor::Black),
}
}
pub fn diff_gutter_bg_add_light(level: DiffColorLevel) -> Color {
match level {
DiffColorLevel::TrueColor => rgb(LIGHT_TC_ADD_NUM_BG),
DiffColorLevel::Ansi256 => indexed(LIGHT_256_ADD_NUM_BG, DiffTheme::Light),
DiffColorLevel::Ansi16 => Color::Ansi(AnsiColor::BrightGreen),
}
}
pub fn diff_gutter_bg_del_light(level: DiffColorLevel) -> Color {
match level {
DiffColorLevel::TrueColor => rgb(LIGHT_TC_DEL_NUM_BG),
DiffColorLevel::Ansi256 => indexed(LIGHT_256_DEL_NUM_BG, DiffTheme::Light),
DiffColorLevel::Ansi16 => Color::Ansi(AnsiColor::BrightRed),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dark_truecolor_add_bg_is_rgb() {
let bg = diff_add_bg(DiffTheme::Dark, DiffColorLevel::TrueColor);
assert!(matches!(bg, Color::Rgb(RgbColor(25, 45, 35))));
}
#[test]
fn dark_truecolor_del_bg_is_rgb() {
let bg = diff_del_bg(DiffTheme::Dark, DiffColorLevel::TrueColor);
assert!(matches!(bg, Color::Rgb(RgbColor(90, 40, 40))));
}
#[test]
fn light_truecolor_add_bg_is_accessible() {
let bg = diff_add_bg(DiffTheme::Light, DiffColorLevel::TrueColor);
assert!(matches!(bg, Color::Rgb(RgbColor(215, 240, 215))));
}
#[test]
fn light_truecolor_del_bg_is_accessible() {
let bg = diff_del_bg(DiffTheme::Light, DiffColorLevel::TrueColor);
assert!(matches!(bg, Color::Rgb(RgbColor(255, 235, 235))));
}
#[test]
fn dark_256_uses_indexed_colors() {
let add = diff_add_bg(DiffTheme::Dark, DiffColorLevel::Ansi256);
let del = diff_del_bg(DiffTheme::Dark, DiffColorLevel::Ansi256);
assert!(matches!(add, Color::Ansi256(Ansi256Color(22))));
assert!(matches!(del, Color::Ansi256(Ansi256Color(52))));
}
#[test]
fn light_256_defaults_to_non_harmonious_adjustment() {
let add = diff_add_bg(DiffTheme::Light, DiffColorLevel::Ansi256);
let del = diff_del_bg(DiffTheme::Light, DiffColorLevel::Ansi256);
assert!(matches!(add, Color::Ansi256(Ansi256Color(22))));
assert!(matches!(del, Color::Ansi256(Ansi256Color(52))));
}
#[test]
fn dark_ansi16_uses_named_colors() {
let add = diff_add_bg(DiffTheme::Dark, DiffColorLevel::Ansi16);
let del = diff_del_bg(DiffTheme::Dark, DiffColorLevel::Ansi16);
assert_eq!(add, Color::Ansi(AnsiColor::Green));
assert_eq!(del, Color::Ansi(AnsiColor::Red));
}
#[test]
fn wt_session_promotes_ansi16_to_truecolor() {
assert_eq!(
diff_color_level_for_terminal(DiffColorLevel::Ansi16, None, true, false),
DiffColorLevel::TrueColor
);
}
#[test]
fn windows_terminal_term_program_promotes_ansi16_to_truecolor() {
assert_eq!(
diff_color_level_for_terminal(
DiffColorLevel::Ansi16,
Some("Windows_Terminal"),
false,
false
),
DiffColorLevel::TrueColor
);
}
#[test]
fn non_windows_terminal_keeps_ansi16() {
assert_eq!(
diff_color_level_for_terminal(DiffColorLevel::Ansi16, Some("WezTerm"), false, false),
DiffColorLevel::Ansi16
);
}
#[test]
fn force_color_keeps_ansi16_when_wt_session_exists() {
assert_eq!(
diff_color_level_for_terminal(DiffColorLevel::Ansi16, None, true, true),
DiffColorLevel::Ansi16
);
}
#[test]
fn force_color_keeps_ansi256_when_wt_session_exists() {
assert_eq!(
diff_color_level_for_terminal(DiffColorLevel::Ansi256, None, true, true),
DiffColorLevel::Ansi256
);
}
#[test]
fn base_level_detects_truecolor_from_colorterm() {
assert_eq!(
base_diff_color_level("truecolor", "xterm-256color"),
DiffColorLevel::TrueColor
);
}
#[test]
fn base_level_detects_ansi256_from_term() {
assert_eq!(
base_diff_color_level("", "xterm-256color"),
DiffColorLevel::Ansi256
);
}
#[test]
fn base_level_falls_back_to_ansi16() {
assert_eq!(base_diff_color_level("", "xterm"), DiffColorLevel::Ansi16);
}
}