cc_switch_lib/cli/ui/
colors.rs1use 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}