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(config.skip_plasma_detection)?;
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 let _lock = installer::UpdateLock::acquire()?;
117 crate::utils::validate_environment(config.skip_plasma_detection)?;
118
119 let api_client = ApiClient::new();
120 let check_result = crate::utils::fetch_updates(&api_client, config)?;
121
122 if check_result.updates.is_empty() {
123 #[cfg(feature = "cli")]
124 println!("no updates available");
125
126 return Ok(UpdateResult::default());
127 }
128
129 let selected = crate::utils::select_updates(&check_result.updates, config)?;
130
131 if selected.is_empty() {
132 #[cfg(feature = "cli")]
133 println!("nothing to update");
134
135 return Ok(UpdateResult::default());
136 }
137
138 let result = crate::utils::install_selected_updates(&selected, &api_client, config)?;
139
140 #[cfg(feature = "debug")]
141 {
142 let n = api_client.request_count();
143 let plural = if n == 1 { "" } else { "s" };
144 println!("{n} web request{plural}");
145 }
146
147 crate::utils::handle_restart(config, &check_result.updates, &result);
148
149 Ok(result)
150}
151
152/// A component that failed to update, with the error message.
153#[derive(Debug, Clone, Serialize)]
154pub struct FailedUpdate {
155 /// Display name of the component that failed.
156 pub name: String,
157 /// Human-readable error description.
158 pub error: String,
159}
160
161/// A component that installed successfully but whose post-install version
162/// could not be verified to match the expected version.
163#[derive(Debug, Clone, Serialize)]
164pub struct UnverifiedUpdate {
165 /// Display name of the component.
166 pub name: String,
167 /// The version that was expected after install.
168 pub expected_version: String,
169 /// The version actually found on disk, if readable.
170 pub actual_version: Option<String>,
171}
172
173/// Result of performing updates.
174///
175/// Returned by [`update()`](crate::update). Tracks which components succeeded,
176/// failed, or were skipped during the update run.
177#[derive(Debug, Clone, Default, Serialize)]
178pub struct UpdateResult {
179 pub succeeded: Vec<String>,
180 pub failed: Vec<FailedUpdate>,
181 pub skipped: Vec<String>,
182 /// Components that installed successfully but whose post-install version
183 /// could not be verified to match the expected version.
184 pub unverified: Vec<UnverifiedUpdate>,
185}
186
187impl UpdateResult {
188 /// Returns `true` if any component failed to update.
189 pub fn has_failures(&self) -> bool {
190 !self.failed.is_empty()
191 }
192
193 /// Returns `true` if no update actions were attempted.
194 pub fn is_empty(&self) -> bool {
195 self.succeeded.is_empty()
196 && self.failed.is_empty()
197 && self.skipped.is_empty()
198 && self.unverified.is_empty()
199 }
200
201 /// Returns the number of successfully updated components.
202 pub fn success_count(&self) -> usize {
203 self.succeeded.len()
204 }
205
206 /// Returns the number of components that failed to update.
207 pub fn failure_count(&self) -> usize {
208 self.failed.len()
209 }
210
211 /// Prints a formatted table of failed updates to stdout.
212 #[cfg(feature = "cli")]
213 pub fn print_error_table(&self) {
214 crate::cli::output::print_error_table(self);
215 }
216
217 /// Prints a one-line summary of the update outcome to stdout.
218 #[cfg(feature = "cli")]
219 pub fn print_summary(&self) {
220 crate::cli::output::print_summary(self);
221 }
222}
223
224/// Returns all installed KDE Plasma components without making network requests.
225///
226/// Scans the filesystem and KNewStuff registry to discover locally installed components.
227/// Useful for building custom UIs or auditing what is installed.
228///
229/// # Errors
230///
231/// Returns an error if the filesystem scan fails.
232pub fn get_installed(config: &Config) -> Result<Vec<InstalledComponent>> {
233 checker::find_installed(config.system)
234}
235
236/// Downloads and installs a single component update with automatic backup and rollback.
237///
238/// On failure, the original component is restored from backup. Does not handle
239/// plasmashell restart — the caller is responsible for restarting if needed.
240///
241/// Respects [`Config::inhibit_idle`] to optionally prevent system sleep during install.
242///
243/// # Errors
244///
245/// Returns an error if download, installation, or backup operations fail.
246pub fn install_update(update: &AvailableUpdate, config: &Config) -> Result<()> {
247 let _lock = installer::UpdateLock::acquire()?;
248 let _inhibit = if config.inhibit_idle {
249 installer::InhibitGuard::acquire()
250 } else {
251 installer::InhibitGuard::None
252 };
253
254 let api_client = ApiClient::new();
255 let counter = api_client.request_counter();
256 installer::update_component(update, api_client.http_client(), |_| {}, &counter).map(|_| ())
257}
258
259/// Discovers and prints all installed KDE components as a formatted table.
260///
261/// Scans the filesystem and KNewStuff registry without making network requests.
262/// Prints a count header followed by a table of all discovered components.
263///
264/// # Errors
265///
266/// Returns an error if the filesystem scan fails.
267#[cfg(feature = "cli")]
268#[doc(hidden)]
269pub fn show_installed(config: &Config) -> Result<()> {
270 let components = checker::find_installed(config.system)?;
271
272 if components.is_empty() {
273 println!("no components installed");
274 return Ok(());
275 }
276
277 cli::output::print_count_message(components.len(), "installed component");
278 cli::output::print_components_table(&components);
279
280 Ok(())
281}