diff --git a/Cargo.lock b/Cargo.lock index 95d15cf..0dce5a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -902,7 +902,7 @@ dependencies = [ [[package]] name = "i3stat" -version = "0.11.0" +version = "0.12.0" dependencies = [ "async-trait", "automod", diff --git a/Cargo.toml b/Cargo.toml index d4ca6d9..b6089eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "i3stat" -version = "0.11.0" +version = "0.12.0" edition = "2021" authors = ["acheronfail "] description = "A lightweight and batteries-included status_command for i3 and sway" @@ -11,6 +11,10 @@ keywords = ["i3", "sway", "status_command", "i3stat", "status"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[[bin]] +name = "i3stat-net" +path = "bin/net.rs" + [[bin]] name = "i3stat-acpi" path = "bin/acpi.rs" diff --git a/IDEAS.md b/IDEAS.md index 13a6c58..93a2864 100644 --- a/IDEAS.md +++ b/IDEAS.md @@ -13,4 +13,4 @@ There's no guarantee they'll ever be added or implemented, and they'll likely be ## Improvements -* ... +* add an aarch64 build to github releases diff --git a/bin/acpi.rs b/bin/acpi.rs index f523d65..5dac9e7 100644 --- a/bin/acpi.rs +++ b/bin/acpi.rs @@ -1,6 +1,7 @@ use clap::{ColorChoice, Parser}; use i3stat::error::Result; use i3stat::util::{local_block_on, netlink_acpi_listen}; +use tokio::io::{stdout, AsyncWriteExt}; #[derive(Debug, Parser)] #[clap(author, version, long_about, name = "i3stat-acpi", color = ColorChoice::Always)] @@ -16,7 +17,13 @@ fn main() -> Result<()> { let (output, _) = local_block_on(async { let mut acpi = netlink_acpi_listen().await?; while let Some(event) = acpi.recv().await { - println!("{}", serde_json::to_string(&event)?); + let line = format!("{}", serde_json::to_string(&event)?); + + // flush output each time to facilitate common usage patterns, such as + // `i3stat-acpi | while read x; do ... done`, etc. + let mut stdout = stdout(); + stdout.write_all(line.as_bytes()).await?; + stdout.flush().await?; } Err("unexpected end of acpi event stream".into()) diff --git a/bin/net.rs b/bin/net.rs new file mode 100644 index 0000000..36fb116 --- /dev/null +++ b/bin/net.rs @@ -0,0 +1,98 @@ +use clap::{ColorChoice, Parser, Subcommand}; +use futures::future::join_all; +use i3stat::error::Result; +use i3stat::util::route::InterfaceUpdate; +use i3stat::util::{local_block_on, netlink_ipaddr_listen}; +use serde_json::json; +use tokio::io::{stdout, AsyncWriteExt}; +use tokio::sync::mpsc; + +#[derive(Debug, Parser)] +#[clap(author, version, long_about, name = "i3stat-net", color = ColorChoice::Always)] +/// A command which prints network/80211 information gathered from netlink. +/// +/// Each line is a JSON array which contains a list of all interfaces reported +/// by netlink. Wireless interfaces also print 80211 information. +struct Cli { + #[command(subcommand)] + command: Command, +} + +#[derive(Debug, Copy, Clone, Subcommand)] +enum Command { + /// Print current network interfaces + Info, + /// Watch and print network interfaces whenever a network address change is detected. + Watch, +} + +fn main() -> Result<()> { + let args = Cli::parse(); + + let (output, _) = local_block_on(async { + let (manual_tx, manual_rx) = mpsc::channel(1); + let mut rx = netlink_ipaddr_listen(manual_rx).await?; + manual_tx.send(()).await?; + + if let Command::Info = args.command { + match rx.recv().await { + Some(interfaces) => print_interfaces(&interfaces).await?, + None => println!("null"), + } + + return Ok(()); + } + + while let Some(interfaces) = rx.recv().await { + print_interfaces(&interfaces).await?; + } + + Err("Unexpected end of netlink subscription".into()) + })?; + + output +} + +async fn print_interfaces(interfaces: &InterfaceUpdate) -> Result<()> { + let s = format!( + "{}\n", + json!( + join_all(interfaces.values().map(|interface| async { + json!({ + "index": interface.index, + "name": interface.name, + "mac": interface.mac_address.as_ref().map(|m| m.to_string()), + "ips": interface.ip_addresses.iter().collect::>(), + "wireless": interface.wireless_info().await.map(|info| json!({ + "index": info.index, + "interface": info.interface, + "mac": info.mac_addr.to_string(), + "ssid": info.ssid, + "bssid": info.bssid.as_ref().map(|m| m.to_string()), + "signal": info.signal.as_ref().map(|s| json!({ + "dbm": s.dbm, + "link": s.link, + "quality": s.quality() + })) + })) + }) + })) + .await + ) + ); + + // flush output each time to facilitate common usage patterns like + // `i3stat-net watch | while read x; do ... done`, etc. + let mut stdout = stdout(); + stdout.write_all(s.as_bytes()).await?; + stdout.flush().await?; + + Ok(()) +} + +#[cfg(test)] +#[path = "../src/test_utils.rs"] +mod test_utils; + +#[cfg(test)] +crate::gen_manpage!(Cli); diff --git a/src/util/netlink/acpi/ffi.rs b/src/util/netlink/acpi/ffi.rs index b33b377..4c25bbe 100644 --- a/src/util/netlink/acpi/ffi.rs +++ b/src/util/netlink/acpi/ffi.rs @@ -1,5 +1,6 @@ use ::std::os::raw::{c_char, c_uint}; use serde_derive::{Deserialize, Serialize}; +use std::any::TypeId; use crate::error::{Error, Result}; @@ -50,17 +51,28 @@ 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> { - slice - .into_iter() - .take_while(|c| **c != 0) - .map(|c| -> Result { - if *c < 0 { - Err(format!("slice contained signed char: {}", c).into()) - } else { - Ok(*c as u8) - } - }) - .collect::>>() + // NOTE: on some platforms `c_char` is `i8` and on others it's `u8`. Instead of targeting those + // directly with `#cfg[...]` attributes (because it's not straightforward, have a look at the + // cfg match for `c_char` in the stdlib, it's huge) we instead perform a runtime comparison + // with `TypeId` here. + // According to my tests (with `cargo-show-asm`), this is always optimised out completely since + // `TypeId` returns a constant value, so it's just as good as a compile-time check. + if TypeId::of::() == TypeId::of::() { + slice + .into_iter() + .take_while(|c| **c != 0) + .map(|c| -> Result { + #[allow(unused_comparisons)] + if *c < 0 { + Err(format!("slice contained signed char: {}", c).into()) + } else { + Ok(*c as u8) + } + }) + .collect::>>() + } else { + Ok(slice.iter().map(|&c| c as u8).collect()) + } } impl<'a> TryFrom<&'a acpi_genl_event> for AcpiGenericNetlinkEvent {