use std::{
collections::HashMap,
ffi::{CStr, c_char, c_void},
net::IpAddr,
ptr,
rc::Rc,
sync::{Arc, Mutex},
time::Duration,
};
use anyhow::Result;
use async_trait::async_trait;
use config::{Config, Value};
use derive_builder::Builder;
use futures::task::AtomicWaker;
use if_addrs::{IfAddr, get_if_addrs};
use rustix::{
io::Errno,
ioctl::{Ioctl, Opcode, ioctl},
net::{AddressFamily, SocketType, socket},
};
use tokio_stream::StreamExt;
use crate::{
Attrs, Highlight, ManagedIntervalStream, PanelConfig, PanelRunResult,
array_to_struct,
bar::PanelDrawInfo,
common::{PanelCommon, ShowHide},
remove_string_from_config, remove_uint_from_config,
};
array_to_struct!(NetworkFormats, connected, disconnected);
#[derive(Builder, Debug, Clone)]
#[builder_struct_attr(allow(missing_docs))]
#[builder_impl_attr(allow(missing_docs))]
pub struct Network {
name: &'static str,
#[builder(default = r#"String::from("wlan0")"#)]
if_name: String,
#[builder(default = r#"Duration::from_secs(10)"#)]
duration: Duration,
#[builder(default)]
waker: Arc<AtomicWaker>,
formats: NetworkFormats<String>,
attrs: Attrs,
#[builder(default, setter(strip_option))]
highlight: Option<Highlight>,
common: PanelCommon,
}
impl Network {
fn draw(
&self,
cr: &Rc<cairo::Context>,
height: i32,
paused: Arc<Mutex<bool>>,
) -> Result<PanelDrawInfo> {
let essid = glib::markup_escape_text(
query_essid(self.if_name.as_str())
.unwrap_or_default()
.as_str(),
);
let ip = query_ip(self.if_name.as_str());
let text = ip.map_or_else(
|| {
self.formats
.disconnected
.replace("%ifname%", self.if_name.as_str())
.replace("%essid%", essid.as_str())
},
|ip| {
self.formats
.connected
.replace("%ifname%", self.if_name.as_str())
.replace("%essid%", essid.as_str())
.replace("%local_ip%", ip.to_string().as_str())
},
);
self.common.draw(
cr,
text.as_str(),
&self.attrs,
self.common.dependence,
self.highlight.clone(),
self.common.images.clone(),
height,
ShowHide::Default(paused, self.waker.clone()),
format!("{self:?}"),
)
}
}
#[async_trait(?Send)]
impl PanelConfig for Network {
fn parse(
name: &'static str,
table: &mut HashMap<String, Value>,
_global: &Config,
) -> Result<Self> {
let mut builder = NetworkBuilder::default();
builder.name(name);
if let Some(if_name) = remove_string_from_config("if_name", table) {
builder.if_name(if_name);
}
if let Some(duration) = remove_uint_from_config("interval", table) {
builder.duration(Duration::from_secs(duration));
}
let common = PanelCommon::parse_common(table)?;
let formats = PanelCommon::parse_formats(
table,
&["_connected", "_disconnected"],
&["%ifname% %essid% %local_ip%", "%ifname% disconnected"],
);
let attrs = PanelCommon::parse_attr(table, "");
let highlight = PanelCommon::parse_highlight(table, "");
builder.common(common);
builder.formats(NetworkFormats::new(formats));
builder.attrs(attrs);
builder.highlight(highlight);
Ok(builder.build()?)
}
fn props(&self) -> (&'static str, bool) {
(self.name, self.common.visible)
}
async fn run(
mut self: Box<Self>,
cr: Rc<cairo::Context>,
global_attrs: Attrs,
height: i32,
) -> PanelRunResult {
self.attrs.apply_to(&global_attrs);
let paused = Arc::new(Mutex::new(false));
let stream = ManagedIntervalStream::builder()
.duration(self.duration)
.paused(paused.clone())
.waker(self.waker.clone())
.build()?
.map(move |_| self.draw(&cr, height, paused.clone()));
Ok((Box::pin(stream), None))
}
}
#[repr(C)]
struct Essid {
ptr: *const c_char,
length: u16,
flags: u16,
}
impl Essid {
const fn new(ptr: *const c_char) -> Self {
Self {
ptr,
length: 33,
flags: 0,
}
}
}
#[repr(C)]
struct Data {
essid: Essid,
}
#[repr(C)]
struct Request {
if_name: [u8; 16],
data: Data,
}
impl Request {
fn new(name: &str, buf: &[c_char; 33]) -> Self {
let mut if_name = [0; 16];
if_name[..name.len()].copy_from_slice(name.as_bytes());
Self {
if_name,
data: Data {
essid: Essid::new(buf.as_ptr()),
},
}
}
}
fn query_essid(if_name: &str) -> Result<String> {
let socket = socket(AddressFamily::INET, SocketType::DGRAM, None)?;
let buf = [0; 33];
let req = EssidIoctl {
data: Request::new(if_name, &buf),
};
unsafe { ioctl(socket, req) }?;
let res = buf.as_ptr();
Ok(unsafe { CStr::from_ptr(res) }.to_str()?.to_owned())
}
fn query_ip(if_name: &str) -> Option<IpAddr> {
let (v4, v6) =
get_if_addrs()
.ok()?
.into_iter()
.partition::<Vec<_>, _>(|i| match i.addr {
IfAddr::V4(_) => true,
IfAddr::V6(_) => false,
});
Some(v4.into_iter().chain(v6).find(|i| i.name == if_name)?.ip())
}
struct EssidIoctl {
data: Request,
}
unsafe impl Ioctl for EssidIoctl {
type Output = String;
const IS_MUTATING: bool = true;
fn opcode(&self) -> Opcode {
0x8b1b
}
fn as_ptr(&mut self) -> *mut c_void {
ptr::addr_of_mut!(self.data) as _
}
unsafe fn output_from_ptr(
_out: rustix::ioctl::IoctlOutput,
extract_output: *mut c_void,
) -> rustix::io::Result<Self::Output> {
unsafe {
let req = &mut *(extract_output as *mut Request);
let data = req.data.essid.ptr;
let res = match CStr::from_ptr(data).to_str() {
Ok(s) => s.to_owned(),
Err(_) => return Err(Errno::BADMSG),
};
Ok(res)
}
}
}