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}