diff --git a/nebula_app/src/display/terminal_color.rs b/nebula_app/src/display/terminal_color.rs index b715e8e..2b0fd43 100644 --- a/nebula_app/src/display/terminal_color.rs +++ b/nebula_app/src/display/terminal_color.rs @@ -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 }); diff --git a/nebula_app/src/display/ui/theme.rs b/nebula_app/src/display/ui/theme.rs index b3bbab6..c654690 100644 --- a/nebula_app/src/display/ui/theme.rs +++ b/nebula_app/src/display/ui/theme.rs @@ -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"). diff --git a/nebula_app/src/gpui_shell/config.rs b/nebula_app/src/gpui_shell/config.rs index 462949b..7abe24a 100644 --- a/nebula_app/src/gpui_shell/config.rs +++ b/nebula_app/src/gpui_shell/config.rs @@ -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(); diff --git a/nebula_settings/src/custom_theme.rs b/nebula_settings/src/custom_theme.rs index 3dcd9b1..68850a7 100644 --- a/nebula_settings/src/custom_theme.rs +++ b/nebula_settings/src/custom_theme.rs @@ -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); diff --git a/nebula_settings/src/lib.rs b/nebula_settings/src/lib.rs index 8250775..bb36c90 100644 --- a/nebula_settings/src/lib.rs +++ b/nebula_settings/src/lib.rs @@ -513,13 +513,10 @@ pub struct TermTheme { /// 只有自带完整终端 palette 的主题使用;现有七个主题保持 `None`,继续 /// 走原来的背景/浅色替换合同。 pub exact: Option, - /// 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]; diff --git a/nebula_terminal/src/tty/windows/mod.rs b/nebula_terminal/src/tty/windows/mod.rs index d39a81b..bc4fb68 100644 --- a/nebula_terminal/src/tty/windows/mod.rs +++ b/nebula_terminal/src/tty/windows/mod.rs @@ -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(