From 59e8481326f58c326bdbd73f078570d25ed55975 Mon Sep 17 00:00:00 2001 From: Ruben Arts Date: Mon, 22 Jul 2024 15:42:40 +0200 Subject: [PATCH 1/3] feat: selection dialog when running pixi run without arguments --- src/cli/run.rs | 54 +++++++++++++++++++++++++++++++++----- src/cli/task.rs | 4 +-- src/project/environment.rs | 22 ++++++++++++++-- src/task/mod.rs | 10 +++++-- 4 files changed, 78 insertions(+), 12 deletions(-) diff --git a/src/cli/run.rs b/src/cli/run.rs index 63352b0ee..d214f8096 100644 --- a/src/cli/run.rs +++ b/src/cli/run.rs @@ -27,11 +27,10 @@ use tracing::Level; /// Runs task in project. #[derive(Parser, Debug, Default)] -#[clap(trailing_var_arg = true, arg_required_else_help = true)] +#[clap(trailing_var_arg = true)] pub struct Args { /// The pixi task or a task shell command you want to run in the project's environment, which can be an executable in the environment's PATH. - #[arg(required = true)] - pub task: Vec, + pub task: Option>, /// The path to 'pixi.toml' or 'pyproject.toml' #[arg(long)] @@ -98,7 +97,7 @@ pub async fn execute(args: Args) -> miette::Result<()> { ) .with_disambiguate_fn(disambiguate_task_interactive); - let task_graph = TaskGraph::from_cmd_args(&project, &search_environment, args.task)?; + let task_graph = TaskGraph::from_cmd_args(&project, &search_environment, determine_task(&args.task, &project)?)?; tracing::info!("Task graph: {}", task_graph); @@ -208,13 +207,13 @@ pub async fn execute(args: Args) -> miette::Result<()> { fn command_not_found<'p>(project: &'p Project, explicit_environment: Option>) { let available_tasks: HashSet = if let Some(explicit_environment) = explicit_environment { - explicit_environment.get_filtered_tasks() + explicit_environment.get_filtered_task_names() } else { project .environments() .into_iter() .filter(|env| verify_current_platform_has_required_virtual_packages(env).is_ok()) - .flat_map(|env| env.get_filtered_tasks()) + .flat_map(|env| env.get_filtered_task_names()) .collect() }; @@ -342,3 +341,46 @@ fn disambiguate_task_interactive<'p>( .map_or(None, identity) .map(|idx| problem.environments[idx].clone()) } + +/// Function to determine the task to run, spawn a dialog if no task is provided. +fn determine_task(task: &Option>, project: &Project) -> miette::Result> { + if let Some(task) = task { + Ok(task.clone()) + } else { + let theme = ColorfulTheme { + active_item_style: console::Style::new().for_stderr().magenta(), + ..ColorfulTheme::default() + }; + let runnable_tasks: Vec<(TaskName, String)> = project + .environments() + .into_iter() + .filter(|env| verify_current_platform_has_required_virtual_packages(env).is_ok()) + .flat_map(|env| env.get_filtered_tasks()) + .map(|(task_name, task)| (task_name, task.description().unwrap_or_else(|| "").to_string())) + .unique() + .sorted() + .collect_vec(); + + // Get longest task name for padding if there is any description + let width = if runnable_tasks.iter().any(|(_, desc)| !desc.is_empty()) { + runnable_tasks.iter().map(|(name, _)| name.fancy_display().to_string().len()).max().unwrap_or(0) + 2 + } else { + 0 + }; + + // Format the items + let formatted_items = runnable_tasks.iter() + .map(|(name, desc)| format!("{:>(); + + dialoguer::Select::with_theme(&theme) + .with_prompt("Please select a task to run:") + .items(&formatted_items) + .default(0) + .interact_opt() + .map(|idx_opt| match idx_opt { + Some(idx) => Ok(vec![runnable_tasks[idx].clone().0.to_string()]), + None => Err(miette::miette!("Task selection cancelled by user.")) + }).into_diagnostic()? + } +} \ No newline at end of file diff --git a/src/cli/task.rs b/src/cli/task.rs index 94937f868..9d87d19e5 100644 --- a/src/cli/task.rs +++ b/src/cli/task.rs @@ -400,7 +400,7 @@ pub fn execute(args: Args) -> miette::Result<()> { .transpose()?; let available_tasks: HashSet = if let Some(explicit_environment) = explicit_environment { - explicit_environment.get_filtered_tasks() + explicit_environment.get_filtered_task_names() } else { project .environments() @@ -408,7 +408,7 @@ pub fn execute(args: Args) -> miette::Result<()> { .filter(|env| { verify_current_platform_has_required_virtual_packages(env).is_ok() }) - .flat_map(|env| env.get_filtered_tasks()) + .flat_map(|env| env.get_filtered_task_names()) .collect() }; diff --git a/src/project/environment.rs b/src/project/environment.rs index 804a61a95..e948ee49c 100644 --- a/src/project/environment.rs +++ b/src/project/environment.rs @@ -168,7 +168,7 @@ impl<'p> Environment<'p> { /// Return all tasks available for the given environment /// This will not return task prefixed with _ - pub fn get_filtered_tasks(&self) -> HashSet { + pub fn get_filtered_task_names(&self) -> HashSet { self.tasks(Some(self.best_platform())) .into_iter() .flat_map(|tasks| { @@ -183,6 +183,24 @@ impl<'p> Environment<'p> { .map(ToOwned::to_owned) .collect() } + + /// Return all tasks available for the given environment + /// This will not return task prefixed with _ + pub fn get_filtered_tasks(&self) -> HashMap { + self.tasks(Some(self.best_platform())) + .into_iter() + .flat_map(|tasks| { + tasks.into_iter().filter_map(|(key, value)| { + if !key.as_str().starts_with('_') { + Some((key, value)) + } else { + None + } + }) + }) + .map(|(key, value)| (key.to_owned(), value.clone())) + .collect() + } /// Returns the task with the given `name` and for the specified `platform` or an `UnknownTask` /// which explains why the task was not available. pub fn task( @@ -416,7 +434,7 @@ mod tests { ) .unwrap(); - let task = manifest.default_environment().get_filtered_tasks(); + let task = manifest.default_environment().get_filtered_task_names(); assert_eq!(task.len(), 1); assert!(task.contains(&"foo".into())); diff --git a/src/task/mod.rs b/src/task/mod.rs index af16e060c..d36876dda 100644 --- a/src/task/mod.rs +++ b/src/task/mod.rs @@ -3,6 +3,7 @@ use itertools::Itertools; use serde::{Deserialize, Serialize}; use serde_with::{formats::PreferMany, serde_as, OneOrMany}; use std::borrow::Cow; +use std::fmt; use std::fmt::{Display, Formatter}; use std::path::{Path, PathBuf}; @@ -51,7 +52,12 @@ impl From for TaskName { } impl From for String { fn from(task_name: TaskName) -> Self { - task_name.0 // Assuming TaskName is a tuple struct with the first element as String + task_name.0 + } +} +impl fmt::Display for TaskName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) } } @@ -156,7 +162,7 @@ impl Task { Task::Plain(_) => None, Task::Custom(_) => None, Task::Execute(exe) => exe.description.as_deref(), - Task::Alias(_) => None, + Task::Alias(alias) => alias.description.as_deref(), } } From 4f3e28bffe6d001e51b3b10123af8eae83ef950b Mon Sep 17 00:00:00 2001 From: Ruben Arts Date: Tue, 23 Jul 2024 08:06:29 +0200 Subject: [PATCH 2/3] fmt --- src/cli/run.rs | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/src/cli/run.rs b/src/cli/run.rs index d214f8096..c9e3734b3 100644 --- a/src/cli/run.rs +++ b/src/cli/run.rs @@ -97,7 +97,11 @@ pub async fn execute(args: Args) -> miette::Result<()> { ) .with_disambiguate_fn(disambiguate_task_interactive); - let task_graph = TaskGraph::from_cmd_args(&project, &search_environment, determine_task(&args.task, &project)?)?; + let task_graph = TaskGraph::from_cmd_args( + &project, + &search_environment, + determine_task(&args.task, &project)?, + )?; tracing::info!("Task graph: {}", task_graph); @@ -356,21 +360,34 @@ fn determine_task(task: &Option>, project: &Project) -> miette::Resu .into_iter() .filter(|env| verify_current_platform_has_required_virtual_packages(env).is_ok()) .flat_map(|env| env.get_filtered_tasks()) - .map(|(task_name, task)| (task_name, task.description().unwrap_or_else(|| "").to_string())) + .map(|(task_name, task)| (task_name, task.description().unwrap_or("").to_string())) .unique() .sorted() .collect_vec(); - // Get longest task name for padding if there is any description + // Get the longest task name for padding if there is any description let width = if runnable_tasks.iter().any(|(_, desc)| !desc.is_empty()) { - runnable_tasks.iter().map(|(name, _)| name.fancy_display().to_string().len()).max().unwrap_or(0) + 2 + runnable_tasks + .iter() + .map(|(name, _)| name.fancy_display().to_string().len()) + .max() + .unwrap_or(0) + + 2 } else { 0 }; // Format the items - let formatted_items = runnable_tasks.iter() - .map(|(name, desc)| format!("{:>(); dialoguer::Select::with_theme(&theme) @@ -380,7 +397,8 @@ fn determine_task(task: &Option>, project: &Project) -> miette::Resu .interact_opt() .map(|idx_opt| match idx_opt { Some(idx) => Ok(vec![runnable_tasks[idx].clone().0.to_string()]), - None => Err(miette::miette!("Task selection cancelled by user.")) - }).into_diagnostic()? + None => Err(miette::miette!("Task selection cancelled by user.")), + }) + .into_diagnostic()? } -} \ No newline at end of file +} From 7164b4a92b5612eb126997242de85a00f14a6462 Mon Sep 17 00:00:00 2001 From: Ruben Arts Date: Tue, 23 Jul 2024 16:06:49 +0200 Subject: [PATCH 3/3] fix: tests --- tests/common/mod.rs | 9 +++++++-- tests/install_tests.rs | 6 +++--- tests/task_tests.rs | 12 ++++++------ 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 2cdcf9eb3..4350a316b 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -323,8 +323,13 @@ impl PixiControl { .map(|e| e.best_platform()) .or(Some(Platform::current())), ); - let task_graph = TaskGraph::from_cmd_args(&project, &search_env, args.task) - .map_err(RunError::TaskGraphError)?; + let task_graph = TaskGraph::from_cmd_args( + &project, + &search_env, + args.task + .expect("expected a task, as tests don't run the dialog"), + ) + .map_err(RunError::TaskGraphError)?; // Iterate over all tasks in the graph and execute them. let mut task_env = None; diff --git a/tests/install_tests.rs b/tests/install_tests.rs index f00403575..d6c456643 100644 --- a/tests/install_tests.rs +++ b/tests/install_tests.rs @@ -46,7 +46,7 @@ async fn install_run_python() { // Check if python is installed and can be run let result = pixi .run(run::Args { - task: string_from_iter(["python", "--version"]), + task: Some(string_from_iter(["python", "--version"])), ..Default::default() }) .await @@ -216,7 +216,7 @@ async fn install_locked_with_config() { let result = pixi .run(Args { - task: vec!["which_python".to_string()], + task: Some(vec!["which_python".to_string()]), manifest_path: None, ..Default::default() }) @@ -264,7 +264,7 @@ async fn install_frozen() { frozen: true, ..Default::default() }, - task: string_from_iter(["python", "--version"]), + task: Some(string_from_iter(["python", "--version"])), ..Default::default() }) .await diff --git a/tests/task_tests.rs b/tests/task_tests.rs index f85aff963..a5eaa4b09 100644 --- a/tests/task_tests.rs +++ b/tests/task_tests.rs @@ -110,7 +110,7 @@ async fn test_alias() { let result = pixi .run(Args { - task: vec!["helloworld".to_string()], + task: Some(vec!["helloworld".to_string()]), manifest_path: None, ..Default::default() }) @@ -184,7 +184,7 @@ async fn test_cwd() { let result = pixi .run(Args { - task: vec!["pwd-test".to_string()], + task: Some(vec!["pwd-test".to_string()]), manifest_path: None, ..Default::default() }) @@ -204,7 +204,7 @@ async fn test_cwd() { assert!(pixi .run(Args { - task: vec!["unknown-cwd".to_string()], + task: Some(vec!["unknown-cwd".to_string()]), manifest_path: None, ..Default::default() }) @@ -229,7 +229,7 @@ async fn test_task_with_env() { let result = pixi .run(Args { - task: vec!["env-test".to_string()], + task: Some(vec!["env-test".to_string()]), manifest_path: None, ..Default::default() }) @@ -253,7 +253,7 @@ async fn test_clean_env() { .unwrap(); let run = pixi.run(Args { - task: vec!["env-test".to_string()], + task: Some(vec!["env-test".to_string()]), manifest_path: None, clean_env: true, ..Default::default() @@ -270,7 +270,7 @@ async fn test_clean_env() { let result = pixi .run(Args { - task: vec!["env-test".to_string()], + task: Some(vec!["env-test".to_string()]), manifest_path: None, clean_env: false, ..Default::default()