From ee5b5bd39e63231a811adb6b3b4ec7496d47df44 Mon Sep 17 00:00:00 2001 From: Bahex Date: Thu, 31 Jul 2025 23:42:10 +0300 Subject: [PATCH] fix(input list): don't leak ansi styling, fuzzy match indicator preserves styles (#16276) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fixes #16200 # Description | | Select | Fuzzy | | -- | ---------------- | --------------- | | ❌ | ![select-before] | ![fuzzy-before] | | ✅ | ![select-fixed] | ![fuzzy-fixed] | [select-before]: https://gist.githubusercontent.com/Bahex/ee2fe5074a9e2368913879159e70998c/raw/8fe913647280191f137023447c84f685e825659e/select-before.svg [select-fixed]: https://gist.githubusercontent.com/Bahex/ee2fe5074a9e2368913879159e70998c/raw/8fe913647280191f137023447c84f685e825659e/select-after.svg [fuzzy-before]: https://gist.githubusercontent.com/Bahex/ee2fe5074a9e2368913879159e70998c/raw/8fe913647280191f137023447c84f685e825659e/fuzzy-before.svg [fuzzy-fixed]: https://gist.githubusercontent.com/Bahex/ee2fe5074a9e2368913879159e70998c/raw/8fe913647280191f137023447c84f685e825659e/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> --- Cargo.lock | 1 + Cargo.toml | 1 + crates/nu-command/Cargo.toml | 1 + crates/nu-command/src/platform/input/list.rs | 90 ++++++++++++++++++-- 4 files changed, 84 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 631547b1c0..df1147928a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3778,6 +3778,7 @@ dependencies = [ "fancy-regex", "filesize", "filetime", + "fuzzy-matcher", "getrandom 0.2.15", "human-date-parser", "indexmap", diff --git a/Cargo.toml b/Cargo.toml index d81e741694..dede732a9d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -83,6 +83,7 @@ csv = "1.3" ctrlc = "3.4" devicons = "0.6.12" dialoguer = { default-features = false, version = "0.11" } +fuzzy-matcher = { version = "^0.3.7" } digest = { default-features = false, version = "0.10" } dirs = "5.0" dirs-sys = "0.4" diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml index 243027786e..a0450aff1c 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -54,6 +54,7 @@ devicons = { workspace = true } dialoguer = { workspace = true, default-features = false, features = [ "fuzzy-select", ] } +fuzzy-matcher = { workspace = true } digest = { workspace = true, default-features = false } dtparse = { workspace = true } encoding_rs = { workspace = true } diff --git a/crates/nu-command/src/platform/input/list.rs b/crates/nu-command/src/platform/input/list.rs index ab12d3a36a..271ad8dde1 100644 --- a/crates/nu-command/src/platform/input/list.rs +++ b/crates/nu-command/src/platform/input/list.rs @@ -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 multi_select = MultiSelect::new(); //::with_theme(&theme); + let multi_select = MultiSelect::with_theme(&NuTheme); InteractMode::Multi( if let Some(prompt) = prompt { @@ -146,7 +140,7 @@ impl Command for InputList { })?, ) } else if fuzzy { - let fuzzy_select = FuzzySelect::new(); //::with_theme(&theme); + let fuzzy_select = FuzzySelect::with_theme(&NuTheme); InteractMode::Single( if let Some(prompt) = prompt { @@ -163,7 +157,7 @@ impl Command for InputList { })?, ) } else { - let select = Select::new(); //::with_theme(&theme); + let select = Select::with_theme(&NuTheme); InteractMode::Single( if let Some(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)] mod test { use super::*;