From fae14e26ba788464540dbdc4317740cc62fbf89c Mon Sep 17 00:00:00 2001 From: acheronfail Date: Sun, 25 Jun 2023 21:26:56 +0930 Subject: [PATCH 01/57] update IDEAS.md --- IDEAS.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/IDEAS.md b/IDEAS.md index 9e73cb1..5e35643 100644 --- a/IDEAS.md +++ b/IDEAS.md @@ -15,8 +15,7 @@ There's no guarantee they'll ever be added or implemented, and they'll likely be ## Improvements -* script to generate and resize screenshots to easily update readme - * `scrot` + `convert` with `Xephyr`, etc +* ... ## Tips From fae145054383cc8993bd6c56dc70c03f9ecffbb5 Mon Sep 17 00:00:00 2001 From: acheronfail Date: Sun, 25 Jun 2023 22:35:06 +0930 Subject: [PATCH 02/57] exit with 0 when shutdown --- src/main.rs | 29 +++++++++++++++++------------ tests/spawn/ipc.rs | 3 +++ tests/util.rs | 2 +- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/main.rs b/src/main.rs index c1ee78e..6a776f3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,3 @@ -use std::convert::Infallible; use std::error::Error; use std::process; @@ -18,15 +17,21 @@ use istat::util::{local_block_on, RcCell}; use tokio::sync::mpsc::{self, Receiver}; use tokio_util::sync::CancellationToken; +enum RuntimeStopReason { + Shutdown, +} + fn main() { - if let Err(err) = start_runtime() { - // TODO: exit with 0 if `shutdown` - log::error!("{}", err); - process::exit(1); + match start_runtime() { + Ok(RuntimeStopReason::Shutdown) => {} + Err(err) => { + log::error!("{}", err); + process::exit(1); + } } } -fn start_runtime() -> Result> { +fn start_runtime() -> Result> { pretty_env_logger::try_init()?; let args = Cli::parse(); @@ -44,7 +49,7 @@ fn start_runtime() -> Result> { result } -async fn async_main(args: Cli) -> Result> { +async fn async_main(args: Cli) -> Result> { let config = RcCell::new(AppConfig::read(args).await?); // create socket first, so it's ready before anything is written to stdout @@ -68,15 +73,15 @@ async fn async_main(args: Cli) -> Result> { ); // handle our inputs: i3's IPC and our own IPC - let err = tokio::select! { - err = handle_ipc_events(socket, ipc_ctx) => err, - err = handle_click_events(dispatcher.clone()) => err, - _ = token.cancelled() => Err("cancelled".into()), + let result = tokio::select! { + Err(err) = handle_ipc_events(socket, ipc_ctx) => Err(err), + Err(err) = handle_click_events(dispatcher.clone()) => Err(err), + _ = token.cancelled() => Ok(RuntimeStopReason::Shutdown), }; // if we reach here, then something went wrong, so clean up signal_handle.close(); - return err; + return result; } fn setup_i3_bar( diff --git a/tests/spawn/ipc.rs b/tests/spawn/ipc.rs index a340e5f..c4b5cb8 100644 --- a/tests/spawn/ipc.rs +++ b/tests/spawn/ipc.rs @@ -12,6 +12,9 @@ spawn_test!( istat.send_shutdown(); // there were no items in the config, so nothing should have been outputted assert_eq!(istat.next_line().unwrap(), None); + // check exit status + let status = istat.child.wait().unwrap(); + assert_eq!(status.code(), Some(0)); } ); diff --git a/tests/util.rs b/tests/util.rs index 4e88ef8..3613cd4 100644 --- a/tests/util.rs +++ b/tests/util.rs @@ -148,7 +148,7 @@ impl Drop for LogOnDropChild { } } - self.kill().unwrap(); + let _ = self.kill(); } } From fae1a86fd1bc7c2ded85887b1ba2f2641ea3c5d1 Mon Sep 17 00:00:00 2001 From: acheronfail Date: Sun, 25 Jun 2023 23:41:05 +0930 Subject: [PATCH 03/57] reset retries after timeout, and update item on max retry reached --- src/main.rs | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/main.rs b/src/main.rs index 6a776f3..29e2fe1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,6 +15,7 @@ use istat::signals::handle_signals; use istat::theme::Theme; use istat::util::{local_block_on, RcCell}; use tokio::sync::mpsc::{self, Receiver}; +use tokio::time::Instant; use tokio_util::sync::CancellationToken; enum RuntimeStopReason { @@ -114,7 +115,9 @@ fn setup_i3_bar( tokio::task::spawn_local(async move { let mut retries = 0; + let mut last_start; loop { + last_start = Instant::now(); let (event_tx, event_rx) = mpsc::channel(32); dispatcher.set(idx, event_tx); @@ -128,13 +131,26 @@ fn setup_i3_bar( let fut = bar_item.start(ctx); match fut.await { - Ok(StopAction::Restart) if retries < 3 => { - log::error!("item[{}] requested restart...", idx); - retries += 1; - continue; - } Ok(StopAction::Restart) => { + // reset retries if no retries have occurred in the last 5 minutes + if last_start.elapsed().as_secs() > 60 * 5 { + retries = 0; + } + + // restart if we haven't exceeded limit + if retries < 3 { + log::warn!("item[{}] requested restart...", idx); + retries += 1; + continue; + } + + // we exceeded the limit, so error out log::error!("item[{}] stopped, exceeded max retries", idx); + let theme = config.theme.clone(); + bar[idx] = I3Item::new("MAX RETRIES") + .color(theme.bg) + .background_color(theme.red); + break; } // since this item has terminated, remove its entry from the bar From fae1653581e2e16128c08640333c4ad0d172de63 Mon Sep 17 00:00:00 2001 From: acheronfail Date: Mon, 26 Jun 2023 11:28:21 +0930 Subject: [PATCH 04/57] nic + krb: various refactoring and updates to net detection --- IDEAS.md | 1 - sample_config.toml | 3 + src/bar_items/krb.rs | 34 +++++- src/bar_items/nic.rs | 103 +++++++++++++++++ src/bar_items/nic/mod.rs | 122 -------------------- src/lib.rs | 2 + src/{bar_items/nic => util/net}/filter.rs | 12 +- src/util/{net.rs => net/interface.rs} | 73 +++--------- src/util/net/mod.rs | 132 ++++++++++++++++++++++ 9 files changed, 301 insertions(+), 181 deletions(-) create mode 100644 src/bar_items/nic.rs delete mode 100644 src/bar_items/nic/mod.rs rename src/{bar_items/nic => util/net}/filter.rs (89%) rename src/util/{net.rs => net/interface.rs} (60%) create mode 100644 src/util/net/mod.rs diff --git a/IDEAS.md b/IDEAS.md index 5e35643..0d72b8f 100644 --- a/IDEAS.md +++ b/IDEAS.md @@ -5,7 +5,6 @@ There's no guarantee they'll ever be added or implemented, and they'll likely be ## Features -* conditionally disable bar items * a bin PKGBUILD for the AUR (would need to setup CI first) * man pages for all binaries diff --git a/sample_config.toml b/sample_config.toml index 410ade2..9903737 100644 --- a/sample_config.toml +++ b/sample_config.toml @@ -92,6 +92,9 @@ thresholds = ["1kiB", "1MiB", "10MiB", "25MiB", "100MiB"] type = "krb" # How often this item should refresh interval = "2m" +# Optionally enable this item only when specific networks are active. +# This is the same format as the `filter` property in the `nic` item. +only_on = ["vpn0:v4"] [[items]] # a raw item - these are static items that don't change, and display the values here diff --git a/src/bar_items/krb.rs b/src/bar_items/krb.rs index 47d05ed..ad9148a 100644 --- a/src/bar_items/krb.rs +++ b/src/bar_items/krb.rs @@ -8,11 +8,15 @@ use tokio::process::Command; use crate::context::{BarItem, Context, StopAction}; use crate::i3::{I3Item, I3Markup}; use crate::theme::Theme; +use crate::util::filter::InterfaceFilter; +use crate::util::net_subscribe; #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct Krb { #[serde(default, with = "crate::human_time::option")] interval: Option, + #[serde(default)] + only_on: Vec, } impl Krb { @@ -35,9 +39,37 @@ impl Krb { #[async_trait(?Send)] impl BarItem for Krb { async fn start(&self, mut ctx: Context) -> Result> { + let mut net = net_subscribe().await?; + let mut disabled = !self.only_on.is_empty(); loop { + tokio::select! { + // any bar event + _ = ctx.wait_for_event(self.interval) => { + // don't update if disabled + if disabled { + continue; + } + }, + // network update - check update disabled state + Ok(interfaces) = net.wait_for_change() => { + // if none of the filters matched + if interfaces.filtered(&self.only_on).is_empty() { + // if the item wasn't disabled, then empty it out + if !disabled { + ctx.update_item(I3Item::empty()).await?; + } + + // and set it to disabled + disabled = true; + + // reset loop and wait to be enabled + continue; + } + } + } + + // update item ctx.update_item(self.item(&ctx.config.theme).await?).await?; - ctx.wait_for_event(self.interval).await; } } } diff --git a/src/bar_items/nic.rs b/src/bar_items/nic.rs new file mode 100644 index 0000000..ca30df2 --- /dev/null +++ b/src/bar_items/nic.rs @@ -0,0 +1,103 @@ +use std::error::Error; +use std::time::Duration; + +use async_trait::async_trait; +use hex_color::HexColor; +use iwlib::WirelessInfo; +use serde_derive::{Deserialize, Serialize}; + +use crate::context::{BarEvent, BarItem, Context, StopAction}; +use crate::i3::{I3Item, I3Markup, I3Modifier}; +use crate::theme::Theme; +use crate::util::filter::InterfaceFilter; +use crate::util::net::Interface; +use crate::util::{net_subscribe, Paginator}; + +impl Interface { + fn format_wireless(&self, i: WirelessInfo, theme: &Theme) -> (String, Option) { + let fg = match i.wi_quality { + 100..=u8::MAX => theme.green, + 80..=99 => theme.green, + 60..=79 => theme.yellow, + 40..=59 => theme.orange, + _ => theme.red, + }; + + ( + format!("({}) {}% at {}", self.addr, i.wi_quality, i.wi_essid), + Some(fg), + ) + } + + fn format_normal(&self, theme: &Theme) -> (String, Option) { + (format!("({})", self.addr), Some(theme.green)) + } + + fn format(&self, theme: &Theme) -> (String, String) { + let (addr, fg) = match self.get_wireless_info() { + Some(info) => self.format_wireless(info, theme), + None => self.format_normal(theme), + }; + + let fg = fg + .map(|c| format!(r#" foreground="{}""#, c)) + .unwrap_or("".into()); + ( + format!(r#"{}{}"#, fg, self.name, addr), + format!(r#"{}"#, fg, self.name), + ) + } +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct Nic { + #[serde(default, with = "crate::human_time::option")] + interval: Option, + #[serde(default)] + filter: Vec, +} + +#[async_trait(?Send)] +impl BarItem for Nic { + async fn start(&self, mut ctx: Context) -> Result> { + let mut net = net_subscribe().await?; + let mut p = Paginator::new(); + + let mut interfaces = vec![]; + loop { + tokio::select! { + // wait for network changes + Ok(list) = net.wait_for_change() => { + interfaces = list.filtered(&self.filter); + }, + // on any bar event + Some(event) = ctx.wait_for_event(self.interval) => { + // update paginator + p.update(&event); + + // request interfaces update + if let BarEvent::Click(click) = event { + if click.modifiers.contains(&I3Modifier::Control) { + net.trigger_update().await?; + } + } + } + } + + let item = if interfaces.is_empty() { + // TODO: differentiate between empty after filtering, and completely disconnected? + I3Item::new("inactive").color(ctx.config.theme.dim) + } else { + p.set_len(interfaces.len()); + + let theme = &ctx.config.theme; + let (full, short) = interfaces[p.idx()].format(theme); + let full = format!(r#"{}{}"#, full, p.format(theme)); + + I3Item::new(full).short_text(short).markup(I3Markup::Pango) + }; + + ctx.update_item(item).await?; + } + } +} diff --git a/src/bar_items/nic/mod.rs b/src/bar_items/nic/mod.rs deleted file mode 100644 index 7d7673c..0000000 --- a/src/bar_items/nic/mod.rs +++ /dev/null @@ -1,122 +0,0 @@ -mod filter; - -use std::error::Error; -use std::time::Duration; - -use async_trait::async_trait; -use futures::StreamExt; -use hex_color::HexColor; -use iwlib::WirelessInfo; -use serde_derive::{Deserialize, Serialize}; - -use self::filter::InterfaceFilter; -use crate::context::{BarItem, Context, StopAction}; -use crate::dbus::network_manager::NetworkManagerProxy; -use crate::dbus::{dbus_connection, BusType}; -use crate::i3::{I3Item, I3Markup}; -use crate::theme::Theme; -use crate::util::net::Interface; -use crate::util::Paginator; - -impl Interface { - fn format_wireless(&self, i: WirelessInfo, theme: &Theme) -> (String, Option) { - let fg = match i.wi_quality { - 100..=u8::MAX => theme.green, - 80..=99 => theme.green, - 60..=79 => theme.yellow, - 40..=59 => theme.orange, - _ => theme.red, - }; - - ( - format!("({}) {}% at {}", self.addr, i.wi_quality, i.wi_essid), - Some(fg), - ) - } - - fn format_normal(&self, theme: &Theme) -> (String, Option) { - (format!("({})", self.addr), Some(theme.green)) - } - - fn format(&mut self, theme: &Theme) -> (String, String) { - let (addr, fg) = match self.get_wireless_info() { - Some(info) => self.format_wireless(info, theme), - None => self.format_normal(theme), - }; - - let fg = fg - .map(|c| format!(r#" foreground="{}""#, c)) - .unwrap_or("".into()); - ( - format!(r#"{}{}"#, fg, self.name, addr), - format!(r#"{}"#, fg, self.name), - ) - } -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -pub struct Nic { - #[serde(default, with = "crate::human_time::option")] - interval: Option, - /// This type is in the format of `interface[:type]`, where `interface` is the interface name, and - /// `type` is an optional part which is either `ipv4` or `ipv6`. - /// - /// If `interface` is an empty string, then all interfaces are matched, for example: - /// - `vpn0:ipv4` will match ip4 addresses for the `vpn` interface - /// - `:ipv6` will match all interfaces which have an ip6 address - // TODO: better filtering? don't match docker interfaces, or libvirtd ones, etc? - #[serde(default)] - filter: Vec, -} - -#[async_trait(?Send)] -impl BarItem for Nic { - async fn start(&self, mut ctx: Context) -> Result> { - let connection = dbus_connection(BusType::System).await?; - let nm = NetworkManagerProxy::new(&connection).await?; - let mut nm_state_change = nm.receive_state_changed().await?; - - let mut p = Paginator::new(); - loop { - let mut interfaces = Interface::get_interfaces()? - .into_iter() - .filter(|i| { - if self.filter.is_empty() { - true - } else { - self.filter.iter().any(|f| f.matches(i)) - } - }) - .collect::>(); - - // no networks active - if interfaces.is_empty() { - ctx.update_item(I3Item::new("inactive").color(ctx.config.theme.dim)) - .await?; - - tokio::select! { - Some(_) = ctx.wait_for_event(self.interval) => continue, - Some(_) = nm_state_change.next() => continue, - } - } - - p.set_len(interfaces.len()); - - let theme = &ctx.config.theme; - let (full, short) = interfaces[p.idx()].format(theme); - let full = format!(r#"{}{}"#, full, p.format(theme)); - - let item = I3Item::new(full).short_text(short).markup(I3Markup::Pango); - ctx.update_item(item).await?; - - tokio::select! { - // update on network manager changes - Some(_) = nm_state_change.next() => continue, - // cycle through networks on click - Some(event) = ctx.wait_for_event(self.interval) => { - p.update(&event); - }, - } - } - } -} diff --git a/src/lib.rs b/src/lib.rs index 828b18b..73de04e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ +#![feature(ip)] + // NOTE: this exists only so `/bin/*.rs` files can access the same modules #[macro_use] diff --git a/src/bar_items/nic/filter.rs b/src/util/net/filter.rs similarity index 89% rename from src/bar_items/nic/filter.rs rename to src/util/net/filter.rs index cf6ffd8..3605d4f 100644 --- a/src/bar_items/nic/filter.rs +++ b/src/util/net/filter.rs @@ -3,8 +3,16 @@ use std::str::FromStr; use serde::{de, Deserialize, Serialize}; -use crate::util::net::{Interface, InterfaceKind}; - +use super::interface::InterfaceKind; +use crate::util::net::Interface; + +/// This type is in the format of `interface[:type]`, where `interface` is the interface name, and +/// `type` is an optional part which is either `ipv4` or `ipv6`. +/// +/// If `interface` is an empty string, then all interfaces are matched, for example: +/// - `vpn0:ipv4` will match ip4 addresses for the `vpn` interface +/// - `:ipv6` will match all interfaces which have an ip6 address +// TODO: better filtering? don't match docker interfaces, or libvirtd ones, etc? #[derive(Debug, Clone, PartialEq, Eq)] pub struct InterfaceFilter { name: String, diff --git a/src/util/net.rs b/src/util/net/interface.rs similarity index 60% rename from src/util/net.rs rename to src/util/net/interface.rs index 876600c..fdb8010 100644 --- a/src/util/net.rs +++ b/src/util/net/interface.rs @@ -1,4 +1,3 @@ -use std::cmp::Ordering; use std::error::Error; use std::net::{SocketAddrV4, SocketAddrV6}; use std::str::FromStr; @@ -7,7 +6,7 @@ use iwlib::{get_wireless_info, WirelessInfo}; use nix::ifaddrs::getifaddrs; use nix::net::if_::InterfaceFlags; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum InterfaceKind { V4, V6, @@ -34,33 +33,14 @@ impl FromStr for InterfaceKind { } } -#[derive(Debug, PartialEq, Eq)] +// TODO: cache these? pass them all around by reference? interior mutability for wireless or not? +// cache list of wireless ones? +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct Interface { pub name: String, pub addr: String, pub kind: InterfaceKind, pub flags: InterfaceFlags, - pub is_wireless: Option, -} - -impl PartialOrd for Interface { - fn partial_cmp(&self, other: &Self) -> Option { - match self.name.partial_cmp(&other.name) { - Some(Ordering::Equal) => {} - ord => return ord, - } - match self.addr.partial_cmp(&other.addr) { - Some(Ordering::Equal) => {} - ord => return ord, - } - self.flags.partial_cmp(&other.flags) - } -} - -impl Ord for Interface { - fn cmp(&self, other: &Self) -> Ordering { - self.partial_cmp(other).unwrap_or(Ordering::Equal) - } } impl Interface { @@ -75,7 +55,6 @@ impl Interface { addr: addr.as_ref().into(), kind, flags, - is_wireless: None, } } @@ -83,32 +62,9 @@ impl Interface { self.flags.contains(InterfaceFlags::IFF_TAP) || self.flags.contains(InterfaceFlags::IFF_TUN) } - pub fn is_wireless(&mut self) -> bool { - match self.is_wireless { - Some(b) => b, - None => self.get_wireless_info().is_some(), - } - } - - pub fn get_wireless_info(&mut self) -> Option { - // check if this is a wireless network - match self.is_wireless { - // not a wireless interface, just return defaults - Some(false) => None, - // SAFETY: we've previously checked if this is a wireless network - Some(true) => get_wireless_info(&self.name), - // check if we're a wireless network and remember for next time - None => match get_wireless_info(&self.name) { - Some(i) => { - self.is_wireless = Some(true); - Some(i) - } - None => { - self.is_wireless = Some(false); - None - } - }, - } + /// If this is a wireless network, then return info from `iwlib` + pub fn get_wireless_info(&self) -> Option { + get_wireless_info(&self.name) } pub fn get_interfaces() -> Result, Box> { @@ -141,10 +97,17 @@ impl Interface { format!("{}", SocketAddrV4::from(*ipv4).ip()), InterfaceKind::V4, ), - (_, Some(ipv6)) => ( - format!("{}", SocketAddrV6::from(*ipv6).ip()), - InterfaceKind::V6, - ), + (_, Some(ipv6)) => { + // filter out non-global ipv6 addresses + if !ipv6.ip().is_global() { + continue; + } + + ( + format!("{}", SocketAddrV6::from(*ipv6).ip()), + InterfaceKind::V6, + ) + } (None, None) => continue, }; diff --git a/src/util/net/mod.rs b/src/util/net/mod.rs new file mode 100644 index 0000000..13e7eae --- /dev/null +++ b/src/util/net/mod.rs @@ -0,0 +1,132 @@ +pub mod filter; +pub mod interface; + +use std::error::Error; +use std::ops::Deref; + +use futures::StreamExt; +use tokio::sync::{broadcast, mpsc, OnceCell}; + +use self::filter::InterfaceFilter; +pub use self::interface::Interface; +use crate::dbus::network_manager::NetworkManagerProxy; +use crate::dbus::{dbus_connection, BusType}; + +// FIXME: I don't like this interface list thing +#[derive(Debug, Clone)] +pub struct InterfaceList { + inner: Vec, +} + +impl InterfaceList { + pub fn filtered(self, filter: &[InterfaceFilter]) -> Vec { + self.inner + .into_iter() + .filter(|i| { + if filter.is_empty() { + true + } else { + filter.iter().any(|filter| filter.matches(i)) + } + }) + .collect() + } +} + +impl Deref for InterfaceList { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +static NET_RX: OnceCell = OnceCell::const_new(); + +#[derive(Debug)] +pub struct Net { + tx: mpsc::Sender<()>, + rx: broadcast::Receiver, +} + +impl Net { + fn new(tx: mpsc::Sender<()>, rx: broadcast::Receiver) -> Net { + Net { tx, rx } + } + + pub async fn wait_for_change(&mut self) -> Result> { + Ok(self.rx.recv().await?) + } + + pub async fn trigger_update(&self) -> Result<(), Box> { + Ok(self.tx.send(()).await?) + } + + pub async fn update_now(&mut self) -> Result> { + self.trigger_update().await?; + self.wait_for_change().await + } +} + +impl Clone for Net { + fn clone(&self) -> Self { + Self { + tx: self.tx.clone(), + rx: self.rx.resubscribe(), + } + } +} + +pub async fn net_subscribe() -> Result> { + Ok(NET_RX.get_or_try_init(start_task).await?.clone()) +} + +async fn start_task() -> Result> { + let (iface_tx, iface_rx) = broadcast::channel(2); + let (manual_tx, manual_rx) = mpsc::channel(1); + tokio::task::spawn_local(watch_net_updates(iface_tx, manual_rx)); + + Ok::<_, Box>(Net::new(manual_tx, iface_rx)) +} + +async fn watch_net_updates( + tx: broadcast::Sender, + mut rx: mpsc::Receiver<()>, +) -> Result<(), Box> { + // TODO: investigate effort of checking network state with netlink rather than dbus + let connection = dbus_connection(BusType::System).await?; + let nm = NetworkManagerProxy::new(&connection).await?; + // this captures all network connect/disconnect events + let mut state_changed = nm.receive_state_changed().await?; + // this captures all vpn interface connect/disconnect events + let mut active_con_change = nm.receive_active_connections_objpath_changed().await; + + let mut force_update = true; + let mut last_value = vec![]; + loop { + // check current interfaces + let interfaces = Interface::get_interfaces()?; + + // send updates to subscribers only if it's changed since last time + if force_update || last_value != interfaces { + force_update = false; + last_value = interfaces.clone(); + tx.send(InterfaceList { inner: interfaces })?; + } + + tokio::select! { + // callers can manually trigger updates + Some(()) = rx.recv() => { + force_update = true; + continue; + }, + // catch updates from NetworkManager via dbus + opt = state_changed.next() => if opt.is_none() { + bail!("unexpected end of NetworkManagerProxy::receive_state_changed stream"); + }, + opt = active_con_change.next() => if opt.is_none() { + bail!("unexpected end of NetworkManagerProxy::receive_active_connections_objpath_changed stream"); + } + } + } +} From fae17b2e5183037d97855ddecbd40f1e1d3e2bf2 Mon Sep 17 00:00:00 2001 From: acheronfail Date: Mon, 26 Jun 2023 13:51:08 +0930 Subject: [PATCH 05/57] remove header when serialising ipc response in dev loop --- scripts/node/dev.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/node/dev.ts b/scripts/node/dev.ts index 3bd2120..f97c809 100755 --- a/scripts/node/dev.ts +++ b/scripts/node/dev.ts @@ -174,7 +174,10 @@ function handleInput(input: string) { const payload = Buffer.concat([header, message]); socket.write(payload); socket.on('data', (data) => { - process.stdout.write(c.green(`Refreshed all items. IPC response: ${data.toString()}\n`)); + // first 8 bytes are the header + const message = data.subarray(8); + process.stdout.clearLine(0); + process.stdout.write(c.green(`Refreshed all items. IPC response: ${message.toString()}\n`)); }); }); } From fae1431fd0d1e7d910fa79a28f5e417dcd22a0e2 Mon Sep 17 00:00:00 2001 From: acheronfail Date: Mon, 26 Jun 2023 15:01:13 +0930 Subject: [PATCH 06/57] create crate Result and Error types --- bin/acpi.rs | 5 ++--- bin/ipc.rs | 16 +++++----------- src/bar_items/battery.rs | 24 ++++++++++++------------ src/bar_items/cpu.rs | 4 ++-- src/bar_items/disk.rs | 4 ++-- src/bar_items/dunst.rs | 4 ++-- src/bar_items/kbd.rs | 10 +++++----- src/bar_items/krb.rs | 8 ++++---- src/bar_items/mem.rs | 4 ++-- src/bar_items/net_usage.rs | 4 ++-- src/bar_items/nic.rs | 4 ++-- src/bar_items/pulse/mod.rs | 4 ++-- src/bar_items/script.rs | 6 +++--- src/bar_items/sensors.rs | 4 ++-- src/bar_items/time.rs | 4 ++-- src/config/mod.rs | 6 +++--- src/config/parse.rs | 13 +++++-------- src/context.rs | 9 ++++++--- src/dbus/mod.rs | 6 +++--- src/dispatcher.rs | 7 +++---- src/error.rs | 2 ++ src/i3/bar_item.rs | 6 +++--- src/i3/ipc.rs | 6 ++---- src/ipc/client.rs | 10 +++------- src/ipc/mod.rs | 4 ++-- src/ipc/protocol.rs | 5 ++--- src/ipc/server.rs | 11 ++++------- src/lib.rs | 1 + src/main.rs | 12 +++++------- src/signals.rs | 7 ++----- src/test_utils.rs | 6 +++++- src/theme.rs | 6 +++--- src/util/mod.rs | 6 +++--- src/util/net/filter.rs | 10 +++++----- src/util/net/interface.rs | 9 +++++---- src/util/net/mod.rs | 16 ++++++++-------- src/util/netlink/acpi.rs | 13 ++++++------- src/util/netlink/ffi.rs | 14 +++++++------- src/util/netlink/mod.rs | 9 ++++----- 39 files changed, 141 insertions(+), 158 deletions(-) create mode 100644 src/error.rs diff --git a/bin/acpi.rs b/bin/acpi.rs index 748af08..54deeb8 100644 --- a/bin/acpi.rs +++ b/bin/acpi.rs @@ -1,8 +1,7 @@ -use std::error::Error; - +use istat::error::Result; use istat::util::{local_block_on, netlink_acpi_listen}; -fn main() -> Result<(), Box> { +fn main() -> Result<()> { let (output, _) = local_block_on(async { let mut acpi = netlink_acpi_listen().await?; while let Some(event) = acpi.recv().await { diff --git a/bin/ipc.rs b/bin/ipc.rs index 831257c..f2a2352 100644 --- a/bin/ipc.rs +++ b/bin/ipc.rs @@ -1,4 +1,3 @@ -use std::error::Error; use std::ffi::OsStr; use std::io::{Read, Write}; use std::os::unix::net::UnixStream; @@ -7,6 +6,7 @@ use std::path::PathBuf; use clap::builder::PossibleValue; use clap::{ColorChoice, Parser, Subcommand, ValueEnum}; use istat::bail; +use istat::error::Result; use istat::i3::{I3Button, I3ClickEvent, I3Modifier}; use istat::ipc::get_socket_path; use istat::ipc::protocol::{encode_ipc_msg, IpcBarEvent, IpcMessage, IpcReply, IPC_HEADER_LEN}; @@ -151,10 +151,7 @@ impl ValueEnum for Modifier { } } -fn send_message( - socket_path: impl AsRef, - msg: IpcMessage, -) -> Result> { +fn send_message(socket_path: impl AsRef, msg: IpcMessage) -> Result { let mut stream = UnixStream::connect(socket_path.as_ref())?; let msg = encode_ipc_msg(msg)?; @@ -173,10 +170,7 @@ fn send_message( Ok(serde_json::from_slice(&buf[IPC_HEADER_LEN..n])?) } -fn send_and_print_response( - socket_path: impl AsRef, - msg: IpcMessage, -) -> Result<(), Box> { +fn send_and_print_response(socket_path: impl AsRef, msg: IpcMessage) -> Result<()> { let resp = match send_message(&socket_path, msg) { Ok(resp) => resp, Err(e) => bail!("failed to send ipc message: {}", e), @@ -194,14 +188,14 @@ fn send_and_print_response( Ok(()) } -fn get_json_response(socket_path: &PathBuf, msg: IpcMessage) -> Result> { +fn get_json_response(socket_path: &PathBuf, msg: IpcMessage) -> Result { Ok(match send_message(socket_path, msg)? { IpcReply::Value(json) => json, _ => unreachable!(), }) } -fn main() -> Result<(), Box> { +fn main() -> Result<()> { let args = Cli::parse(); let socket_path = get_socket_path(args.socket.as_ref())?; diff --git a/src/bar_items/battery.rs b/src/bar_items/battery.rs index 7b2c4d6..745ae87 100644 --- a/src/bar_items/battery.rs +++ b/src/bar_items/battery.rs @@ -1,4 +1,3 @@ -use std::error::Error; use std::path::PathBuf; use std::str::FromStr; use std::time::Duration; @@ -13,6 +12,7 @@ use tokio::sync::mpsc::Receiver; use crate::context::{BarEvent, BarItem, Context, StopAction}; use crate::dbus::notifications::NotificationsProxy; use crate::dbus::{dbus_connection, BusType}; +use crate::error::Result; use crate::i3::{I3Button, I3Item, I3Markup}; use crate::theme::Theme; use crate::util::ffi::AcpiGenericNetlinkEvent; @@ -38,7 +38,7 @@ impl BatState { impl FromStr for BatState { type Err = String; - fn from_str(s: &str) -> Result { + fn from_str(s: &str) -> std::result::Result { // https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-power match s { "Unknown" => Ok(Self::Unknown), @@ -55,27 +55,27 @@ impl FromStr for BatState { struct Bat(PathBuf); impl Bat { - async fn read(&self, file_name: impl AsRef) -> Result> { + async fn read(&self, file_name: impl AsRef) -> Result { Ok(read_to_string(self.0.join(file_name.as_ref())).await?) } - async fn read_usize(&self, file_name: impl AsRef) -> Result> { + async fn read_usize(&self, file_name: impl AsRef) -> Result { Ok(self.read(file_name).await?.trim().parse::()?) } - fn name(&self) -> Result> { + fn name(&self) -> Result { match self.0.file_name() { Some(name) => Ok(name.to_string_lossy().into_owned()), None => Err(format!("failed to parse file name from: {}", self.0.display()).into()), } } - async fn get_state(&self) -> Result> { + async fn get_state(&self) -> Result { Ok(BatState::from_str(self.read("status").await?.trim())?) } // NOTE: there is also `/capacity` which returns an integer percentage - async fn percent(&self) -> Result> { + async fn percent(&self) -> Result { let (charge_now, charge_full) = try_join!( self.read_usize("charge_now"), self.read_usize("charge_full"), @@ -83,7 +83,7 @@ impl Bat { Ok((charge_now as f32) / (charge_full as f32) * 100.0) } - async fn watts_now(&self) -> Result> { + async fn watts_now(&self) -> Result { let (current_pico, voltage_pico) = try_join!( self.read_usize("current_now"), self.read_usize("voltage_now"), @@ -91,7 +91,7 @@ impl Bat { Ok((current_pico as f64) * (voltage_pico as f64) / 1_000_000_000_000.0) } - async fn format(&self, theme: &Theme, show_watts: bool) -> Result> { + async fn format(&self, theme: &Theme, show_watts: bool) -> Result { let (charge, state) = match try_join!(self.percent(), self.get_state()) { Ok((charge, state)) => (charge, state), // Return unknown state: the files in sysfs aren't present at times, such as when connecting @@ -138,7 +138,7 @@ impl Bat { }) } - async fn find_all() -> Result, Box> { + async fn find_all() -> Result> { let battery_dir = PathBuf::from("/sys/class/power_supply"); let mut entries = fs::read_dir(&battery_dir).await?; @@ -168,7 +168,7 @@ pub struct Battery { #[async_trait(?Send)] impl BarItem for Battery { - async fn start(&self, mut ctx: Context) -> Result> { + async fn start(&self, mut ctx: Context) -> Result { let batteries = match self.batteries.clone() { Some(inner) => inner, None => Bat::find_all().await?, @@ -233,7 +233,7 @@ enum BatteryAcpiEvent { AcAdapterPlugged(bool), } -async fn battery_acpi_events() -> Result, Box> { +async fn battery_acpi_events() -> Result> { let mut acpi_event = netlink_acpi_listen().await?; let (tx, rx) = tokio::sync::mpsc::channel(1); tokio::task::spawn_local(async move { diff --git a/src/bar_items/cpu.rs b/src/bar_items/cpu.rs index 7ca7810..29bdb86 100644 --- a/src/bar_items/cpu.rs +++ b/src/bar_items/cpu.rs @@ -1,4 +1,4 @@ -use std::error::Error; +use crate::error::Result; use std::time::Duration; use async_trait::async_trait; @@ -37,7 +37,7 @@ impl Cpu { #[async_trait(?Send)] impl BarItem for Cpu { - async fn start(&self, mut ctx: Context) -> Result> { + async fn start(&self, mut ctx: Context) -> Result { loop { let pct = { // refresh cpu usage diff --git a/src/bar_items/disk.rs b/src/bar_items/disk.rs index f5aff18..38a9cdd 100644 --- a/src/bar_items/disk.rs +++ b/src/bar_items/disk.rs @@ -1,5 +1,5 @@ use std::collections::HashSet; -use std::error::Error; +use crate::error::Result; use std::path::PathBuf; use std::time::Duration; @@ -61,7 +61,7 @@ impl DiskStats { #[async_trait(?Send)] impl BarItem for Disk { - async fn start(&self, mut ctx: Context) -> Result> { + async fn start(&self, mut ctx: Context) -> Result { let mut p = Paginator::new(); loop { let stats: Vec = { diff --git a/src/bar_items/dunst.rs b/src/bar_items/dunst.rs index ed3f157..01f899c 100644 --- a/src/bar_items/dunst.rs +++ b/src/bar_items/dunst.rs @@ -1,4 +1,4 @@ -use std::error::Error; +use crate::error::Result; use async_trait::async_trait; use futures::StreamExt; @@ -28,7 +28,7 @@ impl Dunst { #[async_trait(?Send)] impl BarItem for Dunst { - async fn start(&self, mut ctx: Context) -> Result> { + async fn start(&self, mut ctx: Context) -> Result { // get initial state let connection = dbus_connection(BusType::Session).await?; let dunst_proxy = DunstProxy::new(&connection).await?; diff --git a/src/bar_items/kbd.rs b/src/bar_items/kbd.rs index 84c4295..5fc893d 100644 --- a/src/bar_items/kbd.rs +++ b/src/bar_items/kbd.rs @@ -1,4 +1,3 @@ -use std::error::Error; use std::time::Duration; use async_trait::async_trait; @@ -7,6 +6,7 @@ use strum::{EnumIter, IntoEnumIterator}; use tokio::fs; use crate::context::{BarEvent, BarItem, Context, StopAction}; +use crate::error::Result; use crate::i3::{I3Item, I3Markup}; use crate::theme::Theme; @@ -42,7 +42,7 @@ impl Keys { } } - async fn is_on(&self) -> Result> { + async fn is_on(&self) -> Result { let mut entries = fs::read_dir("/sys/class/leds/").await?; let suffix = self.sys_dir_suffix(); @@ -72,7 +72,7 @@ impl Keys { } } - async fn format(self, theme: &Theme) -> Result> { + async fn format(self, theme: &Theme) -> Result { Ok(match self.is_on().await { Ok(is_on) => format!( r#"{}"#, @@ -94,14 +94,14 @@ impl Keys { #[async_trait(?Send)] impl BarItem for Kbd { - async fn start(&self, mut ctx: Context) -> Result> { + async fn start(&self, mut ctx: Context) -> Result { let keys = self.show.clone().unwrap_or_else(|| Keys::iter().collect()); 'outer: loop { let text = futures::future::join_all(keys.iter().map(|k| k.format(&ctx.config.theme))) .await .into_iter() - .collect::, _>>()? + .collect::>>()? .join(""); let item = I3Item::new(text).markup(I3Markup::Pango); diff --git a/src/bar_items/krb.rs b/src/bar_items/krb.rs index ad9148a..dbf6b6c 100644 --- a/src/bar_items/krb.rs +++ b/src/bar_items/krb.rs @@ -1,4 +1,4 @@ -use std::error::Error; +use crate::error::Result; use std::time::Duration; use async_trait::async_trait; @@ -20,12 +20,12 @@ pub struct Krb { } impl Krb { - async fn get_state(&self) -> Result> { + async fn get_state(&self) -> Result { let output = Command::new("klist").arg("-s").output().await?; Ok(output.status.success()) } - async fn item(&self, theme: &Theme) -> Result> { + async fn item(&self, theme: &Theme) -> Result { Ok(I3Item::new("󱕵") .markup(I3Markup::Pango) .color(if self.get_state().await? { @@ -38,7 +38,7 @@ impl Krb { #[async_trait(?Send)] impl BarItem for Krb { - async fn start(&self, mut ctx: Context) -> Result> { + async fn start(&self, mut ctx: Context) -> Result { let mut net = net_subscribe().await?; let mut disabled = !self.only_on.is_empty(); loop { diff --git a/src/bar_items/mem.rs b/src/bar_items/mem.rs index 8733e30..0595c9f 100644 --- a/src/bar_items/mem.rs +++ b/src/bar_items/mem.rs @@ -1,4 +1,4 @@ -use std::error::Error; +use crate::error::Result; use std::time::Duration; use async_trait::async_trait; @@ -45,7 +45,7 @@ impl Mem { #[async_trait(?Send)] impl BarItem for Mem { - async fn start(&self, mut ctx: Context) -> Result> { + async fn start(&self, mut ctx: Context) -> Result { let mut total = None; let mut display = EnumCycle::new_at(self.display); loop { diff --git a/src/bar_items/net_usage.rs b/src/bar_items/net_usage.rs index 20d1143..df74657 100644 --- a/src/bar_items/net_usage.rs +++ b/src/bar_items/net_usage.rs @@ -1,4 +1,4 @@ -use std::error::Error; +use crate::error::Result; use std::time::Duration; use async_trait::async_trait; @@ -89,7 +89,7 @@ fn format_bytes(bytes: u64, si: bool, as_bits: bool) -> String { #[async_trait(?Send)] impl BarItem for NetUsage { - async fn start(&self, mut ctx: Context) -> Result> { + async fn start(&self, mut ctx: Context) -> Result { let fg = |bytes: u64, theme: &Theme| { self.get_color(&theme, bytes) .map(|c| format!(r#" foreground="{}""#, c)) diff --git a/src/bar_items/nic.rs b/src/bar_items/nic.rs index ca30df2..7b34d8c 100644 --- a/src/bar_items/nic.rs +++ b/src/bar_items/nic.rs @@ -1,4 +1,4 @@ -use std::error::Error; +use crate::error::Result; use std::time::Duration; use async_trait::async_trait; @@ -59,7 +59,7 @@ pub struct Nic { #[async_trait(?Send)] impl BarItem for Nic { - async fn start(&self, mut ctx: Context) -> Result> { + async fn start(&self, mut ctx: Context) -> Result { let mut net = net_subscribe().await?; let mut p = Paginator::new(); diff --git a/src/bar_items/pulse/mod.rs b/src/bar_items/pulse/mod.rs index 77a5c7b..d7b07bc 100644 --- a/src/bar_items/pulse/mod.rs +++ b/src/bar_items/pulse/mod.rs @@ -1,6 +1,6 @@ mod custom; -use std::error::Error; +use crate::error::Result; use std::fmt::Debug; use std::process; use std::rc::Rc; @@ -610,7 +610,7 @@ impl RcCell { #[async_trait(?Send)] impl BarItem for Pulse { - async fn start(&self, mut ctx: Context) -> Result> { + async fn start(&self, mut ctx: Context) -> Result { // setup pulse main loop let (mut main_loop, pa_ctx) = { let mut main_loop = TokioMain::new(); diff --git a/src/bar_items/script.rs b/src/bar_items/script.rs index d50d00c..c1161c0 100644 --- a/src/bar_items/script.rs +++ b/src/bar_items/script.rs @@ -1,5 +1,5 @@ use std::collections::HashMap; -use std::error::Error; +use crate::error::Result; use std::time::Duration; use async_trait::async_trait; @@ -31,7 +31,7 @@ pub struct Script { impl Script { // returns stdout - async fn run(&self, env: &HashMap<&str, String>) -> Result> { + async fn run(&self, env: &HashMap<&str, String>) -> Result { let output = Command::new("sh") .arg("-c") .arg(&self.command) @@ -45,7 +45,7 @@ impl Script { #[async_trait(?Send)] impl BarItem for Script { - async fn start(&self, mut ctx: Context) -> Result> { + async fn start(&self, mut ctx: Context) -> Result { // update script environment on any click event let mut script_env = HashMap::new(); let handle_event = |event: BarEvent, env: &mut HashMap<_, _>| match event { diff --git a/src/bar_items/sensors.rs b/src/bar_items/sensors.rs index 37d96b9..e9d3df4 100644 --- a/src/bar_items/sensors.rs +++ b/src/bar_items/sensors.rs @@ -1,4 +1,4 @@ -use std::error::Error; +use crate::error::Result; use std::time::Duration; use async_trait::async_trait; @@ -35,7 +35,7 @@ impl Sensors { #[async_trait(?Send)] impl BarItem for Sensors { - async fn start(&self, mut ctx: Context) -> Result> { + async fn start(&self, mut ctx: Context) -> Result { { ctx.state.sys.refresh_components_list(); } diff --git a/src/bar_items/time.rs b/src/bar_items/time.rs index bcdd3b6..1655899 100644 --- a/src/bar_items/time.rs +++ b/src/bar_items/time.rs @@ -1,4 +1,4 @@ -use std::error::Error; +use crate::error::Result; use std::time::Duration; use async_trait::async_trait; @@ -19,7 +19,7 @@ pub struct Time { #[async_trait(?Send)] impl BarItem for Time { - async fn start(&self, mut ctx: Context) -> Result> { + async fn start(&self, mut ctx: Context) -> Result { loop { let now = Local::now(); let item = I3Item::new(format!("󰥔 {}", now.format(&self.format_long))) diff --git a/src/config/mod.rs b/src/config/mod.rs index 3b9494f..0a35429 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -2,7 +2,6 @@ mod item; mod parse; use std::cell::OnceCell; -use std::error::Error; use std::path::PathBuf; use indexmap::IndexMap; @@ -10,6 +9,7 @@ use serde_derive::{Deserialize, Serialize}; use crate::cli::Cli; use crate::config::item::Item; +use crate::error::Result; use crate::ipc::get_socket_path; use crate::theme::Theme; use crate::util::sort_by_indices; @@ -78,7 +78,7 @@ impl AppConfig { } /// Ensure configuration of item names have no duplicates. - fn validate_names(items: &[Item]) -> Result<(), Box> { + fn validate_names(items: &[Item]) -> Result<()> { for (i, a) in items.iter().enumerate().rev() { for (j, b) in items.iter().enumerate() { if i == j { @@ -99,7 +99,7 @@ impl AppConfig { Ok(()) } - pub async fn read(args: Cli) -> Result> { + pub async fn read(args: Cli) -> Result { let mut cfg = parse::parse(&args)?; // set socket path explicitly here diff --git a/src/config/parse.rs b/src/config/parse.rs index 9877d20..c4da32f 100644 --- a/src/config/parse.rs +++ b/src/config/parse.rs @@ -1,5 +1,4 @@ use std::collections::HashSet; -use std::error::Error; use std::ffi::OsStr; use std::path::{Path, PathBuf}; @@ -10,15 +9,13 @@ use wordexp::{wordexp, Wordexp}; use crate::cli::Cli; use crate::config::AppConfig; +use crate::error::Result; -fn expand_include_path( - s: impl AsRef, - cfg_dir: impl AsRef, -) -> Result, Box> { +fn expand_include_path(s: impl AsRef, cfg_dir: impl AsRef) -> Result> { let cfg_dir = cfg_dir.as_ref(); // perform expansion, see: man 3 wordexp Ok(wordexp(s.as_ref(), Wordexp::new(0), 0)? - .map(|path| -> Result<_, Box> { + .map(|path| -> Result<_> { // convert expansion to path let path = PathBuf::from(path); if path.is_absolute() { @@ -33,10 +30,10 @@ fn expand_include_path( } } }) - .collect::>()?) + .collect::>()?) } -pub fn parse(args: &Cli) -> Result> { +pub fn parse(args: &Cli) -> Result { let cfg_file = args .config .as_ref() diff --git a/src/context.rs b/src/context.rs index bc56fe3..9c412e4 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1,4 +1,3 @@ -use std::error::Error; use std::time::Duration; use async_trait::async_trait; @@ -11,6 +10,7 @@ use tokio::sync::{mpsc, oneshot}; use tokio::time::sleep; use crate::config::AppConfig; +use crate::error::Result; use crate::i3::bar_item::I3Item; use crate::i3::I3ClickEvent; use crate::util::RcCell; @@ -71,7 +71,10 @@ impl Context { } } - pub async fn update_item(&self, item: I3Item) -> Result<(), SendError<(I3Item, usize)>> { + pub async fn update_item( + &self, + item: I3Item, + ) -> std::result::Result<(), SendError<(I3Item, usize)>> { self.tx_item.send((item, self.index)).await?; Ok(()) } @@ -123,5 +126,5 @@ pub enum StopAction { #[async_trait(?Send)] pub trait BarItem: Send { - async fn start(&self, ctx: Context) -> Result>; + async fn start(&self, ctx: Context) -> Result; } diff --git a/src/dbus/mod.rs b/src/dbus/mod.rs index 965b11b..ed3dac9 100644 --- a/src/dbus/mod.rs +++ b/src/dbus/mod.rs @@ -2,11 +2,11 @@ pub mod dunst; pub mod network_manager; pub mod notifications; -use std::error::Error; - use tokio::sync::OnceCell; use zbus::Connection; +use crate::error::Result; + #[derive(Debug, Copy, Clone)] pub enum BusType { Session, @@ -16,7 +16,7 @@ pub enum BusType { static DBUS_SYSTEM: OnceCell = OnceCell::const_new(); static DBUS_SESSION: OnceCell = OnceCell::const_new(); -pub async fn dbus_connection(bus: BusType) -> Result<&'static Connection, Box> { +pub async fn dbus_connection(bus: BusType) -> Result<&'static Connection> { Ok(match bus { BusType::Session => { DBUS_SESSION diff --git a/src/dispatcher.rs b/src/dispatcher.rs index 2713a95..77df9c6 100644 --- a/src/dispatcher.rs +++ b/src/dispatcher.rs @@ -1,10 +1,9 @@ -use std::error::Error; - use futures::future::join_all; use tokio::sync::mpsc::error::SendError; use tokio::sync::mpsc::Sender; use crate::context::BarEvent; +use crate::error::Result; #[derive(Debug, Clone)] pub struct Dispatcher { @@ -26,7 +25,7 @@ impl Dispatcher { self.inner[idx] = Some(tx); } - pub async fn signal_all(&self) -> Result<(), Box> { + pub async fn signal_all(&self) -> Result<()> { Ok(join_all( self.inner .iter() @@ -42,7 +41,7 @@ impl Dispatcher { })) } - pub async fn send_bar_event(&self, idx: usize, ev: BarEvent) -> Result<(), Box> { + pub async fn send_bar_event(&self, idx: usize, ev: BarEvent) -> Result<()> { match self.inner.get(idx) { Some(Some(tx)) => { // if the channel fills up (the bar never reads click events), since this is a bounded channel diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..0d9675d --- /dev/null +++ b/src/error.rs @@ -0,0 +1,2 @@ +pub type Error = Box; +pub type Result = std::result::Result; diff --git a/src/i3/bar_item.rs b/src/i3/bar_item.rs index c776728..b2ab32e 100644 --- a/src/i3/bar_item.rs +++ b/src/i3/bar_item.rs @@ -1,5 +1,4 @@ use std::collections::HashMap; -use std::error::Error; use async_trait::async_trait; use hex_color::HexColor; @@ -8,6 +7,7 @@ use serde_derive::Deserialize; use serde_json::Value; use crate::context::{BarItem, Context, StopAction}; +use crate::error::Result; #[derive(Debug, Default, Serialize, Deserialize, Copy, Clone, PartialEq, Eq)] #[serde(rename_all = "lowercase")] @@ -44,7 +44,7 @@ pub enum I3MinWidth { } impl Serialize for I3MinWidth { - fn serialize(&self, serializer: S) -> Result + fn serialize(&self, serializer: S) -> std::result::Result where S: serde::Serializer, { @@ -218,7 +218,7 @@ impl I3Item { #[async_trait(?Send)] impl BarItem for I3Item { - async fn start(&self, ctx: Context) -> Result> { + async fn start(&self, ctx: Context) -> Result { ctx.update_item(self.clone()).await?; Ok(StopAction::Complete) } diff --git a/src/i3/ipc.rs b/src/i3/ipc.rs index 94652c2..eb946b1 100644 --- a/src/i3/ipc.rs +++ b/src/i3/ipc.rs @@ -1,16 +1,14 @@ use std::convert::Infallible; -use std::error::Error; use tokio::io::{stdin, AsyncBufReadExt, BufReader}; use super::I3ClickEvent; use crate::context::BarEvent; use crate::dispatcher::Dispatcher; +use crate::error::Result; use crate::util::RcCell; -pub async fn handle_click_events( - dispatcher: RcCell, -) -> Result> { +pub async fn handle_click_events(dispatcher: RcCell) -> Result { let s = BufReader::new(stdin()); let mut lines = s.lines(); loop { diff --git a/src/ipc/client.rs b/src/ipc/client.rs index 7331696..5fb5e7c 100644 --- a/src/ipc/client.rs +++ b/src/ipc/client.rs @@ -1,16 +1,16 @@ -use std::error::Error; use std::io::ErrorKind; use tokio::net::UnixStream; use tokio::sync::oneshot; use crate::context::{BarEvent, CustomResponse}; +use crate::error::Result; use crate::ipc::protocol::{IpcBarEvent, IpcMessage, IpcReply, IpcResult, IPC_HEADER_LEN}; use crate::ipc::server::send_ipc_response; use crate::ipc::IpcContext; use crate::theme::Theme; -pub async fn handle_ipc_client(stream: UnixStream, ctx: IpcContext) -> Result<(), Box> { +pub async fn handle_ipc_client(stream: UnixStream, ctx: IpcContext) -> Result<()> { // first read the length header of the IPC message let mut buf = [0; IPC_HEADER_LEN]; loop { @@ -38,11 +38,7 @@ pub async fn handle_ipc_client(stream: UnixStream, ctx: IpcContext) -> Result<() Ok(()) } -async fn handle_ipc_request( - stream: &UnixStream, - mut ctx: IpcContext, - len: usize, -) -> Result<(), Box> { +async fn handle_ipc_request(stream: &UnixStream, mut ctx: IpcContext, len: usize) -> Result<()> { // read ipc message entirely let mut buf = vec![0; len]; let mut idx = 0; diff --git a/src/ipc/mod.rs b/src/ipc/mod.rs index da17d1d..57f72d4 100644 --- a/src/ipc/mod.rs +++ b/src/ipc/mod.rs @@ -3,7 +3,6 @@ pub mod protocol; mod server; use std::env; -use std::error::Error; use std::path::PathBuf; use tokio_util::sync::CancellationToken; @@ -11,6 +10,7 @@ use tokio_util::sync::CancellationToken; pub use self::server::{create_ipc_socket, handle_ipc_events}; use crate::config::AppConfig; use crate::dispatcher::Dispatcher; +use crate::error::Result; use crate::i3::I3Item; use crate::util::RcCell; @@ -38,7 +38,7 @@ impl IpcContext { } } -pub fn get_socket_path(socket_path: Option<&PathBuf>) -> Result> { +pub fn get_socket_path(socket_path: Option<&PathBuf>) -> Result { socket_path.map_or_else( || { let i3_socket = PathBuf::from(match env::var("I3SOCK") { diff --git a/src/ipc/protocol.rs b/src/ipc/protocol.rs index 2ece093..eacffe8 100644 --- a/src/ipc/protocol.rs +++ b/src/ipc/protocol.rs @@ -1,9 +1,8 @@ -use std::error::Error; - use serde::Serialize; use serde_derive::Deserialize; use serde_json::Value; +use crate::error::Result; use crate::i3::I3ClickEvent; pub const IPC_HEADER_LEN: usize = std::mem::size_of::(); @@ -48,7 +47,7 @@ pub enum IpcResult { Failure(String), } -pub fn encode_ipc_msg(t: T) -> Result, Box> { +pub fn encode_ipc_msg(t: T) -> Result> { let msg = serde_json::to_vec(&t)?; // header is a u64 of length let mut payload = (msg.len() as u64).to_le_bytes().to_vec(); diff --git a/src/ipc/server.rs b/src/ipc/server.rs index 89fa95f..09a5414 100644 --- a/src/ipc/server.rs +++ b/src/ipc/server.rs @@ -1,16 +1,16 @@ use std::convert::Infallible; -use std::error::Error; use std::io::ErrorKind; use tokio::net::{UnixListener, UnixStream}; use super::client::handle_ipc_client; use crate::config::AppConfig; +use crate::error::Result; use crate::ipc::protocol::{encode_ipc_msg, IpcReply}; use crate::ipc::IpcContext; use crate::util::RcCell; -pub async fn create_ipc_socket(config: &RcCell) -> Result> { +pub async fn create_ipc_socket(config: &RcCell) -> Result { let socket_path = config.socket(); // try to remove socket if one exists @@ -23,10 +23,7 @@ pub async fn create_ipc_socket(config: &RcCell) -> Result Result> { +pub async fn handle_ipc_events(listener: UnixListener, ctx: IpcContext) -> Result { loop { match listener.accept().await { Ok((stream, _)) => { @@ -43,7 +40,7 @@ pub async fn handle_ipc_events( } } -pub async fn send_ipc_response(stream: &UnixStream, resp: &IpcReply) -> Result<(), Box> { +pub async fn send_ipc_response(stream: &UnixStream, resp: &IpcReply) -> Result<()> { let data = encode_ipc_msg(resp)?; let mut idx = 0; loop { diff --git a/src/lib.rs b/src/lib.rs index 73de04e..bfce04f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,6 +11,7 @@ pub mod config; pub mod context; pub mod dbus; pub mod dispatcher; +pub mod error; pub mod human_time; pub mod i3; pub mod ipc; diff --git a/src/main.rs b/src/main.rs index 29e2fe1..2a9da59 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,3 @@ -use std::error::Error; use std::process; use clap::Parser; @@ -7,6 +6,7 @@ use istat::cli::Cli; use istat::config::AppConfig; use istat::context::{Context, SharedState, StopAction}; use istat::dispatcher::Dispatcher; +use istat::error::Result; use istat::i3::header::I3BarHeader; use istat::i3::ipc::handle_click_events; use istat::i3::{I3Item, I3Markup}; @@ -32,7 +32,7 @@ fn main() { } } -fn start_runtime() -> Result> { +fn start_runtime() -> Result { pretty_env_logger::try_init()?; let args = Cli::parse(); @@ -50,7 +50,7 @@ fn start_runtime() -> Result> { result } -async fn async_main(args: Cli) -> Result> { +async fn async_main(args: Cli) -> Result { let config = RcCell::new(AppConfig::read(args).await?); // create socket first, so it's ready before anything is written to stdout @@ -85,9 +85,7 @@ async fn async_main(args: Cli) -> Result> { return result; } -fn setup_i3_bar( - config: &RcCell, -) -> Result<(RcCell>, RcCell), Box> { +fn setup_i3_bar(config: &RcCell) -> Result<(RcCell>, RcCell)> { let item_count = config.items.len(); // shared state @@ -195,7 +193,7 @@ fn handle_item_updates( config: RcCell, mut rx: Receiver<(I3Item, usize)>, mut bar: RcCell>, -) -> Result<(), Box> { +) -> Result<()> { // output first parts of the i3 bar protocol - the header println!("{}", serde_json::to_string(&I3BarHeader::default())?); // and the opening bracket for the "infinite array" diff --git a/src/signals.rs b/src/signals.rs index 6bc1666..f72aef8 100644 --- a/src/signals.rs +++ b/src/signals.rs @@ -1,5 +1,4 @@ use std::collections::HashMap; -use std::error::Error; use libc::{SIGRTMAX, SIGRTMIN, SIGTERM}; use signal_hook_tokio::{Handle, Signals}; @@ -7,16 +6,14 @@ use signal_hook_tokio::{Handle, Signals}; use crate::config::AppConfig; use crate::context::BarEvent; use crate::dispatcher::Dispatcher; +use crate::error::Result; use crate::util::RcCell; // NOTE: the `signal_hook` crate isn't designed to be used with realtime signals, because // they may be lost due to its internal buffering, etc. For our use case, I think this is // fine as is, but if not, we may have to use `signal_hook_register` to do it ourselves. // See: https://docs.rs/signal-hook/latest/signal_hook/index.html#limitations -pub fn handle_signals( - config: RcCell, - dispatcher: RcCell, -) -> Result> { +pub fn handle_signals(config: RcCell, dispatcher: RcCell) -> Result { let min = SIGRTMIN(); let max = SIGRTMAX(); let realtime_signals = min..=max; diff --git a/src/test_utils.rs b/src/test_utils.rs index f0583f4..41c8cee 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -13,7 +13,11 @@ pub fn generate_manpage(cmd: Command) { m(&cmd, &dir, None).expect("failed to generate manpage"); } -fn m(cmd: &Command, dir: &Path, parent_name: Option<&str>) -> Result<(), Box> { +fn m( + cmd: &Command, + dir: &Path, + parent_name: Option<&str>, +) -> std::result::Result<(), Box> { let man = Man::new(cmd.clone()); let mut buf = Vec::new(); man.render(&mut buf)?; diff --git a/src/theme.rs b/src/theme.rs index 8ec0929..b26814e 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -1,8 +1,8 @@ -use std::error::Error; - use hex_color::HexColor; use serde_derive::{Deserialize, Serialize}; +use crate::error::Result; + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ColorPair { pub fg: HexColor, @@ -79,7 +79,7 @@ impl Default for Theme { } impl Theme { - pub fn validate(&self) -> Result<(), Box> { + pub fn validate(&self) -> Result<()> { if self.powerline.len() <= 1 { bail!("theme.powerline must contain at least two values"); } diff --git a/src/util/mod.rs b/src/util/mod.rs index 1db28a9..7648d07 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1,13 +1,13 @@ use_and_export!(cell, enum_cycle, exec, format, net, netlink, paginator, vec); -use std::error::Error; - use futures::Future; use tokio::runtime::{Builder, Runtime}; use tokio::task::LocalSet; +use crate::error::Result; + /// Block on a given future, running it on the current thread inside a `LocalSet`. -pub fn local_block_on(f: F) -> Result<(F::Output, Runtime), Box> +pub fn local_block_on(f: F) -> Result<(F::Output, Runtime)> where F: Future, { diff --git a/src/util/net/filter.rs b/src/util/net/filter.rs index 3605d4f..dbd623b 100644 --- a/src/util/net/filter.rs +++ b/src/util/net/filter.rs @@ -1,9 +1,9 @@ -use std::error::Error; use std::str::FromStr; use serde::{de, Deserialize, Serialize}; use super::interface::InterfaceKind; +use crate::error::Error; use crate::util::net::Interface; /// This type is in the format of `interface[:type]`, where `interface` is the interface name, and @@ -51,9 +51,9 @@ impl ToString for InterfaceFilter { } impl FromStr for InterfaceFilter { - type Err = Box; + type Err = Error; - fn from_str(s: &str) -> Result { + fn from_str(s: &str) -> std::result::Result { let d = ':'; if !s.contains(d) { return Ok(InterfaceFilter::new(s, None)); @@ -69,7 +69,7 @@ impl FromStr for InterfaceFilter { } impl Serialize for InterfaceFilter { - fn serialize(&self, serializer: S) -> Result + fn serialize(&self, serializer: S) -> std::result::Result where S: serde::Serializer, { @@ -78,7 +78,7 @@ impl Serialize for InterfaceFilter { } impl<'de> Deserialize<'de> for InterfaceFilter { - fn deserialize(deserializer: D) -> Result + fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { diff --git a/src/util/net/interface.rs b/src/util/net/interface.rs index fdb8010..d56221d 100644 --- a/src/util/net/interface.rs +++ b/src/util/net/interface.rs @@ -1,4 +1,3 @@ -use std::error::Error; use std::net::{SocketAddrV4, SocketAddrV6}; use std::str::FromStr; @@ -6,6 +5,8 @@ use iwlib::{get_wireless_info, WirelessInfo}; use nix::ifaddrs::getifaddrs; use nix::net::if_::InterfaceFlags; +use crate::error::{Error, Result}; + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum InterfaceKind { V4, @@ -22,9 +23,9 @@ impl ToString for InterfaceKind { } impl FromStr for InterfaceKind { - type Err = Box; + type Err = Error; - fn from_str(s: &str) -> Result { + fn from_str(s: &str) -> std::result::Result { match s { "v4" => Ok(Self::V4), "v6" => Ok(Self::V6), @@ -67,7 +68,7 @@ impl Interface { get_wireless_info(&self.name) } - pub fn get_interfaces() -> Result, Box> { + pub fn get_interfaces() -> Result> { let if_addrs = match getifaddrs() { Ok(if_addrs) => if_addrs, Err(e) => bail!("call to `getifaddrs` failed: {}", e), diff --git a/src/util/net/mod.rs b/src/util/net/mod.rs index 13e7eae..51dd287 100644 --- a/src/util/net/mod.rs +++ b/src/util/net/mod.rs @@ -1,7 +1,6 @@ pub mod filter; pub mod interface; -use std::error::Error; use std::ops::Deref; use futures::StreamExt; @@ -11,6 +10,7 @@ use self::filter::InterfaceFilter; pub use self::interface::Interface; use crate::dbus::network_manager::NetworkManagerProxy; use crate::dbus::{dbus_connection, BusType}; +use crate::error::Result; // FIXME: I don't like this interface list thing #[derive(Debug, Clone)] @@ -54,15 +54,15 @@ impl Net { Net { tx, rx } } - pub async fn wait_for_change(&mut self) -> Result> { + pub async fn wait_for_change(&mut self) -> Result { Ok(self.rx.recv().await?) } - pub async fn trigger_update(&self) -> Result<(), Box> { + pub async fn trigger_update(&self) -> Result<()> { Ok(self.tx.send(()).await?) } - pub async fn update_now(&mut self) -> Result> { + pub async fn update_now(&mut self) -> Result { self.trigger_update().await?; self.wait_for_change().await } @@ -77,22 +77,22 @@ impl Clone for Net { } } -pub async fn net_subscribe() -> Result> { +pub async fn net_subscribe() -> Result { Ok(NET_RX.get_or_try_init(start_task).await?.clone()) } -async fn start_task() -> Result> { +async fn start_task() -> Result { let (iface_tx, iface_rx) = broadcast::channel(2); let (manual_tx, manual_rx) = mpsc::channel(1); tokio::task::spawn_local(watch_net_updates(iface_tx, manual_rx)); - Ok::<_, Box>(Net::new(manual_tx, iface_rx)) + Ok(Net::new(manual_tx, iface_rx)) } async fn watch_net_updates( tx: broadcast::Sender, mut rx: mpsc::Receiver<()>, -) -> Result<(), Box> { +) -> Result<()> { // TODO: investigate effort of checking network state with netlink rather than dbus let connection = dbus_connection(BusType::System).await?; let nm = NetworkManagerProxy::new(&connection).await?; diff --git a/src/util/netlink/acpi.rs b/src/util/netlink/acpi.rs index 4ada34b..9176a14 100644 --- a/src/util/netlink/acpi.rs +++ b/src/util/netlink/acpi.rs @@ -1,36 +1,35 @@ -use std::error::Error; - use neli::consts::socket::NlFamily; use neli::router::asynchronous::NlRouter; use neli::utils::Groups; use tokio::sync::OnceCell; use super::ffi::{ACPI_EVENT_FAMILY_NAME, ACPI_EVENT_MCAST_GROUP_NAME}; +use crate::error::{Error, Result}; // (family id, multicast group id) static ACPI_EVENT_IDS: OnceCell<(u16, u32)> = OnceCell::const_new(); -async fn init_ids() -> Result<&'static (u16, u32), Box> { +async fn init_ids() -> Result<&'static (u16, u32)> { Ok(ACPI_EVENT_IDS .get_or_try_init(|| get_acpi_id_from_netlink()) .await?) } -pub async fn event_family_id() -> Result> { +pub async fn event_family_id() -> Result { let (family_id, _) = init_ids().await?; Ok(*family_id) } -pub async fn multicast_group_id() -> Result> { +pub async fn multicast_group_id() -> Result { let (_, multicast_group_id) = init_ids().await?; Ok(*multicast_group_id) } -async fn get_acpi_id_from_netlink() -> Result<(u16, u32), Box> { +async fn get_acpi_id_from_netlink() -> Result<(u16, u32)> { // open netlink socket let (socket, _) = NlRouter::connect(NlFamily::Generic, None, Groups::empty()) .await - .map_err(|e| -> Box { format!("failed to open socket: {}", e).into() })?; + .map_err(|e| -> Error { format!("failed to open socket: {}", e).into() })?; // thanks `neli` - there was so much to do! let family_id = socket.resolve_genl_family(ACPI_EVENT_FAMILY_NAME).await?; diff --git a/src/util/netlink/ffi.rs b/src/util/netlink/ffi.rs index c2f7c94..b33b377 100644 --- a/src/util/netlink/ffi.rs +++ b/src/util/netlink/ffi.rs @@ -1,8 +1,8 @@ -use std::error::Error; - use ::std::os::raw::{c_char, c_uint}; use serde_derive::{Deserialize, Serialize}; +use crate::error::{Error, Result}; + // https://github.com/torvalds/linux/blob/f8dba31b0a826e691949cd4fdfa5c30defaac8c5/drivers/acpi/event.c#L77 pub const ACPI_EVENT_FAMILY_NAME: &str = "acpi_event"; // https://github.com/torvalds/linux/blob/f8dba31b0a826e691949cd4fdfa5c30defaac8c5/drivers/acpi/event.c#L79 @@ -49,24 +49,24 @@ impl AcpiGenericNetlinkEvent { /// Checks a slice of C's chars to ensure they're not signed, needed because C's `char` type could /// be either signed or unsigned unless specified. See: https://stackoverflow.com/a/2054941/5552584 -fn get_u8_bytes(slice: &[c_char]) -> Result, Box> { +fn get_u8_bytes(slice: &[c_char]) -> Result> { slice .into_iter() .take_while(|c| **c != 0) - .map(|c| -> Result> { + .map(|c| -> Result { if *c < 0 { Err(format!("slice contained signed char: {}", c).into()) } else { Ok(*c as u8) } }) - .collect::, _>>() + .collect::>>() } impl<'a> TryFrom<&'a acpi_genl_event> for AcpiGenericNetlinkEvent { - type Error = Box; + type Error = Error; - fn try_from(value: &'a acpi_genl_event) -> Result { + fn try_from(value: &'a acpi_genl_event) -> std::result::Result { Ok(AcpiGenericNetlinkEvent { device_class: String::from_utf8(get_u8_bytes(&value.device_class)?)?, bus_id: String::from_utf8(get_u8_bytes(&value.bus_id)?)?, diff --git a/src/util/netlink/mod.rs b/src/util/netlink/mod.rs index 536d792..b990e38 100644 --- a/src/util/netlink/mod.rs +++ b/src/util/netlink/mod.rs @@ -1,8 +1,6 @@ pub mod acpi; pub mod ffi; -use std::error::Error; - use neli::consts::socket::NlFamily; use neli::err::RouterError; use neli::genl::Genlmsghdr; @@ -12,12 +10,13 @@ use neli::utils::Groups; use tokio::sync::mpsc::{self, Receiver}; use self::ffi::{acpi_genl_event, AcpiAttrType, AcpiGenericNetlinkEvent}; +use crate::error::{Error, Result}; -pub async fn netlink_acpi_listen() -> Result, Box> { +pub async fn netlink_acpi_listen() -> Result> { // open netlink socket let (socket, mut multicast) = NlRouter::connect(NlFamily::Generic, None, Groups::empty()) .await - .map_err(|e| -> Box { format!("failed to open socket: {}", e).into() })?; + .map_err(|e| -> Error { format!("failed to open socket: {}", e).into() })?; // fetch acpi ids let family_id = acpi::event_family_id().await?; @@ -31,7 +30,7 @@ pub async fn netlink_acpi_listen() -> Result, tokio::task::spawn_local(async move { // rust-analyzer has trouble figuring this type out, so we help it here a little type Payload = Genlmsghdr; - type Next = Option, RouterError>>; + type Next = Option, RouterError>>; loop { match multicast.next::().await as Next { From fae11717d61e6001e92ef9851c7dab24005fe7b0 Mon Sep 17 00:00:00 2001 From: acheronfail Date: Sat, 1 Jul 2023 11:29:19 +0930 Subject: [PATCH 07/57] update fakeroot dependency --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- tests/i3/mod.rs | 8 ++++---- tests/util.rs | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f09df51..40d3558 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -616,9 +616,9 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "fakeroot" -version = "0.3.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0e51568d2ffb1a39dedb2c73fce7cc90244bdf531f01e0a3a1cab13d748ca72" +checksum = "25f6b8b411dcb5303e9cc5514bd09f2f9b301a196def84ead194b379de5e3b18" dependencies = [ "libc", "redhook", diff --git a/Cargo.toml b/Cargo.toml index ecc7b7c..e48830b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,7 +68,7 @@ path = "tests/mod.rs" automod = "1.0.8" clap_mangen = "0.2.11" clap = { version = "4.2.7", features = ["derive"] } -fakeroot = "0.3.0" +fakeroot = "0.4.1" rand = "0.8.5" timeout-readwrite = "0.3.3" xcb = { version = "1.2.1", features = ["xkb", "xtest"] } diff --git a/tests/i3/mod.rs b/tests/i3/mod.rs index 865382a..5e17e47 100644 --- a/tests/i3/mod.rs +++ b/tests/i3/mod.rs @@ -135,8 +135,8 @@ impl<'a> X11Test<'a> { format!("{}:{}", get_faketime_lib(), get_fakeroot_lib()), ) .env("FAKETIME", format!("@{}", FAKE_TIME)) - .env("FAKE_ROOT", &test.fake_root) - .env("FAKE_DIRS", "1") + .env("FAKEROOT", &test.fakeroot) + .env("FAKEROOT_DIRS", "1") // setup logs .env("RUST_LOG", "istat=trace") // spawn in nested X server @@ -179,8 +179,8 @@ impl<'a> X11Test<'a> { .env("I3SOCK", &self.i3_socket) .env("DISPLAY", &self.x_display) .env("LD_PRELOAD", get_fakeroot_lib()) - .env("FAKE_ROOT", &self.test.fake_root) - .env("FAKE_DIRS", "1") + .env("FAKEROOT", &self.test.fakeroot) + .env("FAKEROOT_DIRS", "1") .arg("-c") .arg(format!("{}", cmd.as_ref())) .output() diff --git a/tests/util.rs b/tests/util.rs index 3613cd4..2a2219b 100644 --- a/tests/util.rs +++ b/tests/util.rs @@ -161,7 +161,7 @@ pub struct Test { pub env: HashMap, pub dir: PathBuf, pub bin_dir: PathBuf, - pub fake_root: PathBuf, + pub fakeroot: PathBuf, pub istat_socket_file: PathBuf, pub istat_config_file: PathBuf, } @@ -200,7 +200,7 @@ impl Test { dir, env, bin_dir, - fake_root, + fakeroot: fake_root, istat_config_file: config_file, istat_socket_file: socket_file, } @@ -224,7 +224,7 @@ impl Test { name }; - let path = self.fake_root.join(name); + let path = self.fakeroot.join(name); fs::create_dir_all(path.parent().unwrap()).unwrap(); let mut file = File::create(&path).unwrap(); From fae118c8fa853d457697c71e7e4ea36e036519fb Mon Sep 17 00:00:00 2001 From: acheronfail Date: Sat, 1 Jul 2023 19:51:49 +0930 Subject: [PATCH 08/57] refactor netlink module --- justfile | 4 +- src/bar_items/battery.rs | 2 +- src/util/netlink/acpi.rs | 60 ------------ src/util/netlink/{ => acpi}/ffi.rs | 0 src/util/netlink/acpi/mod.rs | 143 +++++++++++++++++++++++++++++ src/util/netlink/mod.rs | 76 +-------------- 6 files changed, 147 insertions(+), 138 deletions(-) delete mode 100644 src/util/netlink/acpi.rs rename src/util/netlink/{ => acpi}/ffi.rs (100%) create mode 100644 src/util/netlink/acpi/mod.rs diff --git a/justfile b/justfile index 09fcb6f..058eb8d 100644 --- a/justfile +++ b/justfile @@ -52,8 +52,8 @@ debug dimensions="3800x200": _lbuild env -u I3SOCK DISPLAY=:1.0 i3-with-shmlog --config ./scripts/i3.conf # run tests in a nested dbus session so the host session isn't affected -test: - dbus-run-session -- env ISTAT_TEST=1 cargo test --all +test *args: + dbus-run-session -- env RUST_LOG=trace ISTAT_TEST=1 cargo test --all "$@" # `eval` this for an easy debug loop for screenshot tests # NOTE: requires `fd` be present, and the terminal is `kitty` diff --git a/src/bar_items/battery.rs b/src/bar_items/battery.rs index 745ae87..9d266d9 100644 --- a/src/bar_items/battery.rs +++ b/src/bar_items/battery.rs @@ -15,7 +15,7 @@ use crate::dbus::{dbus_connection, BusType}; use crate::error::Result; use crate::i3::{I3Button, I3Item, I3Markup}; use crate::theme::Theme; -use crate::util::ffi::AcpiGenericNetlinkEvent; +use crate::util::acpi::ffi::AcpiGenericNetlinkEvent; use crate::util::{netlink_acpi_listen, Paginator}; enum BatState { diff --git a/src/util/netlink/acpi.rs b/src/util/netlink/acpi.rs deleted file mode 100644 index 9176a14..0000000 --- a/src/util/netlink/acpi.rs +++ /dev/null @@ -1,60 +0,0 @@ -use neli::consts::socket::NlFamily; -use neli::router::asynchronous::NlRouter; -use neli::utils::Groups; -use tokio::sync::OnceCell; - -use super::ffi::{ACPI_EVENT_FAMILY_NAME, ACPI_EVENT_MCAST_GROUP_NAME}; -use crate::error::{Error, Result}; - -// (family id, multicast group id) -static ACPI_EVENT_IDS: OnceCell<(u16, u32)> = OnceCell::const_new(); - -async fn init_ids() -> Result<&'static (u16, u32)> { - Ok(ACPI_EVENT_IDS - .get_or_try_init(|| get_acpi_id_from_netlink()) - .await?) -} - -pub async fn event_family_id() -> Result { - let (family_id, _) = init_ids().await?; - Ok(*family_id) -} - -pub async fn multicast_group_id() -> Result { - let (_, multicast_group_id) = init_ids().await?; - Ok(*multicast_group_id) -} - -async fn get_acpi_id_from_netlink() -> Result<(u16, u32)> { - // open netlink socket - let (socket, _) = NlRouter::connect(NlFamily::Generic, None, Groups::empty()) - .await - .map_err(|e| -> Error { format!("failed to open socket: {}", e).into() })?; - - // thanks `neli` - there was so much to do! - let family_id = socket.resolve_genl_family(ACPI_EVENT_FAMILY_NAME).await?; - let multicast_group = socket - .resolve_nl_mcast_group(ACPI_EVENT_FAMILY_NAME, ACPI_EVENT_MCAST_GROUP_NAME) - .await?; - - Ok((family_id, multicast_group)) -} - -#[cfg(test)] -mod test { - use super::*; - use crate::util::local_block_on; - - #[test] - fn it_works() { - local_block_on(async { - assert!(event_family_id().await.unwrap() > 0); - }) - .unwrap(); - - local_block_on(async { - assert!(multicast_group_id().await.unwrap() > 0); - }) - .unwrap(); - } -} diff --git a/src/util/netlink/ffi.rs b/src/util/netlink/acpi/ffi.rs similarity index 100% rename from src/util/netlink/ffi.rs rename to src/util/netlink/acpi/ffi.rs diff --git a/src/util/netlink/acpi/mod.rs b/src/util/netlink/acpi/mod.rs new file mode 100644 index 0000000..34c78e1 --- /dev/null +++ b/src/util/netlink/acpi/mod.rs @@ -0,0 +1,143 @@ +pub mod ffi; + +use neli::consts::socket::NlFamily; +use neli::err::RouterError; +use neli::genl::Genlmsghdr; +use neli::nl::Nlmsghdr; +use neli::router::asynchronous::NlRouter; +use neli::utils::Groups; +use tokio::sync::mpsc::{self, Receiver}; +use tokio::sync::OnceCell; + +use self::ffi::{ + acpi_genl_event, + AcpiAttrType, + AcpiGenericNetlinkEvent, + ACPI_EVENT_FAMILY_NAME, + ACPI_EVENT_MCAST_GROUP_NAME, +}; +use crate::error::{Error, Result}; + +// public ---------------------------------------------------------------------- + +pub async fn netlink_acpi_listen() -> Result> { + // open netlink socket + let (socket, mut multicast) = NlRouter::connect(NlFamily::Generic, None, Groups::empty()) + .await + .map_err(|e| -> Error { format!("failed to open socket: {}", e).into() })?; + + // fetch acpi ids + let family_id = event_family_id().await?; + let multicast_group_id = multicast_group_id().await?; + + // subscribe to multicast events for acpi + socket.add_mcast_membership(Groups::new_groups(&[multicast_group_id]))?; + + // spawn task to listen and respond to acpi events + let (tx, rx) = mpsc::channel(8); + tokio::task::spawn_local(async move { + // rust-analyzer has trouble figuring this type out, so we help it here a little + type Payload = Genlmsghdr; + type Next = Option, RouterError>>; + + loop { + match multicast.next::().await as Next { + None => break, + Some(response) => match response { + Err(e) => log::error!("error receiving netlink msg: {}", e), + Ok(nl_msg) => { + // skip this message if it's not part of the apci family + if *nl_msg.nl_type() != family_id { + continue; + } + + // if it is, then decode it + if let Some(payload) = nl_msg.get_payload() { + let attrs = payload.attrs().get_attr_handle(); + if let Some(attr) = attrs.get_attribute(AcpiAttrType::Event as u16) { + // cast the attribute payload into its type + let raw = attr.nla_payload().as_ref().as_ptr(); + let event = unsafe { &*(raw as *const acpi_genl_event) }; + + // if there was an error, stop listening and exit + match event.try_into() { + Ok(event) => { + if let Err(e) = tx.send(event).await { + log::error!("failed to send acpi event: {}", e); + break; + }; + } + Err(e) => log::error!("failed to parse event data: {}", e), + } + } + } + } + }, + } + } + + // move the socket into here so it's not dropped earlier than expected + drop(socket); + log::error!("unexpected end of netlink stream") + }); + + Ok(rx) +} + +// internal -------------------------------------------------------------------- + +// (family id, multicast group id) +static ACPI_EVENT_IDS: OnceCell<(u16, u32)> = OnceCell::const_new(); + +/// Initialises local cache of the required ACPI netlink ids +/// You can see these with the tool `genl-ctrl-list`. +async fn init_ids() -> Result<&'static (u16, u32)> { + Ok(ACPI_EVENT_IDS + .get_or_try_init(get_acpi_id_from_netlink) + .await?) +} + +async fn event_family_id() -> Result { + let (family_id, _) = init_ids().await?; + Ok(*family_id) +} + +async fn multicast_group_id() -> Result { + let (_, multicast_group_id) = init_ids().await?; + Ok(*multicast_group_id) +} + +/// Use `netlink(3)` to get the right ACPI ids +async fn get_acpi_id_from_netlink() -> Result<(u16, u32)> { + // open netlink socket + let (socket, _) = NlRouter::connect(NlFamily::Generic, None, Groups::empty()) + .await + .map_err(|e| -> Error { format!("failed to open socket: {}", e).into() })?; + + // thanks `neli` - there was so much to do! + let family_id = socket.resolve_genl_family(ACPI_EVENT_FAMILY_NAME).await?; + let multicast_group = socket + .resolve_nl_mcast_group(ACPI_EVENT_FAMILY_NAME, ACPI_EVENT_MCAST_GROUP_NAME) + .await?; + + Ok((family_id, multicast_group)) +} + +#[cfg(test)] +mod test { + use super::*; + use crate::util::local_block_on; + + #[test] + fn it_works() { + local_block_on(async { + assert!(event_family_id().await.unwrap() > 0); + }) + .unwrap(); + + local_block_on(async { + assert!(multicast_group_id().await.unwrap() > 0); + }) + .unwrap(); + } +} diff --git a/src/util/netlink/mod.rs b/src/util/netlink/mod.rs index b990e38..2c35340 100644 --- a/src/util/netlink/mod.rs +++ b/src/util/netlink/mod.rs @@ -1,77 +1,3 @@ pub mod acpi; -pub mod ffi; -use neli::consts::socket::NlFamily; -use neli::err::RouterError; -use neli::genl::Genlmsghdr; -use neli::nl::Nlmsghdr; -use neli::router::asynchronous::NlRouter; -use neli::utils::Groups; -use tokio::sync::mpsc::{self, Receiver}; - -use self::ffi::{acpi_genl_event, AcpiAttrType, AcpiGenericNetlinkEvent}; -use crate::error::{Error, Result}; - -pub async fn netlink_acpi_listen() -> Result> { - // open netlink socket - let (socket, mut multicast) = NlRouter::connect(NlFamily::Generic, None, Groups::empty()) - .await - .map_err(|e| -> Error { format!("failed to open socket: {}", e).into() })?; - - // fetch acpi ids - let family_id = acpi::event_family_id().await?; - let multicast_group_id = acpi::multicast_group_id().await?; - - // subscribe to multicast events for acpi - socket.add_mcast_membership(Groups::new_groups(&[multicast_group_id]))?; - - // spawn task to listen and respond to acpi events - let (tx, rx) = mpsc::channel(8); - tokio::task::spawn_local(async move { - // rust-analyzer has trouble figuring this type out, so we help it here a little - type Payload = Genlmsghdr; - type Next = Option, RouterError>>; - - loop { - match multicast.next::().await as Next { - None => break, - Some(response) => match response { - Err(e) => log::error!("error receiving netlink msg: {}", e), - Ok(nl_msg) => { - // skip this message if it's not part of the apci family - if *nl_msg.nl_type() != family_id { - continue; - } - - // if it is, then decode it - if let Some(payload) = nl_msg.get_payload() { - let attrs = payload.attrs().get_attr_handle(); - if let Some(attr) = attrs.get_attribute(AcpiAttrType::Event as u16) { - // cast the attribute payload into its type - let raw = attr.nla_payload().as_ref().as_ptr(); - let event = unsafe { &*(raw as *const acpi_genl_event) }; - - // if there was an error, stop listening and exit - match event.try_into() { - Ok(event) => { - if let Err(e) = tx.send(event).await { - log::error!("failed to send acpi event: {}", e); - break; - }; - } - Err(e) => log::error!("failed to parse event data: {}", e), - } - } - } - } - }, - } - } - - // move the socket into here so it's not dropped earlier than expected - drop(socket); - log::error!("unexpected end of netlink stream") - }); - - Ok(rx) -} +pub use acpi::netlink_acpi_listen; From fae1ef3bf0b53cd80dcfbd8f51d60fb22fe45e0b Mon Sep 17 00:00:00 2001 From: acheronfail Date: Mon, 3 Jul 2023 23:13:02 +0930 Subject: [PATCH 09/57] wip: netlink route --- Cargo.lock | 6 +- Cargo.toml | 3 +- justfile | 2 +- src/util/netlink/mod.rs | 1 + src/util/netlink/route.rs | 260 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 266 insertions(+), 6 deletions(-) create mode 100644 src/util/netlink/route.rs diff --git a/Cargo.lock b/Cargo.lock index 40d3558..c141238 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1155,8 +1155,7 @@ dependencies = [ [[package]] name = "neli" version = "0.7.0-rc1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e71ed00191ea664d265711b188bf31ad10b44682b66a1ce314c25ddd0620fa4b" +source = "git+https://github.com/jbaublitz/neli?branch=v0.7.0-rc2#86a0c7a8fdd6db3b19d4971ab58f0d445ca327b5" dependencies = [ "bitflags", "byteorder", @@ -1172,8 +1171,7 @@ dependencies = [ [[package]] name = "neli-proc-macros" version = "0.2.0-rc1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b808ba6686c6357149970326b3494d980dc856260ce35796c85600a112025269" +source = "git+https://github.com/jbaublitz/neli?branch=v0.7.0-rc2#86a0c7a8fdd6db3b19d4971ab58f0d445ca327b5" dependencies = [ "either", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index e48830b..63928a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,7 +42,8 @@ libc = "0.2.142" libpulse-binding = { version = "2.27.1", features = ["pa_v14"] } libpulse-tokio = "0.1.0" log = "0.4.17" -neli = { version = "0.7.0-rc1", features = ["tokio", "async"] } +# FIXME: use crate version once it's published +neli = { git = "https://github.com/jbaublitz/neli", branch = "v0.7.0-rc2", features = ["tokio", "async"] } nix = { version = "0.26.2", features = ["net"] } num-traits = "0.2.15" paste = "1.0.12" diff --git a/justfile b/justfile index 058eb8d..4780e06 100644 --- a/justfile +++ b/justfile @@ -53,7 +53,7 @@ debug dimensions="3800x200": _lbuild # run tests in a nested dbus session so the host session isn't affected test *args: - dbus-run-session -- env RUST_LOG=trace ISTAT_TEST=1 cargo test --all "$@" + dbus-run-session -- env RUST_LOG=istat=trace ISTAT_TEST=1 cargo test --all "$@" # `eval` this for an easy debug loop for screenshot tests # NOTE: requires `fd` be present, and the terminal is `kitty` diff --git a/src/util/netlink/mod.rs b/src/util/netlink/mod.rs index 2c35340..5e4134a 100644 --- a/src/util/netlink/mod.rs +++ b/src/util/netlink/mod.rs @@ -1,3 +1,4 @@ pub mod acpi; +pub mod route; pub use acpi::netlink_acpi_listen; diff --git a/src/util/netlink/route.rs b/src/util/netlink/route.rs new file mode 100644 index 0000000..ce40f1c --- /dev/null +++ b/src/util/netlink/route.rs @@ -0,0 +1,260 @@ +//! TODO: get notified of network change events from netlink rather than dbus + network manager +//! +//! TODO: another, separate (generic) listener for nl80211 events? +//! +//! Useful things: +//! `genl-ctrl-list` returns generic families +//! `ip a add 10.0.0.254 dev wlan0 && sleep 1 && ip a del 10.0.0.254/32 dev wlan0` +//! `ip -6 addr add 2001:0db8:0:f101::1/64 dev wlan1 && sleep 1 && ip -6 addr del 2001:0db8:0:f101::1/64 dev wlan1` + +use std::collections::{HashMap, HashSet}; +use std::convert::Infallible; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +use std::rc::Rc; + +use libc::{RTNLGRP_IPV4_IFADDR, RTNLGRP_IPV6_IFADDR}; +use neli::consts::nl::NlmF; +use neli::consts::rtnl::{Arphrd, Ifa, Ifla, RtAddrFamily, RtScope, Rtm}; +use neli::consts::socket::NlFamily; +use neli::err::RouterError; +use neli::genl::Genlmsghdr; +use neli::nl::{NlPayload, Nlmsghdr}; +use neli::router::asynchronous::{NlRouter, NlRouterReceiverHandle}; +use neli::rtnl::{Ifaddrmsg, IfaddrmsgBuilder, Ifinfomsg, IfinfomsgBuilder}; +use neli::utils::Groups; +use tokio::sync::mpsc::{self, Receiver, Sender}; + +use crate::error::Result; + +pub type InterfaceUpdate = HashMap; + +// TODO: move this file elsewhere (util::net ?) since it's not really netlink specific +// TODO: genl nl80211 checks to see if this interface is wireless? +remove iwlib dep? +// - http://lists.infradead.org/pipermail/hostap/2004-March/006231.html +// - https://blog.onethinglab.com/how-to-check-if-wireless-adapter-supports-monitor-mode/ +#[derive(Debug, Clone)] +pub struct InterfaceInfo { + pub name: Rc, + pub mac_address: Option<[u8; 6]>, + pub ip_addresses: HashSet, +} + +pub async fn netlink_ipaddr_listen() -> Result> { + // setup socket for netlink route + let (socket, multicast) = NlRouter::connect(NlFamily::Route, None, Groups::empty()).await?; + + // enable strict checking + // https://docs.kernel.org/userspace-api/netlink/intro.html#strict-checking + socket.enable_strict_checking(true)?; + + // add multicast membership for ipv4-addr updates + socket + .add_mcast_membership(Groups::new_groups(&[ + RTNLGRP_IPV4_IFADDR, + RTNLGRP_IPV6_IFADDR, + ])) + .unwrap(); + + let (tx, rx) = mpsc::channel(8); + tokio::task::spawn_local(async move { + if let Err(e) = handle_netlink_route_messages(&socket, multicast, tx).await { + log::error!("fatal error handling netlink route messages: {}", e); + } + + // make sure socket is kept alive while we're reading messages + drop(socket); + }); + + Ok(rx) +} + +async fn handle_netlink_route_messages( + socket: &NlRouter, + mut multicast: NlRouterReceiverHandle>, + tx: Sender, +) -> Result { + // listen for multicast events + type Next = Option, RouterError>>; + loop { + match multicast.next().await as Next { + None => bail!("Unexpected end of netlink route stream"), + // we got a multicast event + Some(response) => { + // check we got a message (not an error) + let response = match response { + Ok(response) => response, + Err(e) => { + log::error!("error receiving netlink message: {}", e); + continue; + } + }; + + // check we have a payload + match response.nl_payload() { + // parse payload and send event + NlPayload::Payload(_ifaddrmsg) => { + // request all interfaces from netlink again - we request it each time because we get ifaddrmsg + // events even when the address is deleted (but we can't tell that is was deleted) + // TODO: in the future it would be nice to only update the interface which emitted the event + tx.send(get_all_interfaces(&socket).await?).await? + } + // not payload, something is wrong + payload => { + log::error!("unexpected nl message payload type: {:?}", payload); + continue; + } + } + } + } + } +} + +/// Request all interfaces with their addresses from rtnetlink(7) +async fn get_all_interfaces(socket: &NlRouter) -> Result> { + let mut interface_map = HashMap::::new(); + + // first, get all the interfaces: we need this for the interface names + { + let ifinfomsg = IfinfomsgBuilder::default() + // this is layer 2, so family is unspecified + .ifi_family(RtAddrFamily::Unspecified) + .ifi_type(Arphrd::Netrom) + // when index is zero, it fetches them all + .ifi_index(0) + .build()?; + + let mut recv = socket + .send::( + Rtm::Getlink, + NlmF::REQUEST | NlmF::DUMP | NlmF::ACK, + NlPayload::Payload(ifinfomsg), + ) + .await?; + + type Next = + Option, RouterError>>; + while let Some(response) = recv.next().await as Next { + let header = match response { + Ok(header) => header, + Err(e) => bail!("an error occurred receiving rtnetlink message: {}", e), + }; + + if let NlPayload::Payload(ifinfomsg) = header.nl_payload() { + dbg!(ifinfomsg); + // handle to the attributes of this message + let attr_handle = ifinfomsg.rtattrs().get_attr_handle(); + + // extract interface name + let mut interface_info = InterfaceInfo { + name: match attr_handle.get_attr_payload_as_with_len::(Ifla::Ifname) { + Ok(interface) => interface.into(), + Err(e) => { + log::error!( + "failed to parse interface from ifinfomsg: {} :: {:?}", + e, + ifinfomsg + ); + continue; + } + }, + mac_address: None, + ip_addresses: HashSet::new(), + }; + + // extract mac address if set + if let Ok(bytes) = + attr_handle.get_attr_payload_as_with_len_borrowed::<&[u8]>(Ifla::Address) + { + if let Ok(array) = bytes.try_into() { + interface_info.mac_address = Some(array); + } + } + + interface_map.insert(*ifinfomsg.ifi_index(), interface_info); + } + } + } + + // ... next, get v4 & v6 addresses of each interface + { + for family in [RtAddrFamily::Inet, RtAddrFamily::Inet6] { + let ifaddrmsg = IfaddrmsgBuilder::default() + .ifa_family(family) + .ifa_index(0) + .ifa_prefixlen(0) + .ifa_scope(RtScope::Universe) + .build()?; + + let mut recv = socket + .send::( + Rtm::Getaddr, + NlmF::REQUEST | NlmF::DUMP | NlmF::ACK, + NlPayload::Payload(ifaddrmsg), + ) + .await?; + + type Next = + Option, RouterError>>; + while let Some(response) = recv.next().await as Next { + let header = match response { + Ok(header) => header, + Err(e) => bail!("an error occurred receiving rtnetlink message: {}", e), + }; + + if let NlPayload::Payload(ifaddrmsg) = header.nl_payload() { + match interface_map.get_mut(ifaddrmsg.ifa_index()) { + Some(if_info) => { + // handle to the attributes of this message + let attr_handle = ifaddrmsg.rtattrs().get_attr_handle(); + + // extract address + match ifaddrmsg.ifa_family() { + RtAddrFamily::Inet => { + if let Ok(addr) = + attr_handle.get_attr_payload_as::(Ifa::Address) + { + if_info + .ip_addresses + .insert(IpAddr::V4(Ipv4Addr::from(u32::from_be(addr)))); + } + } + RtAddrFamily::Inet6 => { + if let Ok(addr) = + attr_handle.get_attr_payload_as::(Ifa::Address) + { + if_info.ip_addresses.insert(IpAddr::V6(Ipv6Addr::from( + u128::from_be(addr), + ))); + } + } + _ => { + continue; + } + } + } + None => { + log::error!( + "received ifaddrmsg for unknown interface: {:?}", + ifaddrmsg + ); + continue; + } + } + } + } + } + } + + Ok(interface_map) +} + +#[test] +fn asdf() { + crate::util::local_block_on(async { + let mut rx = netlink_ipaddr_listen().await.unwrap(); + while let Some(ev) = rx.recv().await { + dbg!(ev); + } + }) + .unwrap(); +} From fae149769486ea4453188e35f75ca1d2fe790cf0 Mon Sep 17 00:00:00 2001 From: acheronfail Date: Wed, 5 Jul 2023 22:35:26 +0930 Subject: [PATCH 10/57] wip: netlink nl80211 --- src/util/netlink/mod.rs | 77 ++ src/util/netlink/nl80211/enums.rs | 1301 +++++++++++++++++++++++++++++ src/util/netlink/nl80211/mod.rs | 374 +++++++++ src/util/netlink/route.rs | 52 +- 4 files changed, 1768 insertions(+), 36 deletions(-) create mode 100644 src/util/netlink/nl80211/enums.rs create mode 100644 src/util/netlink/nl80211/mod.rs diff --git a/src/util/netlink/mod.rs b/src/util/netlink/mod.rs index 5e4134a..1b81ec3 100644 --- a/src/util/netlink/mod.rs +++ b/src/util/netlink/mod.rs @@ -1,4 +1,81 @@ pub mod acpi; +pub mod nl80211; pub mod route; +use std::array::TryFromSliceError; +use std::collections::HashSet; +use std::fmt::Debug; +use std::net::IpAddr; +use std::rc::Rc; + pub use acpi::netlink_acpi_listen; + +#[derive(Clone)] +pub struct MacAddr { + octets: [u8; 6], +} + +impl Debug for MacAddr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!( + "MacAddr({})", + self.octets.map(|o| format!("{:2x}", o)).join(":") + )) + } +} + +impl From<&MacAddr> for neli::types::Buffer { + fn from(value: &MacAddr) -> Self { + Self::from(&value.octets[..]) + } +} + +impl From<&[u8; 6]> for MacAddr { + fn from(value: &[u8; 6]) -> Self { + MacAddr { octets: *value } + } +} + +impl TryFrom> for MacAddr { + type Error = TryFromSliceError; + + fn try_from(value: Vec) -> Result { + let octets: &[u8; 6] = (&value[..]).try_into()?; + Ok(MacAddr { octets: *octets }) + } +} + +impl TryFrom<&[u8]> for MacAddr { + type Error = TryFromSliceError; + + fn try_from(value: &[u8]) -> Result { + let octets: &[u8; 6] = value.try_into()?; + Ok(MacAddr { octets: *octets }) + } +} + +impl TryFrom<&str> for MacAddr { + type Error = crate::error::Error; + + fn try_from(value: &str) -> Result { + let parts = value.split(':').collect::>(); + if parts.len() != 6 { + bail!("expected 6 parts"); + } + + let parts = parts + .into_iter() + .map(|s| u8::from_str_radix(s, 16)) + .collect::, _>>()?; + + Ok(parts.try_into()?) + } +} + +#[derive(Debug, Clone)] +pub struct NetlinkInterface { + pub index: i32, + pub name: Rc, + pub mac_address: Option, + pub ip_addresses: HashSet, +} diff --git a/src/util/netlink/nl80211/enums.rs b/src/util/netlink/nl80211/enums.rs new file mode 100644 index 0000000..27b02c6 --- /dev/null +++ b/src/util/netlink/nl80211/enums.rs @@ -0,0 +1,1301 @@ +// see: https://github.com/jbaublitz/neli/blob/v0.7.0-rc2/examples/nl80211.rs +// and: `/usr/include/linux/nl80211.h` + +#[neli::neli_enum(serialized_type = "u8")] +pub enum Nl80211Command { + /// unspecified command to catch errors + Unspec = 0, + /// request information about a wiphy or dump request + /// to get a list of all present wiphys. + GetWiphy = 1, + /// set wiphy parameters, needs %NL80211_ATTR_WIPHY or + /// %NL80211_ATTR_IFINDEX; can be used to set %NL80211_ATTR_WIPHY_NAME, + /// %NL80211_ATTR_WIPHY_TXQ_PARAMS, %NL80211_ATTR_WIPHY_FREQ (and the + /// attributes determining the channel width; this is used for setting + /// monitor mode channel), %NL80211_ATTR_WIPHY_RETRY_SHORT, + /// %NL80211_ATTR_WIPHY_RETRY_LONG, %NL80211_ATTR_WIPHY_FRAG_THRESHOLD, + /// and/or %NL80211_ATTR_WIPHY_RTS_THRESHOLD. + /// However, for setting the channel, see %NL80211_CMD_SET_CHANNEL + /// instead, the support here is for backward compatibility only. + SetWiphy = 2, + /// Newly created wiphy, response to get request + /// or rename notification. Has attributes %NL80211_ATTR_WIPHY and + /// %NL80211_ATTR_WIPHY_NAME. + NewWiphy = 3, + /// Wiphy deleted. Has attributes + /// %NL80211_ATTR_WIPHY and %NL80211_ATTR_WIPHY_NAME. + DelWiphy = 4, + + /// Request an interface's configuration; + /// either a dump request for all interfaces or a specific get with a + /// single %NL80211_ATTR_IFINDEX is supported. + GetInterface = 5, + /// Set type of a virtual interface, requires + /// %NL80211_ATTR_IFINDEX and %NL80211_ATTR_IFTYPE. + SetInterface = 6, + /// Newly created virtual interface or response + /// to %NL80211_CMD_GET_INTERFACE. Has %NL80211_ATTR_IFINDEX, + /// %NL80211_ATTR_WIPHY and %NL80211_ATTR_IFTYPE attributes. Can also + /// be sent from userspace to request creation of a new virtual interface, + /// then requires attributes %NL80211_ATTR_WIPHY, %NL80211_ATTR_IFTYPE and + /// %NL80211_ATTR_IFNAME. + NewInterface = 7, + /// Virtual interface was deleted, has attributes + /// %NL80211_ATTR_IFINDEX and %NL80211_ATTR_WIPHY. Can also be sent from + /// userspace to request deletion of a virtual interface, then requires + /// attribute %NL80211_ATTR_IFINDEX. + DelInterface = 8, + + /// Get sequence counter information for a key specified + /// by %NL80211_ATTR_KEY_IDX and/or %NL80211_ATTR_MAC. + GetKey = 9, + /// Set key attributes %NL80211_ATTR_KEY_DEFAULT, + /// %NL80211_ATTR_KEY_DEFAULT_MGMT, or %NL80211_ATTR_KEY_THRESHOLD. + SetKey = 10, + /// add a key with given %NL80211_ATTR_KEY_DATA, + /// %NL80211_ATTR_KEY_IDX, %NL80211_ATTR_MAC, %NL80211_ATTR_KEY_CIPHER, + /// and %NL80211_ATTR_KEY_SEQ attributes. + NewKey = 11, + /// delete a key identified by %NL80211_ATTR_KEY_IDX + /// or %NL80211_ATTR_MAC. + DelKey = 12, + + /// (not used) + GetBeacon = 13, + /// change the beacon on an access point interface + /// using the %NL80211_ATTR_BEACON_HEAD and %NL80211_ATTR_BEACON_TAIL + /// attributes. For drivers that generate the beacon and probe responses + /// internally, the following attributes must be provided: %NL80211_ATTR_IE, + /// %NL80211_ATTR_IE_PROBE_RESP and %NL80211_ATTR_IE_ASSOC_RESP. + SetBeacon = 14, + /// Start AP operation on an AP interface, parameters + /// are like for %NL80211_CMD_SET_BEACON, and additionally parameters that + /// do not change are used, these include %NL80211_ATTR_BEACON_INTERVAL, + /// %NL80211_ATTR_DTIM_PERIOD, %NL80211_ATTR_SSID, + /// %NL80211_ATTR_HIDDEN_SSID, %NL80211_ATTR_CIPHERS_PAIRWISE, + /// %NL80211_ATTR_CIPHER_GROUP, %NL80211_ATTR_WPA_VERSIONS, + /// %NL80211_ATTR_AKM_SUITES, %NL80211_ATTR_PRIVACY, + /// %NL80211_ATTR_AUTH_TYPE, %NL80211_ATTR_INACTIVITY_TIMEOUT, + /// %NL80211_ATTR_ACL_POLICY and %NL80211_ATTR_MAC_ADDRS. + /// The channel to use can be set on the interface or be given using the + /// %NL80211_ATTR_WIPHY_FREQ and the attributes determining channel width. + StartAp = 15, + /// old alias for %NL80211_CMD_START_AP + NewBeacon = 15, + /// Stop AP operation on the given interface + StopAp = 16, + /// old alias for %NL80211_CMD_STOP_AP + DelBeacon = 16, + + /// Get station attributes for station identified by + /// %NL80211_ATTR_MAC on the interface identified by %NL80211_ATTR_IFINDEX. + GetStation = 17, + /// Set station attributes for station identified by + /// %NL80211_ATTR_MAC on the interface identified by %NL80211_ATTR_IFINDEX. + SetStation = 18, + /// Add a station with given attributes to the + /// the interface identified by %NL80211_ATTR_IFINDEX. + NewStation = 19, + /// Remove a station identified by %NL80211_ATTR_MAC + /// or, if no MAC address given, all stations, on the interface identified + /// by %NL80211_ATTR_IFINDEX. %NL80211_ATTR_MGMT_SUBTYPE and + /// %NL80211_ATTR_REASON_CODE can optionally be used to specify which type + /// of disconnection indication should be sent to the station + /// (Deauthentication or Disassociation frame and reason code for that + /// frame). + DelStation = 20, + + /// Get mesh path attributes for mesh path to + /// destination %NL80211_ATTR_MAC on the interface identified by + /// %NL80211_ATTR_IFINDEX. + GetMpath = 21, + /// Set mesh path attributes for mesh path to + /// destination %NL80211_ATTR_MAC on the interface identified by + /// %NL80211_ATTR_IFINDEX. + SetMpath = 22, + /// Create a new mesh path for the destination given by + /// %NL80211_ATTR_MAC via %NL80211_ATTR_MPATH_NEXT_HOP. + NewMpath = 23, + /// Delete a mesh path to the destination given by + /// %NL80211_ATTR_MAC. + DelMpath = 24, + + /// Set BSS attributes for BSS identified by + /// %NL80211_ATTR_IFINDEX. + SetBss = 25, + + /// Set current regulatory domain. CRDA sends this command + /// after being queried by the kernel. CRDA replies by sending a regulatory + /// domain structure which consists of %NL80211_ATTR_REG_ALPHA set to our + /// current alpha2 if it found a match. It also provides + /// NL80211_ATTR_REG_RULE_FLAGS, and a set of regulatory rules. Each + /// regulatory rule is a nested set of attributes given by + /// %NL80211_ATTR_REG_RULE_FREQ_[START|END] and + /// %NL80211_ATTR_FREQ_RANGE_MAX_BW with an attached power rule given by + /// %NL80211_ATTR_REG_RULE_POWER_MAX_ANT_GAIN and + /// %NL80211_ATTR_REG_RULE_POWER_MAX_EIRP. + SetReg = 26, + /// ask the wireless core to set the regulatory domain + /// to the specified ISO/IEC 3166-1 alpha2 country code. The core will + /// store this as a valid request and then query userspace for it. + ReqSetReg = 27, + + /// Get mesh networking properties for the + /// interface identified by %NL80211_ATTR_IFINDEX + GetMeshConfig = 28, + /// Set mesh networking properties for the + /// interface identified by %NL80211_ATTR_IFINDEX + SetMeshConfig = 29, + + /// Set extra IEs for management frames. The + /// interface is identified with %NL80211_ATTR_IFINDEX and the management + /// frame subtype with %NL80211_ATTR_MGMT_SUBTYPE. The extra IE data to be + /// added to the end of the specified management frame is specified with + /// %NL80211_ATTR_IE. If the command succeeds, the requested data will be + /// added to all specified management frames generated by + /// kernel/firmware/driver. + /// Note: This command has been removed and it is only reserved at this + /// point to avoid re-using existing command number. The functionality this + /// command was planned for has been provided with cleaner design with the + /// option to specify additional IEs in NL80211_CMD_TRIGGER_SCAN, + /// NL80211_CMD_AUTHENTICATE, NL80211_CMD_ASSOCIATE, + /// NL80211_CMD_DEAUTHENTICATE, and NL80211_CMD_DISASSOCIATE. + SetMgmtExtraIe = 30, + + /// ask the wireless core to send us its currently set + /// regulatory domain. If %NL80211_ATTR_WIPHY is specified and the device + /// has a private regulatory domain, it will be returned. Otherwise, the + /// global regdomain will be returned. + /// A device will have a private regulatory domain if it uses the + /// regulatory_hint() API. Even when a private regdomain is used the channel + /// information will still be mended according to further hints from + /// the regulatory core to help with compliance. A dump version of this API + /// is now available which will returns the global regdomain as well as + /// all private regdomains of present wiphys (for those that have it). + /// If a wiphy is self-managed (%NL80211_ATTR_WIPHY_SELF_MANAGED_REG), then + /// its private regdomain is the only valid one for it. The regulatory + /// core is not used to help with compliance in this case. + GetReg = 31, + + /// get scan results; can dump + GetScan = 32, + /// trigger a new scan with the given parameters + /// %NL80211_ATTR_TX_NO_CCK_RATE is used to decide whether to send the + /// probe requests at CCK rate or not. %NL80211_ATTR_MAC can be used to + /// specify a BSSID to scan for; if not included, the wildcard BSSID will + /// be used. + TriggerScan = 33, + /// scan notification (as a reply to + /// NL80211_CMD_GET_SCAN and on the "scan" multicast group) + NewScanResults = 34, + /// scan was aborted, for unspecified reasons, + /// partial scan results may be available + ScanAborted = 35, + + /// indicates to userspace the regulatory domain + /// has been changed and provides details of the request information + /// that caused the change such as who initiated the regulatory request + /// (%NL80211_ATTR_REG_INITIATOR), the wiphy_idx + /// (%NL80211_ATTR_REG_ALPHA2) on which the request was made from if + /// the initiator was %NL80211_REGDOM_SET_BY_COUNTRY_IE or + /// %NL80211_REGDOM_SET_BY_DRIVER, the type of regulatory domain + /// set (%NL80211_ATTR_REG_TYPE), if the type of regulatory domain is + /// %NL80211_REG_TYPE_COUNTRY the alpha2 to which we have moved on + /// to (%NL80211_ATTR_REG_ALPHA2). + RegChange = 36, + /// authentication request and notification. + /// This command is used both as a command (request to authenticate) and + /// as an event on the "mlme" multicast group indicating completion of the + /// authentication process. + /// When used as a command, %NL80211_ATTR_IFINDEX is used to identify the + /// interface. %NL80211_ATTR_MAC is used to specify PeerSTAAddress (and + /// BSSID in case of station mode). %NL80211_ATTR_SSID is used to specify + /// the SSID (mainly for association, but is included in authentication + /// request, too, to help BSS selection. %NL80211_ATTR_WIPHY_FREQ is used + /// to specify the frequence of the channel in MHz. %NL80211_ATTR_AUTH_TYPE + /// is used to specify the authentication type. %NL80211_ATTR_IE is used to + /// define IEs (VendorSpecificInfo, but also including RSN IE and FT IEs) + /// to be added to the frame. + /// When used as an event, this reports reception of an Authentication + /// frame in station and IBSS modes when the local MLME processed the + /// frame, i.e., it was for the local STA and was received in correct + /// state. This is similar to MLME-AUTHENTICATE.confirm primitive in the + /// MLME SAP interface (kernel providing MLME, userspace SME). The + /// included %NL80211_ATTR_FRAME attribute contains the management frame + /// (including both the header and frame body, but not FCS). This event is + /// also used to indicate if the authentication attempt timed out. In that + /// case the %NL80211_ATTR_FRAME attribute is replaced with a + /// %NL80211_ATTR_TIMED_OUT flag (and %NL80211_ATTR_MAC to indicate which + /// pending authentication timed out). + Authenticate = 37, + /// association request and notification; like + /// NL80211_CMD_AUTHENTICATE but for Association and Reassociation + /// (similar to MLME-ASSOCIATE.request, MLME-REASSOCIATE.request, + /// MLME-ASSOCIATE.confirm or MLME-REASSOCIATE.confirm primitives). The + /// %NL80211_ATTR_PREV_BSSID attribute is used to specify whether the + /// request is for the initial association to an ESS (that attribute not + /// included) or for reassociation within the ESS (that attribute is + /// included). + Associate = 38, + /// deauthentication request and notification; like + /// NL80211_CMD_AUTHENTICATE but for Deauthentication frames (similar to + /// MLME-DEAUTHENTICATION.request and MLME-DEAUTHENTICATE.indication + /// primitives). + Deauthenticate = 39, + /// disassociation request and notification; like + /// NL80211_CMD_AUTHENTICATE but for Disassociation frames (similar to + /// MLME-DISASSOCIATE.request and MLME-DISASSOCIATE.indication primitives). + Disassociate = 40, + + /// notification of a locally detected Michael + /// MIC (part of TKIP) failure; sent on the "mlme" multicast group; the + /// event includes %NL80211_ATTR_MAC to describe the source MAC address of + /// the frame with invalid MIC, %NL80211_ATTR_KEY_TYPE to show the key + /// type, %NL80211_ATTR_KEY_IDX to indicate the key identifier, and + /// %NL80211_ATTR_KEY_SEQ to indicate the TSC value of the frame; this + /// event matches with MLME-MICHAELMICFAILURE.indication() primitive + MichaelMicFailure = 41, + + /// indicates to userspace that an AP beacon + /// has been found while world roaming thus enabling active scan or + /// any mode of operation that initiates TX (beacons) on a channel + /// where we would not have been able to do either before. As an example + /// if you are world roaming (regulatory domain set to world or if your + /// driver is using a custom world roaming regulatory domain) and while + /// doing a passive scan on the 5 GHz band you find an AP there (if not + /// on a DFS channel) you will now be able to actively scan for that AP + /// or use AP mode on your card on that same channel. Note that this will + /// never be used for channels 1-11 on the 2 GHz band as they are always + /// enabled world wide. This beacon hint is only sent if your device had + /// either disabled active scanning or beaconing on a channel. We send to + /// userspace the wiphy on which we removed a restriction from + /// (%NL80211_ATTR_WIPHY) and the channel on which this occurred + /// before (%NL80211_ATTR_FREQ_BEFORE) and after (%NL80211_ATTR_FREQ_AFTER) + /// the beacon hint was processed. + RegBeaconHint = 42, + + /// Join a new IBSS -- given at least an SSID and a + /// FREQ attribute (for the initial frequency if no peer can be found) + /// and optionally a MAC (as BSSID) and FREQ_FIXED attribute if those + /// should be fixed rather than automatically determined. Can only be + /// executed on a network interface that is UP, and fixed BSSID/FREQ + /// may be rejected. Another optional parameter is the beacon interval, + /// given in the %NL80211_ATTR_BEACON_INTERVAL attribute, which if not + /// given defaults to 100 TU (102.4ms). + JoinIbss = 43, + /// Leave the IBSS -- no special arguments, the IBSS is + /// determined by the network interface. + LeaveIbss = 44, + + /// testmode command, takes a wiphy (or ifindex) attribute + /// to identify the device, and the TESTDATA blob attribute to pass through + /// to the driver. + Testmode = 45, + + /// connection request and notification; this command + /// requests to connect to a specified network but without separating + /// auth and assoc steps. For this, you need to specify the SSID in a + /// %NL80211_ATTR_SSID attribute, and can optionally specify the association + /// IEs in %NL80211_ATTR_IE, %NL80211_ATTR_AUTH_TYPE, %NL80211_ATTR_USE_MFP, + /// %NL80211_ATTR_MAC, %NL80211_ATTR_WIPHY_FREQ, %NL80211_ATTR_CONTROL_PORT, + /// %NL80211_ATTR_CONTROL_PORT_ETHERTYPE, + /// %NL80211_ATTR_CONTROL_PORT_NO_ENCRYPT, %NL80211_ATTR_MAC_HINT, and + /// %NL80211_ATTR_WIPHY_FREQ_HINT. + /// If included, %NL80211_ATTR_MAC and %NL80211_ATTR_WIPHY_FREQ are + /// restrictions on BSS selection, i.e., they effectively prevent roaming + /// within the ESS. %NL80211_ATTR_MAC_HINT and %NL80211_ATTR_WIPHY_FREQ_HINT + /// can be included to provide a recommendation of the initial BSS while + /// allowing the driver to roam to other BSSes within the ESS and also to + /// ignore this recommendation if the indicated BSS is not ideal. Only one + /// set of BSSID,frequency parameters is used (i.e., either the enforcing + /// %NL80211_ATTR_MAC,%NL80211_ATTR_WIPHY_FREQ or the less strict + /// %NL80211_ATTR_MAC_HINT and %NL80211_ATTR_WIPHY_FREQ_HINT). + /// %NL80211_ATTR_PREV_BSSID can be used to request a reassociation within + /// the ESS in case the device is already associated and an association with + /// a different BSS is desired. + /// Background scan period can optionally be + /// specified in %NL80211_ATTR_BG_SCAN_PERIOD, + /// if not specified default background scan configuration + /// in driver is used and if period value is 0, bg scan will be disabled. + /// This attribute is ignored if driver does not support roam scan. + /// It is also sent as an event, with the BSSID and response IEs when the + /// connection is established or failed to be established. This can be + /// determined by the %NL80211_ATTR_STATUS_CODE attribute (0 = success, + /// non-zero = failure). If %NL80211_ATTR_TIMED_OUT is included in the + /// event, the connection attempt failed due to not being able to initiate + /// authentication/association or not receiving a response from the AP. + /// Non-zero %NL80211_ATTR_STATUS_CODE value is indicated in that case as + /// well to remain backwards compatible. + Connect = 46, + /// request that the card roam (currently not implemented), + /// sent as an event when the card/driver roamed by itself. + Roam = 47, + /// drop a given connection; also used to notify + /// userspace that a connection was dropped by the AP or due to other + /// reasons, for this the %NL80211_ATTR_DISCONNECTED_BY_AP and + /// %NL80211_ATTR_REASON_CODE attributes are used. + Disconnect = 48, + /// Set a wiphy's netns. Note that all devices + /// associated with this wiphy must be down and will follow. + SetWiphyNetns = 49, + + /// get survey resuls, e.g. channel occupation + /// or noise level + GetSurvey = 50, + /// survey data notification (as a reply to + /// NL80211_CMD_GET_SURVEY and on the "scan" multicast group) + NewSurveyResults = 51, + /// Add a PMKSA cache entry, using %NL80211_ATTR_MAC + /// (for the BSSID) and %NL80211_ATTR_PMKID. + SetPmksa = 52, + /// Delete a PMKSA cache entry, using %NL80211_ATTR_MAC + /// (for the BSSID) and %NL80211_ATTR_PMKID. + DelPmksa = 53, + /// Flush all PMKSA cache entries. + FlushPmksa = 54, + + /// Request to remain awake on the specified + /// channel for the specified amount of time. This can be used to do + /// off-channel operations like transmit a Public Action frame and wait for + /// a response while being associated to an AP on another channel. + /// %NL80211_ATTR_IFINDEX is used to specify which interface (and thus + /// radio) is used. %NL80211_ATTR_WIPHY_FREQ is used to specify the + /// frequency for the operation. + /// %NL80211_ATTR_DURATION is used to specify the duration in milliseconds + /// to remain on the channel. This command is also used as an event to + /// notify when the requested duration starts (it may take a while for the + /// driver to schedule this time due to other concurrent needs for the + /// radio). + /// When called, this operation returns a cookie (%NL80211_ATTR_COOKIE) + /// that will be included with any events pertaining to this request; + /// the cookie is also used to cancel the request. + RemainOnChannel = 55, + /// This command can be used to cancel a + /// pending remain-on-channel duration if the desired operation has been + /// completed prior to expiration of the originally requested duration. + /// %NL80211_ATTR_WIPHY or %NL80211_ATTR_IFINDEX is used to specify the + /// radio. The %NL80211_ATTR_COOKIE attribute must be given as well to + /// uniquely identify the request. + /// This command is also used as an event to notify when a requested + /// remain-on-channel duration has expired. + CancelRemainOnChannel = 56, + + /// Set the mask of rates to be used in TX + /// rate selection. %NL80211_ATTR_IFINDEX is used to specify the interface + /// and @NL80211_ATTR_TX_RATES the set of allowed rates. + SetTxBitrateMask = 57, + + /// Register for receiving certain mgmt frames + /// (via @NL80211_CMD_FRAME) for processing in userspace. This command + /// requires an interface index, a frame type attribute (optional for + /// backward compatibility reasons, if not given assumes action frames) + /// and a match attribute containing the first few bytes of the frame + /// that should match, e.g. a single byte for only a category match or + /// four bytes for vendor frames including the OUI. The registration + /// cannot be dropped, but is removed automatically when the netlink + /// socket is closed. Multiple registrations can be made. + RegisterFrame = 58, + /// Alias for @NL80211_CMD_REGISTER_FRAME for + /// backward compatibility + RegisterAction = 58, + /// Management frame TX request and RX notification. This + /// command is used both as a request to transmit a management frame and + /// as an event indicating reception of a frame that was not processed in + /// kernel code, but is for us (i.e., which may need to be processed in a + /// user space application). %NL80211_ATTR_FRAME is used to specify the + /// frame contents (including header). %NL80211_ATTR_WIPHY_FREQ is used + /// to indicate on which channel the frame is to be transmitted or was + /// received. If this channel is not the current channel (remain-on-channel + /// or the operational channel) the device will switch to the given channel + /// and transmit the frame, optionally waiting for a response for the time + /// specified using %NL80211_ATTR_DURATION. When called, this operation + /// returns a cookie (%NL80211_ATTR_COOKIE) that will be included with the + /// TX status event pertaining to the TX request. + /// %NL80211_ATTR_TX_NO_CCK_RATE is used to decide whether to send the + /// management frames at CCK rate or not in 2GHz band. + /// %NL80211_ATTR_CSA_C_OFFSETS_TX is an array of offsets to CSA + /// counters which will be updated to the current value. This attribute + /// is used during CSA period. + Frame = 59, + /// Alias for @NL80211_CMD_FRAME for backward compatibility. + /// @NL80211_CMD_FRAME_TX_STATUS: Report TX status of a management frame + /// transmitted with %NL80211_CMD_FRAME. %NL80211_ATTR_COOKIE identifies + /// the TX command and %NL80211_ATTR_FRAME includes the contents of the + /// frame. %NL80211_ATTR_ACK flag is included if the recipient acknowledged + /// the frame. + Action = 59, + FrameTxStatus = 60, + /// Alias for @NL80211_CMD_FRAME_TX_STATUS for + /// backward compatibility. + ActionTxStatus = 60, + /// Set powersave, using %NL80211_ATTR_PS_STATE + SetPowerSave = 61, + /// Get powersave status in %NL80211_ATTR_PS_STATE + GetPowerSave = 62, + /// Connection quality monitor configuration. This command + /// is used to configure connection quality monitoring notification trigger + /// levels. + SetCqm = 63, + /// Connection quality monitor notification. This + /// command is used as an event to indicate the that a trigger level was + /// reached. + NotifyCqm = 64, + /// Set the channel (using %NL80211_ATTR_WIPHY_FREQ + /// and the attributes determining channel width) the given interface + /// (identifed by %NL80211_ATTR_IFINDEX) shall operate on. + /// In case multiple channels are supported by the device, the mechanism + /// with which it switches channels is implementation-defined. + /// When a monitor interface is given, it can only switch channel while + /// no other interfaces are operating to avoid disturbing the operation + /// of any other interfaces, and other interfaces will again take + /// precedence when they are used. + SetChannel = 65, + + /// Set the MAC address of the peer on a WDS interface. + SetWdsPeer = 66, + + /// When an off-channel TX was requested, this + /// command may be used with the corresponding cookie to cancel the wait + /// time if it is known that it is no longer necessary. + FrameWaitCancel = 67, + + /// Join a mesh. The mesh ID must be given, and initial + /// mesh config parameters may be given. + JoinMesh = 68, + /// Leave the mesh network -- no special arguments, the + /// network is determined by the network interface. + LeaveMesh = 69, + + /// Unprotected deauthentication frame + /// notification. This event is used to indicate that an unprotected + /// deauthentication frame was dropped when MFP is in use. + UnprotDeauthenticate = 70, + /// Unprotected disassociation frame + /// notification. This event is used to indicate that an unprotected + /// disassociation frame was dropped when MFP is in use. + UnprotDisassociate = 71, + + /// Notification on the reception of a + /// beacon or probe response from a compatible mesh peer. This is only + /// sent while no station information (sta_info) exists for the new peer + /// candidate and when @NL80211_MESH_SETUP_USERSPACE_AUTH, + /// @NL80211_MESH_SETUP_USERSPACE_AMPE, or + /// @NL80211_MESH_SETUP_USERSPACE_MPM is set. On reception of this + /// notification, userspace may decide to create a new station + /// (@NL80211_CMD_NEW_STATION). To stop this notification from + /// reoccurring, the userspace authentication daemon may want to create the + /// new station with the AUTHENTICATED flag unset and maybe change it later + /// depending on the authentication result. + NewPeerCandidate = 72, + + /// get Wake-on-Wireless-LAN (WoWLAN) settings. + GetWowlan = 73, + /// set Wake-on-Wireless-LAN (WoWLAN) settings. + /// Since wireless is more complex than wired ethernet, it supports + /// various triggers. These triggers can be configured through this + /// command with the %NL80211_ATTR_WOWLAN_TRIGGERS attribute. For + /// more background information, see + /// http://wireless.kernel.org/en/users/Documentation/WoWLAN. + /// The @NL80211_CMD_SET_WOWLAN command can also be used as a notification + /// from the driver reporting the wakeup reason. In this case, the + /// @NL80211_ATTR_WOWLAN_TRIGGERS attribute will contain the reason + /// for the wakeup, if it was caused by wireless. If it is not present + /// in the wakeup notification, the wireless device didn't cause the + /// wakeup but reports that it was woken up. + SetWowlan = 74, + + /// start a scheduled scan at certain + /// intervals and certain number of cycles, as specified by + /// %NL80211_ATTR_SCHED_SCAN_PLANS. If %NL80211_ATTR_SCHED_SCAN_PLANS is + /// not specified and only %NL80211_ATTR_SCHED_SCAN_INTERVAL is specified, + /// scheduled scan will run in an infinite loop with the specified interval. + /// These attributes are mutually exculsive, + /// i.e. NL80211_ATTR_SCHED_SCAN_INTERVAL must not be passed if + /// NL80211_ATTR_SCHED_SCAN_PLANS is defined. + /// If for some reason scheduled scan is aborted by the driver, all scan + /// plans are canceled (including scan plans that did not start yet). + /// Like with normal scans, if SSIDs (%NL80211_ATTR_SCAN_SSIDS) + /// are passed, they are used in the probe requests. For + /// broadcast, a broadcast SSID must be passed (ie. an empty + /// string). If no SSID is passed, no probe requests are sent and + /// a passive scan is performed. %NL80211_ATTR_SCAN_FREQUENCIES, + /// if passed, define which channels should be scanned; if not + /// passed, all channels allowed for the current regulatory domain + /// are used. Extra IEs can also be passed from the userspace by + /// using the %NL80211_ATTR_IE attribute. The first cycle of the + /// scheduled scan can be delayed by %NL80211_ATTR_SCHED_SCAN_DELAY + /// is supplied. + StartSchedScan = 75, + /// stop a scheduled scan. Returns -ENOENT if + /// scheduled scan is not running. The caller may assume that as soon + /// as the call returns, it is safe to start a new scheduled scan again. + StopSchedScan = 76, + /// indicates that there are scheduled scan + /// results available. + SchedScanResults = 77, + /// indicates that the scheduled scan has + /// stopped. The driver may issue this event at any time during a + /// scheduled scan. One reason for stopping the scan is if the hardware + /// does not support starting an association or a normal scan while running + /// a scheduled scan. This event is also sent when the + /// %NL80211_CMD_STOP_SCHED_SCAN command is received or when the interface + /// is brought down while a scheduled scan was running. + SchedScanStopped = 78, + + /// This command is used give the driver + /// the necessary information for supporting GTK rekey offload. This + /// feature is typically used during WoWLAN. The configuration data + /// is contained in %NL80211_ATTR_REKEY_DATA (which is nested and + /// contains the data in sub-attributes). After rekeying happened, + /// this command may also be sent by the driver as an MLME event to + /// inform userspace of the new replay counter. + SetRekeyOffload = 79, + + /// This is used as an event to inform userspace + /// of PMKSA caching dandidates + PmksaCandidate = 80, + + /// Perform a high-level TDLS command (e.g. link setup). + /// In addition, this can be used as an event to request userspace to take + /// actions on TDLS links (set up a new link or tear down an existing one). + /// In such events, %NL80211_ATTR_TDLS_OPERATION indicates the requested + /// operation, %NL80211_ATTR_MAC contains the peer MAC address, and + /// %NL80211_ATTR_REASON_CODE the reason code to be used (only with + /// %NL80211_TDLS_TEARDOWN). + TdlsOper = 81, + /// Send a TDLS management frame. The + /// %NL80211_ATTR_TDLS_ACTION attribute determines the type of frame to be + /// sent. Public Action codes (802.11-2012 8.1.5.1) will be sent as + /// 802.11 management frames, while TDLS action codes (802.11-2012 + /// 8.5.13.1) will be encapsulated and sent as data frames. The currently + /// supported Public Action code is %WLAN_PUB_ACTION_TDLS_DISCOVER_RES + /// and the currently supported TDLS actions codes are given in + /// &enum ieee80211_tdls_actioncode. + TdlsMgmt = 82, + + /// Used by an application controlling an AP + /// (or GO) interface (i.e. hostapd) to ask for unexpected frames to + /// implement sending deauth to stations that send unexpected class 3 + /// frames. Also used as the event sent by the kernel when such a frame + /// is received. + /// For the event, the %NL80211_ATTR_MAC attribute carries the TA and + /// other attributes like the interface index are present. + /// If used as the command it must have an interface index and you can + /// only unsubscribe from the event by closing the socket. Subscription + /// is also for %NL80211_CMD_UNEXPECTED_4ADDR_FRAME events. + UnexpectedFrame = 83, + + /// Probe an associated station on an AP interface + /// by sending a null data frame to it and reporting when the frame is + /// acknowleged. This is used to allow timing out inactive clients. Uses + /// %NL80211_ATTR_IFINDEX and %NL80211_ATTR_MAC. The command returns a + /// direct reply with an %NL80211_ATTR_COOKIE that is later used to match + /// up the event with the request. The event includes the same data and + /// has %NL80211_ATTR_ACK set if the frame was ACKed. + ProbeClient = 84, + + /// Register this socket to receive beacons from + /// other BSSes when any interfaces are in AP mode. This helps implement + /// OLBC handling in hostapd. Beacons are reported in %NL80211_CMD_FRAME + /// messages. Note that per PHY only one application may register. + RegisterBeacons = 85, + + /// Sent as an event indicating that the + /// associated station identified by %NL80211_ATTR_MAC sent a 4addr frame + /// and wasn't already in a 4-addr VLAN. The event will be sent similarly + /// to the %NL80211_CMD_UNEXPECTED_FRAME event, to the same listener. + Unexpected4AddrFrame = 86, + + /// sets a bitmap for the individual TIDs whether + /// No Acknowledgement Policy should be applied. + SetNoackMap = 87, + + /// An AP or GO may decide to switch channels + /// independently of the userspace SME, send this event indicating + /// %NL80211_ATTR_IFINDEX is now on %NL80211_ATTR_WIPHY_FREQ and the + /// attributes determining channel width. This indication may also be + /// sent when a remotely-initiated switch (e.g., when a STA receives a CSA + /// from the remote AP) is completed; + ChSwitchNotify = 88, + + /// Start the given P2P Device, identified by + /// its %NL80211_ATTR_WDEV identifier. It must have been created with + /// %NL80211_CMD_NEW_INTERFACE previously. After it has been started, the + /// P2P Device can be used for P2P operations, e.g. remain-on-channel and + /// public action frame TX. + StartP2PDevice = 89, + /// Stop the given P2P Device, identified by + /// its %NL80211_ATTR_WDEV identifier. + StopP2PDevice = 90, + + /// connection request to an AP failed; used to + /// notify userspace that AP has rejected the connection request from a + /// station, due to particular reason. %NL80211_ATTR_CONN_FAILED_REASON + /// is used for this. + ConnFailed = 91, + + /// Change the rate used to send multicast frames + /// for IBSS or MESH vif. + SetMcastRate = 92, + + /// sets ACL for MAC address based access control. + /// This is to be used with the drivers advertising the support of MAC + /// address based access control. List of MAC addresses is passed in + /// %NL80211_ATTR_MAC_ADDRS and ACL policy is passed in + /// %NL80211_ATTR_ACL_POLICY. Driver will enable ACL with this list, if it + /// is not already done. The new list will replace any existing list. Driver + /// will clear its ACL when the list of MAC addresses passed is empty. This + /// command is used in AP/P2P GO mode. Driver has to make sure to clear its + /// ACL list during %NL80211_CMD_STOP_AP. + SetMacAcl = 93, + + /// Start a Channel availability check (CAC). Once + /// a radar is detected or the channel availability scan (CAC) has finished + /// or was aborted, or a radar was detected, usermode will be notified with + /// this event. This command is also used to notify userspace about radars + /// while operating on this channel. + /// %NL80211_ATTR_RADAR_EVENT is used to inform about the type of the + /// event. + RadarDetect = 94, + + /// Get global nl80211 protocol features, + /// i.e. features for the nl80211 protocol rather than device features. + /// Returns the features in the %NL80211_ATTR_PROTOCOL_FEATURES bitmap. + GetProtocolFeatures = 95, + + /// Pass down the most up-to-date Fast Transition + /// Information Element to the WLAN driver + UpdateFtIes = 96, + + /// Send a Fast transition event from the WLAN driver + /// to the supplicant. This will carry the target AP's MAC address along + /// with the relevant Information Elements. This event is used to report + /// received FT IEs (MDIE, FTIE, RSN IE, TIE, RICIE). + FtEvent = 97, + + /// Indicates user-space will start running + /// a critical protocol that needs more reliability in the connection to + /// complete. + CritProtocolStart = 98, + /// Indicates the connection reliability can + /// return back to normal. + CritProtocolStop = 99, + + /// Get currently supported coalesce rules. + GetCoalesce = 100, + /// Configure coalesce rules or clear existing rules. + SetCoalesce = 101, + + /// Perform a channel switch by announcing the + /// the new channel information (Channel Switch Announcement - CSA) + /// in the beacon for some time (as defined in the + /// %NL80211_ATTR_CH_SWITCH_COUNT parameter) and then change to the + /// new channel. Userspace provides the new channel information (using + /// %NL80211_ATTR_WIPHY_FREQ and the attributes determining channel + /// width). %NL80211_ATTR_CH_SWITCH_BLOCK_TX may be supplied to inform + /// other station that transmission must be blocked until the channel + /// switch is complete. + ChannelSwitch = 102, + + /// Vendor-specified command/event. The command is specified + /// by the %NL80211_ATTR_VENDOR_ID attribute and a sub-command in + /// %NL80211_ATTR_VENDOR_SUBCMD. Parameter(s) can be transported in + /// %NL80211_ATTR_VENDOR_DATA. + /// For feature advertisement, the %NL80211_ATTR_VENDOR_DATA attribute is + /// used in the wiphy data as a nested attribute containing descriptions + /// (&struct nl80211_vendor_cmd_info) of the supported vendor commands. + /// This may also be sent as an event with the same attributes. + Vendor = 103, + + /// Set Interworking QoS mapping for IP DSCP values. + /// The QoS mapping information is included in %NL80211_ATTR_QOS_MAP. If + /// that attribute is not included, QoS mapping is disabled. Since this + /// QoS mapping is relevant for IP packets, it is only valid during an + /// association. This is cleared on disassociation and AP restart. + SetQosMap = 104, + + /// Ask the kernel to add a traffic stream for the given + /// %NL80211_ATTR_TSID and %NL80211_ATTR_MAC with %NL80211_ATTR_USER_PRIO + /// and %NL80211_ATTR_ADMITTED_TIME parameters. + /// Note that the action frame handshake with the AP shall be handled by + /// userspace via the normal management RX/TX framework, this only sets + /// up the TX TS in the driver/device. + /// If the admitted time attribute is not added then the request just checks + /// if a subsequent setup could be successful, the intent is to use this to + /// avoid setting up a session with the AP when local restrictions would + /// make that impossible. However, the subsequent "real" setup may still + /// fail even if the check was successful. + AddTxTs = 105, + /// Remove an existing TS with the %NL80211_ATTR_TSID + /// and %NL80211_ATTR_MAC parameters. It isn't necessary to call this + /// before removing a station entry entirely, or before disassociating + /// or similar, cleanup will happen in the driver/device in this case. + DelTxTs = 106, + + /// Get mesh path attributes for mesh proxy path to + /// destination %NL80211_ATTR_MAC on the interface identified by + /// %NL80211_ATTR_IFINDEX. + GetMpp = 107, + + /// Join the OCB network. The center frequency and + /// bandwidth of a channel must be given. + JoinOcb = 108, + /// Leave the OCB network -- no special arguments, the + /// network is determined by the network interface. + LeaveOcb = 109, + + /// Notify that a channel switch + /// has been started on an interface, regardless of the initiator + /// (ie. whether it was requested from a remote device or + /// initiated on our own). It indicates that + /// %NL80211_ATTR_IFINDEX will be on %NL80211_ATTR_WIPHY_FREQ + /// after %NL80211_ATTR_CH_SWITCH_COUNT TBTT's. The userspace may + /// decide to react to this indication by requesting other + /// interfaces to change channel as well. + ChSwitchStartedNotify = 110, + + /// Start channel-switching with a TDLS peer, + /// identified by the %NL80211_ATTR_MAC parameter. A target channel is + /// provided via %NL80211_ATTR_WIPHY_FREQ and other attributes determining + /// channel width/type. The target operating class is given via + /// %NL80211_ATTR_OPER_CLASS. + /// The driver is responsible for continually initiating channel-switching + /// operations and returning to the base channel for communication with the + /// AP. + TdlsChannelSwitch = 111, + /// Stop channel-switching with a TDLS + /// peer given by %NL80211_ATTR_MAC. Both peers must be on the base channel + /// when this command completes. + TdlsCancelChannelSwitch = 112, + + /// Similar to %NL80211_CMD_REG_CHANGE, but used + /// as an event to indicate changes for devices with wiphy-specific regdom + /// management. + WiphyRegChange = 113, + + /// Stop an ongoing scan. Returns -ENOENT if a scan is + /// not running. The driver indicates the status of the scan through + /// cfg80211_scan_done(). + AbortScan = 114, + + /// Start NAN operation, identified by its + /// %NL80211_ATTR_WDEV interface. This interface must have been previously + /// created with %NL80211_CMD_NEW_INTERFACE. After it has been started, the + /// NAN interface will create or join a cluster. This command must have a + /// valid %NL80211_ATTR_NAN_MASTER_PREF attribute and optional + /// %NL80211_ATTR_NAN_DUAL attributes. + /// After this command NAN functions can be added. + StartNan = 115, + /// Stop the NAN operation, identified by + /// its %NL80211_ATTR_WDEV interface. + StopNan = 116, + /// Add a NAN function. The function is defined + /// with %NL80211_ATTR_NAN_FUNC nested attribute. When called, this + /// operation returns the strictly positive and unique instance id + /// (%NL80211_ATTR_NAN_FUNC_INST_ID) and a cookie (%NL80211_ATTR_COOKIE) + /// of the function upon success. + /// Since instance ID's can be re-used, this cookie is the right + /// way to identify the function. This will avoid races when a termination + /// event is handled by the user space after it has already added a new + /// function that got the same instance id from the kernel as the one + /// which just terminated. + /// This cookie may be used in NAN events even before the command + /// returns, so userspace shouldn't process NAN events until it processes + /// the response to this command. + /// Look at %NL80211_ATTR_SOCKET_OWNER as well. + AddNanFunction = 117, + /// Delete a NAN function by cookie. + /// This command is also used as a notification sent when a NAN function is + /// terminated. This will contain a %NL80211_ATTR_NAN_FUNC_INST_ID + /// and %NL80211_ATTR_COOKIE attributes. + DelNanFunction = 118, + /// Change current NAN configuration. NAN + /// must be operational (%NL80211_CMD_START_NAN was executed). + /// It must contain at least one of the following attributes: + /// %NL80211_ATTR_NAN_MASTER_PREF, %NL80211_ATTR_NAN_DUAL. + ChangeNanConfig = 119, + /// Notification sent when a match is reported. + /// This will contain a %NL80211_ATTR_NAN_MATCH nested attribute and + /// %NL80211_ATTR_COOKIE. + NanMatch = 120, + + SetMulticastToUnicast = 121, + UpdateConnectParams = 122, + SetPmk = 123, + DelPmk = 124, + PortAuthorized = 125, + ReloadRegdb = 126, + ExternalAuth = 127, + StaOpmodeChanged = 128, + ControlPortFrame = 129, + GetFtmResponderStats = 130, + PeerMeasurementStart = 131, + PeerMeasurementResult = 132, + PeerMeasurementComplete = 133, + NotifyRadar = 134, + UpdateOweInfo = 135, + ProbeMeshLink = 136, + SetTidConfig = 137, + UnprotBeacon = 138, + ControlPortFrameTxStatus = 139, + SetSarSpecs = 140, + ObssColorCollision = 141, + ColorChangeRequest = 142, + ColorChangeStarted = 143, + ColorChangeAborted = 144, + ColorChangeCompleted = 145, + SetFilsAad = 146, + AssocComeback = 147, + AddLink = 148, + RemoveLink = 149, + AddLinkSta = 150, + ModifyLinkSta = 151, + RemoveLinkSta = 152, + Max = 152, +} +impl neli::consts::genl::Cmd for Nl80211Command {} + +#[neli::neli_enum(serialized_type = "u16")] +pub enum Nl80211Attribute { + Unspec = 0, + Wiphy = 1, + WiphyName = 2, + Ifindex = 3, + Ifname = 4, + Iftype = 5, + Mac = 6, + KeyData = 7, + KeyIdx = 8, + KeyCipher = 9, + KeySeq = 10, + KeyDefault = 11, + BeaconInterval = 12, + DtimPeriod = 13, + BeaconHead = 14, + BeaconTail = 15, + StaAid = 16, + StaFlags = 17, + StaListenInterval = 18, + StaSupportedRates = 19, + StaVlan = 20, + StaInfo = 21, + WiphyBands = 22, + MntrFlags = 23, + MeshId = 24, + StaPlinkAction = 25, + MpathNextHop = 26, + MpathInfo = 27, + BssCtsProt = 28, + BssShortPreamble = 29, + BssShortSlotTime = 30, + HtCapability = 31, + SupportedIftypes = 32, + RegAlpha2 = 33, + RegRules = 34, + MeshConfig = 35, + BssBasicRates = 36, + WiphyTxqParams = 37, + WiphyFreq = 38, + WiphyChannelType = 39, + KeyDefaultMgmt = 40, + MgmtSubtype = 41, + Ie = 42, + MaxNumScanSsids = 43, + ScanFrequencies = 44, + ScanSsids = 45, + /// replaces old SCAN_GENERATION + Generation = 46, + Bss = 47, + RegInitiator = 48, + RegType = 49, + SupportedCommands = 50, + Frame = 51, + Ssid = 52, + AuthType = 53, + ReasonCode = 54, + KeyType = 55, + MaxScanIeLen = 56, + CipherSuites = 57, + FreqBefore = 58, + FreqAfter = 59, + FreqFixed = 60, + WiphyRetryShort = 61, + WiphyRetryLong = 62, + WiphyFragThreshold = 63, + WiphyRtsThreshold = 64, + TimedOut = 65, + UseMfp = 66, + StaFlags2 = 67, + ControlPort = 68, + Testdata = 69, + Privacy = 70, + DisconnectedByAp = 71, + StatusCode = 72, + CipherSuitesPairwise = 73, + CipherSuiteGroup = 74, + WpaVersions = 75, + AkmSuites = 76, + ReqIe = 77, + RespIe = 78, + PrevBssid = 79, + Key = 80, + Keys = 81, + Pid = 82, + FourAddr = 83, // NOTE: called NL80211_ATTR_4ADDR + SurveyInfo = 84, + Pmkid = 85, + MaxNumPmkids = 86, + Duration = 87, + Cookie = 88, + WiphyCoverageClass = 89, + TxRates = 90, + FrameMatch = 91, + Ack = 92, + PsState = 93, + Cqm = 94, + LocalStateChange = 95, + ApIsolate = 96, + WiphyTxPowerSetting = 97, + WiphyTxPowerLevel = 98, + TxFrameTypes = 99, + RxFrameTypes = 100, + FrameType = 101, + ControlPortEthertype = 102, + ControlPortNoEncrypt = 103, + SupportIbssRsn = 104, + WiphyAntennaTx = 105, + WiphyAntennaRx = 106, + McastRate = 107, + OffchannelTxOk = 108, + BssHtOpmode = 109, + KeyDefaultTypes = 110, + MaxRemainOnChannelDuration = 111, + MeshSetup = 112, + WiphyAntennaAvailTx = 113, + WiphyAntennaAvailRx = 114, + SupportMeshAuth = 115, + StaPlinkState = 116, + WowlanTriggers = 117, + WowlanTriggersSupported = 118, + SchedScanInterval = 119, + InterfaceCombinations = 120, + SoftwareIftypes = 121, + RekeyData = 122, + MaxNumSchedScanSsids = 123, + MaxSchedScanIeLen = 124, + ScanSuppRates = 125, + HiddenSsid = 126, + IeProbeResp = 127, + IeAssocResp = 128, + StaWme = 129, + SupportApUapsd = 130, + RoamSupport = 131, + SchedScanMatch = 132, + MaxMatchSets = 133, + PmksaCandidate = 134, + TxNoCckRate = 135, + TdlsAction = 136, + TdlsDialogToken = 137, + TdlsOperation = 138, + TdlsSupport = 139, + TdlsExternalSetup = 140, + DeviceApSme = 141, + DontWaitForAck = 142, + FeatureFlags = 143, + ProbeRespOffload = 144, + ProbeResp = 145, + DfsRegion = 146, + DisableHt = 147, + HtCapabilityMask = 148, + NoackMap = 149, + InactivityTimeout = 150, + RxSignalDbm = 151, + BgScanPeriod = 152, + Wdev = 153, + UserRegHintType = 154, + ConnFailedReason = 155, + AuthData = 156, + VhtCapability = 157, + ScanFlags = 158, + ChannelWidth = 159, + CenterFreq1 = 160, + CenterFreq2 = 161, + P2PCtwindow = 162, + P2POppps = 163, + LocalMeshPowerMode = 164, + AclPolicy = 165, + MacAddrs = 166, + MacAclMax = 167, + RadarEvent = 168, + ExtCapa = 169, + ExtCapaMask = 170, + StaCapability = 171, + StaExtCapability = 172, + ProtocolFeatures = 173, + SplitWiphyDump = 174, + DisableVht = 175, + VhtCapabilityMask = 176, + Mdid = 177, + IeRic = 178, + CritProtId = 179, + MaxCritProtDuration = 180, + PeerAid = 181, + CoalesceRule = 182, + ChSwitchCount = 183, + ChSwitchBlockTx = 184, + CsaIes = 185, + CntdwnOffsBeacon = 186, + CntdwnOffsPresp = 187, + RxmgmtFlags = 188, + StaSupportedChannels = 189, + StaSupportedOperClasses = 190, + HandleDfs = 191, + Support5Mhz = 192, + Support10Mhz = 193, + OpmodeNotif = 194, + VendorId = 195, + VendorSubcmd = 196, + VendorData = 197, + VendorEvents = 198, + QosMap = 199, + MacHint = 200, + WiphyFreqHint = 201, + MaxApAssocSta = 202, + TdlsPeerCapability = 203, + SocketOwner = 204, + CsaCOffsetsTx = 205, + MaxCsaCounters = 206, + TdlsInitiator = 207, + UseRrm = 208, + WiphyDynAck = 209, + Tsid = 210, + UserPrio = 211, + AdmittedTime = 212, + SmpsMode = 213, + OperClass = 214, + MacMask = 215, + WiphySelfManagedReg = 216, + ExtFeatures = 217, + SurveyRadioStats = 218, + NetnsFd = 219, + SchedScanDelay = 220, + RegIndoor = 221, + MaxNumSchedScanPlans = 222, + MaxScanPlanInterval = 223, + MaxScanPlanIterations = 224, + SchedScanPlans = 225, + Pbss = 226, + BssSelect = 227, + StaSupportP2PPs = 228, + Pad = 229, + IftypeExtCapa = 230, + MuMimoGroupData = 231, + MuMimoFollowMacAddr = 232, + ScanStartTimeTsf = 233, + ScanStartTimeTsfBssid = 234, + MeasurementDuration = 235, + MeasurementDurationMandatory = 236, + MeshPeerAid = 237, + NanMasterPref = 238, + Bands = 239, + NanFunc = 240, + NanMatch = 241, + FilsKek = 242, + FilsNonces = 243, + MulticastToUnicastEnabled = 244, + Bssid = 245, + SchedScanRelativeRssi = 246, + SchedScanRssiAdjust = 247, + TimeoutReason = 248, + FilsErpUsername = 249, + FilsErpRealm = 250, + FilsErpNextSeqNum = 251, + FilsErpRrk = 252, + FilsCacheId = 253, + Pmk = 254, + SchedScanMulti = 255, + SchedScanMaxReqs = 256, + Want1X4WayHs = 257, + Pmkr0Name = 258, + PortAuthorized = 259, + ExternalAuthAction = 260, + ExternalAuthSupport = 261, + Nss = 262, + AckSignal = 263, + ControlPortOverNl80211 = 264, + TxqStats = 265, + TxqLimit = 266, + TxqMemoryLimit = 267, + TxqQuantum = 268, + HeCapability = 269, + FtmResponder = 270, + FtmResponderStats = 271, + Timeout = 272, + PeerMeasurements = 273, + AirtimeWeight = 274, + StaTxPowerSetting = 275, + StaTxPower = 276, + SaePassword = 277, + TwtResponder = 278, + HeObssPd = 279, + WiphyEdmgChannels = 280, + WiphyEdmgBwConfig = 281, + VlanId = 282, + HeBssColor = 283, + IftypeAkmSuites = 284, + TidConfig = 285, + ControlPortNoPreauth = 286, + PmkLifetime = 287, + PmkReauthThreshold = 288, + ReceiveMulticast = 289, + WiphyFreqOffset = 290, + CenterFreq1Offset = 291, + ScanFreqKhz = 292, + He6GhzCapability = 293, + FilsDiscovery = 294, + UnsolBcastProbeResp = 295, + S1GCapability = 296, + S1GCapabilityMask = 297, + SaePwe = 298, + ReconnectRequested = 299, + SarSpec = 300, + DisableHe = 301, + ObssColorBitmap = 302, + ColorChangeCount = 303, + ColorChangeColor = 304, + ColorChangeElems = 305, + MbssidConfig = 306, + MbssidElems = 307, + RadarBackground = 308, + ApSettingsFlags = 309, + EhtCapability = 310, + DisableEht = 311, + MloLinks = 312, + MloLinkId = 313, + MldAddr = 314, + MloSupport = 315, + MaxNumAkmSuites = 316, + EmlCapability = 317, + MldCapaAndOps = 318, + TxHwTimestamp = 319, + RxHwTimestamp = 320, + TdBitmap = 321, + PunctBitmap = 322, + Max = 322, +} +impl neli::consts::genl::NlAttrType for Nl80211Attribute {} + +#[neli::neli_enum(serialized_type = "u16")] +pub enum Nl80211StationInfo { + Invalid = 0, + InactiveTime = 1, + RxBytes = 2, + TxBytes = 3, + Llid = 4, + Plid = 5, + PlinkState = 6, + Signal = 7, + TxBitrate = 8, + RxPackets = 9, + TxPackets = 10, + TxRetries = 11, + TxFailed = 12, + SignalAvg = 13, + RxBitrate = 14, + BssParam = 15, + ConnectedTime = 16, + StaFlags = 17, + BeaconLoss = 18, + TOffset = 19, + LocalPm = 20, + PeerPm = 21, + NonpeerPm = 22, + RxBytes64 = 23, + TxBytes64 = 24, + ChainSignal = 25, + ChainSignalAvg = 26, + ExpectedThroughput = 27, + RxDropMisc = 28, + BeaconRx = 29, + BeaconSignalAvg = 30, + TidStats = 31, + RxDuration = 32, + Pad = 33, + AckSignal = 34, + AckSignalAvg = 35, + RxMpdus = 36, + FcsErrorCount = 37, + ConnectedToGate = 38, + TxDuration = 39, + AirtimeWeight = 40, + AirtimeLinkMetric = 41, + AssocAtBoottime = 42, + ConnectedToAs = 43, +} +impl neli::consts::genl::NlAttrType for Nl80211StationInfo {} + +#[neli::neli_enum(serialized_type = "u16")] +pub enum Nl80211Bss { + Invalid = 0, + Bssid = 1, + Frequency = 2, + Tsf = 3, + BeaconInterval = 4, + Capability = 5, + InformationElements = 6, + SignalMbm = 7, + SignalUnspec = 8, + Status = 9, + SeenMsAgo = 10, + BeaconIes = 11, + ChanWidth = 12, + BeaconTsf = 13, + PrespData = 14, + LastSeenBoottime = 15, + Pad = 16, + ParentTsf = 17, + ParentBssid = 18, + ChainSignal = 19, + FrequencyOffset = 20, + MloLinkId = 21, + MldAddr = 22, + AfterLast = 23, + Max = 22, +} +impl neli::consts::genl::NlAttrType for Nl80211Bss {} + +#[neli::neli_enum(serialized_type = "u32")] +pub enum Nl80211IfType { + /// unspecified type, driver decides + Unspecified = 0, + /// independent BSS member + Adhoc = 1, + /// managed BSS member + Station = 2, + /// access point + Ap = 3, + /// VLAN interface for access points; VLAN interfaces + /// are a bit special in that they must always be tied to a pre-existing + /// AP type interface. + ApVlan = 4, + /// wireless distribution interface + Wds = 5, + /// monitor interface receiving all frames + Monitor = 6, + /// mesh point + MeshPoint = 7, + /// P2P client + P2PClient = 8, + /// P2P group owner + P2PGo = 9, + /// P2P device interface type, this is not a netdev + /// and therefore can't be created in the normal ways, use the + /// %NL80211_CMD_START_P2P_DEVICE and %NL80211_CMD_STOP_P2P_DEVICE + /// commands to create and destroy one + P2PDevice = 10, + /// Outside Context of a BSS + /// This mode corresponds to the MIB variable dot11OCBActivated=true + Ocb = 11, + /// NAN device interface type (not a netdev) + Nan = 12, +} diff --git a/src/util/netlink/nl80211/mod.rs b/src/util/netlink/nl80211/mod.rs new file mode 100644 index 0000000..ba3e59e --- /dev/null +++ b/src/util/netlink/nl80211/mod.rs @@ -0,0 +1,374 @@ +//! Use generic netlink nl80211 in order to gather wireless information such as the SSID, BSSID and signal strength. +//! +//! The following resources were helpful when writing this: +//! - `/usr/include/linux/nl80211.h` +//! - https://github.com/jbaublitz/neli/blob/86a0c7a8fdd6db3b19d4971ab58f0d445ca327b5/examples/nl80211.rs#L1 +//! - https://github.com/bmegli/wifi-scan/blob/master/wifi_scan.c +//! - https://github.com/uoaerg/wavemon +//! - https://github.com/HewlettPackard/wireless-tools/blob/master/wireless_tools/iwlib.c +//! - https://github.com/Alamot/code-snippets/blob/master/nl80211_info/nl80211_info.c +//! - http://lists.infradead.org/pipermail/hostap/2004-March/006231.html +//! - https://blog.onethinglab.com/how-to-check-if-wireless-adapter-supports-monitor-mode/ +//! - https://git.sipsolutions.net/iw.git/ +//! - https://wireless.wiki.kernel.org/en/users/Documentation/iw + +mod enums; + +use std::rc::Rc; + +use neli::consts::nl::{GenlId, NlmF}; +use neli::consts::socket::NlFamily; +use neli::err::RouterError; +use neli::genl::{AttrTypeBuilder, Genlmsghdr, GenlmsghdrBuilder, NlattrBuilder, NoUserHeader}; +use neli::nl::{NlPayload, Nlmsghdr}; +use neli::router::asynchronous::{NlRouter, NlRouterReceiverHandle}; +use neli::types::{Buffer, GenlBuffer}; +use neli::utils::Groups; +use tokio::sync::OnceCell; + +use self::enums::{ + Nl80211Attribute, + Nl80211Bss, + Nl80211Command, + Nl80211IfType, + Nl80211StationInfo, +}; +use super::NetlinkInterface; +use crate::util::{MacAddr, Result as Res}; + +// init ------------------------------------------------------------------------ + +type Nl80211Socket = (NlRouter, NlRouterReceiverHandle>); +static NL80211_SOCKET: OnceCell = OnceCell::const_new(); +static NL80211_FAMILY: OnceCell = OnceCell::const_new(); + +async fn init_socket() -> Res { + Ok(NlRouter::connect(NlFamily::Generic, Some(0), Groups::empty()).await?) +} + +async fn init_family(socket: &NlRouter) -> Res { + Ok(socket.resolve_genl_family("nl80211").await?) +} + +// impl ------------------------------------------------------------------------ + +#[derive(Debug)] +pub struct WirelessInfo { + /// Wireless interface index + index: i32, + /// Wireless interface name + interface: Rc, + /// MAC address of the wireless interface + mac_addr: MacAddr, + /// SSID of the network; only set when connected to a wireless network + ssid: Option>, + /// BSSID of the network; only set when connected to a wireless network + bssid: Option, + /// Signal strength of the connection; only set when connected to a wireless network + signal: Option, +} + +type Payload = Genlmsghdr; +type NextNl80211 = Option, RouterError>>; + +impl NetlinkInterface { + pub async fn wireless_info(&self) -> Option { + match self.get_wireless_info().await { + Ok(info) => Some(info), + Err(e) => { + log::warn!("NetlinkInterface::wireless_info(): {}", e); + None + } + } + } + + pub async fn get_wireless_info(&self) -> Res { + let (socket, _) = NL80211_SOCKET.get_or_try_init(init_socket).await?; + let family_id = NL80211_FAMILY + .get_or_try_init(|| init_family(socket)) + .await?; + + // prepare generic message attributes + let mut attrs = GenlBuffer::new(); + + // ... the `Nl80211Command::GetScan` command requires the interface index as `Nl80211Attribute::Ifindex` + attrs.push( + NlattrBuilder::default() + .nla_type( + AttrTypeBuilder::default() + .nla_type(Nl80211Attribute::Ifindex) + .build()?, + ) + .nla_payload(self.index) + .build()?, + ); + + let mut recv = socket + .send::<_, _, u16, Genlmsghdr>( + *family_id, + NlmF::ACK | NlmF::REQUEST, + NlPayload::Payload( + GenlmsghdrBuilder::::default() + .cmd(Nl80211Command::GetInterface) + .version(1) + .attrs(attrs) + .build()?, + ), + ) + .await?; + + while let Some(Ok(msg)) = recv.next().await as NextNl80211 { + if let NlPayload::Payload(gen_msg) = msg.nl_payload() { + let attr_handle = gen_msg.attrs().get_attr_handle(); + + // only inspect Station interface types - other types may not be wireless devices + // this seems to work for my wireless cards, other `Nl80211IfType`'s may need to be + // added to fully support everything else + if !matches!( + attr_handle.get_attr_payload_as::(Nl80211Attribute::Iftype), + Ok(Nl80211IfType::Station) + ) { + continue; + } + + // interface name - not really needed since we'll use the index + let interface = match attr_handle + .get_attr_payload_as_with_len::(Nl80211Attribute::Ifname) + { + Ok(name) => name.into(), + Err(e) => { + log::error!("failed to parse ifname from nl80211 msg: {}", e); + "".into() + } + }; + + // interface MAC address + let mac_addr = match attr_handle + .get_attr_payload_as_with_len_borrowed::<&[u8]>(Nl80211Attribute::Mac) + { + Ok(bytes) => <&[u8] as TryInto>::try_into(bytes)?, + Err(e) => { + log::error!("failed to parse mac from nl80211 msg: {}", e); + continue; + } + }; + + // NOTE: it seems that nl80211 netlink doesn't null terminate the SSID here, so fetch + // it as bytes and convert it to a string ourselves + let ssid = match attr_handle + .get_attr_payload_as_with_len_borrowed::<&[u8]>(Nl80211Attribute::Ssid) + { + Ok(name) => Some(String::from_utf8_lossy(name).into()), + // if there's no SSID, then the interface is likely not connected to a network + Err(_) => None, + }; + + // don't bother fetching these if we don't have an ssid, since the interface is probably + // not connected to a network + let (bssid, signal) = { + match ssid { + Some(_) => { + let bssid = get_bssid(socket, self.index).await?; + let signal = match bssid.as_ref() { + Some(bssid) => { + get_signal_strength(socket, self.index, bssid).await? + } + None => None, + }; + (bssid, signal) + } + None => (None, None), + } + }; + + return Ok(WirelessInfo { + index: self.index, + interface, + mac_addr, + ssid, + bssid, + signal, + }); + } + } + + bail!("no wireless info found for index: {}", self.index); + } +} + +#[derive(Debug, Clone)] +struct SignalStrength { + /// Signal strength in decibels + dbm: i8, + /// I'm not really sure what this is, but it matches whatever `link` is in `/proc/net/wireless` + // TODO: find out what it actually is + link: u8, + /// Best guess of a percentage of network quality + quality: f32, +} + +/// Get the current BSSID of the connected network (if any) for the given interface +async fn get_bssid(socket: &NlRouter, index: i32) -> Res> { + let family_id = NL80211_FAMILY + .get_or_try_init(|| init_family(socket)) + .await?; + + // prepare generic message attributes + let mut attrs = GenlBuffer::new(); + + // ... the `Nl80211Command::GetScan` command requires the interface index as `Nl80211Attribute::Ifindex` + attrs.push( + NlattrBuilder::default() + .nla_type( + AttrTypeBuilder::default() + .nla_type(Nl80211Attribute::Ifindex) + .build()?, + ) + .nla_payload(index) + .build()?, + ); + + // create generic message + let genl_payload: Genlmsghdr = GenlmsghdrBuilder::default() + .cmd(Nl80211Command::GetScan) + .version(1) + .attrs(attrs) + .build()?; + + // send it to netlink + let mut recv = socket + .send::<_, _, u16, Genlmsghdr>( + *family_id, + NlmF::DUMP, + NlPayload::Payload(genl_payload), + ) + .await?; + + // look for our requested data inside netlink's results + while let Some(result) = recv.next().await as NextNl80211 { + match result { + Ok(msg) => { + if let NlPayload::Payload(gen_msg) = msg.nl_payload() { + let mut attr_handle = gen_msg.attrs().get_attr_handle(); + + if let Ok(bss_attrs) = + attr_handle.get_nested_attributes::(Nl80211Attribute::Bss) + { + if let Ok(bytes) = bss_attrs + .get_attr_payload_as_with_len_borrowed::<&[u8]>(Nl80211Bss::Bssid) + { + if let Ok(bssid) = MacAddr::try_from(bytes) { + return Ok(Some(bssid)); + } + } + } + } + } + Err(e) => { + log::error!("Nl80211Command::GetStation error: {}", e); + continue; + } + } + } + + Ok(None) +} + +/// Gets the signal strength of a wireless network connection +async fn get_signal_strength( + socket: &NlRouter, + index: i32, + bssid: &MacAddr, +) -> Res> { + let family_id = NL80211_FAMILY + .get_or_try_init(|| init_family(socket)) + .await?; + + // prepare generic message attributes... + let mut attrs = GenlBuffer::new(); + + // ... the `Nl80211Command::GetStation` command requires the interface index as `Nl80211Attribute::Ifindex`... + attrs.push( + NlattrBuilder::default() + .nla_type( + AttrTypeBuilder::default() + .nla_type(Nl80211Attribute::Ifindex) + .build()?, + ) + .nla_payload(index) + .build()?, + ); + + // ... and also the BSSID as `Nl80211Attribute::Mac` + attrs.push( + NlattrBuilder::default() + .nla_type( + AttrTypeBuilder::default() + .nla_type(Nl80211Attribute::Mac) + .build()?, + ) + .nla_payload(Buffer::from(bssid)) + .build()?, + ); + + // create generic message + let genl_payload: Genlmsghdr = GenlmsghdrBuilder::default() + .cmd(Nl80211Command::GetStation) + .version(1) + .attrs(attrs) + .build()?; + + // send it to netlink + let mut recv = socket + .send::<_, _, u16, Genlmsghdr>( + *family_id, + NlmF::ACK | NlmF::REQUEST, + NlPayload::Payload(genl_payload), + ) + .await?; + + // look for our requested data inside netlink's results + while let Some(result) = recv.next().await as NextNl80211 { + match result { + Ok(msg) => { + if let NlPayload::Payload(gen_msg) = msg.nl_payload() { + let mut attr_handle = gen_msg.attrs().get_attr_handle(); + + // FIXME: upstream - I don't think this needs to be mutable... + if let Ok(station_info) = attr_handle + .get_nested_attributes::(Nl80211Attribute::StaInfo) + { + if let Ok(signal) = + station_info.get_attr_payload_as::(Nl80211StationInfo::Signal) + { + // this is the same as `/proc/net/wireless`'s `link` + let link = 110_u8.wrapping_add(signal); + // this is the same as `/proc/net/wireless`'s `level` + let dbm = signal as i8; + // just a guess at a percentage - there's not really a good way to represent this easily + // - https://github.com/bmegli/wifi-scan/issues/18 + // - https://github.com/psibi/iwlib-rs/blob/master/src/lib.rs#L48 + // - https://www.intuitibits.com/2016/03/23/dbm-to-percent-conversion/ + // - https://eyesaas.com/wi-fi-signal-strength/ + let quality = (if dbm < -110 { + 0_f32 + } else if dbm > -40 { + 100_f32 + } else { + (dbm + 40).abs() as f32 / 70.0 + }) * 100.0; + + return Ok(Some(SignalStrength { dbm, link, quality })); + } + } + } + } + Err(e) => { + log::error!("Nl80211Command::GetStation error: {}", e); + continue; + } + } + } + + Ok(None) +} diff --git a/src/util/netlink/route.rs b/src/util/netlink/route.rs index ce40f1c..941e986 100644 --- a/src/util/netlink/route.rs +++ b/src/util/netlink/route.rs @@ -1,16 +1,17 @@ -//! TODO: get notified of network change events from netlink rather than dbus + network manager +//! Use rtnetlink (route netlink) for the following: +//! - fetching information about all current network interfaces +//! - be notified when ip addresses change //! -//! TODO: another, separate (generic) listener for nl80211 events? -//! -//! Useful things: -//! `genl-ctrl-list` returns generic families -//! `ip a add 10.0.0.254 dev wlan0 && sleep 1 && ip a del 10.0.0.254/32 dev wlan0` -//! `ip -6 addr add 2001:0db8:0:f101::1/64 dev wlan1 && sleep 1 && ip -6 addr del 2001:0db8:0:f101::1/64 dev wlan1` +//! Useful things when developing this: +//! - https://man7.org/linux/man-pages/man7/rtnetlink.7.html +//! - https://docs.kernel.org/userspace-api/netlink/intro.html +//! - `genl-ctrl-list` returns generic families +//! - simulate ipv4 activity: `ip a add 10.0.0.254 dev wlan0 && sleep 1 && ip a del 10.0.0.254/32 dev wlan0` +//! - simulate ipv6 activity: `ip -6 addr add 2001:0db8:0:f101::1/64 dev lo && sleep 1 && ip -6 addr del 2001:0db8:0:f101::1/64 dev lo` use std::collections::{HashMap, HashSet}; use std::convert::Infallible; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; -use std::rc::Rc; use libc::{RTNLGRP_IPV4_IFADDR, RTNLGRP_IPV6_IFADDR}; use neli::consts::nl::NlmF; @@ -24,20 +25,10 @@ use neli::rtnl::{Ifaddrmsg, IfaddrmsgBuilder, Ifinfomsg, IfinfomsgBuilder}; use neli::utils::Groups; use tokio::sync::mpsc::{self, Receiver, Sender}; +use super::NetlinkInterface; use crate::error::Result; -pub type InterfaceUpdate = HashMap; - -// TODO: move this file elsewhere (util::net ?) since it's not really netlink specific -// TODO: genl nl80211 checks to see if this interface is wireless? +remove iwlib dep? -// - http://lists.infradead.org/pipermail/hostap/2004-March/006231.html -// - https://blog.onethinglab.com/how-to-check-if-wireless-adapter-supports-monitor-mode/ -#[derive(Debug, Clone)] -pub struct InterfaceInfo { - pub name: Rc, - pub mac_address: Option<[u8; 6]>, - pub ip_addresses: HashSet, -} +pub type InterfaceUpdate = HashMap; pub async fn netlink_ipaddr_listen() -> Result> { // setup socket for netlink route @@ -110,8 +101,8 @@ async fn handle_netlink_route_messages( } /// Request all interfaces with their addresses from rtnetlink(7) -async fn get_all_interfaces(socket: &NlRouter) -> Result> { - let mut interface_map = HashMap::::new(); +async fn get_all_interfaces(socket: &NlRouter) -> Result> { + let mut interface_map = HashMap::::new(); // first, get all the interfaces: we need this for the interface names { @@ -140,17 +131,17 @@ async fn get_all_interfaces(socket: &NlRouter) -> Result(Ifla::Ifname) { Ok(interface) => interface.into(), Err(e) => { log::error!( - "failed to parse interface from ifinfomsg: {} :: {:?}", + "failed to parse interface name from ifinfomsg: {} :: {:?}", e, ifinfomsg ); @@ -247,14 +238,3 @@ async fn get_all_interfaces(socket: &NlRouter) -> Result Date: Thu, 6 Jul 2023 00:56:09 +0930 Subject: [PATCH 11/57] wip: integrate netlink --- src/bar_items/krb.rs | 2 +- src/bar_items/nic.rs | 88 +++++++++++++-------- src/util/net/filter.rs | 14 +++- src/util/net/interface.rs | 100 +----------------------- src/util/net/mod.rs | 133 ++++++++++++++------------------ src/util/netlink/mod.rs | 6 +- src/util/netlink/nl80211/mod.rs | 20 ++--- src/util/netlink/route.rs | 44 +++++++++-- 8 files changed, 178 insertions(+), 229 deletions(-) diff --git a/src/bar_items/krb.rs b/src/bar_items/krb.rs index dbf6b6c..9fde5cd 100644 --- a/src/bar_items/krb.rs +++ b/src/bar_items/krb.rs @@ -1,4 +1,3 @@ -use crate::error::Result; use std::time::Duration; use async_trait::async_trait; @@ -6,6 +5,7 @@ use serde_derive::{Deserialize, Serialize}; use tokio::process::Command; use crate::context::{BarItem, Context, StopAction}; +use crate::error::Result; use crate::i3::{I3Item, I3Markup}; use crate::theme::Theme; use crate::util::filter::InterfaceFilter; diff --git a/src/bar_items/nic.rs b/src/bar_items/nic.rs index 7b34d8c..0760d48 100644 --- a/src/bar_items/nic.rs +++ b/src/bar_items/nic.rs @@ -1,52 +1,76 @@ -use crate::error::Result; use std::time::Duration; use async_trait::async_trait; use hex_color::HexColor; -use iwlib::WirelessInfo; use serde_derive::{Deserialize, Serialize}; use crate::context::{BarEvent, BarItem, Context, StopAction}; +use crate::error::Result; use crate::i3::{I3Item, I3Markup, I3Modifier}; use crate::theme::Theme; use crate::util::filter::InterfaceFilter; -use crate::util::net::Interface; -use crate::util::{net_subscribe, Paginator}; +use crate::util::{net_subscribe, NetlinkInterface, Paginator}; -impl Interface { - fn format_wireless(&self, i: WirelessInfo, theme: &Theme) -> (String, Option) { - let fg = match i.wi_quality { - 100..=u8::MAX => theme.green, - 80..=99 => theme.green, - 60..=79 => theme.yellow, - 40..=59 => theme.orange, - _ => theme.red, - }; +struct Connection { + // TODO: borrow? + name: String, + addr: String, + // TODO: if wireless, refresh at an interval? + // FIXME: compute only when needed, not all the time + detail: Option, + fg: HexColor, +} +impl Connection { + pub fn format(&self, _theme: &Theme) -> (String, String) { + let fg = format!(r#" foreground="{}""#, self.fg); ( - format!("({}) {}% at {}", self.addr, i.wi_quality, i.wi_essid), - Some(fg), + format!( + r#"{}({}){}"#, + fg, + self.name, + self.addr, + match &self.detail { + Some(detail) => format!(" {}", detail), + None => "".into(), + } + ), + format!(r#"{}"#, fg, self.name), ) } +} - fn format_normal(&self, theme: &Theme) -> (String, Option) { - (format!("({})", self.addr), Some(theme.green)) +async fn connections_from_interfaces( + theme: &Theme, + interfaces: Vec, +) -> Result> { + let mut result = vec![]; + for interface in interfaces { + for addr in &interface.ip_addresses { + let wireless_info = interface.wireless_info().await; + result.push(Connection { + fg: wireless_info + .as_ref() + .and_then(|info| info.signal.as_ref()) + .map_or(theme.green, |signal| match signal.quality as u8 { + 100..=u8::MAX => theme.green, + 80..=99 => theme.green, + 60..=79 => theme.yellow, + 40..=59 => theme.orange, + _ => theme.red, + }), + name: interface.name.to_string(), + addr: addr.to_string(), + detail: wireless_info.map(|info| match (info.ssid, info.signal) { + (Some(ssid), Some(signal)) => format!("{:.0}% at {}", signal.quality, ssid), + (Some(ssid), None) => ssid.to_string(), + _ => "".into(), + }), + }); + } } - fn format(&self, theme: &Theme) -> (String, String) { - let (addr, fg) = match self.get_wireless_info() { - Some(info) => self.format_wireless(info, theme), - None => self.format_normal(theme), - }; - - let fg = fg - .map(|c| format!(r#" foreground="{}""#, c)) - .unwrap_or("".into()); - ( - format!(r#"{}{}"#, fg, self.name, addr), - format!(r#"{}"#, fg, self.name), - ) - } + Ok(result) } #[derive(Debug, Default, Clone, Serialize, Deserialize)] @@ -68,7 +92,7 @@ impl BarItem for Nic { tokio::select! { // wait for network changes Ok(list) = net.wait_for_change() => { - interfaces = list.filtered(&self.filter); + interfaces = connections_from_interfaces(&ctx.config.theme, list.filtered(&self.filter)).await?; }, // on any bar event Some(event) = ctx.wait_for_event(self.interval) => { diff --git a/src/util/net/filter.rs b/src/util/net/filter.rs index dbd623b..9067a2d 100644 --- a/src/util/net/filter.rs +++ b/src/util/net/filter.rs @@ -1,10 +1,10 @@ +use std::net::IpAddr; use std::str::FromStr; use serde::{de, Deserialize, Serialize}; use super::interface::InterfaceKind; use crate::error::Error; -use crate::util::net::Interface; /// This type is in the format of `interface[:type]`, where `interface` is the interface name, and /// `type` is an optional part which is either `ipv4` or `ipv6`. @@ -27,16 +27,22 @@ impl InterfaceFilter { } } - pub fn matches(&self, interface: &Interface) -> bool { + pub fn matches(&self, name: impl AsRef, addr: &IpAddr) -> bool { let name_match = if self.name.is_empty() { true } else { - self.name == interface.name + self.name == name.as_ref() }; match self.kind { None => name_match, - Some(k) => name_match && k == interface.kind, + Some(k) => { + name_match + && match k { + InterfaceKind::V4 => addr.is_ipv4(), + InterfaceKind::V6 => addr.is_ipv6(), + } + } } } } diff --git a/src/util/net/interface.rs b/src/util/net/interface.rs index d56221d..eb378dd 100644 --- a/src/util/net/interface.rs +++ b/src/util/net/interface.rs @@ -1,11 +1,6 @@ -use std::net::{SocketAddrV4, SocketAddrV6}; use std::str::FromStr; -use iwlib::{get_wireless_info, WirelessInfo}; -use nix::ifaddrs::getifaddrs; -use nix::net::if_::InterfaceFlags; - -use crate::error::{Error, Result}; +use crate::error::Error; #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum InterfaceKind { @@ -22,6 +17,7 @@ impl ToString for InterfaceKind { } } +// TODO replace with TryFrom impl FromStr for InterfaceKind { type Err = Error; @@ -33,95 +29,3 @@ impl FromStr for InterfaceKind { } } } - -// TODO: cache these? pass them all around by reference? interior mutability for wireless or not? -// cache list of wireless ones? -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub struct Interface { - pub name: String, - pub addr: String, - pub kind: InterfaceKind, - pub flags: InterfaceFlags, -} - -impl Interface { - pub fn new( - name: impl AsRef, - addr: impl AsRef, - kind: InterfaceKind, - flags: InterfaceFlags, - ) -> Interface { - Interface { - name: name.as_ref().into(), - addr: addr.as_ref().into(), - kind, - flags, - } - } - - pub fn is_vpn(&self) -> bool { - self.flags.contains(InterfaceFlags::IFF_TAP) || self.flags.contains(InterfaceFlags::IFF_TUN) - } - - /// If this is a wireless network, then return info from `iwlib` - pub fn get_wireless_info(&self) -> Option { - get_wireless_info(&self.name) - } - - pub fn get_interfaces() -> Result> { - let if_addrs = match getifaddrs() { - Ok(if_addrs) => if_addrs, - Err(e) => bail!("call to `getifaddrs` failed: {}", e), - }; - - let mut interfaces = vec![]; - for if_addr in if_addrs.into_iter() { - // skip any interfaces that aren't active - if !if_addr.flags.contains(InterfaceFlags::IFF_UP) { - continue; - } - - // skip the local loopback interface - if if_addr.flags.contains(InterfaceFlags::IFF_LOOPBACK) { - continue; - } - - // skip any unsupported entry (see nix's `getifaddrs` documentation) - let addr = match if_addr.address { - Some(addr) => addr, - None => continue, - }; - - // extract ip address - let (addr, kind) = match (addr.as_sockaddr_in(), addr.as_sockaddr_in6()) { - (Some(ipv4), _) => ( - format!("{}", SocketAddrV4::from(*ipv4).ip()), - InterfaceKind::V4, - ), - (_, Some(ipv6)) => { - // filter out non-global ipv6 addresses - if !ipv6.ip().is_global() { - continue; - } - - ( - format!("{}", SocketAddrV6::from(*ipv6).ip()), - InterfaceKind::V6, - ) - } - (None, None) => continue, - }; - - interfaces.push(Interface::new( - if_addr.interface_name, - addr, - kind, - if_addr.flags, - )); - } - - interfaces.sort(); - - Ok(interfaces) - } -} diff --git a/src/util/net/mod.rs b/src/util/net/mod.rs index 51dd287..7a04266 100644 --- a/src/util/net/mod.rs +++ b/src/util/net/mod.rs @@ -1,68 +1,38 @@ pub mod filter; pub mod interface; -use std::ops::Deref; - -use futures::StreamExt; use tokio::sync::{broadcast, mpsc, OnceCell}; use self::filter::InterfaceFilter; -pub use self::interface::Interface; -use crate::dbus::network_manager::NetworkManagerProxy; -use crate::dbus::{dbus_connection, BusType}; +use super::route::InterfaceUpdate; +use super::NetlinkInterface; use crate::error::Result; - -// FIXME: I don't like this interface list thing -#[derive(Debug, Clone)] -pub struct InterfaceList { - inner: Vec, -} - -impl InterfaceList { - pub fn filtered(self, filter: &[InterfaceFilter]) -> Vec { - self.inner - .into_iter() - .filter(|i| { - if filter.is_empty() { - true - } else { - filter.iter().any(|filter| filter.matches(i)) - } - }) - .collect() - } -} - -impl Deref for InterfaceList { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} +use crate::util::netlink_ipaddr_listen; static NET_RX: OnceCell = OnceCell::const_new(); +// structs --------------------------------------------------------------------- + #[derive(Debug)] pub struct Net { tx: mpsc::Sender<()>, - rx: broadcast::Receiver, + rx: broadcast::Receiver, } impl Net { - fn new(tx: mpsc::Sender<()>, rx: broadcast::Receiver) -> Net { + fn new(tx: mpsc::Sender<()>, rx: broadcast::Receiver) -> Net { Net { tx, rx } } - pub async fn wait_for_change(&mut self) -> Result { - Ok(self.rx.recv().await?) + pub async fn wait_for_change(&mut self) -> Result { + Ok(self.rx.recv().await?.into()) } pub async fn trigger_update(&self) -> Result<()> { Ok(self.tx.send(()).await?) } - pub async fn update_now(&mut self) -> Result { + pub async fn update_now(&mut self) -> Result { self.trigger_update().await?; self.wait_for_change().await } @@ -77,6 +47,42 @@ impl Clone for Net { } } +#[derive(Debug)] +pub struct Interfaces { + inner: Vec, +} + +impl Interfaces { + pub fn filtered(self, filters: &[InterfaceFilter]) -> Vec { + if filters.is_empty() { + return self.inner; + } + + let mut filtered = vec![]; + for mut interface in self.inner { + interface + .ip_addresses + .retain(|addr| filters.iter().any(|f| f.matches(&interface.name, addr))); + + if !interface.ip_addresses.is_empty() { + filtered.push(interface); + } + } + + filtered + } +} + +impl From for Interfaces { + fn from(value: InterfaceUpdate) -> Self { + let mut inner = value.into_values().collect::>(); + inner.sort_unstable_by_key(|int| int.index); + Interfaces { inner } + } +} + +// subscribe ------------------------------------------------------------------- + pub async fn net_subscribe() -> Result { Ok(NET_RX.get_or_try_init(start_task).await?.clone()) } @@ -84,49 +90,26 @@ pub async fn net_subscribe() -> Result { async fn start_task() -> Result { let (iface_tx, iface_rx) = broadcast::channel(2); let (manual_tx, manual_rx) = mpsc::channel(1); + + // spawn task to watch for network updates tokio::task::spawn_local(watch_net_updates(iface_tx, manual_rx)); + // trigger an initial update + manual_tx.send(()).await?; + Ok(Net::new(manual_tx, iface_rx)) } async fn watch_net_updates( - tx: broadcast::Sender, - mut rx: mpsc::Receiver<()>, + tx: broadcast::Sender, + manual_trigger: mpsc::Receiver<()>, ) -> Result<()> { - // TODO: investigate effort of checking network state with netlink rather than dbus - let connection = dbus_connection(BusType::System).await?; - let nm = NetworkManagerProxy::new(&connection).await?; - // this captures all network connect/disconnect events - let mut state_changed = nm.receive_state_changed().await?; - // this captures all vpn interface connect/disconnect events - let mut active_con_change = nm.receive_active_connections_objpath_changed().await; - - let mut force_update = true; - let mut last_value = vec![]; + let mut rx = netlink_ipaddr_listen(manual_trigger).await?; loop { - // check current interfaces - let interfaces = Interface::get_interfaces()?; - - // send updates to subscribers only if it's changed since last time - if force_update || last_value != interfaces { - force_update = false; - last_value = interfaces.clone(); - tx.send(InterfaceList { inner: interfaces })?; - } - - tokio::select! { - // callers can manually trigger updates - Some(()) = rx.recv() => { - force_update = true; - continue; - }, - // catch updates from NetworkManager via dbus - opt = state_changed.next() => if opt.is_none() { - bail!("unexpected end of NetworkManagerProxy::receive_state_changed stream"); - }, - opt = active_con_change.next() => if opt.is_none() { - bail!("unexpected end of NetworkManagerProxy::receive_active_connections_objpath_changed stream"); - } + if let Some(mut interfaces) = rx.recv().await { + // filter out loopback + interfaces.retain(|_, int| int.name.as_ref() != "lo"); + tx.send(interfaces)?; } } } diff --git a/src/util/netlink/mod.rs b/src/util/netlink/mod.rs index 1b81ec3..4d8e28d 100644 --- a/src/util/netlink/mod.rs +++ b/src/util/netlink/mod.rs @@ -6,9 +6,10 @@ use std::array::TryFromSliceError; use std::collections::HashSet; use std::fmt::Debug; use std::net::IpAddr; -use std::rc::Rc; +use std::sync::Arc; pub use acpi::netlink_acpi_listen; +pub use route::netlink_ipaddr_listen; #[derive(Clone)] pub struct MacAddr { @@ -75,7 +76,8 @@ impl TryFrom<&str> for MacAddr { #[derive(Debug, Clone)] pub struct NetlinkInterface { pub index: i32, - pub name: Rc, + // NOTE: `Arc` rather than `Rc` here because `Send` is needed by `tokio::sync::broadcast` + pub name: Arc, pub mac_address: Option, pub ip_addresses: HashSet, } diff --git a/src/util/netlink/nl80211/mod.rs b/src/util/netlink/nl80211/mod.rs index ba3e59e..18a29ea 100644 --- a/src/util/netlink/nl80211/mod.rs +++ b/src/util/netlink/nl80211/mod.rs @@ -55,17 +55,17 @@ async fn init_family(socket: &NlRouter) -> Res { #[derive(Debug)] pub struct WirelessInfo { /// Wireless interface index - index: i32, + pub index: i32, /// Wireless interface name - interface: Rc, + pub interface: Rc, /// MAC address of the wireless interface - mac_addr: MacAddr, + pub mac_addr: MacAddr, /// SSID of the network; only set when connected to a wireless network - ssid: Option>, + pub ssid: Option>, /// BSSID of the network; only set when connected to a wireless network - bssid: Option, + pub bssid: Option, /// Signal strength of the connection; only set when connected to a wireless network - signal: Option, + pub signal: Option, } type Payload = Genlmsghdr; @@ -197,14 +197,14 @@ impl NetlinkInterface { } #[derive(Debug, Clone)] -struct SignalStrength { +pub struct SignalStrength { /// Signal strength in decibels - dbm: i8, + pub dbm: i8, /// I'm not really sure what this is, but it matches whatever `link` is in `/proc/net/wireless` // TODO: find out what it actually is - link: u8, + pub link: u8, /// Best guess of a percentage of network quality - quality: f32, + pub quality: f32, } /// Get the current BSSID of the connected network (if any) for the given interface diff --git a/src/util/netlink/route.rs b/src/util/netlink/route.rs index 941e986..32f5bd5 100644 --- a/src/util/netlink/route.rs +++ b/src/util/netlink/route.rs @@ -12,6 +12,7 @@ use std::collections::{HashMap, HashSet}; use std::convert::Infallible; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +use std::rc::Rc; use libc::{RTNLGRP_IPV4_IFADDR, RTNLGRP_IPV6_IFADDR}; use neli::consts::nl::NlmF; @@ -30,7 +31,9 @@ use crate::error::Result; pub type InterfaceUpdate = HashMap; -pub async fn netlink_ipaddr_listen() -> Result> { +pub async fn netlink_ipaddr_listen( + manual_trigger: mpsc::Receiver<()>, +) -> Result> { // setup socket for netlink route let (socket, multicast) = NlRouter::connect(NlFamily::Route, None, Groups::empty()).await?; @@ -47,20 +50,47 @@ pub async fn netlink_ipaddr_listen() -> Result> { .unwrap(); let (tx, rx) = mpsc::channel(8); + + // wrap socket in an `Rc` to prevent it from being cleaned up earlier than expected + // and also to share it between tasks + let socket = Rc::new(socket); + + // spawn task to listen for manual requests to update + tokio::task::spawn_local({ + let tx = tx.clone(); + let socket = socket.clone(); + async move { + if let Err(e) = handle_manual_trigger(socket, manual_trigger, tx).await { + log::error!("fatal error while handling manual network updates: {}", e); + } + } + }); + + // spawn task to listen for network address updates tokio::task::spawn_local(async move { - if let Err(e) = handle_netlink_route_messages(&socket, multicast, tx).await { + if let Err(e) = handle_netlink_route_messages(socket, multicast, tx).await { log::error!("fatal error handling netlink route messages: {}", e); } - - // make sure socket is kept alive while we're reading messages - drop(socket); }); Ok(rx) } +async fn handle_manual_trigger( + socket: Rc, + mut manual_trigger: mpsc::Receiver<()>, + tx: Sender, +) -> Result { + while let Some(()) = manual_trigger.recv().await { + log::debug!("manual network update requested"); + tx.send(get_all_interfaces(&socket).await?).await?; + } + + bail!("unexpected drop of manual trigger senders"); +} + async fn handle_netlink_route_messages( - socket: &NlRouter, + socket: Rc, mut multicast: NlRouterReceiverHandle>, tx: Sender, ) -> Result { @@ -101,7 +131,7 @@ async fn handle_netlink_route_messages( } /// Request all interfaces with their addresses from rtnetlink(7) -async fn get_all_interfaces(socket: &NlRouter) -> Result> { +async fn get_all_interfaces(socket: &Rc) -> Result> { let mut interface_map = HashMap::::new(); // first, get all the interfaces: we need this for the interface names From fae15a6c7f78f069b0d0ab47fe15da0a7ba0c7d6 Mon Sep 17 00:00:00 2001 From: acheronfail Date: Thu, 6 Jul 2023 18:55:15 +0930 Subject: [PATCH 12/57] nic: update to use new netlink interface --- sample_config.toml | 10 ++ scripts/i3.conf | 4 + src/bar_items/nic.rs | 208 +++++++++++++++++++++++--------- src/util/net/filter.rs | 33 ++++- src/util/net/interface.rs | 31 ----- src/util/net/mod.rs | 10 +- src/util/netlink/mod.rs | 4 +- src/util/netlink/nl80211/mod.rs | 49 +++++--- src/util/netlink/route.rs | 10 +- 9 files changed, 240 insertions(+), 119 deletions(-) delete mode 100644 src/util/net/interface.rs diff --git a/sample_config.toml b/sample_config.toml index 9903737..c652be0 100644 --- a/sample_config.toml +++ b/sample_config.toml @@ -109,6 +109,16 @@ short_text = "!" # (via NetworkManager). type = "nic" +# Optionally define how connection details will be displayed when connected to a WiFi network +# Values are: +# - `percent`: e.g., "80% at " (default) +# - `dbm`: e.g., "-65 dBm at " +# - `hidden`: nothing is shown +wireless_display = "percent" +# The `wireless_display` setting can be optionally paired with this one to refresh the WiFi details +# at a desired interval. If unset, it only updates when the item itself updates. +wireless_refresh_interval = "2m" + # Optionally pass an `interval` to force updates (useful if you don't use NetworkManager) # interval = "60s" diff --git a/scripts/i3.conf b/scripts/i3.conf index b6dd3b1..0137439 100644 --- a/scripts/i3.conf +++ b/scripts/i3.conf @@ -23,6 +23,10 @@ bindsym 7 exec --no-startup-id pkill -RTMIN+7 istat bindsym 8 exec --no-startup-id pkill -RTMIN+8 istat bindsym 9 exec --no-startup-id pkill -RTMIN+9 istat +# theme ipc +bindsym p exec istat-ipc --socket /tmp/istat-socket.dev set-theme /powerline_enable true +bindsym shift+p exec istat-ipc --socket /tmp/istat-socket.dev set-theme /powerline_enable false + # custom ipc bindsym bracketleft exec istat-ipc --socket /tmp/istat-socket.dev custom pulse volume-down sink bindsym bracketright exec istat-ipc --socket /tmp/istat-socket.dev custom pulse volume-up sink diff --git a/src/bar_items/nic.rs b/src/bar_items/nic.rs index 0760d48..0c80e4d 100644 --- a/src/bar_items/nic.rs +++ b/src/bar_items/nic.rs @@ -1,7 +1,7 @@ +use std::net::IpAddr; use std::time::Duration; use async_trait::async_trait; -use hex_color::HexColor; use serde_derive::{Deserialize, Serialize}; use crate::context::{BarEvent, BarItem, Context, StopAction}; @@ -9,30 +9,140 @@ use crate::error::Result; use crate::i3::{I3Item, I3Markup, I3Modifier}; use crate::theme::Theme; use crate::util::filter::InterfaceFilter; +use crate::util::nl80211::SignalStrength; use crate::util::{net_subscribe, NetlinkInterface, Paginator}; -struct Connection { - // TODO: borrow? - name: String, - addr: String, - // TODO: if wireless, refresh at an interval? - // FIXME: compute only when needed, not all the time - detail: Option, - fg: HexColor, +struct Connections { + inner: Vec, } -impl Connection { - pub fn format(&self, _theme: &Theme) -> (String, String) { - let fg = format!(r#" foreground="{}""#, self.fg); +impl Connections { + fn new(inner: Vec) -> Self { + Self { inner } + } + + fn len(&self) -> usize { + self.inner.iter().map(|int| int.ip_addresses.len()).sum() + } + + fn is_empty(&self) -> bool { + self.len() == 0 + } + + async fn get_index<'a>(&'a self, index: usize) -> Option> { + let pair = self + .inner + .iter() + .flat_map(|int| { + int.ip_addresses + .iter() + .map(|addr| (int, addr)) + .collect::>() + }) + .nth(index); + + match pair { + Some((interface, addr)) => Some(Connection::new(interface, addr).await), + None => None, + } + } +} + +#[derive(Debug, Default, Serialize, Deserialize, Copy, Clone)] +#[serde(rename_all = "snake_case")] +enum WirelessDisplay { + Hidden, + #[default] + Percent, + Dbm, +} + +enum ConnectionDetail { + None, + Ssid(String), + SsidAndSignal(String, SignalStrength), +} + +impl ConnectionDetail { + fn display(&self, wireless_display: WirelessDisplay) -> String { + if matches!(wireless_display, WirelessDisplay::Hidden) { + return "".into(); + } + + match self { + ConnectionDetail::SsidAndSignal(ssid, signal) => { + let signal = match wireless_display { + WirelessDisplay::Percent => format!("{}%", signal.quality() as u8), + WirelessDisplay::Dbm => format!("{} dBm", signal.dbm), + // SAFETY: we match and early return on this at the beginning of this function + WirelessDisplay::Hidden => unreachable!(), + }; + format!("{signal} at {ssid}", ssid = ssid, signal = signal) + } + ConnectionDetail::Ssid(ssid) => ssid.into(), + ConnectionDetail::None => "".into(), + } + } +} + +struct Connection<'a> { + /// Interface name + name: &'a str, + /// Interface address as a string + addr: &'a IpAddr, + /// Extra detail about the connection + detail: Option, + /// Connection quality expressed as a percentage value between 0 and 100 + /// Only set when connection is wireless, and expresses the signal strength + /// This is used to infer which colour the item should be + quality: Option, +} + +impl<'a> Connection<'a> { + async fn new(interface: &'a NetlinkInterface, addr: &'a IpAddr) -> Connection<'a> { + let wireless_info = interface.wireless_info().await; + let quality = wireless_info + .as_ref() + .and_then(|info| info.signal.as_ref()) + .map(|signal| signal.quality() as u8); + + Connection { + name: &interface.name, + addr: &addr, + detail: wireless_info.map(|info| match (info.ssid, info.signal) { + (Some(ssid), Some(signal)) => { + ConnectionDetail::SsidAndSignal(ssid.to_string(), signal) + } + (Some(ssid), None) => ConnectionDetail::Ssid(ssid.to_string()), + _ => ConnectionDetail::None, + }), + quality, + } + } + + fn format(&self, theme: &Theme, wireless_display: WirelessDisplay) -> (String, String) { + let fg = format!( + r#" foreground="{}""#, + match self.quality { + Some(quality) => match quality { + 100..=u8::MAX => theme.green, + 80..=99 => theme.green, + 60..=79 => theme.yellow, + 40..=59 => theme.orange, + _ => theme.red, + }, + None => theme.green, + } + ); ( format!( r#"{}({}){}"#, fg, self.name, self.addr, - match &self.detail { - Some(detail) => format!(" {}", detail), - None => "".into(), + match (wireless_display, &self.detail) { + (WirelessDisplay::Hidden, _) | (_, None) => "".into(), + (_, Some(detail)) => format!(" {}", detail.display(wireless_display)), } ), format!(r#"{}"#, fg, self.name), @@ -40,45 +150,16 @@ impl Connection { } } -async fn connections_from_interfaces( - theme: &Theme, - interfaces: Vec, -) -> Result> { - let mut result = vec![]; - for interface in interfaces { - for addr in &interface.ip_addresses { - let wireless_info = interface.wireless_info().await; - result.push(Connection { - fg: wireless_info - .as_ref() - .and_then(|info| info.signal.as_ref()) - .map_or(theme.green, |signal| match signal.quality as u8 { - 100..=u8::MAX => theme.green, - 80..=99 => theme.green, - 60..=79 => theme.yellow, - 40..=59 => theme.orange, - _ => theme.red, - }), - name: interface.name.to_string(), - addr: addr.to_string(), - detail: wireless_info.map(|info| match (info.ssid, info.signal) { - (Some(ssid), Some(signal)) => format!("{:.0}% at {}", signal.quality, ssid), - (Some(ssid), None) => ssid.to_string(), - _ => "".into(), - }), - }); - } - } - - Ok(result) -} - #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct Nic { #[serde(default, with = "crate::human_time::option")] interval: Option, #[serde(default)] filter: Vec, + #[serde(default)] + wireless_display: WirelessDisplay, + #[serde(default, with = "crate::human_time::option")] + wireless_refresh_interval: Option, } #[async_trait(?Send)] @@ -87,12 +168,22 @@ impl BarItem for Nic { let mut net = net_subscribe().await?; let mut p = Paginator::new(); - let mut interfaces = vec![]; + let mut connections = Connections::new(vec![]); loop { + let wireless_refresh_trigger = || async { + match (self.wireless_display, self.wireless_refresh_interval) { + (WirelessDisplay::Hidden, _) | (_, None) => { + futures::future::pending::<()>().await + } + (_, Some(duration)) => tokio::time::sleep(duration).await, + } + }; + tokio::select! { // wait for network changes Ok(list) = net.wait_for_change() => { - interfaces = connections_from_interfaces(&ctx.config.theme, list.filtered(&self.filter)).await?; + connections = Connections::new(list.filtered(&self.filter)); + p.set_len(connections.len()); }, // on any bar event Some(event) = ctx.wait_for_event(self.interval) => { @@ -106,18 +197,25 @@ impl BarItem for Nic { } } } + // if set, start a timeout to refresh the wireless details + // this just breaks the `select!` so the wireless details will be fetched again + () = wireless_refresh_trigger() => {} } - let item = if interfaces.is_empty() { + let item = if connections.is_empty() { // TODO: differentiate between empty after filtering, and completely disconnected? I3Item::new("inactive").color(ctx.config.theme.dim) } else { - p.set_len(interfaces.len()); - let theme = &ctx.config.theme; - let (full, short) = interfaces[p.idx()].format(theme); - let full = format!(r#"{}{}"#, full, p.format(theme)); + // SAFETY(unwrap): we always set the paginator's length to the connection's length + // so it should always be within bounds + let (full, short) = connections + .get_index(p.idx()) + .await + .unwrap() + .format(theme, self.wireless_display); + let full = format!(r#"{}{}"#, full, p.format(theme)); I3Item::new(full).short_text(short).markup(I3Markup::Pango) }; diff --git a/src/util/net/filter.rs b/src/util/net/filter.rs index 9067a2d..9911644 100644 --- a/src/util/net/filter.rs +++ b/src/util/net/filter.rs @@ -3,9 +3,35 @@ use std::str::FromStr; use serde::{de, Deserialize, Serialize}; -use super::interface::InterfaceKind; use crate::error::Error; +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum InterfaceKind { + V4, + V6, +} + +impl ToString for InterfaceKind { + fn to_string(&self) -> String { + match self { + InterfaceKind::V4 => "v4".into(), + InterfaceKind::V6 => "v6".into(), + } + } +} + +impl TryFrom<&str> for InterfaceKind { + type Error = Error; + + fn try_from(value: &str) -> Result { + match value { + "v4" => Ok(Self::V4), + "v6" => Ok(Self::V6), + s => bail!("unrecognised InterfaceKind, expected v4 or v6, got: {}", s), + } + } +} + /// This type is in the format of `interface[:type]`, where `interface` is the interface name, and /// `type` is an optional part which is either `ipv4` or `ipv6`. /// @@ -67,10 +93,7 @@ impl FromStr for InterfaceFilter { // SAFETY: we just checked for the delimiter above let (name, kind) = s.split_once(d).unwrap(); - match kind.parse() { - Ok(kind) => Ok(InterfaceFilter::new(name, Some(kind))), - Err(e) => Err(e), - } + Ok(InterfaceFilter::new(name, Some(kind.try_into()?))) } } diff --git a/src/util/net/interface.rs b/src/util/net/interface.rs deleted file mode 100644 index eb378dd..0000000 --- a/src/util/net/interface.rs +++ /dev/null @@ -1,31 +0,0 @@ -use std::str::FromStr; - -use crate::error::Error; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub enum InterfaceKind { - V4, - V6, -} - -impl ToString for InterfaceKind { - fn to_string(&self) -> String { - match self { - InterfaceKind::V4 => "v4".into(), - InterfaceKind::V6 => "v6".into(), - } - } -} - -// TODO replace with TryFrom -impl FromStr for InterfaceKind { - type Err = Error; - - fn from_str(s: &str) -> std::result::Result { - match s { - "v4" => Ok(Self::V4), - "v6" => Ok(Self::V6), - _ => Err(format!("unrecognised InterfaceKind, expected v4 or v6, got: {}", s).into()), - } - } -} diff --git a/src/util/net/mod.rs b/src/util/net/mod.rs index 7a04266..5023f56 100644 --- a/src/util/net/mod.rs +++ b/src/util/net/mod.rs @@ -1,5 +1,4 @@ pub mod filter; -pub mod interface; use tokio::sync::{broadcast, mpsc, OnceCell}; @@ -75,9 +74,10 @@ impl Interfaces { impl From for Interfaces { fn from(value: InterfaceUpdate) -> Self { - let mut inner = value.into_values().collect::>(); - inner.sort_unstable_by_key(|int| int.index); - Interfaces { inner } + Interfaces { + // TODO: since index map is used, we don't really need to convert to vec here + inner: value.into_values().collect::>(), + } } } @@ -107,7 +107,7 @@ async fn watch_net_updates( let mut rx = netlink_ipaddr_listen(manual_trigger).await?; loop { if let Some(mut interfaces) = rx.recv().await { - // filter out loopback + // filter out loopback interfaces interfaces.retain(|_, int| int.name.as_ref() != "lo"); tx.send(interfaces)?; } diff --git a/src/util/netlink/mod.rs b/src/util/netlink/mod.rs index 4d8e28d..3a6413c 100644 --- a/src/util/netlink/mod.rs +++ b/src/util/netlink/mod.rs @@ -3,12 +3,12 @@ pub mod nl80211; pub mod route; use std::array::TryFromSliceError; -use std::collections::HashSet; use std::fmt::Debug; use std::net::IpAddr; use std::sync::Arc; pub use acpi::netlink_acpi_listen; +use indexmap::IndexSet; pub use route::netlink_ipaddr_listen; #[derive(Clone)] @@ -79,5 +79,5 @@ pub struct NetlinkInterface { // NOTE: `Arc` rather than `Rc` here because `Send` is needed by `tokio::sync::broadcast` pub name: Arc, pub mac_address: Option, - pub ip_addresses: HashSet, + pub ip_addresses: IndexSet, } diff --git a/src/util/netlink/nl80211/mod.rs b/src/util/netlink/nl80211/mod.rs index 18a29ea..468c479 100644 --- a/src/util/netlink/nl80211/mod.rs +++ b/src/util/netlink/nl80211/mod.rs @@ -203,8 +203,37 @@ pub struct SignalStrength { /// I'm not really sure what this is, but it matches whatever `link` is in `/proc/net/wireless` // TODO: find out what it actually is pub link: u8, - /// Best guess of a percentage of network quality - pub quality: f32, + + /// Cached quality value + quality: std::cell::OnceCell, +} + +impl SignalStrength { + pub fn new(dbm: i8, link: u8) -> SignalStrength { + SignalStrength { + dbm, + link, + quality: std::cell::OnceCell::new(), + } + } + + /// Just a guess at a percentage - there's not really a good way to represent this easily + /// - https://github.com/bmegli/wifi-scan/issues/18 + /// - https://github.com/psibi/iwlib-rs/blob/master/src/lib.rs#L48 + /// - https://www.intuitibits.com/2016/03/23/dbm-to-percent-conversion/ + /// - https://eyesaas.com/wi-fi-signal-strength/ + pub fn quality(&self) -> f32 { + *self.quality.get_or_init(|| { + (if self.dbm < -110 { + 0_f32 + } else if self.dbm > -40 { + 100_f32 + } else { + // lerp between -70 and 0 + (70.0 - (self.dbm + 40).abs() as f32) / 70.0 + }) * 100.0 + }) + } } /// Get the current BSSID of the connected network (if any) for the given interface @@ -213,6 +242,7 @@ async fn get_bssid(socket: &NlRouter, index: i32) -> Res> { .get_or_try_init(|| init_family(socket)) .await?; + // TODO: de-duplicate message sending boilerplate (id by, etc) // prepare generic message attributes let mut attrs = GenlBuffer::new(); @@ -345,20 +375,7 @@ async fn get_signal_strength( let link = 110_u8.wrapping_add(signal); // this is the same as `/proc/net/wireless`'s `level` let dbm = signal as i8; - // just a guess at a percentage - there's not really a good way to represent this easily - // - https://github.com/bmegli/wifi-scan/issues/18 - // - https://github.com/psibi/iwlib-rs/blob/master/src/lib.rs#L48 - // - https://www.intuitibits.com/2016/03/23/dbm-to-percent-conversion/ - // - https://eyesaas.com/wi-fi-signal-strength/ - let quality = (if dbm < -110 { - 0_f32 - } else if dbm > -40 { - 100_f32 - } else { - (dbm + 40).abs() as f32 / 70.0 - }) * 100.0; - - return Ok(Some(SignalStrength { dbm, link, quality })); + return Ok(Some(SignalStrength::new(dbm, link))); } } } diff --git a/src/util/netlink/route.rs b/src/util/netlink/route.rs index 32f5bd5..ea2e868 100644 --- a/src/util/netlink/route.rs +++ b/src/util/netlink/route.rs @@ -9,11 +9,11 @@ //! - simulate ipv4 activity: `ip a add 10.0.0.254 dev wlan0 && sleep 1 && ip a del 10.0.0.254/32 dev wlan0` //! - simulate ipv6 activity: `ip -6 addr add 2001:0db8:0:f101::1/64 dev lo && sleep 1 && ip -6 addr del 2001:0db8:0:f101::1/64 dev lo` -use std::collections::{HashMap, HashSet}; use std::convert::Infallible; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use std::rc::Rc; +use indexmap::{IndexMap, IndexSet}; use libc::{RTNLGRP_IPV4_IFADDR, RTNLGRP_IPV6_IFADDR}; use neli::consts::nl::NlmF; use neli::consts::rtnl::{Arphrd, Ifa, Ifla, RtAddrFamily, RtScope, Rtm}; @@ -29,7 +29,7 @@ use tokio::sync::mpsc::{self, Receiver, Sender}; use super::NetlinkInterface; use crate::error::Result; -pub type InterfaceUpdate = HashMap; +pub type InterfaceUpdate = IndexMap; pub async fn netlink_ipaddr_listen( manual_trigger: mpsc::Receiver<()>, @@ -131,8 +131,8 @@ async fn handle_netlink_route_messages( } /// Request all interfaces with their addresses from rtnetlink(7) -async fn get_all_interfaces(socket: &Rc) -> Result> { - let mut interface_map = HashMap::::new(); +async fn get_all_interfaces(socket: &Rc) -> Result { + let mut interface_map = IndexMap::::new(); // first, get all the interfaces: we need this for the interface names { @@ -179,7 +179,7 @@ async fn get_all_interfaces(socket: &Rc) -> Result Date: Thu, 6 Jul 2023 21:16:19 +0930 Subject: [PATCH 13/57] remove network manager dbus proxy and iwlib dependency --- Cargo.lock | 147 ------------------------ Cargo.toml | 1 - sample_config.toml | 10 +- src/dbus/mod.rs | 1 - src/dbus/network_manager.rs | 218 ------------------------------------ 5 files changed, 5 insertions(+), 372 deletions(-) delete mode 100644 src/dbus/network_manager.rs diff --git a/Cargo.lock b/Cargo.lock index c141238..a6c0656 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -211,29 +211,6 @@ dependencies = [ "syn 2.0.16", ] -[[package]] -name = "bindgen" -version = "0.65.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5" -dependencies = [ - "bitflags", - "cexpr", - "clang-sys", - "lazy_static", - "lazycell", - "log", - "peeking_take_while", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn 2.0.16", - "which", -] - [[package]] name = "bitflags" version = "1.3.2" @@ -297,15 +274,6 @@ version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - [[package]] name = "cfg-if" version = "1.0.0" @@ -327,17 +295,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "clang-sys" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f" -dependencies = [ - "glob", - "libc", - "libloading", -] - [[package]] name = "clap" version = "4.3.0" @@ -791,12 +748,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "glob" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" - [[package]] name = "hashbrown" version = "0.12.3" @@ -964,7 +915,6 @@ dependencies = [ "hex_color", "humantime-serde", "indexmap", - "iwlib", "libc", "libpulse-binding", "libpulse-tokio", @@ -998,26 +948,6 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" -[[package]] -name = "iwlib" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f7612166a7b3aeb249ce90cae62fc56d57fb5bb01c40cd57ae432530dca3b9d" -dependencies = [ - "bindgen", - "iwlib_sys", - "libc", -] - -[[package]] -name = "iwlib_sys" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a01f0a5bb1a76e2a6dedcc12ab73c3e8bcd282989d5f9ab4795d5c0fbd81e6e" -dependencies = [ - "bindgen", -] - [[package]] name = "js-sys" version = "0.3.63" @@ -1027,34 +957,12 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - [[package]] name = "libc" version = "0.2.146" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b" -[[package]] -name = "libloading" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" -dependencies = [ - "cfg-if", - "winapi", -] - [[package]] name = "libpulse-binding" version = "2.27.1" @@ -1134,12 +1042,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - [[package]] name = "mio" version = "0.8.6" @@ -1194,16 +1096,6 @@ dependencies = [ "static_assertions", ] -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - [[package]] name = "ntapi" version = "0.4.1" @@ -1310,12 +1202,6 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" -[[package]] -name = "peeking_take_while" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" - [[package]] name = "pin-project-lite" version = "0.2.9" @@ -1366,16 +1252,6 @@ dependencies = [ "log", ] -[[package]] -name = "prettyplease" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b69d39aab54d069e7f2fe8cb970493e7834601ca2d8c65fd7bbd183578080d1" -dependencies = [ - "proc-macro2", - "syn 2.0.16", -] - [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -1534,12 +1410,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316" -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - [[package]] name = "rustix" version = "0.37.19" @@ -1647,12 +1517,6 @@ dependencies = [ "digest", ] -[[package]] -name = "shlex" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" - [[package]] name = "signal-hook" version = "0.3.15" @@ -2072,17 +1936,6 @@ version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93" -[[package]] -name = "which" -version = "4.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" -dependencies = [ - "either", - "libc", - "once_cell", -] - [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 63928a5..8d21a58 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,6 @@ futures = "0.3.28" hex_color = { version = "2.0.0", features = ["serde"] } humantime-serde = "1.1.1" indexmap = { version = "1.9.3", features = ["serde"] } -iwlib = "0.1.1" libc = "0.2.142" libpulse-binding = { version = "2.27.1", features = ["pa_v14"] } libpulse-tokio = "0.1.0" diff --git a/sample_config.toml b/sample_config.toml index c652be0..a92f8cf 100644 --- a/sample_config.toml +++ b/sample_config.toml @@ -105,10 +105,13 @@ short_text = "!" [[items]] # "Network Interfaces" item, provides an interactive list of interfaces and ip addresses, as well as -# WiFi signal strength and SSIDs if found. Supports updating itself automatically on network changes -# (via NetworkManager). +# WiFi signal strength and SSIDs if found. Supports updating itself automatically on network changes. type = "nic" +# Optionally pass an `interval` to force updates - since it updates automatically this shouldn't be +# needed. +# interval = "60s" + # Optionally define how connection details will be displayed when connected to a WiFi network # Values are: # - `percent`: e.g., "80% at " (default) @@ -119,9 +122,6 @@ wireless_display = "percent" # at a desired interval. If unset, it only updates when the item itself updates. wireless_refresh_interval = "2m" -# Optionally pass an `interval` to force updates (useful if you don't use NetworkManager) -# interval = "60s" - # Optionally pass a filter. Filters are formatted as `name[:type]`, where `name` is the interface # name, and `type` is an optional part which is either `v4` or `v6`. # diff --git a/src/dbus/mod.rs b/src/dbus/mod.rs index ed3dac9..fa6cb7a 100644 --- a/src/dbus/mod.rs +++ b/src/dbus/mod.rs @@ -1,5 +1,4 @@ pub mod dunst; -pub mod network_manager; pub mod notifications; use tokio::sync::OnceCell; diff --git a/src/dbus/network_manager.rs b/src/dbus/network_manager.rs deleted file mode 100644 index 1ae9b3b..0000000 --- a/src/dbus/network_manager.rs +++ /dev/null @@ -1,218 +0,0 @@ -use zbus::dbus_proxy; -use zbus::zvariant::{DeserializeDict, OwnedObjectPath, OwnedValue, SerializeDict, Type, Value}; - -#[derive(Debug, DeserializeDict, SerializeDict, Value, OwnedValue, Type)] -#[zvariant(signature = "dict")] -pub struct AddressData { - pub address: String, - pub prefix: u32, -} - -#[dbus_proxy( - default_path = "/org/freedesktop/NetworkManager/IP4Config", - default_service = "org.freedesktop.NetworkManager", - interface = "org.freedesktop.NetworkManager.IP4Config", - gen_blocking = false -)] -pub trait NetworkManagerIP4Config { - #[dbus_proxy(property)] - fn address_data(&self) -> zbus::Result>; -} - -impl<'a> NetworkManagerIP4ConfigProxy<'a> { - pub async fn extract_address(&self) -> zbus::Result> { - Ok(self - .address_data() - .await? - .first() - .map(|d| d.address.to_owned())) - } -} - -#[dbus_proxy( - default_path = "/org/freedesktop/NetworkManager/IP6Config", - default_service = "org.freedesktop.NetworkManager", - interface = "org.freedesktop.NetworkManager.IP6Config", - gen_blocking = false -)] -pub trait NetworkManagerIP6Config { - #[dbus_proxy(property)] - fn address_data(&self) -> zbus::Result>; -} - -impl<'a> NetworkManagerIP6ConfigProxy<'a> { - pub async fn extract_address(&self) -> zbus::Result> { - Ok(self - .address_data() - .await? - .first() - .map(|d| d.address.to_owned())) - } -} - -#[dbus_proxy( - default_path = "/org/freedesktop/NetworkManager/ActiveConnection", - default_service = "org.freedesktop.NetworkManager", - interface = "org.freedesktop.NetworkManager.Connection.Active", - gen_blocking = false -)] -trait NetworkManagerActiveConnection { - #[dbus_proxy(property)] - fn vpn(&self) -> zbus::Result; - - #[dbus_proxy(property, name = "Ip4Config")] - fn ip4_config_objpath(&self) -> zbus::Result; - - #[dbus_proxy(property, name = "Ip6Config")] - fn ip6_config_objpath(&self) -> zbus::Result; - - #[dbus_proxy(property, name = "Devices")] - fn devices_objpath(&self) -> zbus::Result>; - - #[dbus_proxy(property)] - fn id(&self) -> zbus::Result; - - #[dbus_proxy(property)] - fn state(&self) -> zbus::Result; - - #[dbus_proxy(property, name = "Type")] - fn typ(&self) -> zbus::Result; -} - -#[dbus_proxy( - default_path = "/org/freedesktop/NetworkManager/Devices", - default_service = "org.freedesktop.NetworkManager", - interface = "org.freedesktop.NetworkManager.Device", - gen_blocking = false -)] -pub trait NetworkManagerDevice { - #[dbus_proxy(property)] - fn interface(&self) -> zbus::Result; - - #[dbus_proxy(property, name = "Ip4Config")] - fn ip4_config_objpath(&self) -> zbus::Result; - - #[dbus_proxy(property, name = "Ip6Config")] - fn ip6_config_objpath(&self) -> zbus::Result; - - #[dbus_proxy(property)] - fn hw_address(&self) -> zbus::Result; -} - -#[dbus_proxy( - default_path = "/org/freedesktop/NetworkManager", - default_service = "org.freedesktop.NetworkManager", - interface = "org.freedesktop.NetworkManager", - gen_blocking = false -)] -pub trait NetworkManager { - #[dbus_proxy(signal)] - fn state_changed(&self) -> zbus::Result<()>; - - #[dbus_proxy(property, name = "ActiveConnections")] - fn active_connections_objpath(&self) -> zbus::Result>; - - #[dbus_proxy(name = "GetAllDevices")] - fn get_all_devices_objpath(&self) -> zbus::Result>; - - #[dbus_proxy(property, name = "AllDevices")] - fn all_devices_objpath(&self) -> zbus::Result>; - - #[dbus_proxy(property)] - fn networking_enabled(&self) -> zbus::Result; - - #[dbus_proxy(property)] - fn wireless_enabled(&self) -> zbus::Result; - - #[dbus_proxy(property)] - fn wireless_hardware_enabled(&self) -> zbus::Result; -} - -/** - * The following macros and uses are a workaround for a limitation in zbus - * See: https://github.com/dbus2/zbus/issues/332 - */ - -macro_rules! impl_object_vec { - ($parent:ident, $child:ident, $($method:ident),+) => { - paste::paste! { - impl<'a> [<$parent Proxy>]<'a> { - pub async fn [](&self, paths: Vec) -> zbus::Result]>> { - let list = futures::future::join_all(paths.into_iter().map(|p| async { - Ok::<_, zbus::Error>( - <[<$child Proxy>]>::builder(self.connection()) - .path(p)? - .build() - .await?, - ) - })) - .await - .into_iter() - .collect::, _>>()?; - - Ok(list) - } - - $( - pub async fn $method(&self) -> zbus::Result]>> { - let paths = self.[<$method _objpath>]().await?; - self.[](paths).await - } - )+ - } - } - }; -} - -impl_object_vec!( - NetworkManager, - NetworkManagerDevice, - get_all_devices, - all_devices -); - -impl_object_vec!( - NetworkManager, - NetworkManagerActiveConnection, - active_connections -); - -impl_object_vec!( - NetworkManagerActiveConnection, - NetworkManagerDevice, - devices -); - -/** - * This is a workaround for a limitation in zbus: the `[dbus_proxy(object = "...")]` attribute - * only works for _methods_ not for _properties_. - */ - -macro_rules! impl_object_prop { - ($parent:ident, $child:ident, $($method:ident),+) => { - paste::paste! { - $(impl<'a> [<$parent Proxy>]<'a> { - pub async fn $method(&self) -> zbus::Result<[<$child Proxy>]> { - let path = self.[<$method _objpath>]().await?; - Ok(<[<$child Proxy>]>::builder(self.connection()) - .path(path)? - .build() - .await?) - } - })+ - } - }; -} - -impl_object_prop!(NetworkManagerDevice, NetworkManagerIP4Config, ip4_config); -impl_object_prop!(NetworkManagerDevice, NetworkManagerIP6Config, ip6_config); -impl_object_prop!( - NetworkManagerActiveConnection, - NetworkManagerIP4Config, - ip4_config -); -impl_object_prop!( - NetworkManagerActiveConnection, - NetworkManagerIP6Config, - ip6_config -); From fae19cc91fc4bc01af6f6e7d56f4238f34f6835f Mon Sep 17 00:00:00 2001 From: acheronfail Date: Fri, 7 Jul 2023 10:20:08 +0930 Subject: [PATCH 14/57] refactor nl80211 to deduplcate some code --- src/util/netlink/nl80211/mod.rs | 224 ++++++++++++++------------------ 1 file changed, 95 insertions(+), 129 deletions(-) diff --git a/src/util/netlink/nl80211/mod.rs b/src/util/netlink/nl80211/mod.rs index 468c479..4f82acc 100644 --- a/src/util/netlink/nl80211/mod.rs +++ b/src/util/netlink/nl80211/mod.rs @@ -15,11 +15,12 @@ mod enums; use std::rc::Rc; +use std::result::Result as StdRes; use neli::consts::nl::{GenlId, NlmF}; use neli::consts::socket::NlFamily; use neli::err::RouterError; -use neli::genl::{AttrTypeBuilder, Genlmsghdr, GenlmsghdrBuilder, NlattrBuilder, NoUserHeader}; +use neli::genl::{AttrTypeBuilder, Genlmsghdr, GenlmsghdrBuilder, NlattrBuilder}; use neli::nl::{NlPayload, Nlmsghdr}; use neli::router::asynchronous::{NlRouter, NlRouterReceiverHandle}; use neli::types::{Buffer, GenlBuffer}; @@ -34,7 +35,7 @@ use self::enums::{ Nl80211StationInfo, }; use super::NetlinkInterface; -use crate::util::{MacAddr, Result as Res}; +use crate::util::{MacAddr, Result}; // init ------------------------------------------------------------------------ @@ -42,14 +43,70 @@ type Nl80211Socket = (NlRouter, NlRouterReceiverHandle> static NL80211_SOCKET: OnceCell = OnceCell::const_new(); static NL80211_FAMILY: OnceCell = OnceCell::const_new(); -async fn init_socket() -> Res { +async fn init_socket() -> Result { Ok(NlRouter::connect(NlFamily::Generic, Some(0), Groups::empty()).await?) } -async fn init_family(socket: &NlRouter) -> Res { +async fn init_family(socket: &NlRouter) -> Result { Ok(socket.resolve_genl_family("nl80211").await?) } +// util ------------------------------------------------------------------------ + +// Explicitly type these since the compiler struggles to infer `neli` types in async contexts. +type Nl80211Payload = Genlmsghdr; +type NextNl80211 = + Option, RouterError>>; + +/// Easily create a `GenlBuffer` with the given attributes and payloads. +macro_rules! attrs { + () => { + GenlBuffer::new() + }; + + ($($attr:ident => $payload:expr$(,)?)+) => {{ + let mut genl_attrs = GenlBuffer::new(); + $( + genl_attrs.push( + NlattrBuilder::default() + .nla_type(AttrTypeBuilder::default().nla_type(Nl80211Attribute::$attr).build()?) + .nla_payload($payload) + .build()? + ); + )+ + + genl_attrs + }}; +} + +/// Send an nl80211 command via generic netlink, and get its response. +/// Build the `attrs` parameter with the `attrs!()` macro. +async fn genl80211_send( + socket: &NlRouter, + cmd: Nl80211Command, + flags: NlmF, + attrs: GenlBuffer, +) -> Result> { + let family_id = NL80211_FAMILY + .get_or_try_init(|| init_family(socket)) + .await?; + + // create generic netlink message + let genl_payload: Nl80211Payload = { + let mut builder = GenlmsghdrBuilder::default().version(1).cmd(cmd); + if !attrs.is_empty() { + builder = builder.attrs(attrs); + } + + builder.build()? + }; + + // send it to netlink + Ok(socket + .send::<_, _, u16, Nl80211Payload>(*family_id, flags, NlPayload::Payload(genl_payload)) + .await?) +} + // impl ------------------------------------------------------------------------ #[derive(Debug)] @@ -68,54 +125,30 @@ pub struct WirelessInfo { pub signal: Option, } -type Payload = Genlmsghdr; -type NextNl80211 = Option, RouterError>>; - impl NetlinkInterface { + /// Get wireless information for this interface (if there is any). pub async fn wireless_info(&self) -> Option { match self.get_wireless_info().await { - Ok(info) => Some(info), + Ok(info) => info, Err(e) => { - log::warn!("NetlinkInterface::wireless_info(): {}", e); + log::error!("NetlinkInterface::wireless_info(): {}", e); None } } } - pub async fn get_wireless_info(&self) -> Res { + /// Gets wireless information for this interface. + /// Returns `None` if the interface was not a wireless interface, or if no wireless information + /// could be found. + async fn get_wireless_info(&self) -> Result> { let (socket, _) = NL80211_SOCKET.get_or_try_init(init_socket).await?; - let family_id = NL80211_FAMILY - .get_or_try_init(|| init_family(socket)) - .await?; - - // prepare generic message attributes - let mut attrs = GenlBuffer::new(); - - // ... the `Nl80211Command::GetScan` command requires the interface index as `Nl80211Attribute::Ifindex` - attrs.push( - NlattrBuilder::default() - .nla_type( - AttrTypeBuilder::default() - .nla_type(Nl80211Attribute::Ifindex) - .build()?, - ) - .nla_payload(self.index) - .build()?, - ); - - let mut recv = socket - .send::<_, _, u16, Genlmsghdr>( - *family_id, - NlmF::ACK | NlmF::REQUEST, - NlPayload::Payload( - GenlmsghdrBuilder::::default() - .cmd(Nl80211Command::GetInterface) - .version(1) - .attrs(attrs) - .build()?, - ), - ) - .await?; + let mut recv = genl80211_send( + socket, + Nl80211Command::GetInterface, + NlmF::ACK | NlmF::REQUEST, + attrs![Ifindex => self.index], + ) + .await?; while let Some(Ok(msg)) = recv.next().await as NextNl80211 { if let NlPayload::Payload(gen_msg) = msg.nl_payload() { @@ -128,7 +161,7 @@ impl NetlinkInterface { attr_handle.get_attr_payload_as::(Nl80211Attribute::Iftype), Ok(Nl80211IfType::Station) ) { - continue; + return Ok(None); } // interface name - not really needed since we'll use the index @@ -181,18 +214,18 @@ impl NetlinkInterface { } }; - return Ok(WirelessInfo { + return Ok(Some(WirelessInfo { index: self.index, interface, mac_addr, ssid, bssid, signal, - }); + })); } } - bail!("no wireless info found for index: {}", self.index); + Ok(None) } } @@ -237,42 +270,14 @@ impl SignalStrength { } /// Get the current BSSID of the connected network (if any) for the given interface -async fn get_bssid(socket: &NlRouter, index: i32) -> Res> { - let family_id = NL80211_FAMILY - .get_or_try_init(|| init_family(socket)) - .await?; - - // TODO: de-duplicate message sending boilerplate (id by, etc) - // prepare generic message attributes - let mut attrs = GenlBuffer::new(); - - // ... the `Nl80211Command::GetScan` command requires the interface index as `Nl80211Attribute::Ifindex` - attrs.push( - NlattrBuilder::default() - .nla_type( - AttrTypeBuilder::default() - .nla_type(Nl80211Attribute::Ifindex) - .build()?, - ) - .nla_payload(index) - .build()?, - ); - - // create generic message - let genl_payload: Genlmsghdr = GenlmsghdrBuilder::default() - .cmd(Nl80211Command::GetScan) - .version(1) - .attrs(attrs) - .build()?; - - // send it to netlink - let mut recv = socket - .send::<_, _, u16, Genlmsghdr>( - *family_id, - NlmF::DUMP, - NlPayload::Payload(genl_payload), - ) - .await?; +async fn get_bssid(socket: &NlRouter, index: i32) -> Result> { + let mut recv = genl80211_send( + socket, + Nl80211Command::GetScan, + NlmF::DUMP, + attrs![Ifindex => index], + ) + .await?; // look for our requested data inside netlink's results while let Some(result) = recv.next().await as NextNl80211 { @@ -309,53 +314,14 @@ async fn get_signal_strength( socket: &NlRouter, index: i32, bssid: &MacAddr, -) -> Res> { - let family_id = NL80211_FAMILY - .get_or_try_init(|| init_family(socket)) - .await?; - - // prepare generic message attributes... - let mut attrs = GenlBuffer::new(); - - // ... the `Nl80211Command::GetStation` command requires the interface index as `Nl80211Attribute::Ifindex`... - attrs.push( - NlattrBuilder::default() - .nla_type( - AttrTypeBuilder::default() - .nla_type(Nl80211Attribute::Ifindex) - .build()?, - ) - .nla_payload(index) - .build()?, - ); - - // ... and also the BSSID as `Nl80211Attribute::Mac` - attrs.push( - NlattrBuilder::default() - .nla_type( - AttrTypeBuilder::default() - .nla_type(Nl80211Attribute::Mac) - .build()?, - ) - .nla_payload(Buffer::from(bssid)) - .build()?, - ); - - // create generic message - let genl_payload: Genlmsghdr = GenlmsghdrBuilder::default() - .cmd(Nl80211Command::GetStation) - .version(1) - .attrs(attrs) - .build()?; - - // send it to netlink - let mut recv = socket - .send::<_, _, u16, Genlmsghdr>( - *family_id, - NlmF::ACK | NlmF::REQUEST, - NlPayload::Payload(genl_payload), - ) - .await?; +) -> Result> { + let mut recv = genl80211_send( + socket, + Nl80211Command::GetStation, + NlmF::ACK | NlmF::REQUEST, + attrs![Ifindex => index, Mac => Buffer::from(bssid)], + ) + .await?; // look for our requested data inside netlink's results while let Some(result) = recv.next().await as NextNl80211 { From fae17034d797648552f20db70336361a1a99d8f1 Mon Sep 17 00:00:00 2001 From: acheronfail Date: Fri, 7 Jul 2023 18:32:32 +0930 Subject: [PATCH 15/57] more minor improvements to nl80211 --- src/util/netlink/nl80211/mod.rs | 52 ++++++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/src/util/netlink/nl80211/mod.rs b/src/util/netlink/nl80211/mod.rs index 4f82acc..7f37553 100644 --- a/src/util/netlink/nl80211/mod.rs +++ b/src/util/netlink/nl80211/mod.rs @@ -42,13 +42,14 @@ use crate::util::{MacAddr, Result}; type Nl80211Socket = (NlRouter, NlRouterReceiverHandle>); static NL80211_SOCKET: OnceCell = OnceCell::const_new(); static NL80211_FAMILY: OnceCell = OnceCell::const_new(); +const NL80211_FAMILY_NAME: &str = "nl80211"; async fn init_socket() -> Result { Ok(NlRouter::connect(NlFamily::Generic, Some(0), Groups::empty()).await?) } async fn init_family(socket: &NlRouter) -> Result { - Ok(socket.resolve_genl_family("nl80211").await?) + Ok(socket.resolve_genl_family(NL80211_FAMILY_NAME).await?) } // util ------------------------------------------------------------------------ @@ -87,7 +88,7 @@ async fn genl80211_send( flags: NlmF, attrs: GenlBuffer, ) -> Result> { - let family_id = NL80211_FAMILY + let family_id = *NL80211_FAMILY .get_or_try_init(|| init_family(socket)) .await?; @@ -103,7 +104,7 @@ async fn genl80211_send( // send it to netlink Ok(socket - .send::<_, _, u16, Nl80211Payload>(*family_id, flags, NlPayload::Payload(genl_payload)) + .send::<_, _, u16, Nl80211Payload>(family_id, flags, NlPayload::Payload(genl_payload)) .await?) } @@ -242,10 +243,10 @@ pub struct SignalStrength { } impl SignalStrength { - pub fn new(dbm: i8, link: u8) -> SignalStrength { + pub fn new(dbm: i8) -> SignalStrength { SignalStrength { dbm, - link, + link: 110_u8.wrapping_add(dbm as u8), quality: std::cell::OnceCell::new(), } } @@ -260,10 +261,10 @@ impl SignalStrength { (if self.dbm < -110 { 0_f32 } else if self.dbm > -40 { - 100_f32 + 1_f32 } else { // lerp between -70 and 0 - (70.0 - (self.dbm + 40).abs() as f32) / 70.0 + 1.0 - ((self.dbm as f32 + 40.0) / -70.0) }) * 100.0 }) } @@ -284,6 +285,7 @@ async fn get_bssid(socket: &NlRouter, index: i32) -> Result> { match result { Ok(msg) => { if let NlPayload::Payload(gen_msg) = msg.nl_payload() { + // TODO: remove mut when upstream merges https://github.com/jbaublitz/neli/pull/220 let mut attr_handle = gen_msg.attrs().get_attr_handle(); if let Ok(bss_attrs) = @@ -300,7 +302,7 @@ async fn get_bssid(socket: &NlRouter, index: i32) -> Result> { } } Err(e) => { - log::error!("Nl80211Command::GetStation error: {}", e); + log::error!("Nl80211Command::GetScan error: {}", e); continue; } } @@ -328,20 +330,16 @@ async fn get_signal_strength( match result { Ok(msg) => { if let NlPayload::Payload(gen_msg) = msg.nl_payload() { + // TODO: remove mut when upstream merges https://github.com/jbaublitz/neli/pull/220 let mut attr_handle = gen_msg.attrs().get_attr_handle(); - // FIXME: upstream - I don't think this needs to be mutable... if let Ok(station_info) = attr_handle .get_nested_attributes::(Nl80211Attribute::StaInfo) { if let Ok(signal) = station_info.get_attr_payload_as::(Nl80211StationInfo::Signal) { - // this is the same as `/proc/net/wireless`'s `link` - let link = 110_u8.wrapping_add(signal); - // this is the same as `/proc/net/wireless`'s `level` - let dbm = signal as i8; - return Ok(Some(SignalStrength::new(dbm, link))); + return Ok(Some(SignalStrength::new(signal as i8))); } } } @@ -355,3 +353,29 @@ async fn get_signal_strength( Ok(None) } + +#[cfg(test)] +mod tests { + use super::*; + + // signal strength tests --------------------------------------------------- + + #[test] + fn signal_strength_quality() { + let quality = |dbm| SignalStrength::new(dbm).quality() as u8; + + // anything at or below -110 should be 0% + assert_eq!(0, quality(-120)); + assert_eq!(0, quality(-110)); + // lerping between -70 and 0 + assert_eq!(25, quality(-92)); + assert_eq!(50, quality(-75)); + assert_eq!(75, quality(-57)); + assert_eq!(85, quality(-50)); + // anything at or above -40 should be 100% + assert_eq!(100, quality(-40)); + assert_eq!(100, quality(-1)); + assert_eq!(100, quality(0)); + assert_eq!(100, quality(100)); + } +} From fae1eb1e3c6b71b4972a92895226d5d13a41310a Mon Sep 17 00:00:00 2001 From: acheronfail Date: Fri, 7 Jul 2023 19:17:31 +0930 Subject: [PATCH 16/57] add some results and make EnumCycle safer --- src/bar_items/battery.rs | 2 +- src/bar_items/disk.rs | 6 +++-- src/bar_items/mem.rs | 4 +-- src/bar_items/net_usage.rs | 4 +-- src/bar_items/nic.rs | 2 +- src/dbus/notifications.rs | 1 + src/util/enum_cycle.rs | 53 +++++++++++++++++++++++++++++++------- src/util/paginator.rs | 21 +++++++++------ 8 files changed, 68 insertions(+), 25 deletions(-) diff --git a/src/bar_items/battery.rs b/src/bar_items/battery.rs index 9d266d9..d87329b 100644 --- a/src/bar_items/battery.rs +++ b/src/bar_items/battery.rs @@ -179,7 +179,7 @@ impl BarItem for Battery { if batteries.len() == 0 { bail!("no batteries found"); } else { - p.set_len(batteries.len()); + p.set_len(batteries.len())?; } let dbus = dbus_connection(BusType::Session).await?; diff --git a/src/bar_items/disk.rs b/src/bar_items/disk.rs index 38a9cdd..e99c381 100644 --- a/src/bar_items/disk.rs +++ b/src/bar_items/disk.rs @@ -1,5 +1,4 @@ use std::collections::HashSet; -use crate::error::Result; use std::path::PathBuf; use std::time::Duration; @@ -10,6 +9,7 @@ use serde_derive::{Deserialize, Serialize}; use sysinfo::{Disk as SysDisk, DiskExt, SystemExt}; use crate::context::{BarItem, Context, StopAction}; +use crate::error::Result; use crate::i3::{I3Item, I3Markup}; use crate::theme::Theme; use crate::util::Paginator; @@ -83,7 +83,7 @@ impl BarItem for Disk { }; let len = stats.len(); if len > 0 { - p.set_len(len); + p.set_len(len)?; let disk = &stats[p.idx()]; let theme = &ctx.config.theme; @@ -97,6 +97,8 @@ impl BarItem for Disk { } ctx.update_item(item).await?; + } else { + ctx.update_item(I3Item::empty()).await?; } // cycle through disks diff --git a/src/bar_items/mem.rs b/src/bar_items/mem.rs index 0595c9f..dbf9a21 100644 --- a/src/bar_items/mem.rs +++ b/src/bar_items/mem.rs @@ -1,4 +1,3 @@ -use crate::error::Result; use std::time::Duration; use async_trait::async_trait; @@ -9,6 +8,7 @@ use strum::EnumIter; use sysinfo::SystemExt; use crate::context::{BarEvent, BarItem, Context, StopAction}; +use crate::error::Result; use crate::i3::{I3Button, I3Item, I3Markup}; use crate::theme::Theme; use crate::util::format::{float, FloatFormat}; @@ -47,7 +47,7 @@ impl Mem { impl BarItem for Mem { async fn start(&self, mut ctx: Context) -> Result { let mut total = None; - let mut display = EnumCycle::new_at(self.display); + let mut display = EnumCycle::new_at(self.display)?; loop { let (available, total) = { ctx.state.sys.refresh_memory(); diff --git a/src/bar_items/net_usage.rs b/src/bar_items/net_usage.rs index df74657..cc675aa 100644 --- a/src/bar_items/net_usage.rs +++ b/src/bar_items/net_usage.rs @@ -1,4 +1,3 @@ -use crate::error::Result; use std::time::Duration; use async_trait::async_trait; @@ -10,6 +9,7 @@ use sysinfo::{NetworkExt, NetworksExt, SystemExt}; use tokio::time::Instant; use crate::context::{BarEvent, BarItem, Context, StopAction}; +use crate::error::Result; use crate::i3::{I3Button, I3Item, I3Markup}; use crate::theme::Theme; use crate::util::EnumCycle; @@ -112,7 +112,7 @@ impl BarItem for NetUsage { ) }; - let mut display = EnumCycle::new_at(self.display); + let mut display = EnumCycle::new_at(self.display)?; let div_as_u64 = |u, f| (u as f64 / f) as u64; let mut last_check = Instant::now(); diff --git a/src/bar_items/nic.rs b/src/bar_items/nic.rs index 0c80e4d..61ccadb 100644 --- a/src/bar_items/nic.rs +++ b/src/bar_items/nic.rs @@ -183,7 +183,6 @@ impl BarItem for Nic { // wait for network changes Ok(list) = net.wait_for_change() => { connections = Connections::new(list.filtered(&self.filter)); - p.set_len(connections.len()); }, // on any bar event Some(event) = ctx.wait_for_event(self.interval) => { @@ -206,6 +205,7 @@ impl BarItem for Nic { // TODO: differentiate between empty after filtering, and completely disconnected? I3Item::new("inactive").color(ctx.config.theme.dim) } else { + p.set_len(connections.len())?; let theme = &ctx.config.theme; // SAFETY(unwrap): we always set the paginator's length to the connection's length // so it should always be within bounds diff --git a/src/dbus/notifications.rs b/src/dbus/notifications.rs index 1fe006c..6f426d9 100644 --- a/src/dbus/notifications.rs +++ b/src/dbus/notifications.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use zbus::dbus_proxy; use zbus::zvariant::Value; +// TODO: share a single proxy instance of this, and use it wherever notifications are needed? #[dbus_proxy( default_path = "/org/freedesktop/Notifications", default_service = "org.freedesktop.Notifications", diff --git a/src/util/enum_cycle.rs b/src/util/enum_cycle.rs index 1d77e04..9202ff2 100644 --- a/src/util/enum_cycle.rs +++ b/src/util/enum_cycle.rs @@ -2,46 +2,81 @@ use std::iter::Peekable; use strum::IntoEnumIterator; +use crate::error::Result; + +/// A safe wrapper for an iterator that cycles endlessly. pub struct EnumCycle { inner: Peekable>>, } impl EnumCycle { - pub fn new() -> EnumCycle { + /// Creates a new `EnumCycle` for the given type. + /// Returns an error if the enum doesn't have at least one variant. + pub fn new() -> Result> { let all = T::iter().collect::>(); - EnumCycle { - inner: (Box::new(all.into_iter().cycle()) as Box>).peekable(), + if all.is_empty() { + bail!("enum to cycle must contain at least one variant!"); } + + Ok(EnumCycle { + inner: (Box::new(all.into_iter().cycle()) as Box>).peekable(), + }) } pub fn current(&mut self) -> &T { + // SAFETY: `self.inner` is a cycling iterator that has at least one variant self.inner.peek().unwrap() } pub fn next(&mut self) -> T { + // SAFETY: `self.inner` is a cycling iterator that has at least one variant self.inner.next().unwrap() } } impl EnumCycle { - pub fn new_at(start: T) -> EnumCycle { - let mut me = Self::new(); + /// Creates a new `EnumCycle` for the given type, starting at the given variant. + /// Returns an error if the enum doesn't have at least one variant. + pub fn new_at(start: T) -> Result> { + let mut me = Self::new()?; while *me.current() != start { me.next(); } - me + Ok(me) } } impl EnumCycle { - pub fn new_at_default() -> EnumCycle { + /// Creates a new `EnumCycle` for the given type, starting at the default variant. + /// Returns an error if the enum doesn't have at least one variant. + pub fn new_at_default() -> Result> { let start = T::default(); - let mut me = Self::new(); + let mut me = Self::new()?; while *me.current() != start { me.next(); } - me + Ok(me) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn empty_iterator() { + #[derive(Debug, Clone, strum::EnumIter)] + enum Empty {} + + // should not allow an empty iterator + match EnumCycle::::new() { + Ok(_) => panic!("should not be Ok"), + Err(e) => assert_eq!( + e.to_string(), + "enum to cycle must contain at least one variant!" + ), + } } } diff --git a/src/util/paginator.rs b/src/util/paginator.rs index 201132f..6a851cf 100644 --- a/src/util/paginator.rs +++ b/src/util/paginator.rs @@ -1,5 +1,6 @@ use super::fraction; use crate::context::BarEvent; +use crate::error::Result; use crate::i3::I3Button::*; use crate::theme::Theme; @@ -21,15 +22,19 @@ impl Paginator { self.idx } - pub fn set_len(&mut self, len: usize) { + /// Set the length of the paginator. + /// Returns an error if `len == 0`, which is invalid. + pub fn set_len(&mut self, len: usize) -> Result<()> { if len == 0 { - panic!("a Paginator's length must be > 0"); + bail!("a Paginator's length must be > 0"); } self.len = len; if self.idx >= len { self.idx = 0; } + + Ok(()) } fn incr(&mut self) { @@ -61,22 +66,22 @@ mod paginator_tests { use super::*; #[test] - #[should_panic] + #[should_panic(expected = "a Paginator's length must be > 0")] fn set_len0() { let mut p = Paginator::new(); - p.set_len(0); + p.set_len(0).unwrap(); } #[test] fn forward_wrap() { let mut p = Paginator::new(); - p.set_len(1); + p.set_len(1).unwrap(); assert_eq!(p.idx(), 0); p.incr(); assert_eq!(p.idx(), 0); - p.set_len(2); + p.set_len(2).unwrap(); p.incr(); assert_eq!(p.idx(), 1); p.incr(); @@ -91,12 +96,12 @@ mod paginator_tests { fn backward_wrap() { let mut p = Paginator::new(); - p.set_len(1); + p.set_len(1).unwrap(); assert_eq!(p.idx(), 0); p.decr(); assert_eq!(p.idx(), 0); - p.set_len(2); + p.set_len(2).unwrap(); p.decr(); assert_eq!(p.idx(), 1); p.decr(); From fae1fa13084ff4aec66745fee2c756685d3a196b Mon Sep 17 00:00:00 2001 From: acheronfail Date: Fri, 7 Jul 2023 19:09:52 +0930 Subject: [PATCH 17/57] set DEBUG=true when testing in CI --- .github/workflows/rust.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 0ca742e..ea6388c 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -22,6 +22,6 @@ jobs: - run: just setup - run: just build - - run: just test + - run: DEBUG=true just test # TODO: build and upload to release tag From fae133fd6a75926a9140e9780b88e2ff31594817 Mon Sep 17 00:00:00 2001 From: acheronfail Date: Fri, 7 Jul 2023 18:23:43 +0930 Subject: [PATCH 18/57] default `just install` to release mode, but add args option --- justfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/justfile b/justfile index 4780e06..a04a38e 100644 --- a/justfile +++ b/justfile @@ -39,8 +39,8 @@ run bin *args: cargo lrun --bin istat-{{bin}} -- "$@" # install locally, copy sample configuration and restart i3 -install: - cargo install --debug --offline --path . +install *args: + cargo install --offline --path . "$@" mkdir -p ~/.config/istat/ -cp --no-clobber ./sample_config.toml ~/.config/istat/config.toml i3-msg restart From fae13ac475e7828e5bc565aa2ece45b77cca9284 Mon Sep 17 00:00:00 2001 From: acheronfail Date: Fri, 7 Jul 2023 18:29:27 +0930 Subject: [PATCH 19/57] immediately update the bar when the theme is changed via ipc --- scripts/i3.conf | 6 +++++- src/dispatcher.rs | 24 +++++++++++++++------- src/ipc/client.rs | 1 + src/main.rs | 51 ++++++++++++++++++++++++++++------------------- 4 files changed, 54 insertions(+), 28 deletions(-) diff --git a/scripts/i3.conf b/scripts/i3.conf index 0137439..0128d1a 100644 --- a/scripts/i3.conf +++ b/scripts/i3.conf @@ -26,6 +26,9 @@ bindsym 9 exec --no-startup-id pkill -RTMIN+9 istat # theme ipc bindsym p exec istat-ipc --socket /tmp/istat-socket.dev set-theme /powerline_enable true bindsym shift+p exec istat-ipc --socket /tmp/istat-socket.dev set-theme /powerline_enable false +# same but for the second bar +bindsym ctrl+p exec istat-ipc --socket /tmp/istat-socket-2.dev set-theme /powerline_enable true +bindsym ctrl+shift+p exec istat-ipc --socket /tmp/istat-socket-2.dev set-theme /powerline_enable false # custom ipc bindsym bracketleft exec istat-ipc --socket /tmp/istat-socket.dev custom pulse volume-down sink @@ -68,7 +71,8 @@ bar { binding_mode #c67bb9 #b48ead #2e3440 } } -# a second bar to make sure everything is working + +# a second bar to make sure multiple instances work bar { font $bar_font position bottom diff --git a/src/dispatcher.rs b/src/dispatcher.rs index 77df9c6..71575f7 100644 --- a/src/dispatcher.rs +++ b/src/dispatcher.rs @@ -7,27 +7,36 @@ use crate::error::Result; #[derive(Debug, Clone)] pub struct Dispatcher { - inner: Vec>>, + bar_senders: Vec>>, + bar_updater: Sender<()>, } impl Dispatcher { - pub fn new(capacity: usize) -> Dispatcher { + pub fn new(bar_updater: Sender<()>, capacity: usize) -> Dispatcher { Dispatcher { - inner: vec![None; capacity], + bar_senders: vec![None; capacity], + bar_updater, } } pub fn remove(&mut self, idx: usize) { - self.inner[idx] = None; + self.bar_senders[idx] = None; } pub fn set(&mut self, idx: usize, tx: Sender) { - self.inner[idx] = Some(tx); + self.bar_senders[idx] = Some(tx); } + /// Tell the bar to manually emit an update + pub async fn manual_bar_update(&self) -> Result<()> { + self.bar_updater.send(()).await?; + Ok(()) + } + + /// Send `BarEvent::Signal` to all bar items pub async fn signal_all(&self) -> Result<()> { Ok(join_all( - self.inner + self.bar_senders .iter() .enumerate() .filter_map(|(i, o)| o.as_ref().map(|_| self.send_bar_event(i, BarEvent::Signal))), @@ -41,8 +50,9 @@ impl Dispatcher { })) } + /// Send the given `BarEvent` to the item at the given index pub async fn send_bar_event(&self, idx: usize, ev: BarEvent) -> Result<()> { - match self.inner.get(idx) { + match self.bar_senders.get(idx) { Some(Some(tx)) => { // if the channel fills up (the bar never reads click events), since this is a bounded channel // sending the event would block forever, so just drop the event diff --git a/src/ipc/client.rs b/src/ipc/client.rs index 5fb5e7c..c7446df 100644 --- a/src/ipc/client.rs +++ b/src/ipc/client.rs @@ -101,6 +101,7 @@ async fn handle_ipc_request(stream: &UnixStream, mut ctx: IpcContext, len: usize Err(e) => IpcReply::Result(IpcResult::Failure(e.to_string())), }; send_ipc_response(&stream, &reply).await?; + ctx.dispatcher.manual_bar_update().await?; } IpcMessage::RefreshAll => { ctx.dispatcher.signal_all().await?; diff --git a/src/main.rs b/src/main.rs index 2a9da59..550e869 100644 --- a/src/main.rs +++ b/src/main.rs @@ -39,7 +39,7 @@ fn start_runtime() -> Result { let (result, runtime) = local_block_on(async_main(args))?; - // NOTE: we use tokio's stdin implementation which spawns a background thread and blocks, + // NOTE: since we use tokio's stdin implementation which spawns a background thread and blocks, // we have to shutdown the runtime ourselves here. If we didn't, then when the runtime is // dropped it would block indefinitely until that background thread unblocked (i.e., another // JSON line from i3). @@ -94,8 +94,9 @@ fn setup_i3_bar(config: &RcCell) -> Result<(RcCell>, RcCe // A list of items which represents the i3 bar let bar = RcCell::new(vec![I3Item::empty(); item_count]); - // Used to send events to each bar item - let dispatcher = RcCell::new(Dispatcher::new(item_count)); + // Used to send events to each bar item, and also to trigger updates of the bar + let (update_tx, update_rx) = mpsc::channel(1); + let dispatcher = RcCell::new(Dispatcher::new(update_tx, item_count)); // Used by items to send updates back to the bar let (item_tx, item_rx) = mpsc::channel(item_count + 1); @@ -183,7 +184,7 @@ fn setup_i3_bar(config: &RcCell) -> Result<(RcCell>, RcCe } // setup listener for handling item updates and printing the bar to STDOUT - handle_item_updates(config.clone(), item_rx, bar.clone())?; + handle_item_updates(config.clone(), item_rx, update_rx, bar.clone())?; Ok((bar, dispatcher)) } @@ -191,7 +192,8 @@ fn setup_i3_bar(config: &RcCell) -> Result<(RcCell>, RcCe // task to manage updating the bar and printing it as JSON fn handle_item_updates( config: RcCell, - mut rx: Receiver<(I3Item, usize)>, + mut item_rx: Receiver<(I3Item, usize)>, + mut update_rx: Receiver<()>, mut bar: RcCell>, ) -> Result<()> { // output first parts of the i3 bar protocol - the header @@ -202,25 +204,32 @@ fn handle_item_updates( tokio::task::spawn_local(async move { let item_names = config.item_idx_to_name(); - while let Some((i3_item, idx)) = rx.recv().await { - let mut i3_item = i3_item - // the name of the item - .name(item_names[idx].clone()) - // always override the bar item's `instance`, since we track that ourselves - .instance(idx.to_string()); + loop { + tokio::select! { + // a manual update was requested + Some(()) = update_rx.recv() => {} + // an item is requesting an update, update the bar state + Some((i3_item, idx)) = item_rx.recv() => { + let mut i3_item = i3_item + // the name of the item + .name(item_names[idx].clone()) + // always override the bar item's `instance`, since we track that ourselves + .instance(idx.to_string()); + + if let Some(separator) = config.items[idx].common.separator { + i3_item = i3_item.separator(separator); + } - if let Some(separator) = config.items[idx].common.separator { - i3_item = i3_item.separator(separator); - } + // don't bother doing anything if the item hasn't changed + if bar[idx] == i3_item { + continue; + } - // don't bother doing anything if the item hasn't changed - if bar[idx] == i3_item { - continue; + // update item in bar + bar[idx] = i3_item; + } } - // update item in bar - bar[idx] = i3_item; - // serialise to JSON let theme = config.theme.clone(); let bar_json = match theme.powerline_enable { @@ -234,7 +243,9 @@ fn handle_item_updates( // print bar to STDOUT for i3 match bar_json { + // make sure to include the trailing comma `,` as part of the protocol Ok(json) => println!("{},", json), + // on any serialisation error, emit an error that will be drawn to the status bar Err(e) => { log::error!("failed to serialise bar to json: {}", e); println!( From fae1554bece171d974bb0c8520181e3617d6370a Mon Sep 17 00:00:00 2001 From: acheronfail Date: Sat, 8 Jul 2023 12:52:41 +0930 Subject: [PATCH 20/57] add _powerline_sep to item json for local dev loop --- scripts/node/dev.ts | 10 +++++++--- src/i3/ipc.rs | 2 +- src/main.rs | 3 ++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/scripts/node/dev.ts b/scripts/node/dev.ts index f97c809..2dc64cc 100755 --- a/scripts/node/dev.ts +++ b/scripts/node/dev.ts @@ -298,8 +298,12 @@ let instances: { name: string; id: string }[] = []; const sep = chalk.gray('|'); function formatLine(line: string) { const items: Record[] = JSON.parse(line.slice(0, -1)); - // TODO: this assumes that an item without a name is a separator, probably should make this clearer somehow - instances = items.filter((i) => i.name).map((i) => ({ name: i.name, id: i.instance })); + // extract bar item instances from JSON output + instances = items + // filter out powerline separator items + .filter((i) => !i._powerline_sep) + .map((i) => ({ name: i.name, id: i.instance })); + const getText = (item: Record) => item[`${display}_text`] || item.full_text; let result: string[] = []; @@ -347,7 +351,7 @@ function formatLine(line: string) { } } - // TODO: is there even a way to draw a border in the terminal? + // NOTE: AFAICT there's no way to draw a border in the terminal, so we can't display that here result.push(c(item.color, item.background, item.urgent)(root.textContent)); if (hasSeparator && i < items.length - 1) result.push(sep); diff --git a/src/i3/ipc.rs b/src/i3/ipc.rs index eb946b1..496ac4b 100644 --- a/src/i3/ipc.rs +++ b/src/i3/ipc.rs @@ -29,8 +29,8 @@ pub async fn handle_click_events(dispatcher: RcCell) -> Result(&line)?; - log::trace!("i3 click: {:?}", click); // parse bar item index from the "instance" property let idx = match click.instance.as_ref() { diff --git a/src/main.rs b/src/main.rs index 550e869..e193119 100644 --- a/src/main.rs +++ b/src/main.rs @@ -286,7 +286,8 @@ where .separator(false) .markup(I3Markup::Pango) .separator_block_width_px(0) - .color(c2.bg); + .color(c2.bg) + .with_data("powerline_sep", true.into()); // the first separator doesn't blend with any other item if i > 0 { From fae1ffe00064bb3f0630fc208ae287ea3ed3b0d5 Mon Sep 17 00:00:00 2001 From: acheronfail Date: Sat, 8 Jul 2023 14:06:23 +0930 Subject: [PATCH 21/57] update pulse custom ipc to return correct responses + set default sink/source --- src/bar_items/pulse/custom.rs | 85 ++++++++++++++++++++---- src/bar_items/pulse/mod.rs | 117 ++++++++++++++++++++++------------ src/ipc/client.rs | 6 +- 3 files changed, 152 insertions(+), 56 deletions(-) diff --git a/src/bar_items/pulse/custom.rs b/src/bar_items/pulse/custom.rs index 688fbb3..d64e2fc 100644 --- a/src/bar_items/pulse/custom.rs +++ b/src/bar_items/pulse/custom.rs @@ -45,7 +45,7 @@ enum PulseCommand { VolumeSet { what: Object, vol: u32 }, Mute { what: Object, mute: Bool }, MuteToggle { what: Object }, - // TODO: set default object with idx or name + SetDefault { what: Object, name: String }, } #[derive(Debug, Serialize, Deserialize)] @@ -70,7 +70,32 @@ impl InOut { } impl RcCell { - pub fn handle_custom_message(&self, args: Vec, tx: oneshot::Sender) { + // NOTE: since pulse's callback API requires `FnMut`, but `oneshot::tx.send` consumes itself + // we wrap it in an option so it's only send once. This should be fine, because pulse only runs + // this callback once anyway. + fn custom_responder(tx: oneshot::Sender, f: F) -> impl FnMut(bool) + 'static + where + F: FnOnce() -> String + 'static, + { + let mut tx = Some(tx); + let mut f = Some(f); + move |success| match (tx.take(), f.take()) { + (Some(tx), Some(f)) => { + let _ = tx.send(CustomResponse::Json(json!(match success { + true => PulseResponse::Success, + false => PulseResponse::Failure(f()), + }))); + } + _ => {} + } + } + + pub fn handle_custom_message( + &mut self, + args: Vec, + tx: oneshot::Sender, + ) { + // TODO: send pulse response from pulse success callbacks for all "success" responses let resp = match PulseCommand::try_parse_from(args) { Ok(cmd) => { let resp = match cmd { @@ -89,24 +114,60 @@ impl RcCell { } }, PulseCommand::VolumeUp { what } => { - self.set_volume(what, Vol::Incr(self.increment)); - PulseResponse::Success + return self.set_volume( + what, + Vol::Incr(self.increment), + Self::custom_responder(tx, move || { + format!("failed to increment {} volume", what) + }), + ); } PulseCommand::VolumeDown { what } => { - self.set_volume(what, Vol::Decr(self.increment)); - PulseResponse::Success + return self.set_volume( + what, + Vol::Decr(self.increment), + Self::custom_responder(tx, move || { + format!("failed to decrement {} volume", what) + }), + ); } PulseCommand::VolumeSet { what, vol } => { - self.set_volume(what, Vol::Set(vol)); - PulseResponse::Success + return self.set_volume( + what, + Vol::Set(vol), + Self::custom_responder(tx, move || { + format!("failed to set {} volume", what) + }), + ); } PulseCommand::Mute { what, mute } => { - self.set_mute(what, mute.into()); - PulseResponse::Success + return self.set_mute( + what, + mute.into(), + Self::custom_responder(tx, move || { + format!("failed to set mute for {}", what) + }), + ); } PulseCommand::MuteToggle { what } => { - self.toggle_mute(what); - PulseResponse::Success + return self.toggle_mute( + what, + Self::custom_responder(tx, move || { + format!("failed to toggle mute for {}", what) + }), + ); + } + PulseCommand::SetDefault { what, name } => { + return self.set_default( + what, + name.clone(), + Self::custom_responder(tx, move || { + format!( + "failed to set default {} to {}, is the name right?", + what, name + ) + }), + ); } }; diff --git a/src/bar_items/pulse/mod.rs b/src/bar_items/pulse/mod.rs index d7b07bc..bb682db 100644 --- a/src/bar_items/pulse/mod.rs +++ b/src/bar_items/pulse/mod.rs @@ -1,7 +1,6 @@ mod custom; -use crate::error::Result; -use std::fmt::Debug; +use std::fmt::{Debug, Display}; use std::process; use std::rc::Rc; @@ -30,6 +29,7 @@ use tokio::sync::mpsc::{self, UnboundedSender}; use crate::context::{BarEvent, BarItem, Context, StopAction}; use crate::dbus::notifications::NotificationsProxy; use crate::dbus::{dbus_connection, BusType}; +use crate::error::Result; use crate::i3::{I3Button, I3Item, I3Markup, I3Modifier}; use crate::theme::Theme; use crate::util::{exec, RcCell}; @@ -40,6 +40,13 @@ pub enum Object { Sink, } +impl Display for Object { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = >>::into(*self); + f.write_str(&s) + } +} + impl From for Rc { fn from(value: Object) -> Self { match value { @@ -267,26 +274,18 @@ macro_rules! impl_pa_methods { self.[<$name s>].retain(|s| s.index == idx); } - fn [](&self, idx: u32, mute: bool) { - let inner = self.clone(); + fn [](&self, idx: u32, mute: bool, f: F) + where F: FnMut(bool) + 'static, + { let mut inspect = self.pa_ctx.introspect(); - inspect.[](idx, mute, Some(Box::new(move |success| { - if !success { - let io = inner.get_io_by_idx(idx); - log::error!("set_mute_{} failed: idx={}, io={:?}", stringify!(name), idx, io); - } - }))); + inspect.[](idx, mute, Some(Box::new(f))); } - fn [](&self, idx: u32, cv: &ChannelVolumes) { - let inner = self.clone(); + fn [](&self, idx: u32, cv: &ChannelVolumes, f: F) + where F: FnMut(bool) + 'static, + { let mut inspect = self.pa_ctx.introspect(); - inspect.[](idx, cv, Some(Box::new(move |success| { - if !success { - let io = inner.get_io_by_idx(idx); - log::error!("set_volume_{} failed: idx={}, io={:?}", stringify!(name), idx, io); - } - }))); + inspect.[](idx, cv, Some(Box::new(f))); } } }; @@ -296,14 +295,6 @@ impl RcCell { impl_pa_methods!(sink); impl_pa_methods!(source); - fn get_io_by_idx(&self, idx: u32) -> Option { - self.sinks - .iter() - .chain(self.sources.iter()) - .find(|p| p.index == idx) - .cloned() - } - fn default_sink(&self) -> Option { self.sinks .iter() @@ -409,14 +400,17 @@ impl RcCell { cv } - fn set_volume(&self, what: Object, vol: Vol) { + fn set_volume(&self, what: Object, vol: Vol, f: F) + where + F: FnMut(bool) + 'static, + { (match what { Object::Sink => self.default_sink().map(|mut p| { - self.set_volume_sink(p.index, self.update_volume(&mut p.volume, vol)); + self.set_volume_sink(p.index, self.update_volume(&mut p.volume, vol), f); p }), Object::Source => self.default_source().map(|mut p| { - self.set_volume_source(p.index, self.update_volume(&mut p.volume, vol)); + self.set_volume_source(p.index, self.update_volume(&mut p.volume, vol), f); p }), }) @@ -425,16 +419,19 @@ impl RcCell { }); } - fn set_mute(&self, what: Object, mute: bool) { + fn set_mute(&self, what: Object, mute: bool, f: F) + where + F: FnMut(bool) + 'static, + { (match what { Object::Sink => self.default_sink().map(|mut p| { p.mute = mute; - self.set_mute_sink(p.index, p.mute); + self.set_mute_sink(p.index, p.mute, f); p }), Object::Source => self.default_source().map(|mut p| { p.mute = mute; - self.set_mute_source(p.index, p.mute); + self.set_mute_source(p.index, p.mute, f); p }), }) @@ -443,16 +440,19 @@ impl RcCell { }); } - fn toggle_mute(&self, what: Object) { + fn toggle_mute(&self, what: Object, f: F) + where + F: FnMut(bool) + 'static, + { (match what { Object::Sink => self.default_sink().map(|mut p| { p.mute = !p.mute; - self.set_mute_sink(p.index, p.mute); + self.set_mute_sink(p.index, p.mute, f); p }), Object::Source => self.default_source().map(|mut p| { p.mute = !p.mute; - self.set_mute_source(p.index, p.mute); + self.set_mute_source(p.index, p.mute, f); p }), }) @@ -461,6 +461,17 @@ impl RcCell { }); } + fn set_default(&mut self, what: Object, name: impl AsRef, f: F) + where + F: FnMut(bool) + 'static, + { + let name = name.as_ref(); + match what { + Object::Sink => self.pa_ctx.set_default_sink(name, f), + Object::Source => self.pa_ctx.set_default_source(name, f), + }; + } + fn update_item(&self) { let (default_sink, default_source) = match (self.default_sink(), self.default_source()) { (Some(sink), Some(source)) => (sink, source), @@ -706,24 +717,48 @@ impl BarItem for Pulse { // source I3Button::Middle if click.modifiers.contains(&I3Modifier::Shift) => { - inner.toggle_mute(Object::Source); + inner.toggle_mute(Object::Source, |success| { + if !success { + log::warn!("failed to toggle mute for default {}", Object::Source); + } + }); }, I3Button::ScrollUp if click.modifiers.contains(&I3Modifier::Shift) => { - inner.set_volume(Object::Source, Vol::Incr(inner.increment)); + inner.set_volume(Object::Source, Vol::Incr(inner.increment), |success| { + if !success { + log::warn!("failed to increment volume for default {}", Object::Source); + } + }); } I3Button::ScrollDown if click.modifiers.contains(&I3Modifier::Shift) => { - inner.set_volume(Object::Source, Vol::Decr(inner.increment)); + inner.set_volume(Object::Source, Vol::Decr(inner.increment), |success| { + if !success { + log::warn!("failed to decrement volume for default {}", Object::Source); + } + }); } // sink I3Button::Middle => { - inner.toggle_mute(Object::Sink); + inner.toggle_mute(Object::Sink, |success| { + if !success { + log::warn!("failed to toggle mute for default {}", Object::Sink); + } + }); }, I3Button::ScrollUp => { - inner.set_volume(Object::Sink, Vol::Incr(inner.increment)); + inner.set_volume(Object::Sink, Vol::Incr(inner.increment), |success| { + if !success { + log::warn!("failed to increment volume for default {}", Object::Sink); + } + }); } I3Button::ScrollDown => { - inner.set_volume(Object::Sink, Vol::Decr(inner.increment)); + inner.set_volume(Object::Sink, Vol::Decr(inner.increment), |success| { + if !success { + log::warn!("failed to decrement volume for default {}", Object::Sink); + } + }); } } _ => {} diff --git a/src/ipc/client.rs b/src/ipc/client.rs index c7446df..27973e5 100644 --- a/src/ipc/client.rs +++ b/src/ipc/client.rs @@ -156,9 +156,9 @@ async fn handle_ipc_request(stream: &UnixStream, mut ctx: IpcContext, len: usize Some(rx) => match rx.await { Ok(CustomResponse::Help(help)) => IpcReply::Help(help.ansi().to_string()), Ok(CustomResponse::Json(value)) => IpcReply::Value(value), - Err(_) => { - IpcReply::Result(IpcResult::Failure("not listening for events".into())) - } + Err(_) => IpcReply::Result(IpcResult::Failure( + "bar item not listening for response".into(), + )), }, None => IpcReply::Result(IpcResult::Success(None)), }, From fae19b33997ffdf1f7e7bcbfc05f1d5a222b79f5 Mon Sep 17 00:00:00 2001 From: acheronfail Date: Sat, 8 Jul 2023 19:45:37 +0930 Subject: [PATCH 22/57] workaround a nasty bug where neli would block forever This could be a bug upstream, I'll need to try and reproduce it. --- .vscode/settings.json | 5 +++++ src/main.rs | 2 +- src/util/net/mod.rs | 6 +++++- src/util/netlink/nl80211/mod.rs | 37 +++++++++++++++++++++++++++------ 4 files changed, 42 insertions(+), 8 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 05cd028..8fd95c4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,10 @@ { "files.exclude": { "**/aur/{target,pkg,src}": true + }, + "rust-analyzer.debug.openDebugPane": true, + "rust-analyzer.runnables.extraEnv": { + // enable debug symbols for dependencies + "RUSTFLAGS": "-g" } } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index e193119..58455f4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -33,7 +33,7 @@ fn main() { } fn start_runtime() -> Result { - pretty_env_logger::try_init()?; + pretty_env_logger::try_init_timed()?; let args = Cli::parse(); diff --git a/src/util/net/mod.rs b/src/util/net/mod.rs index 5023f56..4b09a0e 100644 --- a/src/util/net/mod.rs +++ b/src/util/net/mod.rs @@ -108,7 +108,11 @@ async fn watch_net_updates( loop { if let Some(mut interfaces) = rx.recv().await { // filter out loopback interfaces - interfaces.retain(|_, int| int.name.as_ref() != "lo"); + interfaces.retain(|_, int| { + log::trace!("found interface: {:?}", int); + + int.name.as_ref() != "lo" + }); tx.send(interfaces)?; } } diff --git a/src/util/netlink/nl80211/mod.rs b/src/util/netlink/nl80211/mod.rs index 7f37553..2d153df 100644 --- a/src/util/netlink/nl80211/mod.rs +++ b/src/util/netlink/nl80211/mod.rs @@ -11,6 +11,9 @@ //! - https://blog.onethinglab.com/how-to-check-if-wireless-adapter-supports-monitor-mode/ //! - https://git.sipsolutions.net/iw.git/ //! - https://wireless.wiki.kernel.org/en/users/Documentation/iw +//! +//! Some things for me to remember: +//! - the `nl_type` in a generic netlink payload is the family id mod enums; @@ -142,6 +145,12 @@ impl NetlinkInterface { /// Returns `None` if the interface was not a wireless interface, or if no wireless information /// could be found. async fn get_wireless_info(&self) -> Result> { + log::trace!( + "getting wireless info for interface: {}:{}", + self.index, + self.name + ); + let (socket, _) = NL80211_SOCKET.get_or_try_init(init_socket).await?; let mut recv = genl80211_send( socket, @@ -151,7 +160,15 @@ impl NetlinkInterface { ) .await?; - while let Some(Ok(msg)) = recv.next().await as NextNl80211 { + while let Some(result) = recv.next().await as NextNl80211 { + let msg = match result { + Ok(msg) => msg, + Err(e) => { + log::error!("error occurred receiving nl80211 message: {}", e); + continue; + } + }; + if let NlPayload::Payload(gen_msg) = msg.nl_payload() { let attr_handle = gen_msg.attrs().get_attr_handle(); @@ -320,14 +337,14 @@ async fn get_signal_strength( let mut recv = genl80211_send( socket, Nl80211Command::GetStation, - NlmF::ACK | NlmF::REQUEST, + NlmF::REQUEST, attrs![Ifindex => index, Mac => Buffer::from(bssid)], ) .await?; // look for our requested data inside netlink's results - while let Some(result) = recv.next().await as NextNl80211 { - match result { + while let Some(msg) = recv.next().await as NextNl80211 { + match msg { Ok(msg) => { if let NlPayload::Payload(gen_msg) = msg.nl_payload() { // TODO: remove mut when upstream merges https://github.com/jbaublitz/neli/pull/220 @@ -345,8 +362,16 @@ async fn get_signal_strength( } } Err(e) => { - log::error!("Nl80211Command::GetStation error: {}", e); - continue; + match e { + // if this error packet is returned, it means that the interface wasn't connected to the station + RouterError::Nlmsgerr(_) => {} + // any other error we should log + _ => log::error!("Nl80211Command::GetStation error: {}", e), + } + + // TODO: when this errors, calling `recv.next().await` never completes - so return immediately + // see: https://github.com/jbaublitz/neli/issues/221 + return Ok(None); } } } From fae18e28e42a2e937d05483a7e51fe4ad87f3a51 Mon Sep 17 00:00:00 2001 From: acheronfail Date: Sun, 9 Jul 2023 19:02:50 +0930 Subject: [PATCH 23/57] simplify types for rtnetlink --- src/util/netlink/route.rs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/util/netlink/route.rs b/src/util/netlink/route.rs index ea2e868..34f5973 100644 --- a/src/util/netlink/route.rs +++ b/src/util/netlink/route.rs @@ -3,8 +3,10 @@ //! - be notified when ip addresses change //! //! Useful things when developing this: +//! - https://github.com/thom311/libnl/blob/main/src/nl-monitor.c //! - https://man7.org/linux/man-pages/man7/rtnetlink.7.html //! - https://docs.kernel.org/userspace-api/netlink/intro.html +//! - `nl-monitor` is a good way to test which groups emit events //! - `genl-ctrl-list` returns generic families //! - simulate ipv4 activity: `ip a add 10.0.0.254 dev wlan0 && sleep 1 && ip a del 10.0.0.254/32 dev wlan0` //! - simulate ipv6 activity: `ip -6 addr add 2001:0db8:0:f101::1/64 dev lo && sleep 1 && ip -6 addr del 2001:0db8:0:f101::1/64 dev lo` @@ -31,6 +33,8 @@ use crate::error::Result; pub type InterfaceUpdate = IndexMap; +type RtNext = Option, RouterError>>; + pub async fn netlink_ipaddr_listen( manual_trigger: mpsc::Receiver<()>, ) -> Result> { @@ -41,7 +45,7 @@ pub async fn netlink_ipaddr_listen( // https://docs.kernel.org/userspace-api/netlink/intro.html#strict-checking socket.enable_strict_checking(true)?; - // add multicast membership for ipv4-addr updates + // add multicast membership for ipv4 and ipv6 addr updates socket .add_mcast_membership(Groups::new_groups(&[ RTNLGRP_IPV4_IFADDR, @@ -95,9 +99,8 @@ async fn handle_netlink_route_messages( tx: Sender, ) -> Result { // listen for multicast events - type Next = Option, RouterError>>; loop { - match multicast.next().await as Next { + match multicast.next().await as RtNext { None => bail!("Unexpected end of netlink route stream"), // we got a multicast event Some(response) => { @@ -152,9 +155,7 @@ async fn get_all_interfaces(socket: &Rc) -> Result { ) .await?; - type Next = - Option, RouterError>>; - while let Some(response) = recv.next().await as Next { + while let Some(response) = recv.next().await as RtNext { let header = match response { Ok(header) => header, Err(e) => bail!("an error occurred receiving rtnetlink message: {}", e), @@ -214,9 +215,7 @@ async fn get_all_interfaces(socket: &Rc) -> Result { ) .await?; - type Next = - Option, RouterError>>; - while let Some(response) = recv.next().await as Next { + while let Some(response) = recv.next().await as RtNext { let header = match response { Ok(header) => header, Err(e) => bail!("an error occurred receiving rtnetlink message: {}", e), From fae190b0f60b646055cc603f41e656cf81158f56 Mon Sep 17 00:00:00 2001 From: acheronfail Date: Sun, 9 Jul 2023 19:32:08 +0930 Subject: [PATCH 24/57] continue on rtnetwork error rather than bailing And only updating a single interface at a time doesn't seem worth it. I've tried it a few times, and keep coming up short. This link could be somewhat useful in the future though: http://www.infradead.org/~tgr/libnl/doc/route.html --- src/util/netlink/route.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/util/netlink/route.rs b/src/util/netlink/route.rs index 34f5973..6404429 100644 --- a/src/util/netlink/route.rs +++ b/src/util/netlink/route.rs @@ -119,7 +119,6 @@ async fn handle_netlink_route_messages( NlPayload::Payload(_ifaddrmsg) => { // request all interfaces from netlink again - we request it each time because we get ifaddrmsg // events even when the address is deleted (but we can't tell that is was deleted) - // TODO: in the future it would be nice to only update the interface which emitted the event tx.send(get_all_interfaces(&socket).await?).await? } // not payload, something is wrong @@ -158,7 +157,10 @@ async fn get_all_interfaces(socket: &Rc) -> Result { while let Some(response) = recv.next().await as RtNext { let header = match response { Ok(header) => header, - Err(e) => bail!("an error occurred receiving rtnetlink message: {}", e), + Err(e) => { + log::error!("an error occurred receiving rtnetlink message: {}", e); + continue; + } }; if let NlPayload::Payload(ifinfomsg) = header.nl_payload() { @@ -218,7 +220,10 @@ async fn get_all_interfaces(socket: &Rc) -> Result { while let Some(response) = recv.next().await as RtNext { let header = match response { Ok(header) => header, - Err(e) => bail!("an error occurred receiving rtnetlink message: {}", e), + Err(e) => { + log::warn!("an error occurred receiving rtnetlink message: {}", e); + continue; + } }; if let NlPayload::Payload(ifaddrmsg) = header.nl_payload() { From fae148246655128159b9ba6b153a51351ffa7e0e Mon Sep 17 00:00:00 2001 From: acheronfail Date: Sun, 9 Jul 2023 20:58:04 +0930 Subject: [PATCH 25/57] get rid of nic::Connections by improving Interfaces --- src/bar_items/nic.rs | 67 ++++++++++++-------------------------------- src/util/net/mod.rs | 59 +++++++++++++++++++++++++++----------- 2 files changed, 61 insertions(+), 65 deletions(-) diff --git a/src/bar_items/nic.rs b/src/bar_items/nic.rs index 61ccadb..c0e9023 100644 --- a/src/bar_items/nic.rs +++ b/src/bar_items/nic.rs @@ -10,43 +10,7 @@ use crate::i3::{I3Item, I3Markup, I3Modifier}; use crate::theme::Theme; use crate::util::filter::InterfaceFilter; use crate::util::nl80211::SignalStrength; -use crate::util::{net_subscribe, NetlinkInterface, Paginator}; - -struct Connections { - inner: Vec, -} - -impl Connections { - fn new(inner: Vec) -> Self { - Self { inner } - } - - fn len(&self) -> usize { - self.inner.iter().map(|int| int.ip_addresses.len()).sum() - } - - fn is_empty(&self) -> bool { - self.len() == 0 - } - - async fn get_index<'a>(&'a self, index: usize) -> Option> { - let pair = self - .inner - .iter() - .flat_map(|int| { - int.ip_addresses - .iter() - .map(|addr| (int, addr)) - .collect::>() - }) - .nth(index); - - match pair { - Some((interface, addr)) => Some(Connection::new(interface, addr).await), - None => None, - } - } -} +use crate::util::{net_subscribe, Interfaces, NetlinkInterface, Paginator}; #[derive(Debug, Default, Serialize, Deserialize, Copy, Clone)] #[serde(rename_all = "snake_case")] @@ -168,7 +132,8 @@ impl BarItem for Nic { let mut net = net_subscribe().await?; let mut p = Paginator::new(); - let mut connections = Connections::new(vec![]); + let mut interfaces = Interfaces::default(); + let mut total_address_count = interfaces.len_addresses(); loop { let wireless_refresh_trigger = || async { match (self.wireless_display, self.wireless_refresh_interval) { @@ -181,8 +146,9 @@ impl BarItem for Nic { tokio::select! { // wait for network changes - Ok(list) = net.wait_for_change() => { - connections = Connections::new(list.filtered(&self.filter)); + Ok(new_interfaces) = net.wait_for_change() => { + total_address_count = new_interfaces.len_addresses(); + interfaces = new_interfaces.filtered(&self.filter); }, // on any bar event Some(event) = ctx.wait_for_event(self.interval) => { @@ -201,18 +167,21 @@ impl BarItem for Nic { () = wireless_refresh_trigger() => {} } - let item = if connections.is_empty() { - // TODO: differentiate between empty after filtering, and completely disconnected? - I3Item::new("inactive").color(ctx.config.theme.dim) + let item = if interfaces.is_empty() { + if total_address_count > 0 { + I3Item::new(format!("filtered: {}", total_address_count)) + } else { + I3Item::new("disconnected") + } + .color(ctx.config.theme.dim) } else { - p.set_len(connections.len())?; + p.set_len(interfaces.len_addresses())?; let theme = &ctx.config.theme; - // SAFETY(unwrap): we always set the paginator's length to the connection's length - // so it should always be within bounds - let (full, short) = connections - .get_index(p.idx()) + // SAFETY(unwrap): we always set the paginator's length to `len_addresses` so it + // should always be within bounds + let (interface, ip_addr) = interfaces.get_address_at(p.idx()).unwrap(); + let (full, short) = Connection::new(interface, ip_addr) .await - .unwrap() .format(theme, self.wireless_display); let full = format!(r#"{}{}"#, full, p.format(theme)); diff --git a/src/util/net/mod.rs b/src/util/net/mod.rs index 4b09a0e..ccf2732 100644 --- a/src/util/net/mod.rs +++ b/src/util/net/mod.rs @@ -1,5 +1,7 @@ pub mod filter; +use std::net::IpAddr; + use tokio::sync::{broadcast, mpsc, OnceCell}; use self::filter::InterfaceFilter; @@ -46,38 +48,63 @@ impl Clone for Net { } } -#[derive(Debug)] +#[derive(Debug, Default)] pub struct Interfaces { - inner: Vec, + inner: InterfaceUpdate, } impl Interfaces { - pub fn filtered(self, filters: &[InterfaceFilter]) -> Vec { + pub fn len_interfaces(&self) -> usize { + self.inner.len() + } + + pub fn len_addresses(&self) -> usize { + self.inner + .iter() + .map(|(_, int)| int.ip_addresses.len()) + .sum() + } + + pub fn is_empty(&self) -> bool { + self.len_addresses() == 0 + } + + pub fn get_interface(&self, index: usize) -> Option<&NetlinkInterface> { + self.inner.get_index(index).map(|(_, v)| v) + } + + pub fn get_address_at(&self, address_index: usize) -> Option<(&NetlinkInterface, &IpAddr)> { + self.inner + .iter() + .flat_map(|(_, int)| { + int.ip_addresses + .iter() + .map(|addr| (int, addr)) + .collect::>() + }) + .nth(address_index) + } + + pub fn filtered(mut self, filters: &[InterfaceFilter]) -> Interfaces { if filters.is_empty() { - return self.inner; + return self; } - let mut filtered = vec![]; - for mut interface in self.inner { + self.inner.retain(|_, interface| { interface .ip_addresses .retain(|addr| filters.iter().any(|f| f.matches(&interface.name, addr))); - if !interface.ip_addresses.is_empty() { - filtered.push(interface); - } - } + !interface.ip_addresses.is_empty() + }); - filtered + self } } impl From for Interfaces { - fn from(value: InterfaceUpdate) -> Self { - Interfaces { - // TODO: since index map is used, we don't really need to convert to vec here - inner: value.into_values().collect::>(), - } + fn from(inner: InterfaceUpdate) -> Self { + Interfaces { inner } } } From fae1d8d65684d0c3c9ccc2b9ae35249964c0e107 Mon Sep 17 00:00:00 2001 From: acheronfail Date: Sun, 9 Jul 2023 19:56:26 +0930 Subject: [PATCH 26/57] move `wireless_refresh_trigger` out of the loop --- src/bar_items/nic.rs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/bar_items/nic.rs b/src/bar_items/nic.rs index c0e9023..577d768 100644 --- a/src/bar_items/nic.rs +++ b/src/bar_items/nic.rs @@ -129,21 +129,19 @@ pub struct Nic { #[async_trait(?Send)] impl BarItem for Nic { async fn start(&self, mut ctx: Context) -> Result { + let wireless_refresh_trigger = || async { + match (self.wireless_display, self.wireless_refresh_interval) { + (WirelessDisplay::Hidden, _) | (_, None) => futures::future::pending::<()>().await, + (_, Some(duration)) => tokio::time::sleep(duration).await, + } + }; + let mut net = net_subscribe().await?; let mut p = Paginator::new(); let mut interfaces = Interfaces::default(); let mut total_address_count = interfaces.len_addresses(); loop { - let wireless_refresh_trigger = || async { - match (self.wireless_display, self.wireless_refresh_interval) { - (WirelessDisplay::Hidden, _) | (_, None) => { - futures::future::pending::<()>().await - } - (_, Some(duration)) => tokio::time::sleep(duration).await, - } - }; - tokio::select! { // wait for network changes Ok(new_interfaces) = net.wait_for_change() => { From fae15f18887f2f0ec4bd16c74e6d422de56bfee6 Mon Sep 17 00:00:00 2001 From: acheronfail Date: Sun, 9 Jul 2023 20:21:27 +0930 Subject: [PATCH 27/57] doc comments for Interfaces --- src/util/net/mod.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/util/net/mod.rs b/src/util/net/mod.rs index ccf2732..0a28398 100644 --- a/src/util/net/mod.rs +++ b/src/util/net/mod.rs @@ -54,10 +54,12 @@ pub struct Interfaces { } impl Interfaces { + /// Count of total interfaces pub fn len_interfaces(&self) -> usize { self.inner.len() } + /// Count of total addresses across all interfaces pub fn len_addresses(&self) -> usize { self.inner .iter() @@ -65,14 +67,17 @@ impl Interfaces { .sum() } + /// Checks if there are any addresses (and thus interfaces) at all pub fn is_empty(&self) -> bool { self.len_addresses() == 0 } - pub fn get_interface(&self, index: usize) -> Option<&NetlinkInterface> { - self.inner.get_index(index).map(|(_, v)| v) + /// Get a specific interface by its index + pub fn get_interface(&self, index: i32) -> Option<&NetlinkInterface> { + self.inner.get(&index) } + /// Get an address by its index (where index is `0..interfaces.len_addresses()`) pub fn get_address_at(&self, address_index: usize) -> Option<(&NetlinkInterface, &IpAddr)> { self.inner .iter() @@ -85,6 +90,7 @@ impl Interfaces { .nth(address_index) } + /// Apply a set of filters to this struct and return a new struct pub fn filtered(mut self, filters: &[InterfaceFilter]) -> Interfaces { if filters.is_empty() { return self; From fae1dbbb43e80077e3c48300a4c537f5b0e292fe Mon Sep 17 00:00:00 2001 From: acheronfail Date: Tue, 11 Jul 2023 20:45:18 +0930 Subject: [PATCH 28/57] fix panic when encountering mouse buttons 6 and 7 --- bin/ipc.rs | 2 ++ src/bar_items/pulse/mod.rs | 2 ++ src/i3/click.rs | 4 ++++ 3 files changed, 8 insertions(+) diff --git a/bin/ipc.rs b/bin/ipc.rs index f2a2352..c73048a 100644 --- a/bin/ipc.rs +++ b/bin/ipc.rs @@ -118,6 +118,8 @@ impl ValueEnum for Button { I3Button::Right => Some(PossibleValue::new("right")), I3Button::ScrollUp => Some(PossibleValue::new("scroll_up")), I3Button::ScrollDown => Some(PossibleValue::new("scroll_down")), + I3Button::ScrollRight => Some(PossibleValue::new("scroll_right")), + I3Button::ScrollLeft => Some(PossibleValue::new("scroll_left")), } } } diff --git a/src/bar_items/pulse/mod.rs b/src/bar_items/pulse/mod.rs index bb682db..9539f3b 100644 --- a/src/bar_items/pulse/mod.rs +++ b/src/bar_items/pulse/mod.rs @@ -760,6 +760,8 @@ impl BarItem for Pulse { } }); } + + _ => {} } _ => {} }, diff --git a/src/i3/click.rs b/src/i3/click.rs index 8ab38e7..721c087 100644 --- a/src/i3/click.rs +++ b/src/i3/click.rs @@ -10,6 +10,10 @@ pub enum I3Button { Right = 3, ScrollUp = 4, ScrollDown = 5, + ScrollRight = 6, + ScrollLeft = 7, + // TODO: apparently the maximum number of mouse buttons is 24! capture those unknowns? + // see: https://www.x.org/releases/current/doc/man/man4/mousedrv.4.xhtml } #[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] From fae1b0b673e3429c66ce58f1f7b0863c166bc7b1 Mon Sep 17 00:00:00 2001 From: acheronfail Date: Tue, 11 Jul 2023 22:37:20 +0930 Subject: [PATCH 29/57] more returns to workaround neli issue --- src/util/netlink/nl80211/mod.rs | 6 ++++-- src/util/netlink/route.rs | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/util/netlink/nl80211/mod.rs b/src/util/netlink/nl80211/mod.rs index 2d153df..101c00d 100644 --- a/src/util/netlink/nl80211/mod.rs +++ b/src/util/netlink/nl80211/mod.rs @@ -165,7 +165,8 @@ impl NetlinkInterface { Ok(msg) => msg, Err(e) => { log::error!("error occurred receiving nl80211 message: {}", e); - continue; + // return immediately, see: https://github.com/jbaublitz/neli/issues/221 + return Ok(None); } }; @@ -320,7 +321,8 @@ async fn get_bssid(socket: &NlRouter, index: i32) -> Result> { } Err(e) => { log::error!("Nl80211Command::GetScan error: {}", e); - continue; + // return immediately, see: https://github.com/jbaublitz/neli/issues/221 + return Ok(None); } } } diff --git a/src/util/netlink/route.rs b/src/util/netlink/route.rs index 6404429..5fcf36b 100644 --- a/src/util/netlink/route.rs +++ b/src/util/netlink/route.rs @@ -159,7 +159,8 @@ async fn get_all_interfaces(socket: &Rc) -> Result { Ok(header) => header, Err(e) => { log::error!("an error occurred receiving rtnetlink message: {}", e); - continue; + // return immediately, see: https://github.com/jbaublitz/neli/issues/221 + return Ok(interface_map); } }; @@ -222,7 +223,8 @@ async fn get_all_interfaces(socket: &Rc) -> Result { Ok(header) => header, Err(e) => { log::warn!("an error occurred receiving rtnetlink message: {}", e); - continue; + // return immediately, see: https://github.com/jbaublitz/neli/issues/221 + return Ok(interface_map); } }; From fae1c6a3632984e3756ef7f69cdf9a0bf8fc095f Mon Sep 17 00:00:00 2001 From: acheronfail Date: Tue, 11 Jul 2023 21:09:50 +0930 Subject: [PATCH 30/57] implement playing sound when volume is changed --- Cargo.lock | 7 +++ Cargo.toml | 1 + justfile | 2 +- sample_config.toml | 8 +++ src/bar_items/pulse/audio.rs | 57 ++++++++++++++++++++ src/bar_items/pulse/mod.rs | 102 +++++++++++++++++++++++++++++------ 6 files changed, 159 insertions(+), 18 deletions(-) create mode 100644 src/bar_items/pulse/audio.rs diff --git a/Cargo.lock b/Cargo.lock index a6c0656..9a0c8e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -801,6 +801,12 @@ dependencies = [ "serde", ] +[[package]] +name = "hound" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d13cdbd5dbb29f9c88095bbdc2590c9cba0d0a1269b983fef6b2cdd7e9f4db1" + [[package]] name = "humantime" version = "1.3.0" @@ -913,6 +919,7 @@ dependencies = [ "figment", "futures", "hex_color", + "hound", "humantime-serde", "indexmap", "libc", diff --git a/Cargo.toml b/Cargo.toml index 8d21a58..ca4bc47 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ dirs = "5.0.1" figment = { version = "0.10.10", features = ["toml", "yaml", "json"] } futures = "0.3.28" hex_color = { version = "2.0.0", features = ["serde"] } +hound = "3.5.0" humantime-serde = "1.1.1" indexmap = { version = "1.9.3", features = ["serde"] } libc = "0.2.142" diff --git a/justfile b/justfile index a04a38e..d6c4458 100644 --- a/justfile +++ b/justfile @@ -24,7 +24,7 @@ setup: build: cargo build --all --all-features _lbuild: - cargo lbuild --all --quiet + cargo lbuild --all # run `istat` in the terminal and interact with it dev *args: _lbuild diff --git a/sample_config.toml b/sample_config.toml index a92f8cf..1f1a5a3 100644 --- a/sample_config.toml +++ b/sample_config.toml @@ -180,6 +180,14 @@ display = "bytes" # # This item also has a highly featured ipc interface, see `istat-ipc custom pulse` for more info. type = "pulse" + +# Control the increment of volume changes (expressed in percent) when increasing/decreasing the volume. +# Defaults to 5 +increment = 5 + +# Optionally provide a `.wav` file to be played each time the volume is changed. +# increment_sound = "path/to/your/volume/sound.wav" + # Set an upper limit to the volume, expressed in percent. max_volume = 120 # Configure optional notifications, supported values are: diff --git a/src/bar_items/pulse/audio.rs b/src/bar_items/pulse/audio.rs new file mode 100644 index 0000000..a239a7e --- /dev/null +++ b/src/bar_items/pulse/audio.rs @@ -0,0 +1,57 @@ +use std::path::Path; + +use hound::{SampleFormat, WavReader}; +use libpulse_binding::sample::{Format, Spec}; +use tokio::fs::{File, OpenOptions}; +use tokio::io::AsyncReadExt; + +use crate::error::Result; + +/// Read a wav file at the given path, and return a `Spec` and the raw audio data +pub async fn read_wav_file(path: impl AsRef) -> Result<(Spec, Vec)> { + // NOTE: pacat (or paplay) uses libsndfile to extract sample rate, channel count and format from the sound file + // it then also extracts the raw audio data and uses that to write to the stream + let (spec, mut data) = { + let file = OpenOptions::new().read(true).open(path.as_ref()).await?; + let meta = file.metadata().await?; + + // now use `hound` to read the wav specification + let wav_reader = WavReader::new(file.into_std().await)?; + let wav_spec = wav_reader.spec(); + + // convert back to an async `File` to read the rest of the data now that the `WavReader` has + // read the header and metadata parts + let mut file = File::from_std(wav_reader.into_inner()); + let mut buf = Vec::with_capacity(meta.len() as usize); + file.read_to_end(&mut buf).await?; + + // create a pulse spec from the wav spec + let spec = Spec { + format: match wav_spec.sample_format { + SampleFormat::Float => Format::FLOAT32NE, + SampleFormat::Int => match wav_spec.bits_per_sample { + 16 => Format::S16NE, + 24 => Format::S24NE, + 32 => Format::S32NE, + n => bail!("unsupported bits per sample: {}", n), + }, + }, + channels: wav_spec.channels as u8, + rate: wav_spec.sample_rate, + }; + + if !spec.is_valid() { + bail!("format specification wasn't valid: {:?}", spec); + } + + (spec, buf) + }; + + // pad out sound data to the next frame size + let frame_size = spec.frame_size(); + if let Some(rem) = data.len().checked_rem(frame_size) { + data.extend(vec![0; rem]); + } + + Ok((spec, data)) +} diff --git a/src/bar_items/pulse/mod.rs b/src/bar_items/pulse/mod.rs index 9539f3b..78052a2 100644 --- a/src/bar_items/pulse/mod.rs +++ b/src/bar_items/pulse/mod.rs @@ -1,6 +1,8 @@ +mod audio; mod custom; use std::fmt::{Debug, Display}; +use std::path::{Path, PathBuf}; use std::process; use std::rc::Rc; @@ -20,6 +22,7 @@ use libpulse_binding::def::{DevicePortType, PortAvailable}; use libpulse_binding::error::{Code, PAErr}; use libpulse_binding::proplist::properties::{APPLICATION_NAME, APPLICATION_PROCESS_ID}; use libpulse_binding::proplist::Proplist; +use libpulse_binding::stream::{SeekMode, Stream}; use libpulse_binding::volume::{ChannelVolumes, Volume}; use libpulse_tokio::TokioMain; use num_traits::ToPrimitive; @@ -212,11 +215,15 @@ impl NotificationSetting { } } +const SAMPLE_NAME: &str = "istat-pulse-volume"; + #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct Pulse { /// How much to increment when increasing/decreasing the volume; measured in percent #[serde(default = "Pulse::default_increment")] increment: u32, + /// Path to a `.wav` file to play each time the sound is changed + increment_sound: Option, /// The maximum allowed volume; measured in percent max_volume: Option, /// Whether to send notifications on server state changes @@ -224,19 +231,18 @@ pub struct Pulse { notify: NotificationSetting, /// Name of the audio server to try to connect to server_name: Option, - // TODO: a sample to play each time the volume is changed? - // See: https://docs.rs/libpulse-binding/2.26.0/libpulse_binding/mainloop/threaded/index.html#example } impl Pulse { pub const fn default_increment() -> u32 { - 2 + 5 } } pub struct PulseState { tx: UnboundedSender, increment: u32, + increment_sound: bool, max_volume: Option, pa_ctx: PAContext, default_sink: Rc, @@ -400,7 +406,7 @@ impl RcCell { cv } - fn set_volume(&self, what: Object, vol: Vol, f: F) + fn set_volume(&mut self, what: Object, vol: Vol, f: F) where F: FnMut(bool) + 'static, { @@ -415,11 +421,16 @@ impl RcCell { }), }) .map(|p| { + // send notification let _ = self.tx.send(p.notify_volume_mute()); + // play volume sound if enabled + if self.increment_sound { + self.pa_ctx.play_sample(SAMPLE_NAME, None, None, None); + } }); } - fn set_mute(&self, what: Object, mute: bool, f: F) + fn set_mute(&mut self, what: Object, mute: bool, f: F) where F: FnMut(bool) + 'static, { @@ -437,10 +448,14 @@ impl RcCell { }) .map(|p| { let _ = self.tx.send(p.notify_volume_mute()); + // play volume sound if enabled + if self.increment_sound { + self.pa_ctx.play_sample(SAMPLE_NAME, None, None, None); + } }); } - fn toggle_mute(&self, what: Object, f: F) + fn toggle_mute(&mut self, what: Object, f: F) where F: FnMut(bool) + 'static, { @@ -458,6 +473,10 @@ impl RcCell { }) .map(|p| { let _ = self.tx.send(p.notify_volume_mute()); + // play volume sound if enabled + if self.increment_sound { + self.pa_ctx.play_sample(SAMPLE_NAME, None, None, None); + } }); } @@ -617,6 +636,51 @@ impl RcCell { inner.update_item(); }); } + + async fn setup_volume_sample(&mut self, wav_path: impl AsRef) -> Result<()> { + let (spec, audio_data) = audio::read_wav_file(wav_path.as_ref()).await?; + let audio_data_len = audio_data.len(); + + // create stream + let mut stream = match Stream::new(&mut self.pa_ctx, SAMPLE_NAME, &spec, None) { + Some(stream) => RcCell::new(stream), + None => bail!("failed to create new stream"), + }; + + // set up write callback for writing audio data to the stream + let mut inner = self.clone(); + let mut stream_ref = stream.clone(); + let mut bytes_written = 0; + + // NOTE: calling `stream_ref.set_write_callback(None)` causes a segmentation fault + // see: https://github.com/acheronfail/pulse-stream-segfault + stream.set_write_callback(Some(Box::new(move |len| { + if let Err(e) = stream_ref.write(&audio_data, None, 0, SeekMode::Relative) { + log::error!( + "failed to write to stream: {:?} - {:?}", + e, + inner.pa_ctx.errno().to_string() + ); + return; + } + + bytes_written += len; + + // we're finished writing the audio data, finish the upload, thereby saving the audio stream + // as a sample in the audio server (so we can play it later) + if bytes_written == audio_data_len { + if let Ok(()) = stream_ref.finish_upload() { + // the upload to the audio server has completed - we're ready to use the sample now + inner.increment_sound = true; + } + } + }))); + + // connect the stream as an upload, which sends it to the audio server instead of playing it directly + stream.connect_upload(audio_data_len)?; + + Ok(()) + } } #[async_trait(?Send)] @@ -637,16 +701,12 @@ impl BarItem for Pulse { pa_ctx.connect(self.server_name.as_deref(), FlagSet::NOFAIL, None)?; match main_loop.wait_for_ready(&pa_ctx).await { Ok(State::Ready) => {} - Ok(state) => { - bail!( - "failed to connect: state={:?}, err={:?}", - state, - pa_ctx.errno().to_string() - ); - } - Err(_) => { - bail!("Pulse mainloop exited while waiting on context, not continuing",); - } + Ok(state) => bail!( + "failed to connect: state={:?}, err={:?}", + state, + pa_ctx.errno().to_string() + ), + Err(_) => bail!("Pulse mainloop exited while waiting on context, not continuing"), } (main_loop, pa_ctx) @@ -657,6 +717,7 @@ impl BarItem for Pulse { let mut inner = RcCell::new(PulseState { tx, increment: self.increment, + increment_sound: false, max_volume: self.max_volume, pa_ctx, @@ -672,6 +733,13 @@ impl BarItem for Pulse { inner.subscribe_to_server_changes(); inner.fetch_server_state(); + // if a sound file was given, then setup a sample + if let Some(ref path) = self.increment_sound { + if let Err(e) = inner.setup_volume_sample(path).await { + log::error!("failed to setup volume sample: {}", e); + } + } + // run pulse main loop tokio::task::spawn_local(async move { let ret = main_loop.run().await; @@ -766,7 +834,7 @@ impl BarItem for Pulse { _ => {} }, - // whenever we want to refresh our item, an event it send on this channel + // whenever we want to refresh our item, an event is send on this channel Some(cmd) = rx.recv() => match cmd { Command::UpdateItem(cb) => { ctx.update_item(cb(&ctx.config.theme)).await?; From fae1ca33121470d3326b5ea9c7759c00519b419c Mon Sep 17 00:00:00 2001 From: acheronfail Date: Tue, 11 Jul 2023 22:11:32 +0930 Subject: [PATCH 31/57] fix debug representation of MacAddr --- src/util/netlink/mod.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/util/netlink/mod.rs b/src/util/netlink/mod.rs index 3a6413c..870e46c 100644 --- a/src/util/netlink/mod.rs +++ b/src/util/netlink/mod.rs @@ -20,7 +20,7 @@ impl Debug for MacAddr { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_fmt(format_args!( "MacAddr({})", - self.octets.map(|o| format!("{:2x}", o)).join(":") + self.octets.map(|o| format!("{:02x}", o)).join(":") )) } } @@ -81,3 +81,14 @@ pub struct NetlinkInterface { pub mac_address: Option, pub ip_addresses: IndexSet, } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn debug() { + let mac = MacAddr::from(&[1, 42, 83, 124, 165, 206]); + assert_eq!(format!("{:?}", mac), "MacAddr(01:2a:53:7c:a5:ce)"); + } +} From fae1c93b28fbb44dee45b7bd92e5cb0ceb3e4b71 Mon Sep 17 00:00:00 2001 From: acheronfail Date: Wed, 12 Jul 2023 00:24:56 +0930 Subject: [PATCH 32/57] at least parse unknown buttons --- bin/ipc.rs | 1 + src/i3/click.rs | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/bin/ipc.rs b/bin/ipc.rs index c73048a..d2717df 100644 --- a/bin/ipc.rs +++ b/bin/ipc.rs @@ -120,6 +120,7 @@ impl ValueEnum for Button { I3Button::ScrollDown => Some(PossibleValue::new("scroll_down")), I3Button::ScrollRight => Some(PossibleValue::new("scroll_right")), I3Button::ScrollLeft => Some(PossibleValue::new("scroll_left")), + _ => None, } } } diff --git a/src/i3/click.rs b/src/i3/click.rs index 721c087..6da4339 100644 --- a/src/i3/click.rs +++ b/src/i3/click.rs @@ -12,8 +12,10 @@ pub enum I3Button { ScrollDown = 5, ScrollRight = 6, ScrollLeft = 7, - // TODO: apparently the maximum number of mouse buttons is 24! capture those unknowns? + // apparently the maximum number of mouse buttons is 24! // see: https://www.x.org/releases/current/doc/man/man4/mousedrv.4.xhtml + #[serde(other)] + Unknown, } #[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] From fae1eeaa798a6e5d2d498dcd357236f9c4c9a2a2 Mon Sep 17 00:00:00 2001 From: acheronfail Date: Tue, 11 Jul 2023 23:28:46 +0930 Subject: [PATCH 33/57] new expand_path() function for simple wordexp use cases --- sample_config.toml | 3 ++- src/bar_items/pulse/mod.rs | 4 ++-- src/human_time/mod.rs | 5 ++-- src/human_time/option.rs | 5 ++-- src/util/mod.rs | 2 +- src/util/path.rs | 49 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 60 insertions(+), 8 deletions(-) create mode 100644 src/util/path.rs diff --git a/sample_config.toml b/sample_config.toml index 1f1a5a3..742fb47 100644 --- a/sample_config.toml +++ b/sample_config.toml @@ -1,5 +1,6 @@ # Optionally include other config files to be merged with this one. -# The paths are relative to the main configuration file's directory. +# These paths are relative to the main configuration file's directory. +# These paths support shell expansion - via wordexp(3) - and such can refer to dynamic paths. include = ["sample_included_config.toml"] # Optionally pass a path for the socket. This is really only useful if you have multiple bars and diff --git a/src/bar_items/pulse/mod.rs b/src/bar_items/pulse/mod.rs index 78052a2..2a413b4 100644 --- a/src/bar_items/pulse/mod.rs +++ b/src/bar_items/pulse/mod.rs @@ -35,7 +35,7 @@ use crate::dbus::{dbus_connection, BusType}; use crate::error::Result; use crate::i3::{I3Button, I3Item, I3Markup, I3Modifier}; use crate::theme::Theme; -use crate::util::{exec, RcCell}; +use crate::util::{exec, expand_path, RcCell}; #[derive(Debug, Copy, Clone, ValueEnum)] pub enum Object { @@ -735,7 +735,7 @@ impl BarItem for Pulse { // if a sound file was given, then setup a sample if let Some(ref path) = self.increment_sound { - if let Err(e) = inner.setup_volume_sample(path).await { + if let Err(e) = inner.setup_volume_sample(expand_path(path)?).await { log::error!("failed to setup volume sample: {}", e); } } diff --git a/src/human_time/mod.rs b/src/human_time/mod.rs index f797f05..0c144a3 100644 --- a/src/human_time/mod.rs +++ b/src/human_time/mod.rs @@ -1,5 +1,6 @@ -/// We use `humantime_serde` for intervals defined in the configuration file, but we want to disallow -/// any interval that's too low. So we hook into it here to override any intervals. +//! We use `humantime_serde` for intervals defined in the configuration file, but we want to disallow +//! any interval that's too low. So we hook into it here to override any intervals. + pub mod option; use std::time::Duration; diff --git a/src/human_time/option.rs b/src/human_time/option.rs index b3fcf2b..64ec075 100644 --- a/src/human_time/option.rs +++ b/src/human_time/option.rs @@ -1,5 +1,6 @@ -/// We use `humantime_serde` for intervals defined in the configuration file, but we want to disallow -/// any interval that's too low. So we hook into it here to override any intervals. +//! We use `humantime_serde` for intervals defined in the configuration file, but we want to disallow +//! any interval that's too low. So we hook into it here to override any intervals. + use std::time::Duration; pub use humantime_serde::option::serialize; diff --git a/src/util/mod.rs b/src/util/mod.rs index 7648d07..d2a02d8 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1,4 +1,4 @@ -use_and_export!(cell, enum_cycle, exec, format, net, netlink, paginator, vec); +use_and_export!(cell, enum_cycle, exec, format, net, netlink, paginator, path, vec); use futures::Future; use tokio::runtime::{Builder, Runtime}; diff --git a/src/util/path.rs b/src/util/path.rs new file mode 100644 index 0000000..55bf5a1 --- /dev/null +++ b/src/util/path.rs @@ -0,0 +1,49 @@ +use std::os::unix::prelude::OsStrExt; +use std::path::{Path, PathBuf}; + +use wordexp::{wordexp, Wordexp}; + +use crate::error::Result; + +pub fn expand_path(path: impl AsRef) -> Result { + // SAFETY: there's no need to do conversions ot UTF-8 checks here, since `wordexp` immediately + // converts the `&str` to a `CString` to pass it to C code. So, just re-interpret the given path + // as a `&str` and pass it on + let s = unsafe { std::str::from_utf8_unchecked(path.as_ref().as_os_str().as_bytes()) }; + + let mut expand = wordexp(s, Wordexp::new(0), 0)?; + // only take the first + match expand.next() { + Some(first) => Ok(PathBuf::from(first)), + // is this even reachable? + None => bail!("expansion resulted in nothing"), + } +} + +#[cfg(test)] +mod tests { + use std::ffi::OsStr; + + use super::*; + + #[test] + fn it_works() { + assert_eq!( + expand_path(PathBuf::from("path")).unwrap(), + PathBuf::from("path") + ); + + assert_eq!( + expand_path(PathBuf::from("~/path")).unwrap(), + PathBuf::from(format!("{}/path", std::env::var("HOME").unwrap())) + ); + } + + #[test] + #[should_panic(expected = "expansion resulted in nothing")] + fn passthrough_to_wordexp() { + let invalid_utf8 = vec![1, 159, 146, 150]; + let os_str = OsStr::from_bytes(&invalid_utf8); + expand_path(PathBuf::from(os_str)).unwrap(); + } +} From fae1e8eda7e92a70f6cf1eaea72b1aa3dc3e1d58 Mon Sep 17 00:00:00 2001 From: acheronfail Date: Wed, 12 Jul 2023 23:45:32 +0930 Subject: [PATCH 34/57] better filtering of loopback addresses --- src/util/net/mod.rs | 17 +++++++++++++++-- src/util/netlink/mod.rs | 13 ++++++++----- src/util/netlink/nl80211/mod.rs | 11 +++++++++++ 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/util/net/mod.rs b/src/util/net/mod.rs index 0a28398..fc19e4f 100644 --- a/src/util/net/mod.rs +++ b/src/util/net/mod.rs @@ -140,12 +140,25 @@ async fn watch_net_updates( let mut rx = netlink_ipaddr_listen(manual_trigger).await?; loop { if let Some(mut interfaces) = rx.recv().await { - // filter out loopback interfaces interfaces.retain(|_, int| { log::trace!("found interface: {:?}", int); - int.name.as_ref() != "lo" + // some address filtering + int.ip_addresses.retain(|addr| match addr { + // get rid of loopback addresses + any if any.is_loopback() => false, + // get rid of link local addresses + IpAddr::V4(v4) if v4.is_link_local() => false, + IpAddr::V6(v6) if v6.is_unicast_link_local() => false, + _ => true, + }); + + // filter out interfaces without any addresses + !int.ip_addresses.is_empty() }); + + log::trace!("interfaces: {:#?}", &interfaces); + tx.send(interfaces)?; } } diff --git a/src/util/netlink/mod.rs b/src/util/netlink/mod.rs index 870e46c..0a36957 100644 --- a/src/util/netlink/mod.rs +++ b/src/util/netlink/mod.rs @@ -3,7 +3,7 @@ pub mod nl80211; pub mod route; use std::array::TryFromSliceError; -use std::fmt::Debug; +use std::fmt::{Debug, Display}; use std::net::IpAddr; use std::sync::Arc; @@ -16,12 +16,15 @@ pub struct MacAddr { octets: [u8; 6], } +impl Display for MacAddr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.octets.map(|o| format!("{:02x}", o)).join(":")) + } +} + impl Debug for MacAddr { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!( - "MacAddr({})", - self.octets.map(|o| format!("{:02x}", o)).join(":") - )) + f.write_fmt(format_args!("MacAddr({})", self)) } } diff --git a/src/util/netlink/nl80211/mod.rs b/src/util/netlink/nl80211/mod.rs index 101c00d..53507c0 100644 --- a/src/util/netlink/nl80211/mod.rs +++ b/src/util/netlink/nl80211/mod.rs @@ -180,6 +180,7 @@ impl NetlinkInterface { attr_handle.get_attr_payload_as::(Nl80211Attribute::Iftype), Ok(Nl80211IfType::Station) ) { + log::debug!("interface is not a station"); return Ok(None); } @@ -244,6 +245,10 @@ impl NetlinkInterface { } } + log::debug!( + "no wireless information found for interface: {}", + self.index + ); Ok(None) } } @@ -327,6 +332,7 @@ async fn get_bssid(socket: &NlRouter, index: i32) -> Result> { } } + log::debug!("no bssid found for interface: {}", index); Ok(None) } @@ -378,6 +384,11 @@ async fn get_signal_strength( } } + log::debug!( + "no signal strength found for interface: {} with bssid: {}", + index, + bssid + ); Ok(None) } From fae1da458a3826977b80c95b50113a0db9170491 Mon Sep 17 00:00:00 2001 From: acheronfail Date: Thu, 13 Jul 2023 06:15:10 +0930 Subject: [PATCH 35/57] fixup bugs with krb and update sample_config.toml A couple issues: - `krb` wasn't working in the tests because there was a filter in the sample_config.toml file - `krb` if there were no filters, then the first update would occur only _after_ the first interval --- sample_config.toml | 16 ++++++++-------- src/bar_items/krb.rs | 16 +++++++++------- src/bar_items/pulse/custom.rs | 1 - src/dbus/notifications.rs | 1 - src/util/net/mod.rs | 2 -- 5 files changed, 17 insertions(+), 19 deletions(-) diff --git a/sample_config.toml b/sample_config.toml index 742fb47..00ba8ab 100644 --- a/sample_config.toml +++ b/sample_config.toml @@ -88,6 +88,13 @@ thresholds = ["1kiB", "1MiB", "10MiB", "25MiB", "100MiB"] # Optionally provide a list of interface names to ignore when calculating usage # ignored_interfaces = ["vpn0"] +[[items]] +# a raw item - these are static items that don't change, and display the values here +type = "raw" +# see i3's bar documentation for what fields are available: https://i3wm.org/docs/i3bar-protocol.html +full_text = "raw" +short_text = "!" + [[items]] # Kerberos item - simply calls `klist` and displays the result type = "krb" @@ -95,14 +102,7 @@ type = "krb" interval = "2m" # Optionally enable this item only when specific networks are active. # This is the same format as the `filter` property in the `nic` item. -only_on = ["vpn0:v4"] - -[[items]] -# a raw item - these are static items that don't change, and display the values here -type = "raw" -# see i3's bar documentation for what fields are available: https://i3wm.org/docs/i3bar-protocol.html -full_text = "raw" -short_text = "!" +# only_on = ["vpn0:v4"] [[items]] # "Network Interfaces" item, provides an interactive list of interfaces and ip addresses, as well as diff --git a/src/bar_items/krb.rs b/src/bar_items/krb.rs index 9fde5cd..ba3a2d3 100644 --- a/src/bar_items/krb.rs +++ b/src/bar_items/krb.rs @@ -40,13 +40,18 @@ impl Krb { impl BarItem for Krb { async fn start(&self, mut ctx: Context) -> Result { let mut net = net_subscribe().await?; - let mut disabled = !self.only_on.is_empty(); + let mut enabled = self.only_on.is_empty(); loop { + // update item + if enabled { + ctx.update_item(self.item(&ctx.config.theme).await?).await?; + } + tokio::select! { // any bar event _ = ctx.wait_for_event(self.interval) => { // don't update if disabled - if disabled { + if !enabled { continue; } }, @@ -55,21 +60,18 @@ impl BarItem for Krb { // if none of the filters matched if interfaces.filtered(&self.only_on).is_empty() { // if the item wasn't disabled, then empty it out - if !disabled { + if enabled { ctx.update_item(I3Item::empty()).await?; } // and set it to disabled - disabled = true; + enabled = false; // reset loop and wait to be enabled continue; } } } - - // update item - ctx.update_item(self.item(&ctx.config.theme).await?).await?; } } } diff --git a/src/bar_items/pulse/custom.rs b/src/bar_items/pulse/custom.rs index d64e2fc..294ca25 100644 --- a/src/bar_items/pulse/custom.rs +++ b/src/bar_items/pulse/custom.rs @@ -95,7 +95,6 @@ impl RcCell { args: Vec, tx: oneshot::Sender, ) { - // TODO: send pulse response from pulse success callbacks for all "success" responses let resp = match PulseCommand::try_parse_from(args) { Ok(cmd) => { let resp = match cmd { diff --git a/src/dbus/notifications.rs b/src/dbus/notifications.rs index 6f426d9..1fe006c 100644 --- a/src/dbus/notifications.rs +++ b/src/dbus/notifications.rs @@ -3,7 +3,6 @@ use std::collections::HashMap; use zbus::dbus_proxy; use zbus::zvariant::Value; -// TODO: share a single proxy instance of this, and use it wherever notifications are needed? #[dbus_proxy( default_path = "/org/freedesktop/Notifications", default_service = "org.freedesktop.Notifications", diff --git a/src/util/net/mod.rs b/src/util/net/mod.rs index fc19e4f..31e546b 100644 --- a/src/util/net/mod.rs +++ b/src/util/net/mod.rs @@ -157,8 +157,6 @@ async fn watch_net_updates( !int.ip_addresses.is_empty() }); - log::trace!("interfaces: {:#?}", &interfaces); - tx.send(interfaces)?; } } From fae1500512fad8cbd38e014d37fad7323be6bec8 Mon Sep 17 00:00:00 2001 From: acheronfail Date: Sat, 15 Jul 2023 20:10:18 +0930 Subject: [PATCH 36/57] add critical battery notifications --- sample_config.toml | 4 + src/bar_items/battery.rs | 135 ++++++++++++++++--------- src/dbus/notifications.rs | 202 ++++++++++++++++++++++++-------------- src/util/net/filter.rs | 1 - 4 files changed, 221 insertions(+), 121 deletions(-) diff --git a/sample_config.toml b/sample_config.toml index 00ba8ab..a1dab13 100644 --- a/sample_config.toml +++ b/sample_config.toml @@ -219,6 +219,10 @@ type = "battery" interval = "10s" # Should notifications be sent when an AC Adapter is plugged/unplugged? notify_on_adapter = true +# Optionally trigger a critical (and sticky) notification under a certain percentage. +# This notification will stay unless the percentage goes above the threshold, or the battery state +# is anything other than discharging. +notify_percentage = 5 # Optionally specify a list of particular batteries to show. If not provided, it will attempt to # discover all the batteries on the system. diff --git a/src/bar_items/battery.rs b/src/bar_items/battery.rs index d87329b..eb3c967 100644 --- a/src/bar_items/battery.rs +++ b/src/bar_items/battery.rs @@ -18,6 +18,7 @@ use crate::theme::Theme; use crate::util::acpi::ffi::AcpiGenericNetlinkEvent; use crate::util::{netlink_acpi_listen, Paginator}; +#[derive(Debug)] enum BatState { Unknown, Charging, @@ -27,7 +28,7 @@ enum BatState { } impl BatState { - fn get_color(&self, theme: &Theme) -> (Option<&str>, Option) { + fn get_color(&self, theme: &Theme) -> (Option<&'static str>, Option) { match self { Self::Full => (None, Some(theme.purple)), Self::Charging => (Some("󰚥"), Some(theme.blue)), @@ -51,6 +52,13 @@ impl FromStr for BatState { } } +#[derive(Debug)] +struct BatInfo { + name: String, + charge: f32, + state: BatState, +} + #[derive(Debug, Clone, Serialize, Deserialize)] struct Bat(PathBuf); @@ -91,51 +99,15 @@ impl Bat { Ok((current_pico as f64) * (voltage_pico as f64) / 1_000_000_000_000.0) } - async fn format(&self, theme: &Theme, show_watts: bool) -> Result { - let (charge, state) = match try_join!(self.percent(), self.get_state()) { - Ok((charge, state)) => (charge, state), - // Return unknown state: the files in sysfs aren't present at times, such as when connecting - // ac adapters, etc. In these scenarios we just end early here without an error and let the - // item retry on the next interval/acpi event. - Err(e) => { - log::warn!("failed to read battery {}: {}", self.0.display(), e); - return Ok(I3Item::new("???").color(theme.red)); - } - }; - - let (charge_icon, charge_fg, urgent) = match charge as u32 { - 0..=15 => { - let urgent = !matches!(state, BatState::Charging | BatState::NotCharging); - ("", Some(theme.red), urgent) - } - 16..=25 => ("", Some(theme.orange), false), - 26..=50 => ("", Some(theme.yellow), false), - 51..=75 => ("", None, false), - 76..=u32::MAX => ("", Some(theme.green), false), - }; - - let (state_icon, state_fg) = state.get_color(theme); - let icon = state_icon.unwrap_or(charge_icon); - let fg = state_fg.or(charge_fg); - - let item = if show_watts { - let watts = self.watts_now().await?; - I3Item::new(format!("{:.2} W", watts)).short_text(format!("{:.0}", watts)) - } else { - let name = self.name()?; - let name = if name == "BAT0" { - icon - } else { - name.as_str().into() - }; - I3Item::new(format!("{} {:.0}%", name, charge)).short_text(format!("{:.0}%", charge)) - }; - - Ok(match (urgent, fg) { - (true, _) => item.urgent(true), - (false, Some(fg)) => item.color(fg), - (false, None) => item, - }) + async fn get_info(&self) -> Result { + let name = self.name()?; + Ok( + try_join!(self.percent(), self.get_state()).map(|(charge, state)| BatInfo { + name, + charge, + state, + })?, + ) } async fn find_all() -> Result> { @@ -164,6 +136,43 @@ pub struct Battery { #[serde(default)] notify_on_adapter: bool, // TODO: option to run command(s) at certain percentage(s) + #[serde(default)] + notify_percentage: Option, +} + +impl Battery { + fn detail(theme: &Theme, info: &BatInfo) -> (&'static str, Option, bool) { + let (charge_icon, charge_fg, urgent) = match info.charge as u32 { + 0..=15 => { + let urgent = !matches!(info.state, BatState::Charging | BatState::NotCharging); + ("", Some(theme.red), urgent) + } + 16..=25 => ("", Some(theme.orange), false), + 26..=50 => ("", Some(theme.yellow), false), + 51..=75 => ("", None, false), + 76..=u32::MAX => ("", Some(theme.green), false), + }; + + let (state_icon, state_fg) = info.state.get_color(theme); + let icon = state_icon.unwrap_or(charge_icon); + let fg = state_fg.or(charge_fg); + + (icon, fg, urgent) + } + + fn format_watts(_: &Theme, watts: f64) -> I3Item { + I3Item::new(format!("{:.2} W", watts)).short_text(format!("{:.0}", watts)) + } + + async fn format(_: &Theme, info: &BatInfo, icon: &str) -> I3Item { + let name = if info.name == "BAT0" { + icon + } else { + info.name.as_str().into() + }; + I3Item::new(format!("{} {:.0}%", name, info.charge)) + .short_text(format!("{:.0}%", info.charge)) + } } #[async_trait(?Send)] @@ -185,10 +194,42 @@ impl BarItem for Battery { let dbus = dbus_connection(BusType::Session).await?; let notifications = NotificationsProxy::new(&dbus).await?; let mut on_acpi_event = battery_acpi_events().await?; + let mut sent_critical_notification = false; loop { let theme = &ctx.config.theme; - let item = batteries[p.idx()].format(theme, show_watts).await?; + // get info for selected battery + let bat = &batteries[p.idx()]; + let info = bat.get_info().await?; + + // send critical battery notification if configured + if let Some(pct) = self.notify_percentage { + let charge = info.charge as u8; + if charge <= pct && matches!(info.state, BatState::Discharging) { + notifications.battery_critical(charge).await; + sent_critical_notification = true; + } else if sent_critical_notification { + notifications.battery_critical_off().await; + sent_critical_notification = false; + } + } + + // build battery item + let (icon, fg, urgent) = Self::detail(theme, &info); + let item = if show_watts { + Self::format_watts(theme, bat.watts_now().await?) + } else { + Self::format(theme, &info, icon).await + }; + + // format item + let item = match (fg, urgent) { + (_, true) => item.urgent(true), + (Some(fg), false) => item.color(fg), + (None, false) => item, + }; + + // update item let full_text = format!("{}{}", item.get_full_text(), p.format(theme)); let item = item.full_text(full_text).markup(I3Markup::Pango); ctx.update_item(item).await?; diff --git a/src/dbus/notifications.rs b/src/dbus/notifications.rs index 1fe006c..e2cbc66 100644 --- a/src/dbus/notifications.rs +++ b/src/dbus/notifications.rs @@ -1,8 +1,13 @@ +//! Represents the DBUS API for notifications. +//! See: https://specifications.freedesktop.org/notification-spec/notification-spec-latest.html + use std::collections::HashMap; +use tokio::sync::OnceCell; use zbus::dbus_proxy; use zbus::zvariant::Value; +type Hints = HashMap<&'static str, Value<'static>>; #[dbus_proxy( default_path = "/org/freedesktop/Notifications", default_service = "org.freedesktop.Notifications", @@ -10,7 +15,6 @@ use zbus::zvariant::Value; gen_blocking = false )] trait Notifications { - // See: https://specifications.freedesktop.org/notification-spec/notification-spec-latest.html #[dbus_proxy(name = "Notify")] fn notify_full( &self, @@ -20,7 +24,7 @@ trait Notifications { summary: &str, body: &str, actions: &[&str], - hints: HashMap<&str, Value<'_>>, + hints: Hints, expire_timeout: i32, ) -> zbus::Result; } @@ -38,97 +42,149 @@ impl<'a> From for Value<'a> { } } +/// Easily create a hints notifications map. +macro_rules! hints { + () => { + HashMap::new() as Hints + }; + + ($($key:expr => $value:expr $(,)?)+) => {{ + let mut hints: Hints = HashMap::new(); + $( + hints.insert($key, $value.into()); + )+ + + hints + + }}; +} + +static PULSE_NOTIFICATION_ID: OnceCell = OnceCell::const_new(); +static BATTERY_NOTIFICATION_ID: OnceCell = OnceCell::const_new(); + impl<'a> NotificationsProxy<'a> { const APP_NAME: &str = "istat"; - pub async fn pulse_volume_mute(&self, name: impl AsRef, pct: u32, mute: bool) { - let mut hints = HashMap::new(); - hints.insert("value", Value::U32(pct)); - hints.insert("urgency", Urgency::Low.into()); + // util ---------------------------------------------------------------------------------------- - if let Err(e) = self + async fn notify( + &self, + id: Option, + hints: Hints, + summary: impl AsRef, + body: impl AsRef, + timeout: i32, + ) -> Option { + match self .notify_full( - &format!("{}:volume", Self::APP_NAME), - 0, - // TODO: icon - "audio-card", - name.as_ref(), - // TODO: better muted state (notification icon or more obvious in notification) - &format!("{}{}%", if mute { " " } else { " " }, pct), + Self::APP_NAME, + id.unwrap_or(0), + "", + summary.as_ref(), + body.as_ref(), &[], hints, - 2_000, + timeout, ) .await { - log::warn!("failed to send notification: {}", e); + Ok(id) => Some(id), + Err(e) => { + log::warn!("failed to send notification: {}", e); + id + } } } - pub async fn pulse_new_source_sink(&self, name: impl AsRef, what: impl AsRef) { - let mut hints = HashMap::new(); - hints.insert("urgency", Urgency::Low.into()); - - if let Err(e) = self - .notify_full( - Self::APP_NAME, - 0, - "", - &format!("New {} added", what.as_ref()), - name.as_ref(), - &[], - hints, - 2_000, - ) - .await - { - log::warn!("failed to send notification: {}", e); + async fn notify_id( + &self, + once_cell: &OnceCell, + hints: Hints, + summary: impl AsRef, + body: impl AsRef, + timeout: i32, + ) { + let cached_id = once_cell.get().cloned(); + match self.notify(cached_id, hints, summary, body, timeout).await { + Some(id) => match cached_id { + Some(_) => { /* do nothing, id already saved */ } + None => { + let _ = once_cell.set(id); + } + }, + None => { /* do nothing, an error occurred */ } } } - pub async fn pulse_defaults_change(&self, name: impl AsRef, what: impl AsRef) { - let mut hints = HashMap::new(); - hints.insert("urgency", Urgency::Low.into()); + // impl ---------------------------------------------------------------------------------------- - if let Err(e) = self - .notify_full( - Self::APP_NAME, - 0, - "", - &format!("Default {}", what.as_ref()), - name.as_ref(), - &[], - hints, - 2_000, - ) - .await - { - log::warn!("failed to send notification: {}", e); - } + pub async fn pulse_volume_mute(&self, name: impl AsRef, pct: u32, mute: bool) { + self.notify_id( + &PULSE_NOTIFICATION_ID, + hints! { + "value" => pct, + "urgency" => Urgency::Low, + }, + name, + format!("{}{}%", if mute { " " } else { " " }, pct), + 2_000, + ) + .await; + } + + pub async fn pulse_new_source_sink(&self, name: impl AsRef, what: impl AsRef) { + self.notify( + None, + hints! { "urgency" => Urgency::Low }, + format!("New {} added", what.as_ref()), + name, + 2_000, + ) + .await; + } + + pub async fn pulse_defaults_change(&self, name: impl AsRef, what: impl AsRef) { + self.notify( + None, + hints! { "urgency" => Urgency::Low }, + format!("Default {}", what.as_ref()), + name, + 2_000, + ) + .await; } pub async fn ac_adapter(&self, plugged_in: bool) { - let mut hints = HashMap::new(); - hints.insert("urgency", Urgency::Low.into()); + self.notify( + None, + hints! { "urgency" => Urgency::Low }, + "AC Adapter", + if plugged_in { + "Connected" + } else { + "Disconnected" + }, + 2_000, + ) + .await; + } - if let Err(e) = self - .notify_full( - Self::APP_NAME, - 0, - "", - "AC Adapter", - if plugged_in { - "Connected" - } else { - "Disconnected" - }, - &[], - hints, - 2_000, - ) - .await - { - log::warn!("failed to send notification: {}", e); - } + /// Trigger a critical battery charge notification that will never timeout + pub async fn battery_critical(&self, pct: u8) { + self.notify_id( + &BATTERY_NOTIFICATION_ID, + hints! { "urgency" => Urgency::Critical }, + "Critical Battery Warning!", + format!("Remaining: {}%", pct), + // NOTE: timeout of `0` means that this notification will not go away + 0, + ) + .await; + } + + /// Use to disable a previously sent critical battery notification + pub async fn battery_critical_off(&self) { + self.notify_id(&BATTERY_NOTIFICATION_ID, hints! {}, "", "", 1) + .await; } } diff --git a/src/util/net/filter.rs b/src/util/net/filter.rs index 9911644..2f95369 100644 --- a/src/util/net/filter.rs +++ b/src/util/net/filter.rs @@ -38,7 +38,6 @@ impl TryFrom<&str> for InterfaceKind { /// If `interface` is an empty string, then all interfaces are matched, for example: /// - `vpn0:ipv4` will match ip4 addresses for the `vpn` interface /// - `:ipv6` will match all interfaces which have an ip6 address -// TODO: better filtering? don't match docker interfaces, or libvirtd ones, etc? #[derive(Debug, Clone, PartialEq, Eq)] pub struct InterfaceFilter { name: String, From fae1c25e3fc667adaed996be7c5ad49968f610a5 Mon Sep 17 00:00:00 2001 From: acheronfail Date: Mon, 17 Jul 2023 22:59:31 +0930 Subject: [PATCH 37/57] only parse battery name a single time --- src/bar_items/battery.rs | 75 ++++++++++++++++++++++++++++++---------- 1 file changed, 57 insertions(+), 18 deletions(-) diff --git a/src/bar_items/battery.rs b/src/bar_items/battery.rs index eb3c967..7a9883d 100644 --- a/src/bar_items/battery.rs +++ b/src/bar_items/battery.rs @@ -1,3 +1,4 @@ +use std::cell::OnceCell; use std::path::PathBuf; use std::str::FromStr; use std::time::Duration; @@ -60,30 +61,49 @@ struct BatInfo { } #[derive(Debug, Clone, Serialize, Deserialize)] -struct Bat(PathBuf); +#[serde(transparent)] +struct Bat { + file: PathBuf, + + #[serde(skip)] + cached_name: OnceCell, +} impl Bat { + pub fn new(file: PathBuf) -> Bat { + Bat { + file, + cached_name: OnceCell::new(), + } + } + + fn name(&self) -> Result<&String> { + match self.cached_name.get() { + Some(cached) => Ok(cached), + None => match self.file.file_name() { + Some(name) => match self.cached_name.set(name.to_string_lossy().into_owned()) { + Ok(()) => self.name(), + Err(_) => bail!("failed to set name cache"), + }, + None => bail!("failed to parse file name from: {}", self.file.display()), + }, + } + } + async fn read(&self, file_name: impl AsRef) -> Result { - Ok(read_to_string(self.0.join(file_name.as_ref())).await?) + Ok(read_to_string(self.file.join(file_name.as_ref())).await?) } async fn read_usize(&self, file_name: impl AsRef) -> Result { Ok(self.read(file_name).await?.trim().parse::()?) } - fn name(&self) -> Result { - match self.0.file_name() { - Some(name) => Ok(name.to_string_lossy().into_owned()), - None => Err(format!("failed to parse file name from: {}", self.0.display()).into()), - } - } - - async fn get_state(&self) -> Result { + pub async fn get_state(&self) -> Result { Ok(BatState::from_str(self.read("status").await?.trim())?) } // NOTE: there is also `/capacity` which returns an integer percentage - async fn percent(&self) -> Result { + pub async fn percent(&self) -> Result { let (charge_now, charge_full) = try_join!( self.read_usize("charge_now"), self.read_usize("charge_full"), @@ -91,7 +111,7 @@ impl Bat { Ok((charge_now as f32) / (charge_full as f32) * 100.0) } - async fn watts_now(&self) -> Result { + pub async fn watts_now(&self) -> Result { let (current_pico, voltage_pico) = try_join!( self.read_usize("current_now"), self.read_usize("voltage_now"), @@ -99,8 +119,8 @@ impl Bat { Ok((current_pico as f64) * (voltage_pico as f64) / 1_000_000_000_000.0) } - async fn get_info(&self) -> Result { - let name = self.name()?; + pub async fn get_info(&self) -> Result { + let name = self.name()?.to_owned(); Ok( try_join!(self.percent(), self.get_state()).map(|(charge, state)| BatInfo { name, @@ -110,16 +130,16 @@ impl Bat { ) } - async fn find_all() -> Result> { + pub async fn find_all() -> Result> { let battery_dir = PathBuf::from("/sys/class/power_supply"); let mut entries = fs::read_dir(&battery_dir).await?; let mut batteries = vec![]; while let Some(entry) = entries.next_entry().await? { if entry.file_type().await?.is_symlink() { - let path = entry.path(); - if fs::try_exists(path.join("charge_now")).await? { - batteries.push(Bat(path)); + let file = entry.path(); + if fs::try_exists(file.join("charge_now")).await? { + batteries.push(Bat::new(file)); } } } @@ -306,3 +326,22 @@ async fn battery_acpi_events() -> Result> { Ok(rx) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn de() { + let path = "/sys/class/power_supply/BAT0"; + let battery = serde_json::from_str::(&format!(r#""{}""#, path)).unwrap(); + assert_eq!(battery.file, PathBuf::from(path)); + } + + #[test] + fn name() { + let battery = Bat::new(PathBuf::from("/sys/class/power_supply/BAT0")); + assert_eq!(battery.name().unwrap(), "BAT0"); + assert_eq!(battery.name().unwrap(), "BAT0"); + } +} From fae10f2d23ef916dcfe2d7adc6e0376751b5326a Mon Sep 17 00:00:00 2001 From: acheronfail Date: Wed, 19 Jul 2023 20:01:56 +0930 Subject: [PATCH 38/57] update log messages for nl80211 --- src/util/netlink/nl80211/mod.rs | 67 ++++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 23 deletions(-) diff --git a/src/util/netlink/nl80211/mod.rs b/src/util/netlink/nl80211/mod.rs index 53507c0..326df6e 100644 --- a/src/util/netlink/nl80211/mod.rs +++ b/src/util/netlink/nl80211/mod.rs @@ -135,7 +135,11 @@ impl NetlinkInterface { match self.get_wireless_info().await { Ok(info) => info, Err(e) => { - log::error!("NetlinkInterface::wireless_info(): {}", e); + log::error!( + "index {} NetlinkInterface::wireless_info(): {}", + self.index, + e + ); None } } @@ -145,11 +149,7 @@ impl NetlinkInterface { /// Returns `None` if the interface was not a wireless interface, or if no wireless information /// could be found. async fn get_wireless_info(&self) -> Result> { - log::trace!( - "getting wireless info for interface: {}:{}", - self.index, - self.name - ); + log::trace!("index {} getting wireless info", self.index); let (socket, _) = NL80211_SOCKET.get_or_try_init(init_socket).await?; let mut recv = genl80211_send( @@ -164,7 +164,11 @@ impl NetlinkInterface { let msg = match result { Ok(msg) => msg, Err(e) => { - log::error!("error occurred receiving nl80211 message: {}", e); + log::error!( + "index {} error occurred receiving nl80211 message: {}", + self.index, + e + ); // return immediately, see: https://github.com/jbaublitz/neli/issues/221 return Ok(None); } @@ -180,7 +184,7 @@ impl NetlinkInterface { attr_handle.get_attr_payload_as::(Nl80211Attribute::Iftype), Ok(Nl80211IfType::Station) ) { - log::debug!("interface is not a station"); + log::debug!("index {} interface is not a station", self.index); return Ok(None); } @@ -190,7 +194,11 @@ impl NetlinkInterface { { Ok(name) => name.into(), Err(e) => { - log::error!("failed to parse ifname from nl80211 msg: {}", e); + log::error!( + "index {} failed to parse ifname from nl80211 msg: {}", + self.index, + e + ); "".into() } }; @@ -201,7 +209,11 @@ impl NetlinkInterface { { Ok(bytes) => <&[u8] as TryInto>::try_into(bytes)?, Err(e) => { - log::error!("failed to parse mac from nl80211 msg: {}", e); + log::error!( + "index {} failed to parse mac from nl80211 msg: {}", + self.index, + e + ); continue; } }; @@ -245,10 +257,7 @@ impl NetlinkInterface { } } - log::debug!( - "no wireless information found for interface: {}", - self.index - ); + log::debug!("index {} no wireless information found", self.index); Ok(None) } } @@ -318,6 +327,7 @@ async fn get_bssid(socket: &NlRouter, index: i32) -> Result> { .get_attr_payload_as_with_len_borrowed::<&[u8]>(Nl80211Bss::Bssid) { if let Ok(bssid) = MacAddr::try_from(bytes) { + log::debug!("index {} found bssid: {}", index, bssid); return Ok(Some(bssid)); } } @@ -325,14 +335,14 @@ async fn get_bssid(socket: &NlRouter, index: i32) -> Result> { } } Err(e) => { - log::error!("Nl80211Command::GetScan error: {}", e); + log::error!("index {} Nl80211Command::GetScan error: {}", index, e); // return immediately, see: https://github.com/jbaublitz/neli/issues/221 return Ok(None); } } } - log::debug!("no bssid found for interface: {}", index); + log::debug!("index {} no bssid found", index); Ok(None) } @@ -364,7 +374,15 @@ async fn get_signal_strength( if let Ok(signal) = station_info.get_attr_payload_as::(Nl80211StationInfo::Signal) { - return Ok(Some(SignalStrength::new(signal as i8))); + let signal_strength = SignalStrength::new(signal as i8); + log::debug!( + "index {} bssid {} found signal: {} dBm", + index, + bssid, + signal_strength.dbm + ); + + return Ok(Some(signal_strength)); } } } @@ -374,7 +392,14 @@ async fn get_signal_strength( // if this error packet is returned, it means that the interface wasn't connected to the station RouterError::Nlmsgerr(_) => {} // any other error we should log - _ => log::error!("Nl80211Command::GetStation error: {}", e), + _ => { + log::error!( + "index {} bssid {} Nl80211Command::GetStation error: {}", + index, + bssid, + e + ) + } } // TODO: when this errors, calling `recv.next().await` never completes - so return immediately @@ -384,11 +409,7 @@ async fn get_signal_strength( } } - log::debug!( - "no signal strength found for interface: {} with bssid: {}", - index, - bssid - ); + log::debug!("index {} bssid {} no signal strength found", index, bssid); Ok(None) } From fae128ec0992e1aa54cce6701632ca7cf733491e Mon Sep 17 00:00:00 2001 From: acheronfail Date: Sat, 22 Jul 2023 20:15:25 +0930 Subject: [PATCH 39/57] only play volume sound if sink is changed --- src/bar_items/pulse/mod.rs | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/bar_items/pulse/mod.rs b/src/bar_items/pulse/mod.rs index 2a413b4..04b2f5c 100644 --- a/src/bar_items/pulse/mod.rs +++ b/src/bar_items/pulse/mod.rs @@ -423,10 +423,7 @@ impl RcCell { .map(|p| { // send notification let _ = self.tx.send(p.notify_volume_mute()); - // play volume sound if enabled - if self.increment_sound { - self.pa_ctx.play_sample(SAMPLE_NAME, None, None, None); - } + self.play_volume_sample_if_enabled(what); }); } @@ -448,10 +445,7 @@ impl RcCell { }) .map(|p| { let _ = self.tx.send(p.notify_volume_mute()); - // play volume sound if enabled - if self.increment_sound { - self.pa_ctx.play_sample(SAMPLE_NAME, None, None, None); - } + self.play_volume_sample_if_enabled(what); }); } @@ -473,13 +467,16 @@ impl RcCell { }) .map(|p| { let _ = self.tx.send(p.notify_volume_mute()); - // play volume sound if enabled - if self.increment_sound { - self.pa_ctx.play_sample(SAMPLE_NAME, None, None, None); - } + self.play_volume_sample_if_enabled(what); }); } + fn play_volume_sample_if_enabled(&mut self, what: Object) { + if matches!(what, Object::Sink) && self.increment_sound { + self.pa_ctx.play_sample(SAMPLE_NAME, None, None, None); + } + } + fn set_default(&mut self, what: Object, name: impl AsRef, f: F) where F: FnMut(bool) + 'static, From fae15292b23e9ab7214ce3ea512b4eddda8687d1 Mon Sep 17 00:00:00 2001 From: acheronfail Date: Mon, 24 Jul 2023 22:28:34 +0930 Subject: [PATCH 40/57] update comment with upstream github issue --- src/bar_items/pulse/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bar_items/pulse/mod.rs b/src/bar_items/pulse/mod.rs index 04b2f5c..9f38e6d 100644 --- a/src/bar_items/pulse/mod.rs +++ b/src/bar_items/pulse/mod.rs @@ -650,7 +650,7 @@ impl RcCell { let mut bytes_written = 0; // NOTE: calling `stream_ref.set_write_callback(None)` causes a segmentation fault - // see: https://github.com/acheronfail/pulse-stream-segfault + // see: https://github.com/jnqnfe/pulse-binding-rust/issues/56 stream.set_write_callback(Some(Box::new(move |len| { if let Err(e) = stream_ref.write(&audio_data, None, 0, SeekMode::Relative) { log::error!( From fae14391356077557c070882a4f917fbc454fb14 Mon Sep 17 00:00:00 2001 From: acheronfail Date: Tue, 25 Jul 2023 10:27:16 +0930 Subject: [PATCH 41/57] add new light item for controlling backlights --- sample_config.toml | 8 +++ src/bar_items/light.rs | 139 +++++++++++++++++++++++++++++++++++++++++ src/bar_items/mod.rs | 2 +- src/config/item.rs | 3 + 4 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 src/bar_items/light.rs diff --git a/sample_config.toml b/sample_config.toml index a1dab13..855682b 100644 --- a/sample_config.toml +++ b/sample_config.toml @@ -201,6 +201,14 @@ notify = "none" # server_name = "pipewire-0" +[[items]] +# Show current light brightness (and also adjust it). +type = "light" +# Optionally provide a path to a specific backlight: +# path = "/sys/class/backlight/intel_backlight" +# Optionally specify how much percentage to increment the light by when scrolling (default is 5): +# increment = 10 + [[items]] # Show information about CapsLock/NumLock/ScrollLock. type = "kbd" diff --git a/src/bar_items/light.rs b/src/bar_items/light.rs new file mode 100644 index 0000000..9e21d45 --- /dev/null +++ b/src/bar_items/light.rs @@ -0,0 +1,139 @@ +//! An item which controls lights. +//! https://github.com/haikarainen/light has been a good inspiration, and could +//! be for future features (if things like razer devices should ever be supported, etc). + +use std::path::{Path, PathBuf}; + +use async_trait::async_trait; +use serde_derive::{Deserialize, Serialize}; +use tokio::fs; + +use crate::context::{BarEvent, BarItem, Context, StopAction}; +use crate::error::Result; +use crate::i3::{I3Button, I3Item}; + +struct LightFile { + /// Max brightness of this device + max_brightness: u64, + /// The file to read to or write from to get/set the brightness. + brightness_file: PathBuf, +} + +impl LightFile { + async fn read_u64(path: impl AsRef) -> Result { + Ok(fs::read_to_string(path.as_ref()) + .await? + .trim() + .parse::()?) + } + + pub async fn new(path: impl AsRef) -> Result { + let path = path.as_ref(); + + let max_brightness_path = path.join("max_brightness"); + let max_brightness = Self::read_u64(&max_brightness_path).await?; + + let brightness_file = path.join("brightness"); + match brightness_file.exists() { + true => Ok(LightFile { + max_brightness, + brightness_file, + }), + false => bail!("{}/brightness does not exist", path.display()), + } + } + + /// Get the brightness of this light as a percentage + pub async fn get(&self) -> Result { + let value = Self::read_u64(&self.brightness_file).await?; + Ok(((value * 100 + self.max_brightness / 2) / self.max_brightness) as u8) + } + + /// Set the brightness of this light to a percentage + pub async fn set(&self, pct: u8) -> Result<()> { + let step = self.max_brightness / 100; + let value = (pct as u64) * step; + fs::write(&self.brightness_file, value.to_string()).await?; + + Ok(()) + } + + pub async fn adjust(&self, amount: i8) -> Result<()> { + let pct = self.get().await?; + self.set(pct.saturating_add_signed(amount).clamp(0, 100)) + .await + } + + /// Detects what is most likely the default backlight. + /// It does this by just looking for the backlight with the largest value for max_brightness. + pub async fn detect() -> Result { + // read all backlights + let mut entries = fs::read_dir("/sys/class/backlight").await?; + let mut backlights = vec![]; + while let Some(entry) = entries.next_entry().await? { + let path = entry.path(); + match Self::read_u64(path.join("max_brightness")).await { + Ok(value) => backlights.push((path, value)), + _ => continue, + } + } + + // sort by max brightness + backlights.sort_unstable_by_key(|ref pair| pair.1); + + // return a light for the "brightest" backlight + match backlights.last() { + Some((path, _)) => LightFile::new(path).await, + None => bail!("no backlights found"), + } + } + + pub async fn format(&self) -> Result { + let pct = self.get().await?; + let icon = match pct { + 0..=14 => "󰃚", + 15..=29 => "󰃛", + 30..=44 => "󰃜", + 45..=59 => "󰃝", + 60..=74 => "󰃞", + 75..=89 => "󰃟", + 90..=u8::MAX => "󰃠", + }; + + Ok(I3Item::new(format!("{} {:>3}%", icon, pct))) + } +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct Light { + /// Optional path to a specific light. + path: Option, + /// How much to increment the light when scrolling up or down. + /// Defaults to 5. + increment: Option, +} + +#[async_trait(?Send)] +impl BarItem for Light { + async fn start(&self, mut ctx: Context) -> Result { + let light = match &self.path { + Some(path) => LightFile::new(path).await?, + None => LightFile::detect().await?, + }; + + let increment = self.increment.unwrap_or(5) as i8; + loop { + ctx.update_item(light.format().await?).await?; + match ctx.wait_for_event(None).await { + Some(BarEvent::Click(click)) => match click.button { + I3Button::Left => light.set(1).await?, + I3Button::Right => light.set(100).await?, + I3Button::ScrollUp => light.adjust(increment).await?, + I3Button::ScrollDown => light.adjust(-increment).await?, + _ => {} + }, + _ => {} + } + } + } +} diff --git a/src/bar_items/mod.rs b/src/bar_items/mod.rs index 92ca6c7..cd5e672 100644 --- a/src/bar_items/mod.rs +++ b/src/bar_items/mod.rs @@ -1,3 +1,3 @@ use_and_export!( - battery, cpu, disk, dunst, kbd, krb, mem, net_usage, nic, pulse, script, sensors, time + battery, cpu, disk, dunst, kbd, krb, light, mem, net_usage, nic, pulse, script, sensors, time ); diff --git a/src/config/item.rs b/src/config/item.rs index c910f68..9e5cf1d 100644 --- a/src/config/item.rs +++ b/src/config/item.rs @@ -30,6 +30,7 @@ pub enum ItemInner { Dunst(Dunst), Kbd(Kbd), Krb(Krb), + Light(Light), Mem(Mem), NetUsage(NetUsage), Nic(Nic), @@ -52,6 +53,7 @@ impl ItemInner { ItemInner::Dunst(_) => "dunst", ItemInner::Kbd(_) => "kbd", ItemInner::Krb(_) => "krb", + ItemInner::Light(_) => "light", ItemInner::Mem(_) => "mem", ItemInner::NetUsage(_) => "net_usage", ItemInner::Nic(_) => "nic", @@ -85,6 +87,7 @@ impl Item { ItemInner::Dunst(inner) => Box::new(inner.clone()), ItemInner::Kbd(inner) => Box::new(inner.clone()), ItemInner::Krb(inner) => Box::new(inner.clone()), + ItemInner::Light(inner) => Box::new(inner.clone()), ItemInner::Mem(inner) => Box::new(inner.clone()), ItemInner::NetUsage(inner) => Box::new(inner.clone()), ItemInner::Nic(inner) => Box::new(inner.clone()), From fae1e821f298ea1006700a5a37be5fc5b0508781 Mon Sep 17 00:00:00 2001 From: acheronfail Date: Thu, 27 Jul 2023 11:19:54 +0930 Subject: [PATCH 42/57] ensure powerline colours are consistent from right to left --- src/i3/bar_item.rs | 4 ++++ src/main.rs | 17 ++++++++++------- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/i3/bar_item.rs b/src/i3/bar_item.rs index b2ab32e..6493192 100644 --- a/src/i3/bar_item.rs +++ b/src/i3/bar_item.rs @@ -171,6 +171,10 @@ impl I3Item { } } + pub fn is_empty(&self) -> bool { + self.full_text.is_empty() + } + pub fn empty() -> I3Item { I3Item::new("") } diff --git a/src/main.rs b/src/main.rs index 58455f4..abc4c0e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -263,21 +263,24 @@ fn create_powerline(bar: &[I3Item], theme: &Theme, adjuster: F) -> Vec HexColor, { - let len = theme.powerline.len(); + let visible_items = bar.iter().filter(|i| !i.is_empty()).count(); + + // start the powerline index so the theme colours are consistent from right to left + let powerline_len = theme.powerline.len(); let mut powerline_bar = vec![]; - let mut powerline_idx = 0; + let mut powerline_idx = powerline_len - (visible_items % powerline_len); + for i in 0..bar.len() { let item = &bar[i]; - if item.full_text.is_empty() { + if item.is_empty() { continue; } let instance = i.to_string(); - #[cfg(debug_assertions)] - assert_eq!(item.get_instance().unwrap(), &instance); + debug_assert_eq!(item.get_instance().unwrap(), &instance); - let c1 = &theme.powerline[powerline_idx % len]; - let c2 = &theme.powerline[(powerline_idx + 1) % len]; + let c1 = &theme.powerline[powerline_idx % powerline_len]; + let c2 = &theme.powerline[(powerline_idx + 1) % powerline_len]; powerline_idx += 1; // create the powerline separator From fae16f7bad55cce53dd321d95682a71b829e9fd5 Mon Sep 17 00:00:00 2001 From: acheronfail Date: Sun, 30 Jul 2023 23:58:00 +0930 Subject: [PATCH 43/57] increment to nearest multiple of increment amount in light item --- src/bar_items/light.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/bar_items/light.rs b/src/bar_items/light.rs index 9e21d45..ab350f9 100644 --- a/src/bar_items/light.rs +++ b/src/bar_items/light.rs @@ -60,8 +60,11 @@ impl LightFile { pub async fn adjust(&self, amount: i8) -> Result<()> { let pct = self.get().await?; - self.set(pct.saturating_add_signed(amount).clamp(0, 100)) - .await + self.set( + pct.saturating_add_signed(amount - (pct as i8 % amount)) + .clamp(0, 100), + ) + .await } /// Detects what is most likely the default backlight. From fae1ce104ee65e7db927cd588fab49944de387d5 Mon Sep 17 00:00:00 2001 From: acheronfail Date: Sun, 30 Jul 2023 23:44:00 +0930 Subject: [PATCH 44/57] update IDEAS.md --- IDEAS.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/IDEAS.md b/IDEAS.md index 0d72b8f..1da12fd 100644 --- a/IDEAS.md +++ b/IDEAS.md @@ -14,7 +14,9 @@ There's no guarantee they'll ever be added or implemented, and they'll likely be ## Improvements -* ... +* when displaying the bar, check for `urgent` and provide own translation of it? (disable urgent, set bg and fg) + * that way, powerline separators will also work + * and also, there won't be a bad conflict of fg to bg colours, since i3bar just sets its background to the "urgent" colour ## Tips From fae12dece45227344176fb0b646d5f5b36f62297 Mon Sep 17 00:00:00 2001 From: acheronfail Date: Mon, 31 Jul 2023 21:09:03 +0930 Subject: [PATCH 45/57] unset urgent and style ourselves when powerline is enabled --- IDEAS.md | 4 +--- src/main.rs | 30 ++++++++++++++++++++++-------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/IDEAS.md b/IDEAS.md index 1da12fd..0d72b8f 100644 --- a/IDEAS.md +++ b/IDEAS.md @@ -14,9 +14,7 @@ There's no guarantee they'll ever be added or implemented, and they'll likely be ## Improvements -* when displaying the bar, check for `urgent` and provide own translation of it? (disable urgent, set bg and fg) - * that way, powerline separators will also work - * and also, there won't be a bad conflict of fg to bg colours, since i3bar just sets its background to the "urgent" colour +* ... ## Tips diff --git a/src/main.rs b/src/main.rs index abc4c0e..c582f68 100644 --- a/src/main.rs +++ b/src/main.rs @@ -279,26 +279,35 @@ where let instance = i.to_string(); debug_assert_eq!(item.get_instance().unwrap(), &instance); - let c1 = &theme.powerline[powerline_idx % powerline_len]; - let c2 = &theme.powerline[(powerline_idx + 1) % powerline_len]; + let prev_color = &theme.powerline[powerline_idx % powerline_len]; + let this_color = &theme.powerline[(powerline_idx + 1) % powerline_len]; powerline_idx += 1; + let is_urgent = *item.get_urgent().unwrap_or(&false); + let item_fg = if is_urgent { theme.bg } else { this_color.fg }; + let item_bg = if is_urgent { theme.red } else { this_color.bg }; + // create the powerline separator let mut sep_item = I3Item::new(theme.powerline_separator.to_span()) .instance(instance) .separator(false) .markup(I3Markup::Pango) .separator_block_width_px(0) - .color(c2.bg) + .color(item_bg) .with_data("powerline_sep", true.into()); - // the first separator doesn't blend with any other item + // the first separator doesn't blend with any other item (hence > 0) if i > 0 { - sep_item = sep_item.background_color(c1.bg); + // ensure the separator meshes with the previous item if it's urgent + if *bar[i - 1].get_urgent().unwrap_or(&false) { + sep_item = sep_item.background_color(theme.red); + } else { + sep_item = sep_item.background_color(prev_color.bg); + } } // replace `config.theme.dim` so it's easy to see - let adjusted_dim = adjuster(&c2.bg); + let adjusted_dim = adjuster(&item_bg); powerline_bar.push(sep_item); powerline_bar.push( @@ -312,11 +321,16 @@ where .separator(false) .separator_block_width_px(0) .color(match item.get_color() { + _ if is_urgent => item_fg, Some(color) if color == &theme.dim => adjusted_dim, Some(color) => *color, - _ => c2.fg, + _ => item_fg, }) - .background_color(c2.bg), + .background_color(item_bg) + // disable urgent here, since we override it ourselves to style the powerline more nicely + // but we set it as additional data just in case someone wants to use it + .urgent(false) + .with_data("urgent", true.into()), ); } powerline_bar From fae1f9df39b374661025fade25c3bdb0a21ca841 Mon Sep 17 00:00:00 2001 From: acheronfail Date: Sun, 6 Aug 2023 09:37:42 +0930 Subject: [PATCH 46/57] return an option to fix trailing space in nic detail --- src/bar_items/nic.rs | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/bar_items/nic.rs b/src/bar_items/nic.rs index 577d768..e62b217 100644 --- a/src/bar_items/nic.rs +++ b/src/bar_items/nic.rs @@ -21,6 +21,7 @@ enum WirelessDisplay { Dbm, } +#[derive(Debug)] enum ConnectionDetail { None, Ssid(String), @@ -28,9 +29,9 @@ enum ConnectionDetail { } impl ConnectionDetail { - fn display(&self, wireless_display: WirelessDisplay) -> String { + fn display(&self, wireless_display: WirelessDisplay) -> Option { if matches!(wireless_display, WirelessDisplay::Hidden) { - return "".into(); + return None; } match self { @@ -41,10 +42,10 @@ impl ConnectionDetail { // SAFETY: we match and early return on this at the beginning of this function WirelessDisplay::Hidden => unreachable!(), }; - format!("{signal} at {ssid}", ssid = ssid, signal = signal) + Some(format!("{signal} at {ssid}", ssid = ssid, signal = signal)) } - ConnectionDetail::Ssid(ssid) => ssid.into(), - ConnectionDetail::None => "".into(), + ConnectionDetail::Ssid(ssid) => Some(ssid.into()), + ConnectionDetail::None => None, } } } @@ -104,9 +105,13 @@ impl<'a> Connection<'a> { fg, self.name, self.addr, - match (wireless_display, &self.detail) { - (WirelessDisplay::Hidden, _) | (_, None) => "".into(), - (_, Some(detail)) => format!(" {}", detail.display(wireless_display)), + match self + .detail + .as_ref() + .and_then(|cd| cd.display(wireless_display)) + { + Some(detail) => format!(" {}", detail), + _ => "".into(), } ), format!(r#"{}"#, fg, self.name), From fae1edb2a6eb85a0f5d24c83078caba48d98a815 Mon Sep 17 00:00:00 2001 From: acheronfail Date: Sun, 6 Aug 2023 20:39:55 +0930 Subject: [PATCH 47/57] fix some wireless cards not returning ssid in GetInterface nl80211 response --- src/util/netlink/nl80211/mod.rs | 82 ++++++++++++++++++++++++--------- 1 file changed, 61 insertions(+), 21 deletions(-) diff --git a/src/util/netlink/nl80211/mod.rs b/src/util/netlink/nl80211/mod.rs index 326df6e..a0cc906 100644 --- a/src/util/netlink/nl80211/mod.rs +++ b/src/util/netlink/nl80211/mod.rs @@ -220,30 +220,23 @@ impl NetlinkInterface { // NOTE: it seems that nl80211 netlink doesn't null terminate the SSID here, so fetch // it as bytes and convert it to a string ourselves - let ssid = match attr_handle + let mut ssid = match attr_handle .get_attr_payload_as_with_len_borrowed::<&[u8]>(Nl80211Attribute::Ssid) { Ok(name) => Some(String::from_utf8_lossy(name).into()), - // if there's no SSID, then the interface is likely not connected to a network + // sometimes the `GetInterface` response doesn't include the ssid, but that's okay + // since we can fetch it later when we send a `GetScan` request + // see: https://github.com/systemd/systemd/issues/24585 Err(_) => None, }; - // don't bother fetching these if we don't have an ssid, since the interface is probably - // not connected to a network - let (bssid, signal) = { - match ssid { - Some(_) => { - let bssid = get_bssid(socket, self.index).await?; - let signal = match bssid.as_ref() { - Some(bssid) => { - get_signal_strength(socket, self.index, bssid).await? - } - None => None, - }; - (bssid, signal) - } - None => (None, None), - } + // NOTE: if we didn't get the ssid before, it's also returned in the `GetScan` response + // so pass it in here too just in case + let bssid = get_scan(socket, self.index, &mut ssid).await?; + let signal = match bssid.as_ref() { + Some(bssid) => get_signal_strength(socket, self.index, bssid).await?, + // we can't fetch the signal strength without the bssid + None => None, }; return Ok(Some(WirelessInfo { @@ -302,12 +295,37 @@ impl SignalStrength { } } -/// Get the current BSSID of the connected network (if any) for the given interface -async fn get_bssid(socket: &NlRouter, index: i32) -> Result> { +// NOTE: see iw's scan.c for a reference of how to parse information elements +// e.g.: https://git.sipsolutions.net/iw.git/tree/scan.c#n1718 +const INFORMATION_ELEMENT_SSID: u8 = 0; +fn ssid_from_ie(information_elements: &[u8]) -> Result>> { + let mut idx = 0; + while idx < information_elements.len() { + let el_type = information_elements[idx]; + let el_len = information_elements[idx + 1] as usize; + if el_type == INFORMATION_ELEMENT_SSID { + return Ok(Some( + String::from_utf8(information_elements[idx + 2..idx + 2 + el_len].to_vec())?.into(), + )); + } + + idx += el_len + 2; + } + + Ok(None) +} + +/// Get the current BSSID of the connected network (if any) for the given interface and also +/// optionally set the ssid if it's not already set previously +async fn get_scan( + socket: &NlRouter, + index: i32, + ssid: &mut Option>, +) -> Result> { let mut recv = genl80211_send( socket, Nl80211Command::GetScan, - NlmF::DUMP, + NlmF::REQUEST | NlmF::ACK | NlmF::ROOT | NlmF::MATCH, attrs![Ifindex => index], ) .await?; @@ -323,6 +341,28 @@ async fn get_bssid(socket: &NlRouter, index: i32) -> Result> { if let Ok(bss_attrs) = attr_handle.get_nested_attributes::(Nl80211Attribute::Bss) { + // set the ssid if it's not set + if ssid.is_none() { + if let Ok(bytes) = bss_attrs + .get_attr_payload_as_with_len_borrowed::<&[u8]>( + Nl80211Bss::InformationElements, + ) + { + log::debug!( + "index {} updating ssid from information elements", + index + ); + match ssid_from_ie(bytes) { + Ok(option) => *ssid = option, + Err(e) => log::error!( + "index {} failed to parse information elements: {}", + index, + e + ), + } + } + } + if let Ok(bytes) = bss_attrs .get_attr_payload_as_with_len_borrowed::<&[u8]>(Nl80211Bss::Bssid) { From fae1cd4731cc84768634640dd387a55870c0d3ce Mon Sep 17 00:00:00 2001 From: acheronfail Date: Sun, 6 Aug 2023 18:45:46 +0930 Subject: [PATCH 48/57] update IDEAS.md --- IDEAS.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/IDEAS.md b/IDEAS.md index 0d72b8f..d7fc87f 100644 --- a/IDEAS.md +++ b/IDEAS.md @@ -10,7 +10,8 @@ There's no guarantee they'll ever be added or implemented, and they'll likely be ## Bugs -* ... +* light bar item doesn't update when brightness changed by other means + * implement an ipc command and map that in i3, so that it's in sync ## Improvements From fae1a044ee5e96d2389e82a562a965996b321026 Mon Sep 17 00:00:00 2001 From: acheronfail Date: Sun, 6 Aug 2023 19:30:55 +0930 Subject: [PATCH 49/57] add an ipc api for the light item --- IDEAS.md | 3 +-- scripts/i3.conf | 6 +++--- src/bar_items/light.rs | 39 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/IDEAS.md b/IDEAS.md index d7fc87f..0d72b8f 100644 --- a/IDEAS.md +++ b/IDEAS.md @@ -10,8 +10,7 @@ There's no guarantee they'll ever be added or implemented, and they'll likely be ## Bugs -* light bar item doesn't update when brightness changed by other means - * implement an ipc command and map that in i3, so that it's in sync +* ... ## Improvements diff --git a/scripts/i3.conf b/scripts/i3.conf index 0128d1a..17731f4 100644 --- a/scripts/i3.conf +++ b/scripts/i3.conf @@ -34,9 +34,9 @@ bindsym ctrl+shift+p exec istat-ipc --socket /tmp/istat-socket-2.dev set-theme / bindsym bracketleft exec istat-ipc --socket /tmp/istat-socket.dev custom pulse volume-down sink bindsym bracketright exec istat-ipc --socket /tmp/istat-socket.dev custom pulse volume-up sink bindsym backslash exec istat-ipc --socket /tmp/istat-socket.dev custom pulse mute-toggle sink -bindsym shift+bracketleft exec istat-ipc --socket /tmp/istat-socket.dev custom pulse volume-down source -bindsym shift+bracketright exec istat-ipc --socket /tmp/istat-socket.dev custom pulse volume-up source -bindsym shift+backslash exec istat-ipc --socket /tmp/istat-socket.dev custom pulse mute-toggle source +bindsym shift+bracketleft exec istat-ipc --socket /tmp/istat-socket.dev custom light increase +bindsym shift+bracketright exec istat-ipc --socket /tmp/istat-socket.dev custom light decrease +bindsym shift+backslash exec istat-ipc --socket /tmp/istat-socket.dev custom light set 50 # click events bindsym a exec istat-ipc --socket /tmp/istat-socket.dev click pulse scroll_down diff --git a/src/bar_items/light.rs b/src/bar_items/light.rs index ab350f9..39d3029 100644 --- a/src/bar_items/light.rs +++ b/src/bar_items/light.rs @@ -5,10 +5,12 @@ use std::path::{Path, PathBuf}; use async_trait::async_trait; +use clap::Parser; use serde_derive::{Deserialize, Serialize}; +use serde_json::json; use tokio::fs; -use crate::context::{BarEvent, BarItem, Context, StopAction}; +use crate::context::{BarEvent, BarItem, Context, CustomResponse, StopAction}; use crate::error::Result; use crate::i3::{I3Button, I3Item}; @@ -52,7 +54,7 @@ impl LightFile { /// Set the brightness of this light to a percentage pub async fn set(&self, pct: u8) -> Result<()> { let step = self.max_brightness / 100; - let value = (pct as u64) * step; + let value = (pct.clamp(0, 100) as u64) * step; fs::write(&self.brightness_file, value.to_string()).await?; Ok(()) @@ -128,6 +130,7 @@ impl BarItem for Light { loop { ctx.update_item(light.format().await?).await?; match ctx.wait_for_event(None).await { + // mouse events Some(BarEvent::Click(click)) => match click.button { I3Button::Left => light.set(1).await?, I3Button::Right => light.set(100).await?, @@ -135,8 +138,40 @@ impl BarItem for Light { I3Button::ScrollDown => light.adjust(-increment).await?, _ => {} }, + // custom ipc events + Some(BarEvent::Custom { payload, responder }) => { + let resp = match LightCommand::try_parse_from(payload) { + Ok(cmd) => { + match match cmd { + LightCommand::Increase => light.adjust(increment).await, + LightCommand::Decrease => light.adjust(-increment).await, + LightCommand::Set { pct } => light.set(pct).await, + } { + Ok(()) => CustomResponse::Json(json!(())), + Err(e) => CustomResponse::Json(json!({ + "failure": e.to_string() + })), + } + } + Err(e) => CustomResponse::Help(e.render()), + }; + + let _ = responder.send(resp); + } + // other events just trigger a refresh _ => {} } } } } + +#[derive(Debug, Parser)] +#[command(name = "light", no_binary_name = true)] +enum LightCommand { + /// Increase the brightness by the configured increment amount + Increase, + /// Decrease the brightness by the configured increment amount + Decrease, + /// Set the brightness to a specific value + Set { pct: u8 }, +} From fae13992f45374511442d4023f26ebef9c804dbf Mon Sep 17 00:00:00 2001 From: acheronfail Date: Mon, 7 Aug 2023 08:18:44 +0930 Subject: [PATCH 50/57] fix backgrounds not meshing with powerline separators --- sample_included_config.toml | 2 ++ src/main.rs | 19 +++++++++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/sample_included_config.toml b/sample_included_config.toml index abdaa32..7bf0a3b 100644 --- a/sample_included_config.toml +++ b/sample_included_config.toml @@ -7,6 +7,8 @@ type = "raw" # items from the parent configuration file. index = 7 full_text = "hello" +color = "#000000" +background = "#aaaa00" # Example of updating the theme. [theme] diff --git a/src/main.rs b/src/main.rs index c582f68..6b55d07 100644 --- a/src/main.rs +++ b/src/main.rs @@ -285,7 +285,14 @@ where let is_urgent = *item.get_urgent().unwrap_or(&false); let item_fg = if is_urgent { theme.bg } else { this_color.fg }; - let item_bg = if is_urgent { theme.red } else { this_color.bg }; + let item_bg = if is_urgent { + theme.red + } else { + match item.get_background_color() { + Some(bg) => *bg, + None => this_color.bg, + } + }; // create the powerline separator let mut sep_item = I3Item::new(theme.powerline_separator.to_span()) @@ -298,11 +305,15 @@ where // the first separator doesn't blend with any other item (hence > 0) if i > 0 { - // ensure the separator meshes with the previous item if it's urgent - if *bar[i - 1].get_urgent().unwrap_or(&false) { + // ensure the separator meshes with the previous item's background + let prev_item = &bar[i - 1]; + if *prev_item.get_urgent().unwrap_or(&false) { sep_item = sep_item.background_color(theme.red); } else { - sep_item = sep_item.background_color(prev_color.bg); + sep_item = sep_item.background_color(match prev_item.get_background_color() { + Some(bg) => *bg, + None => prev_color.bg, + }); } } From fae151005305b3556ad900c10f094e6586e0c8ff Mon Sep 17 00:00:00 2001 From: acheronfail Date: Mon, 7 Aug 2023 08:17:06 +0930 Subject: [PATCH 51/57] update IDEAS.md --- IDEAS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/IDEAS.md b/IDEAS.md index 0d72b8f..3259cce 100644 --- a/IDEAS.md +++ b/IDEAS.md @@ -7,6 +7,8 @@ There's no guarantee they'll ever be added or implemented, and they'll likely be * a bin PKGBUILD for the AUR (would need to setup CI first) * man pages for all binaries +* urgent items + * rather than just changing the colours, make them flash between two states? ## Bugs From fae187f22417f9750cc23b0aaf165e3c7fc1b9e5 Mon Sep 17 00:00:00 2001 From: acheronfail Date: Mon, 7 Aug 2023 19:49:17 +0930 Subject: [PATCH 52/57] move bar formatting related code to a new bar mod --- src/bar.rs | 187 ++++++++++++++++++++++++++++++++++++++++++++++ src/ipc/client.rs | 6 +- src/ipc/mod.rs | 6 +- src/lib.rs | 1 + src/main.rs | 128 ++----------------------------- 5 files changed, 203 insertions(+), 125 deletions(-) create mode 100644 src/bar.rs diff --git a/src/bar.rs b/src/bar.rs new file mode 100644 index 0000000..4427689 --- /dev/null +++ b/src/bar.rs @@ -0,0 +1,187 @@ +use std::cell::OnceCell; +use std::fmt::Debug; +use std::ops::{Index, IndexMut}; + +use hex_color::HexColor; +use serde_json::Value; + +use crate::error::Result; +use crate::i3::{I3Item, I3Markup}; +use crate::theme::Theme; + +pub struct Bar { + /// The actual bar items - represents the latest state of each individual bar item + items: Vec, + /// Cache for the adjuster for the dim fg theme colour + dim_adjuster: OnceCell HexColor>>, +} + +impl Debug for Bar { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Bar") + .field("items", &self.items) + .field( + "dim_adjuster", + &if self.dim_adjuster.get().is_some() { + "Some(_)" + } else { + "None" + }, + ) + .finish() + } +} + +impl Index for Bar { + type Output = I3Item; + + fn index(&self, index: usize) -> &Self::Output { + &self.items[index] + } +} + +impl IndexMut for Bar { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + &mut self.items[index] + } +} + +impl Bar { + /// Construct a new bar + pub fn new(item_count: usize) -> Bar { + Bar { + items: vec![I3Item::empty(); item_count], + dim_adjuster: OnceCell::new(), + } + } + + /// Convert the bar to json + pub fn to_json(&self, theme: &Theme) -> Result { + let json = if theme.powerline_enable { + let powerline_items = self.create_powerline(theme); + serde_json::to_string(&powerline_items)? + } else { + serde_json::to_string(&self.items)? + }; + + Ok(json) + } + + /// Convert the bar to a `Value` + pub fn to_value(&self, theme: &Theme) -> Result { + let value = if theme.powerline_enable { + let powerline_items = self.create_powerline(theme); + serde_json::to_value(&powerline_items)? + } else { + serde_json::to_value(&self.items)? + }; + + Ok(value) + } + + /// Return a list of items representing the bar formatted as a powerline + fn create_powerline(&self, theme: &Theme) -> Vec { + let visible_items = self.items.iter().filter(|i| !i.is_empty()).count(); + + // start the powerline index so the theme colours are consistent from right to left + let powerline_len = theme.powerline.len(); + let mut powerline_bar = vec![]; + let mut powerline_idx = powerline_len - (visible_items % powerline_len); + + for i in 0..self.items.len() { + let item = &self.items[i]; + if item.is_empty() { + continue; + } + + let instance = i.to_string(); + debug_assert_eq!(item.get_instance().unwrap(), &instance); + + let prev_color = &theme.powerline[powerline_idx % powerline_len]; + let this_color = &theme.powerline[(powerline_idx + 1) % powerline_len]; + powerline_idx += 1; + + let is_urgent = *item.get_urgent().unwrap_or(&false); + let item_fg = if is_urgent { theme.bg } else { this_color.fg }; + let item_bg = if is_urgent { + theme.red + } else { + match item.get_background_color() { + Some(bg) => *bg, + None => this_color.bg, + } + }; + + // create the powerline separator + let mut sep_item = I3Item::new(theme.powerline_separator.to_span()) + .instance(instance) + .separator(false) + .markup(I3Markup::Pango) + .separator_block_width_px(0) + .color(item_bg) + .with_data("powerline_sep", true.into()); + + // the first separator doesn't blend with any other item (hence > 0) + if i > 0 { + // ensure the separator meshes with the previous item's background + let prev_item = &self.items[i - 1]; + if *prev_item.get_urgent().unwrap_or(&false) { + sep_item = sep_item.background_color(theme.red); + } else { + sep_item = sep_item.background_color(match prev_item.get_background_color() { + Some(bg) => *bg, + None => prev_color.bg, + }); + } + } + + // replace `config.theme.dim` so it's easy to see + let adjusted_dim = self + .dim_adjuster + .get_or_init(|| Box::new(make_color_adjuster(&theme.bg, &theme.dim)))( + &item_bg + ); + + powerline_bar.push(sep_item); + powerline_bar.push( + item.clone() + .full_text(format!( + " {} ", + // replace `config.theme.dim` use in pango spans + item.full_text + .replace(&theme.dim.to_string(), &adjusted_dim.to_string()) + )) + .separator(false) + .separator_block_width_px(0) + .color(match item.get_color() { + _ if is_urgent => item_fg, + Some(color) if color == &theme.dim => adjusted_dim, + Some(color) => *color, + _ => item_fg, + }) + .background_color(item_bg) + // disable urgent here, since we override it ourselves to style the powerline more nicely + // but we set it as additional data just in case someone wants to use it + .urgent(false) + .with_data("urgent", true.into()), + ); + } + + powerline_bar + } +} + +/// HACK: this assumes that RGB colours scale linearly - I don't know if they do or not. +/// Used to render the powerline bar and make sure that dim text is visible. +fn make_color_adjuster(bg: &HexColor, fg: &HexColor) -> impl Fn(&HexColor) -> HexColor { + let r = fg.r.abs_diff(bg.r); + let g = fg.g.abs_diff(bg.g); + let b = fg.b.abs_diff(bg.b); + move |c| { + HexColor::rgb( + r.saturating_add(c.r), + g.saturating_add(c.g), + b.saturating_add(c.b), + ) + } +} diff --git a/src/ipc/client.rs b/src/ipc/client.rs index 27973e5..7b8e5ba 100644 --- a/src/ipc/client.rs +++ b/src/ipc/client.rs @@ -72,7 +72,11 @@ async fn handle_ipc_request(stream: &UnixStream, mut ctx: IpcContext, len: usize ctx.token.cancel(); } IpcMessage::GetBar => { - send_ipc_response(&stream, &IpcReply::Value(serde_json::to_value(&*ctx.bar)?)).await?; + send_ipc_response( + &stream, + &IpcReply::Value(ctx.bar.to_value(&ctx.config.theme)?), + ) + .await?; } IpcMessage::Info => { let info = serde_json::to_value(ctx.config.item_idx_to_name())?; diff --git a/src/ipc/mod.rs b/src/ipc/mod.rs index 57f72d4..9decdfa 100644 --- a/src/ipc/mod.rs +++ b/src/ipc/mod.rs @@ -8,15 +8,15 @@ use std::path::PathBuf; use tokio_util::sync::CancellationToken; pub use self::server::{create_ipc_socket, handle_ipc_events}; +use crate::bar::Bar; use crate::config::AppConfig; use crate::dispatcher::Dispatcher; use crate::error::Result; -use crate::i3::I3Item; use crate::util::RcCell; #[derive(Debug, Clone)] pub struct IpcContext { - bar: RcCell>, + bar: RcCell, token: CancellationToken, config: RcCell, dispatcher: RcCell, @@ -24,7 +24,7 @@ pub struct IpcContext { impl IpcContext { pub fn new( - bar: RcCell>, + bar: RcCell, token: CancellationToken, config: RcCell, dispatcher: RcCell, diff --git a/src/lib.rs b/src/lib.rs index bfce04f..dd53d6a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,7 @@ #[macro_use] pub mod macros; +pub mod bar; pub mod bar_items; pub mod cli; pub mod config; diff --git a/src/main.rs b/src/main.rs index 6b55d07..bd76638 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ use std::process; use clap::Parser; -use hex_color::HexColor; +use istat::bar::Bar; use istat::cli::Cli; use istat::config::AppConfig; use istat::context::{Context, SharedState, StopAction}; @@ -9,10 +9,9 @@ use istat::dispatcher::Dispatcher; use istat::error::Result; use istat::i3::header::I3BarHeader; use istat::i3::ipc::handle_click_events; -use istat::i3::{I3Item, I3Markup}; +use istat::i3::I3Item; use istat::ipc::{create_ipc_socket, handle_ipc_events, IpcContext}; use istat::signals::handle_signals; -use istat::theme::Theme; use istat::util::{local_block_on, RcCell}; use tokio::sync::mpsc::{self, Receiver}; use tokio::time::Instant; @@ -85,14 +84,14 @@ async fn async_main(args: Cli) -> Result { return result; } -fn setup_i3_bar(config: &RcCell) -> Result<(RcCell>, RcCell)> { +fn setup_i3_bar(config: &RcCell) -> Result<(RcCell, RcCell)> { let item_count = config.items.len(); // shared state let state = SharedState::new(); // A list of items which represents the i3 bar - let bar = RcCell::new(vec![I3Item::empty(); item_count]); + let bar = RcCell::new(Bar::new(item_count)); // Used to send events to each bar item, and also to trigger updates of the bar let (update_tx, update_rx) = mpsc::channel(1); @@ -194,7 +193,7 @@ fn handle_item_updates( config: RcCell, mut item_rx: Receiver<(I3Item, usize)>, mut update_rx: Receiver<()>, - mut bar: RcCell>, + mut bar: RcCell, ) -> Result<()> { // output first parts of the i3 bar protocol - the header println!("{}", serde_json::to_string(&I3BarHeader::default())?); @@ -230,19 +229,9 @@ fn handle_item_updates( } } - // serialise to JSON - let theme = config.theme.clone(); - let bar_json = match theme.powerline_enable { - true => serde_json::to_string(&create_powerline( - &bar, - &theme, - &make_color_adjuster(&theme.bg, &theme.dim), - )), - false => serde_json::to_string(&*bar), - }; - // print bar to STDOUT for i3 - match bar_json { + let theme = config.theme.clone(); + match bar.to_json(&theme) { // make sure to include the trailing comma `,` as part of the protocol Ok(json) => println!("{},", json), // on any serialisation error, emit an error that will be drawn to the status bar @@ -258,106 +247,3 @@ fn handle_item_updates( Ok(()) } - -fn create_powerline(bar: &[I3Item], theme: &Theme, adjuster: F) -> Vec -where - F: Fn(&HexColor) -> HexColor, -{ - let visible_items = bar.iter().filter(|i| !i.is_empty()).count(); - - // start the powerline index so the theme colours are consistent from right to left - let powerline_len = theme.powerline.len(); - let mut powerline_bar = vec![]; - let mut powerline_idx = powerline_len - (visible_items % powerline_len); - - for i in 0..bar.len() { - let item = &bar[i]; - if item.is_empty() { - continue; - } - - let instance = i.to_string(); - debug_assert_eq!(item.get_instance().unwrap(), &instance); - - let prev_color = &theme.powerline[powerline_idx % powerline_len]; - let this_color = &theme.powerline[(powerline_idx + 1) % powerline_len]; - powerline_idx += 1; - - let is_urgent = *item.get_urgent().unwrap_or(&false); - let item_fg = if is_urgent { theme.bg } else { this_color.fg }; - let item_bg = if is_urgent { - theme.red - } else { - match item.get_background_color() { - Some(bg) => *bg, - None => this_color.bg, - } - }; - - // create the powerline separator - let mut sep_item = I3Item::new(theme.powerline_separator.to_span()) - .instance(instance) - .separator(false) - .markup(I3Markup::Pango) - .separator_block_width_px(0) - .color(item_bg) - .with_data("powerline_sep", true.into()); - - // the first separator doesn't blend with any other item (hence > 0) - if i > 0 { - // ensure the separator meshes with the previous item's background - let prev_item = &bar[i - 1]; - if *prev_item.get_urgent().unwrap_or(&false) { - sep_item = sep_item.background_color(theme.red); - } else { - sep_item = sep_item.background_color(match prev_item.get_background_color() { - Some(bg) => *bg, - None => prev_color.bg, - }); - } - } - - // replace `config.theme.dim` so it's easy to see - let adjusted_dim = adjuster(&item_bg); - - powerline_bar.push(sep_item); - powerline_bar.push( - item.clone() - .full_text(format!( - " {} ", - // replace `config.theme.dim` use in pango spans - item.full_text - .replace(&theme.dim.to_string(), &adjusted_dim.to_string()) - )) - .separator(false) - .separator_block_width_px(0) - .color(match item.get_color() { - _ if is_urgent => item_fg, - Some(color) if color == &theme.dim => adjusted_dim, - Some(color) => *color, - _ => item_fg, - }) - .background_color(item_bg) - // disable urgent here, since we override it ourselves to style the powerline more nicely - // but we set it as additional data just in case someone wants to use it - .urgent(false) - .with_data("urgent", true.into()), - ); - } - powerline_bar -} - -/// HACK: this assumes that RGB colours scale linearly - I don't know if they do or not. -/// Used to render the powerline bar and make sure that dim text is visible. -fn make_color_adjuster(bg: &HexColor, fg: &HexColor) -> impl Fn(&HexColor) -> HexColor { - let r = fg.r.abs_diff(bg.r); - let g = fg.g.abs_diff(bg.g); - let b = fg.b.abs_diff(bg.b); - move |c| { - HexColor::rgb( - r.saturating_add(c.r), - g.saturating_add(c.g), - b.saturating_add(c.b), - ) - } -} From fae12d3d616154fa81e787f1cf70e880e04bb07b Mon Sep 17 00:00:00 2001 From: acheronfail Date: Mon, 7 Aug 2023 21:19:04 +0930 Subject: [PATCH 53/57] use a hashmap to future proof ipc theme updates --- src/bar.rs | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/src/bar.rs b/src/bar.rs index 4427689..405486b 100644 --- a/src/bar.rs +++ b/src/bar.rs @@ -1,4 +1,4 @@ -use std::cell::OnceCell; +use std::collections::HashMap; use std::fmt::Debug; use std::ops::{Index, IndexMut}; @@ -12,8 +12,8 @@ use crate::theme::Theme; pub struct Bar { /// The actual bar items - represents the latest state of each individual bar item items: Vec, - /// Cache for the adjuster for the dim fg theme colour - dim_adjuster: OnceCell HexColor>>, + /// Cache for any colour adjusters created + color_adjusters: HashMap HexColor>>, } impl Debug for Bar { @@ -21,12 +21,8 @@ impl Debug for Bar { f.debug_struct("Bar") .field("items", &self.items) .field( - "dim_adjuster", - &if self.dim_adjuster.get().is_some() { - "Some(_)" - } else { - "None" - }, + "color_adjusters", + &self.color_adjusters.keys().collect::>(), ) .finish() } @@ -51,12 +47,12 @@ impl Bar { pub fn new(item_count: usize) -> Bar { Bar { items: vec![I3Item::empty(); item_count], - dim_adjuster: OnceCell::new(), + color_adjusters: HashMap::new(), } } /// Convert the bar to json - pub fn to_json(&self, theme: &Theme) -> Result { + pub fn to_json(&mut self, theme: &Theme) -> Result { let json = if theme.powerline_enable { let powerline_items = self.create_powerline(theme); serde_json::to_string(&powerline_items)? @@ -68,7 +64,7 @@ impl Bar { } /// Convert the bar to a `Value` - pub fn to_value(&self, theme: &Theme) -> Result { + pub fn to_value(&mut self, theme: &Theme) -> Result { let value = if theme.powerline_enable { let powerline_items = self.create_powerline(theme); serde_json::to_value(&powerline_items)? @@ -80,7 +76,7 @@ impl Bar { } /// Return a list of items representing the bar formatted as a powerline - fn create_powerline(&self, theme: &Theme) -> Vec { + fn create_powerline(&mut self, theme: &Theme) -> Vec { let visible_items = self.items.iter().filter(|i| !i.is_empty()).count(); // start the powerline index so the theme colours are consistent from right to left @@ -137,8 +133,9 @@ impl Bar { // replace `config.theme.dim` so it's easy to see let adjusted_dim = self - .dim_adjuster - .get_or_init(|| Box::new(make_color_adjuster(&theme.bg, &theme.dim)))( + .color_adjusters + .entry(theme.dim) + .or_insert_with(|| Box::new(make_color_adjuster(&theme.bg, &theme.dim)))( &item_bg ); From fae12e20cceecd2d7139c914d4f1f6641b4a3099 Mon Sep 17 00:00:00 2001 From: acheronfail Date: Tue, 8 Aug 2023 18:30:03 +0930 Subject: [PATCH 54/57] when an item is urgent, make it swap between two states for visibility --- src/bar.rs | 61 ++++++++++++++++++++++++++++++++-------------- src/main.rs | 18 +++++++++++--- src/theme.rs | 14 +++++++++++ src/util/mod.rs | 2 +- src/util/urgent.rs | 58 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 131 insertions(+), 22 deletions(-) create mode 100644 src/util/urgent.rs diff --git a/src/bar.rs b/src/bar.rs index 405486b..632a022 100644 --- a/src/bar.rs +++ b/src/bar.rs @@ -51,32 +51,53 @@ impl Bar { } } + /// Are there any urgent items? + pub fn any_urgent(&self) -> bool { + self.items + .iter() + .any(|item| item.get_urgent().is_some_and(|urgent| *urgent)) + } + /// Convert the bar to json pub fn to_json(&mut self, theme: &Theme) -> Result { - let json = if theme.powerline_enable { - let powerline_items = self.create_powerline(theme); - serde_json::to_string(&powerline_items)? - } else { - serde_json::to_string(&self.items)? - }; - - Ok(json) + Ok(serde_json::to_string(&self.get_items(theme))?) } /// Convert the bar to a `Value` pub fn to_value(&mut self, theme: &Theme) -> Result { - let value = if theme.powerline_enable { - let powerline_items = self.create_powerline(theme); - serde_json::to_value(&powerline_items)? + Ok(serde_json::to_value(&self.get_items(theme))?) + } + + fn get_items(&mut self, theme: &Theme) -> Vec { + if theme.powerline_enable { + self.create_powerline_bar(theme) } else { - serde_json::to_value(&self.items)? - }; + self.create_bar(theme) + } + } - Ok(value) + /// Return a list of items representing the bar + fn create_bar(&mut self, theme: &Theme) -> Vec { + self.items + .iter() + .cloned() + .map(|item| { + if let Some(true) = item.get_urgent() { + item.color(theme.urgent_fg) + .background_color(theme.urgent_bg) + // disable urgent here, since we override it ourselves to style it more nicely + // but we set it as additional data just in case someone wants to use it + .urgent(false) + .with_data("urgent", true.into()) + } else { + item + } + }) + .collect() } /// Return a list of items representing the bar formatted as a powerline - fn create_powerline(&mut self, theme: &Theme) -> Vec { + fn create_powerline_bar(&mut self, theme: &Theme) -> Vec { let visible_items = self.items.iter().filter(|i| !i.is_empty()).count(); // start the powerline index so the theme colours are consistent from right to left @@ -98,9 +119,13 @@ impl Bar { powerline_idx += 1; let is_urgent = *item.get_urgent().unwrap_or(&false); - let item_fg = if is_urgent { theme.bg } else { this_color.fg }; + let item_fg = if is_urgent { + theme.urgent_fg + } else { + this_color.fg + }; let item_bg = if is_urgent { - theme.red + theme.urgent_bg } else { match item.get_background_color() { Some(bg) => *bg, @@ -122,7 +147,7 @@ impl Bar { // ensure the separator meshes with the previous item's background let prev_item = &self.items[i - 1]; if *prev_item.get_urgent().unwrap_or(&false) { - sep_item = sep_item.background_color(theme.red); + sep_item = sep_item.background_color(theme.urgent_bg); } else { sep_item = sep_item.background_color(match prev_item.get_background_color() { Some(bg) => *bg, diff --git a/src/main.rs b/src/main.rs index bd76638..729be9a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,7 +12,7 @@ use istat::i3::ipc::handle_click_events; use istat::i3::I3Item; use istat::ipc::{create_ipc_socket, handle_ipc_events, IpcContext}; use istat::signals::handle_signals; -use istat::util::{local_block_on, RcCell}; +use istat::util::{local_block_on, RcCell, UrgentTimer}; use tokio::sync::mpsc::{self, Receiver}; use tokio::time::Instant; use tokio_util::sync::CancellationToken; @@ -202,9 +202,15 @@ fn handle_item_updates( tokio::task::spawn_local(async move { let item_names = config.item_idx_to_name(); - + let mut urgent_timer = UrgentTimer::new(); loop { + // enable urgent timer if any item is urgent + urgent_timer.toggle(bar.any_urgent()); + tokio::select! { + // the urgent timer triggered, so update the timer and start it again + // this logic makes urgent items "flash" between two coloured states + () = urgent_timer.wait() => urgent_timer.reset(), // a manual update was requested Some(()) = update_rx.recv() => {} // an item is requesting an update, update the bar state @@ -229,8 +235,14 @@ fn handle_item_updates( } } + // style urgent colours differently based on the urgent_timer's status + let mut theme = config.theme.clone(); + if urgent_timer.swapped() { + theme.urgent_bg = config.theme.urgent_fg; + theme.urgent_fg = config.theme.urgent_bg; + } + // print bar to STDOUT for i3 - let theme = config.theme.clone(); match bar.to_json(&theme) { // make sure to include the trailing comma `,` as part of the protocol Ok(json) => println!("{},", json), diff --git a/src/theme.rs b/src/theme.rs index b26814e..d76db7d 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -39,6 +39,7 @@ pub struct Theme { pub fg: HexColor, #[serde(default = "Theme::default_dim")] pub dim: HexColor, + #[serde(default = "Theme::default_red")] pub red: HexColor, #[serde(default = "Theme::default_orange")] @@ -51,6 +52,14 @@ pub struct Theme { pub purple: HexColor, #[serde(default = "Theme::default_blue")] pub blue: HexColor, + + /// The foreground for an urgent item. Defaults to `theme.fg`. + #[serde(default = "Theme::default_bg")] + pub urgent_fg: HexColor, + /// The background for an urgent item. Defaults to `theme.red`. + #[serde(default = "Theme::default_red")] + pub urgent_bg: HexColor, + #[serde(default = "Theme::default_powerline")] pub powerline: Vec, #[serde(default)] @@ -65,12 +74,17 @@ impl Default for Theme { bg: Self::default_bg(), fg: Self::default_fg(), dim: Self::default_dim(), + red: Self::default_red(), orange: Self::default_orange(), yellow: Self::default_yellow(), green: Self::default_green(), purple: Self::default_purple(), blue: Self::default_blue(), + + urgent_fg: Self::default_bg(), + urgent_bg: Self::default_red(), + powerline: Self::default_powerline(), powerline_enable: false, powerline_separator: Self::default_powerline_separator(), diff --git a/src/util/mod.rs b/src/util/mod.rs index d2a02d8..d42a3d6 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1,4 +1,4 @@ -use_and_export!(cell, enum_cycle, exec, format, net, netlink, paginator, path, vec); +use_and_export!(cell, enum_cycle, exec, format, net, netlink, paginator, path, urgent, vec); use futures::Future; use tokio::runtime::{Builder, Runtime}; diff --git a/src/util/urgent.rs b/src/util/urgent.rs new file mode 100644 index 0000000..f5f2398 --- /dev/null +++ b/src/util/urgent.rs @@ -0,0 +1,58 @@ +use std::time::{Duration, Instant}; + +pub struct UrgentTimer { + /// If set, then the timer is active. Marks the start of the timer. + started: Option, + /// Whether or not the urgent bg should be swapped with the urgent fg. + swapped: bool, +} + +impl UrgentTimer { + pub fn new() -> UrgentTimer { + UrgentTimer { + started: None, + swapped: false, + } + } + + pub fn swapped(&self) -> bool { + self.swapped + } + + pub fn toggle(&mut self, on: bool) { + match on { + true => { + if let None = self.started { + self.reset_timer(); + self.swapped = false; + } + } + false => { + self.started = None; + self.swapped = false; + } + } + } + + pub fn reset(&mut self) { + if self.started.is_some() { + self.reset_timer(); + self.swapped = !self.swapped; + } + } + + pub async fn wait(&self) { + match self.started { + Some(started) => { + if let Some(time_left) = Duration::from_secs(1).checked_sub(started.elapsed()) { + tokio::time::sleep(time_left).await + } + } + None => futures::future::pending::<()>().await, + } + } + + fn reset_timer(&mut self) { + self.started = Some(Instant::now()); + } +} From fae1004da95e1e7430d3467b3c39f3e5b8e9907b Mon Sep 17 00:00:00 2001 From: acheronfail Date: Wed, 9 Aug 2023 16:49:20 +0930 Subject: [PATCH 55/57] update IDEAS.md --- IDEAS.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/IDEAS.md b/IDEAS.md index 3259cce..0d72b8f 100644 --- a/IDEAS.md +++ b/IDEAS.md @@ -7,8 +7,6 @@ There's no guarantee they'll ever be added or implemented, and they'll likely be * a bin PKGBUILD for the AUR (would need to setup CI first) * man pages for all binaries -* urgent items - * rather than just changing the colours, make them flash between two states? ## Bugs From fae1999ceaed43814333cdd4f6cec79188080178 Mon Sep 17 00:00:00 2001 From: acheronfail Date: Tue, 15 Aug 2023 20:00:39 +0930 Subject: [PATCH 56/57] update workarounds for neli after updating it --- Cargo.lock | 34 ++++++++++++++++++++------------- Cargo.toml | 3 +-- src/util/netlink/nl80211/mod.rs | 24 ++++++++++------------- src/util/netlink/route.rs | 6 ++---- 4 files changed, 34 insertions(+), 33 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9a0c8e2..c88d310 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -217,6 +217,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" + [[package]] name = "block-buffer" version = "0.10.4" @@ -314,7 +320,7 @@ checksum = "4f423e341edefb78c9caba2d9c7f7687d0e72e89df3ce3394554754393ac3990" dependencies = [ "anstream", "anstyle", - "bitflags", + "bitflags 1.3.2", "clap_lex", "strsim", ] @@ -976,7 +982,7 @@ version = "2.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1745b20bfc194ac12ef828f144f0ec2d4a7fe993281fa3567a0bd4969aee6890" dependencies = [ - "bitflags", + "bitflags 1.3.2", "libc", "libpulse-sys", "num-derive", @@ -1063,10 +1069,11 @@ dependencies = [ [[package]] name = "neli" -version = "0.7.0-rc1" -source = "git+https://github.com/jbaublitz/neli?branch=v0.7.0-rc2#86a0c7a8fdd6db3b19d4971ab58f0d445ca327b5" +version = "0.7.0-rc2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4de016d5ec13f40df05f92660c9322325d7e15e3ea6f9b365a6e0ecc7cb0b7f2" dependencies = [ - "bitflags", + "bitflags 2.4.0", "byteorder", "derive_builder", "getset", @@ -1079,8 +1086,9 @@ dependencies = [ [[package]] name = "neli-proc-macros" -version = "0.2.0-rc1" -source = "git+https://github.com/jbaublitz/neli?branch=v0.7.0-rc2#86a0c7a8fdd6db3b19d4971ab58f0d445ca327b5" +version = "0.2.0-rc2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce2ca79a37046e8897da8e967f22d7a6c76b9d5ffafbc8d7c57185a63ffe9ff" dependencies = [ "either", "proc-macro2", @@ -1095,7 +1103,7 @@ version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cfg-if", "libc", "memoffset", @@ -1234,7 +1242,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" dependencies = [ "autocfg", - "bitflags", + "bitflags 1.3.2", "cfg-if", "concurrent-queue", "libc", @@ -1371,7 +1379,7 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -1380,7 +1388,7 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -1423,7 +1431,7 @@ version = "0.37.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" dependencies = [ - "bitflags", + "bitflags 1.3.2", "errno", "io-lifetimes", "libc", @@ -2149,7 +2157,7 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b90c622d513012e7419594a2138953603c63848cb189041e7b5dc04d3895da5" dependencies = [ - "bitflags", + "bitflags 1.3.2", "libc", "quick-xml", ] diff --git a/Cargo.toml b/Cargo.toml index ca4bc47..7ba35eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,8 +42,7 @@ libc = "0.2.142" libpulse-binding = { version = "2.27.1", features = ["pa_v14"] } libpulse-tokio = "0.1.0" log = "0.4.17" -# FIXME: use crate version once it's published -neli = { git = "https://github.com/jbaublitz/neli", branch = "v0.7.0-rc2", features = ["tokio", "async"] } +neli = { version = "0.7.0-rc2", features = ["tokio", "async"] } nix = { version = "0.26.2", features = ["net"] } num-traits = "0.2.15" paste = "1.0.12" diff --git a/src/util/netlink/nl80211/mod.rs b/src/util/netlink/nl80211/mod.rs index a0cc906..d20eb34 100644 --- a/src/util/netlink/nl80211/mod.rs +++ b/src/util/netlink/nl80211/mod.rs @@ -169,8 +169,7 @@ impl NetlinkInterface { self.index, e ); - // return immediately, see: https://github.com/jbaublitz/neli/issues/221 - return Ok(None); + continue; } }; @@ -185,7 +184,7 @@ impl NetlinkInterface { Ok(Nl80211IfType::Station) ) { log::debug!("index {} interface is not a station", self.index); - return Ok(None); + continue; } // interface name - not really needed since we'll use the index @@ -223,7 +222,12 @@ impl NetlinkInterface { let mut ssid = match attr_handle .get_attr_payload_as_with_len_borrowed::<&[u8]>(Nl80211Attribute::Ssid) { - Ok(name) => Some(String::from_utf8_lossy(name).into()), + Ok(name) => { + let ssid = String::from_utf8_lossy(name).into(); + log::debug!("index {} found ssid: {}", self.index, ssid); + + Some(ssid) + } // sometimes the `GetInterface` response doesn't include the ssid, but that's okay // since we can fetch it later when we send a `GetScan` request // see: https://github.com/systemd/systemd/issues/24585 @@ -335,8 +339,7 @@ async fn get_scan( match result { Ok(msg) => { if let NlPayload::Payload(gen_msg) = msg.nl_payload() { - // TODO: remove mut when upstream merges https://github.com/jbaublitz/neli/pull/220 - let mut attr_handle = gen_msg.attrs().get_attr_handle(); + let attr_handle = gen_msg.attrs().get_attr_handle(); if let Ok(bss_attrs) = attr_handle.get_nested_attributes::(Nl80211Attribute::Bss) @@ -376,8 +379,6 @@ async fn get_scan( } Err(e) => { log::error!("index {} Nl80211Command::GetScan error: {}", index, e); - // return immediately, see: https://github.com/jbaublitz/neli/issues/221 - return Ok(None); } } } @@ -405,8 +406,7 @@ async fn get_signal_strength( match msg { Ok(msg) => { if let NlPayload::Payload(gen_msg) = msg.nl_payload() { - // TODO: remove mut when upstream merges https://github.com/jbaublitz/neli/pull/220 - let mut attr_handle = gen_msg.attrs().get_attr_handle(); + let attr_handle = gen_msg.attrs().get_attr_handle(); if let Ok(station_info) = attr_handle .get_nested_attributes::(Nl80211Attribute::StaInfo) @@ -441,10 +441,6 @@ async fn get_signal_strength( ) } } - - // TODO: when this errors, calling `recv.next().await` never completes - so return immediately - // see: https://github.com/jbaublitz/neli/issues/221 - return Ok(None); } } } diff --git a/src/util/netlink/route.rs b/src/util/netlink/route.rs index 5fcf36b..6404429 100644 --- a/src/util/netlink/route.rs +++ b/src/util/netlink/route.rs @@ -159,8 +159,7 @@ async fn get_all_interfaces(socket: &Rc) -> Result { Ok(header) => header, Err(e) => { log::error!("an error occurred receiving rtnetlink message: {}", e); - // return immediately, see: https://github.com/jbaublitz/neli/issues/221 - return Ok(interface_map); + continue; } }; @@ -223,8 +222,7 @@ async fn get_all_interfaces(socket: &Rc) -> Result { Ok(header) => header, Err(e) => { log::warn!("an error occurred receiving rtnetlink message: {}", e); - // return immediately, see: https://github.com/jbaublitz/neli/issues/221 - return Ok(interface_map); + continue; } }; From fae15eeb846c93cd716bd1d0eda2513421889a55 Mon Sep 17 00:00:00 2001 From: acheronfail Date: Tue, 15 Aug 2023 21:41:11 +0930 Subject: [PATCH 57/57] 0.6.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- aur | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c88d310..ff10c82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -912,7 +912,7 @@ dependencies = [ [[package]] name = "istat" -version = "0.5.1" +version = "0.6.0" dependencies = [ "async-trait", "automod", diff --git a/Cargo.toml b/Cargo.toml index 7ba35eb..c8b42c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "istat" -version = "0.5.1" +version = "0.6.0" edition = "2021" description = "A lightweight and batteries-included status_command for i3 and sway" license = "GPL-3.0-only" diff --git a/aur b/aur index fae1c03..fae17b0 160000 --- a/aur +++ b/aur @@ -1 +1 @@ -Subproject commit fae1c038fca23f2686bd89ccec3ebb2bf74fe7ef +Subproject commit fae17b05634b1d9d3410f9434af67fb5a43d04e4