mirror of
https://github.com/Zedfrigg/ironbar.git
synced 2025-09-16 03:36:58 +02:00
Merge branch 'master' into develop
# Conflicts: # Cargo.lock # Cargo.toml # src/clients/networkmanager.rs # src/modules/networkmanager.rs # src/modules/volume.rs
This commit is contained in:
commit
c42024d48a
159 changed files with 13519 additions and 5655 deletions
|
@ -1,7 +1,8 @@
|
|||
use super::wayland::{self, ClipboardItem};
|
||||
use crate::{arc_mut, lock, register_client, spawn, try_send};
|
||||
use indexmap::map::Iter;
|
||||
use crate::channels::AsyncSenderExt;
|
||||
use crate::{arc_mut, lock, register_client, spawn};
|
||||
use indexmap::IndexMap;
|
||||
use indexmap::map::Iter;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{debug, trace};
|
||||
|
@ -46,7 +47,7 @@ impl Client {
|
|||
let senders = lock!(senders);
|
||||
let iter = senders.iter();
|
||||
for (tx, _) in iter {
|
||||
try_send!(tx, ClipboardEvent::Add(item.clone()));
|
||||
tx.send_spawn(ClipboardEvent::Add(item.clone()));
|
||||
}
|
||||
|
||||
lock!(cache).insert(item, senders.len());
|
||||
|
@ -74,16 +75,17 @@ impl Client {
|
|||
let removed_id = lock!(cache)
|
||||
.remove_ref_first()
|
||||
.expect("Clipboard cache unexpectedly empty");
|
||||
try_send!(tx, ClipboardEvent::Remove(removed_id));
|
||||
|
||||
tx.send_spawn(ClipboardEvent::Remove(removed_id));
|
||||
}
|
||||
try_send!(tx, ClipboardEvent::Add(item.clone()));
|
||||
tx.send_spawn(ClipboardEvent::Add(item.clone()));
|
||||
}
|
||||
},
|
||||
|existing_id| {
|
||||
let senders = lock!(senders);
|
||||
let iter = senders.iter();
|
||||
for (tx, _) in iter {
|
||||
try_send!(tx, ClipboardEvent::Activate(existing_id));
|
||||
tx.send_spawn(ClipboardEvent::Activate(existing_id));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
@ -106,7 +108,7 @@ impl Client {
|
|||
|
||||
let iter = cache.iter();
|
||||
for (_, (item, _)) in iter {
|
||||
try_send!(tx, ClipboardEvent::Add(item.clone()));
|
||||
tx.send_spawn(ClipboardEvent::Add(item.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -130,7 +132,7 @@ impl Client {
|
|||
let senders = lock!(self.senders);
|
||||
let iter = senders.iter();
|
||||
for (tx, _) in iter {
|
||||
try_send!(tx, ClipboardEvent::Activate(id));
|
||||
tx.send_spawn(ClipboardEvent::Activate(id));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -140,7 +142,7 @@ impl Client {
|
|||
let senders = lock!(self.senders);
|
||||
let iter = senders.iter();
|
||||
for (tx, _) in iter {
|
||||
try_send!(tx, ClipboardEvent::Remove(id));
|
||||
tx.send_spawn(ClipboardEvent::Remove(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,37 +1,73 @@
|
|||
use super::{Visibility, Workspace, WorkspaceClient, WorkspaceUpdate};
|
||||
use crate::{arc_mut, lock, send, spawn_blocking};
|
||||
#[cfg(feature = "bindmode+hyprland")]
|
||||
use super::{BindModeClient, BindModeUpdate};
|
||||
#[cfg(feature = "keyboard+hyprland")]
|
||||
use super::{KeyboardLayoutClient, KeyboardLayoutUpdate};
|
||||
use super::{Visibility, Workspace};
|
||||
use crate::channels::SyncSenderExt;
|
||||
use crate::{arc_mut, lock, spawn_blocking};
|
||||
use color_eyre::Result;
|
||||
use hyprland::data::{Workspace as HWorkspace, Workspaces};
|
||||
use hyprland::ctl::switch_xkb_layout;
|
||||
use hyprland::data::{Devices, Workspace as HWorkspace, Workspaces};
|
||||
use hyprland::dispatch::{Dispatch, DispatchType, WorkspaceIdentifierWithSpecial};
|
||||
use hyprland::event_listener::EventListener;
|
||||
use hyprland::prelude::*;
|
||||
use hyprland::shared::{HyprDataVec, WorkspaceType};
|
||||
use tokio::sync::broadcast::{channel, Receiver, Sender};
|
||||
use tracing::{debug, error, info};
|
||||
use tokio::sync::broadcast::{Receiver, Sender, channel};
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
#[cfg(feature = "workspaces")]
|
||||
use super::WorkspaceUpdate;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TxRx<T> {
|
||||
tx: Sender<T>,
|
||||
_rx: Receiver<T>,
|
||||
}
|
||||
impl<T: Clone> TxRx<T> {
|
||||
fn new() -> Self {
|
||||
let (tx, rx) = channel(16);
|
||||
Self { tx, _rx: rx }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Client {
|
||||
workspace_tx: Sender<WorkspaceUpdate>,
|
||||
_workspace_rx: Receiver<WorkspaceUpdate>,
|
||||
#[cfg(feature = "workspaces+hyprland")]
|
||||
workspace: TxRx<WorkspaceUpdate>,
|
||||
|
||||
#[cfg(feature = "keyboard+hyprland")]
|
||||
keyboard_layout: TxRx<KeyboardLayoutUpdate>,
|
||||
|
||||
#[cfg(feature = "bindmode+hyprland")]
|
||||
bindmode: TxRx<BindModeUpdate>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub(crate) fn new() -> Self {
|
||||
let (workspace_tx, workspace_rx) = channel(16);
|
||||
|
||||
let instance = Self {
|
||||
workspace_tx,
|
||||
_workspace_rx: workspace_rx,
|
||||
#[cfg(feature = "workspaces+hyprland")]
|
||||
workspace: TxRx::new(),
|
||||
#[cfg(feature = "keyboard+hyprland")]
|
||||
keyboard_layout: TxRx::new(),
|
||||
#[cfg(feature = "bindmode+hyprland")]
|
||||
bindmode: TxRx::new(),
|
||||
};
|
||||
|
||||
instance.listen_workspace_events();
|
||||
instance.listen_events();
|
||||
instance
|
||||
}
|
||||
|
||||
fn listen_workspace_events(&self) {
|
||||
fn listen_events(&self) {
|
||||
info!("Starting Hyprland event listener");
|
||||
|
||||
let tx = self.workspace_tx.clone();
|
||||
#[cfg(feature = "workspaces+hyprland")]
|
||||
let workspace_tx = self.workspace.tx.clone();
|
||||
|
||||
#[cfg(feature = "keyboard+hyprland")]
|
||||
let keyboard_layout_tx = self.keyboard_layout.tx.clone();
|
||||
|
||||
#[cfg(feature = "bindmode+hyprland")]
|
||||
let bindmode_tx = self.bindmode.tx.clone();
|
||||
|
||||
spawn_blocking(move || {
|
||||
let mut event_listener = EventListener::new();
|
||||
|
@ -40,179 +76,319 @@ impl Client {
|
|||
let lock = arc_mut!(());
|
||||
|
||||
// cache the active workspace since Hyprland doesn't give us the prev active
|
||||
let active = Self::get_active_workspace().expect("Failed to get active workspace");
|
||||
let active = arc_mut!(Some(active));
|
||||
#[cfg(feature = "workspaces+hyprland")]
|
||||
Self::listen_workspace_events(&workspace_tx, &mut event_listener, &lock);
|
||||
|
||||
{
|
||||
let tx = tx.clone();
|
||||
let lock = lock.clone();
|
||||
let active = active.clone();
|
||||
#[cfg(feature = "keyboard+hyprland")]
|
||||
Self::listen_keyboard_events(&keyboard_layout_tx, &mut event_listener, &lock);
|
||||
|
||||
event_listener.add_workspace_added_handler(move |workspace_type| {
|
||||
let _lock = lock!(lock);
|
||||
debug!("Added workspace: {workspace_type:?}");
|
||||
#[cfg(feature = "bindmode+hyprland")]
|
||||
Self::listen_bindmode_events(&bindmode_tx, &mut event_listener, &lock);
|
||||
|
||||
let workspace_name = get_workspace_name(workspace_type);
|
||||
let prev_workspace = lock!(active);
|
||||
if let Err(err) = event_listener.start_listener() {
|
||||
error!("Failed to start listener: {err:#}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let workspace = Self::get_workspace(&workspace_name, prev_workspace.as_ref());
|
||||
#[cfg(feature = "workspaces+hyprland")]
|
||||
fn listen_workspace_events(
|
||||
tx: &Sender<WorkspaceUpdate>,
|
||||
event_listener: &mut EventListener,
|
||||
lock: &std::sync::Arc<std::sync::Mutex<()>>,
|
||||
) {
|
||||
let active = Self::get_active_workspace().map_or_else(
|
||||
|err| {
|
||||
error!("Failed to get active workspace: {err:#?}");
|
||||
None
|
||||
},
|
||||
Some,
|
||||
);
|
||||
let active = arc_mut!(active);
|
||||
|
||||
if let Some(workspace) = workspace {
|
||||
send!(tx, WorkspaceUpdate::Add(workspace));
|
||||
{
|
||||
let tx = tx.clone();
|
||||
let lock = lock.clone();
|
||||
let active = active.clone();
|
||||
|
||||
event_listener.add_workspace_added_handler(move |event| {
|
||||
let _lock = lock!(lock);
|
||||
debug!("Added workspace: {event:?}");
|
||||
|
||||
let workspace_name = get_workspace_name(event.name);
|
||||
let prev_workspace = lock!(active);
|
||||
|
||||
let workspace = Self::get_workspace(&workspace_name, prev_workspace.as_ref());
|
||||
|
||||
match workspace {
|
||||
Ok(Some(workspace)) => {
|
||||
tx.send_expect(WorkspaceUpdate::Add(workspace));
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(e) => error!("Failed to get workspace: {e:#}"),
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let tx = tx.clone();
|
||||
let lock = lock.clone();
|
||||
let active = active.clone();
|
||||
{
|
||||
let tx = tx.clone();
|
||||
let lock = lock.clone();
|
||||
let active = active.clone();
|
||||
|
||||
event_listener.add_workspace_change_handler(move |workspace_type| {
|
||||
let _lock = lock!(lock);
|
||||
event_listener.add_workspace_changed_handler(move |event| {
|
||||
let _lock = lock!(lock);
|
||||
|
||||
let mut prev_workspace = lock!(active);
|
||||
let mut prev_workspace = lock!(active);
|
||||
|
||||
debug!(
|
||||
"Received workspace change: {:?} -> {workspace_type:?}",
|
||||
prev_workspace.as_ref().map(|w| &w.id)
|
||||
);
|
||||
debug!(
|
||||
"Received workspace change: {:?} -> {event:?}",
|
||||
prev_workspace.as_ref().map(|w| &w.id)
|
||||
);
|
||||
|
||||
let workspace_name = get_workspace_name(workspace_type);
|
||||
let workspace = Self::get_workspace(&workspace_name, prev_workspace.as_ref());
|
||||
let workspace_name = get_workspace_name(event.name);
|
||||
let workspace = Self::get_workspace(&workspace_name, prev_workspace.as_ref());
|
||||
|
||||
workspace.map_or_else(
|
||||
|| {
|
||||
error!("Unable to locate workspace");
|
||||
},
|
||||
|workspace| {
|
||||
// there may be another type of update so dispatch that regardless of focus change
|
||||
if !workspace.visibility.is_focused() {
|
||||
Self::send_focus_change(&mut prev_workspace, workspace, &tx);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let tx = tx.clone();
|
||||
let lock = lock.clone();
|
||||
let active = active.clone();
|
||||
|
||||
event_listener.add_active_monitor_change_handler(move |event_data| {
|
||||
let _lock = lock!(lock);
|
||||
let workspace_type = event_data.workspace;
|
||||
|
||||
let mut prev_workspace = lock!(active);
|
||||
|
||||
debug!(
|
||||
"Received active monitor change: {:?} -> {workspace_type:?}",
|
||||
prev_workspace.as_ref().map(|w| &w.name)
|
||||
);
|
||||
|
||||
let workspace_name = get_workspace_name(workspace_type);
|
||||
let workspace = Self::get_workspace(&workspace_name, prev_workspace.as_ref());
|
||||
|
||||
if let Some((false, workspace)) =
|
||||
workspace.map(|w| (w.visibility.is_focused(), w))
|
||||
{
|
||||
match workspace {
|
||||
Ok(Some(workspace)) if !workspace.visibility.is_focused() => {
|
||||
Self::send_focus_change(&mut prev_workspace, workspace, &tx);
|
||||
} else {
|
||||
}
|
||||
Ok(None) => {
|
||||
error!("Unable to locate workspace");
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(e) => error!("Failed to get workspace: {e:#}"),
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let tx = tx.clone();
|
||||
let lock = lock.clone();
|
||||
{
|
||||
let tx = tx.clone();
|
||||
let lock = lock.clone();
|
||||
let active = active.clone();
|
||||
|
||||
event_listener.add_workspace_moved_handler(move |event_data| {
|
||||
let _lock = lock!(lock);
|
||||
let workspace_type = event_data.workspace;
|
||||
debug!("Received workspace move: {workspace_type:?}");
|
||||
event_listener.add_active_monitor_changed_handler(move |event_data| {
|
||||
let _lock = lock!(lock);
|
||||
let Some(workspace_type) = event_data.workspace_name else {
|
||||
warn!("Received active monitor change with no workspace name");
|
||||
return;
|
||||
};
|
||||
|
||||
let mut prev_workspace = lock!(active);
|
||||
let mut prev_workspace = lock!(active);
|
||||
|
||||
let workspace_name = get_workspace_name(workspace_type);
|
||||
let workspace = Self::get_workspace(&workspace_name, prev_workspace.as_ref());
|
||||
debug!(
|
||||
"Received active monitor change: {:?} -> {workspace_type:?}",
|
||||
prev_workspace.as_ref().map(|w| &w.name)
|
||||
);
|
||||
|
||||
if let Some(workspace) = workspace {
|
||||
send!(tx, WorkspaceUpdate::Move(workspace.clone()));
|
||||
let workspace_name = get_workspace_name(workspace_type);
|
||||
let workspace = Self::get_workspace(&workspace_name, prev_workspace.as_ref());
|
||||
|
||||
if !workspace.visibility.is_focused() {
|
||||
Self::send_focus_change(&mut prev_workspace, workspace, &tx);
|
||||
}
|
||||
match workspace {
|
||||
Ok(Some(workspace)) if !workspace.visibility.is_focused() => {
|
||||
Self::send_focus_change(&mut prev_workspace, workspace, &tx);
|
||||
}
|
||||
Ok(None) => {
|
||||
error!("Unable to locate workspace");
|
||||
}
|
||||
Err(e) => error!("Failed to get workspace: {e:#}"),
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let tx = tx.clone();
|
||||
let lock = lock.clone();
|
||||
|
||||
event_listener.add_workspace_moved_handler(move |event_data| {
|
||||
let _lock = lock!(lock);
|
||||
let workspace_type = event_data.name;
|
||||
debug!("Received workspace move: {workspace_type:?}");
|
||||
|
||||
let mut prev_workspace = lock!(active);
|
||||
|
||||
let workspace_name = get_workspace_name(workspace_type);
|
||||
let workspace = Self::get_workspace(&workspace_name, prev_workspace.as_ref());
|
||||
|
||||
match workspace {
|
||||
Ok(Some(workspace)) if !workspace.visibility.is_focused() => {
|
||||
Self::send_focus_change(&mut prev_workspace, workspace, &tx);
|
||||
}
|
||||
Ok(None) => {
|
||||
error!("Unable to locate workspace");
|
||||
}
|
||||
Err(e) => error!("Failed to get workspace: {e:#}"),
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let tx = tx.clone();
|
||||
let lock = lock.clone();
|
||||
|
||||
event_listener.add_workspace_renamed_handler(move |data| {
|
||||
let _lock = lock!(lock);
|
||||
debug!("Received workspace rename: {data:?}");
|
||||
|
||||
tx.send_expect(WorkspaceUpdate::Rename {
|
||||
id: data.id as i64,
|
||||
name: data.name,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let tx = tx.clone();
|
||||
let lock = lock.clone();
|
||||
|
||||
event_listener.add_workspace_deleted_handler(move |data| {
|
||||
let _lock = lock!(lock);
|
||||
debug!("Received workspace destroy: {data:?}");
|
||||
tx.send_expect(WorkspaceUpdate::Remove(data.id as i64));
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let tx = tx.clone();
|
||||
let lock = lock.clone();
|
||||
|
||||
event_listener.add_urgent_state_changed_handler(move |address| {
|
||||
let _lock = lock!(lock);
|
||||
debug!("Received urgent state: {address:?}");
|
||||
|
||||
let clients = match hyprland::data::Clients::get() {
|
||||
Ok(clients) => clients,
|
||||
Err(err) => {
|
||||
error!("Failed to get clients: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
clients.iter().find(|c| c.address == address).map_or_else(
|
||||
|| {
|
||||
error!("Unable to locate client");
|
||||
},
|
||||
|c| {
|
||||
tx.send_expect(WorkspaceUpdate::Urgent {
|
||||
id: c.workspace.id as i64,
|
||||
urgent: true,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "keyboard+hyprland")]
|
||||
fn listen_keyboard_events(
|
||||
keyboard_layout_tx: &Sender<KeyboardLayoutUpdate>,
|
||||
event_listener: &mut EventListener,
|
||||
lock: &std::sync::Arc<std::sync::Mutex<()>>,
|
||||
) {
|
||||
let tx = keyboard_layout_tx.clone();
|
||||
let lock = lock.clone();
|
||||
|
||||
event_listener.add_layout_changed_handler(move |layout_event| {
|
||||
let _lock = lock!(lock);
|
||||
|
||||
let layout = if layout_event.layout_name.is_empty() {
|
||||
// FIXME: This field is empty due to bug in `hyprland-rs_0.4.0-alpha.3`. Which is already fixed in last betas
|
||||
|
||||
// The layout may be empty due to a bug in `hyprland-rs`, because of which the `layout_event` is incorrect.
|
||||
//
|
||||
// Instead of:
|
||||
// ```
|
||||
// LayoutEvent {
|
||||
// keyboard_name: "keychron-keychron-c2",
|
||||
// layout_name: "English (US)",
|
||||
// }
|
||||
// ```
|
||||
//
|
||||
// We get:
|
||||
// ```
|
||||
// LayoutEvent {
|
||||
// keyboard_name: "keychron-keychron-c2,English (US)",
|
||||
// layout_name: "",
|
||||
// }
|
||||
// ```
|
||||
//
|
||||
// Here we are trying to recover `layout_name` from `keyboard_name`
|
||||
|
||||
let layout = layout_event.keyboard_name.as_str().split(',').nth(1);
|
||||
let Some(layout) = layout else {
|
||||
error!(
|
||||
"Failed to get layout from string: {}. The failed logic is a workaround for a bug in `hyprland 0.4.0-alpha.3`", layout_event.keyboard_name);
|
||||
return;
|
||||
};
|
||||
|
||||
layout.into()
|
||||
}
|
||||
else {
|
||||
layout_event.layout_name
|
||||
};
|
||||
|
||||
{
|
||||
let tx = tx.clone();
|
||||
let lock = lock.clone();
|
||||
debug!("Received layout: {layout:?}");
|
||||
tx.send_expect(KeyboardLayoutUpdate(layout));
|
||||
});
|
||||
}
|
||||
|
||||
event_listener.add_workspace_rename_handler(move |data| {
|
||||
let _lock = lock!(lock);
|
||||
#[cfg(feature = "bindmode+hyprland")]
|
||||
fn listen_bindmode_events(
|
||||
bindmode_tx: &Sender<BindModeUpdate>,
|
||||
event_listener: &mut EventListener,
|
||||
lock: &std::sync::Arc<std::sync::Mutex<()>>,
|
||||
) {
|
||||
let tx = bindmode_tx.clone();
|
||||
let lock = lock.clone();
|
||||
|
||||
send!(
|
||||
tx,
|
||||
WorkspaceUpdate::Rename {
|
||||
id: data.workspace_id as i64,
|
||||
name: data.workspace_name
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
event_listener.add_sub_map_changed_handler(move |bind_mode| {
|
||||
let _lock = lock!(lock);
|
||||
debug!("Received bind mode: {bind_mode:?}");
|
||||
|
||||
{
|
||||
event_listener.add_workspace_destroy_handler(move |data| {
|
||||
let _lock = lock!(lock);
|
||||
debug!("Received workspace destroy: {data:?}");
|
||||
send!(tx, WorkspaceUpdate::Remove(data.workspace_id as i64));
|
||||
});
|
||||
}
|
||||
|
||||
event_listener
|
||||
.start_listener()
|
||||
.expect("Failed to start listener");
|
||||
tx.send_expect(BindModeUpdate {
|
||||
name: bind_mode,
|
||||
pango_markup: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Sends a `WorkspaceUpdate::Focus` event
|
||||
/// and updates the active workspace cache.
|
||||
#[cfg(feature = "workspaces+hyprland")]
|
||||
fn send_focus_change(
|
||||
prev_workspace: &mut Option<Workspace>,
|
||||
workspace: Workspace,
|
||||
tx: &Sender<WorkspaceUpdate>,
|
||||
) {
|
||||
send!(
|
||||
tx,
|
||||
WorkspaceUpdate::Focus {
|
||||
old: prev_workspace.take(),
|
||||
new: workspace.clone(),
|
||||
}
|
||||
);
|
||||
tx.send_expect(WorkspaceUpdate::Focus {
|
||||
old: prev_workspace.take(),
|
||||
new: workspace.clone(),
|
||||
});
|
||||
|
||||
tx.send_expect(WorkspaceUpdate::Urgent {
|
||||
id: workspace.id,
|
||||
urgent: false,
|
||||
});
|
||||
|
||||
prev_workspace.replace(workspace);
|
||||
}
|
||||
|
||||
/// Gets a workspace by name from the server, given the active workspace if known.
|
||||
fn get_workspace(name: &str, active: Option<&Workspace>) -> Option<Workspace> {
|
||||
Workspaces::get()
|
||||
.expect("Failed to get workspaces")
|
||||
.into_iter()
|
||||
.find_map(|w| {
|
||||
if w.name == name {
|
||||
let vis = Visibility::from((&w, active.map(|w| w.name.as_ref()), &|w| {
|
||||
create_is_visible()(w)
|
||||
}));
|
||||
#[cfg(feature = "workspaces+hyprland")]
|
||||
fn get_workspace(name: &str, active: Option<&Workspace>) -> Result<Option<Workspace>> {
|
||||
let workspace = Workspaces::get()?.into_iter().find_map(|w| {
|
||||
if w.name == name {
|
||||
let vis = Visibility::from((&w, active.map(|w| w.name.as_ref()), &|w| {
|
||||
create_is_visible()(w)
|
||||
}));
|
||||
|
||||
Some(Workspace::from((vis, w)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
Some(Workspace::from((vis, w)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
Ok(workspace)
|
||||
}
|
||||
|
||||
/// Gets the active workspace from the server.
|
||||
|
@ -222,43 +398,100 @@ impl Client {
|
|||
}
|
||||
}
|
||||
|
||||
impl WorkspaceClient for Client {
|
||||
fn focus(&self, id: String) -> Result<()> {
|
||||
let identifier = id.parse::<i32>().map_or_else(
|
||||
|_| WorkspaceIdentifierWithSpecial::Name(&id),
|
||||
WorkspaceIdentifierWithSpecial::Id,
|
||||
);
|
||||
#[cfg(feature = "workspaces+hyprland")]
|
||||
impl super::WorkspaceClient for Client {
|
||||
fn focus(&self, id: i64) {
|
||||
let identifier = WorkspaceIdentifierWithSpecial::Id(id as i32);
|
||||
|
||||
Dispatch::call(DispatchType::Workspace(identifier))?;
|
||||
Ok(())
|
||||
if let Err(e) = Dispatch::call(DispatchType::Workspace(identifier)) {
|
||||
error!("Couldn't focus workspace '{id}': {e:#}");
|
||||
}
|
||||
}
|
||||
|
||||
fn subscribe_workspace_change(&self) -> Receiver<WorkspaceUpdate> {
|
||||
let rx = self.workspace_tx.subscribe();
|
||||
fn subscribe(&self) -> Receiver<WorkspaceUpdate> {
|
||||
let rx = self.workspace.tx.subscribe();
|
||||
|
||||
{
|
||||
let tx = self.workspace_tx.clone();
|
||||
let active_id = HWorkspace::get_active().ok().map(|active| active.name);
|
||||
let is_visible = create_is_visible();
|
||||
|
||||
let active_id = HWorkspace::get_active().ok().map(|active| active.name);
|
||||
let is_visible = create_is_visible();
|
||||
match Workspaces::get() {
|
||||
Ok(workspaces) => {
|
||||
let workspaces = workspaces
|
||||
.into_iter()
|
||||
.map(|w| {
|
||||
let vis = Visibility::from((&w, active_id.as_deref(), &is_visible));
|
||||
Workspace::from((vis, w))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let workspaces = Workspaces::get()
|
||||
.expect("Failed to get workspaces")
|
||||
.into_iter()
|
||||
.map(|w| {
|
||||
let vis = Visibility::from((&w, active_id.as_deref(), &is_visible));
|
||||
|
||||
Workspace::from((vis, w))
|
||||
})
|
||||
.collect();
|
||||
|
||||
send!(tx, WorkspaceUpdate::Init(workspaces));
|
||||
self.workspace
|
||||
.tx
|
||||
.send_expect(WorkspaceUpdate::Init(workspaces));
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to get workspaces: {e:#}");
|
||||
}
|
||||
}
|
||||
|
||||
rx
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "keyboard+hyprland")]
|
||||
impl KeyboardLayoutClient for Client {
|
||||
fn set_next_active(&self) {
|
||||
let Ok(devices) = Devices::get() else {
|
||||
error!("Failed to get devices");
|
||||
return;
|
||||
};
|
||||
|
||||
let device = devices
|
||||
.keyboards
|
||||
.iter()
|
||||
.find(|k| k.main)
|
||||
.map(|k| k.name.clone());
|
||||
|
||||
if let Some(device) = device {
|
||||
if let Err(e) =
|
||||
switch_xkb_layout::call(device, switch_xkb_layout::SwitchXKBLayoutCmdTypes::Next)
|
||||
{
|
||||
error!("Failed to switch keyboard layout due to Hyprland error: {e}");
|
||||
}
|
||||
} else {
|
||||
error!("Failed to get keyboard device from hyprland");
|
||||
}
|
||||
}
|
||||
|
||||
fn subscribe(&self) -> Receiver<KeyboardLayoutUpdate> {
|
||||
let rx = self.keyboard_layout.tx.subscribe();
|
||||
|
||||
match Devices::get().map(|devices| {
|
||||
devices
|
||||
.keyboards
|
||||
.iter()
|
||||
.find(|k| k.main)
|
||||
.map(|k| k.active_keymap.clone())
|
||||
}) {
|
||||
Ok(Some(layout)) => {
|
||||
self.keyboard_layout
|
||||
.tx
|
||||
.send_expect(KeyboardLayoutUpdate(layout));
|
||||
}
|
||||
Ok(None) => error!("Failed to get current keyboard layout hyprland"),
|
||||
Err(err) => error!("Failed to get devices: {err:#?}"),
|
||||
}
|
||||
|
||||
rx
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "bindmode+hyprland")]
|
||||
impl BindModeClient for Client {
|
||||
fn subscribe(&self) -> Result<Receiver<BindModeUpdate>> {
|
||||
Ok(self.bindmode.tx.subscribe())
|
||||
}
|
||||
}
|
||||
|
||||
fn get_workspace_name(name: WorkspaceType) -> String {
|
||||
match name {
|
||||
WorkspaceType::Regular(name) => name,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use crate::{await_sync, register_fallible_client};
|
||||
use crate::clients::ClientResult;
|
||||
use crate::register_fallible_client;
|
||||
use cfg_if::cfg_if;
|
||||
use color_eyre::{Help, Report, Result};
|
||||
use std::fmt::{Debug, Display, Formatter};
|
||||
|
@ -6,16 +7,20 @@ use std::sync::Arc;
|
|||
use tokio::sync::broadcast;
|
||||
use tracing::debug;
|
||||
|
||||
#[cfg(feature = "workspaces+hyprland")]
|
||||
#[cfg(feature = "hyprland")]
|
||||
pub mod hyprland;
|
||||
#[cfg(feature = "workspaces+sway")]
|
||||
#[cfg(feature = "niri")]
|
||||
pub mod niri;
|
||||
#[cfg(feature = "sway")]
|
||||
pub mod sway;
|
||||
|
||||
pub enum Compositor {
|
||||
#[cfg(feature = "workspaces+sway")]
|
||||
#[cfg(feature = "sway")]
|
||||
Sway,
|
||||
#[cfg(feature = "workspaces+hyprland")]
|
||||
#[cfg(feature = "hyprland")]
|
||||
Hyprland,
|
||||
#[cfg(feature = "niri")]
|
||||
Niri,
|
||||
Unsupported,
|
||||
}
|
||||
|
||||
|
@ -25,10 +30,12 @@ impl Display for Compositor {
|
|||
f,
|
||||
"{}",
|
||||
match self {
|
||||
#[cfg(feature = "workspaces+sway")]
|
||||
#[cfg(any(feature = "sway"))]
|
||||
Self::Sway => "Sway",
|
||||
#[cfg(feature = "workspaces+hyprland")]
|
||||
#[cfg(any(feature = "hyprland"))]
|
||||
Self::Hyprland => "Hyprland",
|
||||
#[cfg(feature = "workspaces+niri")]
|
||||
Self::Niri => "Niri",
|
||||
Self::Unsupported => "Unsupported",
|
||||
}
|
||||
)
|
||||
|
@ -41,32 +48,90 @@ impl Compositor {
|
|||
fn get_current() -> Self {
|
||||
if std::env::var("SWAYSOCK").is_ok() {
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "workspaces+sway")] { Self::Sway }
|
||||
if #[cfg(feature = "sway")] { Self::Sway }
|
||||
else { tracing::error!("Not compiled with Sway support"); Self::Unsupported }
|
||||
}
|
||||
} else if std::env::var("HYPRLAND_INSTANCE_SIGNATURE").is_ok() {
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "workspaces+hyprland")] { Self::Hyprland }
|
||||
if #[cfg(feature = "hyprland")] { Self::Hyprland }
|
||||
else { tracing::error!("Not compiled with Hyprland support"); Self::Unsupported }
|
||||
}
|
||||
} else if std::env::var("NIRI_SOCKET").is_ok() {
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "niri")] { Self::Niri }
|
||||
else {tracing::error!("Not compiled with Niri support"); Self::Unsupported }
|
||||
}
|
||||
} else {
|
||||
Self::Unsupported
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "bindmode")]
|
||||
pub fn create_bindmode_client(
|
||||
clients: &mut super::Clients,
|
||||
) -> ClientResult<dyn BindModeClient + Send + Sync> {
|
||||
let current = Self::get_current();
|
||||
debug!("Getting keyboard_layout client for: {current}");
|
||||
match current {
|
||||
#[cfg(feature = "bindmode+sway")]
|
||||
Self::Sway => Ok(clients.sway()?),
|
||||
#[cfg(feature = "bindmode+hyprland")]
|
||||
Self::Hyprland => Ok(clients.hyprland()),
|
||||
#[cfg(feature = "niri")]
|
||||
Self::Niri => Err(Report::msg("Unsupported compositor")
|
||||
.note("Currently bindmode is only supported by Sway and Hyprland")),
|
||||
Self::Unsupported => Err(Report::msg("Unsupported compositor")
|
||||
.note("Currently bindmode is only supported by Sway and Hyprland")),
|
||||
#[allow(unreachable_patterns)]
|
||||
_ => Err(Report::msg("Unsupported compositor")
|
||||
.note("Bindmode feature is disabled for this compositor")),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "keyboard")]
|
||||
pub fn create_keyboard_layout_client(
|
||||
clients: &mut super::Clients,
|
||||
) -> ClientResult<dyn KeyboardLayoutClient + Send + Sync> {
|
||||
let current = Self::get_current();
|
||||
debug!("Getting keyboard_layout client for: {current}");
|
||||
match current {
|
||||
#[cfg(feature = "keyboard+sway")]
|
||||
Self::Sway => Ok(clients.sway()?),
|
||||
#[cfg(feature = "keyboard+hyprland")]
|
||||
Self::Hyprland => Ok(clients.hyprland()),
|
||||
#[cfg(feature = "niri")]
|
||||
Self::Niri => Err(Report::msg("Unsupported compositor").note(
|
||||
"Currently keyboard layout functionality are only supported by Sway and Hyprland",
|
||||
)),
|
||||
Self::Unsupported => Err(Report::msg("Unsupported compositor").note(
|
||||
"Currently keyboard layout functionality are only supported by Sway and Hyprland",
|
||||
)),
|
||||
#[allow(unreachable_patterns)]
|
||||
_ => Err(Report::msg("Unsupported compositor")
|
||||
.note("Keyboard layout feature is disabled for this compositor")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new instance of
|
||||
/// the workspace client for the current compositor.
|
||||
pub fn create_workspace_client() -> Result<Arc<dyn WorkspaceClient + Send + Sync>> {
|
||||
#[cfg(feature = "workspaces")]
|
||||
pub fn create_workspace_client(
|
||||
clients: &mut super::Clients,
|
||||
) -> Result<Arc<dyn WorkspaceClient + Send + Sync>> {
|
||||
let current = Self::get_current();
|
||||
debug!("Getting workspace client for: {current}");
|
||||
match current {
|
||||
#[cfg(feature = "workspaces+sway")]
|
||||
Self::Sway => await_sync(async { sway::Client::new().await })
|
||||
.map(|client| Arc::new(client) as Arc<dyn WorkspaceClient + Send + Sync>),
|
||||
Self::Sway => Ok(clients.sway()?),
|
||||
#[cfg(feature = "workspaces+hyprland")]
|
||||
Self::Hyprland => Ok(Arc::new(hyprland::Client::new())),
|
||||
Self::Hyprland => Ok(clients.hyprland()),
|
||||
#[cfg(feature = "workspaces+niri")]
|
||||
Self::Niri => Ok(Arc::new(niri::Client::new())),
|
||||
Self::Unsupported => Err(Report::msg("Unsupported compositor")
|
||||
.note("Currently workspaces are only supported by Sway and Hyprland")),
|
||||
.note("Currently workspaces are only supported by Sway, Niri and Hyprland")),
|
||||
#[allow(unreachable_patterns)]
|
||||
_ => Err(Report::msg("Unsupported compositor")
|
||||
.note("Workspaces feature is disabled for this compositor")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -83,29 +148,29 @@ pub struct Workspace {
|
|||
pub visibility: Visibility,
|
||||
}
|
||||
|
||||
/// Indicates workspace visibility. Visible workspaces have a boolean flag to indicate if they are also focused.
|
||||
/// Yes, this is the same signature as Option<bool>, but it's impl is a lot more suited for our case.
|
||||
/// Indicates workspace visibility.
|
||||
/// Visible workspaces have a boolean flag to indicate if they are also focused.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum Visibility {
|
||||
Visible(bool),
|
||||
Visible { focused: bool },
|
||||
Hidden,
|
||||
}
|
||||
|
||||
impl Visibility {
|
||||
pub fn visible() -> Self {
|
||||
Self::Visible(false)
|
||||
Self::Visible { focused: false }
|
||||
}
|
||||
|
||||
pub fn focused() -> Self {
|
||||
Self::Visible(true)
|
||||
Self::Visible { focused: true }
|
||||
}
|
||||
|
||||
pub fn is_visible(self) -> bool {
|
||||
matches!(self, Self::Visible(_))
|
||||
matches!(self, Self::Visible { .. })
|
||||
}
|
||||
|
||||
pub fn is_focused(self) -> bool {
|
||||
if let Self::Visible(focused) = self {
|
||||
if let Self::Visible { focused } = self {
|
||||
focused
|
||||
} else {
|
||||
false
|
||||
|
@ -114,6 +179,11 @@ impl Visibility {
|
|||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg(feature = "keyboard")]
|
||||
pub struct KeyboardLayoutUpdate(pub String);
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg(feature = "workspaces")]
|
||||
pub enum WorkspaceUpdate {
|
||||
/// Provides an initial list of workspaces.
|
||||
/// This is re-sent to all subscribers when a new subscription is created.
|
||||
|
@ -132,6 +202,12 @@ pub enum WorkspaceUpdate {
|
|||
name: String,
|
||||
},
|
||||
|
||||
/// The urgent state of a node changed.
|
||||
Urgent {
|
||||
id: i64,
|
||||
urgent: bool,
|
||||
},
|
||||
|
||||
/// An update was triggered by the compositor but this was not mapped by Ironbar.
|
||||
///
|
||||
/// This is purely used for ergonomics within the compositor clients
|
||||
|
@ -139,12 +215,44 @@ pub enum WorkspaceUpdate {
|
|||
Unknown,
|
||||
}
|
||||
|
||||
pub trait WorkspaceClient: Debug + Send + Sync {
|
||||
/// Requests the workspace with this name is focused.
|
||||
fn focus(&self, name: String) -> Result<()>;
|
||||
|
||||
/// Creates a new to workspace event receiver.
|
||||
fn subscribe_workspace_change(&self) -> broadcast::Receiver<WorkspaceUpdate>;
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg(feature = "bindmode")]
|
||||
pub struct BindModeUpdate {
|
||||
/// The binding mode that became active.
|
||||
pub name: String,
|
||||
/// Whether the mode should be parsed as pango markup.
|
||||
pub pango_markup: bool,
|
||||
}
|
||||
|
||||
#[cfg(feature = "workspaces")]
|
||||
pub trait WorkspaceClient: Debug + Send + Sync {
|
||||
/// Requests the workspace with this id is focused.
|
||||
fn focus(&self, id: i64);
|
||||
|
||||
/// Creates a new to workspace event receiver.
|
||||
fn subscribe(&self) -> broadcast::Receiver<WorkspaceUpdate>;
|
||||
}
|
||||
|
||||
#[cfg(feature = "workspaces")]
|
||||
register_fallible_client!(dyn WorkspaceClient, workspaces);
|
||||
|
||||
#[cfg(feature = "keyboard")]
|
||||
pub trait KeyboardLayoutClient: Debug + Send + Sync {
|
||||
/// Switches to the next layout.
|
||||
fn set_next_active(&self);
|
||||
|
||||
/// Creates a new to keyboard layout event receiver.
|
||||
fn subscribe(&self) -> broadcast::Receiver<KeyboardLayoutUpdate>;
|
||||
}
|
||||
|
||||
#[cfg(feature = "keyboard")]
|
||||
register_fallible_client!(dyn KeyboardLayoutClient, keyboard_layout);
|
||||
|
||||
#[cfg(feature = "bindmode")]
|
||||
pub trait BindModeClient: Debug + Send + Sync {
|
||||
/// Add a callback for bindmode updates.
|
||||
fn subscribe(&self) -> Result<broadcast::Receiver<BindModeUpdate>>;
|
||||
}
|
||||
|
||||
#[cfg(feature = "bindmode")]
|
||||
register_fallible_client!(dyn BindModeClient, bindmode);
|
||||
|
|
117
src/clients/compositor/niri/connection.rs
Normal file
117
src/clients/compositor/niri/connection.rs
Normal file
|
@ -0,0 +1,117 @@
|
|||
/// Taken from the `niri_ipc` crate.
|
||||
/// Only a relevant snippet has been extracted
|
||||
/// to reduce compile times.
|
||||
use crate::clients::compositor::Workspace as IronWorkspace;
|
||||
use crate::{await_sync, clients::compositor::Visibility};
|
||||
use color_eyre::eyre::{Result, eyre};
|
||||
use core::str;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{env, path::Path};
|
||||
use tokio::{
|
||||
io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
|
||||
net::UnixStream,
|
||||
};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub enum Request {
|
||||
Action(Action),
|
||||
EventStream,
|
||||
}
|
||||
|
||||
pub type Reply = Result<Response, String>;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub enum Response {
|
||||
Handled,
|
||||
Workspaces(Vec<Workspace>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub enum Action {
|
||||
FocusWorkspace { reference: WorkspaceReferenceArg },
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
pub enum WorkspaceReferenceArg {
|
||||
Name(String),
|
||||
Id(u64),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Workspace {
|
||||
pub id: u64,
|
||||
pub idx: u8,
|
||||
pub name: Option<String>,
|
||||
pub output: Option<String>,
|
||||
pub is_active: bool,
|
||||
pub is_focused: bool,
|
||||
}
|
||||
|
||||
impl From<&Workspace> for IronWorkspace {
|
||||
fn from(workspace: &Workspace) -> IronWorkspace {
|
||||
// Workspaces in niri don't neccessarily have names.
|
||||
// If the niri workspace has a name then it is assigned as is,
|
||||
// but if it does not have a name, the monitor index is used.
|
||||
Self {
|
||||
id: workspace.id as i64,
|
||||
name: workspace.name.clone().unwrap_or(workspace.idx.to_string()),
|
||||
monitor: workspace.output.clone().unwrap_or_default(),
|
||||
visibility: if workspace.is_active {
|
||||
Visibility::Visible {
|
||||
focused: workspace.is_focused,
|
||||
}
|
||||
} else {
|
||||
Visibility::Hidden
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub enum Event {
|
||||
WorkspacesChanged { workspaces: Vec<Workspace> },
|
||||
WorkspaceActivated { id: u64, focused: bool },
|
||||
Other,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Connection(UnixStream);
|
||||
impl Connection {
|
||||
pub async fn connect() -> Result<Self> {
|
||||
let socket_path =
|
||||
env::var_os("NIRI_SOCKET").ok_or_else(|| eyre!("NIRI_SOCKET not found!"))?;
|
||||
Self::connect_to(socket_path).await
|
||||
}
|
||||
|
||||
pub async fn connect_to(path: impl AsRef<Path>) -> Result<Self> {
|
||||
let raw_stream = UnixStream::connect(path.as_ref()).await?;
|
||||
let stream = raw_stream;
|
||||
Ok(Self(stream))
|
||||
}
|
||||
|
||||
pub async fn send(
|
||||
&mut self,
|
||||
request: Request,
|
||||
) -> Result<(Reply, impl FnMut() -> Result<Event> + '_)> {
|
||||
let Self(stream) = self;
|
||||
let mut buf = serde_json::to_string(&request)?;
|
||||
|
||||
stream.write_all(buf.as_bytes()).await?;
|
||||
stream.shutdown().await?;
|
||||
|
||||
buf.clear();
|
||||
let mut reader = BufReader::new(stream);
|
||||
reader.read_line(&mut buf).await?;
|
||||
let reply = serde_json::from_str(&buf)?;
|
||||
|
||||
let events = move || {
|
||||
buf.clear();
|
||||
await_sync(async {
|
||||
reader.read_line(&mut buf).await.unwrap_or(0);
|
||||
});
|
||||
let event: Event = serde_json::from_str(&buf).unwrap_or(Event::Other);
|
||||
Ok(event)
|
||||
};
|
||||
Ok((reply, events))
|
||||
}
|
||||
}
|
223
src/clients/compositor/niri/mod.rs
Normal file
223
src/clients/compositor/niri/mod.rs
Normal file
|
@ -0,0 +1,223 @@
|
|||
use super::{Workspace as IronWorkspace, WorkspaceClient, WorkspaceUpdate};
|
||||
use crate::channels::SyncSenderExt;
|
||||
use crate::clients::compositor::Visibility;
|
||||
use crate::{arc_rw, read_lock, spawn, write_lock};
|
||||
use color_eyre::Report;
|
||||
use connection::{Action, Connection, Event, Request, WorkspaceReferenceArg};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use tokio::sync::broadcast;
|
||||
use tracing::{debug, error, warn};
|
||||
|
||||
mod connection;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Client {
|
||||
tx: broadcast::Sender<WorkspaceUpdate>,
|
||||
_rx: broadcast::Receiver<WorkspaceUpdate>,
|
||||
|
||||
workspaces: Arc<RwLock<Vec<IronWorkspace>>>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new() -> Self {
|
||||
let (tx, rx) = broadcast::channel(32);
|
||||
let tx2 = tx.clone();
|
||||
|
||||
let workspace_state = arc_rw!(vec![]);
|
||||
let workspace_state2 = workspace_state.clone();
|
||||
|
||||
spawn(async move {
|
||||
let mut conn = Connection::connect().await?;
|
||||
let (_, mut event_listener) = conn.send(Request::EventStream).await?;
|
||||
|
||||
let mut first_event = true;
|
||||
|
||||
loop {
|
||||
let events = match event_listener() {
|
||||
Ok(Event::WorkspacesChanged { workspaces }) => {
|
||||
debug!("WorkspacesChanged: {:?}", workspaces);
|
||||
|
||||
// Niri only has a WorkspacesChanged Event and Ironbar has 4 events which have to be handled: Add, Remove, Rename and Move.
|
||||
// This is handled by keeping a previous state of workspaces and comparing with the new state for changes.
|
||||
let new_workspaces: Vec<IronWorkspace> = workspaces
|
||||
.into_iter()
|
||||
.map(|w| IronWorkspace::from(&w))
|
||||
.collect();
|
||||
|
||||
let mut updates: Vec<WorkspaceUpdate> = vec![];
|
||||
|
||||
if first_event {
|
||||
// Niri's WorkspacesChanged event does not initially sort workspaces by ID when first output,
|
||||
// which makes sort = added meaningless. Therefore, new_workspaces are sorted by ID here to ensure a consistent addition order.
|
||||
let mut new_workspaces = new_workspaces.clone();
|
||||
new_workspaces.sort_by_key(|w| w.id);
|
||||
updates.push(WorkspaceUpdate::Init(new_workspaces));
|
||||
first_event = false;
|
||||
} else {
|
||||
// first pass - add/update
|
||||
for workspace in &new_workspaces {
|
||||
let workspace_state = read_lock!(workspace_state);
|
||||
let old_workspace = workspace_state
|
||||
.iter()
|
||||
.find(|&w: &&IronWorkspace| w.id == workspace.id);
|
||||
|
||||
match old_workspace {
|
||||
None => updates.push(WorkspaceUpdate::Add(workspace.clone())),
|
||||
Some(old_workspace) => {
|
||||
if workspace.name != old_workspace.name {
|
||||
updates.push(WorkspaceUpdate::Rename {
|
||||
id: workspace.id,
|
||||
name: workspace.name.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
if workspace.monitor != old_workspace.monitor {
|
||||
updates.push(WorkspaceUpdate::Move(workspace.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// second pass - delete
|
||||
for workspace in read_lock!(workspace_state).iter() {
|
||||
let exists = new_workspaces.iter().any(|w| w.id == workspace.id);
|
||||
|
||||
if !exists {
|
||||
updates.push(WorkspaceUpdate::Remove(workspace.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*write_lock!(workspace_state) = new_workspaces;
|
||||
updates
|
||||
}
|
||||
|
||||
Ok(Event::WorkspaceActivated { id, focused }) => {
|
||||
debug!("WorkspaceActivated: id: {}, focused: {}", id, focused);
|
||||
|
||||
// workspace with id is activated, if focus is true then it is also focused
|
||||
// if focused is true then focus has changed => find old focused workspace. set it to inactive and set current
|
||||
//
|
||||
// we use indexes here as both new/old need to be mutable
|
||||
|
||||
let new_index = read_lock!(workspace_state)
|
||||
.iter()
|
||||
.position(|w| w.id == id as i64);
|
||||
|
||||
if let Some(new_index) = new_index {
|
||||
if focused {
|
||||
let old_index = read_lock!(workspace_state)
|
||||
.iter()
|
||||
.position(|w| w.visibility.is_focused());
|
||||
|
||||
if let Some(old_index) = old_index {
|
||||
write_lock!(workspace_state)[new_index].visibility =
|
||||
Visibility::focused();
|
||||
|
||||
if read_lock!(workspace_state)[old_index].monitor
|
||||
== read_lock!(workspace_state)[new_index].monitor
|
||||
{
|
||||
write_lock!(workspace_state)[old_index].visibility =
|
||||
Visibility::Hidden;
|
||||
} else {
|
||||
write_lock!(workspace_state)[old_index].visibility =
|
||||
Visibility::visible();
|
||||
}
|
||||
|
||||
vec![WorkspaceUpdate::Focus {
|
||||
old: Some(read_lock!(workspace_state)[old_index].clone()),
|
||||
new: read_lock!(workspace_state)[new_index].clone(),
|
||||
}]
|
||||
} else {
|
||||
write_lock!(workspace_state)[new_index].visibility =
|
||||
Visibility::focused();
|
||||
|
||||
vec![WorkspaceUpdate::Focus {
|
||||
old: None,
|
||||
new: read_lock!(workspace_state)[new_index].clone(),
|
||||
}]
|
||||
}
|
||||
} else {
|
||||
// if focused is false means active workspace on a particular monitor has changed =>
|
||||
// change all workspaces on monitor to inactive and change current workspace as active
|
||||
write_lock!(workspace_state)[new_index].visibility =
|
||||
Visibility::visible();
|
||||
|
||||
let old_index = read_lock!(workspace_state).iter().position(|w| {
|
||||
(w.visibility.is_focused() || w.visibility.is_visible())
|
||||
&& w.monitor
|
||||
== read_lock!(workspace_state)[new_index].monitor
|
||||
});
|
||||
|
||||
if let Some(old_index) = old_index {
|
||||
write_lock!(workspace_state)[old_index].visibility =
|
||||
Visibility::Hidden;
|
||||
|
||||
vec![]
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!("No workspace with id for new focus/visible workspace found");
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
Ok(Event::Other) => {
|
||||
vec![]
|
||||
}
|
||||
Err(err) => {
|
||||
error!("{err:?}");
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
for event in events {
|
||||
tx.send_expect(event);
|
||||
}
|
||||
}
|
||||
|
||||
Ok::<(), Report>(())
|
||||
});
|
||||
|
||||
Self {
|
||||
tx: tx2,
|
||||
_rx: rx,
|
||||
workspaces: workspace_state2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WorkspaceClient for Client {
|
||||
fn focus(&self, id: i64) {
|
||||
debug!("focusing workspace with id: {}", id);
|
||||
|
||||
// this does annoyingly require spawning a separate connection for every focus call
|
||||
// the alternative is sticking the conn behind a mutex which could perform worse
|
||||
spawn(async move {
|
||||
let mut conn = Connection::connect().await?;
|
||||
|
||||
let command = Request::Action(Action::FocusWorkspace {
|
||||
reference: WorkspaceReferenceArg::Id(id as u64),
|
||||
});
|
||||
|
||||
if let Err(err) = conn.send(command).await {
|
||||
error!("failed to send command: {err:?}");
|
||||
}
|
||||
|
||||
Ok::<(), Report>(())
|
||||
});
|
||||
}
|
||||
|
||||
fn subscribe(&self) -> broadcast::Receiver<WorkspaceUpdate> {
|
||||
let rx = self.tx.subscribe();
|
||||
|
||||
let workspaces = read_lock!(self.workspaces);
|
||||
if !workspaces.is_empty() {
|
||||
self.tx
|
||||
.send_expect(WorkspaceUpdate::Init(workspaces.clone()));
|
||||
}
|
||||
|
||||
rx
|
||||
}
|
||||
}
|
|
@ -1,85 +1,66 @@
|
|||
use super::{Visibility, Workspace, WorkspaceClient, WorkspaceUpdate};
|
||||
use crate::{await_sync, send, spawn};
|
||||
use color_eyre::{Report, Result};
|
||||
use futures_lite::StreamExt;
|
||||
use std::sync::Arc;
|
||||
use swayipc_async::{Connection, Event, EventType, Node, WorkspaceChange, WorkspaceEvent};
|
||||
use tokio::sync::broadcast::{channel, Receiver, Sender};
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::{info, trace};
|
||||
use super::{Visibility, Workspace};
|
||||
use crate::channels::SyncSenderExt;
|
||||
use crate::clients::sway::Client;
|
||||
use crate::{await_sync, error, spawn};
|
||||
use color_eyre::Report;
|
||||
use swayipc_async::{InputChange, InputEvent, Node, WorkspaceChange, WorkspaceEvent};
|
||||
use tokio::sync::broadcast::{Receiver, channel};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Client {
|
||||
client: Arc<Mutex<Connection>>,
|
||||
workspace_tx: Sender<WorkspaceUpdate>,
|
||||
_workspace_rx: Receiver<WorkspaceUpdate>,
|
||||
}
|
||||
#[cfg(feature = "workspaces")]
|
||||
use super::WorkspaceUpdate;
|
||||
|
||||
impl Client {
|
||||
pub(crate) async fn new() -> Result<Self> {
|
||||
// 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");
|
||||
#[cfg(feature = "workspaces+sway")]
|
||||
impl super::WorkspaceClient for Client {
|
||||
fn focus(&self, id: i64) {
|
||||
let client = self.connection().clone();
|
||||
spawn(async move {
|
||||
let mut client = client.lock().await;
|
||||
|
||||
let (workspace_tx, workspace_rx) = channel(16);
|
||||
let name = client
|
||||
.get_workspaces()
|
||||
.await?
|
||||
.into_iter()
|
||||
.find(|w| w.id == id)
|
||||
.map(|w| w.name);
|
||||
|
||||
{
|
||||
// create 2nd client as subscription takes ownership
|
||||
let client = Connection::new().await?;
|
||||
let workspace_tx = workspace_tx.clone();
|
||||
let Some(name) = name else {
|
||||
return Err(Report::msg(format!("couldn't find workspace with id {id}")));
|
||||
};
|
||||
|
||||
spawn(async move {
|
||||
let event_types = [EventType::Workspace];
|
||||
let mut events = client.subscribe(event_types).await?;
|
||||
if let Err(e) = client.run_command(format!("workspace {name}")).await {
|
||||
return Err(Report::msg(format!(
|
||||
"Couldn't focus workspace '{id}': {e:#}"
|
||||
)));
|
||||
}
|
||||
|
||||
while let Some(event) = events.next().await {
|
||||
trace!("event: {:?}", event);
|
||||
if let Event::Workspace(event) = event? {
|
||||
let event = WorkspaceUpdate::from(*event);
|
||||
if !matches!(event, WorkspaceUpdate::Unknown) {
|
||||
workspace_tx.send(event)?;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Ok::<(), Report>(())
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
client,
|
||||
workspace_tx,
|
||||
_workspace_rx: workspace_rx,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl WorkspaceClient for Client {
|
||||
fn focus(&self, id: String) -> Result<()> {
|
||||
await_sync(async move {
|
||||
let mut client = self.client.lock().await;
|
||||
client.run_command(format!("workspace {id}")).await
|
||||
})?;
|
||||
Ok(())
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
fn subscribe_workspace_change(&self) -> Receiver<WorkspaceUpdate> {
|
||||
let rx = self.workspace_tx.subscribe();
|
||||
fn subscribe(&self) -> Receiver<WorkspaceUpdate> {
|
||||
let (tx, rx) = channel(16);
|
||||
|
||||
{
|
||||
let tx = self.workspace_tx.clone();
|
||||
let client = self.client.clone();
|
||||
let client = self.connection().clone();
|
||||
|
||||
await_sync(async {
|
||||
let mut client = client.lock().await;
|
||||
let workspaces = client.get_workspaces().await.expect("to get workspaces");
|
||||
// TODO: this needs refactoring
|
||||
await_sync(async {
|
||||
let mut client = client.lock().await;
|
||||
let workspaces = client.get_workspaces().await.expect("to get workspaces");
|
||||
|
||||
let event =
|
||||
WorkspaceUpdate::Init(workspaces.into_iter().map(Workspace::from).collect());
|
||||
let event =
|
||||
WorkspaceUpdate::Init(workspaces.into_iter().map(Workspace::from).collect());
|
||||
|
||||
send!(tx, event);
|
||||
});
|
||||
}
|
||||
tx.send_expect(event);
|
||||
|
||||
drop(client);
|
||||
|
||||
self.add_listener::<WorkspaceEvent>(move |event| {
|
||||
let update = WorkspaceUpdate::from(event.clone());
|
||||
tx.send_expect(update);
|
||||
})
|
||||
.await
|
||||
.expect("to add listener");
|
||||
});
|
||||
|
||||
rx
|
||||
}
|
||||
|
@ -135,6 +116,7 @@ impl From<&swayipc_async::Workspace> for Visibility {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "workspaces")]
|
||||
impl From<WorkspaceEvent> for WorkspaceUpdate {
|
||||
fn from(event: WorkspaceEvent) -> Self {
|
||||
match event.change {
|
||||
|
@ -151,7 +133,136 @@ impl From<WorkspaceEvent> for WorkspaceUpdate {
|
|||
WorkspaceChange::Move => {
|
||||
Self::Move(event.current.expect("Missing current workspace").into())
|
||||
}
|
||||
WorkspaceChange::Rename => {
|
||||
if let Some(node) = event.current {
|
||||
Self::Rename {
|
||||
id: node.id,
|
||||
name: node.name.unwrap_or_default(),
|
||||
}
|
||||
} else {
|
||||
Self::Unknown
|
||||
}
|
||||
}
|
||||
WorkspaceChange::Urgent => {
|
||||
if let Some(node) = event.current {
|
||||
Self::Urgent {
|
||||
id: node.id,
|
||||
urgent: node.urgent,
|
||||
}
|
||||
} else {
|
||||
Self::Unknown
|
||||
}
|
||||
}
|
||||
_ => Self::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "keyboard+sway")]
|
||||
use super::{KeyboardLayoutClient, KeyboardLayoutUpdate};
|
||||
|
||||
#[cfg(feature = "keyboard+sway")]
|
||||
impl KeyboardLayoutClient for Client {
|
||||
fn set_next_active(&self) {
|
||||
let client = self.connection().clone();
|
||||
spawn(async move {
|
||||
let mut client = client.lock().await;
|
||||
|
||||
let inputs = client.get_inputs().await.expect("to get inputs");
|
||||
|
||||
if let Some(keyboard) = inputs
|
||||
.into_iter()
|
||||
.find(|i| i.xkb_active_layout_name.is_some())
|
||||
{
|
||||
if let Err(e) = client
|
||||
.run_command(format!(
|
||||
"input {} xkb_switch_layout next",
|
||||
keyboard.identifier
|
||||
))
|
||||
.await
|
||||
{
|
||||
error!("Failed to switch keyboard layout due to Sway error: {e}");
|
||||
}
|
||||
} else {
|
||||
error!("Failed to get keyboard identifier from Sway");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn subscribe(&self) -> Receiver<KeyboardLayoutUpdate> {
|
||||
let (tx, rx) = channel(16);
|
||||
|
||||
let client = self.connection().clone();
|
||||
|
||||
await_sync(async {
|
||||
let mut client = client.lock().await;
|
||||
let inputs = client.get_inputs().await.expect("to get inputs");
|
||||
|
||||
if let Some(layout) = inputs.into_iter().find_map(|i| i.xkb_active_layout_name) {
|
||||
tx.send_expect(KeyboardLayoutUpdate(layout));
|
||||
} else {
|
||||
error!("Failed to get keyboard layout from Sway!");
|
||||
}
|
||||
|
||||
drop(client);
|
||||
|
||||
self.add_listener::<InputEvent>(move |event| {
|
||||
if let Ok(layout) = KeyboardLayoutUpdate::try_from(event.clone()) {
|
||||
tx.send_expect(layout);
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("to add listener");
|
||||
});
|
||||
|
||||
rx
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "keyboard+sway")]
|
||||
impl TryFrom<InputEvent> for KeyboardLayoutUpdate {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: InputEvent) -> Result<Self, Self::Error> {
|
||||
match value.change {
|
||||
InputChange::XkbLayout => {
|
||||
if let Some(layout) = value.input.xkb_active_layout_name {
|
||||
Ok(KeyboardLayoutUpdate(layout))
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "bindmode+sway")]
|
||||
use super::{BindModeClient, BindModeUpdate};
|
||||
|
||||
#[cfg(feature = "bindmode+sway")]
|
||||
impl BindModeClient for Client {
|
||||
fn subscribe(&self) -> Result<Receiver<BindModeUpdate>, Report> {
|
||||
let (tx, rx) = channel(16);
|
||||
await_sync(async {
|
||||
self.add_listener::<swayipc_async::ModeEvent>(move |mode| {
|
||||
tracing::trace!("mode: {:?}", mode);
|
||||
|
||||
// when no binding is active the bindmode is named "default", but we must display
|
||||
// nothing in this case.
|
||||
let name = if mode.change == "default" {
|
||||
String::new()
|
||||
} else {
|
||||
mode.change.clone()
|
||||
};
|
||||
|
||||
tx.send_expect(BindModeUpdate {
|
||||
name,
|
||||
pango_markup: mode.pango_markup,
|
||||
});
|
||||
})
|
||||
.await
|
||||
})?;
|
||||
Ok(rx)
|
||||
}
|
||||
}
|
||||
|
|
236
src/clients/libinput.rs
Normal file
236
src/clients/libinput.rs
Normal file
|
@ -0,0 +1,236 @@
|
|||
use crate::channels::SyncSenderExt;
|
||||
use crate::{Ironbar, arc_rw, read_lock, spawn, write_lock};
|
||||
use color_eyre::{Report, Result};
|
||||
use colpetto::event::{AsRawEvent, DeviceEvent, KeyState, KeyboardEvent};
|
||||
use colpetto::{DeviceCapability, Libinput};
|
||||
use evdev_rs::DeviceWrapper;
|
||||
use evdev_rs::enums::{EV_KEY, EV_LED, EventCode, int_to_ev_key};
|
||||
use futures_lite::StreamExt;
|
||||
use rustix::fs::{Mode, OFlags, open};
|
||||
use rustix::io::Errno;
|
||||
use std::ffi::{CStr, CString, c_int};
|
||||
use std::os::fd::{FromRawFd, IntoRawFd, RawFd};
|
||||
use std::os::unix::io::OwnedFd;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::time::Duration;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::task::LocalSet;
|
||||
use tokio::time::sleep;
|
||||
use tracing::{debug, error};
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum Key {
|
||||
Caps,
|
||||
Num,
|
||||
Scroll,
|
||||
}
|
||||
|
||||
impl From<Key> for EV_KEY {
|
||||
fn from(value: Key) -> Self {
|
||||
match value {
|
||||
Key::Caps => Self::KEY_CAPSLOCK,
|
||||
Key::Num => Self::KEY_NUMLOCK,
|
||||
Key::Scroll => Self::KEY_SCROLLLOCK,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<EV_KEY> for Key {
|
||||
type Error = Report;
|
||||
|
||||
fn try_from(value: EV_KEY) -> std::result::Result<Self, Self::Error> {
|
||||
match value {
|
||||
EV_KEY::KEY_CAPSLOCK => Ok(Key::Caps),
|
||||
EV_KEY::KEY_NUMLOCK => Ok(Key::Num),
|
||||
EV_KEY::KEY_SCROLLLOCK => Ok(Key::Scroll),
|
||||
_ => Err(Report::msg("provided key is not supported toggle key")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Key {
|
||||
fn get_state<P: AsRef<Path>>(self, device_path: P) -> Result<bool> {
|
||||
let device = evdev_rs::Device::new_from_path(device_path)?;
|
||||
|
||||
match self {
|
||||
Self::Caps => device.event_value(&EventCode::EV_LED(EV_LED::LED_CAPSL)),
|
||||
Self::Num => device.event_value(&EventCode::EV_LED(EV_LED::LED_NUML)),
|
||||
Self::Scroll => device.event_value(&EventCode::EV_LED(EV_LED::LED_SCROLLL)),
|
||||
}
|
||||
.map(|v| v > 0)
|
||||
.ok_or_else(|| Report::msg("failed to get key status"))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct KeyEvent {
|
||||
pub key: Key,
|
||||
pub state: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum Event {
|
||||
Device,
|
||||
Key(KeyEvent),
|
||||
}
|
||||
|
||||
struct KeyData<P: AsRef<Path>> {
|
||||
device_path: P,
|
||||
key: EV_KEY,
|
||||
}
|
||||
|
||||
impl<P: AsRef<Path>> TryFrom<KeyData<P>> for Event {
|
||||
type Error = Report;
|
||||
|
||||
fn try_from(data: KeyData<P>) -> Result<Self> {
|
||||
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<Event>,
|
||||
_rx: broadcast::Receiver<Event>,
|
||||
|
||||
seat: String,
|
||||
known_devices: Arc<RwLock<Vec<PathBuf>>>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn init(seat: String) -> Arc<Self> {
|
||||
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<RawFd, i32> {
|
||||
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<Event> = 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<Event> {
|
||||
self.tx.subscribe()
|
||||
}
|
||||
}
|
|
@ -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<Arc<wayland::Client>>,
|
||||
#[cfg(feature = "workspaces")]
|
||||
workspaces: Option<Arc<dyn compositor::WorkspaceClient>>,
|
||||
#[cfg(feature = "sway")]
|
||||
sway: Option<Arc<sway::Client>>,
|
||||
#[cfg(feature = "hyprland")]
|
||||
hyprland: Option<Arc<compositor::hyprland::Client>>,
|
||||
#[cfg(feature = "bindmode")]
|
||||
bindmode: Option<Arc<dyn compositor::BindModeClient>>,
|
||||
#[cfg(feature = "clipboard")]
|
||||
clipboard: Option<Arc<clipboard::Client>>,
|
||||
#[cfg(feature = "keyboard")]
|
||||
libinput: HashMap<Box<str>, Arc<libinput::Client>>,
|
||||
#[cfg(feature = "keyboard")]
|
||||
keyboard_layout: Option<Arc<dyn compositor::KeyboardLayoutClient>>,
|
||||
#[cfg(feature = "cairo")]
|
||||
lua: Option<Rc<lua::LuaEngine>>,
|
||||
#[cfg(feature = "music")]
|
||||
music: std::collections::HashMap<music::ClientType, Arc<dyn music::MusicClient>>,
|
||||
music: HashMap<music::ClientType, Arc<dyn music::MusicClient>>,
|
||||
#[cfg(feature = "network_manager")]
|
||||
network_manager: Option<Arc<networkmanager::Client>>,
|
||||
#[cfg(feature = "notifications")]
|
||||
notifications: Option<Arc<swaync::Client>>,
|
||||
#[cfg(feature = "sys_info")]
|
||||
sys_info: Option<Arc<sysinfo::Client>>,
|
||||
#[cfg(feature = "tray")]
|
||||
tray: Option<Arc<tray::Client>>,
|
||||
#[cfg(feature = "upower")]
|
||||
|
@ -73,18 +97,68 @@ impl Clients {
|
|||
|
||||
#[cfg(feature = "workspaces")]
|
||||
pub fn workspaces(&mut self) -> ClientResult<dyn compositor::WorkspaceClient> {
|
||||
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<dyn compositor::KeyboardLayoutClient> {
|
||||
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<dyn compositor::BindModeClient> {
|
||||
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<sway::Client> {
|
||||
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<compositor::hyprland::Client> {
|
||||
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<lua::LuaEngine> {
|
||||
self.lua
|
||||
|
@ -92,6 +166,17 @@ impl Clients {
|
|||
.clone()
|
||||
}
|
||||
|
||||
#[cfg(feature = "keyboard")]
|
||||
pub fn libinput(&mut self, seat: &str) -> Arc<libinput::Client> {
|
||||
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<dyn music::MusicClient> {
|
||||
self.music
|
||||
|
@ -102,55 +187,68 @@ impl Clients {
|
|||
|
||||
#[cfg(feature = "network_manager")]
|
||||
pub fn network_manager(&mut self) -> ClientResult<networkmanager::Client> {
|
||||
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<swaync::Client> {
|
||||
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<sysinfo::Client> {
|
||||
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<tray::Client> {
|
||||
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<zbus::fdo::PropertiesProxy<'static>> {
|
||||
self.upower
|
||||
.get_or_insert_with(|| {
|
||||
crate::await_sync(async { upower::create_display_proxy().await })
|
||||
})
|
||||
.clone()
|
||||
pub fn upower(&mut self) -> ClientResult<zbus::fdo::PropertiesProxy<'static>> {
|
||||
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")]
|
||||
|
|
|
@ -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<dyn MusicClient> {
|
||||
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()),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
181
src/clients/sway.rs
Normal file
181
src/clients/sway.rs
Normal file
|
@ -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<T> = dyn Fn(&T) + Sync + Send;
|
||||
|
||||
struct TaskState {
|
||||
join_handle: Option<tokio::task::JoinHandle<Result<()>>>,
|
||||
// could have been a `HashMap<EventType, Vec<Box<dyn Fn(&Event) + Sync + Send>>>`, but we don't
|
||||
// expect enough listeners to justify the constant overhead of a hashmap.
|
||||
listeners: Arc<Vec<(EventType, Box<SyncFn<Event>>)>>,
|
||||
}
|
||||
|
||||
pub struct Client {
|
||||
connection: Arc<Mutex<Connection>>,
|
||||
task_state: Mutex<TaskState>,
|
||||
}
|
||||
|
||||
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<Self> {
|
||||
// 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<Mutex<Connection>> {
|
||||
&self.connection
|
||||
}
|
||||
|
||||
pub async fn add_listener<T: SwayIpcEvent>(
|
||||
&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<SyncFn<Event>>,
|
||||
) -> 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::<Vec<_>>();
|
||||
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);
|
|
@ -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<bool>;
|
||||
|
||||
|
@ -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<bool>;
|
||||
#[dbus_proxy(property)]
|
||||
#[zbus(property)]
|
||||
fn set_inhibited(&self, value: bool) -> zbus::Result<()>;
|
||||
}
|
||||
|
|
|
@ -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::<Event>().expect("to deserialize");
|
||||
let ev = ev
|
||||
.message()
|
||||
.body()
|
||||
.deserialize::<Event>()
|
||||
.expect("to deserialize");
|
||||
debug!("Received event: {ev:?}");
|
||||
send!(tx, ev);
|
||||
tx.send_expect(ev);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
621
src/clients/sysinfo.rs
Normal file
621
src/clients/sysinfo.rs
Normal file
|
@ -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<Self, Self::Err> {
|
||||
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<Box<str>, Value>,
|
||||
}
|
||||
|
||||
impl FromIterator<(Box<str>, Value)> for ValueSet {
|
||||
fn from_iter<T: IntoIterator<Item = (Box<str>, Value)>>(iter: T) -> Self {
|
||||
Self {
|
||||
values: iter.into_iter().collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ValueSet {
|
||||
fn values(&self, prefix: Prefix) -> impl Iterator<Item = f64> + 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<System>,
|
||||
disks: Mutex<Disks>,
|
||||
components: Mutex<Components>,
|
||||
networks: Mutex<Networks>,
|
||||
load_average: Mutex<LoadAvg>,
|
||||
}
|
||||
|
||||
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<Self> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
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<Arc<dyn Namespace + Sync + Send>> {
|
||||
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<String> {
|
||||
let function = Function::from_str(key).ok()?;
|
||||
Some(self.apply(&function, Prefix::None).to_string())
|
||||
}
|
||||
|
||||
fn list(&self) -> Vec<String> {
|
||||
let mut vec = vec!["sum", "min", "max", "mean"]
|
||||
.into_iter()
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
vec.extend(self.values.keys().map(ToString::to_string));
|
||||
vec
|
||||
}
|
||||
|
||||
fn namespaces(&self) -> Vec<String> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn get_namespace(&self, _key: &str) -> Option<Arc<dyn Namespace + Sync + Send>> {
|
||||
None
|
||||
}
|
||||
}
|
|
@ -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<PropertiesProxy<'static>> {
|
||||
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);
|
159
src/clients/upower/dbus.rs
Normal file
159
src/clients/upower/dbus.rs
Normal file
|
@ -0,0 +1,159 @@
|
|||
/// Originally taken from `upower-dbus` crate
|
||||
/// <https://github.com/pop-os/upower-dbus/blob/main/LICENSE>
|
||||
// Copyright 2021 System76 <info@system76.com>
|
||||
// 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<BatteryLevel>;
|
||||
|
||||
#[zbus(property)]
|
||||
fn capacity(&self) -> zbus::Result<f64>;
|
||||
|
||||
#[zbus(property)]
|
||||
fn energy(&self) -> zbus::Result<f64>;
|
||||
|
||||
#[zbus(property)]
|
||||
fn energy_empty(&self) -> zbus::Result<f64>;
|
||||
|
||||
#[zbus(property)]
|
||||
fn energy_full(&self) -> zbus::Result<f64>;
|
||||
|
||||
#[zbus(property)]
|
||||
fn energy_full_design(&self) -> zbus::Result<f64>;
|
||||
|
||||
#[zbus(property)]
|
||||
fn has_history(&self) -> zbus::Result<bool>;
|
||||
|
||||
#[zbus(property)]
|
||||
fn has_statistics(&self) -> zbus::Result<bool>;
|
||||
|
||||
#[zbus(property)]
|
||||
fn icon_name(&self) -> zbus::Result<String>;
|
||||
|
||||
#[zbus(property)]
|
||||
fn is_present(&self) -> zbus::Result<bool>;
|
||||
|
||||
#[zbus(property)]
|
||||
fn is_rechargeable(&self) -> zbus::Result<bool>;
|
||||
|
||||
#[zbus(property)]
|
||||
fn luminosity(&self) -> zbus::Result<f64>;
|
||||
|
||||
#[zbus(property)]
|
||||
fn model(&self) -> zbus::Result<String>;
|
||||
|
||||
#[zbus(property)]
|
||||
fn native_path(&self) -> zbus::Result<String>;
|
||||
|
||||
#[zbus(property)]
|
||||
fn online(&self) -> zbus::Result<bool>;
|
||||
|
||||
#[zbus(property)]
|
||||
fn percentage(&self) -> zbus::Result<f64>;
|
||||
|
||||
#[zbus(property)]
|
||||
fn power_supply(&self) -> zbus::Result<bool>;
|
||||
|
||||
fn refresh(&self) -> zbus::Result<()>;
|
||||
|
||||
#[zbus(property)]
|
||||
fn serial(&self) -> zbus::Result<String>;
|
||||
|
||||
#[zbus(property)]
|
||||
fn state(&self) -> zbus::Result<BatteryState>;
|
||||
|
||||
#[zbus(property)]
|
||||
fn temperature(&self) -> zbus::Result<f64>;
|
||||
|
||||
#[zbus(property, name = "Type")]
|
||||
fn type_(&self) -> zbus::Result<BatteryType>;
|
||||
|
||||
#[zbus(property)]
|
||||
fn vendor(&self) -> zbus::Result<String>;
|
||||
|
||||
#[zbus(property)]
|
||||
fn voltage(&self) -> zbus::Result<f64>;
|
||||
}
|
||||
|
||||
#[proxy(interface = "org.freedesktop.UPower", assume_defaults = true)]
|
||||
pub trait UPower {
|
||||
/// EnumerateDevices method
|
||||
fn enumerate_devices(&self) -> zbus::Result<Vec<zbus::zvariant::OwnedObjectPath>>;
|
||||
|
||||
/// GetCriticalAction method
|
||||
fn get_critical_action(&self) -> zbus::Result<String>;
|
||||
|
||||
/// 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<String>;
|
||||
|
||||
/// LidIsClosed property
|
||||
#[zbus(property)]
|
||||
fn lid_is_closed(&self) -> zbus::Result<bool>;
|
||||
|
||||
/// LidIsPresent property
|
||||
#[zbus(property)]
|
||||
fn lid_is_present(&self) -> zbus::Result<bool>;
|
||||
|
||||
/// OnBattery property
|
||||
#[zbus(property)]
|
||||
fn on_battery(&self) -> zbus::Result<bool>;
|
||||
}
|
33
src/clients/upower/mod.rs
Normal file
33
src/clients/upower/mod.rs
Normal file
|
@ -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<PropertiesProxy<'static>> {
|
||||
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);
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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<Mutex<Vec<Sink>>> {
|
||||
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<Sink>, 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<Sink>, tx: &broadcast::Sender<Event>) {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Mutex<Vec<SinkInput>>> {
|
||||
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<SinkInput>, tx: &broadcast::Sender<Event>) {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Self>,
|
||||
loop_handle: LoopHandle<'static, Self>,
|
||||
|
||||
event_tx: mpsc::Sender<Event>,
|
||||
response_tx: std::sync::mpsc::Sender<Response>,
|
||||
|
@ -204,14 +205,12 @@ pub struct Environment {
|
|||
|
||||
// -- clipboard --
|
||||
#[cfg(feature = "clipboard")]
|
||||
data_control_device_manager_state: DataControlDeviceManagerState,
|
||||
data_control_device_manager_state: Option<DataControlDeviceManagerState>,
|
||||
|
||||
#[cfg(feature = "clipboard")]
|
||||
data_control_devices: Vec<DataControlDeviceEntry>,
|
||||
#[cfg(feature = "clipboard")]
|
||||
copy_paste_sources: Vec<CopyPasteSource>,
|
||||
#[cfg(feature = "clipboard")]
|
||||
selection_offers: Vec<SelectionOfferItem>,
|
||||
|
||||
// local state
|
||||
#[cfg(feature = "clipboard")]
|
||||
|
@ -265,12 +264,30 @@ impl Environment {
|
|||
let output_state = OutputState::new(&globals, &qh);
|
||||
let seat_state = SeatState::new(&globals, &qh);
|
||||
#[cfg(any(feature = "focused", feature = "launcher"))]
|
||||
ToplevelManagerState::bind(&globals, &qh)
|
||||
.expect("to bind to wlr_foreign_toplevel_manager global");
|
||||
if let Err(err) = ToplevelManagerState::bind(&globals, &qh) {
|
||||
error!("{:?}",
|
||||
Report::new(err)
|
||||
.wrap_err("Failed to bind to wlr_foreign_toplevel_manager global")
|
||||
.note("This is likely a due to the current compositor not supporting the required protocol")
|
||||
.note("launcher and focused modules will not work")
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "clipboard")]
|
||||
let data_control_device_manager_state = DataControlDeviceManagerState::bind(&globals, &qh)
|
||||
.expect("to bind to wlr_data_control_device_manager global");
|
||||
let data_control_device_manager_state = match DataControlDeviceManagerState::bind(
|
||||
&globals, &qh,
|
||||
) {
|
||||
Ok(state) => Some(state),
|
||||
Err(err) => {
|
||||
error!("{:?}",
|
||||
Report::new(err)
|
||||
.wrap_err("Failed to bind to wlr_data_control_device global")
|
||||
.note("This is likely a due to the current compositor not supporting the required protocol")
|
||||
.note("clipboard module will not work")
|
||||
);
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let mut env = Self {
|
||||
registry_state,
|
||||
|
@ -279,7 +296,6 @@ impl Environment {
|
|||
#[cfg(feature = "clipboard")]
|
||||
data_control_device_manager_state,
|
||||
queue_handle: qh,
|
||||
loop_handle: loop_handle.clone(),
|
||||
event_tx,
|
||||
response_tx,
|
||||
#[cfg(any(feature = "focused", feature = "launcher"))]
|
||||
|
@ -290,8 +306,6 @@ impl Environment {
|
|||
#[cfg(feature = "clipboard")]
|
||||
copy_paste_sources: vec![],
|
||||
#[cfg(feature = "clipboard")]
|
||||
selection_offers: vec![],
|
||||
#[cfg(feature = "clipboard")]
|
||||
clipboard: arc_mut!(None),
|
||||
};
|
||||
|
||||
|
@ -320,12 +334,12 @@ impl Environment {
|
|||
match event {
|
||||
Msg(Request::Roundtrip) => {
|
||||
debug!("received roundtrip request");
|
||||
send!(env.response_tx, Response::Ok);
|
||||
env.response_tx.send_expect(Response::Ok);
|
||||
}
|
||||
#[cfg(feature = "ipc")]
|
||||
Msg(Request::OutputInfoAll) => {
|
||||
let infos = env.output_info_all();
|
||||
send!(env.response_tx, Response::OutputInfoAll(infos));
|
||||
env.response_tx.send_expect(Response::OutputInfoAll(infos));
|
||||
}
|
||||
#[cfg(any(feature = "focused", feature = "launcher"))]
|
||||
Msg(Request::ToplevelInfoAll) => {
|
||||
|
@ -334,31 +348,46 @@ impl Environment {
|
|||
.iter()
|
||||
.filter_map(ToplevelHandle::info)
|
||||
.collect();
|
||||
send!(env.response_tx, Response::ToplevelInfoAll(infos));
|
||||
|
||||
env.response_tx
|
||||
.send_expect(Response::ToplevelInfoAll(infos));
|
||||
}
|
||||
#[cfg(feature = "launcher")]
|
||||
Msg(Request::ToplevelFocus(id)) => {
|
||||
let handle = env
|
||||
.handles
|
||||
.iter()
|
||||
.find(|handle| handle.info().map_or(false, |info| info.id == id));
|
||||
.find(|handle| handle.info().is_some_and(|info| info.id == id));
|
||||
|
||||
if let Some(handle) = handle {
|
||||
let seat = env.default_seat();
|
||||
handle.focus(&seat);
|
||||
}
|
||||
|
||||
send!(env.response_tx, Response::Ok);
|
||||
env.response_tx.send_expect(Response::Ok);
|
||||
}
|
||||
#[cfg(feature = "launcher")]
|
||||
Msg(Request::ToplevelMinimize(id)) => {
|
||||
let handle = env
|
||||
.handles
|
||||
.iter()
|
||||
.find(|handle| handle.info().is_some_and(|info| info.id == id));
|
||||
|
||||
if let Some(handle) = handle {
|
||||
handle.minimize();
|
||||
}
|
||||
|
||||
env.response_tx.send_expect(Response::Ok);
|
||||
}
|
||||
#[cfg(feature = "clipboard")]
|
||||
Msg(Request::CopyToClipboard(item)) => {
|
||||
env.copy_to_clipboard(item);
|
||||
send!(env.response_tx, Response::Ok);
|
||||
env.response_tx.send_expect(Response::Ok);
|
||||
}
|
||||
#[cfg(feature = "clipboard")]
|
||||
Msg(Request::ClipboardItem) => {
|
||||
let item = lock!(env.clipboard).clone();
|
||||
send!(env.response_tx, Response::ClipboardItem(item));
|
||||
env.response_tx.send_expect(Response::ClipboardItem(item));
|
||||
}
|
||||
calloop_channel::Event::Closed => error!("request channel unexpectedly closed"),
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use super::{Client, Environment, Event};
|
||||
use crate::try_send;
|
||||
use crate::channels::AsyncSenderExt;
|
||||
use smithay_client_toolkit::output::{OutputHandler, OutputInfo, OutputState};
|
||||
use tokio::sync::broadcast;
|
||||
use tracing::{debug, error};
|
||||
|
@ -12,7 +12,7 @@ pub struct OutputEvent {
|
|||
pub event_type: OutputEventType,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum OutputEventType {
|
||||
New,
|
||||
Update,
|
||||
|
@ -63,13 +63,10 @@ impl OutputHandler for Environment {
|
|||
fn new_output(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, output: WlOutput) {
|
||||
debug!("Handler received new output");
|
||||
if let Some(info) = self.output_state.info(&output) {
|
||||
try_send!(
|
||||
self.event_tx,
|
||||
Event::Output(OutputEvent {
|
||||
output: info,
|
||||
event_type: OutputEventType::New
|
||||
})
|
||||
);
|
||||
self.event_tx.send_spawn(Event::Output(OutputEvent {
|
||||
output: info,
|
||||
event_type: OutputEventType::New,
|
||||
}));
|
||||
} else {
|
||||
error!("Output is missing information!");
|
||||
}
|
||||
|
@ -78,13 +75,10 @@ impl OutputHandler for Environment {
|
|||
fn update_output(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, output: WlOutput) {
|
||||
debug!("Handle received output update");
|
||||
if let Some(info) = self.output_state.info(&output) {
|
||||
try_send!(
|
||||
self.event_tx,
|
||||
Event::Output(OutputEvent {
|
||||
output: info,
|
||||
event_type: OutputEventType::Update
|
||||
})
|
||||
);
|
||||
self.event_tx.send_spawn(Event::Output(OutputEvent {
|
||||
output: info,
|
||||
event_type: OutputEventType::Update,
|
||||
}));
|
||||
} else {
|
||||
error!("Output is missing information!");
|
||||
}
|
||||
|
@ -93,13 +87,10 @@ impl OutputHandler for Environment {
|
|||
fn output_destroyed(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, output: WlOutput) {
|
||||
debug!("Handle received output destruction");
|
||||
if let Some(info) = self.output_state.info(&output) {
|
||||
try_send!(
|
||||
self.event_tx,
|
||||
Event::Output(OutputEvent {
|
||||
output: info,
|
||||
event_type: OutputEventType::Destroyed
|
||||
})
|
||||
);
|
||||
self.event_tx.send_spawn(Event::Output(OutputEvent {
|
||||
output: info,
|
||||
event_type: OutputEventType::Destroyed,
|
||||
}));
|
||||
} else {
|
||||
error!("Output is missing information!");
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use super::Environment;
|
||||
use smithay_client_toolkit::seat::{Capability, SeatHandler, SeatState};
|
||||
use tracing::debug;
|
||||
use tracing::{debug, error};
|
||||
use wayland_client::protocol::wl_seat::WlSeat;
|
||||
use wayland_client::{Connection, QueueHandle};
|
||||
|
||||
|
@ -37,7 +37,11 @@ impl SeatHandler for Environment {
|
|||
{
|
||||
debug!("Adding new data control device");
|
||||
// create the data device here for this seat
|
||||
let data_control_device_manager = &self.data_control_device_manager_state;
|
||||
let Some(data_control_device_manager) = &self.data_control_device_manager_state else {
|
||||
error!("data_control_device_manager not available, cannot copy");
|
||||
return;
|
||||
};
|
||||
|
||||
let data_control_device = data_control_device_manager.get_data_device(qh, &seat);
|
||||
self.data_control_devices
|
||||
.push(super::DataControlDeviceEntry {
|
||||
|
|
|
@ -6,7 +6,7 @@ use crate::error::ERR_WAYLAND_DATA;
|
|||
use crate::lock;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tracing::warn;
|
||||
use wayland_client::{event_created_child, Connection, Dispatch, Proxy, QueueHandle};
|
||||
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle, event_created_child};
|
||||
use wayland_protocols_wlr::data_control::v1::client::{
|
||||
zwlr_data_control_device_v1::{Event, ZwlrDataControlDeviceV1},
|
||||
zwlr_data_control_offer_v1::ZwlrDataControlOfferV1,
|
||||
|
@ -37,7 +37,9 @@ pub trait DataControlDeviceDataExt: Send + Sync {
|
|||
|
||||
fn selection_mime_types(&self) -> Vec<String> {
|
||||
let inner = self.data_control_device_data();
|
||||
lock!(lock!(inner.inner).selection_offer)
|
||||
let offer = &lock!(inner.inner).selection_offer;
|
||||
|
||||
lock!(offer)
|
||||
.as_ref()
|
||||
.map(|offer| {
|
||||
let data = offer
|
||||
|
@ -51,14 +53,14 @@ pub trait DataControlDeviceDataExt: Send + Sync {
|
|||
/// Get the active selection offer if it exists.
|
||||
fn selection_offer(&self) -> Option<SelectionOffer> {
|
||||
let inner = self.data_control_device_data();
|
||||
lock!(lock!(inner.inner).selection_offer)
|
||||
.as_ref()
|
||||
.and_then(|offer| {
|
||||
let data = offer
|
||||
.data::<Self::DataControlOfferInner>()
|
||||
.expect(ERR_WAYLAND_DATA);
|
||||
data.as_selection_offer()
|
||||
})
|
||||
let offer = &lock!(inner.inner).selection_offer;
|
||||
|
||||
lock!(offer).as_ref().and_then(|offer| {
|
||||
let data = offer
|
||||
.data::<Self::DataControlOfferInner>()
|
||||
.expect(ERR_WAYLAND_DATA);
|
||||
data.as_selection_offer()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -159,7 +161,9 @@ where
|
|||
}
|
||||
}
|
||||
Event::Finished => {
|
||||
warn!("Data control offer is no longer valid, but has not been dropped by client. This could cause clipboard issues.");
|
||||
warn!(
|
||||
"Data control offer is no longer valid, but has not been dropped by client. This could cause clipboard issues."
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
|
@ -4,23 +4,29 @@ pub mod offer;
|
|||
pub mod source;
|
||||
|
||||
use self::device::{DataControlDeviceDataExt, DataControlDeviceHandler};
|
||||
use self::offer::{DataControlDeviceOffer, DataControlOfferHandler, SelectionOffer};
|
||||
use self::offer::{DataControlDeviceOffer, DataControlOfferHandler};
|
||||
use self::source::DataControlSourceHandler;
|
||||
use super::{Client, Environment, Event, Request, Response};
|
||||
use crate::{lock, try_send, Ironbar};
|
||||
use crate::channels::AsyncSenderExt;
|
||||
use crate::{Ironbar, lock, spawn};
|
||||
use color_eyre::Result;
|
||||
use device::DataControlDevice;
|
||||
use glib::Bytes;
|
||||
use nix::fcntl::{fcntl, F_GETPIPE_SZ, F_SETPIPE_SZ};
|
||||
use nix::sys::epoll::{Epoll, EpollCreateFlags, EpollEvent, EpollFlags, EpollTimeout};
|
||||
use rustix::buffer::spare_capacity;
|
||||
use rustix::event::epoll;
|
||||
use rustix::event::epoll::CreateFlags;
|
||||
use rustix::fs::Timespec;
|
||||
use rustix::pipe::{fcntl_getpipe_size, fcntl_setpipe_size};
|
||||
use smithay_client_toolkit::data_device_manager::WritePipe;
|
||||
use smithay_client_toolkit::reexports::calloop::{PostAction, RegistrationToken};
|
||||
use std::cmp::min;
|
||||
use std::fmt::{Debug, Formatter};
|
||||
use std::fs::File;
|
||||
use std::io::{ErrorKind, Read, Write};
|
||||
use std::os::fd::{AsRawFd, OwnedFd, RawFd};
|
||||
use std::io::Write;
|
||||
use std::os::fd::{AsFd, BorrowedFd, OwnedFd};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::{fs, io};
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::sync::broadcast;
|
||||
use tracing::{debug, error, trace};
|
||||
use wayland_client::{Connection, QueueHandle};
|
||||
|
@ -28,12 +34,6 @@ use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_source_v1
|
|||
|
||||
const INTERNAL_MIME_TYPE: &str = "x-ironbar-internal";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SelectionOfferItem {
|
||||
offer: SelectionOffer,
|
||||
token: Option<RegistrationToken>,
|
||||
}
|
||||
|
||||
/// Represents a value which can be read/written
|
||||
/// to/from the system clipboard and surrounding metadata.
|
||||
///
|
||||
|
@ -148,6 +148,11 @@ impl Environment {
|
|||
pub fn copy_to_clipboard(&mut self, item: ClipboardItem) {
|
||||
debug!("Copying item to clipboard: {item:?}");
|
||||
|
||||
let Some(data_control_device_manager) = &self.data_control_device_manager_state else {
|
||||
error!("data_control_device_manager not available, cannot copy");
|
||||
return;
|
||||
};
|
||||
|
||||
let seat = self.default_seat();
|
||||
let Some(device) = self
|
||||
.data_control_devices
|
||||
|
@ -157,9 +162,8 @@ impl Environment {
|
|||
return;
|
||||
};
|
||||
|
||||
let source = self
|
||||
.data_control_device_manager_state
|
||||
.create_copy_paste_source(&self.queue_handle, [INTERNAL_MIME_TYPE, &item.mime_type]);
|
||||
let source = data_control_device_manager
|
||||
.create_copy_paste_source(&self.queue_handle, [&item.mime_type, INTERNAL_MIME_TYPE]);
|
||||
|
||||
source.set_selection(&device.device);
|
||||
self.copy_paste_sources.push(source);
|
||||
|
@ -168,22 +172,20 @@ impl Environment {
|
|||
}
|
||||
|
||||
/// Reads an offer file handle into a new `ClipboardItem`.
|
||||
fn read_file(mime_type: &MimeType, file: &mut File) -> io::Result<ClipboardItem> {
|
||||
async fn read_file(
|
||||
mime_type: &MimeType,
|
||||
file: &mut tokio::net::unix::pipe::Receiver,
|
||||
) -> io::Result<ClipboardItem> {
|
||||
let mut buf = vec![];
|
||||
file.read_to_end(&mut buf).await?;
|
||||
|
||||
let value = match mime_type.category {
|
||||
MimeTypeCategory::Text => {
|
||||
let mut txt = String::new();
|
||||
file.read_to_string(&mut txt)?;
|
||||
|
||||
let txt = String::from_utf8_lossy(&buf).to_string();
|
||||
ClipboardValue::Text(txt)
|
||||
}
|
||||
MimeTypeCategory::Image => {
|
||||
let mut bytes = vec![];
|
||||
file.read_to_end(&mut bytes)?;
|
||||
|
||||
debug!("Read bytes: {}", bytes.len());
|
||||
|
||||
let bytes = Bytes::from(&bytes);
|
||||
|
||||
let bytes = Bytes::from(&buf);
|
||||
ClipboardValue::Image(bytes)
|
||||
}
|
||||
};
|
||||
|
@ -214,68 +216,33 @@ impl DataControlDeviceHandler for Environment {
|
|||
}
|
||||
|
||||
if let Some(offer) = data_device.selection_offer() {
|
||||
self.selection_offers
|
||||
.push(SelectionOfferItem { offer, token: None });
|
||||
|
||||
let cur_offer = self
|
||||
.selection_offers
|
||||
.last_mut()
|
||||
.expect("Failed to get current offer");
|
||||
|
||||
// clear prev
|
||||
let Some(mime_type) = MimeType::parse_multiple(&mime_types) else {
|
||||
lock!(self.clipboard).take();
|
||||
// send an event so the clipboard module is aware it's changed
|
||||
try_send!(
|
||||
self.event_tx,
|
||||
Event::Clipboard(ClipboardItem {
|
||||
id: usize::MAX,
|
||||
mime_type: String::new().into(),
|
||||
value: Arc::new(ClipboardValue::Other)
|
||||
})
|
||||
);
|
||||
self.event_tx.send_spawn(Event::Clipboard(ClipboardItem {
|
||||
id: usize::MAX,
|
||||
mime_type: String::new().into(),
|
||||
value: Arc::new(ClipboardValue::Other),
|
||||
}));
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
debug!("Receiving mime type: {}", mime_type.value);
|
||||
|
||||
if let Ok(read_pipe) = cur_offer.offer.receive(mime_type.value.clone()) {
|
||||
let offer_clone = cur_offer.offer.clone();
|
||||
|
||||
if let Ok(mut read_pipe) = offer.receive(mime_type.value.clone()) {
|
||||
let tx = self.event_tx.clone();
|
||||
let clipboard = self.clipboard.clone();
|
||||
|
||||
let token =
|
||||
self.loop_handle
|
||||
.insert_source(read_pipe, move |(), file, state| unsafe {
|
||||
let item = state
|
||||
.selection_offers
|
||||
.iter()
|
||||
.position(|o| o.offer == offer_clone)
|
||||
.map(|p| state.selection_offers.remove(p))
|
||||
.expect("Failed to find selection offer item");
|
||||
|
||||
match Self::read_file(&mime_type, file.get_mut()) {
|
||||
Ok(item) => {
|
||||
lock!(clipboard).replace(item.clone());
|
||||
try_send!(tx, Event::Clipboard(item));
|
||||
}
|
||||
Err(err) => error!("{err:?}"),
|
||||
}
|
||||
|
||||
state
|
||||
.loop_handle
|
||||
.remove(item.token.expect("Missing item token"));
|
||||
|
||||
PostAction::Remove
|
||||
});
|
||||
|
||||
match token {
|
||||
Ok(token) => {
|
||||
cur_offer.token.replace(token);
|
||||
spawn(async move {
|
||||
match Self::read_file(&mime_type, &mut read_pipe).await {
|
||||
Ok(item) => {
|
||||
lock!(clipboard).replace(item.clone());
|
||||
tx.send_spawn(Event::Clipboard(item));
|
||||
}
|
||||
Err(err) => error!("{err:?}"),
|
||||
}
|
||||
Err(err) => error!("Failed to insert read pipe event: {err:?}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -313,7 +280,7 @@ impl DataControlSourceHandler for Environment {
|
|||
source: &ZwlrDataControlSourceV1,
|
||||
mime: String,
|
||||
write_pipe: WritePipe,
|
||||
) {
|
||||
) -> Result<()> {
|
||||
debug!("Handler received source send request event ({mime})");
|
||||
|
||||
if let Some(item) = lock!(self.clipboard).clone() {
|
||||
|
@ -330,32 +297,34 @@ impl DataControlSourceHandler for Environment {
|
|||
ClipboardValue::Image(bytes) => bytes.as_ref(),
|
||||
ClipboardValue::Other => panic!(
|
||||
"{:?}",
|
||||
io::Error::new(ErrorKind::Other, "Attempted to copy unsupported mime type")
|
||||
io::Error::other("Attempted to copy unsupported mime type")
|
||||
),
|
||||
};
|
||||
|
||||
let pipe_size = set_pipe_size(fd.as_raw_fd(), bytes.len())
|
||||
.expect("Failed to increase pipe size");
|
||||
let pipe_size =
|
||||
set_pipe_size(fd.as_fd(), bytes.len()).expect("Failed to increase pipe size");
|
||||
let mut file = File::from(fd.try_clone().expect("to be able to clone"));
|
||||
|
||||
debug!("Writing {} bytes", bytes.len());
|
||||
|
||||
let mut events = (0..16).map(|_| EpollEvent::empty()).collect::<Vec<_>>();
|
||||
let epoll_event = EpollEvent::new(EpollFlags::EPOLLOUT, 0);
|
||||
let epoll = epoll::create(CreateFlags::CLOEXEC)?;
|
||||
epoll::add(
|
||||
&epoll,
|
||||
fd,
|
||||
epoll::EventData::new_u64(0),
|
||||
epoll::EventFlags::OUT,
|
||||
)?;
|
||||
|
||||
let epoll_fd =
|
||||
Epoll::new(EpollCreateFlags::empty()).expect("to get valid file descriptor");
|
||||
epoll_fd
|
||||
.add(fd, epoll_event)
|
||||
.expect("to send valid epoll operation");
|
||||
let mut events = Vec::with_capacity(16);
|
||||
|
||||
let timeout = EpollTimeout::from(100u16);
|
||||
while !bytes.is_empty() {
|
||||
let chunk = &bytes[..min(pipe_size as usize, bytes.len())];
|
||||
let chunk = &bytes[..min(pipe_size, bytes.len())];
|
||||
|
||||
epoll_fd
|
||||
.wait(&mut events, timeout)
|
||||
.expect("Failed to wait to epoll");
|
||||
epoll::wait(
|
||||
&epoll,
|
||||
spare_capacity(&mut events),
|
||||
Some(&Timespec::try_from(Duration::from_millis(100))?),
|
||||
)?;
|
||||
|
||||
match file.write(chunk) {
|
||||
Ok(written) => {
|
||||
|
@ -371,9 +340,11 @@ impl DataControlSourceHandler for Environment {
|
|||
|
||||
debug!("Done writing");
|
||||
} else {
|
||||
error!("Failed to find source");
|
||||
error!("Failed to find source (mime: '{mime}')");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cancelled(
|
||||
|
@ -398,7 +369,7 @@ impl DataControlSourceHandler for Environment {
|
|||
/// it will be clamped at this.
|
||||
///
|
||||
/// Returns the new size if succeeded.
|
||||
fn set_pipe_size(fd: RawFd, size: usize) -> io::Result<i32> {
|
||||
fn set_pipe_size(fd: BorrowedFd, size: usize) -> io::Result<usize> {
|
||||
// clamp size at kernel max
|
||||
let max_pipe_size = fs::read_to_string("/proc/sys/fs/pipe-max-size")
|
||||
.expect("Failed to find pipe-max-size virtual kernel file")
|
||||
|
@ -408,23 +379,24 @@ fn set_pipe_size(fd: RawFd, size: usize) -> io::Result<i32> {
|
|||
|
||||
let size = min(size, max_pipe_size);
|
||||
|
||||
let curr_size = fcntl(fd, F_GETPIPE_SZ)? as usize;
|
||||
let curr_size = fcntl_getpipe_size(fd)?;
|
||||
|
||||
trace!("Current pipe size: {curr_size}");
|
||||
|
||||
let new_size = if size > curr_size {
|
||||
trace!("Requesting pipe size increase to (at least): {size}");
|
||||
|
||||
let res = fcntl(fd, F_SETPIPE_SZ(size as i32))?;
|
||||
fcntl_setpipe_size(fd, size)?;
|
||||
let res = fcntl_getpipe_size(fd)?;
|
||||
trace!("New pipe size: {res}");
|
||||
|
||||
if res < size as i32 {
|
||||
if res < size {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
|
||||
res
|
||||
} else {
|
||||
size as i32
|
||||
size
|
||||
};
|
||||
|
||||
Ok(new_size)
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
use super::manager::DataControlDeviceManagerState;
|
||||
use crate::lock;
|
||||
use nix::fcntl::OFlag;
|
||||
use nix::unistd::pipe2;
|
||||
use rustix::pipe::{PipeFlags, pipe_with};
|
||||
use smithay_client_toolkit::data_device_manager::data_offer::DataOfferError;
|
||||
use smithay_client_toolkit::data_device_manager::ReadPipe;
|
||||
use std::ops::DerefMut;
|
||||
use std::os::fd::AsFd;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::net::unix::pipe::Receiver;
|
||||
use tracing::trace;
|
||||
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle};
|
||||
use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_offer_v1::{
|
||||
|
@ -36,8 +35,8 @@ impl PartialEq for SelectionOffer {
|
|||
}
|
||||
|
||||
impl SelectionOffer {
|
||||
pub fn receive(&self, mime_type: String) -> Result<ReadPipe, DataOfferError> {
|
||||
unsafe { receive(&self.data_offer, mime_type) }.map_err(DataOfferError::Io)
|
||||
pub fn receive(&self, mime_type: String) -> Result<Receiver, DataOfferError> {
|
||||
receive(&self.data_offer, mime_type).map_err(DataOfferError::Io)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -169,14 +168,11 @@ where
|
|||
///
|
||||
/// Fails if too many file descriptors were already open and a pipe
|
||||
/// could not be created.
|
||||
pub unsafe fn receive(
|
||||
offer: &ZwlrDataControlOfferV1,
|
||||
mime_type: String,
|
||||
) -> std::io::Result<ReadPipe> {
|
||||
pub fn receive(offer: &ZwlrDataControlOfferV1, mime_type: String) -> std::io::Result<Receiver> {
|
||||
// create a pipe
|
||||
let (readfd, writefd) = pipe2(OFlag::O_CLOEXEC)?;
|
||||
let (readfd, writefd) = pipe_with(PipeFlags::CLOEXEC)?;
|
||||
|
||||
offer.receive(mime_type, writefd.as_fd());
|
||||
|
||||
Ok(ReadPipe::from(readfd))
|
||||
Receiver::from_owned_fd(readfd)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
use super::device::DataControlDevice;
|
||||
use super::manager::DataControlDeviceManagerState;
|
||||
use color_eyre::Result;
|
||||
use smithay_client_toolkit::data_device_manager::WritePipe;
|
||||
use tracing::error;
|
||||
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle};
|
||||
use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_source_v1::{
|
||||
Event, ZwlrDataControlSourceV1,
|
||||
|
@ -23,7 +25,7 @@ impl DataControlSourceDataExt for DataControlSourceData {
|
|||
///
|
||||
/// The functions defined in this trait are called as `DataSource` events are received from the compositor.
|
||||
pub trait DataControlSourceHandler: Sized {
|
||||
/// This may be called multiple times, once for each accepted mime type from the destination, if any.
|
||||
// /// This may be called multiple times, once for each accepted mime type from the destination, if any.
|
||||
// fn accept_mime(
|
||||
// &mut self,
|
||||
// conn: &Connection,
|
||||
|
@ -41,7 +43,7 @@ pub trait DataControlSourceHandler: Sized {
|
|||
source: &ZwlrDataControlSourceV1,
|
||||
mime: String,
|
||||
fd: WritePipe,
|
||||
);
|
||||
) -> Result<()>;
|
||||
|
||||
/// The data source is no longer valid
|
||||
/// Cleanup & destroy this resource
|
||||
|
@ -68,7 +70,9 @@ where
|
|||
) {
|
||||
match event {
|
||||
Event::Send { mime_type, fd } => {
|
||||
state.send_request(conn, qh, source, mime_type, fd.into());
|
||||
if let Err(err) = state.send_request(conn, qh, source, mime_type, fd.into()) {
|
||||
error!("{err:#}");
|
||||
}
|
||||
}
|
||||
Event::Cancelled => {
|
||||
state.cancelled(conn, qh, source);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use super::manager::ToplevelManagerState;
|
||||
use crate::{lock, Ironbar};
|
||||
use crate::{Ironbar, lock};
|
||||
use std::collections::HashSet;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tracing::trace;
|
||||
|
@ -33,6 +33,11 @@ impl ToplevelHandle {
|
|||
trace!("Activating handle");
|
||||
self.handle.activate(seat);
|
||||
}
|
||||
|
||||
pub fn minimize(&self) {
|
||||
trace!("Minimizing handle");
|
||||
self.handle.set_minimized();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
|
@ -146,7 +151,7 @@ where
|
|||
ToplevelHandle {
|
||||
handle: handle.clone(),
|
||||
},
|
||||
)
|
||||
);
|
||||
}
|
||||
Event::Done if !lock!(data.inner).closed => {
|
||||
{
|
||||
|
|
|
@ -4,7 +4,7 @@ use smithay_client_toolkit::globals::{GlobalData, ProvidesBoundGlobal};
|
|||
use std::marker::PhantomData;
|
||||
use tracing::{debug, warn};
|
||||
use wayland_client::globals::{BindError, GlobalList};
|
||||
use wayland_client::{event_created_child, Connection, Dispatch, QueueHandle};
|
||||
use wayland_client::{Connection, Dispatch, QueueHandle, event_created_child};
|
||||
use wayland_protocols_wlr::foreign_toplevel::v1::client::{
|
||||
zwlr_foreign_toplevel_handle_v1::ZwlrForeignToplevelHandleV1,
|
||||
zwlr_foreign_toplevel_manager_v1::{Event, ZwlrForeignToplevelManagerV1},
|
||||
|
@ -67,7 +67,9 @@ where
|
|||
state.toplevel(conn, qhandle);
|
||||
}
|
||||
Event::Finished => {
|
||||
warn!("Foreign toplevel manager is no longer valid, but has not been dropped by client. This could cause window tracking issues.");
|
||||
warn!(
|
||||
"Foreign toplevel manager is no longer valid, but has not been dropped by client. This could cause window tracking issues."
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
|
@ -4,11 +4,11 @@ pub mod manager;
|
|||
use self::handle::ToplevelHandleHandler;
|
||||
use self::manager::ToplevelManagerHandler;
|
||||
use super::{Client, Environment, Event, Request, Response};
|
||||
use crate::try_send;
|
||||
use tokio::sync::broadcast;
|
||||
use tracing::{debug, error, trace};
|
||||
use wayland_client::{Connection, QueueHandle};
|
||||
|
||||
use crate::channels::AsyncSenderExt;
|
||||
pub use handle::{ToplevelHandle, ToplevelInfo};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -36,6 +36,15 @@ impl Client {
|
|||
}
|
||||
}
|
||||
|
||||
/// Minimizes the toplevel with the provided ID.
|
||||
#[cfg(feature = "launcher")]
|
||||
pub fn toplevel_minimize(&self, handle_id: usize) {
|
||||
match self.send_request(Request::ToplevelMinimize(handle_id)) {
|
||||
Response::Ok => (),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Subscribes to events from toplevels.
|
||||
pub fn subscribe_toplevels(&self) -> broadcast::Receiver<ToplevelEvent> {
|
||||
self.toplevel_channel.0.subscribe()
|
||||
|
@ -54,10 +63,16 @@ impl ToplevelHandleHandler for Environment {
|
|||
|
||||
match handle.info() {
|
||||
Some(info) => {
|
||||
if info.app_id.is_empty() {
|
||||
trace!("ignoring xwayland dialog");
|
||||
return;
|
||||
}
|
||||
|
||||
trace!("Adding new handle: {info:?}");
|
||||
self.handles.push(handle.clone());
|
||||
if let Some(info) = handle.info() {
|
||||
try_send!(self.event_tx, Event::Toplevel(ToplevelEvent::New(info)));
|
||||
self.event_tx
|
||||
.send_spawn(Event::Toplevel(ToplevelEvent::New(info)));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
|
@ -78,7 +93,8 @@ impl ToplevelHandleHandler for Environment {
|
|||
Some(info) => {
|
||||
trace!("Updating handle: {info:?}");
|
||||
if let Some(info) = handle.info() {
|
||||
try_send!(self.event_tx, Event::Toplevel(ToplevelEvent::Update(info)));
|
||||
self.event_tx
|
||||
.send_spawn(Event::Toplevel(ToplevelEvent::Update(info)));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
|
@ -97,7 +113,8 @@ impl ToplevelHandleHandler for Environment {
|
|||
|
||||
self.handles.retain(|h| h != &handle);
|
||||
if let Some(info) = handle.info() {
|
||||
try_send!(self.event_tx, Event::Toplevel(ToplevelEvent::Remove(info)));
|
||||
self.event_tx
|
||||
.send_spawn(Event::Toplevel(ToplevelEvent::Remove(info)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue