i3status_rs/blocks/
packages.rs

1//! Pending updates for different package manager like apt, pacman, etc.
2//!
3//! Currently these package managers are available:
4//! - `apt` for Debian/Ubuntu based system
5//! - `pacman` for Arch based system
6//! - `aur` for Arch based system
7//! - `dnf` for Fedora based system
8//! - `xbps` for Void Linux
9//!
10//! # Configuration
11//!
12//! Key | Values | Default
13//! ----|--------|--------
14//! `interval` | Update interval in seconds. | `600`
15//! `package_manager` | Package manager to check for updates | Automatically derived from format templates, but can be used to influence the `$total` value
16//! `format` | A string to customise the output of this block. See below for available placeholders. | `" $icon $total.eng(w:1) "`
17//! `format_singular` | Same as `format`, but for when exactly one update is available. | `" $icon $total.eng(w:1) "`
18//! `format_up_to_date` | Same as `format`, but for when no updates are available. | `" $icon $total.eng(w:1) "`
19//! `warning_updates_regex` | Display block as warning if updates matching regex are available. | `None`
20//! `critical_updates_regex` | Display block as critical if updates matching regex are available. | `None`
21//! `ignore_updates_regex` | Doesn't include updates matching regex in the count. | `None`
22//! `ignore_phased_updates` | Doesn't include potentially held back phased updates in the count. (For Debian/Ubuntu based system) | `false`
23//! `aur_command` | AUR command to check available updates, which outputs in the same format as pacman. e.g. `yay -Qua` (For Arch based system) | Required if `$aur` are used
24//!
25//!  Placeholder | Value                                                                            | Type   | Unit
26//! -------------|----------------------------------------------------------------------------------|--------|-----
27//! `icon`       | A static icon                                                                    | Icon   | -
28//! `apt`        | Number of updates available in Debian/Ubuntu based system                        | Number | -
29//! `pacman`     | Number of updates available in Arch based system                                 | Number | -
30//! `aur`        | Number of updates available in Arch based system                                 | Number | -
31//! `dnf`        | Number of updates available in Fedora based system                               | Number | -
32//! `xbps`       | Number of updates available in Void Linux                                        | Number | -
33//! `total`      | Number of updates available in all package manager listed                        | Number | -
34//!
35//! # Apt
36//!
37//! 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.
38//!
39//! Tip: You can grab the list of available updates using `APT_CONFIG=/tmp/i3rs-apt/apt.conf apt list --upgradable`
40//!
41//! # Pacman
42//!
43//! Requires fakeroot to be installed (only required for pacman).
44//!
45//! Tip: You can grab the list of available updates using `fakeroot pacman -Qu --dbpath /tmp/checkup-db-i3statusrs-$USER/`.
46//! If you have the `CHECKUPDATES_DB` env var set on your system then substitute that dir instead.
47//!
48//! 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.
49//!
50//! ### Pacman hook
51//!
52//! Tip: On Arch Linux you can setup a `pacman` hook to signal i3status-rs to update after packages
53//! have been upgraded, so you won't have stale info in your pacman block.
54//!
55//! In the block configuration, set `signal = 1` (or other number if `1` is being used by some
56//! other block):
57//!
58//! ```toml
59//! [[block]]
60//! block = "packages"
61//! signal = 1
62//! ```
63//!
64//! Create `/etc/pacman.d/hooks/i3status-rust.hook` with the below contents:
65//!
66//! ```ini
67//! [Trigger]
68//! Operation = Upgrade
69//! Type = Package
70//! Target = *
71//!
72//! [Action]
73//! When = PostTransaction
74//! Exec = /usr/bin/pkill -SIGRTMIN+1 i3status-rs
75//! ```
76//!
77//! # Example
78//!
79//! Apt only config
80//!
81//! ```toml
82//! [[block]]
83//! block = "packages"
84//! interval = 1800
85//! package_manager = ["apt"]
86//! format = " $icon $apt updates available"
87//! format_singular = " $icon One update available "
88//! format_up_to_date = " $icon system up to date "
89//! [[block.click]]
90//! # shows dmenu with cached available updates. Any dmenu alternative should also work.
91//! button = "left"
92//! cmd = "APT_CONFIG=/tmp/i3rs-apt/apt.conf apt list --upgradable | tail -n +2 | rofi -dmenu"
93//! [[block.click]]
94//! # Updates the block on right click
95//! button = "right"
96//! update = true
97//! ```
98//!
99//! Pacman only config:
100//!
101//! ```toml
102//! [[block]]
103//! block = "packages"
104//! package_manager = ["pacman"]
105//! interval = 600
106//! format = " $icon $pacman updates available "
107//! format_singular = " $icon $pacman update available "
108//! format_up_to_date = " $icon system up to date "
109//! [[block.click]]
110//! # pop-up a menu showing the available updates. Replace wofi with your favourite menu command.
111//! button = "left"
112//! cmd = "fakeroot pacman -Qu --dbpath /tmp/checkup-db-i3statusrs-$USER/ | wofi --show dmenu"
113//! [[block.click]]
114//! # Updates the block on right click
115//! button = "right"
116//! update = true
117//! ```
118//!
119//! Pacman and AUR helper config:
120//!
121//! ```toml
122//! [[block]]
123//! block = "packages"
124//! package_manager = ["pacman", "aur"]
125//! interval = 600
126//! error_interval = 300
127//! format = " $icon $pacman + $aur = $total updates available "
128//! format_singular = " $icon $total update available "
129//! format_up_to_date = " $icon system up to date "
130//! # aur_command should output available updates to stdout (ie behave as echo -ne "update\n")
131//! aur_command = "yay -Qua"
132//! ```
133//!
134//!
135//! Dnf only config:
136//!
137//! ```toml
138//! [[block]]
139//! block = "packages"
140//! package_manager = ["dnf"]
141//! interval = 1800
142//! format = " $icon $dnf.eng(w:1) updates available "
143//! format_singular = " $icon One update available "
144//! format_up_to_date = " $icon system up to date "
145//! [[block.click]]
146//! # shows dmenu with cached available updates. Any dmenu alternative should also work.
147//! button = "left"
148//! cmd = "dnf list -q --upgrades | tail -n +2 | rofi -dmenu"
149//! ```
150//!
151//!
152//! Xbps only config:
153//!
154//! ```toml
155//! [[block]]
156//! block = "packages"
157//! package_manager = ["xbps"]
158//! interval = 1800
159//! format = " $icon $xbps.eng(w:1) updates available "
160//! format_singular = " $icon One update available "
161//! format_up_to_date = " $icon system up to date "
162//! [[block.click]]
163//! # shows dmenu with available updates. Any dmenu alternative should also work.
164//! button = "left"
165//! cmd = "xbps-install -Mun | dmenu -l 10"
166//! ```
167//!
168//! Multiple package managers config:
169//!
170//! Update the list of pending updates every thirty minutes (1800 seconds):
171//!
172//! ```toml
173//! [[block]]
174//! block = "packages"
175//! package_manager = ["apt", "pacman", "aur", "dnf", "xbps"]
176//! interval = 1800
177//! format = " $icon $apt + $pacman + $aur + $dnf + $xbps = $total updates available "
178//! format_singular = " $icon One update available "
179//! format_up_to_date = " $icon system up to date "
180//! # If a linux update is available, but no ZFS package, it won't be possible to
181//! # actually perform a system upgrade, so we show a warning.
182//! warning_updates_regex = "(linux|linux-lts|linux-zen)"
183//! # If ZFS is available, we know that we can and should do an upgrade, so we show
184//! # the status as critical.
185//! critical_updates_regex = "(zfs|zfs-lts)"
186//! ```
187//!
188//! # Icons Used
189//!
190//! - `update`
191
192pub mod apt;
193use apt::Apt;
194
195pub mod pacman;
196use pacman::{Aur, Pacman};
197
198pub mod dnf;
199use dnf::Dnf;
200
201pub mod xbps;
202use xbps::Xbps;
203
204use regex::Regex;
205
206use super::prelude::*;
207
208#[derive(Deserialize, Debug, SmartDefault, Clone)]
209#[serde(deny_unknown_fields, default)]
210pub struct Config {
211    #[default(600.into())]
212    pub interval: Seconds,
213    pub package_manager: Vec<PackageManager>,
214    pub format: FormatConfig,
215    pub format_singular: FormatConfig,
216    pub format_up_to_date: FormatConfig,
217    pub warning_updates_regex: Option<String>,
218    pub critical_updates_regex: Option<String>,
219    pub ignore_updates_regex: Option<String>,
220    pub ignore_phased_updates: bool,
221    pub aur_command: Option<String>,
222}
223
224#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
225#[serde(rename_all = "lowercase")]
226pub enum PackageManager {
227    Apt,
228    Pacman,
229    Aur,
230    Dnf,
231    Xbps,
232}
233
234pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
235    let mut config: Config = config.clone();
236
237    let format = config.format.with_default(" $icon $total.eng(w:1) ")?;
238    let format_singular = config
239        .format_singular
240        .with_default(" $icon $total.eng(w:1) ")?;
241    let format_up_to_date = config
242        .format_up_to_date
243        .with_default(" $icon $total.eng(w:1) ")?;
244
245    // If user provide package manager in any of the formats then consider that also
246    macro_rules! any_format_contains {
247        ($name:expr) => {
248            format.contains_key($name)
249                || format_singular.contains_key($name)
250                || format_up_to_date.contains_key($name)
251        };
252    }
253
254    let apt = any_format_contains!("apt");
255    let aur = any_format_contains!("aur");
256    let pacman = any_format_contains!("pacman");
257    let dnf = any_format_contains!("dnf");
258    let xbps = any_format_contains!("xbps");
259
260    if !config.package_manager.contains(&PackageManager::Apt) && apt {
261        config.package_manager.push(PackageManager::Apt);
262    }
263    if !config.package_manager.contains(&PackageManager::Pacman) && pacman {
264        config.package_manager.push(PackageManager::Pacman);
265    }
266    if !config.package_manager.contains(&PackageManager::Aur) && aur {
267        config.package_manager.push(PackageManager::Aur);
268    }
269    if !config.package_manager.contains(&PackageManager::Dnf) && dnf {
270        config.package_manager.push(PackageManager::Dnf);
271    }
272    if !config.package_manager.contains(&PackageManager::Xbps) && xbps {
273        config.package_manager.push(PackageManager::Xbps);
274    }
275
276    let warning_updates_regex = config
277        .warning_updates_regex
278        .as_deref()
279        .map(Regex::new)
280        .transpose()
281        .error("invalid warning updates regex")?;
282    let critical_updates_regex = config
283        .critical_updates_regex
284        .as_deref()
285        .map(Regex::new)
286        .transpose()
287        .error("invalid critical updates regex")?;
288    let ignore_updates_regex = config
289        .ignore_updates_regex
290        .as_deref()
291        .map(Regex::new)
292        .transpose()
293        .error("invalid ignore updates regex")?;
294
295    let mut package_manager_vec: Vec<Box<dyn Backend>> = Vec::new();
296
297    for &package_manager in config.package_manager.iter() {
298        package_manager_vec.push(match package_manager {
299            PackageManager::Apt => Box::new(Apt::new(config.ignore_phased_updates).await?),
300            PackageManager::Pacman => Box::new(Pacman::new().await?),
301            PackageManager::Aur => Box::new(Aur::new(
302                config.aur_command.clone().error("aur_command is not set")?,
303            )),
304            PackageManager::Dnf => Box::new(Dnf::new()),
305            PackageManager::Xbps => Box::new(Xbps::new()),
306        });
307    }
308
309    loop {
310        let mut package_manager_map: HashMap<Cow<'static, str>, Value> = HashMap::new();
311
312        let mut critical = false;
313        let mut warning = false;
314        let mut total_count = 0;
315
316        // Iterate over the all package manager listed in Config
317        for package_manager in &package_manager_vec {
318            let mut updates = package_manager.get_updates_list().await?;
319            if let Some(regex) = ignore_updates_regex.clone() {
320                updates.retain(|u| !regex.is_match(u));
321            }
322
323            let updates_count = updates.len();
324
325            package_manager_map.insert(package_manager.name(), Value::number(updates_count));
326            total_count += updates_count;
327
328            warning |= warning_updates_regex
329                .as_ref()
330                .is_some_and(|regex| has_matching_update(&updates, regex));
331            critical |= critical_updates_regex
332                .as_ref()
333                .is_some_and(|regex| has_matching_update(&updates, regex));
334        }
335
336        let mut widget = Widget::new();
337
338        package_manager_map.insert("icon".into(), Value::icon("update"));
339        package_manager_map.insert("total".into(), Value::number(total_count));
340
341        widget.set_format(match total_count {
342            0 => format_up_to_date.clone(),
343            1 => format_singular.clone(),
344            _ => format.clone(),
345        });
346        widget.set_values(package_manager_map);
347
348        widget.state = match total_count {
349            0 => State::Idle,
350            _ => {
351                if critical {
352                    State::Critical
353                } else if warning {
354                    State::Warning
355                } else {
356                    State::Info
357                }
358            }
359        };
360        api.set_widget(widget)?;
361
362        select! {
363            _ = sleep(config.interval.0) => (),
364            _ = api.wait_for_update_request() => (),
365        }
366    }
367}
368
369#[async_trait]
370pub trait Backend {
371    fn name(&self) -> Cow<'static, str>;
372
373    async fn get_updates_list(&self) -> Result<Vec<String>>;
374}
375
376pub fn has_matching_update(updates: &[String], regex: &Regex) -> bool {
377    updates.iter().any(|line| regex.is_match(line))
378}