fix(tui/theme): cache terminal background detection result

This commit is contained in:
James Peter
2025-09-25 21:12:40 +10:00
parent 69f446ac1b
commit eeefdf5565
5 changed files with 183 additions and 12 deletions
+48
View File
@@ -5,6 +5,7 @@ use crate::config_types::AgentConfig;
use crate::config_types::AllowedCommand;
use crate::config_types::AllowedCommandMatchKind;
use crate::config_types::BrowserConfig;
use crate::config_types::CachedTerminalBackground;
use crate::config_types::ClientTools;
use crate::config_types::History;
use crate::config_types::GithubConfig;
@@ -647,6 +648,53 @@ pub fn set_tui_theme_name(codex_home: &Path, theme: ThemeName) -> anyhow::Result
Ok(())
}
/// Record the most recent terminal background autodetect result under `[tui.cached_terminal_background]`.
pub fn set_cached_terminal_background(
codex_home: &Path,
cache: &CachedTerminalBackground,
) -> anyhow::Result<()> {
let config_path = codex_home.join(CONFIG_TOML_FILE);
let read_path = resolve_codex_path_for_read(codex_home, Path::new(CONFIG_TOML_FILE));
let mut doc = match std::fs::read_to_string(&read_path) {
Ok(s) => s.parse::<DocumentMut>()?,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => DocumentMut::new(),
Err(e) => return Err(e.into()),
};
let mut tbl = toml_edit::Table::new();
tbl.set_implicit(false);
tbl.insert("is_dark", toml_edit::value(cache.is_dark));
if let Some(term) = &cache.term {
tbl.insert("term", toml_edit::value(term.as_str()));
}
if let Some(term_program) = &cache.term_program {
tbl.insert("term_program", toml_edit::value(term_program.as_str()));
}
if let Some(term_program_version) = &cache.term_program_version {
tbl.insert(
"term_program_version",
toml_edit::value(term_program_version.as_str()),
);
}
if let Some(colorfgbg) = &cache.colorfgbg {
tbl.insert("colorfgbg", toml_edit::value(colorfgbg.as_str()));
}
if let Some(source) = &cache.source {
tbl.insert("source", toml_edit::value(source.as_str()));
}
if let Some(rgb) = &cache.rgb {
tbl.insert("rgb", toml_edit::value(rgb.as_str()));
}
doc["tui"]["cached_terminal_background"] = toml_edit::Item::Table(tbl);
std::fs::create_dir_all(codex_home)?;
let tmp_file = NamedTempFile::new_in(codex_home)?;
std::fs::write(tmp_file.path(), doc.to_string())?;
tmp_file.persist(config_path)?;
Ok(())
}
/// Persist the selected spinner into `CODEX_HOME/config.toml` at `[tui.spinner].name`.
pub fn set_tui_spinner_name(codex_home: &Path, spinner_name: &str) -> anyhow::Result<()> {
let config_path = codex_home.join(CONFIG_TOML_FILE);
+22
View File
@@ -457,12 +457,33 @@ impl Default for Notifications {
}
/// Collection of settings that are specific to the TUI.
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
pub struct CachedTerminalBackground {
pub is_dark: bool,
#[serde(default)]
pub term: Option<String>,
#[serde(default)]
pub term_program: Option<String>,
#[serde(default)]
pub term_program_version: Option<String>,
#[serde(default)]
pub colorfgbg: Option<String>,
#[serde(default)]
pub source: Option<String>,
#[serde(default)]
pub rgb: Option<String>,
}
#[derive(Deserialize, Debug, Clone, PartialEq)]
pub struct Tui {
/// Theme configuration for the TUI
#[serde(default)]
pub theme: ThemeConfig,
/// Cached autodetect result so we can skip probing the terminal repeatedly.
#[serde(default)]
pub cached_terminal_background: Option<CachedTerminalBackground>,
/// Syntax highlighting configuration (Markdown fenced code blocks)
#[serde(default)]
pub highlight: HighlightConfig,
@@ -501,6 +522,7 @@ impl Default for Tui {
fn default() -> Self {
Self {
theme: ThemeConfig::default(),
cached_terminal_background: None,
highlight: HighlightConfig::default(),
show_reasoning: false,
stream: StreamConfig::default(),
+7
View File
@@ -1,5 +1,12 @@
# History Cell Refactor Plan
## Background
We're seeing a lot of issues with the conversation history in the TUI. It's slow to scroll (not easily cached), has inconsistent ordering, does not reconstruct correctly when using resume or undo. We also want to be able to import conversations from sub-agents, so need a portable format. We're also seeing some really large files (such as chatwidget.rs) which we want to reduce to improve maintainability.
To do that we're working on refactoring the history cells so that state is separated from rendering. Each cell should be able to be rendered from a state object along with the system settings. This state should be React-style so determine how the cell works, but the actual rendering logic should be entirely in the cell. The state should be easily serializable in JSON and will be built entirely from events in the new HistoryState. So, for example, the state should not have presentation logic like color etc.. that would be handled by the cell during rendering.
## Primary Goals (Plan & Status)
- [x] **Extract per-cell modules** – core message/tool/plan/upgrade/reasoning/image/loading/animated files live under `history_cell/`; exec/diff/streaming still inline and pending extraction.
- [ ] **Finish semantic state refactor** – continue replacing ad-hoc `SemanticLine` usage with rich typed structs (`MessageSegment`, `ToolArgument`, etc.) so renderers never infer structure from strings.
+82 -9
View File
@@ -5,6 +5,7 @@
#![deny(clippy::disallowed_methods)]
use app::App;
use codex_core::BUILT_IN_OSS_MODEL_PROVIDER_ID;
use codex_core::config::set_cached_terminal_background;
use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
use codex_core::config::ConfigToml;
@@ -12,7 +13,9 @@ use codex_core::config::find_codex_home;
use codex_core::config::load_config_as_toml_with_cli_overrides;
use codex_core::protocol::AskForApproval;
use codex_core::protocol::SandboxPolicy;
use codex_core::config_types::CachedTerminalBackground;
use codex_core::config_types::ThemeColors;
use codex_core::config_types::ThemeConfig;
use codex_core::config_types::ThemeName;
use regex_lite::Regex;
use codex_login::AuthMode;
@@ -461,17 +464,54 @@ fn maybe_apply_terminal_theme_detection(config: &mut Config, theme_configured_ex
return;
}
match crate::terminal_info::detect_dark_terminal_background() {
Some(true) => {
theme.name = ThemeName::DarkCarbonNight;
tracing::info!(
"Detected dark terminal background; switching default theme to Dark - Carbon Night"
let term = std::env::var("TERM").ok().filter(|value| !value.is_empty());
let term_program = std::env::var("TERM_PROGRAM").ok().filter(|value| !value.is_empty());
let term_program_version =
std::env::var("TERM_PROGRAM_VERSION").ok().filter(|value| !value.is_empty());
let colorfgbg = std::env::var("COLORFGBG").ok().filter(|value| !value.is_empty());
if let Some(cached) = config.tui.cached_terminal_background.as_ref() {
if cached_background_matches_env(
cached,
&term,
&term_program,
&term_program_version,
&colorfgbg,
) {
tracing::debug!(
source = cached.source.as_deref().unwrap_or("cached"),
"Using cached terminal background detection result",
);
apply_detected_theme(theme, cached.is_dark);
return;
}
Some(false) => {
tracing::info!(
"Detected light terminal background; keeping default Light - Photon theme"
);
}
match crate::terminal_info::detect_dark_terminal_background() {
Some(detection) => {
apply_detected_theme(theme, detection.is_dark);
let source = match detection.source {
crate::terminal_info::TerminalBackgroundSource::Osc11 => "osc-11",
crate::terminal_info::TerminalBackgroundSource::ColorFgBg => "colorfgbg",
};
let cache = CachedTerminalBackground {
is_dark: detection.is_dark,
term,
term_program,
term_program_version,
colorfgbg,
source: Some(source.to_string()),
rgb: detection
.rgb
.map(|(r, g, b)| format!("{:02x}{:02x}{:02x}", r, g, b)),
};
config.tui.cached_terminal_background = Some(cache.clone());
if let Err(err) = set_cached_terminal_background(&config.codex_home, &cache) {
tracing::warn!("Failed to persist terminal background autodetect result: {err}");
}
}
None => {
tracing::debug!(
@@ -481,6 +521,39 @@ fn maybe_apply_terminal_theme_detection(config: &mut Config, theme_configured_ex
}
}
fn apply_detected_theme(theme: &mut ThemeConfig, is_dark: bool) {
if is_dark {
theme.name = ThemeName::DarkCarbonNight;
tracing::info!(
"Detected dark terminal background; switching default theme to Dark - Carbon Night"
);
} else {
tracing::info!(
"Detected light terminal background; keeping default Light - Photon theme"
);
}
}
fn cached_background_matches_env(
cached: &CachedTerminalBackground,
term: &Option<String>,
term_program: &Option<String>,
term_program_version: &Option<String>,
colorfgbg: &Option<String>,
) -> bool {
fn matches(expected: &Option<String>, actual: &Option<String>) -> bool {
match expected {
Some(expected) => actual.as_ref().map(|value| value == expected).unwrap_or(false),
None => true,
}
}
matches(&cached.term, term)
&& matches(&cached.term_program, term_program)
&& matches(&cached.term_program_version, term_program_version)
&& matches(&cached.colorfgbg, colorfgbg)
}
/// Minimal login status indicator for onboarding flow.
#[derive(Debug, Clone, Copy)]
pub enum LoginStatus {
+24 -3
View File
@@ -24,6 +24,19 @@ const ANSI_16_TO_RGB: [(u8, u8, u8); 16] = [
(255, 255, 255),
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TerminalBackgroundSource {
Osc11,
ColorFgBg,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TerminalBackgroundDetection {
pub is_dark: bool,
pub source: TerminalBackgroundSource,
pub rgb: Option<(u8, u8, u8)>,
}
fn set_nonblocking(tty: &std::fs::File) {
#[cfg(unix)]
{
@@ -314,7 +327,7 @@ fn detect_dark_from_rgb(rgb: (u8, u8, u8)) -> bool {
relative_luminance(rgb) < 0.45
}
pub fn detect_dark_terminal_background() -> Option<bool> {
pub fn detect_dark_terminal_background() -> Option<TerminalBackgroundDetection> {
if let Ok(value) = env::var("CODE_DISABLE_THEME_AUTODETECT") {
if matches!(value.as_str(), "1" | "true" | "TRUE" | "True") {
return None;
@@ -323,12 +336,20 @@ pub fn detect_dark_terminal_background() -> Option<bool> {
if osc_background_query_supported() {
if let Some(rgb) = query_osc_background_color() {
return Some(detect_dark_from_rgb(rgb));
return Some(TerminalBackgroundDetection {
is_dark: detect_dark_from_rgb(rgb),
source: TerminalBackgroundSource::Osc11,
rgb: Some(rgb),
});
}
}
if let Some(rgb) = parse_colorfgbg_env() {
return Some(detect_dark_from_rgb(rgb));
return Some(TerminalBackgroundDetection {
is_dark: detect_dark_from_rgb(rgb),
source: TerminalBackgroundSource::ColorFgBg,
rgb: Some(rgb),
});
}
None