1
0
Fork 0
mirror of https://github.com/Zedfrigg/ironbar.git synced 2025-07-03 03:31:03 +02:00

Merge branch 'master' into feat/networkmanager

This commit is contained in:
Reinout Meliesie 2024-03-12 14:27:46 +01:00
commit 9031438f56
Signed by: zedfrigg
GPG key ID: 3AFCC06481308BC6
42 changed files with 2378 additions and 383 deletions

View file

@ -10,7 +10,6 @@ use gtk::gdk::Monitor;
use gtk::prelude::*;
use gtk::{Application, ApplicationWindow, IconTheme, Orientation, Window, WindowType};
use gtk_layer_shell::LayerShell;
use std::cell::RefCell;
use std::rc::Rc;
use std::time::Duration;
use tracing::{debug, info};
@ -18,7 +17,7 @@ use tracing::{debug, info};
#[derive(Debug, Clone)]
enum Inner {
New { config: Option<Config> },
Loaded { popup: Rc<RefCell<Popup>> },
Loaded { popup: Rc<Popup> },
}
#[derive(Debug, Clone)]
@ -60,7 +59,7 @@ impl Bar {
window.set_widget_name(&name);
let position = config.position;
let orientation = position.get_orientation();
let orientation = position.orientation();
let content = gtk::Box::builder()
.orientation(orientation)
@ -187,7 +186,7 @@ impl Bar {
win.set_layer_shell_margin(gtk_layer_shell::Edge::Left, margin.left);
win.set_layer_shell_margin(gtk_layer_shell::Edge::Right, margin.right);
let bar_orientation = position.get_orientation();
let bar_orientation = position.orientation();
win.set_anchor(
gtk_layer_shell::Edge::Top,
@ -269,7 +268,7 @@ impl Bar {
// popup ignores module location so can bodge this for now
let popup = Popup::new(&info!(ModuleLocation::Left), config.popup_gap);
let popup = Rc::new(RefCell::new(popup));
let popup = Rc::new(popup);
if let Some(modules) = config.start {
let info = info!(ModuleLocation::Left);
@ -315,7 +314,7 @@ impl Bar {
&self.monitor_name
}
pub fn popup(&self) -> Rc<RefCell<Popup>> {
pub fn popup(&self) -> Rc<Popup> {
match &self.inner {
Inner::New { .. } => {
panic!("Attempted to get popup of uninitialized bar. This is a serious bug!")
@ -339,7 +338,7 @@ fn create_container(name: &str, orientation: Orientation) -> gtk::Box {
#[derive(Debug)]
struct BarLoadResult {
popup: Rc<RefCell<Popup>>,
popup: Rc<Popup>,
}
/// Adds modules into a provided GTK box,
@ -349,9 +348,9 @@ fn add_modules(
modules: Vec<ModuleConfig>,
info: &ModuleInfo,
ironbar: &Rc<Ironbar>,
popup: &Rc<RefCell<Popup>>,
popup: &Rc<Popup>,
) -> Result<()> {
let orientation = info.bar_position.get_orientation();
let orientation = info.bar_position.orientation();
macro_rules! add_module {
($module:expr, $id:expr) => {{
@ -388,6 +387,8 @@ fn add_modules(
ModuleConfig::Music(mut module) => add_module!(module, id),
#[cfg(feature = "networkmanager")]
ModuleConfig::Networkmanager(mut module) => add_module!(module, id),
#[cfg(feature = "notifications")]
ModuleConfig::Notifications(mut module) => add_module!(module, id),
ModuleConfig::Script(mut module) => add_module!(module, id),
#[cfg(feature = "sys_info")]
ModuleConfig::SysInfo(mut module) => add_module!(module, id),
@ -395,6 +396,8 @@ fn add_modules(
ModuleConfig::Tray(mut module) => add_module!(module, id),
#[cfg(feature = "upower")]
ModuleConfig::Upower(mut module) => add_module!(module, id),
#[cfg(feature = "volume")]
ModuleConfig::Volume(mut module) => add_module!(module, id),
#[cfg(feature = "workspaces")]
ModuleConfig::Workspaces(mut module) => add_module!(module, id),
}

View file

@ -6,10 +6,14 @@ pub mod clipboard;
pub mod compositor;
#[cfg(feature = "music")]
pub mod music;
#[cfg(feature = "notifications")]
pub mod swaync;
#[cfg(feature = "tray")]
pub mod system_tray;
#[cfg(feature = "upower")]
pub mod upower;
#[cfg(feature = "volume")]
pub mod volume;
pub mod wayland;
/// Singleton wrapper consisting of
@ -23,10 +27,14 @@ pub struct Clients {
clipboard: Option<Arc<clipboard::Client>>,
#[cfg(feature = "music")]
music: std::collections::HashMap<music::ClientType, Arc<dyn music::MusicClient>>,
#[cfg(feature = "notifications")]
notifications: Option<Arc<swaync::Client>>,
#[cfg(feature = "tray")]
tray: Option<Arc<system_tray::TrayEventReceiver>>,
#[cfg(feature = "upower")]
upower: Option<Arc<zbus::fdo::PropertiesProxy<'static>>>,
#[cfg(feature = "volume")]
volume: Option<Arc<volume::Client>>,
}
impl Clients {
@ -67,6 +75,15 @@ impl Clients {
.clone()
}
#[cfg(feature = "notifications")]
pub fn notifications(&mut self) -> Arc<swaync::Client> {
self.notifications
.get_or_insert_with(|| {
Arc::new(crate::await_sync(async { swaync::Client::new().await }))
})
.clone()
}
#[cfg(feature = "tray")]
pub fn tray(&mut self) -> Arc<system_tray::TrayEventReceiver> {
self.tray
@ -86,6 +103,13 @@ impl Clients {
})
.clone()
}
#[cfg(feature = "volume")]
pub fn volume(&mut self) -> Arc<volume::Client> {
self.volume
.get_or_insert_with(volume::create_client)
.clone()
}
}
/// Types implementing this trait
@ -111,7 +135,7 @@ macro_rules! register_client {
where
TSend: Clone,
{
fn provide(&self) -> Arc<$ty> {
fn provide(&self) -> std::sync::Arc<$ty> {
self.ironbar.clients.borrow_mut().$method()
}
}

111
src/clients/swaync/dbus.rs Normal file
View file

@ -0,0 +1,111 @@
//! # D-Bus interface proxy for: `org.erikreider.swaync.cc`
//!
//! This code was generated by `zbus-xmlgen` `4.0.1` from D-Bus introspection data.
//! Source: `Interface '/org/erikreider/swaync/cc' from service 'org.erikreider.swaync.cc' on session bus`.
//!
//! You may prefer to adapt it, instead of using it verbatim.
//!
//! More information can be found in the [Writing a client proxy] section of the zbus
//! documentation.
//!
//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the
//! following zbus API can be used:
//!
//! * [`zbus::fdo::PropertiesProxy`]
//! * [`zbus::fdo::IntrospectableProxy`]
//! * [`zbus::fdo::PeerProxy`]
//!
//! Consequently `zbus-xmlgen` did not generate code for the above interfaces.
//!
//! [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(
interface = "org.erikreider.swaync.cc",
default_service = "org.erikreider.swaync.cc",
default_path = "/org/erikreider/swaync/cc"
)]
trait SwayNc {
/// AddInhibitor method
fn add_inhibitor(&self, application_id: &str) -> zbus::Result<bool>;
/// ChangeConfigValue method
fn change_config_value(
&self,
name: &str,
value: &zbus::zvariant::Value<'_>,
write_to_file: bool,
path: &str,
) -> zbus::Result<()>;
/// ClearInhibitors method
fn clear_inhibitors(&self) -> zbus::Result<bool>;
/// CloseAllNotifications method
fn close_all_notifications(&self) -> zbus::Result<()>;
/// CloseNotification method
fn close_notification(&self, id: u32) -> zbus::Result<()>;
/// GetDnd method
fn get_dnd(&self) -> zbus::Result<bool>;
/// GetSubscribeData method
fn get_subscribe_data(&self) -> zbus::Result<(bool, bool, u32, bool)>;
/// GetVisibility method
fn get_visibility(&self) -> zbus::Result<bool>;
/// HideLatestNotifications method
fn hide_latest_notifications(&self, close: bool) -> zbus::Result<()>;
/// IsInhibited method
fn is_inhibited(&self) -> zbus::Result<bool>;
/// NotificationCount method
fn notification_count(&self) -> zbus::Result<u32>;
/// NumberOfInhibitors method
fn number_of_inhibitors(&self) -> zbus::Result<u32>;
/// ReloadConfig method
fn reload_config(&self) -> zbus::Result<()>;
/// ReloadCss method
fn reload_css(&self) -> zbus::Result<bool>;
/// RemoveInhibitor method
fn remove_inhibitor(&self, application_id: &str) -> zbus::Result<bool>;
/// SetDnd method
fn set_dnd(&self, state: bool) -> zbus::Result<()>;
/// SetVisibility method
fn set_visibility(&self, visibility: bool) -> zbus::Result<()>;
/// ToggleDnd method
fn toggle_dnd(&self) -> zbus::Result<bool>;
/// ToggleVisibility method
fn toggle_visibility(&self) -> zbus::Result<()>;
/// Subscribe signal
#[dbus_proxy(signal)]
fn subscribe(&self, count: u32, dnd: bool, cc_open: bool) -> zbus::Result<()>;
/// SubscribeV2 signal
#[dbus_proxy(signal)]
fn subscribe_v2(
&self,
count: u32,
dnd: bool,
cc_open: bool,
inhibited: bool,
) -> zbus::Result<()>;
/// Inhibited property
#[dbus_proxy(property)]
fn inhibited(&self) -> zbus::Result<bool>;
#[dbus_proxy(property)]
fn set_inhibited(&self, value: bool) -> zbus::Result<()>;
}

88
src/clients/swaync/mod.rs Normal file
View file

@ -0,0 +1,88 @@
mod dbus;
use crate::{register_client, send, spawn};
use color_eyre::{Report, Result};
use dbus::SwayNcProxy;
use serde::Deserialize;
use tokio::sync::broadcast;
use tracing::{debug, error};
use zbus::export::ordered_stream::OrderedStreamExt;
use zbus::zvariant::Type;
#[derive(Debug, Clone, Copy, Type, Deserialize)]
pub struct Event {
pub count: u32,
pub dnd: bool,
pub cc_open: bool,
pub inhibited: bool,
}
type GetSubscribeData = (bool, bool, u32, bool);
/// Converts the data returned from
/// `get_subscribe_data` into an event for convenience.
impl From<GetSubscribeData> for Event {
fn from((dnd, cc_open, count, inhibited): (bool, bool, u32, bool)) -> Self {
Self {
dnd,
cc_open,
count,
inhibited,
}
}
}
#[derive(Debug)]
pub struct Client {
proxy: SwayNcProxy<'static>,
tx: broadcast::Sender<Event>,
_rx: broadcast::Receiver<Event>,
}
impl Client {
pub async fn new() -> Self {
let dbus = Box::pin(zbus::Connection::session())
.await
.expect("failed to create connection to system bus");
let proxy = SwayNcProxy::new(&dbus).await.unwrap();
let (tx, rx) = broadcast::channel(8);
let mut stream = proxy.receive_subscribe_v2().await.unwrap();
{
let tx = tx.clone();
spawn(async move {
while let Some(ev) = stream.next().await {
let ev = ev.body::<Event>().expect("to deserialize");
debug!("Received event: {ev:?}");
send!(tx, ev);
}
});
}
Self { proxy, tx, _rx: rx }
}
pub fn subscribe(&self) -> broadcast::Receiver<Event> {
self.tx.subscribe()
}
pub async fn state(&self) -> Result<Event> {
debug!("Getting subscribe data (current state)");
match self.proxy.get_subscribe_data().await {
Ok(data) => Ok(data.into()),
Err(err) => Err(Report::new(err)),
}
}
pub async fn toggle_visibility(&self) {
debug!("Toggling visibility");
if let Err(err) = self.proxy.toggle_visibility().await {
error!("{err:?}");
}
}
}
register_client!(Client, notifications);

309
src/clients/volume/mod.rs Normal file
View file

@ -0,0 +1,309 @@
mod sink;
mod sink_input;
use crate::{arc_mut, lock, register_client, send, spawn_blocking, APP_ID};
use libpulse_binding::callbacks::ListResult;
use libpulse_binding::context::introspect::{Introspector, ServerInfo};
use libpulse_binding::context::subscribe::{Facility, InterestMaskSet, Operation};
use libpulse_binding::context::{Context, FlagSet, State};
use libpulse_binding::mainloop::standard::{IterateResult, Mainloop};
use libpulse_binding::proplist::Proplist;
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};
pub use sink::Sink;
pub use sink_input::SinkInput;
type ArcMutVec<T> = Arc<Mutex<Vec<T>>>;
#[derive(Debug, Clone)]
pub enum Event {
AddSink(Sink),
UpdateSink(Sink),
RemoveSink(String),
AddInput(SinkInput),
UpdateInput(SinkInput),
RemoveInput(u32),
}
#[derive(Debug)]
pub struct Client {
connection: Arc<Mutex<ConnectionState>>,
data: Data,
tx: broadcast::Sender<Event>,
_rx: broadcast::Receiver<Event>,
}
#[derive(Debug, Default, Clone)]
struct Data {
sinks: ArcMutVec<Sink>,
sink_inputs: ArcMutVec<SinkInput>,
default_sink_name: Arc<Mutex<Option<String>>>,
}
pub enum ConnectionState {
Disconnected,
Connected {
context: Arc<Mutex<Context>>,
introspector: Introspector,
},
}
impl Debug for ConnectionState {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Disconnected => "Disconnected",
Self::Connected { .. } => "Connected",
}
)
}
}
impl Client {
pub fn new() -> Self {
let (tx, rx) = broadcast::channel(32);
Self {
connection: arc_mut!(ConnectionState::Disconnected),
data: Data::default(),
tx,
_rx: rx,
}
}
/// Starts the client.
fn run(&self) {
let Some(mut proplist) = Proplist::new() else {
error!("Failed to create PA proplist");
return;
};
if proplist.set_str("APPLICATION_NAME", APP_ID).is_err() {
error!("Failed to update PA proplist");
}
let Some(mut mainloop) = Mainloop::new() else {
error!("Failed to create PA mainloop");
return;
};
let Some(context) = Context::new_with_proplist(&mainloop, "Ironbar Context", &proplist)
else {
error!("Failed to create PA context");
return;
};
let context = arc_mut!(context);
let state_callback = Box::new({
let context = context.clone();
let data = self.data.clone();
let tx = self.tx.clone();
move || on_state_change(&context, &data, &tx)
});
lock!(context).set_state_callback(Some(state_callback));
if let Err(err) = lock!(context).connect(None, FlagSet::NOAUTOSPAWN, None) {
error!("{err:?}");
}
let introspector = lock!(context).introspect();
{
let mut inner = lock!(self.connection);
*inner = ConnectionState::Connected {
context,
introspector,
};
}
loop {
match mainloop.iterate(true) {
IterateResult::Success(_) => {}
IterateResult::Err(err) => error!("{err:?}"),
IterateResult::Quit(_) => break,
}
}
}
/// Gets an event receiver.
pub fn subscribe(&self) -> broadcast::Receiver<Event> {
self.tx.subscribe()
}
}
/// Creates a new Pulse volume client.
pub fn create_client() -> Arc<Client> {
let client = Arc::new(Client::new());
{
let client = client.clone();
spawn_blocking(move || {
client.run();
});
}
client
}
fn on_state_change(context: &Arc<Mutex<Context>>, data: &Data, tx: &broadcast::Sender<Event>) {
let Ok(state) = context.try_lock().map(|lock| lock.get_state()) else {
return;
};
match state {
State::Ready => {
info!("connected to server");
let introspect = lock!(context).introspect();
let introspect2 = lock!(context).introspect();
introspect.get_sink_info_list({
let sinks = data.sinks.clone();
let default_sink = data.default_sink_name.clone();
let tx = tx.clone();
move |info| match info {
ListResult::Item(_) => sink::add(info, &sinks, &tx),
ListResult::End => {
introspect2.get_server_info({
let sinks = sinks.clone();
let default_sink = default_sink.clone();
let tx = tx.clone();
move |info| set_default_sink(info, &sinks, &default_sink, &tx)
});
}
ListResult::Error => error!("Error while receiving sinks"),
}
});
introspect.get_sink_input_info_list({
let inputs = data.sink_inputs.clone();
let tx = tx.clone();
move |info| sink_input::add(info, &inputs, &tx)
});
let subscribe_callback = Box::new({
let context = context.clone();
let data = data.clone();
let tx = tx.clone();
move |facility, op, i| on_event(&context, &data, &tx, facility, op, i)
});
lock!(context).set_subscribe_callback(Some(subscribe_callback));
lock!(context).subscribe(
InterestMaskSet::SERVER | InterestMaskSet::SINK_INPUT | InterestMaskSet::SINK,
|_| (),
);
}
State::Failed => error!("Failed to connect to audio server"),
State::Terminated => error!("Connection to audio server terminated"),
_ => {}
}
}
fn on_event(
context: &Arc<Mutex<Context>>,
data: &Data,
tx: &broadcast::Sender<Event>,
facility: Option<Facility>,
op: Option<Operation>,
i: u32,
) {
let (Some(facility), Some(op)) = (facility, op) else {
return;
};
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),
Facility::SinkInput => sink_input::on_event(context, &data.sink_inputs, tx, op, i),
_ => error!("Received unhandled facility: {facility:?}"),
}
}
fn on_server_event(
context: &Arc<Mutex<Context>>,
sinks: &ArcMutVec<Sink>,
default_sink: &Arc<Mutex<Option<String>>>,
tx: &broadcast::Sender<Event>,
) {
lock!(context).introspect().get_server_info({
let sinks = sinks.clone();
let default_sink = default_sink.clone();
let tx = tx.clone();
move |info| set_default_sink(info, &sinks, &default_sink, &tx)
});
}
fn set_default_sink(
info: &ServerInfo,
sinks: &ArcMutVec<Sink>,
default_sink: &Arc<Mutex<Option<String>>>,
tx: &broadcast::Sender<Event>,
) {
let default_sink_name = info.default_sink_name.as_ref().map(ToString::to_string);
if default_sink_name != *lock!(default_sink) {
if let Some(ref default_sink_name) = default_sink_name {
if let Some(sink) = lock!(sinks)
.iter_mut()
.find(|s| s.name.as_str() == default_sink_name.as_str())
{
sink.active = true;
debug!("Set sink active: {}", sink.name);
send!(tx, Event::UpdateSink(sink.clone()));
} else {
warn!("Couldn't find sink: {}", default_sink_name);
}
}
}
*lock!(default_sink) = default_sink_name;
}
/// Converts a Pulse `ChannelVolumes` struct into a single percentage value,
/// representing the average value across all channels.
fn volume_to_percent(volume: ChannelVolumes) -> f64 {
let avg = volume.avg().0;
let base_delta = (Volume::NORMAL.0 - Volume::MUTED.0) as f64 / 100.0;
((avg - Volume::MUTED.0) as f64 / base_delta).round()
}
/// Converts a percentage volume into a Pulse volume value,
/// which can be used for setting channel volumes.
pub fn percent_to_volume(target_percent: f64) -> u32 {
let base_delta = (Volume::NORMAL.0 as f32 - Volume::MUTED.0 as f32) / 100.0;
if target_percent < 0.0 {
Volume::MUTED.0
} else if target_percent == 100.0 {
Volume::NORMAL.0
} else if target_percent >= 150.0 {
(Volume::NORMAL.0 as f32 * 1.5) as u32
} else if target_percent < 100.0 {
Volume::MUTED.0 + target_percent as u32 * base_delta as u32
} else {
Volume::NORMAL.0 + (target_percent - 100.0) as u32 * base_delta as u32
}
}
register_client!(Client, volume);

175
src/clients/volume/sink.rs Normal file
View file

@ -0,0 +1,175 @@
use super::{percent_to_volume, volume_to_percent, ArcMutVec, Client, ConnectionState, Event};
use crate::{lock, send};
use libpulse_binding::callbacks::ListResult;
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 tokio::sync::broadcast;
use tracing::{debug, error};
#[derive(Debug, Clone)]
pub struct Sink {
index: u32,
pub name: String,
pub description: String,
pub volume: f64,
pub muted: bool,
pub active: bool,
}
impl From<&SinkInfo<'_>> for Sink {
fn from(value: &SinkInfo) -> Self {
Self {
index: value.index,
name: value
.name
.as_ref()
.map(ToString::to_string)
.unwrap_or_default(),
description: value
.description
.as_ref()
.map(ToString::to_string)
.unwrap_or_default(),
muted: value.mute,
volume: volume_to_percent(value.volume),
active: value.state == SinkState::Running,
}
}
}
impl Client {
pub fn sinks(&self) -> Arc<Mutex<Vec<Sink>>> {
self.data.sinks.clone()
}
pub fn set_default_sink(&self, name: &str) {
if let ConnectionState::Connected { context, .. } = &*lock!(self.connection) {
lock!(context).set_default_sink(name, |_| {});
}
}
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();
introspector.get_sink_info_by_name(name, move |info| {
let ListResult::Item(info) = info else {
return;
};
send!(tx, info.volume);
});
let new_volume = percent_to_volume(volume_percent);
let mut volume = rx.recv().expect("to receive info");
for v in volume.get_mut() {
v.0 = new_volume;
}
introspector.set_sink_volume_by_name(name, &volume, None);
}
}
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);
}
}
}
pub fn on_event(
context: &Arc<Mutex<Context>>,
sinks: &ArcMutVec<Sink>,
default_sink: &Arc<Mutex<Option<String>>>,
tx: &broadcast::Sender<Event>,
op: Operation,
i: u32,
) {
let introspect = lock!(context).introspect();
match op {
Operation::New => {
debug!("new sink");
introspect.get_sink_info_by_index(i, {
let sinks = sinks.clone();
let tx = tx.clone();
move |info| add(info, &sinks, &tx)
});
}
Operation::Changed => {
debug!("sink changed");
introspect.get_sink_info_by_index(i, {
let sinks = sinks.clone();
let default_sink = default_sink.clone();
let tx = tx.clone();
move |info| update(info, &sinks, &default_sink, &tx)
});
}
Operation::Removed => {
debug!("sink removed");
remove(i, sinks, tx);
}
}
}
pub fn add(info: ListResult<&SinkInfo>, sinks: &ArcMutVec<Sink>, tx: &broadcast::Sender<Event>) {
let ListResult::Item(info) = info else {
return;
};
lock!(sinks).push(info.into());
send!(tx, Event::AddSink(info.into()));
}
fn update(
info: ListResult<&SinkInfo>,
sinks: &ArcMutVec<Sink>,
default_sink: &Arc<Mutex<Option<String>>>,
tx: &broadcast::Sender<Event>,
) {
let ListResult::Item(info) = info else {
return;
};
{
let mut sinks = lock!(sinks);
let Some(pos) = sinks.iter().position(|sink| sink.index == info.index) else {
error!("received update to untracked sink input");
return;
};
sinks[pos] = info.into();
// update in local copy
if !sinks[pos].active {
if let Some(default_sink) = &*lock!(default_sink) {
sinks[pos].active = &sinks[pos].name == default_sink;
}
}
}
let mut sink: Sink = info.into();
// update in broadcast copy
if !sink.active {
if let Some(default_sink) = &*lock!(default_sink) {
sink.active = &sink.name == default_sink;
}
}
send!(tx, Event::UpdateSink(sink));
}
fn remove(index: u32, sinks: &ArcMutVec<Sink>, tx: &broadcast::Sender<Event>) {
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));
}
}

View file

@ -0,0 +1,148 @@
use super::{percent_to_volume, volume_to_percent, ArcMutVec, Client, ConnectionState, Event};
use crate::{lock, send};
use libpulse_binding::callbacks::ListResult;
use libpulse_binding::context::introspect::SinkInputInfo;
use libpulse_binding::context::subscribe::Operation;
use libpulse_binding::context::Context;
use std::sync::{mpsc, Arc, Mutex};
use tokio::sync::broadcast;
use tracing::{debug, error};
#[derive(Debug, Clone)]
pub struct SinkInput {
pub index: u32,
pub name: String,
pub volume: f64,
pub muted: bool,
pub can_set_volume: bool,
}
impl From<&SinkInputInfo<'_>> for SinkInput {
fn from(value: &SinkInputInfo) -> Self {
Self {
index: value.index,
name: value
.name
.as_ref()
.map(ToString::to_string)
.unwrap_or_default(),
muted: value.mute,
volume: volume_to_percent(value.volume),
can_set_volume: value.has_volume && value.volume_writable,
}
}
}
impl Client {
pub fn sink_inputs(&self) -> Arc<Mutex<Vec<SinkInput>>> {
self.data.sink_inputs.clone()
}
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();
introspector.get_sink_input_info(index, move |info| {
let ListResult::Item(info) = info else {
return;
};
send!(tx, info.volume);
});
let new_volume = percent_to_volume(volume_percent);
let mut volume = rx.recv().expect("to receive info");
for v in volume.get_mut() {
v.0 = new_volume;
}
introspector.set_sink_input_volume(index, &volume, None);
}
}
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);
}
}
}
pub fn on_event(
context: &Arc<Mutex<Context>>,
inputs: &ArcMutVec<SinkInput>,
tx: &broadcast::Sender<Event>,
op: Operation,
i: u32,
) {
let introspect = lock!(context).introspect();
match op {
Operation::New => {
debug!("new sink input");
introspect.get_sink_input_info(i, {
let inputs = inputs.clone();
let tx = tx.clone();
move |info| add(info, &inputs, &tx)
});
}
Operation::Changed => {
debug!("sink input changed");
introspect.get_sink_input_info(i, {
let inputs = inputs.clone();
let tx = tx.clone();
move |info| update(info, &inputs, &tx)
});
}
Operation::Removed => {
debug!("sink input removed");
remove(i, inputs, tx);
}
}
}
pub fn add(
info: ListResult<&SinkInputInfo>,
inputs: &ArcMutVec<SinkInput>,
tx: &broadcast::Sender<Event>,
) {
let ListResult::Item(info) = info else {
return;
};
lock!(inputs).push(info.into());
send!(tx, Event::AddInput(info.into()));
}
fn update(
info: ListResult<&SinkInputInfo>,
inputs: &ArcMutVec<SinkInput>,
tx: &broadcast::Sender<Event>,
) {
let ListResult::Item(info) = info else {
return;
};
{
let mut inputs = lock!(inputs);
let Some(pos) = inputs.iter().position(|input| input.index == info.index) else {
error!("received update to untracked sink input");
return;
};
inputs[pos] = info.into();
}
send!(tx, Event::UpdateInput(info.into()));
}
fn remove(index: u32, inputs: &ArcMutVec<SinkInput>, tx: &broadcast::Sender<Event>) {
let mut inputs = lock!(inputs);
if let Some(pos) = inputs.iter().position(|s| s.index == index) {
let info = inputs.remove(pos);
send!(tx, Event::RemoveInput(info.index));
}
}

