> for Event {
+ type Error = Report;
+
+ fn try_from(data: KeyData) -> Result {
+ let key = Key::try_from(data.key)?;
+
+ key.get_state(data.device_path)
+ .map(|state| KeyEvent { key, state })
+ .map(Event::Key)
+ }
+}
+
+#[derive(Debug)]
+pub struct Client {
+ tx: broadcast::Sender,
+ _rx: broadcast::Receiver,
+
+ seat: String,
+ known_devices: Arc>>,
+}
+
+impl Client {
+ pub fn init(seat: String) -> Arc {
+ let client = Arc::new(Self::new(seat));
+
+ {
+ let client = client.clone();
+
+ std::thread::spawn(move || {
+ let local = LocalSet::new();
+
+ local.spawn_local(async move {
+ if let Err(err) = client.run().await {
+ error!("{err:?}");
+ }
+ });
+
+ Ironbar::runtime().block_on(local);
+ });
+ }
+
+ client
+ }
+
+ fn new(seat: String) -> Self {
+ let (tx, rx) = broadcast::channel(4);
+
+ Self {
+ tx,
+ _rx: rx,
+ seat,
+ known_devices: arc_rw!(vec![]),
+ }
+ }
+
+ fn open_restricted(path: &CStr, flags: c_int) -> std::result::Result {
+ open(path, OFlags::from_bits_retain(flags as u32), Mode::empty())
+ .map(IntoRawFd::into_raw_fd)
+ .map_err(Errno::raw_os_error)
+ }
+
+ fn close_restricted(fd: c_int) {
+ drop(unsafe { OwnedFd::from_raw_fd(fd) });
+ }
+
+ async fn run(&self) -> Result<()> {
+ let mut libinput = Libinput::with_tracing(Self::open_restricted, Self::close_restricted)?;
+
+ libinput.udev_assign_seat(CString::new(&*self.seat)?.as_c_str())?;
+
+ let mut stream = libinput.event_stream()?;
+ while let Some(event) = stream.try_next().await? {
+ match event {
+ colpetto::Event::Device(DeviceEvent::Added(event)) => {
+ let device = event.device();
+ if !device.has_capability(DeviceCapability::Keyboard) {
+ continue;
+ }
+
+ let name = device.name();
+ let Some(device) = event.device().udev_device() else {
+ continue;
+ };
+
+ if let Some(device_path) = device.devnode() {
+ // not all devices which report as keyboards actually are one -
+ // fire test event so we can figure out if it is
+ let caps_event: Result = KeyData {
+ device_path,
+ key: EV_KEY::KEY_CAPSLOCK,
+ }
+ .try_into();
+
+ if caps_event.is_ok() {
+ debug!(
+ "new keyboard device: {} | {}",
+ name.to_string_lossy(),
+ device_path.display()
+ );
+ write_lock!(self.known_devices).push(device_path.to_path_buf());
+ self.tx.send_expect(Event::Device);
+ }
+ }
+ }
+ colpetto::Event::Keyboard(KeyboardEvent::Key(event))
+ if event.key_state() == KeyState::Released =>
+ {
+ let Some(device) = event.device().udev_device() else {
+ continue;
+ };
+
+ let Some(
+ key @ (EV_KEY::KEY_CAPSLOCK | EV_KEY::KEY_NUMLOCK | EV_KEY::KEY_SCROLLLOCK),
+ ) = int_to_ev_key(event.key())
+ else {
+ continue;
+ };
+
+ if let Some(device_path) = device.devnode().map(PathBuf::from) {
+ let tx = self.tx.clone();
+
+ // need to spawn a task to avoid blocking
+ spawn(async move {
+ // wait for kb to change
+ sleep(Duration::from_millis(50)).await;
+
+ let data = KeyData { device_path, key };
+
+ if let Ok(event) = data.try_into() {
+ tx.send_expect(event);
+ }
+ });
+ }
+ }
+ _ => {}
+ }
+ }
+
+ Err(Report::msg("unexpected end of stream"))
+ }
+
+ pub fn get_state(&self, key: Key) -> bool {
+ read_lock!(self.known_devices)
+ .iter()
+ .map(|device_path| key.get_state(device_path))
+ .filter_map(Result::ok)
+ .reduce(|state, curr| state || curr)
+ .unwrap_or_default()
+ }
+
+ pub fn subscribe(&self) -> broadcast::Receiver {
+ self.tx.subscribe()
+ }
+}
diff --git a/src/clients/mod.rs b/src/clients/mod.rs
index ebe2bda..c4fce0d 100644
--- a/src/clients/mod.rs
+++ b/src/clients/mod.rs
@@ -1,21 +1,33 @@
-use crate::{await_sync, Ironbar};
+use crate::{Ironbar, await_sync};
use color_eyre::Result;
+use std::collections::HashMap;
use std::path::Path;
use std::rc::Rc;
use std::sync::Arc;
#[cfg(feature = "clipboard")]
pub mod clipboard;
-#[cfg(feature = "workspaces")]
+#[cfg(any(
+ feature = "bindmode",
+ feature = "hyprland",
+ feature = "keyboard",
+ feature = "workspaces",
+))]
pub mod compositor;
+#[cfg(feature = "keyboard")]
+pub mod libinput;
#[cfg(feature = "cairo")]
pub mod lua;
#[cfg(feature = "music")]
pub mod music;
#[cfg(feature = "network_manager")]
pub mod networkmanager;
+#[cfg(feature = "sway")]
+pub mod sway;
#[cfg(feature = "notifications")]
pub mod swaync;
+#[cfg(feature = "sys_info")]
+pub mod sysinfo;
#[cfg(feature = "tray")]
pub mod tray;
#[cfg(feature = "upower")]
@@ -31,16 +43,28 @@ pub struct Clients {
wayland: Option>,
#[cfg(feature = "workspaces")]
workspaces: Option>,
+ #[cfg(feature = "sway")]
+ sway: Option>,
+ #[cfg(feature = "hyprland")]
+ hyprland: Option>,
+ #[cfg(feature = "bindmode")]
+ bindmode: Option>,
#[cfg(feature = "clipboard")]
clipboard: Option>,
+ #[cfg(feature = "keyboard")]
+ libinput: HashMap, Arc>,
+ #[cfg(feature = "keyboard")]
+ keyboard_layout: Option>,
#[cfg(feature = "cairo")]
lua: Option>,
#[cfg(feature = "music")]
- music: std::collections::HashMap>,
+ music: HashMap>,
#[cfg(feature = "network_manager")]
network_manager: Option>,
#[cfg(feature = "notifications")]
notifications: Option>,
+ #[cfg(feature = "sys_info")]
+ sys_info: Option>,
#[cfg(feature = "tray")]
tray: Option>,
#[cfg(feature = "upower")]
@@ -73,18 +97,68 @@ impl Clients {
#[cfg(feature = "workspaces")]
pub fn workspaces(&mut self) -> ClientResult {
- let client = match &self.workspaces {
- Some(workspaces) => workspaces.clone(),
- None => {
- let client = compositor::Compositor::create_workspace_client()?;
- self.workspaces.replace(client.clone());
- client
- }
+ let client = if let Some(workspaces) = &self.workspaces {
+ workspaces.clone()
+ } else {
+ let client = compositor::Compositor::create_workspace_client(self)?;
+ self.workspaces.replace(client.clone());
+ client
};
Ok(client)
}
+ #[cfg(feature = "keyboard")]
+ pub fn keyboard_layout(&mut self) -> ClientResult {
+ let client = if let Some(keyboard_layout) = &self.keyboard_layout {
+ keyboard_layout.clone()
+ } else {
+ let client = compositor::Compositor::create_keyboard_layout_client(self)?;
+ self.keyboard_layout.replace(client.clone());
+ client
+ };
+
+ Ok(client)
+ }
+
+ #[cfg(feature = "bindmode")]
+ pub fn bindmode(&mut self) -> ClientResult {
+ let client = if let Some(client) = &self.bindmode {
+ client.clone()
+ } else {
+ let client = compositor::Compositor::create_bindmode_client(self)?;
+ self.bindmode.replace(client.clone());
+ client
+ };
+
+ Ok(client)
+ }
+
+ #[cfg(feature = "sway")]
+ pub fn sway(&mut self) -> ClientResult {
+ let client = if let Some(client) = &self.sway {
+ client.clone()
+ } else {
+ let client = await_sync(async { sway::Client::new().await })?;
+ let client = Arc::new(client);
+ self.sway.replace(client.clone());
+ client
+ };
+
+ Ok(client)
+ }
+
+ #[cfg(feature = "hyprland")]
+ pub fn hyprland(&mut self) -> Arc {
+ if let Some(client) = &self.hyprland {
+ client.clone()
+ } else {
+ let client = Arc::new(compositor::hyprland::Client::new());
+ self.hyprland.replace(client.clone());
+ client
+ }
+ }
+
#[cfg(feature = "cairo")]
pub fn lua(&mut self, config_dir: &Path) -> Rc {
self.lua
@@ -92,6 +166,17 @@ impl Clients {
.clone()
}
+ #[cfg(feature = "keyboard")]
+ pub fn libinput(&mut self, seat: &str) -> Arc {
+ if let Some(client) = self.libinput.get(seat) {
+ client.clone()
+ } else {
+ let client = libinput::Client::init(seat.to_string());
+ self.libinput.insert(seat.into(), client.clone());
+ client
+ }
+ }
+
#[cfg(feature = "music")]
pub fn music(&mut self, client_type: music::ClientType) -> Arc {
self.music
@@ -102,55 +187,68 @@ impl Clients {
#[cfg(feature = "network_manager")]
pub fn network_manager(&mut self) -> ClientResult {
- match &self.network_manager {
- Some(client) => Ok(client.clone()),
- None => {
- let client = networkmanager::create_client()?;
- self.network_manager = Some(client.clone());
- Ok(client)
- }
+ if let Some(client) = &self.network_manager {
+ Ok(client.clone())
+ } else {
+ let client = await_sync(async move { networkmanager::create_client().await })?;
+ self.network_manager = Some(client.clone());
+ Ok(client)
}
}
#[cfg(feature = "notifications")]
pub fn notifications(&mut self) -> ClientResult {
- let client = match &self.notifications {
- Some(client) => client.clone(),
- None => {
- let client = await_sync(async { swaync::Client::new().await })?;
- let client = Arc::new(client);
- self.notifications.replace(client.clone());
- client
- }
+ let client = if let Some(client) = &self.notifications {
+ client.clone()
+ } else {
+ let client = await_sync(async { swaync::Client::new().await })?;
+ let client = Arc::new(client);
+ self.notifications.replace(client.clone());
+ client
};
Ok(client)
}
+ #[cfg(feature = "sys_info")]
+ pub fn sys_info(&mut self) -> Arc {
+ self.sys_info
+ .get_or_insert_with(|| {
+ let client = Arc::new(sysinfo::Client::new());
+
+ #[cfg(feature = "ipc")]
+ Ironbar::variable_manager().register_namespace("sysinfo", client.clone());
+
+ client
+ })
+ .clone()
+ }
+
#[cfg(feature = "tray")]
pub fn tray(&mut self) -> ClientResult {
- let client = match &self.tray {
- Some(client) => client.clone(),
- None => {
- let service_name = format!("{}-{}", env!("CARGO_CRATE_NAME"), Ironbar::unique_id());
-
- let client = await_sync(async { tray::Client::new(&service_name).await })?;
- let client = Arc::new(client);
- self.tray.replace(client.clone());
- client
- }
+ let client = if let Some(client) = &self.tray {
+ client.clone()
+ } else {
+ let client = await_sync(async { tray::Client::new().await })?;
+ let client = Arc::new(client);
+ self.tray.replace(client.clone());
+ client
};
Ok(client)
}
#[cfg(feature = "upower")]
- pub fn upower(&mut self) -> Arc> {
- self.upower
- .get_or_insert_with(|| {
- crate::await_sync(async { upower::create_display_proxy().await })
- })
- .clone()
+ pub fn upower(&mut self) -> ClientResult> {
+ let client = if let Some(client) = &self.upower {
+ client.clone()
+ } else {
+ let client = await_sync(async { upower::create_display_proxy().await })?;
+ self.upower.replace(client.clone());
+ client
+ };
+
+ Ok(client)
}
#[cfg(feature = "volume")]
diff --git a/src/clients/music/mod.rs b/src/clients/music/mod.rs
index e42b60f..52895b7 100644
--- a/src/clients/music/mod.rs
+++ b/src/clients/music/mod.rs
@@ -70,13 +70,17 @@ pub trait MusicClient: Debug + Send + Sync {
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum ClientType {
+ #[cfg(feature = "music+mpd")]
Mpd { host: String, music_dir: PathBuf },
+ #[cfg(feature = "music+mpris")]
Mpris,
}
pub fn create_client(client_type: ClientType) -> Arc {
match client_type {
+ #[cfg(feature = "music+mpd")]
ClientType::Mpd { host, music_dir } => Arc::new(mpd::Client::new(host, music_dir)),
+ #[cfg(feature = "music+mpris")]
ClientType::Mpris => Arc::new(mpris::Client::new()),
}
}
diff --git a/src/clients/music/mpd.rs b/src/clients/music/mpd.rs
index f1239c9..790d6ba 100644
--- a/src/clients/music/mpd.rs
+++ b/src/clients/music/mpd.rs
@@ -1,14 +1,15 @@
use super::{
- MusicClient, PlayerState, PlayerUpdate, ProgressTick, Status, Track, TICK_INTERVAL_MS,
+ MusicClient, PlayerState, PlayerUpdate, ProgressTick, Status, TICK_INTERVAL_MS, Track,
};
-use crate::{await_sync, send, spawn, Ironbar};
+use crate::channels::SyncSenderExt;
+use crate::{Ironbar, await_sync, spawn};
use color_eyre::Report;
use color_eyre::Result;
use mpd_client::client::{ConnectionEvent, Subsystem};
use mpd_client::commands::{self, SeekMode};
use mpd_client::responses::{PlayState, Song};
use mpd_client::tag::Tag;
-use mpd_utils::{mpd_client, PersistentClient};
+use mpd_utils::{PersistentClient, mpd_client};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
@@ -97,7 +98,7 @@ impl Client {
let status = Status::from(status);
let update = PlayerUpdate::Update(Box::new(track), status);
- send!(tx, update);
+ tx.send_expect(update);
}
Ok(())
@@ -113,7 +114,7 @@ impl Client {
elapsed: status.elapsed,
});
- send!(tx, update);
+ tx.send_expect(update);
}
}
}
diff --git a/src/clients/music/mpris.rs b/src/clients/music/mpris.rs
index d505560..96af8cd 100644
--- a/src/clients/music/mpris.rs
+++ b/src/clients/music/mpris.rs
@@ -1,6 +1,7 @@
-use super::{MusicClient, PlayerState, PlayerUpdate, Status, Track, TICK_INTERVAL_MS};
+use super::{MusicClient, PlayerState, PlayerUpdate, Status, TICK_INTERVAL_MS, Track};
+use crate::channels::SyncSenderExt;
use crate::clients::music::ProgressTick;
-use crate::{arc_mut, lock, send, spawn_blocking};
+use crate::{arc_mut, lock, spawn_blocking};
use color_eyre::Result;
use mpris::{DBusError, Event, Metadata, PlaybackStatus, Player, PlayerFinder};
use std::cmp;
@@ -47,10 +48,14 @@ impl Client {
)) if transport_error.name() == Some(NO_ACTIVE_PLAYER)
|| transport_error.name() == Some(NO_REPLY) =>
{
- Vec::new()
+ vec![]
+ }
+ _ => {
+ error!("D-Bus error getting MPRIS players: {e:?}");
+ vec![]
}
- _ => panic!("Failed to connect to D-Bus"),
});
+
// Acquire the lock of current_player before players to avoid deadlock.
// There are places where we lock on current_player and players, but we always lock on current_player first.
// This is because we almost never need to lock on players without locking on current_player.
@@ -133,7 +138,7 @@ impl Client {
let mut players_locked = lock!(players);
players_locked.remove(identity);
if players_locked.is_empty() {
- send!(tx, PlayerUpdate::Update(Box::new(None), Status::default()));
+ tx.send_expect(PlayerUpdate::Update(Box::new(None), Status::default()));
}
};
@@ -208,7 +213,7 @@ impl Client {
let track = Track::from(metadata);
let player_update = PlayerUpdate::Update(Box::new(Some(track)), status);
- send!(tx, player_update);
+ tx.send_expect(player_update);
Ok(())
}
@@ -238,7 +243,7 @@ impl Client {
duration: metadata.length(),
});
- send!(tx, update);
+ tx.send_expect(update);
}
}
}
@@ -286,14 +291,22 @@ impl MusicClient for Client {
fn seek(&self, duration: Duration) -> Result<()> {
if let Some(player) = Self::get_player(self) {
- let pos = player.get_position().unwrap_or_default();
+ // if possible, use `set_position` instead of `seek` because some players have issues with seeking
+ // see https://github.com/JakeStanger/ironbar/issues/970
+ if let Ok(metadata) = player.get_metadata() {
+ if let Some(track_id) = metadata.track_id() {
+ player.set_position(track_id, &duration)?;
+ } else {
+ let pos = player.get_position().unwrap_or_default();
- let duration = duration.as_micros() as i64;
- let position = pos.as_micros() as i64;
+ let duration = duration.as_micros() as i64;
+ let position = pos.as_micros() as i64;
- let seek = cmp::max(duration, 0) - position;
+ let seek = cmp::max(duration, 0) - position;
- player.seek(seek)?;
+ player.seek(seek)?;
+ }
+ }
} else {
error!("Could not find player");
}
@@ -315,7 +328,9 @@ impl MusicClient for Client {
state: PlayerState::Stopped,
volume_percent: None,
};
- send!(self.tx, PlayerUpdate::Update(Box::new(None), status));
+
+ self.tx
+ .send_expect(PlayerUpdate::Update(Box::new(None), status));
}
rx
diff --git a/src/clients/networkmanager/dbus.rs b/src/clients/networkmanager/dbus.rs
index 5ab3889..b3dd915 100644
--- a/src/clients/networkmanager/dbus.rs
+++ b/src/clients/networkmanager/dbus.rs
@@ -1,71 +1,71 @@
use color_eyre::Result;
-use zbus::dbus_proxy;
+use zbus::proxy;
use zbus::zvariant::{ObjectPath, OwnedValue, Str};
-#[dbus_proxy(
+#[proxy(
default_service = "org.freedesktop.NetworkManager",
interface = "org.freedesktop.NetworkManager",
default_path = "/org/freedesktop/NetworkManager"
)]
-trait Dbus {
- #[dbus_proxy(property)]
+pub(super) trait Dbus {
+ #[zbus(property)]
fn active_connections(&self) -> Result>;
- #[dbus_proxy(property)]
+ #[zbus(property)]
fn devices(&self) -> Result>;
- // #[dbus_proxy(property)]
+ // #[zbus(property)]
// fn networking_enabled(&self) -> Result;
- // #[dbus_proxy(property)]
+ // #[zbus(property)]
// fn primary_connection(&self) -> Result;
- // #[dbus_proxy(property)]
+ // #[zbus(property)]
// fn primary_connection_type(&self) -> Result;
- // #[dbus_proxy(property)]
+ // #[zbus(property)]
// fn wireless_enabled(&self) -> Result;
}
-#[dbus_proxy(
+#[proxy(
default_service = "org.freedesktop.NetworkManager",
interface = "org.freedesktop.NetworkManager.Connection.Active"
)]
-trait ActiveConnectionDbus {
- // #[dbus_proxy(property)]
+pub(super) trait ActiveConnectionDbus {
+ // #[zbus(property)]
// fn connection(&self) -> Result;
- // #[dbus_proxy(property)]
+ // #[zbus(property)]
// fn default(&self) -> Result;
- // #[dbus_proxy(property)]
+ // #[zbus(property)]
// fn default6(&self) -> Result;
- #[dbus_proxy(property)]
+ #[zbus(property)]
fn devices(&self) -> Result>;
- // #[dbus_proxy(property)]
+ // #[zbus(property)]
// fn id(&self) -> Result;
- #[dbus_proxy(property)]
+ #[zbus(property)]
fn type_(&self) -> Result;
- // #[dbus_proxy(property)]
+ // #[zbus(property)]
// fn uuid(&self) -> Result;
}
-#[dbus_proxy(
+#[proxy(
default_service = "org.freedesktop.NetworkManager",
interface = "org.freedesktop.NetworkManager.Device"
)]
-trait DeviceDbus {
- // #[dbus_proxy(property)]
+pub(super) trait DeviceDbus {
+ // #[zbus(property)]
// fn active_connection(&self) -> Result;
- #[dbus_proxy(property)]
+ #[zbus(property)]
fn device_type(&self) -> Result;
- #[dbus_proxy(property)]
+ #[zbus(property)]
fn state(&self) -> Result;
}
diff --git a/src/clients/networkmanager/mod.rs b/src/clients/networkmanager/mod.rs
index 40c64e1..5f75b08 100644
--- a/src/clients/networkmanager/mod.rs
+++ b/src/clients/networkmanager/mod.rs
@@ -2,17 +2,16 @@ use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use color_eyre::Result;
+use futures_lite::StreamExt;
use futures_signals::signal::{Mutable, MutableSignalCloned};
use tracing::error;
-use zbus::blocking::Connection;
+use zbus::Connection;
use zbus::zvariant::ObjectPath;
-use crate::clients::networkmanager::dbus::{
- ActiveConnectionDbusProxyBlocking, DbusProxyBlocking, DeviceDbusProxyBlocking,
-};
+use crate::clients::networkmanager::dbus::{ActiveConnectionDbusProxy, DbusProxy, DeviceDbusProxy};
use crate::clients::networkmanager::state::{
- determine_cellular_state, determine_vpn_state, determine_wifi_state, determine_wired_state,
- CellularState, State, VpnState, WifiState, WiredState,
+ CellularState, State, VpnState, WifiState, WiredState, determine_cellular_state,
+ determine_vpn_state, determine_wifi_state, determine_wired_state,
};
use crate::{
read_lock, register_fallible_client, spawn_blocking, spawn_blocking_result, write_lock,
@@ -29,23 +28,23 @@ pub struct Client(Arc>);
#[derive(Debug)]
struct ClientInner<'l> {
state: Mutable,
- root_object: &'l DbusProxyBlocking<'l>,
- active_connections: RwLock>>,
- devices: RwLock>>,
+ root_object: &'l DbusProxy<'l>,
+ active_connections: RwLock>>,
+ devices: RwLock>>,
dbus_connection: Connection,
}
impl Client {
- fn new() -> Result {
+ async fn new() -> Result {
let state = Mutable::new(State {
wired: WiredState::Unknown,
wifi: WifiState::Unknown,
cellular: CellularState::Unknown,
vpn: VpnState::Unknown,
});
- let dbus_connection = Connection::system()?;
+ let dbus_connection = Connection::system().await?;
let root_object = {
- let root_object = DbusProxyBlocking::new(&dbus_connection)?;
+ let root_object = DbusProxy::new(&dbus_connection).await?;
// Workaround for the fact that zbus (unnecessarily) requires a static lifetime here
Box::leak(Box::new(root_object))
};
@@ -59,158 +58,159 @@ impl Client {
})))
}
- fn run(&self) -> Result<()> {
- macro_rules! update_state_for_device_change {
- ($client:ident) => {
- $client.state.set(State {
- wired: determine_wired_state(&read_lock!($client.devices))?,
- wifi: determine_wifi_state(&read_lock!($client.devices))?,
- cellular: determine_cellular_state(&read_lock!($client.devices))?,
- vpn: $client.state.get_cloned().vpn,
- });
- };
+ async fn run(&self) -> Result<()> {
+ // TODO: Reimplement DBus watching without these write-only macros
+
+ let mut active_connections_stream = self.0.root_object.receive_active_connections_changed().await;
+ while let Some(change) = active_connections_stream.next().await {
+
}
- macro_rules! initialise_path_map {
- (
- $client:expr,
- $path_map:ident,
- $proxy_type:ident
- $(, |$new_path:ident| $property_watcher:expr)*
- ) => {
- let new_paths = $client.root_object.$path_map()?;
- let mut path_map = HashMap::new();
- for new_path in new_paths {
- let new_proxy = $proxy_type::builder(&$client.dbus_connection)
- .path(new_path.clone())?
- .build()?;
- path_map.insert(new_path.clone(), new_proxy);
- $({
- let $new_path = &new_path;
- $property_watcher;
- })*
- }
- *write_lock!($client.$path_map) = path_map;
- };
- }
+ // ActiveConnectionDbusProxy::builder(&self.0.dbus_connection)
- macro_rules! spawn_path_list_watcher {
- (
- $client:expr,
- $property:ident,
- $property_changes:ident,
- $proxy_type:ident,
- |$state_client:ident| $state_update:expr
- $(, |$property_client:ident, $new_path:ident| $property_watcher:expr)*
- ) => {
- let client = $client.clone();
- spawn_blocking_result!({
- let changes = client.root_object.$property_changes();
- for _ in changes {
- let mut new_path_map = HashMap::new();
- {
- let new_paths = client.root_object.$property()?;
- let path_map = read_lock!(client.$property);
- for new_path in new_paths {
- if path_map.contains_key(&new_path) {
- let proxy = path_map
- .get(&new_path)
- .expect("Should contain the key, guarded by runtime check");
- new_path_map.insert(new_path, proxy.to_owned());
- } else {
- let new_proxy = $proxy_type::builder(&client.dbus_connection)
- .path(new_path.clone())?
- .build()?;
- new_path_map.insert(new_path.clone(), new_proxy);
- $({
- let $property_client = &client;
- let $new_path = &new_path;
- $property_watcher;
- })*
- }
- }
- }
- *write_lock!(client.$property) = new_path_map;
- let $state_client = &client;
- $state_update;
- }
- Ok(())
- });
- }
- }
-
- macro_rules! spawn_property_watcher {
- (
- $client:expr,
- $path:expr,
- $property_changes:ident,
- $containing_list:ident,
- |$inner_client:ident| $state_update:expr
- ) => {
- let client = $client.clone();
- let path = $path.clone();
- spawn_blocking_result!({
- let changes = read_lock!(client.$containing_list)
- .get(&path)
- .expect("Should contain the key upon watcher start")
- .$property_changes();
- for _ in changes {
- if !read_lock!(client.$containing_list).contains_key(&path) {
- break;
- }
- let $inner_client = &client;
- $state_update;
- }
- Ok(())
- });
- };
- }
-
- initialise_path_map!(
- self.0,
- active_connections,
- ActiveConnectionDbusProxyBlocking
- );
- initialise_path_map!(self.0, devices, DeviceDbusProxyBlocking, |path| {
- spawn_property_watcher!(self.0, path, receive_state_changed, devices, |client| {
- update_state_for_device_change!(client);
- });
- });
- self.0.state.set(State {
- wired: determine_wired_state(&read_lock!(self.0.devices))?,
- wifi: determine_wifi_state(&read_lock!(self.0.devices))?,
- cellular: determine_cellular_state(&read_lock!(self.0.devices))?,
- vpn: determine_vpn_state(&read_lock!(self.0.active_connections))?,
- });
-
- spawn_path_list_watcher!(
- self.0,
- active_connections,
- receive_active_connections_changed,
- ActiveConnectionDbusProxyBlocking,
- |client| {
- client.state.set(State {
- wired: client.state.get_cloned().wired,
- wifi: client.state.get_cloned().wifi,
- cellular: client.state.get_cloned().cellular,
- vpn: determine_vpn_state(&read_lock!(client.active_connections))?,
- });
- }
- );
- spawn_path_list_watcher!(
- self.0,
- devices,
- receive_devices_changed,
- DeviceDbusProxyBlocking,
- |client| {
- update_state_for_device_change!(client);
- },
- |client, path| {
- spawn_property_watcher!(client, path, receive_state_changed, devices, |client| {
- update_state_for_device_change!(client);
- });
- }
- );
+ // macro_rules! update_state_for_device_change {
+ // ($client:ident) => {
+ // $client.state.set(State {
+ // wired: determine_wired_state(&read_lock!($client.devices)).await?,
+ // wifi: determine_wifi_state(&read_lock!($client.devices)).await?,
+ // cellular: determine_cellular_state(&read_lock!($client.devices)).await?,
+ // vpn: $client.state.get_cloned().vpn,
+ // });
+ // };
+ // }
+ //
+ // macro_rules! initialise_path_map {
+ // (
+ // $client:expr,
+ // $path_map:ident,
+ // $proxy_type:ident
+ // $(, |$new_path:ident| $property_watcher:expr)*
+ // ) => {
+ // let new_paths = $client.root_object.$path_map().await?;
+ // let mut path_map = HashMap::new();
+ // for new_path in new_paths {
+ // let new_proxy = $proxy_type::builder(&$client.dbus_connection)
+ // .path(new_path.clone())?
+ // .build().await?;
+ // path_map.insert(new_path.clone(), new_proxy);
+ // $({
+ // let $new_path = &new_path;
+ // $property_watcher;
+ // })*
+ // }
+ // *write_lock!($client.$path_map) = path_map;
+ // };
+ // }
+ //
+ // macro_rules! spawn_path_list_watcher {
+ // (
+ // $client:expr,
+ // $property:ident,
+ // $property_changes:ident,
+ // $proxy_type:ident,
+ // |$state_client:ident| $state_update:expr
+ // $(, |$property_client:ident, $new_path:ident| $property_watcher:expr)*
+ // ) => {
+ // let client = $client.clone();
+ //
+ // let changes = client.root_object.$property_changes();
+ // for _ in changes {
+ // let mut new_path_map = HashMap::new();
+ // {
+ // let new_paths = client.root_object.$property()?;
+ // let path_map = read_lock!(client.$property);
+ // for new_path in new_paths {
+ // if path_map.contains_key(&new_path) {
+ // let proxy = path_map
+ // .get(&new_path)
+ // .expect("Should contain the key, guarded by runtime check");
+ // new_path_map.insert(new_path, proxy.to_owned());
+ // } else {
+ // let new_proxy = $proxy_type::builder(&client.dbus_connection)
+ // .path(new_path.clone())?
+ // .build()?;
+ // new_path_map.insert(new_path.clone(), new_proxy);
+ // $({
+ // let $property_client = &client;
+ // let $new_path = &new_path;
+ // $property_watcher;
+ // })*
+ // }
+ // }
+ // }
+ // *write_lock!(client.$property) = new_path_map;
+ // let $state_client = &client;
+ // $state_update;
+ // }
+ // }
+ // }
+ //
+ // macro_rules! spawn_property_watcher {
+ // (
+ // $client:expr,
+ // $path:expr,
+ // $property_changes:ident,
+ // $containing_list:ident,
+ // |$inner_client:ident| $state_update:expr
+ // ) => {
+ // let client = $client.clone();
+ // let path = $path.clone();
+ //
+ // let changes = read_lock!(client.$containing_list)
+ // .get(&path)
+ // .expect("Should contain the key upon watcher start")
+ // .$property_changes().await;
+ // for _ in changes {
+ // if !read_lock!(client.$containing_list).contains_key(&path) {
+ // break;
+ // }
+ // let $inner_client = &client;
+ // $state_update;
+ // }
+ // };
+ // }
+ //
+ // initialise_path_map!(self.0, active_connections, ActiveConnectionDbusProxy);
+ // initialise_path_map!(self.0, devices, DeviceDbusProxy, |path| {
+ // spawn_property_watcher!(self.0, path, receive_state_changed, devices, |client| {
+ // update_state_for_device_change!(client);
+ // });
+ // });
+ // self.0.state.set(State {
+ // wired: determine_wired_state(&read_lock!(self.0.devices))?,
+ // wifi: determine_wifi_state(&read_lock!(self.0.devices))?,
+ // cellular: determine_cellular_state(&read_lock!(self.0.devices))?,
+ // vpn: determine_vpn_state(&read_lock!(self.0.active_connections))?,
+ // });
+ //
+ // spawn_path_list_watcher!(
+ // self.0,
+ // active_connections,
+ // receive_active_connections_changed,
+ // ActiveConnectionDbusProxy,
+ // |client| {
+ // client.state.set(State {
+ // wired: client.state.get_cloned().wired,
+ // wifi: client.state.get_cloned().wifi,
+ // cellular: client.state.get_cloned().cellular,
+ // vpn: determine_vpn_state(&read_lock!(client.active_connections))?,
+ // });
+ // }
+ // );
+ // spawn_path_list_watcher!(
+ // self.0,
+ // devices,
+ // receive_devices_changed,
+ // DeviceDbusProxy,
+ // |client| {
+ // update_state_for_device_change!(client);
+ // },
+ // |client, path| {
+ // spawn_property_watcher!(client, path, receive_state_changed, devices, |client| {
+ // update_state_for_device_change!(client);
+ // });
+ // }
+ // );
Ok(())
}
@@ -220,15 +220,9 @@ impl Client {
}
}
-pub fn create_client() -> Result> {
- let client = Arc::new(Client::new()?);
- {
- let client = client.clone();
- spawn_blocking_result!({
- client.run()?;
- Ok(())
- });
- }
+pub async fn create_client() -> Result> {
+ let client = Arc::new(Client::new().await?);
+ client.run().await?;
Ok(client)
}
diff --git a/src/clients/networkmanager/state.rs b/src/clients/networkmanager/state.rs
index e5e226e..079bbfa 100644
--- a/src/clients/networkmanager/state.rs
+++ b/src/clients/networkmanager/state.rs
@@ -1,9 +1,9 @@
use color_eyre::Result;
-use crate::clients::networkmanager::dbus::{
- ActiveConnectionDbusProxyBlocking, DeviceDbusProxyBlocking, DeviceState, DeviceType,
-};
use crate::clients::networkmanager::PathMap;
+use crate::clients::networkmanager::dbus::{
+ ActiveConnectionDbusProxy, DeviceDbusProxy, DeviceState, DeviceType,
+};
#[derive(Clone, Debug)]
pub struct State {
@@ -56,16 +56,16 @@ pub struct VpnConnectedState {
pub name: String,
}
-pub(super) fn determine_wired_state(
- devices: &PathMap,
+pub(super) async fn determine_wired_state(
+ devices: &PathMap<'_, DeviceDbusProxy<'_>>,
) -> Result {
let mut present = false;
let mut connected = false;
for device in devices.values() {
- if device.device_type()? == DeviceType::Ethernet {
+ if device.device_type().await? == DeviceType::Ethernet {
present = true;
- if device.state()?.is_enabled() {
+ if device.state().await?.is_enabled() {
connected = true;
break;
}
@@ -81,19 +81,19 @@ pub(super) fn determine_wired_state(
}
}
-pub(super) fn determine_wifi_state(
- devices: &PathMap,
+pub(super) async fn determine_wifi_state(
+ devices: &PathMap<'_, DeviceDbusProxy<'_>>,
) -> Result {
let mut present = false;
let mut enabled = false;
let mut connected = false;
for device in devices.values() {
- if device.device_type()? == DeviceType::Wifi {
+ if device.device_type().await? == DeviceType::Wifi {
present = true;
- if device.state()?.is_enabled() {
+ if device.state().await?.is_enabled() {
enabled = true;
- if device.state()? == DeviceState::Activated {
+ if device.state().await? == DeviceState::Activated {
connected = true;
break;
}
@@ -115,19 +115,19 @@ pub(super) fn determine_wifi_state(
}
}
-pub(super) fn determine_cellular_state(
- devices: &PathMap,
+pub(super) async fn determine_cellular_state(
+ devices: &PathMap<'_, DeviceDbusProxy<'_>>,
) -> Result {
let mut present = false;
let mut enabled = false;
let mut connected = false;
for device in devices.values() {
- if device.device_type()? == DeviceType::Modem {
+ if device.device_type().await? == DeviceType::Modem {
present = true;
- if device.state()?.is_enabled() {
+ if device.state().await?.is_enabled() {
enabled = true;
- if device.state()? == DeviceState::Activated {
+ if device.state().await? == DeviceState::Activated {
connected = true;
break;
}
@@ -146,11 +146,11 @@ pub(super) fn determine_cellular_state(
}
}
-pub(super) fn determine_vpn_state(
- active_connections: &PathMap,
+pub(super) async fn determine_vpn_state(
+ active_connections: &PathMap<'_, ActiveConnectionDbusProxy<'_>>,
) -> Result {
for connection in active_connections.values() {
- match connection.type_()?.as_str() {
+ match connection.type_().await?.as_str() {
"vpn" | "wireguard" => {
return Ok(VpnState::Connected(VpnConnectedState {
name: "unknown".into(),
diff --git a/src/clients/sway.rs b/src/clients/sway.rs
new file mode 100644
index 0000000..492709e
--- /dev/null
+++ b/src/clients/sway.rs
@@ -0,0 +1,181 @@
+use crate::spawn;
+use color_eyre::{Report, Result};
+use futures_lite::StreamExt;
+use std::sync::Arc;
+use swayipc_async::{Connection, Event, EventType};
+use tokio::sync::Mutex;
+use tracing::{info, trace};
+
+type SyncFn = dyn Fn(&T) + Sync + Send;
+
+struct TaskState {
+ join_handle: Option>>,
+ // could have been a `HashMap>>`, but we don't
+ // expect enough listeners to justify the constant overhead of a hashmap.
+ listeners: Arc>)>>,
+}
+
+pub struct Client {
+ connection: Arc>,
+ task_state: Mutex,
+}
+
+impl std::fmt::Debug for Client {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("Client")
+ .field("client", &"Connection")
+ .field("task_state", &format_args!("<...>"))
+ .finish()
+ }
+}
+
+impl Client {
+ pub(crate) async fn new() -> Result {
+ // Avoid using `arc_mut!` here because we need tokio Mutex.
+ let client = Arc::new(Mutex::new(Connection::new().await?));
+ info!("Sway IPC subscription client connected");
+
+ Ok(Self {
+ connection: client,
+ task_state: Mutex::new(TaskState {
+ listeners: Arc::new(Vec::new()),
+ join_handle: None,
+ }),
+ })
+ }
+
+ pub fn connection(&self) -> &Arc> {
+ &self.connection
+ }
+
+ pub async fn add_listener(
+ &self,
+ f: impl Fn(&T) + Sync + Send + 'static,
+ ) -> Result<()> {
+ self.add_listener_type(
+ T::EVENT_TYPE,
+ Box::new(move |event| {
+ let event = T::from_event(event).expect("event type mismatch");
+ f(event);
+ }),
+ )
+ .await
+ }
+
+ pub async fn add_listener_type(
+ &self,
+ event_type: EventType,
+ f: Box>,
+ ) -> Result<()> {
+ // abort current running task
+ let TaskState {
+ join_handle,
+ listeners,
+ } = &mut *self.task_state.lock().await;
+
+ if let Some(handle) = join_handle.take() {
+ handle.abort();
+ let _ = handle.await;
+ }
+
+ // Only the task and self have a reference to listeners, and we just abort the task. This
+ // is the only reference to listeners, so we can safely get a mutable reference.
+ let listeners_mut = Arc::get_mut(listeners)
+ .ok_or_else(|| Report::msg("Failed to get mutable reference to listeners"))?;
+
+ listeners_mut.push((event_type, f));
+
+ // create new client as subscription takes ownership
+ let client = Connection::new().await?;
+
+ let event_types = listeners.iter().map(|(t, _)| *t).collect::>();
+ let listeners = listeners.clone();
+
+ let handle = spawn(async move {
+ let mut events = client.subscribe(&event_types).await?;
+
+ while let Some(event) = events.next().await {
+ trace!("event: {:?}", event);
+ let event = event?;
+ let ty = sway_event_to_event_type(&event);
+ for (t, f) in listeners.iter() {
+ if *t == ty {
+ f(&event);
+ }
+ }
+ }
+
+ Ok::<(), Report>(())
+ });
+
+ *join_handle = Some(handle);
+
+ Ok(())
+ }
+}
+
+fn sway_event_to_event_type(event: &Event) -> EventType {
+ match event {
+ Event::Workspace(_) => EventType::Workspace,
+ Event::Mode(_) => EventType::Mode,
+ Event::Window(_) => EventType::Window,
+ Event::BarConfigUpdate(_) => EventType::BarConfigUpdate,
+ Event::Binding(_) => EventType::Binding,
+ Event::Shutdown(_) => EventType::Shutdown,
+ Event::Tick(_) => EventType::Tick,
+ Event::BarStateUpdate(_) => EventType::BarStateUpdate,
+ Event::Input(_) => EventType::Input,
+ _ => todo!(),
+ }
+}
+
+pub trait SwayIpcEvent {
+ const EVENT_TYPE: EventType;
+ fn from_event(e: &Event) -> Option<&Self>;
+}
+macro_rules! sway_ipc_event_impl {
+ (@ $($t:tt)*) => { $($t)* };
+ ($t:ty, $v:expr, $($m:tt)*) => {
+ sway_ipc_event_impl! {@
+ impl SwayIpcEvent for $t {
+ const EVENT_TYPE: EventType = $v;
+ fn from_event(e: &Event) -> Option<&Self> {
+ match e {
+ $($m)* (x) => Some(x),
+ _ => None,
+ }
+ }
+ }
+ }
+ };
+}
+
+sway_ipc_event_impl!(
+ swayipc_async::WorkspaceEvent,
+ EventType::Workspace,
+ Event::Workspace
+);
+sway_ipc_event_impl!(swayipc_async::ModeEvent, EventType::Mode, Event::Mode);
+sway_ipc_event_impl!(swayipc_async::WindowEvent, EventType::Window, Event::Window);
+sway_ipc_event_impl!(
+ swayipc_async::BarConfig,
+ EventType::BarConfigUpdate,
+ Event::BarConfigUpdate
+);
+sway_ipc_event_impl!(
+ swayipc_async::BindingEvent,
+ EventType::Binding,
+ Event::Binding
+);
+sway_ipc_event_impl!(
+ swayipc_async::ShutdownEvent,
+ EventType::Shutdown,
+ Event::Shutdown
+);
+sway_ipc_event_impl!(swayipc_async::TickEvent, EventType::Tick, Event::Tick);
+sway_ipc_event_impl!(
+ swayipc_async::BarStateUpdateEvent,
+ EventType::BarStateUpdate,
+ Event::BarStateUpdate
+);
+sway_ipc_event_impl!(swayipc_async::InputEvent, EventType::Input, Event::Input);
diff --git a/src/clients/swaync/dbus.rs b/src/clients/swaync/dbus.rs
index 1bc292b..1909163 100644
--- a/src/clients/swaync/dbus.rs
+++ b/src/clients/swaync/dbus.rs
@@ -20,12 +20,14 @@
//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html
//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces,
-#[zbus::dbus_proxy(
+use zbus::proxy;
+
+#[proxy(
interface = "org.erikreider.swaync.cc",
default_service = "org.erikreider.swaync.cc",
default_path = "/org/erikreider/swaync/cc"
)]
-trait SwayNc {
+pub trait SwayNc {
/// AddInhibitor method
fn add_inhibitor(&self, application_id: &str) -> zbus::Result;
@@ -90,11 +92,11 @@ trait SwayNc {
fn toggle_visibility(&self) -> zbus::Result<()>;
/// Subscribe signal
- #[dbus_proxy(signal)]
+ #[zbus(signal)]
fn subscribe(&self, count: u32, dnd: bool, cc_open: bool) -> zbus::Result<()>;
/// SubscribeV2 signal
- #[dbus_proxy(signal)]
+ #[zbus(signal)]
fn subscribe_v2(
&self,
count: u32,
@@ -104,8 +106,8 @@ trait SwayNc {
) -> zbus::Result<()>;
/// Inhibited property
- #[dbus_proxy(property)]
+ #[zbus(property)]
fn inhibited(&self) -> zbus::Result;
- #[dbus_proxy(property)]
+ #[zbus(property)]
fn set_inhibited(&self, value: bool) -> zbus::Result<()>;
}
diff --git a/src/clients/swaync/mod.rs b/src/clients/swaync/mod.rs
index 9b5c116..e1ee1db 100644
--- a/src/clients/swaync/mod.rs
+++ b/src/clients/swaync/mod.rs
@@ -1,6 +1,7 @@
mod dbus;
-use crate::{register_fallible_client, send, spawn};
+use crate::channels::SyncSenderExt;
+use crate::{register_fallible_client, spawn};
use color_eyre::{Report, Result};
use dbus::SwayNcProxy;
use serde::Deserialize;
@@ -54,9 +55,13 @@ impl Client {
spawn(async move {
while let Some(ev) = stream.next().await {
- let ev = ev.body::().expect("to deserialize");
+ let ev = ev
+ .message()
+ .body()
+ .deserialize::()
+ .expect("to deserialize");
debug!("Received event: {ev:?}");
- send!(tx, ev);
+ tx.send_expect(ev);
}
});
}
diff --git a/src/clients/sysinfo.rs b/src/clients/sysinfo.rs
new file mode 100644
index 0000000..a9466c0
--- /dev/null
+++ b/src/clients/sysinfo.rs
@@ -0,0 +1,621 @@
+use crate::modules::sysinfo::Interval;
+use crate::{lock, register_client};
+use color_eyre::{Report, Result};
+use std::cmp::Ordering;
+use std::collections::HashMap;
+use std::fmt::Debug;
+use std::str::FromStr;
+use std::sync::{Arc, Mutex};
+use sysinfo::{Components, Disks, LoadAvg, Networks, RefreshKind, System};
+
+#[repr(u64)]
+#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)]
+pub enum Prefix {
+ #[default]
+ None = 1,
+
+ Kilo = 1000,
+ Mega = Prefix::Kilo as u64 * 1000,
+ Giga = Prefix::Mega as u64 * 1000,
+ Tera = Prefix::Giga as u64 * 1000,
+ Peta = Prefix::Tera as u64 * 1000,
+
+ Kibi = 1024,
+ Mebi = Prefix::Kibi as u64 * 1024,
+ Gibi = Prefix::Mebi as u64 * 1024,
+ Tebi = Prefix::Gibi as u64 * 1024,
+ Pebi = Prefix::Tebi as u64 * 1024,
+
+ // # Units
+ // These are special cases
+ // where you'd actually want to do slightly more than a prefix alone.
+ // Included as part of the prefix system for simplicity.
+ KiloBit = 128,
+ MegaBit = Prefix::KiloBit as u64 * 1024,
+ GigaBit = Prefix::MegaBit as u64 * 1024,
+}
+
+#[derive(Debug, Clone)]
+pub enum Function {
+ None,
+ Sum,
+ Min,
+ Max,
+ Mean,
+ Name(String),
+}
+
+impl FromStr for Function {
+ type Err = ();
+
+ fn from_str(s: &str) -> Result {
+ match s.to_lowercase().as_str() {
+ "sum" => Ok(Self::Sum),
+ "min" => Ok(Self::Min),
+ "max" => Ok(Self::Max),
+ "mean" => Ok(Self::Mean),
+ "" => Err(()),
+ _ => Ok(Self::Name(s.to_string())),
+ }
+ }
+}
+
+#[derive(Debug)]
+pub struct ValueSet {
+ values: HashMap, Value>,
+}
+
+impl FromIterator<(Box, Value)> for ValueSet {
+ fn from_iter, Value)>>(iter: T) -> Self {
+ Self {
+ values: iter.into_iter().collect(),
+ }
+ }
+}
+
+impl ValueSet {
+ fn values(&self, prefix: Prefix) -> impl Iterator- + use<'_> {
+ self.values
+ .values()
+ .map(move |v| v.get(prefix))
+ .filter(|v| !v.is_nan())
+ }
+
+ pub fn apply(&self, function: &Function, prefix: Prefix) -> f64 {
+ match function {
+ Function::None => 0.0,
+ Function::Sum => self.sum(prefix),
+ Function::Min => self.min(prefix),
+ Function::Max => self.max(prefix),
+ Function::Mean => self.mean(prefix),
+ Function::Name(name) => self
+ .values
+ .get(&Box::from(name.as_str()))
+ .map(|v| v.get(prefix))
+ .unwrap_or_default(),
+ }
+ }
+
+ fn sum(&self, prefix: Prefix) -> f64 {
+ self.values(prefix).sum()
+ }
+
+ fn min(&self, prefix: Prefix) -> f64 {
+ self.values(prefix)
+ .min_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal))
+ .unwrap_or_default()
+ }
+
+ fn max(&self, prefix: Prefix) -> f64 {
+ self.values(prefix)
+ .max_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal))
+ .unwrap_or_default()
+ }
+
+ fn mean(&self, prefix: Prefix) -> f64 {
+ self.sum(prefix) / self.values.len() as f64
+ }
+}
+
+#[derive(Debug, Default, Clone, Copy, PartialEq)]
+pub struct Value {
+ value: f64,
+ prefix: Prefix,
+}
+
+impl Value {
+ pub fn new(value: f64) -> Self {
+ Self::new_with_prefix(value, Prefix::None)
+ }
+
+ pub fn new_with_prefix(value: f64, prefix: Prefix) -> Self {
+ Self { value, prefix }
+ }
+
+ pub fn get(self, prefix: Prefix) -> f64 {
+ if prefix == self.prefix {
+ self.value
+ } else {
+ let scale = self.prefix as u64 as f64 / prefix as u64 as f64;
+ self.value * scale
+ }
+ }
+}
+
+#[derive(Debug)]
+pub struct Client {
+ system: Mutex,
+ disks: Mutex,
+ components: Mutex,
+ networks: Mutex,
+ load_average: Mutex,
+}
+
+impl Client {
+ pub fn new() -> Self {
+ let refresh_kind = RefreshKind::everything().without_processes();
+
+ let system = System::new_with_specifics(refresh_kind);
+ let disks = Disks::new_with_refreshed_list();
+ let components = Components::new_with_refreshed_list();
+ let networks = Networks::new_with_refreshed_list();
+ let load_average = System::load_average();
+
+ Self {
+ system: Mutex::new(system),
+ disks: Mutex::new(disks),
+ components: Mutex::new(components),
+ networks: Mutex::new(networks),
+ load_average: Mutex::new(load_average),
+ }
+ }
+
+ pub fn refresh_cpu(&self) {
+ lock!(self.system).refresh_cpu_all();
+ }
+
+ pub fn refresh_memory(&self) {
+ lock!(self.system).refresh_memory();
+ }
+
+ pub fn refresh_network(&self) {
+ lock!(self.networks).refresh(true);
+ }
+
+ pub fn refresh_temps(&self) {
+ lock!(self.components).refresh(true);
+ }
+
+ pub fn refresh_disks(&self) {
+ lock!(self.disks).refresh(true);
+ }
+
+ pub fn refresh_load_average(&self) {
+ *lock!(self.load_average) = System::load_average();
+ }
+
+ pub fn cpu_frequency(&self) -> ValueSet {
+ lock!(self.system)
+ .cpus()
+ .iter()
+ .map(|cpu| {
+ (
+ cpu.name().into(),
+ Value::new_with_prefix(cpu.frequency() as f64, Prefix::Mega),
+ )
+ })
+ .collect()
+ }
+
+ pub fn cpu_percent(&self) -> ValueSet {
+ lock!(self.system)
+ .cpus()
+ .iter()
+ .map(|cpu| (cpu.name().into(), Value::new(cpu.cpu_usage() as f64)))
+ .collect()
+ }
+
+ pub fn memory_free(&self) -> Value {
+ Value::new(lock!(self.system).free_memory() as f64)
+ }
+
+ pub fn memory_available(&self) -> Value {
+ Value::new(lock!(self.system).available_memory() as f64)
+ }
+
+ pub fn memory_total(&self) -> Value {
+ Value::new(lock!(self.system).total_memory() as f64)
+ }
+
+ pub fn memory_used(&self) -> Value {
+ Value::new(lock!(self.system).used_memory() as f64)
+ }
+
+ pub fn memory_percent(&self) -> Value {
+ let total = lock!(self.system).total_memory() as f64;
+ let used = lock!(self.system).used_memory() as f64;
+
+ Value::new(used / total * 100.0)
+ }
+
+ pub fn swap_free(&self) -> Value {
+ Value::new(lock!(self.system).free_swap() as f64)
+ }
+
+ pub fn swap_total(&self) -> Value {
+ Value::new(lock!(self.system).total_swap() as f64)
+ }
+
+ pub fn swap_used(&self) -> Value {
+ Value::new(lock!(self.system).used_swap() as f64)
+ }
+ pub fn swap_percent(&self) -> Value {
+ let total = lock!(self.system).total_swap() as f64;
+ let used = lock!(self.system).used_swap() as f64;
+
+ Value::new(used / total * 100.0)
+ }
+
+ pub fn temp_c(&self) -> ValueSet {
+ lock!(self.components)
+ .iter()
+ .map(|comp| {
+ (
+ comp.label().into(),
+ Value::new(comp.temperature().unwrap_or_default() as f64),
+ )
+ })
+ .collect()
+ }
+
+ pub fn temp_f(&self) -> ValueSet {
+ lock!(self.components)
+ .iter()
+ .map(|comp| {
+ (
+ comp.label().into(),
+ Value::new(c_to_f(comp.temperature().unwrap_or_default() as f64)),
+ )
+ })
+ .collect()
+ }
+
+ pub fn disk_free(&self) -> ValueSet {
+ lock!(self.disks)
+ .iter()
+ .map(|disk| {
+ (
+ disk.mount_point().to_string_lossy().into(),
+ Value::new(disk.available_space() as f64),
+ )
+ })
+ .collect()
+ }
+
+ pub fn disk_total(&self) -> ValueSet {
+ lock!(self.disks)
+ .iter()
+ .map(|disk| {
+ (
+ disk.mount_point().to_string_lossy().into(),
+ Value::new(disk.total_space() as f64),
+ )
+ })
+ .collect()
+ }
+
+ pub fn disk_used(&self) -> ValueSet {
+ lock!(self.disks)
+ .iter()
+ .map(|disk| {
+ (
+ disk.mount_point().to_string_lossy().into(),
+ Value::new((disk.total_space() - disk.available_space()) as f64),
+ )
+ })
+ .collect()
+ }
+
+ pub fn disk_percent(&self) -> ValueSet {
+ lock!(self.disks)
+ .iter()
+ .map(|disk| {
+ (
+ disk.mount_point().to_string_lossy().into(),
+ Value::new(
+ (disk.total_space() - disk.available_space()) as f64
+ / disk.total_space() as f64
+ * 100.0,
+ ),
+ )
+ })
+ .collect()
+ }
+
+ pub fn disk_read(&self, interval: Interval) -> ValueSet {
+ lock!(self.disks)
+ .iter()
+ .map(|disk| {
+ (
+ disk.mount_point().to_string_lossy().into(),
+ Value::new(disk.usage().read_bytes as f64 / interval.disks() as f64),
+ )
+ })
+ .collect()
+ }
+
+ pub fn disk_write(&self, interval: Interval) -> ValueSet {
+ lock!(self.disks)
+ .iter()
+ .map(|disk| {
+ (
+ disk.mount_point().to_string_lossy().into(),
+ Value::new(disk.usage().written_bytes as f64 / interval.disks() as f64),
+ )
+ })
+ .collect()
+ }
+
+ pub fn net_down(&self, interval: Interval) -> ValueSet {
+ lock!(self.networks)
+ .iter()
+ .map(|(name, net)| {
+ (
+ name.as_str().into(),
+ Value::new(net.received() as f64 / interval.networks() as f64),
+ )
+ })
+ .collect()
+ }
+
+ pub fn net_up(&self, interval: Interval) -> ValueSet {
+ lock!(self.networks)
+ .iter()
+ .map(|(name, net)| {
+ (
+ name.as_str().into(),
+ Value::new(net.transmitted() as f64 / interval.networks() as f64),
+ )
+ })
+ .collect()
+ }
+
+ pub fn load_average_1(&self) -> Value {
+ Value::new(lock!(self.load_average).one)
+ }
+
+ pub fn load_average_5(&self) -> Value {
+ Value::new(lock!(self.load_average).five)
+ }
+
+ pub fn load_average_15(&self) -> Value {
+ Value::new(lock!(self.load_average).fifteen)
+ }
+
+ /// Gets system uptime formatted as `HH:mm`.
+ pub fn uptime() -> String {
+ let uptime = System::uptime();
+ let hours = uptime / 3600;
+ format!("{:0>2}:{:0>2}", hours, (uptime % 3600) / 60)
+ }
+}
+
+register_client!(Client, sys_info);
+
+const fn c_to_f(c: f64) -> f64 {
+ c / 5.0 * 9.0 + 32.0
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum TokenType {
+ CpuFrequency,
+ CpuPercent,
+
+ MemoryFree,
+ MemoryAvailable,
+ MemoryTotal,
+ MemoryUsed,
+ MemoryPercent,
+
+ SwapFree,
+ SwapTotal,
+ SwapUsed,
+ SwapPercent,
+
+ TempC,
+ TempF,
+
+ DiskFree,
+ DiskTotal,
+ DiskUsed,
+ DiskPercent,
+ DiskRead,
+ DiskWrite,
+
+ NetDown,
+ NetUp,
+
+ LoadAverage1,
+ LoadAverage5,
+ LoadAverage15,
+ Uptime,
+}
+
+impl FromStr for TokenType {
+ type Err = Report;
+
+ fn from_str(s: &str) -> Result {
+ match s {
+ "cpu_frequency" => Ok(Self::CpuFrequency),
+ "cpu_percent" => Ok(Self::CpuPercent),
+
+ "memory_free" => Ok(Self::MemoryFree),
+ "memory_available" => Ok(Self::MemoryAvailable),
+ "memory_total" => Ok(Self::MemoryTotal),
+ "memory_used" => Ok(Self::MemoryUsed),
+ "memory_percent" => Ok(Self::MemoryPercent),
+
+ "swap_free" => Ok(Self::SwapFree),
+ "swap_total" => Ok(Self::SwapTotal),
+ "swap_used" => Ok(Self::SwapUsed),
+ "swap_percent" => Ok(Self::SwapPercent),
+
+ "temp_c" => Ok(Self::TempC),
+ "temp_f" => Ok(Self::TempF),
+
+ "disk_free" => Ok(Self::DiskFree),
+ "disk_total" => Ok(Self::DiskTotal),
+ "disk_used" => Ok(Self::DiskUsed),
+ "disk_percent" => Ok(Self::DiskPercent),
+ "disk_read" => Ok(Self::DiskRead),
+ "disk_write" => Ok(Self::DiskWrite),
+
+ "net_down" => Ok(Self::NetDown),
+ "net_up" => Ok(Self::NetUp),
+
+ "load_average_1" => Ok(Self::LoadAverage1),
+ "load_average_5" => Ok(Self::LoadAverage5),
+ "load_average_15" => Ok(Self::LoadAverage15),
+ "uptime" => Ok(Self::Uptime),
+ _ => Err(Report::msg(format!("invalid token type: '{s}'"))),
+ }
+ }
+}
+
+#[cfg(feature = "ipc")]
+use crate::ironvar::Namespace;
+
+#[cfg(feature = "ipc")]
+impl Namespace for Client {
+ fn get(&self, key: &str) -> Option {
+ let get = |value: Value| Some(value.get(Prefix::None).to_string());
+
+ let token = TokenType::from_str(key).ok()?;
+ match token {
+ TokenType::CpuFrequency => None,
+ TokenType::CpuPercent => None,
+ TokenType::MemoryFree => get(self.memory_free()),
+ TokenType::MemoryAvailable => get(self.memory_available()),
+ TokenType::MemoryTotal => get(self.memory_total()),
+ TokenType::MemoryUsed => get(self.memory_used()),
+ TokenType::MemoryPercent => get(self.memory_percent()),
+ TokenType::SwapFree => get(self.swap_free()),
+ TokenType::SwapTotal => get(self.swap_total()),
+ TokenType::SwapUsed => get(self.swap_used()),
+ TokenType::SwapPercent => get(self.swap_percent()),
+ TokenType::TempC => None,
+ TokenType::TempF => None,
+ TokenType::DiskFree => None,
+ TokenType::DiskTotal => None,
+ TokenType::DiskUsed => None,
+ TokenType::DiskPercent => None,
+ TokenType::DiskRead => None,
+ TokenType::DiskWrite => None,
+ TokenType::NetDown => None,
+ TokenType::NetUp => None,
+ TokenType::LoadAverage1 => get(self.load_average_1()),
+ TokenType::LoadAverage5 => get(self.load_average_5()),
+ TokenType::LoadAverage15 => get(self.load_average_15()),
+ TokenType::Uptime => Some(Client::uptime()),
+ }
+ }
+
+ fn list(&self) -> Vec {
+ vec![
+ "memory_free",
+ "memory_available",
+ "memory_total",
+ "memory_used",
+ "memory_percent",
+ "swap_free",
+ "swap_total",
+ "swap_used",
+ "swap_percent",
+ "load_average_1",
+ "load_average_5",
+ "load_average_15",
+ "uptime",
+ ]
+ .into_iter()
+ .map(ToString::to_string)
+ .collect()
+ }
+
+ fn namespaces(&self) -> Vec {
+ vec![
+ "cpu_frequency",
+ "cpu_percent",
+ "temp_c",
+ "temp_f",
+ "disk_free",
+ "disk_total",
+ "disk_used",
+ "disk_percent",
+ "disk_read",
+ "disk_write",
+ "net_down",
+ "net_up",
+ ]
+ .into_iter()
+ .map(ToString::to_string)
+ .collect()
+ }
+
+ fn get_namespace(&self, key: &str) -> Option> {
+ let token = TokenType::from_str(key).ok()?;
+
+ match token {
+ TokenType::CpuFrequency => Some(Arc::new(self.cpu_frequency())),
+ TokenType::CpuPercent => Some(Arc::new(self.cpu_percent())),
+ TokenType::MemoryFree => None,
+ TokenType::MemoryAvailable => None,
+ TokenType::MemoryTotal => None,
+ TokenType::MemoryUsed => None,
+ TokenType::MemoryPercent => None,
+ TokenType::SwapFree => None,
+ TokenType::SwapTotal => None,
+ TokenType::SwapUsed => None,
+ TokenType::SwapPercent => None,
+ TokenType::TempC => Some(Arc::new(self.temp_c())),
+ TokenType::TempF => Some(Arc::new(self.temp_f())),
+ TokenType::DiskFree => Some(Arc::new(self.disk_free())),
+ TokenType::DiskTotal => Some(Arc::new(self.disk_total())),
+ TokenType::DiskUsed => Some(Arc::new(self.disk_used())),
+ TokenType::DiskPercent => Some(Arc::new(self.disk_percent())),
+ TokenType::DiskRead => Some(Arc::new(self.disk_read(Interval::All(1)))),
+ TokenType::DiskWrite => Some(Arc::new(self.disk_write(Interval::All(1)))),
+ TokenType::NetDown => Some(Arc::new(self.net_down(Interval::All(1)))),
+ TokenType::NetUp => Some(Arc::new(self.net_up(Interval::All(1)))),
+ TokenType::LoadAverage1 => None,
+ TokenType::LoadAverage5 => None,
+ TokenType::LoadAverage15 => None,
+ TokenType::Uptime => None,
+ }
+ }
+}
+
+#[cfg(feature = "ipc")]
+impl Namespace for ValueSet {
+ fn get(&self, key: &str) -> Option {
+ let function = Function::from_str(key).ok()?;
+ Some(self.apply(&function, Prefix::None).to_string())
+ }
+
+ fn list(&self) -> Vec {
+ let mut vec = vec!["sum", "min", "max", "mean"]
+ .into_iter()
+ .map(ToString::to_string)
+ .collect::>();
+
+ vec.extend(self.values.keys().map(ToString::to_string));
+ vec
+ }
+
+ fn namespaces(&self) -> Vec {
+ vec![]
+ }
+
+ fn get_namespace(&self, _key: &str) -> Option> {
+ None
+ }
+}
diff --git a/src/clients/upower.rs b/src/clients/upower.rs
deleted file mode 100644
index f3cade1..0000000
--- a/src/clients/upower.rs
+++ /dev/null
@@ -1,35 +0,0 @@
-use crate::register_client;
-use std::sync::Arc;
-use upower_dbus::UPowerProxy;
-use zbus::fdo::PropertiesProxy;
-
-pub async fn create_display_proxy() -> Arc> {
- let dbus = Box::pin(zbus::Connection::system())
- .await
- .expect("failed to create connection to system bus");
-
- let device_proxy = UPowerProxy::new(&dbus)
- .await
- .expect("failed to create upower proxy");
-
- let display_device = device_proxy
- .get_display_device()
- .await
- .unwrap_or_else(|_| panic!("failed to get display device for {device_proxy:?}"));
-
- let path = display_device.path().to_owned();
-
- let proxy = PropertiesProxy::builder(&dbus)
- .destination("org.freedesktop.UPower")
- .expect("failed to set proxy destination address")
- .path(path)
- .expect("failed to set proxy path")
- .cache_properties(zbus::CacheProperties::No)
- .build()
- .await
- .expect("failed to build proxy");
-
- Arc::new(proxy)
-}
-
-register_client!(PropertiesProxy<'static>, upower);
diff --git a/src/clients/upower/dbus.rs b/src/clients/upower/dbus.rs
new file mode 100644
index 0000000..c3733d6
--- /dev/null
+++ b/src/clients/upower/dbus.rs
@@ -0,0 +1,159 @@
+/// Originally taken from `upower-dbus` crate
+///
+// Copyright 2021 System76
+// SPDX-License-Identifier: MPL-2.0
+use zbus::proxy;
+use zbus::zvariant::OwnedValue;
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq, OwnedValue)]
+#[repr(u32)]
+pub enum BatteryState {
+ Unknown = 0,
+ Charging = 1,
+ Discharging = 2,
+ Empty = 3,
+ FullyCharged = 4,
+ PendingCharge = 5,
+ PendingDischarge = 6,
+}
+
+#[derive(Debug, Copy, Clone, OwnedValue)]
+#[repr(u32)]
+pub enum BatteryType {
+ Unknown = 0,
+ LinePower = 1,
+ Battery = 2,
+ Ups = 3,
+ Monitor = 4,
+ Mouse = 5,
+ Keyboard = 6,
+ Pda = 7,
+ Phone = 8,
+}
+
+#[derive(Debug, Copy, Clone, OwnedValue)]
+#[repr(u32)]
+pub enum BatteryLevel {
+ Unknown = 0,
+ None = 1,
+ Low = 3,
+ Critical = 4,
+ Normal = 6,
+ High = 7,
+ Full = 8,
+}
+
+#[proxy(
+ interface = "org.freedesktop.UPower.Device",
+ default_service = "org.freedesktop.UPower",
+ assume_defaults = false
+)]
+pub trait Device {
+ #[zbus(property)]
+ fn battery_level(&self) -> zbus::Result;
+
+ #[zbus(property)]
+ fn capacity(&self) -> zbus::Result;
+
+ #[zbus(property)]
+ fn energy(&self) -> zbus::Result;
+
+ #[zbus(property)]
+ fn energy_empty(&self) -> zbus::Result;
+
+ #[zbus(property)]
+ fn energy_full(&self) -> zbus::Result;
+
+ #[zbus(property)]
+ fn energy_full_design(&self) -> zbus::Result