mirror of
https://github.com/openai/codex.git
synced 2026-09-20 12:47:38 +00:00
Add six bundled TUI themes and theme-aware accents (#46504)
## What changed - Bundle `ada`, `babbage`, `curie`, `cushman`, `dali`, and `davinci` themes for configuration and the theme picker, preserving precedence and invalid-file warnings for custom themes with the same names. - Use `codex.accent` for active and selected controls on truecolor and 256-color terminals, retaining existing fallbacks at lower color depths. - Honor terminal-default diff backgrounds and clear matching gutter fills, allowing `dali` and `davinci` to show diffs without background fills. - Restrict Windows native palette fallback to `ConsoleWindowClass` so a ConPTY palette is not mistaken for the renderer's colors when OSC probing fails. ## Testing Add tests and snapshots for theme preview, selection, cancellation, custom-theme precedence, invalid-file warnings, accent color depth, and disabling individual diff fills. GitOrigin-RevId: 72f3ef08f82626436dd2d80d40210cc77e1a5616
This commit is contained in:
53
codex-rs/tui/assets/themes/ada.tmTheme
Normal file
53
codex-rs/tui/assets/themes/ada.tmTheme
Normal file
@@ -0,0 +1,53 @@
|
||||
<?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>Ada</string>
|
||||
<key>settings</key>
|
||||
<array>
|
||||
<dict><key>settings</key><dict><key>foreground</key><string>#D0D0D0</string></dict></dict>
|
||||
<dict>
|
||||
<key>scope</key><string>codex.accent</string>
|
||||
<key>settings</key><dict><key>foreground</key><string>#5FAFFF</string></dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>scope</key><string>comment, punctuation.definition.comment</string>
|
||||
<key>settings</key><dict><key>foreground</key><string>#808080</string></dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>scope</key><string>string, punctuation.definition.string</string>
|
||||
<key>settings</key><dict><key>foreground</key><string>#87FF87</string></dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>scope</key><string>keyword, storage, markup.heading</string>
|
||||
<key>settings</key><dict><key>foreground</key><string>#5FAFFF</string></dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>scope</key><string>support.type, support.class, support.variable, entity.name.type</string>
|
||||
<key>settings</key><dict><key>foreground</key><string>#5FD7FF</string></dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>scope</key><string>constant, constant.numeric</string>
|
||||
<key>settings</key><dict><key>foreground</key><string>#AF87FF</string></dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>scope</key><string>punctuation, keyword.operator</string>
|
||||
<key>settings</key><dict><key>foreground</key><string>#BCBCBC</string></dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>scope</key><string>markup.inserted</string>
|
||||
<key>settings</key><dict>
|
||||
<key>foreground</key><string>#00AF00</string>
|
||||
<key>background</key><string>#213A2B</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>scope</key><string>markup.deleted</string>
|
||||
<key>settings</key><dict>
|
||||
<key>foreground</key><string>#FF5F5F</string>
|
||||
<key>background</key><string>#4A221D</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
53
codex-rs/tui/assets/themes/babbage.tmTheme
Normal file
53
codex-rs/tui/assets/themes/babbage.tmTheme
Normal file
@@ -0,0 +1,53 @@
|
||||
<?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>Babbage</string>
|
||||
<key>settings</key>
|
||||
<array>
|
||||
<dict><key>settings</key><dict><key>foreground</key><string>#444444</string></dict></dict>
|
||||
<dict>
|
||||
<key>scope</key><string>codex.accent</string>
|
||||
<key>settings</key><dict><key>foreground</key><string>#008700</string></dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>scope</key><string>comment, punctuation.definition.comment</string>
|
||||
<key>settings</key><dict><key>foreground</key><string>#626262</string></dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>scope</key><string>string, punctuation.definition.string</string>
|
||||
<key>settings</key><dict><key>foreground</key><string>#008700</string></dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>scope</key><string>keyword, storage, markup.heading</string>
|
||||
<key>settings</key><dict><key>foreground</key><string>#008700</string></dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>scope</key><string>support.type, support.class, support.variable, entity.name.type</string>
|
||||
<key>settings</key><dict><key>foreground</key><string>#0087AF</string></dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>scope</key><string>constant, constant.numeric</string>
|
||||
<key>settings</key><dict><key>foreground</key><string>#870087</string></dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>scope</key><string>punctuation, keyword.operator</string>
|
||||
<key>settings</key><dict><key>foreground</key><string>#585858</string></dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>scope</key><string>markup.inserted</string>
|
||||
<key>settings</key><dict>
|
||||
<key>foreground</key><string>#00AF00</string>
|
||||
<key>background</key><string>#DAFBE1</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>scope</key><string>markup.deleted</string>
|
||||
<key>settings</key><dict>
|
||||
<key>foreground</key><string>#FF5F5F</string>
|
||||
<key>background</key><string>#FFEBE9</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
53
codex-rs/tui/assets/themes/curie.tmTheme
Normal file
53
codex-rs/tui/assets/themes/curie.tmTheme
Normal file
@@ -0,0 +1,53 @@
|
||||
<?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>Curie</string>
|
||||
<key>settings</key>
|
||||
<array>
|
||||
<dict><key>settings</key><dict><key>foreground</key><string>#D0D0D0</string></dict></dict>
|
||||
<dict>
|
||||
<key>scope</key><string>codex.accent</string>
|
||||
<key>settings</key><dict><key>foreground</key><string>#D7AF00</string></dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>scope</key><string>comment, punctuation.definition.comment</string>
|
||||
<key>settings</key><dict><key>foreground</key><string>#808080</string></dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>scope</key><string>string, punctuation.definition.string</string>
|
||||
<key>settings</key><dict><key>foreground</key><string>#FFAF5F</string></dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>scope</key><string>keyword, storage, markup.heading</string>
|
||||
<key>settings</key><dict><key>foreground</key><string>#D7AF00</string></dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>scope</key><string>support.type, support.class, support.variable, entity.name.type</string>
|
||||
<key>settings</key><dict><key>foreground</key><string>#87AFD7</string></dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>scope</key><string>constant, constant.numeric</string>
|
||||
<key>settings</key><dict><key>foreground</key><string>#D7AF5F</string></dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>scope</key><string>punctuation, keyword.operator</string>
|
||||
<key>settings</key><dict><key>foreground</key><string>#BCBCBC</string></dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>scope</key><string>markup.inserted</string>
|
||||
<key>settings</key><dict>
|
||||
<key>foreground</key><string>#00AF00</string>
|
||||
<key>background</key><string>#213A2B</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>scope</key><string>markup.deleted</string>
|
||||
<key>settings</key><dict>
|
||||
<key>foreground</key><string>#FF5F5F</string>
|
||||
<key>background</key><string>#4A221D</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
53
codex-rs/tui/assets/themes/cushman.tmTheme
Normal file
53
codex-rs/tui/assets/themes/cushman.tmTheme
Normal file
@@ -0,0 +1,53 @@
|
||||
<?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>Cushman</string>
|
||||
<key>settings</key>
|
||||
<array>
|
||||
<dict><key>settings</key><dict><key>foreground</key><string>#444444</string></dict></dict>
|
||||
<dict>
|
||||
<key>scope</key><string>codex.accent</string>
|
||||
<key>settings</key><dict><key>foreground</key><string>#0087FF</string></dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>scope</key><string>comment, punctuation.definition.comment</string>
|
||||
<key>settings</key><dict><key>foreground</key><string>#626262</string></dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>scope</key><string>string, punctuation.definition.string</string>
|
||||
<key>settings</key><dict><key>foreground</key><string>#008700</string></dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>scope</key><string>keyword, storage, markup.heading</string>
|
||||
<key>settings</key><dict><key>foreground</key><string>#005FAF</string></dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>scope</key><string>support.type, support.class, support.variable, entity.name.type</string>
|
||||
<key>settings</key><dict><key>foreground</key><string>#0087AF</string></dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>scope</key><string>constant, constant.numeric</string>
|
||||
<key>settings</key><dict><key>foreground</key><string>#870087</string></dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>scope</key><string>punctuation, keyword.operator</string>
|
||||
<key>settings</key><dict><key>foreground</key><string>#585858</string></dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>scope</key><string>markup.inserted</string>
|
||||
<key>settings</key><dict>
|
||||
<key>foreground</key><string>#00AF00</string>
|
||||
<key>background</key><string>#DAFBE1</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>scope</key><string>markup.deleted</string>
|
||||
<key>settings</key><dict>
|
||||
<key>foreground</key><string>#FF5F5F</string>
|
||||
<key>background</key><string>#FFEBE9</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
53
codex-rs/tui/assets/themes/dali.tmTheme
Normal file
53
codex-rs/tui/assets/themes/dali.tmTheme
Normal file
@@ -0,0 +1,53 @@
|
||||
<?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>Dali</string>
|
||||
<key>settings</key>
|
||||
<array>
|
||||
<dict><key>settings</key><dict><key>foreground</key><string>#D0D0D0</string></dict></dict>
|
||||
<dict>
|
||||
<key>scope</key><string>codex.accent</string>
|
||||
<key>settings</key><dict><key>foreground</key><string>#D7AF00</string></dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>scope</key><string>comment, punctuation.definition.comment</string>
|
||||
<key>settings</key><dict><key>foreground</key><string>#808080</string></dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>scope</key><string>string, punctuation.definition.string</string>
|
||||
<key>settings</key><dict><key>foreground</key><string>#FF87FF</string></dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>scope</key><string>keyword, storage, markup.heading</string>
|
||||
<key>settings</key><dict><key>foreground</key><string>#D7AF00</string></dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>scope</key><string>support.type, support.class, support.variable, entity.name.type</string>
|
||||
<key>settings</key><dict><key>foreground</key><string>#5FD7FF</string></dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>scope</key><string>constant, constant.numeric</string>
|
||||
<key>settings</key><dict><key>foreground</key><string>#FF5FFF</string></dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>scope</key><string>punctuation, keyword.operator</string>
|
||||
<key>settings</key><dict><key>foreground</key><string>#BCBCBC</string></dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>scope</key><string>markup.inserted</string>
|
||||
<key>settings</key><dict>
|
||||
<key>foreground</key><string>#00AF00</string>
|
||||
<key>background</key><string>#00000001</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>scope</key><string>markup.deleted</string>
|
||||
<key>settings</key><dict>
|
||||
<key>foreground</key><string>#FF5F5F</string>
|
||||
<key>background</key><string>#00000001</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
53
codex-rs/tui/assets/themes/davinci.tmTheme
Normal file
53
codex-rs/tui/assets/themes/davinci.tmTheme
Normal file
@@ -0,0 +1,53 @@
|
||||
<?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>Davinci</string>
|
||||
<key>settings</key>
|
||||
<array>
|
||||
<dict><key>settings</key><dict><key>foreground</key><string>#444444</string></dict></dict>
|
||||
<dict>
|
||||
<key>scope</key><string>codex.accent</string>
|
||||
<key>settings</key><dict><key>foreground</key><string>#D75F00</string></dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>scope</key><string>comment, punctuation.definition.comment</string>
|
||||
<key>settings</key><dict><key>foreground</key><string>#626262</string></dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>scope</key><string>string, punctuation.definition.string</string>
|
||||
<key>settings</key><dict><key>foreground</key><string>#AF5F00</string></dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>scope</key><string>keyword, storage, markup.heading</string>
|
||||
<key>settings</key><dict><key>foreground</key><string>#AF5F00</string></dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>scope</key><string>support.type, support.class, support.variable, entity.name.type</string>
|
||||
<key>settings</key><dict><key>foreground</key><string>#005FAF</string></dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>scope</key><string>constant, constant.numeric</string>
|
||||
<key>settings</key><dict><key>foreground</key><string>#AF8700</string></dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>scope</key><string>punctuation, keyword.operator</string>
|
||||
<key>settings</key><dict><key>foreground</key><string>#585858</string></dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>scope</key><string>markup.inserted</string>
|
||||
<key>settings</key><dict>
|
||||
<key>foreground</key><string>#00AF00</string>
|
||||
<key>background</key><string>#00000001</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>scope</key><string>markup.deleted</string>
|
||||
<key>settings</key><dict>
|
||||
<key>foreground</key><string>#FF5F5F</string>
|
||||
<key>background</key><string>#00000001</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -82,8 +82,9 @@ use crate::color::perceptual_distance;
|
||||
use crate::diff_model::FileChange;
|
||||
use crate::exec_command::relativize_to_home;
|
||||
use crate::render::Insets;
|
||||
use crate::render::highlight::DiffScopeBackgroundRgbs;
|
||||
use crate::render::highlight::diff_scope_background_rgbs;
|
||||
use crate::render::highlight::DiffScopeBackground;
|
||||
use crate::render::highlight::DiffScopeBackgrounds;
|
||||
use crate::render::highlight::diff_scope_backgrounds;
|
||||
use crate::render::highlight::exceeds_highlight_limits;
|
||||
use crate::render::highlight::highlight_code_to_styled_spans;
|
||||
use crate::render::line_utils::prefix_lines;
|
||||
@@ -202,7 +203,7 @@ fn resolve_diff_backgrounds(
|
||||
theme: DiffTheme,
|
||||
color_level: DiffColorLevel,
|
||||
) -> ResolvedDiffBackgrounds {
|
||||
resolve_diff_backgrounds_for(theme, color_level, diff_scope_background_rgbs())
|
||||
resolve_diff_backgrounds_for(theme, color_level, diff_scope_backgrounds())
|
||||
}
|
||||
|
||||
/// Snapshot the current terminal environment into a reusable style context.
|
||||
@@ -230,22 +231,28 @@ pub(crate) fn current_diff_render_style_context() -> DiffRenderStyleContext {
|
||||
/// Starts from the hardcoded fallback palette and then overrides with theme
|
||||
/// scope backgrounds when both (a) the color level is rich enough and (b) the
|
||||
/// theme defines a matching scope. This means the fallback palette is always
|
||||
/// the baseline and theme scopes are strictly additive.
|
||||
/// the baseline. An explicit terminal-default marker disables that scope's fill.
|
||||
fn resolve_diff_backgrounds_for(
|
||||
theme: DiffTheme,
|
||||
color_level: DiffColorLevel,
|
||||
scope_backgrounds: DiffScopeBackgroundRgbs,
|
||||
scope_backgrounds: DiffScopeBackgrounds,
|
||||
) -> ResolvedDiffBackgrounds {
|
||||
let mut resolved = fallback_diff_backgrounds(theme, color_level);
|
||||
let Some(level) = RichDiffColorLevel::from_diff_color_level(color_level) else {
|
||||
return resolved;
|
||||
};
|
||||
|
||||
if let Some(rgb) = scope_backgrounds.inserted {
|
||||
resolved.add = Some(color_from_rgb_for_level(rgb, level));
|
||||
}
|
||||
if let Some(rgb) = scope_backgrounds.deleted {
|
||||
resolved.del = Some(color_from_rgb_for_level(rgb, level));
|
||||
for (target, background) in [
|
||||
(&mut resolved.add, scope_backgrounds.inserted),
|
||||
(&mut resolved.del, scope_backgrounds.deleted),
|
||||
] {
|
||||
match background {
|
||||
Some(DiffScopeBackground::Rgb(rgb)) => {
|
||||
*target = Some(color_from_rgb_for_level(rgb, level))
|
||||
}
|
||||
Some(DiffScopeBackground::TerminalDefault) => *target = None,
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
resolved
|
||||
}
|
||||
@@ -878,7 +885,10 @@ fn push_wrapped_diff_line_inner_with_theme_and_color_level(
|
||||
};
|
||||
|
||||
let line_bg = style_line_bg_for(kind, diff_backgrounds);
|
||||
let gutter_style = style_gutter_for(kind, theme, color_level);
|
||||
let mut gutter_style = style_gutter_for(kind, theme, color_level);
|
||||
if line_bg.bg.is_none() {
|
||||
gutter_style.bg = None;
|
||||
}
|
||||
|
||||
// When we have syntax spans, compose them with the diff style for a richer
|
||||
// view. The sign character keeps the diff color; content gets syntax colors
|
||||
@@ -1939,9 +1949,9 @@ mod tests {
|
||||
let backgrounds = resolve_diff_backgrounds_for(
|
||||
DiffTheme::Dark,
|
||||
DiffColorLevel::TrueColor,
|
||||
DiffScopeBackgroundRgbs {
|
||||
inserted: Some((1, 2, 3)),
|
||||
deleted: Some((4, 5, 6)),
|
||||
DiffScopeBackgrounds {
|
||||
inserted: Some(DiffScopeBackground::Rgb((1, 2, 3))),
|
||||
deleted: Some(DiffScopeBackground::Rgb((4, 5, 6))),
|
||||
},
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -1954,13 +1964,44 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_default_scope_disables_only_its_diff_fill() {
|
||||
for theme in [DiffTheme::Light, DiffTheme::Dark] {
|
||||
for color_level in [
|
||||
DiffColorLevel::TrueColor,
|
||||
DiffColorLevel::Ansi256,
|
||||
DiffColorLevel::Ansi16,
|
||||
] {
|
||||
let backgrounds = resolve_diff_backgrounds_for(
|
||||
theme,
|
||||
color_level,
|
||||
DiffScopeBackgrounds {
|
||||
inserted: Some(DiffScopeBackground::TerminalDefault),
|
||||
deleted: None,
|
||||
},
|
||||
);
|
||||
assert_eq!(
|
||||
style_line_bg_for(DiffLineType::Insert, backgrounds),
|
||||
Style::default()
|
||||
);
|
||||
assert_eq!(
|
||||
style_line_bg_for(DiffLineType::Delete, backgrounds),
|
||||
style_line_bg_for(
|
||||
DiffLineType::Delete,
|
||||
fallback_diff_backgrounds(theme, color_level)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn theme_scope_backgrounds_quantize_to_ansi256() {
|
||||
let backgrounds = resolve_diff_backgrounds_for(
|
||||
DiffTheme::Dark,
|
||||
DiffColorLevel::Ansi256,
|
||||
DiffScopeBackgroundRgbs {
|
||||
inserted: Some((0, 95, 0)),
|
||||
DiffScopeBackgrounds {
|
||||
inserted: Some(DiffScopeBackground::Rgb((0, 95, 0))),
|
||||
deleted: None,
|
||||
},
|
||||
);
|
||||
@@ -1979,8 +2020,8 @@ mod tests {
|
||||
let backgrounds = resolve_diff_backgrounds_for(
|
||||
DiffTheme::Dark,
|
||||
DiffColorLevel::TrueColor,
|
||||
DiffScopeBackgroundRgbs {
|
||||
inserted: Some((12, 34, 56)),
|
||||
DiffScopeBackgrounds {
|
||||
inserted: Some(DiffScopeBackground::Rgb((12, 34, 56))),
|
||||
deleted: None,
|
||||
},
|
||||
);
|
||||
@@ -2027,9 +2068,9 @@ mod tests {
|
||||
let themed_backgrounds = resolve_diff_backgrounds_for(
|
||||
DiffTheme::Light,
|
||||
DiffColorLevel::Ansi16,
|
||||
DiffScopeBackgroundRgbs {
|
||||
inserted: Some((8, 9, 10)),
|
||||
deleted: Some((11, 12, 13)),
|
||||
DiffScopeBackgrounds {
|
||||
inserted: Some(DiffScopeBackground::Rgb((8, 9, 10))),
|
||||
deleted: Some(DiffScopeBackground::Rgb((11, 12, 13))),
|
||||
},
|
||||
);
|
||||
assert_eq!(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Syntax highlighting engine for the TUI.
|
||||
//!
|
||||
//! Wraps [syntect] with the [two_face] grammar and theme bundles to provide
|
||||
//! ~250-language syntax highlighting and 32 bundled color themes. The module
|
||||
//! ~250-language syntax highlighting and bundled color themes. The module
|
||||
//! owns five process-global singletons:
|
||||
//!
|
||||
//! | Singleton | Type | Purpose |
|
||||
@@ -24,6 +24,7 @@
|
||||
//! prevent pathological CPU/memory usage. Callers must fall back to plain
|
||||
//! unstyled text.
|
||||
|
||||
use super::model_themes;
|
||||
use ratatui::style::Color as RtColor;
|
||||
use ratatui::style::Modifier;
|
||||
use ratatui::style::Style;
|
||||
@@ -132,18 +133,14 @@ pub(crate) fn validate_theme_name(name: Option<&str>, codex_home: Option<&Path>)
|
||||
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() {
|
||||
if resolve_theme_by_name(name, codex_home).is_some() {
|
||||
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;
|
||||
}
|
||||
if custom_path.try_exists().unwrap_or(true) {
|
||||
return Some(format!(
|
||||
"Custom theme \"{name}\" at {custom_theme_path_display} could not \
|
||||
be loaded (invalid .tmTheme format). Falling back to the default theme."
|
||||
@@ -232,14 +229,7 @@ fn resolve_theme_with_override(name: Option<&str>, codex_home: Option<&Path>) ->
|
||||
|
||||
// Honor user-configured theme if valid.
|
||||
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_HOME}/themes/{name}.tmTheme from disk.
|
||||
if let Some(home) = codex_home
|
||||
&& let Some(theme) = load_custom_theme(name, home)
|
||||
{
|
||||
if let Some(theme) = resolve_theme_by_name(name, codex_home) {
|
||||
return theme;
|
||||
}
|
||||
tracing::debug!("Theme \"{name}\" not recognized; using default theme");
|
||||
@@ -304,7 +294,14 @@ pub(crate) fn current_syntax_theme() -> Theme {
|
||||
}
|
||||
}
|
||||
|
||||
/// Raw RGB background colors extracted from syntax theme diff/markup scopes.
|
||||
/// An explicit diff background, including the ANSI terminal-default marker.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum DiffScopeBackground {
|
||||
Rgb((u8, u8, u8)),
|
||||
TerminalDefault,
|
||||
}
|
||||
|
||||
/// Background colors extracted from syntax theme diff/markup scopes.
|
||||
///
|
||||
/// These are theme-provided colors, not yet adapted for any particular color
|
||||
/// depth. [`diff_render`](crate::diff_render) converts them to ratatui
|
||||
@@ -315,9 +312,9 @@ pub(crate) fn current_syntax_theme() -> Theme {
|
||||
/// backgrounds, in which case the diff renderer falls back to its hardcoded
|
||||
/// palette.
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub(crate) struct DiffScopeBackgroundRgbs {
|
||||
pub inserted: Option<(u8, u8, u8)>,
|
||||
pub deleted: Option<(u8, u8, u8)>,
|
||||
pub(crate) struct DiffScopeBackgrounds {
|
||||
pub inserted: Option<DiffScopeBackground>,
|
||||
pub deleted: Option<DiffScopeBackground>,
|
||||
}
|
||||
|
||||
/// Query the active syntax theme for diff-scope background colors.
|
||||
@@ -325,27 +322,34 @@ pub(crate) struct DiffScopeBackgroundRgbs {
|
||||
/// Prefers `markup.inserted` / `markup.deleted` (the TextMate convention used
|
||||
/// by most VS Code themes) and falls back to `diff.inserted` / `diff.deleted`
|
||||
/// (used by some older `.tmTheme` files).
|
||||
pub(crate) fn diff_scope_background_rgbs() -> DiffScopeBackgroundRgbs {
|
||||
pub(crate) fn diff_scope_backgrounds() -> DiffScopeBackgrounds {
|
||||
let theme = current_syntax_theme();
|
||||
diff_scope_background_rgbs_for_theme(&theme)
|
||||
diff_scope_backgrounds_for_theme(&theme)
|
||||
}
|
||||
|
||||
/// Pure extraction helper, separated from the global theme singleton so tests
|
||||
/// can pass arbitrary themes.
|
||||
fn diff_scope_background_rgbs_for_theme(theme: &Theme) -> DiffScopeBackgroundRgbs {
|
||||
fn diff_scope_backgrounds_for_theme(theme: &Theme) -> DiffScopeBackgrounds {
|
||||
let highlighter = Highlighter::new(theme);
|
||||
let inserted = scope_background_rgb(&highlighter, "markup.inserted")
|
||||
.or_else(|| scope_background_rgb(&highlighter, "diff.inserted"));
|
||||
let deleted = scope_background_rgb(&highlighter, "markup.deleted")
|
||||
.or_else(|| scope_background_rgb(&highlighter, "diff.deleted"));
|
||||
DiffScopeBackgroundRgbs { inserted, deleted }
|
||||
let inserted = scope_background(&highlighter, "markup.inserted")
|
||||
.or_else(|| scope_background(&highlighter, "diff.inserted"));
|
||||
let deleted = scope_background(&highlighter, "markup.deleted")
|
||||
.or_else(|| scope_background(&highlighter, "diff.deleted"));
|
||||
DiffScopeBackgrounds { inserted, deleted }
|
||||
}
|
||||
|
||||
/// Extract the background color for a single TextMate scope, if defined.
|
||||
fn scope_background_rgb(highlighter: &Highlighter<'_>, scope_name: &str) -> Option<(u8, u8, u8)> {
|
||||
fn scope_background(
|
||||
highlighter: &Highlighter<'_>,
|
||||
scope_name: &str,
|
||||
) -> Option<DiffScopeBackground> {
|
||||
let scope = Scope::new(scope_name).ok()?;
|
||||
let bg = highlighter.style_mod_for_stack(&[scope]).background?;
|
||||
Some((bg.r, bg.g, bg.b))
|
||||
Some(if bg.a == ANSI_ALPHA_DEFAULT {
|
||||
DiffScopeBackground::TerminalDefault
|
||||
} else {
|
||||
DiffScopeBackground::Rgb((bg.r, bg.g, bg.b))
|
||||
})
|
||||
}
|
||||
|
||||
/// Query the active syntax theme for the first foreground style provided by the
|
||||
@@ -375,12 +379,8 @@ pub(crate) fn foreground_style_for_scopes_with_theme(
|
||||
pub(crate) fn configured_theme_name() -> String {
|
||||
// Explicit user override?
|
||||
if let Some(Some(name)) = THEME_OVERRIDE.get() {
|
||||
if parse_theme_name(name).is_some() {
|
||||
return name.clone();
|
||||
}
|
||||
if let Some(Some(home)) = CODEX_HOME.get()
|
||||
&& load_custom_theme(name, home).is_some()
|
||||
{
|
||||
let home = CODEX_HOME.get().and_then(|home| home.as_deref());
|
||||
if resolve_theme_by_name(name, home).is_some() {
|
||||
return name.clone();
|
||||
}
|
||||
}
|
||||
@@ -397,11 +397,11 @@ pub(crate) fn resolve_theme_by_name(name: &str, codex_home: Option<&Path>) -> Op
|
||||
}
|
||||
// Custom .tmTheme file?
|
||||
if let Some(home) = codex_home
|
||||
&& let Some(theme) = load_custom_theme(name, home)
|
||||
&& custom_theme_path(name, home).try_exists().unwrap_or(true)
|
||||
{
|
||||
return Some(theme);
|
||||
return load_custom_theme(name, home);
|
||||
}
|
||||
None
|
||||
model_themes::resolve(name)
|
||||
}
|
||||
|
||||
/// A theme available in the picker, either bundled or loaded from a custom
|
||||
@@ -447,6 +447,18 @@ pub(crate) fn list_available_themes(codex_home: Option<&Path>) -> Vec<ThemeEntry
|
||||
}
|
||||
}
|
||||
|
||||
// Existing custom files take precedence over the new bundled names.
|
||||
for (name, _) in model_themes::THEMES {
|
||||
if !entries.iter().any(|entry| entry.name == *name)
|
||||
&& resolve_theme_by_name(name, codex_home).is_some()
|
||||
{
|
||||
entries.push(ThemeEntry {
|
||||
name: (*name).to_string(),
|
||||
is_custom: codex_home.is_some_and(|home| custom_theme_path(name, home).exists()),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Keep picker ordering stable across platforms/filesystems while sorting
|
||||
// custom and bundled themes together, case-insensitively.
|
||||
entries.sort_by_cached_key(|entry| (entry.name.to_ascii_lowercase(), entry.name.clone()));
|
||||
@@ -1316,12 +1328,12 @@ mod tests {
|
||||
],
|
||||
..Theme::default()
|
||||
};
|
||||
let rgbs = diff_scope_background_rgbs_for_theme(&theme);
|
||||
let rgbs = diff_scope_backgrounds_for_theme(&theme);
|
||||
assert_eq!(
|
||||
rgbs,
|
||||
DiffScopeBackgroundRgbs {
|
||||
inserted: Some((10, 20, 30)),
|
||||
deleted: Some((40, 50, 60)),
|
||||
DiffScopeBackgrounds {
|
||||
inserted: Some(DiffScopeBackground::Rgb((10, 20, 30))),
|
||||
deleted: Some(DiffScopeBackground::Rgb((40, 50, 60))),
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -1333,10 +1345,10 @@ mod tests {
|
||||
scopes: vec![theme_item("constant.numeric", Some((1, 2, 3)))],
|
||||
..Theme::default()
|
||||
};
|
||||
let rgbs = diff_scope_background_rgbs_for_theme(&theme);
|
||||
let rgbs = diff_scope_backgrounds_for_theme(&theme);
|
||||
assert_eq!(
|
||||
rgbs,
|
||||
DiffScopeBackgroundRgbs {
|
||||
DiffScopeBackgrounds {
|
||||
inserted: None,
|
||||
deleted: None,
|
||||
}
|
||||
@@ -1375,7 +1387,7 @@ mod tests {
|
||||
fn bundled_theme_can_provide_diff_scope_backgrounds() {
|
||||
let theme = resolve_theme_by_name("github", /*codex_home*/ None)
|
||||
.expect("expected built-in GitHub theme to load");
|
||||
let rgbs = diff_scope_background_rgbs_for_theme(&theme);
|
||||
let rgbs = diff_scope_backgrounds_for_theme(&theme);
|
||||
assert!(
|
||||
rgbs.inserted.is_some() && rgbs.deleted.is_some(),
|
||||
"expected built-in theme to provide insert/delete backgrounds, got {rgbs:?}"
|
||||
@@ -1397,12 +1409,12 @@ mod tests {
|
||||
|
||||
let theme = resolve_theme_by_name("custom-diff", Some(dir.path()))
|
||||
.expect("expected custom theme to resolve");
|
||||
let rgbs = diff_scope_background_rgbs_for_theme(&theme);
|
||||
let rgbs = diff_scope_backgrounds_for_theme(&theme);
|
||||
assert_eq!(
|
||||
rgbs,
|
||||
DiffScopeBackgroundRgbs {
|
||||
inserted: Some((16, 32, 48)),
|
||||
deleted: Some((64, 80, 96)),
|
||||
DiffScopeBackgrounds {
|
||||
inserted: Some(DiffScopeBackground::Rgb((16, 32, 48))),
|
||||
deleted: Some(DiffScopeBackground::Rgb((64, 80, 96))),
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -1655,3 +1667,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "highlight_model_tests.rs"]
|
||||
mod model_tests;
|
||||
|
||||
52
codex-rs/tui/src/render/highlight_model_tests.rs
Normal file
52
codex-rs/tui/src/render/highlight_model_tests.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
//! Compatibility coverage for bundled theme files and existing user overrides.
|
||||
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn model_theme_files_preserve_custom_precedence_and_invalid_file_warnings() {
|
||||
let home = tempfile::tempdir().unwrap();
|
||||
std::fs::create_dir(home.path().join("themes")).unwrap();
|
||||
let path = custom_theme_path("ada", home.path());
|
||||
std::fs::write(&path, model_themes::THEMES[1].1).unwrap();
|
||||
assert_eq!(
|
||||
resolve_theme_by_name("ada", Some(home.path())),
|
||||
resolve_theme_by_name("babbage", /*codex_home*/ None)
|
||||
);
|
||||
let entries = list_available_themes(Some(home.path()));
|
||||
let entries = entries
|
||||
.iter()
|
||||
.filter(|entry| entry.name == "ada")
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(entries.len(), 1);
|
||||
assert!(entries[0].is_custom);
|
||||
|
||||
let uppercase_path = custom_theme_path("Ada", home.path());
|
||||
std::fs::rename(&path, &uppercase_path).unwrap();
|
||||
let entries = list_available_themes(Some(home.path()));
|
||||
let lowercase = entries.iter().find(|entry| entry.name == "ada").unwrap();
|
||||
assert_eq!(lowercase.is_custom, path.exists());
|
||||
assert!(
|
||||
entries
|
||||
.iter()
|
||||
.any(|entry| entry.name == "Ada" && entry.is_custom)
|
||||
);
|
||||
std::fs::rename(&uppercase_path, &path).unwrap();
|
||||
|
||||
std::fs::write(&path, "invalid theme").unwrap();
|
||||
assert!(
|
||||
validate_theme_name(Some("ada"), Some(home.path()))
|
||||
.unwrap()
|
||||
.contains("could not be loaded")
|
||||
);
|
||||
assert!(resolve_theme_by_name("ada", Some(home.path())).is_none());
|
||||
assert_eq!(
|
||||
resolve_theme_with_override(Some("ada"), Some(home.path())),
|
||||
resolve_theme_with_override(/*name*/ None, Some(home.path()))
|
||||
);
|
||||
assert!(
|
||||
!list_available_themes(Some(home.path()))
|
||||
.iter()
|
||||
.any(|entry| entry.name == "ada")
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ use ratatui::layout::Rect;
|
||||
|
||||
pub(crate) mod highlight;
|
||||
pub(crate) mod line_utils;
|
||||
mod model_themes;
|
||||
pub(crate) mod renderable;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
|
||||
28
codex-rs/tui/src/render/model_themes.rs
Normal file
28
codex-rs/tui/src/render/model_themes.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
//! Fixed model-named theme assets loaded through the standard TextMate parser.
|
||||
|
||||
use std::io::Cursor;
|
||||
use syntect::highlighting::Theme;
|
||||
use syntect::highlighting::ThemeSet;
|
||||
|
||||
pub(super) const THEMES: &[(&str, &str)] = &[
|
||||
("ada", include_str!("../../assets/themes/ada.tmTheme")),
|
||||
(
|
||||
"babbage",
|
||||
include_str!("../../assets/themes/babbage.tmTheme"),
|
||||
),
|
||||
("curie", include_str!("../../assets/themes/curie.tmTheme")),
|
||||
(
|
||||
"cushman",
|
||||
include_str!("../../assets/themes/cushman.tmTheme"),
|
||||
),
|
||||
("dali", include_str!("../../assets/themes/dali.tmTheme")),
|
||||
(
|
||||
"davinci",
|
||||
include_str!("../../assets/themes/davinci.tmTheme"),
|
||||
),
|
||||
];
|
||||
|
||||
pub(super) fn resolve(name: &str) -> Option<Theme> {
|
||||
let (_, source) = THEMES.iter().find(|(key, _)| *key == name)?;
|
||||
ThemeSet::load_from_reader(&mut Cursor::new(source.as_bytes())).ok()
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
source: tui/src/theme_picker_model_tests.rs
|
||||
assertion_line: 64
|
||||
expression: snapshot
|
||||
---
|
||||
ada: accent=Style::new().fg(Color::Rgb(95, 175, 255)).bold()
|
||||
12 fn greet(name: &str) -> String {
|
||||
13 - format!("Hello, {}!", name)
|
||||
13 + format!("Hello, {name}!")
|
||||
14 }
|
||||
preview styles: {"Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset)", "Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim()", "Style::new().fg(Color::Reset).bg(Color::Rgb(33, 58, 43)).underline_color(Color::Reset)", "Style::new().fg(Color::Reset).bg(Color::Rgb(33, 58, 43)).underline_color(Color::Reset).dim()", "Style::new().fg(Color::Reset).bg(Color::Rgb(74, 34, 29)).underline_color(Color::Reset)", "Style::new().fg(Color::Reset).bg(Color::Rgb(74, 34, 29)).underline_color(Color::Reset).dim()", "Style::new().fg(Color::Rgb(135, 255, 135)).bg(Color::Rgb(33, 58, 43)).underline_color(Color::Reset)", "Style::new().fg(Color::Rgb(135, 255, 135)).bg(Color::Rgb(74, 34, 29)).underline_color(Color::Reset).dim()", "Style::new().fg(Color::Rgb(175, 135, 255)).bg(Color::Rgb(33, 58, 43)).underline_color(Color::Reset)", "Style::new().fg(Color::Rgb(175, 135, 255)).bg(Color::Rgb(74, 34, 29)).underline_color(Color::Reset).dim()", "Style::new().fg(Color::Rgb(188, 188, 188)).bg(Color::Reset).underline_color(Color::Reset)", "Style::new().fg(Color::Rgb(188, 188, 188)).bg(Color::Rgb(33, 58, 43)).underline_color(Color::Reset)", "Style::new().fg(Color::Rgb(188, 188, 188)).bg(Color::Rgb(74, 34, 29)).underline_color(Color::Reset).dim()", "Style::new().fg(Color::Rgb(208, 208, 208)).bg(Color::Reset).underline_color(Color::Reset)", "Style::new().fg(Color::Rgb(208, 208, 208)).bg(Color::Rgb(33, 58, 43)).underline_color(Color::Reset)", "Style::new().fg(Color::Rgb(208, 208, 208)).bg(Color::Rgb(74, 34, 29)).underline_color(Color::Reset).dim()", "Style::new().fg(Color::Rgb(95, 175, 255)).bg(Color::Reset).underline_color(Color::Reset)", "Style::new().green().bg(Color::Rgb(33, 58, 43)).underline_color(Color::Reset)", "Style::new().red().bg(Color::Rgb(74, 34, 29)).underline_color(Color::Reset)"}
|
||||
syntax: [Line::from_iter([Span::from("let").fg(Color::Rgb(95, 175, 255)), Span::from(" answer ").fg(Color::Rgb(208, 208, 208)), Span::from("=").fg(Color::Rgb(188, 188, 188)), Span::from(" ").fg(Color::Rgb(208, 208, 208)), Span::from("42").fg(Color::Rgb(175, 135, 255)), Span::from(";").fg(Color::Rgb(188, 188, 188))])]
|
||||
babbage: accent=Style::new().fg(Color::Rgb(0, 135, 0)).bold()
|
||||
12 fn greet(name: &str) -> String {
|
||||
13 - format!("Hello, {}!", name)
|
||||
13 + format!("Hello, {name}!")
|
||||
14 }
|
||||
preview styles: {"Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset)", "Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim()", "Style::new().fg(Color::Reset).bg(Color::Rgb(218, 251, 225)).underline_color(Color::Reset)", "Style::new().fg(Color::Reset).bg(Color::Rgb(255, 235, 233)).underline_color(Color::Reset)", "Style::new().fg(Color::Rgb(0, 135, 0)).bg(Color::Reset).underline_color(Color::Reset)", "Style::new().fg(Color::Rgb(0, 135, 0)).bg(Color::Rgb(218, 251, 225)).underline_color(Color::Reset)", "Style::new().fg(Color::Rgb(0, 135, 0)).bg(Color::Rgb(255, 235, 233)).underline_color(Color::Reset).dim()", "Style::new().fg(Color::Rgb(135, 0, 135)).bg(Color::Rgb(218, 251, 225)).underline_color(Color::Reset)", "Style::new().fg(Color::Rgb(135, 0, 135)).bg(Color::Rgb(255, 235, 233)).underline_color(Color::Reset).dim()", "Style::new().fg(Color::Rgb(31, 35, 40)).bg(Color::Rgb(172, 238, 187)).underline_color(Color::Reset)", "Style::new().fg(Color::Rgb(31, 35, 40)).bg(Color::Rgb(255, 206, 203)).underline_color(Color::Reset)", "Style::new().fg(Color::Rgb(68, 68, 68)).bg(Color::Reset).underline_color(Color::Reset)", "Style::new().fg(Color::Rgb(68, 68, 68)).bg(Color::Rgb(218, 251, 225)).underline_color(Color::Reset)", "Style::new().fg(Color::Rgb(68, 68, 68)).bg(Color::Rgb(255, 235, 233)).underline_color(Color::Reset).dim()", "Style::new().fg(Color::Rgb(88, 88, 88)).bg(Color::Reset).underline_color(Color::Reset)", "Style::new().fg(Color::Rgb(88, 88, 88)).bg(Color::Rgb(218, 251, 225)).underline_color(Color::Reset)", "Style::new().fg(Color::Rgb(88, 88, 88)).bg(Color::Rgb(255, 235, 233)).underline_color(Color::Reset).dim()", "Style::new().green().bg(Color::Rgb(218, 251, 225)).underline_color(Color::Reset)", "Style::new().red().bg(Color::Rgb(255, 235, 233)).underline_color(Color::Reset)"}
|
||||
syntax: [Line::from_iter([Span::from("let").fg(Color::Rgb(0, 135, 0)), Span::from(" answer ").fg(Color::Rgb(68, 68, 68)), Span::from("=").fg(Color::Rgb(88, 88, 88)), Span::from(" ").fg(Color::Rgb(68, 68, 68)), Span::from("42").fg(Color::Rgb(135, 0, 135)), Span::from(";").fg(Color::Rgb(88, 88, 88))])]
|
||||
curie: accent=Style::new().fg(Color::Rgb(215, 175, 0)).bold()
|
||||
12 fn greet(name: &str) -> String {
|
||||
13 - format!("Hello, {}!", name)
|
||||
13 + format!("Hello, {name}!")
|
||||
14 }
|
||||
preview styles: {"Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset)", "Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim()", "Style::new().fg(Color::Reset).bg(Color::Rgb(33, 58, 43)).underline_color(Color::Reset)", "Style::new().fg(Color::Reset).bg(Color::Rgb(33, 58, 43)).underline_color(Color::Reset).dim()", "Style::new().fg(Color::Reset).bg(Color::Rgb(74, 34, 29)).underline_color(Color::Reset)", "Style::new().fg(Color::Reset).bg(Color::Rgb(74, 34, 29)).underline_color(Color::Reset).dim()", "Style::new().fg(Color::Rgb(188, 188, 188)).bg(Color::Reset).underline_color(Color::Reset)", "Style::new().fg(Color::Rgb(188, 188, 188)).bg(Color::Rgb(33, 58, 43)).underline_color(Color::Reset)", "Style::new().fg(Color::Rgb(188, 188, 188)).bg(Color::Rgb(74, 34, 29)).underline_color(Color::Reset).dim()", "Style::new().fg(Color::Rgb(208, 208, 208)).bg(Color::Reset).underline_color(Color::Reset)", "Style::new().fg(Color::Rgb(208, 208, 208)).bg(Color::Rgb(33, 58, 43)).underline_color(Color::Reset)", "Style::new().fg(Color::Rgb(208, 208, 208)).bg(Color::Rgb(74, 34, 29)).underline_color(Color::Reset).dim()", "Style::new().fg(Color::Rgb(215, 175, 0)).bg(Color::Reset).underline_color(Color::Reset)", "Style::new().fg(Color::Rgb(215, 175, 95)).bg(Color::Rgb(33, 58, 43)).underline_color(Color::Reset)", "Style::new().fg(Color::Rgb(215, 175, 95)).bg(Color::Rgb(74, 34, 29)).underline_color(Color::Reset).dim()", "Style::new().fg(Color::Rgb(255, 175, 95)).bg(Color::Rgb(33, 58, 43)).underline_color(Color::Reset)", "Style::new().fg(Color::Rgb(255, 175, 95)).bg(Color::Rgb(74, 34, 29)).underline_color(Color::Reset).dim()", "Style::new().green().bg(Color::Rgb(33, 58, 43)).underline_color(Color::Reset)", "Style::new().red().bg(Color::Rgb(74, 34, 29)).underline_color(Color::Reset)"}
|
||||
syntax: [Line::from_iter([Span::from("let").fg(Color::Rgb(215, 175, 0)), Span::from(" answer ").fg(Color::Rgb(208, 208, 208)), Span::from("=").fg(Color::Rgb(188, 188, 188)), Span::from(" ").fg(Color::Rgb(208, 208, 208)), Span::from("42").fg(Color::Rgb(215, 175, 95)), Span::from(";").fg(Color::Rgb(188, 188, 188))])]
|
||||
cushman: accent=Style::new().fg(Color::Rgb(0, 135, 255)).bold()
|
||||
12 fn greet(name: &str) -> String {
|
||||
13 - format!("Hello, {}!", name)
|
||||
13 + format!("Hello, {name}!")
|
||||
14 }
|
||||
preview styles: {"Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset)", "Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim()", "Style::new().fg(Color::Reset).bg(Color::Rgb(218, 251, 225)).underline_color(Color::Reset)", "Style::new().fg(Color::Reset).bg(Color::Rgb(255, 235, 233)).underline_color(Color::Reset)", "Style::new().fg(Color::Rgb(0, 135, 0)).bg(Color::Rgb(218, 251, 225)).underline_color(Color::Reset)", "Style::new().fg(Color::Rgb(0, 135, 0)).bg(Color::Rgb(255, 235, 233)).underline_color(Color::Reset).dim()", "Style::new().fg(Color::Rgb(0, 95, 175)).bg(Color::Reset).underline_color(Color::Reset)", "Style::new().fg(Color::Rgb(135, 0, 135)).bg(Color::Rgb(218, 251, 225)).underline_color(Color::Reset)", "Style::new().fg(Color::Rgb(135, 0, 135)).bg(Color::Rgb(255, 235, 233)).underline_color(Color::Reset).dim()", "Style::new().fg(Color::Rgb(31, 35, 40)).bg(Color::Rgb(172, 238, 187)).underline_color(Color::Reset)", "Style::new().fg(Color::Rgb(31, 35, 40)).bg(Color::Rgb(255, 206, 203)).underline_color(Color::Reset)", "Style::new().fg(Color::Rgb(68, 68, 68)).bg(Color::Reset).underline_color(Color::Reset)", "Style::new().fg(Color::Rgb(68, 68, 68)).bg(Color::Rgb(218, 251, 225)).underline_color(Color::Reset)", "Style::new().fg(Color::Rgb(68, 68, 68)).bg(Color::Rgb(255, 235, 233)).underline_color(Color::Reset).dim()", "Style::new().fg(Color::Rgb(88, 88, 88)).bg(Color::Reset).underline_color(Color::Reset)", "Style::new().fg(Color::Rgb(88, 88, 88)).bg(Color::Rgb(218, 251, 225)).underline_color(Color::Reset)", "Style::new().fg(Color::Rgb(88, 88, 88)).bg(Color::Rgb(255, 235, 233)).underline_color(Color::Reset).dim()", "Style::new().green().bg(Color::Rgb(218, 251, 225)).underline_color(Color::Reset)", "Style::new().red().bg(Color::Rgb(255, 235, 233)).underline_color(Color::Reset)"}
|
||||
syntax: [Line::from_iter([Span::from("let").fg(Color::Rgb(0, 95, 175)), Span::from(" answer ").fg(Color::Rgb(68, 68, 68)), Span::from("=").fg(Color::Rgb(88, 88, 88)), Span::from(" ").fg(Color::Rgb(68, 68, 68)), Span::from("42").fg(Color::Rgb(135, 0, 135)), Span::from(";").fg(Color::Rgb(88, 88, 88))])]
|
||||
dali: accent=Style::new().fg(Color::Rgb(215, 175, 0)).bold()
|
||||
12 fn greet(name: &str) -> String {
|
||||
13 - format!("Hello, {}!", name)
|
||||
13 + format!("Hello, {name}!")
|
||||
14 }
|
||||
preview styles: {"Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset)", "Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim()", "Style::new().fg(Color::Rgb(188, 188, 188)).bg(Color::Reset).underline_color(Color::Reset)", "Style::new().fg(Color::Rgb(188, 188, 188)).bg(Color::Reset).underline_color(Color::Reset).dim()", "Style::new().fg(Color::Rgb(208, 208, 208)).bg(Color::Reset).underline_color(Color::Reset)", "Style::new().fg(Color::Rgb(208, 208, 208)).bg(Color::Reset).underline_color(Color::Reset).dim()", "Style::new().fg(Color::Rgb(215, 175, 0)).bg(Color::Reset).underline_color(Color::Reset)", "Style::new().fg(Color::Rgb(255, 135, 255)).bg(Color::Reset).underline_color(Color::Reset)", "Style::new().fg(Color::Rgb(255, 135, 255)).bg(Color::Reset).underline_color(Color::Reset).dim()", "Style::new().fg(Color::Rgb(255, 95, 255)).bg(Color::Reset).underline_color(Color::Reset)", "Style::new().fg(Color::Rgb(255, 95, 255)).bg(Color::Reset).underline_color(Color::Reset).dim()", "Style::new().green().bg(Color::Reset).underline_color(Color::Reset)", "Style::new().red().bg(Color::Reset).underline_color(Color::Reset)"}
|
||||
syntax: [Line::from_iter([Span::from("let").fg(Color::Rgb(215, 175, 0)), Span::from(" answer ").fg(Color::Rgb(208, 208, 208)), Span::from("=").fg(Color::Rgb(188, 188, 188)), Span::from(" ").fg(Color::Rgb(208, 208, 208)), Span::from("42").fg(Color::Rgb(255, 95, 255)), Span::from(";").fg(Color::Rgb(188, 188, 188))])]
|
||||
davinci: accent=Style::new().fg(Color::Rgb(215, 95, 0)).bold()
|
||||
12 fn greet(name: &str) -> String {
|
||||
13 - format!("Hello, {}!", name)
|
||||
13 + format!("Hello, {name}!")
|
||||
14 }
|
||||
preview styles: {"Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset)", "Style::new().fg(Color::Reset).bg(Color::Reset).underline_color(Color::Reset).dim()", "Style::new().fg(Color::Rgb(175, 135, 0)).bg(Color::Reset).underline_color(Color::Reset)", "Style::new().fg(Color::Rgb(175, 135, 0)).bg(Color::Reset).underline_color(Color::Reset).dim()", "Style::new().fg(Color::Rgb(175, 95, 0)).bg(Color::Reset).underline_color(Color::Reset)", "Style::new().fg(Color::Rgb(175, 95, 0)).bg(Color::Reset).underline_color(Color::Reset).dim()", "Style::new().fg(Color::Rgb(31, 35, 40)).bg(Color::Reset).underline_color(Color::Reset)", "Style::new().fg(Color::Rgb(68, 68, 68)).bg(Color::Reset).underline_color(Color::Reset)", "Style::new().fg(Color::Rgb(68, 68, 68)).bg(Color::Reset).underline_color(Color::Reset).dim()", "Style::new().fg(Color::Rgb(88, 88, 88)).bg(Color::Reset).underline_color(Color::Reset)", "Style::new().fg(Color::Rgb(88, 88, 88)).bg(Color::Reset).underline_color(Color::Reset).dim()", "Style::new().green().bg(Color::Reset).underline_color(Color::Reset)", "Style::new().red().bg(Color::Reset).underline_color(Color::Reset)"}
|
||||
syntax: [Line::from_iter([Span::from("let").fg(Color::Rgb(175, 95, 0)), Span::from(" answer ").fg(Color::Rgb(68, 68, 68)), Span::from("=").fg(Color::Rgb(88, 88, 88)), Span::from(" ").fg(Color::Rgb(68, 68, 68)), Span::from("42").fg(Color::Rgb(175, 135, 0)), Span::from(";").fg(Color::Rgb(88, 88, 88))])]
|
||||
@@ -58,6 +58,17 @@ pub(crate) fn table_separator_style() -> Style {
|
||||
|
||||
/// Returns the shared accent style for active or selected TUI controls.
|
||||
pub(crate) fn accent_style() -> Style {
|
||||
if matches!(
|
||||
effective_stdout_color_level(),
|
||||
StdoutColorLevel::TrueColor | StdoutColorLevel::Ansi256
|
||||
) && let Some(mut style) =
|
||||
crate::render::highlight::foreground_style_for_scopes(&["codex.accent"])
|
||||
{
|
||||
if let Some(Color::Rgb(r, g, b)) = style.fg {
|
||||
style = style.fg(best_color((r, g, b)));
|
||||
}
|
||||
return style.bold();
|
||||
}
|
||||
accent_style_for(default_bg())
|
||||
}
|
||||
|
||||
@@ -190,6 +201,43 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn theme_accents_respect_terminal_color_depth() {
|
||||
const CHILD: &str = "CODEX_ACCENT_COLOR_TEST_CHILD";
|
||||
let Ok(level) = std::env::var(CHILD) else {
|
||||
for level in ["0", "1", "2", "3"] {
|
||||
let output = std::process::Command::new(std::env::current_exe().unwrap())
|
||||
.args([
|
||||
"--exact",
|
||||
"style::tests::theme_accents_respect_terminal_color_depth",
|
||||
])
|
||||
.env(CHILD, level)
|
||||
.env("FORCE_COLOR", level)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"level {level}: {}\n{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
return;
|
||||
};
|
||||
let theme =
|
||||
crate::render::highlight::resolve_theme_by_name("ada", /*codex_home*/ None).unwrap();
|
||||
crate::render::highlight::set_syntax_theme(theme);
|
||||
let expected = match level.as_str() {
|
||||
"3" => Style::default().fg(rgb_color((95, 175, 255))).bold(),
|
||||
"2" => Style::default()
|
||||
.fg(crate::terminal_palette::indexed_color(/*index*/ 75))
|
||||
.bold(),
|
||||
"0" | "1" => accent_style_for(default_bg()),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
assert_eq!(accent_style(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accent_style_uses_darker_cyan_on_light_backgrounds() {
|
||||
let style = accent_style_for(Some((255, 255, 255)));
|
||||
|
||||
@@ -14,6 +14,7 @@ use windows_sys::Win32::Foundation::WAIT_TIMEOUT;
|
||||
use windows_sys::Win32::Storage::FileSystem::WriteFile;
|
||||
use windows_sys::Win32::System::Console::CONSOLE_SCREEN_BUFFER_INFOEX;
|
||||
use windows_sys::Win32::System::Console::GetConsoleScreenBufferInfoEx;
|
||||
use windows_sys::Win32::System::Console::GetConsoleWindow;
|
||||
use windows_sys::Win32::System::Console::GetNumberOfConsoleInputEvents;
|
||||
use windows_sys::Win32::System::Console::GetStdHandle;
|
||||
use windows_sys::Win32::System::Console::INPUT_RECORD;
|
||||
@@ -23,11 +24,12 @@ use windows_sys::Win32::System::Console::STD_INPUT_HANDLE;
|
||||
use windows_sys::Win32::System::Console::STD_OUTPUT_HANDLE;
|
||||
use windows_sys::Win32::System::Console::WriteConsoleInputW;
|
||||
use windows_sys::Win32::System::Threading::WaitForSingleObject;
|
||||
use windows_sys::Win32::UI::WindowsAndMessaging::GetClassNameW;
|
||||
|
||||
const MAX_WINDOWS_PROBE_RECORDS: usize = 64 * 1_024;
|
||||
const WINDOWS_PROBE_READ_RECORDS: usize = 64;
|
||||
|
||||
/// Prefer the terminal renderer's OSC colors while retaining the native console palette fallback.
|
||||
/// Prefer OSC colors; use native colors only when the renderer shares the console palette.
|
||||
///
|
||||
/// Windows Terminal and xterm.js can render a theme unrelated to ConPTY's console color table.
|
||||
/// Their OSC responses share the input queue with user input, so the probe preserves and restores
|
||||
@@ -48,6 +50,24 @@ pub(crate) fn default_colors(timeout: Duration) -> io::Result<Option<DefaultColo
|
||||
return Ok(Some(colors));
|
||||
}
|
||||
|
||||
// ConPTY's PseudoConsoleWindow can report itself as visible, but its palette does not
|
||||
// describe the renderer. Only the native ConsoleWindowClass owns the visible colors.
|
||||
// SAFETY: Both APIs inspect the process's console window without modifying it.
|
||||
let mut window_class = [0_u16; 32];
|
||||
let class_len = unsafe {
|
||||
GetClassNameW(
|
||||
GetConsoleWindow(),
|
||||
window_class.as_mut_ptr(),
|
||||
window_class.len() as i32,
|
||||
)
|
||||
};
|
||||
if class_len == 0
|
||||
|| String::from_utf16_lossy(&window_class[..class_len as usize]) != "ConsoleWindowClass"
|
||||
{
|
||||
tracing::debug!("terminal colors unavailable; retaining terminal defaults");
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let colors = query_console_default_colors(output).ok().flatten();
|
||||
if let Some(colors) = colors {
|
||||
tracing::debug!(
|
||||
|
||||
@@ -655,3 +655,7 @@ mod tests {
|
||||
assert_eq!(selected_name, configured_or_default_theme);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "theme_picker_model_tests.rs"]
|
||||
mod model_tests;
|
||||
|
||||
91
codex-rs/tui/src/theme_picker_model_tests.rs
Normal file
91
codex-rs/tui/src/theme_picker_model_tests.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
//! Exercises fixed model theme files through selection, preview, and cancellation.
|
||||
|
||||
use super::*;
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
use crate::terminal_palette::with_test_default_colors;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn model_theme_files_preview_select_and_restore() {
|
||||
if std::env::var_os("CODEX_MODEL_THEME_TEST_CHILD").is_none() {
|
||||
let output = std::process::Command::new(std::env::current_exe().unwrap())
|
||||
.args([
|
||||
"--exact",
|
||||
"theme_picker::model_tests::model_theme_files_preview_select_and_restore",
|
||||
"--nocapture",
|
||||
])
|
||||
.env("CODEX_MODEL_THEME_TEST_CHILD", "1")
|
||||
.env("FORCE_COLOR", "3")
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{}\n{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
return;
|
||||
}
|
||||
let original = highlight::current_syntax_theme();
|
||||
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
let tx = AppEventSender::new(tx);
|
||||
let mut snapshot = String::new();
|
||||
for name in ["ada", "babbage", "curie", "cushman", "dali", "davinci"] {
|
||||
let light = matches!(name, "babbage" | "cushman" | "davinci");
|
||||
let colors = crate::terminal_probe::DefaultColors {
|
||||
fg: if light { (32, 32, 32) } else { (224, 224, 224) },
|
||||
bg: if light { (255, 255, 255) } else { (24, 24, 24) },
|
||||
};
|
||||
with_test_default_colors(colors, || {
|
||||
let params = build_theme_picker_params(Some(name), /*codex_home*/ None, Some(120));
|
||||
let idx = params.initial_selected_idx.unwrap();
|
||||
assert_eq!(params.items[idx].search_value.as_deref(), Some(name));
|
||||
params.on_selection_changed.as_ref().unwrap()(idx, &tx);
|
||||
assert!(matches!(
|
||||
rx.try_recv().unwrap(),
|
||||
AppEvent::SyntaxThemePreviewed
|
||||
));
|
||||
let code = "let answer = 42;\n";
|
||||
let lines = highlight::highlight_code_to_lines(code, "rust");
|
||||
assert!(lines.iter().all(|line| line.style.bg.is_none()
|
||||
&& line.spans.iter().all(|span| span.style.bg.is_none())));
|
||||
let area = Rect::new(0, 0, 54, 4);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
ThemePreviewNarrowRenderable.render(area, &mut buffer);
|
||||
if matches!(name, "dali" | "davinci") {
|
||||
assert!(
|
||||
buffer
|
||||
.content
|
||||
.iter()
|
||||
.all(|cell| cell.bg == ratatui::style::Color::Reset)
|
||||
);
|
||||
}
|
||||
snapshot.push_str(&format!(
|
||||
"{name}: accent={:?}\n",
|
||||
crate::style::accent_style()
|
||||
));
|
||||
for y in 0..area.height {
|
||||
let row: String = (0..area.width).map(|x| buffer[(x, y)].symbol()).collect();
|
||||
snapshot.push_str(row.trim_end());
|
||||
snapshot.push('\n');
|
||||
}
|
||||
let styles = buffer
|
||||
.content
|
||||
.iter()
|
||||
.map(|cell| format!("{:?}", cell.style()))
|
||||
.collect::<std::collections::BTreeSet<_>>();
|
||||
snapshot.push_str(&format!("preview styles: {styles:?}\nsyntax: {lines:?}\n"));
|
||||
params.items[idx].actions[0](&tx);
|
||||
assert!(
|
||||
matches!(rx.try_recv().unwrap(), AppEvent::SyntaxThemeSelected { name: selected } if selected == name)
|
||||
);
|
||||
params.on_cancel.as_ref().unwrap()(&tx);
|
||||
assert!(matches!(
|
||||
rx.try_recv().unwrap(),
|
||||
AppEvent::SyntaxThemePreviewed
|
||||
));
|
||||
assert_eq!(highlight::current_syntax_theme(), original);
|
||||
});
|
||||
}
|
||||
insta::assert_snapshot!(snapshot);
|
||||
}
|
||||
Reference in New Issue
Block a user