mirror of
https://github.com/openai/codex.git
synced 2026-04-24 14:45:27 +00:00
feat(tui): live theme override and dynamic CODEX_HOME paths
set_theme_override now updates the runtime theme immediately instead of only setting the OnceLock, enabling live switching from /theme. Replace hardcoded ~/.codex/themes references with resolved $CODEX_HOME/themes paths in warnings, schema docs, and picker subtitle.
This commit is contained in:
@@ -1403,7 +1403,7 @@
|
||||
},
|
||||
"theme": {
|
||||
"default": null,
|
||||
"description": "Syntax highlighting theme name (kebab-case).\n\nWhen set, overrides automatic light/dark theme detection. Run with `--list-themes` or see docs for available names.",
|
||||
"description": "Syntax highlighting theme name (kebab-case).\n\nWhen set, overrides automatic light/dark theme detection. Use `/theme` in the TUI or see `$CODEX_HOME/themes` for custom themes.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -685,7 +685,7 @@ pub struct Tui {
|
||||
/// Syntax highlighting theme name (kebab-case).
|
||||
///
|
||||
/// When set, overrides automatic light/dark theme detection.
|
||||
/// Run with `--list-themes` or see docs for available names.
|
||||
/// Use `/theme` in the TUI or see `$CODEX_HOME/themes` for custom themes.
|
||||
#[serde(default)]
|
||||
pub theme: Option<String>,
|
||||
}
|
||||
|
||||
@@ -362,7 +362,9 @@ pub(crate) enum AppEvent {
|
||||
StatusLineSetupCancelled,
|
||||
|
||||
/// Apply a user-confirmed syntax theme selection.
|
||||
SyntaxThemeSelected { name: String },
|
||||
SyntaxThemeSelected {
|
||||
name: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// The exit strategy requested by the UI layer.
|
||||
|
||||
@@ -104,8 +104,8 @@ mod status_indicator_widget;
|
||||
mod streaming;
|
||||
mod style;
|
||||
mod terminal_palette;
|
||||
mod theme_picker;
|
||||
mod text_formatting;
|
||||
mod theme_picker;
|
||||
mod tooltips;
|
||||
mod tui;
|
||||
mod ui_consts;
|
||||
@@ -1227,9 +1227,7 @@ trust_level = "untrusted"
|
||||
|
||||
// Theme override must use the final config (not initial_config).
|
||||
// This mirrors the real call site in run_ratatui_app.
|
||||
if let Some(w) =
|
||||
validate_theme_name(config.tui_theme.as_deref(), Some(temp_dir.path()))
|
||||
{
|
||||
if let Some(w) = validate_theme_name(config.tui_theme.as_deref(), Some(temp_dir.path())) {
|
||||
config.startup_warnings.push(w);
|
||||
}
|
||||
|
||||
|
||||
@@ -39,9 +39,22 @@ pub(crate) fn set_theme_override(
|
||||
name: Option<String>,
|
||||
codex_home: Option<PathBuf>,
|
||||
) -> Option<String> {
|
||||
let warning = validate_theme_name(name.as_deref(), codex_home.as_deref());
|
||||
let _ = THEME_OVERRIDE.set(name);
|
||||
let _ = CODEX_HOME.set(codex_home);
|
||||
let mut warning = validate_theme_name(name.as_deref(), codex_home.as_deref());
|
||||
let override_set_ok = THEME_OVERRIDE.set(name.clone()).is_ok();
|
||||
let codex_home_set_ok = CODEX_HOME.set(codex_home.clone()).is_ok();
|
||||
if THEME.get().is_some() {
|
||||
set_syntax_theme(resolve_theme_with_override(
|
||||
name.as_deref(),
|
||||
codex_home.as_deref(),
|
||||
));
|
||||
}
|
||||
if !override_set_ok || !codex_home_set_ok {
|
||||
let duplicate_msg = "Ignoring duplicate or late syntax theme override persistence; runtime theme was updated from the latest override, but persisted override config can only be initialized once.";
|
||||
tracing::warn!("{duplicate_msg}");
|
||||
if warning.is_none() {
|
||||
warning = Some(duplicate_msg.to_string());
|
||||
}
|
||||
}
|
||||
warning
|
||||
}
|
||||
|
||||
@@ -49,6 +62,9 @@ pub(crate) fn set_theme_override(
|
||||
/// `.tmTheme` file. Returns a user-facing warning when it does not.
|
||||
pub(crate) fn validate_theme_name(name: Option<&str>, codex_home: Option<&Path>) -> Option<String> {
|
||||
let name = name?;
|
||||
let custom_theme_path_display = codex_home
|
||||
.map(|home| custom_theme_path(name, home).display().to_string())
|
||||
.unwrap_or_else(|| format!("$CODEX_HOME/themes/{name}.tmTheme"));
|
||||
// Bundled themes always resolve.
|
||||
if parse_theme_name(name).is_some() {
|
||||
return None;
|
||||
@@ -62,7 +78,7 @@ pub(crate) fn validate_theme_name(name: Option<&str>, codex_home: Option<&Path>)
|
||||
return None;
|
||||
}
|
||||
return Some(format!(
|
||||
"Syntax theme \"{name}\" was found at ~/.codex/themes/{name}.tmTheme \
|
||||
"Syntax theme \"{name}\" was found at {custom_theme_path_display} \
|
||||
but could not be parsed. Falling back to auto-detection."
|
||||
));
|
||||
}
|
||||
@@ -70,7 +86,7 @@ pub(crate) fn validate_theme_name(name: Option<&str>, codex_home: Option<&Path>)
|
||||
Some(format!(
|
||||
"Unknown syntax theme \"{name}\", falling back to auto-detection. \
|
||||
Use a bundled name or place a .tmTheme file at \
|
||||
~/.codex/themes/{name}.tmTheme"
|
||||
{custom_theme_path_display}"
|
||||
))
|
||||
}
|
||||
|
||||
@@ -125,17 +141,17 @@ fn load_custom_theme(name: &str, codex_home: &Path) -> Option<Theme> {
|
||||
|
||||
/// Build the theme from current override/auto-detection settings.
|
||||
/// Extracted from the old `theme()` init closure so it can be reused.
|
||||
fn build_default_theme() -> Theme {
|
||||
fn resolve_theme_with_override(name: Option<&str>, codex_home: Option<&Path>) -> Theme {
|
||||
let ts = two_face::theme::extra();
|
||||
|
||||
// Honor user-configured theme if valid.
|
||||
if let Some(Some(name)) = THEME_OVERRIDE.get() {
|
||||
if let Some(name) = name {
|
||||
// 1. Try bundled theme by kebab-case name.
|
||||
if let Some(theme_name) = parse_theme_name(name) {
|
||||
return ts.get(theme_name).clone();
|
||||
}
|
||||
// 2. Try loading ~/.codex/themes/{name}.tmTheme from disk.
|
||||
if let Some(Some(home)) = CODEX_HOME.get() {
|
||||
// 2. Try loading {CODEX_HOME}/themes/{name}.tmTheme from disk.
|
||||
if let Some(home) = codex_home {
|
||||
if let Some(theme) = load_custom_theme(name, home) {
|
||||
return theme;
|
||||
}
|
||||
@@ -151,6 +167,16 @@ fn build_default_theme() -> Theme {
|
||||
ts.get(name).clone()
|
||||
}
|
||||
|
||||
/// Build the theme from current override/auto-detection settings.
|
||||
/// Extracted from the old `theme()` init closure so it can be reused.
|
||||
fn build_default_theme() -> Theme {
|
||||
let name = THEME_OVERRIDE.get().and_then(|name| name.as_deref());
|
||||
let codex_home = CODEX_HOME
|
||||
.get()
|
||||
.and_then(|codex_home| codex_home.as_deref());
|
||||
resolve_theme_with_override(name, codex_home)
|
||||
}
|
||||
|
||||
fn theme_lock() -> &'static RwLock<Theme> {
|
||||
THEME.get_or_init(|| RwLock::new(build_default_theme()))
|
||||
}
|
||||
|
||||
@@ -204,12 +204,18 @@ pub(crate) fn build_theme_picker_params(
|
||||
highlight::set_syntax_theme(original_theme.clone());
|
||||
})
|
||||
as Box<dyn Fn(&crate::app_event_sender::AppEventSender) + Send + Sync>);
|
||||
let themes_dir_display = codex_home_owned
|
||||
.as_ref()
|
||||
.map(|home| home.join("themes"))
|
||||
.unwrap_or_else(|| Path::new("$CODEX_HOME").join("themes"))
|
||||
.display()
|
||||
.to_string();
|
||||
|
||||
SelectionViewParams {
|
||||
title: Some("Select Syntax Theme".to_string()),
|
||||
subtitle: Some(
|
||||
"Custom .tmTheme files can be added to the ~/.codex/themes directory.".to_string(),
|
||||
),
|
||||
subtitle: Some(format!(
|
||||
"Custom .tmTheme files can be added to the {themes_dir_display} directory."
|
||||
)),
|
||||
footer_hint: Some(standard_popup_hint_line()),
|
||||
items,
|
||||
is_searchable: true,
|
||||
|
||||
Reference in New Issue
Block a user