feat: redesign cli ux progress

This commit is contained in:
stoorps 2026-03-20 17:47:32 +00:00
parent ab60ee641f
commit c63b2917da
Signed by: stoorps
SSH key fingerprint: SHA256:AZlPfu9hTu042EGtZElmDQoy+KvMOeShLDan/fYLoNI
21 changed files with 994 additions and 99 deletions

View file

@ -4,7 +4,13 @@ use aim_core::domain::update::UpdateExecutionStatus;
use crate::DispatchResult;
pub fn render_update_summary(total: usize, selected: usize, failed: usize) -> String {
format!("updates found: {total}, selected: {selected}, failed: {failed}",)
[
crate::ui::theme::heading("Update Review"),
format!("apps with updates: {total}"),
format!("selected: {selected}"),
format!("failed: {failed}"),
]
.join("\n")
}
pub fn render_dispatch_result(result: &DispatchResult) -> String {
@ -13,9 +19,7 @@ pub fn render_dispatch_result(result: &DispatchResult) -> String {
DispatchResult::List(rows) => render_list(rows),
DispatchResult::PendingAdd(plan) => render_pending_add(plan),
DispatchResult::Removed(removed) => render_removed_app(removed),
DispatchResult::UpdatePlan(plan) => {
render_update_summary(plan.items.len(), plan.items.len(), 0)
}
DispatchResult::UpdatePlan(plan) => render_update_plan(plan),
DispatchResult::Updated(result) => render_updated_apps(result),
DispatchResult::Noop => String::new(),
}
@ -31,46 +35,68 @@ fn render_added_app(added: &aim_core::app::add::InstalledApp) -> String {
.warnings
.iter()
.chain(added.install_outcome.warnings.iter())
.map(|warning| format!("warning: {warning}"))
.collect::<Vec<_>>()
.join("\n");
.map(|warning| format!("Warning: {warning}"))
.collect::<Vec<_>>();
let summary = format!(
"installing as {scope}\ninstalled app: {} ({})\nsource: {} {}\nselected artifact: {} [{}]",
added.record.display_name,
added.record.stable_id,
added.source.kind.as_str(),
added.source.locator,
added.selected_artifact.url,
added.selected_artifact.selection_reason,
);
let mut lines = vec![
crate::ui::theme::heading("Installation Summary"),
format!(
"{} {} ({})",
crate::ui::theme::label("Application"),
added.record.display_name,
added.record.stable_id,
),
format!("{} {scope}", crate::ui::theme::label("Install scope")),
format!(
"{} {} {}",
crate::ui::theme::label("Source"),
added.source.kind.as_str(),
added.source.locator,
),
format!(
"{} {} [{}]",
crate::ui::theme::label("Selected artifact"),
added.selected_artifact.url,
added.selected_artifact.selection_reason,
),
];
if warning_lines.is_empty() {
summary
} else {
format!("{summary}\n{warning_lines}")
}
lines.extend(warning_lines);
lines.join("\n")
}
fn render_pending_add(plan: &AddPlan) -> String {
let prompts = crate::ui::prompt::render_interactions(&plan.interactions);
format!(
"resolved source: {} {}\nselected artifact: {} [{}]\n{prompts}",
plan.resolution.source.kind.as_str(),
plan.resolution.source.locator,
plan.selected_artifact.url,
plan.selected_artifact.selection_reason,
)
[
crate::ui::theme::heading("Installation Review"),
format!(
"{} {} {}",
crate::ui::theme::label("Resolved source"),
plan.resolution.source.kind.as_str(),
plan.resolution.source.locator,
),
format!(
"{} {} [{}]",
crate::ui::theme::label("Selected artifact"),
plan.selected_artifact.url,
plan.selected_artifact.selection_reason,
),
prompts,
]
.join("\n")
}
fn render_list(rows: &[aim_core::app::list::ListRow]) -> String {
if rows.is_empty() {
return "installed apps: none".to_owned();
return crate::ui::theme::muted("No installed apps yet");
}
let mut output = String::from("installed apps:\n");
let mut output = format!("{}\n", crate::ui::theme::heading("Installed Apps"));
for row in rows {
output.push_str(&format!("- {} ({})\n", row.display_name, row.stable_id));
output.push_str(&format!(
"{}\n",
crate::ui::theme::bullet(&format!("{} ({})", row.display_name, row.stable_id))
));
}
output.trim_end().to_owned()
}
@ -79,36 +105,38 @@ fn render_removed_app(removed: &aim_core::app::remove::RemovalResult) -> String
let warning_lines = removed
.warnings
.iter()
.map(|warning| format!("warning: {warning}"))
.collect::<Vec<_>>()
.join("\n");
let summary = format!("removed: {}", removed.removed.display_name);
if warning_lines.is_empty() {
summary
} else {
format!("{summary}\n{warning_lines}")
}
.map(|warning| format!("Warning: {warning}"))
.collect::<Vec<_>>();
let mut lines = vec![
crate::ui::theme::heading("Removal Summary"),
format!(
"{} {}",
crate::ui::theme::label("Removed app"),
removed.removed.display_name,
),
];
lines.extend(warning_lines);
lines.join("\n")
}
fn render_updated_apps(result: &aim_core::domain::update::UpdateExecutionResult) -> String {
let mut lines = vec![format!(
"updated apps: {}, failed: {}",
result.updated_count(),
result.failed_count()
)];
let mut lines = vec![
crate::ui::theme::heading("Update Summary"),
format!("updated apps: {}", result.updated_count()),
format!("failed updates: {}", result.failed_count()),
];
for item in &result.items {
match &item.status {
UpdateExecutionStatus::Updated => lines.push(format!(
"updated: {} ({}) {} -> {}",
"Updated: {} ({}) {} -> {}",
item.display_name,
item.stable_id,
item.from_version.as_deref().unwrap_or("unknown"),
item.to_version.as_deref().unwrap_or("unknown")
)),
UpdateExecutionStatus::Failed { reason } => lines.push(format!(
"failed: {} ({}) {}",
"Failed: {} ({}) {}",
item.display_name, item.stable_id, reason
)),
}
@ -116,3 +144,16 @@ fn render_updated_apps(result: &aim_core::domain::update::UpdateExecutionResult)
lines.join("\n")
}
fn render_update_plan(plan: &aim_core::domain::update::UpdatePlan) -> String {
let mut lines = vec![render_update_summary(plan.items.len(), plan.items.len(), 0)];
for item in &plan.items {
lines.push(format!(
"{} ({}) via {}",
item.display_name, item.stable_id, item.selected_channel.locator
));
}
lines.join("\n")
}