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:
parent
c63b2917da
commit
9d8ec1e4fd
17 changed files with 1277 additions and 74 deletions
|
|
@ -27,19 +27,37 @@ const FIXTURE_MODE_ENV: &str = "AIM_GITHUB_FIXTURE_MODE";
|
|||
|
||||
pub fn build_add_plan(query: &str) -> Result<AddPlan, BuildAddPlanError> {
|
||||
let transport = crate::source::github::default_transport();
|
||||
build_add_plan_with(query, transport.as_ref())
|
||||
let mut reporter = NoopReporter;
|
||||
build_add_plan_with_reporter(query, transport.as_ref(), &mut reporter)
|
||||
}
|
||||
|
||||
pub fn build_add_plan_with<T: GitHubTransport + ?Sized>(
|
||||
query: &str,
|
||||
transport: &T,
|
||||
) -> Result<AddPlan, BuildAddPlanError> {
|
||||
let mut reporter = NoopReporter;
|
||||
build_add_plan_with_reporter(query, transport, &mut reporter)
|
||||
}
|
||||
|
||||
pub fn build_add_plan_with_reporter<T: GitHubTransport + ?Sized>(
|
||||
query: &str,
|
||||
transport: &T,
|
||||
reporter: &mut impl ProgressReporter,
|
||||
) -> Result<AddPlan, BuildAddPlanError> {
|
||||
reporter.report(&OperationEvent::StageChanged {
|
||||
stage: OperationStage::ResolveQuery,
|
||||
message: "resolving source".to_owned(),
|
||||
});
|
||||
let source = resolve_query(query).map_err(BuildAddPlanError::Query)?;
|
||||
|
||||
let mut interactions = Vec::new();
|
||||
let mut parsed_metadata = Vec::new();
|
||||
let (resolution, selected_artifact, update_strategy) = match source.kind {
|
||||
SourceKind::GitHub => {
|
||||
reporter.report(&OperationEvent::StageChanged {
|
||||
stage: OperationStage::DiscoverRelease,
|
||||
message: "discovering release".to_owned(),
|
||||
});
|
||||
let discovery = discover_github_candidates_with(&source, transport)
|
||||
.map_err(BuildAddPlanError::GitHubDiscovery)?;
|
||||
for document in &discovery.metadata_documents {
|
||||
|
|
@ -60,6 +78,10 @@ pub fn build_add_plan_with<T: GitHubTransport + ?Sized>(
|
|||
.iter()
|
||||
.find(|item| item.hints.primary_download.is_some())
|
||||
.map(|item| &item.hints);
|
||||
reporter.report(&OperationEvent::StageChanged {
|
||||
stage: OperationStage::SelectArtifact,
|
||||
message: "selecting artifact".to_owned(),
|
||||
});
|
||||
let artifact = select_artifact(&preferred, metadata_hints);
|
||||
|
||||
if discovery.requested_is_older_release {
|
||||
|
|
@ -89,6 +111,10 @@ pub fn build_add_plan_with<T: GitHubTransport + ?Sized>(
|
|||
)
|
||||
}
|
||||
_ => {
|
||||
reporter.report(&OperationEvent::StageChanged {
|
||||
stage: OperationStage::SelectArtifact,
|
||||
message: "selecting artifact".to_owned(),
|
||||
});
|
||||
let resolution = AdapterResolution {
|
||||
source: source.clone(),
|
||||
release: ResolvedRelease {
|
||||
|
|
@ -249,17 +275,6 @@ pub fn install_app_with_reporter(
|
|||
)),
|
||||
};
|
||||
|
||||
if desktop_owned.is_some() {
|
||||
reporter.report(&OperationEvent::StageChanged {
|
||||
stage: OperationStage::WriteDesktopEntry,
|
||||
message: "writing desktop entry".to_owned(),
|
||||
});
|
||||
reporter.report(&OperationEvent::StageChanged {
|
||||
stage: OperationStage::ExtractIcon,
|
||||
message: "extracting icon".to_owned(),
|
||||
});
|
||||
}
|
||||
|
||||
reporter.report(&OperationEvent::StageChanged {
|
||||
stage: OperationStage::StagePayload,
|
||||
message: "staging payload".to_owned(),
|
||||
|
|
@ -280,6 +295,20 @@ pub fn install_app_with_reporter(
|
|||
})
|
||||
.map_err(InstallAppError::Install)?;
|
||||
|
||||
if install_outcome.desktop_entry_path.is_some() {
|
||||
reporter.report(&OperationEvent::StageChanged {
|
||||
stage: OperationStage::WriteDesktopEntry,
|
||||
message: "writing desktop entry".to_owned(),
|
||||
});
|
||||
}
|
||||
|
||||
if install_outcome.icon_path.is_some() {
|
||||
reporter.report(&OperationEvent::StageChanged {
|
||||
stage: OperationStage::ExtractIcon,
|
||||
message: "extracting icon".to_owned(),
|
||||
});
|
||||
}
|
||||
|
||||
reporter.report(&OperationEvent::StageChanged {
|
||||
stage: OperationStage::RefreshIntegration,
|
||||
message: "refreshing desktop integration".to_owned(),
|
||||
|
|
@ -308,6 +337,7 @@ pub fn install_app_with_reporter(
|
|||
let installed = InstalledApp {
|
||||
record,
|
||||
selected_artifact: plan.selected_artifact.clone(),
|
||||
artifact_size_bytes: artifact_bytes.len() as u64,
|
||||
source: plan.resolution.source.clone(),
|
||||
install_scope: policy.scope,
|
||||
integration_mode: policy.integration_mode,
|
||||
|
|
@ -326,6 +356,7 @@ pub fn install_app_with_reporter(
|
|||
pub struct InstalledApp {
|
||||
pub record: AppRecord,
|
||||
pub selected_artifact: ArtifactCandidate,
|
||||
pub artifact_size_bytes: u64,
|
||||
pub source: crate::domain::source::SourceRef,
|
||||
pub install_scope: InstallScope,
|
||||
pub integration_mode: IntegrationMode,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ use crate::domain::app::AppRecord;
|
|||
pub struct ListRow {
|
||||
pub stable_id: String,
|
||||
pub display_name: String,
|
||||
pub version: Option<String>,
|
||||
pub source: String,
|
||||
}
|
||||
|
||||
pub fn build_list_rows(apps: &[AppRecord]) -> Vec<ListRow> {
|
||||
|
|
@ -11,6 +13,13 @@ pub fn build_list_rows(apps: &[AppRecord]) -> Vec<ListRow> {
|
|||
.map(|app| ListRow {
|
||||
stable_id: app.stable_id.clone(),
|
||||
display_name: app.display_name.clone(),
|
||||
version: app.installed_version.clone(),
|
||||
source: app
|
||||
.source
|
||||
.as_ref()
|
||||
.map(|source| source.locator.clone())
|
||||
.or_else(|| app.source_input.clone())
|
||||
.unwrap_or_else(|| "-".to_owned()),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ pub fn remove_registered_app_with_reporter(
|
|||
stage: OperationStage::StagePayload,
|
||||
message: "removing managed artifacts".to_owned(),
|
||||
});
|
||||
let warnings = delete_artifacts(&plan)?;
|
||||
let deletion = delete_artifacts(&plan)?;
|
||||
let remaining_apps = apps
|
||||
.iter()
|
||||
.filter(|candidate| candidate.stable_id != app.stable_id)
|
||||
|
|
@ -102,8 +102,9 @@ pub fn remove_registered_app_with_reporter(
|
|||
|
||||
let result = RemovalResult {
|
||||
removed: plan,
|
||||
removed_paths: deletion.removed_paths,
|
||||
remaining_apps,
|
||||
warnings,
|
||||
warnings: deletion.warnings,
|
||||
};
|
||||
|
||||
reporter.report(&OperationEvent::StageChanged {
|
||||
|
|
@ -120,6 +121,7 @@ pub fn remove_registered_app_with_reporter(
|
|||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub struct RemovalResult {
|
||||
pub removed: RemovalPlan,
|
||||
pub removed_paths: Vec<String>,
|
||||
pub remaining_apps: Vec<AppRecord>,
|
||||
pub warnings: Vec<String>,
|
||||
}
|
||||
|
|
@ -161,13 +163,19 @@ fn removal_artifact_paths(app: &AppRecord, install_home: &Path) -> Vec<PathBuf>
|
|||
]
|
||||
}
|
||||
|
||||
fn delete_artifacts(plan: &RemovalPlan) -> Result<Vec<String>, RemoveRegisteredAppError> {
|
||||
struct DeletionOutcome {
|
||||
removed_paths: Vec<String>,
|
||||
warnings: Vec<String>,
|
||||
}
|
||||
|
||||
fn delete_artifacts(plan: &RemovalPlan) -> Result<DeletionOutcome, RemoveRegisteredAppError> {
|
||||
let desktop_path = plan.artifact_paths.get(1).map(PathBuf::from);
|
||||
let icon_path = plan.artifact_paths.get(2).map(PathBuf::from);
|
||||
let mut removed_paths = Vec::new();
|
||||
|
||||
for artifact_path in &plan.artifact_paths {
|
||||
match fs::remove_file(artifact_path) {
|
||||
Ok(()) => {}
|
||||
Ok(()) => removed_paths.push(artifact_path.clone()),
|
||||
Err(error) if error.kind() == io::ErrorKind::NotFound => {}
|
||||
Err(error) => return Err(RemoveRegisteredAppError::Io(error)),
|
||||
}
|
||||
|
|
@ -182,5 +190,8 @@ fn delete_artifacts(plan: &RemovalPlan) -> Result<Vec<String>, RemoveRegisteredA
|
|||
));
|
||||
}
|
||||
|
||||
Ok(warnings)
|
||||
Ok(DeletionOutcome {
|
||||
removed_paths,
|
||||
warnings,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue