Skip to main content

libplasmoid_updater/
lib.rs

1// SPDX-FileCopyrightText: 2025 uwuclxdy
2// SPDX-License-Identifier: GPL-3.0-or-later
3//
4// This implementation is based on:
5// - Apdatifier (https://github.com/exequtic/apdatifier) - MIT License
6// - KDE Discover's KNewStuff backend (https://invent.kde.org/plasma/discover) -
7//   GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
8//
9// The update detection algorithm, KDE Store API interaction, and widget ID resolution
10// approach are derived from Apdatifier's shell scripts. The KNewStuff registry format
11// and installation process knowledge comes from KDE Discover's source code.
12
13pub(crate) mod api;
14pub(crate) mod checker;
15pub(crate) mod config;
16pub(crate) mod error;
17pub(crate) mod installer;
18pub(crate) mod paths;
19pub(crate) mod registry;
20pub(crate) mod types;
21pub(crate) mod utils;
22pub(crate) mod version;
23
24#[cfg(feature = "cli")]
25pub mod cli;
26
27use api::ApiClient;
28use serde::Serialize;
29use types::UpdateCheckResult;
30
31pub use config::{Config, RestartBehavior};
32pub use error::Error;
33pub use types::{AvailableUpdate, ComponentType, Diagnostic, InstalledComponent};
34
35/// A specialized `Result` type for libplasmoid-updater operations.
36pub type Result<T> = std::result::Result<T, Error>;
37
38/// Checks for available updates to installed KDE Plasma components.
39///
40/// Scans the local filesystem for installed KDE components and queries the KDE Store API
41/// for newer versions. Returns an empty [`CheckResult`] when no updates are found — not an error.
42///
43/// With the `cli` feature enabled, displays a spinner during fetch and a summary table of updates.
44///
45/// # Errors
46///
47/// - [`Error::UnsupportedOS`] — not running on Linux
48/// - [`Error::NotKDE`] — KDE Plasma not detected
49pub fn check(config: &Config) -> Result<CheckResult> {
50    crate::utils::validate_environment()?;
51
52    let api_client = ApiClient::new();
53    let result = crate::utils::fetch_updates(&api_client, config)?;
54
55    #[cfg(feature = "cli")]
56    crate::utils::display_check_results(&result);
57
58    Ok(CheckResult::from_internal(result))
59}
60
61/// Result of checking for available updates.
62///
63/// Returned by [`check()`](crate::check). Contains the full [`AvailableUpdate`] data
64/// for each pending update, plus diagnostics for components that could not be checked.
65#[derive(Debug, Clone, Serialize)]
66pub struct CheckResult {
67    /// Available updates found during the check.
68    pub available_updates: Vec<AvailableUpdate>,
69    /// Components that could not be checked, with the reason for each failure.
70    pub diagnostics: Vec<Diagnostic>,
71}
72
73impl CheckResult {
74    pub(crate) fn from_internal(result: UpdateCheckResult) -> Self {
75        let diagnostics = result
76            .unresolved
77            .into_iter()
78            .chain(result.check_failures)
79            .collect();
80
81        Self {
82            available_updates: result.updates,
83            diagnostics,
84        }
85    }
86
87    /// Returns `true` if at least one update is available.
88    pub fn has_updates(&self) -> bool {
89        !self.available_updates.is_empty()
90    }
91
92    /// Returns the number of available updates.
93    pub fn update_count(&self) -> usize {
94        self.available_updates.len()
95    }
96
97    /// Returns `true` if there are no updates and no diagnostics.
98    pub fn is_empty(&self) -> bool {
99        self.available_updates.is_empty() && self.diagnostics.is_empty()
100    }
101}
102
103/// Downloads and installs all available updates for installed KDE Plasma components.
104///
105/// Runs the full update pipeline: scan installed components, check for updates, select
106/// which to apply, then download and install. Handles plasmashell restart based on
107/// [`Config::restart`]. Components in [`Config::excluded_packages`] are always skipped.
108///
109/// With the `cli` feature enabled and [`Config::auto_confirm`] unset, shows an interactive
110/// multi-select menu. Otherwise, all available updates are applied automatically.
111///
112/// # Errors
113///
114/// Returns an [`Error`] if environment validation, network requests, or installation fails.
115pub fn update(config: &Config) -> Result<UpdateResult> {
116    crate::utils::validate_environment()?;
117
118    let api_client = ApiClient::new();
119    let check_result = crate::utils::fetch_updates(&api_client, config)?;
120
121    if check_result.updates.is_empty() {
122        #[cfg(feature = "cli")]
123        println!("no updates available");
124
125        return Ok(UpdateResult::default());
126    }
127
128    let selected = crate::utils::select_updates(&check_result.updates, config)?;
129
130    if selected.is_empty() {
131        #[cfg(feature = "cli")]
132        println!("nothing to update");
133
134        return Ok(UpdateResult::default());
135    }
136
137    let result = crate::utils::install_selected_updates(&selected, &api_client, config)?;
138
139    #[cfg(feature = "debug")]
140    {
141        let n = api_client.request_count();
142        let plural = if n == 1 { "" } else { "s" };
143        println!("{n} web request{plural}");
144    }
145
146    crate::utils::handle_restart(config, &check_result.updates, &result);
147
148    Ok(result)
149}
150
151/// Result of performing updates.
152///
153/// Returned by [`update()`](crate::update). Tracks which components succeeded,
154/// failed, or were skipped during the update run.
155#[derive(Debug, Clone, Default, Serialize)]
156pub struct UpdateResult {
157    pub succeeded: Vec<String>,
158    pub failed: Vec<(String, String)>,
159    pub skipped: Vec<String>,
160}
161
162impl UpdateResult {
163    /// Returns `true` if any component failed to update.
164    pub fn has_failures(&self) -> bool {
165        !self.failed.is_empty()
166    }
167
168    /// Returns `true` if no update actions were attempted.
169    pub fn is_empty(&self) -> bool {
170        self.succeeded.is_empty() && self.failed.is_empty() && self.skipped.is_empty()
171    }
172
173    /// Returns the number of successfully updated components.
174    pub fn success_count(&self) -> usize {
175        self.succeeded.len()
176    }
177
178    /// Returns the number of components that failed to update.
179    pub fn failure_count(&self) -> usize {
180        self.failed.len()
181    }
182
183    /// Prints a formatted table of failed updates to stdout.
184    #[cfg(feature = "cli")]
185    pub fn print_error_table(&self) {
186        crate::cli::output::print_error_table(self);
187    }
188
189    /// Prints a one-line summary of the update outcome to stdout.
190    #[cfg(feature = "cli")]
191    pub fn print_summary(&self) {
192        crate::cli::output::print_summary(self);
193    }
194}
195
196/// Returns all installed KDE Plasma components without making network requests.
197///
198/// Scans the filesystem and KNewStuff registry to discover locally installed components.
199/// Useful for building custom UIs or auditing what is installed.
200///
201/// # Errors
202///
203/// Returns an error if the filesystem scan fails.
204pub fn get_installed(config: &Config) -> Result<Vec<InstalledComponent>> {
205    checker::find_installed(config.system)
206}
207
208/// Downloads and installs a single component update with automatic backup and rollback.
209///
210/// On failure, the original component is restored from backup. Does not handle
211/// plasmashell restart — the caller is responsible for restarting if needed.
212///
213/// # Errors
214///
215/// Returns an error if download, installation, or backup operations fail.
216pub fn install_update(update: &AvailableUpdate, config: &Config) -> Result<()> {
217    let _ = config;
218    let api_client = ApiClient::new();
219    let counter = api_client.request_counter();
220    installer::update_component(update, api_client.http_client(), |_| {}, &counter)
221}
222
223/// Discovers and prints all installed KDE components as a formatted table.
224///
225/// Scans the filesystem and KNewStuff registry without making network requests.
226/// Prints a count header followed by a table of all discovered components.
227///
228/// # Errors
229///
230/// Returns an error if the filesystem scan fails.
231#[cfg(feature = "cli")]
232#[doc(hidden)]
233pub fn show_installed(config: &Config) -> Result<()> {
234    let components = checker::find_installed(config.system)?;
235
236    if components.is_empty() {
237        println!("no components installed");
238        return Ok(());
239    }
240
241    cli::output::print_count_message(components.len(), "installed component");
242    cli::output::print_components_table(&components);
243
244    Ok(())
245}