i3status_rs/blocks/packages.rs
1//! Pending updates for different package manager like apt, pacman, etc.
2//!
3//! Currently, these package managers are supported:
4//! - `apk` for Alpine Linux
5//! - `apt` for Debian/Ubuntu-based systems
6//! - `aur` for Arch-based systems
7//! - `brew` for the Homebrew Package Manager
8//! - `dnf` for Fedora-based systems
9//! - `flatpak` for Flatpak packages
10//! - `pacman` for Arch-based systems
11//! - `snap` for Snap packages
12//! - `xbps` for Void Linux
13//!
14//! # Configuration
15//!
16//! Key | Values | Default
17//! ----|--------|--------
18//! `interval` | Update interval in seconds. | `600`
19//! `package_manager` | Package manager to check for updates | Automatically derived from format templates, but can be used to influence the `$total` value
20//! `format` | A string to customise the output of this block. See below for available placeholders. | `" $icon $total.eng(w:1) "`
21//! `format_singular` | Same as `format`, but for when exactly one update is available. | `" $icon $total.eng(w:1) "`
22//! `format_up_to_date` | Same as `format`, but for when no updates are available. | `" $icon $total.eng(w:1) "`
23//! `warning_updates_regex` | Display block as warning if updates matching regex are available. | `None`
24//! `critical_updates_regex` | Display block as critical if updates matching regex are available. | `None`
25//! `ignore_updates_regex` | Doesn't include updates matching regex in the count. | `None`
26//! `ignore_phased_updates` | Doesn't include potentially held back phased updates in the count. (For Debian/Ubuntu-based systems) | `false`
27//! `aur_command` | AUR command to check available updates, which outputs in the same format as pacman. E.g. `yay -Qua` (For Arch-based systems) | Required if `$aur` is used
28//!
29//! Placeholder | Value | Type | Unit
30//! -------------|----------------------------------------------------------------------------------|--------|-----
31//! `icon` | A static icon | Icon | -
32//! `apk` | Number of updates available in Alpine Linux | Number | -
33//! `apt` | Number of updates available in Debian/Ubuntu-based systems | Number | -
34//! `aur` | Number of updates available in Arch-based systems | Number | -
35//! `brew` | Number of updates available in the Homebrew Package Manager | Number | -
36//! `dnf` | Number of updates available in Fedora-based systems | Number | -
37//! `flatpak` | Number of updates available in Flatpak packages | Number | -
38//! `pacman` | Number of updates available in Arch-based systems | Number | -
39//! `snap` | Number of updates available in Snap packages | Number | -
40//! `xbps` | Number of updates available in Void Linux | Number | -
41//! `total` | Number of updates available in all package manager listed | Number | -
42//!
43//! # Apt
44//!
45//! Behind the scenes this uses `apt`, and in order to run it without root privileges i3status-rust will create its own package database in `/tmp/i3rs-apt/` which may take up several MB or more. If you have a custom apt config then this block may not work as expected - in that case please open an issue.
46//!
47//! Tip: You can grab the list of available updates using `APT_CONFIG=/tmp/i3rs-apt/apt.conf apt list --upgradable`
48//!
49//! # Pacman
50//!
51//! Requires fakeroot to be installed (only required for pacman).
52//!
53//! Tip: You can grab the list of available updates using `fakeroot pacman -Qu --dbpath /tmp/checkup-db-i3statusrs-$USER/`.
54//! If you have the `CHECKUPDATES_DB` env var set on your system then substitute that dir instead.
55//!
56//! Note: `pikaur` may hang the whole block if there is no internet connectivity [reference](https://github.com/actionless/pikaur/issues/595). In that case, try a different AUR helper.
57//!
58//! ### Pacman hook
59//!
60//! Tip: On Arch Linux you can setup a `pacman` hook to signal i3status-rs to update after packages
61//! have been upgraded, so you won't have stale info in your pacman block.
62//!
63//! In the block configuration, set `signal = 1` (or another number if `1` is being used by some
64//! other block):
65//!
66//! ```toml
67//! [[block]]
68//! block = "packages"
69//! signal = 1
70//! ```
71//!
72//! Create `/etc/pacman.d/hooks/i3status-rust.hook` with the below contents:
73//!
74//! ```ini
75//! [Trigger]
76//! Operation = Upgrade
77//! Type = Package
78//! Target = *
79//!
80//! [Action]
81//! When = PostTransaction
82//! Exec = /usr/bin/pkill -SIGRTMIN+1 i3status-rs
83//! ```
84//!
85//! # Example
86//!
87//! Apk-only config:
88//!
89//! ```toml
90//! [[block]]
91//! block = "packages"
92//! package_manager = ["apk"]
93//! interval = 1800
94//! error_interval = 300
95//! max_retries = 5
96//! format = " $icon $apk.eng(w:1) updates available "
97//! format_singular = " $icon One update available "
98//! format_up_to_date = " $icon system up to date "
99//! [[block.click]]
100//! # shows dmenu with available updates. Any dmenu alternative should also work.
101//! button = "left"
102//! cmd = "apk --no-cache --upgradable list | dmenu -l 10"
103//! ```
104//!
105//! Apt-only config
106//!
107//! ```toml
108//! [[block]]
109//! block = "packages"
110//! interval = 1800
111//! error_interval = 300
112//! max_retries = 5
113//! package_manager = ["apt"]
114//! format = " $icon $apt updates available"
115//! format_singular = " $icon One update available "
116//! format_up_to_date = " $icon system up to date "
117//! [[block.click]]
118//! # shows dmenu with cached available updates. Any dmenu alternative should also work.
119//! button = "left"
120//! cmd = "APT_CONFIG=/tmp/i3rs-apt/apt.conf apt list --upgradable | tail -n +2 | rofi -dmenu"
121//! [[block.click]]
122//! # Updates the block on right click
123//! button = "right"
124//! update = true
125//! ```
126//!
127//! Brew-only config:
128//!
129//! ```toml
130//! [[block]]
131//! block = "packages"
132//! package_manager = ["brew"]
133//! interval = 1800
134//! error_interval = 300
135//! max_retries = 5
136//! format = " $icon $brew.eng(w:1) updates available "
137//! format_singular = " $icon One update available "
138//! format_up_to_date = " $icon system up to date "
139//! [[block.click]]
140//! # shows dmenu with available updates. Any dmenu alternative should also work.
141//! button = "left"
142//! cmd = "brew outdated | dmenu -l 10"
143//! ```
144//!
145//! Dnf-only config:
146//!
147//! ```toml
148//! [[block]]
149//! block = "packages"
150//! package_manager = ["dnf"]
151//! interval = 1800
152//! error_interval = 300
153//! max_retries = 5
154//! format = " $icon $dnf.eng(w:1) updates available "
155//! format_singular = " $icon One update available "
156//! format_up_to_date = " $icon system up to date "
157//! [[block.click]]
158//! # shows dmenu with cached available updates. Any dmenu alternative should also work.
159//! button = "left"
160//! cmd = "dnf list -q --upgrades | tail -n +2 | rofi -dmenu"
161//! ```
162//!
163//! Flatpak-only config:
164//!
165//! ```toml
166//! [[block]]
167//! block = "packages"
168//! package_manager = ["flatpak"]
169//! interval = 1800
170//! error_interval = 300
171//! max_retries = 5
172//! format = " $icon $flatpak.eng(w:1) updates available "
173//! format_singular = " $icon One update available "
174//! format_up_to_date = " $icon system up to date "
175//! [[block.click]]
176//! # shows dmenu with cached available updates. Any dmenu alternative should also work.
177//! button = "left"
178//! cmd = "flatpak remote-ls --updates --columns=ref | rofi -dmenu"
179//! ```
180//!
181//! Pacman-only config:
182//!
183//! ```toml
184//! [[block]]
185//! block = "packages"
186//! package_manager = ["pacman"]
187//! interval = 600
188//! error_interval = 300
189//! max_retries = 5
190//! format = " $icon $pacman updates available "
191//! format_singular = " $icon $pacman update available "
192//! format_up_to_date = " $icon system up to date "
193//! [[block.click]]
194//! # pop-up a menu showing the available updates. Replace wofi with your favourite menu command.
195//! button = "left"
196//! cmd = "fakeroot pacman -Qu --dbpath /tmp/checkup-db-i3statusrs-$USER/ | wofi --show dmenu"
197//! [[block.click]]
198//! # Updates the block on right click
199//! button = "right"
200//! update = true
201//! ```
202//!
203//! Pacman and AUR helper config:
204//!
205//! ```toml
206//! [[block]]
207//! block = "packages"
208//! package_manager = ["pacman", "aur"]
209//! interval = 600
210//! error_interval = 300
211//! max_retries = 5
212//! format = " $icon $pacman + $aur = $total updates available "
213//! format_singular = " $icon $total update available "
214//! format_up_to_date = " $icon system up to date "
215//! # aur_command should output available updates to stdout (ie behave as echo -ne "update\n")
216//! aur_command = "yay -Qua"
217//! ```
218//!
219//! Snap-only config:
220//!
221//! ```toml
222//! [[block]]
223//! block = "packages"
224//! package_manager = ["snap"]
225//! interval = 1800
226//! error_interval = 300
227//! max_retries = 5
228//! format = " $icon $snap.eng(w:1) updates available "
229//! format_singular = " $icon One update available "
230//! format_up_to_date = " $icon system up to date "
231//! [[block.click]]
232//! # shows dmenu with available updates. Any dmenu alternative should also work.
233//! button = "left"
234//! cmd = "snap refresh --list | dmenu -l 10"
235//! ```
236//!
237//! Xbps-only config:
238//!
239//! ```toml
240//! [[block]]
241//! block = "packages"
242//! package_manager = ["xbps"]
243//! interval = 1800
244//! error_interval = 300
245//! max_retries = 5
246//! format = " $icon $xbps.eng(w:1) updates available "
247//! format_singular = " $icon One update available "
248//! format_up_to_date = " $icon system up to date "
249//! [[block.click]]
250//! # shows dmenu with available updates. Any dmenu alternative should also work.
251//! button = "left"
252//! cmd = "xbps-install -Mun | dmenu -l 10"
253//! ```
254//!
255//! Multiple package managers config:
256//!
257//! Update the list of pending updates every thirty minutes (1800 seconds):
258//!
259//! ```toml
260//! [[block]]
261//! block = "packages"
262//! package_manager = ["apk", "apt", "aur", "brew", "dnf", "flatpak", "pacman", "snap", "xbps"]
263//! interval = 1800
264//! error_interval = 300
265//! max_retries = 5
266//! format = " $icon $apk + $apt + $aur + $brew + $dnf + $flatpak + $pacman + $snap + $xbps = $total updates available "
267//! format_singular = " $icon One update available "
268//! format_up_to_date = " $icon system up to date "
269//! # If a linux update is available, but no ZFS package, it won't be possible to
270//! # actually perform a system upgrade, so we show a warning.
271//! warning_updates_regex = "(linux|linux-lts|linux-zen)"
272//! # If ZFS is available, we know that we can and should do an upgrade, so we show
273//! # the status as critical.
274//! critical_updates_regex = "(zfs|zfs-lts)"
275//! ```
276//!
277//! # Icons Used
278//!
279//! - `update`
280
281pub mod apk;
282use apk::Apk;
283
284pub mod apt;
285use apt::Apt;
286
287pub mod brew;
288use brew::Brew;
289
290pub mod dnf;
291use dnf::Dnf;
292
293pub mod flatpak;
294use flatpak::Flatpak;
295
296pub mod pacman;
297use pacman::{Aur, Pacman};
298
299pub mod xbps;
300use xbps::Xbps;
301
302pub mod snap;
303use snap::Snap;
304
305use regex::Regex;
306
307use super::prelude::*;
308
309#[derive(Deserialize, Debug, SmartDefault, Clone)]
310#[serde(deny_unknown_fields, default)]
311pub struct Config {
312 #[default(600.into())]
313 pub interval: Seconds,
314 pub package_manager: Vec<PackageManager>,
315 pub format: FormatConfig,
316 pub format_singular: FormatConfig,
317 pub format_up_to_date: FormatConfig,
318 pub warning_updates_regex: Option<String>,
319 pub critical_updates_regex: Option<String>,
320 pub ignore_updates_regex: Option<String>,
321 pub ignore_phased_updates: bool,
322 pub aur_command: Option<String>,
323}
324
325#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
326#[serde(rename_all = "lowercase")]
327pub enum PackageManager {
328 Apk,
329 Apt,
330 Aur,
331 Brew,
332 Dnf,
333 Flatpak,
334 Pacman,
335 Snap,
336 Xbps,
337}
338
339impl PackageManager {
340 /// The name of the package manager, as used in format strings.
341 fn name(&self) -> &'static str {
342 match self {
343 PackageManager::Apk => "apk",
344 PackageManager::Apt => "apt",
345 PackageManager::Aur => "aur",
346 PackageManager::Brew => "brew",
347 PackageManager::Dnf => "dnf",
348 PackageManager::Flatpak => "flatpak",
349 PackageManager::Pacman => "pacman",
350 PackageManager::Snap => "snap",
351 PackageManager::Xbps => "xbps",
352 }
353 }
354
355 /// Builds a backend for the package manager.
356 async fn build(&self, config: &Config) -> Result<Box<dyn Backend>> {
357 Ok(match self {
358 PackageManager::Apk => Box::new(Apk::new()),
359 PackageManager::Apt => Box::new(Apt::new(config.ignore_phased_updates).await?),
360 PackageManager::Aur => Box::new(Aur::new(
361 config.aur_command.clone().error("aur_command is not set")?,
362 )),
363 PackageManager::Brew => Box::new(Brew::new()),
364 PackageManager::Dnf => Box::new(Dnf::new()),
365 PackageManager::Flatpak => Box::new(Flatpak::new()),
366 PackageManager::Pacman => Box::new(Pacman::new().await?),
367 PackageManager::Snap => Box::new(Snap::new()),
368 PackageManager::Xbps => Box::new(Xbps::new()),
369 })
370 }
371}
372
373pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
374 let mut config: Config = config.clone();
375
376 let format = config.format.with_default(" $icon $total.eng(w:1) ")?;
377 let format_singular = config
378 .format_singular
379 .with_default(" $icon $total.eng(w:1) ")?;
380 let format_up_to_date = config
381 .format_up_to_date
382 .with_default(" $icon $total.eng(w:1) ")?;
383
384 // Check if the user specified a package manager in any format string, then
385 // add that package manager to the config list.
386 macro_rules! check_manager {
387 ($manager:expr) => {{
388 let name = $manager.name();
389 let in_format = format.contains_key(name)
390 || format_singular.contains_key(name)
391 || format_up_to_date.contains_key(name);
392
393 if !config.package_manager.contains(&$manager) && in_format {
394 config.package_manager.push($manager);
395 }
396 }};
397 }
398
399 check_manager!(PackageManager::Apk);
400 check_manager!(PackageManager::Apt);
401 check_manager!(PackageManager::Aur);
402 check_manager!(PackageManager::Brew);
403 check_manager!(PackageManager::Dnf);
404 check_manager!(PackageManager::Flatpak);
405 check_manager!(PackageManager::Pacman);
406 check_manager!(PackageManager::Snap);
407 check_manager!(PackageManager::Xbps);
408
409 let warning_updates_regex = config
410 .warning_updates_regex
411 .as_deref()
412 .map(Regex::new)
413 .transpose()
414 .error("invalid warning updates regex")?;
415 let critical_updates_regex = config
416 .critical_updates_regex
417 .as_deref()
418 .map(Regex::new)
419 .transpose()
420 .error("invalid critical updates regex")?;
421 let ignore_updates_regex = config
422 .ignore_updates_regex
423 .as_deref()
424 .map(Regex::new)
425 .transpose()
426 .error("invalid ignore updates regex")?;
427
428 let mut package_manager_vec: Vec<Box<dyn Backend>> = Vec::new();
429
430 for &package_manager in config.package_manager.iter() {
431 package_manager_vec.push(package_manager.build(&config).await?);
432 }
433
434 loop {
435 let mut package_manager_map: HashMap<Cow<'static, str>, Value> = HashMap::new();
436
437 let mut critical = false;
438 let mut warning = false;
439 let mut total_count = 0;
440
441 // Iterate over the all package manager listed in Config
442 for package_manager in &package_manager_vec {
443 let mut updates = package_manager.get_updates_list().await?;
444 if let Some(regex) = ignore_updates_regex.clone() {
445 updates.retain(|u| !regex.is_match(u));
446 }
447
448 let updates_count = updates.len();
449
450 package_manager_map.insert(package_manager.name(), Value::number(updates_count));
451 total_count += updates_count;
452
453 warning |= warning_updates_regex
454 .as_ref()
455 .is_some_and(|regex| has_matching_update(&updates, regex));
456 critical |= critical_updates_regex
457 .as_ref()
458 .is_some_and(|regex| has_matching_update(&updates, regex));
459 }
460
461 let mut widget = Widget::new();
462
463 package_manager_map.insert("icon".into(), Value::icon("update"));
464 package_manager_map.insert("total".into(), Value::number(total_count));
465
466 widget.set_format(match total_count {
467 0 => format_up_to_date.clone(),
468 1 => format_singular.clone(),
469 _ => format.clone(),
470 });
471 widget.set_values(package_manager_map);
472
473 widget.state = match total_count {
474 0 => State::Idle,
475 _ => {
476 if critical {
477 State::Critical
478 } else if warning {
479 State::Warning
480 } else {
481 State::Info
482 }
483 }
484 };
485 api.set_widget(widget)?;
486
487 select! {
488 _ = sleep(config.interval.0) => (),
489 _ = api.wait_for_update_request() => (),
490 }
491 }
492}
493
494#[async_trait]
495pub trait Backend {
496 fn name(&self) -> Cow<'static, str>;
497
498 async fn get_updates_list(&self) -> Result<Vec<String>>;
499}
500
501pub fn has_matching_update(updates: &[String], regex: &Regex) -> bool {
502 updates.iter().any(|line| regex.is_match(line))
503}