diff --git a/crates/pixi/tests/integration_rust/common/mod.rs b/crates/pixi/tests/integration_rust/common/mod.rs index 774946e3fe..a5c49ca000 100644 --- a/crates/pixi/tests/integration_rust/common/mod.rs +++ b/crates/pixi/tests/integration_rust/common/mod.rs @@ -658,6 +658,7 @@ impl PixiControl { dry_run: false, specs: Default::default(), json: false, + interactive: false, }, } } diff --git a/crates/pixi_cli/src/update.rs b/crates/pixi_cli/src/update.rs index 70a64ed8ca..00ad8464be 100644 --- a/crates/pixi_cli/src/update.rs +++ b/crates/pixi_cli/src/update.rs @@ -1,6 +1,7 @@ use std::{cmp::Ordering, collections::HashSet}; use clap::Parser; +use dialoguer::{MultiSelect, theme::ColorfulTheme}; use fancy_display::FancyDisplay; use itertools::Itertools; use miette::{Context, IntoDiagnostic, MietteDiagnostic}; @@ -44,6 +45,10 @@ pub struct Args { /// Output the changes in JSON format. #[clap(long)] pub json: bool, + + /// Run in interactive mode + #[clap(short = 'i', long)] + pub interactive: bool, } #[derive(Parser, Debug, Default)] @@ -130,7 +135,7 @@ pub async fn execute(args: Args) -> miette::Result<()> { .locate()? .with_cli_config(config); - let specs = UpdateSpecs::from(args.specs); + let mut specs = UpdateSpecs::from(args.specs); // If the user specified an environment name, check to see if it exists. if let Some(env) = &specs.environments { @@ -151,6 +156,56 @@ pub async fn execute(args: Args) -> miette::Result<()> { .await? .into_lock_file_or_empty_with_warning(); + // If interactive mode is requested and no packages were explicitly + // specified, prompt the user to choose which packages to update. + if args.interactive && specs.packages.is_none() { + // Collect unique package names and current versions from the lock-file. + let packages: Vec<(String, String)> = loaded_lock_file + .environments() + .flat_map(|(_, env)| { + env.packages_by_platform() + .flat_map(|(_, packages)| { + packages.into_iter().map(|p| match p { + LockedPackageRef::Conda(ver) => { + (p.name().to_string(), ver.record().version.to_string()) + } + LockedPackageRef::Pypi(ver, _) => { + (p.name().to_string(), ver.version.to_string()) + } + }) + }) + .collect::>() + }) + .unique() + .sorted() + .collect(); + let package_items: Vec = packages + .iter() + .map(|(name, version)| format!("{name} ({version})")) + .collect(); + if !packages.is_empty() { + let theme = ColorfulTheme { + active_item_style: console::Style::new().for_stderr().magenta(), + ..ColorfulTheme::default() + }; + + let prompt = "Select packages to update (space to select, enter to confirm):"; + let selections = MultiSelect::with_theme(&theme) + .with_prompt(prompt) + .items(&package_items) + .interact() + .expect("Failed to load packages."); + + if !selections.is_empty() { + let selected: HashSet = selections + .into_iter() + .map(|i| packages[i].0.clone()) + .collect(); + specs.packages = Some(selected); + } + } + } + // If the user specified a package name, check to see if it is even locked. if let Some(packages) = &specs.packages { for package in packages {