fix(input list): don't leak ansi styling, fuzzy match indicator preserves styles (#16276)

- fixes #16200

# Description

|    | Select           | Fuzzy           |
| -- | ---------------- | --------------- |
|  | ![select-before] | ![fuzzy-before] |
|  | ![select-fixed]  | ![fuzzy-fixed]  |

[select-before]:
8fe9136472/select-before.svg
[select-fixed]:
8fe9136472/select-after.svg
[fuzzy-before]:
8fe9136472/fuzzy-before.svg
[fuzzy-fixed]:
8fe9136472/fuzzy-after.svg

Using a custom `dialoguer::theme::Theme` implementation, how `input
list` renders items are overridden.

Unfortunately, implementing one of the methods requires
`fuzzy_matcher::skim::SkimMatcherV2` which `dialoguer` does not export
by itself.
Had to add an explicit dependency to `fuzzy_matcher`, which we already
depend on through `dialoguer`. Version specification is copied from
`dialoguer`.

# Tests + Formatting
No tests added.
Couldn't find existing tests, not sure how to test this.

---------

Co-authored-by: Bahex <17417311+Bahex@users.noreply.github.com>
This commit is contained in:
Bahex
2025-07-31 23:42:10 +03:00
committed by GitHub
parent d565c9ed01
commit ee5b5bd39e
4 changed files with 84 additions and 9 deletions

1
Cargo.lock generated
View File

@ -3778,6 +3778,7 @@ dependencies = [
"fancy-regex", "fancy-regex",
"filesize", "filesize",
"filetime", "filetime",
"fuzzy-matcher",
"getrandom 0.2.15", "getrandom 0.2.15",
"human-date-parser", "human-date-parser",
"indexmap", "indexmap",

View File

@ -83,6 +83,7 @@ csv = "1.3"
ctrlc = "3.4" ctrlc = "3.4"
devicons = "0.6.12" devicons = "0.6.12"
dialoguer = { default-features = false, version = "0.11" } dialoguer = { default-features = false, version = "0.11" }
fuzzy-matcher = { version = "^0.3.7" }
digest = { default-features = false, version = "0.10" } digest = { default-features = false, version = "0.10" }
dirs = "5.0" dirs = "5.0"
dirs-sys = "0.4" dirs-sys = "0.4"

View File

@ -54,6 +54,7 @@ devicons = { workspace = true }
dialoguer = { workspace = true, default-features = false, features = [ dialoguer = { workspace = true, default-features = false, features = [
"fuzzy-select", "fuzzy-select",
] } ] }
fuzzy-matcher = { workspace = true }
digest = { workspace = true, default-features = false } digest = { workspace = true, default-features = false }
dtparse = { workspace = true } dtparse = { workspace = true }
encoding_rs = { workspace = true } encoding_rs = { workspace = true }

View File

@ -123,14 +123,8 @@ impl Command for InputList {
}); });
} }
// could potentially be used to map the use theme colors at some point
// let theme = dialoguer::theme::ColorfulTheme {
// active_item_style: Style::new().fg(Color::Cyan).bold(),
// ..Default::default()
// };
let answer: InteractMode = if multi { let answer: InteractMode = if multi {
let multi_select = MultiSelect::new(); //::with_theme(&theme); let multi_select = MultiSelect::with_theme(&NuTheme);
InteractMode::Multi( InteractMode::Multi(
if let Some(prompt) = prompt { if let Some(prompt) = prompt {
@ -146,7 +140,7 @@ impl Command for InputList {
})?, })?,
) )
} else if fuzzy { } else if fuzzy {
let fuzzy_select = FuzzySelect::new(); //::with_theme(&theme); let fuzzy_select = FuzzySelect::with_theme(&NuTheme);
InteractMode::Single( InteractMode::Single(
if let Some(prompt) = prompt { if let Some(prompt) = prompt {
@ -163,7 +157,7 @@ impl Command for InputList {
})?, })?,
) )
} else { } else {
let select = Select::new(); //::with_theme(&theme); let select = Select::with_theme(&NuTheme);
InteractMode::Single( InteractMode::Single(
if let Some(prompt) = prompt { if let Some(prompt) = prompt {
select.with_prompt(&prompt) select.with_prompt(&prompt)
@ -255,6 +249,84 @@ impl Command for InputList {
} }
} }
use dialoguer::theme::{SimpleTheme, Theme};
use nu_ansi_term::ansi::RESET;
// could potentially be used to map the use theme colors at some point
/// Theme for handling already colored items gracefully.
struct NuTheme;
impl Theme for NuTheme {
fn format_select_prompt_item(
&self,
f: &mut dyn std::fmt::Write,
text: &str,
active: bool,
) -> std::fmt::Result {
SimpleTheme.format_select_prompt_item(f, text, active)?;
write!(f, "{RESET}")
}
fn format_multi_select_prompt_item(
&self,
f: &mut dyn std::fmt::Write,
text: &str,
checked: bool,
active: bool,
) -> std::fmt::Result {
SimpleTheme.format_multi_select_prompt_item(f, text, checked, active)?;
write!(f, "{RESET}")
}
fn format_sort_prompt_item(
&self,
f: &mut dyn std::fmt::Write,
text: &str,
picked: bool,
active: bool,
) -> std::fmt::Result {
SimpleTheme.format_sort_prompt_item(f, text, picked, active)?;
writeln!(f, "{RESET}")
}
fn format_fuzzy_select_prompt_item(
&self,
f: &mut dyn std::fmt::Write,
text: &str,
active: bool,
highlight_matches: bool,
matcher: &fuzzy_matcher::skim::SkimMatcherV2,
search_term: &str,
) -> std::fmt::Result {
use fuzzy_matcher::FuzzyMatcher;
write!(f, "{} ", if active { ">" } else { " " })?;
if !highlight_matches {
return write!(f, "{text}{RESET}");
}
let Some((_score, indices)) = matcher.fuzzy_indices(text, search_term) else {
return write!(f, "{text}{RESET}");
};
let prefix = nu_ansi_term::Style::new()
.italic()
.underline()
.prefix()
.to_string();
// HACK: Reset italic and underline, from the `ansi` command, should be moved to `nu_ansi_term`
let suffix = "\x1b[23;24m";
for (idx, c) in text.chars().enumerate() {
if indices.contains(&idx) {
write!(f, "{prefix}{c}{suffix}")?;
} else {
write!(f, "{}", c)?;
}
}
write!(f, "{RESET}")
}
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;