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