#[cfg(feature = "cli")]
use crate::cli::{self, progress::create_fetch_spinner};
#[cfg(feature = "cli")]
use inquire::InquireError;
use crate::{FailedUpdate, UnverifiedUpdate};
use rayon::iter::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator};
use std::sync::Arc;
use crate::{
Config, Error, RestartBehavior, UpdateResult,
api::ApiClient,
checker::{check_with_components, find_installed},
installer,
types::{AvailableUpdate, UpdateCheckResult},
};
pub(crate) fn validate_environment(skip_plasma_detection: bool) -> crate::Result<()> {
if cfg!(not(target_os = "linux")) {
return Err(Error::UnsupportedOS(std::env::consts::OS.to_string()));
}
let plasma_found = skip_plasma_detection || crate::paths::is_kde();
if !plasma_found {
return Err(Error::NotKDE);
}
check_dependency("bsdtar")?;
Ok(())
}
fn check_dependency(name: &str) -> crate::Result<()> {
use std::process::Command;
match Command::new("which").arg(name).output() {
Ok(output) if output.status.success() => Ok(()),
_ => Err(Error::MissingDependency(name.to_string())),
}
}
pub(crate) fn fetch_updates(
api_client: &ApiClient,
config: &Config,
) -> crate::Result<UpdateCheckResult> {
#[cfg(feature = "cli")]
let spinner = create_fetch_spinner();
let components = find_installed(config.system)?;
let result = check_with_components(config, api_client, components)?;
#[cfg(feature = "cli")]
spinner.finish_and_clear();
Ok(result)
}
pub(crate) fn select_updates<'a>(
updates: &'a [AvailableUpdate],
config: &Config,
) -> crate::Result<Vec<&'a AvailableUpdate>> {
#[cfg(feature = "cli")]
if !config.auto_confirm && stdin_is_terminal() {
return prompt_update_selection(updates, &config.excluded_packages);
}
Ok(filter_excluded(updates, &config.excluded_packages))
}
pub(crate) fn filter_excluded<'a>(
updates: &'a [AvailableUpdate],
excluded: &[String],
) -> Vec<&'a AvailableUpdate> {
updates
.iter()
.filter(|u| !is_excluded(u, excluded))
.collect()
}
pub(crate) fn is_excluded(update: &AvailableUpdate, excluded: &[String]) -> bool {
excluded
.iter()
.any(|e| e == &update.installed.directory_name || e == &update.installed.name)
}
#[cfg(feature = "cli")]
pub(crate) fn stdin_is_terminal() -> bool {
use is_terminal::IsTerminal;
std::io::stdin().is_terminal()
}
#[cfg(feature = "cli")]
pub(crate) fn prompt_update_selection<'a>(
updates: &'a [AvailableUpdate],
excluded: &[String],
) -> crate::Result<Vec<&'a AvailableUpdate>> {
let options = format_menu_options(updates);
let defaults: Vec<usize> = updates
.iter()
.enumerate()
.filter(|(_, u)| !is_excluded(u, excluded))
.map(|(i, _)| i)
.collect();
let plural = if updates.len() == 1 { "" } else { "s" };
let prompt = format!(
"{} update{plural} available, select to apply:",
updates.len()
);
match inquire::MultiSelect::new(&prompt, options)
.with_default(&defaults)
.with_page_size(15)
.raw_prompt()
{
Ok(selected) => {
let result: Vec<&AvailableUpdate> = selected
.into_iter()
.map(|opt| &updates[opt.index])
.collect();
use std::io::Write;
print!("{}", cli::CLEAR_LINE_SEQUENCE);
std::io::stdout().flush().ok();
Ok(result)
}
Err(InquireError::OperationCanceled | InquireError::OperationInterrupted) => Ok(vec![]),
Err(e) => Err(crate::Error::other(format!("prompt failed: {e}"))),
}
}
#[cfg(feature = "cli")]
pub(crate) fn format_menu_options(updates: &[AvailableUpdate]) -> Vec<String> {
let name_width = updates
.iter()
.map(|u| u.installed.name.len())
.max()
.unwrap_or(10)
.max(10);
updates
.iter()
.map(|u| {
format!(
"{:<name_width$} {} \u{2192} {}",
u.installed.name,
cli::output::format_version(&u.installed.version),
cli::output::format_version(&u.latest_version),
)
})
.collect()
}
pub(crate) fn install_selected_updates(
updates: &[&AvailableUpdate],
api_client: &ApiClient,
config: &Config,
) -> crate::Result<UpdateResult> {
let result = Arc::new(parking_lot::Mutex::new(UpdateResult::default()));
let _inhibit = if config.inhibit_idle {
installer::InhibitGuard::acquire()
} else {
installer::InhibitGuard::None
};
#[cfg(feature = "cli")]
let ui = cli::update_ui::UpdateUi::new(updates);
let thread_count = config.threads.unwrap_or(0);
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(thread_count)
.build()
.unwrap_or_else(|_| {
rayon::ThreadPoolBuilder::new()
.build()
.expect("failed to build default thread pool")
});
let counter = api_client.request_counter();
pool.install(|| {
updates.par_iter().enumerate().for_each(|(index, update)| {
#[cfg(not(feature = "cli"))]
let _ = index;
let name = update.installed.name.clone();
#[cfg(feature = "cli")]
let reporter = ui.reporter(index);
#[cfg(not(feature = "cli"))]
let reporter = |_: u8| {};
match installer::update_component(update, api_client.http_client(), reporter, &counter)
{
Ok(outcome) => {
#[cfg(feature = "cli")]
ui.complete_task(index, true);
let mut r = result.lock();
if !outcome.verified {
r.unverified.push(UnverifiedUpdate {
name: name.clone(),
expected_version: outcome.expected_version,
actual_version: outcome.actual_version,
});
}
r.succeeded.push(name);
}
Err(e) => {
#[cfg(feature = "cli")]
ui.complete_task(index, false);
result.lock().failed.push(FailedUpdate {
name,
error: e.to_string(),
});
}
}
});
});
#[cfg(feature = "cli")]
ui.finish();
Ok(Arc::try_unwrap(result)
.expect("thread pool completed; Arc should have single owner")
.into_inner())
}
pub(crate) fn handle_restart(config: &Config, updates: &[AvailableUpdate], result: &UpdateResult) {
if result.succeeded.is_empty() {
return;
}
let succeeded_updates: Vec<&AvailableUpdate> = updates
.iter()
.filter(|u| result.succeeded.contains(&u.installed.name))
.collect();
if !installer::any_requires_restart(&succeeded_updates) {
return;
}
match config.restart {
RestartBehavior::Never => {}
RestartBehavior::Always => {
if let Err(e) = installer::restart_plasmashell() {
log::warn!(target: "restart", "failed to restart plasmashell: {e}");
}
}
#[cfg(feature = "cli")]
RestartBehavior::Prompt => {
if stdin_is_terminal() {
prompt_restart();
}
}
#[cfg(not(feature = "cli"))]
RestartBehavior::Prompt => {
log::info!(target: "restart", "prompt restart requested but no CLI available, skipping");
}
}
}
#[cfg(feature = "cli")]
pub(crate) fn prompt_restart() {
match inquire::Confirm::new("Restart plasmashell now?")
.with_default(false)
.prompt()
{
Ok(true) => {
if let Err(e) = installer::restart_plasmashell() {
log::warn!(target: "restart", "failed to restart plasmashell: {e}");
}
}
Ok(false) | Err(InquireError::OperationCanceled | InquireError::OperationInterrupted) => {}
Err(e) => log::warn!(target: "restart", "prompt failed: {e}"),
}
}
#[cfg(feature = "cli")]
pub(crate) fn display_check_results(result: &crate::types::UpdateCheckResult) {
if result.updates.is_empty() {
println!("no updates available");
return;
}
cli::output::print_count_message(result.updates.len(), "update");
cli::output::print_updates_table(&result.updates);
}