mirror of
https://github.com/openai/codex.git
synced 2026-11-03 19:53:25 +00:00
tui: sync theme picker state and validate custom themes
This commit is contained in:
+19
-1
@@ -2623,7 +2623,7 @@ impl App {
|
||||
) {
|
||||
crate::render::highlight::set_syntax_theme(theme);
|
||||
}
|
||||
self.config.tui_theme = Some(name);
|
||||
self.sync_tui_theme_selection(name);
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!(error = %err, "failed to persist theme selection");
|
||||
@@ -2810,6 +2810,11 @@ impl App {
|
||||
self.chat_widget.set_personality(personality);
|
||||
}
|
||||
|
||||
fn sync_tui_theme_selection(&mut self, name: String) {
|
||||
self.config.tui_theme = Some(name.clone());
|
||||
self.chat_widget.set_tui_theme(Some(name));
|
||||
}
|
||||
|
||||
fn personality_label(personality: Personality) -> &'static str {
|
||||
match personality {
|
||||
Personality::None => "None",
|
||||
@@ -3682,6 +3687,19 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sync_tui_theme_selection_updates_chat_widget_config_copy() {
|
||||
let mut app = make_test_app().await;
|
||||
|
||||
app.sync_tui_theme_selection("dracula".to_string());
|
||||
|
||||
assert_eq!(app.config.tui_theme.as_deref(), Some("dracula"));
|
||||
assert_eq!(
|
||||
app.chat_widget.config_ref().tui_theme.as_deref(),
|
||||
Some("dracula")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn backtrack_selection_with_duplicate_history_targets_unique_turn() {
|
||||
let (mut app, _app_event_rx, mut op_rx) = make_test_app_with_channels().await;
|
||||
|
||||
@@ -6350,6 +6350,11 @@ impl ChatWidget {
|
||||
self.config.personality = Some(personality);
|
||||
}
|
||||
|
||||
/// Set the syntax theme override in the widget's config copy.
|
||||
pub(crate) fn set_tui_theme(&mut self, theme: Option<String>) {
|
||||
self.config.tui_theme = theme;
|
||||
}
|
||||
|
||||
/// Set the model in the widget's config copy and stored collaboration mode.
|
||||
pub(crate) fn set_model(&mut self, model: &str) {
|
||||
self.current_collaboration_mode =
|
||||
|
||||
@@ -53,10 +53,19 @@ pub(crate) fn validate_theme_name(name: Option<&str>, codex_home: Option<&Path>)
|
||||
if parse_theme_name(name).is_some() {
|
||||
return None;
|
||||
}
|
||||
// Check for a custom .tmTheme file on disk.
|
||||
let has_custom_file = codex_home.is_some_and(|home| custom_theme_path(name, home).is_file());
|
||||
if has_custom_file {
|
||||
return None;
|
||||
// Custom themes must parse successfully; an unreadable/invalid file should
|
||||
// still surface a startup warning so users can diagnose configuration issues.
|
||||
if let Some(home) = codex_home {
|
||||
let custom_path = custom_theme_path(name, home);
|
||||
if custom_path.is_file() {
|
||||
if load_custom_theme(name, home).is_some() {
|
||||
return None;
|
||||
}
|
||||
return Some(format!(
|
||||
"Syntax theme \"{name}\" was found at ~/.codex/themes/{name}.tmTheme \
|
||||
but could not be parsed. Falling back to auto-detection."
|
||||
));
|
||||
}
|
||||
}
|
||||
Some(format!(
|
||||
"Unknown syntax theme \"{name}\", falling back to auto-detection. \
|
||||
@@ -167,10 +176,10 @@ pub(crate) fn current_theme_name() -> String {
|
||||
if parse_theme_name(name).is_some() {
|
||||
return name.clone();
|
||||
}
|
||||
if let Some(Some(home)) = CODEX_HOME.get() {
|
||||
if custom_theme_path(name, home).is_file() {
|
||||
return name.clone();
|
||||
}
|
||||
if let Some(Some(home)) = CODEX_HOME.get()
|
||||
&& load_custom_theme(name, home).is_some()
|
||||
{
|
||||
return name.clone();
|
||||
}
|
||||
}
|
||||
// Adaptive default: light or dark based on terminal background.
|
||||
@@ -445,6 +454,24 @@ mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
fn write_minimal_tmtheme(path: &Path) {
|
||||
// Minimal valid .tmTheme plist (enough for syntect to parse).
|
||||
std::fs::write(
|
||||
path,
|
||||
r#"<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0"><dict>
|
||||
<key>name</key><string>Test</string>
|
||||
<key>settings</key><array><dict>
|
||||
<key>settings</key><dict>
|
||||
<key>foreground</key><string>#FFFFFF</string>
|
||||
<key>background</key><string>#000000</string>
|
||||
</dict></dict></array>
|
||||
</dict></plist>"#,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Reconstruct plain text from highlighted Lines.
|
||||
fn reconstructed(lines: &[Line<'static>]) -> String {
|
||||
lines
|
||||
@@ -793,21 +820,7 @@ mod tests {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let themes_dir = dir.path().join("themes");
|
||||
std::fs::create_dir(&themes_dir).unwrap();
|
||||
// Minimal valid .tmTheme plist (enough for syntect to parse).
|
||||
std::fs::write(
|
||||
themes_dir.join("test-custom.tmTheme"),
|
||||
r#"<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0"><dict>
|
||||
<key>name</key><string>Test</string>
|
||||
<key>settings</key><array><dict>
|
||||
<key>settings</key><dict>
|
||||
<key>foreground</key><string>#FFFFFF</string>
|
||||
<key>background</key><string>#000000</string>
|
||||
</dict></dict></array>
|
||||
</dict></plist>"#,
|
||||
)
|
||||
.unwrap();
|
||||
write_minimal_tmtheme(&themes_dir.join("test-custom.tmTheme"));
|
||||
let theme = load_custom_theme("test-custom", dir.path());
|
||||
assert!(theme.is_some(), "should load .tmTheme from themes dir");
|
||||
}
|
||||
@@ -843,14 +856,33 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_theme_name_none_when_custom_file_exists() {
|
||||
fn validate_theme_name_none_when_custom_file_is_valid() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let themes_dir = dir.path().join("themes");
|
||||
std::fs::create_dir(&themes_dir).unwrap();
|
||||
write_minimal_tmtheme(&themes_dir.join("my-fancy.tmTheme"));
|
||||
assert!(
|
||||
validate_theme_name(Some("my-fancy"), Some(dir.path())).is_none(),
|
||||
"should not warn when custom .tmTheme file parses successfully"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_theme_name_warns_when_custom_file_is_invalid() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let themes_dir = dir.path().join("themes");
|
||||
std::fs::create_dir(&themes_dir).unwrap();
|
||||
std::fs::write(themes_dir.join("my-fancy.tmTheme"), "placeholder").unwrap();
|
||||
let warning = validate_theme_name(Some("my-fancy"), Some(dir.path()));
|
||||
assert!(
|
||||
validate_theme_name(Some("my-fancy"), Some(dir.path())).is_none(),
|
||||
"should not warn when custom .tmTheme file exists on disk"
|
||||
warning.is_some(),
|
||||
"should warn when custom .tmTheme exists but cannot be parsed"
|
||||
);
|
||||
assert!(
|
||||
warning
|
||||
.as_deref()
|
||||
.is_some_and(|msg| msg.contains("could not be parsed")),
|
||||
"warning should explain that the theme file is invalid"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user