feat: redesign cli ux progress
This commit is contained in:
parent
ab60ee641f
commit
c63b2917da
21 changed files with 994 additions and 99 deletions
|
|
@ -1,9 +1,13 @@
|
|||
use std::env;
|
||||
use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::adapters::traits::AdapterResolution;
|
||||
use crate::app::identity::{IdentityFallback, ResolveIdentityError, resolve_identity};
|
||||
use crate::app::interaction::{InteractionKind, InteractionRequest};
|
||||
use crate::app::progress::{
|
||||
NoopReporter, OperationEvent, OperationKind, OperationStage, ProgressReporter,
|
||||
};
|
||||
use crate::app::query::{ResolveQueryError, resolve_query};
|
||||
use crate::app::scope::{ScopeOverride, resolve_install_scope_with_default};
|
||||
use crate::domain::app::{AppRecord, InstallMetadata, InstallScope};
|
||||
|
|
@ -187,6 +191,27 @@ pub fn install_app(
|
|||
install_home: &Path,
|
||||
requested_scope: InstallScope,
|
||||
) -> Result<InstalledApp, InstallAppError> {
|
||||
let mut reporter = NoopReporter;
|
||||
install_app_with_reporter(
|
||||
source_input,
|
||||
plan,
|
||||
install_home,
|
||||
requested_scope,
|
||||
&mut reporter,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn install_app_with_reporter(
|
||||
source_input: &str,
|
||||
plan: &AddPlan,
|
||||
install_home: &Path,
|
||||
requested_scope: InstallScope,
|
||||
reporter: &mut impl ProgressReporter,
|
||||
) -> Result<InstalledApp, InstallAppError> {
|
||||
reporter.report(&OperationEvent::Started {
|
||||
kind: OperationKind::Add,
|
||||
label: source_input.to_owned(),
|
||||
});
|
||||
let mut record =
|
||||
materialize_app_record(source_input, plan).map_err(InstallAppError::Materialize)?;
|
||||
let (family, capabilities) =
|
||||
|
|
@ -209,7 +234,12 @@ pub fn install_app(
|
|||
install_home,
|
||||
&policy.icon_root.join(format!("{}.png", record.stable_id)),
|
||||
);
|
||||
let artifact_bytes = download_artifact_bytes(&plan.selected_artifact.url)?;
|
||||
reporter.report(&OperationEvent::StageChanged {
|
||||
stage: OperationStage::DownloadArtifact,
|
||||
message: "downloading artifact".to_owned(),
|
||||
});
|
||||
let artifact_bytes =
|
||||
download_artifact_bytes_with_reporter(&plan.selected_artifact.url, reporter)?;
|
||||
let payload_exec = payload_path.clone();
|
||||
let desktop_owned = match policy.integration_mode {
|
||||
IntegrationMode::PayloadOnly | IntegrationMode::Denied => None,
|
||||
|
|
@ -219,6 +249,21 @@ pub fn install_app(
|
|||
)),
|
||||
};
|
||||
|
||||
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(),
|
||||
});
|
||||
let install_outcome = execute_install(&InstallRequest {
|
||||
staging_root: &install_home.join(".local/share/aim/staging"),
|
||||
final_payload_path: &payload_path,
|
||||
|
|
@ -235,6 +280,18 @@ pub fn install_app(
|
|||
})
|
||||
.map_err(InstallAppError::Install)?;
|
||||
|
||||
reporter.report(&OperationEvent::StageChanged {
|
||||
stage: OperationStage::RefreshIntegration,
|
||||
message: "refreshing desktop integration".to_owned(),
|
||||
});
|
||||
if !install_outcome.warnings.is_empty() {
|
||||
for warning in &install_outcome.warnings {
|
||||
reporter.report(&OperationEvent::Warning {
|
||||
message: warning.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
record.install = Some(InstallMetadata {
|
||||
scope: policy.scope,
|
||||
payload_path: Some(install_outcome.final_payload_path.display().to_string()),
|
||||
|
|
@ -248,7 +305,7 @@ pub fn install_app(
|
|||
.map(|path| path.display().to_string()),
|
||||
});
|
||||
|
||||
Ok(InstalledApp {
|
||||
let installed = InstalledApp {
|
||||
record,
|
||||
selected_artifact: plan.selected_artifact.clone(),
|
||||
source: plan.resolution.source.clone(),
|
||||
|
|
@ -256,7 +313,13 @@ pub fn install_app(
|
|||
integration_mode: policy.integration_mode,
|
||||
install_outcome,
|
||||
warnings: policy.warnings,
|
||||
})
|
||||
};
|
||||
|
||||
reporter.report(&OperationEvent::Finished {
|
||||
summary: format!("installed {}", installed.record.stable_id),
|
||||
});
|
||||
|
||||
Ok(installed)
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
|
|
@ -287,21 +350,48 @@ pub enum InstallAppError {
|
|||
Materialize(MaterializeAddRecordError),
|
||||
Policy(String),
|
||||
Download(reqwest::Error),
|
||||
DownloadIo(std::io::Error),
|
||||
HostProbe(std::io::Error),
|
||||
Install(crate::integration::install::PayloadInstallError),
|
||||
}
|
||||
|
||||
fn download_artifact_bytes(url: &str) -> Result<Vec<u8>, InstallAppError> {
|
||||
fn download_artifact_bytes_with_reporter(
|
||||
url: &str,
|
||||
reporter: &mut impl ProgressReporter,
|
||||
) -> Result<Vec<u8>, InstallAppError> {
|
||||
if env::var(FIXTURE_MODE_ENV).ok().as_deref() == Some("1") {
|
||||
return Ok(b"\x7fELFAppImage\x89PNG\r\n\x1a\nicondataIEND\xaeB`\x82".to_vec());
|
||||
let bytes = b"\x7fELFAppImage\x89PNG\r\n\x1a\nicondataIEND\xaeB`\x82".to_vec();
|
||||
reporter.report(&OperationEvent::Progress {
|
||||
current: bytes.len() as u64,
|
||||
total: Some(bytes.len() as u64),
|
||||
});
|
||||
return Ok(bytes);
|
||||
}
|
||||
|
||||
let response = reqwest::blocking::get(url).map_err(InstallAppError::Download)?;
|
||||
let response = response
|
||||
.error_for_status()
|
||||
.map_err(InstallAppError::Download)?;
|
||||
let bytes = response.bytes().map_err(InstallAppError::Download)?;
|
||||
Ok(bytes.to_vec())
|
||||
let total = response.content_length();
|
||||
let mut response = response;
|
||||
let mut bytes = Vec::new();
|
||||
let mut buffer = [0_u8; 16 * 1024];
|
||||
let mut current = 0_u64;
|
||||
|
||||
loop {
|
||||
let read = response
|
||||
.read(&mut buffer)
|
||||
.map_err(InstallAppError::DownloadIo)?;
|
||||
if read == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
bytes.extend_from_slice(&buffer[..read]);
|
||||
current += read as u64;
|
||||
reporter.report(&OperationEvent::Progress { current, total });
|
||||
}
|
||||
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
fn render_desktop_entry(display_name: &str, exec_path: &Path) -> String {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ pub mod add;
|
|||
pub mod identity;
|
||||
pub mod interaction;
|
||||
pub mod list;
|
||||
pub mod progress;
|
||||
pub mod query;
|
||||
pub mod remove;
|
||||
pub mod scope;
|
||||
|
|
|
|||
67
crates/aim-core/src/app/progress.rs
Normal file
67
crates/aim-core/src/app/progress.rs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum OperationKind {
|
||||
Add,
|
||||
UpdateBatch,
|
||||
UpdateItem,
|
||||
Remove,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum OperationStage {
|
||||
ResolveQuery,
|
||||
DiscoverRelease,
|
||||
SelectArtifact,
|
||||
DownloadArtifact,
|
||||
StagePayload,
|
||||
WriteDesktopEntry,
|
||||
ExtractIcon,
|
||||
RefreshIntegration,
|
||||
SaveRegistry,
|
||||
Finalize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum OperationEvent {
|
||||
Started {
|
||||
kind: OperationKind,
|
||||
label: String,
|
||||
},
|
||||
StageChanged {
|
||||
stage: OperationStage,
|
||||
message: String,
|
||||
},
|
||||
Progress {
|
||||
current: u64,
|
||||
total: Option<u64>,
|
||||
},
|
||||
Warning {
|
||||
message: String,
|
||||
},
|
||||
Finished {
|
||||
summary: String,
|
||||
},
|
||||
Failed {
|
||||
stage: OperationStage,
|
||||
reason: String,
|
||||
},
|
||||
}
|
||||
|
||||
pub trait ProgressReporter {
|
||||
fn report(&mut self, event: &OperationEvent);
|
||||
}
|
||||
|
||||
impl<F> ProgressReporter for F
|
||||
where
|
||||
F: FnMut(&OperationEvent),
|
||||
{
|
||||
fn report(&mut self, event: &OperationEvent) {
|
||||
self(event);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct NoopReporter;
|
||||
|
||||
impl ProgressReporter for NoopReporter {
|
||||
fn report(&mut self, _event: &OperationEvent) {}
|
||||
}
|
||||
|
|
@ -3,6 +3,9 @@ use std::io;
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::app::interaction::{InteractionKind, InteractionRequest};
|
||||
use crate::app::progress::{
|
||||
NoopReporter, OperationEvent, OperationKind, OperationStage, ProgressReporter,
|
||||
};
|
||||
use crate::domain::app::{AppRecord, InstallScope};
|
||||
use crate::integration::paths::{desktop_entry_path, icon_path, managed_appimage_path};
|
||||
use crate::integration::refresh::refresh_integration;
|
||||
|
|
@ -66,8 +69,30 @@ pub fn remove_registered_app(
|
|||
apps: &[AppRecord],
|
||||
install_home: &Path,
|
||||
) -> Result<RemovalResult, RemoveRegisteredAppError> {
|
||||
let mut reporter = NoopReporter;
|
||||
remove_registered_app_with_reporter(query, apps, install_home, &mut reporter)
|
||||
}
|
||||
|
||||
pub fn remove_registered_app_with_reporter(
|
||||
query: &str,
|
||||
apps: &[AppRecord],
|
||||
install_home: &Path,
|
||||
reporter: &mut impl ProgressReporter,
|
||||
) -> Result<RemovalResult, RemoveRegisteredAppError> {
|
||||
reporter.report(&OperationEvent::Started {
|
||||
kind: OperationKind::Remove,
|
||||
label: query.to_owned(),
|
||||
});
|
||||
reporter.report(&OperationEvent::StageChanged {
|
||||
stage: OperationStage::ResolveQuery,
|
||||
message: format!("resolving {query}"),
|
||||
});
|
||||
let app = resolve_registered_app(query, apps).map_err(RemoveRegisteredAppError::Resolve)?;
|
||||
let plan = build_removal_plan(app, install_home);
|
||||
reporter.report(&OperationEvent::StageChanged {
|
||||
stage: OperationStage::StagePayload,
|
||||
message: "removing managed artifacts".to_owned(),
|
||||
});
|
||||
let warnings = delete_artifacts(&plan)?;
|
||||
let remaining_apps = apps
|
||||
.iter()
|
||||
|
|
@ -75,11 +100,21 @@ pub fn remove_registered_app(
|
|||
.cloned()
|
||||
.collect();
|
||||
|
||||
Ok(RemovalResult {
|
||||
let result = RemovalResult {
|
||||
removed: plan,
|
||||
remaining_apps,
|
||||
warnings,
|
||||
})
|
||||
};
|
||||
|
||||
reporter.report(&OperationEvent::StageChanged {
|
||||
stage: OperationStage::Finalize,
|
||||
message: format!("removed {}", result.removed.stable_id),
|
||||
});
|
||||
reporter.report(&OperationEvent::Finished {
|
||||
summary: format!("removed {}", result.removed.stable_id),
|
||||
});
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
use std::path::Path;
|
||||
|
||||
use crate::app::add::{build_add_plan, install_app};
|
||||
use crate::app::add::{build_add_plan, install_app_with_reporter};
|
||||
use crate::app::progress::{
|
||||
NoopReporter, OperationEvent, OperationKind, OperationStage, ProgressReporter,
|
||||
};
|
||||
use crate::domain::app::{AppRecord, InstallScope};
|
||||
use crate::domain::update::{
|
||||
ChannelPreference, ExecutedUpdate, PlannedUpdate, UpdateChannelKind, UpdateExecutionResult,
|
||||
|
|
@ -17,11 +20,28 @@ pub fn execute_updates(
|
|||
apps: &[AppRecord],
|
||||
install_home: &Path,
|
||||
) -> Result<UpdateExecutionResult, ExecuteUpdatesError> {
|
||||
let mut reporter = NoopReporter;
|
||||
execute_updates_with_reporter(apps, install_home, &mut reporter)
|
||||
}
|
||||
|
||||
pub fn execute_updates_with_reporter(
|
||||
apps: &[AppRecord],
|
||||
install_home: &Path,
|
||||
reporter: &mut impl ProgressReporter,
|
||||
) -> Result<UpdateExecutionResult, ExecuteUpdatesError> {
|
||||
reporter.report(&OperationEvent::Started {
|
||||
kind: OperationKind::UpdateBatch,
|
||||
label: format!("{} apps", apps.len()),
|
||||
});
|
||||
let mut updated_apps = Vec::with_capacity(apps.len());
|
||||
let mut items = Vec::with_capacity(apps.len());
|
||||
|
||||
for app in apps {
|
||||
match execute_update(app, install_home) {
|
||||
reporter.report(&OperationEvent::Started {
|
||||
kind: OperationKind::UpdateItem,
|
||||
label: app.stable_id.clone(),
|
||||
});
|
||||
match execute_update(app, install_home, reporter) {
|
||||
Ok(updated) => {
|
||||
let warnings = updated
|
||||
.warnings
|
||||
|
|
@ -39,6 +59,9 @@ pub fn execute_updates(
|
|||
status: UpdateExecutionStatus::Updated,
|
||||
});
|
||||
updated_apps.push(record);
|
||||
reporter.report(&OperationEvent::Finished {
|
||||
summary: format!("updated {}", app.stable_id),
|
||||
});
|
||||
}
|
||||
Err(reason) => {
|
||||
items.push(ExecutedUpdate {
|
||||
|
|
@ -54,10 +77,20 @@ pub fn execute_updates(
|
|||
}
|
||||
}
|
||||
|
||||
Ok(UpdateExecutionResult {
|
||||
let result = UpdateExecutionResult {
|
||||
apps: updated_apps,
|
||||
items,
|
||||
})
|
||||
};
|
||||
|
||||
reporter.report(&OperationEvent::Finished {
|
||||
summary: format!(
|
||||
"updated {}, failed {}",
|
||||
result.updated_count(),
|
||||
result.failed_count()
|
||||
),
|
||||
});
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
|
|
@ -107,18 +140,44 @@ fn plan_update(app: &AppRecord) -> PlannedUpdate {
|
|||
fn execute_update(
|
||||
app: &AppRecord,
|
||||
install_home: &Path,
|
||||
reporter: &mut impl ProgressReporter,
|
||||
) -> Result<crate::app::add::InstalledApp, String> {
|
||||
let query = update_query(app).ok_or_else(|| "missing install source".to_owned())?;
|
||||
reporter.report(&OperationEvent::StageChanged {
|
||||
stage: OperationStage::ResolveQuery,
|
||||
message: format!("resolving {}", app.stable_id),
|
||||
});
|
||||
let query = update_query(app).ok_or_else(|| {
|
||||
let reason = "missing install source".to_owned();
|
||||
reporter.report(&OperationEvent::Failed {
|
||||
stage: OperationStage::ResolveQuery,
|
||||
reason: reason.clone(),
|
||||
});
|
||||
reason
|
||||
})?;
|
||||
let requested_scope = app
|
||||
.install
|
||||
.as_ref()
|
||||
.map(|install| install.scope)
|
||||
.unwrap_or(InstallScope::User);
|
||||
let plan = build_add_plan(&query)
|
||||
.map_err(|error| format!("failed to build update plan: {error:?}"))?;
|
||||
let plan = build_add_plan(&query).map_err(|error| {
|
||||
let reason = format!("failed to build update plan: {error:?}");
|
||||
reporter.report(&OperationEvent::Failed {
|
||||
stage: OperationStage::ResolveQuery,
|
||||
reason: reason.clone(),
|
||||
});
|
||||
reason
|
||||
})?;
|
||||
|
||||
install_app(&query, &plan, install_home, requested_scope)
|
||||
.map_err(|error| format!("failed to install update: {error:?}"))
|
||||
install_app_with_reporter(&query, &plan, install_home, requested_scope, reporter).map_err(
|
||||
|error| {
|
||||
let reason = format!("failed to install update: {error:?}");
|
||||
reporter.report(&OperationEvent::Failed {
|
||||
stage: OperationStage::Finalize,
|
||||
reason: reason.clone(),
|
||||
});
|
||||
reason
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn update_query(app: &AppRecord) -> Option<String> {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue