Skip to main content

libplasmoid_updater/
config.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2
3use std::collections::HashMap;
4
5/// Default embedded widgets-id mapping file provided by Apdatifier.
6///
7/// This file maps component directory names to KDE Store content IDs
8/// and is used as a fallback when other resolution methods fail.
9const DEFAULT_WIDGETS_ID: &str = include_str!("../widgets-id");
10
11/// Controls plasmashell restart behavior after updates.
12#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
13pub enum RestartBehavior {
14    /// Never restart plasmashell (default).
15    #[default]
16    Never,
17    /// Always restart plasmashell after successful updates that require it.
18    Always,
19    /// Prompt the user interactively. Falls back to [`Never`](Self::Never) if
20    /// stdin is not a terminal.
21    Prompt,
22}
23
24/// Configuration for libplasmoid-updater operations.
25///
26/// This struct contains all configuration options used by the library.
27/// Library consumers (like topgrade or other automation tools) can construct
28/// this directly without needing config file parsing.
29///
30/// # Examples
31///
32/// ## Basic Configuration
33///
34/// ```rust
35/// use libplasmoid_updater::Config;
36///
37/// let config = Config::new();
38/// ```
39///
40/// ## With Custom Settings
41///
42/// ```rust
43/// use libplasmoid_updater::{Config, RestartBehavior};
44/// use std::collections::HashMap;
45///
46/// let mut widgets_table = HashMap::new();
47/// widgets_table.insert("com.example.widget".to_string(), 123456);
48///
49/// let config = Config::new()
50///     .with_excluded_packages(vec!["problematic-widget".to_string()])
51///     .with_widgets_id_table(widgets_table)
52///     .with_restart(RestartBehavior::Always);
53/// ```
54#[derive(Debug, Clone, Default)]
55pub struct Config {
56    /// If `true`, operate on system-wide components (in `/usr/share`).
57    /// If `false` (default), operate on user components (in `~/.local/share`).
58    /// System operations require root privileges.
59    pub system: bool,
60
61    /// Packages to exclude from updates.
62    ///
63    /// Can match either directory names (e.g., "org.kde.plasma.systemmonitor")
64    /// or display names (e.g., "System Monitor"). Components in this list
65    /// will be skipped during update operations.
66    pub excluded_packages: Vec<String>,
67
68    /// Widget ID fallback table mapping directory names to KDE Store content IDs.
69    ///
70    /// This table is used as a fallback when content ID resolution via KNewStuff
71    /// registry or exact name matching fails. The library uses a three-tier
72    /// resolution strategy:
73    ///
74    /// 1. KNewStuff registry lookup (most reliable)
75    /// 2. Exact name match from KDE Store API
76    /// 3. Fallback to this widgets_id_table
77    ///
78    /// # Format
79    ///
80    /// - Key: Component directory name (e.g., "org.kde.plasma.systemmonitor")
81    /// - Value: KDE Store content ID (numeric)
82    ///
83    /// The CLI application loads this from a `widgets-id` file, but library
84    /// consumers can provide it programmatically or leave it empty.
85    pub widgets_id_table: HashMap<String, u64>,
86
87    /// Controls plasmashell restart behavior after successful updates.
88    pub restart: RestartBehavior,
89
90    /// When `true`, skip interactive prompts and apply all non-excluded updates
91    /// automatically. Has no effect without the `cli` feature.
92    pub auto_confirm: bool,
93
94    /// Maximum number of parallel installation threads.
95    ///
96    /// `None` (default) uses the number of logical CPU threads available.
97    /// `Some(n)` pins the pool to exactly `n` threads.
98    pub threads: Option<usize>,
99}
100
101impl Config {
102    /// Creates a new configuration with default values.
103    ///
104    /// Default values:
105    /// - `system`: false (user components)
106    /// - `excluded_packages`: empty
107    /// - `widgets_id_table`: loaded from embedded widgets-id file
108    /// - `restart`: [`RestartBehavior::Never`]
109    ///
110    /// The embedded widgets-id table provides fallback content ID mappings
111    /// for components that cannot be resolved via KNewStuff registry or
112    /// exact name matching.
113    pub fn new() -> Self {
114        Self {
115            widgets_id_table: Self::parse_widgets_id(DEFAULT_WIDGETS_ID),
116            ..Default::default()
117        }
118    }
119
120    /// Sets whether to operate on system-wide components.
121    ///
122    /// When true, the library scans and updates components in `/usr/share`
123    /// instead of `~/.local/share`. System operations require root privileges.
124    ///
125    /// # Example
126    ///
127    /// ```rust
128    /// use libplasmoid_updater::Config;
129    ///
130    /// let config = Config::new().with_system(true);
131    /// ```
132    pub fn with_system(mut self, system: bool) -> Self {
133        self.system = system;
134        self
135    }
136
137    /// Sets the widgets ID fallback table.
138    ///
139    /// This table maps component directory names to KDE Store content IDs
140    /// and is used as a fallback when other resolution methods fail.
141    ///
142    /// # Arguments
143    ///
144    /// * `table` - HashMap mapping directory names to content IDs
145    ///
146    /// # Example
147    ///
148    /// ```rust
149    /// use libplasmoid_updater::Config;
150    /// use std::collections::HashMap;
151    ///
152    /// let mut table = HashMap::new();
153    /// table.insert("org.kde.plasma.systemmonitor".to_string(), 998890);
154    ///
155    /// let config = Config::new().with_widgets_id_table(table);
156    /// ```
157    pub fn with_widgets_id_table(mut self, table: HashMap<String, u64>) -> Self {
158        self.widgets_id_table = table;
159        self
160    }
161
162    /// Sets the list of Plasmoids to exclude from updates.
163    ///
164    /// Components in this list will be skipped during updates.
165    /// The list can contain either directory names or display names.
166    ///
167    /// # Arguments
168    ///
169    /// * `packages` - Vector of package names to exclude
170    ///
171    /// # Example
172    ///
173    /// ```rust
174    /// use libplasmoid_updater::Config;
175    ///
176    /// let config = Config::new()
177    ///     .with_excluded_packages(vec![
178    ///         "org.kde.plasma.systemmonitor".to_string(),
179    ///         "Problematic Widget".to_string(),
180    ///     ]);
181    /// ```
182    pub fn with_excluded_packages(mut self, packages: Vec<String>) -> Self {
183        self.excluded_packages = packages;
184        self
185    }
186
187    /// Sets the plasmashell restart behavior after updates.
188    ///
189    /// # Example
190    ///
191    /// ```rust
192    /// use libplasmoid_updater::{Config, RestartBehavior};
193    ///
194    /// let config = Config::new().with_restart(RestartBehavior::Always);
195    /// ```
196    pub fn with_restart(mut self, restart: RestartBehavior) -> Self {
197        self.restart = restart;
198        self
199    }
200
201    /// Parses a widgets-id table from a string.
202    ///
203    /// The format is one entry per line: `content_id directory_name`
204    /// Lines starting with `#` are comments.
205    pub fn parse_widgets_id(content: &str) -> HashMap<String, u64> {
206        let mut table = HashMap::with_capacity(content.lines().count());
207        for line in content.lines() {
208            if let Some((id, name)) = parse_widgets_id_line(line) {
209                table.insert(name, id);
210            }
211        }
212        table
213    }
214
215    pub fn with_auto_confirm(mut self, auto_confirm: bool) -> Self {
216        self.auto_confirm = auto_confirm;
217        self
218    }
219
220    pub fn with_threads(mut self, threads: usize) -> Self {
221        self.threads = Some(threads);
222        self
223    }
224}
225
226pub(crate) fn parse_widgets_id_line(line: &str) -> Option<(u64, String)> {
227    let line = line.trim();
228    if line.is_empty() || line.starts_with('#') {
229        return None;
230    }
231
232    let mut parts = line.splitn(2, ' ');
233    let id = parts.next()?.parse::<u64>().ok()?;
234    let name = parts.next()?.trim();
235    if name.is_empty() {
236        return None;
237    }
238    Some((id, name.to_string()))
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244
245    #[test]
246    fn test_parse_widgets_id_line_valid() {
247        let line = "998890 com.bxabi.bumblebee-indicator";
248        let result = parse_widgets_id_line(line);
249        assert_eq!(
250            result,
251            Some((998890, "com.bxabi.bumblebee-indicator".to_string()))
252        );
253    }
254
255    #[test]
256    fn test_parse_widgets_id_line_comment() {
257        let line = "#2182964 adhe.menu.11 #Ignored, not a unique ID";
258        let result = parse_widgets_id_line(line);
259        assert_eq!(result, None);
260    }
261
262    #[test]
263    fn test_parse_widgets_id_line_empty() {
264        let line = "";
265        let result = parse_widgets_id_line(line);
266        assert_eq!(result, None);
267    }
268
269    #[test]
270    fn test_parse_widgets_id_table() {
271        let content = "998890 com.bxabi.bumblebee-indicator\n\
272                       998913 org.kde.plasma.awesomewidget\n\
273                       # Comment line\n\
274                       1155946 com.dschopf.plasma.qalculate\n";
275        let table = Config::parse_widgets_id(content);
276        assert_eq!(table.len(), 3);
277        assert_eq!(table.get("com.bxabi.bumblebee-indicator"), Some(&998890));
278        assert_eq!(table.get("org.kde.plasma.awesomewidget"), Some(&998913));
279        assert_eq!(table.get("com.dschopf.plasma.qalculate"), Some(&1155946));
280    }
281
282    #[test]
283    fn test_default_widgets_id_table_loads() {
284        let config = Config::new();
285        // Verify the embedded file is loaded and contains expected entries
286        assert!(
287            !config.widgets_id_table.is_empty(),
288            "Default widgets_id_table should not be empty"
289        );
290        // Check for a few known entries from the widgets-id file
291        assert_eq!(
292            config.widgets_id_table.get("com.bxabi.bumblebee-indicator"),
293            Some(&998890)
294        );
295        assert_eq!(
296            config.widgets_id_table.get("org.kde.plasma.awesomewidget"),
297            Some(&998913)
298        );
299    }
300
301    #[test]
302    fn test_config_with_custom_widgets_id_table() {
303        let mut custom_table = HashMap::new();
304        custom_table.insert("custom.widget".to_string(), 123456);
305
306        let config = Config::new().with_widgets_id_table(custom_table.clone());
307
308        // Should use custom table, not default
309        assert_eq!(config.widgets_id_table, custom_table);
310        assert_eq!(config.widgets_id_table.get("custom.widget"), Some(&123456));
311        // Default entry should not be present
312        assert_eq!(
313            config.widgets_id_table.get("com.bxabi.bumblebee-indicator"),
314            None
315        );
316    }
317}