diff --git a/crates/pixi_manifest/src/task.rs b/crates/pixi_manifest/src/task.rs index e05f27ab6..27ea06dd6 100644 --- a/crates/pixi_manifest/src/task.rs +++ b/crates/pixi_manifest/src/task.rs @@ -38,7 +38,6 @@ impl From for String { task_name.0 // Assuming TaskName is a tuple struct with the first element as String } } - /// Represents different types of scripts #[derive(Debug, Clone, Deserialize)] #[serde(untagged)] @@ -140,7 +139,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(), } } diff --git a/src/cli/run.rs b/src/cli/run.rs index 6524fb834..cb27229f5 100644 --- a/src/cli/run.rs +++ b/src/cli/run.rs @@ -29,11 +29,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)] @@ -100,7 +99,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, args.task)?; + let task_graph = TaskGraph::from_cmd_args( + &project, + &search_environment, + determine_task(&args.task, &project)?, + )?; tracing::info!("Task graph: {}", task_graph); @@ -210,13 +213,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() }; @@ -344,3 +347,60 @@ 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("").to_string())) + .unique() + .sorted() + .collect_vec(); + + // 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 + } 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()? + } +} diff --git a/src/cli/task.rs b/src/cli/task.rs index ac366d313..a84311306 100644 --- a/src/cli/task.rs +++ b/src/cli/task.rs @@ -399,7 +399,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() @@ -407,7 +407,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 9185eec8f..089615b3c 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,25 @@ 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( @@ -415,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/tests/common/mod.rs b/tests/common/mod.rs index c2c862527..d96b771fc 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 f66ed0f58..76724f4ac 100644 --- a/tests/install_tests.rs +++ b/tests/install_tests.rs @@ -43,7 +43,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 @@ -229,7 +229,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() }) @@ -277,7 +277,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()