From dbc50ed49f3f4f62734b2d3fdfbd7242c3f22012 Mon Sep 17 00:00:00 2001 From: arookieofc <2128194521@qq.com> Date: Mon, 6 Apr 2026 23:14:20 +0800 Subject: [PATCH] feat(list_menu): add DescriptionPosition to control description placement --- src/lib.rs | 4 +- src/menu/list_menu.rs | 129 +++++++++++++++++++++++++++++++----------- src/menu/mod.rs | 1 + 3 files changed, 100 insertions(+), 34 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 4d14261b6..f06992160 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -277,8 +277,8 @@ pub use validator::{DefaultValidator, ValidationResult, Validator}; mod menu; pub use menu::{ - menu_functions, ColumnarMenu, DescriptionMenu, DescriptionMode, IdeMenu, ListMenu, Menu, - MenuBuilder, MenuEvent, MenuTextStyle, ReedlineMenu, + menu_functions, ColumnarMenu, DescriptionMenu, DescriptionMode, DescriptionPosition, IdeMenu, + ListMenu, Menu, MenuBuilder, MenuEvent, MenuTextStyle, ReedlineMenu, }; mod terminal_extensions; diff --git a/src/menu/list_menu.rs b/src/menu/list_menu.rs index 3ed16f6bb..e6ed9120e 100644 --- a/src/menu/list_menu.rs +++ b/src/menu/list_menu.rs @@ -13,6 +13,19 @@ use { const SELECTION_CHAR: char = '!'; +/// Controls where the description is rendered relative to the completion value +/// in a [`ListMenu`] row. +#[derive(Clone, Default, Debug, PartialEq, Eq)] +pub enum DescriptionPosition { + /// Description is shown **before** the value, wrapped in parentheses: + /// `(description) value` — the original behaviour. + #[default] + Before, + /// Description is shown **after** the value with a leading space: + /// `value description` + After, +} + struct Page { size: usize, full: bool, @@ -67,6 +80,8 @@ pub struct ListMenu { event: Option, /// String collected after the menu is activated input: Option, + /// Controls where the description is rendered relative to the completion value + description_position: DescriptionPosition, } impl Default for ListMenu { @@ -87,6 +102,7 @@ impl Default for ListMenu { pages: Vec::new(), event: None, input: None, + description_position: DescriptionPosition::default(), } } } @@ -113,6 +129,15 @@ impl ListMenu { self.max_lines = max_lines; self } + + /// Menu builder to set where descriptions are rendered relative to the + /// completion value. Defaults to [`DescriptionPosition::Before`] for + /// backwards compatibility. + #[must_use] + pub fn with_description_position(mut self, position: DescriptionPosition) -> Self { + self.description_position = position; + self + } } // Menu functionality @@ -265,40 +290,80 @@ impl ListMenu { row_number: &str, use_ansi_coloring: bool, ) -> String { - let description = description.map_or("".to_string(), |desc| { - if use_ansi_coloring { - format!( - "{}({}) {}", - self.settings.color.description_style.prefix(), - desc, - RESET - ) - } else { - format!("({desc}) ") - } - }); + match self.description_position { + DescriptionPosition::Before => { + let description = description.map_or("".to_string(), |desc| { + if use_ansi_coloring { + format!( + "{}({}) ", + self.settings.color.description_style.prefix(), + desc, + ) + } else { + format!("({desc}) ") + } + }); + + if use_ansi_coloring { + format!( + "{}{}{}{}{}{}", + row_number, + description, + self.text_style(index), + &line, + RESET, + Self::end_of_line(), + ) + } else { + // If no ansi coloring is found, then the selection word is + // the line in uppercase + let line_str = if index == self.index() { + format!("{}{}>{}", row_number, description, line.to_uppercase()) + } else { + format!("{row_number}{description}{line}") + }; - if use_ansi_coloring { - format!( - "{}{}{}{}{}{}", - row_number, - description, - self.text_style(index), - &line, - RESET, - Self::end_of_line(), - ) - } else { - // If no ansi coloring is found, then the selection word is - // the line in uppercase - let line_str = if index == self.index() { - format!("{}{}>{}", row_number, description, line.to_uppercase()) - } else { - format!("{row_number}{description}{line}") - }; + // Final string with formatting + format!("{}{}", line_str, Self::end_of_line()) + } + } + DescriptionPosition::After => { + let description = description.map_or("".to_string(), |desc| { + if use_ansi_coloring { + format!( + " {}{}{}", + self.settings.color.description_style.prefix(), + desc, + RESET + ) + } else { + format!(" {desc}") + } + }); + + if use_ansi_coloring { + format!( + "{}{}{}{}{}{}", + row_number, + self.text_style(index), + &line, + RESET, + description, + Self::end_of_line(), + ) + } else { + // If no ansi coloring is found, then the selection word is + // the line in uppercase + let line_str = if index == self.index() { + format!("{}>{}{}", row_number, line.to_uppercase(), description) + } else { + format!("{row_number}{line}{description}") + }; - // Final string with formatting - format!("{}{}", line_str, Self::end_of_line()) + // Final string with formatting + format!("{}{}", line_str, Self::end_of_line()) + } + } } } } diff --git a/src/menu/mod.rs b/src/menu/mod.rs index a08ccbc09..b27acda40 100644 --- a/src/menu/mod.rs +++ b/src/menu/mod.rs @@ -11,6 +11,7 @@ pub use columnar_menu::ColumnarMenu; pub use description_menu::DescriptionMenu; pub use ide_menu::DescriptionMode; pub use ide_menu::IdeMenu; +pub use list_menu::DescriptionPosition; pub use list_menu::ListMenu; use nu_ansi_term::{Color, Style};