fix: preserve application extended colors across terminal themes
This commit is contained in:
1 parent
75dc0b9481
commit
96f481339c
6 files changed
+117
-58
No files matched your search
@@ -142,11 +142,7 @@ pub(crate) fn is_fixed_color(color: Color, overrides: &Colors) -> bool {
|
||||
match color {
|
||||
Color::Spec(_) => true,
|
||||
Color::Named(name) => overrides[name].is_some(),
|
||||
Color::Indexed(index) => {
|
||||
overrides[index as usize].is_some()
|
||||
// 16..=23 是 Nebula 随主题重建的 powerline 动态槽位。
|
||||
|| index >= 24
|
||||
},
|
||||
Color::Indexed(index) => overrides[index as usize].is_some() || index >= 16,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,7 +324,10 @@ mod tests {
|
||||
let mut overrides = Colors::default();
|
||||
assert!(is_fixed_color(Color::Spec(VteRgb { r: 1, g: 2, b: 3 }), &overrides));
|
||||
assert!(is_fixed_color(Color::Indexed(24), &overrides));
|
||||
assert!(!is_fixed_color(Color::Indexed(16), &overrides));
|
||||
for index in 16..=255 {
|
||||
assert!(is_fixed_color(Color::Indexed(index), &overrides));
|
||||
}
|
||||
assert!(!is_fixed_color(Color::Indexed(15), &overrides));
|
||||
assert!(!is_fixed_color(Color::Named(NamedColor::Red), &overrides));
|
||||
|
||||
overrides[NamedColor::Red] = Some(VteRgb { r: 1, g: 2, b: 3 });
|
||||
|
||||
@@ -22,12 +22,6 @@ use crate::display::color::{List, Rgb};
|
||||
use crate::renderer::ui::Rgba;
|
||||
use nebula_terminal::vte::ansi::NamedColor;
|
||||
|
||||
/// First 256-color palette slot claimed for the powerline prompt chips
|
||||
/// (16..=23: icon bg/fg, path bg/fg, branch bg/fg, time bg/fg). Chosen at the
|
||||
/// very start of the 6×6×6 cube — the darkest corner, rarely load-bearing for
|
||||
/// TUIs — so hijacking eight slots stays invisible in practice.
|
||||
pub(crate) const POWERLINE_SLOT0: usize = 16;
|
||||
|
||||
/// Built-in chrome themes exposed from the settings panel. The original seven
|
||||
/// Nebula looks remain unchanged; Nord/Paper are an additive dark/light pair
|
||||
/// carrying their own complete palettes.
|
||||
@@ -292,14 +286,6 @@ impl NebulaTheme {
|
||||
*colors = *defaults;
|
||||
let p = self.palette();
|
||||
colors[NamedColor::Background] = p.term_bg;
|
||||
// Powerline prompt slots: the injected prompt paints its segment chips
|
||||
// with indexed colors 16..=23 instead of baked-in truecolor, so a
|
||||
// theme switch remaps the palette and every chip ALREADY PRINTED in
|
||||
// scrollback recolors instantly — indexed cells resolve the palette at
|
||||
// draw time; truecolor is frozen the moment it is printed.
|
||||
for (i, rgb) in self.powerline_colors().into_iter().enumerate() {
|
||||
colors[POWERLINE_SLOT0 + i] = rgb;
|
||||
}
|
||||
if let Some(exact) = self.exact_term_colors() {
|
||||
let foreground = rgb8(exact.foreground);
|
||||
colors[NamedColor::Foreground] = foreground;
|
||||
@@ -376,8 +362,7 @@ impl NebulaTheme {
|
||||
}
|
||||
}
|
||||
|
||||
/// Segment colors for the injected powerline prompt, published into the
|
||||
/// 256-color palette at [`POWERLINE_SLOT0`]`..+8` by [`Self::apply_term_colors`].
|
||||
/// 旧提示符段色参考;不发布到终端扩展色表,避免覆盖应用的索引色。
|
||||
/// Order: icon bg/fg, path bg/fg, branch bg/fg, time bg/fg — one flat color
|
||||
/// per chip (the old per-character truecolor gradient could never follow a
|
||||
/// theme switch retroactively, which users read as "the prompt is stuck").
|
||||
|
||||
@@ -361,7 +361,7 @@ fn runtime_background(
|
||||
}
|
||||
|
||||
/// 主题叠加在 toml 配色之上(镜像旧壳 `apply_term_colors` 的范围与次序):
|
||||
/// 背景永远替换;Powerline 槽位 16..=23 替换。原有主题保持旧合同:仅浅色
|
||||
/// 背景永远替换;保留应用使用的扩展色槽。原有主题保持旧合同:仅浅色
|
||||
/// 主题替换前景与 ANSI-16;自带完整 palette 的主题(Nord/Paper)应用其明确色表。
|
||||
fn apply_theme(palette: &mut Palette, theme: nebula_settings::ThemeName) {
|
||||
let resolved = crate::gpui_shell::theme::ResolvedTheme::builtin(theme, None);
|
||||
@@ -398,9 +398,6 @@ fn apply_resolved_theme(palette: &mut Palette, resolved: &crate::gpui_shell::the
|
||||
return;
|
||||
}
|
||||
palette.background = rgba8(term.background);
|
||||
for (i, color) in term.powerline.into_iter().enumerate() {
|
||||
set_indexed(palette, nebula_settings::POWERLINE_SLOT0 + i as u8, rgba8(color));
|
||||
}
|
||||
if let Some(exact) = term.exact {
|
||||
let foreground = rgba8(resolved.foreground_override().unwrap_or(exact.foreground));
|
||||
palette.foreground = foreground;
|
||||
@@ -440,11 +437,6 @@ fn apply_resolved_theme(palette: &mut Palette, resolved: &crate::gpui_shell::the
|
||||
}
|
||||
}
|
||||
|
||||
fn set_indexed(palette: &mut Palette, index: u8, color: gpui::Rgba) {
|
||||
palette.indexed.retain(|(existing, _)| *existing != index);
|
||||
palette.indexed.push((index, color));
|
||||
}
|
||||
|
||||
fn rgba8(color: nebula_settings::Rgb8) -> gpui::Rgba {
|
||||
gpui::Rgba {
|
||||
r: color[0] as f32 / 255.0,
|
||||
@@ -810,6 +802,41 @@ mod tests {
|
||||
assert!(!settings.term_config().default_cursor_style.blinking);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builtin_themes_preserve_application_extended_colors() {
|
||||
use nebula_terminal::term::color::Colors;
|
||||
use nebula_terminal::vte::ansi::{Color, Rgb};
|
||||
|
||||
for theme in ThemeName::BUILTIN {
|
||||
let mut palette = Palette::default();
|
||||
apply_theme(&mut palette, theme);
|
||||
let overrides = Colors::default();
|
||||
for (index, expected) in [
|
||||
(16, [0, 0, 0]),
|
||||
(17, [0, 0, 95]),
|
||||
(18, [0, 0, 135]),
|
||||
(19, [0, 0, 175]),
|
||||
(20, [0, 0, 215]),
|
||||
(21, [0, 0, 255]),
|
||||
(22, [0, 95, 0]),
|
||||
(23, [0, 95, 95]),
|
||||
(231, [255, 255, 255]),
|
||||
(255, [238, 238, 238]),
|
||||
] {
|
||||
assert_eq!(
|
||||
palette.resolve(Color::Indexed(index), &overrides, false),
|
||||
rgba8(expected),
|
||||
"{} index {index}",
|
||||
theme.prompt_name()
|
||||
);
|
||||
assert_eq!(
|
||||
palette.query_reply(index as usize, &overrides),
|
||||
Rgb { r: expected[0], g: expected[1], b: expected[2] }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn existing_dark_theme_keeps_user_terminal_colors() {
|
||||
let mut palette = Palette::default();
|
||||
|
||||
@@ -60,13 +60,7 @@ impl ThemeDefinition {
|
||||
}
|
||||
});
|
||||
|
||||
let mut indexed = IndexedPalette::from_ansi(exact.ansi);
|
||||
// These slots are part of Nebula's existing prompt contract. Keeping
|
||||
// them in the snapshot means a custom theme recolors prompt chips and
|
||||
// the rest of the terminal from one value object.
|
||||
for (offset, color) in term.powerline.into_iter().enumerate() {
|
||||
indexed.colors[16 + offset] = color;
|
||||
}
|
||||
let indexed = IndexedPalette::from_ansi(exact.ansi);
|
||||
|
||||
Self {
|
||||
name: base.prompt_name().to_owned(),
|
||||
@@ -724,6 +718,33 @@ fn relative_luminance(color: Rgb8) -> f64 {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn builtin_snapshots_preserve_standard_extended_colors() {
|
||||
// 提示符配色不能覆盖应用使用的 xterm 色立方。
|
||||
for name in ThemeName::BUILTIN {
|
||||
let snapshot = ThemeDefinition::from_builtin(name);
|
||||
for (index, expected) in [
|
||||
(16, [0, 0, 0]),
|
||||
(17, [0, 0, 95]),
|
||||
(18, [0, 0, 135]),
|
||||
(19, [0, 0, 175]),
|
||||
(20, [0, 0, 215]),
|
||||
(21, [0, 0, 255]),
|
||||
(22, [0, 95, 0]),
|
||||
(23, [0, 95, 95]),
|
||||
(231, [255, 255, 255]),
|
||||
(255, [238, 238, 238]),
|
||||
] {
|
||||
assert_eq!(
|
||||
snapshot.terminal.palette.colors[index],
|
||||
expected,
|
||||
"{} index {index}",
|
||||
name.prompt_name()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builtin_snapshot_keeps_declared_exact_colors() {
|
||||
let nord = ThemeDefinition::from_builtin(ThemeName::Nord);
|
||||
@@ -733,7 +754,7 @@ mod tests {
|
||||
assert_eq!(nord.terminal.cursor, exact.cursor);
|
||||
assert_eq!(nord.terminal.selection_background, exact.selection_background);
|
||||
assert_eq!(nord.terminal.palette.ansi_colors(), exact.ansi);
|
||||
assert_eq!(nord.terminal.palette.colors[16], ThemeName::Nord.term_theme().powerline[0]);
|
||||
assert_eq!(nord.terminal.palette.colors[16], [0, 0, 0]);
|
||||
assert_eq!(nord.ui.selection[3], 255);
|
||||
assert_eq!(nord.ui.line[3], 255);
|
||||
|
||||
|
||||
@@ -513,13 +513,10 @@ pub struct TermTheme {
|
||||
/// 只有自带完整终端 palette 的主题使用;现有七个主题保持 `None`,继续
|
||||
/// 走原来的背景/浅色替换合同。
|
||||
pub exact: Option<ExactTermColors>,
|
||||
/// Powerline 提示符段色,发布到 256 色表 [`POWERLINE_SLOT0`]`..+8`。
|
||||
/// 旧提示符段色参考;不再发布到终端扩展色表。
|
||||
pub powerline: [Rgb8; 8],
|
||||
}
|
||||
|
||||
/// Powerline 槽位起点(索引色 16..=23)。
|
||||
pub const POWERLINE_SLOT0: u8 = 16;
|
||||
|
||||
/// 浅色主题的替换前景(#24292f)。
|
||||
pub const LIGHT_FOREGROUND: Rgb8 = [36, 41, 47];
|
||||
|
||||
|
||||
@@ -496,11 +496,8 @@ function global:prompt {
|
||||
if ($global:NebulaPromptCount -gt 1) { $leadingNewline = "`n" }
|
||||
}
|
||||
|
||||
# Segment colors come from the terminal's 256-color palette, slots
|
||||
# 16..=23 (icon bg/fg, path bg/fg, branch bg/fg, time bg/fg), published
|
||||
# per-theme by Nebula (theme.rs::apply_term_colors). Indexed colors mean a
|
||||
# theme switch recolors every prompt already in scrollback — truecolor
|
||||
# (the old scheme) is frozen the moment it prints. No theme file, no polling.
|
||||
# 使用主题的 ANSI-16 索引,历史提示符仍随主题换色;不占用应用所需的
|
||||
# xterm 扩展色槽。路径和时间用默认前景/背景,避免浅色主题的 ANSI 灰阶低对比。
|
||||
|
||||
if ($userPrompt) {
|
||||
# 视觉全部来自用户提示符;Nebula 只补协议:133;A 标出提示符起点,标题
|
||||
@@ -508,25 +505,25 @@ function global:prompt {
|
||||
$output = "$e]133;A$([char]7)$e]2;NEBULA|$cwd|$branch$([char]7)$userPrompt"
|
||||
} elseif (-not (Get-NebulaBoolSetting 'powerline' $true)) {
|
||||
$branchText = if ($branch) { " ($branch)" } else { "" }
|
||||
$output = "$leadingNewline$e]133;A$([char]7)$e]2;NEBULA|$cwd|$branch$([char]7)$e[38;5;19m$loc$branchText $e[35m$NebPromptArrow $reset"
|
||||
$output = "$leadingNewline$e]133;A$([char]7)$e]2;NEBULA|$cwd|$branch$([char]7)$e[38;5;6m$loc$branchText $e[35m$NebPromptArrow $reset"
|
||||
} else {
|
||||
$segs = New-Object System.Collections.ArrayList
|
||||
[void]$segs.Add(@{ bg=16; fg=17; t=" $NebFolderIcon " })
|
||||
[void]$segs.Add(@{ bg=18; fg=19; t=" $loc " })
|
||||
if ($branch) { [void]$segs.Add(@{ bg=20; fg=21; t=" $NebGitBranchIcon $branch " }) }
|
||||
[void]$segs.Add(@{ bg=22; fg=23; t=" $NebClockIcon $time " })
|
||||
[void]$segs.Add(@{ bg=4; fg='38;5;0'; t=" $NebFolderIcon " })
|
||||
[void]$segs.Add(@{ bg=$null; fg='39'; t=" $loc " })
|
||||
if ($branch) { [void]$segs.Add(@{ bg=$null; fg='38;5;6'; t=" $NebGitBranchIcon $branch " }) }
|
||||
[void]$segs.Add(@{ bg=$null; fg='39'; t=" $NebClockIcon $time " })
|
||||
|
||||
# 49 = default background on both caps: the cap cell's square corners
|
||||
# always match the real terminal bg (any theme / wallpaper).
|
||||
$out = "$reset$e[38;5;$($segs[0].bg)m$e[49m$NebLeftRound$reset"
|
||||
for ($i = 0; $i -lt $segs.Count; $i++) {
|
||||
$s = $segs[$i]
|
||||
$out += "$e[48;5;$($s.bg)m$e[38;5;$($s.fg)m$($s.t)"
|
||||
if ($i -lt $segs.Count - 1) {
|
||||
$nb = $segs[$i + 1].bg
|
||||
$out += "$reset$e[38;5;$($s.bg)m$e[48;5;${nb}m$NebArrow$reset"
|
||||
$bg = if ($null -eq $s.bg) { '49' } else { "48;5;$($s.bg)" }
|
||||
$out += "$e[${bg}m$e[$($s.fg)m$($s.t)"
|
||||
if ($null -ne $s.bg) {
|
||||
$out += "$reset$e[38;5;$($s.bg)m$e[49m$NebArrow$reset"
|
||||
} else {
|
||||
$out += "$reset$e[38;5;$($s.bg)m$e[49m$NebRightRound$reset"
|
||||
$out += $reset
|
||||
}
|
||||
}
|
||||
$output = "$leadingNewline$e]133;A$([char]7)$e]2;NEBULA|$cwd|$branch$([char]7)$out`n`n$e[35m$NebPromptArrow $reset"
|
||||
@@ -1357,6 +1354,39 @@ $env:NEBULA_CONFIG_DIR = $env:PEBREL_CONFIG_DIR
|
||||
function global:Set-PSReadLineOption { }
|
||||
"#;
|
||||
|
||||
#[test]
|
||||
fn powershell_prompt_uses_only_theme_ansi_slots() {
|
||||
run_powershell_integration_case(
|
||||
PS_PRELUDE,
|
||||
r#"
|
||||
function global:git { $global:LASTEXITCODE = 0; 'test-branch' }
|
||||
foreach ($powerline in @($true, $false)) {
|
||||
$global:TestPowerline = $powerline
|
||||
function global:Get-NebulaBoolSetting { param($key, $default); $global:TestPowerline }
|
||||
$rendered = prompt
|
||||
$colors = [regex]::Matches($rendered, '\x1b\[(?:38|48);5;(\d+)m')
|
||||
if ($colors.Count -eq 0) { throw 'No indexed prompt colors' }
|
||||
foreach ($color in $colors) {
|
||||
if ([int]$color.Groups[1].Value -gt 15) {
|
||||
throw "Prompt occupied extended color $($color.Groups[1].Value)"
|
||||
}
|
||||
}
|
||||
if ($powerline) {
|
||||
$e = [char]27
|
||||
if (-not $rendered.Contains("$e[49m$e[39m $((Get-Location).Path) ")) {
|
||||
throw 'Path must use theme default foreground and background'
|
||||
}
|
||||
if (-not $rendered.Contains("$e[49m$e[39m $([char]0xf017)")) {
|
||||
throw 'Clock must use theme default foreground and background'
|
||||
}
|
||||
}
|
||||
if ($rendered -notlike '*test-branch*') { throw 'Missing branch' }
|
||||
if ($rendered -notlike "*$([char]27)]133;A*") { throw 'Missing prompt boundary' }
|
||||
}
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn powershell_integration_preserves_the_users_prediction_source() {
|
||||
run_powershell_integration_case(
|
||||
|
||||
Reference in new issue
Block a user