Download crates/forge_select/src/select.rs from SaylorTwift/forgecode: direct link, hf CLI and curl.
- Browser
- Download file 8.93 kB
-
https://huggingface.co/SaylorTwift/forgecode/resolve/main/crates/forge_select/src/select.rs
- Command line
-
hf download hf://SaylorTwift/forgecode/crates/forge_select/src/select.rs
-
curl -L -o select.rs https://huggingface.co/SaylorTwift/forgecode/resolve/main/crates/forge_select/src/select.rs
8.93 kB
| use std::io::IsTerminal; | |
| use anyhow::Result; | |
| use console::strip_ansi_codes; | |
| use crate::preview::{PreviewLayout, PreviewPlacement, SelectMode, SelectRow, SelectUiOptions}; | |
| /// Builder for select prompts with fuzzy search. | |
| pub struct SelectBuilder<T> { | |
| pub(crate) message: String, | |
| pub(crate) options: Vec<T>, | |
| pub(crate) starting_cursor: Option<usize>, | |
| pub(crate) default: Option<bool>, | |
| pub(crate) help_message: Option<&'static str>, | |
| pub(crate) initial_text: Option<String>, | |
| pub(crate) header_lines: usize, | |
| pub(crate) preview: Option<String>, | |
| pub(crate) preview_window: Option<String>, | |
| } | |
| impl<T: 'static> SelectBuilder<T> { | |
| /// Set starting cursor position. | |
| pub fn with_starting_cursor(mut self, cursor: usize) -> Self { | |
| self.starting_cursor = Some(cursor); | |
| self | |
| } | |
| /// Set a preview command shown in a side panel as the user navigates items. | |
| pub fn with_preview(mut self, command: impl Into<String>) -> Self { | |
| self.preview = Some(command.into()); | |
| self | |
| } | |
| /// Set the layout of the preview panel. | |
| pub fn with_preview_window(mut self, layout: impl Into<String>) -> Self { | |
| self.preview_window = Some(layout.into()); | |
| self | |
| } | |
| /// Set default for confirm prompts using bool options. | |
| pub fn with_default(mut self, default: bool) -> Self { | |
| self.default = Some(default); | |
| self | |
| } | |
| /// Set help message displayed as a header above the list. | |
| pub fn with_help_message(mut self, message: &'static str) -> Self { | |
| self.help_message = Some(message); | |
| self | |
| } | |
| /// Set initial search text for fuzzy search. | |
| pub fn with_initial_text(mut self, text: impl Into<String>) -> Self { | |
| self.initial_text = Some(text.into()); | |
| self | |
| } | |
| /// Set the number of header lines treated as non-selectable options. | |
| pub fn with_header_lines(mut self, n: usize) -> Self { | |
| self.header_lines = n; | |
| self | |
| } | |
| /// Execute select prompt with fuzzy search. | |
| /// | |
| /// # Returns | |
| /// | |
| /// - `Ok(Some(T))` when the user selects an option. | |
| /// - `Ok(None)` when no options are available or the user cancels. | |
| /// | |
| /// # Errors | |
| /// | |
| /// Returns an error if the picker cannot set up terminal interaction, | |
| /// render, process events, or run a preview command. | |
| pub fn prompt(self) -> Result<Option<T>> | |
| where | |
| T: std::fmt::Display + Clone, | |
| { | |
| if !std::io::stderr().is_terminal() { | |
| return Ok(None); | |
| } | |
| if std::any::TypeId::of::<T>() == std::any::TypeId::of::<bool>() { | |
| return prompt_confirm_as(&self.message, self.default); | |
| } | |
| if self.options.is_empty() { | |
| return Ok(None); | |
| } | |
| let rows = self | |
| .options | |
| .iter() | |
| .enumerate() | |
| .map(|(index, item)| { | |
| let display = strip_ansi_codes(&item.to_string()).trim().to_string(); | |
| if index < self.header_lines { | |
| SelectRow::header(display) | |
| } else { | |
| SelectRow::new(index.to_string(), display.clone()).search(display) | |
| } | |
| }) | |
| .collect::<Vec<_>>(); | |
| let header_count = self.header_lines.min(rows.len()); | |
| if rows.len() == header_count { | |
| return Ok(None); | |
| } | |
| let mut selector = SelectUiOptions::new(format!("{} ❯ ", self.message), rows) | |
| .header_lines(header_count) | |
| .mode(SelectMode::Single) | |
| .preview_layout(parse_preview_layout(self.preview_window.as_deref())); | |
| if let Some(query) = self.initial_text { | |
| selector = selector.query(Some(query)); | |
| } | |
| if let Some(preview) = self.preview { | |
| selector = selector.preview(Some(preview)); | |
| } | |
| if let Some(cursor) = self.starting_cursor { | |
| selector = selector.initial_raw(Some(cursor.to_string())); | |
| } | |
| if let Some(help) = self.help_message { | |
| selector.rows.insert(0, SelectRow::header(help)); | |
| selector.header_lines = selector.header_lines.saturating_add(1); | |
| } | |
| let selected = selector.prompt()?; | |
| Ok(selected.and_then(|row| { | |
| row.raw | |
| .parse::<usize>() | |
| .ok() | |
| .and_then(|index| self.options.get(index).cloned()) | |
| })) | |
| } | |
| } | |
| fn parse_preview_layout(layout: Option<&str>) -> PreviewLayout { | |
| let Some(layout) = layout else { | |
| return PreviewLayout::default(); | |
| }; | |
| let placement = if layout.contains("down") || layout.contains("bottom") { | |
| PreviewPlacement::Bottom | |
| } else { | |
| PreviewPlacement::Right | |
| }; | |
| let percent = layout | |
| .split(|ch: char| !ch.is_ascii_digit()) | |
| .find_map(|part| part.parse::<u16>().ok()) | |
| .unwrap_or_else(|| PreviewLayout::default().percent) | |
| .clamp(1, 99); | |
| PreviewLayout { placement, percent } | |
| } | |
| /// Runs a yes/no confirmation prompt. | |
| /// | |
| /// Returns `Ok(Some(true))` for Yes, `Ok(Some(false))` for No, and `Ok(None)` | |
| /// if cancelled. | |
| fn prompt_confirm(message: &str, default: Option<bool>) -> Result<Option<bool>> { | |
| let rows = if default == Some(false) { | |
| vec![SelectRow::new("no", "No"), SelectRow::new("yes", "Yes")] | |
| } else { | |
| vec![SelectRow::new("yes", "Yes"), SelectRow::new("no", "No")] | |
| }; | |
| let selected = SelectUiOptions::new(format!("{} ❯ ", message), rows).prompt()?; | |
| Ok(selected.and_then(|row| match row.raw.as_str() { | |
| "yes" => Some(true), | |
| "no" => Some(false), | |
| _ => None, | |
| })) | |
| } | |
| /// Wrapper around [`prompt_confirm`] that safely converts the `bool` result | |
| /// into the generic type `T`. | |
| /// | |
| /// This must only be called when `T` is known to be `bool`. | |
| fn prompt_confirm_as<T: 'static + Clone>( | |
| message: &str, | |
| default: Option<bool>, | |
| ) -> Result<Option<T>> { | |
| let result = prompt_confirm(message, default)?; | |
| Ok(result.and_then(|value| { | |
| let any_value: Box<dyn std::any::Any> = Box::new(value); | |
| any_value.downcast::<T>().ok().map(|boxed| *boxed) | |
| })) | |
| } | |
| mod tests { | |
| use pretty_assertions::assert_eq; | |
| use super::*; | |
| use crate::ForgeWidget; | |
| fn test_select_builder_creates() { | |
| let builder = ForgeWidget::select("Test", vec!["a", "b", "c"]); | |
| assert_eq!(builder.message, "Test"); | |
| assert_eq!(builder.options, vec!["a", "b", "c"]); | |
| } | |
| fn test_confirm_builder_creates() { | |
| let builder = ForgeWidget::confirm("Confirm?"); | |
| assert_eq!(builder.message, "Confirm?"); | |
| } | |
| fn test_select_builder_with_initial_text() { | |
| let builder = | |
| ForgeWidget::select("Test", vec!["apple", "banana", "cherry"]).with_initial_text("app"); | |
| assert_eq!(builder.initial_text, Some("app".to_string())); | |
| } | |
| fn test_select_owned_builder_with_initial_text() { | |
| let builder = | |
| ForgeWidget::select("Test", vec!["apple", "banana", "cherry"]).with_initial_text("ban"); | |
| assert_eq!(builder.initial_text, Some("ban".to_string())); | |
| } | |
| fn test_ansi_stripping() { | |
| let fixture = ["\x1b[1mBold\x1b[0m", "\x1b[31mRed\x1b[0m"]; | |
| let actual: Vec<String> = fixture | |
| .iter() | |
| .map(|value| strip_ansi_codes(value).to_string()) | |
| .collect(); | |
| let expected = vec!["Bold", "Red"]; | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_display_options_are_trimmed() { | |
| let fixture = [ | |
| " openai [empty]", | |
| "✓ anthropic [api.anthropic.com]", | |
| ]; | |
| let actual: Vec<String> = fixture | |
| .iter() | |
| .map(|value| strip_ansi_codes(value).trim().to_string()) | |
| .collect(); | |
| let expected = vec![ | |
| "openai [empty]".to_string(), | |
| "✓ anthropic [api.anthropic.com]".to_string(), | |
| ]; | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_with_starting_cursor() { | |
| let builder = ForgeWidget::select("Test", vec!["a", "b", "c"]).with_starting_cursor(2); | |
| assert_eq!(builder.starting_cursor, Some(2)); | |
| } | |
| fn test_parse_preview_layout_defaults_to_right() { | |
| let fixture = None; | |
| let actual = parse_preview_layout(fixture); | |
| let expected = PreviewLayout { placement: PreviewPlacement::Right, percent: 50 }; | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_parse_preview_layout_supports_bottom_percent() { | |
| let fixture = Some("down,60%"); | |
| let actual = parse_preview_layout(fixture); | |
| let expected = PreviewLayout { placement: PreviewPlacement::Bottom, percent: 60 }; | |
| assert_eq!(actual, expected); | |
| } | |
| } | |