1mod color;
8#[cfg(unix)]
9mod osc;
10
11use std::time::Duration;
12
13use anyhow::Result;
14
15use crate::config::Config;
16use crate::env::commands::format_env_export;
17
18pub use color::{Theme, parse_colorfgbg, parse_theme_str};
19
20const OSC_QUERY_BUDGET: Duration = Duration::from_millis(200);
26
27#[cfg(unix)]
28fn query_terminal_theme(budget: Duration) -> Option<Theme> {
29 osc::query_terminal_theme(budget)
30}
31
32#[cfg(not(unix))]
33fn query_terminal_theme(_budget: Duration) -> Option<Theme> {
34 None
35}
36
37pub fn resolve_local_terminal_theme_for_injection() -> Option<Theme> {
44 if let Ok(existing) = std::env::var("SHINE_TERMINAL_THEME")
45 && let Some(theme) = parse_theme_str(existing.trim())
46 {
47 return Some(theme);
48 }
49 query_terminal_theme(OSC_QUERY_BUDGET)
50}
51
52fn auto_sync_enabled(config: &Config) -> bool {
58 match std::env::var("SHINE_SYNC_TERMINAL_THEME") {
59 Ok(value) => value.trim() != "0",
60 Err(_) => config.sync_terminal_theme,
61 }
62}
63
64fn resolve_bat_theme_override(theme: Theme) -> String {
68 let (var, default) = match theme {
69 Theme::Light => ("SHINE_BAT_LIGHT_THEME", "GitHub"),
70 Theme::Dark => ("SHINE_BAT_DARK_THEME", "OneHalfDark"),
71 };
72 std::env::var(var)
73 .ok()
74 .filter(|value| !value.is_empty())
75 .unwrap_or_else(|| default.to_string())
76}
77
78fn resolve_theme() -> Option<Theme> {
83 if let Ok(existing) = std::env::var("SHINE_TERMINAL_THEME")
84 && let Some(theme) = parse_theme_str(existing.trim())
85 {
86 return Some(theme);
87 }
88 if let Ok(colorfgbg) = std::env::var("COLORFGBG")
89 && let Some(theme) = parse_colorfgbg(&colorfgbg)
90 {
91 return Some(theme);
92 }
93 query_terminal_theme(OSC_QUERY_BUDGET)
94}
95
96pub async fn handle_sync(auto: bool, quiet: bool) -> Result<()> {
102 let config = Config::load_global_runtime_for_dry_run().await?;
106
107 if auto && !auto_sync_enabled(&config) {
108 return Ok(());
109 }
110
111 let Some(theme) = resolve_theme() else {
112 if !quiet {
113 eprintln!("shine: could not determine terminal theme; leaving BAT_THEME unchanged");
114 }
115 return Ok(());
116 };
117
118 println!(
119 "{}",
120 format_env_export(&config.shell_type, "SHINE_TERMINAL_THEME", theme.as_str())
121 );
122
123 if std::env::var("BAT_THEME").is_ok() {
129 return Ok(());
130 }
131 let bat_theme = resolve_bat_theme_override(theme);
132 println!(
133 "{}",
134 format_env_export(&config.shell_type, "BAT_THEME", &bat_theme)
135 );
136 Ok(())
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142 use crate::test_support::env_lock;
143
144 #[allow(clippy::await_holding_lock)]
145 #[tokio::test(flavor = "current_thread")]
146 async fn handle_sync_skips_when_auto_and_env_var_disabled() {
147 let _guard = env_lock();
148 unsafe { std::env::set_var("SHINE_SYNC_TERMINAL_THEME", "0") };
150 unsafe { std::env::set_var("SHINE_TERMINAL_THEME", "dark") };
151
152 let result = handle_sync(true, true).await;
153 assert!(result.is_ok());
154
155 unsafe { std::env::remove_var("SHINE_SYNC_TERMINAL_THEME") };
156 unsafe { std::env::remove_var("SHINE_TERMINAL_THEME") };
157 }
158
159 #[allow(clippy::await_holding_lock)]
160 #[tokio::test(flavor = "current_thread")]
161 async fn handle_sync_manual_ignores_env_disable() {
162 let _guard = env_lock();
163 unsafe { std::env::set_var("SHINE_SYNC_TERMINAL_THEME", "0") };
164 unsafe { std::env::set_var("SHINE_TERMINAL_THEME", "dark") };
165 unsafe { std::env::remove_var("BAT_THEME") };
166
167 let result = handle_sync(false, true).await;
171 assert!(result.is_ok());
172
173 unsafe { std::env::remove_var("SHINE_SYNC_TERMINAL_THEME") };
174 unsafe { std::env::remove_var("SHINE_TERMINAL_THEME") };
175 }
176
177 #[test]
178 fn resolve_bat_theme_override_uses_published_env_vars() {
179 let _guard = env_lock();
180 unsafe { std::env::set_var("SHINE_BAT_LIGHT_THEME", "Solarized") };
182 assert_eq!(resolve_bat_theme_override(Theme::Light), "Solarized");
183 unsafe { std::env::remove_var("SHINE_BAT_LIGHT_THEME") };
184
185 assert_eq!(resolve_bat_theme_override(Theme::Light), "GitHub");
186 assert_eq!(resolve_bat_theme_override(Theme::Dark), "OneHalfDark");
187 }
188
189 #[test]
190 fn resolve_bat_theme_override_ignores_empty_env_var() {
191 let _guard = env_lock();
192 unsafe { std::env::set_var("SHINE_BAT_DARK_THEME", "") };
193 assert_eq!(resolve_bat_theme_override(Theme::Dark), "OneHalfDark");
194 unsafe { std::env::remove_var("SHINE_BAT_DARK_THEME") };
195 }
196
197 #[test]
198 fn resolve_theme_prefers_already_exported_var_over_colorfgbg() {
199 let _guard = env_lock();
200 unsafe { std::env::set_var("SHINE_TERMINAL_THEME", "light") };
201 unsafe { std::env::set_var("COLORFGBG", "15;0") }; assert_eq!(resolve_theme(), Some(Theme::Light));
203 unsafe { std::env::remove_var("SHINE_TERMINAL_THEME") };
204 unsafe { std::env::remove_var("COLORFGBG") };
205 }
206
207 #[test]
208 fn resolve_theme_falls_back_to_colorfgbg() {
209 let _guard = env_lock();
210 unsafe { std::env::remove_var("SHINE_TERMINAL_THEME") };
211 unsafe { std::env::set_var("COLORFGBG", "0;15") };
212 assert_eq!(resolve_theme(), Some(Theme::Light));
213 unsafe { std::env::remove_var("COLORFGBG") };
214 }
215}