tui: harden theme picker cancel and custom theme handling

This commit is contained in:
Felipe Coury
2026-02-09 16:00:00 -03:00
parent 85e1e7bf5a
commit 197131762b
3 changed files with 82 additions and 1 deletions

View File

@@ -2626,6 +2626,7 @@ impl App {
self.sync_tui_theme_selection(name);
}
Err(err) => {
self.restore_runtime_theme_from_config();
tracing::error!(error = %err, "failed to persist theme selection");
self.chat_widget
.add_error_message(format!("Failed to save theme: {err}"));
@@ -2815,6 +2816,27 @@ impl App {
self.chat_widget.set_tui_theme(Some(name));
}
fn restore_runtime_theme_from_config(&self) {
if let Some(name) = self.config.tui_theme.as_deref()
&& let Some(theme) =
crate::render::highlight::resolve_theme_by_name(name, Some(&self.config.codex_home))
{
crate::render::highlight::set_syntax_theme(theme);
return;
}
let auto_theme_name = match crate::terminal_palette::default_bg() {
Some(bg) if crate::color::is_light(bg) => "catppuccin-latte",
_ => "catppuccin-mocha",
};
if let Some(theme) = crate::render::highlight::resolve_theme_by_name(
auto_theme_name,
Some(&self.config.codex_home),
) {
crate::render::highlight::set_syntax_theme(theme);
}
}
fn personality_label(personality: Personality) -> &'static str {
match personality {
Personality::None => "None",

View File

@@ -407,6 +407,9 @@ impl ListSelectionView {
self.complete = true;
}
} else if selected_item.is_none() {
if let Some(cb) = &self.on_cancel {
cb(&self.app_event_tx);
}
self.complete = true;
}
}
@@ -1145,6 +1148,37 @@ mod tests {
);
}
#[test]
fn enter_with_no_matches_triggers_cancel_callback() {
let (tx_raw, mut rx) = unbounded_channel::<AppEvent>();
let tx = AppEventSender::new(tx_raw);
let mut view = ListSelectionView::new(
SelectionViewParams {
items: vec![SelectionItem {
name: "Read Only".to_string(),
dismiss_on_select: true,
..Default::default()
}],
is_searchable: true,
on_cancel: Some(Box::new(|tx: &_| {
tx.send(AppEvent::OpenApprovalsPopup);
})),
..Default::default()
},
tx,
);
view.set_search_query("no-matches".to_string());
view.handle_key_event(KeyEvent::from(KeyCode::Enter));
assert!(view.is_complete());
match rx.try_recv() {
Ok(AppEvent::OpenApprovalsPopup) => {}
Ok(other) => panic!("expected OpenApprovalsPopup cancel event, got {other:?}"),
Err(err) => panic!("expected cancel callback event, got {err}"),
}
}
#[test]
fn wraps_long_option_without_overflowing_columns() {
let (tx_raw, _rx) = unbounded_channel::<AppEvent>();

View File

@@ -232,7 +232,8 @@ pub(crate) fn list_available_themes(codex_home: Option<&Path>) -> Vec<ThemeEntry
if path.extension().and_then(|e| e.to_str()) == Some("tmTheme") {
if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
let name = stem.to_string();
if !entries.iter().any(|e| e.name == name) {
let is_valid_theme = ThemeSet::get_theme(&path).is_ok();
if is_valid_theme && !entries.iter().any(|e| e.name == name) {
entries.push(ThemeEntry {
name,
is_custom: true,
@@ -886,6 +887,30 @@ mod tests {
);
}
#[test]
fn list_available_themes_excludes_invalid_custom_files() {
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("valid-custom.tmTheme"));
std::fs::write(themes_dir.join("broken-custom.tmTheme"), "not a plist").unwrap();
let entries = list_available_themes(Some(dir.path()));
assert!(
entries
.iter()
.any(|entry| entry.name == "valid-custom" && entry.is_custom),
"expected valid custom theme to be listed"
);
assert!(
!entries
.iter()
.any(|entry| entry.name == "broken-custom" && entry.is_custom),
"expected invalid custom theme to be excluded from list"
);
}
#[test]
fn parse_theme_name_is_exhaustive() {
use two_face::theme::EmbeddedLazyThemeSet;