From b270e3963d7c020acdd5a8ed630828176390e341 Mon Sep 17 00:00:00 2001 From: kronberger-droid Date: Wed, 22 Jul 2026 15:04:00 +0200 Subject: [PATCH 1/3] refactor(prompt)!: use nu_ansi_term::Color throughout BREAKING CHANGE: Prompt::get_prompt_color, get_indicator_color and get_prompt_right_color now take nu_ansi_term::Color, and reedline::Color re-exports it instead of crossterm's. Note crossterm's Green/Cyan are the bright palette entries; the nu-ansi-term equivalents are LightGreen/LightCyan. --- src/lib.rs | 9 ++++----- src/painting/painter.rs | 14 +++++++------- src/prompt/base.rs | 13 ++++++++----- src/prompt/mod.rs | 3 ++- 4 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 997b56393..18cbfa01e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -259,7 +259,8 @@ pub use history::{ mod prompt; pub use prompt::{ DefaultPrompt, DefaultPromptSegment, Prompt, PromptEditMode, PromptEditModeDiscriminants, - PromptHistorySearch, PromptHistorySearchStatus, PromptViMode, + PromptHistorySearch, PromptHistorySearchStatus, PromptViMode, DEFAULT_INDICATOR_COLOR, + DEFAULT_PROMPT_COLOR, DEFAULT_PROMPT_MULTILINE_COLOR, DEFAULT_PROMPT_RIGHT_COLOR, }; mod edit_mode; @@ -306,9 +307,7 @@ pub use utils::{ }; // Reexport the key types to be independent from an explicit crossterm dependency. -pub use crossterm::{ - event::{KeyCode, KeyModifiers}, - style::Color, -}; +pub use crossterm::event::{KeyCode, KeyModifiers}; #[cfg(feature = "external_printer")] pub use external_printer::ExternalPrinter; +pub use nu_ansi_term::Color; diff --git a/src/painting/painter.rs b/src/painting/painter.rs index 80a47881a..ce1652d2a 100644 --- a/src/painting/painter.rs +++ b/src/painting/painter.rs @@ -10,7 +10,7 @@ use { }, crossterm::{ cursor::{self, MoveTo, RestorePosition, SavePosition}, - style::{Attribute, Print, ResetColor, SetAttribute, SetForegroundColor}, + style::{Attribute, Print, ResetColor, SetAttribute}, terminal::{self, Clear, ClearType}, QueueableCommand, }, @@ -914,7 +914,7 @@ impl Painter { // print our prompt with color if use_ansi_coloring { self.stdout - .queue(SetForegroundColor(prompt.get_prompt_color()))?; + .queue(Print(prompt.get_prompt_color().prefix()))?; } self.stdout @@ -922,7 +922,7 @@ impl Painter { if use_ansi_coloring { self.stdout - .queue(SetForegroundColor(prompt.get_indicator_color()))?; + .queue(Print(prompt.get_indicator_color().prefix()))?; } self.stdout @@ -930,7 +930,7 @@ impl Painter { if use_ansi_coloring { self.stdout - .queue(SetForegroundColor(prompt.get_prompt_right_color()))?; + .queue(Print(prompt.get_prompt_right_color().prefix()))?; } self.print_right_prompt(lines, layout)?; @@ -987,7 +987,7 @@ impl Painter { // print our prompt with color if use_ansi_coloring { self.stdout - .queue(SetForegroundColor(prompt.get_prompt_color()))?; + .queue(Print(prompt.get_prompt_color().prefix()))?; } // In case the prompt is made out of multiple lines, the prompt is split by @@ -998,7 +998,7 @@ impl Painter { if extra_rows == 0 { if use_ansi_coloring { self.stdout - .queue(SetForegroundColor(prompt.get_prompt_right_color()))?; + .queue(Print(prompt.get_prompt_right_color().prefix()))?; } self.print_right_prompt(lines, layout)?; @@ -1006,7 +1006,7 @@ impl Painter { if use_ansi_coloring { self.stdout - .queue(SetForegroundColor(prompt.get_indicator_color()))?; + .queue(Print(prompt.get_indicator_color().prefix()))?; } let indicator_skipped = skip_buffer_lines(&lines.prompt_indicator, extra_rows_after_prompt, None); diff --git a/src/prompt/base.rs b/src/prompt/base.rs index 13de74c8c..233099a04 100644 --- a/src/prompt/base.rs +++ b/src/prompt/base.rs @@ -1,6 +1,6 @@ use { crate::core_editor::{RestPolicy, SelectionExtent}, - crossterm::style::Color, + nu_ansi_term::Color, serde::{Deserialize, Serialize}, std::{ borrow::Cow, @@ -9,11 +9,14 @@ use { strum::{EnumIter, EnumString, IntoDiscriminant}, }; -/// The default color for the prompt, indicator, and right prompt +/// The default color for the prompt pub static DEFAULT_PROMPT_COLOR: Color = Color::Green; -pub static DEFAULT_PROMPT_MULTILINE_COLOR: nu_ansi_term::Color = nu_ansi_term::Color::LightBlue; +/// The default color for the multiline prompt indicator +pub static DEFAULT_PROMPT_MULTILINE_COLOR: Color = Color::LightBlue; +/// The default color for the prompt indicator pub static DEFAULT_INDICATOR_COLOR: Color = Color::Cyan; -pub static DEFAULT_PROMPT_RIGHT_COLOR: Color = Color::AnsiValue(5); +/// The default color for the right prompt +pub static DEFAULT_PROMPT_RIGHT_COLOR: Color = Color::Fixed(5); /// The current success/failure of the history search pub enum PromptHistorySearchStatus { @@ -192,7 +195,7 @@ pub trait Prompt: Send { DEFAULT_PROMPT_COLOR } /// Get the default multiline prompt color - fn get_prompt_multiline_color(&self) -> nu_ansi_term::Color { + fn get_prompt_multiline_color(&self) -> Color { DEFAULT_PROMPT_MULTILINE_COLOR } /// Get the default indicator color diff --git a/src/prompt/mod.rs b/src/prompt/mod.rs index 52e0f0758..d698cf4c3 100644 --- a/src/prompt/mod.rs +++ b/src/prompt/mod.rs @@ -3,7 +3,8 @@ mod default; pub use base::{ Prompt, PromptEditMode, PromptEditModeDiscriminants, PromptHistorySearch, - PromptHistorySearchStatus, PromptViMode, + PromptHistorySearchStatus, PromptViMode, DEFAULT_INDICATOR_COLOR, DEFAULT_PROMPT_COLOR, + DEFAULT_PROMPT_MULTILINE_COLOR, DEFAULT_PROMPT_RIGHT_COLOR, }; pub use default::{DefaultPrompt, DefaultPromptSegment}; From e6c893554ade37ab33bc2d5e077a078abd7cf03b Mon Sep 17 00:00:00 2001 From: kronberger-droid Date: Wed, 22 Jul 2026 14:40:42 +0200 Subject: [PATCH 2/3] fix(prompt): keep default prompt colors on the bright palette crossterm's Green/Cyan are palette 10/14 (it spells the dark ones DarkGreen/DarkCyan); nu-ansi-term's are 2/6. The name-for-name translation darkened the prompt and indicator. Use LightGreen/LightCyan, which also match nushell's light_green / light_cyan config names. --- src/prompt/base.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/prompt/base.rs b/src/prompt/base.rs index 233099a04..bf9543157 100644 --- a/src/prompt/base.rs +++ b/src/prompt/base.rs @@ -9,14 +9,20 @@ use { strum::{EnumIter, EnumString, IntoDiscriminant}, }; +// The *light* variants are deliberate. Before the nu-ansi-term migration these +// were crossterm's `Color::Green`/`Color::Cyan`, which are palette 10 and 14; +// crossterm spells the dark ones `DarkGreen`/`DarkCyan`. nu-ansi-term has no +// `Dark*` prefix, so its `Green` is palette 2. Naming them here would darken +// every default prompt. + /// The default color for the prompt -pub static DEFAULT_PROMPT_COLOR: Color = Color::Green; +pub static DEFAULT_PROMPT_COLOR: Color = Color::LightGreen; /// The default color for the multiline prompt indicator pub static DEFAULT_PROMPT_MULTILINE_COLOR: Color = Color::LightBlue; /// The default color for the prompt indicator -pub static DEFAULT_INDICATOR_COLOR: Color = Color::Cyan; +pub static DEFAULT_INDICATOR_COLOR: Color = Color::LightCyan; /// The default color for the right prompt -pub static DEFAULT_PROMPT_RIGHT_COLOR: Color = Color::Fixed(5); +pub static DEFAULT_PROMPT_RIGHT_COLOR: Color = Color::Purple; /// The current success/failure of the history search pub enum PromptHistorySearchStatus { From cf4652e642bce3cf21ed53a17dce018945fea03f Mon Sep 17 00:00:00 2001 From: kronberger-droid Date: Wed, 22 Jul 2026 14:41:30 +0200 Subject: [PATCH 3/3] test(prompt): pin prompt color output across nu-ansi-term switch Covers the SGR bytes emitted for prompt, indicator and right prompt on both buffer paths: the trait defaults, Color::Default emitting an explicit SGR 39 (#1046), the use_ansi_coloring guards, and that crossterm's Green/Cyan were the bright palette entries. --- src/painting/painter.rs | 194 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 193 insertions(+), 1 deletion(-) diff --git a/src/painting/painter.rs b/src/painting/painter.rs index ce1652d2a..fab7fee65 100644 --- a/src/painting/painter.rs +++ b/src/painting/painter.rs @@ -1220,7 +1220,7 @@ impl Painter { mod tests { use super::*; use crate::menu::MenuEvent; - use crate::{Completer, Editor, PromptHistorySearch, Suggestion}; + use crate::{Color, Completer, Editor, PromptHistorySearch, Suggestion}; use pretty_assertions::assert_eq; use std::borrow::Cow; use std::sync::{Arc, Mutex}; @@ -1741,4 +1741,196 @@ mod tests { ] ); } + + const SGR_GREEN: &str = "\x1b[92m"; // DEFAULT_PROMPT_COLOR (LightGreen, palette 10) + const SGR_CYAN: &str = "\x1b[96m"; // DEFAULT_INDICATOR_COLOR (LightCyan, palette 14) + const SGR_PURPLE: &str = "\x1b[35m"; // DEFAULT_PROMPT_RIGHT_COLOR (Purple, palette 5) + const SGR_DEFAULT_FG: &str = "\x1b[39m"; // Color::Default — "terminal foreground" + + /// A prompt whose color methods are set per-test. Rendering is inherited from + /// `TestPrompt` since these tests only care about the emitted escapes. + struct ColoredPrompt { + prompt: Color, + indicator: Color, + right: Color, + } + + impl Prompt for ColoredPrompt { + fn render_prompt_left(&self) -> Cow<'_, str> { + TestPrompt.render_prompt_left() + } + fn render_prompt_right(&self) -> Cow<'_, str> { + TestPrompt.render_prompt_right() + } + fn render_prompt_indicator(&self, mode: PromptEditMode) -> Cow<'_, str> { + TestPrompt.render_prompt_indicator(mode) + } + fn render_prompt_multiline_indicator(&self) -> Cow<'_, str> { + TestPrompt.render_prompt_multiline_indicator() + } + fn render_prompt_history_search_indicator( + &self, + search: PromptHistorySearch, + ) -> Cow<'_, str> { + TestPrompt.render_prompt_history_search_indicator(search) + } + + fn get_prompt_color(&self) -> Color { + self.prompt + } + fn get_indicator_color(&self) -> Color { + self.indicator + } + fn get_prompt_right_color(&self) -> Color { + self.right + } + } + /// Like `capture_repaint`, but with configurable ANSI coloring. Also returns + /// whether the paint took the large-buffer branch, since `repaint_buffer` + /// recomputes that field and a caller cannot force it. + fn capture_repaint_ansi( + prompt: &dyn Prompt, + lines: &PromptLines, + use_ansi_coloring: bool, + ) -> (String, bool) { + let mut p = Painter::new(W::capture()); + p.terminal_size = (20, 10); + p.prompt_start_row.mark_verified(0); + p.prompt_height = 1; + p.repaint_buffer( + prompt, + lines, + PromptEditMode::Default, + None, + use_ansi_coloring, + &None, + ) + .expect("repaint_buffer failed"); + let large = p.large_buffer; + ( + String::from_utf8_lossy(p.stdout.captured()).into_owned(), + large, + ) + } + + /// Records which palette entries crossterm's `SetForegroundColor` selected + /// for the pre-migration defaults. crossterm's `Green`/`Cyan` are the + /// *bright* entries (10/14) — `DarkGreen`/`DarkCyan` are 2/6 — so the + /// nu-ansi-term replacements have to be the `Light*` variants to keep the + /// prompt looking the same. + /// + /// Green and cyan intentionally re-encode from the 256-color form + /// (`38;5;10`) to the aixterm form (`92`); both select palette entry 10. + #[test] + fn crossterm_defaults_were_the_bright_palette_entries() { + use crossterm::{ + style::{Color as CtColor, SetForegroundColor}, + Command, + }; + + fn crossterm_sgr(color: CtColor) -> String { + let mut buf = String::new(); + SetForegroundColor(color) + .write_ansi(&mut buf) + .expect("write_ansi failed"); + buf + } + + for (name, crossterm, palette) in [ + ("prompt", CtColor::Green, 10), + ("indicator", CtColor::Cyan, 14), + ("right prompt", CtColor::AnsiValue(5), 5), + ] { + assert_eq!( + crossterm_sgr(crossterm), + Color::Fixed(palette).prefix().to_string(), + "{name} default selected a different palette entry than assumed" + ); + } + + // "Unstyled" has no palette entry; both spellings are SGR 39. + assert_eq!( + Color::Default.prefix().to_string(), + crossterm_sgr(CtColor::Reset) + ); + } + + /// The trait's default colors reach the terminal as the expected SGR + /// sequences, on the small-buffer path. + #[test] + fn default_prompt_colors_emit_expected_sgr() { + let (out, _) = + capture_repaint_ansi(&TestPrompt, &make_lines("> ", "", "RP", "hi", ""), true); + + assert!( + out.contains(SGR_GREEN), + "left prompt color missing: {out:?}" + ); + assert!(out.contains(SGR_CYAN), "indicator color missing: {out:?}"); + assert!( + out.contains(SGR_PURPLE), + "right prompt color missing: {out:?}" + ); + } + + /// `Color::Default` must emit an explicit SGR 39, never an empty prefix — + /// an empty one would let the active color bleed into an unstyled prompt, + /// which is the starship bug (#1046). + #[test] + fn default_color_emits_explicit_foreground_reset() { + let prompt = ColoredPrompt { + prompt: Color::Default, + indicator: Color::Default, + right: Color::Default, + }; + let (out, _) = capture_repaint_ansi(&prompt, &make_lines("> ", "", "RP", "hi", ""), true); + + assert!( + out.contains(SGR_DEFAULT_FG), + "Color::Default must emit an explicit SGR 39, not nothing: {out:?}" + ); + assert!( + !out.contains(SGR_GREEN), + "no default color should leak through: {out:?}" + ); + } + + /// `print_large_buffer` has its own three color call sites that the other + /// capture tests never reach. + /// + /// The bulk sits in `after_cursor` so `extra_rows` stays 0 and the right + /// prompt is still drawn; a tall `before_cursor` would suppress it (see + /// `test_layout_right_prompt_suppressed_in_large_buffer`). + #[test] + fn large_buffer_path_emits_prompt_colors() { + let tall = "line\n".repeat(15); + let (out, large) = + capture_repaint_ansi(&TestPrompt, &make_lines("> ", "", "RP", "hi", &tall), true); + + assert!(large, "expected the large-buffer path to be taken"); + assert!( + out.contains(SGR_GREEN), + "left prompt color missing: {out:?}" + ); + assert!(out.contains(SGR_CYAN), "indicator color missing: {out:?}"); + assert!( + out.contains(SGR_PURPLE), + "right prompt color missing: {out:?}" + ); + } + + /// Every color write stays inside its `use_ansi_coloring` guard. Checks the + /// color sequences only — `repaint_buffer` always emits a leading `\x1b[0m`. + #[test] + fn no_prompt_colors_when_ansi_coloring_disabled() { + let (out, _) = + capture_repaint_ansi(&TestPrompt, &make_lines("> ", "", "RP", "hi", ""), false); + + for sgr in [SGR_GREEN, SGR_CYAN, SGR_PURPLE] { + assert!( + !out.contains(sgr), + "emitted {sgr:?} with coloring disabled: {out:?}" + ); + } + } }