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:
commit
4e2352c9e9
22 changed files with 579 additions and 390 deletions
|
@ -1,3 +1,4 @@
|
|||
use crate::Ironbar;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(feature = "clipboard")]
|
||||
|
@ -9,7 +10,7 @@ pub mod music;
|
|||
#[cfg(feature = "notifications")]
|
||||
pub mod swaync;
|
||||
#[cfg(feature = "tray")]
|
||||
pub mod system_tray;
|
||||
pub mod tray;
|
||||
#[cfg(feature = "upower")]
|
||||
pub mod upower;
|
||||
#[cfg(feature = "volume")]
|
||||
|
@ -30,7 +31,7 @@ pub struct Clients {
|
|||
#[cfg(feature = "notifications")]
|
||||
notifications: Option<Arc<swaync::Client>>,
|
||||
#[cfg(feature = "tray")]
|
||||
tray: Option<Arc<system_tray::TrayEventReceiver>>,
|
||||
tray: Option<Arc<tray::Client>>,
|
||||
#[cfg(feature = "upower")]
|
||||
upower: Option<Arc<zbus::fdo::PropertiesProxy<'static>>>,
|
||||
#[cfg(feature = "volume")]
|
||||
|
@ -85,11 +86,17 @@ impl Clients {
|
|||
}
|
||||
|
||||
#[cfg(feature = "tray")]
|
||||
pub fn tray(&mut self) -> Arc<system_tray::TrayEventReceiver> {
|
||||
pub fn tray(&mut self) -> Arc<tray::Client> {
|
||||
// TODO: Error handling here isn't great - should throw a user-friendly error
|
||||
self.tray
|
||||
.get_or_insert_with(|| {
|
||||
Arc::new(crate::await_sync(async {
|
||||
system_tray::create_client().await
|
||||
let service_name =
|
||||
format!("{}-{}", env!("CARGO_CRATE_NAME"), Ironbar::unique_id());
|
||||
|
||||
tray::Client::new(&service_name)
|
||||
.await
|
||||
.expect("to be able to start client")
|
||||
}))
|
||||
})
|
||||
.clone()
|
||||
|
|
|
@ -34,14 +34,15 @@ pub struct Track {
|
|||
pub cover_path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub enum PlayerState {
|
||||
#[default]
|
||||
Stopped,
|
||||
Playing,
|
||||
Paused,
|
||||
Stopped,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct Status {
|
||||
pub state: PlayerState,
|
||||
pub volume_percent: Option<u8>,
|
||||
|
|
|
@ -18,6 +18,11 @@ pub struct Client {
|
|||
_rx: broadcast::Receiver<PlayerUpdate>,
|
||||
}
|
||||
|
||||
const NO_ACTIVE_PLAYER: &str = "com.github.altdesktop.playerctld.NoActivePlayer";
|
||||
const NO_REPLY: &str = "org.freedesktop.DBus.Error.NoReply";
|
||||
const NO_SERVICE: &str = "org.freedesktop.DBus.Error.ServiceUnknown";
|
||||
const NO_METHOD: &str = "org.freedesktop.DBus.Error.UnknownMethod";
|
||||
|
||||
impl Client {
|
||||
pub(crate) fn new() -> Self {
|
||||
let (tx, rx) = broadcast::channel(32);
|
||||
|
@ -35,44 +40,48 @@ impl Client {
|
|||
// D-Bus gives no event for new players,
|
||||
// so we have to keep polling the player list
|
||||
loop {
|
||||
let players = player_finder
|
||||
.find_all()
|
||||
.expect("Failed to connect to D-Bus");
|
||||
// mpris-rs does not filter NoActivePlayer errors, so we have to do it ourselves
|
||||
let players = player_finder.find_all().unwrap_or_else(|e| match e {
|
||||
mpris::FindingError::DBusError(DBusError::TransportError(
|
||||
transport_error,
|
||||
)) if transport_error.name() == Some(NO_ACTIVE_PLAYER)
|
||||
|| transport_error.name() == Some(NO_REPLY) =>
|
||||
{
|
||||
Vec::new()
|
||||
}
|
||||
_ => 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.
|
||||
{
|
||||
let mut current_player_lock = lock!(current_player);
|
||||
|
||||
let mut players_list_val = lock!(players_list);
|
||||
for player in players {
|
||||
let identity = player.identity();
|
||||
let mut players_list_val = lock!(players_list);
|
||||
for player in players {
|
||||
let identity = player.identity();
|
||||
|
||||
if !players_list_val.contains(identity) {
|
||||
debug!("Adding MPRIS player '{identity}'");
|
||||
players_list_val.insert(identity.to_string());
|
||||
if current_player_lock.is_none() {
|
||||
debug!("Setting active player to '{identity}'");
|
||||
current_player_lock.replace(identity.to_string());
|
||||
|
||||
let status = player
|
||||
.get_playback_status()
|
||||
.expect("Failed to connect to D-Bus");
|
||||
|
||||
{
|
||||
let mut current_player = lock!(current_player);
|
||||
|
||||
if status == PlaybackStatus::Playing || current_player.is_none() {
|
||||
debug!("Setting active player to '{identity}'");
|
||||
|
||||
current_player.replace(identity.to_string());
|
||||
if let Err(err) = Self::send_update(&player, &tx) {
|
||||
error!("{err:?}");
|
||||
}
|
||||
if let Err(err) = Self::send_update(&player, &tx) {
|
||||
error!("{err:?}");
|
||||
}
|
||||
}
|
||||
if !players_list_val.contains(identity) {
|
||||
debug!("Adding MPRIS player '{identity}'");
|
||||
players_list_val.insert(identity.to_string());
|
||||
|
||||
Self::listen_player_events(
|
||||
identity.to_string(),
|
||||
players_list.clone(),
|
||||
current_player.clone(),
|
||||
tx.clone(),
|
||||
);
|
||||
Self::listen_player_events(
|
||||
identity.to_string(),
|
||||
players_list.clone(),
|
||||
current_player.clone(),
|
||||
tx.clone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// wait 1 second before re-checking players
|
||||
sleep(Duration::from_secs(1));
|
||||
}
|
||||
|
@ -111,28 +120,56 @@ impl Client {
|
|||
|
||||
if let Ok(player) = player_finder.find_by_name(&player_id) {
|
||||
let identity = player.identity();
|
||||
let handle_shutdown = |current_player_lock_option: Option<
|
||||
std::sync::MutexGuard<'_, Option<String>>,
|
||||
>| {
|
||||
debug!("Player '{identity}' shutting down");
|
||||
// Lock of player before players (see new() to make sure order is consistent)
|
||||
if let Some(mut guard) = current_player_lock_option {
|
||||
guard.take();
|
||||
} else {
|
||||
lock!(current_player).take();
|
||||
}
|
||||
let mut players_locked = lock!(players);
|
||||
players_locked.remove(identity);
|
||||
if players_locked.is_empty() {
|
||||
send!(tx, PlayerUpdate::Update(Box::new(None), Status::default()));
|
||||
}
|
||||
};
|
||||
|
||||
for event in player.events()? {
|
||||
trace!("Received player event from '{identity}': {event:?}");
|
||||
match event {
|
||||
Ok(Event::PlayerShutDown) => {
|
||||
lock!(current_player).take();
|
||||
lock!(players).remove(identity);
|
||||
handle_shutdown(None);
|
||||
break;
|
||||
}
|
||||
Ok(Event::Playing) => {
|
||||
lock!(current_player).replace(identity.to_string());
|
||||
|
||||
if let Err(err) = Self::send_update(&player, &tx) {
|
||||
error!("{err:?}");
|
||||
}
|
||||
Err(mpris::EventError::DBusError(DBusError::TransportError(
|
||||
transport_error,
|
||||
))) if transport_error.name() == Some(NO_ACTIVE_PLAYER)
|
||||
|| transport_error.name() == Some(NO_REPLY)
|
||||
|| transport_error.name() == Some(NO_METHOD)
|
||||
|| transport_error.name() == Some(NO_SERVICE) =>
|
||||
{
|
||||
handle_shutdown(None);
|
||||
break;
|
||||
}
|
||||
Ok(_) => {
|
||||
let current_player = lock!(current_player);
|
||||
let current_player = current_player.as_ref();
|
||||
if let Some(current_player) = current_player {
|
||||
if current_player == identity {
|
||||
let mut current_player_lock = lock!(current_player);
|
||||
if matches!(event, Ok(Event::Playing)) {
|
||||
current_player_lock.replace(identity.to_string());
|
||||
}
|
||||
if let Some(current_identity) = current_player_lock.as_ref() {
|
||||
if current_identity == identity {
|
||||
if let Err(err) = Self::send_update(&player, &tx) {
|
||||
if let Some(DBusError::TransportError(transport_error)) =
|
||||
err.downcast_ref::<DBusError>()
|
||||
{
|
||||
if transport_error.name() == Some(NO_SERVICE) {
|
||||
handle_shutdown(Some(current_player_lock));
|
||||
break;
|
||||
}
|
||||
}
|
||||
error!("{err:?}");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,127 +0,0 @@
|
|||
use crate::{arc_mut, lock, register_client, send, spawn, Ironbar};
|
||||
use color_eyre::Report;
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use system_tray::message::menu::TrayMenu;
|
||||
use system_tray::message::tray::StatusNotifierItem;
|
||||
use system_tray::message::{NotifierItemCommand, NotifierItemMessage};
|
||||
use system_tray::StatusNotifierWatcher;
|
||||
use tokio::sync::{broadcast, mpsc};
|
||||
use tracing::{debug, error, trace};
|
||||
|
||||
type Tray = BTreeMap<String, (Box<StatusNotifierItem>, Option<TrayMenu>)>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TrayEventReceiver {
|
||||
tx: mpsc::Sender<NotifierItemCommand>,
|
||||
b_tx: broadcast::Sender<NotifierItemMessage>,
|
||||
_b_rx: broadcast::Receiver<NotifierItemMessage>,
|
||||
|
||||
tray: Arc<Mutex<Tray>>,
|
||||
}
|
||||
|
||||
impl TrayEventReceiver {
|
||||
async fn new() -> system_tray::error::Result<Self> {
|
||||
let id = format!("ironbar-{}", Ironbar::unique_id());
|
||||
|
||||
let (tx, rx) = mpsc::channel(16);
|
||||
let (b_tx, b_rx) = broadcast::channel(64);
|
||||
|
||||
let tray = StatusNotifierWatcher::new(rx).await?;
|
||||
let mut host = Box::pin(tray.create_notifier_host(&id)).await?;
|
||||
|
||||
let tray = arc_mut!(BTreeMap::new());
|
||||
|
||||
{
|
||||
let b_tx = b_tx.clone();
|
||||
let tray = tray.clone();
|
||||
|
||||
spawn(async move {
|
||||
while let Ok(message) = host.recv().await {
|
||||
trace!("Received message: {message:?}");
|
||||
|
||||
send!(b_tx, message.clone());
|
||||
let mut tray = lock!(tray);
|
||||
match message {
|
||||
NotifierItemMessage::Update {
|
||||
address,
|
||||
item,
|
||||
menu,
|
||||
} => {
|
||||
debug!("Adding/updating item with address '{address}'");
|
||||
tray.insert(address, (item, menu));
|
||||
}
|
||||
NotifierItemMessage::Remove { address } => {
|
||||
debug!("Removing item with address '{address}'");
|
||||
tray.remove(&address);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok::<(), broadcast::error::SendError<NotifierItemMessage>>(())
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
tx,
|
||||
b_tx,
|
||||
_b_rx: b_rx,
|
||||
tray,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn subscribe(
|
||||
&self,
|
||||
) -> (
|
||||
mpsc::Sender<NotifierItemCommand>,
|
||||
broadcast::Receiver<NotifierItemMessage>,
|
||||
) {
|
||||
let tx = self.tx.clone();
|
||||
let b_rx = self.b_tx.subscribe();
|
||||
|
||||
let tray = lock!(self.tray).clone();
|
||||
for (address, (item, menu)) in tray {
|
||||
let update = NotifierItemMessage::Update {
|
||||
address,
|
||||
item,
|
||||
menu,
|
||||
};
|
||||
send!(self.b_tx, update);
|
||||
}
|
||||
|
||||
(tx, b_rx)
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to create a new `TrayEventReceiver` instance,
|
||||
/// retrying a maximum of 10 times before panicking the thread.
|
||||
pub async fn create_client() -> TrayEventReceiver {
|
||||
const MAX_RETRIES: i32 = 10;
|
||||
|
||||
// sometimes this can fail
|
||||
let mut retries = 0;
|
||||
|
||||
let value = loop {
|
||||
retries += 1;
|
||||
|
||||
let tray = Box::pin(TrayEventReceiver::new()).await;
|
||||
|
||||
match tray {
|
||||
Ok(tray) => break Some(tray),
|
||||
Err(err) => error!(
|
||||
"{:?}",
|
||||
Report::new(err).wrap_err(format!(
|
||||
"Failed to create StatusNotifierWatcher (attempt {retries})"
|
||||
))
|
||||
),
|
||||
}
|
||||
|
||||
if retries == MAX_RETRIES {
|
||||
break None;
|
||||
}
|
||||
};
|
||||
|
||||
value.expect("Failed to create StatusNotifierWatcher")
|
||||
}
|
||||
|
||||
register_client!(TrayEventReceiver, tray);
|
4
src/clients/tray.rs
Normal file
4
src/clients/tray.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
use crate::register_client;
|
||||
pub use system_tray::client::Client;
|
||||
|
||||
register_client!(Client, tray);
|
|
@ -98,6 +98,7 @@ pub enum Response {
|
|||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
struct BroadcastChannel<T>(broadcast::Sender<T>, Arc<Mutex<broadcast::Receiver<T>>>);
|
||||
|
||||
impl<T> From<(broadcast::Sender<T>, broadcast::Receiver<T>)> for BroadcastChannel<T> {
|
||||
|
|
|
@ -11,7 +11,7 @@ use gtk::{IconLookupFlags, IconTheme};
|
|||
use std::path::{Path, PathBuf};
|
||||
#[cfg(feature = "http")]
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::warn;
|
||||
use tracing::{debug, warn};
|
||||
|
||||
cfg_if!(
|
||||
if #[cfg(feature = "http")] {
|
||||
|
@ -45,6 +45,7 @@ impl<'a> ImageProvider<'a> {
|
|||
/// but no other check is performed.
|
||||
pub fn parse(input: &str, theme: &'a IconTheme, use_fallback: bool, size: i32) -> Option<Self> {
|
||||
let location = Self::get_location(input, theme, size, use_fallback, 0)?;
|
||||
debug!("Resolved {input} --> {location:?} (size: {size})");
|
||||
|
||||
Some(Self { location, size })
|
||||
}
|
||||
|
@ -171,7 +172,7 @@ impl<'a> ImageProvider<'a> {
|
|||
);
|
||||
|
||||
// Different error types makes this a bit awkward
|
||||
match pixbuf.map(|pixbuf| Self::create_and_load_surface(&pixbuf, &image, scale))
|
||||
match pixbuf.map(|pixbuf| Self::create_and_load_surface(&pixbuf, &image))
|
||||
{
|
||||
Ok(Err(err)) => error!("{err:?}"),
|
||||
Err(err) => error!("{err:?}"),
|
||||
|
@ -202,7 +203,7 @@ impl<'a> ImageProvider<'a> {
|
|||
_ => unreachable!(), // handled above
|
||||
}?;
|
||||
|
||||
Self::create_and_load_surface(&pixbuf, image, scale)
|
||||
Self::create_and_load_surface(&pixbuf, image)
|
||||
}
|
||||
|
||||
/// Attempts to create a Cairo surface from the provided `Pixbuf`,
|
||||
|
@ -210,10 +211,13 @@ impl<'a> ImageProvider<'a> {
|
|||
/// The surface is then loaded into the provided image.
|
||||
///
|
||||
/// This is necessary for HiDPI since `Pixbuf`s are always treated as scale factor 1.
|
||||
fn create_and_load_surface(pixbuf: &Pixbuf, image: >k::Image, scale: i32) -> Result<()> {
|
||||
pub fn create_and_load_surface(pixbuf: &Pixbuf, image: >k::Image) -> Result<()> {
|
||||
let surface = unsafe {
|
||||
let ptr =
|
||||
gdk_cairo_surface_create_from_pixbuf(pixbuf.as_ptr(), scale, std::ptr::null_mut());
|
||||
let ptr = gdk_cairo_surface_create_from_pixbuf(
|
||||
pixbuf.as_ptr(),
|
||||
image.scale_factor(),
|
||||
std::ptr::null_mut(),
|
||||
);
|
||||
Surface::from_raw_full(ptr)
|
||||
}?;
|
||||
|
||||
|
|
|
@ -181,6 +181,13 @@ macro_rules! arc_rw {
|
|||
};
|
||||
}
|
||||
|
||||
/// Wraps `val` in a new `Rc<RefCell<T>>`.
|
||||
///
|
||||
/// # Usage
|
||||
///
|
||||
/// ```rs
|
||||
/// let val = rc_mut!(MyService::new())
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! rc_mut {
|
||||
($val:expr) => {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
use system_tray::message::menu::{MenuItem as MenuItemInfo, ToggleState};
|
||||
use system_tray::menu::{MenuItem, ToggleState};
|
||||
|
||||
/// Diff change type and associated info.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Diff {
|
||||
Add(MenuItemInfo),
|
||||
Add(MenuItem),
|
||||
Update(i32, MenuItemDiff),
|
||||
Remove(i32),
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ pub enum Diff {
|
|||
#[derive(Debug, Clone)]
|
||||
pub struct MenuItemDiff {
|
||||
/// Text of the item,
|
||||
pub label: Option<String>,
|
||||
pub label: Option<Option<String>>,
|
||||
/// Whether the item can be activated or not.
|
||||
pub enabled: Option<bool>,
|
||||
/// True if the item is visible in the menu.
|
||||
|
@ -29,7 +29,7 @@ pub struct MenuItemDiff {
|
|||
}
|
||||
|
||||
impl MenuItemDiff {
|
||||
fn new(old: &MenuItemInfo, new: &MenuItemInfo) -> Self {
|
||||
fn new(old: &MenuItem, new: &MenuItem) -> Self {
|
||||
macro_rules! diff {
|
||||
($field:ident) => {
|
||||
if old.$field == new.$field {
|
||||
|
@ -70,7 +70,7 @@ impl MenuItemDiff {
|
|||
}
|
||||
|
||||
/// Gets a diff set between old and new state.
|
||||
pub fn get_diffs(old: &[MenuItemInfo], new: &[MenuItemInfo]) -> Vec<Diff> {
|
||||
pub fn get_diffs(old: &[MenuItem], new: &[MenuItem]) -> Vec<Diff> {
|
||||
let mut diffs = vec![];
|
||||
|
||||
for new_item in new {
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
use crate::image::ImageProvider;
|
||||
use crate::modules::tray::interface::TrayMenu;
|
||||
use color_eyre::{Report, Result};
|
||||
use glib::ffi::g_strfreev;
|
||||
use glib::translate::ToGlibPtr;
|
||||
use gtk::ffi::gtk_icon_theme_get_search_path;
|
||||
use gtk::gdk_pixbuf::{Colorspace, InterpType};
|
||||
use gtk::gdk_pixbuf::{Colorspace, InterpType, Pixbuf};
|
||||
use gtk::prelude::IconThemeExt;
|
||||
use gtk::{gdk_pixbuf, IconLookupFlags, IconTheme, Image};
|
||||
use gtk::{IconLookupFlags, IconTheme, Image};
|
||||
use std::collections::HashSet;
|
||||
use std::ffi::CStr;
|
||||
use std::os::raw::{c_char, c_int};
|
||||
use std::ptr;
|
||||
use system_tray::message::tray::StatusNotifierItem;
|
||||
|
||||
/// Gets the GTK icon theme search paths by calling the FFI function.
|
||||
/// Conveniently returns the result as a `HashSet`.
|
||||
|
@ -36,40 +38,60 @@ fn get_icon_theme_search_paths(icon_theme: &IconTheme) -> HashSet<String> {
|
|||
paths
|
||||
}
|
||||
|
||||
pub fn get_image(
|
||||
item: &TrayMenu,
|
||||
icon_theme: &IconTheme,
|
||||
size: u32,
|
||||
prefer_icons: bool,
|
||||
) -> Result<Image> {
|
||||
if !prefer_icons && item.icon_pixmap.is_some() {
|
||||
get_image_from_pixmap(item, size)
|
||||
} else {
|
||||
get_image_from_icon_name(item, icon_theme, size)
|
||||
.or_else(|_| get_image_from_pixmap(item, size))
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to get a GTK `Image` component
|
||||
/// for the status notifier item's icon.
|
||||
pub(crate) fn get_image_from_icon_name(
|
||||
item: &StatusNotifierItem,
|
||||
icon_theme: &IconTheme,
|
||||
) -> Option<Image> {
|
||||
fn get_image_from_icon_name(item: &TrayMenu, icon_theme: &IconTheme, size: u32) -> Result<Image> {
|
||||
if let Some(path) = item.icon_theme_path.as_ref() {
|
||||
if !path.is_empty() && !get_icon_theme_search_paths(icon_theme).contains(path) {
|
||||
icon_theme.append_search_path(path);
|
||||
}
|
||||
}
|
||||
|
||||
item.icon_name.as_ref().and_then(|icon_name| {
|
||||
let icon_info = icon_theme.lookup_icon(icon_name, 16, IconLookupFlags::empty());
|
||||
icon_info.map(|icon_info| Image::from_pixbuf(icon_info.load_icon().ok().as_ref()))
|
||||
})
|
||||
let icon_info = item.icon_name.as_ref().and_then(|icon_name| {
|
||||
icon_theme.lookup_icon(icon_name, size as i32, IconLookupFlags::empty())
|
||||
});
|
||||
|
||||
if let Some(icon_info) = icon_info {
|
||||
let pixbuf = icon_info.load_icon()?;
|
||||
let image = Image::new();
|
||||
ImageProvider::create_and_load_surface(&pixbuf, &image)?;
|
||||
Ok(image)
|
||||
} else {
|
||||
Err(Report::msg("could not find icon"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to get an image from the item pixmap.
|
||||
///
|
||||
/// The pixmap is supplied in ARGB32 format,
|
||||
/// which has 8 bits per sample and a bit stride of `4*width`.
|
||||
pub(crate) fn get_image_from_pixmap(item: &StatusNotifierItem) -> Option<Image> {
|
||||
fn get_image_from_pixmap(item: &TrayMenu, size: u32) -> Result<Image> {
|
||||
const BITS_PER_SAMPLE: i32 = 8;
|
||||
|
||||
let pixmap = item
|
||||
.icon_pixmap
|
||||
.as_ref()
|
||||
.and_then(|pixmap| pixmap.first())?;
|
||||
.and_then(|pixmap| pixmap.first())
|
||||
.ok_or_else(|| Report::msg("Failed to get pixmap from tray icon"))?;
|
||||
|
||||
let bytes = glib::Bytes::from(&pixmap.pixels);
|
||||
let row_stride = pixmap.width * 4; //
|
||||
let row_stride = pixmap.width * 4;
|
||||
|
||||
let pixbuf = gdk_pixbuf::Pixbuf::from_bytes(
|
||||
let pixbuf = Pixbuf::from_bytes(
|
||||
&bytes,
|
||||
Colorspace::Rgb,
|
||||
true,
|
||||
|
@ -80,7 +102,10 @@ pub(crate) fn get_image_from_pixmap(item: &StatusNotifierItem) -> Option<Image>
|
|||
);
|
||||
|
||||
let pixbuf = pixbuf
|
||||
.scale_simple(16, 16, InterpType::Bilinear)
|
||||
.scale_simple(size as i32, size as i32, InterpType::Bilinear)
|
||||
.unwrap_or(pixbuf);
|
||||
Some(Image::from_pixbuf(Some(&pixbuf)))
|
||||
|
||||
let image = Image::new();
|
||||
ImageProvider::create_and_load_surface(&pixbuf, &image)?;
|
||||
Ok(image)
|
||||
}
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
use crate::modules::tray::diff::{Diff, MenuItemDiff};
|
||||
use super::diff::{Diff, MenuItemDiff};
|
||||
use crate::{spawn, try_send};
|
||||
use glib::Propagation;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{CheckMenuItem, Image, Label, Menu, MenuItem, SeparatorMenuItem};
|
||||
use std::collections::HashMap;
|
||||
use system_tray::message::menu::{MenuItem as MenuItemInfo, MenuType, ToggleState, ToggleType};
|
||||
use system_tray::message::NotifierItemCommand;
|
||||
use system_tray::client::ActivateRequest;
|
||||
use system_tray::item::{IconPixmap, StatusNotifierItem};
|
||||
use system_tray::menu::{MenuItem as MenuItemInfo, MenuType, ToggleState, ToggleType};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
/// Calls a method on the underlying widget,
|
||||
|
@ -49,37 +51,47 @@ macro_rules! call {
|
|||
|
||||
/// Main tray icon to show on the bar
|
||||
pub(crate) struct TrayMenu {
|
||||
pub(crate) widget: MenuItem,
|
||||
pub widget: MenuItem,
|
||||
menu_widget: Menu,
|
||||
image_widget: Option<Image>,
|
||||
label_widget: Option<Label>,
|
||||
|
||||
menu: HashMap<i32, TrayMenuItem>,
|
||||
state: Vec<MenuItemInfo>,
|
||||
icon_name: Option<String>,
|
||||
|
||||
pub title: Option<String>,
|
||||
pub icon_name: Option<String>,
|
||||
pub icon_theme_path: Option<String>,
|
||||
pub icon_pixmap: Option<Vec<IconPixmap>>,
|
||||
|
||||
tx: mpsc::Sender<i32>,
|
||||
}
|
||||
|
||||
impl TrayMenu {
|
||||
pub fn new(tx: mpsc::Sender<NotifierItemCommand>, address: String, path: String) -> Self {
|
||||
pub fn new(
|
||||
tx: mpsc::Sender<ActivateRequest>,
|
||||
address: String,
|
||||
item: StatusNotifierItem,
|
||||
) -> Self {
|
||||
let widget = MenuItem::new();
|
||||
widget.style_context().add_class("item");
|
||||
|
||||
let (item_tx, mut item_rx) = mpsc::channel(8);
|
||||
|
||||
spawn(async move {
|
||||
while let Some(id) = item_rx.recv().await {
|
||||
try_send!(
|
||||
tx,
|
||||
NotifierItemCommand::MenuItemClicked {
|
||||
submenu_id: id,
|
||||
menu_path: path.clone(),
|
||||
notifier_address: address.clone(),
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
if let Some(menu) = item.menu {
|
||||
spawn(async move {
|
||||
while let Some(id) = item_rx.recv().await {
|
||||
try_send!(
|
||||
tx,
|
||||
ActivateRequest {
|
||||
submenu_id: id,
|
||||
menu_path: menu.clone(),
|
||||
address: address.clone(),
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let menu = Menu::new();
|
||||
widget.set_submenu(Some(&menu));
|
||||
|
@ -90,7 +102,10 @@ impl TrayMenu {
|
|||
image_widget: None,
|
||||
label_widget: None,
|
||||
state: vec![],
|
||||
icon_name: None,
|
||||
title: item.title,
|
||||
icon_name: item.icon_name,
|
||||
icon_theme_path: item.icon_theme_path,
|
||||
icon_pixmap: item.icon_pixmap,
|
||||
menu: HashMap::new(),
|
||||
tx: item_tx,
|
||||
}
|
||||
|
@ -112,6 +127,18 @@ impl TrayMenu {
|
|||
.set_label(text);
|
||||
}
|
||||
|
||||
/// Shows the label, using its current text.
|
||||
/// The image is hidden if present.
|
||||
pub fn show_label(&self) {
|
||||
if let Some(image) = &self.image_widget {
|
||||
image.hide();
|
||||
}
|
||||
|
||||
if let Some(label) = &self.label_widget {
|
||||
label.show();
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the image, and shows it in favour of the label.
|
||||
pub fn set_image(&mut self, image: &Image) {
|
||||
if let Some(label) = &self.label_widget {
|
||||
|
@ -134,6 +161,7 @@ impl TrayMenu {
|
|||
let item = TrayMenuItem::new(&info, self.tx.clone());
|
||||
call!(self.menu_widget, add, item.widget);
|
||||
self.menu.insert(item.id, item);
|
||||
// self.widget.show_all();
|
||||
}
|
||||
Diff::Update(id, info) => {
|
||||
if let Some(item) = self.menu.get_mut(&id) {
|
||||
|
@ -188,36 +216,61 @@ enum TrayMenuWidget {
|
|||
|
||||
impl TrayMenuItem {
|
||||
fn new(info: &MenuItemInfo, tx: mpsc::Sender<i32>) -> Self {
|
||||
let mut submenu = HashMap::new();
|
||||
let menu = Menu::new();
|
||||
|
||||
macro_rules! add_submenu {
|
||||
($menu:expr, $widget:expr) => {
|
||||
if !info.submenu.is_empty() {
|
||||
for sub_item in &info.submenu {
|
||||
let sub_item = TrayMenuItem::new(sub_item, tx.clone());
|
||||
call!($menu, add, sub_item.widget);
|
||||
submenu.insert(sub_item.id, sub_item);
|
||||
}
|
||||
|
||||
$widget.set_submenu(Some(&menu));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let widget = match (info.menu_type, info.toggle_type) {
|
||||
(MenuType::Separator, _) => TrayMenuWidget::Separator(SeparatorMenuItem::new()),
|
||||
(MenuType::Standard, ToggleType::Checkmark) => {
|
||||
let widget = CheckMenuItem::builder()
|
||||
.label(info.label.as_str())
|
||||
.visible(info.visible)
|
||||
.sensitive(info.enabled)
|
||||
.active(info.toggle_state == ToggleState::On)
|
||||
.build();
|
||||
|
||||
if let Some(label) = &info.label {
|
||||
widget.set_label(label);
|
||||
}
|
||||
|
||||
add_submenu!(menu, widget);
|
||||
|
||||
{
|
||||
let tx = tx.clone();
|
||||
let id = info.id;
|
||||
|
||||
widget.connect_activate(move |_item| {
|
||||
widget.connect_button_press_event(move |_item, _button| {
|
||||
try_send!(tx, id);
|
||||
Propagation::Proceed
|
||||
});
|
||||
}
|
||||
|
||||
TrayMenuWidget::Checkbox(widget)
|
||||
}
|
||||
(MenuType::Standard, _) => {
|
||||
let builder = MenuItem::builder()
|
||||
.label(&info.label)
|
||||
let widget = MenuItem::builder()
|
||||
.visible(info.visible)
|
||||
.sensitive(info.enabled);
|
||||
.sensitive(info.enabled)
|
||||
.build();
|
||||
|
||||
let widget = builder.build();
|
||||
if let Some(label) = &info.label {
|
||||
widget.set_label(label);
|
||||
}
|
||||
|
||||
add_submenu!(menu, widget);
|
||||
|
||||
{
|
||||
let tx = tx.clone();
|
||||
|
@ -236,7 +289,7 @@ impl TrayMenuItem {
|
|||
id: info.id,
|
||||
widget,
|
||||
menu_widget: menu,
|
||||
submenu: HashMap::new(),
|
||||
submenu,
|
||||
tx,
|
||||
}
|
||||
}
|
||||
|
@ -247,6 +300,7 @@ impl TrayMenuItem {
|
|||
/// applying the submenu diffs to any further submenu items.
|
||||
fn apply_diff(&mut self, diff: MenuItemDiff) {
|
||||
if let Some(label) = diff.label {
|
||||
let label = label.unwrap_or_default();
|
||||
match &self.widget {
|
||||
TrayMenuWidget::Separator(widget) => widget.set_label(&label),
|
||||
TrayMenuWidget::Standard(widget) => widget.set_label(&label),
|
||||
|
|
|
@ -2,28 +2,41 @@ mod diff;
|
|||
mod icon;
|
||||
mod interface;
|
||||
|
||||
use crate::clients::system_tray::TrayEventReceiver;
|
||||
use crate::clients::tray;
|
||||
use crate::config::CommonConfig;
|
||||
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 crate::{glib_recv, lock, send_async, spawn};
|
||||
use color_eyre::{Report, Result};
|
||||
use gtk::{prelude::*, PackDirection};
|
||||
use gtk::{IconTheme, MenuBar};
|
||||
use interface::TrayMenu;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use system_tray::message::{NotifierItemCommand, NotifierItemMessage};
|
||||
use system_tray::client::Event;
|
||||
use system_tray::client::{ActivateRequest, UpdateEvent};
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{debug, error, warn};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct TrayModule {
|
||||
#[serde(default = "crate::config::default_true")]
|
||||
prefer_theme_icons: bool,
|
||||
|
||||
#[serde(default = "default_icon_size")]
|
||||
icon_size: u32,
|
||||
|
||||
#[serde(default, deserialize_with = "deserialize_orientation")]
|
||||
pub direction: Option<PackDirection>,
|
||||
direction: Option<PackDirection>,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub common: Option<CommonConfig>,
|
||||
}
|
||||
|
||||
const fn default_icon_size() -> u32 {
|
||||
16
|
||||
}
|
||||
|
||||
fn deserialize_orientation<'de, D>(deserializer: D) -> Result<Option<PackDirection>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
|
@ -41,8 +54,8 @@ where
|
|||
}
|
||||
|
||||
impl Module<MenuBar> for TrayModule {
|
||||
type SendMessage = NotifierItemMessage;
|
||||
type ReceiveMessage = NotifierItemCommand;
|
||||
type SendMessage = Event;
|
||||
type ReceiveMessage = ActivateRequest;
|
||||
|
||||
fn name() -> &'static str {
|
||||
"tray"
|
||||
|
@ -56,26 +69,39 @@ impl Module<MenuBar> for TrayModule {
|
|||
) -> Result<()> {
|
||||
let tx = context.tx.clone();
|
||||
|
||||
let client = context.client::<TrayEventReceiver>();
|
||||
let client = context.client::<tray::Client>();
|
||||
let mut tray_rx = client.subscribe();
|
||||
|
||||
let (tray_tx, mut tray_rx) = client.subscribe();
|
||||
let initial_items = lock!(client.items()).clone();
|
||||
|
||||
// listen to tray updates
|
||||
spawn(async move {
|
||||
while let Ok(message) = tray_rx.recv().await {
|
||||
tx.send(ModuleUpdateEvent::Update(message)).await?;
|
||||
for (key, (item, menu)) in initial_items.into_iter() {
|
||||
send_async!(
|
||||
tx,
|
||||
ModuleUpdateEvent::Update(Event::Add(key.clone(), item.into()))
|
||||
);
|
||||
|
||||
if let Some(menu) = menu.clone() {
|
||||
send_async!(
|
||||
tx,
|
||||
ModuleUpdateEvent::Update(Event::Update(key, UpdateEvent::Menu(menu)))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok::<(), mpsc::error::SendError<ModuleUpdateEvent<Self::SendMessage>>>(())
|
||||
while let Ok(message) = tray_rx.recv().await {
|
||||
send_async!(tx, ModuleUpdateEvent::Update(message))
|
||||
}
|
||||
});
|
||||
|
||||
// send tray commands
|
||||
spawn(async move {
|
||||
while let Some(cmd) = rx.recv().await {
|
||||
tray_tx.send(cmd).await?;
|
||||
client.activate(cmd).await?;
|
||||
}
|
||||
|
||||
Ok::<(), mpsc::error::SendError<NotifierItemCommand>>(())
|
||||
Ok::<_, Report>(())
|
||||
});
|
||||
|
||||
Ok(())
|
||||
|
@ -106,7 +132,7 @@ impl Module<MenuBar> for TrayModule {
|
|||
|
||||
// listen for UI updates
|
||||
glib_recv!(context.subscribe(), update =>
|
||||
on_update(update, &container, &mut menus, &icon_theme, &context.controller_tx)
|
||||
on_update(update, &container, &mut menus, &icon_theme, self.icon_size, self.prefer_theme_icons, &context.controller_tx)
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -120,53 +146,81 @@ impl Module<MenuBar> for TrayModule {
|
|||
/// Handles UI updates as callback,
|
||||
/// getting the diff since the previous update and applying it to the menu.
|
||||
fn on_update(
|
||||
update: NotifierItemMessage,
|
||||
update: Event,
|
||||
container: &MenuBar,
|
||||
menus: &mut HashMap<Box<str>, TrayMenu>,
|
||||
icon_theme: &IconTheme,
|
||||
tx: &mpsc::Sender<NotifierItemCommand>,
|
||||
icon_size: u32,
|
||||
prefer_icons: bool,
|
||||
tx: &mpsc::Sender<ActivateRequest>,
|
||||
) {
|
||||
match update {
|
||||
NotifierItemMessage::Update {
|
||||
item,
|
||||
address,
|
||||
menu,
|
||||
} => {
|
||||
if let (Some(menu_opts), Some(menu_path)) = (menu, &item.menu) {
|
||||
let submenus = menu_opts.submenus;
|
||||
Event::Add(address, item) => {
|
||||
debug!("Received new tray item at '{address}': {item:?}");
|
||||
|
||||
let mut menu_item = menus.remove(address.as_str()).unwrap_or_else(|| {
|
||||
let item = TrayMenu::new(tx.clone(), address.clone(), menu_path.to_string());
|
||||
container.add(&item.widget);
|
||||
let mut menu_item = TrayMenu::new(tx.clone(), address.clone(), *item);
|
||||
container.add(&menu_item.widget);
|
||||
|
||||
item
|
||||
});
|
||||
|
||||
let label = item.title.as_ref().unwrap_or(&address);
|
||||
if let Some(label_widget) = menu_item.label_widget() {
|
||||
label_widget.set_label(label);
|
||||
match icon::get_image(&menu_item, icon_theme, icon_size, prefer_icons) {
|
||||
Ok(image) => menu_item.set_image(&image),
|
||||
Err(_) => {
|
||||
let label = menu_item.title.clone().unwrap_or(address.clone());
|
||||
menu_item.set_label(&label)
|
||||
}
|
||||
};
|
||||
|
||||
if item.icon_name.as_ref() != menu_item.icon_name() {
|
||||
match icon::get_image_from_icon_name(&item, icon_theme)
|
||||
.or_else(|| icon::get_image_from_pixmap(&item))
|
||||
{
|
||||
Some(image) => menu_item.set_image(&image),
|
||||
None => menu_item.set_label(label),
|
||||
};
|
||||
menu_item.widget.show();
|
||||
menus.insert(address.into(), menu_item);
|
||||
}
|
||||
Event::Update(address, update) => {
|
||||
debug!("Received tray update for '{address}': {update:?}");
|
||||
|
||||
let Some(menu_item) = menus.get_mut(address.as_str()) else {
|
||||
error!("Attempted to update menu at '{address}' but could not find it");
|
||||
return;
|
||||
};
|
||||
|
||||
match update {
|
||||
UpdateEvent::AttentionIcon(_icon) => {
|
||||
warn!("received unimplemented NewAttentionIcon event");
|
||||
}
|
||||
UpdateEvent::Icon(icon) => {
|
||||
if icon.as_ref() != menu_item.icon_name() {
|
||||
match icon::get_image(menu_item, icon_theme, icon_size, prefer_icons) {
|
||||
Ok(image) => menu_item.set_image(&image),
|
||||
Err(_) => menu_item.show_label(),
|
||||
};
|
||||
}
|
||||
|
||||
let diffs = get_diffs(menu_item.state(), &submenus);
|
||||
menu_item.apply_diffs(diffs);
|
||||
menu_item.widget.show();
|
||||
menu_item.set_icon_name(icon);
|
||||
}
|
||||
UpdateEvent::OverlayIcon(_icon) => {
|
||||
warn!("received unimplemented NewOverlayIcon event");
|
||||
}
|
||||
UpdateEvent::Status(_status) => {
|
||||
warn!("received unimplemented NewStatus event");
|
||||
}
|
||||
UpdateEvent::Title(title) => {
|
||||
if let Some(label_widget) = menu_item.label_widget() {
|
||||
label_widget.set_label(&title.unwrap_or_default());
|
||||
}
|
||||
}
|
||||
// UpdateEvent::Tooltip(_tooltip) => {
|
||||
// warn!("received unimplemented NewAttentionIcon event");
|
||||
// }
|
||||
UpdateEvent::Menu(menu) => {
|
||||
debug!("received new menu for '{}'", address);
|
||||
|
||||
menu_item.set_state(submenus);
|
||||
menu_item.set_icon_name(item.icon_name);
|
||||
let diffs = get_diffs(menu_item.state(), &menu.submenus);
|
||||
|
||||
menus.insert(address.into(), menu_item);
|
||||
menu_item.apply_diffs(diffs);
|
||||
menu_item.set_state(menu.submenus);
|
||||
}
|
||||
}
|
||||
}
|
||||
NotifierItemMessage::Remove { address } => {
|
||||
Event::Remove(address) => {
|
||||
debug!("Removing tray item at '{address}'");
|
||||
|
||||
if let Some(menu) = menus.get(address.as_str()) {
|
||||
container.remove(&menu.widget);
|
||||
}
|
||||
|
|
|
@ -199,7 +199,9 @@ impl Module<gtk::Button> for UpowerModule {
|
|||
let format = format.replace("{percentage}", &properties.percentage.to_string())
|
||||
.replace("{time_remaining}", &time_remaining)
|
||||
.replace("{state}", battery_state_to_string(state));
|
||||
let icon_name = String::from("icon:") + &properties.icon_name;
|
||||
|
||||
let mut icon_name = String::from("icon:");
|
||||
icon_name.push_str(&properties.icon_name);
|
||||
|
||||
ImageProvider::parse(&icon_name, &icon_theme, false, self.icon_size)
|
||||
.map(|provider| provider.load_into_image(icon.clone()));
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue