Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
208 changes: 200 additions & 8 deletions src/painting/painter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down Expand Up @@ -914,23 +914,23 @@ 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
.queue(Print(&coerce_crlf(&lines.prompt_str_left)))?;

if use_ansi_coloring {
self.stdout
.queue(SetForegroundColor(prompt.get_indicator_color()))?;
.queue(Print(prompt.get_indicator_color().prefix()))?;
}

self.stdout
.queue(Print(&coerce_crlf(&lines.prompt_indicator)))?;

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)?;
Expand Down Expand Up @@ -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
Expand All @@ -998,15 +998,15 @@ 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)?;
}

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);
Expand Down Expand Up @@ -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};
Expand Down Expand Up @@ -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:?}"
);
}
}
}
23 changes: 16 additions & 7 deletions src/prompt/base.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use {
crate::core_editor::{RestPolicy, SelectionExtent},
crossterm::style::Color,
nu_ansi_term::Color,
serde::{Deserialize, Serialize},
std::{
borrow::Cow,
Expand All @@ -9,11 +9,20 @@ use {
strum::{EnumIter, EnumString, IntoDiscriminant},
};

/// The default color for the prompt, indicator, and right prompt
pub static DEFAULT_PROMPT_COLOR: Color = Color::Green;
pub static DEFAULT_PROMPT_MULTILINE_COLOR: nu_ansi_term::Color = nu_ansi_term::Color::LightBlue;
pub static DEFAULT_INDICATOR_COLOR: Color = Color::Cyan;
pub static DEFAULT_PROMPT_RIGHT_COLOR: Color = Color::AnsiValue(5);
// 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::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::LightCyan;
/// The default color for the right prompt
pub static DEFAULT_PROMPT_RIGHT_COLOR: Color = Color::Purple;

/// The current success/failure of the history search
pub enum PromptHistorySearchStatus {
Expand Down Expand Up @@ -192,7 +201,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
Expand Down
3 changes: 2 additions & 1 deletion src/prompt/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Loading