use anyhow::{Result, bail};
use async_trait::async_trait;
use clap::Args;
use defaults_rs::{Domain, PrefValue, Preferences};
use crate::{
cli::atomic::should_dry_run,
commands::{ResetCmd, Runnable, RunnableInvokeRules},
context::AppContext,
domains::convert::serializable_to_prefvalue,
log_cute, log_dry, log_err, log_info, log_warn,
util::{
io::{confirm, restart_services},
sha::get_digest,
},
};
#[derive(Args, Debug)]
pub struct UnapplyCmd;
#[async_trait]
impl Runnable for UnapplyCmd {
fn get_invoke_rules(&self) -> RunnableInvokeRules {
RunnableInvokeRules {
do_config_autosync: true,
require_sudo: false,
respect_lock: true,
}
}
async fn run(&self, ctx: &AppContext) -> Result<()> {
if !ctx.snapshot.is_loadable() {
log_warn!("No snapshot found to revert.");
if confirm("Reset all System Settings instead?") {
return ResetCmd.run(ctx).await;
}
bail!("Abort operation.")
}
let dry_run = should_dry_run();
let snapshot = match ctx.snapshot.load().await {
Ok(snap) => snap,
Err(_) => {
bail!(
"Could not read snapshot since it might be corrupt. \n\
Use `cutler reset` instead to return System Settings to factory defaults."
)
}
};
if snapshot.digest != get_digest(ctx.config.path())? {
log_warn!("Config has been modified since last application.",);
log_warn!("Please note that only the applied modifications will be unapplied.",);
}
let mut restore_jobs: Vec<(Domain, String, PrefValue)> = Vec::new();
let mut delete_jobs: Vec<(Domain, String)> = Vec::new();
for s in snapshot.settings.clone().into_iter().rev() {
let domain_obj = if s.domain == "NSGlobalDomain" {
Domain::Global
} else {
Domain::User(s.domain.clone())
};
if let Some(orig) = s.original_value {
let pref_value = serializable_to_prefvalue(&orig);
restore_jobs.push((domain_obj, s.key, pref_value));
} else {
delete_jobs.push((domain_obj, s.key));
}
}
if dry_run {
for (domain, key, original_value) in restore_jobs {
log_dry!("Would restore: {domain} | {key} -> {original_value}",);
}
for (domain, key) in &delete_jobs {
log_dry!("Would delete setting: {domain} | {key}",);
}
log_dry!("Would delete snapshot at path: {:?}", ctx.snapshot.path());
} else {
let mut settings_modified_count = 0;
if !restore_jobs.is_empty() {
for (domain, key, value) in restore_jobs {
log_info!("Restoring: {domain} | {key} -> {value}",);
if let Err(e) = Preferences::write(domain.clone(), &key, value.clone()) {
log_err!("Restore failed: {e}");
} else {
settings_modified_count += 1;
}
}
}
if !delete_jobs.is_empty() {
for (domain, key) in delete_jobs {
log_info!("Deleting: {domain} | {key}");
if let Err(e) = Preferences::delete(domain.clone(), &key) {
log_err!("Delete failed: {e}");
} else {
settings_modified_count += 1;
}
}
}
if snapshot.exec_run_count > 0 {
log_warn!(
"{} commands were executed previously; revert them manually.",
snapshot.exec_run_count
);
}
if settings_modified_count > 0 {
log_info!("Modified {settings_modified_count} settings; restarting services.");
restart_services().await;
}
snapshot.delete().await?;
log_cute!("Unapply operation complete.");
}
Ok(())
}
}