use std::io::IsTerminal;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Mutex, OnceLock};
use owo_colors::{AnsiColors, DynColors, Style};
use crate::config;
static COLOR_ENABLED: AtomicBool = AtomicBool::new(true);
#[derive(Debug, Clone, Copy)]
pub(crate) struct ColorConfig {
enabled: bool,
}
impl ColorConfig {
pub fn new(choice: clap::ColorChoice) -> Self {
let enabled = match choice {
clap::ColorChoice::Always => true,
clap::ColorChoice::Never => false,
clap::ColorChoice::Auto => {
if let Ok(val) = std::env::var("NO_COLOR")
&& !val.is_empty()
{
return Self::with_enabled(false);
}
std::io::stdout().is_terminal()
}
};
Self::with_enabled(enabled)
}
fn with_enabled(enabled: bool) -> Self {
COLOR_ENABLED.store(enabled, Ordering::Relaxed);
Self { enabled }
}
pub fn is_enabled(self) -> bool {
self.enabled
}
}
pub(crate) struct ColorScheme;
impl ColorScheme {
pub fn operation(text: &str) -> String {
apply_style(theme().operation, text)
}
pub fn path(text: &str) -> String {
apply_style(theme().path, text)
}
pub fn skip(text: &str) -> String {
apply_style(theme().skip, text)
}
pub fn hook_type(text: &str) -> String {
apply_style(theme().hook_type, text)
}
pub fn hook_description(text: &str) -> String {
apply_style(theme().hook_description, text)
}
pub fn hook_arrow(text: &str) -> String {
apply_style(theme().hook_arrow, text)
}
pub fn hook_running(text: &str) -> String {
apply_style(theme().hook_running, text)
}
pub fn success_label(text: &str) -> String {
apply_style(theme().success_label, text)
}
pub fn failure_label(text: &str) -> String {
apply_style(theme().failure_label, text)
}
pub fn error(text: &str) -> String {
apply_style(theme().error, text)
}
pub fn warning(text: &str) -> String {
apply_style(theme().warning, text)
}
pub fn exit_code(text: &str) -> String {
apply_style(theme().exit_code, text)
}
pub fn header(text: &str) -> String {
apply_style(theme().header, text)
}
pub fn branch(text: &str) -> String {
apply_style(theme().branch, text)
}
pub fn hash(text: &str) -> String {
apply_style(theme().hash, text)
}
pub fn diff_removed(text: &str) -> String {
apply_style(theme().diff_removed, text)
}
pub fn diff_added(text: &str) -> String {
apply_style(theme().diff_added, text)
}
pub fn dimmed(text: &str) -> String {
apply_style(theme().dimmed, text)
}
}
#[derive(Clone, Copy)]
struct CliTheme {
operation: Style,
path: Style,
skip: Style,
hook_type: Style,
hook_description: Style,
hook_arrow: Style,
hook_running: Style,
success_label: Style,
failure_label: Style,
error: Style,
warning: Style,
exit_code: Style,
header: Style,
branch: Style,
hash: Style,
diff_removed: Style,
diff_added: Style,
dimmed: Style,
}
impl Default for CliTheme {
fn default() -> Self {
Self {
operation: Style::new().color(DynColors::Ansi(AnsiColors::Green)),
path: Style::new().color(DynColors::Ansi(AnsiColors::Cyan)),
skip: Style::new().color(DynColors::Ansi(AnsiColors::Yellow)),
hook_type: Style::new().color(DynColors::Ansi(AnsiColors::Magenta)),
hook_description: Style::new().color(DynColors::Ansi(AnsiColors::Green)),
hook_arrow: Style::new().dimmed(),
hook_running: Style::new().color(DynColors::Ansi(AnsiColors::Green)),
success_label: Style::new().color(DynColors::Ansi(AnsiColors::Green)),
failure_label: Style::new().color(DynColors::Ansi(AnsiColors::Red)),
error: Style::new().color(DynColors::Ansi(AnsiColors::Red)),
warning: Style::new()
.color(DynColors::Ansi(AnsiColors::Yellow))
.bold(),
exit_code: Style::new().color(DynColors::Ansi(AnsiColors::Red)),
header: Style::new()
.color(DynColors::Ansi(AnsiColors::White))
.bold(),
branch: Style::new().color(DynColors::Ansi(AnsiColors::Magenta)),
hash: Style::new().color(DynColors::Ansi(AnsiColors::Yellow)),
diff_removed: Style::new().color(DynColors::Ansi(AnsiColors::Red)),
diff_added: Style::new().color(DynColors::Ansi(AnsiColors::Green)),
dimmed: Style::new().dimmed(),
}
}
}
impl CliTheme {
fn from_colors(colors: &config::UiColors) -> Self {
let mut theme = CliTheme::default();
if let Some(color) = colors.accent {
let style = Style::new().color(to_dyn_color(color));
theme.operation = style;
theme.path = style;
theme.hook_running = style;
theme.success_label = style;
theme.diff_added = style;
}
if let Some(color) = colors.warning {
theme.skip = Style::new().color(to_dyn_color(color));
theme.warning = Style::new().color(to_dyn_color(color)).bold();
theme.hash = Style::new().color(to_dyn_color(color));
}
if let Some(color) = colors.error {
theme.failure_label = Style::new().color(to_dyn_color(color));
theme.error = Style::new().color(to_dyn_color(color));
theme.exit_code = Style::new().color(to_dyn_color(color));
theme.diff_removed = Style::new().color(to_dyn_color(color));
}
if let Some(color) = colors.title {
theme.hook_type = Style::new().color(to_dyn_color(color));
}
if let Some(color) = colors.label {
theme.branch = Style::new().color(to_dyn_color(color));
}
if let Some(color) = colors.text {
theme.hook_description = Style::new().color(to_dyn_color(color));
}
if let Some(color) = colors.header {
theme.header = Style::new().color(to_dyn_color(color)).bold();
}
if let Some(color) = colors.muted {
theme.hook_arrow = Style::new().color(to_dyn_color(color)).dimmed();
theme.dimmed = Style::new().color(to_dyn_color(color)).dimmed();
}
theme
}
}
static CLI_THEME: OnceLock<Mutex<CliTheme>> = OnceLock::new();
pub(crate) fn set_cli_theme(colors: &config::UiColors) {
let lock = CLI_THEME.get_or_init(|| Mutex::new(CliTheme::default()));
if let Ok(mut theme) = lock.lock() {
*theme = CliTheme::from_colors(colors);
}
}
fn theme() -> CliTheme {
let lock = CLI_THEME.get_or_init(|| Mutex::new(CliTheme::default()));
match lock.lock() {
Ok(theme) => *theme,
Err(_) => CliTheme::default(),
}
}
fn apply_style(style: Style, text: &str) -> String {
if COLOR_ENABLED.load(Ordering::Relaxed) {
style.style(text).to_string()
} else {
text.to_string()
}
}
fn to_dyn_color(color: config::UiColor) -> DynColors {
match color {
config::UiColor::Rgb(r, g, b) => DynColors::Rgb(r, g, b),
config::UiColor::Named(name) => {
let ansi = match name {
config::UiColorName::Default => AnsiColors::Default,
config::UiColorName::Black => AnsiColors::Black,
config::UiColorName::Red => AnsiColors::Red,
config::UiColorName::Green => AnsiColors::Green,
config::UiColorName::Yellow => AnsiColors::Yellow,
config::UiColorName::Blue => AnsiColors::Blue,
config::UiColorName::Magenta => AnsiColors::Magenta,
config::UiColorName::Cyan => AnsiColors::Cyan,
config::UiColorName::Gray => AnsiColors::BrightBlack,
config::UiColorName::DarkGray => AnsiColors::Black,
config::UiColorName::LightRed => AnsiColors::BrightRed,
config::UiColorName::LightGreen => AnsiColors::BrightGreen,
config::UiColorName::LightYellow => AnsiColors::BrightYellow,
config::UiColorName::LightBlue => AnsiColors::BrightBlue,
config::UiColorName::LightMagenta => AnsiColors::BrightMagenta,
config::UiColorName::LightCyan => AnsiColors::BrightCyan,
config::UiColorName::White => AnsiColors::White,
};
DynColors::Ansi(ansi)
}
}
}