feat(cli): enhance install and removal UX with progress visibility and theming

- Introduced visible progress stages during installation, including source resolution and artifact selection.
- Improved separation between live transcript output and final summaries, ensuring clarity.
- Removed redundant recap text from installation summaries.
- Centralized terminal styling using a configurable theme system, allowing for warm defaults and user overrides.
- Added support for hex colors and named colors in the configuration.
- Updated tests to verify new behaviors and configurations.
This commit is contained in:
stoorps 2026-03-20 19:44:04 +00:00
parent c63b2917da
commit 9d8ec1e4fd
Signed by: stoorps
SSH key fingerprint: SHA256:AZlPfu9hTu042EGtZElmDQoy+KvMOeShLDan/fYLoNI
17 changed files with 1277 additions and 74 deletions

View file

@ -39,14 +39,10 @@ fn render_added_app(added: &aim_core::app::add::InstalledApp) -> String {
.collect::<Vec<_>>();
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")),
crate::ui::theme::heading(&format!(
"Installed {} ({scope})",
added.record.display_name
)),
format!(
"{} {} {}",
crate::ui::theme::label("Source"),
@ -54,13 +50,22 @@ fn render_added_app(added: &aim_core::app::add::InstalledApp) -> String {
added.source.locator,
),
format!(
"{} {} [{}]",
crate::ui::theme::label("Selected artifact"),
"{} {}",
crate::ui::theme::label("Artifact"),
added.selected_artifact.url,
added.selected_artifact.selection_reason,
),
];
let installed_files = install_file_paths(added);
if !installed_files.is_empty() {
lines.push(crate::ui::theme::label("Installed files"));
lines.extend(
installed_files
.iter()
.map(|path| crate::ui::theme::bullet(path)),
);
}
lines.extend(warning_lines);
lines.join("\n")
}
@ -91,14 +96,65 @@ fn render_list(rows: &[aim_core::app::list::ListRow]) -> String {
return crate::ui::theme::muted("No installed apps yet");
}
let mut output = format!("{}\n", crate::ui::theme::heading("Installed Apps"));
let name_width = rows
.iter()
.map(|row| row.display_name.len())
.max()
.unwrap_or(0)
.max("Name".len());
let version_width = rows
.iter()
.map(|row| row.version.as_deref().unwrap_or("-").len())
.max()
.unwrap_or(0)
.max("Version".len());
let mut lines = vec![crate::ui::theme::heading("Installed Apps")];
lines.push(format_list_row(
"Name",
"Version",
"Source",
name_width,
version_width,
true,
));
for row in rows {
output.push_str(&format!(
"{}\n",
crate::ui::theme::bullet(&format!("{} ({})", row.display_name, row.stable_id))
lines.push(format_list_row(
&row.display_name,
row.version.as_deref().unwrap_or("-"),
&row.source,
name_width,
version_width,
false,
));
}
output.trim_end().to_owned()
lines.join("\n")
}
fn format_list_row(
name: &str,
version: &str,
source: &str,
name_width: usize,
version_width: usize,
is_header: bool,
) -> String {
let row = format!(
"{name:<name_width$} {version:<version_width$} {source}",
name = name,
version = version,
source = source,
name_width = name_width,
version_width = version_width,
);
if is_header {
crate::ui::theme::label(&row)
} else {
row
}
}
fn render_removed_app(removed: &aim_core::app::remove::RemovalResult) -> String {
@ -107,18 +163,50 @@ fn render_removed_app(removed: &aim_core::app::remove::RemovalResult) -> String
.iter()
.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,
),
];
let mut lines = vec![crate::ui::theme::heading(&format!(
"Removed {}",
removed.removed.display_name,
))];
if !removed.removed_paths.is_empty() {
lines.push(crate::ui::theme::label("Removed files"));
lines.extend(
removed
.removed_paths
.iter()
.map(|path| crate::ui::theme::bullet(path)),
);
}
lines.extend(warning_lines);
lines.join("\n")
}
fn install_file_paths(added: &aim_core::app::add::InstalledApp) -> Vec<String> {
[
Some(
added
.install_outcome
.final_payload_path
.display()
.to_string(),
),
added
.install_outcome
.desktop_entry_path
.as_ref()
.map(|path| path.display().to_string()),
added
.install_outcome
.icon_path
.as_ref()
.map(|path| path.display().to_string()),
]
.into_iter()
.flatten()
.collect()
}
fn render_updated_apps(result: &aim_core::domain::update::UpdateExecutionResult) -> String {
let mut lines = vec![
crate::ui::theme::heading("Update Summary"),