> 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/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;
+
+ #[zbus(property)]
+ fn has_history(&self) -> zbus::Result;
+
+ #[zbus(property)]
+ fn has_statistics(&self) -> zbus::Result;
+
+ #[zbus(property)]
+ fn icon_name(&self) -> zbus::Result;
+
+ #[zbus(property)]
+ fn is_present(&self) -> zbus::Result;
+
+ #[zbus(property)]
+ fn is_rechargeable(&self) -> zbus::Result;
+
+ #[zbus(property)]
+ fn luminosity(&self) -> zbus::Result;
+
+ #[zbus(property)]
+ fn model(&self) -> zbus::Result;
+
+ #[zbus(property)]
+ fn native_path(&self) -> zbus::Result;
+
+ #[zbus(property)]
+ fn online(&self) -> zbus::Result;
+
+ #[zbus(property)]
+ fn percentage(&self) -> zbus::Result;
+
+ #[zbus(property)]
+ fn power_supply(&self) -> zbus::Result;
+
+ fn refresh(&self) -> zbus::Result<()>;
+
+ #[zbus(property)]
+ fn serial(&self) -> zbus::Result;
+
+ #[zbus(property)]
+ fn state(&self) -> zbus::Result;
+
+ #[zbus(property)]
+ fn temperature(&self) -> zbus::Result;
+
+ #[zbus(property, name = "Type")]
+ fn type_(&self) -> zbus::Result;
+
+ #[zbus(property)]
+ fn vendor(&self) -> zbus::Result;
+
+ #[zbus(property)]
+ fn voltage(&self) -> zbus::Result;
+}
+
+#[proxy(interface = "org.freedesktop.UPower", assume_defaults = true)]
+pub trait UPower {
+ /// EnumerateDevices method
+ fn enumerate_devices(&self) -> zbus::Result>;
+
+ /// GetCriticalAction method
+ fn get_critical_action(&self) -> zbus::Result;
+
+ /// GetDisplayDevice method
+ #[zbus(object = "Device")]
+ fn get_display_device(&self);
+
+ /// DeviceAdded signal
+ #[zbus(signal)]
+ fn device_added(&self, device: zbus::zvariant::ObjectPath<'_>) -> zbus::Result<()>;
+
+ /// DeviceRemoved signal
+ #[zbus(signal)]
+ fn device_removed(&self, device: zbus::zvariant::ObjectPath<'_>) -> zbus::Result<()>;
+
+ /// DaemonVersion property
+ #[zbus(property)]
+ fn daemon_version(&self) -> zbus::Result;
+
+ /// LidIsClosed property
+ #[zbus(property)]
+ fn lid_is_closed(&self) -> zbus::Result;
+
+ /// LidIsPresent property
+ #[zbus(property)]
+ fn lid_is_present(&self) -> zbus::Result;
+
+ /// OnBattery property
+ #[zbus(property)]
+ fn on_battery(&self) -> zbus::Result;
+}
diff --git a/src/clients/upower/mod.rs b/src/clients/upower/mod.rs
new file mode 100644
index 0000000..1d2231f
--- /dev/null
+++ b/src/clients/upower/mod.rs
@@ -0,0 +1,33 @@
+mod dbus;
+
+use crate::clients::ClientResult;
+use crate::register_fallible_client;
+use dbus::UPowerProxy;
+use std::sync::Arc;
+use zbus::fdo::PropertiesProxy;
+use zbus::proxy::CacheProperties;
+
+pub use dbus::BatteryState;
+
+pub async fn create_display_proxy() -> ClientResult> {
+ let dbus = Box::pin(zbus::Connection::system()).await?;
+
+ let device_proxy = UPowerProxy::new(&dbus).await?;
+
+ let display_device = device_proxy.get_display_device().await?;
+
+ let path = display_device.inner().path();
+
+ 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(CacheProperties::No)
+ .build()
+ .await?;
+
+ Ok(Arc::new(proxy))
+}
+
+register_fallible_client!(PropertiesProxy<'static>, upower);
diff --git a/src/clients/volume/mod.rs b/src/clients/volume/mod.rs
index 3db503f..5496ff3 100644
--- a/src/clients/volume/mod.rs
+++ b/src/clients/volume/mod.rs
@@ -1,7 +1,7 @@
mod sink;
mod sink_input;
-use crate::{arc_mut, lock, register_client, send, spawn_blocking, APP_ID};
+use crate::{APP_ID, arc_mut, lock, register_client, spawn_blocking};
use libpulse_binding::callbacks::ListResult;
use libpulse_binding::context::introspect::{Introspector, ServerInfo};
use libpulse_binding::context::subscribe::{Facility, InterestMaskSet, Operation};
@@ -12,8 +12,9 @@ use libpulse_binding::volume::{ChannelVolumes, Volume};
use std::fmt::{Debug, Formatter};
use std::sync::{Arc, Mutex};
use tokio::sync::broadcast;
-use tracing::{debug, error, info, warn};
+use tracing::{debug, error, info, trace, warn};
+use crate::channels::SyncSenderExt;
pub use sink::Sink;
pub use sink_input::SinkInput;
@@ -230,6 +231,8 @@ fn on_event(
return;
};
+ trace!("server event: {facility:?}, op: {op:?}, i: {i}");
+
match facility {
Facility::Server => on_server_event(context, &data.sinks, &data.default_sink_name, tx),
Facility::Sink => sink::on_event(context, &data.sinks, &data.default_sink_name, tx, op, i),
@@ -269,7 +272,7 @@ fn set_default_sink(
{
sink.active = true;
debug!("Set sink active: {}", sink.name);
- send!(tx, Event::UpdateSink(sink.clone()));
+ tx.send_expect(Event::UpdateSink(sink.clone()));
} else {
warn!("Couldn't find sink: {}", default_sink_name);
}
diff --git a/src/clients/volume/sink.rs b/src/clients/volume/sink.rs
index d08b99e..1269c9d 100644
--- a/src/clients/volume/sink.rs
+++ b/src/clients/volume/sink.rs
@@ -1,13 +1,14 @@
-use super::{percent_to_volume, volume_to_percent, ArcMutVec, Client, ConnectionState, Event};
-use crate::{lock, send};
+use super::{ArcMutVec, Client, ConnectionState, Event, percent_to_volume, volume_to_percent};
+use crate::channels::SyncSenderExt;
+use crate::lock;
use libpulse_binding::callbacks::ListResult;
+use libpulse_binding::context::Context;
use libpulse_binding::context::introspect::SinkInfo;
use libpulse_binding::context::subscribe::Operation;
-use libpulse_binding::context::Context;
use libpulse_binding::def::SinkState;
-use std::sync::{mpsc, Arc, Mutex};
+use std::sync::{Arc, Mutex, mpsc};
use tokio::sync::broadcast;
-use tracing::{debug, error};
+use tracing::{debug, error, instrument, trace};
#[derive(Debug, Clone)]
pub struct Sink {
@@ -41,16 +42,19 @@ impl From<&SinkInfo<'_>> for Sink {
}
impl Client {
+ #[instrument(level = "trace")]
pub fn sinks(&self) -> Arc>> {
self.data.sinks.clone()
}
+ #[instrument(level = "trace")]
pub fn set_default_sink(&self, name: &str) {
if let ConnectionState::Connected { context, .. } = &*lock!(self.connection) {
lock!(context).set_default_sink(name, |_| {});
}
}
+ #[instrument(level = "trace")]
pub fn set_sink_volume(&self, name: &str, volume_percent: f64) {
if let ConnectionState::Connected { introspector, .. } = &mut *lock!(self.connection) {
let (tx, rx) = mpsc::channel();
@@ -59,7 +63,7 @@ impl Client {
let ListResult::Item(info) = info else {
return;
};
- send!(tx, info.volume);
+ tx.send_expect(info.volume);
});
let new_volume = percent_to_volume(volume_percent);
@@ -73,6 +77,7 @@ impl Client {
}
}
+ #[instrument(level = "trace")]
pub fn set_sink_muted(&self, name: &str, muted: bool) {
if let ConnectionState::Connected { introspector, .. } = &mut *lock!(self.connection) {
introspector.set_sink_mute_by_name(name, muted, None);
@@ -122,8 +127,10 @@ pub fn add(info: ListResult<&SinkInfo>, sinks: &ArcMutVec, tx: &broadcast:
return;
};
+ trace!("adding {info:?}");
+
lock!(sinks).push(info.into());
- send!(tx, Event::AddSink(info.into()));
+ tx.send_expect(Event::AddSink(info.into()));
}
fn update(
@@ -136,6 +143,8 @@ fn update(
return;
};
+ trace!("updating {info:?}");
+
{
let mut sinks = lock!(sinks);
let Some(pos) = sinks.iter().position(|sink| sink.index == info.index) else {
@@ -162,14 +171,16 @@ fn update(
}
}
- send!(tx, Event::UpdateSink(sink));
+ tx.send_expect(Event::UpdateSink(sink));
}
fn remove(index: u32, sinks: &ArcMutVec, tx: &broadcast::Sender) {
+ trace!("removing {index}");
+
let mut sinks = lock!(sinks);
if let Some(pos) = sinks.iter().position(|s| s.index == index) {
let info = sinks.remove(pos);
- send!(tx, Event::RemoveSink(info.name));
+ tx.send_expect(Event::RemoveSink(info.name));
}
}
diff --git a/src/clients/volume/sink_input.rs b/src/clients/volume/sink_input.rs
index 102aed2..b6e11aa 100644
--- a/src/clients/volume/sink_input.rs
+++ b/src/clients/volume/sink_input.rs
@@ -1,12 +1,13 @@
-use super::{percent_to_volume, volume_to_percent, ArcMutVec, Client, ConnectionState, Event};
-use crate::{lock, send};
+use super::{ArcMutVec, Client, ConnectionState, Event, percent_to_volume, volume_to_percent};
+use crate::channels::SyncSenderExt;
+use crate::lock;
use libpulse_binding::callbacks::ListResult;
+use libpulse_binding::context::Context;
use libpulse_binding::context::introspect::SinkInputInfo;
use libpulse_binding::context::subscribe::Operation;
-use libpulse_binding::context::Context;
-use std::sync::{mpsc, Arc, Mutex};
+use std::sync::{Arc, Mutex, mpsc};
use tokio::sync::broadcast;
-use tracing::{debug, error};
+use tracing::{debug, error, instrument, trace};
#[derive(Debug, Clone)]
pub struct SinkInput {
@@ -35,10 +36,12 @@ impl From<&SinkInputInfo<'_>> for SinkInput {
}
impl Client {
+ #[instrument(level = "trace")]
pub fn sink_inputs(&self) -> Arc>> {
self.data.sink_inputs.clone()
}
+ #[instrument(level = "trace")]
pub fn set_input_volume(&self, index: u32, volume_percent: f64) {
if let ConnectionState::Connected { introspector, .. } = &mut *lock!(self.connection) {
let (tx, rx) = mpsc::channel();
@@ -47,7 +50,7 @@ impl Client {
let ListResult::Item(info) = info else {
return;
};
- send!(tx, info.volume);
+ tx.send_expect(info.volume);
});
let new_volume = percent_to_volume(volume_percent);
@@ -61,6 +64,7 @@ impl Client {
}
}
+ #[instrument(level = "trace")]
pub fn set_input_muted(&self, index: u32, muted: bool) {
if let ConnectionState::Connected { introspector, .. } = &mut *lock!(self.connection) {
introspector.set_sink_input_mute(index, muted, None);
@@ -112,8 +116,10 @@ pub fn add(
return;
};
+ trace!("adding {info:?}");
+
lock!(inputs).push(info.into());
- send!(tx, Event::AddInput(info.into()));
+ tx.send_expect(Event::AddInput(info.into()));
}
fn update(
@@ -125,6 +131,8 @@ fn update(
return;
};
+ trace!("updating {info:?}");
+
{
let mut inputs = lock!(inputs);
let Some(pos) = inputs.iter().position(|input| input.index == info.index) else {
@@ -135,14 +143,16 @@ fn update(
inputs[pos] = info.into();
}
- send!(tx, Event::UpdateInput(info.into()));
+ tx.send_expect(Event::UpdateInput(info.into()));
}
fn remove(index: u32, inputs: &ArcMutVec, tx: &broadcast::Sender) {
let mut inputs = lock!(inputs);
+ trace!("removing {index}");
+
if let Some(pos) = inputs.iter().position(|s| s.index == index) {
let info = inputs.remove(pos);
- send!(tx, Event::RemoveInput(info.index));
+ tx.send_expect(Event::RemoveInput(info.index));
}
}
diff --git a/src/clients/wayland/mod.rs b/src/clients/wayland/mod.rs
index ca7c367..299b087 100644
--- a/src/clients/wayland/mod.rs
+++ b/src/clients/wayland/mod.rs
@@ -2,17 +2,18 @@ mod macros;
mod wl_output;
mod wl_seat;
-use crate::error::{ExitCode, ERR_CHANNEL_RECV};
-use crate::{arc_mut, lock, register_client, send, spawn, spawn_blocking};
+use crate::error::{ERR_CHANNEL_RECV, ExitCode};
+use crate::{arc_mut, lock, register_client, spawn, spawn_blocking};
use std::process::exit;
use std::sync::{Arc, Mutex};
+use crate::channels::SyncSenderExt;
use calloop_channel::Event::Msg;
use cfg_if::cfg_if;
-use color_eyre::Report;
+use color_eyre::{Help, Report};
use smithay_client_toolkit::output::OutputState;
+use smithay_client_toolkit::reexports::calloop::EventLoop;
use smithay_client_toolkit::reexports::calloop::channel as calloop_channel;
-use smithay_client_toolkit::reexports::calloop::{EventLoop, LoopHandle};
use smithay_client_toolkit::reexports::calloop_wayland_source::WaylandSource;
use smithay_client_toolkit::registry::{ProvidesRegistryState, RegistryState};
use smithay_client_toolkit::seat::SeatState;
@@ -43,7 +44,6 @@ cfg_if! {
use self::wlr_data_control::device::DataControlDevice;
use self::wlr_data_control::manager::DataControlDeviceManagerState;
use self::wlr_data_control::source::CopyPasteSource;
- use self::wlr_data_control::SelectionOfferItem;
use wayland_client::protocol::wl_seat::WlSeat;
pub use wlr_data_control::{ClipboardItem, ClipboardValue};
@@ -76,6 +76,8 @@ pub enum Request {
ToplevelInfoAll,
#[cfg(feature = "launcher")]
ToplevelFocus(usize),
+ #[cfg(feature = "launcher")]
+ ToplevelMinimize(usize),
#[cfg(feature = "clipboard")]
CopyToClipboard(ClipboardItem),
@@ -150,12 +152,12 @@ impl Client {
spawn(async move {
while let Some(event) = event_rx.recv().await {
match event {
- Event::Output(event) => send!(output_tx, event),
+ Event::Output(event) => output_tx.send_expect(event),
#[cfg(any(feature = "focused", feature = "launcher"))]
- Event::Toplevel(event) => send!(toplevel_tx, event),
+ Event::Toplevel(event) => toplevel_tx.send_expect(event),
#[cfg(feature = "clipboard")]
- Event::Clipboard(item) => send!(clipboard_tx, item),
- };
+ Event::Clipboard(item) => clipboard_tx.send_expect(item),
+ }
}
});
}
@@ -175,7 +177,7 @@ impl Client {
/// Sends a request to the environment event loop,
/// and returns the response.
fn send_request(&self, request: Request) -> Response {
- send!(self.tx, request);
+ self.tx.send_expect(request);
lock!(self.rx).recv().expect(ERR_CHANNEL_RECV)
}
@@ -193,7 +195,6 @@ pub struct Environment {
seat_state: SeatState,
queue_handle: QueueHandle,
- loop_handle: LoopHandle<'static, Self>,
event_tx: mpsc::Sender,
response_tx: std::sync::mpsc::Sender