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}