From b1fe6527b6656adebb48f12e1ec4f5829d6fc3f9 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 11:59:25 +0000 Subject: [PATCH] feat(cli): enhance interactive file selection UX with validation and tilde expansion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add `✏️ Enter custom path...` option to `select_playbook` - Implement path existence validation for both inventory and playbook custom inputs - Use `shellexpand::tilde` to support `~/` home directory expansion in interactive inputs - Improve "No playbooks found" UX by prompting for manual entry instead of returning immediately - Update imports in `src/cli/interactive.rs` to include `Path` Co-authored-by: dolagoartur <146357947+dolagoartur@users.noreply.github.com> --- src/cli/interactive.rs | 76 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 70 insertions(+), 6 deletions(-) diff --git a/src/cli/interactive.rs b/src/cli/interactive.rs index 93953375..71af980a 100644 --- a/src/cli/interactive.rs +++ b/src/cli/interactive.rs @@ -6,7 +6,7 @@ use anyhow::{Context, Result}; use colored::Colorize; use console::Term; use dialoguer::{theme::ColorfulTheme, Confirm, Input, MultiSelect, Select}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; /// Interactive session state pub struct InteractiveSession { @@ -101,14 +101,31 @@ impl InteractiveSession { /// Prompt for playbook selection pub fn select_playbook(&self, playbooks: &[PathBuf]) -> Result> { if playbooks.is_empty() { - println!("{}", "No playbooks found in current directory.".yellow()); - return Ok(None); + println!( + "{}", + "No playbooks found. Please enter path manually.".yellow() + ); + let custom: String = Input::with_theme(&self.theme) + .with_prompt("✏️ Enter playbook path") + .validate_with(|input: &String| -> Result<(), &str> { + let expanded = shellexpand::tilde(input); + let path = Path::new(expanded.as_ref()); + if path.exists() { + Ok(()) + } else { + Err("Path does not exist") + } + }) + .interact_on(&self.term)?; + let expanded = shellexpand::tilde(&custom); + return Ok(Some(PathBuf::from(expanded.as_ref()))); } - let items: Vec = playbooks + let mut items: Vec = playbooks .iter() .map(|p| format!("📖 {}", p.display())) .collect(); + items.push("✏️ Enter custom path...".to_string()); let selection = Select::with_theme(&self.theme) .with_prompt("📖 Select a playbook") @@ -116,6 +133,23 @@ impl InteractiveSession { .default(0) .interact_on(&self.term)?; + if selection == items.len() - 1 { + let custom: String = Input::with_theme(&self.theme) + .with_prompt("✏️ Enter playbook path") + .validate_with(|input: &String| -> Result<(), &str> { + let expanded = shellexpand::tilde(input); + let path = Path::new(expanded.as_ref()); + if path.exists() { + Ok(()) + } else { + Err("Path does not exist") + } + }) + .interact_on(&self.term)?; + let expanded = shellexpand::tilde(&custom); + return Ok(Some(PathBuf::from(expanded.as_ref()))); + } + Ok(Some(playbooks[selection].clone())) } @@ -125,12 +159,25 @@ impl InteractiveSession { let custom: String = Input::with_theme(&self.theme) .with_prompt("✏️ Enter inventory path (or 'localhost' for local)") .default("localhost".to_string()) + .validate_with(|input: &String| -> Result<(), &str> { + if input == "localhost" { + return Ok(()); + } + let expanded = shellexpand::tilde(input); + let path = Path::new(expanded.as_ref()); + if path.exists() { + Ok(()) + } else { + Err("Path does not exist") + } + }) .interact_on(&self.term)?; if custom == "localhost" { return Ok(None); } - return Ok(Some(PathBuf::from(custom))); + let expanded = shellexpand::tilde(&custom); + return Ok(Some(PathBuf::from(expanded.as_ref()))); } let mut items: Vec = inventories @@ -153,8 +200,25 @@ impl InteractiveSession { if selection == items.len() - 2 { let custom: String = Input::with_theme(&self.theme) .with_prompt("✏️ Enter inventory path") + .validate_with(|input: &String| -> Result<(), &str> { + if input == "localhost" { + return Ok(()); + } + let expanded = shellexpand::tilde(input); + let path = Path::new(expanded.as_ref()); + if path.exists() { + Ok(()) + } else { + Err("Path does not exist") + } + }) .interact_on(&self.term)?; - return Ok(Some(PathBuf::from(custom))); + + if custom == "localhost" { + return Ok(None); + } + let expanded = shellexpand::tilde(&custom); + return Ok(Some(PathBuf::from(expanded.as_ref()))); } Ok(Some(inventories[selection].clone()))