//! Interactive project selection using inquire //! //! Provides a terminal UI for selecting projects when auto-detection is not possible. //! Uses arrow keys for navigation (requires "interactive" feature). //! //! # Features //! Requires the "interactive" feature to be enabled: //! //! ```toml //! [dependencies] //! tools-shared = { version = "0.1", features = ["interactive"] } //! ``` //! //! # Examples //! ```no_run //! use tools_shared::{select_project, SelectableProject}; //! //! let projects = vec![ //! SelectableProject { //! id: "my-project".to_string(), //! name: "My Project".to_string(), //! path: Some("/Users/akasha/my-project".to_string()), //! metadata: Some("Phase: Devel".to_string()), //! }, //! ]; //! //! let selected = select_project(projects, "Select a project:", None)?; //! println!("Selected: {}", selected); //! # Ok::<(), Box>(()) //! ``` /// Project information for interactive selection #[derive(Debug, Clone)] pub struct SelectableProject { /// Project ID (used internally) pub id: String, /// Display name pub name: String, /// Optional project path pub path: Option, /// Optional metadata (e.g., "Phase: Devel") pub metadata: Option, } #[cfg(feature = "interactive")] /// Show interactive project selector using inquire /// /// Displays projects with arrow key navigation and returns selected project ID. /// /// # Arguments /// * `projects` - List of selectable projects /// * `prompt` - Prompt text to display /// * `default_id` - Optional default project ID (pre-selected) /// /// # Returns /// Selected project ID /// /// # Errors /// Returns error if user cancels selection or inquire fails /// /// # Examples /// ```no_run /// use tools_shared::{select_project, SelectableProject}; /// /// let projects = vec![ /// SelectableProject { /// id: "proj1".to_string(), /// name: "Project 1".to_string(), /// path: Some("/path/to/proj1".to_string()), /// metadata: None, /// }, /// ]; /// /// let selected = select_project(projects, "Choose project:", None)?; /// # Ok::<(), Box>(()) /// ``` pub fn select_project( projects: Vec, prompt: &str, default_id: Option<&str>, ) -> Result> { use inquire::Select; if projects.is_empty() { return Err("No projects available".into()); } // Format options for display let options: Vec = projects.iter().map(format_project_option).collect(); // Find default index let default_index = default_id .and_then(|id| projects.iter().position(|p| p.id == id)) .unwrap_or(0); // Show selector let selection = Select::new(prompt, options) .with_starting_cursor(default_index) .prompt()?; // Find and return the ID of selected project let selected_name = selection.split(" • ").next().unwrap_or(&selection); let selected_project = projects .iter() .find(|p| p.name == selected_name) .ok_or("Project not found")?; Ok(selected_project.id.clone()) } /// Format project for display in selector /// /// Creates a readable option string with project name, path, and metadata. /// /// # Examples /// ``` /// use tools_shared::{format_project_option, SelectableProject}; /// /// let project = SelectableProject { /// id: "proj1".to_string(), /// name: "My Project".to_string(), /// path: Some("/home/user/my-project".to_string()), /// metadata: Some("Phase: Devel".to_string()), /// }; /// /// let formatted = format_project_option(&project); /// assert!(formatted.contains("My Project")); /// ``` pub fn format_project_option(project: &SelectableProject) -> String { let mut parts = vec![project.name.clone()]; if let Some(ref path) = project.path { parts.push(format!("({})", path)); } if let Some(ref metadata) = project.metadata { parts.push(metadata.clone()); } parts.join(" • ") } #[cfg(test)] mod tests { use super::*; #[test] fn test_format_project_option_full() { let project = SelectableProject { id: "proj1".to_string(), name: "My Project".to_string(), path: Some("/home/user/project".to_string()), metadata: Some("Phase: Devel".to_string()), }; let formatted = format_project_option(&project); assert!(formatted.contains("My Project")); assert!(formatted.contains("/home/user/project")); assert!(formatted.contains("Phase: Devel")); assert!(formatted.contains("•")); } #[test] fn test_format_project_option_minimal() { let project = SelectableProject { id: "proj1".to_string(), name: "My Project".to_string(), path: None, metadata: None, }; let formatted = format_project_option(&project); assert_eq!(formatted, "My Project"); } #[test] fn test_format_project_option_with_metadata() { let project = SelectableProject { id: "proj1".to_string(), name: "My Project".to_string(), path: None, metadata: Some("Status: Active".to_string()), }; let formatted = format_project_option(&project); assert!(formatted.contains("My Project")); assert!(formatted.contains("Status: Active")); } #[test] #[cfg(feature = "interactive")] fn test_select_project_error_on_empty() { let result = select_project(vec![], "Select project:", None); assert!(result.is_err()); } }