Skip to main content

libplasmoid_updater/
types.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2
3use std::path::PathBuf;
4
5use serde::{Deserialize, Serialize};
6
7/// KDE Store category IDs for supported component types.
8const CATEGORY_PLASMA_WIDGET: u16 = 705;
9const CATEGORY_WALLPAPER_PLUGIN: u16 = 715;
10const CATEGORY_KWIN_EFFECT: u16 = 719;
11const CATEGORY_KWIN_SCRIPT: u16 = 720;
12const CATEGORY_KWIN_SWITCHER: u16 = 721;
13const CATEGORY_GLOBAL_THEME: u16 = 722;
14const CATEGORY_PLASMA_STYLE: u16 = 709;
15const CATEGORY_AURORAE_DECORATION: u16 = 114;
16const CATEGORY_COLOR_SCHEME: u16 = 112;
17const CATEGORY_SPLASH_SCREEN: u16 = 708;
18const CATEGORY_SDDM_THEME: u16 = 101;
19const CATEGORY_ICON_THEME: u16 = 132;
20const CATEGORY_WALLPAPER: u16 = 299;
21
22/// Type of KDE Plasma component.
23///
24/// Maps to KDE Store category IDs and determines installation paths,
25/// registry files, and update strategies.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
27#[serde(rename_all = "snake_case")]
28pub enum ComponentType {
29    PlasmaWidget,
30    WallpaperPlugin,
31    KWinEffect,
32    KWinScript,
33    KWinSwitcher,
34    GlobalTheme,
35    PlasmaStyle,
36    AuroraeDecoration,
37    ColorScheme,
38    SplashScreen,
39    SddmTheme,
40    IconTheme,
41    Wallpaper,
42}
43
44impl ComponentType {
45    pub(crate) const fn category_id(self) -> u16 {
46        match self {
47            Self::PlasmaWidget => CATEGORY_PLASMA_WIDGET,
48            Self::WallpaperPlugin => CATEGORY_WALLPAPER_PLUGIN,
49            Self::KWinEffect => CATEGORY_KWIN_EFFECT,
50            Self::KWinScript => CATEGORY_KWIN_SCRIPT,
51            Self::KWinSwitcher => CATEGORY_KWIN_SWITCHER,
52            Self::GlobalTheme => CATEGORY_GLOBAL_THEME,
53            Self::PlasmaStyle => CATEGORY_PLASMA_STYLE,
54            Self::AuroraeDecoration => CATEGORY_AURORAE_DECORATION,
55            Self::ColorScheme => CATEGORY_COLOR_SCHEME,
56            Self::SplashScreen => CATEGORY_SPLASH_SCREEN,
57            Self::SddmTheme => CATEGORY_SDDM_THEME,
58            Self::IconTheme => CATEGORY_ICON_THEME,
59            Self::Wallpaper => CATEGORY_WALLPAPER,
60        }
61    }
62
63    /// Returns true if the given store `type_id` belongs to this component type.
64    ///
65    /// The OCS API returns subcategory IDs in the `typeid` field. For example,
66    /// PlasmaWidget queries with parent category 705 but store entries have
67    /// specific subcategory IDs (706 "Applets", 708 "Clocks", 710 "Monitoring",
68    /// etc.). This method accounts for those parent-child relationships.
69    pub(crate) const fn matches_type_id(self, type_id: u16) -> bool {
70        if self.category_id() == type_id {
71            return true;
72        }
73        // PlasmaWidget (705) is the parent of subcategories 706-713, 723
74        matches!((self, type_id), (Self::PlasmaWidget, 706..=713 | 723))
75    }
76
77    pub(crate) const fn kpackage_type(self) -> Option<&'static str> {
78        match self {
79            Self::PlasmaWidget => Some("Plasma/Applet"),
80            Self::WallpaperPlugin => Some("Plasma/Wallpaper"),
81            Self::KWinEffect => Some("KWin/Effect"),
82            Self::KWinScript => Some("KWin/Script"),
83            Self::KWinSwitcher => Some("KWin/WindowSwitcher"),
84            _ => None,
85        }
86    }
87
88    /// Returns true if this type uses registry-based discovery only
89    /// (no metadata files on disk).
90    pub(crate) const fn registry_only(self) -> bool {
91        matches!(self, Self::IconTheme | Self::Wallpaper | Self::ColorScheme)
92    }
93
94    // -- Filesystem paths --
95
96    /// Returns the user-local data directory suffix, or `None` for system-only types (e.g., SDDM).
97    pub(crate) const fn user_suffix(self) -> Option<&'static str> {
98        match self {
99            Self::PlasmaWidget => Some("plasma/plasmoids"),
100            Self::WallpaperPlugin => Some("plasma/wallpapers"),
101            Self::KWinEffect => Some("kwin/effects"),
102            Self::KWinScript => Some("kwin/scripts"),
103            Self::KWinSwitcher => Some("kwin/tabbox"),
104            Self::GlobalTheme | Self::SplashScreen => Some("plasma/look-and-feel"),
105            Self::PlasmaStyle => Some("plasma/desktoptheme"),
106            Self::AuroraeDecoration => Some("aurorae/themes"),
107            Self::ColorScheme => Some("color-schemes"),
108            Self::SddmTheme => None,
109            Self::IconTheme => Some("icons"),
110            Self::Wallpaper => Some("wallpapers"),
111        }
112    }
113
114    /// Returns the full user-local installation path for this component type.
115    pub fn user_path(self) -> PathBuf {
116        match self.user_suffix() {
117            Some(suffix) => crate::paths::data_home().join(suffix),
118            None => PathBuf::new(),
119        }
120    }
121
122    /// Returns the system-wide installation path for this component type.
123    pub fn system_path(self) -> PathBuf {
124        PathBuf::from(match self {
125            Self::PlasmaWidget => "/usr/share/plasma/plasmoids",
126            Self::WallpaperPlugin => "/usr/share/plasma/wallpapers",
127            Self::KWinEffect => "/usr/share/kwin/effects",
128            Self::KWinScript => "/usr/share/kwin/scripts",
129            Self::KWinSwitcher => "/usr/share/kwin/tabbox",
130            Self::GlobalTheme | Self::SplashScreen => "/usr/share/plasma/look-and-feel",
131            Self::PlasmaStyle => "/usr/share/plasma/desktoptheme",
132            Self::AuroraeDecoration => "/usr/share/aurorae/themes",
133            Self::ColorScheme => "/usr/share/color-schemes",
134            Self::SddmTheme => "/usr/share/sddm/themes",
135            Self::IconTheme => "/usr/share/icons",
136            Self::Wallpaper => "/usr/share/wallpapers",
137        })
138    }
139
140    /// Returns the backup subdirectory name for this component type.
141    pub(crate) const fn backup_subdir(self) -> &'static str {
142        match self {
143            Self::PlasmaWidget => "plasma-plasmoids",
144            Self::WallpaperPlugin => "plasma-wallpapers",
145            Self::KWinEffect => "kwin-effects",
146            Self::KWinScript => "kwin-scripts",
147            Self::KWinSwitcher => "kwin-tabbox",
148            Self::GlobalTheme => "plasma-look-and-feel",
149            Self::PlasmaStyle => "plasma-desktoptheme",
150            Self::AuroraeDecoration => "aurorae-themes",
151            Self::ColorScheme => "color-schemes",
152            Self::SplashScreen => "plasma-splash",
153            Self::SddmTheme => "sddm-themes",
154            Self::IconTheme => "icons",
155            Self::Wallpaper => "wallpapers",
156        }
157    }
158
159    // -- Registry --
160
161    pub(crate) const fn registry_file(self) -> Option<&'static str> {
162        match self {
163            Self::PlasmaWidget => Some("plasmoids.knsregistry"),
164            Self::KWinEffect => Some("kwineffect.knsregistry"),
165            Self::KWinScript => Some("kwinscripts.knsregistry"),
166            Self::KWinSwitcher => Some("kwinswitcher.knsregistry"),
167            Self::WallpaperPlugin => Some("wallpaperplugin.knsregistry"),
168            Self::GlobalTheme => Some("lookandfeel.knsregistry"),
169            Self::PlasmaStyle => Some("plasma-themes.knsregistry"),
170            Self::AuroraeDecoration => Some("aurorae.knsregistry"),
171            Self::ColorScheme => Some("colorschemes.knsregistry"),
172            Self::SplashScreen => Some("ksplash.knsregistry"),
173            Self::SddmTheme => Some("sddmtheme.knsregistry"),
174            Self::IconTheme => Some("icons.knsregistry"),
175            Self::Wallpaper => Some("wallpaper.knsregistry"),
176        }
177    }
178
179    // -- Enumeration --
180
181    pub const fn all() -> &'static [ComponentType] {
182        &[
183            Self::PlasmaWidget,
184            Self::WallpaperPlugin,
185            Self::KWinEffect,
186            Self::KWinScript,
187            Self::KWinSwitcher,
188            Self::GlobalTheme,
189            Self::PlasmaStyle,
190            Self::AuroraeDecoration,
191            Self::ColorScheme,
192            Self::SplashScreen,
193            Self::SddmTheme,
194            Self::IconTheme,
195            Self::Wallpaper,
196        ]
197    }
198
199    pub const fn all_user() -> &'static [ComponentType] {
200        &[
201            Self::PlasmaWidget,
202            Self::WallpaperPlugin,
203            Self::KWinEffect,
204            Self::KWinScript,
205            Self::KWinSwitcher,
206            Self::GlobalTheme,
207            Self::PlasmaStyle,
208            Self::AuroraeDecoration,
209            Self::ColorScheme,
210            Self::SplashScreen,
211            Self::IconTheme,
212            Self::Wallpaper,
213        ]
214    }
215}
216
217impl std::fmt::Display for ComponentType {
218    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
219        match self {
220            Self::PlasmaWidget => write!(f, "Plasma Widget"),
221            Self::WallpaperPlugin => write!(f, "Wallpaper Plugin"),
222            Self::KWinEffect => write!(f, "KWin Effect"),
223            Self::KWinScript => write!(f, "KWin Script"),
224            Self::KWinSwitcher => write!(f, "KWin Switcher"),
225            Self::GlobalTheme => write!(f, "Global Theme"),
226            Self::PlasmaStyle => write!(f, "Plasma Style"),
227            Self::AuroraeDecoration => write!(f, "Aurorae Decoration"),
228            Self::ColorScheme => write!(f, "Color Scheme"),
229            Self::SplashScreen => write!(f, "Splash Screen"),
230            Self::SddmTheme => write!(f, "SDDM Theme"),
231            Self::IconTheme => write!(f, "Icon Theme"),
232            Self::Wallpaper => write!(f, "Wallpaper"),
233        }
234    }
235}
236
237// -- Internal types --
238
239/// A KDE component installed on the local system.
240#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct InstalledComponent {
242    pub name: String,
243    pub directory_name: String,
244    pub version: String,
245    pub component_type: ComponentType,
246    #[serde(with = "pathbuf_serde")]
247    pub path: PathBuf,
248    pub is_system: bool,
249    pub release_date: String,
250}
251
252/// An available update for an installed component, with download metadata.
253#[derive(Debug, Clone, Serialize, Deserialize)]
254pub struct AvailableUpdate {
255    pub installed: InstalledComponent,
256    pub content_id: u64,
257    pub latest_version: String,
258    pub download_url: String,
259    pub store_url: String,
260    pub release_date: String,
261    #[serde(skip_serializing_if = "Option::is_none")]
262    pub checksum: Option<String>,
263    #[serde(skip_serializing_if = "Option::is_none")]
264    pub download_size: Option<u64>,
265}
266
267/// Builder for constructing [`AvailableUpdate`] instances with optional fields.
268pub(crate) struct AvailableUpdateBuilder {
269    installed: InstalledComponent,
270    content_id: u64,
271    latest_version: String,
272    download_url: String,
273    release_date: String,
274    checksum: Option<String>,
275    download_size: Option<u64>,
276}
277
278impl AvailableUpdateBuilder {
279    pub(crate) fn checksum(mut self, checksum: Option<String>) -> Self {
280        self.checksum = checksum;
281        self
282    }
283
284    pub(crate) fn download_size(mut self, size: Option<u64>) -> Self {
285        self.download_size = size;
286        self
287    }
288
289    pub(crate) fn build(self) -> AvailableUpdate {
290        let store_url = format!("https://store.kde.org/p/{}", self.content_id);
291        AvailableUpdate {
292            installed: self.installed,
293            content_id: self.content_id,
294            latest_version: self.latest_version,
295            download_url: self.download_url,
296            store_url,
297            release_date: self.release_date,
298            checksum: self.checksum,
299            download_size: self.download_size,
300        }
301    }
302}
303
304impl AvailableUpdate {
305    pub(crate) fn builder(
306        installed: InstalledComponent,
307        content_id: u64,
308        latest_version: String,
309        download_url: String,
310        release_date: String,
311    ) -> AvailableUpdateBuilder {
312        AvailableUpdateBuilder {
313            installed,
314            content_id,
315            latest_version,
316            download_url,
317            release_date,
318            checksum: None,
319            download_size: None,
320        }
321    }
322}
323
324/// An entry from the KDE Store API representing a published component.
325#[derive(Debug, Clone)]
326pub(crate) struct StoreEntry {
327    pub id: u64,
328    pub name: String,
329    pub version: String,
330    pub type_id: u16,
331    pub download_links: Vec<DownloadLink>,
332    pub changed_date: String,
333}
334
335/// A download link for a store entry, with optional checksum and size.
336#[derive(Debug, Clone)]
337pub(crate) struct DownloadLink {
338    pub url: String,
339    pub version: String,
340    pub checksum: Option<String>,
341    pub size_kb: Option<u64>,
342}
343
344/// Metadata parsed from a component's `metadata.json` or `metadata.desktop` file.
345#[derive(Debug, Clone, Default, Deserialize)]
346pub(crate) struct PackageMetadata {
347    #[serde(rename = "KPlugin")]
348    pub kplugin: Option<KPluginInfo>,
349}
350
351/// Plugin metadata from the `KPlugin` section of `metadata.json`.
352#[derive(Debug, Clone, Default, Deserialize, Serialize)]
353pub(crate) struct KPluginInfo {
354    #[serde(rename = "Name")]
355    pub name: Option<String>,
356    #[serde(rename = "Version")]
357    pub version: Option<String>,
358    #[serde(rename = "Description")]
359    pub description: Option<String>,
360    #[serde(rename = "Icon")]
361    pub icon: Option<String>,
362}
363
364impl PackageMetadata {
365    pub(crate) fn name(&self) -> Option<&str> {
366        self.kplugin.as_ref()?.name.as_deref()
367    }
368
369    pub(crate) fn version(&self) -> Option<&str> {
370        self.kplugin.as_ref()?.version.as_deref()
371    }
372}
373
374/// Diagnostic information about a component that could not be checked or updated.
375///
376/// Returned as part of [`CheckResult::diagnostics`](crate::CheckResult::diagnostics).
377/// Contains the component name, the reason it was skipped, and optional version/ID metadata.
378#[derive(Debug, Clone, Serialize, Deserialize)]
379pub struct Diagnostic {
380    pub name: String,
381    pub reason: String,
382    #[serde(skip_serializing_if = "Option::is_none")]
383    pub installed_version: Option<String>,
384    #[serde(skip_serializing_if = "Option::is_none")]
385    pub available_version: Option<String>,
386    #[serde(skip_serializing_if = "Option::is_none")]
387    pub content_id: Option<u64>,
388}
389
390impl Diagnostic {
391    pub(crate) fn new(name: String, reason: String) -> Self {
392        Self {
393            name,
394            reason,
395            installed_version: None,
396            available_version: None,
397            content_id: None,
398        }
399    }
400
401    pub(crate) fn with_versions(
402        mut self,
403        installed: Option<String>,
404        available: Option<String>,
405    ) -> Self {
406        self.installed_version = installed;
407        self.available_version = available;
408        self
409    }
410
411    pub(crate) fn with_content_id(mut self, id: u64) -> Self {
412        self.content_id = Some(id);
413        self
414    }
415}
416
417/// Internal result of checking for available updates, including diagnostics.
418#[derive(Debug, Clone, Default, Serialize, Deserialize)]
419pub(crate) struct UpdateCheckResult {
420    pub updates: Vec<AvailableUpdate>,
421    pub unresolved: Vec<Diagnostic>,
422    pub check_failures: Vec<Diagnostic>,
423}
424
425impl UpdateCheckResult {
426    pub fn add_update(&mut self, update: AvailableUpdate) {
427        self.updates.push(update);
428    }
429
430    pub fn add_unresolved(&mut self, diagnostic: Diagnostic) {
431        self.unresolved.push(diagnostic);
432    }
433
434    pub fn add_check_failure(&mut self, diagnostic: Diagnostic) {
435        self.check_failures.push(diagnostic);
436    }
437}
438
439mod pathbuf_serde {
440    use std::path::{Path, PathBuf};
441
442    use serde::{self, Deserialize, Deserializer, Serializer};
443
444    pub fn serialize<S>(path: &Path, serializer: S) -> Result<S::Ok, S::Error>
445    where
446        S: Serializer,
447    {
448        serializer.serialize_str(&path.to_string_lossy())
449    }
450
451    pub fn deserialize<'de, D>(deserializer: D) -> Result<PathBuf, D::Error>
452    where
453        D: Deserializer<'de>,
454    {
455        let s = String::deserialize(deserializer)?;
456        Ok(PathBuf::from(s))
457    }
458}