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

@ -1,4 +1,4 @@
use aim_core::app::add::{build_add_plan_with, install_app_with_reporter};
use aim_core::app::add::{build_add_plan_with_reporter, install_app_with_reporter};
use aim_core::app::progress::{OperationEvent, OperationStage};
use aim_core::domain::app::InstallScope;
use aim_core::integration::install::{DesktopIntegrationRequest, InstallRequest, execute_install};
@ -133,7 +133,6 @@ fn install_extracts_icon_from_appimage_payload_when_icon_path_is_requested() {
#[test]
fn install_app_reports_operation_stages_in_order() {
let root = tempdir().unwrap();
let plan = build_add_plan_with("sharkdp/bat", &FixtureGitHubTransport).unwrap();
let mut events: Vec<OperationEvent> = Vec::new();
unsafe {
@ -142,6 +141,9 @@ fn install_app_reports_operation_stages_in_order() {
let mut reporter = |event: &OperationEvent| events.push(event.clone());
let plan = build_add_plan_with_reporter("sharkdp/bat", &FixtureGitHubTransport, &mut reporter)
.unwrap();
let installed = install_app_with_reporter(
"sharkdp/bat",
&plan,
@ -152,6 +154,18 @@ fn install_app_reports_operation_stages_in_order() {
.unwrap();
assert_eq!(installed.record.stable_id, "sharkdp-bat");
assert!(events.contains(&OperationEvent::StageChanged {
stage: OperationStage::ResolveQuery,
message: "resolving source".to_owned(),
}));
assert!(events.contains(&OperationEvent::StageChanged {
stage: OperationStage::DiscoverRelease,
message: "discovering release".to_owned(),
}));
assert!(events.contains(&OperationEvent::StageChanged {
stage: OperationStage::SelectArtifact,
message: "selecting artifact".to_owned(),
}));
assert!(events.contains(&OperationEvent::StageChanged {
stage: OperationStage::DownloadArtifact,
message: "downloading artifact".to_owned(),
@ -182,4 +196,33 @@ fn install_app_reports_operation_stages_in_order() {
}
)
}));
let stage_order = events
.iter()
.filter_map(|event| match event {
OperationEvent::StageChanged { stage, .. } => Some(*stage),
_ => None,
})
.collect::<Vec<_>>();
assert!(stage_order.windows(2).any(|window| {
window
== [
OperationStage::ResolveQuery,
OperationStage::DiscoverRelease,
]
}));
assert!(stage_order.windows(2).any(|window| {
window
== [
OperationStage::DiscoverRelease,
OperationStage::SelectArtifact,
]
}));
assert!(stage_order.windows(2).any(|window| {
window
== [
OperationStage::SelectArtifact,
OperationStage::DownloadArtifact,
]
}));
}

View file

@ -5,6 +5,7 @@ use aim_core::app::remove::{
build_removal_plan, remove_registered_app_with_reporter, resolve_registered_app,
};
use aim_core::domain::app::{AppRecord, InstallMetadata, InstallScope};
use aim_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceKind, SourceRef};
use std::path::Path;
use tempfile::tempdir;
@ -20,9 +21,18 @@ fn list_flow_returns_display_rows_for_registered_apps() {
let rows = build_list_rows(&[AppRecord {
stable_id: "bat".to_owned(),
display_name: "Bat".to_owned(),
source_input: None,
source: None,
installed_version: None,
source_input: Some("sharkdp/bat".to_owned()),
source: Some(SourceRef {
kind: SourceKind::GitHub,
input_kind: SourceInputKind::RepoShorthand,
normalized_kind: NormalizedSourceKind::GitHubRepository,
locator: "sharkdp/bat".to_owned(),
canonical_locator: Some("sharkdp/bat".to_owned()),
requested_tag: None,
requested_asset_name: None,
tracks_latest: true,
}),
installed_version: Some("0.25.0".to_owned()),
update_strategy: None,
metadata: Vec::new(),
install: None,
@ -31,6 +41,8 @@ fn list_flow_returns_display_rows_for_registered_apps() {
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].stable_id, "bat");
assert_eq!(rows[0].display_name, "Bat");
assert_eq!(rows[0].version.as_deref(), Some("0.25.0"));
assert_eq!(rows[0].source, "sharkdp/bat");
}
#[test]
@ -162,6 +174,7 @@ fn remove_flow_reports_resolution_and_cleanup_events() {
.unwrap();
assert_eq!(result.removed.stable_id, "bat");
assert_eq!(result.removed_paths.len(), 0);
assert!(events.iter().any(|event| {
matches!(
event,