feat(custom-theme/mode): include generated name and is_dark in UI + config; toggle preview re-applies custom colors and falls back to Photon when off; fix pattern/initializers

This commit is contained in:
James Peter
2025-09-11 09:26:12 +10:00
parent 9be8212c93
commit 4d9335a3cd
5 changed files with 62 additions and 21 deletions
+3 -1
View File
@@ -475,6 +475,7 @@ pub fn set_custom_theme(
label: &str,
colors: &ThemeColors,
set_active: bool,
is_dark: Option<bool>,
) -> anyhow::Result<()> {
let config_path = codex_home.join(CONFIG_TOML_FILE);
let mut doc = match std::fs::read_to_string(&config_path) {
@@ -488,6 +489,7 @@ pub fn set_custom_theme(
doc["tui"]["theme"]["name"] = toml_edit::value("custom");
}
doc["tui"]["theme"]["label"] = toml_edit::value(label);
if let Some(d) = is_dark { doc["tui"]["theme"]["is_dark"] = toml_edit::value(d); }
// Ensure colors table exists and write provided keys
{
@@ -499,7 +501,7 @@ pub fn set_custom_theme(
if !theme_tbl.contains_key("colors") {
theme_tbl.insert("colors", It::Table(toml_edit::Table::new()));
}
let colors_tbl = theme_tbl["colors"].as_table_mut().unwrap();
let colors_tbl = theme_tbl["colors"].as_table_mut().unwrap();
macro_rules! set_opt {
($key:ident) => {
if let Some(ref v) = colors.$key { colors_tbl.insert(stringify!($key), toml_edit::value(v.clone())); }
+7
View File
@@ -216,6 +216,12 @@ pub struct ThemeConfig {
/// UI may display it in place of the generic "Custom" label.
#[serde(default)]
pub label: Option<String>,
/// Optional hint whether the custom theme targets a dark background.
/// When present and `name == Custom`, the UI can show "Dark - <label>"
/// or "Light - <label>" in lists.
#[serde(default)]
pub is_dark: Option<bool>,
}
impl Default for ThemeConfig {
@@ -224,6 +230,7 @@ impl Default for ThemeConfig {
name: ThemeName::default(),
colors: ThemeColors::default(),
label: None,
is_dark: None,
}
}
}
+2 -2
View File
@@ -969,7 +969,7 @@ impl App<'_> {
if matches!(new_theme, codex_core::config_types::ThemeName::Custom) {
// Prefer runtime custom colors; fall back to config on disk
if let Some(colors) = crate::theme::custom_theme_colors() {
crate::theme::init_theme(&codex_core::config_types::ThemeConfig { name: new_theme, colors, label: crate::theme::custom_theme_label() });
crate::theme::init_theme(&codex_core::config_types::ThemeConfig { name: new_theme, colors, label: crate::theme::custom_theme_label(), is_dark: crate::theme::custom_theme_is_dark() });
} else if let Ok(cfg) = codex_core::config::Config::load_with_cli_overrides(vec![], codex_core::config::ConfigOverrides::default()) {
crate::theme::init_theme(&cfg.tui.theme);
} else {
@@ -1010,7 +1010,7 @@ impl App<'_> {
// Switch the theme immediately for preview (no history event)
if matches!(new_theme, codex_core::config_types::ThemeName::Custom) {
if let Some(colors) = crate::theme::custom_theme_colors() {
crate::theme::init_theme(&codex_core::config_types::ThemeConfig { name: new_theme, colors, label: crate::theme::custom_theme_label() });
crate::theme::init_theme(&codex_core::config_types::ThemeConfig { name: new_theme, colors, label: crate::theme::custom_theme_label(), is_dark: crate::theme::custom_theme_is_dark() });
} else if let Ok(cfg) = codex_core::config::Config::load_with_cli_overrides(vec![], codex_core::config::ConfigOverrides::default()) {
crate::theme::init_theme(&cfg.tui.theme);
} else {
@@ -153,9 +153,14 @@ impl ThemeSelectionView {
"Calm and peaceful",
),
];
// Append custom theme if available
if crate::theme::custom_theme_label().is_some() {
v.push((ThemeName::Custom, "Custom", "Your saved custom theme"));
// Append custom theme if available (use saved label and light/dark prefix)
if let Some(label) = crate::theme::custom_theme_label() {
let name = if crate::theme::custom_theme_is_dark().unwrap_or(false) {
format!("Dark - {}", label)
} else {
format!("Light - {}", label)
};
v.push((ThemeName::Custom, Box::leak(name.into_boxed_str()), "Your saved custom theme"));
}
v
}
@@ -557,6 +562,7 @@ impl ThemeSelectionView {
"type": "object",
"properties": {
"name": {"type": "string", "minLength": 1, "maxLength": 40},
"is_dark": {"type": "boolean"},
"colors": {
"type": "object",
"properties": {
@@ -679,6 +685,7 @@ impl ThemeSelectionView {
}
};
let name = v.get("name").and_then(|x| x.as_str()).unwrap_or("Custom").trim().to_string();
let is_dark = v.get("is_dark").and_then(|x| x.as_bool());
let mut colors = codex_core::config_types::ThemeColors::default();
if let Some(map) = v.get("colors").and_then(|x| x.as_object()) {
let get = |k: &str| map.get(k).and_then(|x| x.as_str()).map(|s| s.trim().to_string());
@@ -704,7 +711,7 @@ impl ThemeSelectionView {
colors.spinner = get("spinner");
colors.progress = get("progress");
}
let _ = progress_tx.send(ProgressMsg::CompletedThemeOk(name, colors));
let _ = progress_tx.send(ProgressMsg::CompletedThemeOk(name, colors, is_dark));
});
});
}
@@ -750,6 +757,7 @@ struct CreateThemeState {
preview_on: std::cell::Cell<bool>,
review_focus_is_toggle: std::cell::Cell<bool>,
last_raw_output: std::cell::RefCell<Option<String>>,
proposed_is_dark: std::cell::Cell<Option<bool>>,
}
#[derive(Copy, Clone, PartialEq)]
@@ -769,7 +777,7 @@ enum ProgressMsg {
interval: u64,
frames: Vec<String>,
},
CompletedThemeOk(String, codex_core::config_types::ThemeColors),
CompletedThemeOk(String, codex_core::config_types::ThemeColors, Option<bool>),
// `_raw_snippet` is captured for potential future display/debugging
CompletedErr {
error: String,
@@ -970,6 +978,7 @@ impl<'a> BottomPaneView<'a> for ThemeSelectionView {
preview_on: std::cell::Cell::new(true),
review_focus_is_toggle: std::cell::Cell::new(true),
last_raw_output: std::cell::RefCell::new(None),
proposed_is_dark: std::cell::Cell::new(None),
});
} else {
// confirm_theme sets self.mode back to Overview
@@ -1135,20 +1144,26 @@ impl<'a> BottomPaneView<'a> for ThemeSelectionView {
s.proposed_name.borrow().clone(),
s.proposed_colors.borrow().clone(),
) {
crate::theme::set_custom_theme_colors(colors.clone());
crate::theme::set_custom_theme_label(name.clone());
crate::theme::init_theme(
&codex_core::config_types::ThemeConfig {
name: ThemeName::Custom,
colors,
label: Some(name),
},
);
crate::theme::init_theme(&codex_core::config_types::ThemeConfig {
name: ThemeName::Custom,
colors,
label: Some(name),
is_dark: s.proposed_is_dark.get(),
});
}
} else {
// Revert to previous theme
self.app_event_tx.send(AppEvent::PreviewTheme(
self.revert_theme_on_back,
));
// Revert to previous built-in or Photon if previous was Custom
let fallback = if self.revert_theme_on_back
== ThemeName::Custom
{
ThemeName::LightPhoton
} else {
self.revert_theme_on_back
};
self.app_event_tx
.send(AppEvent::PreviewTheme(fallback));
}
self.app_event_tx.send(AppEvent::RequestRedraw);
} else if s.action_idx == 0 {
@@ -1163,9 +1178,12 @@ impl<'a> BottomPaneView<'a> for ThemeSelectionView {
&name,
&colors,
s.preview_on.get(),
s.proposed_is_dark.get(),
);
}
crate::theme::set_custom_theme_label(name.clone());
crate::theme::set_custom_theme_colors(colors.clone());
crate::theme::set_custom_theme_is_dark(s.proposed_is_dark.get());
if s.preview_on.get() {
// Keep preview and set active in UI if chosen
crate::theme::init_theme(
@@ -1173,6 +1191,7 @@ impl<'a> BottomPaneView<'a> for ThemeSelectionView {
name: ThemeName::Custom,
colors: colors.clone(),
label: Some(name.clone()),
is_dark: s.proposed_is_dark.get(),
},
);
self.revert_theme_on_back = ThemeName::Custom;
@@ -1597,7 +1616,7 @@ impl<'a> BottomPaneView<'a> for ThemeSelectionView {
self.app_event_tx.send(AppEvent::RequestRedraw);
}
Err(std::sync::mpsc::TryRecvError::Empty) => break,
Ok(ProgressMsg::CompletedThemeOk(_, _)) => {}
Ok(ProgressMsg::CompletedThemeOk(..)) => {}
Err(std::sync::mpsc::TryRecvError::Disconnected) => break,
}
}
@@ -1913,18 +1932,21 @@ impl<'a> BottomPaneView<'a> for ThemeSelectionView {
}
self.app_event_tx.send(AppEvent::RequestRedraw);
}
Ok(ProgressMsg::CompletedThemeOk(name, colors)) => {
Ok(ProgressMsg::CompletedThemeOk(name, colors, is_dark)) => {
if let Mode::CreateTheme(ref sm) = self.mode {
sm.is_loading.set(false);
sm.step.set(CreateStep::Review);
sm.proposed_name.replace(Some(name.clone()));
sm.proposed_colors.replace(Some(colors.clone()));
sm.proposed_is_dark.set(is_dark);
crate::theme::set_custom_theme_label(name.clone());
crate::theme::set_custom_theme_is_dark(is_dark);
crate::theme::init_theme(
&codex_core::config_types::ThemeConfig {
name: ThemeName::Custom,
colors: colors.clone(),
label: Some(name),
is_dark,
},
);
}
+10
View File
@@ -9,6 +9,7 @@ lazy_static! {
static ref CURRENT_THEME: RwLock<Theme> = RwLock::new(Theme::default());
static ref CUSTOM_THEME_LABEL: RwLock<Option<String>> = RwLock::new(None);
static ref CUSTOM_THEME_COLORS: RwLock<Option<codex_core::config_types::ThemeColors>> = RwLock::new(None);
static ref CUSTOM_THEME_IS_DARK: RwLock<Option<bool>> = RwLock::new(None);
}
/// Represents a complete theme with all colors resolved
@@ -77,6 +78,7 @@ pub fn init_theme(config: &ThemeConfig) {
if matches!(config.name, ThemeName::Custom) {
*CUSTOM_THEME_LABEL.write().unwrap() = config.label.clone();
*CUSTOM_THEME_COLORS.write().unwrap() = Some(config.colors.clone());
*CUSTOM_THEME_IS_DARK.write().unwrap() = config.is_dark;
}
}
@@ -105,6 +107,14 @@ pub fn custom_theme_colors() -> Option<codex_core::config_types::ThemeColors> {
CUSTOM_THEME_COLORS.read().unwrap().clone()
}
pub fn set_custom_theme_is_dark(is_dark: Option<bool>) {
*CUSTOM_THEME_IS_DARK.write().unwrap() = is_dark;
}
pub fn custom_theme_is_dark() -> Option<bool> {
CUSTOM_THEME_IS_DARK.read().unwrap().clone()
}
/// Switch to a different predefined theme
pub fn switch_theme(theme_name: ThemeName) {
let theme = get_predefined_theme(theme_name);