#[cfg(target_os = "macos")]
use std::process::Command;
use std::sync::OnceLock;
use ratatui::style::Color;
use super::contrast::relative_luminance;
use super::osc11;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PaletteMode {
Dark,
Light,
Grayscale,
SolarizedLight,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BackgroundSource {
Osc11,
ColorFgBg,
MacOsAppearance,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TerminalBackground {
mode: PaletteMode,
color: Option<Color>,
source: BackgroundSource,
}
impl TerminalBackground {
#[must_use]
pub const fn new(mode: PaletteMode, color: Option<Color>, source: BackgroundSource) -> Self {
Self {
mode,
color,
source,
}
}
#[must_use]
pub const fn unknown() -> Self {
Self::new(PaletteMode::Dark, None, BackgroundSource::Unknown)
}
#[must_use]
pub const fn mode(&self) -> PaletteMode {
self.mode
}
#[must_use]
pub const fn color(&self) -> Option<Color> {
self.color
}
#[must_use]
pub const fn source(&self) -> BackgroundSource {
self.source
}
}
const LIGHT_SURFACE_LUMINANCE: f32 = 0.179_129_5;
#[must_use]
pub fn palette_mode_for_background(color: Color) -> Option<PaletteMode> {
let luminance = relative_luminance(color)?;
Some(if luminance > LIGHT_SURFACE_LUMINANCE {
PaletteMode::Light
} else {
PaletteMode::Dark
})
}
impl PaletteMode {
#[must_use]
pub fn from_colorfgbg(value: &str) -> Option<Self> {
let bg = colorfgbg_index(value)?;
Some(if bg >= 8 { Self::Light } else { Self::Dark })
}
#[must_use]
pub fn detect() -> Self {
terminal_background().mode()
}
}
fn colorfgbg_index(value: &str) -> Option<u16> {
value
.split(';')
.rev()
.find_map(|part| part.parse::<u16>().ok())
}
fn colorfgbg_background(value: &str) -> Option<(PaletteMode, Option<Color>)> {
let index = colorfgbg_index(value)?;
if let Ok(index) = u8::try_from(index)
&& index >= 16
&& let Some(mode) = palette_mode_for_background(Color::Indexed(index))
{
return Some((mode, Some(Color::Indexed(index))));
}
Some((PaletteMode::from_colorfgbg(value)?, None))
}
#[must_use]
pub fn resolve_terminal_background(
osc11_rgb: Option<(u8, u8, u8)>,
colorfgbg: Option<&str>,
macos_fallback: Option<PaletteMode>,
) -> TerminalBackground {
if let Some((r, g, b)) = osc11_rgb {
let color = Color::Rgb(r, g, b);
if let Some(mode) = palette_mode_for_background(color) {
return TerminalBackground::new(mode, Some(color), BackgroundSource::Osc11);
}
}
if let Some((mode, color)) = colorfgbg.and_then(colorfgbg_background) {
return TerminalBackground::new(mode, color, BackgroundSource::ColorFgBg);
}
if let Some(mode) = macos_fallback {
return TerminalBackground::new(mode, None, BackgroundSource::MacOsAppearance);
}
TerminalBackground::unknown()
}
static TERMINAL_BACKGROUND: OnceLock<TerminalBackground> = OnceLock::new();
#[must_use]
pub fn terminal_background() -> TerminalBackground {
if let Some(background) = TERMINAL_BACKGROUND.get() {
return *background;
}
resolve_terminal_background(
None,
std::env::var("COLORFGBG").ok().as_deref(),
detect_macos_palette_mode(),
)
}
pub fn probe_terminal_background() -> TerminalBackground {
if let Some(background) = TERMINAL_BACKGROUND.get() {
return *background;
}
let background = resolve_terminal_background(
osc11::query_terminal_background(osc11::OSC11_QUERY_TIMEOUT),
std::env::var("COLORFGBG").ok().as_deref(),
detect_macos_palette_mode(),
);
*TERMINAL_BACKGROUND.get_or_init(|| background)
}
#[cfg(target_os = "macos")]
fn detect_macos_palette_mode() -> Option<PaletteMode> {
let output = Command::new("defaults")
.args(["read", "-g", "AppleInterfaceStyle"])
.output()
.ok()?;
if output.status.success() {
Some(palette_mode_from_apple_interface_style(
&String::from_utf8_lossy(&output.stdout),
))
} else {
Some(PaletteMode::Light)
}
}
#[cfg(not(target_os = "macos"))]
fn detect_macos_palette_mode() -> Option<PaletteMode> {
None
}
#[cfg(any(target_os = "macos", test))]
pub(crate) fn palette_mode_from_apple_interface_style(value: &str) -> PaletteMode {
if value.trim().eq_ignore_ascii_case("dark") {
PaletteMode::Dark
} else {
PaletteMode::Light
}
}