View file

@ -179,6 +179,9 @@ impl Environment {
MimeTypeCategory::Image => {
let mut bytes = vec![];
file.read_to_end(&mut bytes)?;
debug!("Read bytes: {}", bytes.len());
let bytes = Bytes::from(&bytes);
ClipboardValue::Image(bytes)
@ -234,6 +237,8 @@ impl DataControlDeviceHandler for Environment {
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();
@ -331,9 +336,9 @@ impl DataControlSourceHandler for Environment {
let pipe_size = set_pipe_size(fd.as_raw_fd(), bytes.len())
.expect("Failed to increase pipe size");
let mut file = File::from(fd.try_clone().expect("Failed to clone fd"));
let mut file = File::from(fd.try_clone().expect("to be able to clone"));
trace!("Num bytes: {}", bytes.len());
debug!("Writing {} bytes", bytes.len());
let mut events = (0..16).map(|_| EpollEvent::empty()).collect::<Vec<_>>();
let epoll_event = EpollEvent::new(EpollFlags::EPOLLOUT, 0);
@ -347,20 +352,23 @@ impl DataControlSourceHandler for Environment {
while !bytes.is_empty() {
let chunk = &bytes[..min(pipe_size as usize, bytes.len())];
trace!("Writing {} bytes ({} remain)", chunk.len(), bytes.len());
epoll_fd
.wait(&mut events, 100)
.expect("Failed to wait to epoll");
match file.write(chunk) {
Ok(_) => bytes = &bytes[chunk.len()..],
Ok(written) => {
trace!("Wrote {} bytes ({} remain)", written, bytes.len());
bytes = &bytes[written..];
}
Err(err) => {
error!("{err:?}");
break;
}
}
}
debug!("Done writing");
} else {
error!("Failed to find source");
}
@ -388,7 +396,7 @@ impl DataControlSourceHandler for Environment {
/// If the requested size is larger than the kernel max (normally 1MB),
/// it will be clamped at this.
///
/// Returns the new size if succeeded
/// Returns the new size if succeeded.
fn set_pipe_size(fd: RawFd, size: usize) -> io::Result<i32> {
// clamp size at kernel max
let max_pipe_size = fs::read_to_string("/proc/sys/fs/pipe-max-size")

View file

@ -38,7 +38,7 @@ impl<'de> Deserialize<'de> for MonitorConfig {
impl BarPosition {
/// Gets the orientation the bar and widgets should use
/// based on this position.
pub fn get_orientation(self) -> Orientation {
pub fn orientation(self) -> Orientation {
if self == Self::Top || self == Self::Bottom {
Orientation::Horizontal
} else {

View file

@ -16,6 +16,8 @@ use crate::modules::launcher::LauncherModule;
use crate::modules::music::MusicModule;
#[cfg(feature = "networkmanager")]
use crate::modules::networkmanager::NetworkmanagerModule;
#[cfg(feature = "notifications")]
use crate::modules::notifications::NotificationsModule;
use crate::modules::script::ScriptModule;
#[cfg(feature = "sys_info")]
use crate::modules::sysinfo::SysInfoModule;
@ -23,6 +25,8 @@ use crate::modules::sysinfo::SysInfoModule;
use crate::modules::tray::TrayModule;
#[cfg(feature = "upower")]
use crate::modules::upower::UpowerModule;
#[cfg(feature = "volume")]
use crate::modules::volume::VolumeModule;
#[cfg(feature = "workspaces")]
use crate::modules::workspaces::WorkspacesModule;
use cfg_if::cfg_if;
@ -49,6 +53,8 @@ pub enum ModuleConfig {
Music(Box<MusicModule>),
#[cfg(feature = "networkmanager")]
Networkmanager(Box<NetworkmanagerModule>),
#[cfg(feature = "notifications")]
Notifications(Box<NotificationsModule>),
Script(Box<ScriptModule>),
#[cfg(feature = "sys_info")]
SysInfo(Box<SysInfoModule>),
@ -56,6 +62,8 @@ pub enum ModuleConfig {
Tray(Box<TrayModule>),
#[cfg(feature = "upower")]
Upower(Box<UpowerModule>),
#[cfg(feature = "volume")]
Volume(Box<VolumeModule>),
#[cfg(feature = "workspaces")]
Workspaces(Box<WorkspacesModule>),
}

View file

@ -167,13 +167,13 @@ impl Ipc {
match bar {
Some(bar) => {
let popup = bar.popup();
let current_widget = popup.borrow().current_widget();
let current_widget = popup.current_widget();
popup.borrow_mut().hide();
popup.hide();
let data = popup
.borrow()
.cache
.borrow()
.iter()
.find(|(_, value)| value.name == name)
.map(|(id, value)| (*id, value.content.buttons.first().cloned()));
@ -181,7 +181,6 @@ impl Ipc {
match data {
Some((id, Some(button))) if current_widget != Some(id) => {
let button_id = button.popup_id();
let mut popup = popup.borrow_mut();
if popup.is_visible() {
popup.hide();
@ -207,11 +206,11 @@ impl Ipc {
let popup = bar.popup();
// only one popup per bar, so hide if open for another widget
popup.borrow_mut().hide();
popup.hide();
let data = popup
.borrow()
.cache
.borrow()
.iter()
.find(|(_, value)| value.name == name)
.map(|(id, value)| (*id, value.content.buttons.first().cloned()));
@ -219,7 +218,7 @@ impl Ipc {
match data {
Some((id, Some(button))) => {
let button_id = button.popup_id();
popup.borrow_mut().show(id, button_id);
popup.show(id, button_id);
Response::Ok
}
@ -236,7 +235,7 @@ impl Ipc {
match bar {
Some(bar) => {
let popup = bar.popup();
popup.borrow_mut().hide();
popup.hide();
Response::Ok
}

View file

@ -180,3 +180,10 @@ macro_rules! arc_rw {
std::sync::Arc::new(std::sync::RwLock::new($val))
};
}
#[macro_export]
macro_rules! rc_mut {
($val:expr) => {
std::rc::Rc::new(std::cell::RefCell::new($val))
};
}

View file

@ -57,7 +57,7 @@ mod popup;
mod script;
mod style;
const GTK_APP_ID: &str = "dev.jstanger.ironbar";
pub const APP_ID: &str = "dev.jstanger.ironbar";
const VERSION: &str = env!("CARGO_PKG_VERSION");
fn main() {
@ -111,7 +111,7 @@ impl Ironbar {
info!("Ironbar version {}", VERSION);
info!("Starting application");
let app = Application::builder().application_id(GTK_APP_ID).build();
let app = Application::builder().application_id(APP_ID).build();
let running = AtomicBool::new(false);

View file

@ -194,7 +194,7 @@ impl Module<gtk::Box> for CustomModule {
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> Result<ModuleParts<gtk::Box>> {
let orientation = info.bar_position.get_orientation();
let orientation = info.bar_position.orientation();
let container = gtk::Box::builder().orientation(orientation).build();
let popup_buttons = Rc::new(RefCell::new(Vec::new()));
@ -236,7 +236,7 @@ impl Module<gtk::Box> for CustomModule {
if let Some(popup) = self.popup {
let custom_context = CustomWidgetContext {
tx: &tx,
bar_orientation: info.bar_position.get_orientation(),
bar_orientation: info.bar_position.orientation(),
icon_theme: info.icon_theme,
popup_buttons: Rc::new(RefCell::new(vec![])),
};

View file

@ -113,7 +113,7 @@ impl Module<gtk::Box> for FocusedModule {
) -> Result<ModuleParts<gtk::Box>> {
let icon_theme = info.icon_theme;
let container = gtk::Box::new(info.bar_position.get_orientation(), 5);
let container = gtk::Box::new(info.bar_position.orientation(), 5);
let icon = gtk::Image::new();
if self.show_icon {

View file

@ -166,8 +166,12 @@ impl ItemButton {
if appearance.show_icons {
let gtk_image = gtk::Image::new();
let image =
ImageProvider::parse(&item.app_id.clone(), icon_theme, true, appearance.icon_size);
let input = if item.app_id.is_empty() {
item.name.clone()
} else {
item.app_id.clone()
};
let image = ImageProvider::parse(&input, icon_theme, true, appearance.icon_size);
if let Some(image) = image {
button.set_image(Some(&gtk_image));
button.set_always_show_image(true);
@ -225,9 +229,7 @@ impl ItemButton {
try_send!(
tx,
ModuleUpdateEvent::OpenPopupAt(
button.geometry(bar_position.get_orientation())
)
ModuleUpdateEvent::OpenPopupAt(button.geometry(bar_position.orientation()))
);
} else {
try_send!(tx, ModuleUpdateEvent::ClosePopup);

View file

@ -301,7 +301,7 @@ impl Module<gtk::Box> for LauncherModule {
) -> crate::Result<ModuleParts<gtk::Box>> {
let icon_theme = info.icon_theme;
let container = gtk::Box::new(info.bar_position.get_orientation(), 0);
let container = gtk::Box::new(info.bar_position.orientation(), 0);
{
let container = container.clone();

View file

@ -1,4 +1,3 @@
use std::cell::RefCell;
use std::fmt::Debug;
use std::rc::Rc;
use std::sync::Arc;
@ -37,6 +36,8 @@ pub mod launcher;
pub mod music;
#[cfg(feature = "networkmanager")]
pub mod networkmanager;
#[cfg(feature = "notifications")]
pub mod notifications;
pub mod script;
#[cfg(feature = "sys_info")]
pub mod sysinfo;
@ -44,6 +45,8 @@ pub mod sysinfo;
pub mod tray;
#[cfg(feature = "upower")]
pub mod upower;
#[cfg(feature = "volume")]
pub mod volume;
#[cfg(feature = "workspaces")]
pub mod workspaces;
@ -217,7 +220,7 @@ pub fn create_module<TModule, TWidget, TSend, TRec>(
ironbar: Rc<Ironbar>,
name: Option<String>,
info: &ModuleInfo,
popup: &Rc<RefCell<Popup>>,
popup: &Rc<Popup>,
) -> Result<ModuleParts<TWidget>>
where
TModule: Module<TWidget, SendMessage = TSend, ReceiveMessage = TRec>,
@ -253,7 +256,7 @@ where
.style_context()
.add_class(&format!("popup-{module_name}"));
register_popup_content(popup, id, instance_name, popup_content);
popup.register_content(id, instance_name, popup_content);
}
setup_receiver(tx, ui_rx, popup.clone(), module_name, id);
@ -261,16 +264,6 @@ where
Ok(module_parts)
}
/// Registers the popup content with the popup.
fn register_popup_content(
popup: &Rc<RefCell<Popup>>,
id: usize,
name: String,
popup_content: ModulePopupParts,
) {
popup.borrow_mut().register_content(id, name, popup_content);
}
/// Sets up the bridge channel receiver
/// to pick up events from the controller, widget or popup.
///
@ -279,7 +272,7 @@ fn register_popup_content(
fn setup_receiver<TSend>(
tx: broadcast::Sender<TSend>,
rx: mpsc::Receiver<ModuleUpdateEvent<TSend>>,
popup: Rc<RefCell<Popup>>,
popup: Rc<Popup>,
name: &'static str,
id: usize,
) where
@ -296,7 +289,6 @@ fn setup_receiver<TSend>(
}
ModuleUpdateEvent::TogglePopup(button_id) => {
debug!("Toggling popup for {} [#{}]", name, id);
let mut popup = popup.borrow_mut();
if popup.is_visible() {
popup.hide();
} else {
@ -311,8 +303,6 @@ fn setup_receiver<TSend>(
}
ModuleUpdateEvent::OpenPopup(button_id) => {
debug!("Opening popup for {} [#{}]", name, id);
let mut popup = popup.borrow_mut();
popup.hide();
popup.show(id, button_id);
@ -326,7 +316,6 @@ fn setup_receiver<TSend>(
ModuleUpdateEvent::OpenPopupAt(geometry) => {
debug!("Opening popup for {} [#{}]", name, id);
let mut popup = popup.borrow_mut();
popup.hide();
popup.show_at(id, geometry);
@ -338,8 +327,6 @@ fn setup_receiver<TSend>(
}
ModuleUpdateEvent::ClosePopup => {
debug!("Closing popup for {} [#{}]", name, id);
let mut popup = popup.borrow_mut();
popup.hide();
}
}

View file

@ -0,0 +1,190 @@
use crate::clients::swaync;
use crate::config::CommonConfig;
use crate::gtk_helpers::IronbarGtkExt;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::{glib_recv, send_async, spawn, try_send};
use gtk::prelude::*;
use gtk::{Align, Button, Label, Overlay};
use serde::Deserialize;
use tokio::sync::mpsc::Receiver;
use tracing::error;
#[derive(Debug, Deserialize, Clone)]
pub struct NotificationsModule {
#[serde(default = "crate::config::default_true")]
show_count: bool,
#[serde(default)]
icons: Icons,
#[serde(flatten)]
pub common: Option<CommonConfig>,
}
#[derive(Debug, Deserialize, Clone)]
struct Icons {
#[serde(default = "default_icon_closed_none")]
closed_none: String,
#[serde(default = "default_icon_closed_some")]
closed_some: String,
#[serde(default = "default_icon_closed_dnd")]
closed_dnd: String,
#[serde(default = "default_icon_open_none")]
open_none: String,
#[serde(default = "default_icon_open_some")]
open_some: String,
#[serde(default = "default_icon_open_dnd")]
open_dnd: String,
}
impl Default for Icons {
fn default() -> Self {
Self {
closed_none: default_icon_closed_none(),
closed_some: default_icon_closed_some(),
closed_dnd: default_icon_closed_dnd(),
open_none: default_icon_open_none(),
open_some: default_icon_open_some(),
open_dnd: default_icon_open_dnd(),
}
}
}
fn default_icon_closed_none() -> String {
String::from("󰍥")
}
fn default_icon_closed_some() -> String {
String::from("󱥂")
}
fn default_icon_closed_dnd() -> String {
String::from("󱅯")
}
fn default_icon_open_none() -> String {
String::from("󰍡")
}
fn default_icon_open_some() -> String {
String::from("󱥁")
}
fn default_icon_open_dnd() -> String {
String::from("󱅮")
}
impl Icons {
fn icon(&self, value: &swaync::Event) -> &str {
match (value.cc_open, value.count > 0, value.dnd) {
(true, _, true) => &self.open_dnd,
(true, true, false) => &self.open_some,
(true, false, false) => &self.open_none,
(false, _, true) => &self.closed_dnd,
(false, true, false) => &self.closed_some,
(false, false, false) => &self.closed_none,
}
.as_str()
}
}
#[derive(Debug, Clone, Copy)]
pub enum UiEvent {
ToggleVisibility,
}
impl Module<Overlay> for NotificationsModule {
type SendMessage = swaync::Event;
type ReceiveMessage = UiEvent;
fn name() -> &'static str {
"notifications"
}
fn spawn_controller(
&self,
_info: &ModuleInfo,
context: &WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
mut rx: Receiver<Self::ReceiveMessage>,
) -> color_eyre::Result<()>
where
<Self as Module<Overlay>>::SendMessage: Clone,
{
let client = context.client::<swaync::Client>();
{
let client = client.clone();
let mut rx = client.subscribe();
let tx = context.tx.clone();
spawn(async move {
let initial_state = client.state().await;
match initial_state {
Ok(ev) => send_async!(tx, ModuleUpdateEvent::Update(ev)),
Err(err) => error!("{err:?}"),
};
while let Ok(ev) = rx.recv().await {
send_async!(tx, ModuleUpdateEvent::Update(ev));
}
});
}
spawn(async move {
while let Some(event) = rx.recv().await {
match event {
UiEvent::ToggleVisibility => client.toggle_visibility().await,
}
}
});
Ok(())
}
fn into_widget(
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
_info: &ModuleInfo,
) -> color_eyre::Result<ModuleParts<Overlay>>
where
<Self as Module<Overlay>>::SendMessage: Clone,
{
let overlay = Overlay::new();
let button = Button::with_label(&self.icons.closed_none);
overlay.add(&button);
let label = Label::builder()
.label("0")
.halign(Align::End)
.valign(Align::Start)
.build();
if self.show_count {
label.add_class("count");
overlay.add_overlay(&label);
}
let ctx = context.controller_tx.clone();
button.connect_clicked(move |_| {
try_send!(ctx, UiEvent::ToggleVisibility);
});
{
let button = button.clone();
glib_recv!(context.subscribe(), ev => {
let icon = self.icons.icon(&ev);
button.set_label(icon);
label.set_label(&ev.count.to_string());
label.set_visible(self.show_count && ev.count > 0);
});
}
Ok(ModuleParts {
widget: overlay,
popup: None,
})
}
}

View file

@ -188,7 +188,7 @@ impl Module<gtk::Box> for SysInfoModule {
) -> Result<ModuleParts<gtk::Box>> {
let re = Regex::new(r"\{([^}]+)}")?;
let container = gtk::Box::new(info.bar_position.get_orientation(), 10);
let container = gtk::Box::new(info.bar_position.orientation(), 10);
let mut labels = Vec::new();

View file

@ -8,7 +8,7 @@ use crate::modules::tray::diff::get_diffs;
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
use crate::{glib_recv, spawn};
use color_eyre::Result;
use gtk::prelude::*;
use gtk::{prelude::*, PackDirection};
use gtk::{IconTheme, MenuBar};
use interface::TrayMenu;
use serde::Deserialize;
@ -18,10 +18,28 @@ use tokio::sync::mpsc;
#[derive(Debug, Deserialize, Clone)]
pub struct TrayModule {
#[serde(default, deserialize_with = "deserialize_orientation")]
pub direction: Option<PackDirection>,
#[serde(flatten)]
pub common: Option<CommonConfig>,
}
fn deserialize_orientation<'de, D>(deserializer: D) -> Result<Option<PackDirection>, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = Option::<String>::deserialize(deserializer)?;
value
.map(|v| match v.as_str() {
"left_to_right" => Ok(PackDirection::Ltr),
"right_to_left" => Ok(PackDirection::Rtl),
"top_to_bottom" => Ok(PackDirection::Ttb),
"bottom_to_top" => Ok(PackDirection::Btt),
_ => Err(serde::de::Error::custom("invalid value for orientation")),
})
.transpose()
}
impl Module<MenuBar> for TrayModule {
type SendMessage = NotifierItemMessage;
type ReceiveMessage = NotifierItemCommand;
@ -70,6 +88,17 @@ impl Module<MenuBar> for TrayModule {
) -> Result<ModuleParts<MenuBar>> {
let container = MenuBar::new();
let direction = self.direction.unwrap_or(
if info.bar_position.orientation() == gtk::Orientation::Vertical {
PackDirection::Ttb
} else {
PackDirection::Ltr
},
);
container.set_pack_direction(direction);
container.set_child_pack_direction(direction);
{
let container = container.clone();
let mut menus = HashMap::new();

426
src/modules/volume.rs Normal file
View file

@ -0,0 +1,426 @@
use crate::clients::volume::{self, Event};
use crate::config::CommonConfig;
use crate::gtk_helpers::IronbarGtkExt;
use crate::modules::{
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, PopupButton, WidgetContext,
};
use crate::{glib_recv, lock, send_async, spawn, try_send};
use glib::Propagation;
use gtk::pango::EllipsizeMode;
use gtk::prelude::*;
use gtk::{Button, CellRendererText, ComboBoxText, Label, Orientation, Scale, ToggleButton};
use serde::Deserialize;
use std::collections::HashMap;
use tokio::sync::mpsc;
#[derive(Debug, Clone, Deserialize)]
pub struct VolumeModule {
#[serde(default = "default_format")]
format: String,
#[serde(default = "default_max_volume")]
max_volume: f64,
#[serde(default)]
icons: Icons,
#[serde(flatten)]
pub common: Option<CommonConfig>,
}
fn default_format() -> String {
String::from("{icon} {percentage}%")
}
#[derive(Debug, Clone, Deserialize)]
pub struct Icons {
#[serde(default = "default_icon_volume_high")]
volume_high: String,
#[serde(default = "default_icon_volume_medium")]
volume_medium: String,
#[serde(default = "default_icon_volume_low")]
volume_low: String,
#[serde(default = "default_icon_muted")]
muted: String,
}
impl Icons {
fn volume_icon(&self, volume_percent: f64) -> &str {
match volume_percent as u32 {
0..=33 => &self.volume_low,
34..=66 => &self.volume_medium,
67.. => &self.volume_high,
}
}
}
impl Default for Icons {
fn default() -> Self {
Self {
volume_high: default_icon_volume_high(),
volume_medium: default_icon_volume_medium(),
volume_low: default_icon_volume_low(),
muted: default_icon_muted(),
}
}
}
const fn default_max_volume() -> f64 {
100.0
}
fn default_icon_volume_high() -> String {
String::from("󰕾")
}
fn default_icon_volume_medium() -> String {
String::from("󰖀")
}
fn default_icon_volume_low() -> String {
String::from("󰕿")
}
fn default_icon_muted() -> String {
String::from("󰝟")
}
#[derive(Debug, Clone)]
pub enum Update {
SinkChange(String),
SinkVolume(String, f64),
SinkMute(String, bool),
InputVolume(u32, f64),
InputMute(u32, bool),
}
impl Module<Button> for VolumeModule {
type SendMessage = Event;
type ReceiveMessage = Update;
fn name() -> &'static str {
"volume"
}
fn spawn_controller(
&self,
_info: &ModuleInfo,
context: &WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
mut rx: mpsc::Receiver<Self::ReceiveMessage>,
) -> color_eyre::Result<()>
where
<Self as Module<Button>>::SendMessage: Clone,
{
let client = context.client::<volume::Client>();
{
let client = client.clone();
let mut rx = client.subscribe();
let tx = context.tx.clone();
spawn(async move {
// init
let sinks = {
let sinks = client.sinks();
let sinks = lock!(sinks);
sinks.iter().cloned().collect::<Vec<_>>()
};
let inputs = {
let inputs = client.sink_inputs();
let inputs = lock!(inputs);
inputs.iter().cloned().collect::<Vec<_>>()
};
for sink in sinks {
send_async!(tx, ModuleUpdateEvent::Update(Event::AddSink(sink)));
}
for input in inputs {
send_async!(
tx,
ModuleUpdateEvent::Update(Event::AddInput(input.clone()))
);
}
// recv loop
while let Ok(event) = rx.recv().await {
send_async!(tx, ModuleUpdateEvent::Update(event));
}
});
}
// ui events
spawn(async move {
while let Some(update) = rx.recv().await {
match update {
Update::SinkChange(name) => client.set_default_sink(&name),
Update::SinkVolume(name, volume) => client.set_sink_volume(&name, volume),
Update::SinkMute(name, muted) => client.set_sink_muted(&name, muted),
Update::InputVolume(index, volume) => client.set_input_volume(index, volume),
Update::InputMute(index, muted) => client.set_input_muted(index, muted),
}
}
});
Ok(())
}
fn into_widget(
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> color_eyre::Result<ModuleParts<Button>>
where
<Self as Module<Button>>::SendMessage: Clone,
{
let button = Button::new();
{
let tx = context.tx.clone();
button.connect_clicked(move |button| {
try_send!(tx, ModuleUpdateEvent::TogglePopup(button.popup_id()));
});
}
{
let rx = context.subscribe();
let icons = self.icons.clone();
let button = button.clone();
let format = self.format.clone();
glib_recv!(rx, event => {
match event {
Event::AddSink(sink) | Event::UpdateSink(sink) if sink.active => {
let label = format
.replace("{icon}", if sink.muted { &icons.muted } else { icons.volume_icon(sink.volume) })
.replace("{percentage}", &sink.volume.to_string())
.replace("{name}", &sink.description);
button.set_label(&label);
},
_ => {}
}
});
}
let popup = self
.into_popup(context.controller_tx.clone(), context.subscribe(), info)
.into_popup_parts(vec![&button]);
Ok(ModuleParts::new(button, popup))
}
fn into_popup(
self,
tx: mpsc::Sender<Self::ReceiveMessage>,
rx: tokio::sync::broadcast::Receiver<Self::SendMessage>,
_info: &ModuleInfo,
) -> Option<gtk::Box>
where
Self: Sized,
{
let container = gtk::Box::new(Orientation::Horizontal, 10);
let sink_container = gtk::Box::new(Orientation::Vertical, 5);
sink_container.add_class("device-box");
let input_container = gtk::Box::new(Orientation::Vertical, 5);
input_container.add_class("apps-box");
container.add(&sink_container);
container.add(&input_container);
let sink_selector = ComboBoxText::new();
sink_selector.add_class("device-selector");
let renderer = sink_selector
.cells()
.first()
.expect("to exist")
.clone()
.downcast::<CellRendererText>()
.expect("to be valid cast");
renderer.set_width_chars(20);
renderer.set_ellipsize(EllipsizeMode::End);
{
let tx = tx.clone();
sink_selector.connect_changed(move |selector| {
if let Some(name) = selector.active_id() {
try_send!(tx, Update::SinkChange(name.into()));
}
});
}
sink_container.add(&sink_selector);
let slider = Scale::builder()
.orientation(Orientation::Vertical)
.height_request(100)
.inverted(true)
.build();
slider.add_class("slider");
slider.set_range(0.0, self.max_volume);
slider.set_value(50.0);
sink_container.add(&slider);
{
let tx = tx.clone();
let selector = sink_selector.clone();
slider.connect_button_release_event(move |scale, _| {
if let Some(sink) = selector.active_id() {
// GTK will send values outside min/max range
let val = scale.value().clamp(0.0, self.max_volume);
try_send!(tx, Update::SinkVolume(sink.into(), val));
}
Propagation::Proceed
});
}
let btn_mute = ToggleButton::new();
btn_mute.add_class("btn-mute");
sink_container.add(&btn_mute);
{
let tx = tx.clone();
let selector = sink_selector.clone();
btn_mute.connect_toggled(move |btn| {
if let Some(sink) = selector.active_id() {
let muted = btn.is_active();
try_send!(tx, Update::SinkMute(sink.into(), muted));
}
});
}
container.show_all();
let mut inputs = HashMap::new();
{
let input_container = input_container.clone();
let mut sinks = vec![];
glib_recv!(rx, event => {
match event {
Event::AddSink(info) => {
sink_selector.append(Some(&info.name), &info.description);
if info.active {
sink_selector.set_active(Some(sinks.len() as u32));
slider.set_value(info.volume);
btn_mute.set_active(info.muted);
btn_mute.set_label(if info.muted { &self.icons.muted } else { self.icons.volume_icon(info.volume) });
}
sinks.push(info);
}
Event::UpdateSink(info) => {
if info.active {
if let Some(pos) = sinks.iter().position(|s| s.name == info.name) {
sink_selector.set_active(Some(pos as u32));
slider.set_value(info.volume);
btn_mute.set_active(info.muted);
btn_mute.set_label(if info.muted { &self.icons.muted } else { self.icons.volume_icon(info.volume) });
}
}
}
Event::RemoveSink(name) => {
if let Some(pos) = sinks.iter().position(|s| s.name == name) {
ComboBoxTextExt::remove(&sink_selector, pos as i32);
sinks.remove(pos);
}
}
Event::AddInput(info) => {
let index = info.index;
let item_container = gtk::Box::new(Orientation::Vertical, 0);
item_container.add_class("app-box");
let label = Label::new(Some(&info.name));
label.add_class("title");
let slider = Scale::builder().sensitive(info.can_set_volume).build();
slider.set_range(0.0, self.max_volume);
slider.set_value(info.volume);
slider.add_class("slider");
{
let tx = tx.clone();
slider.connect_button_release_event(move |scale, _| {
// GTK will send values outside min/max range
let val = scale.value().clamp(0.0, self.max_volume);
try_send!(tx, Update::InputVolume(index, val));
Propagation::Proceed
});
}
let btn_mute = ToggleButton::new();
btn_mute.add_class("btn-mute");
btn_mute.set_active(info.muted);
btn_mute.set_label(if info.muted { &self.icons.muted } else { self.icons.volume_icon(info.volume) });
{
let tx = tx.clone();
btn_mute.connect_toggled(move |btn| {
let muted = btn.is_active();
try_send!(tx, Update::InputMute(index, muted));
});
}
item_container.add(&label);
item_container.add(&slider);
item_container.add(&btn_mute);
item_container.show_all();
input_container.add(&item_container);
inputs.insert(info.index, InputUi {
container: item_container,
label,
slider,
btn_mute
});
}
Event::UpdateInput(info) => {
if let Some(ui) = inputs.get(&info.index) {
ui.label.set_label(&info.name);
ui.slider.set_value(info.volume);
ui.slider.set_sensitive(info.can_set_volume);
ui.btn_mute.set_label(if info.muted { &self.icons.muted } else { self.icons.volume_icon(info.volume) });
}
}
Event::RemoveInput(index) => {
if let Some(ui) = inputs.remove(&index) {
input_container.remove(&ui.container);
}
}
}
});
}
Some(container)
}
}
struct InputUi {
container: gtk::Box,
label: Label,
slider: Scale,
btn_mute: ToggleButton,
}

View file

@ -189,7 +189,7 @@ impl Module<gtk::Box> for WorkspacesModule {
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> Result<ModuleParts<gtk::Box>> {
let container = gtk::Box::new(info.bar_position.get_orientation(), 0);
let container = gtk::Box::new(info.bar_position.orientation(), 0);
let name_map = self.name_map.clone().unwrap_or_default();
let favs = self.favorites.clone();
@ -239,10 +239,11 @@ impl Module<gtk::Box> for WorkspacesModule {
let mut add_favourites = |names: &Vec<String>| {
for name in names {
fav_names.push(name.to_string());
if !added.contains(name) {
add_workspace(name, Visibility::Hidden);
added.insert(name.to_string());
fav_names.push(name.to_string());
}
}
};

View file

@ -1,11 +1,13 @@
use glib::Propagation;
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use gtk::gdk::Monitor;
use gtk::prelude::*;
use gtk::{ApplicationWindow, Orientation};
use gtk::{ApplicationWindow, Button, Orientation};
use gtk_layer_shell::LayerShell;
use tracing::debug;
use tracing::{debug, trace};
use crate::config::BarPosition;
use crate::gtk_helpers::{IronbarGtkExt, WidgetGeometry};
@ -21,10 +23,10 @@ pub struct PopupCacheValue {
#[derive(Debug, Clone)]
pub struct Popup {
pub window: ApplicationWindow,
pub cache: HashMap<usize, PopupCacheValue>,
pub cache: Rc<RefCell<HashMap<usize, PopupCacheValue>>>,
monitor: Monitor,
pos: BarPosition,
current_widget: Option<usize>,
current_widget: Rc<RefCell<Option<(usize, usize)>>>,
}
impl Popup {
@ -33,7 +35,7 @@ impl Popup {
/// and an empty `gtk::Box` container.
pub fn new(module_info: &ModuleInfo, gap: i32) -> Self {
let pos = module_info.bar_position;
let orientation = pos.get_orientation();
let orientation = pos.orientation();
let win = ApplicationWindow::builder()
.application(module_info.app)
@ -104,14 +106,14 @@ impl Popup {
Self {
window: win,
cache: HashMap::new(),
cache: Rc::new(RefCell::new(HashMap::new())),
monitor: module_info.monitor.clone(),
pos,
current_widget: None,
current_widget: Rc::new(RefCell::new(None)),
}
}
pub fn register_content(&mut self, key: usize, name: String, content: ModulePopupParts) {
pub fn register_content(&self, key: usize, name: String, content: ModulePopupParts) {
debug!("Registered popup content for #{}", key);
for button in &content.buttons {
@ -119,45 +121,94 @@ impl Popup {
button.set_tag("popup-id", id);
}
self.cache.insert(key, PopupCacheValue { name, content });
let orientation = self.pos.orientation();
let monitor = self.monitor.clone();
let window = self.window.clone();
let current_widget = self.current_widget.clone();
let cache = self.cache.clone();
content
.container
.connect_size_allocate(move |container, rect| {
if container.is_visible() {
trace!("Resized: {}x{}", rect.width(), rect.height());
if let Some((widget_id, button_id)) = *current_widget.borrow() {
if let Some(PopupCacheValue { content, .. }) =
cache.borrow().get(&widget_id)
{
Self::set_position(
&content.buttons,
button_id,
orientation,
&monitor,
&window,
);
}
}
}
});
self.cache
.borrow_mut()
.insert(key, PopupCacheValue { name, content });
}
pub fn show(&mut self, widget_id: usize, button_id: usize) {
pub fn show(&self, widget_id: usize, button_id: usize) {
self.clear_window();
if let Some(PopupCacheValue { content, .. }) = self.cache.get(&widget_id) {
self.current_widget = Some(widget_id);
if let Some(PopupCacheValue { content, .. }) = self.cache.borrow().get(&widget_id) {
*self.current_widget.borrow_mut() = Some((widget_id, button_id));
content.container.style_context().add_class("popup");
self.window.add(&content.container);
self.window.show();
let button = content
.buttons
.iter()
.find(|b| b.popup_id() == button_id)
.expect("to find valid button");
let orientation = self.pos.get_orientation();
let geometry = button.geometry(orientation);
self.set_pos(geometry);
Self::set_position(
&content.buttons,
button_id,
self.pos.orientation(),
&self.monitor,
&self.window,
);
}
}
pub fn show_at(&self, widget_id: usize, geometry: WidgetGeometry) {
self.clear_window();
if let Some(PopupCacheValue { content, .. }) = self.cache.get(&widget_id) {
if let Some(PopupCacheValue { content, .. }) = self.cache.borrow().get(&widget_id) {
content.container.style_context().add_class("popup");
self.window.add(&content.container);
self.window.show();
self.set_pos(geometry);
Self::set_pos(
geometry,
self.pos.orientation(),
&self.monitor,
&self.window,
);
}
}
fn set_position(
buttons: &[Button],
button_id: usize,
orientation: Orientation,
monitor: &Monitor,
window: &ApplicationWindow,
) {
let button = buttons
.iter()
.find(|b| b.popup_id() == button_id)
.expect("to find valid button");
let geometry = button.geometry(orientation);
Self::set_pos(geometry, orientation, monitor, window);
}
fn clear_window(&self) {
let children = self.window.children();
for child in children {
@ -166,8 +217,8 @@ impl Popup {
}
/// Hides the popover
pub fn hide(&mut self) {
self.current_widget = None;
pub fn hide(&self) {
*self.current_widget.borrow_mut() = None;
self.window.hide();
}
@ -177,22 +228,25 @@ impl Popup {
}
pub fn current_widget(&self) -> Option<usize> {
self.current_widget
self.current_widget.borrow().map(|w| w.0)
}
/// Sets the popup's X/Y position relative to the left or border of the screen
/// (depending on orientation).
fn set_pos(&self, geometry: WidgetGeometry) {
let orientation = self.pos.get_orientation();
let mon_workarea = self.monitor.workarea();
fn set_pos(
geometry: WidgetGeometry,
orientation: Orientation,
monitor: &Monitor,
window: &ApplicationWindow,
) {
let mon_workarea = monitor.workarea();
let screen_size = if orientation == Orientation::Horizontal {
mon_workarea.width()
} else {
mon_workarea.height()
};
let (popup_width, popup_height) = self.window.size();
let (popup_width, popup_height) = window.size();
let popup_size = if orientation == Orientation::Horizontal {
popup_width
} else {
@ -217,6 +271,6 @@ impl Popup {
gtk_layer_shell::Edge::Top
};
self.window.set_layer_shell_margin(edge, offset as i32);
window.set_layer_shell_margin(edge, offset as i32);
}
}