Skip to main content

cc_switch_lib/cli/ui/
colors.rs

1use colored::Color;
2use colored::Colorize;
3use std::sync::{OnceLock, RwLock};
4
5use crate::app_config::AppType;
6
7use inquire::set_global_render_config;
8use inquire::ui::{Color as InquireColor, RenderConfig, StyleSheet, Styled};
9
10static TUI_THEME_APP: OnceLock<RwLock<Option<AppType>>> = OnceLock::new();
11
12fn tui_theme_app_cell() -> &'static RwLock<Option<AppType>> {
13    TUI_THEME_APP.get_or_init(|| RwLock::new(None))
14}
15
16pub fn set_tui_theme_app(app_type: Option<AppType>) {
17    *tui_theme_app_cell()
18        .write()
19        .expect("tui theme app lock poisoned") = app_type;
20
21    apply_inquire_theme();
22}
23
24fn get_tui_theme_app() -> Option<AppType> {
25    tui_theme_app_cell()
26        .read()
27        .expect("tui theme app lock poisoned")
28        .clone()
29}
30
31fn inquire_color_for_app(app_type: &AppType) -> InquireColor {
32    match app_type {
33        AppType::Codex => InquireColor::LightGreen,
34        AppType::Claude => InquireColor::LightCyan,
35        AppType::Gemini => InquireColor::LightMagenta,
36        AppType::OpenCode => InquireColor::LightGreen,
37        AppType::OpenClaw => InquireColor::LightRed,
38        AppType::Hermes => InquireColor::LightYellow,
39    }
40}
41
42fn apply_inquire_theme() {
43    if std::env::var("NO_COLOR").is_ok() {
44        set_global_render_config(RenderConfig::empty());
45        return;
46    }
47
48    let Some(app_type) = get_tui_theme_app() else {
49        set_global_render_config(RenderConfig::default());
50        return;
51    };
52
53    let accent = inquire_color_for_app(&app_type);
54
55    let cfg = RenderConfig::default_colored()
56        .with_prompt_prefix(Styled::new("?").with_fg(accent))
57        .with_answered_prompt_prefix(Styled::new(">").with_fg(accent))
58        .with_highlighted_option_prefix(Styled::new(">").with_fg(accent))
59        .with_selected_option(Some(StyleSheet::new().with_fg(accent)))
60        .with_selected_checkbox(Styled::new("[x]").with_fg(accent))
61        .with_help_message(StyleSheet::new().with_fg(accent))
62        .with_answer(StyleSheet::new().with_fg(accent));
63
64    set_global_render_config(cfg);
65}
66
67pub fn success(text: &str) -> String {
68    text.green().to_string()
69}
70
71pub fn error(text: &str) -> String {
72    text.red().to_string()
73}
74
75pub fn warning(text: &str) -> String {
76    text.yellow().to_string()
77}
78
79pub fn info(text: &str) -> String {
80    text.cyan().to_string()
81}
82
83fn highlight_color_for_app(app_type: &AppType) -> Color {
84    match app_type {
85        AppType::Codex => Color::BrightGreen,
86        AppType::Claude => Color::BrightCyan,
87        AppType::Gemini => Color::BrightMagenta,
88        AppType::OpenCode => Color::BrightGreen,
89        AppType::OpenClaw => Color::BrightRed,
90        AppType::Hermes => Color::BrightYellow,
91    }
92}
93
94pub fn highlight(text: &str) -> String {
95    let Some(app_type) = get_tui_theme_app() else {
96        return text.bright_blue().bold().to_string();
97    };
98
99    text.color(highlight_color_for_app(&app_type))
100        .bold()
101        .to_string()
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use serial_test::serial;
108
109    struct ColorOverrideGuard;
110
111    impl ColorOverrideGuard {
112        fn force_on() -> Self {
113            colored::control::set_override(true);
114            Self
115        }
116    }
117
118    impl Drop for ColorOverrideGuard {
119        fn drop(&mut self) {
120            colored::control::unset_override();
121            set_tui_theme_app(None);
122        }
123    }
124
125    #[test]
126    #[serial]
127    fn highlight_uses_app_theme_in_tui() {
128        let _guard = ColorOverrideGuard::force_on();
129
130        set_tui_theme_app(Some(AppType::Codex));
131        assert_eq!(
132            highlight("x"),
133            "x".color(Color::BrightGreen).bold().to_string()
134        );
135
136        set_tui_theme_app(Some(AppType::Claude));
137        assert_eq!(
138            highlight("x"),
139            "x".color(Color::BrightCyan).bold().to_string()
140        );
141
142        set_tui_theme_app(Some(AppType::Gemini));
143        assert_eq!(
144            highlight("x"),
145            "x".color(Color::BrightMagenta).bold().to_string()
146        );
147    }
148
149    #[test]
150    #[serial]
151    fn openclaw_theme_colors_are_distinct_from_existing_apps() {
152        let _guard = ColorOverrideGuard::force_on();
153
154        assert_eq!(
155            inquire_color_for_app(&AppType::OpenClaw),
156            InquireColor::LightRed
157        );
158        assert_ne!(
159            inquire_color_for_app(&AppType::OpenClaw),
160            inquire_color_for_app(&AppType::OpenCode)
161        );
162        assert_ne!(
163            inquire_color_for_app(&AppType::OpenClaw),
164            inquire_color_for_app(&AppType::Codex)
165        );
166
167        set_tui_theme_app(Some(AppType::OpenClaw));
168        assert_eq!(
169            highlight("x"),
170            "x".color(Color::BrightRed).bold().to_string()
171        );
172    }
173}