use color_print::cformat;
use super::command_executor::PreparedStep;
use super::hook_filter::HookSource;
pub struct SourcedStep {
pub step: PreparedStep,
pub source: HookSource,
pub is_pipeline: bool,
}
pub(crate) fn step_names_from_config(
cfg: &worktrunk::config::CommandConfig,
) -> Vec<Vec<Option<&str>>> {
cfg.steps()
.iter()
.map(|step| match step {
worktrunk::config::HookStep::Single(cmd) => vec![cmd.name.as_deref()],
worktrunk::config::HookStep::Concurrent(cmds) => {
cmds.iter().map(|c| c.name.as_deref()).collect()
}
})
.collect()
}
pub(crate) fn format_pipeline_summary_from_names(
step_names: &[Vec<Option<&str>>],
label_named: impl Fn(&str) -> String,
label_unnamed: impl Fn(usize) -> Option<String>,
) -> String {
let mut parts: Vec<String> = Vec::new();
let mut unnamed_count: usize = 0;
for step in step_names {
let mut named = Vec::new();
for entry in step {
match entry {
Some(name) => named.push(label_named(name)),
None => unnamed_count += 1,
}
}
if !named.is_empty() {
if unnamed_count > 0
&& let Some(s) = label_unnamed(unnamed_count)
{
parts.push(s);
}
unnamed_count = 0;
parts.push(named.join(" & "));
}
}
if unnamed_count > 0
&& let Some(s) = label_unnamed(unnamed_count)
{
parts.push(s);
}
parts.join(", ")
}
pub(crate) fn format_pipeline_summary(steps: &[SourcedStep]) -> String {
let source_label = steps[0].source.to_string();
let step_names: Vec<Vec<Option<&str>>> = steps
.iter()
.map(|step| match &step.step {
PreparedStep::Single(cmd) => vec![cmd.name.as_deref()],
PreparedStep::Concurrent(cmds) => cmds.iter().map(|c| c.name.as_deref()).collect(),
})
.collect();
let total_unnamed: usize = step_names.iter().flatten().filter(|n| n.is_none()).count();
let any_named = step_names.iter().flatten().any(|n| n.is_some());
if !any_named {
return if total_unnamed == 1 {
source_label
} else {
format!("{source_label} ×{total_unnamed}")
};
}
let body = format_pipeline_summary_from_names(
&step_names,
|name| cformat!("<bold>{name}</>"),
|count| {
Some(if count == 1 {
"…".to_string()
} else {
format!("…×{count}")
})
},
);
format!("{body} ({source_label})")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::commands::command_executor::PreparedCommand;
use ansi_str::AnsiStr;
use insta::assert_snapshot;
fn make_sourced_step(step: PreparedStep) -> SourcedStep {
SourcedStep {
step,
source: HookSource::User,
is_pipeline: false,
}
}
fn make_cmd(name: Option<&str>, expanded: &str) -> PreparedCommand {
let label = match name {
Some(n) => format!("user:{n}"),
None => "user".to_string(),
};
PreparedCommand {
name: name.map(String::from),
expanded: expanded.to_string(),
context_json: "{}".to_string(),
lazy_template: None,
label,
log_label: None,
}
}
#[test]
fn test_format_pipeline_summary_named() {
let steps = vec![
make_sourced_step(PreparedStep::Single(make_cmd(
Some("install"),
"npm install",
))),
make_sourced_step(PreparedStep::Concurrent(vec![
make_cmd(Some("build"), "npm run build"),
make_cmd(Some("lint"), "npm run lint"),
])),
];
let summary = format_pipeline_summary(&steps);
assert_snapshot!(summary.ansi_strip(), @"install, build & lint (user)");
}
#[test]
fn test_format_pipeline_summary_unnamed() {
let steps = vec![
make_sourced_step(PreparedStep::Single(make_cmd(None, "npm install"))),
make_sourced_step(PreparedStep::Single(make_cmd(None, "npm run build"))),
];
let summary = format_pipeline_summary(&steps);
assert_snapshot!(summary.ansi_strip(), @"user ×2");
}
#[test]
fn test_format_pipeline_summary_mixed_named_unnamed() {
let steps = vec![
make_sourced_step(PreparedStep::Single(make_cmd(None, "npm install"))),
make_sourced_step(PreparedStep::Single(make_cmd(Some("bg"), "npm run dev"))),
];
let summary = format_pipeline_summary(&steps);
assert_snapshot!(summary.ansi_strip(), @"…, bg (user)");
}
#[test]
fn test_format_pipeline_summary_single_unnamed() {
let steps = vec![make_sourced_step(PreparedStep::Single(make_cmd(
None,
"npm install",
)))];
let summary = format_pipeline_summary(&steps);
assert_snapshot!(summary.ansi_strip(), @"user");
}
#[test]
fn test_format_pipeline_summary_concurrent_then_concurrent() {
let steps = vec![
make_sourced_step(PreparedStep::Concurrent(vec![
make_cmd(Some("install"), "npm install"),
make_cmd(Some("setup"), "setup-db"),
])),
make_sourced_step(PreparedStep::Concurrent(vec![
make_cmd(Some("build"), "npm run build"),
make_cmd(Some("lint"), "npm run lint"),
])),
];
let summary = format_pipeline_summary(&steps);
assert_snapshot!(summary.ansi_strip(), @"install & setup, build & lint (user)");
}
}