use std::sync::atomic::{AtomicU8, Ordering};
use ratatui::style::Color;
use super::icon::Icon;
use crate::domain::session::{ReviewRequestState, Status};
use crate::domain::theme::ColorTheme;
static ACTIVE_THEME: AtomicU8 = AtomicU8::new(theme_index(ColorTheme::Current));
pub mod palette {
use ratatui::style::Color;
use super::{active_palette, token_color};
#[must_use]
pub fn accent() -> Color {
token_color(|palette| palette.accent)
}
#[must_use]
pub fn accent_soft() -> Color {
token_color(|palette| palette.accent_soft)
}
#[must_use]
pub fn border() -> Color {
token_color(|palette| palette.border)
}
#[must_use]
pub fn danger() -> Color {
token_color(|palette| palette.danger)
}
#[must_use]
pub fn danger_soft() -> Color {
token_color(|palette| palette.danger_soft)
}
#[must_use]
pub fn info() -> Color {
token_color(|palette| palette.info)
}
#[must_use]
pub fn question() -> Color {
token_color(|palette| palette.question)
}
#[must_use]
pub fn surface() -> Color {
token_color(|palette| palette.surface)
}
#[must_use]
pub fn surface_clarification() -> Color {
token_color(|palette| palette.surface_clarification)
}
#[must_use]
pub fn surface_danger() -> Color {
token_color(|palette| palette.surface_danger)
}
#[must_use]
pub fn surface_elevated() -> Color {
token_color(|palette| palette.surface_elevated)
}
#[must_use]
pub fn surface_success() -> Color {
token_color(|palette| palette.surface_success)
}
#[must_use]
pub fn surface_overlay() -> Color {
token_color(|palette| palette.surface_overlay)
}
#[must_use]
pub fn text() -> Color {
token_color(|palette| palette.text)
}
#[must_use]
pub fn text_muted() -> Color {
token_color(|palette| palette.text_muted)
}
#[must_use]
pub fn text_subtle() -> Color {
token_color(|palette| palette.text_subtle)
}
#[must_use]
pub fn success() -> Color {
token_color(|palette| palette.success)
}
#[must_use]
pub fn success_soft() -> Color {
token_color(|palette| palette.success_soft)
}
#[must_use]
pub fn warning() -> Color {
token_color(|palette| palette.warning)
}
#[must_use]
pub fn warning_soft() -> Color {
token_color(|palette| palette.warning_soft)
}
#[must_use]
pub fn active() -> super::ThemePalette {
active_palette()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ThemePalette {
pub accent: Color,
pub accent_soft: Color,
pub border: Color,
pub danger: Color,
pub danger_soft: Color,
pub info: Color,
pub question: Color,
pub surface: Color,
pub surface_clarification: Color,
pub surface_danger: Color,
pub surface_elevated: Color,
pub surface_success: Color,
pub surface_overlay: Color,
pub text: Color,
pub text_muted: Color,
pub text_subtle: Color,
pub success: Color,
pub success_soft: Color,
pub warning: Color,
pub warning_soft: Color,
}
const CURRENT_PALETTE: ThemePalette = ThemePalette {
accent: Color::Cyan,
accent_soft: Color::LightCyan,
border: Color::DarkGray,
danger: Color::Red,
danger_soft: Color::LightRed,
info: Color::LightBlue,
question: Color::LightMagenta,
surface: Color::DarkGray,
surface_clarification: Color::Rgb(28, 38, 48),
surface_danger: Color::Rgb(48, 24, 24),
surface_elevated: Color::Gray,
surface_success: Color::Rgb(18, 44, 26),
surface_overlay: Color::Black,
text: Color::White,
text_muted: Color::Gray,
text_subtle: Color::DarkGray,
success: Color::Green,
success_soft: Color::LightGreen,
warning: Color::Yellow,
warning_soft: Color::LightYellow,
};
const HACKER_PALETTE: ThemePalette = ThemePalette {
accent: Color::Rgb(61, 221, 106),
accent_soft: Color::Rgb(42, 179, 85),
border: Color::Rgb(27, 61, 32),
danger: Color::Rgb(255, 94, 94),
danger_soft: Color::Rgb(214, 88, 88),
info: Color::Rgb(80, 122, 92),
question: Color::Rgb(184, 255, 198),
surface: Color::Rgb(13, 22, 13),
surface_clarification: Color::Rgb(20, 34, 22),
surface_danger: Color::Rgb(48, 20, 20),
surface_elevated: Color::Rgb(27, 61, 32),
surface_success: Color::Rgb(18, 44, 26),
surface_overlay: Color::Black,
text: Color::Rgb(224, 255, 230),
text_muted: Color::Rgb(80, 122, 92),
text_subtle: Color::Rgb(42, 89, 53),
success: Color::Rgb(61, 221, 106),
success_soft: Color::Rgb(42, 179, 85),
warning: Color::Rgb(230, 219, 116),
warning_soft: Color::Rgb(250, 245, 160),
};
pub fn set_active_theme(theme: ColorTheme) {
ACTIVE_THEME.store(theme_index(theme), Ordering::Relaxed);
}
#[must_use]
pub(crate) fn active_theme_cache_version() -> u64 {
u64::from(ACTIVE_THEME.load(Ordering::Relaxed))
}
#[must_use]
pub fn status_color(status: Status) -> Color {
match status {
Status::New => palette::text_muted(),
Status::InProgress => palette::warning(),
Status::Review | Status::AgentReview => palette::info(),
Status::Question => palette::question(),
Status::Queued => palette::accent_soft(),
Status::Rebasing | Status::Merging => palette::accent(),
Status::Done => palette::success(),
Status::Canceled => palette::danger(),
}
}
#[must_use]
pub fn status_icon(status: Status) -> Icon {
match status {
Status::New | Status::Review | Status::Question | Status::Queued => Icon::Pending,
Status::InProgress | Status::AgentReview | Status::Rebasing | Status::Merging => {
Icon::current_spinner()
}
Status::Done => Icon::Check,
Status::Canceled => Icon::Cross,
}
}
#[must_use]
pub fn forge_indicator_color(state: Option<ReviewRequestState>) -> Color {
match state {
Some(ReviewRequestState::Open) => palette::warning(),
Some(ReviewRequestState::Merged) => palette::success(),
Some(ReviewRequestState::Closed) => palette::danger(),
None => palette::accent_soft(),
}
}
#[must_use]
fn active_palette() -> ThemePalette {
match ACTIVE_THEME.load(Ordering::Relaxed) {
1 => HACKER_PALETTE,
_ => CURRENT_PALETTE,
}
}
#[must_use]
fn token_color(selector: impl FnOnce(ThemePalette) -> Color) -> Color {
selector(active_palette())
}
const fn theme_index(theme: ColorTheme) -> u8 {
match theme {
ColorTheme::Current => 0,
ColorTheme::Hacker => 1,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn status_color_returns_muted_text_for_new() {
set_active_theme(ColorTheme::Current);
let color = status_color(Status::New);
assert_eq!(color, palette::text_muted());
}
#[test]
fn status_color_returns_success_for_done() {
set_active_theme(ColorTheme::Current);
let color = status_color(Status::Done);
assert_eq!(color, palette::success());
}
#[test]
fn status_color_returns_danger_for_canceled() {
set_active_theme(ColorTheme::Current);
let color = status_color(Status::Canceled);
assert_eq!(color, palette::danger());
}
#[test]
fn status_color_returns_warning_for_in_progress() {
set_active_theme(ColorTheme::Current);
let color = status_color(Status::InProgress);
assert_eq!(color, palette::warning());
}
#[test]
fn status_color_returns_info_for_review() {
set_active_theme(ColorTheme::Current);
let color = status_color(Status::Review);
assert_eq!(color, palette::info());
}
#[test]
fn status_color_returns_accent_for_merging() {
set_active_theme(ColorTheme::Current);
let color = status_color(Status::Merging);
assert_eq!(color, palette::accent());
}
#[test]
fn status_color_uses_hacker_palette_when_active() {
set_active_theme(ColorTheme::Hacker);
let color = status_color(Status::Merging);
assert_eq!(color, HACKER_PALETTE.accent);
set_active_theme(ColorTheme::Current);
}
#[test]
fn status_icon_returns_check_for_done() {
let icon = status_icon(Status::Done);
assert!(matches!(icon, Icon::Check));
}
#[test]
fn status_icon_returns_cross_for_canceled() {
let icon = status_icon(Status::Canceled);
assert!(matches!(icon, Icon::Cross));
}
#[test]
fn status_icon_returns_pending_for_new() {
let icon = status_icon(Status::New);
assert!(matches!(icon, Icon::Pending));
}
#[test]
fn forge_indicator_color_returns_warning_for_open() {
set_active_theme(ColorTheme::Current);
let color = forge_indicator_color(Some(ReviewRequestState::Open));
assert_eq!(color, palette::warning());
}
#[test]
fn forge_indicator_color_returns_success_for_merged() {
set_active_theme(ColorTheme::Current);
let color = forge_indicator_color(Some(ReviewRequestState::Merged));
assert_eq!(color, palette::success());
}
#[test]
fn forge_indicator_color_returns_danger_for_closed() {
set_active_theme(ColorTheme::Current);
let color = forge_indicator_color(Some(ReviewRequestState::Closed));
assert_eq!(color, palette::danger());
}
#[test]
fn forge_indicator_color_returns_accent_soft_for_published_only() {
set_active_theme(ColorTheme::Current);
let color = forge_indicator_color(None);
assert_eq!(color, palette::accent_soft());
}
}