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