From 8eb4a471ef618de493e8b08fa9787f9a5a4cb89c Mon Sep 17 00:00:00 2001 From: IshanGrover2004 Date: Fri, 19 Jan 2024 20:29:29 +0530 Subject: [PATCH 01/32] refactor: Add `packages` in block and remove apt,pacman --- src/blocks.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/blocks.rs b/src/blocks.rs index 518b00823d..e9257cca1d 100644 --- a/src/blocks.rs +++ b/src/blocks.rs @@ -136,7 +136,7 @@ macro_rules! define_blocks { define_blocks!( amd_gpu, - apt, + packages, backlight, battery, bluetooth, @@ -162,7 +162,6 @@ define_blocks!( #[cfg(feature = "notmuch")] notmuch, nvidia_gpu, - pacman, pomodoro, rofication, service_status, From 36025edf91970d0945ebf9c3a5f28eaa2c486bd8 Mon Sep 17 00:00:00 2001 From: IshanGrover2004 Date: Fri, 19 Jan 2024 20:32:01 +0530 Subject: [PATCH 02/32] refactor: Remove `pacman.rs` file to add in `packages` block --- src/blocks/pacman.rs | 395 ------------------------------------------- 1 file changed, 395 deletions(-) delete mode 100644 src/blocks/pacman.rs diff --git a/src/blocks/pacman.rs b/src/blocks/pacman.rs deleted file mode 100644 index 0e6c74a1f9..0000000000 --- a/src/blocks/pacman.rs +++ /dev/null @@ -1,395 +0,0 @@ -//! Pending updates available on pacman or an AUR helper. -//! -//! Requires fakeroot to be installed (only required for pacman). -//! -//! Tip: You can grab the list of available updates using `fakeroot pacman -Qu --dbpath /tmp/checkup-db-i3statusrs-$USER/`. -//! If you have the `CHECKUPDATES_DB` env var set on your system then substitute that dir instead. -//! -//! 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. -//! -//! # Pacman hook -//! -//! Tip: On Arch Linux you can setup a `pacman` hook to signal i3status-rs to update after packages -//! have been upgraded, so you won't have stale info in your pacman block. -//! -//! In the block configuration, set `signal = 1` (or other number if `1` is being used by some -//! other block): -//! -//! ```toml -//! [[block]] -//! block = "pacman" -//! signal = 1 -//! ``` -//! -//! Create `/etc/pacman.d/hooks/i3status-rust.hook` with the below contents: -//! -//! ```ini -//! [Trigger] -//! Operation = Upgrade -//! Type = Package -//! Target = * -//! -//! [Action] -//! When = PostTransaction -//! Exec = /usr/bin/pkill -SIGRTMIN+1 i3status-rs -//! ``` -//! -//! # Configuration -//! -//! Key | Values | Default -//! ----|--------|--------- -//! `interval` | Update interval, in seconds. If setting `aur_command` then set interval appropriately as to not exceed the AUR's daily rate limit. | `600` -//! `format` | A string to customise the output of this block. See below for available placeholders. | `" $icon $pacman.eng(w:1) "` -//! `format_singular` | Same as `format` but for when exactly one update is available. | `" $icon $pacman.eng(w:1) "` -//! `format_up_to_date` | Same as `format` but for when no updates are available. | `" $icon $pacman.eng(w:1) "` -//! `warning_updates_regex` | Display block as warning if updates matching regex are available. | `None` -//! `critical_updates_regex` | Display block as critical if updates matching regex are available. | `None` -//! `aur_command` | AUR command to check available updates, which outputs in the same format as pacman. e.g. `yay -Qua` | Required if `$both` or `$aur` are used -//! -//! Placeholder | Value | Type | Unit -//! -------------|----------------------------------------------------------------------------------|--------|----- -//! `icon` | A static icon | Icon | - -//! `pacman` | Number of updates available according to `pacman` | Number | - -//! `aur` | Number of updates available according to `` | Number | - -//! `both` | Cumulative number of updates available according to `pacman` and `` | Number | - -//! -//! # Examples -//! -//! pacman only config: -//! -//! ```toml -//! [[block]] -//! block = "pacman" -//! interval = 600 -//! format = " $icon $pacman updates available " -//! format_singular = " $icon $pacman update available " -//! format_up_to_date = " $icon system up to date " -//! critical_updates_regex = "(linux|linux-lts|linux-zen)" -//! [[block.click]] -//! # pop-up a menu showing the available updates. Replace wofi with your favourite menu command. -//! button = "left" -//! cmd = "fakeroot pacman -Qu --dbpath /tmp/checkup-db-i3statusrs-$USER/ | wofi --show dmenu" -//! [[block.click]] -//! # Updates the block on right click -//! button = "right" -//! update = true -//! ``` -//! -//! pacman only config using warnings with ZFS modules: -//! -//! ```toml -//! [[block]] -//! block = "pacman" -//! interval = 600 -//! format = " $icon $pacman updates available " -//! format_singular = " $icon $pacman update available " -//! format_up_to_date = " $icon system up to date " -//! # If a linux update is available, but no ZFS package, it won't be possible to -//! # actually perform a system upgrade, so we show a warning. -//! warning_updates_regex = "(linux|linux-lts|linux-zen)" -//! # If ZFS is available, we know that we can and should do an upgrade, so we show -//! # the status as critical. -//! critical_updates_regex = "(zfs|zfs-lts)" -//! ``` -//! -//! pacman and AUR helper config: -//! -//! ```toml -//! [[block]] -//! block = "pacman" -//! interval = 600 -//! error_interval = 300 -//! format = " $icon $pacman + $aur = $both updates available " -//! format_singular = " $icon $both update available " -//! format_up_to_date = " $icon system up to date " -//! critical_updates_regex = "(linux|linux-lts|linux-zen)" -//! # aur_command should output available updates to stdout (ie behave as echo -ne "update\n") -//! aur_command = "yay -Qua" -//! ``` -//! -//! # Icons Used -//! -//! - `update` - -use std::env; -use std::path::PathBuf; -use std::process::Stdio; - -use regex::Regex; - -use tokio::fs::{create_dir_all, symlink}; -use tokio::process::Command; - -use super::prelude::*; -use crate::util::has_command; - -make_log_macro!(debug, "pacman"); - -static PACMAN_UPDATES_DB: Lazy = Lazy::new(|| { - let path = match env::var_os("CHECKUPDATES_DB") { - Some(val) => val.into(), - None => { - let mut path = env::temp_dir(); - let user = env::var("USER"); - path.push(format!( - "checkup-db-i3statusrs-{}", - user.as_deref().unwrap_or("no-user") - )); - path - } - }; - debug!("Using {} as updates DB path", path.display()); - path -}); - -static PACMAN_DB: Lazy = Lazy::new(|| { - let path = env::var_os("DBPath") - .map(Into::into) - .unwrap_or_else(|| PathBuf::from("/var/lib/pacman/")); - debug!("Using {} as pacman DB path", path.display()); - path -}); - -#[derive(Deserialize, Debug, SmartDefault)] -#[serde(deny_unknown_fields, default)] -pub struct Config { - #[default(600.into())] - pub interval: Seconds, - pub format: FormatConfig, - pub format_singular: FormatConfig, - pub format_up_to_date: FormatConfig, - pub warning_updates_regex: Option, - pub critical_updates_regex: Option, - pub aur_command: Option, -} - -pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { - let format = config.format.with_default(" $icon $pacman.eng(w:1) ")?; - let format_singular = config - .format_singular - .with_default(" $icon $pacman.eng(w:1) ")?; - let format_up_to_date = config - .format_up_to_date - .with_default(" $icon $pacman.eng(w:1) ")?; - - macro_rules! any_format_contains { - ($name:expr) => { - format.contains_key($name) - || format_singular.contains_key($name) - || format_up_to_date.contains_key($name) - }; - } - let aur = any_format_contains!("aur"); - let pacman = any_format_contains!("pacman"); - let both = any_format_contains!("both"); - let watched = if both || (pacman && aur) { - Watched::Both( - config - .aur_command - .as_deref() - .error("$aur or $both found in format string but no aur_command supplied")?, - ) - } else if pacman && !aur { - Watched::Pacman - } else if !pacman && aur { - Watched::Aur( - config - .aur_command - .as_deref() - .error("$aur or $both found in format string but no aur_command supplied")?, - ) - } else { - Watched::None - }; - - if matches!(watched, Watched::Pacman | Watched::Both(_)) { - check_fakeroot_command_exists().await?; - } - - let warning_updates_regex = config - .warning_updates_regex - .as_deref() - .map(Regex::new) - .transpose() - .error("invalid warning updates regex")?; - let critical_updates_regex = config - .critical_updates_regex - .as_deref() - .map(Regex::new) - .transpose() - .error("invalid critical updates regex")?; - - loop { - let (mut values, warning, critical, total) = match &watched { - Watched::Pacman => { - let updates = get_pacman_available_updates().await?; - let count = get_update_count(&updates); - let values = map!("pacman" => Value::number(count)); - let warning = warning_updates_regex - .as_ref() - .is_some_and(|regex| has_matching_update(&updates, regex)); - let critical = critical_updates_regex - .as_ref() - .is_some_and(|regex| has_matching_update(&updates, regex)); - (values, warning, critical, count) - } - Watched::Aur(aur_command) => { - let updates = get_aur_available_updates(aur_command).await?; - let count = get_update_count(&updates); - let values = map!( - "aur" => Value::number(count) - ); - let warning = warning_updates_regex - .as_ref() - .is_some_and(|regex| has_matching_update(&updates, regex)); - let critical = critical_updates_regex - .as_ref() - .is_some_and(|regex| has_matching_update(&updates, regex)); - (values, warning, critical, count) - } - Watched::Both(aur_command) => { - let (pacman_updates, aur_updates) = tokio::try_join!( - get_pacman_available_updates(), - get_aur_available_updates(aur_command) - )?; - let pacman_count = get_update_count(&pacman_updates); - let aur_count = get_update_count(&aur_updates); - let values = map! { - "pacman" => Value::number(pacman_count), - "aur" => Value::number(aur_count), - "both" => Value::number(pacman_count + aur_count), - }; - let warning = warning_updates_regex.as_ref().is_some_and(|regex| { - has_matching_update(&aur_updates, regex) - || has_matching_update(&pacman_updates, regex) - }); - let critical = critical_updates_regex.as_ref().is_some_and(|regex| { - has_matching_update(&aur_updates, regex) - || has_matching_update(&pacman_updates, regex) - }); - (values, warning, critical, pacman_count + aur_count) - } - Watched::None => (HashMap::new(), false, false, 0), - }; - values.insert("icon".into(), Value::icon("update")); - - let mut widget = Widget::new(); - widget.set_format(match total { - 0 => format_up_to_date.clone(), - 1 => format_singular.clone(), - _ => format.clone(), - }); - widget.set_values(values); - widget.state = match total { - 0 => State::Idle, - _ => { - if critical { - State::Critical - } else if warning { - State::Warning - } else { - State::Info - } - } - }; - api.set_widget(widget)?; - - select! { - _ = sleep(config.interval.0) => (), - _ = api.wait_for_update_request() => (), - } - } -} - -#[derive(Debug, PartialEq, Eq)] -enum Watched<'a> { - None, - Pacman, - Aur(&'a str), - Both(&'a str), -} - -async fn check_fakeroot_command_exists() -> Result<()> { - if !has_command("fakeroot").await? { - Err(Error::new("fakeroot not found")) - } else { - Ok(()) - } -} - -async fn get_pacman_available_updates() -> Result { - // Create the determined `checkup-db` path recursively - create_dir_all(&*PACMAN_UPDATES_DB).await.or_error(|| { - format!( - "Failed to create checkup-db directory at '{}'", - PACMAN_UPDATES_DB.display() - ) - })?; - - // Create symlink to local cache in `checkup-db` if required - let local_cache = PACMAN_UPDATES_DB.join("local"); - if !local_cache.exists() { - symlink(PACMAN_DB.join("local"), local_cache) - .await - .error("Failed to created required symlink")?; - } - - // Update database - let status = Command::new("fakeroot") - .env("LC_ALL", "C") - .args([ - "--".as_ref(), - "pacman".as_ref(), - "-Sy".as_ref(), - "--dbpath".as_ref(), - PACMAN_UPDATES_DB.as_os_str(), - "--logfile".as_ref(), - "/dev/null".as_ref(), - ]) - .stdout(Stdio::null()) - .status() - .await - .error("Failed to run command")?; - if !status.success() { - debug!("{}", status); - return Err(Error::new("pacman -Sy exited with non zero exit status")); - } - - let stdout = Command::new("fakeroot") - .env("LC_ALL", "C") - .args([ - "--".as_ref(), - "pacman".as_ref(), - "-Qu".as_ref(), - "--dbpath".as_ref(), - PACMAN_UPDATES_DB.as_os_str(), - ]) - .output() - .await - .error("There was a problem running the pacman commands")? - .stdout; - - String::from_utf8(stdout).error("Pacman produced non-UTF8 output") -} - -async fn get_aur_available_updates(aur_command: &str) -> Result { - let stdout = Command::new("sh") - .args(["-c", aur_command]) - .output() - .await - .or_error(|| format!("aur command: {aur_command} failed"))? - .stdout; - String::from_utf8(stdout) - .error("There was a problem while converting the aur command output to a string") -} - -fn get_update_count(updates: &str) -> usize { - updates - .lines() - .filter(|line| !line.contains("[ignored]")) - .count() -} - -fn has_matching_update(updates: &str, regex: &Regex) -> bool { - updates.lines().any(|line| regex.is_match(line)) -} From 9a6bb88fe94a856098b8cf2bf3d29264e7fb425f Mon Sep 17 00:00:00 2001 From: IshanGrover2004 Date: Fri, 19 Jan 2024 20:32:17 +0530 Subject: [PATCH 03/32] refactor: Remove `apt.rs` file to add in `packages` block --- src/blocks/apt.rs | 249 ---------------------------------------------- 1 file changed, 249 deletions(-) delete mode 100644 src/blocks/apt.rs diff --git a/src/blocks/apt.rs b/src/blocks/apt.rs deleted file mode 100644 index 53cc460fc3..0000000000 --- a/src/blocks/apt.rs +++ /dev/null @@ -1,249 +0,0 @@ -//! Pending updates available for your Debian/Ubuntu based system -//! -//! 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. -//! -//! Tip: You can grab the list of available updates using `APT_CONFIG=/tmp/i3rs-apt/apt.conf apt list --upgradable` -//! -//! # Configuration -//! -//! Key | Values | Default -//! ----|--------|-------- -//! `interval` | Update interval in seconds. | `600` -//! `format` | A string to customise the output of this block. See below for available placeholders. | `" $icon $count.eng(w:1) "` -//! `format_singular` | Same as `format`, but for when exactly one update is available. | `" $icon $count.eng(w:1) "` -//! `format_up_to_date` | Same as `format`, but for when no updates are available. | `" $icon $count.eng(w:1) "` -//! `warning_updates_regex` | Display block as warning if updates matching regex are available. | `None` -//! `critical_updates_regex` | Display block as critical if updates matching regex are available. | `None` -//! `ignore_updates_regex` | Doesn't include updates matching regex in the count. | `None` -//! `ignore_phased_updates` | Doesn't include potentially held back phased updates in the count. | `false` -//! -//! Placeholder | Value | Type | Unit -//! ------------|-----------------------------|--------|------ -//! `icon` | A static icon | Icon | - -//! `count` | Number of updates available | Number | - -//! -//! # Example -//! -//! Update the list of pending updates every thirty minutes (1800 seconds): -//! -//! ```toml -//! [[block]] -//! block = "apt" -//! interval = 1800 -//! format = " $icon $count updates available " -//! format_singular = " $icon One update available " -//! format_up_to_date = " $icon system up to date " -//! critical_updates_regex = "(linux|linux-lts|linux-zen)" -//! [[block.click]] -//! # shows dmenu with cached available updates. Any dmenu alternative should also work. -//! button = "left" -//! cmd = "APT_CONFIG=/tmp/i3rs-apt/apt.conf apt list --upgradable | tail -n +2 | rofi -dmenu" -//! [[block.click]] -//! # Updates the block on right click -//! button = "right" -//! update = true -//! ``` -//! -//! # Icons Used -//! -//! - `update` - -use std::env; -use std::process::Stdio; - -use regex::Regex; - -use tokio::fs::{create_dir_all, File}; -use tokio::process::Command; - -use super::prelude::*; - -#[derive(Deserialize, Debug, SmartDefault)] -#[serde(deny_unknown_fields, default)] -pub struct Config { - #[default(600.into())] - pub interval: Seconds, - pub format: FormatConfig, - pub format_singular: FormatConfig, - pub format_up_to_date: FormatConfig, - pub warning_updates_regex: Option, - pub critical_updates_regex: Option, - pub ignore_updates_regex: Option, - pub ignore_phased_updates: bool, -} - -pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { - let format = config.format.with_default(" $icon $count.eng(w:1) ")?; - let format_singular = config - .format_singular - .with_default(" $icon $count.eng(w:1) ")?; - let format_up_to_date = config - .format_up_to_date - .with_default(" $icon $count.eng(w:1) ")?; - - let warning_updates_regex = config - .warning_updates_regex - .as_deref() - .map(Regex::new) - .transpose() - .error("invalid warning updates regex")?; - let critical_updates_regex = config - .critical_updates_regex - .as_deref() - .map(Regex::new) - .transpose() - .error("invalid critical updates regex")?; - let ignore_updates_regex = config - .ignore_updates_regex - .as_deref() - .map(Regex::new) - .transpose() - .error("invalid ignore updates regex")?; - - let mut cache_dir = env::temp_dir(); - cache_dir.push("i3rs-apt"); - if !cache_dir.exists() { - create_dir_all(&cache_dir) - .await - .error("Failed to create temp dir")?; - } - - let apt_config = format!( - "Dir::State \"{}\";\n - Dir::State::lists \"lists\";\n - Dir::Cache \"{}\";\n - Dir::Cache::srcpkgcache \"srcpkgcache.bin\";\n - Dir::Cache::pkgcache \"pkgcache.bin\";", - cache_dir.display(), - cache_dir.display(), - ); - - let mut config_file = cache_dir; - config_file.push("apt.conf"); - let config_file = config_file.to_str().unwrap(); - - let mut file = File::create(&config_file) - .await - .error("Failed to create config file")?; - file.write_all(apt_config.as_bytes()) - .await - .error("Failed to write to config file")?; - - loop { - let mut widget = Widget::new(); - let updates = get_updates_list(config_file).await?; - let count = get_update_count( - config_file, - config.ignore_phased_updates, - ignore_updates_regex.as_ref(), - &updates, - ) - .await?; - - widget.set_format(match count { - 0 => format_up_to_date.clone(), - 1 => format_singular.clone(), - _ => format.clone(), - }); - widget.set_values(map!( - "count" => Value::number(count), - "icon" => Value::icon("update"), - )); - - let warning = warning_updates_regex - .as_ref() - .is_some_and(|regex| has_matching_update(&updates, regex)); - let critical = critical_updates_regex - .as_ref() - .is_some_and(|regex| has_matching_update(&updates, regex)); - widget.state = match count { - 0 => State::Idle, - _ => { - if critical { - State::Critical - } else if warning { - State::Warning - } else { - State::Info - } - } - }; - - api.set_widget(widget)?; - - select! { - _ = sleep(config.interval.0) => (), - _ = api.wait_for_update_request() => (), - } - } -} - -async fn get_updates_list(config_path: &str) -> Result { - Command::new("apt") - .env("APT_CONFIG", config_path) - .args(["update"]) - .stdout(Stdio::null()) - .stdin(Stdio::null()) - .spawn() - .error("Failed to run `apt update`")? - .wait() - .await - .error("Failed to run `apt update`")?; - let stdout = Command::new("apt") - .env("LANG", "C") - .env("APT_CONFIG", config_path) - .args(["list", "--upgradable"]) - .output() - .await - .error("Problem running apt command")? - .stdout; - String::from_utf8(stdout).error("apt produced non-UTF8 output") -} - -async fn get_update_count( - config_path: &str, - ignore_phased_updates: bool, - ignore_updates_regex: Option<&Regex>, - updates: &str, -) -> Result { - let mut cnt = 0; - - for update_line in updates - .lines() - .filter(|line| line.contains("[upgradable")) - .filter(|line| ignore_updates_regex.map_or(true, |re| !re.is_match(line))) - { - if !ignore_phased_updates || !is_phased_update(config_path, update_line).await? { - cnt += 1; - } - } - - Ok(cnt) -} - -fn has_matching_update(updates: &str, regex: &Regex) -> bool { - updates.lines().any(|line| regex.is_match(line)) -} - -async fn is_phased_update(config_path: &str, package_line: &str) -> Result { - let package_name_regex = regex!(r#"(.*)/.*"#); - let package_name = &package_name_regex - .captures(package_line) - .error("Couldn't find package name")?[1]; - - let output = String::from_utf8( - Command::new("apt-cache") - .args(["-c", config_path, "policy", package_name]) - .output() - .await - .error("Problem running apt-cache command")? - .stdout, - ) - .error("Problem capturing apt-cache command output")?; - - let phased_regex = regex!(r".*\(phased (\d+)%\).*"); - Ok(match phased_regex.captures(&output) { - Some(matches) => &matches[1] != "100", - None => false, - }) -} From ba1dc958dc51c93ad2ee8ed8560210961c8e0354 Mon Sep 17 00:00:00 2001 From: IshanGrover2004 Date: Sat, 20 Jan 2024 02:00:12 +0530 Subject: [PATCH 04/32] feat: Add `packages` block to check updates for package managers --- src/blocks/packages.rs | 245 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 245 insertions(+) create mode 100644 src/blocks/packages.rs diff --git a/src/blocks/packages.rs b/src/blocks/packages.rs new file mode 100644 index 0000000000..50b8b67c96 --- /dev/null +++ b/src/blocks/packages.rs @@ -0,0 +1,245 @@ +//! Shows pending updates for different package manager like apt, pacman, etc. +//! +//! Currently 2 package manager are avaliable: +//! - `apt` for Debian/Ubuntu based system +//! - `pacman` for Arch based system +//! +//! # Configuration +//! +//! Key | Values | Default +//! ----|--------|-------- +//! `interval` | Update interval in seconds. | `600` +//! `package_manager` | Package manager to check for updates | - +//! `format` | A string to customise the output of this block. See below for available placeholders. | `" $icon $count.eng(w:1) "` +//! `format_singular` | Same as `format`, but for when exactly one update is available. | `" $icon $count.eng(w:1) "` +//! `format_up_to_date` | Same as `format`, but for when no updates are available. | `" $icon $count.eng(w:1) "` +//! `warning_updates_regex` | Display block as warning if updates matching regex are available. | `None` +//! `critical_updates_regex` | Display block as critical if updates matching regex are available. | `None` +//! `ignore_updates_regex` | Doesn't include updates matching regex in the count. | `None` +//! `ignore_phased_updates` | Doesn't include potentially held back phased updates in the count. (For Debian/Ubuntu based system) | `false` +//! `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 `$both` or `$aur` are used +//! +//! Placeholder | Value | Type | Unit +//! -------------|----------------------------------------------------------------------------------|--------|----- +//! `icon` | A static icon | Icon | - +//! `count` | Number of updates available | Number | - +//! +//! # Example +//! +//! Update the list of pending updates every thirty minutes (1800 seconds): +//! +//! ```toml +//! [[block]] +//! block = "packages" +//! interval = 1800 +//! format = " $icon $count updates available " +//! format_singular = " $icon One update available " +//! format_up_to_date = " $icon system up to date " +//! ``` +//! +//! # Icons Used +//! +//! - `update` + +pub mod apt; +use apt::Apt; + +pub mod pacman; +use pacman::{Aur, Pacman}; + +use regex::Regex; + +use super::prelude::*; + +#[derive(Deserialize, Debug, SmartDefault, Clone)] +#[serde(deny_unknown_fields, default)] +pub struct Config { + #[default(600.into())] + pub interval: Seconds, + pub package_manager: Vec, + pub format: FormatConfig, + pub format_singular: FormatConfig, + pub format_up_to_date: FormatConfig, + pub warning_updates_regex: Option, + pub critical_updates_regex: Option, + pub ignore_updates_regex: Option, + pub ignore_phased_updates: bool, + pub aur_command: Option, +} + +#[derive(Deserialize, Debug, Clone, Copy, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum PackageManager { + Apt, + Pacman, + Aur, +} + +pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { + let mut config: Config = config.clone(); + + let format = config.format.with_default(" $icon $total.eng(w:1) ")?; + let format_singular = config + .format_singular + .with_default(" $icon $total.eng(w:1) ")?; + let format_up_to_date = config + .format_up_to_date + .with_default(" $icon $total.eng(w:1) ")?; + + // If user provide package manager in any of the formats then consider that also + macro_rules! any_format_contains { + ($name:expr) => { + format.contains_key($name) + || format_singular.contains_key($name) + || format_up_to_date.contains_key($name) + }; + } + + let apt = any_format_contains!("apt"); + let aur = any_format_contains!("aur"); + let pacman = any_format_contains!("pacman"); + + if !config.package_manager.contains(&PackageManager::Apt) && apt { + config.package_manager.push(PackageManager::Apt); + } + if !config.package_manager.contains(&PackageManager::Pacman) && pacman { + config.package_manager.push(PackageManager::Pacman); + } + if !config.package_manager.contains(&PackageManager::Aur) && aur { + config.package_manager.push(PackageManager::Aur); + } + + let warning_updates_regex = config + .warning_updates_regex + .as_deref() + .map(Regex::new) + .transpose() + .error("invalid warning updates regex")?; + let critical_updates_regex = config + .critical_updates_regex + .as_deref() + .map(Regex::new) + .transpose() + .error("invalid critical updates regex")?; + let ignore_updates_regex = config + .ignore_updates_regex + .as_deref() + .map(Regex::new) + .transpose() + .error("invalid ignore updates regex")?; + + // Setup once everything it takes to check updates for every package manager + for package_manager in &config.package_manager { + let mut backend: Box = match package_manager { + PackageManager::Apt => Box::new(Apt::new()), + PackageManager::Pacman => Box::new(Pacman::new()), + PackageManager::Aur => Box::new(Aur::new()), + }; + + backend.setup().await?; + } + + loop { + let (mut apt_count, mut pacman_count, mut aur_count) = (0, 0, 0); + let mut critical_vec = vec![false]; + let mut warning_vec = vec![false]; + + // Iterate over the all package manager listed in Config + for package_manager in config.package_manager.clone() { + match package_manager { + PackageManager::Apt => { + let mut apt = Apt::new(); + apt.ignore_updates_regex = ignore_updates_regex.clone(); + apt.ignore_phased_updates = config.ignore_phased_updates; + let updates = apt.get_updates_list().await?; + apt_count = apt.get_update_count(&updates).await?; + let warning = warning_updates_regex + .as_ref() + .is_some_and(|regex| apt.has_matching_update(&updates, regex)); + let critical = critical_updates_regex + .as_ref() + .is_some_and(|regex| apt.has_matching_update(&updates, regex)); + warning_vec.push(warning); + critical_vec.push(critical); + } + PackageManager::Pacman => { + let pacman = Pacman::new(); + let updates = pacman.get_updates_list().await?; + pacman_count = pacman.get_update_count(&updates).await?; + let warning = warning_updates_regex + .as_ref() + .is_some_and(|regex| pacman.has_matching_update(&updates, regex)); + let critical = critical_updates_regex + .as_ref() + .is_some_and(|regex| pacman.has_matching_update(&updates, regex)); + warning_vec.push(warning); + critical_vec.push(critical); + } + PackageManager::Aur => { + let aur = Aur::new(); + let updates = aur.get_updates_list().await?; + aur_count = aur.get_update_count(&updates).await?; + let warning = warning_updates_regex + .as_ref() + .is_some_and(|regex| aur.has_matching_update(&updates, regex)); + let critical = critical_updates_regex + .as_ref() + .is_some_and(|regex| aur.has_matching_update(&updates, regex)); + warning_vec.push(warning); + critical_vec.push(critical); + } + } + } + + let mut widget = Widget::new(); + + let total_count = apt_count + pacman_count + aur_count; + widget.set_format(match total_count { + 0 => format_up_to_date.clone(), + 1 => format_singular.clone(), + _ => format.clone(), + }); + widget.set_values(map!( + "icon" => Value::icon("update"), + "apt" => Value::number(apt_count), + "pacman" => Value::number(pacman_count), + "aur" => Value::number(aur_count), + "total" => Value::number(total_count), + )); + + let warning = warning_vec.iter().any(|&x| x); + let critical = critical_vec.iter().any(|&x| x); + + widget.state = match total_count { + 0 => State::Idle, + _ => { + if critical { + State::Critical + } else if warning { + State::Warning + } else { + State::Info + } + } + }; + api.set_widget(widget)?; + + select! { + _ = sleep(config.interval.0) => (), + _ = api.wait_for_update_request() => (), + } + } +} + +#[async_trait] +trait Backend { + async fn setup(&mut self) -> Result<()>; + + async fn get_updates_list(&self) -> Result; + + async fn get_update_count(&self, updates: &str) -> Result; + + fn has_matching_update(&self, updates: &str, regex: &Regex) -> bool { + updates.lines().any(|line| regex.is_match(line)) + } +} From 76c829e33ea9750755ba9c6f1656906011fc439b Mon Sep 17 00:00:00 2001 From: IshanGrover2004 Date: Sat, 20 Jan 2024 02:01:27 +0530 Subject: [PATCH 05/32] feat: Add `apt` package manager to `packages` block --- src/blocks/packages/apt.rs | 178 +++++++++++++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 src/blocks/packages/apt.rs diff --git a/src/blocks/packages/apt.rs b/src/blocks/packages/apt.rs new file mode 100644 index 0000000000..4b75635098 --- /dev/null +++ b/src/blocks/packages/apt.rs @@ -0,0 +1,178 @@ +//! Pending updates available for your Debian/Ubuntu based system +//! +//! 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. +//! +//! Tip: You can grab the list of available updates using `APT_CONFIG=/tmp/i3rs-apt/apt.conf apt list --upgradable` +//! +//! # Configuration +//! +//! Key | Values | Default +//! ----|--------|-------- +//! `interval` | Update interval in seconds. | `600` +//! `format` | A string to customise the output of this block. See below for available placeholders. | `" $icon $count.eng(w:1) "` +//! `format_singular` | Same as `format`, but for when exactly one update is available. | `" $icon $count.eng(w:1) "` +//! `format_up_to_date` | Same as `format`, but for when no updates are available. | `" $icon $count.eng(w:1) "` +//! `warning_updates_regex` | Display block as warning if updates matching regex are available. | `None` +//! `critical_updates_regex` | Display block as critical if updates matching regex are available. | `None` +//! `ignore_updates_regex` | Doesn't include updates matching regex in the count. | `None` +//! `ignore_phased_updates` | Doesn't include potentially held back phased updates in the count. | `false` +//! +//! Placeholder | Value | Type | Unit +//! ------------|-----------------------------|--------|------ +//! `icon` | A static icon | Icon | - +//! `apt` | Number of updates available | Number | - +//! +//! # Example +//! +//! Update the list of pending updates every thirty minutes (1800 seconds): +//! +//! ```toml +//! [[block]] +//! block = "packages" +//! interval = 1800 +//! format = " $icon $apt updates available" +//! format_singular = " $icon One update available " +//! format_up_to_date = " $icon system up to date " +//! critical_updates_regex = "(linux|linux-lts|linux-zen)" +//! [[block.click]] +//! # shows dmenu with cached available updates. Any dmenu alternative should also work. +//! button = "left" +//! cmd = "APT_CONFIG=/tmp/i3rs-apt/apt.conf apt list --upgradable | tail -n +2 | rofi -dmenu" +//! [[block.click]] +//! # Updates the block on right click +//! button = "right" +//! update = true +//! ``` +//! +//! # Icons Used +//! +//! - `update` + +use std::env; +use std::process::Stdio; + +use tokio::fs::{create_dir_all, File}; +use tokio::process::Command; + +use super::*; + +pub(super) struct Apt { + pub(super) config_file: String, + pub(super) ignore_phased_updates: bool, + pub(super) ignore_updates_regex: Option, +} + +impl Apt { + pub(super) fn new() -> Self { + Apt { + config_file: String::new(), + ignore_phased_updates: false, + ignore_updates_regex: Default::default(), + } + } +} + +#[async_trait] +impl Backend for Apt { + async fn setup(&mut self) -> Result<()> { + let mut cache_dir = env::temp_dir(); + cache_dir.push("i3rs-apt"); + if !cache_dir.exists() { + create_dir_all(&cache_dir) + .await + .error("Failed to create temp dir")?; + } + + let apt_config = format!( + "Dir::State \"{}\";\n + Dir::State::lists \"lists\";\n + Dir::Cache \"{}\";\n + Dir::Cache::srcpkgcache \"srcpkgcache.bin\";\n + Dir::Cache::pkgcache \"pkgcache.bin\";", + cache_dir.display(), + cache_dir.display(), + ); + + let mut config_file = cache_dir; + config_file.push("apt.conf"); + let config_file = config_file.to_str().unwrap(); + + self.config_file = config_file.to_string(); + + let mut file = File::create(&config_file) + .await + .error("Failed to create config file")?; + file.write_all(apt_config.as_bytes()) + .await + .error("Failed to write to config file")?; + + Ok(()) + } + + async fn get_updates_list(&self) -> Result { + Command::new("apt") + .env("APT_CONFIG", &self.config_file) + .args(["update"]) + .stdout(Stdio::null()) + .stdin(Stdio::null()) + .spawn() + .error("Failed to run `apt update`")? + .wait() + .await + .error("Failed to run `apt update`")?; + let stdout = Command::new("apt") + .env("LANG", "C") + .env("APT_CONFIG", &self.config_file) + .args(["list", "--upgradable"]) + .output() + .await + .error("Problem running apt command")? + .stdout; + String::from_utf8(stdout).error("apt produced non-UTF8 output") + } + + async fn get_update_count(&self, updates: &str) -> Result { + let mut cnt = 0; + + for update_line in updates + .lines() + .filter(|line| line.contains("[upgradable")) + .filter(|line| { + self.ignore_updates_regex + .as_ref() + .map_or(true, |re| !re.is_match(line)) + }) + { + if !self.ignore_phased_updates + || !is_phased_update(&self.config_file, update_line).await? + { + cnt += 1; + } + } + + Ok(cnt) + } +} + +async fn is_phased_update(config_path: &str, package_line: &str) -> Result { + let package_name_regex = regex!(r#"(.*)/.*"#); + let package_name = &package_name_regex + .captures(package_line) + .error("Couldn't find package name")?[1]; + + let output = String::from_utf8( + Command::new("apt-cache") + .args(["-c", config_path, "policy", package_name]) + .output() + .await + .error("Problem running apt-cache command")? + .stdout, + ) + .error("Problem capturing apt-cache command output")?; + + let phased_regex = regex!(r".*\(phased (\d+)%\).*"); + Ok(match phased_regex.captures(&output) { + Some(matches) => &matches[1] != "100", + None => false, + }) +} From 8d2b5f4d1d926ad3c584a982e2931a1e2dbf551b Mon Sep 17 00:00:00 2001 From: IshanGrover2004 Date: Sat, 20 Jan 2024 02:01:42 +0530 Subject: [PATCH 06/32] feat: Add `pacman` & `aur` package manager to `packages` block --- src/blocks/packages/pacman.rs | 273 ++++++++++++++++++++++++++++++++++ 1 file changed, 273 insertions(+) create mode 100644 src/blocks/packages/pacman.rs diff --git a/src/blocks/packages/pacman.rs b/src/blocks/packages/pacman.rs new file mode 100644 index 0000000000..83f6cc6b3d --- /dev/null +++ b/src/blocks/packages/pacman.rs @@ -0,0 +1,273 @@ +//! Pending updates available on pacman or an AUR helper. +//! +//! Requires fakeroot to be installed (only required for pacman). +//! +//! Tip: You can grab the list of available updates using `fakeroot pacman -Qu --dbpath /tmp/checkup-db-i3statusrs-$USER/`. +//! If you have the `CHECKUPDATES_DB` env var set on your system then substitute that dir instead. +//! +//! 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. +//! +//! # Pacman hook +//! +//! Tip: On Arch Linux you can setup a `pacman` hook to signal i3status-rs to update after packages +//! have been upgraded, so you won't have stale info in your pacman block. +//! +//! In the block configuration, set `signal = 1` (or other number if `1` is being used by some +//! other block): +//! +//! ```toml +//! [[block]] +//! block = "packages" +//! signal = 1 +//! ``` +//! +//! Create `/etc/pacman.d/hooks/i3status-rust.hook` with the below contents: +//! +//! ```ini +//! [Trigger] +//! Operation = Upgrade +//! Type = Package +//! Target = * +//! +//! [Action] +//! When = PostTransaction +//! Exec = /usr/bin/pkill -SIGRTMIN+1 i3status-rs +//! ``` +//! +//! # Configuration +//! +//! Key | Values | Default +//! ----|--------|--------- +//! `interval` | Update interval, in seconds. If setting `aur_command` then set interval appropriately as to not exceed the AUR's daily rate limit. | `600` +//! `format` | A string to customise the output of this block. See below for available placeholders. | `" $icon $pacman.eng(w:1) "` +//! `format_singular` | Same as `format` but for when exactly one update is available. | `" $icon $pacman.eng(w:1) "` +//! `format_up_to_date` | Same as `format` but for when no updates are available. | `" $icon $pacman.eng(w:1) "` +//! `warning_updates_regex` | Display block as warning if updates matching regex are available. | `None` +//! `critical_updates_regex` | Display block as critical if updates matching regex are available. | `None` +//! `aur_command` | AUR command to check available updates, which outputs in the same format as pacman. e.g. `yay -Qua` | Required if `$both` or `$aur` are used +//! +//! Placeholder | Value | Type | Unit +//! -------------|----------------------------------------------------------------------------------|--------|----- +//! `icon` | A static icon | Icon | - +//! `pacman` | Number of updates available according to `pacman` | Number | - +//! `aur` | Number of updates available according to `` | Number | - +//! +//! # Examples +//! +//! pacman only config: +//! +//! ```toml +//! [[block]] +//! block = "packages" +//! interval = 600 +//! format = " $icon $pacman updates available " +//! format_singular = " $icon $pacman update available " +//! format_up_to_date = " $icon system up to date " +//! critical_updates_regex = "(linux|linux-lts|linux-zen)" +//! [[block.click]] +//! # pop-up a menu showing the available updates. Replace wofi with your favourite menu command. +//! button = "left" +//! cmd = "fakeroot pacman -Qu --dbpath /tmp/checkup-db-i3statusrs-$USER/ | wofi --show dmenu" +//! [[block.click]] +//! # Updates the block on right click +//! button = "right" +//! update = true +//! ``` +//! +//! pacman only config using warnings with ZFS modules: +//! +//! ```toml +//! [[block]] +//! block = "packages" +//! interval = 600 +//! format = " $icon $pacman updates available " +//! format_singular = " $icon $pacman update available " +//! format_up_to_date = " $icon system up to date " +//! # If a linux update is available, but no ZFS package, it won't be possible to +//! # actually perform a system upgrade, so we show a warning. +//! warning_updates_regex = "(linux|linux-lts|linux-zen)" +//! # If ZFS is available, we know that we can and should do an upgrade, so we show +//! # the status as critical. +//! critical_updates_regex = "(zfs|zfs-lts)" +//! ``` +//! +//! pacman and AUR helper config: +//! +//! ```toml +//! [[block]] +//! block = "pacman" +//! interval = 600 +//! error_interval = 300 +//! format = " $icon $pacman + $aur = $both updates available " +//! format_singular = " $icon $both update available " +//! format_up_to_date = " $icon system up to date " +//! critical_updates_regex = "(linux|linux-lts|linux-zen)" +//! # aur_command should output available updates to stdout (ie behave as echo -ne "update\n") +//! aur_command = "yay -Qua" +//! ``` +//! +//! # Icons Used +//! +//! - `update` + +use std::env; +use std::path::PathBuf; +use std::process::Stdio; + +use tokio::fs::{create_dir_all, symlink}; +use tokio::process::Command; + +use super::*; +use crate::util::has_command; + +make_log_macro!(debug, "pacman"); + +static PACMAN_UPDATES_DB: Lazy = Lazy::new(|| { + let path = match env::var_os("CHECKUPDATES_DB") { + Some(val) => val.into(), + None => { + let mut path = env::temp_dir(); + let user = env::var("USER"); + path.push(format!( + "checkup-db-i3statusrs-{}", + user.as_deref().unwrap_or("no-user") + )); + path + } + }; + debug!("Using {} as updates DB path", path.display()); + path +}); + +static PACMAN_DB: Lazy = Lazy::new(|| { + let path = env::var_os("DBPath") + .map(Into::into) + .unwrap_or_else(|| PathBuf::from("/var/lib/pacman/")); + debug!("Using {} as pacman DB path", path.display()); + path +}); + +pub(super) struct Pacman; + +pub(super) struct Aur { + aur_command: String, +} + +impl Pacman { + pub(super) fn new() -> Self { + Self + } +} + +impl Aur { + pub(super) fn new() -> Self { + Aur { + aur_command: String::new(), + } + } +} + +#[async_trait] +impl Backend for Pacman { + async fn setup(&mut self) -> Result<()> { + check_fakeroot_command_exists().await?; + + Ok(()) + } + + async fn get_updates_list(&self) -> Result { + // Create the determined `checkup-db` path recursively + create_dir_all(&*PACMAN_UPDATES_DB).await.or_error(|| { + format!( + "Failed to create checkup-db directory at '{}'", + PACMAN_UPDATES_DB.display() + ) + })?; + + // Create symlink to local cache in `checkup-db` if required + let local_cache = PACMAN_UPDATES_DB.join("local"); + if !local_cache.exists() { + symlink(PACMAN_DB.join("local"), local_cache) + .await + .error("Failed to created required symlink")?; + } + + // Update database + let status = Command::new("fakeroot") + .env("LC_ALL", "C") + .args([ + "--".as_ref(), + "pacman".as_ref(), + "-Sy".as_ref(), + "--dbpath".as_ref(), + PACMAN_UPDATES_DB.as_os_str(), + "--logfile".as_ref(), + "/dev/null".as_ref(), + ]) + .stdout(Stdio::null()) + .status() + .await + .error("Failed to run command")?; + if !status.success() { + debug!("{}", status); + return Err(Error::new("pacman -Sy exited with non zero exit status")); + } + + let stdout = Command::new("fakeroot") + .env("LC_ALL", "C") + .args([ + "--".as_ref(), + "pacman".as_ref(), + "-Qu".as_ref(), + "--dbpath".as_ref(), + PACMAN_UPDATES_DB.as_os_str(), + ]) + .output() + .await + .error("There was a problem running the pacman commands")? + .stdout; + + String::from_utf8(stdout).error("Pacman produced non-UTF8 output") + } + + async fn get_update_count(&self, updates: &str) -> Result { + Ok(updates + .lines() + .filter(|line| !line.contains("[ignored]")) + .count()) + } +} + +#[async_trait] +impl Backend for Aur { + async fn setup(&mut self) -> Result<()> { + // Nothing to setup here + Ok(()) + } + + async fn get_updates_list(&self) -> Result { + let stdout = Command::new("sh") + .args(["-c", &self.aur_command]) + .output() + .await + .or_error(|| format!("aur command: {} failed", self.aur_command))? + .stdout; + String::from_utf8(stdout) + .error("There was a problem while converting the aur command output to a string") + } + + async fn get_update_count(&self, updates: &str) -> Result { + Ok(updates + .lines() + .filter(|line| !line.contains("[ignored]")) + .count()) + } +} + +async fn check_fakeroot_command_exists() -> Result<()> { + if !has_command("fakeroot").await? { + Err(Error::new("fakeroot not found")) + } else { + Ok(()) + } +} From bd5b9d65ac74dbb4383e6861f2710db309d109d3 Mon Sep 17 00:00:00 2001 From: IshanGrover2004 Date: Sat, 20 Jan 2024 02:36:16 +0530 Subject: [PATCH 07/32] docs: Add examples to documentation for packages block --- src/blocks/packages.rs | 9 +++++++-- src/blocks/packages/apt.rs | 1 + src/blocks/packages/pacman.rs | 7 +++++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/blocks/packages.rs b/src/blocks/packages.rs index 50b8b67c96..2bd186ead5 100644 --- a/src/blocks/packages.rs +++ b/src/blocks/packages.rs @@ -3,6 +3,7 @@ //! Currently 2 package manager are avaliable: //! - `apt` for Debian/Ubuntu based system //! - `pacman` for Arch based system +//! - `aur` for Arch based system //! //! # Configuration //! @@ -22,7 +23,10 @@ //! Placeholder | Value | Type | Unit //! -------------|----------------------------------------------------------------------------------|--------|----- //! `icon` | A static icon | Icon | - -//! `count` | Number of updates available | Number | - +//! `apt` | Number of updates available in Debian/Ubuntu based system | Number | - +//! `pacman` | Number of updates available in Arch based system | Number | - +//! `aur` | Number of updates available in Arch based system | Number | - +//! `total` | Number of updates available in all package manager listed | Number | - //! //! # Example //! @@ -31,8 +35,9 @@ //! ```toml //! [[block]] //! block = "packages" +//! package_manager = ["apt", "pacman", "aur"] //! interval = 1800 -//! format = " $icon $count updates available " +//! format = " $icon $apt + $pacman + $aur = $total updates available " //! format_singular = " $icon One update available " //! format_up_to_date = " $icon system up to date " //! ``` diff --git a/src/blocks/packages/apt.rs b/src/blocks/packages/apt.rs index 4b75635098..c7d2930172 100644 --- a/src/blocks/packages/apt.rs +++ b/src/blocks/packages/apt.rs @@ -30,6 +30,7 @@ //! [[block]] //! block = "packages" //! interval = 1800 +//! package_manager = ["apt"] //! format = " $icon $apt updates available" //! format_singular = " $icon One update available " //! format_up_to_date = " $icon system up to date " diff --git a/src/blocks/packages/pacman.rs b/src/blocks/packages/pacman.rs index 83f6cc6b3d..30aedb9d08 100644 --- a/src/blocks/packages/pacman.rs +++ b/src/blocks/packages/pacman.rs @@ -46,7 +46,7 @@ //! `critical_updates_regex` | Display block as critical if updates matching regex are available. | `None` //! `aur_command` | AUR command to check available updates, which outputs in the same format as pacman. e.g. `yay -Qua` | Required if `$both` or `$aur` are used //! -//! Placeholder | Value | Type | Unit +//! Placeholder | Value | Type | Unit //! -------------|----------------------------------------------------------------------------------|--------|----- //! `icon` | A static icon | Icon | - //! `pacman` | Number of updates available according to `pacman` | Number | - @@ -59,6 +59,7 @@ //! ```toml //! [[block]] //! block = "packages" +//! package_manager = ["pacman"] //! interval = 600 //! format = " $icon $pacman updates available " //! format_singular = " $icon $pacman update available " @@ -79,6 +80,7 @@ //! ```toml //! [[block]] //! block = "packages" +//! package_manager = ["pacman"] //! interval = 600 //! format = " $icon $pacman updates available " //! format_singular = " $icon $pacman update available " @@ -95,7 +97,8 @@ //! //! ```toml //! [[block]] -//! block = "pacman" +//! block = "packages" +//! package_manager = ["pacman", "aur"] //! interval = 600 //! error_interval = 300 //! format = " $icon $pacman + $aur = $both updates available " From b15ffe49439a1469dd73148dd76f3ed9da15cd6a Mon Sep 17 00:00:00 2001 From: IshanGrover2004 Date: Sat, 20 Jan 2024 03:05:19 +0530 Subject: [PATCH 08/32] fix: Spelling check --- src/blocks/packages.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blocks/packages.rs b/src/blocks/packages.rs index 2bd186ead5..b7e919a464 100644 --- a/src/blocks/packages.rs +++ b/src/blocks/packages.rs @@ -1,6 +1,6 @@ //! Shows pending updates for different package manager like apt, pacman, etc. //! -//! Currently 2 package manager are avaliable: +//! Currently 2 package manager are available: //! - `apt` for Debian/Ubuntu based system //! - `pacman` for Arch based system //! - `aur` for Arch based system From 85eff453a4d53d95e89662fb8ed7b4ed4a6cabd5 Mon Sep 17 00:00:00 2001 From: IshanGrover2004 Date: Sat, 20 Jan 2024 03:16:02 +0530 Subject: [PATCH 09/32] feat: Add deleted old blocks --- src/blocks.rs | 2 + src/blocks/apt.rs | 249 +++++++++++++++++++++++++++ src/blocks/pacman.rs | 395 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 646 insertions(+) create mode 100644 src/blocks/apt.rs create mode 100644 src/blocks/pacman.rs diff --git a/src/blocks.rs b/src/blocks.rs index e9257cca1d..c2bcd18dbd 100644 --- a/src/blocks.rs +++ b/src/blocks.rs @@ -136,6 +136,8 @@ macro_rules! define_blocks { define_blocks!( amd_gpu, + apt, + pacman, packages, backlight, battery, diff --git a/src/blocks/apt.rs b/src/blocks/apt.rs new file mode 100644 index 0000000000..53cc460fc3 --- /dev/null +++ b/src/blocks/apt.rs @@ -0,0 +1,249 @@ +//! Pending updates available for your Debian/Ubuntu based system +//! +//! 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. +//! +//! Tip: You can grab the list of available updates using `APT_CONFIG=/tmp/i3rs-apt/apt.conf apt list --upgradable` +//! +//! # Configuration +//! +//! Key | Values | Default +//! ----|--------|-------- +//! `interval` | Update interval in seconds. | `600` +//! `format` | A string to customise the output of this block. See below for available placeholders. | `" $icon $count.eng(w:1) "` +//! `format_singular` | Same as `format`, but for when exactly one update is available. | `" $icon $count.eng(w:1) "` +//! `format_up_to_date` | Same as `format`, but for when no updates are available. | `" $icon $count.eng(w:1) "` +//! `warning_updates_regex` | Display block as warning if updates matching regex are available. | `None` +//! `critical_updates_regex` | Display block as critical if updates matching regex are available. | `None` +//! `ignore_updates_regex` | Doesn't include updates matching regex in the count. | `None` +//! `ignore_phased_updates` | Doesn't include potentially held back phased updates in the count. | `false` +//! +//! Placeholder | Value | Type | Unit +//! ------------|-----------------------------|--------|------ +//! `icon` | A static icon | Icon | - +//! `count` | Number of updates available | Number | - +//! +//! # Example +//! +//! Update the list of pending updates every thirty minutes (1800 seconds): +//! +//! ```toml +//! [[block]] +//! block = "apt" +//! interval = 1800 +//! format = " $icon $count updates available " +//! format_singular = " $icon One update available " +//! format_up_to_date = " $icon system up to date " +//! critical_updates_regex = "(linux|linux-lts|linux-zen)" +//! [[block.click]] +//! # shows dmenu with cached available updates. Any dmenu alternative should also work. +//! button = "left" +//! cmd = "APT_CONFIG=/tmp/i3rs-apt/apt.conf apt list --upgradable | tail -n +2 | rofi -dmenu" +//! [[block.click]] +//! # Updates the block on right click +//! button = "right" +//! update = true +//! ``` +//! +//! # Icons Used +//! +//! - `update` + +use std::env; +use std::process::Stdio; + +use regex::Regex; + +use tokio::fs::{create_dir_all, File}; +use tokio::process::Command; + +use super::prelude::*; + +#[derive(Deserialize, Debug, SmartDefault)] +#[serde(deny_unknown_fields, default)] +pub struct Config { + #[default(600.into())] + pub interval: Seconds, + pub format: FormatConfig, + pub format_singular: FormatConfig, + pub format_up_to_date: FormatConfig, + pub warning_updates_regex: Option, + pub critical_updates_regex: Option, + pub ignore_updates_regex: Option, + pub ignore_phased_updates: bool, +} + +pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { + let format = config.format.with_default(" $icon $count.eng(w:1) ")?; + let format_singular = config + .format_singular + .with_default(" $icon $count.eng(w:1) ")?; + let format_up_to_date = config + .format_up_to_date + .with_default(" $icon $count.eng(w:1) ")?; + + let warning_updates_regex = config + .warning_updates_regex + .as_deref() + .map(Regex::new) + .transpose() + .error("invalid warning updates regex")?; + let critical_updates_regex = config + .critical_updates_regex + .as_deref() + .map(Regex::new) + .transpose() + .error("invalid critical updates regex")?; + let ignore_updates_regex = config + .ignore_updates_regex + .as_deref() + .map(Regex::new) + .transpose() + .error("invalid ignore updates regex")?; + + let mut cache_dir = env::temp_dir(); + cache_dir.push("i3rs-apt"); + if !cache_dir.exists() { + create_dir_all(&cache_dir) + .await + .error("Failed to create temp dir")?; + } + + let apt_config = format!( + "Dir::State \"{}\";\n + Dir::State::lists \"lists\";\n + Dir::Cache \"{}\";\n + Dir::Cache::srcpkgcache \"srcpkgcache.bin\";\n + Dir::Cache::pkgcache \"pkgcache.bin\";", + cache_dir.display(), + cache_dir.display(), + ); + + let mut config_file = cache_dir; + config_file.push("apt.conf"); + let config_file = config_file.to_str().unwrap(); + + let mut file = File::create(&config_file) + .await + .error("Failed to create config file")?; + file.write_all(apt_config.as_bytes()) + .await + .error("Failed to write to config file")?; + + loop { + let mut widget = Widget::new(); + let updates = get_updates_list(config_file).await?; + let count = get_update_count( + config_file, + config.ignore_phased_updates, + ignore_updates_regex.as_ref(), + &updates, + ) + .await?; + + widget.set_format(match count { + 0 => format_up_to_date.clone(), + 1 => format_singular.clone(), + _ => format.clone(), + }); + widget.set_values(map!( + "count" => Value::number(count), + "icon" => Value::icon("update"), + )); + + let warning = warning_updates_regex + .as_ref() + .is_some_and(|regex| has_matching_update(&updates, regex)); + let critical = critical_updates_regex + .as_ref() + .is_some_and(|regex| has_matching_update(&updates, regex)); + widget.state = match count { + 0 => State::Idle, + _ => { + if critical { + State::Critical + } else if warning { + State::Warning + } else { + State::Info + } + } + }; + + api.set_widget(widget)?; + + select! { + _ = sleep(config.interval.0) => (), + _ = api.wait_for_update_request() => (), + } + } +} + +async fn get_updates_list(config_path: &str) -> Result { + Command::new("apt") + .env("APT_CONFIG", config_path) + .args(["update"]) + .stdout(Stdio::null()) + .stdin(Stdio::null()) + .spawn() + .error("Failed to run `apt update`")? + .wait() + .await + .error("Failed to run `apt update`")?; + let stdout = Command::new("apt") + .env("LANG", "C") + .env("APT_CONFIG", config_path) + .args(["list", "--upgradable"]) + .output() + .await + .error("Problem running apt command")? + .stdout; + String::from_utf8(stdout).error("apt produced non-UTF8 output") +} + +async fn get_update_count( + config_path: &str, + ignore_phased_updates: bool, + ignore_updates_regex: Option<&Regex>, + updates: &str, +) -> Result { + let mut cnt = 0; + + for update_line in updates + .lines() + .filter(|line| line.contains("[upgradable")) + .filter(|line| ignore_updates_regex.map_or(true, |re| !re.is_match(line))) + { + if !ignore_phased_updates || !is_phased_update(config_path, update_line).await? { + cnt += 1; + } + } + + Ok(cnt) +} + +fn has_matching_update(updates: &str, regex: &Regex) -> bool { + updates.lines().any(|line| regex.is_match(line)) +} + +async fn is_phased_update(config_path: &str, package_line: &str) -> Result { + let package_name_regex = regex!(r#"(.*)/.*"#); + let package_name = &package_name_regex + .captures(package_line) + .error("Couldn't find package name")?[1]; + + let output = String::from_utf8( + Command::new("apt-cache") + .args(["-c", config_path, "policy", package_name]) + .output() + .await + .error("Problem running apt-cache command")? + .stdout, + ) + .error("Problem capturing apt-cache command output")?; + + let phased_regex = regex!(r".*\(phased (\d+)%\).*"); + Ok(match phased_regex.captures(&output) { + Some(matches) => &matches[1] != "100", + None => false, + }) +} diff --git a/src/blocks/pacman.rs b/src/blocks/pacman.rs new file mode 100644 index 0000000000..0e6c74a1f9 --- /dev/null +++ b/src/blocks/pacman.rs @@ -0,0 +1,395 @@ +//! Pending updates available on pacman or an AUR helper. +//! +//! Requires fakeroot to be installed (only required for pacman). +//! +//! Tip: You can grab the list of available updates using `fakeroot pacman -Qu --dbpath /tmp/checkup-db-i3statusrs-$USER/`. +//! If you have the `CHECKUPDATES_DB` env var set on your system then substitute that dir instead. +//! +//! 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. +//! +//! # Pacman hook +//! +//! Tip: On Arch Linux you can setup a `pacman` hook to signal i3status-rs to update after packages +//! have been upgraded, so you won't have stale info in your pacman block. +//! +//! In the block configuration, set `signal = 1` (or other number if `1` is being used by some +//! other block): +//! +//! ```toml +//! [[block]] +//! block = "pacman" +//! signal = 1 +//! ``` +//! +//! Create `/etc/pacman.d/hooks/i3status-rust.hook` with the below contents: +//! +//! ```ini +//! [Trigger] +//! Operation = Upgrade +//! Type = Package +//! Target = * +//! +//! [Action] +//! When = PostTransaction +//! Exec = /usr/bin/pkill -SIGRTMIN+1 i3status-rs +//! ``` +//! +//! # Configuration +//! +//! Key | Values | Default +//! ----|--------|--------- +//! `interval` | Update interval, in seconds. If setting `aur_command` then set interval appropriately as to not exceed the AUR's daily rate limit. | `600` +//! `format` | A string to customise the output of this block. See below for available placeholders. | `" $icon $pacman.eng(w:1) "` +//! `format_singular` | Same as `format` but for when exactly one update is available. | `" $icon $pacman.eng(w:1) "` +//! `format_up_to_date` | Same as `format` but for when no updates are available. | `" $icon $pacman.eng(w:1) "` +//! `warning_updates_regex` | Display block as warning if updates matching regex are available. | `None` +//! `critical_updates_regex` | Display block as critical if updates matching regex are available. | `None` +//! `aur_command` | AUR command to check available updates, which outputs in the same format as pacman. e.g. `yay -Qua` | Required if `$both` or `$aur` are used +//! +//! Placeholder | Value | Type | Unit +//! -------------|----------------------------------------------------------------------------------|--------|----- +//! `icon` | A static icon | Icon | - +//! `pacman` | Number of updates available according to `pacman` | Number | - +//! `aur` | Number of updates available according to `` | Number | - +//! `both` | Cumulative number of updates available according to `pacman` and `` | Number | - +//! +//! # Examples +//! +//! pacman only config: +//! +//! ```toml +//! [[block]] +//! block = "pacman" +//! interval = 600 +//! format = " $icon $pacman updates available " +//! format_singular = " $icon $pacman update available " +//! format_up_to_date = " $icon system up to date " +//! critical_updates_regex = "(linux|linux-lts|linux-zen)" +//! [[block.click]] +//! # pop-up a menu showing the available updates. Replace wofi with your favourite menu command. +//! button = "left" +//! cmd = "fakeroot pacman -Qu --dbpath /tmp/checkup-db-i3statusrs-$USER/ | wofi --show dmenu" +//! [[block.click]] +//! # Updates the block on right click +//! button = "right" +//! update = true +//! ``` +//! +//! pacman only config using warnings with ZFS modules: +//! +//! ```toml +//! [[block]] +//! block = "pacman" +//! interval = 600 +//! format = " $icon $pacman updates available " +//! format_singular = " $icon $pacman update available " +//! format_up_to_date = " $icon system up to date " +//! # If a linux update is available, but no ZFS package, it won't be possible to +//! # actually perform a system upgrade, so we show a warning. +//! warning_updates_regex = "(linux|linux-lts|linux-zen)" +//! # If ZFS is available, we know that we can and should do an upgrade, so we show +//! # the status as critical. +//! critical_updates_regex = "(zfs|zfs-lts)" +//! ``` +//! +//! pacman and AUR helper config: +//! +//! ```toml +//! [[block]] +//! block = "pacman" +//! interval = 600 +//! error_interval = 300 +//! format = " $icon $pacman + $aur = $both updates available " +//! format_singular = " $icon $both update available " +//! format_up_to_date = " $icon system up to date " +//! critical_updates_regex = "(linux|linux-lts|linux-zen)" +//! # aur_command should output available updates to stdout (ie behave as echo -ne "update\n") +//! aur_command = "yay -Qua" +//! ``` +//! +//! # Icons Used +//! +//! - `update` + +use std::env; +use std::path::PathBuf; +use std::process::Stdio; + +use regex::Regex; + +use tokio::fs::{create_dir_all, symlink}; +use tokio::process::Command; + +use super::prelude::*; +use crate::util::has_command; + +make_log_macro!(debug, "pacman"); + +static PACMAN_UPDATES_DB: Lazy = Lazy::new(|| { + let path = match env::var_os("CHECKUPDATES_DB") { + Some(val) => val.into(), + None => { + let mut path = env::temp_dir(); + let user = env::var("USER"); + path.push(format!( + "checkup-db-i3statusrs-{}", + user.as_deref().unwrap_or("no-user") + )); + path + } + }; + debug!("Using {} as updates DB path", path.display()); + path +}); + +static PACMAN_DB: Lazy = Lazy::new(|| { + let path = env::var_os("DBPath") + .map(Into::into) + .unwrap_or_else(|| PathBuf::from("/var/lib/pacman/")); + debug!("Using {} as pacman DB path", path.display()); + path +}); + +#[derive(Deserialize, Debug, SmartDefault)] +#[serde(deny_unknown_fields, default)] +pub struct Config { + #[default(600.into())] + pub interval: Seconds, + pub format: FormatConfig, + pub format_singular: FormatConfig, + pub format_up_to_date: FormatConfig, + pub warning_updates_regex: Option, + pub critical_updates_regex: Option, + pub aur_command: Option, +} + +pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { + let format = config.format.with_default(" $icon $pacman.eng(w:1) ")?; + let format_singular = config + .format_singular + .with_default(" $icon $pacman.eng(w:1) ")?; + let format_up_to_date = config + .format_up_to_date + .with_default(" $icon $pacman.eng(w:1) ")?; + + macro_rules! any_format_contains { + ($name:expr) => { + format.contains_key($name) + || format_singular.contains_key($name) + || format_up_to_date.contains_key($name) + }; + } + let aur = any_format_contains!("aur"); + let pacman = any_format_contains!("pacman"); + let both = any_format_contains!("both"); + let watched = if both || (pacman && aur) { + Watched::Both( + config + .aur_command + .as_deref() + .error("$aur or $both found in format string but no aur_command supplied")?, + ) + } else if pacman && !aur { + Watched::Pacman + } else if !pacman && aur { + Watched::Aur( + config + .aur_command + .as_deref() + .error("$aur or $both found in format string but no aur_command supplied")?, + ) + } else { + Watched::None + }; + + if matches!(watched, Watched::Pacman | Watched::Both(_)) { + check_fakeroot_command_exists().await?; + } + + let warning_updates_regex = config + .warning_updates_regex + .as_deref() + .map(Regex::new) + .transpose() + .error("invalid warning updates regex")?; + let critical_updates_regex = config + .critical_updates_regex + .as_deref() + .map(Regex::new) + .transpose() + .error("invalid critical updates regex")?; + + loop { + let (mut values, warning, critical, total) = match &watched { + Watched::Pacman => { + let updates = get_pacman_available_updates().await?; + let count = get_update_count(&updates); + let values = map!("pacman" => Value::number(count)); + let warning = warning_updates_regex + .as_ref() + .is_some_and(|regex| has_matching_update(&updates, regex)); + let critical = critical_updates_regex + .as_ref() + .is_some_and(|regex| has_matching_update(&updates, regex)); + (values, warning, critical, count) + } + Watched::Aur(aur_command) => { + let updates = get_aur_available_updates(aur_command).await?; + let count = get_update_count(&updates); + let values = map!( + "aur" => Value::number(count) + ); + let warning = warning_updates_regex + .as_ref() + .is_some_and(|regex| has_matching_update(&updates, regex)); + let critical = critical_updates_regex + .as_ref() + .is_some_and(|regex| has_matching_update(&updates, regex)); + (values, warning, critical, count) + } + Watched::Both(aur_command) => { + let (pacman_updates, aur_updates) = tokio::try_join!( + get_pacman_available_updates(), + get_aur_available_updates(aur_command) + )?; + let pacman_count = get_update_count(&pacman_updates); + let aur_count = get_update_count(&aur_updates); + let values = map! { + "pacman" => Value::number(pacman_count), + "aur" => Value::number(aur_count), + "both" => Value::number(pacman_count + aur_count), + }; + let warning = warning_updates_regex.as_ref().is_some_and(|regex| { + has_matching_update(&aur_updates, regex) + || has_matching_update(&pacman_updates, regex) + }); + let critical = critical_updates_regex.as_ref().is_some_and(|regex| { + has_matching_update(&aur_updates, regex) + || has_matching_update(&pacman_updates, regex) + }); + (values, warning, critical, pacman_count + aur_count) + } + Watched::None => (HashMap::new(), false, false, 0), + }; + values.insert("icon".into(), Value::icon("update")); + + let mut widget = Widget::new(); + widget.set_format(match total { + 0 => format_up_to_date.clone(), + 1 => format_singular.clone(), + _ => format.clone(), + }); + widget.set_values(values); + widget.state = match total { + 0 => State::Idle, + _ => { + if critical { + State::Critical + } else if warning { + State::Warning + } else { + State::Info + } + } + }; + api.set_widget(widget)?; + + select! { + _ = sleep(config.interval.0) => (), + _ = api.wait_for_update_request() => (), + } + } +} + +#[derive(Debug, PartialEq, Eq)] +enum Watched<'a> { + None, + Pacman, + Aur(&'a str), + Both(&'a str), +} + +async fn check_fakeroot_command_exists() -> Result<()> { + if !has_command("fakeroot").await? { + Err(Error::new("fakeroot not found")) + } else { + Ok(()) + } +} + +async fn get_pacman_available_updates() -> Result { + // Create the determined `checkup-db` path recursively + create_dir_all(&*PACMAN_UPDATES_DB).await.or_error(|| { + format!( + "Failed to create checkup-db directory at '{}'", + PACMAN_UPDATES_DB.display() + ) + })?; + + // Create symlink to local cache in `checkup-db` if required + let local_cache = PACMAN_UPDATES_DB.join("local"); + if !local_cache.exists() { + symlink(PACMAN_DB.join("local"), local_cache) + .await + .error("Failed to created required symlink")?; + } + + // Update database + let status = Command::new("fakeroot") + .env("LC_ALL", "C") + .args([ + "--".as_ref(), + "pacman".as_ref(), + "-Sy".as_ref(), + "--dbpath".as_ref(), + PACMAN_UPDATES_DB.as_os_str(), + "--logfile".as_ref(), + "/dev/null".as_ref(), + ]) + .stdout(Stdio::null()) + .status() + .await + .error("Failed to run command")?; + if !status.success() { + debug!("{}", status); + return Err(Error::new("pacman -Sy exited with non zero exit status")); + } + + let stdout = Command::new("fakeroot") + .env("LC_ALL", "C") + .args([ + "--".as_ref(), + "pacman".as_ref(), + "-Qu".as_ref(), + "--dbpath".as_ref(), + PACMAN_UPDATES_DB.as_os_str(), + ]) + .output() + .await + .error("There was a problem running the pacman commands")? + .stdout; + + String::from_utf8(stdout).error("Pacman produced non-UTF8 output") +} + +async fn get_aur_available_updates(aur_command: &str) -> Result { + let stdout = Command::new("sh") + .args(["-c", aur_command]) + .output() + .await + .or_error(|| format!("aur command: {aur_command} failed"))? + .stdout; + String::from_utf8(stdout) + .error("There was a problem while converting the aur command output to a string") +} + +fn get_update_count(updates: &str) -> usize { + updates + .lines() + .filter(|line| !line.contains("[ignored]")) + .count() +} + +fn has_matching_update(updates: &str, regex: &Regex) -> bool { + updates.lines().any(|line| regex.is_match(line)) +} From 0c4dee8bb107a347a16d0900d8d3f8f7aff2d44b Mon Sep 17 00:00:00 2001 From: IshanGrover2004 Date: Sat, 20 Jan 2024 04:37:31 +0530 Subject: [PATCH 10/32] refactor: Re-arrange block list in alphabetical order --- src/blocks.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/blocks.rs b/src/blocks.rs index c2bcd18dbd..836e65f7d2 100644 --- a/src/blocks.rs +++ b/src/blocks.rs @@ -137,8 +137,6 @@ macro_rules! define_blocks { define_blocks!( amd_gpu, apt, - pacman, - packages, backlight, battery, bluetooth, @@ -164,6 +162,8 @@ define_blocks!( #[cfg(feature = "notmuch")] notmuch, nvidia_gpu, + packages, + pacman, pomodoro, rofication, service_status, From 3dcc9efb8179655580b1ca738d8e0c0e81fac12c Mon Sep 17 00:00:00 2001 From: IshanGrover2004 Date: Sat, 20 Jan 2024 04:43:17 +0530 Subject: [PATCH 11/32] refactor: Add function to `impl Apt` block --- src/blocks/packages/apt.rs | 50 ++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/src/blocks/packages/apt.rs b/src/blocks/packages/apt.rs index c7d2930172..d6a082d3de 100644 --- a/src/blocks/packages/apt.rs +++ b/src/blocks/packages/apt.rs @@ -71,6 +71,29 @@ impl Apt { ignore_updates_regex: Default::default(), } } + + async fn is_phased_update(&self, package_line: &str) -> Result { + let package_name_regex = regex!(r#"(.*)/.*"#); + let package_name = &package_name_regex + .captures(package_line) + .error("Couldn't find package name")?[1]; + + let output = String::from_utf8( + Command::new("apt-cache") + .args(["-c", &self.config_file, "policy", package_name]) + .output() + .await + .error("Problem running apt-cache command")? + .stdout, + ) + .error("Problem capturing apt-cache command output")?; + + let phased_regex = regex!(r".*\(phased (\d+)%\).*"); + Ok(match phased_regex.captures(&output) { + Some(matches) => &matches[1] != "100", + None => false, + }) + } } #[async_trait] @@ -144,9 +167,7 @@ impl Backend for Apt { .map_or(true, |re| !re.is_match(line)) }) { - if !self.ignore_phased_updates - || !is_phased_update(&self.config_file, update_line).await? - { + if !self.ignore_phased_updates || !self.is_phased_update(update_line).await? { cnt += 1; } } @@ -154,26 +175,3 @@ impl Backend for Apt { Ok(cnt) } } - -async fn is_phased_update(config_path: &str, package_line: &str) -> Result { - let package_name_regex = regex!(r#"(.*)/.*"#); - let package_name = &package_name_regex - .captures(package_line) - .error("Couldn't find package name")?[1]; - - let output = String::from_utf8( - Command::new("apt-cache") - .args(["-c", config_path, "policy", package_name]) - .output() - .await - .error("Problem running apt-cache command")? - .stdout, - ) - .error("Problem capturing apt-cache command output")?; - - let phased_regex = regex!(r".*\(phased (\d+)%\).*"); - Ok(match phased_regex.captures(&output) { - Some(matches) => &matches[1] != "100", - None => false, - }) -} From 9897b3ac2713c5963da876ad7d7c33e83ec599ea Mon Sep 17 00:00:00 2001 From: IshanGrover2004 Date: Sat, 20 Jan 2024 06:02:42 +0530 Subject: [PATCH 12/32] docs: Remove documentation from submodules and add to `packages` block --- src/blocks/packages.rs | 126 +++++++++++++++++++++++++++++++++- src/blocks/packages/apt.rs | 51 -------------- src/blocks/packages/pacman.rs | 115 ------------------------------- 3 files changed, 123 insertions(+), 169 deletions(-) diff --git a/src/blocks/packages.rs b/src/blocks/packages.rs index b7e919a464..a55c5886c9 100644 --- a/src/blocks/packages.rs +++ b/src/blocks/packages.rs @@ -1,6 +1,6 @@ -//! Shows pending updates for different package manager like apt, pacman, etc. +//! Pending updates for different package manager like apt, pacman, etc. //! -//! Currently 2 package manager are available: +//! Currently these package managers are available: //! - `apt` for Debian/Ubuntu based system //! - `pacman` for Arch based system //! - `aur` for Arch based system @@ -18,7 +18,7 @@ //! `critical_updates_regex` | Display block as critical if updates matching regex are available. | `None` //! `ignore_updates_regex` | Doesn't include updates matching regex in the count. | `None` //! `ignore_phased_updates` | Doesn't include potentially held back phased updates in the count. (For Debian/Ubuntu based system) | `false` -//! `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 `$both` or `$aur` are used +//! `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 //! //! Placeholder | Value | Type | Unit //! -------------|----------------------------------------------------------------------------------|--------|----- @@ -28,8 +28,128 @@ //! `aur` | Number of updates available in Arch based system | Number | - //! `total` | Number of updates available in all package manager listed | Number | - //! +//! # Apt +//! +//! 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. +//! +//! Tip: You can grab the list of available updates using `APT_CONFIG=/tmp/i3rs-apt/apt.conf apt list --upgradable` +//! +//! # Pacman +//! +//! Requires fakeroot to be installed (only required for pacman). +//! +//! Tip: You can grab the list of available updates using `fakeroot pacman -Qu --dbpath /tmp/checkup-db-i3statusrs-$USER/`. +//! If you have the `CHECKUPDATES_DB` env var set on your system then substitute that dir instead. +//! +//! 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. +//! +//! ### Pacman hook +//! +//! Tip: On Arch Linux you can setup a `pacman` hook to signal i3status-rs to update after packages +//! have been upgraded, so you won't have stale info in your pacman block. +//! +//! In the block configuration, set `signal = 1` (or other number if `1` is being used by some +//! other block): +//! +//! ```toml +//! [[block]] +//! block = "packages" +//! signal = 1 +//! ``` +//! +//! Create `/etc/pacman.d/hooks/i3status-rust.hook` with the below contents: +//! +//! ```ini +//! [Trigger] +//! Operation = Upgrade +//! Type = Package +//! Target = * +//! +//! [Action] +//! When = PostTransaction +//! Exec = /usr/bin/pkill -SIGRTMIN+1 i3status-rs +//! ``` +//! //! # Example //! +//! Apt only config +//! +//! ```toml +//! [[block]] +//! block = "packages" +//! interval = 1800 +//! package_manager = ["apt"] +//! format = " $icon $apt updates available" +//! format_singular = " $icon One update available " +//! format_up_to_date = " $icon system up to date " +//! critical_updates_regex = "(linux|linux-lts|linux-zen)" +//! [[block.click]] +//! # shows dmenu with cached available updates. Any dmenu alternative should also work. +//! button = "left" +//! cmd = "APT_CONFIG=/tmp/i3rs-apt/apt.conf apt list --upgradable | tail -n +2 | rofi -dmenu" +//! [[block.click]] +//! # Updates the block on right click +//! button = "right" +//! update = true +//! ``` +//! +//! Pacman only config: +//! +//! ```toml +//! [[block]] +//! block = "packages" +//! package_manager = ["pacman"] +//! interval = 600 +//! format = " $icon $pacman updates available " +//! format_singular = " $icon $pacman update available " +//! format_up_to_date = " $icon system up to date " +//! critical_updates_regex = "(linux|linux-lts|linux-zen)" +//! [[block.click]] +//! # pop-up a menu showing the available updates. Replace wofi with your favourite menu command. +//! button = "left" +//! cmd = "fakeroot pacman -Qu --dbpath /tmp/checkup-db-i3statusrs-$USER/ | wofi --show dmenu" +//! [[block.click]] +//! # Updates the block on right click +//! button = "right" +//! update = true +//! ``` +//! +//! Pacman only config using warnings with ZFS modules: +//! +//! ```toml +//! [[block]] +//! block = "packages +//! package_manager = ["pacman"] +//! interval = 600 +//! format = " $icon $pacman updates available " +//! format_singular = " $icon $pacman update available " +//! format_up_to_date = " $icon system up to date " +//! # If a linux update is available, but no ZFS package, it won't be possible to +//! # actually perform a system upgrade, so we show a warning. +//! warning_updates_regex = "(linux|linux-lts|linux-zen)" +//! # If ZFS is available, we know that we can and should do an upgrade, so we show +//! # the status as critical. +//! critical_updates_regex = "(zfs|zfs-lts)" +//! ``` +//! +//! Pacman and AUR helper config: +//! +//! ```toml +//! [[block]] +//! block = "packages" +//! package_manager = ["pacman", "aur"] +//! interval = 600 +//! error_interval = 300 +//! format = " $icon $pacman + $aur = $both updates available " +//! format_singular = " $icon $both update available " +//! format_up_to_date = " $icon system up to date " +//! critical_updates_regex = "(linux|linux-lts|linux-zen)" +//! # aur_command should output available updates to stdout (ie behave as echo -ne "update\n") +//! aur_command = "yay -Qua" +//! ``` +//! +//! Multiple package managers config: +//! //! Update the list of pending updates every thirty minutes (1800 seconds): //! //! ```toml diff --git a/src/blocks/packages/apt.rs b/src/blocks/packages/apt.rs index d6a082d3de..4c529320cd 100644 --- a/src/blocks/packages/apt.rs +++ b/src/blocks/packages/apt.rs @@ -1,54 +1,3 @@ -//! Pending updates available for your Debian/Ubuntu based system -//! -//! 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. -//! -//! Tip: You can grab the list of available updates using `APT_CONFIG=/tmp/i3rs-apt/apt.conf apt list --upgradable` -//! -//! # Configuration -//! -//! Key | Values | Default -//! ----|--------|-------- -//! `interval` | Update interval in seconds. | `600` -//! `format` | A string to customise the output of this block. See below for available placeholders. | `" $icon $count.eng(w:1) "` -//! `format_singular` | Same as `format`, but for when exactly one update is available. | `" $icon $count.eng(w:1) "` -//! `format_up_to_date` | Same as `format`, but for when no updates are available. | `" $icon $count.eng(w:1) "` -//! `warning_updates_regex` | Display block as warning if updates matching regex are available. | `None` -//! `critical_updates_regex` | Display block as critical if updates matching regex are available. | `None` -//! `ignore_updates_regex` | Doesn't include updates matching regex in the count. | `None` -//! `ignore_phased_updates` | Doesn't include potentially held back phased updates in the count. | `false` -//! -//! Placeholder | Value | Type | Unit -//! ------------|-----------------------------|--------|------ -//! `icon` | A static icon | Icon | - -//! `apt` | Number of updates available | Number | - -//! -//! # Example -//! -//! Update the list of pending updates every thirty minutes (1800 seconds): -//! -//! ```toml -//! [[block]] -//! block = "packages" -//! interval = 1800 -//! package_manager = ["apt"] -//! format = " $icon $apt updates available" -//! format_singular = " $icon One update available " -//! format_up_to_date = " $icon system up to date " -//! critical_updates_regex = "(linux|linux-lts|linux-zen)" -//! [[block.click]] -//! # shows dmenu with cached available updates. Any dmenu alternative should also work. -//! button = "left" -//! cmd = "APT_CONFIG=/tmp/i3rs-apt/apt.conf apt list --upgradable | tail -n +2 | rofi -dmenu" -//! [[block.click]] -//! # Updates the block on right click -//! button = "right" -//! update = true -//! ``` -//! -//! # Icons Used -//! -//! - `update` - use std::env; use std::process::Stdio; diff --git a/src/blocks/packages/pacman.rs b/src/blocks/packages/pacman.rs index 30aedb9d08..d23b9ff230 100644 --- a/src/blocks/packages/pacman.rs +++ b/src/blocks/packages/pacman.rs @@ -1,118 +1,3 @@ -//! Pending updates available on pacman or an AUR helper. -//! -//! Requires fakeroot to be installed (only required for pacman). -//! -//! Tip: You can grab the list of available updates using `fakeroot pacman -Qu --dbpath /tmp/checkup-db-i3statusrs-$USER/`. -//! If you have the `CHECKUPDATES_DB` env var set on your system then substitute that dir instead. -//! -//! 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. -//! -//! # Pacman hook -//! -//! Tip: On Arch Linux you can setup a `pacman` hook to signal i3status-rs to update after packages -//! have been upgraded, so you won't have stale info in your pacman block. -//! -//! In the block configuration, set `signal = 1` (or other number if `1` is being used by some -//! other block): -//! -//! ```toml -//! [[block]] -//! block = "packages" -//! signal = 1 -//! ``` -//! -//! Create `/etc/pacman.d/hooks/i3status-rust.hook` with the below contents: -//! -//! ```ini -//! [Trigger] -//! Operation = Upgrade -//! Type = Package -//! Target = * -//! -//! [Action] -//! When = PostTransaction -//! Exec = /usr/bin/pkill -SIGRTMIN+1 i3status-rs -//! ``` -//! -//! # Configuration -//! -//! Key | Values | Default -//! ----|--------|--------- -//! `interval` | Update interval, in seconds. If setting `aur_command` then set interval appropriately as to not exceed the AUR's daily rate limit. | `600` -//! `format` | A string to customise the output of this block. See below for available placeholders. | `" $icon $pacman.eng(w:1) "` -//! `format_singular` | Same as `format` but for when exactly one update is available. | `" $icon $pacman.eng(w:1) "` -//! `format_up_to_date` | Same as `format` but for when no updates are available. | `" $icon $pacman.eng(w:1) "` -//! `warning_updates_regex` | Display block as warning if updates matching regex are available. | `None` -//! `critical_updates_regex` | Display block as critical if updates matching regex are available. | `None` -//! `aur_command` | AUR command to check available updates, which outputs in the same format as pacman. e.g. `yay -Qua` | Required if `$both` or `$aur` are used -//! -//! Placeholder | Value | Type | Unit -//! -------------|----------------------------------------------------------------------------------|--------|----- -//! `icon` | A static icon | Icon | - -//! `pacman` | Number of updates available according to `pacman` | Number | - -//! `aur` | Number of updates available according to `` | Number | - -//! -//! # Examples -//! -//! pacman only config: -//! -//! ```toml -//! [[block]] -//! block = "packages" -//! package_manager = ["pacman"] -//! interval = 600 -//! format = " $icon $pacman updates available " -//! format_singular = " $icon $pacman update available " -//! format_up_to_date = " $icon system up to date " -//! critical_updates_regex = "(linux|linux-lts|linux-zen)" -//! [[block.click]] -//! # pop-up a menu showing the available updates. Replace wofi with your favourite menu command. -//! button = "left" -//! cmd = "fakeroot pacman -Qu --dbpath /tmp/checkup-db-i3statusrs-$USER/ | wofi --show dmenu" -//! [[block.click]] -//! # Updates the block on right click -//! button = "right" -//! update = true -//! ``` -//! -//! pacman only config using warnings with ZFS modules: -//! -//! ```toml -//! [[block]] -//! block = "packages" -//! package_manager = ["pacman"] -//! interval = 600 -//! format = " $icon $pacman updates available " -//! format_singular = " $icon $pacman update available " -//! format_up_to_date = " $icon system up to date " -//! # If a linux update is available, but no ZFS package, it won't be possible to -//! # actually perform a system upgrade, so we show a warning. -//! warning_updates_regex = "(linux|linux-lts|linux-zen)" -//! # If ZFS is available, we know that we can and should do an upgrade, so we show -//! # the status as critical. -//! critical_updates_regex = "(zfs|zfs-lts)" -//! ``` -//! -//! pacman and AUR helper config: -//! -//! ```toml -//! [[block]] -//! block = "packages" -//! package_manager = ["pacman", "aur"] -//! interval = 600 -//! error_interval = 300 -//! format = " $icon $pacman + $aur = $both updates available " -//! format_singular = " $icon $both update available " -//! format_up_to_date = " $icon system up to date " -//! critical_updates_regex = "(linux|linux-lts|linux-zen)" -//! # aur_command should output available updates to stdout (ie behave as echo -ne "update\n") -//! aur_command = "yay -Qua" -//! ``` -//! -//! # Icons Used -//! -//! - `update` - use std::env; use std::path::PathBuf; use std::process::Stdio; From 4c115ccc6336c14c52710f2bcdbb3b5eaf862097 Mon Sep 17 00:00:00 2001 From: IshanGrover2004 Date: Sat, 20 Jan 2024 23:06:46 +0530 Subject: [PATCH 13/32] refactor: Use `Vec>` for later use also and clean code --- src/blocks/packages.rs | 71 +++++++++++++---------------------- src/blocks/packages/apt.rs | 5 +++ src/blocks/packages/pacman.rs | 8 ++++ 3 files changed, 40 insertions(+), 44 deletions(-) diff --git a/src/blocks/packages.rs b/src/blocks/packages.rs index a55c5886c9..e82f412b1a 100644 --- a/src/blocks/packages.rs +++ b/src/blocks/packages.rs @@ -253,16 +253,15 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { .transpose() .error("invalid ignore updates regex")?; - // Setup once everything it takes to check updates for every package manager - for package_manager in &config.package_manager { - let mut backend: Box = match package_manager { - PackageManager::Apt => Box::new(Apt::new()), - PackageManager::Pacman => Box::new(Pacman::new()), - PackageManager::Aur => Box::new(Aur::new()), - }; - - backend.setup().await?; - } + let package_manager_vec: Vec> = config + .package_manager + .iter() + .map(|&package_manager| match package_manager { + PackageManager::Apt => Box::new(Apt::new()) as Box, + PackageManager::Pacman => Box::new(Pacman::new()) as Box, + PackageManager::Aur => Box::new(Aur::new()) as Box, + }) + .collect(); loop { let (mut apt_count, mut pacman_count, mut aur_count) = (0, 0, 0); @@ -270,50 +269,32 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { let mut warning_vec = vec![false]; // Iterate over the all package manager listed in Config - for package_manager in config.package_manager.clone() { - match package_manager { + for package_manager in &package_manager_vec { + let updates = package_manager.get_updates_list().await?; + + match package_manager.package_manager() { PackageManager::Apt => { - let mut apt = Apt::new(); + let mut apt = Apt::default(); apt.ignore_updates_regex = ignore_updates_regex.clone(); apt.ignore_phased_updates = config.ignore_phased_updates; - let updates = apt.get_updates_list().await?; apt_count = apt.get_update_count(&updates).await?; - let warning = warning_updates_regex - .as_ref() - .is_some_and(|regex| apt.has_matching_update(&updates, regex)); - let critical = critical_updates_regex - .as_ref() - .is_some_and(|regex| apt.has_matching_update(&updates, regex)); - warning_vec.push(warning); - critical_vec.push(critical); } PackageManager::Pacman => { - let pacman = Pacman::new(); - let updates = pacman.get_updates_list().await?; - pacman_count = pacman.get_update_count(&updates).await?; - let warning = warning_updates_regex - .as_ref() - .is_some_and(|regex| pacman.has_matching_update(&updates, regex)); - let critical = critical_updates_regex - .as_ref() - .is_some_and(|regex| pacman.has_matching_update(&updates, regex)); - warning_vec.push(warning); - critical_vec.push(critical); + pacman_count = package_manager.get_update_count(&updates).await?; } PackageManager::Aur => { - let aur = Aur::new(); - let updates = aur.get_updates_list().await?; - aur_count = aur.get_update_count(&updates).await?; - let warning = warning_updates_regex - .as_ref() - .is_some_and(|regex| aur.has_matching_update(&updates, regex)); - let critical = critical_updates_regex - .as_ref() - .is_some_and(|regex| aur.has_matching_update(&updates, regex)); - warning_vec.push(warning); - critical_vec.push(critical); + aur_count = package_manager.get_update_count(&updates).await?; } } + + let warning = warning_updates_regex + .as_ref() + .is_some_and(|regex| package_manager.has_matching_update(&updates, regex)); + let critical = critical_updates_regex + .as_ref() + .is_some_and(|regex| package_manager.has_matching_update(&updates, regex)); + warning_vec.push(warning); + critical_vec.push(critical); } let mut widget = Widget::new(); @@ -358,6 +339,8 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { #[async_trait] trait Backend { + fn package_manager(&self) -> PackageManager; + async fn setup(&mut self) -> Result<()>; async fn get_updates_list(&self) -> Result; diff --git a/src/blocks/packages/apt.rs b/src/blocks/packages/apt.rs index 4c529320cd..dfc1f4c4c7 100644 --- a/src/blocks/packages/apt.rs +++ b/src/blocks/packages/apt.rs @@ -6,6 +6,7 @@ use tokio::process::Command; use super::*; +#[derive(Default)] pub(super) struct Apt { pub(super) config_file: String, pub(super) ignore_phased_updates: bool, @@ -47,6 +48,10 @@ impl Apt { #[async_trait] impl Backend for Apt { + fn package_manager(&self) -> PackageManager { + PackageManager::Apt + } + async fn setup(&mut self) -> Result<()> { let mut cache_dir = env::temp_dir(); cache_dir.push("i3rs-apt"); diff --git a/src/blocks/packages/pacman.rs b/src/blocks/packages/pacman.rs index d23b9ff230..a314cdd0ee 100644 --- a/src/blocks/packages/pacman.rs +++ b/src/blocks/packages/pacman.rs @@ -57,6 +57,10 @@ impl Aur { #[async_trait] impl Backend for Pacman { + fn package_manager(&self) -> PackageManager { + PackageManager::Pacman + } + async fn setup(&mut self) -> Result<()> { check_fakeroot_command_exists().await?; @@ -128,6 +132,10 @@ impl Backend for Pacman { #[async_trait] impl Backend for Aur { + fn package_manager(&self) -> PackageManager { + PackageManager::Aur + } + async fn setup(&mut self) -> Result<()> { // Nothing to setup here Ok(()) From 762c3e6d8031b68b4d08274192ddbb001eee668d Mon Sep 17 00:00:00 2001 From: IshanGrover2004 Date: Sun, 21 Jan 2024 00:25:55 +0530 Subject: [PATCH 14/32] refactor: Remove `setup` function and add in constructor in backends --- src/blocks/packages.rs | 20 ++++++++++---------- src/blocks/packages/apt.rs | 24 ++++++++++++++---------- src/blocks/packages/pacman.rs | 17 ++++------------- 3 files changed, 28 insertions(+), 33 deletions(-) diff --git a/src/blocks/packages.rs b/src/blocks/packages.rs index e82f412b1a..1106623189 100644 --- a/src/blocks/packages.rs +++ b/src/blocks/packages.rs @@ -253,15 +253,17 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { .transpose() .error("invalid ignore updates regex")?; - let package_manager_vec: Vec> = config - .package_manager - .iter() - .map(|&package_manager| match package_manager { - PackageManager::Apt => Box::new(Apt::new()) as Box, - PackageManager::Pacman => Box::new(Pacman::new()) as Box, + let mut package_manager_vec = Vec::new(); + + for &package_manager in config.package_manager.iter() { + let backend = match package_manager { + PackageManager::Apt => Box::new(Apt::new().await?) as Box, + PackageManager::Pacman => Box::new(Pacman::new().await?) as Box, PackageManager::Aur => Box::new(Aur::new()) as Box, - }) - .collect(); + }; + + package_manager_vec.push(backend); + } loop { let (mut apt_count, mut pacman_count, mut aur_count) = (0, 0, 0); @@ -341,8 +343,6 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { trait Backend { fn package_manager(&self) -> PackageManager; - async fn setup(&mut self) -> Result<()>; - async fn get_updates_list(&self) -> Result; async fn get_update_count(&self, updates: &str) -> Result; diff --git a/src/blocks/packages/apt.rs b/src/blocks/packages/apt.rs index dfc1f4c4c7..30d87de866 100644 --- a/src/blocks/packages/apt.rs +++ b/src/blocks/packages/apt.rs @@ -14,12 +14,16 @@ pub(super) struct Apt { } impl Apt { - pub(super) fn new() -> Self { - Apt { + pub(super) async fn new() -> Result { + let mut apt = Apt { config_file: String::new(), ignore_phased_updates: false, ignore_updates_regex: Default::default(), - } + }; + + apt.setup().await?; + + Ok(apt) } async fn is_phased_update(&self, package_line: &str) -> Result { @@ -44,13 +48,6 @@ impl Apt { None => false, }) } -} - -#[async_trait] -impl Backend for Apt { - fn package_manager(&self) -> PackageManager { - PackageManager::Apt - } async fn setup(&mut self) -> Result<()> { let mut cache_dir = env::temp_dir(); @@ -86,6 +83,13 @@ impl Backend for Apt { Ok(()) } +} + +#[async_trait] +impl Backend for Apt { + fn package_manager(&self) -> PackageManager { + PackageManager::Apt + } async fn get_updates_list(&self) -> Result { Command::new("apt") diff --git a/src/blocks/packages/pacman.rs b/src/blocks/packages/pacman.rs index a314cdd0ee..4ddb82fdd5 100644 --- a/src/blocks/packages/pacman.rs +++ b/src/blocks/packages/pacman.rs @@ -42,8 +42,10 @@ pub(super) struct Aur { } impl Pacman { - pub(super) fn new() -> Self { - Self + pub(super) async fn new() -> Result { + check_fakeroot_command_exists().await?; + + Ok(Self) } } @@ -61,12 +63,6 @@ impl Backend for Pacman { PackageManager::Pacman } - async fn setup(&mut self) -> Result<()> { - check_fakeroot_command_exists().await?; - - Ok(()) - } - async fn get_updates_list(&self) -> Result { // Create the determined `checkup-db` path recursively create_dir_all(&*PACMAN_UPDATES_DB).await.or_error(|| { @@ -136,11 +132,6 @@ impl Backend for Aur { PackageManager::Aur } - async fn setup(&mut self) -> Result<()> { - // Nothing to setup here - Ok(()) - } - async fn get_updates_list(&self) -> Result { let stdout = Command::new("sh") .args(["-c", &self.aur_command]) From 3ff13973a9099fb7a37704c51f7e66459c957a62 Mon Sep 17 00:00:00 2001 From: IshanGrover2004 Date: Sun, 21 Jan 2024 00:34:54 +0530 Subject: [PATCH 15/32] chore: clippy fixes --- src/blocks/packages.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/blocks/packages.rs b/src/blocks/packages.rs index 1106623189..cef1a0ad61 100644 --- a/src/blocks/packages.rs +++ b/src/blocks/packages.rs @@ -276,9 +276,12 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { match package_manager.package_manager() { PackageManager::Apt => { - let mut apt = Apt::default(); - apt.ignore_updates_regex = ignore_updates_regex.clone(); - apt.ignore_phased_updates = config.ignore_phased_updates; + let apt = Apt { + // Config file not needed in counting updates + config_file: String::new(), + ignore_phased_updates: config.ignore_phased_updates, + ignore_updates_regex: ignore_updates_regex.clone(), + }; apt_count = apt.get_update_count(&updates).await?; } PackageManager::Pacman => { From b4f229bb8b26733243d8c5b691b497c736951b12 Mon Sep 17 00:00:00 2001 From: IshanGrover2004 Date: Sun, 21 Jan 2024 00:51:59 +0530 Subject: [PATCH 16/32] feat: Add depreciation patches for apt & pacman blocks --- src/blocks.rs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/blocks.rs b/src/blocks.rs index 836e65f7d2..291d7606f9 100644 --- a/src/blocks.rs +++ b/src/blocks.rs @@ -45,11 +45,16 @@ use crate::{BoxedFuture, Request, RequestCmd}; macro_rules! define_blocks { { - $( $(#[cfg(feature = $feat: literal)])? $block: ident $(,)? )* + $( + $(#[cfg(feature = $feat: literal)])? + $(#[deprecated($($dep_k: ident = $dep_v: literal),+)])? + $block: ident $(,)? + )* } => { $( $(#[cfg(feature = $feat)])? $(#[cfg_attr(docsrs, doc(cfg(feature = $feat)))])? + $(#[deprecated($($dep_k = $dep_v),+)])? pub mod $block; )* @@ -58,6 +63,7 @@ macro_rules! define_blocks { $( $(#[cfg(feature = $feat)])? #[allow(non_camel_case_types)] + #[allow(deprecated)] $block($block::Config), )* Err(&'static str, Error), @@ -78,6 +84,7 @@ macro_rules! define_blocks { match self { $( $(#[cfg(feature = $feat)])? + #[allow(deprecated)] Self::$block(config) => futures.push(async move { while let Err(err) = $block::run(&config, &api).await { if api.set_error(err).is_err() { @@ -114,6 +121,7 @@ macro_rules! define_blocks { match block_name { $( $(#[cfg(feature = $feat)])? + #[allow(deprecated)] stringify!($block) => match $block::Config::deserialize(table) { Ok(config) => Ok(BlockConfig::$block(config)), Err(err) => Ok(BlockConfig::Err(stringify!($block), crate::errors::Error::new(err.to_string()))), @@ -136,6 +144,10 @@ macro_rules! define_blocks { define_blocks!( amd_gpu, + #[deprecated( + since = "0.33.0", + note = "The block has been deprecated in favor of the the packages block" + )] apt, backlight, battery, @@ -163,6 +175,10 @@ define_blocks!( notmuch, nvidia_gpu, packages, + #[deprecated( + since = "0.33.0", + note = "The block has been deprecated in favor of the the packages block" + )] pacman, pomodoro, rofication, From ad035f0b764ff59b2b81c027388ae8e0ee300caf Mon Sep 17 00:00:00 2001 From: IshanGrover2004 Date: Sun, 21 Jan 2024 11:47:18 +0530 Subject: [PATCH 17/32] refactor: Remove vec of critical & warning updates regex and Add optimized solution --- src/blocks/packages.rs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/blocks/packages.rs b/src/blocks/packages.rs index cef1a0ad61..86223378b2 100644 --- a/src/blocks/packages.rs +++ b/src/blocks/packages.rs @@ -267,8 +267,8 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { loop { let (mut apt_count, mut pacman_count, mut aur_count) = (0, 0, 0); - let mut critical_vec = vec![false]; - let mut warning_vec = vec![false]; + let mut critical = false; + let mut warning = false; // Iterate over the all package manager listed in Config for package_manager in &package_manager_vec { @@ -292,14 +292,12 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { } } - let warning = warning_updates_regex + warning |= warning_updates_regex .as_ref() .is_some_and(|regex| package_manager.has_matching_update(&updates, regex)); - let critical = critical_updates_regex + critical |= critical_updates_regex .as_ref() .is_some_and(|regex| package_manager.has_matching_update(&updates, regex)); - warning_vec.push(warning); - critical_vec.push(critical); } let mut widget = Widget::new(); @@ -318,9 +316,6 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { "total" => Value::number(total_count), )); - let warning = warning_vec.iter().any(|&x| x); - let critical = critical_vec.iter().any(|&x| x); - widget.state = match total_count { 0 => State::Idle, _ => { From 7d61b37a22f34cdbf86ab506311620278f3ab605 Mon Sep 17 00:00:00 2001 From: IshanGrover2004 Date: Sun, 21 Jan 2024 16:32:36 +0530 Subject: [PATCH 18/32] refactor: Add parameters to Apt::new() --- src/blocks/packages.rs | 12 ++++-------- src/blocks/packages/apt.rs | 9 ++++++--- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/blocks/packages.rs b/src/blocks/packages.rs index 86223378b2..adfca03576 100644 --- a/src/blocks/packages.rs +++ b/src/blocks/packages.rs @@ -257,7 +257,9 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { for &package_manager in config.package_manager.iter() { let backend = match package_manager { - PackageManager::Apt => Box::new(Apt::new().await?) as Box, + PackageManager::Apt => Box::new( + Apt::new(config.ignore_phased_updates, ignore_updates_regex.clone()).await?, + ) as Box, PackageManager::Pacman => Box::new(Pacman::new().await?) as Box, PackageManager::Aur => Box::new(Aur::new()) as Box, }; @@ -276,13 +278,7 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { match package_manager.package_manager() { PackageManager::Apt => { - let apt = Apt { - // Config file not needed in counting updates - config_file: String::new(), - ignore_phased_updates: config.ignore_phased_updates, - ignore_updates_regex: ignore_updates_regex.clone(), - }; - apt_count = apt.get_update_count(&updates).await?; + apt_count = package_manager.get_update_count(&updates).await?; } PackageManager::Pacman => { pacman_count = package_manager.get_update_count(&updates).await?; diff --git a/src/blocks/packages/apt.rs b/src/blocks/packages/apt.rs index 30d87de866..adfbca8597 100644 --- a/src/blocks/packages/apt.rs +++ b/src/blocks/packages/apt.rs @@ -14,11 +14,14 @@ pub(super) struct Apt { } impl Apt { - pub(super) async fn new() -> Result { + pub(super) async fn new( + ignore_phased_updates: bool, + ignore_updates_regex: Option, + ) -> Result { let mut apt = Apt { config_file: String::new(), - ignore_phased_updates: false, - ignore_updates_regex: Default::default(), + ignore_phased_updates, + ignore_updates_regex, }; apt.setup().await?; From 12fc73679f4addc5f9fb4787d9ddc76d67f72cea Mon Sep 17 00:00:00 2001 From: IshanGrover2004 Date: Sun, 21 Jan 2024 16:50:10 +0530 Subject: [PATCH 19/32] refactor: Add HashMap for getting updates in generic way --- src/blocks/packages.rs | 25 ++++++++----------------- src/blocks/packages/apt.rs | 4 ++-- src/blocks/packages/pacman.rs | 8 ++++---- 3 files changed, 14 insertions(+), 23 deletions(-) diff --git a/src/blocks/packages.rs b/src/blocks/packages.rs index adfca03576..f13da5c1e2 100644 --- a/src/blocks/packages.rs +++ b/src/blocks/packages.rs @@ -268,25 +268,16 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { } loop { - let (mut apt_count, mut pacman_count, mut aur_count) = (0, 0, 0); + let mut package_manager_map: HashMap<&str, u32> = HashMap::new(); let mut critical = false; let mut warning = false; // Iterate over the all package manager listed in Config for package_manager in &package_manager_vec { let updates = package_manager.get_updates_list().await?; + let updates_count = package_manager.get_update_count(&updates).await?; - match package_manager.package_manager() { - PackageManager::Apt => { - apt_count = package_manager.get_update_count(&updates).await?; - } - PackageManager::Pacman => { - pacman_count = package_manager.get_update_count(&updates).await?; - } - PackageManager::Aur => { - aur_count = package_manager.get_update_count(&updates).await?; - } - } + package_manager_map.insert(package_manager.name(), updates_count as u32); warning |= warning_updates_regex .as_ref() @@ -298,7 +289,7 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { let mut widget = Widget::new(); - let total_count = apt_count + pacman_count + aur_count; + let total_count = package_manager_map.values().sum(); widget.set_format(match total_count { 0 => format_up_to_date.clone(), 1 => format_singular.clone(), @@ -306,9 +297,9 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { }); widget.set_values(map!( "icon" => Value::icon("update"), - "apt" => Value::number(apt_count), - "pacman" => Value::number(pacman_count), - "aur" => Value::number(aur_count), + "apt" => Value::number(package_manager_map["apt"]), + "pacman" => Value::number(package_manager_map["pacman"]), + "aur" => Value::number(package_manager_map["aur"]), "total" => Value::number(total_count), )); @@ -335,7 +326,7 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { #[async_trait] trait Backend { - fn package_manager(&self) -> PackageManager; + fn name(&self) -> &str; async fn get_updates_list(&self) -> Result; diff --git a/src/blocks/packages/apt.rs b/src/blocks/packages/apt.rs index adfbca8597..7372b8bf21 100644 --- a/src/blocks/packages/apt.rs +++ b/src/blocks/packages/apt.rs @@ -90,8 +90,8 @@ impl Apt { #[async_trait] impl Backend for Apt { - fn package_manager(&self) -> PackageManager { - PackageManager::Apt + fn name(&self) -> &str { + "apt" } async fn get_updates_list(&self) -> Result { diff --git a/src/blocks/packages/pacman.rs b/src/blocks/packages/pacman.rs index 4ddb82fdd5..1baa12bc6e 100644 --- a/src/blocks/packages/pacman.rs +++ b/src/blocks/packages/pacman.rs @@ -59,8 +59,8 @@ impl Aur { #[async_trait] impl Backend for Pacman { - fn package_manager(&self) -> PackageManager { - PackageManager::Pacman + fn name(&self) -> &str { + "pacman" } async fn get_updates_list(&self) -> Result { @@ -128,8 +128,8 @@ impl Backend for Pacman { #[async_trait] impl Backend for Aur { - fn package_manager(&self) -> PackageManager { - PackageManager::Aur + fn name(&self) -> &str { + "aur" } async fn get_updates_list(&self) -> Result { From b76fed9749c3bde84e4e87d39e1de8720f59a227 Mon Sep 17 00:00:00 2001 From: IshanGrover2004 Date: Sun, 21 Jan 2024 18:32:11 +0530 Subject: [PATCH 20/32] feat: Re-use the code in `blocks/apt.rs` & `blocks/pacman.rs` --- src/blocks/apt.rs | 121 ++---------------------- src/blocks/packages.rs | 17 ++-- src/blocks/packages/apt.rs | 4 +- src/blocks/packages/pacman.rs | 22 +++-- src/blocks/pacman.rs | 167 +++++----------------------------- 5 files changed, 57 insertions(+), 274 deletions(-) diff --git a/src/blocks/apt.rs b/src/blocks/apt.rs index 53cc460fc3..cfa455c630 100644 --- a/src/blocks/apt.rs +++ b/src/blocks/apt.rs @@ -48,15 +48,11 @@ //! //! - `update` -use std::env; -use std::process::Stdio; - use regex::Regex; -use tokio::fs::{create_dir_all, File}; -use tokio::process::Command; +use crate::blocks::packages::Backend; -use super::prelude::*; +use super::{packages::apt::Apt, prelude::*}; #[derive(Deserialize, Debug, SmartDefault)] #[serde(deny_unknown_fields, default)] @@ -100,45 +96,12 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { .transpose() .error("invalid ignore updates regex")?; - let mut cache_dir = env::temp_dir(); - cache_dir.push("i3rs-apt"); - if !cache_dir.exists() { - create_dir_all(&cache_dir) - .await - .error("Failed to create temp dir")?; - } - - let apt_config = format!( - "Dir::State \"{}\";\n - Dir::State::lists \"lists\";\n - Dir::Cache \"{}\";\n - Dir::Cache::srcpkgcache \"srcpkgcache.bin\";\n - Dir::Cache::pkgcache \"pkgcache.bin\";", - cache_dir.display(), - cache_dir.display(), - ); - - let mut config_file = cache_dir; - config_file.push("apt.conf"); - let config_file = config_file.to_str().unwrap(); - - let mut file = File::create(&config_file) - .await - .error("Failed to create config file")?; - file.write_all(apt_config.as_bytes()) - .await - .error("Failed to write to config file")?; + let backend = Apt::new(config.ignore_phased_updates, ignore_updates_regex.clone()).await?; loop { let mut widget = Widget::new(); - let updates = get_updates_list(config_file).await?; - let count = get_update_count( - config_file, - config.ignore_phased_updates, - ignore_updates_regex.as_ref(), - &updates, - ) - .await?; + let updates = backend.get_updates_list().await?; + let count = backend.get_update_count(&updates).await?; widget.set_format(match count { 0 => format_up_to_date.clone(), @@ -152,10 +115,10 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { let warning = warning_updates_regex .as_ref() - .is_some_and(|regex| has_matching_update(&updates, regex)); + .is_some_and(|regex| backend.has_matching_update(&updates, regex)); let critical = critical_updates_regex .as_ref() - .is_some_and(|regex| has_matching_update(&updates, regex)); + .is_some_and(|regex| backend.has_matching_update(&updates, regex)); widget.state = match count { 0 => State::Idle, _ => { @@ -177,73 +140,3 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { } } } - -async fn get_updates_list(config_path: &str) -> Result { - Command::new("apt") - .env("APT_CONFIG", config_path) - .args(["update"]) - .stdout(Stdio::null()) - .stdin(Stdio::null()) - .spawn() - .error("Failed to run `apt update`")? - .wait() - .await - .error("Failed to run `apt update`")?; - let stdout = Command::new("apt") - .env("LANG", "C") - .env("APT_CONFIG", config_path) - .args(["list", "--upgradable"]) - .output() - .await - .error("Problem running apt command")? - .stdout; - String::from_utf8(stdout).error("apt produced non-UTF8 output") -} - -async fn get_update_count( - config_path: &str, - ignore_phased_updates: bool, - ignore_updates_regex: Option<&Regex>, - updates: &str, -) -> Result { - let mut cnt = 0; - - for update_line in updates - .lines() - .filter(|line| line.contains("[upgradable")) - .filter(|line| ignore_updates_regex.map_or(true, |re| !re.is_match(line))) - { - if !ignore_phased_updates || !is_phased_update(config_path, update_line).await? { - cnt += 1; - } - } - - Ok(cnt) -} - -fn has_matching_update(updates: &str, regex: &Regex) -> bool { - updates.lines().any(|line| regex.is_match(line)) -} - -async fn is_phased_update(config_path: &str, package_line: &str) -> Result { - let package_name_regex = regex!(r#"(.*)/.*"#); - let package_name = &package_name_regex - .captures(package_line) - .error("Couldn't find package name")?[1]; - - let output = String::from_utf8( - Command::new("apt-cache") - .args(["-c", config_path, "policy", package_name]) - .output() - .await - .error("Problem running apt-cache command")? - .stdout, - ) - .error("Problem capturing apt-cache command output")?; - - let phased_regex = regex!(r".*\(phased (\d+)%\).*"); - Ok(match phased_regex.captures(&output) { - Some(matches) => &matches[1] != "100", - None => false, - }) -} diff --git a/src/blocks/packages.rs b/src/blocks/packages.rs index f13da5c1e2..31db524e11 100644 --- a/src/blocks/packages.rs +++ b/src/blocks/packages.rs @@ -11,9 +11,9 @@ //! ----|--------|-------- //! `interval` | Update interval in seconds. | `600` //! `package_manager` | Package manager to check for updates | - -//! `format` | A string to customise the output of this block. See below for available placeholders. | `" $icon $count.eng(w:1) "` -//! `format_singular` | Same as `format`, but for when exactly one update is available. | `" $icon $count.eng(w:1) "` -//! `format_up_to_date` | Same as `format`, but for when no updates are available. | `" $icon $count.eng(w:1) "` +//! `format` | A string to customise the output of this block. See below for available placeholders. | `" $icon $total.eng(w:1) "` +//! `format_singular` | Same as `format`, but for when exactly one update is available. | `" $icon $total.eng(w:1) "` +//! `format_up_to_date` | Same as `format`, but for when no updates are available. | `" $icon $total.eng(w:1) "` //! `warning_updates_regex` | Display block as warning if updates matching regex are available. | `None` //! `critical_updates_regex` | Display block as critical if updates matching regex are available. | `None` //! `ignore_updates_regex` | Doesn't include updates matching regex in the count. | `None` @@ -260,8 +260,13 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { PackageManager::Apt => Box::new( Apt::new(config.ignore_phased_updates, ignore_updates_regex.clone()).await?, ) as Box, - PackageManager::Pacman => Box::new(Pacman::new().await?) as Box, - PackageManager::Aur => Box::new(Aur::new()) as Box, + PackageManager::Pacman => { + Box::new(Pacman::new(ignore_updates_regex.clone()).await?) as Box + } + PackageManager::Aur => Box::new(Aur::new( + config.aur_command.clone().unwrap_or_default(), + ignore_updates_regex.clone(), + )) as Box, }; package_manager_vec.push(backend); @@ -325,7 +330,7 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { } #[async_trait] -trait Backend { +pub trait Backend { fn name(&self) -> &str; async fn get_updates_list(&self) -> Result; diff --git a/src/blocks/packages/apt.rs b/src/blocks/packages/apt.rs index 7372b8bf21..740ce868bf 100644 --- a/src/blocks/packages/apt.rs +++ b/src/blocks/packages/apt.rs @@ -7,14 +7,14 @@ use tokio::process::Command; use super::*; #[derive(Default)] -pub(super) struct Apt { +pub struct Apt { pub(super) config_file: String, pub(super) ignore_phased_updates: bool, pub(super) ignore_updates_regex: Option, } impl Apt { - pub(super) async fn new( + pub async fn new( ignore_phased_updates: bool, ignore_updates_regex: Option, ) -> Result { diff --git a/src/blocks/packages/pacman.rs b/src/blocks/packages/pacman.rs index 1baa12bc6e..e6633331f6 100644 --- a/src/blocks/packages/pacman.rs +++ b/src/blocks/packages/pacman.rs @@ -10,7 +10,7 @@ use crate::util::has_command; make_log_macro!(debug, "pacman"); -static PACMAN_UPDATES_DB: Lazy = Lazy::new(|| { +pub static PACMAN_UPDATES_DB: Lazy = Lazy::new(|| { let path = match env::var_os("CHECKUPDATES_DB") { Some(val) => val.into(), None => { @@ -27,7 +27,7 @@ static PACMAN_UPDATES_DB: Lazy = Lazy::new(|| { path }); -static PACMAN_DB: Lazy = Lazy::new(|| { +pub static PACMAN_DB: Lazy = Lazy::new(|| { let path = env::var_os("DBPath") .map(Into::into) .unwrap_or_else(|| PathBuf::from("/var/lib/pacman/")); @@ -35,24 +35,30 @@ static PACMAN_DB: Lazy = Lazy::new(|| { path }); -pub(super) struct Pacman; +pub struct Pacman { + _ignore_updates_regex: Option, +} -pub(super) struct Aur { +pub struct Aur { aur_command: String, + _ignore_updates_regex: Option, } impl Pacman { - pub(super) async fn new() -> Result { + pub async fn new(ignore_updates_regex: Option) -> Result { check_fakeroot_command_exists().await?; - Ok(Self) + Ok(Self { + _ignore_updates_regex: ignore_updates_regex, + }) } } impl Aur { - pub(super) fn new() -> Self { + pub fn new(aur_command: String, ignore_updates_regex: Option) -> Self { Aur { - aur_command: String::new(), + aur_command, + _ignore_updates_regex: ignore_updates_regex, } } } diff --git a/src/blocks/pacman.rs b/src/blocks/pacman.rs index 0e6c74a1f9..9f6ecd2559 100644 --- a/src/blocks/pacman.rs +++ b/src/blocks/pacman.rs @@ -111,44 +111,11 @@ //! //! - `update` -use std::env; -use std::path::PathBuf; -use std::process::Stdio; - use regex::Regex; -use tokio::fs::{create_dir_all, symlink}; -use tokio::process::Command; - +use super::packages::pacman::{Aur, Pacman}; use super::prelude::*; -use crate::util::has_command; - -make_log_macro!(debug, "pacman"); - -static PACMAN_UPDATES_DB: Lazy = Lazy::new(|| { - let path = match env::var_os("CHECKUPDATES_DB") { - Some(val) => val.into(), - None => { - let mut path = env::temp_dir(); - let user = env::var("USER"); - path.push(format!( - "checkup-db-i3statusrs-{}", - user.as_deref().unwrap_or("no-user") - )); - path - } - }; - debug!("Using {} as updates DB path", path.display()); - path -}); - -static PACMAN_DB: Lazy = Lazy::new(|| { - let path = env::var_os("DBPath") - .map(Into::into) - .unwrap_or_else(|| PathBuf::from("/var/lib/pacman/")); - debug!("Using {} as pacman DB path", path.display()); - path -}); +use crate::blocks::packages::Backend; #[derive(Deserialize, Debug, SmartDefault)] #[serde(deny_unknown_fields, default)] @@ -202,10 +169,6 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { Watched::None }; - if matches!(watched, Watched::Pacman | Watched::Both(_)) { - check_fakeroot_command_exists().await?; - } - let warning_updates_regex = config .warning_updates_regex .as_deref() @@ -219,53 +182,54 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { .transpose() .error("invalid critical updates regex")?; + let pacman_backend = Pacman::new(None).await?; + let aur_backend = Aur::new(config.aur_command.clone().unwrap_or_default(), None); + loop { let (mut values, warning, critical, total) = match &watched { Watched::Pacman => { - let updates = get_pacman_available_updates().await?; - let count = get_update_count(&updates); + let updates = pacman_backend.get_updates_list().await?; + let count = pacman_backend.get_update_count(&updates).await?; let values = map!("pacman" => Value::number(count)); let warning = warning_updates_regex .as_ref() - .is_some_and(|regex| has_matching_update(&updates, regex)); + .is_some_and(|regex| pacman_backend.has_matching_update(&updates, regex)); let critical = critical_updates_regex .as_ref() - .is_some_and(|regex| has_matching_update(&updates, regex)); + .is_some_and(|regex| pacman_backend.has_matching_update(&updates, regex)); (values, warning, critical, count) } - Watched::Aur(aur_command) => { - let updates = get_aur_available_updates(aur_command).await?; - let count = get_update_count(&updates); + Watched::Aur(_) => { + let updates = aur_backend.get_updates_list().await?; + let count = aur_backend.get_update_count(&updates).await?; let values = map!( "aur" => Value::number(count) ); let warning = warning_updates_regex .as_ref() - .is_some_and(|regex| has_matching_update(&updates, regex)); + .is_some_and(|regex| aur_backend.has_matching_update(&updates, regex)); let critical = critical_updates_regex .as_ref() - .is_some_and(|regex| has_matching_update(&updates, regex)); + .is_some_and(|regex| aur_backend.has_matching_update(&updates, regex)); (values, warning, critical, count) } - Watched::Both(aur_command) => { - let (pacman_updates, aur_updates) = tokio::try_join!( - get_pacman_available_updates(), - get_aur_available_updates(aur_command) - )?; - let pacman_count = get_update_count(&pacman_updates); - let aur_count = get_update_count(&aur_updates); + Watched::Both(_) => { + let pacman_updates = pacman_backend.get_updates_list().await?; + let aur_updates = aur_backend.get_updates_list().await?; + let pacman_count = pacman_backend.get_update_count(&pacman_updates).await?; + let aur_count = aur_backend.get_update_count(&aur_updates).await?; let values = map! { "pacman" => Value::number(pacman_count), "aur" => Value::number(aur_count), "both" => Value::number(pacman_count + aur_count), }; let warning = warning_updates_regex.as_ref().is_some_and(|regex| { - has_matching_update(&aur_updates, regex) - || has_matching_update(&pacman_updates, regex) + pacman_backend.has_matching_update(&aur_updates, regex) + || aur_backend.has_matching_update(&pacman_updates, regex) }); let critical = critical_updates_regex.as_ref().is_some_and(|regex| { - has_matching_update(&aur_updates, regex) - || has_matching_update(&pacman_updates, regex) + pacman_backend.has_matching_update(&aur_updates, regex) + || aur_backend.has_matching_update(&pacman_updates, regex) }); (values, warning, critical, pacman_count + aur_count) } @@ -308,88 +272,3 @@ enum Watched<'a> { Aur(&'a str), Both(&'a str), } - -async fn check_fakeroot_command_exists() -> Result<()> { - if !has_command("fakeroot").await? { - Err(Error::new("fakeroot not found")) - } else { - Ok(()) - } -} - -async fn get_pacman_available_updates() -> Result { - // Create the determined `checkup-db` path recursively - create_dir_all(&*PACMAN_UPDATES_DB).await.or_error(|| { - format!( - "Failed to create checkup-db directory at '{}'", - PACMAN_UPDATES_DB.display() - ) - })?; - - // Create symlink to local cache in `checkup-db` if required - let local_cache = PACMAN_UPDATES_DB.join("local"); - if !local_cache.exists() { - symlink(PACMAN_DB.join("local"), local_cache) - .await - .error("Failed to created required symlink")?; - } - - // Update database - let status = Command::new("fakeroot") - .env("LC_ALL", "C") - .args([ - "--".as_ref(), - "pacman".as_ref(), - "-Sy".as_ref(), - "--dbpath".as_ref(), - PACMAN_UPDATES_DB.as_os_str(), - "--logfile".as_ref(), - "/dev/null".as_ref(), - ]) - .stdout(Stdio::null()) - .status() - .await - .error("Failed to run command")?; - if !status.success() { - debug!("{}", status); - return Err(Error::new("pacman -Sy exited with non zero exit status")); - } - - let stdout = Command::new("fakeroot") - .env("LC_ALL", "C") - .args([ - "--".as_ref(), - "pacman".as_ref(), - "-Qu".as_ref(), - "--dbpath".as_ref(), - PACMAN_UPDATES_DB.as_os_str(), - ]) - .output() - .await - .error("There was a problem running the pacman commands")? - .stdout; - - String::from_utf8(stdout).error("Pacman produced non-UTF8 output") -} - -async fn get_aur_available_updates(aur_command: &str) -> Result { - let stdout = Command::new("sh") - .args(["-c", aur_command]) - .output() - .await - .or_error(|| format!("aur command: {aur_command} failed"))? - .stdout; - String::from_utf8(stdout) - .error("There was a problem while converting the aur command output to a string") -} - -fn get_update_count(updates: &str) -> usize { - updates - .lines() - .filter(|line| !line.contains("[ignored]")) - .count() -} - -fn has_matching_update(updates: &str, regex: &Regex) -> bool { - updates.lines().any(|line| regex.is_match(line)) -} From 2d6c7026a294dfbb05ef3bb50c187a0d8c5471b7 Mon Sep 17 00:00:00 2001 From: IshanGrover2004 Date: Sun, 21 Jan 2024 21:13:01 +0530 Subject: [PATCH 21/32] feat: Add `dnf` to packages block and re-use code in `dnf.rs` --- src/blocks/apt.rs | 7 ++++--- src/blocks/dnf.rs | 36 +++++++++++------------------------- src/blocks/packages.rs | 37 ++++++++++++++++++++++++++++++++++--- src/blocks/packages/dnf.rs | 33 +++++++++++++++++++++++++++++++++ src/blocks/pacman.rs | 10 +++++++--- 5 files changed, 89 insertions(+), 34 deletions(-) create mode 100644 src/blocks/packages/dnf.rs diff --git a/src/blocks/apt.rs b/src/blocks/apt.rs index cfa455c630..875f76356e 100644 --- a/src/blocks/apt.rs +++ b/src/blocks/apt.rs @@ -50,9 +50,10 @@ use regex::Regex; -use crate::blocks::packages::Backend; - -use super::{packages::apt::Apt, prelude::*}; +use super::{ + packages::{apt::Apt, Backend}, + prelude::*, +}; #[derive(Deserialize, Debug, SmartDefault)] #[serde(deny_unknown_fields, default)] diff --git a/src/blocks/dnf.rs b/src/blocks/dnf.rs index e967bbe81e..9a1851ff41 100644 --- a/src/blocks/dnf.rs +++ b/src/blocks/dnf.rs @@ -39,9 +39,12 @@ //! //! - `update` -use super::prelude::*; use regex::Regex; -use tokio::process::Command; + +use super::{ + packages::{dnf::Dnf, Backend}, + prelude::*, +}; #[derive(Deserialize, Debug, SmartDefault)] #[serde(deny_unknown_fields, default)] @@ -77,11 +80,13 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { .transpose() .error("invalid critical updates regex")?; + let backend = Dnf::new(); + loop { let mut widget = Widget::new(); - let updates = get_updates_list().await?; - let count = get_update_count(&updates); + let updates = backend.get_updates_list().await?; + let count = backend.get_update_count(&updates).await?; widget.set_format(match count { 0 => format_up_to_date.clone(), @@ -95,10 +100,10 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { let warning = warning_updates_regex .as_ref() - .is_some_and(|regex| has_matching_update(&updates, regex)); + .is_some_and(|regex| backend.has_matching_update(&updates, regex)); let critical = critical_updates_regex .as_ref() - .is_some_and(|regex| has_matching_update(&updates, regex)); + .is_some_and(|regex| backend.has_matching_update(&updates, regex)); widget.state = match count { 0 => State::Idle, _ => { @@ -120,22 +125,3 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { } } } - -async fn get_updates_list() -> Result { - let stdout = Command::new("sh") - .env("LC_LANG", "C") - .args(["-c", "dnf check-update -q --skip-broken"]) - .output() - .await - .error("Failed to run dnf check-update")? - .stdout; - String::from_utf8(stdout).error("dnf produced non-UTF8 output") -} - -fn get_update_count(updates: &str) -> usize { - updates.lines().filter(|line| line.len() > 1).count() -} - -fn has_matching_update(updates: &str, regex: &Regex) -> bool { - updates.lines().any(|line| regex.is_match(line)) -} diff --git a/src/blocks/packages.rs b/src/blocks/packages.rs index 31db524e11..551e5e951d 100644 --- a/src/blocks/packages.rs +++ b/src/blocks/packages.rs @@ -4,6 +4,7 @@ //! - `apt` for Debian/Ubuntu based system //! - `pacman` for Arch based system //! - `aur` for Arch based system +//! - `dnf` for Fedora based system //! //! # Configuration //! @@ -26,6 +27,7 @@ //! `apt` | Number of updates available in Debian/Ubuntu based system | Number | - //! `pacman` | Number of updates available in Arch based system | Number | - //! `aur` | Number of updates available in Arch based system | Number | - +//! `dnf` | Number of updates available in Fedora based system | Number | - //! `total` | Number of updates available in all package manager listed | Number | - //! //! # Apt @@ -148,6 +150,24 @@ //! aur_command = "yay -Qua" //! ``` //! +//! +//! Dnf only config: +//! +//! ```toml +//! [[block]] +//! block = "packages" +//! package_manager = ["dnf"] +//! interval = 1800 +//! format = " $icon $dnf.eng(w:1) updates available " +//! format_singular = " $icon One update available " +//! format_up_to_date = " $icon system up to date " +//! critical_updates_regex = "(linux|linux-lts|linux-zen)" +//! [[block.click]] +//! # shows dmenu with cached available updates. Any dmenu alternative should also work. +//! button = "left" +//! cmd = "dnf list -q --upgrades | tail -n +2 | rofi -dmenu" +//! ``` +//! //! Multiple package managers config: //! //! Update the list of pending updates every thirty minutes (1800 seconds): @@ -155,9 +175,9 @@ //! ```toml //! [[block]] //! block = "packages" -//! package_manager = ["apt", "pacman", "aur"] +//! package_manager = ["apt", "pacman", "aur","dnf"] //! interval = 1800 -//! format = " $icon $apt + $pacman + $aur = $total updates available " +//! format = " $icon $apt + $pacman + $aur + $dnf = $total updates available " //! format_singular = " $icon One update available " //! format_up_to_date = " $icon system up to date " //! ``` @@ -172,6 +192,9 @@ use apt::Apt; pub mod pacman; use pacman::{Aur, Pacman}; +pub mod dnf; +use dnf::Dnf; + use regex::Regex; use super::prelude::*; @@ -198,6 +221,7 @@ pub enum PackageManager { Apt, Pacman, Aur, + Dnf, } pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { @@ -233,6 +257,9 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { if !config.package_manager.contains(&PackageManager::Aur) && aur { config.package_manager.push(PackageManager::Aur); } + if !config.package_manager.contains(&PackageManager::Dnf) && aur { + config.package_manager.push(PackageManager::Dnf); + } let warning_updates_regex = config .warning_updates_regex @@ -267,13 +294,16 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { config.aur_command.clone().unwrap_or_default(), ignore_updates_regex.clone(), )) as Box, + PackageManager::Dnf => Box::new(Dnf::new()) as Box, }; package_manager_vec.push(backend); } loop { - let mut package_manager_map: HashMap<&str, u32> = HashMap::new(); + let mut package_manager_map: HashMap<&str, u32> = + [("apt", 0), ("pacman", 0), ("aur", 0), ("dnf", 0)].into(); + let mut critical = false; let mut warning = false; @@ -305,6 +335,7 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { "apt" => Value::number(package_manager_map["apt"]), "pacman" => Value::number(package_manager_map["pacman"]), "aur" => Value::number(package_manager_map["aur"]), + "dnf" => Value::number(package_manager_map["dnf"]), "total" => Value::number(total_count), )); diff --git a/src/blocks/packages/dnf.rs b/src/blocks/packages/dnf.rs new file mode 100644 index 0000000000..b59e862bb7 --- /dev/null +++ b/src/blocks/packages/dnf.rs @@ -0,0 +1,33 @@ +use tokio::process::Command; + +use super::super::packages::*; + +pub struct Dnf; + +impl Dnf { + pub fn new() -> Self { + Self + } +} + +#[async_trait] +impl Backend for Dnf { + fn name(&self) -> &str { + "dnf" + } + + async fn get_updates_list(&self) -> Result { + let stdout = Command::new("sh") + .env("LC_LANG", "C") + .args(["-c", "dnf check-update -q --skip-broken"]) + .output() + .await + .error("Failed to run dnf check-update")? + .stdout; + String::from_utf8(stdout).error("dnf produced non-UTF8 output") + } + + async fn get_update_count(&self, updates: &str) -> Result { + Ok(updates.lines().filter(|line| line.len() > 1).count()) + } +} diff --git a/src/blocks/pacman.rs b/src/blocks/pacman.rs index 9f6ecd2559..d678c3e96b 100644 --- a/src/blocks/pacman.rs +++ b/src/blocks/pacman.rs @@ -113,9 +113,13 @@ use regex::Regex; -use super::packages::pacman::{Aur, Pacman}; -use super::prelude::*; -use crate::blocks::packages::Backend; +use super::{ + packages::{ + pacman::{Aur, Pacman}, + Backend, + }, + prelude::*, +}; #[derive(Deserialize, Debug, SmartDefault)] #[serde(deny_unknown_fields, default)] From 94b6bea5cf3de7ea16e19837f586031f3e8a2df7 Mon Sep 17 00:00:00 2001 From: IshanGrover2004 Date: Sun, 21 Jan 2024 21:16:26 +0530 Subject: [PATCH 22/32] chore: Clippy fixes --- src/blocks/packages/dnf.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/blocks/packages/dnf.rs b/src/blocks/packages/dnf.rs index b59e862bb7..e4bd9a7137 100644 --- a/src/blocks/packages/dnf.rs +++ b/src/blocks/packages/dnf.rs @@ -2,11 +2,12 @@ use tokio::process::Command; use super::super::packages::*; +#[derive(Default)] pub struct Dnf; impl Dnf { pub fn new() -> Self { - Self + Default::default() } } From 1da106f864398efb02e829f6ad47ed9bac5e084c Mon Sep 17 00:00:00 2001 From: IshanGrover2004 Date: Sun, 21 Jan 2024 22:46:21 +0530 Subject: [PATCH 23/32] refactor: Update `get_update_list` to return `Vec` and remove count function --- src/blocks/apt.rs | 9 ++++--- src/blocks/dnf.rs | 2 +- src/blocks/packages.rs | 32 ++++++++++++------------ src/blocks/packages/apt.rs | 41 ++++++++++++++----------------- src/blocks/packages/dnf.rs | 13 ++++++---- src/blocks/packages/pacman.rs | 46 +++++++++++++++-------------------- src/blocks/pacman.rs | 12 ++++----- 7 files changed, 75 insertions(+), 80 deletions(-) diff --git a/src/blocks/apt.rs b/src/blocks/apt.rs index 875f76356e..e005a88f46 100644 --- a/src/blocks/apt.rs +++ b/src/blocks/apt.rs @@ -97,12 +97,15 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { .transpose() .error("invalid ignore updates regex")?; - let backend = Apt::new(config.ignore_phased_updates, ignore_updates_regex.clone()).await?; + let backend = Apt::new(config.ignore_phased_updates).await?; loop { let mut widget = Widget::new(); - let updates = backend.get_updates_list().await?; - let count = backend.get_update_count(&updates).await?; + let mut updates = backend.get_updates_list().await?; + if let Some(regex) = ignore_updates_regex.clone() { + updates.retain(|u| !regex.is_match(u)); + } + let count = updates.len(); widget.set_format(match count { 0 => format_up_to_date.clone(), diff --git a/src/blocks/dnf.rs b/src/blocks/dnf.rs index 9a1851ff41..d5de4246ad 100644 --- a/src/blocks/dnf.rs +++ b/src/blocks/dnf.rs @@ -86,7 +86,7 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { let mut widget = Widget::new(); let updates = backend.get_updates_list().await?; - let count = backend.get_update_count(&updates).await?; + let count = updates.len(); widget.set_format(match count { 0 => format_up_to_date.clone(), diff --git a/src/blocks/packages.rs b/src/blocks/packages.rs index 551e5e951d..917a5f7eb9 100644 --- a/src/blocks/packages.rs +++ b/src/blocks/packages.rs @@ -284,16 +284,14 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { for &package_manager in config.package_manager.iter() { let backend = match package_manager { - PackageManager::Apt => Box::new( - Apt::new(config.ignore_phased_updates, ignore_updates_regex.clone()).await?, - ) as Box, - PackageManager::Pacman => { - Box::new(Pacman::new(ignore_updates_regex.clone()).await?) as Box + PackageManager::Apt => { + Box::new(Apt::new(config.ignore_phased_updates).await?) as Box + } + PackageManager::Pacman => Box::new(Pacman::new().await?) as Box, + PackageManager::Aur => { + Box::new(Aur::new(config.aur_command.clone().unwrap_or_default())) + as Box } - PackageManager::Aur => Box::new(Aur::new( - config.aur_command.clone().unwrap_or_default(), - ignore_updates_regex.clone(), - )) as Box, PackageManager::Dnf => Box::new(Dnf::new()) as Box, }; @@ -309,8 +307,12 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { // Iterate over the all package manager listed in Config for package_manager in &package_manager_vec { - let updates = package_manager.get_updates_list().await?; - let updates_count = package_manager.get_update_count(&updates).await?; + let mut updates = package_manager.get_updates_list().await?; + if let Some(regex) = ignore_updates_regex.clone() { + updates.retain(|u| !regex.is_match(u)); + } + + let updates_count = updates.len(); package_manager_map.insert(package_manager.name(), updates_count as u32); @@ -364,11 +366,9 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { pub trait Backend { fn name(&self) -> &str; - async fn get_updates_list(&self) -> Result; - - async fn get_update_count(&self, updates: &str) -> Result; + async fn get_updates_list(&self) -> Result>; - fn has_matching_update(&self, updates: &str, regex: &Regex) -> bool { - updates.lines().any(|line| regex.is_match(line)) + fn has_matching_update(&self, updates: &[String], regex: &Regex) -> bool { + updates.iter().any(|line| regex.is_match(line)) } } diff --git a/src/blocks/packages/apt.rs b/src/blocks/packages/apt.rs index 740ce868bf..504d57c36e 100644 --- a/src/blocks/packages/apt.rs +++ b/src/blocks/packages/apt.rs @@ -10,18 +10,13 @@ use super::*; pub struct Apt { pub(super) config_file: String, pub(super) ignore_phased_updates: bool, - pub(super) ignore_updates_regex: Option, } impl Apt { - pub async fn new( - ignore_phased_updates: bool, - ignore_updates_regex: Option, - ) -> Result { + pub async fn new(ignore_phased_updates: bool) -> Result { let mut apt = Apt { config_file: String::new(), ignore_phased_updates, - ignore_updates_regex, }; apt.setup().await?; @@ -94,7 +89,7 @@ impl Backend for Apt { "apt" } - async fn get_updates_list(&self) -> Result { + async fn get_updates_list(&self) -> Result> { Command::new("apt") .env("APT_CONFIG", &self.config_file) .args(["update"]) @@ -113,26 +108,26 @@ impl Backend for Apt { .await .error("Problem running apt command")? .stdout; - String::from_utf8(stdout).error("apt produced non-UTF8 output") - } - - async fn get_update_count(&self, updates: &str) -> Result { - let mut cnt = 0; - for update_line in updates + let updates = String::from_utf8(stdout).error("apt produced non-UTF8 output")?; + let updates: Vec = updates .lines() .filter(|line| line.contains("[upgradable")) - .filter(|line| { - self.ignore_updates_regex - .as_ref() - .map_or(true, |re| !re.is_match(line)) + .filter_map(|update_line| { + let is_phased_update = + async { self.is_phased_update(update_line).await.unwrap_or(false) }; + + Some(update_line.to_string()).filter(|_| { + !self.ignore_phased_updates + || !tokio::task::block_in_place(|| { + tokio::runtime::Runtime::new() + .unwrap() + .block_on(is_phased_update) + }) + }) }) - { - if !self.ignore_phased_updates || !self.is_phased_update(update_line).await? { - cnt += 1; - } - } + .collect(); - Ok(cnt) + Ok(updates) } } diff --git a/src/blocks/packages/dnf.rs b/src/blocks/packages/dnf.rs index e4bd9a7137..462450dc84 100644 --- a/src/blocks/packages/dnf.rs +++ b/src/blocks/packages/dnf.rs @@ -17,7 +17,7 @@ impl Backend for Dnf { "dnf" } - async fn get_updates_list(&self) -> Result { + async fn get_updates_list(&self) -> Result> { let stdout = Command::new("sh") .env("LC_LANG", "C") .args(["-c", "dnf check-update -q --skip-broken"]) @@ -25,10 +25,13 @@ impl Backend for Dnf { .await .error("Failed to run dnf check-update")? .stdout; - String::from_utf8(stdout).error("dnf produced non-UTF8 output") - } + let updates = String::from_utf8(stdout).error("dnf produced non-UTF8 output")?; + let updates: Vec = updates + .lines() + .filter(|line| line.len() > 1) + .map(|lines| lines.to_string()) + .collect(); - async fn get_update_count(&self, updates: &str) -> Result { - Ok(updates.lines().filter(|line| line.len() > 1).count()) + Ok(updates) } } diff --git a/src/blocks/packages/pacman.rs b/src/blocks/packages/pacman.rs index e6633331f6..0d838c3ed3 100644 --- a/src/blocks/packages/pacman.rs +++ b/src/blocks/packages/pacman.rs @@ -35,31 +35,23 @@ pub static PACMAN_DB: Lazy = Lazy::new(|| { path }); -pub struct Pacman { - _ignore_updates_regex: Option, -} +pub struct Pacman; pub struct Aur { aur_command: String, - _ignore_updates_regex: Option, } impl Pacman { - pub async fn new(ignore_updates_regex: Option) -> Result { + pub async fn new() -> Result { check_fakeroot_command_exists().await?; - Ok(Self { - _ignore_updates_regex: ignore_updates_regex, - }) + Ok(Self) } } impl Aur { - pub fn new(aur_command: String, ignore_updates_regex: Option) -> Self { - Aur { - aur_command, - _ignore_updates_regex: ignore_updates_regex, - } + pub fn new(aur_command: String) -> Self { + Aur { aur_command } } } @@ -69,7 +61,7 @@ impl Backend for Pacman { "pacman" } - async fn get_updates_list(&self) -> Result { + async fn get_updates_list(&self) -> Result> { // Create the determined `checkup-db` path recursively create_dir_all(&*PACMAN_UPDATES_DB).await.or_error(|| { format!( @@ -121,14 +113,15 @@ impl Backend for Pacman { .error("There was a problem running the pacman commands")? .stdout; - String::from_utf8(stdout).error("Pacman produced non-UTF8 output") - } + let updates = String::from_utf8(stdout).error("Pacman produced non-UTF8 output")?; - async fn get_update_count(&self, updates: &str) -> Result { - Ok(updates + let updates = updates .lines() .filter(|line| !line.contains("[ignored]")) - .count()) + .map(|line| line.to_string()) + .collect(); + + Ok(updates) } } @@ -138,22 +131,23 @@ impl Backend for Aur { "aur" } - async fn get_updates_list(&self) -> Result { + async fn get_updates_list(&self) -> Result> { let stdout = Command::new("sh") .args(["-c", &self.aur_command]) .output() .await .or_error(|| format!("aur command: {} failed", self.aur_command))? .stdout; - String::from_utf8(stdout) - .error("There was a problem while converting the aur command output to a string") - } + let updates = String::from_utf8(stdout) + .error("There was a problem while converting the aur command output to a string")?; - async fn get_update_count(&self, updates: &str) -> Result { - Ok(updates + let updates = updates .lines() .filter(|line| !line.contains("[ignored]")) - .count()) + .map(|line| line.to_string()) + .collect(); + + Ok(updates) } } diff --git a/src/blocks/pacman.rs b/src/blocks/pacman.rs index d678c3e96b..6f501f047d 100644 --- a/src/blocks/pacman.rs +++ b/src/blocks/pacman.rs @@ -186,14 +186,14 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { .transpose() .error("invalid critical updates regex")?; - let pacman_backend = Pacman::new(None).await?; - let aur_backend = Aur::new(config.aur_command.clone().unwrap_or_default(), None); + let pacman_backend = Pacman::new().await?; + let aur_backend = Aur::new(config.aur_command.clone().unwrap_or_default()); loop { let (mut values, warning, critical, total) = match &watched { Watched::Pacman => { let updates = pacman_backend.get_updates_list().await?; - let count = pacman_backend.get_update_count(&updates).await?; + let count = updates.len(); let values = map!("pacman" => Value::number(count)); let warning = warning_updates_regex .as_ref() @@ -205,7 +205,7 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { } Watched::Aur(_) => { let updates = aur_backend.get_updates_list().await?; - let count = aur_backend.get_update_count(&updates).await?; + let count = updates.len(); let values = map!( "aur" => Value::number(count) ); @@ -220,8 +220,8 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { Watched::Both(_) => { let pacman_updates = pacman_backend.get_updates_list().await?; let aur_updates = aur_backend.get_updates_list().await?; - let pacman_count = pacman_backend.get_update_count(&pacman_updates).await?; - let aur_count = aur_backend.get_update_count(&aur_updates).await?; + let pacman_count = pacman_updates.len(); + let aur_count = aur_updates.len(); let values = map! { "pacman" => Value::number(pacman_count), "aur" => Value::number(aur_count), From d48d0f73f49f7dfcded1d53c177eb9631a9b78f5 Mon Sep 17 00:00:00 2001 From: IshanGrover2004 Date: Sun, 21 Jan 2024 23:21:17 +0530 Subject: [PATCH 24/32] refactor: Move `has_matching_updates` function out of Backend trait --- src/blocks/apt.rs | 6 +++--- src/blocks/dnf.rs | 6 +++--- src/blocks/packages.rs | 10 +++++----- src/blocks/pacman.rs | 17 +++++++++-------- 4 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/blocks/apt.rs b/src/blocks/apt.rs index e005a88f46..30c4ba5100 100644 --- a/src/blocks/apt.rs +++ b/src/blocks/apt.rs @@ -51,7 +51,7 @@ use regex::Regex; use super::{ - packages::{apt::Apt, Backend}, + packages::{apt::Apt, has_matching_update, Backend}, prelude::*, }; @@ -119,10 +119,10 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { let warning = warning_updates_regex .as_ref() - .is_some_and(|regex| backend.has_matching_update(&updates, regex)); + .is_some_and(|regex| has_matching_update(&updates, regex)); let critical = critical_updates_regex .as_ref() - .is_some_and(|regex| backend.has_matching_update(&updates, regex)); + .is_some_and(|regex| has_matching_update(&updates, regex)); widget.state = match count { 0 => State::Idle, _ => { diff --git a/src/blocks/dnf.rs b/src/blocks/dnf.rs index d5de4246ad..4aca82a241 100644 --- a/src/blocks/dnf.rs +++ b/src/blocks/dnf.rs @@ -42,7 +42,7 @@ use regex::Regex; use super::{ - packages::{dnf::Dnf, Backend}, + packages::{dnf::Dnf, has_matching_update, Backend}, prelude::*, }; @@ -100,10 +100,10 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { let warning = warning_updates_regex .as_ref() - .is_some_and(|regex| backend.has_matching_update(&updates, regex)); + .is_some_and(|regex| has_matching_update(&updates, regex)); let critical = critical_updates_regex .as_ref() - .is_some_and(|regex| backend.has_matching_update(&updates, regex)); + .is_some_and(|regex| has_matching_update(&updates, regex)); widget.state = match count { 0 => State::Idle, _ => { diff --git a/src/blocks/packages.rs b/src/blocks/packages.rs index 917a5f7eb9..9f6775a3ad 100644 --- a/src/blocks/packages.rs +++ b/src/blocks/packages.rs @@ -318,10 +318,10 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { warning |= warning_updates_regex .as_ref() - .is_some_and(|regex| package_manager.has_matching_update(&updates, regex)); + .is_some_and(|regex| has_matching_update(&updates, regex)); critical |= critical_updates_regex .as_ref() - .is_some_and(|regex| package_manager.has_matching_update(&updates, regex)); + .is_some_and(|regex| has_matching_update(&updates, regex)); } let mut widget = Widget::new(); @@ -367,8 +367,8 @@ pub trait Backend { fn name(&self) -> &str; async fn get_updates_list(&self) -> Result>; +} - fn has_matching_update(&self, updates: &[String], regex: &Regex) -> bool { - updates.iter().any(|line| regex.is_match(line)) - } +pub fn has_matching_update(updates: &[String], regex: &Regex) -> bool { + updates.iter().any(|line| regex.is_match(line)) } diff --git a/src/blocks/pacman.rs b/src/blocks/pacman.rs index 6f501f047d..878610983c 100644 --- a/src/blocks/pacman.rs +++ b/src/blocks/pacman.rs @@ -115,6 +115,7 @@ use regex::Regex; use super::{ packages::{ + has_matching_update, pacman::{Aur, Pacman}, Backend, }, @@ -197,10 +198,10 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { let values = map!("pacman" => Value::number(count)); let warning = warning_updates_regex .as_ref() - .is_some_and(|regex| pacman_backend.has_matching_update(&updates, regex)); + .is_some_and(|regex| has_matching_update(&updates, regex)); let critical = critical_updates_regex .as_ref() - .is_some_and(|regex| pacman_backend.has_matching_update(&updates, regex)); + .is_some_and(|regex| has_matching_update(&updates, regex)); (values, warning, critical, count) } Watched::Aur(_) => { @@ -211,10 +212,10 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { ); let warning = warning_updates_regex .as_ref() - .is_some_and(|regex| aur_backend.has_matching_update(&updates, regex)); + .is_some_and(|regex| has_matching_update(&updates, regex)); let critical = critical_updates_regex .as_ref() - .is_some_and(|regex| aur_backend.has_matching_update(&updates, regex)); + .is_some_and(|regex| has_matching_update(&updates, regex)); (values, warning, critical, count) } Watched::Both(_) => { @@ -228,12 +229,12 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { "both" => Value::number(pacman_count + aur_count), }; let warning = warning_updates_regex.as_ref().is_some_and(|regex| { - pacman_backend.has_matching_update(&aur_updates, regex) - || aur_backend.has_matching_update(&pacman_updates, regex) + has_matching_update(&aur_updates, regex) + || has_matching_update(&pacman_updates, regex) }); let critical = critical_updates_regex.as_ref().is_some_and(|regex| { - pacman_backend.has_matching_update(&aur_updates, regex) - || aur_backend.has_matching_update(&pacman_updates, regex) + has_matching_update(&aur_updates, regex) + || has_matching_update(&pacman_updates, regex) }); (values, warning, critical, pacman_count + aur_count) } From 142e701381958e719e9194535875a32d59d533bf Mon Sep 17 00:00:00 2001 From: IshanGrover2004 Date: Sun, 21 Jan 2024 23:44:02 +0530 Subject: [PATCH 25/32] fix: Remove tokio runtime usage --- src/blocks/packages/apt.rs | 28 +++++++++------------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/src/blocks/packages/apt.rs b/src/blocks/packages/apt.rs index 504d57c36e..fa9ee0ce2a 100644 --- a/src/blocks/packages/apt.rs +++ b/src/blocks/packages/apt.rs @@ -110,24 +110,14 @@ impl Backend for Apt { .stdout; let updates = String::from_utf8(stdout).error("apt produced non-UTF8 output")?; - let updates: Vec = updates - .lines() - .filter(|line| line.contains("[upgradable")) - .filter_map(|update_line| { - let is_phased_update = - async { self.is_phased_update(update_line).await.unwrap_or(false) }; - - Some(update_line.to_string()).filter(|_| { - !self.ignore_phased_updates - || !tokio::task::block_in_place(|| { - tokio::runtime::Runtime::new() - .unwrap() - .block_on(is_phased_update) - }) - }) - }) - .collect(); - - Ok(updates) + let mut updates_list: Vec = Vec::new(); + + for update in updates.lines().filter(|line| line.contains("[upgradable")) { + if !self.ignore_phased_updates || !self.is_phased_update(update).await? { + updates_list.push(update.to_string()); + } + } + + Ok(updates_list) } } From 61902f4ce149bac052cad582585bb1d12e94f017 Mon Sep 17 00:00:00 2001 From: IshanGrover2004 Date: Sun, 21 Jan 2024 23:55:47 +0530 Subject: [PATCH 26/32] refactor: Inline backend match block --- src/blocks/packages.rs | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/blocks/packages.rs b/src/blocks/packages.rs index 9f6775a3ad..7c42fb411e 100644 --- a/src/blocks/packages.rs +++ b/src/blocks/packages.rs @@ -280,22 +280,17 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { .transpose() .error("invalid ignore updates regex")?; - let mut package_manager_vec = Vec::new(); + let mut package_manager_vec: Vec> = Vec::new(); for &package_manager in config.package_manager.iter() { - let backend = match package_manager { - PackageManager::Apt => { - Box::new(Apt::new(config.ignore_phased_updates).await?) as Box - } - PackageManager::Pacman => Box::new(Pacman::new().await?) as Box, + package_manager_vec.push(match package_manager { + PackageManager::Apt => Box::new(Apt::new(config.ignore_phased_updates).await?), + PackageManager::Pacman => Box::new(Pacman::new().await?), PackageManager::Aur => { Box::new(Aur::new(config.aur_command.clone().unwrap_or_default())) - as Box } - PackageManager::Dnf => Box::new(Dnf::new()) as Box, - }; - - package_manager_vec.push(backend); + PackageManager::Dnf => Box::new(Dnf::new()), + }); } loop { From bff8686466705265b512cb5f5d617b588d34c9cc Mon Sep 17 00:00:00 2001 From: IshanGrover2004 Date: Mon, 22 Jan 2024 01:04:40 +0530 Subject: [PATCH 27/32] refactor: Use same hashmap while setting the values refactor: Use same hashmap while setting the values --- src/blocks/packages.rs | 22 +++++++++------------- src/blocks/packages/apt.rs | 4 ++-- src/blocks/packages/dnf.rs | 4 ++-- src/blocks/packages/pacman.rs | 8 ++++---- 4 files changed, 17 insertions(+), 21 deletions(-) diff --git a/src/blocks/packages.rs b/src/blocks/packages.rs index 7c42fb411e..8ab8fd71be 100644 --- a/src/blocks/packages.rs +++ b/src/blocks/packages.rs @@ -294,11 +294,11 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { } loop { - let mut package_manager_map: HashMap<&str, u32> = - [("apt", 0), ("pacman", 0), ("aur", 0), ("dnf", 0)].into(); + let mut package_manager_map: HashMap, Value> = HashMap::new(); let mut critical = false; let mut warning = false; + let mut total_count = 0; // Iterate over the all package manager listed in Config for package_manager in &package_manager_vec { @@ -309,7 +309,8 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { let updates_count = updates.len(); - package_manager_map.insert(package_manager.name(), updates_count as u32); + package_manager_map.insert(package_manager.name(), Value::number(updates_count)); + total_count += updates_count; warning |= warning_updates_regex .as_ref() @@ -321,20 +322,15 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { let mut widget = Widget::new(); - let total_count = package_manager_map.values().sum(); + package_manager_map.insert("icon".into(), Value::icon("update")); + package_manager_map.insert("total".into(), Value::number(total_count)); + widget.set_format(match total_count { 0 => format_up_to_date.clone(), 1 => format_singular.clone(), _ => format.clone(), }); - widget.set_values(map!( - "icon" => Value::icon("update"), - "apt" => Value::number(package_manager_map["apt"]), - "pacman" => Value::number(package_manager_map["pacman"]), - "aur" => Value::number(package_manager_map["aur"]), - "dnf" => Value::number(package_manager_map["dnf"]), - "total" => Value::number(total_count), - )); + widget.set_values(package_manager_map); widget.state = match total_count { 0 => State::Idle, @@ -359,7 +355,7 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { #[async_trait] pub trait Backend { - fn name(&self) -> &str; + fn name(&self) -> Cow<'static, str>; async fn get_updates_list(&self) -> Result>; } diff --git a/src/blocks/packages/apt.rs b/src/blocks/packages/apt.rs index fa9ee0ce2a..8d15692fc9 100644 --- a/src/blocks/packages/apt.rs +++ b/src/blocks/packages/apt.rs @@ -85,8 +85,8 @@ impl Apt { #[async_trait] impl Backend for Apt { - fn name(&self) -> &str { - "apt" + fn name(&self) -> Cow<'static, str> { + "apt".into() } async fn get_updates_list(&self) -> Result> { diff --git a/src/blocks/packages/dnf.rs b/src/blocks/packages/dnf.rs index 462450dc84..85a9f73089 100644 --- a/src/blocks/packages/dnf.rs +++ b/src/blocks/packages/dnf.rs @@ -13,8 +13,8 @@ impl Dnf { #[async_trait] impl Backend for Dnf { - fn name(&self) -> &str { - "dnf" + fn name(&self) -> Cow<'static, str> { + "dnf".into() } async fn get_updates_list(&self) -> Result> { diff --git a/src/blocks/packages/pacman.rs b/src/blocks/packages/pacman.rs index 0d838c3ed3..0f74dc9635 100644 --- a/src/blocks/packages/pacman.rs +++ b/src/blocks/packages/pacman.rs @@ -57,8 +57,8 @@ impl Aur { #[async_trait] impl Backend for Pacman { - fn name(&self) -> &str { - "pacman" + fn name(&self) -> Cow<'static, str> { + "pacman".into() } async fn get_updates_list(&self) -> Result> { @@ -127,8 +127,8 @@ impl Backend for Pacman { #[async_trait] impl Backend for Aur { - fn name(&self) -> &str { - "aur" + fn name(&self) -> Cow<'static, str> { + "aur".into() } async fn get_updates_list(&self) -> Result> { From 203de1735da5058c7ab9face54aca20b962f08ab Mon Sep 17 00:00:00 2001 From: IshanGrover2004 Date: Mon, 22 Jan 2024 10:12:18 +0530 Subject: [PATCH 28/32] feat: Restore `tokio::try_join!` for parallel execution --- src/blocks/pacman.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/blocks/pacman.rs b/src/blocks/pacman.rs index 878610983c..c315dd6ce4 100644 --- a/src/blocks/pacman.rs +++ b/src/blocks/pacman.rs @@ -219,8 +219,10 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { (values, warning, critical, count) } Watched::Both(_) => { - let pacman_updates = pacman_backend.get_updates_list().await?; - let aur_updates = aur_backend.get_updates_list().await?; + let (pacman_updates, aur_updates) = tokio::try_join!( + pacman_backend.get_updates_list(), + aur_backend.get_updates_list(), + )?; let pacman_count = pacman_updates.len(); let aur_count = aur_updates.len(); let values = map! { From cd6abc2a462ecdee8cb1345eb7f109b122e199cd Mon Sep 17 00:00:00 2001 From: IshanGrover2004 Date: Tue, 23 Jan 2024 16:09:53 +0530 Subject: [PATCH 29/32] feat: Add depreciation patches for `dnf` blocks --- src/blocks.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/blocks.rs b/src/blocks.rs index 291d7606f9..a3e8acb0e9 100644 --- a/src/blocks.rs +++ b/src/blocks.rs @@ -156,6 +156,10 @@ define_blocks!( custom, custom_dbus, disk_space, + #[deprecated( + since = "0.33.0", + note = "The block has been deprecated in favor of the the packages block" + )] dnf, docker, external_ip, From 8806b572f71dd8e54bc43a0101e05da3b626e984 Mon Sep 17 00:00:00 2001 From: IshanGrover2004 Date: Wed, 24 Jan 2024 09:19:17 +0530 Subject: [PATCH 30/32] docs: Add Default value for `package_manager` --- src/blocks/packages.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blocks/packages.rs b/src/blocks/packages.rs index 8ab8fd71be..2bf591b528 100644 --- a/src/blocks/packages.rs +++ b/src/blocks/packages.rs @@ -11,7 +11,7 @@ //! Key | Values | Default //! ----|--------|-------- //! `interval` | Update interval in seconds. | `600` -//! `package_manager` | Package manager to check for updates | - +//! `package_manager` | Package manager to check for updates | Automatically derived from format templates, but can be used to influence the `$total` value //! `format` | A string to customise the output of this block. See below for available placeholders. | `" $icon $total.eng(w:1) "` //! `format_singular` | Same as `format`, but for when exactly one update is available. | `" $icon $total.eng(w:1) "` //! `format_up_to_date` | Same as `format`, but for when no updates are available. | `" $icon $total.eng(w:1) "` From 94aad81e372b6f7aae8628506e2878dd13b5f34e Mon Sep 17 00:00:00 2001 From: IshanGrover2004 Date: Wed, 24 Jan 2024 09:41:16 +0530 Subject: [PATCH 31/32] docs: Make less verbose by deleting repetitive info --- src/blocks/packages.rs | 28 ++++++---------------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/src/blocks/packages.rs b/src/blocks/packages.rs index 2bf591b528..01f806c094 100644 --- a/src/blocks/packages.rs +++ b/src/blocks/packages.rs @@ -84,7 +84,6 @@ //! format = " $icon $apt updates available" //! format_singular = " $icon One update available " //! format_up_to_date = " $icon system up to date " -//! critical_updates_regex = "(linux|linux-lts|linux-zen)" //! [[block.click]] //! # shows dmenu with cached available updates. Any dmenu alternative should also work. //! button = "left" @@ -105,7 +104,6 @@ //! format = " $icon $pacman updates available " //! format_singular = " $icon $pacman update available " //! format_up_to_date = " $icon system up to date " -//! critical_updates_regex = "(linux|linux-lts|linux-zen)" //! [[block.click]] //! # pop-up a menu showing the available updates. Replace wofi with your favourite menu command. //! button = "left" @@ -116,24 +114,6 @@ //! update = true //! ``` //! -//! Pacman only config using warnings with ZFS modules: -//! -//! ```toml -//! [[block]] -//! block = "packages -//! package_manager = ["pacman"] -//! interval = 600 -//! format = " $icon $pacman updates available " -//! format_singular = " $icon $pacman update available " -//! format_up_to_date = " $icon system up to date " -//! # If a linux update is available, but no ZFS package, it won't be possible to -//! # actually perform a system upgrade, so we show a warning. -//! warning_updates_regex = "(linux|linux-lts|linux-zen)" -//! # If ZFS is available, we know that we can and should do an upgrade, so we show -//! # the status as critical. -//! critical_updates_regex = "(zfs|zfs-lts)" -//! ``` -//! //! Pacman and AUR helper config: //! //! ```toml @@ -145,7 +125,6 @@ //! format = " $icon $pacman + $aur = $both updates available " //! format_singular = " $icon $both update available " //! format_up_to_date = " $icon system up to date " -//! critical_updates_regex = "(linux|linux-lts|linux-zen)" //! # aur_command should output available updates to stdout (ie behave as echo -ne "update\n") //! aur_command = "yay -Qua" //! ``` @@ -161,7 +140,6 @@ //! format = " $icon $dnf.eng(w:1) updates available " //! format_singular = " $icon One update available " //! format_up_to_date = " $icon system up to date " -//! critical_updates_regex = "(linux|linux-lts|linux-zen)" //! [[block.click]] //! # shows dmenu with cached available updates. Any dmenu alternative should also work. //! button = "left" @@ -180,6 +158,12 @@ //! format = " $icon $apt + $pacman + $aur + $dnf = $total updates available " //! format_singular = " $icon One update available " //! format_up_to_date = " $icon system up to date " +//! # If a linux update is available, but no ZFS package, it won't be possible to +//! # actually perform a system upgrade, so we show a warning. +//! warning_updates_regex = "(linux|linux-lts|linux-zen)" +//! # If ZFS is available, we know that we can and should do an upgrade, so we show +//! # the status as critical. +//! critical_updates_regex = "(zfs|zfs-lts)" //! ``` //! //! # Icons Used From 4ce8aba550606dadc7ef7bf0aaf8a81bfbe96ca5 Mon Sep 17 00:00:00 2001 From: IshanGrover2004 Date: Wed, 24 Jan 2024 13:05:28 +0530 Subject: [PATCH 32/32] fix: Swap `$both` with `$total` in docs --- src/blocks/packages.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/blocks/packages.rs b/src/blocks/packages.rs index 01f806c094..bd95dc1c8c 100644 --- a/src/blocks/packages.rs +++ b/src/blocks/packages.rs @@ -122,8 +122,8 @@ //! package_manager = ["pacman", "aur"] //! interval = 600 //! error_interval = 300 -//! format = " $icon $pacman + $aur = $both updates available " -//! format_singular = " $icon $both update available " +//! format = " $icon $pacman + $aur = $total updates available " +//! format_singular = " $icon $total update available " //! format_up_to_date = " $icon system up to date " //! # aur_command should output available updates to stdout (ie behave as echo -ne "update\n") //! aur_command = "yay -Qua"