mirror of
https://github.com/Zedfrigg/ironbar.git
synced 2026-06-01 13:30:47 +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 13428 additions and 5564 deletions
41
src/bar.rs
41
src/bar.rs
|
|
@ -1,16 +1,16 @@
|
|||
use crate::Ironbar;
|
||||
use crate::config::{BarConfig, BarPosition, MarginConfig, ModuleConfig};
|
||||
use crate::modules::{BarModuleFactory, ModuleInfo, ModuleLocation};
|
||||
use crate::popup::Popup;
|
||||
use crate::Ironbar;
|
||||
use color_eyre::Result;
|
||||
use glib::Propagation;
|
||||
use gtk::gdk::Monitor;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Application, ApplicationWindow, IconTheme, Orientation, Window, WindowType};
|
||||
use gtk::{Application, ApplicationWindow, Orientation, Window, WindowType};
|
||||
use gtk_layer_shell::LayerShell;
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
use tracing::{debug, info};
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum Inner {
|
||||
|
|
@ -22,6 +22,7 @@ enum Inner {
|
|||
pub struct Bar {
|
||||
name: String,
|
||||
monitor_name: String,
|
||||
monitor_size: (i32, i32),
|
||||
position: BarPosition,
|
||||
|
||||
ironbar: Rc<Ironbar>,
|
||||
|
|
@ -41,6 +42,7 @@ impl Bar {
|
|||
pub fn new(
|
||||
app: &Application,
|
||||
monitor_name: String,
|
||||
monitor_size: (i32, i32),
|
||||
config: BarConfig,
|
||||
ironbar: Rc<Ironbar>,
|
||||
) -> Self {
|
||||
|
|
@ -89,6 +91,7 @@ impl Bar {
|
|||
Self {
|
||||
name,
|
||||
monitor_name,
|
||||
monitor_size,
|
||||
position,
|
||||
ironbar,
|
||||
window,
|
||||
|
|
@ -146,7 +149,7 @@ impl Bar {
|
|||
}
|
||||
}
|
||||
|
||||
let load_result = self.load_modules(config, monitor)?;
|
||||
let load_result = self.load_modules(config, monitor, self.monitor_size)?;
|
||||
|
||||
self.show(!start_hidden);
|
||||
|
||||
|
|
@ -243,12 +246,12 @@ impl Bar {
|
|||
}
|
||||
|
||||
/// Loads the configured modules onto a bar.
|
||||
fn load_modules(&self, config: BarConfig, monitor: &Monitor) -> Result<BarLoadResult> {
|
||||
let icon_theme = IconTheme::new();
|
||||
if let Some(ref theme) = config.icon_theme {
|
||||
icon_theme.set_custom_theme(Some(theme));
|
||||
}
|
||||
|
||||
fn load_modules(
|
||||
&self,
|
||||
config: BarConfig,
|
||||
monitor: &Monitor,
|
||||
output_size: (i32, i32),
|
||||
) -> Result<BarLoadResult> {
|
||||
let app = &self.window.application().expect("to exist");
|
||||
|
||||
macro_rules! info {
|
||||
|
|
@ -259,13 +262,17 @@ impl Bar {
|
|||
monitor,
|
||||
output_name: &self.monitor_name,
|
||||
location: $location,
|
||||
icon_theme: &icon_theme,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// popup ignores module location so can bodge this for now
|
||||
let popup = Popup::new(&info!(ModuleLocation::Left), config.popup_gap);
|
||||
let popup = Popup::new(
|
||||
&self.ironbar,
|
||||
&info!(ModuleLocation::Left),
|
||||
output_size,
|
||||
config.popup_gap,
|
||||
);
|
||||
let popup = Rc::new(popup);
|
||||
|
||||
if let Some(modules) = config.start {
|
||||
|
|
@ -333,7 +340,7 @@ impl Bar {
|
|||
|
||||
/// Sets the window visibility status
|
||||
pub fn set_visible(&self, visible: bool) {
|
||||
self.window.set_visible(visible)
|
||||
self.window.set_visible(visible);
|
||||
}
|
||||
|
||||
pub fn set_exclusive(&self, exclusive: bool) {
|
||||
|
|
@ -374,7 +381,10 @@ fn add_modules(
|
|||
let module_factory = BarModuleFactory::new(ironbar.clone(), popup.clone()).into();
|
||||
|
||||
for config in modules {
|
||||
config.create(&module_factory, content, info)?;
|
||||
let name = config.name();
|
||||
if let Err(err) = config.create(&module_factory, content, info) {
|
||||
error!("failed to create module {name}: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
@ -384,9 +394,10 @@ pub fn create_bar(
|
|||
app: &Application,
|
||||
monitor: &Monitor,
|
||||
monitor_name: String,
|
||||
monitor_size: (i32, i32),
|
||||
config: BarConfig,
|
||||
ironbar: Rc<Ironbar>,
|
||||
) -> Result<Bar> {
|
||||
let bar = Bar::new(app, monitor_name, config, ironbar);
|
||||
let bar = Bar::new(app, monitor_name, monitor_size, config, ironbar);
|
||||
bar.init(monitor)
|
||||
}
|
||||
|
|
|
|||
261
src/channels.rs
Normal file
261
src/channels.rs
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
use crate::modules::ModuleUpdateEvent;
|
||||
use crate::spawn;
|
||||
use smithay_client_toolkit::reexports::calloop;
|
||||
use std::fmt::Debug;
|
||||
use tokio::sync::{broadcast, mpsc};
|
||||
|
||||
pub trait SyncSenderExt<T> {
|
||||
/// Asynchronously sends a message on the channel,
|
||||
/// panicking if it cannot be sent.
|
||||
///
|
||||
/// This should be used in cases where sending should *never* fail,
|
||||
/// or where failing indicates a serious bug.
|
||||
fn send_expect(&self, message: T);
|
||||
}
|
||||
|
||||
impl<T> SyncSenderExt<T> for std::sync::mpsc::Sender<T> {
|
||||
#[inline]
|
||||
fn send_expect(&self, message: T) {
|
||||
self.send(message).expect(crate::error::ERR_CHANNEL_SEND);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> SyncSenderExt<T> for calloop::channel::Sender<T> {
|
||||
#[inline]
|
||||
fn send_expect(&self, message: T) {
|
||||
self.send(message).expect(crate::error::ERR_CHANNEL_SEND);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Debug> SyncSenderExt<T> for broadcast::Sender<T> {
|
||||
#[inline]
|
||||
fn send_expect(&self, message: T) {
|
||||
self.send(message).expect(crate::error::ERR_CHANNEL_SEND);
|
||||
}
|
||||
}
|
||||
|
||||
pub trait AsyncSenderExt<T>: Sync + Send + Sized + Clone {
|
||||
/// Asynchronously sends a message on the channel,
|
||||
/// panicking if it cannot be sent.
|
||||
///
|
||||
/// This should be used in cases where sending should *never* fail,
|
||||
/// or where failing indicates a serious bug.
|
||||
fn send_expect(&self, message: T) -> impl Future<Output = ()> + Send;
|
||||
|
||||
/// Asynchronously sends a message on the channel,
|
||||
/// spawning a task to allow it to be sent in the background,
|
||||
/// and panicking if it cannot be sent.
|
||||
///
|
||||
/// Note that this function will return *before* the message is sent.
|
||||
///
|
||||
/// This should be used in cases where sending should *never* fail,
|
||||
/// or where failing indicates a serious bug.
|
||||
#[inline]
|
||||
fn send_spawn(&self, message: T)
|
||||
where
|
||||
Self: 'static,
|
||||
T: Send + 'static,
|
||||
{
|
||||
let tx = self.clone();
|
||||
spawn(async move { tx.send_expect(message).await });
|
||||
}
|
||||
|
||||
/// Shorthand for [`AsyncSenderExt::send_expect`]
|
||||
/// when sending a [`ModuleUpdateEvent::Update`].
|
||||
#[inline]
|
||||
async fn send_update<U: Clone>(&self, update: U)
|
||||
where
|
||||
Self: AsyncSenderExt<ModuleUpdateEvent<U>>,
|
||||
{
|
||||
self.send_expect(ModuleUpdateEvent::Update(update)).await;
|
||||
}
|
||||
|
||||
/// Shorthand for [`AsyncSenderExt::send_spawn`]
|
||||
/// when sending a [`ModuleUpdateEvent::Update`].
|
||||
#[inline]
|
||||
fn send_update_spawn<U>(&self, update: U)
|
||||
where
|
||||
Self: AsyncSenderExt<ModuleUpdateEvent<U>> + 'static,
|
||||
U: Clone + Send + 'static,
|
||||
{
|
||||
self.send_spawn(ModuleUpdateEvent::Update(update));
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Send> AsyncSenderExt<T> for mpsc::Sender<T> {
|
||||
#[inline]
|
||||
async fn send_expect(&self, message: T) {
|
||||
self.send(message)
|
||||
.await
|
||||
.expect(crate::error::ERR_CHANNEL_SEND);
|
||||
}
|
||||
}
|
||||
|
||||
pub trait MpscReceiverExt<T> {
|
||||
/// Spawns a `GLib` future on the local thread, and calls `rx.recv()`
|
||||
/// in a loop, passing the message to `f`.
|
||||
///
|
||||
/// This allows use of `GObjects` and futures in the same context.#
|
||||
///
|
||||
/// `deps` is a single reference, or tuple of references of clonable objects,
|
||||
/// to be consumed inside the closure.
|
||||
/// This avoids needing to `element.clone()` everywhere.
|
||||
fn recv_glib<D, Fn>(self, deps: D, f: Fn)
|
||||
where
|
||||
D: Dependency,
|
||||
D::Target: 'static,
|
||||
Fn: FnMut(&D::Target, T) + 'static;
|
||||
}
|
||||
|
||||
impl<T: 'static> MpscReceiverExt<T> for mpsc::Receiver<T> {
|
||||
fn recv_glib<D, Fn>(mut self, deps: D, mut f: Fn)
|
||||
where
|
||||
D: Dependency,
|
||||
D::Target: 'static,
|
||||
Fn: FnMut(&D::Target, T) + 'static,
|
||||
{
|
||||
let deps = deps.clone_content();
|
||||
glib::spawn_future_local(async move {
|
||||
while let Some(val) = self.recv().await {
|
||||
f(&deps, val);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub trait BroadcastReceiverExt<T>
|
||||
where
|
||||
T: Debug + Clone + 'static,
|
||||
{
|
||||
/// Spawns a `GLib` future on the local thread, and calls `rx.recv()`
|
||||
/// in a loop, passing the message to `f`.
|
||||
///
|
||||
/// This allows use of `GObjects` and futures in the same context.
|
||||
///
|
||||
/// `deps` is a single reference, or tuple of references of clonable objects,
|
||||
/// to be consumed inside the closure.
|
||||
/// This avoids needing to `element.clone()` everywhere.
|
||||
fn recv_glib<D, Fn>(self, deps: D, f: Fn)
|
||||
where
|
||||
D: Dependency,
|
||||
D::Target: 'static,
|
||||
Fn: FnMut(&D::Target, T) + 'static;
|
||||
|
||||
/// Like [`BroadcastReceiverExt::recv_glib`], but the closure must return a [`Future`].
|
||||
fn recv_glib_async<D, Fn, F>(self, deps: D, f: Fn)
|
||||
where
|
||||
D: Dependency,
|
||||
D::Target: 'static,
|
||||
Fn: FnMut(&D::Target, T) -> F + 'static,
|
||||
F: Future;
|
||||
}
|
||||
|
||||
impl<T> BroadcastReceiverExt<T> for broadcast::Receiver<T>
|
||||
where
|
||||
T: Debug + Clone + 'static,
|
||||
{
|
||||
fn recv_glib<D, Fn>(mut self, deps: D, mut f: Fn)
|
||||
where
|
||||
D: Dependency,
|
||||
D::Target: 'static,
|
||||
Fn: FnMut(&D::Target, T) + 'static,
|
||||
{
|
||||
let deps = deps.clone_content();
|
||||
glib::spawn_future_local(async move {
|
||||
loop {
|
||||
match self.recv().await {
|
||||
Ok(val) => f(&deps, val),
|
||||
Err(broadcast::error::RecvError::Lagged(count)) => {
|
||||
tracing::warn!(
|
||||
"Channel lagged behind by {count}, this may result in unexpected or broken behaviour"
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("{err:?}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn recv_glib_async<D, Fn, F>(mut self, deps: D, mut f: Fn)
|
||||
where
|
||||
D: Dependency,
|
||||
D::Target: 'static,
|
||||
Fn: FnMut(&D::Target, T) -> F + 'static,
|
||||
F: Future,
|
||||
{
|
||||
let deps = deps.clone_content();
|
||||
glib::spawn_future_local(async move {
|
||||
loop {
|
||||
match self.recv().await {
|
||||
Ok(val) => {
|
||||
f(&deps, val).await;
|
||||
}
|
||||
Err(broadcast::error::RecvError::Lagged(count)) => {
|
||||
tracing::warn!(
|
||||
"Channel lagged behind by {count}, this may result in unexpected or broken behaviour"
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("{err:?}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// `recv_glib` callback dependency
|
||||
/// or dependency tuple.
|
||||
pub trait Dependency: Clone {
|
||||
type Target;
|
||||
|
||||
fn clone_content(&self) -> Self::Target;
|
||||
}
|
||||
|
||||
impl Dependency for () {
|
||||
type Target = ();
|
||||
|
||||
fn clone_content(&self) -> Self::Target {}
|
||||
}
|
||||
|
||||
impl<'a, T> Dependency for &'a T
|
||||
where
|
||||
T: Clone + 'a,
|
||||
{
|
||||
type Target = T;
|
||||
|
||||
fn clone_content(&self) -> T {
|
||||
T::clone(self)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_dependency {
|
||||
($($idx:tt $t:ident),+) => {
|
||||
impl<'a, $($t),+> Dependency for ($(&'a $t),+)
|
||||
where
|
||||
$($t: Clone + 'a),+
|
||||
{
|
||||
type Target = ($($t),+);
|
||||
|
||||
fn clone_content(&self) -> Self::Target {
|
||||
($(self.$idx.clone()),+)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_dependency!(0 T1, 1 T2);
|
||||
impl_dependency!(0 T1, 1 T2, 2 T3);
|
||||
impl_dependency!(0 T1, 1 T2, 2 T3, 3 T4);
|
||||
impl_dependency!(0 T1, 1 T2, 2 T3, 3 T4, 4 T5);
|
||||
impl_dependency!(0 T1, 1 T2, 2 T3, 3 T4, 4 T5, 5 T6);
|
||||
impl_dependency!(0 T1, 1 T2, 2 T3, 3 T4, 4 T5, 5 T6, 6 T7);
|
||||
impl_dependency!(0 T1, 1 T2, 2 T3, 3 T4, 4 T5, 5 T6, 6 T7, 7 T8);
|
||||
impl_dependency!(0 T1, 1 T2, 2 T3, 3 T4, 4 T5, 5 T6, 6 T7, 7 T8, 8 T9);
|
||||
impl_dependency!(0 T1, 1 T2, 2 T3, 3 T4, 4 T5, 5 T6, 6 T7, 7 T8, 8 T9, 9 T10);
|
||||
impl_dependency!(0 T1, 1 T2, 2 T3, 3 T4, 4 T5, 5 T6, 6 T7, 7 T8, 8 T9, 9 T10, 10 T11);
|
||||
impl_dependency!(0 T1, 1 T2, 2 T3, 3 T4, 4 T5, 5 T6, 6 T7, 7 T8, 8 T9, 9 T10, 10 T11, 11 T12);
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
use crate::error::ExitCode;
|
||||
use crate::ipc::commands::Command;
|
||||
use crate::ipc::responses::Response;
|
||||
use crate::ipc::{Command, Response};
|
||||
use clap::{Parser, ValueEnum};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::process::exit;
|
||||
|
|
@ -46,6 +45,7 @@ pub fn handle_response(response: Response, format: Format) {
|
|||
Format::Plain => match response {
|
||||
Response::Ok => println!("ok"),
|
||||
Response::OkValue { value } => println!("{value}"),
|
||||
Response::Multi { values } => println!("{}", values.join("\n")),
|
||||
Response::Err { message } => eprintln!("error\n{}", message.unwrap_or_default()),
|
||||
},
|
||||
Format::Json => println!(
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
use crate::dynamic_value::{dynamic_string, DynamicBool};
|
||||
use crate::dynamic_value::{DynamicBool, dynamic_string};
|
||||
use crate::script::{Script, ScriptInput};
|
||||
use glib::Propagation;
|
||||
use gtk::gdk::ScrollDirection;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{EventBox, Orientation, Revealer, RevealerTransitionType};
|
||||
use gtk::{EventBox, Justification, Orientation, Revealer, RevealerTransitionType};
|
||||
use serde::Deserialize;
|
||||
use tracing::trace;
|
||||
|
||||
|
|
@ -198,6 +198,28 @@ impl From<ModuleOrientation> for Orientation {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Clone, Copy)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub enum ModuleJustification {
|
||||
#[default]
|
||||
Left,
|
||||
Right,
|
||||
Center,
|
||||
Fill,
|
||||
}
|
||||
|
||||
impl From<ModuleJustification> for Justification {
|
||||
fn from(o: ModuleJustification) -> Self {
|
||||
match o {
|
||||
ModuleJustification::Left => Self::Left,
|
||||
ModuleJustification::Right => Self::Right,
|
||||
ModuleJustification::Center => Self::Center,
|
||||
ModuleJustification::Fill => Self::Fill,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TransitionType {
|
||||
pub const fn to_revealer_transition_type(
|
||||
&self,
|
||||
|
|
@ -246,6 +268,13 @@ impl CommonConfig {
|
|||
let script = match event.direction() {
|
||||
ScrollDirection::Up => scroll_up_script.as_ref(),
|
||||
ScrollDirection::Down => scroll_down_script.as_ref(),
|
||||
ScrollDirection::Smooth => {
|
||||
if event.scroll_deltas().unwrap_or_default().1 > 0.0 {
|
||||
scroll_down_script.as_ref()
|
||||
} else {
|
||||
scroll_up_script.as_ref()
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
|
|
@ -272,8 +301,7 @@ impl CommonConfig {
|
|||
install_oneshot!(self.on_mouse_exit, connect_leave_notify_event);
|
||||
|
||||
if let Some(tooltip) = self.tooltip {
|
||||
let container = container.clone();
|
||||
dynamic_string(&tooltip, move |string| {
|
||||
dynamic_string(&tooltip, container, move |container, string| {
|
||||
container.set_tooltip_text(Some(&string));
|
||||
});
|
||||
}
|
||||
|
|
@ -285,19 +313,15 @@ impl CommonConfig {
|
|||
container.show_all();
|
||||
},
|
||||
|show_if| {
|
||||
// need to keep clone here for the notify callback
|
||||
let container = container.clone();
|
||||
|
||||
{
|
||||
let revealer = revealer.clone();
|
||||
let container = container.clone();
|
||||
|
||||
show_if.subscribe(move |success| {
|
||||
if success {
|
||||
container.show_all();
|
||||
}
|
||||
revealer.set_reveal_child(success);
|
||||
});
|
||||
}
|
||||
show_if.subscribe((revealer, &container), |(revealer, container), success| {
|
||||
if success {
|
||||
container.show_all();
|
||||
}
|
||||
revealer.set_reveal_child(success);
|
||||
});
|
||||
|
||||
revealer.connect_child_revealed_notify(move |revealer| {
|
||||
if !revealer.reveals_child() {
|
||||
|
|
|
|||
|
|
@ -37,26 +37,24 @@ impl<'de> Deserialize<'de> for MonitorConfig {
|
|||
|
||||
pub fn deserialize_layer<'de, D>(deserializer: D) -> Result<gtk_layer_shell::Layer, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
use gtk_layer_shell::Layer;
|
||||
|
||||
let value = Option::<String>::deserialize(deserializer)?;
|
||||
value
|
||||
.map(|v| match v.as_str() {
|
||||
"background" => Ok(Layer::Background),
|
||||
"bottom" => Ok(Layer::Bottom),
|
||||
"top" => Ok(Layer::Top),
|
||||
"overlay" => Ok(Layer::Overlay),
|
||||
_ => Err(serde::de::Error::custom("invalid value for orientation")),
|
||||
})
|
||||
.unwrap_or(Ok(Layer::Top))
|
||||
value.map_or(Ok(Layer::Top), |v| match v.as_str() {
|
||||
"background" => Ok(Layer::Background),
|
||||
"bottom" => Ok(Layer::Bottom),
|
||||
"top" => Ok(Layer::Top),
|
||||
"overlay" => Ok(Layer::Overlay),
|
||||
_ => Err(serde::de::Error::custom("invalid value for orientation")),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "schema")]
|
||||
pub fn schema_layer(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
|
||||
pub fn schema_layer(generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
|
||||
use schemars::JsonSchema;
|
||||
let mut schema: schemars::schema::SchemaObject = <String>::json_schema(gen).into();
|
||||
let mut schema: schemars::schema::SchemaObject = <String>::json_schema(generator).into();
|
||||
schema.enum_values = Some(vec![
|
||||
"background".into(),
|
||||
"bottom".into(),
|
||||
|
|
@ -79,7 +77,7 @@ impl BarPosition {
|
|||
|
||||
/// Gets the angle that label text should be displayed at
|
||||
/// based on this position.
|
||||
pub const fn get_angle(self) -> f64 {
|
||||
pub const fn angle(self) -> f64 {
|
||||
match self {
|
||||
Self::Top | Self::Bottom => 0.0,
|
||||
Self::Left => 90.0,
|
||||
|
|
|
|||
35
src/config/layout.rs
Normal file
35
src/config/layout.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
use crate::config::{ModuleJustification, ModuleOrientation};
|
||||
use crate::modules::ModuleInfo;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Default)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct LayoutConfig {
|
||||
/// The orientation to display the widget contents.
|
||||
/// Setting to vertical will rotate text 90 degrees.
|
||||
///
|
||||
/// **Valid options**: `horizontal`, `vertical`
|
||||
/// <br>
|
||||
/// **Default**: `horizontal`
|
||||
orientation: Option<ModuleOrientation>,
|
||||
|
||||
/// The justification (alignment) of the widget text shown on the bar.
|
||||
///
|
||||
/// **Valid options**: `left`, `right`, `center`, `fill`
|
||||
/// <br>
|
||||
/// **Default**: `left`
|
||||
#[serde(default)]
|
||||
pub justify: ModuleJustification,
|
||||
}
|
||||
|
||||
impl LayoutConfig {
|
||||
pub fn orientation(&self, info: &ModuleInfo) -> gtk::Orientation {
|
||||
self.orientation
|
||||
.map_or(info.bar_position.orientation(), ModuleOrientation::into)
|
||||
}
|
||||
|
||||
pub fn angle(&self, info: &ModuleInfo) -> f64 {
|
||||
self.orientation
|
||||
.map_or(info.bar_position.angle(), ModuleOrientation::to_angle)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,25 +1,35 @@
|
|||
mod common;
|
||||
mod r#impl;
|
||||
mod layout;
|
||||
mod truncate;
|
||||
|
||||
#[cfg(feature = "bindmode")]
|
||||
use crate::modules::bindmode::Bindmode;
|
||||
#[cfg(feature = "cairo")]
|
||||
use crate::modules::cairo::CairoModule;
|
||||
#[cfg(feature = "clipboard")]
|
||||
use crate::modules::clipboard::ClipboardModule;
|
||||
#[cfg(feature = "clock")]
|
||||
use crate::modules::clock::ClockModule;
|
||||
#[cfg(feature = "custom")]
|
||||
use crate::modules::custom::CustomModule;
|
||||
#[cfg(feature = "focused")]
|
||||
use crate::modules::focused::FocusedModule;
|
||||
#[cfg(feature = "keyboard")]
|
||||
use crate::modules::keyboard::KeyboardModule;
|
||||
#[cfg(feature = "label")]
|
||||
use crate::modules::label::LabelModule;
|
||||
#[cfg(feature = "launcher")]
|
||||
use crate::modules::launcher::LauncherModule;
|
||||
#[cfg(feature = "menu")]
|
||||
use crate::modules::menu::MenuModule;
|
||||
#[cfg(feature = "music")]
|
||||
use crate::modules::music::MusicModule;
|
||||
#[cfg(feature = "network_manager")]
|
||||
use crate::modules::networkmanager::NetworkManagerModule;
|
||||
#[cfg(feature = "notifications")]
|
||||
use crate::modules::notifications::NotificationsModule;
|
||||
#[cfg(feature = "script")]
|
||||
use crate::modules::script::ScriptModule;
|
||||
#[cfg(feature = "sys_info")]
|
||||
use crate::modules::sysinfo::SysInfoModule;
|
||||
|
|
@ -35,37 +45,46 @@ use crate::modules::workspaces::WorkspacesModule;
|
|||
use crate::modules::{AnyModuleFactory, ModuleFactory, ModuleInfo};
|
||||
use cfg_if::cfg_if;
|
||||
use color_eyre::Result;
|
||||
#[cfg(feature = "schema")]
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[cfg(feature = "schema")]
|
||||
use schemars::JsonSchema;
|
||||
|
||||
pub use self::common::{CommonConfig, ModuleOrientation, TransitionType};
|
||||
pub use self::truncate::TruncateMode;
|
||||
pub use self::common::{CommonConfig, ModuleJustification, ModuleOrientation, TransitionType};
|
||||
pub use self::layout::LayoutConfig;
|
||||
pub use self::truncate::{EllipsizeMode, TruncateMode};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
#[cfg_attr(feature = "schema", derive(JsonSchema))]
|
||||
pub enum ModuleConfig {
|
||||
#[cfg(feature = "bindmode")]
|
||||
Bindmode(Box<Bindmode>),
|
||||
#[cfg(feature = "cairo")]
|
||||
Cairo(Box<CairoModule>),
|
||||
#[cfg(feature = "clipboard")]
|
||||
Clipboard(Box<ClipboardModule>),
|
||||
#[cfg(feature = "clock")]
|
||||
Clock(Box<ClockModule>),
|
||||
#[cfg(feature = "custom")]
|
||||
Custom(Box<CustomModule>),
|
||||
#[cfg(feature = "focused")]
|
||||
Focused(Box<FocusedModule>),
|
||||
#[cfg(feature = "keyboard")]
|
||||
Keyboard(Box<KeyboardModule>),
|
||||
#[cfg(feature = "label")]
|
||||
Label(Box<LabelModule>),
|
||||
#[cfg(feature = "launcher")]
|
||||
Launcher(Box<LauncherModule>),
|
||||
#[cfg(feature = "menu")]
|
||||
Menu(Box<MenuModule>),
|
||||
#[cfg(feature = "music")]
|
||||
Music(Box<MusicModule>),
|
||||
#[cfg(feature = "network_manager")]
|
||||
NetworkManager(Box<NetworkManagerModule>),
|
||||
#[cfg(feature = "notifications")]
|
||||
Notifications(Box<NotificationsModule>),
|
||||
#[cfg(feature = "script")]
|
||||
Script(Box<ScriptModule>),
|
||||
#[cfg(feature = "sys_info")]
|
||||
SysInfo(Box<SysInfoModule>),
|
||||
|
|
@ -93,24 +112,33 @@ impl ModuleConfig {
|
|||
}
|
||||
|
||||
match self {
|
||||
#[cfg(feature = "bindmode")]
|
||||
Self::Bindmode(module) => create!(module),
|
||||
#[cfg(feature = "cairo")]
|
||||
Self::Cairo(module) => create!(module),
|
||||
#[cfg(feature = "clipboard")]
|
||||
Self::Clipboard(module) => create!(module),
|
||||
#[cfg(feature = "clock")]
|
||||
Self::Clock(module) => create!(module),
|
||||
#[cfg(feature = "custom")]
|
||||
Self::Custom(module) => create!(module),
|
||||
#[cfg(feature = "focused")]
|
||||
Self::Focused(module) => create!(module),
|
||||
#[cfg(feature = "keyboard")]
|
||||
Self::Keyboard(module) => create!(module),
|
||||
#[cfg(feature = "label")]
|
||||
Self::Label(module) => create!(module),
|
||||
#[cfg(feature = "launcher")]
|
||||
Self::Launcher(module) => create!(module),
|
||||
#[cfg(feature = "menu")]
|
||||
Self::Menu(module) => create!(module),
|
||||
#[cfg(feature = "music")]
|
||||
Self::Music(module) => create!(module),
|
||||
#[cfg(feature = "network_manager")]
|
||||
Self::NetworkManager(module) => create!(module),
|
||||
#[cfg(feature = "notifications")]
|
||||
Self::Notifications(module) => create!(module),
|
||||
#[cfg(feature = "script")]
|
||||
Self::Script(module) => create!(module),
|
||||
#[cfg(feature = "sys_info")]
|
||||
Self::SysInfo(module) => create!(module),
|
||||
|
|
@ -124,6 +152,53 @@ impl ModuleConfig {
|
|||
Self::Workspaces(module) => create!(module),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> String {
|
||||
match self {
|
||||
#[cfg(feature = "bindmode")]
|
||||
ModuleConfig::Bindmode(_) => "Bindmode",
|
||||
#[cfg(feature = "cairo")]
|
||||
ModuleConfig::Cairo(_) => "Cario",
|
||||
#[cfg(feature = "clipboard")]
|
||||
ModuleConfig::Clipboard(_) => "Clipboard",
|
||||
#[cfg(feature = "clock")]
|
||||
ModuleConfig::Clock(_) => "Clock",
|
||||
#[cfg(feature = "custom")]
|
||||
ModuleConfig::Custom(_) => "Custom",
|
||||
#[cfg(feature = "focused")]
|
||||
ModuleConfig::Focused(_) => "Focused",
|
||||
#[cfg(feature = "keyboard")]
|
||||
ModuleConfig::Keyboard(_) => "Keyboard",
|
||||
#[cfg(feature = "label")]
|
||||
ModuleConfig::Label(_) => "Label",
|
||||
#[cfg(feature = "launcher")]
|
||||
ModuleConfig::Launcher(_) => "Launcher",
|
||||
#[cfg(feature = "menu")]
|
||||
ModuleConfig::Menu(_) => "Menu",
|
||||
#[cfg(feature = "music")]
|
||||
ModuleConfig::Music(_) => "Music",
|
||||
#[cfg(feature = "network_manager")]
|
||||
ModuleConfig::NetworkManager(_) => "NetworkManager",
|
||||
#[cfg(feature = "notifications")]
|
||||
ModuleConfig::Notifications(_) => "Notifications",
|
||||
#[cfg(feature = "script")]
|
||||
ModuleConfig::Script(_) => "Script",
|
||||
#[cfg(feature = "sys_info")]
|
||||
ModuleConfig::SysInfo(_) => "SysInfo",
|
||||
#[cfg(feature = "tray")]
|
||||
ModuleConfig::Tray(_) => "Tray",
|
||||
#[cfg(feature = "upower")]
|
||||
ModuleConfig::Upower(_) => "UPower",
|
||||
#[cfg(feature = "volume")]
|
||||
ModuleConfig::Volume(_) => "Volume",
|
||||
#[cfg(feature = "workspaces")]
|
||||
ModuleConfig::Workspaces(_) => "Workspaces",
|
||||
// in case no modules are compiled
|
||||
#[allow(unreachable_patterns)]
|
||||
_ => "",
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
@ -272,12 +347,6 @@ pub struct BarConfig {
|
|||
#[serde(default)]
|
||||
pub autohide: Option<u64>,
|
||||
|
||||
/// The name of the GTK icon theme to use.
|
||||
/// Leave unset to use the default Adwaita theme.
|
||||
///
|
||||
/// **Default**: `null`
|
||||
pub icon_theme: Option<String>,
|
||||
|
||||
/// An array of modules to append to the start of the bar.
|
||||
/// Depending on the orientation, this is either the top of the left edge.
|
||||
///
|
||||
|
|
@ -325,10 +394,12 @@ impl Default for BarConfig {
|
|||
height: default_bar_height(),
|
||||
start_hidden: None,
|
||||
autohide: None,
|
||||
icon_theme: None,
|
||||
#[cfg(feature = "label")]
|
||||
start: Some(vec![ModuleConfig::Label(
|
||||
LabelModule::new("ℹ️ Using default config".to_string()).into(),
|
||||
)]),
|
||||
#[cfg(not(feature = "label"))]
|
||||
start: None,
|
||||
center,
|
||||
end,
|
||||
anchor_to_edges: default_true(),
|
||||
|
|
@ -376,6 +447,19 @@ pub struct Config {
|
|||
///
|
||||
/// Providing this option overrides the single, global `bar` option.
|
||||
pub monitors: Option<HashMap<String, MonitorConfig>>,
|
||||
|
||||
/// The name of the GTK icon theme to use.
|
||||
/// Leave unset to use the default Adwaita theme.
|
||||
///
|
||||
/// **Default**: `null`
|
||||
pub icon_theme: Option<String>,
|
||||
|
||||
/// Map of app IDs (or classes) to icon names,
|
||||
/// overriding the app's default icon.
|
||||
///
|
||||
/// **Default**: `{}`
|
||||
#[serde(default)]
|
||||
pub icon_overrides: HashMap<String, String>,
|
||||
}
|
||||
|
||||
const fn default_layer() -> gtk_layer_shell::Layer {
|
||||
|
|
@ -397,3 +481,7 @@ pub const fn default_false() -> bool {
|
|||
pub const fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
pub fn default_launch_command() -> String {
|
||||
String::from("gtk-launch {app_name}")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,21 @@
|
|||
use gtk::pango::EllipsizeMode as GtkEllipsizeMode;
|
||||
use gtk::prelude::*;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Copy)]
|
||||
#[derive(Debug, Deserialize, Clone, Copy, Default)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub enum EllipsizeMode {
|
||||
None,
|
||||
Start,
|
||||
Middle,
|
||||
#[default]
|
||||
End,
|
||||
}
|
||||
|
||||
impl From<EllipsizeMode> for GtkEllipsizeMode {
|
||||
fn from(value: EllipsizeMode) -> Self {
|
||||
match value {
|
||||
EllipsizeMode::None => Self::None,
|
||||
EllipsizeMode::Start => Self::Start,
|
||||
EllipsizeMode::Middle => Self::Middle,
|
||||
EllipsizeMode::End => Self::End,
|
||||
|
|
@ -27,10 +29,23 @@ impl From<EllipsizeMode> for GtkEllipsizeMode {
|
|||
///
|
||||
/// The option can be configured in one of two modes.
|
||||
///
|
||||
/// **Default**: `Auto (end)`
|
||||
///
|
||||
#[derive(Debug, Deserialize, Clone, Copy)]
|
||||
#[serde(untagged)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub enum TruncateMode {
|
||||
/// Do not truncate content.
|
||||
///
|
||||
/// Setting this option may cause excessively long content to overflow other widgets,
|
||||
/// shifting them off-screen.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```corn
|
||||
/// { truncate = "off" }
|
||||
Off,
|
||||
|
||||
/// Auto mode lets GTK decide when to ellipsize.
|
||||
///
|
||||
/// To use this mode, set the truncate option to a string
|
||||
|
|
@ -44,7 +59,7 @@ pub enum TruncateMode {
|
|||
///
|
||||
/// **Valid options**: `start`, `middle`, `end`
|
||||
/// <br>
|
||||
/// **Default**: `null`
|
||||
/// **Default**: `end`
|
||||
Auto(EllipsizeMode),
|
||||
|
||||
/// Length mode defines a fixed point at which to ellipsize.
|
||||
|
|
@ -88,36 +103,34 @@ pub enum TruncateMode {
|
|||
},
|
||||
}
|
||||
|
||||
impl TruncateMode {
|
||||
const fn mode(&self) -> EllipsizeMode {
|
||||
match self {
|
||||
Self::Length { mode, .. } | Self::Auto(mode) => *mode,
|
||||
}
|
||||
impl Default for TruncateMode {
|
||||
fn default() -> Self {
|
||||
Self::Auto(EllipsizeMode::default())
|
||||
}
|
||||
}
|
||||
|
||||
const fn length(&self) -> Option<i32> {
|
||||
impl TruncateMode {
|
||||
pub const fn length(&self) -> Option<i32> {
|
||||
match self {
|
||||
Self::Auto(_) => None,
|
||||
Self::Auto(_) | Self::Off => None,
|
||||
Self::Length { length, .. } => *length,
|
||||
}
|
||||
}
|
||||
|
||||
const fn max_length(&self) -> Option<i32> {
|
||||
pub const fn max_length(&self) -> Option<i32> {
|
||||
match self {
|
||||
Self::Auto(_) => None,
|
||||
Self::Auto(_) | Self::Off => None,
|
||||
Self::Length { max_length, .. } => *max_length,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn truncate_label(&self, label: >k::Label) {
|
||||
label.set_ellipsize(self.mode().into());
|
||||
|
||||
if let Some(length) = self.length() {
|
||||
label.set_width_chars(length);
|
||||
}
|
||||
|
||||
if let Some(length) = self.max_length() {
|
||||
label.set_max_width_chars(length);
|
||||
}
|
||||
impl From<TruncateMode> for GtkEllipsizeMode {
|
||||
fn from(value: TruncateMode) -> Self {
|
||||
let mode = match value {
|
||||
TruncateMode::Off => EllipsizeMode::None,
|
||||
TruncateMode::Length { mode, .. } | TruncateMode::Auto(mode) => mode,
|
||||
};
|
||||
mode.into()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,37 +1,302 @@
|
|||
use std::collections::{HashMap, HashSet};
|
||||
use crate::spawn;
|
||||
use color_eyre::{Help, Report, Result};
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
use tracing::warn;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::sync::Arc;
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::{debug, error};
|
||||
use walkdir::{DirEntry, WalkDir};
|
||||
|
||||
use crate::lock;
|
||||
|
||||
type DesktopFile = HashMap<String, Vec<String>>;
|
||||
|
||||
fn desktop_files() -> &'static Mutex<HashMap<PathBuf, DesktopFile>> {
|
||||
static DESKTOP_FILES: OnceLock<Mutex<HashMap<PathBuf, DesktopFile>>> = OnceLock::new();
|
||||
DESKTOP_FILES.get_or_init(|| Mutex::new(HashMap::new()))
|
||||
#[derive(Debug, Clone)]
|
||||
enum DesktopFileRef {
|
||||
Unloaded(PathBuf),
|
||||
Loaded(DesktopFile),
|
||||
}
|
||||
|
||||
fn desktop_files_look_out_keys() -> &'static HashSet<&'static str> {
|
||||
static DESKTOP_FILES_LOOK_OUT_KEYS: OnceLock<HashSet<&'static str>> = OnceLock::new();
|
||||
DESKTOP_FILES_LOOK_OUT_KEYS
|
||||
.get_or_init(|| HashSet::from(["Name", "StartupWMClass", "Exec", "Icon"]))
|
||||
impl DesktopFileRef {
|
||||
async fn get(&mut self) -> Result<DesktopFile> {
|
||||
match self {
|
||||
DesktopFileRef::Unloaded(path) => {
|
||||
let (tx, rx) = tokio::sync::oneshot::channel();
|
||||
let path = path.clone();
|
||||
|
||||
spawn(async move { tx.send(Self::load(&path).await) });
|
||||
|
||||
let file = rx.await??;
|
||||
*self = DesktopFileRef::Loaded(file.clone());
|
||||
|
||||
Ok(file)
|
||||
}
|
||||
DesktopFileRef::Loaded(file) => Ok(file.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn load(file_path: &Path) -> Result<DesktopFile> {
|
||||
debug!("loading applications file: {}", file_path.display());
|
||||
|
||||
let file = tokio::fs::File::open(file_path).await?;
|
||||
|
||||
let mut desktop_file = DesktopFile::new(
|
||||
file_path
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
let mut lines = BufReader::new(file).lines();
|
||||
|
||||
let mut has_name = false;
|
||||
let mut has_type = false;
|
||||
let mut has_wm_class = false;
|
||||
let mut has_exec = false;
|
||||
let mut has_icon = false;
|
||||
let mut has_categories = false;
|
||||
let mut has_no_display = false;
|
||||
|
||||
while let Ok(Some(line)) = lines.next_line().await {
|
||||
let Some((key, value)) = line.split_once('=') else {
|
||||
continue;
|
||||
};
|
||||
|
||||
match key {
|
||||
"Name" if !has_name => {
|
||||
desktop_file.name = Some(value.to_string());
|
||||
has_name = true;
|
||||
}
|
||||
"Type" if !has_type => {
|
||||
desktop_file.app_type = Some(value.to_string());
|
||||
has_type = true;
|
||||
}
|
||||
"StartupWMClass" if !has_wm_class => {
|
||||
desktop_file.startup_wm_class = Some(value.to_string());
|
||||
has_wm_class = true;
|
||||
}
|
||||
"Exec" if !has_exec => {
|
||||
desktop_file.exec = Some(value.to_string());
|
||||
has_exec = true;
|
||||
}
|
||||
"Icon" if !has_icon => {
|
||||
desktop_file.icon = Some(value.to_string());
|
||||
has_icon = true;
|
||||
}
|
||||
"Categories" if !has_categories => {
|
||||
desktop_file.categories = value.split(';').map(|s| s.to_string()).collect();
|
||||
has_categories = true;
|
||||
}
|
||||
"NoDisplay" if !has_no_display => {
|
||||
desktop_file.no_display = Some(value.parse()?);
|
||||
has_no_display = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// parsing complete - don't bother with the rest of the lines
|
||||
if has_name
|
||||
&& has_type
|
||||
&& has_wm_class
|
||||
&& has_exec
|
||||
&& has_icon
|
||||
&& has_categories
|
||||
&& has_no_display
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(desktop_file)
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds directories that should contain `.desktop` files
|
||||
/// and exist on the filesystem.
|
||||
fn find_application_dirs() -> Vec<PathBuf> {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DesktopFile {
|
||||
pub file_name: String,
|
||||
pub name: Option<String>,
|
||||
pub app_type: Option<String>,
|
||||
pub startup_wm_class: Option<String>,
|
||||
pub exec: Option<String>,
|
||||
pub icon: Option<String>,
|
||||
pub categories: Vec<String>,
|
||||
pub no_display: Option<bool>,
|
||||
}
|
||||
|
||||
impl DesktopFile {
|
||||
fn new(file_name: String) -> Self {
|
||||
Self {
|
||||
file_name,
|
||||
name: None,
|
||||
app_type: None,
|
||||
startup_wm_class: None,
|
||||
exec: None,
|
||||
icon: None,
|
||||
categories: vec![],
|
||||
no_display: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type FileMap = HashMap<Box<str>, DesktopFileRef>;
|
||||
|
||||
/// Desktop file cache and resolver.
|
||||
///
|
||||
/// Files are lazy-loaded as required on resolution.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DesktopFiles {
|
||||
files: Arc<Mutex<FileMap>>,
|
||||
}
|
||||
|
||||
impl Default for DesktopFiles {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl DesktopFiles {
|
||||
/// Creates a new instance,
|
||||
/// scanning disk to generate a list of (unloaded) file refs in the process.
|
||||
pub fn new() -> Self {
|
||||
let desktop_files: FileMap = dirs()
|
||||
.iter()
|
||||
.flat_map(|path| files(path))
|
||||
.map(|file| {
|
||||
(
|
||||
file.file_stem()
|
||||
.unwrap_or_default()
|
||||
.to_str()
|
||||
.unwrap_or_default()
|
||||
.to_string()
|
||||
.into(),
|
||||
DesktopFileRef::Unloaded(file),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
debug!("resolved {} files", desktop_files.len());
|
||||
|
||||
Self {
|
||||
files: Arc::new(Mutex::new(desktop_files)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_all(&self) -> Result<Vec<DesktopFile>> {
|
||||
let mut files = self.files.lock().await;
|
||||
|
||||
let mut res = Vec::with_capacity(files.len());
|
||||
for file in files.values_mut() {
|
||||
let file = file.get().await?;
|
||||
res.push(file);
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Attempts to locate a applications file by file name or contents.
|
||||
///
|
||||
/// Input should typically be the app id, app name or icon.
|
||||
pub async fn find(&self, input: &str) -> Result<Option<DesktopFile>> {
|
||||
let mut res = self.find_by_file_name(input).await?;
|
||||
if res.is_none() {
|
||||
res = self.find_by_file_contents(input).await?;
|
||||
}
|
||||
|
||||
debug!("found match for app_id {input}: {}", res.is_some());
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Checks file names for an exact or partial match of the provided input.
|
||||
async fn find_by_file_name(&self, input: &str) -> Result<Option<DesktopFile>> {
|
||||
let mut files = self.files.lock().await;
|
||||
|
||||
let mut file_ref = files
|
||||
.iter_mut()
|
||||
.find(|&(name, _)| name.eq_ignore_ascii_case(input));
|
||||
|
||||
if file_ref.is_none() {
|
||||
file_ref = files.iter_mut().find(
|
||||
|&(name, _)| // this will attempt to find flatpak apps that are in the format
|
||||
// `com.company.app` or `com.app.something`
|
||||
input
|
||||
.split(&[' ', ':', '@', '.', '_'][..])
|
||||
.any(|part| name.eq_ignore_ascii_case(part)),
|
||||
);
|
||||
}
|
||||
|
||||
let file_ref = file_ref.map(|(_, file)| file);
|
||||
|
||||
if let Some(file_ref) = file_ref {
|
||||
let file = file_ref.get().await?;
|
||||
Ok(Some(file))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks file contents for an exact or partial match of the provided input.
|
||||
async fn find_by_file_contents(&self, app_id: &str) -> Result<Option<DesktopFile>> {
|
||||
let mut files = self.files.lock().await;
|
||||
|
||||
// first pass - check name for exact match
|
||||
for (_, file_ref) in files.iter_mut() {
|
||||
let file = file_ref.get().await?;
|
||||
if let Some(name) = &file.name {
|
||||
if name.eq_ignore_ascii_case(app_id) {
|
||||
return Ok(Some(file));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// second pass - check name for partial match
|
||||
for (_, file_ref) in files.iter_mut() {
|
||||
let file = file_ref.get().await?;
|
||||
if let Some(name) = &file.name {
|
||||
if name.to_lowercase().contains(app_id) {
|
||||
return Ok(Some(file));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// third pass - check remaining fields for partial match
|
||||
for (_, file_ref) in files.iter_mut() {
|
||||
let file = file_ref.get().await?;
|
||||
|
||||
if let Some(name) = &file.exec {
|
||||
if name.to_lowercase().contains(app_id) {
|
||||
return Ok(Some(file));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(name) = &file.startup_wm_class {
|
||||
if name.to_lowercase().contains(app_id) {
|
||||
return Ok(Some(file));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(name) = &file.icon {
|
||||
if name.to_lowercase().contains(app_id) {
|
||||
return Ok(Some(file));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets a list of paths to all directories
|
||||
/// containing `.applications` files.
|
||||
fn dirs() -> Vec<PathBuf> {
|
||||
let mut dirs = vec![
|
||||
PathBuf::from("/usr/share/applications"), // system installed apps
|
||||
PathBuf::from("/var/lib/flatpak/exports/share/applications"), // flatpak apps
|
||||
];
|
||||
|
||||
let xdg_dirs = env::var_os("XDG_DATA_DIRS");
|
||||
if let Some(xdg_dirs) = xdg_dirs {
|
||||
for mut xdg_dir in env::split_paths(&xdg_dirs).map(PathBuf::from) {
|
||||
let xdg_dirs = env::var("XDG_DATA_DIRS");
|
||||
if let Ok(xdg_dirs) = xdg_dirs {
|
||||
for mut xdg_dir in env::split_paths(&xdg_dirs) {
|
||||
xdg_dir.push("applications");
|
||||
dirs.push(xdg_dir);
|
||||
}
|
||||
|
|
@ -43,157 +308,85 @@ fn find_application_dirs() -> Vec<PathBuf> {
|
|||
dirs.push(user_dir);
|
||||
}
|
||||
|
||||
dirs.into_iter().filter(|dir| dir.exists()).collect()
|
||||
dirs.into_iter().filter(|dir| dir.exists()).rev().collect()
|
||||
}
|
||||
|
||||
/// Finds all the desktop files
|
||||
fn find_desktop_files() -> Vec<PathBuf> {
|
||||
let dirs = find_application_dirs();
|
||||
dirs.into_iter()
|
||||
.flat_map(|dir| {
|
||||
WalkDir::new(dir)
|
||||
.max_depth(5)
|
||||
.into_iter()
|
||||
.filter_map(Result::ok)
|
||||
.map(DirEntry::into_path)
|
||||
.filter(|file| file.is_file() && file.extension().unwrap_or_default() == "desktop")
|
||||
})
|
||||
/// Gets a list of all `.applications` files in the provided directory.
|
||||
///
|
||||
/// The directory is recursed to a maximum depth of 5.
|
||||
fn files(dir: &Path) -> Vec<PathBuf> {
|
||||
WalkDir::new(dir)
|
||||
.max_depth(5)
|
||||
.into_iter()
|
||||
.filter_map(Result::ok)
|
||||
.map(DirEntry::into_path)
|
||||
.filter(|file| file.is_file() && file.extension().unwrap_or_default() == "desktop")
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Attempts to locate a `.desktop` file for an app id
|
||||
pub fn find_desktop_file(app_id: &str) -> Option<PathBuf> {
|
||||
// this is necessary to invalidate the cache
|
||||
let files = find_desktop_files();
|
||||
|
||||
find_desktop_file_by_filename(app_id, &files)
|
||||
.or_else(|| find_desktop_file_by_filedata(app_id, &files))
|
||||
/// Starts a `.desktop` file with the provided formatted command.
|
||||
pub fn open_program(file_name: &str, str: &str) {
|
||||
let expanded = str.replace("{app_name}", file_name);
|
||||
let launch_command_parts: Vec<&str> = expanded.split_whitespace().collect();
|
||||
if let Err(err) = Command::new(&launch_command_parts[0])
|
||||
.args(&launch_command_parts[1..])
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
{
|
||||
error!(
|
||||
"{:?}",
|
||||
Report::new(err)
|
||||
.wrap_err("Failed to run launch command.")
|
||||
.suggestion("Perhaps the applications file is invalid?")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds the correct desktop file using a simple condition check
|
||||
fn find_desktop_file_by_filename(app_id: &str, files: &[PathBuf]) -> Option<PathBuf> {
|
||||
let with_names = files
|
||||
.iter()
|
||||
.map(|f| {
|
||||
(
|
||||
f,
|
||||
f.file_stem()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_lowercase(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
with_names
|
||||
.iter()
|
||||
// first pass - check for exact match
|
||||
.find(|(_, name)| name.eq_ignore_ascii_case(app_id))
|
||||
// second pass - check for substring
|
||||
.or_else(|| {
|
||||
with_names.iter().find(|(_, name)| {
|
||||
// this will attempt to find flatpak apps that are in the format
|
||||
// `com.company.app` or `com.app.something`
|
||||
app_id
|
||||
.split(&[' ', ':', '@', '.', '_'][..])
|
||||
.any(|part| name.eq_ignore_ascii_case(part))
|
||||
})
|
||||
})
|
||||
.map(|(file, _)| file.into())
|
||||
}
|
||||
|
||||
/// Finds the correct desktop file using the keys in `DESKTOP_FILES_LOOK_OUT_KEYS`
|
||||
fn find_desktop_file_by_filedata(app_id: &str, files: &[PathBuf]) -> Option<PathBuf> {
|
||||
let app_id = &app_id.to_lowercase();
|
||||
let mut desktop_files_cache = lock!(desktop_files());
|
||||
|
||||
let files = files
|
||||
.iter()
|
||||
.filter_map(|file| {
|
||||
let parsed_desktop_file = parse_desktop_file(file)?;
|
||||
|
||||
desktop_files_cache.insert(file.clone(), parsed_desktop_file.clone());
|
||||
Some((file.clone(), parsed_desktop_file))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let file = files
|
||||
.iter()
|
||||
// first pass - check name key for exact match
|
||||
.find(|(_, desktop_file)| {
|
||||
desktop_file
|
||||
.get("Name")
|
||||
.is_some_and(|names| names.iter().any(|name| name.eq_ignore_ascii_case(app_id)))
|
||||
})
|
||||
// second pass - check name key for substring
|
||||
.or_else(|| {
|
||||
files.iter().find(|(_, desktop_file)| {
|
||||
desktop_file.get("Name").is_some_and(|names| {
|
||||
names
|
||||
.iter()
|
||||
.any(|name| name.to_lowercase().contains(app_id))
|
||||
})
|
||||
})
|
||||
})
|
||||
// third pass - check all keys for substring
|
||||
.or_else(|| {
|
||||
files.iter().find(|(_, desktop_file)| {
|
||||
desktop_file
|
||||
.values()
|
||||
.flatten()
|
||||
.any(|value| value.to_lowercase().contains(app_id))
|
||||
})
|
||||
});
|
||||
|
||||
file.map(|(path, _)| path).cloned()
|
||||
}
|
||||
|
||||
/// Parses a desktop file into a hashmap of keys/vector(values).
|
||||
fn parse_desktop_file(path: &Path) -> Option<DesktopFile> {
|
||||
let Ok(file) = fs::read_to_string(path) else {
|
||||
warn!("Couldn't Open File: {}", path.display());
|
||||
return None;
|
||||
};
|
||||
|
||||
let mut desktop_file: DesktopFile = DesktopFile::new();
|
||||
|
||||
file.lines()
|
||||
.filter_map(|line| {
|
||||
let (key, value) = line.split_once('=')?;
|
||||
|
||||
let key = key.trim();
|
||||
let value = value.trim();
|
||||
|
||||
if desktop_files_look_out_keys().contains(key) {
|
||||
Some((key, value))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.for_each(|(key, value)| {
|
||||
desktop_file
|
||||
.entry(key.to_string())
|
||||
.or_default()
|
||||
.push(value.to_string());
|
||||
});
|
||||
|
||||
Some(desktop_file)
|
||||
}
|
||||
|
||||
/// Attempts to get the icon name from the app's `.desktop` file.
|
||||
pub fn get_desktop_icon_name(app_id: &str) -> Option<String> {
|
||||
let path = find_desktop_file(app_id)?;
|
||||
|
||||
let mut desktop_files_cache = lock!(desktop_files());
|
||||
|
||||
let desktop_file = match desktop_files_cache.get(&path) {
|
||||
Some(desktop_file) => desktop_file,
|
||||
_ => desktop_files_cache
|
||||
.entry(path.clone())
|
||||
.or_insert_with(|| parse_desktop_file(&path).expect("desktop_file")),
|
||||
};
|
||||
|
||||
let mut icons = desktop_file.get("Icon").into_iter().flatten();
|
||||
|
||||
icons.next().map(std::string::ToString::to_string)
|
||||
fn setup() {
|
||||
unsafe {
|
||||
let pwd = env::current_dir().unwrap();
|
||||
env::set_var("XDG_DATA_DIRS", format!("{}/test-configs", pwd.display()));
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn find_by_filename() {
|
||||
setup();
|
||||
|
||||
let desktop_files = DesktopFiles::new();
|
||||
let file = desktop_files.find_by_file_name("firefox").await.unwrap();
|
||||
|
||||
assert!(file.is_some());
|
||||
assert_eq!(file.unwrap().file_name, "firefox.desktop");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn find_by_file_contents() {
|
||||
setup();
|
||||
|
||||
let desktop_files = DesktopFiles::new();
|
||||
|
||||
let file = desktop_files.find_by_file_contents("427520").await.unwrap();
|
||||
|
||||
assert!(file.is_some());
|
||||
assert_eq!(file.unwrap().file_name, "Factorio.desktop");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn parser() {
|
||||
let mut file_ref =
|
||||
DesktopFileRef::Unloaded(PathBuf::from("test-configs/applications/firefox.desktop"));
|
||||
let file = file_ref.get().await.unwrap();
|
||||
|
||||
assert_eq!(file.name, Some("Firefox".to_string()));
|
||||
assert_eq!(file.icon, Some("firefox".to_string()));
|
||||
assert_eq!(file.exec, Some("/usr/lib/firefox/firefox %u".to_string()));
|
||||
assert_eq!(file.startup_wm_class, Some("firefox".to_string()));
|
||||
assert_eq!(file.app_type, Some("Application".to_string()));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
use crate::script::Script;
|
||||
use crate::{glib_recv_mpsc, spawn, try_send};
|
||||
#[cfg(feature = "ipc")]
|
||||
use crate::{send_async, Ironbar};
|
||||
use crate::Ironbar;
|
||||
use crate::channels::{AsyncSenderExt, Dependency, MpscReceiverExt};
|
||||
use crate::script::Script;
|
||||
use crate::spawn;
|
||||
use cfg_if::cfg_if;
|
||||
use serde::Deserialize;
|
||||
use tokio::sync::mpsc;
|
||||
|
|
@ -18,9 +19,11 @@ pub enum DynamicBool {
|
|||
}
|
||||
|
||||
impl DynamicBool {
|
||||
pub fn subscribe<F>(self, mut f: F)
|
||||
pub fn subscribe<D, F>(self, deps: D, f: F)
|
||||
where
|
||||
F: FnMut(bool) + 'static,
|
||||
D: Dependency,
|
||||
D::Target: Clone + 'static,
|
||||
F: FnMut(&D::Target, bool) + 'static,
|
||||
{
|
||||
let value = match self {
|
||||
Self::Unknown(input) => {
|
||||
|
|
@ -42,14 +45,14 @@ impl DynamicBool {
|
|||
|
||||
let (tx, rx) = mpsc::channel(32);
|
||||
|
||||
glib_recv_mpsc!(rx, val => f(val));
|
||||
rx.recv_glib(deps, f);
|
||||
|
||||
spawn(async move {
|
||||
match value {
|
||||
DynamicBool::Script(script) => {
|
||||
script
|
||||
.run(None, |_, success| {
|
||||
try_send!(tx, success);
|
||||
tx.send_spawn(success);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
|
@ -58,11 +61,11 @@ impl DynamicBool {
|
|||
let variable_manager = Ironbar::variable_manager();
|
||||
|
||||
let variable_name = variable[1..].into(); // remove hash
|
||||
let mut rx = crate::write_lock!(variable_manager).subscribe(variable_name);
|
||||
let mut rx = variable_manager.subscribe(variable_name);
|
||||
|
||||
while let Ok(value) = rx.recv().await {
|
||||
let has_value = value.is_some_and(|s| is_truthy(&s));
|
||||
send_async!(tx, has_value);
|
||||
tx.send_expect(has_value).await;
|
||||
}
|
||||
}
|
||||
DynamicBool::Unknown(_) => unreachable!(),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
use crate::script::{OutputStream, Script};
|
||||
#[cfg(feature = "ipc")]
|
||||
use crate::Ironbar;
|
||||
use crate::{arc_mut, glib_recv_mpsc, lock, spawn, try_send};
|
||||
use crate::channels::{AsyncSenderExt, Dependency, MpscReceiverExt};
|
||||
use crate::script::{OutputStream, Script};
|
||||
use crate::{arc_mut, lock, spawn};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
/// A segment of a dynamic string,
|
||||
|
|
@ -22,12 +23,14 @@ enum DynamicStringSegment {
|
|||
///
|
||||
/// ```rs
|
||||
/// dynamic_string(&text, move |string| {
|
||||
/// label.set_markup(&string);
|
||||
/// label.set_label_escaped(&string);
|
||||
/// });
|
||||
/// ```
|
||||
pub fn dynamic_string<F>(input: &str, mut f: F)
|
||||
pub fn dynamic_string<D, F>(input: &str, deps: D, f: F)
|
||||
where
|
||||
F: FnMut(String) + 'static,
|
||||
D: Dependency,
|
||||
D::Target: Clone + 'static,
|
||||
F: FnMut(&D::Target, String) + 'static,
|
||||
{
|
||||
let (tokens, is_static) = parse_input(input);
|
||||
|
||||
|
|
@ -55,7 +58,7 @@ where
|
|||
let _: String = std::mem::replace(&mut label_parts[i], out);
|
||||
|
||||
let string = label_parts.join("");
|
||||
try_send!(tx, string);
|
||||
tx.send_spawn(string);
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
|
@ -71,7 +74,7 @@ where
|
|||
|
||||
spawn(async move {
|
||||
let variable_manager = Ironbar::variable_manager();
|
||||
let mut rx = crate::write_lock!(variable_manager).subscribe(name);
|
||||
let mut rx = variable_manager.subscribe(name);
|
||||
|
||||
while let Ok(value) = rx.recv().await {
|
||||
if let Some(value) = value {
|
||||
|
|
@ -80,7 +83,7 @@ where
|
|||
let _: String = std::mem::replace(&mut label_parts[i], value);
|
||||
|
||||
let string = label_parts.join("");
|
||||
try_send!(tx, string);
|
||||
tx.send_spawn(string);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -88,12 +91,12 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
glib_recv_mpsc!(rx , val => f(val));
|
||||
rx.recv_glib(deps, f);
|
||||
|
||||
// initialize
|
||||
if is_static {
|
||||
let label_parts = lock!(label_parts).join("");
|
||||
try_send!(tx, label_parts);
|
||||
tx.send_spawn(label_parts);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
use glib::IsA;
|
||||
use crate::config::TruncateMode;
|
||||
use glib::{IsA, markup_escape_text};
|
||||
use gtk::pango::EllipsizeMode;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Orientation, Widget};
|
||||
use gtk::{Label, Orientation, Widget};
|
||||
|
||||
/// Represents a widget's size
|
||||
/// and location relative to the bar's start edge.
|
||||
|
|
@ -18,6 +20,8 @@ pub struct WidgetGeometry {
|
|||
pub trait IronbarGtkExt {
|
||||
/// Adds a new CSS class to the widget.
|
||||
fn add_class(&self, class: &str);
|
||||
/// Removes a CSS class from the widget
|
||||
fn remove_class(&self, class: &str);
|
||||
/// Gets the geometry for the widget
|
||||
fn geometry(&self, orientation: Orientation) -> WidgetGeometry;
|
||||
|
||||
|
|
@ -32,6 +36,10 @@ impl<W: IsA<Widget>> IronbarGtkExt for W {
|
|||
self.style_context().add_class(class);
|
||||
}
|
||||
|
||||
fn remove_class(&self, class: &str) {
|
||||
self.style_context().remove_class(class);
|
||||
}
|
||||
|
||||
fn geometry(&self, orientation: Orientation) -> WidgetGeometry {
|
||||
let allocation = self.allocation();
|
||||
|
||||
|
|
@ -75,3 +83,36 @@ impl<W: IsA<Widget>> IronbarGtkExt for W {
|
|||
unsafe { self.set_data(key, value) }
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IronbarLabelExt {
|
||||
/// Sets the label value to the provided string.
|
||||
///
|
||||
/// If the label does not contain markup `span` tags,
|
||||
/// the text is escaped to avoid issues with special characters (ie `&`).
|
||||
/// Otherwise, the text is used verbatim, and it is up to the user to escape.
|
||||
fn set_label_escaped(&self, label: &str);
|
||||
|
||||
fn truncate(&self, mode: TruncateMode);
|
||||
}
|
||||
|
||||
impl IronbarLabelExt for Label {
|
||||
fn set_label_escaped(&self, label: &str) {
|
||||
if label.contains("<span") {
|
||||
self.set_label(label);
|
||||
} else {
|
||||
self.set_label(&markup_escape_text(label));
|
||||
}
|
||||
}
|
||||
|
||||
fn truncate(&self, mode: TruncateMode) {
|
||||
self.set_ellipsize(<TruncateMode as Into<EllipsizeMode>>::into(mode));
|
||||
|
||||
if let Some(length) = mode.length() {
|
||||
self.set_width_chars(length);
|
||||
}
|
||||
|
||||
if let Some(length) = mode.max_length() {
|
||||
self.set_max_width_chars(length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
223
src/image/gtk.rs
223
src/image/gtk.rs
|
|
@ -1,55 +1,196 @@
|
|||
use super::ImageProvider;
|
||||
use crate::gtk_helpers::IronbarGtkExt;
|
||||
use crate::gtk_helpers::{IronbarGtkExt, IronbarLabelExt};
|
||||
use crate::image;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Button, IconTheme, Image, Label, Orientation};
|
||||
use gtk::{Button, Image, Label, Orientation};
|
||||
use std::ops::Deref;
|
||||
|
||||
#[cfg(any(feature = "music", feature = "workspaces", feature = "clipboard"))]
|
||||
pub fn new_icon_button(input: &str, icon_theme: &IconTheme, size: i32) -> Button {
|
||||
let button = Button::new();
|
||||
|
||||
if ImageProvider::is_definitely_image_input(input) {
|
||||
let image = Image::new();
|
||||
image.add_class("image");
|
||||
image.add_class("icon");
|
||||
|
||||
match ImageProvider::parse(input, icon_theme, false, size)
|
||||
.map(|provider| provider.load_into_image(image.clone()))
|
||||
{
|
||||
Some(_) => {
|
||||
button.set_image(Some(&image));
|
||||
button.set_always_show_image(true);
|
||||
}
|
||||
None => {
|
||||
button.set_label(input);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
button.set_label(input);
|
||||
}
|
||||
|
||||
button
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg(any(
|
||||
feature = "cairo",
|
||||
feature = "clipboard",
|
||||
feature = "clipboard",
|
||||
feature = "keyboard",
|
||||
feature = "launcher",
|
||||
feature = "music",
|
||||
feature = "workspaces",
|
||||
))]
|
||||
pub struct IconButton {
|
||||
button: Button,
|
||||
label: Label,
|
||||
}
|
||||
|
||||
#[cfg(feature = "music")]
|
||||
pub fn new_icon_label(input: &str, icon_theme: &IconTheme, size: i32) -> gtk::Box {
|
||||
let container = gtk::Box::new(Orientation::Horizontal, 0);
|
||||
#[cfg(any(
|
||||
feature = "cairo",
|
||||
feature = "clipboard",
|
||||
feature = "clipboard",
|
||||
feature = "keyboard",
|
||||
feature = "launcher",
|
||||
feature = "music",
|
||||
feature = "workspaces",
|
||||
))]
|
||||
impl IconButton {
|
||||
pub fn new(input: &str, size: i32, image_provider: image::Provider) -> Self {
|
||||
let button = Button::new();
|
||||
let image = Image::new();
|
||||
let label = Label::new(Some(input));
|
||||
|
||||
if image::Provider::is_explicit_input(input) {
|
||||
image.add_class("image");
|
||||
image.add_class("icon");
|
||||
|
||||
let image = image.clone();
|
||||
let label = label.clone();
|
||||
let button = button.clone();
|
||||
|
||||
let input = input.to_string(); // ew
|
||||
|
||||
glib::spawn_future_local(async move {
|
||||
if let Ok(true) = image_provider
|
||||
.load_into_image(&input, size, false, &image)
|
||||
.await
|
||||
{
|
||||
button.set_image(Some(&image));
|
||||
button.set_always_show_image(true);
|
||||
} else {
|
||||
button.set_child(Some(&label));
|
||||
label.show();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
button.set_child(Some(&label));
|
||||
label.show();
|
||||
}
|
||||
|
||||
Self { button, label }
|
||||
}
|
||||
|
||||
pub fn label(&self) -> &Label {
|
||||
&self.label
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(
|
||||
feature = "clipboard",
|
||||
feature = "keyboard",
|
||||
feature = "music",
|
||||
feature = "workspaces",
|
||||
feature = "cairo",
|
||||
feature = "clipboard",
|
||||
feature = "launcher",
|
||||
))]
|
||||
impl Deref for IconButton {
|
||||
type Target = Button;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.button
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "keyboard", feature = "music", feature = "workspaces"))]
|
||||
pub struct IconLabel {
|
||||
provider: image::Provider,
|
||||
container: gtk::Box,
|
||||
label: Label,
|
||||
image: Image,
|
||||
|
||||
size: i32,
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "keyboard", feature = "music", feature = "workspaces"))]
|
||||
impl IconLabel {
|
||||
pub fn new(input: &str, size: i32, image_provider: &image::Provider) -> Self {
|
||||
let container = gtk::Box::new(Orientation::Horizontal, 0);
|
||||
|
||||
let label = Label::builder().use_markup(true).build();
|
||||
label.add_class("icon");
|
||||
label.add_class("text-icon");
|
||||
|
||||
if ImageProvider::is_definitely_image_input(input) {
|
||||
let image = Image::new();
|
||||
image.add_class("icon");
|
||||
image.add_class("image");
|
||||
|
||||
container.add(&image);
|
||||
|
||||
ImageProvider::parse(input, icon_theme, false, size)
|
||||
.map(|provider| provider.load_into_image(image));
|
||||
} else {
|
||||
let label = Label::builder().use_markup(true).label(input).build();
|
||||
label.add_class("icon");
|
||||
label.add_class("text-icon");
|
||||
|
||||
container.add(&label);
|
||||
|
||||
if image::Provider::is_explicit_input(input) {
|
||||
let image = image.clone();
|
||||
let label = label.clone();
|
||||
let image_provider = image_provider.clone();
|
||||
|
||||
let input = input.to_string();
|
||||
|
||||
glib::spawn_future_local(async move {
|
||||
let res = image_provider
|
||||
.load_into_image(&input, size, false, &image)
|
||||
.await;
|
||||
if matches!(res, Ok(true)) {
|
||||
image.show();
|
||||
} else {
|
||||
label.set_label_escaped(&input);
|
||||
label.show();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
label.set_label_escaped(input);
|
||||
label.show();
|
||||
}
|
||||
|
||||
Self {
|
||||
provider: image_provider.clone(),
|
||||
container,
|
||||
label,
|
||||
image,
|
||||
size,
|
||||
}
|
||||
}
|
||||
|
||||
container
|
||||
pub fn set_label(&self, input: Option<&str>) {
|
||||
let label = &self.label;
|
||||
let image = &self.image;
|
||||
|
||||
if let Some(input) = input {
|
||||
if image::Provider::is_explicit_input(input) {
|
||||
let provider = self.provider.clone();
|
||||
let size = self.size;
|
||||
|
||||
let label = label.clone();
|
||||
let image = image.clone();
|
||||
let input = input.to_string();
|
||||
|
||||
glib::spawn_future_local(async move {
|
||||
let res = provider.load_into_image(&input, size, false, &image).await;
|
||||
if matches!(res, Ok(true)) {
|
||||
label.hide();
|
||||
image.show();
|
||||
} else {
|
||||
label.set_label_escaped(&input);
|
||||
|
||||
image.hide();
|
||||
label.show();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
label.set_label_escaped(input);
|
||||
|
||||
image.hide();
|
||||
label.show();
|
||||
}
|
||||
} else {
|
||||
label.hide();
|
||||
image.hide();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn label(&self) -> &Label {
|
||||
&self.label
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "keyboard", feature = "music", feature = "workspaces"))]
|
||||
impl Deref for IconLabel {
|
||||
type Target = gtk::Box;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.container
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,19 @@
|
|||
#[cfg(any(feature = "music", feature = "workspaces", feature = "clipboard"))]
|
||||
#[cfg(any(
|
||||
feature = "clipboard",
|
||||
feature = "keyboard",
|
||||
feature = "launcher",
|
||||
feature = "music",
|
||||
feature = "workspaces",
|
||||
))]
|
||||
mod gtk;
|
||||
mod provider;
|
||||
|
||||
#[cfg(any(feature = "music", feature = "workspaces"))]
|
||||
#[cfg(any(
|
||||
feature = "clipboard",
|
||||
feature = "keyboard",
|
||||
feature = "launcher",
|
||||
feature = "music",
|
||||
feature = "workspaces",
|
||||
))]
|
||||
pub use self::gtk::*;
|
||||
pub use provider::ImageProvider;
|
||||
pub use provider::{Provider, create_and_load_surface};
|
||||
|
|
|
|||
|
|
@ -1,114 +1,195 @@
|
|||
use crate::desktop_file::get_desktop_icon_name;
|
||||
#[cfg(feature = "http")]
|
||||
use crate::{glib_recv_mpsc, send_async, spawn};
|
||||
use cfg_if::cfg_if;
|
||||
use crate::desktop_file::DesktopFiles;
|
||||
use crate::{arc_mut, lock};
|
||||
use color_eyre::{Help, Report, Result};
|
||||
use gtk::cairo::Surface;
|
||||
use gtk::gdk::ffi::gdk_cairo_surface_create_from_pixbuf;
|
||||
use gtk::gdk_pixbuf::Pixbuf;
|
||||
use gtk::gio::{Cancellable, MemoryInputStream};
|
||||
use gtk::prelude::*;
|
||||
use gtk::{IconLookupFlags, IconTheme};
|
||||
use std::path::{Path, PathBuf};
|
||||
#[cfg(feature = "http")]
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{debug, warn};
|
||||
use gtk::{IconLookupFlags, IconTheme, Image};
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tracing::{debug, trace, warn};
|
||||
|
||||
cfg_if!(
|
||||
if #[cfg(feature = "http")] {
|
||||
use gtk::gio::{Cancellable, MemoryInputStream};
|
||||
use tracing::error;
|
||||
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
|
||||
struct ImageRef {
|
||||
size: i32,
|
||||
location: Option<ImageLocation>,
|
||||
theme: IconTheme,
|
||||
}
|
||||
|
||||
impl ImageRef {
|
||||
fn new(size: i32, location: Option<ImageLocation>, theme: IconTheme) -> Self {
|
||||
Self {
|
||||
size,
|
||||
location,
|
||||
theme,
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ImageLocation<'a> {
|
||||
Icon {
|
||||
name: String,
|
||||
theme: &'a IconTheme,
|
||||
},
|
||||
#[derive(Debug, Clone, Eq, Hash, PartialEq)]
|
||||
enum ImageLocation {
|
||||
Icon(String),
|
||||
Local(PathBuf),
|
||||
Steam(String),
|
||||
#[cfg(feature = "http")]
|
||||
Remote(reqwest::Url),
|
||||
}
|
||||
|
||||
pub struct ImageProvider<'a> {
|
||||
location: ImageLocation<'a>,
|
||||
size: i32,
|
||||
#[derive(Debug)]
|
||||
struct Cache {
|
||||
location_cache: HashMap<(Box<str>, i32), ImageRef>,
|
||||
pixbuf_cache: HashMap<ImageRef, Option<Pixbuf>>,
|
||||
}
|
||||
|
||||
impl<'a> ImageProvider<'a> {
|
||||
/// Attempts to parse the image input to find its location.
|
||||
/// Errors if no valid location type can be found.
|
||||
impl Cache {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
location_cache: HashMap::new(),
|
||||
pixbuf_cache: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Provider {
|
||||
desktop_files: DesktopFiles,
|
||||
icon_theme: RefCell<Option<IconTheme>>,
|
||||
overrides: HashMap<String, String>,
|
||||
cache: Arc<Mutex<Cache>>,
|
||||
}
|
||||
|
||||
impl Provider {
|
||||
pub fn new(desktop_files: DesktopFiles, overrides: &mut HashMap<String, String>) -> Self {
|
||||
let mut overrides_map = HashMap::with_capacity(overrides.len());
|
||||
overrides_map.extend(overrides.drain());
|
||||
|
||||
Self {
|
||||
desktop_files,
|
||||
icon_theme: RefCell::new(None),
|
||||
overrides: overrides_map,
|
||||
cache: arc_mut!(Cache::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to resolve the provided input into a `Pixbuf`,
|
||||
/// and load that `Pixbuf` into the provided `Image` widget.
|
||||
///
|
||||
/// Note this checks that icons exist in theme, or files exist on disk
|
||||
/// 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 })
|
||||
}
|
||||
|
||||
/// Returns true if the input starts with a prefix
|
||||
/// that is supported by the parser
|
||||
/// (ie the parser would not fallback to checking the input).
|
||||
pub fn is_definitely_image_input(input: &str) -> bool {
|
||||
input.starts_with("icon:")
|
||||
|| input.starts_with("file://")
|
||||
|| input.starts_with("http://")
|
||||
|| input.starts_with("https://")
|
||||
|| input.starts_with('/')
|
||||
}
|
||||
|
||||
fn get_location(
|
||||
/// If `use_fallback` is `true`, a fallback icon will be used
|
||||
/// where an image cannot be found.
|
||||
///
|
||||
/// Returns `true` if the image was successfully loaded,
|
||||
/// or `false` if the image could not be found.
|
||||
/// May also return an error if the resolution or loading process failed.
|
||||
pub async fn load_into_image(
|
||||
&self,
|
||||
input: &str,
|
||||
theme: &'a IconTheme,
|
||||
size: i32,
|
||||
use_fallback: bool,
|
||||
recurse_depth: usize,
|
||||
) -> Option<ImageLocation<'a>> {
|
||||
macro_rules! fallback {
|
||||
() => {
|
||||
if use_fallback {
|
||||
Some(Self::get_fallback_icon(theme))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
image: &Image,
|
||||
) -> Result<bool> {
|
||||
let image_ref = self.get_ref(input, size).await?;
|
||||
debug!("image ref for {input}: {:?}", image_ref);
|
||||
|
||||
let pixbuf = if let Some(pixbuf) = lock!(self.cache).pixbuf_cache.get(&image_ref) {
|
||||
pixbuf.clone()
|
||||
} else {
|
||||
let pixbuf = Self::get_pixbuf(&image_ref, image.scale_factor(), use_fallback).await?;
|
||||
|
||||
lock!(self.cache)
|
||||
.pixbuf_cache
|
||||
.insert(image_ref, pixbuf.clone());
|
||||
|
||||
pixbuf
|
||||
};
|
||||
|
||||
if let Some(ref pixbuf) = pixbuf {
|
||||
create_and_load_surface(pixbuf, image)?;
|
||||
}
|
||||
|
||||
const MAX_RECURSE_DEPTH: usize = 2;
|
||||
Ok(pixbuf.is_some())
|
||||
}
|
||||
|
||||
let should_parse_desktop_file = !Self::is_definitely_image_input(input);
|
||||
/// Like [`Provider::load_into_image`], but does not return an error if the image could not be found.
|
||||
///
|
||||
/// If an image is not resolved, a warning is logged. Errors are also logged.
|
||||
pub async fn load_into_image_silent(
|
||||
&self,
|
||||
input: &str,
|
||||
size: i32,
|
||||
use_fallback: bool,
|
||||
image: &Image,
|
||||
) {
|
||||
match self.load_into_image(input, size, use_fallback, image).await {
|
||||
Ok(true) => {}
|
||||
Ok(false) => warn!("failed to resolve image: {input}"),
|
||||
Err(e) => warn!("failed to load image: {input}: {e:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the `ImageRef` for the provided input.
|
||||
///
|
||||
/// This contains the location of the image if it can be resolved.
|
||||
/// The ref will be loaded from cache if present.
|
||||
async fn get_ref(&self, input: &str, size: i32) -> Result<ImageRef> {
|
||||
let key = (input.into(), size);
|
||||
|
||||
if let Some(location) = lock!(self.cache).location_cache.get(&key) {
|
||||
Ok(location.clone())
|
||||
} else {
|
||||
let location = self.resolve_location(input, size, 0).await?;
|
||||
let image_ref = ImageRef::new(size, location, self.icon_theme());
|
||||
|
||||
lock!(self.cache)
|
||||
.location_cache
|
||||
.insert(key, image_ref.clone());
|
||||
Ok(image_ref)
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to resolve the provided input into an `ImageLocation`.
|
||||
///
|
||||
/// This will resolve all of:
|
||||
/// - The current icon theme
|
||||
/// - The file on disk
|
||||
/// - Steam icons
|
||||
/// - Desktop files (`Icon` keys)
|
||||
/// - HTTP(S) URLs
|
||||
async fn resolve_location(
|
||||
&self,
|
||||
input: &str,
|
||||
size: i32,
|
||||
recurse_depth: u8,
|
||||
) -> Result<Option<ImageLocation>> {
|
||||
const MAX_RECURSE_DEPTH: u8 = 2;
|
||||
|
||||
let input = self.overrides.get(input).map_or(input, String::as_str);
|
||||
|
||||
let should_parse_desktop_file = !Self::is_explicit_input(input);
|
||||
|
||||
let (input_type, input_name) = input
|
||||
.split_once(':')
|
||||
.map_or((None, input), |(t, n)| (Some(t), n));
|
||||
|
||||
match input_type {
|
||||
Some(input_type) if input_type == "icon" => Some(ImageLocation::Icon {
|
||||
name: input_name.to_string(),
|
||||
theme,
|
||||
}),
|
||||
Some(input_type) if input_type == "file" => Some(ImageLocation::Local(PathBuf::from(
|
||||
let location = match input_type {
|
||||
Some(_t @ "icon") => Some(ImageLocation::Icon(input_name.to_string())),
|
||||
Some(_t @ "file") => Some(ImageLocation::Local(PathBuf::from(
|
||||
input_name[2..].to_string(),
|
||||
))),
|
||||
#[cfg(feature = "http")]
|
||||
Some(input_type) if input_type == "http" || input_type == "https" => {
|
||||
input.parse().ok().map(ImageLocation::Remote)
|
||||
}
|
||||
None if input.starts_with("steam_app_") => Some(ImageLocation::Steam(
|
||||
Some(_t @ ("http" | "https")) => input_name.parse().ok().map(ImageLocation::Remote),
|
||||
None if input_name.starts_with("steam_app_") => Some(ImageLocation::Steam(
|
||||
input_name.chars().skip("steam_app_".len()).collect(),
|
||||
)),
|
||||
None if theme
|
||||
.lookup_icon(input, size, IconLookupFlags::empty())
|
||||
None if self
|
||||
.icon_theme()
|
||||
.lookup_icon(input_name, size, IconLookupFlags::empty())
|
||||
.is_some() =>
|
||||
{
|
||||
Some(ImageLocation::Icon {
|
||||
name: input_name.to_string(),
|
||||
theme,
|
||||
})
|
||||
Some(ImageLocation::Icon(input_name.to_string()))
|
||||
}
|
||||
Some(input_type) => {
|
||||
warn!(
|
||||
|
|
@ -116,173 +197,154 @@ impl<'a> ImageProvider<'a> {
|
|||
Report::msg(format!("Unsupported image type: {input_type}"))
|
||||
.note("You may need to recompile with support if available")
|
||||
);
|
||||
fallback!()
|
||||
None
|
||||
}
|
||||
None if PathBuf::from(input_name).is_file() => {
|
||||
Some(ImageLocation::Local(PathBuf::from(input_name)))
|
||||
}
|
||||
None if recurse_depth == MAX_RECURSE_DEPTH => fallback!(),
|
||||
None if recurse_depth == MAX_RECURSE_DEPTH => None,
|
||||
None if should_parse_desktop_file => {
|
||||
if let Some(location) = get_desktop_icon_name(input_name).map(|input| {
|
||||
Self::get_location(&input, theme, size, use_fallback, recurse_depth + 1)
|
||||
}) {
|
||||
location
|
||||
} else {
|
||||
warn!("Failed to find image: {input}");
|
||||
fallback!()
|
||||
}
|
||||
}
|
||||
None => {
|
||||
warn!("Failed to find image: {input}");
|
||||
fallback!()
|
||||
}
|
||||
}
|
||||
}
|
||||
let location = self
|
||||
.desktop_files
|
||||
.find(input_name)
|
||||
.await?
|
||||
.and_then(|input| input.icon);
|
||||
|
||||
/// Attempts to fetch the image from the location
|
||||
/// and load it into the provided `GTK::Image` widget.
|
||||
pub fn load_into_image(&self, image: gtk::Image) -> Result<()> {
|
||||
// handle remote locations async to avoid blocking UI thread while downloading
|
||||
#[cfg(feature = "http")]
|
||||
if let ImageLocation::Remote(url) = &self.location {
|
||||
let url = url.clone();
|
||||
let (tx, rx) = mpsc::channel(64);
|
||||
|
||||
spawn(async move {
|
||||
let bytes = Self::get_bytes_from_http(url).await;
|
||||
if let Ok(bytes) = bytes {
|
||||
send_async!(tx, bytes);
|
||||
}
|
||||
});
|
||||
|
||||
{
|
||||
let size = self.size;
|
||||
glib_recv_mpsc!(rx, bytes => {
|
||||
let stream = MemoryInputStream::from_bytes(&bytes);
|
||||
|
||||
let scale = image.scale_factor();
|
||||
let scaled_size = size * scale;
|
||||
|
||||
let pixbuf = Pixbuf::from_stream_at_scale(
|
||||
&stream,
|
||||
scaled_size,
|
||||
scaled_size,
|
||||
true,
|
||||
Some(&Cancellable::new()),
|
||||
);
|
||||
|
||||
// Different error types makes this a bit awkward
|
||||
match pixbuf.map(|pixbuf| Self::create_and_load_surface(&pixbuf, &image))
|
||||
{
|
||||
Ok(Err(err)) => error!("{err:?}"),
|
||||
Err(err) => error!("{err:?}"),
|
||||
_ => {}
|
||||
if let Some(location) = location {
|
||||
if location == input_name {
|
||||
None
|
||||
} else {
|
||||
Box::pin(self.resolve_location(&location, size, recurse_depth + 1)).await?
|
||||
}
|
||||
});
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.load_into_image_sync(&image)?;
|
||||
None => None,
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "http"))]
|
||||
self.load_into_image_sync(&image)?;
|
||||
|
||||
Ok(())
|
||||
Ok(location)
|
||||
}
|
||||
|
||||
/// Attempts to synchronously fetch an image from location
|
||||
/// and load into into the image.
|
||||
fn load_into_image_sync(&self, image: >k::Image) -> Result<()> {
|
||||
let scale = image.scale_factor();
|
||||
|
||||
let pixbuf = match &self.location {
|
||||
ImageLocation::Icon { name, theme } => self.get_from_icon(name, theme, scale),
|
||||
ImageLocation::Local(path) => self.get_from_file(path, scale),
|
||||
ImageLocation::Steam(steam_id) => self.get_from_steam_id(steam_id, scale),
|
||||
#[cfg(feature = "http")]
|
||||
_ => unreachable!(), // handled above
|
||||
}?;
|
||||
|
||||
Self::create_and_load_surface(&pixbuf, image)
|
||||
}
|
||||
|
||||
/// Attempts to create a Cairo surface from the provided `Pixbuf`,
|
||||
/// using the provided scaling factor.
|
||||
/// The surface is then loaded into the provided image.
|
||||
/// Attempts to load the provided `ImageRef` into a `Pixbuf`.
|
||||
///
|
||||
/// This is necessary for HiDPI since `Pixbuf`s are always treated as scale factor 1.
|
||||
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(),
|
||||
image.scale_factor(),
|
||||
std::ptr::null_mut(),
|
||||
);
|
||||
Surface::from_raw_full(ptr)
|
||||
/// If `use_fallback` is `true`, a fallback icon will be used
|
||||
/// where an image cannot be found.
|
||||
async fn get_pixbuf(
|
||||
image_ref: &ImageRef,
|
||||
scale: i32,
|
||||
use_fallback: bool,
|
||||
) -> Result<Option<Pixbuf>> {
|
||||
const FALLBACK_ICON_NAME: &str = "dialog-question-symbolic";
|
||||
|
||||
let buf = match &image_ref.location {
|
||||
Some(ImageLocation::Icon(name)) => image_ref.theme.load_icon_for_scale(
|
||||
name,
|
||||
image_ref.size,
|
||||
scale,
|
||||
IconLookupFlags::FORCE_SIZE,
|
||||
),
|
||||
Some(ImageLocation::Local(path)) => {
|
||||
let scaled_size = image_ref.size * scale;
|
||||
Pixbuf::from_file_at_scale(path, scaled_size, scaled_size, true).map(Some)
|
||||
}
|
||||
Some(ImageLocation::Steam(app_id)) => {
|
||||
let path = dirs::data_dir().map_or_else(
|
||||
|| Err(Report::msg("Missing XDG data dir")),
|
||||
|dir| Ok(dir.join(format!("icons/hicolor/32x32/apps/steam_icon_{app_id}.png"))),
|
||||
)?;
|
||||
|
||||
let scaled_size = image_ref.size * scale;
|
||||
Pixbuf::from_file_at_scale(path, scaled_size, scaled_size, true).map(Some)
|
||||
}
|
||||
#[cfg(feature = "http")]
|
||||
Some(ImageLocation::Remote(uri)) => {
|
||||
let res = reqwest::get(uri.clone()).await?;
|
||||
|
||||
let status = res.status();
|
||||
let bytes = if status.is_success() {
|
||||
let bytes = res.bytes().await?;
|
||||
Ok(glib::Bytes::from_owned(bytes))
|
||||
} else {
|
||||
Err(Report::msg(format!(
|
||||
"Received non-success HTTP code ({status})"
|
||||
)))
|
||||
}?;
|
||||
|
||||
let stream = MemoryInputStream::from_bytes(&bytes);
|
||||
let scaled_size = image_ref.size * scale;
|
||||
|
||||
Pixbuf::from_stream_at_scale(
|
||||
&stream,
|
||||
scaled_size,
|
||||
scaled_size,
|
||||
true,
|
||||
Some(&Cancellable::new()),
|
||||
)
|
||||
.map(Some)
|
||||
}
|
||||
None if use_fallback => image_ref.theme.load_icon_for_scale(
|
||||
FALLBACK_ICON_NAME,
|
||||
image_ref.size,
|
||||
scale,
|
||||
IconLookupFlags::empty(),
|
||||
),
|
||||
None => Ok(None),
|
||||
}?;
|
||||
|
||||
image.set_from_surface(Some(&surface));
|
||||
|
||||
Ok(())
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
/// Attempts to get a `Pixbuf` from the GTK icon theme.
|
||||
fn get_from_icon(&self, name: &str, theme: &IconTheme, scale: i32) -> Result<Pixbuf> {
|
||||
let pixbuf =
|
||||
match theme.lookup_icon_for_scale(name, self.size, scale, IconLookupFlags::empty()) {
|
||||
Some(_) => theme.load_icon(name, self.size * scale, IconLookupFlags::FORCE_SIZE),
|
||||
None => Ok(None),
|
||||
}?;
|
||||
|
||||
pixbuf.map_or_else(
|
||||
|| Err(Report::msg("Icon theme does not contain icon '{name}'")),
|
||||
Ok,
|
||||
)
|
||||
/// Returns true if the input starts with a prefix
|
||||
/// that is supported by the parser
|
||||
/// (i.e. the parser would not fall back to checking the input).
|
||||
pub fn is_explicit_input(input: &str) -> bool {
|
||||
input.starts_with("icon:")
|
||||
|| input.starts_with("file://")
|
||||
|| input.starts_with("http://")
|
||||
|| input.starts_with("https://")
|
||||
|| input.starts_with('/')
|
||||
}
|
||||
|
||||
/// Attempts to get a `Pixbuf` from a local file.
|
||||
fn get_from_file(&self, path: &Path, scale: i32) -> Result<Pixbuf> {
|
||||
let scaled_size = self.size * scale;
|
||||
let pixbuf = Pixbuf::from_file_at_scale(path, scaled_size, scaled_size, true)?;
|
||||
Ok(pixbuf)
|
||||
pub fn icon_theme(&self) -> IconTheme {
|
||||
self.icon_theme
|
||||
.borrow()
|
||||
.clone()
|
||||
.expect("theme should be set at startup")
|
||||
}
|
||||
|
||||
/// Attempts to get a `Pixbuf` from a local file,
|
||||
/// using the Steam game ID to look it up.
|
||||
fn get_from_steam_id(&self, steam_id: &str, scale: i32) -> Result<Pixbuf> {
|
||||
// TODO: Can we load this from icon theme with app id `steam_icon_{}`?
|
||||
let path = dirs::data_dir().map_or_else(
|
||||
|| Err(Report::msg("Missing XDG data dir")),
|
||||
|dir| {
|
||||
Ok(dir.join(format!(
|
||||
"icons/hicolor/32x32/apps/steam_icon_{steam_id}.png"
|
||||
)))
|
||||
},
|
||||
)?;
|
||||
/// Sets the custom icon theme name.
|
||||
/// If no name is provided, the system default is used.
|
||||
pub fn set_icon_theme(&self, theme: Option<&str>) {
|
||||
trace!("Setting icon theme to {:?}", theme);
|
||||
|
||||
self.get_from_file(&path, scale)
|
||||
}
|
||||
|
||||
/// Attempts to get `Bytes` from an HTTP resource asynchronously.
|
||||
#[cfg(feature = "http")]
|
||||
async fn get_bytes_from_http(url: reqwest::Url) -> Result<glib::Bytes> {
|
||||
let res = reqwest::get(url).await?;
|
||||
|
||||
let status = res.status();
|
||||
if status.is_success() {
|
||||
let bytes = res.bytes().await?;
|
||||
Ok(glib::Bytes::from_owned(bytes))
|
||||
*self.icon_theme.borrow_mut() = if theme.is_some() {
|
||||
let icon_theme = IconTheme::new();
|
||||
icon_theme.set_custom_theme(theme);
|
||||
Some(icon_theme)
|
||||
} else {
|
||||
Err(Report::msg(format!(
|
||||
"Received non-success HTTP code ({status})"
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
fn get_fallback_icon(theme: &'a IconTheme) -> ImageLocation<'a> {
|
||||
ImageLocation::Icon {
|
||||
name: "dialog-question-symbolic".to_string(),
|
||||
theme,
|
||||
}
|
||||
IconTheme::default()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to create a Cairo `Surface` from the provided `Pixbuf`,
|
||||
/// using the provided scaling factor.
|
||||
/// The surface is then loaded into the provided image.
|
||||
///
|
||||
/// This is necessary for HiDPI since `Pixbuf`s are always treated as scale factor 1.
|
||||
pub fn create_and_load_surface(pixbuf: &Pixbuf, image: &Image) -> Result<()> {
|
||||
let surface = unsafe {
|
||||
let ptr = gdk_cairo_surface_create_from_pixbuf(
|
||||
pixbuf.as_ptr(),
|
||||
image.scale_factor(),
|
||||
std::ptr::null_mut(),
|
||||
);
|
||||
|
||||
Surface::from_raw_full(ptr)
|
||||
}?;
|
||||
|
||||
image.set_from_surface(Some(&surface));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ pub enum IronvarCommand {
|
|||
},
|
||||
|
||||
/// Gets the current value of all `ironvar`s.
|
||||
List,
|
||||
List { namespace: Option<Box<str>> },
|
||||
}
|
||||
|
||||
#[derive(Args, Debug, Serialize, Deserialize)]
|
||||
|
|
|
|||
|
|
@ -23,7 +23,9 @@ impl Ipc {
|
|||
.join("ironbar-ipc.sock");
|
||||
|
||||
if format!("{}", ipc_socket_file.display()).len() > 100 {
|
||||
warn!("The IPC socket file's absolute path exceeds 100 bytes, the socket may fail to create.");
|
||||
warn!(
|
||||
"The IPC socket file's absolute path exceeds 100 bytes, the socket may fail to create."
|
||||
);
|
||||
}
|
||||
|
||||
Self {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize};
|
|||
pub enum Response {
|
||||
Ok,
|
||||
OkValue { value: String },
|
||||
Multi { values: Vec<String> },
|
||||
Err { message: Option<String> },
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,54 +1,67 @@
|
|||
use super::Response;
|
||||
use crate::Ironbar;
|
||||
use crate::bar::Bar;
|
||||
use crate::ipc::{BarCommand, BarCommandType};
|
||||
use crate::modules::PopupButton;
|
||||
use crate::Ironbar;
|
||||
use std::rc::Rc;
|
||||
|
||||
pub fn handle_command(command: BarCommand, ironbar: &Rc<Ironbar>) -> Response {
|
||||
let bar = ironbar.bar_by_name(&command.name);
|
||||
let Some(bar) = bar else {
|
||||
return Response::error("Invalid bar name");
|
||||
};
|
||||
|
||||
pub fn handle_command(command: &BarCommand, ironbar: &Rc<Ironbar>) -> Response {
|
||||
use BarCommandType::*;
|
||||
match command.subcommand {
|
||||
Show => set_visible(&bar, true),
|
||||
Hide => set_visible(&bar, false),
|
||||
SetVisible { visible } => set_visible(&bar, visible),
|
||||
ToggleVisible => set_visible(&bar, !bar.visible()),
|
||||
GetVisible => Response::OkValue {
|
||||
value: bar.visible().to_string(),
|
||||
},
|
||||
|
||||
ShowPopup { widget_name } => show_popup(&bar, widget_name),
|
||||
HidePopup => hide_popup(&bar),
|
||||
SetPopupVisible {
|
||||
widget_name,
|
||||
visible,
|
||||
} => {
|
||||
if visible {
|
||||
show_popup(&bar, widget_name)
|
||||
} else {
|
||||
hide_popup(&bar)
|
||||
}
|
||||
}
|
||||
TogglePopup { widget_name } => {
|
||||
if bar.popup().visible() {
|
||||
hide_popup(&bar)
|
||||
} else {
|
||||
show_popup(&bar, widget_name)
|
||||
}
|
||||
}
|
||||
GetPopupVisible => Response::OkValue {
|
||||
value: bar.popup().visible().to_string(),
|
||||
},
|
||||
SetExclusive { exclusive } => {
|
||||
bar.set_exclusive(exclusive);
|
||||
let bars = ironbar.bars_by_name(&command.name);
|
||||
|
||||
Response::Ok
|
||||
}
|
||||
}
|
||||
bars.into_iter()
|
||||
.map(|bar| match &command.subcommand {
|
||||
Show => set_visible(&bar, true),
|
||||
Hide => set_visible(&bar, false),
|
||||
SetVisible { visible } => set_visible(&bar, *visible),
|
||||
ToggleVisible => set_visible(&bar, !bar.visible()),
|
||||
GetVisible => Response::OkValue {
|
||||
value: bar.visible().to_string(),
|
||||
},
|
||||
ShowPopup { widget_name } => show_popup(&bar, widget_name),
|
||||
HidePopup => hide_popup(&bar),
|
||||
SetPopupVisible {
|
||||
widget_name,
|
||||
visible,
|
||||
} => {
|
||||
if *visible {
|
||||
show_popup(&bar, widget_name)
|
||||
} else {
|
||||
hide_popup(&bar)
|
||||
};
|
||||
Response::Ok
|
||||
}
|
||||
TogglePopup { widget_name } => {
|
||||
if bar.popup().visible() {
|
||||
hide_popup(&bar)
|
||||
} else {
|
||||
show_popup(&bar, widget_name)
|
||||
};
|
||||
Response::Ok
|
||||
}
|
||||
GetPopupVisible => Response::OkValue {
|
||||
value: bar.popup().visible().to_string(),
|
||||
},
|
||||
SetExclusive { exclusive } => {
|
||||
bar.set_exclusive(*exclusive);
|
||||
Response::Ok
|
||||
}
|
||||
})
|
||||
.reduce(|acc, rsp| match (acc, rsp) {
|
||||
// If all responses are Ok, return one Ok. We assume we'll never mix Ok and OkValue.
|
||||
(Response::Ok, _) => Response::Ok,
|
||||
// Two or more OkValues create a multi:
|
||||
(Response::OkValue { value: v1 }, Response::OkValue { value: v2 }) => Response::Multi {
|
||||
values: vec![v1, v2],
|
||||
},
|
||||
(Response::Multi { mut values }, Response::OkValue { value: v }) => {
|
||||
values.push(v);
|
||||
Response::Multi { values }
|
||||
}
|
||||
_ => unreachable!(),
|
||||
})
|
||||
.unwrap_or(Response::error("Invalid bar name"))
|
||||
}
|
||||
|
||||
fn set_visible(bar: &Bar, visible: bool) -> Response {
|
||||
|
|
@ -56,7 +69,7 @@ fn set_visible(bar: &Bar, visible: bool) -> Response {
|
|||
Response::Ok
|
||||
}
|
||||
|
||||
fn show_popup(bar: &Bar, widget_name: String) -> Response {
|
||||
fn show_popup(bar: &Bar, widget_name: &str) -> Response {
|
||||
let popup = bar.popup();
|
||||
|
||||
// only one popup per bar, so hide if open for another widget
|
||||
|
|
|
|||
|
|
@ -1,36 +1,71 @@
|
|||
use crate::ipc::commands::IronvarCommand;
|
||||
use crate::ipc::Response;
|
||||
use crate::{read_lock, write_lock, Ironbar};
|
||||
use crate::Ironbar;
|
||||
use crate::ipc::{IronvarCommand, Response};
|
||||
use crate::ironvar::{Namespace, WritableNamespace};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn handle_command(command: IronvarCommand) -> Response {
|
||||
match command {
|
||||
IronvarCommand::Set { key, value } => {
|
||||
let variable_manager = Ironbar::variable_manager();
|
||||
let mut variable_manager = write_lock!(variable_manager);
|
||||
match variable_manager.set(key, value) {
|
||||
match variable_manager.set(&key, value) {
|
||||
Ok(()) => Response::Ok,
|
||||
Err(err) => Response::error(&format!("{err}")),
|
||||
}
|
||||
}
|
||||
IronvarCommand::Get { key } => {
|
||||
IronvarCommand::Get { mut key } => {
|
||||
let variable_manager = Ironbar::variable_manager();
|
||||
let value = read_lock!(variable_manager).get(&key);
|
||||
let mut ns: Arc<dyn Namespace + Sync + Send> = variable_manager;
|
||||
|
||||
if key.contains('.') {
|
||||
for part in key.split('.') {
|
||||
ns = if let Some(ns) = ns.get_namespace(part) {
|
||||
ns.clone()
|
||||
} else {
|
||||
key = part.into();
|
||||
break;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let value = ns.get(&key);
|
||||
match value {
|
||||
Some(value) => Response::OkValue { value },
|
||||
None => Response::error("Variable not found"),
|
||||
}
|
||||
}
|
||||
IronvarCommand::List => {
|
||||
IronvarCommand::List { namespace } => {
|
||||
let variable_manager = Ironbar::variable_manager();
|
||||
let mut ns: Arc<dyn Namespace + Sync + Send> = variable_manager;
|
||||
|
||||
let mut values = read_lock!(variable_manager)
|
||||
if let Some(namespace) = namespace {
|
||||
for part in namespace.split('.') {
|
||||
ns = match ns.get_namespace(part) {
|
||||
Some(ns) => ns.clone(),
|
||||
None => return Response::error("Namespace not found"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let mut namespaces = ns
|
||||
.namespaces()
|
||||
.iter()
|
||||
.map(|ns| format!("<{ns}>"))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
namespaces.sort();
|
||||
|
||||
let mut value = namespaces.join("\n");
|
||||
|
||||
let mut values = ns
|
||||
.get_all()
|
||||
.iter()
|
||||
.map(|(k, v)| format!("{k}: {}", v.get().unwrap_or_default()))
|
||||
.map(|(k, v)| format!("{k}: {v}"))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
values.sort();
|
||||
let value = values.join("\n");
|
||||
|
||||
value.push('\n');
|
||||
value.push_str(&values.join("\n"));
|
||||
|
||||
Response::OkValue { value }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,18 +6,18 @@ use std::path::Path;
|
|||
use std::rc::Rc;
|
||||
|
||||
use color_eyre::{Report, Result};
|
||||
use gtk::prelude::*;
|
||||
use gtk::Application;
|
||||
use gtk::prelude::*;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::{UnixListener, UnixStream};
|
||||
use tokio::sync::mpsc::{self, Receiver, Sender};
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
use super::Ipc;
|
||||
use crate::channels::{AsyncSenderExt, MpscReceiverExt};
|
||||
use crate::ipc::{Command, Response};
|
||||
use crate::style::load_css;
|
||||
use crate::{glib_recv_mpsc, send_async, spawn, try_send, Ironbar};
|
||||
|
||||
use super::Ipc;
|
||||
use crate::{Ironbar, spawn};
|
||||
|
||||
impl Ipc {
|
||||
/// Starts the IPC server on its socket.
|
||||
|
|
@ -65,10 +65,9 @@ impl Ipc {
|
|||
}
|
||||
});
|
||||
|
||||
let application = application.clone();
|
||||
glib_recv_mpsc!(cmd_rx, command => {
|
||||
let res = Self::handle_command(command, &application, &ironbar);
|
||||
try_send!(res_tx, res);
|
||||
cmd_rx.recv_glib(application, move |application, command| {
|
||||
let res = Self::handle_command(command, application, &ironbar);
|
||||
res_tx.send_spawn(res);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -91,7 +90,7 @@ impl Ipc {
|
|||
|
||||
debug!("Received command: {command:?}");
|
||||
|
||||
send_async!(cmd_tx, command);
|
||||
cmd_tx.send_expect(command).await;
|
||||
let res = res_rx
|
||||
.recv()
|
||||
.await
|
||||
|
|
@ -143,14 +142,14 @@ impl Ipc {
|
|||
}
|
||||
Command::LoadCss { path } => {
|
||||
if path.exists() {
|
||||
load_css(path);
|
||||
load_css(path, application.clone());
|
||||
Response::Ok
|
||||
} else {
|
||||
Response::error("File not found")
|
||||
}
|
||||
}
|
||||
Command::Var(cmd) => ironvar::handle_command(cmd),
|
||||
Command::Bar(cmd) => bar::handle_command(cmd, ironbar),
|
||||
Command::Bar(cmd) => bar::handle_command(&cmd, ironbar),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
132
src/ironvar.rs
132
src/ironvar.rs
|
|
@ -1,13 +1,37 @@
|
|||
#![doc = include_str!("../docs/Ironvars.md")]
|
||||
|
||||
use crate::send;
|
||||
use crate::channels::SyncSenderExt;
|
||||
use crate::{arc_rw, read_lock, write_lock};
|
||||
use color_eyre::{Report, Result};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
type NamespaceTrait = Arc<dyn Namespace + Sync + Send>;
|
||||
|
||||
pub trait Namespace {
|
||||
fn get(&self, key: &str) -> Option<String>;
|
||||
fn list(&self) -> Vec<String>;
|
||||
|
||||
fn get_all(&self) -> HashMap<Box<str>, String> {
|
||||
self.list()
|
||||
.into_iter()
|
||||
.filter_map(|name| self.get(&name).map(|value| (name.into(), value)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn namespaces(&self) -> Vec<String>;
|
||||
fn get_namespace(&self, key: &str) -> Option<NamespaceTrait>;
|
||||
}
|
||||
|
||||
pub trait WritableNamespace: Namespace {
|
||||
fn set(&self, key: &str, value: String) -> Result<()>;
|
||||
}
|
||||
|
||||
/// Global singleton manager for `IronVar` variables.
|
||||
pub struct VariableManager {
|
||||
variables: HashMap<Box<str>, IronVar>,
|
||||
variables: Arc<RwLock<HashMap<Box<str>, IronVar>>>,
|
||||
namespaces: Arc<RwLock<HashMap<Box<str>, NamespaceTrait>>>,
|
||||
}
|
||||
|
||||
impl Default for VariableManager {
|
||||
|
|
@ -19,41 +43,15 @@ impl Default for VariableManager {
|
|||
impl VariableManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
variables: HashMap::new(),
|
||||
variables: arc_rw!(HashMap::new()),
|
||||
namespaces: arc_rw!(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the value for a variable,
|
||||
/// creating it if it does not exist.
|
||||
pub fn set(&mut self, key: Box<str>, value: String) -> Result<()> {
|
||||
if Self::key_is_valid(&key) {
|
||||
if let Some(var) = self.variables.get_mut(&key) {
|
||||
var.set(Some(value));
|
||||
} else {
|
||||
let var = IronVar::new(Some(value));
|
||||
self.variables.insert(key, var);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Report::msg("Invalid key"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the current value of an `ironvar`.
|
||||
/// Prefer to use `subscribe` where possible.
|
||||
pub fn get(&self, key: &str) -> Option<String> {
|
||||
self.variables.get(key).and_then(IronVar::get)
|
||||
}
|
||||
|
||||
pub fn get_all(&self) -> &HashMap<Box<str>, IronVar> {
|
||||
&self.variables
|
||||
}
|
||||
|
||||
/// Subscribes to an `ironvar`, creating it if it does not exist.
|
||||
/// Any time the var is set, its value is sent on the channel.
|
||||
pub fn subscribe(&mut self, key: Box<str>) -> broadcast::Receiver<Option<String>> {
|
||||
self.variables
|
||||
pub fn subscribe(&self, key: Box<str>) -> broadcast::Receiver<Option<String>> {
|
||||
write_lock!(self.variables)
|
||||
.entry(key)
|
||||
.or_insert_with(|| IronVar::new(None))
|
||||
.subscribe()
|
||||
|
|
@ -65,6 +63,72 @@ impl VariableManager {
|
|||
.chars()
|
||||
.all(|char| char.is_alphanumeric() || char == '_' || char == '-')
|
||||
}
|
||||
|
||||
pub fn register_namespace<N>(&self, name: &str, namespace: Arc<N>)
|
||||
where
|
||||
N: Namespace + Sync + Send + 'static,
|
||||
{
|
||||
write_lock!(self.namespaces).insert(name.into(), namespace);
|
||||
}
|
||||
}
|
||||
|
||||
impl Namespace for VariableManager {
|
||||
fn get(&self, key: &str) -> Option<String> {
|
||||
if key.contains('.') {
|
||||
let (ns, key) = key.split_once('.')?;
|
||||
|
||||
let namespaces = read_lock!(self.namespaces);
|
||||
let ns = namespaces.get(ns)?;
|
||||
|
||||
ns.get(key).as_deref().map(ToOwned::to_owned)
|
||||
} else {
|
||||
read_lock!(self.variables).get(key).and_then(IronVar::get)
|
||||
}
|
||||
}
|
||||
|
||||
fn list(&self) -> Vec<String> {
|
||||
read_lock!(self.variables)
|
||||
.keys()
|
||||
.map(ToString::to_string)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_all(&self) -> HashMap<Box<str>, String> {
|
||||
read_lock!(self.variables)
|
||||
.iter()
|
||||
.filter_map(|(k, v)| v.get().map(|value| (k.clone(), value)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn namespaces(&self) -> Vec<String> {
|
||||
read_lock!(self.namespaces)
|
||||
.keys()
|
||||
.map(ToString::to_string)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_namespace(&self, key: &str) -> Option<NamespaceTrait> {
|
||||
read_lock!(self.namespaces).get(key).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
impl WritableNamespace for VariableManager {
|
||||
/// Sets the value for a variable,
|
||||
/// creating it if it does not exist.
|
||||
fn set(&self, key: &str, value: String) -> Result<()> {
|
||||
if Self::key_is_valid(key) {
|
||||
if let Some(var) = write_lock!(self.variables).get_mut(&Box::from(key)) {
|
||||
var.set(Some(value));
|
||||
} else {
|
||||
let var = IronVar::new(Some(value));
|
||||
write_lock!(self.variables).insert(key.into(), var);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Report::msg("Invalid key"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Ironbar dynamic variable representation.
|
||||
|
|
@ -94,14 +158,14 @@ impl IronVar {
|
|||
/// The change is broadcast to all receivers.
|
||||
fn set(&mut self, value: Option<String>) {
|
||||
self.value.clone_from(&value);
|
||||
send!(self.tx, value);
|
||||
self.tx.send_expect(value);
|
||||
}
|
||||
|
||||
/// Subscribes to the variable.
|
||||
/// The latest value is immediately sent to all receivers.
|
||||
fn subscribe(&self) -> broadcast::Receiver<Option<String>> {
|
||||
let rx = self.tx.subscribe();
|
||||
send!(self.tx, self.value.clone());
|
||||
self.tx.send_expect(self.value.clone());
|
||||
rx
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
use color_eyre::Result;
|
||||
use dirs::data_dir;
|
||||
use glib::{LogLevel, LogWriterOutput};
|
||||
use std::{env, panic};
|
||||
use strip_ansi_escapes::Writer;
|
||||
use tracing::error;
|
||||
use tracing::{debug, error, info, warn};
|
||||
use tracing_appender::non_blocking::{NonBlocking, WorkerGuard};
|
||||
use tracing_appender::rolling::Rotation;
|
||||
use tracing_error::ErrorLayer;
|
||||
use tracing_subscriber::fmt::{Layer, MakeWriter};
|
||||
use tracing_subscriber::prelude::*;
|
||||
use tracing_subscriber::{fmt, EnvFilter};
|
||||
use tracing_subscriber::{EnvFilter, fmt};
|
||||
|
||||
struct MakeFileWriter {
|
||||
file_writer: NonBlocking,
|
||||
|
|
@ -31,7 +32,11 @@ impl<'a> MakeWriter<'a> for MakeFileWriter {
|
|||
pub fn install_logging() -> Result<WorkerGuard> {
|
||||
// Disable backtraces by default
|
||||
if env::var("RUST_LIB_BACKTRACE").is_err() {
|
||||
env::set_var("RUST_LIB_BACKTRACE", "0");
|
||||
// as this is the very first thing we do (before runtimes are set up)
|
||||
// we can be sure that it only runs in a single-thread context
|
||||
unsafe {
|
||||
env::set_var("RUST_LIB_BACKTRACE", "0");
|
||||
}
|
||||
}
|
||||
|
||||
// keep guard in scope
|
||||
|
|
@ -89,5 +94,33 @@ fn install_tracing() -> Result<WorkerGuard> {
|
|||
)
|
||||
.init();
|
||||
|
||||
glib::log_set_writer_func(|level, fields| {
|
||||
const KEY_DOMAIN: &str = "GLIB_DOMAIN";
|
||||
const KEY_MESSAGE: &str = "MESSAGE";
|
||||
|
||||
let domain = fields
|
||||
.iter()
|
||||
.find(|f| f.key() == KEY_DOMAIN)
|
||||
.and_then(|f| f.value_str())
|
||||
.unwrap_or("Glib Unknown");
|
||||
|
||||
let message = fields
|
||||
.iter()
|
||||
.find(|f| f.key() == KEY_MESSAGE)
|
||||
.and_then(|f| f.value_str())
|
||||
.unwrap_or_default();
|
||||
|
||||
match level {
|
||||
LogLevel::Error => error!(target: "GTK", "[{domain}] {message}"),
|
||||
LogLevel::Critical => error!(target: "GTK", "[{domain}] CRITICAL: {message}"),
|
||||
LogLevel::Warning => warn!(target: "GTK", "[{domain}] {message}"),
|
||||
LogLevel::Message => info!(target: "GTK", "[{domain}] MESSAGE: {message}"),
|
||||
LogLevel::Info => info!(target: "GTK", "[{domain}] MESSAGE: {message}"),
|
||||
LogLevel::Debug => debug!(target: "GTK", "[{domain}] {message}"),
|
||||
}
|
||||
|
||||
LogWriterOutput::Handled
|
||||
});
|
||||
|
||||
Ok(guard)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,13 +35,14 @@ macro_rules! module_impl {
|
|||
/// send_async!(tx, "my message");
|
||||
/// ```
|
||||
#[macro_export]
|
||||
#[deprecated(since = "0.17.0", note = "Use `AsyncSenderExt::send_expect` instead")]
|
||||
macro_rules! send_async {
|
||||
($tx:expr, $msg:expr) => {
|
||||
$tx.send($msg).await.expect($crate::error::ERR_CHANNEL_SEND)
|
||||
};
|
||||
}
|
||||
|
||||
/// Sends a message on an synchronous `Sender` using `send()`
|
||||
/// Sends a message on a synchronous `Sender` using `send()`
|
||||
/// Panics if the message cannot be sent.
|
||||
///
|
||||
/// # Usage:
|
||||
|
|
@ -50,13 +51,14 @@ macro_rules! send_async {
|
|||
/// send!(tx, "my message");
|
||||
/// ```
|
||||
#[macro_export]
|
||||
#[deprecated(since = "0.17.0", note = "Use `SyncSenderExt::send_expect` instead")]
|
||||
macro_rules! send {
|
||||
($tx:expr, $msg:expr) => {
|
||||
$tx.send($msg).expect($crate::error::ERR_CHANNEL_SEND)
|
||||
};
|
||||
}
|
||||
|
||||
/// Sends a message on an synchronous `Sender` using `try_send()`
|
||||
/// Sends a message on a synchronous `Sender` using `try_send()`
|
||||
/// Panics if the message cannot be sent.
|
||||
///
|
||||
/// # Usage:
|
||||
|
|
@ -65,12 +67,34 @@ macro_rules! send {
|
|||
/// try_send!(tx, "my message");
|
||||
/// ```
|
||||
#[macro_export]
|
||||
#[deprecated(since = "0.17.0", note = "Use `AsyncSenderExt::send_spawn` instead")]
|
||||
macro_rules! try_send {
|
||||
($tx:expr, $msg:expr) => {
|
||||
$tx.try_send($msg).expect($crate::error::ERR_CHANNEL_SEND)
|
||||
};
|
||||
}
|
||||
|
||||
/// Sends a message, wrapped inside a `ModuleUpdateEvent::Update` variant,
|
||||
/// on an asynchronous `Sender` using `send()`.
|
||||
///
|
||||
/// This is a convenience wrapper around `send_async`
|
||||
/// to avoid needing to write the full enum every time.
|
||||
///
|
||||
/// Panics if the message cannot be sent.
|
||||
///
|
||||
/// # Usage:
|
||||
///
|
||||
/// ```rs
|
||||
/// module_update!(tx, "my event");
|
||||
/// ```
|
||||
#[macro_export]
|
||||
#[deprecated(since = "0.17.0", note = "Use `AsyncSenderExt::send_update` instead")]
|
||||
macro_rules! module_update {
|
||||
($tx:expr, $msg:expr) => {
|
||||
send_async!($tx, $crate::modules::ModuleUpdateEvent::Update($msg))
|
||||
};
|
||||
}
|
||||
|
||||
/// Spawns a `GLib` future on the local thread, and calls `rx.recv()`
|
||||
/// in a loop.
|
||||
///
|
||||
|
|
@ -85,7 +109,13 @@ macro_rules! try_send {
|
|||
/// glib_recv(rx, msg => println!("{msg}"));
|
||||
/// ```
|
||||
#[macro_export]
|
||||
#[deprecated(
|
||||
since = "0.17.0",
|
||||
note = "Use `BroadcastReceiverExt::recv_glib` instead"
|
||||
)]
|
||||
macro_rules! glib_recv {
|
||||
($rx:expr, $func:ident) => { glib_recv!($rx, ev => $func(ev)) };
|
||||
|
||||
($rx:expr, $val:ident => $expr:expr) => {{
|
||||
glib::spawn_future_local(async move {
|
||||
// re-delcare in case ie `context.subscribe()` is passed directly
|
||||
|
|
@ -121,7 +151,10 @@ macro_rules! glib_recv {
|
|||
/// glib_recv_mpsc(rx, msg => println!("{msg}"));
|
||||
/// ```
|
||||
#[macro_export]
|
||||
#[deprecated(since = "0.17.0", note = "Use `MpscReceiverExt::recv_glib` instead")]
|
||||
macro_rules! glib_recv_mpsc {
|
||||
($rx:expr, $func:ident) => { glib_recv_mpsc!($rx, ev => $func(ev)) };
|
||||
|
||||
($rx:expr, $val:ident => $expr:expr) => {{
|
||||
glib::spawn_future_local(async move {
|
||||
// re-delcare in case ie `context.subscribe()` is passed directly
|
||||
|
|
|
|||
121
src/main.rs
121
src/main.rs
|
|
@ -7,36 +7,36 @@ use std::path::PathBuf;
|
|||
use std::process::exit;
|
||||
use std::rc::Rc;
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
#[cfg(feature = "ipc")]
|
||||
use std::sync::RwLock;
|
||||
use std::sync::{mpsc, Arc, Mutex, OnceLock};
|
||||
use std::sync::{Arc, OnceLock, mpsc};
|
||||
|
||||
use cfg_if::cfg_if;
|
||||
#[cfg(feature = "cli")]
|
||||
use clap::Parser;
|
||||
use color_eyre::eyre::Result;
|
||||
use color_eyre::Report;
|
||||
use color_eyre::eyre::Result;
|
||||
use dirs::config_dir;
|
||||
use glib::PropertySet;
|
||||
use gtk::Application;
|
||||
use gtk::gdk::Display;
|
||||
use gtk::prelude::*;
|
||||
use gtk::Application;
|
||||
use smithay_client_toolkit::output::OutputInfo;
|
||||
use tokio::runtime::Runtime;
|
||||
use tokio::task::{block_in_place, JoinHandle};
|
||||
use tokio::task::{JoinHandle, block_in_place};
|
||||
use tracing::{debug, error, info, warn};
|
||||
use universal_config::ConfigLoader;
|
||||
|
||||
use crate::bar::{create_bar, Bar};
|
||||
use crate::clients::wayland::OutputEventType;
|
||||
use crate::bar::{Bar, create_bar};
|
||||
use crate::channels::SyncSenderExt;
|
||||
use crate::clients::Clients;
|
||||
use crate::clients::wayland::OutputEventType;
|
||||
use crate::config::{Config, MonitorConfig};
|
||||
use crate::desktop_file::DesktopFiles;
|
||||
use crate::error::ExitCode;
|
||||
#[cfg(feature = "ipc")]
|
||||
use crate::ironvar::VariableManager;
|
||||
use crate::ironvar::{VariableManager, WritableNamespace};
|
||||
use crate::style::load_css;
|
||||
|
||||
mod bar;
|
||||
mod channels;
|
||||
#[cfg(feature = "cli")]
|
||||
mod cli;
|
||||
mod clients;
|
||||
|
|
@ -79,14 +79,17 @@ fn run_with_args() {
|
|||
#[cfg(feature = "schema")]
|
||||
if args.print_schema {
|
||||
let schema = schemars::schema_for!(Config);
|
||||
println!("{}", serde_json::to_string_pretty(&schema).unwrap());
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&schema).expect("to be serializable")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
match args.command {
|
||||
Some(command) => {
|
||||
if args.debug {
|
||||
eprintln!("REQUEST: {command:?}")
|
||||
eprintln!("REQUEST: {command:?}");
|
||||
}
|
||||
|
||||
let rt = create_runtime();
|
||||
|
|
@ -95,13 +98,16 @@ fn run_with_args() {
|
|||
match ipc.send(command, args.debug).await {
|
||||
Ok(res) => {
|
||||
if args.debug {
|
||||
eprintln!("RESPONSE: {res:?}")
|
||||
eprintln!("RESPONSE: {res:?}");
|
||||
}
|
||||
|
||||
cli::handle_response(res, args.format.unwrap_or_default())
|
||||
cli::handle_response(res, args.format.unwrap_or_default());
|
||||
}
|
||||
Err(err) => error!("{err:?}"),
|
||||
};
|
||||
Err(err) => {
|
||||
error!("{err:#}");
|
||||
exit(ExitCode::IpcResponseError as i32)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
None => start_ironbar(),
|
||||
|
|
@ -114,17 +120,26 @@ pub struct Ironbar {
|
|||
clients: Rc<RefCell<Clients>>,
|
||||
config: Rc<RefCell<Config>>,
|
||||
config_dir: PathBuf,
|
||||
|
||||
desktop_files: DesktopFiles,
|
||||
image_provider: image::Provider,
|
||||
}
|
||||
|
||||
impl Ironbar {
|
||||
fn new() -> Self {
|
||||
let (config, config_dir) = load_config();
|
||||
let (mut config, config_dir) = load_config();
|
||||
|
||||
let desktop_files = DesktopFiles::new();
|
||||
let image_provider =
|
||||
image::Provider::new(desktop_files.clone(), &mut config.icon_overrides);
|
||||
|
||||
Self {
|
||||
bars: Rc::new(RefCell::new(vec![])),
|
||||
clients: Rc::new(RefCell::new(Clients::new())),
|
||||
config: Rc::new(RefCell::new(config)),
|
||||
config_dir,
|
||||
desktop_files,
|
||||
image_provider,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -153,7 +168,7 @@ impl Ironbar {
|
|||
return;
|
||||
}
|
||||
|
||||
running.set(true);
|
||||
running.store(true, Ordering::Relaxed);
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "ipc")] {
|
||||
|
|
@ -177,7 +192,7 @@ impl Ironbar {
|
|||
);
|
||||
|
||||
if style_path.exists() {
|
||||
load_css(style_path);
|
||||
load_css(style_path, app.clone());
|
||||
}
|
||||
|
||||
let (tx, rx) = mpsc::channel();
|
||||
|
|
@ -199,17 +214,21 @@ impl Ironbar {
|
|||
.expect("Error setting Ctrl-C handler");
|
||||
|
||||
let hold = app.hold();
|
||||
send!(activate_tx, hold);
|
||||
activate_tx.send_expect(hold);
|
||||
});
|
||||
|
||||
{
|
||||
let instance = instance2;
|
||||
let instance = instance2.clone();
|
||||
let app = app.clone();
|
||||
|
||||
glib::spawn_future_local(async move {
|
||||
let _hold = activate_rx.recv().expect("to receive activation signal");
|
||||
debug!("Received activation signal, initialising bars");
|
||||
|
||||
instance
|
||||
.image_provider
|
||||
.set_icon_theme(instance.config.borrow().icon_theme.as_deref());
|
||||
|
||||
while let Ok(event) = rx_outputs.recv().await {
|
||||
match event.event_type {
|
||||
OutputEventType::New => {
|
||||
|
|
@ -258,24 +277,35 @@ impl Ironbar {
|
|||
/// Gets the `Ironvar` manager singleton.
|
||||
#[cfg(feature = "ipc")]
|
||||
#[must_use]
|
||||
pub fn variable_manager() -> Arc<RwLock<VariableManager>> {
|
||||
static VARIABLE_MANAGER: OnceLock<Arc<RwLock<VariableManager>>> = OnceLock::new();
|
||||
pub fn variable_manager() -> Arc<VariableManager> {
|
||||
static VARIABLE_MANAGER: OnceLock<Arc<VariableManager>> = OnceLock::new();
|
||||
VARIABLE_MANAGER
|
||||
.get_or_init(|| arc_rw!(VariableManager::new()))
|
||||
.get_or_init(|| Arc::new(VariableManager::new()))
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// Gets a clone of a bar by its unique name.
|
||||
#[must_use]
|
||||
pub fn desktop_files(&self) -> DesktopFiles {
|
||||
self.desktop_files.clone()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn image_provider(&self) -> image::Provider {
|
||||
self.image_provider.clone()
|
||||
}
|
||||
|
||||
/// Gets clones of bars by their name.
|
||||
///
|
||||
/// Since the bar contains mostly GTK objects,
|
||||
/// Since the bars contain mostly GTK objects,
|
||||
/// the clone is cheap enough to not worry about.
|
||||
#[must_use]
|
||||
pub fn bar_by_name(&self, name: &str) -> Option<Bar> {
|
||||
pub fn bars_by_name(&self, name: &str) -> Vec<Bar> {
|
||||
self.bars
|
||||
.borrow()
|
||||
.iter()
|
||||
.find(|&bar| bar.name() == name)
|
||||
.filter(|&bar| bar.name() == name)
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Re-reads the config file from disk and replaces the active config.
|
||||
|
|
@ -330,7 +360,7 @@ fn load_config() -> (Config, PathBuf) {
|
|||
if let Some(ironvars) = config.ironvar_defaults.take() {
|
||||
let variable_manager = Ironbar::variable_manager();
|
||||
for (k, v) in ironvars {
|
||||
if write_lock!(variable_manager).set(k.clone(), v).is_err() {
|
||||
if variable_manager.set(&k, v).is_err() {
|
||||
warn!("Ignoring invalid ironvar: '{k}'");
|
||||
}
|
||||
}
|
||||
|
|
@ -357,36 +387,20 @@ fn load_output_bars(
|
|||
app: &Application,
|
||||
output: &OutputInfo,
|
||||
) -> Result<Vec<Bar>> {
|
||||
// Hack to track monitor positions due to new GTK3/wlroots bug:
|
||||
// https://github.com/swaywm/sway/issues/8164
|
||||
// This relies on Wayland always tracking monitors in the same order as GDK.
|
||||
// We also need this static to ensure hot-reloading continues to work as best we can.
|
||||
static INDEX_MAP: OnceLock<Mutex<Vec<String>>> = OnceLock::new();
|
||||
let output_size = output.logical_size.unwrap_or_default();
|
||||
|
||||
let Some(monitor_name) = &output.name else {
|
||||
return Err(Report::msg("Output missing monitor name"));
|
||||
};
|
||||
|
||||
let map = INDEX_MAP.get_or_init(|| Mutex::new(vec![]));
|
||||
|
||||
let index = lock!(map).iter().position(|n| n == monitor_name);
|
||||
let index = match index {
|
||||
Some(index) => index,
|
||||
None => {
|
||||
lock!(map).push(monitor_name.clone());
|
||||
lock!(map).len() - 1
|
||||
}
|
||||
};
|
||||
|
||||
let config = ironbar.config.borrow();
|
||||
|
||||
let display = get_display();
|
||||
|
||||
// let pos = output.logical_position.unwrap_or_default();
|
||||
// let monitor = display
|
||||
// .monitor_at_point(pos.0, pos.1)
|
||||
// .expect("monitor to exist");
|
||||
|
||||
let monitor = display.monitor(index as i32).expect("monitor to exist");
|
||||
let pos = output.logical_position.unwrap_or_default();
|
||||
let monitor = display
|
||||
.monitor_at_point(pos.0, pos.1)
|
||||
.expect("monitor to exist");
|
||||
|
||||
let show_default_bar =
|
||||
config.bar.start.is_some() || config.bar.center.is_some() || config.bar.end.is_some();
|
||||
|
|
@ -401,6 +415,7 @@ fn load_output_bars(
|
|||
app,
|
||||
&monitor,
|
||||
monitor_name.to_string(),
|
||||
output_size,
|
||||
config.clone(),
|
||||
ironbar.clone(),
|
||||
)?]
|
||||
|
|
@ -412,6 +427,7 @@ fn load_output_bars(
|
|||
app,
|
||||
&monitor,
|
||||
monitor_name.to_string(),
|
||||
output_size,
|
||||
config.clone(),
|
||||
ironbar.clone(),
|
||||
)
|
||||
|
|
@ -421,6 +437,7 @@ fn load_output_bars(
|
|||
app,
|
||||
&monitor,
|
||||
monitor_name.to_string(),
|
||||
output_size,
|
||||
config.bar.clone(),
|
||||
ironbar.clone(),
|
||||
)?],
|
||||
|
|
@ -458,7 +475,7 @@ where
|
|||
/// Blocks on a `Future` until it resolves.
|
||||
///
|
||||
/// This is not an `async` operation
|
||||
/// so can be used outside of an async function.
|
||||
/// so can be used outside an async function.
|
||||
///
|
||||
/// Use sparingly, as this risks blocking the UI thread!
|
||||
/// Prefer async functions wherever possible.
|
||||
|
|
|
|||
101
src/modules/bindmode.rs
Normal file
101
src/modules/bindmode.rs
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
use crate::channels::{AsyncSenderExt, BroadcastReceiverExt};
|
||||
use crate::clients::compositor::BindModeUpdate;
|
||||
use crate::config::{CommonConfig, LayoutConfig, TruncateMode};
|
||||
use crate::gtk_helpers::IronbarLabelExt;
|
||||
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
|
||||
use crate::{module_impl, spawn};
|
||||
use color_eyre::Result;
|
||||
use gtk::Label;
|
||||
use gtk::prelude::*;
|
||||
use serde::Deserialize;
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{info, trace};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct Bindmode {
|
||||
// -- Common --
|
||||
/// See [truncate options](module-level-options#truncate-mode).
|
||||
///
|
||||
/// **Default**: `null`
|
||||
pub truncate: Option<TruncateMode>,
|
||||
|
||||
/// See [layout options](module-level-options#layout)
|
||||
#[serde(default, flatten)]
|
||||
layout: LayoutConfig,
|
||||
|
||||
/// See [common options](module-level-options#common-options).
|
||||
#[serde(flatten)]
|
||||
pub common: Option<CommonConfig>,
|
||||
}
|
||||
|
||||
impl Module<Label> for Bindmode {
|
||||
type SendMessage = BindModeUpdate;
|
||||
type ReceiveMessage = ();
|
||||
|
||||
module_impl!("bindmode");
|
||||
|
||||
fn spawn_controller(
|
||||
&self,
|
||||
_info: &ModuleInfo,
|
||||
context: &WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
_rx: mpsc::Receiver<Self::ReceiveMessage>,
|
||||
) -> Result<()> {
|
||||
info!("Bindmode module started");
|
||||
|
||||
let client = context.try_client::<dyn crate::clients::compositor::BindModeClient>()?;
|
||||
|
||||
let tx = context.tx.clone();
|
||||
|
||||
let mut rx = client.subscribe()?;
|
||||
spawn(async move {
|
||||
while let Ok(ev) = rx.recv().await {
|
||||
tx.send_update(ev).await;
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn into_widget(
|
||||
self,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> Result<ModuleParts<Label>> {
|
||||
let label = Label::builder()
|
||||
.use_markup(true)
|
||||
.angle(self.layout.angle(info))
|
||||
.justify(self.layout.justify.into())
|
||||
.build();
|
||||
|
||||
if let Some(truncate) = self.truncate {
|
||||
label.truncate(truncate);
|
||||
}
|
||||
|
||||
// Send a dummy event on init so that the widget starts hidden
|
||||
{
|
||||
let tx = context.tx.clone();
|
||||
tx.send_spawn(ModuleUpdateEvent::Update(BindModeUpdate {
|
||||
name: String::new(),
|
||||
pango_markup: true,
|
||||
}));
|
||||
}
|
||||
|
||||
context.subscribe().recv_glib(&label, |label, mode| {
|
||||
trace!("mode: {:?}", mode);
|
||||
label.set_use_markup(mode.pango_markup);
|
||||
label.set_label_escaped(&mode.name);
|
||||
|
||||
if mode.name.is_empty() {
|
||||
label.hide();
|
||||
} else {
|
||||
label.show();
|
||||
}
|
||||
});
|
||||
|
||||
Ok(ModuleParts {
|
||||
widget: label,
|
||||
popup: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,15 @@
|
|||
use crate::channels::{AsyncSenderExt, BroadcastReceiverExt};
|
||||
use crate::config::CommonConfig;
|
||||
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
|
||||
use crate::{glib_recv, module_impl, spawn, try_send};
|
||||
use crate::modules::{Module, ModuleInfo, ModuleParts, WidgetContext};
|
||||
use crate::{module_impl, spawn};
|
||||
use cairo::{Format, ImageSurface};
|
||||
use glib::translate::IntoGlibPtr;
|
||||
use glib::Propagation;
|
||||
use gtk::prelude::*;
|
||||
use glib::translate::ToGlibPtr;
|
||||
use gtk::DrawingArea;
|
||||
use gtk::prelude::*;
|
||||
use mlua::{Error, Function, LightUserData};
|
||||
use notify::event::ModifyKind;
|
||||
use notify::{recommended_watcher, Event, EventKind, RecursiveMode, Watcher};
|
||||
use notify::{Event, EventKind, RecursiveMode, Watcher, recommended_watcher};
|
||||
use serde::Deserialize;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
|
@ -74,7 +75,7 @@ impl Module<gtk::Box> for CairoModule {
|
|||
where
|
||||
<Self as Module<gtk::Box>>::SendMessage: Clone,
|
||||
{
|
||||
let path = self.path.to_path_buf();
|
||||
let path = self.path.clone();
|
||||
|
||||
let tx = context.tx.clone();
|
||||
spawn(async move {
|
||||
|
|
@ -87,7 +88,7 @@ impl Module<gtk::Box> for CairoModule {
|
|||
debug!("{event:?}");
|
||||
|
||||
if event.paths.first().is_some_and(|p| p == &path) {
|
||||
try_send!(tx, ModuleUpdateEvent::Update(()));
|
||||
tx.send_update_spawn(());
|
||||
}
|
||||
}
|
||||
Err(e) => error!("Error occurred when watching stylesheet: {:?}", e),
|
||||
|
|
@ -146,35 +147,37 @@ impl Module<gtk::Box> for CairoModule {
|
|||
|
||||
let path = self.path.clone();
|
||||
|
||||
area.connect_draw(move |_, cr| {
|
||||
let function: Function = lua
|
||||
.load(include_str!("../../lua/draw.lua"))
|
||||
.eval()
|
||||
.expect("to be valid");
|
||||
let function: Function = lua
|
||||
.load(include_str!("../../lua/draw.lua"))
|
||||
.eval()
|
||||
.expect("to be valid");
|
||||
|
||||
area.connect_draw(move |_, cr| {
|
||||
if let Err(err) = cr.set_source_surface(&surface, 0.0, 0.0) {
|
||||
error!("{err}");
|
||||
return Propagation::Stop;
|
||||
}
|
||||
|
||||
let ptr = unsafe { cr.clone().into_glib_ptr().cast() };
|
||||
let ptr = cr.to_glib_full();
|
||||
|
||||
// mlua needs a valid return type, even if we don't return anything
|
||||
|
||||
if let Err(err) =
|
||||
function.call::<_, Option<bool>>((id.as_str(), LightUserData(ptr)))
|
||||
function.call::<Option<bool>>((id.as_str(), LightUserData(ptr.cast())))
|
||||
{
|
||||
match err {
|
||||
Error::RuntimeError(message) => {
|
||||
let message = message.split_once("]:").expect("to exist").1;
|
||||
error!("[lua runtime error] {}:{message}", path.display())
|
||||
}
|
||||
_ => error!("{err}"),
|
||||
if let Error::RuntimeError(message) = err {
|
||||
let message = message.split_once("]:").expect("to exist").1;
|
||||
error!("[lua runtime error] {}:{message}", path.display());
|
||||
} else {
|
||||
error!("{err}");
|
||||
}
|
||||
|
||||
return Propagation::Stop;
|
||||
}
|
||||
|
||||
unsafe {
|
||||
cairo::ffi::cairo_destroy(ptr);
|
||||
}
|
||||
|
||||
Propagation::Proceed
|
||||
});
|
||||
}
|
||||
|
|
@ -189,22 +192,20 @@ impl Module<gtk::Box> for CairoModule {
|
|||
}
|
||||
});
|
||||
|
||||
glib_recv!(context.subscribe(), _ev => {
|
||||
context.subscribe().recv_glib((), move |(), _ev| {
|
||||
let res = fs::read_to_string(&self.path)
|
||||
.map(|s| s.replace("function draw", format!("function __draw_{id}").as_str()));
|
||||
|
||||
match res {
|
||||
Ok(script) => {
|
||||
match lua.load(&script).exec() {
|
||||
Ok(_) => {},
|
||||
Err(Error::SyntaxError { message, ..}) => {
|
||||
let message = message.split_once("]:").expect("to exist").1;
|
||||
error!("[lua syntax error] {}:{message}", self.path.display())
|
||||
},
|
||||
Err(err) => error!("lua error: {err:?}")
|
||||
Ok(script) => match lua.load(&script).exec() {
|
||||
Ok(()) => {}
|
||||
Err(Error::SyntaxError { message, .. }) => {
|
||||
let message = message.split_once("]:").expect("to exist").1;
|
||||
error!("[lua syntax error] {}:{message}", self.path.display());
|
||||
}
|
||||
Err(err) => error!("lua error: {err:?}"),
|
||||
},
|
||||
Err(err) => error!("{err:?}")
|
||||
Err(err) => error!("{err:?}"),
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
use crate::channels::{AsyncSenderExt, BroadcastReceiverExt};
|
||||
use crate::clients::clipboard::{self, ClipboardEvent};
|
||||
use crate::clients::wayland::{ClipboardItem, ClipboardValue};
|
||||
use crate::config::{CommonConfig, TruncateMode};
|
||||
use crate::image::new_icon_button;
|
||||
use crate::config::{CommonConfig, LayoutConfig, TruncateMode};
|
||||
use crate::gtk_helpers::IronbarGtkExt;
|
||||
use crate::gtk_helpers::IronbarLabelExt;
|
||||
use crate::image::IconButton;
|
||||
use crate::modules::{
|
||||
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, PopupButton, WidgetContext,
|
||||
};
|
||||
use crate::{glib_recv, module_impl, spawn, try_send};
|
||||
use crate::{module_impl, spawn};
|
||||
use glib::Propagation;
|
||||
use gtk::gdk_pixbuf::Pixbuf;
|
||||
use gtk::gio::{Cancellable, MemoryInputStream};
|
||||
|
|
@ -13,7 +16,8 @@ use gtk::prelude::*;
|
|||
use gtk::{Button, EventBox, Image, Label, Orientation, RadioButton, Widget};
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use tokio::sync::{broadcast, mpsc};
|
||||
use std::ops::Deref;
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{debug, error};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
|
|
@ -46,6 +50,10 @@ pub struct ClipboardModule {
|
|||
/// **Default**: `null`
|
||||
truncate: Option<TruncateMode>,
|
||||
|
||||
/// See [layout options](module-level-options#layout)
|
||||
#[serde(default, flatten)]
|
||||
layout: LayoutConfig,
|
||||
|
||||
/// See [common options](module-level-options#common-options).
|
||||
#[serde(flatten)]
|
||||
pub common: Option<CommonConfig>,
|
||||
|
|
@ -102,18 +110,16 @@ impl Module<Button> for ClipboardModule {
|
|||
match event {
|
||||
ClipboardEvent::Add(item) => {
|
||||
let msg = match item.value.as_ref() {
|
||||
ClipboardValue::Other => {
|
||||
ModuleUpdateEvent::Update(ControllerEvent::Deactivate)
|
||||
}
|
||||
_ => ModuleUpdateEvent::Update(ControllerEvent::Add(item.id, item)),
|
||||
ClipboardValue::Other => ControllerEvent::Deactivate,
|
||||
_ => ControllerEvent::Add(item.id, item),
|
||||
};
|
||||
try_send!(tx, msg);
|
||||
tx.send_update_spawn(msg);
|
||||
}
|
||||
ClipboardEvent::Remove(id) => {
|
||||
try_send!(tx, ModuleUpdateEvent::Update(ControllerEvent::Remove(id)));
|
||||
tx.send_update_spawn(ControllerEvent::Remove(id));
|
||||
}
|
||||
ClipboardEvent::Activate(id) => {
|
||||
try_send!(tx, ModuleUpdateEvent::Update(ControllerEvent::Activate(id)));
|
||||
tx.send_update_spawn(ControllerEvent::Activate(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -141,27 +147,28 @@ impl Module<Button> for ClipboardModule {
|
|||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> color_eyre::Result<ModuleParts<Button>> {
|
||||
let button = new_icon_button(&self.icon, info.icon_theme, self.icon_size);
|
||||
button.style_context().add_class("btn");
|
||||
let button = IconButton::new(&self.icon, self.icon_size, context.ironbar.image_provider());
|
||||
|
||||
button.label().set_angle(self.layout.angle(info));
|
||||
button.label().set_justify(self.layout.justify.into());
|
||||
|
||||
button.add_class("btn");
|
||||
|
||||
let tx = context.tx.clone();
|
||||
button.connect_clicked(move |button| {
|
||||
try_send!(tx, ModuleUpdateEvent::TogglePopup(button.popup_id()));
|
||||
tx.send_spawn(ModuleUpdateEvent::TogglePopup(button.popup_id()));
|
||||
});
|
||||
|
||||
let rx = context.subscribe();
|
||||
let popup = self
|
||||
.into_popup(context.controller_tx.clone(), rx, context, info)
|
||||
.into_popup(context, info)
|
||||
.into_popup_parts(vec![&button]);
|
||||
|
||||
Ok(ModuleParts::new(button, popup))
|
||||
Ok(ModuleParts::new(button.deref().clone(), popup))
|
||||
}
|
||||
|
||||
fn into_popup(
|
||||
self,
|
||||
tx: mpsc::Sender<Self::ReceiveMessage>,
|
||||
rx: broadcast::Receiver<Self::SendMessage>,
|
||||
_context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
_info: &ModuleInfo,
|
||||
) -> Option<gtk::Box>
|
||||
where
|
||||
|
|
@ -177,9 +184,9 @@ impl Module<Button> for ClipboardModule {
|
|||
|
||||
let mut items = HashMap::new();
|
||||
|
||||
{
|
||||
let hidden_option = hidden_option.clone();
|
||||
glib_recv!(rx, event => {
|
||||
context
|
||||
.subscribe()
|
||||
.recv_glib(&hidden_option, move |hidden_option, event| {
|
||||
match event {
|
||||
ControllerEvent::Add(id, item) => {
|
||||
debug!("Adding new value with ID {}", id);
|
||||
|
|
@ -189,13 +196,13 @@ impl Module<Button> for ClipboardModule {
|
|||
|
||||
let button = match item.value.as_ref() {
|
||||
ClipboardValue::Text(value) => {
|
||||
let button = RadioButton::from_widget(&hidden_option);
|
||||
let button = RadioButton::from_widget(hidden_option);
|
||||
|
||||
let label = Label::new(Some(value));
|
||||
button.add(&label);
|
||||
|
||||
if let Some(truncate) = self.truncate {
|
||||
truncate.truncate_label(&label);
|
||||
label.truncate(truncate);
|
||||
}
|
||||
|
||||
button.style_context().add_class("text");
|
||||
|
|
@ -209,16 +216,24 @@ impl Module<Button> for ClipboardModule {
|
|||
64,
|
||||
true,
|
||||
Some(&Cancellable::new()),
|
||||
)
|
||||
.expect("Failed to read Pixbuf from stream");
|
||||
let image = Image::from_pixbuf(Some(&pixbuf));
|
||||
);
|
||||
|
||||
let button = RadioButton::from_widget(&hidden_option);
|
||||
button.set_image(Some(&image));
|
||||
button.set_always_show_image(true);
|
||||
button.style_context().add_class("image");
|
||||
match pixbuf {
|
||||
Ok(pixbuf) => {
|
||||
let image = Image::from_pixbuf(Some(&pixbuf));
|
||||
|
||||
button
|
||||
let button = RadioButton::from_widget(hidden_option);
|
||||
button.set_image(Some(&image));
|
||||
button.set_always_show_image(true);
|
||||
button.style_context().add_class("image");
|
||||
|
||||
button
|
||||
}
|
||||
Err(err) => {
|
||||
error!("{err:?}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
ClipboardValue::Other => unreachable!(),
|
||||
};
|
||||
|
|
@ -233,7 +248,7 @@ impl Module<Button> for ClipboardModule {
|
|||
button_wrapper.set_above_child(true);
|
||||
|
||||
{
|
||||
let tx = tx.clone();
|
||||
let tx = context.controller_tx.clone();
|
||||
button_wrapper.connect_button_press_event(
|
||||
move |button_wrapper, event| {
|
||||
// left click
|
||||
|
|
@ -242,7 +257,7 @@ impl Module<Button> for ClipboardModule {
|
|||
.expect("Failed to get id from button name");
|
||||
|
||||
debug!("Copying item with id: {id}");
|
||||
try_send!(tx, UIEvent::Copy(id));
|
||||
tx.send_spawn(UIEvent::Copy(id));
|
||||
}
|
||||
|
||||
Propagation::Stop
|
||||
|
|
@ -255,7 +270,7 @@ impl Module<Button> for ClipboardModule {
|
|||
remove_button.style_context().add_class("btn-remove");
|
||||
|
||||
{
|
||||
let tx = tx.clone();
|
||||
let tx = context.controller_tx.clone();
|
||||
let entries = entries.clone();
|
||||
let row = row.clone();
|
||||
|
||||
|
|
@ -264,7 +279,7 @@ impl Module<Button> for ClipboardModule {
|
|||
.expect("Failed to get id from button name");
|
||||
|
||||
debug!("Removing item with id: {id}");
|
||||
try_send!(tx, UIEvent::Remove(id));
|
||||
tx.send_spawn(UIEvent::Remove(id));
|
||||
|
||||
entries.remove(&row);
|
||||
});
|
||||
|
|
@ -305,7 +320,6 @@ impl Module<Button> for ClipboardModule {
|
|||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
container.show_all();
|
||||
hidden_option.hide();
|
||||
|
|
|
|||
|
|
@ -1,19 +1,20 @@
|
|||
use std::env;
|
||||
|
||||
use chrono::{DateTime, Local, Locale};
|
||||
use chrono::{DateTime, Datelike, Local, Locale};
|
||||
use color_eyre::Result;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Align, Button, Calendar, Label, Orientation};
|
||||
use serde::Deserialize;
|
||||
use tokio::sync::{broadcast, mpsc};
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::time::sleep;
|
||||
|
||||
use crate::config::{CommonConfig, ModuleOrientation};
|
||||
use crate::channels::{AsyncSenderExt, BroadcastReceiverExt};
|
||||
use crate::config::{CommonConfig, LayoutConfig};
|
||||
use crate::gtk_helpers::IronbarGtkExt;
|
||||
use crate::modules::{
|
||||
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, PopupButton, WidgetContext,
|
||||
};
|
||||
use crate::{glib_recv, module_impl, send_async, spawn, try_send};
|
||||
use crate::{module_impl, spawn};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
|
|
@ -49,14 +50,9 @@ pub struct ClockModule {
|
|||
#[serde(default = "default_locale")]
|
||||
locale: String,
|
||||
|
||||
/// The orientation to display the widget contents.
|
||||
/// Setting to vertical will rotate text 90 degrees.
|
||||
///
|
||||
/// **Valid options**: `horizontal`, `vertical`
|
||||
/// <br>
|
||||
/// **Default**: `horizontal`
|
||||
#[serde(default)]
|
||||
orientation: ModuleOrientation,
|
||||
/// See [layout options](module-level-options#layout)
|
||||
#[serde(default, flatten)]
|
||||
layout: LayoutConfig,
|
||||
|
||||
/// See [common options](module-level-options#common-options).
|
||||
#[serde(flatten)]
|
||||
|
|
@ -69,7 +65,7 @@ impl Default for ClockModule {
|
|||
format: default_format(),
|
||||
format_popup: default_popup_format(),
|
||||
locale: default_locale(),
|
||||
orientation: ModuleOrientation::Horizontal,
|
||||
layout: LayoutConfig::default(),
|
||||
common: Some(CommonConfig::default()),
|
||||
}
|
||||
}
|
||||
|
|
@ -112,7 +108,7 @@ impl Module<Button> for ClockModule {
|
|||
spawn(async move {
|
||||
loop {
|
||||
let date = Local::now();
|
||||
send_async!(tx, ModuleUpdateEvent::Update(date));
|
||||
tx.send_update(date).await;
|
||||
sleep(tokio::time::Duration::from_millis(500)).await;
|
||||
}
|
||||
});
|
||||
|
|
@ -127,32 +123,29 @@ impl Module<Button> for ClockModule {
|
|||
) -> Result<ModuleParts<Button>> {
|
||||
let button = Button::new();
|
||||
let label = Label::builder()
|
||||
.angle(self.orientation.to_angle())
|
||||
.angle(self.layout.angle(info))
|
||||
.use_markup(true)
|
||||
.justify(self.layout.justify.into())
|
||||
.build();
|
||||
|
||||
button.add(&label);
|
||||
|
||||
let tx = context.tx.clone();
|
||||
button.connect_clicked(move |button| {
|
||||
try_send!(tx, ModuleUpdateEvent::TogglePopup(button.popup_id()));
|
||||
tx.send_spawn(ModuleUpdateEvent::TogglePopup(button.popup_id()));
|
||||
});
|
||||
|
||||
let format = self.format.clone();
|
||||
let locale = Locale::try_from(self.locale.as_str()).unwrap_or(Locale::POSIX);
|
||||
|
||||
let rx = context.subscribe();
|
||||
glib_recv!(rx, date => {
|
||||
rx.recv_glib((), move |(), date| {
|
||||
let date_string = format!("{}", date.format_localized(&format, locale));
|
||||
label.set_label(&date_string);
|
||||
});
|
||||
|
||||
let popup = self
|
||||
.into_popup(
|
||||
context.controller_tx.clone(),
|
||||
context.subscribe(),
|
||||
context,
|
||||
info,
|
||||
)
|
||||
.into_popup(context, info)
|
||||
.into_popup_parts(vec![&button]);
|
||||
|
||||
Ok(ModuleParts::new(button, popup))
|
||||
|
|
@ -160,9 +153,7 @@ impl Module<Button> for ClockModule {
|
|||
|
||||
fn into_popup(
|
||||
self,
|
||||
_tx: mpsc::Sender<Self::ReceiveMessage>,
|
||||
rx: broadcast::Receiver<Self::SendMessage>,
|
||||
_context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
_info: &ModuleInfo,
|
||||
) -> Option<gtk::Box> {
|
||||
let container = gtk::Box::new(Orientation::Vertical, 0);
|
||||
|
|
@ -182,11 +173,18 @@ impl Module<Button> for ClockModule {
|
|||
let format = self.format_popup;
|
||||
let locale = Locale::try_from(self.locale.as_str()).unwrap_or(Locale::POSIX);
|
||||
|
||||
glib_recv!(rx, date => {
|
||||
context.subscribe().recv_glib((), move |(), date| {
|
||||
let date_string = format!("{}", date.format_localized(&format, locale));
|
||||
clock.set_label(&date_string);
|
||||
});
|
||||
|
||||
// Reset selected date on each popup open
|
||||
context.popup.window.connect_show(move |_| {
|
||||
let date = Local::now();
|
||||
calendar.select_day(date.day());
|
||||
calendar.select_month(date.month() - 1, date.year() as u32);
|
||||
});
|
||||
|
||||
container.show_all();
|
||||
|
||||
Some(container)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,31 @@ use crate::modules::custom::WidgetConfig;
|
|||
use gtk::prelude::*;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub enum ModuleAlignment {
|
||||
/// Align widget to the start (left for horizontal, top for vertical).
|
||||
Start,
|
||||
/// Align widget to the center.
|
||||
Center,
|
||||
/// Align widget to the end (right for horizontal, bottom for vertical).
|
||||
End,
|
||||
/// Stretch widget to fill available space.
|
||||
Fill,
|
||||
}
|
||||
|
||||
impl From<ModuleAlignment> for gtk::Align {
|
||||
fn from(align: ModuleAlignment) -> Self {
|
||||
match align {
|
||||
ModuleAlignment::Start => gtk::Align::Start,
|
||||
ModuleAlignment::Center => gtk::Align::Center,
|
||||
ModuleAlignment::End => gtk::Align::End,
|
||||
ModuleAlignment::Fill => gtk::Align::Fill,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct BoxWidget {
|
||||
|
|
@ -21,10 +46,21 @@ pub struct BoxWidget {
|
|||
/// Whether child widgets should be horizontally or vertically added.
|
||||
///
|
||||
/// **Valid options**: `horizontal`, `vertical`, `h`, `v`
|
||||
/// <br />
|
||||
/// **Default**: `horizontal`
|
||||
orientation: Option<ModuleOrientation>,
|
||||
|
||||
/// Horizontal alignment of the box relative to its parent.
|
||||
///
|
||||
/// **Valid options**: `start`, `center`, `end`, `fill`
|
||||
/// **Default**: `fill`
|
||||
halign: Option<ModuleAlignment>,
|
||||
|
||||
/// Vertical alignment of the box relative to its parent.
|
||||
///
|
||||
/// **Valid options**: `start`, `center`, `end`, `fill`
|
||||
/// **Default**: `fill`
|
||||
valign: Option<ModuleAlignment>,
|
||||
|
||||
/// Modules and widgets to add to this box.
|
||||
///
|
||||
/// **Default**: `null`
|
||||
|
|
@ -47,6 +83,12 @@ impl CustomWidget for BoxWidget {
|
|||
}
|
||||
}
|
||||
|
||||
let horizontal_alignment = self.halign.unwrap_or(ModuleAlignment::Fill);
|
||||
let vertical_alignment = self.valign.unwrap_or(ModuleAlignment::Fill);
|
||||
|
||||
container.set_halign(horizontal_alignment.into());
|
||||
container.set_valign(vertical_alignment.into());
|
||||
|
||||
container
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
use gtk::prelude::*;
|
||||
use gtk::{Button, Label, Orientation};
|
||||
use gtk::{Button, Label};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::config::ModuleOrientation;
|
||||
use crate::dynamic_value::dynamic_string;
|
||||
use crate::modules::PopupButton;
|
||||
use crate::{build, try_send};
|
||||
|
||||
use super::{CustomWidget, CustomWidgetContext, ExecEvent, WidgetConfig};
|
||||
use crate::build;
|
||||
use crate::channels::AsyncSenderExt;
|
||||
use crate::config::LayoutConfig;
|
||||
use crate::dynamic_value::dynamic_string;
|
||||
use crate::gtk_helpers::IronbarLabelExt;
|
||||
use crate::modules::PopupButton;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
|
|
@ -37,13 +38,9 @@ pub struct ButtonWidget {
|
|||
/// **Default**: `null`
|
||||
on_click: Option<String>,
|
||||
|
||||
/// Orientation of the button.
|
||||
///
|
||||
/// **Valid options**: `horizontal`, `vertical`, `h`, `v`
|
||||
/// <br />
|
||||
/// **Default**: `horizontal`
|
||||
#[serde(default)]
|
||||
orientation: ModuleOrientation,
|
||||
/// See [layout options](module-level-options#layout)
|
||||
#[serde(default, flatten)]
|
||||
layout: LayoutConfig,
|
||||
|
||||
/// Modules and widgets to add to this box.
|
||||
///
|
||||
|
|
@ -59,7 +56,7 @@ impl CustomWidget for ButtonWidget {
|
|||
context.popup_buttons.borrow_mut().push(button.clone());
|
||||
|
||||
if let Some(widgets) = self.widgets {
|
||||
let container = gtk::Box::new(Orientation::Horizontal, 0);
|
||||
let container = gtk::Box::new(self.layout.orientation(context.info), 0);
|
||||
|
||||
for widget in widgets {
|
||||
widget.widget.add_to(&container, &context, widget.common);
|
||||
|
|
@ -70,12 +67,14 @@ impl CustomWidget for ButtonWidget {
|
|||
let label = Label::new(None);
|
||||
label.set_use_markup(true);
|
||||
|
||||
label.set_angle(self.orientation.to_angle());
|
||||
if !context.is_popup {
|
||||
label.set_angle(self.layout.angle(context.info));
|
||||
}
|
||||
|
||||
button.add(&label);
|
||||
|
||||
dynamic_string(&text, move |string| {
|
||||
label.set_markup(&string);
|
||||
dynamic_string(&text, (), move |(), string| {
|
||||
label.set_label_escaped(&string);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -83,14 +82,11 @@ impl CustomWidget for ButtonWidget {
|
|||
let tx = context.tx.clone();
|
||||
|
||||
button.connect_clicked(move |button| {
|
||||
try_send!(
|
||||
tx,
|
||||
ExecEvent {
|
||||
cmd: exec.clone(),
|
||||
args: None,
|
||||
id: button.try_popup_id().unwrap_or(usize::MAX), // may not be a popup button
|
||||
}
|
||||
);
|
||||
tx.send_spawn(ExecEvent {
|
||||
cmd: exec.clone(),
|
||||
args: None,
|
||||
id: button.try_popup_id().unwrap_or(usize::MAX), // may not be a popup button
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
use gtk::prelude::*;
|
||||
use gtk::Image;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::build;
|
||||
use crate::dynamic_value::dynamic_string;
|
||||
use crate::image::ImageProvider;
|
||||
use gtk::Image;
|
||||
use gtk::prelude::*;
|
||||
use serde::Deserialize;
|
||||
|
||||
use super::{CustomWidget, CustomWidgetContext};
|
||||
|
||||
|
|
@ -46,15 +44,15 @@ impl CustomWidget for ImageWidget {
|
|||
fn into_widget(self, context: CustomWidgetContext) -> Self::Widget {
|
||||
let gtk_image = build!(self, Self::Widget);
|
||||
|
||||
{
|
||||
dynamic_string(&self.src, >k_image, move |gtk_image, src| {
|
||||
let gtk_image = gtk_image.clone();
|
||||
let icon_theme = context.icon_theme.clone();
|
||||
|
||||
dynamic_string(&self.src, move |src| {
|
||||
ImageProvider::parse(&src, &icon_theme, false, self.size)
|
||||
.map(|image| image.load_into_image(gtk_image.clone()));
|
||||
let image_provider = context.image_provider.clone();
|
||||
glib::spawn_future_local(async move {
|
||||
image_provider
|
||||
.load_into_image_silent(&src, self.size, false, >k_image)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
gtk_image
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
use gtk::prelude::*;
|
||||
use gtk::Label;
|
||||
use gtk::prelude::*;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::build;
|
||||
use crate::config::ModuleOrientation;
|
||||
use crate::dynamic_value::dynamic_string;
|
||||
|
||||
use super::{CustomWidget, CustomWidgetContext};
|
||||
use crate::build;
|
||||
use crate::config::{LayoutConfig, TruncateMode};
|
||||
use crate::dynamic_value::dynamic_string;
|
||||
use crate::gtk_helpers::IronbarLabelExt;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
|
|
@ -28,32 +28,37 @@ pub struct LabelWidget {
|
|||
/// **Required**
|
||||
label: String,
|
||||
|
||||
/// Orientation of the label.
|
||||
/// Setting to vertical will rotate text 90 degrees.
|
||||
/// See [layout options](module-level-options#layout)
|
||||
#[serde(default, flatten)]
|
||||
layout: LayoutConfig,
|
||||
|
||||
/// See [truncate options](module-level-options#truncate-mode).
|
||||
///
|
||||
/// **Valid options**: `horizontal`, `vertical`, `h`, `v`
|
||||
/// <br />
|
||||
/// **Default**: `horizontal`
|
||||
#[serde(default)]
|
||||
orientation: ModuleOrientation,
|
||||
/// **Default**: `null`
|
||||
truncate: Option<TruncateMode>,
|
||||
}
|
||||
|
||||
impl CustomWidget for LabelWidget {
|
||||
type Widget = Label;
|
||||
|
||||
fn into_widget(self, _context: CustomWidgetContext) -> Self::Widget {
|
||||
fn into_widget(self, context: CustomWidgetContext) -> Self::Widget {
|
||||
let label = build!(self, Self::Widget);
|
||||
|
||||
label.set_angle(self.orientation.to_angle());
|
||||
if !context.is_popup {
|
||||
label.set_angle(self.layout.angle(context.info));
|
||||
}
|
||||
|
||||
label.set_justify(self.layout.justify.into());
|
||||
label.set_use_markup(true);
|
||||
|
||||
{
|
||||
let label = label.clone();
|
||||
dynamic_string(&self.label, move |string| {
|
||||
label.set_markup(&string);
|
||||
});
|
||||
if let Some(truncate) = self.truncate {
|
||||
label.truncate(truncate);
|
||||
}
|
||||
|
||||
dynamic_string(&self.label, &label, move |label, string| {
|
||||
label.set_label_escaped(&string);
|
||||
});
|
||||
|
||||
label
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,26 +5,27 @@ mod label;
|
|||
mod progress;
|
||||
mod slider;
|
||||
|
||||
use self::r#box::BoxWidget;
|
||||
use self::image::ImageWidget;
|
||||
use self::label::LabelWidget;
|
||||
use self::r#box::BoxWidget;
|
||||
use self::slider::SliderWidget;
|
||||
use crate::channels::AsyncSenderExt;
|
||||
use crate::config::{CommonConfig, ModuleConfig};
|
||||
use crate::modules::custom::button::ButtonWidget;
|
||||
use crate::modules::custom::progress::ProgressWidget;
|
||||
use crate::modules::{
|
||||
wrap_widget, AnyModuleFactory, BarModuleFactory, Module, ModuleInfo, ModuleParts, ModulePopup,
|
||||
ModuleUpdateEvent, PopupButton, PopupModuleFactory, WidgetContext,
|
||||
AnyModuleFactory, BarModuleFactory, Module, ModuleInfo, ModuleParts, ModulePopup,
|
||||
ModuleUpdateEvent, PopupButton, PopupModuleFactory, WidgetContext, wrap_widget,
|
||||
};
|
||||
use crate::script::Script;
|
||||
use crate::{module_impl, send_async, spawn};
|
||||
use crate::{module_impl, spawn};
|
||||
use color_eyre::Result;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Button, IconTheme, Orientation};
|
||||
use gtk::{Button, Orientation};
|
||||
use serde::Deserialize;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use tokio::sync::{broadcast, mpsc};
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{debug, error};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
|
|
@ -91,9 +92,10 @@ struct CustomWidgetContext<'a> {
|
|||
info: &'a ModuleInfo<'a>,
|
||||
tx: &'a mpsc::Sender<ExecEvent>,
|
||||
bar_orientation: Orientation,
|
||||
icon_theme: &'a IconTheme,
|
||||
is_popup: bool,
|
||||
popup_buttons: Rc<RefCell<Vec<Button>>>,
|
||||
module_factory: AnyModuleFactory,
|
||||
image_provider: crate::image::Provider,
|
||||
}
|
||||
|
||||
trait CustomWidget {
|
||||
|
|
@ -132,7 +134,7 @@ pub fn set_length<W: WidgetExt>(widget: &W, length: i32, bar_orientation: Orient
|
|||
Orientation::Horizontal => widget.set_width_request(length),
|
||||
Orientation::Vertical => widget.set_height_request(length),
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetOrModule {
|
||||
|
|
@ -202,16 +204,14 @@ impl Module<gtk::Box> for CustomModule {
|
|||
debug!("executing command: '{}'", script.cmd);
|
||||
|
||||
let args = event.args.unwrap_or_default();
|
||||
|
||||
if let Err(err) = script.get_output(Some(&args)).await {
|
||||
error!("{err:?}");
|
||||
}
|
||||
script.run_as_oneshot(Some(&args));
|
||||
} else if event.cmd == "popup:toggle" {
|
||||
send_async!(tx, ModuleUpdateEvent::TogglePopup(event.id));
|
||||
tx.send_expect(ModuleUpdateEvent::TogglePopup(event.id))
|
||||
.await;
|
||||
} else if event.cmd == "popup:open" {
|
||||
send_async!(tx, ModuleUpdateEvent::OpenPopup(event.id));
|
||||
tx.send_expect(ModuleUpdateEvent::OpenPopup(event.id)).await;
|
||||
} else if event.cmd == "popup:close" {
|
||||
send_async!(tx, ModuleUpdateEvent::ClosePopup);
|
||||
tx.send_expect(ModuleUpdateEvent::ClosePopup).await;
|
||||
} else {
|
||||
error!("Received invalid command: '{}'", event.cmd);
|
||||
}
|
||||
|
|
@ -235,10 +235,11 @@ impl Module<gtk::Box> for CustomModule {
|
|||
info,
|
||||
tx: &context.controller_tx,
|
||||
bar_orientation: orientation,
|
||||
icon_theme: info.icon_theme,
|
||||
is_popup: false,
|
||||
popup_buttons: popup_buttons.clone(),
|
||||
module_factory: BarModuleFactory::new(context.ironbar.clone(), context.popup.clone())
|
||||
.into(),
|
||||
image_provider: context.ironbar.image_provider(),
|
||||
};
|
||||
|
||||
self.bar.clone().into_iter().for_each(|widget| {
|
||||
|
|
@ -257,12 +258,7 @@ impl Module<gtk::Box> for CustomModule {
|
|||
.map_or(usize::MAX, PopupButton::popup_id);
|
||||
|
||||
let popup = self
|
||||
.into_popup(
|
||||
context.controller_tx.clone(),
|
||||
context.subscribe(),
|
||||
context,
|
||||
info,
|
||||
)
|
||||
.into_popup(context, info)
|
||||
.into_popup_parts_owned(popup_buttons.take());
|
||||
|
||||
Ok(ModuleParts {
|
||||
|
|
@ -273,8 +269,6 @@ impl Module<gtk::Box> for CustomModule {
|
|||
|
||||
fn into_popup(
|
||||
self,
|
||||
tx: mpsc::Sender<Self::ReceiveMessage>,
|
||||
_rx: broadcast::Receiver<Self::SendMessage>,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> Option<gtk::Box>
|
||||
|
|
@ -286,10 +280,11 @@ impl Module<gtk::Box> for CustomModule {
|
|||
if let Some(popup) = self.popup {
|
||||
let custom_context = CustomWidgetContext {
|
||||
info,
|
||||
tx: &tx,
|
||||
bar_orientation: info.bar_position.orientation(),
|
||||
icon_theme: info.icon_theme,
|
||||
tx: &context.controller_tx,
|
||||
bar_orientation: Orientation::Horizontal,
|
||||
is_popup: true,
|
||||
popup_buttons: Rc::new(RefCell::new(vec![])),
|
||||
image_provider: context.ironbar.image_provider(),
|
||||
module_factory: PopupModuleFactory::new(
|
||||
context.ironbar,
|
||||
context.popup,
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
use gtk::prelude::*;
|
||||
use gtk::ProgressBar;
|
||||
use gtk::prelude::*;
|
||||
use serde::Deserialize;
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::error;
|
||||
|
||||
use super::{CustomWidget, CustomWidgetContext};
|
||||
use crate::channels::{AsyncSenderExt, MpscReceiverExt};
|
||||
use crate::config::ModuleOrientation;
|
||||
use crate::dynamic_value::dynamic_string;
|
||||
use crate::modules::custom::set_length;
|
||||
use crate::script::{OutputStream, Script, ScriptInput};
|
||||
use crate::{build, glib_recv_mpsc, spawn, try_send};
|
||||
|
||||
use super::{CustomWidget, CustomWidgetContext};
|
||||
use crate::{build, spawn};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
|
|
@ -87,7 +87,7 @@ impl CustomWidget for ProgressWidget {
|
|||
script
|
||||
.run(None, move |stream, _success| match stream {
|
||||
OutputStream::Stdout(out) => match out.parse::<f64>() {
|
||||
Ok(value) => try_send!(tx, value),
|
||||
Ok(value) => tx.send_spawn(value),
|
||||
Err(err) => error!("{err:?}"),
|
||||
},
|
||||
OutputStream::Stderr(err) => error!("{err:?}"),
|
||||
|
|
@ -95,14 +95,13 @@ impl CustomWidget for ProgressWidget {
|
|||
.await;
|
||||
});
|
||||
|
||||
glib_recv_mpsc!(rx, value => progress.set_fraction(value / self.max));
|
||||
rx.recv_glib((), move |(), value| progress.set_fraction(value / self.max));
|
||||
}
|
||||
|
||||
if let Some(text) = self.label {
|
||||
let progress = progress.clone();
|
||||
progress.set_show_text(true);
|
||||
|
||||
dynamic_string(&text, move |string| {
|
||||
dynamic_string(&text, &progress, move |progress, string| {
|
||||
progress.set_text(Some(&string));
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,18 +2,18 @@ use glib::Propagation;
|
|||
use std::cell::Cell;
|
||||
use std::ops::Neg;
|
||||
|
||||
use gtk::prelude::*;
|
||||
use gtk::Scale;
|
||||
use gtk::prelude::*;
|
||||
use serde::Deserialize;
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::error;
|
||||
|
||||
use super::{CustomWidget, CustomWidgetContext, ExecEvent};
|
||||
use crate::channels::{AsyncSenderExt, MpscReceiverExt};
|
||||
use crate::config::ModuleOrientation;
|
||||
use crate::modules::custom::set_length;
|
||||
use crate::script::{OutputStream, Script, ScriptInput};
|
||||
use crate::{build, glib_recv_mpsc, spawn, try_send};
|
||||
|
||||
use super::{CustomWidget, CustomWidgetContext, ExecEvent};
|
||||
use crate::{build, spawn};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
|
|
@ -134,14 +134,11 @@ impl CustomWidget for SliderWidget {
|
|||
let val = val.clamp(min, max);
|
||||
|
||||
if val != prev_value.get() {
|
||||
try_send!(
|
||||
tx,
|
||||
ExecEvent {
|
||||
cmd: on_change.clone(),
|
||||
args: Some(vec![val.to_string()]),
|
||||
id: usize::MAX // ignored
|
||||
}
|
||||
);
|
||||
tx.send_spawn(ExecEvent {
|
||||
cmd: on_change.clone(),
|
||||
args: Some(vec![val.to_string()]),
|
||||
id: usize::MAX, // ignored
|
||||
});
|
||||
|
||||
prev_value.set(val);
|
||||
}
|
||||
|
|
@ -160,7 +157,7 @@ impl CustomWidget for SliderWidget {
|
|||
script
|
||||
.run(None, move |stream, _success| match stream {
|
||||
OutputStream::Stdout(out) => match out.parse() {
|
||||
Ok(value) => try_send!(tx, value),
|
||||
Ok(value) => tx.send_spawn(value),
|
||||
Err(err) => error!("{err:?}"),
|
||||
},
|
||||
OutputStream::Stderr(err) => error!("{err:?}"),
|
||||
|
|
@ -168,7 +165,7 @@ impl CustomWidget for SliderWidget {
|
|||
.await;
|
||||
});
|
||||
|
||||
glib_recv_mpsc!(rx, value => scale.set_value(value));
|
||||
rx.recv_glib((), move |(), value| scale.set_value(value));
|
||||
}
|
||||
|
||||
scale
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
use crate::channels::{AsyncSenderExt, BroadcastReceiverExt};
|
||||
use crate::clients::wayland::{self, ToplevelEvent};
|
||||
use crate::config::{CommonConfig, TruncateMode};
|
||||
use crate::config::{CommonConfig, LayoutConfig, TruncateMode};
|
||||
use crate::gtk_helpers::IronbarGtkExt;
|
||||
use crate::image::ImageProvider;
|
||||
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
|
||||
use crate::{glib_recv, module_impl, send_async, spawn, try_send};
|
||||
use crate::gtk_helpers::IronbarLabelExt;
|
||||
use crate::modules::{Module, ModuleInfo, ModuleParts, WidgetContext};
|
||||
use crate::{module_impl, spawn};
|
||||
use color_eyre::Result;
|
||||
use gtk::prelude::*;
|
||||
use gtk::Label;
|
||||
use gtk::prelude::*;
|
||||
use serde::Deserialize;
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::debug;
|
||||
|
|
@ -37,6 +38,10 @@ pub struct FocusedModule {
|
|||
/// **Default**: `null`
|
||||
truncate: Option<TruncateMode>,
|
||||
|
||||
/// See [layout options](module-level-options#layout)
|
||||
#[serde(default, flatten)]
|
||||
layout: LayoutConfig,
|
||||
|
||||
/// See [common options](module-level-options#common-options).
|
||||
#[serde(flatten)]
|
||||
pub common: Option<CommonConfig>,
|
||||
|
|
@ -49,6 +54,7 @@ impl Default for FocusedModule {
|
|||
show_title: crate::config::default_true(),
|
||||
icon_size: default_icon_size(),
|
||||
truncate: None,
|
||||
layout: LayoutConfig::default(),
|
||||
common: Some(CommonConfig::default()),
|
||||
}
|
||||
}
|
||||
|
|
@ -84,11 +90,9 @@ impl Module<gtk::Box> for FocusedModule {
|
|||
if let Some(focused) = focused {
|
||||
current = Some(focused.id);
|
||||
|
||||
try_send!(
|
||||
tx,
|
||||
ModuleUpdateEvent::Update(Some((focused.title.clone(), focused.app_id)))
|
||||
);
|
||||
};
|
||||
tx.send_update(Some((focused.title.clone(), focused.app_id)))
|
||||
.await;
|
||||
}
|
||||
|
||||
while let Ok(event) = wlrx.recv().await {
|
||||
match event {
|
||||
|
|
@ -98,24 +102,19 @@ impl Module<gtk::Box> for FocusedModule {
|
|||
|
||||
current = Some(info.id);
|
||||
|
||||
send_async!(
|
||||
tx,
|
||||
ModuleUpdateEvent::Update(Some((
|
||||
info.title.clone(),
|
||||
info.app_id.clone()
|
||||
)))
|
||||
);
|
||||
tx.send_update(Some((info.title.clone(), info.app_id)))
|
||||
.await;
|
||||
} else if info.id == current.unwrap_or_default() {
|
||||
debug!("Clearing focus");
|
||||
current = None;
|
||||
send_async!(tx, ModuleUpdateEvent::Update(None));
|
||||
tx.send_update(None).await;
|
||||
}
|
||||
}
|
||||
ToplevelEvent::Remove(info) => {
|
||||
if info.focused {
|
||||
debug!("Clearing focus");
|
||||
current = None;
|
||||
send_async!(tx, ModuleUpdateEvent::Update(None));
|
||||
tx.send_update(None).await;
|
||||
}
|
||||
}
|
||||
ToplevelEvent::New(_) => {}
|
||||
|
|
@ -131,9 +130,7 @@ impl Module<gtk::Box> for FocusedModule {
|
|||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> Result<ModuleParts<gtk::Box>> {
|
||||
let icon_theme = info.icon_theme;
|
||||
|
||||
let container = gtk::Box::new(info.bar_position.orientation(), 5);
|
||||
let container = gtk::Box::new(self.layout.orientation(info), 5);
|
||||
|
||||
let icon = gtk::Image::new();
|
||||
if self.show_icon {
|
||||
|
|
@ -141,35 +138,47 @@ impl Module<gtk::Box> for FocusedModule {
|
|||
container.add(&icon);
|
||||
}
|
||||
|
||||
let label = Label::new(None);
|
||||
let label = Label::builder()
|
||||
.angle(self.layout.angle(info))
|
||||
.justify(self.layout.justify.into())
|
||||
.build();
|
||||
|
||||
label.add_class("label");
|
||||
|
||||
if let Some(truncate) = self.truncate {
|
||||
truncate.truncate_label(&label);
|
||||
label.truncate(truncate);
|
||||
}
|
||||
|
||||
container.add(&label);
|
||||
|
||||
{
|
||||
let icon_theme = icon_theme.clone();
|
||||
glib_recv!(context.subscribe(), data => {
|
||||
if let Some((name, id)) = data {
|
||||
if self.show_icon {
|
||||
match ImageProvider::parse(&id, &icon_theme, true, self.icon_size)
|
||||
.map(|image| image.load_into_image(icon.clone()))
|
||||
{
|
||||
Some(Ok(())) => icon.show(),
|
||||
_ => icon.hide(),
|
||||
}
|
||||
}
|
||||
let image_provider = context.ironbar.image_provider();
|
||||
|
||||
if self.show_title {
|
||||
label.show();
|
||||
label.set_label(&name);
|
||||
context.subscribe().recv_glib_async((), move |(), data| {
|
||||
let icon = icon.clone();
|
||||
let label = label.clone();
|
||||
let image_provider = image_provider.clone();
|
||||
|
||||
async move {
|
||||
if let Some((name, id)) = data {
|
||||
if self.show_icon {
|
||||
match image_provider
|
||||
.load_into_image(&id, self.icon_size, true, &icon)
|
||||
.await
|
||||
{
|
||||
Ok(true) => icon.show(),
|
||||
_ => icon.hide(),
|
||||
}
|
||||
}
|
||||
|
||||
if self.show_title {
|
||||
label.show();
|
||||
label.set_label(&name);
|
||||
}
|
||||
} else {
|
||||
icon.hide();
|
||||
label.hide();
|
||||
}
|
||||
} else {
|
||||
icon.hide();
|
||||
label.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
347
src/modules/keyboard.rs
Normal file
347
src/modules/keyboard.rs
Normal file
|
|
@ -0,0 +1,347 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use color_eyre::Result;
|
||||
use color_eyre::eyre::Report;
|
||||
use gtk::prelude::*;
|
||||
use serde::Deserialize;
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{debug, trace};
|
||||
|
||||
use super::{Module, ModuleInfo, ModuleParts, WidgetContext};
|
||||
use crate::channels::{AsyncSenderExt, BroadcastReceiverExt};
|
||||
use crate::clients::compositor::{self, KeyboardLayoutUpdate};
|
||||
use crate::clients::libinput::{Event, Key, KeyEvent};
|
||||
use crate::config::{CommonConfig, LayoutConfig};
|
||||
use crate::gtk_helpers::IronbarGtkExt;
|
||||
use crate::image::{IconButton, IconLabel};
|
||||
use crate::{module_impl, spawn};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct KeyboardModule {
|
||||
/// Whether to show capslock indicator.
|
||||
///
|
||||
/// **Default**: `true`
|
||||
#[serde(default = "crate::config::default_true")]
|
||||
show_caps: bool,
|
||||
|
||||
/// Whether to show num lock indicator.
|
||||
///
|
||||
/// **Default**: `true`
|
||||
#[serde(default = "crate::config::default_true")]
|
||||
show_num: bool,
|
||||
|
||||
/// Whether to show scroll lock indicator.
|
||||
///
|
||||
/// **Default**: `true`
|
||||
#[serde(default = "crate::config::default_true")]
|
||||
show_scroll: bool,
|
||||
|
||||
/// Whether to show the current keyboard layout.
|
||||
///
|
||||
/// **Default**: `true`
|
||||
#[serde(default = "crate::config::default_true")]
|
||||
show_layout: bool,
|
||||
|
||||
/// Size to render the icons at, in pixels (image icons only).
|
||||
///
|
||||
/// **Default** `32`
|
||||
#[serde(default = "default_icon_size")]
|
||||
icon_size: i32,
|
||||
|
||||
/// Player state icons.
|
||||
///
|
||||
/// See [icons](#icons).
|
||||
#[serde(default)]
|
||||
icons: Icons,
|
||||
|
||||
/// The Wayland seat to attach to.
|
||||
/// You almost certainly do not need to change this.
|
||||
///
|
||||
/// **Default**: `seat0`
|
||||
#[serde(default = "default_seat")]
|
||||
seat: String,
|
||||
|
||||
// -- common --
|
||||
/// See [layout options](module-level-options#layout)
|
||||
#[serde(default, flatten)]
|
||||
layout: LayoutConfig,
|
||||
|
||||
/// See [common options](module-level-options#common-options).
|
||||
#[serde(flatten)]
|
||||
pub common: Option<CommonConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
struct Icons {
|
||||
/// Icon to show when capslock is enabled.
|
||||
///
|
||||
/// **Default**: ``
|
||||
#[serde(default = "default_icon_caps")]
|
||||
caps_on: String,
|
||||
|
||||
/// Icon to show when capslock is disabled.
|
||||
///
|
||||
/// **Default**: `""`
|
||||
#[serde(default)]
|
||||
caps_off: String,
|
||||
|
||||
/// Icon to show when num lock is enabled.
|
||||
///
|
||||
/// **Default**: ``
|
||||
#[serde(default = "default_icon_num")]
|
||||
num_on: String,
|
||||
|
||||
/// Icon to show when num lock is disabled.
|
||||
///
|
||||
/// **Default**: `""`
|
||||
#[serde(default)]
|
||||
num_off: String,
|
||||
|
||||
/// Icon to show when scroll lock is enabled.
|
||||
///
|
||||
/// **Default**: ``
|
||||
#[serde(default = "default_icon_scroll")]
|
||||
scroll_on: String,
|
||||
|
||||
/// Icon to show when scroll lock is disabled.
|
||||
///
|
||||
/// **Default**: `""`
|
||||
#[serde(default)]
|
||||
scroll_off: String,
|
||||
|
||||
/// Map of icons or labels to show for a particular keyboard layout.
|
||||
///
|
||||
/// If a layout is not present in the map,
|
||||
/// it will fall back to using its actual name.
|
||||
///
|
||||
/// **Default**: `{}`
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```corn
|
||||
/// {
|
||||
/// type = "keyboard"
|
||||
/// show_layout = true
|
||||
/// icons.layout_map.'English (US)' = "EN"
|
||||
/// icons.layout_map.Ukrainian = "UA"
|
||||
/// }
|
||||
/// ```
|
||||
#[serde(default)]
|
||||
layout_map: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl Default for Icons {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
caps_on: default_icon_caps(),
|
||||
caps_off: String::new(),
|
||||
num_on: default_icon_num(),
|
||||
num_off: String::new(),
|
||||
scroll_on: default_icon_scroll(),
|
||||
scroll_off: String::new(),
|
||||
layout_map: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fn default_icon_size() -> i32 {
|
||||
32
|
||||
}
|
||||
|
||||
fn default_seat() -> String {
|
||||
String::from("seat0")
|
||||
}
|
||||
|
||||
fn default_icon_caps() -> String {
|
||||
String::from("")
|
||||
}
|
||||
|
||||
fn default_icon_num() -> String {
|
||||
String::from("")
|
||||
}
|
||||
|
||||
fn default_icon_scroll() -> String {
|
||||
String::from("")
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum KeyboardUpdate {
|
||||
Key(KeyEvent),
|
||||
Layout(KeyboardLayoutUpdate),
|
||||
}
|
||||
|
||||
impl Module<gtk::Box> for KeyboardModule {
|
||||
type SendMessage = KeyboardUpdate;
|
||||
type ReceiveMessage = ();
|
||||
|
||||
module_impl!("keyboard");
|
||||
|
||||
fn spawn_controller(
|
||||
&self,
|
||||
_info: &ModuleInfo,
|
||||
context: &WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
mut rx: mpsc::Receiver<Self::ReceiveMessage>,
|
||||
) -> Result<()> {
|
||||
let client = context.ironbar.clients.borrow_mut().libinput(&self.seat);
|
||||
|
||||
let tx = context.tx.clone();
|
||||
spawn(async move {
|
||||
let mut rx = client.subscribe();
|
||||
while let Ok(ev) = rx.recv().await {
|
||||
match ev {
|
||||
Event::Device => {
|
||||
for key in [Key::Caps, Key::Num, Key::Scroll] {
|
||||
let event = KeyEvent {
|
||||
key,
|
||||
state: client.get_state(key),
|
||||
};
|
||||
tx.send_update(KeyboardUpdate::Key(event)).await;
|
||||
}
|
||||
}
|
||||
Event::Key(ev) => {
|
||||
tx.send_update(KeyboardUpdate::Key(ev)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let client = context.try_client::<dyn compositor::KeyboardLayoutClient>()?;
|
||||
{
|
||||
let client = client.clone();
|
||||
let tx = context.tx.clone();
|
||||
spawn(async move {
|
||||
let mut srx = client.subscribe();
|
||||
|
||||
trace!("Set up keyboard_layout subscription");
|
||||
|
||||
loop {
|
||||
match srx.recv().await {
|
||||
Ok(payload) => {
|
||||
debug!("Received update: {payload:?}");
|
||||
tx.send_update(KeyboardUpdate::Layout(payload)).await;
|
||||
}
|
||||
Err(tokio::sync::broadcast::error::RecvError::Lagged(count)) => {
|
||||
tracing::warn!(
|
||||
"Channel lagged behind by {count}, this may result in unexpected or broken behaviour"
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("{err:?}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Change keyboard layout
|
||||
spawn(async move {
|
||||
trace!("Setting up keyboard_layout UI event handler");
|
||||
|
||||
while let Some(()) = rx.recv().await {
|
||||
client.set_next_active();
|
||||
}
|
||||
|
||||
Ok::<(), Report>(())
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn into_widget(
|
||||
self,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> Result<ModuleParts<gtk::Box>> {
|
||||
let container = gtk::Box::new(self.layout.orientation(info), 0);
|
||||
|
||||
let image_provider = context.ironbar.image_provider();
|
||||
|
||||
let caps = IconLabel::new(&self.icons.caps_off, self.icon_size, &image_provider);
|
||||
let num = IconLabel::new(&self.icons.num_off, self.icon_size, &image_provider);
|
||||
let scroll = IconLabel::new(&self.icons.scroll_off, self.icon_size, &image_provider);
|
||||
|
||||
caps.label().set_angle(self.layout.angle(info));
|
||||
caps.label().set_justify(self.layout.justify.into());
|
||||
|
||||
num.label().set_angle(self.layout.angle(info));
|
||||
num.label().set_justify(self.layout.justify.into());
|
||||
|
||||
scroll.label().set_angle(self.layout.angle(info));
|
||||
scroll.label().set_justify(self.layout.justify.into());
|
||||
|
||||
let layout_button = IconButton::new("", self.icon_size, image_provider);
|
||||
|
||||
if self.show_caps {
|
||||
caps.add_class("key");
|
||||
caps.add_class("caps");
|
||||
container.add(&*caps);
|
||||
}
|
||||
|
||||
if self.show_num {
|
||||
num.add_class("key");
|
||||
num.add_class("num");
|
||||
container.add(&*num);
|
||||
}
|
||||
|
||||
if self.show_scroll {
|
||||
scroll.add_class("key");
|
||||
scroll.add_class("scroll");
|
||||
container.add(&*scroll);
|
||||
}
|
||||
|
||||
if self.show_layout {
|
||||
layout_button.add_class("layout");
|
||||
container.add(&*layout_button);
|
||||
}
|
||||
|
||||
{
|
||||
let tx = context.controller_tx.clone();
|
||||
layout_button.connect_clicked(move |_| {
|
||||
tx.send_spawn(());
|
||||
});
|
||||
}
|
||||
|
||||
let icons = self.icons;
|
||||
context
|
||||
.subscribe()
|
||||
.recv_glib((), move |(), ev: KeyboardUpdate| match ev {
|
||||
KeyboardUpdate::Key(ev) => {
|
||||
let parts = match (ev.key, ev.state) {
|
||||
(Key::Caps, true) if self.show_caps => {
|
||||
Some((&caps, icons.caps_on.as_str()))
|
||||
}
|
||||
(Key::Caps, false) if self.show_caps => {
|
||||
Some((&caps, icons.caps_off.as_str()))
|
||||
}
|
||||
(Key::Num, true) if self.show_num => Some((&num, icons.num_on.as_str())),
|
||||
(Key::Num, false) if self.show_num => Some((&num, icons.num_off.as_str())),
|
||||
(Key::Scroll, true) if self.show_scroll => {
|
||||
Some((&scroll, icons.scroll_on.as_str()))
|
||||
}
|
||||
(Key::Scroll, false) if self.show_scroll => {
|
||||
Some((&scroll, icons.scroll_off.as_str()))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some((label, input)) = parts {
|
||||
label.set_label(Some(input));
|
||||
|
||||
if ev.state {
|
||||
label.add_class("enabled");
|
||||
} else {
|
||||
label.remove_class("enabled");
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyboardUpdate::Layout(KeyboardLayoutUpdate(language)) => {
|
||||
let text = icons.layout_map.get(&language).unwrap_or(&language);
|
||||
layout_button.set_label(text);
|
||||
}
|
||||
});
|
||||
Ok(ModuleParts::new(container, None))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
use crate::config::CommonConfig;
|
||||
use crate::channels::{AsyncSenderExt, BroadcastReceiverExt};
|
||||
use crate::config::{CommonConfig, LayoutConfig, TruncateMode};
|
||||
use crate::dynamic_value::dynamic_string;
|
||||
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
|
||||
use crate::{glib_recv, module_impl, try_send};
|
||||
use crate::gtk_helpers::IronbarLabelExt;
|
||||
use crate::module_impl;
|
||||
use crate::modules::{Module, ModuleInfo, ModuleParts, WidgetContext};
|
||||
use color_eyre::Result;
|
||||
use gtk::prelude::*;
|
||||
use gtk::Label;
|
||||
use serde::Deserialize;
|
||||
use tokio::sync::mpsc;
|
||||
|
|
@ -17,6 +18,16 @@ pub struct LabelModule {
|
|||
/// **Required**
|
||||
label: String,
|
||||
|
||||
// -- Common --
|
||||
/// See [truncate options](module-level-options#truncate-mode).
|
||||
///
|
||||
/// **Default**: `null`
|
||||
truncate: Option<TruncateMode>,
|
||||
|
||||
/// See [layout options](module-level-options#layout)
|
||||
#[serde(default, flatten)]
|
||||
layout: LayoutConfig,
|
||||
|
||||
/// See [common options](module-level-options#common-options).
|
||||
#[serde(flatten)]
|
||||
pub common: Option<CommonConfig>,
|
||||
|
|
@ -26,6 +37,8 @@ impl LabelModule {
|
|||
pub(crate) fn new(label: String) -> Self {
|
||||
Self {
|
||||
label,
|
||||
truncate: None,
|
||||
layout: LayoutConfig::default(),
|
||||
common: Some(CommonConfig::default()),
|
||||
}
|
||||
}
|
||||
|
|
@ -43,9 +56,8 @@ impl Module<Label> for LabelModule {
|
|||
context: &WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
_rx: mpsc::Receiver<Self::ReceiveMessage>,
|
||||
) -> Result<()> {
|
||||
let tx = context.tx.clone();
|
||||
dynamic_string(&self.label, move |string| {
|
||||
try_send!(tx, ModuleUpdateEvent::Update(string));
|
||||
dynamic_string(&self.label, &context.tx, move |tx, string| {
|
||||
tx.send_update_spawn(string);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
|
|
@ -54,16 +66,22 @@ impl Module<Label> for LabelModule {
|
|||
fn into_widget(
|
||||
self,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
_info: &ModuleInfo,
|
||||
info: &ModuleInfo,
|
||||
) -> Result<ModuleParts<Label>> {
|
||||
let label = Label::new(None);
|
||||
label.set_use_markup(true);
|
||||
let label = Label::builder()
|
||||
.use_markup(true)
|
||||
.angle(self.layout.angle(info))
|
||||
.justify(self.layout.justify.into())
|
||||
.build();
|
||||
|
||||
{
|
||||
let label = label.clone();
|
||||
glib_recv!(context.subscribe(), string => label.set_markup(&string));
|
||||
if let Some(truncate) = self.truncate {
|
||||
label.truncate(truncate);
|
||||
}
|
||||
|
||||
context.subscribe().recv_glib(&label, move |label, string| {
|
||||
label.set_label_escaped(&string)
|
||||
});
|
||||
|
||||
Ok(ModuleParts {
|
||||
widget: label,
|
||||
popup: None,
|
||||
|
|
|
|||
|
|
@ -1,19 +1,20 @@
|
|||
use super::open_state::OpenState;
|
||||
use crate::channels::AsyncSenderExt;
|
||||
use crate::clients::wayland::ToplevelInfo;
|
||||
use crate::config::BarPosition;
|
||||
use crate::gtk_helpers::IronbarGtkExt;
|
||||
use crate::image::ImageProvider;
|
||||
use crate::modules::launcher::{ItemEvent, LauncherUpdate};
|
||||
use crate::config::{BarPosition, TruncateMode};
|
||||
use crate::gtk_helpers::{IronbarGtkExt, IronbarLabelExt};
|
||||
use crate::modules::ModuleUpdateEvent;
|
||||
use crate::{read_lock, try_send};
|
||||
use crate::modules::launcher::{ItemEvent, LauncherUpdate};
|
||||
use crate::{image, read_lock};
|
||||
use glib::Propagation;
|
||||
use gtk::gdk::{BUTTON_MIDDLE, BUTTON_PRIMARY};
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Button, IconTheme};
|
||||
use gtk::{Align, Button, Image, Justification, Label, Orientation};
|
||||
use indexmap::IndexMap;
|
||||
use std::ops::Deref;
|
||||
use std::rc::Rc;
|
||||
use std::sync::RwLock;
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use tracing::error;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Item {
|
||||
|
|
@ -134,7 +135,7 @@ pub struct MenuState {
|
|||
}
|
||||
|
||||
pub struct ItemButton {
|
||||
pub button: Button,
|
||||
pub button: ImageTextButton,
|
||||
pub persistent: bool,
|
||||
pub show_names: bool,
|
||||
pub menu_state: Rc<RwLock<MenuState>>,
|
||||
|
|
@ -145,74 +146,89 @@ pub struct AppearanceOptions {
|
|||
pub show_names: bool,
|
||||
pub show_icons: bool,
|
||||
pub icon_size: i32,
|
||||
pub truncate: TruncateMode,
|
||||
pub orientation: Orientation,
|
||||
pub angle: f64,
|
||||
pub justify: Justification,
|
||||
}
|
||||
|
||||
impl ItemButton {
|
||||
pub fn new(
|
||||
item: &Item,
|
||||
appearance: AppearanceOptions,
|
||||
icon_theme: &IconTheme,
|
||||
image_provider: image::Provider,
|
||||
bar_position: BarPosition,
|
||||
tx: &Sender<ModuleUpdateEvent<LauncherUpdate>>,
|
||||
controller_tx: &Sender<ItemEvent>,
|
||||
) -> Self {
|
||||
let mut button = Button::builder();
|
||||
let button = ImageTextButton::new(appearance.orientation);
|
||||
|
||||
if appearance.show_names {
|
||||
button = button.label(&item.name);
|
||||
button.label.set_label(&item.name);
|
||||
button.label.truncate(appearance.truncate);
|
||||
button.label.set_angle(appearance.angle);
|
||||
button.label.set_justify(appearance.justify);
|
||||
}
|
||||
|
||||
let button = button.build();
|
||||
|
||||
if appearance.show_icons {
|
||||
let gtk_image = gtk::Image::new();
|
||||
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(>k_image));
|
||||
button.set_always_show_image(true);
|
||||
|
||||
if let Err(err) = image.load_into_image(gtk_image) {
|
||||
error!("{err:?}");
|
||||
}
|
||||
};
|
||||
let button = button.clone();
|
||||
glib::spawn_future_local(async move {
|
||||
image_provider
|
||||
.load_into_image_silent(&input, appearance.icon_size, true, &button.image)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
|
||||
let style_context = button.style_context();
|
||||
style_context.add_class("item");
|
||||
button.add_class("item");
|
||||
|
||||
if item.favorite {
|
||||
style_context.add_class("favorite");
|
||||
button.add_class("favorite");
|
||||
}
|
||||
if item.open_state.is_open() {
|
||||
style_context.add_class("open");
|
||||
button.add_class("open");
|
||||
}
|
||||
if item.open_state.is_focused() {
|
||||
style_context.add_class("focused");
|
||||
}
|
||||
|
||||
{
|
||||
let app_id = item.app_id.clone();
|
||||
let tx = controller_tx.clone();
|
||||
button.connect_clicked(move |button| {
|
||||
// lazy check :| TODO: Improve this
|
||||
let style_context = button.style_context();
|
||||
if style_context.has_class("open") {
|
||||
try_send!(tx, ItemEvent::FocusItem(app_id.clone()));
|
||||
} else {
|
||||
try_send!(tx, ItemEvent::OpenItem(app_id.clone()));
|
||||
}
|
||||
});
|
||||
button.add_class("focused");
|
||||
}
|
||||
|
||||
let menu_state = Rc::new(RwLock::new(MenuState {
|
||||
num_windows: item.windows.len(),
|
||||
}));
|
||||
|
||||
{
|
||||
let app_id = item.app_id.clone();
|
||||
let tx = controller_tx.clone();
|
||||
let menu_state = menu_state.clone();
|
||||
|
||||
button.connect_button_release_event(move |button, event| {
|
||||
if event.button() == BUTTON_PRIMARY {
|
||||
// lazy check :| TODO: Improve this
|
||||
let style_context = button.style_context();
|
||||
if style_context.has_class("open") {
|
||||
let menu_state = read_lock!(menu_state);
|
||||
|
||||
if style_context.has_class("focused") && menu_state.num_windows == 1 {
|
||||
tx.send_spawn(ItemEvent::MinimizeItem(app_id.clone()));
|
||||
} else {
|
||||
tx.send_spawn(ItemEvent::FocusItem(app_id.clone()));
|
||||
}
|
||||
} else {
|
||||
tx.send_spawn(ItemEvent::OpenItem(app_id.clone()));
|
||||
}
|
||||
} else if event.button() == BUTTON_MIDDLE {
|
||||
tx.send_spawn(ItemEvent::OpenItem(app_id.clone()));
|
||||
}
|
||||
|
||||
Propagation::Proceed
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let app_id = item.app_id.clone();
|
||||
let tx = tx.clone();
|
||||
|
|
@ -222,17 +238,12 @@ impl ItemButton {
|
|||
let menu_state = read_lock!(menu_state);
|
||||
|
||||
if menu_state.num_windows > 1 {
|
||||
try_send!(
|
||||
tx,
|
||||
ModuleUpdateEvent::Update(LauncherUpdate::Hover(app_id.clone(),))
|
||||
);
|
||||
|
||||
try_send!(
|
||||
tx,
|
||||
ModuleUpdateEvent::OpenPopupAt(button.geometry(bar_position.orientation()))
|
||||
);
|
||||
tx.send_update_spawn(LauncherUpdate::Hover(app_id.clone()));
|
||||
tx.send_spawn(ModuleUpdateEvent::OpenPopupAt(
|
||||
button.geometry(bar_position.orientation()),
|
||||
));
|
||||
} else {
|
||||
try_send!(tx, ModuleUpdateEvent::ClosePopup);
|
||||
tx.send_spawn(ModuleUpdateEvent::ClosePopup);
|
||||
}
|
||||
|
||||
Propagation::Proceed
|
||||
|
|
@ -257,7 +268,7 @@ impl ItemButton {
|
|||
};
|
||||
|
||||
if close {
|
||||
try_send!(tx, ModuleUpdateEvent::ClosePopup);
|
||||
tx.send_spawn(ModuleUpdateEvent::ClosePopup);
|
||||
}
|
||||
|
||||
Propagation::Proceed
|
||||
|
|
@ -297,3 +308,41 @@ impl ItemButton {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ImageTextButton {
|
||||
pub(crate) button: Button,
|
||||
pub(crate) label: Label,
|
||||
image: Image,
|
||||
}
|
||||
|
||||
impl ImageTextButton {
|
||||
pub(crate) fn new(orientation: Orientation) -> Self {
|
||||
let button = Button::new();
|
||||
let container = gtk::Box::new(orientation, 0);
|
||||
|
||||
let label = Label::new(None);
|
||||
let image = Image::new();
|
||||
|
||||
container.add(&image);
|
||||
container.add(&label);
|
||||
|
||||
button.add(&container);
|
||||
container.set_halign(Align::Center);
|
||||
container.set_valign(Align::Center);
|
||||
|
||||
ImageTextButton {
|
||||
button,
|
||||
label,
|
||||
image,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for ImageTextButton {
|
||||
type Target = Button;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.button
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,28 @@
|
|||
mod item;
|
||||
mod open_state;
|
||||
mod pagination;
|
||||
|
||||
use self::item::{AppearanceOptions, Item, ItemButton, Window};
|
||||
use self::open_state::OpenState;
|
||||
use super::{Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, WidgetContext};
|
||||
use crate::channels::{AsyncSenderExt, BroadcastReceiverExt};
|
||||
use crate::clients::wayland::{self, ToplevelEvent};
|
||||
use crate::config::CommonConfig;
|
||||
use crate::desktop_file::find_desktop_file;
|
||||
use crate::{arc_mut, glib_recv, lock, module_impl, send_async, spawn, try_send, write_lock};
|
||||
use color_eyre::{Help, Report};
|
||||
use crate::config::{
|
||||
CommonConfig, EllipsizeMode, LayoutConfig, TruncateMode, default_launch_command,
|
||||
};
|
||||
use crate::desktop_file::open_program;
|
||||
use crate::gtk_helpers::{IronbarGtkExt, IronbarLabelExt};
|
||||
use crate::modules::launcher::item::ImageTextButton;
|
||||
use crate::modules::launcher::pagination::{IconContext, Pagination};
|
||||
use crate::{arc_mut, lock, module_impl, spawn, write_lock};
|
||||
use color_eyre::Report;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Button, Orientation};
|
||||
use indexmap::IndexMap;
|
||||
use serde::Deserialize;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{broadcast, mpsc};
|
||||
use tracing::{debug, error, trace};
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{debug, error, trace, warn};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
|
|
@ -54,15 +60,120 @@ pub struct LauncherModule {
|
|||
#[serde(default = "crate::config::default_false")]
|
||||
reversed: bool,
|
||||
|
||||
/// Whether to minimize a window if it is focused when clicked.
|
||||
///
|
||||
/// **Default**: `true`
|
||||
#[serde(default = "crate::config::default_true")]
|
||||
minimize_focused: bool,
|
||||
|
||||
/// The number of items to show on a page.
|
||||
///
|
||||
/// When the number of items reaches the page size,
|
||||
/// pagination controls appear at the start of the widget
|
||||
/// which can be used to move forward/back through the list of items.
|
||||
///
|
||||
/// If there are too many to fit, the overflow will be truncated
|
||||
/// by the next widget.
|
||||
///
|
||||
/// **Default**: `1000`.
|
||||
#[serde(default = "default_page_size")]
|
||||
page_size: usize,
|
||||
|
||||
/// Module UI icons (separate from app icons shown for items).
|
||||
///
|
||||
/// See [icons](#icons).
|
||||
#[serde(default)]
|
||||
icons: Icons,
|
||||
|
||||
/// Size in pixels to render pagination icons at (image icons only).
|
||||
///
|
||||
/// **Default**: `16`
|
||||
#[serde(default = "default_icon_size_pagination")]
|
||||
pagination_icon_size: i32,
|
||||
|
||||
// -- common --
|
||||
/// Truncate application names on the bar if they get too long.
|
||||
/// See [truncate options](module-level-options#truncate-mode).
|
||||
///
|
||||
/// **Default**: `Auto (end)`
|
||||
#[serde(default)]
|
||||
truncate: TruncateMode,
|
||||
|
||||
/// Truncate application names in popups if they get too long.
|
||||
/// See [truncate options](module-level-options#truncate-mode).
|
||||
///
|
||||
/// **Default**: `{ mode = "middle" max_length = 25 }`
|
||||
#[serde(default = "default_truncate_popup")]
|
||||
truncate_popup: TruncateMode,
|
||||
|
||||
/// See [layout options](module-level-options#layout)
|
||||
#[serde(default, flatten)]
|
||||
layout: LayoutConfig,
|
||||
|
||||
/// Command used to launch applications.
|
||||
///
|
||||
/// **Default**: `gtk-launch`
|
||||
#[serde(default = "default_launch_command")]
|
||||
launch_command: String,
|
||||
|
||||
/// See [common options](module-level-options#common-options).
|
||||
#[serde(flatten)]
|
||||
pub common: Option<CommonConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
struct Icons {
|
||||
/// Icon to show for page back button.
|
||||
///
|
||||
/// **Default**: ``
|
||||
#[serde(default = "default_icon_page_back")]
|
||||
page_back: String,
|
||||
|
||||
/// Icon to show for page back button.
|
||||
///
|
||||
/// **Default**: `>`
|
||||
#[serde(default = "default_icon_page_forward")]
|
||||
page_forward: String,
|
||||
}
|
||||
|
||||
impl Default for Icons {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
page_back: default_icon_page_back(),
|
||||
page_forward: default_icon_page_forward(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fn default_icon_size() -> i32 {
|
||||
32
|
||||
}
|
||||
|
||||
const fn default_icon_size_pagination() -> i32 {
|
||||
default_icon_size() / 2
|
||||
}
|
||||
|
||||
const fn default_page_size() -> usize {
|
||||
1000
|
||||
}
|
||||
|
||||
fn default_icon_page_back() -> String {
|
||||
String::from("")
|
||||
}
|
||||
|
||||
fn default_icon_page_forward() -> String {
|
||||
String::from("")
|
||||
}
|
||||
|
||||
const fn default_truncate_popup() -> TruncateMode {
|
||||
TruncateMode::Length {
|
||||
mode: EllipsizeMode::Middle,
|
||||
length: None,
|
||||
max_length: Some(25),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum LauncherUpdate {
|
||||
/// Adds item
|
||||
|
|
@ -86,6 +197,7 @@ pub enum ItemEvent {
|
|||
FocusItem(String),
|
||||
FocusWindow(usize),
|
||||
OpenItem(String),
|
||||
MinimizeItem(String),
|
||||
}
|
||||
|
||||
enum ItemOrWindow {
|
||||
|
|
@ -126,7 +238,6 @@ impl Module<gtk::Box> for LauncherModule {
|
|||
});
|
||||
|
||||
let items = arc_mut!(items);
|
||||
|
||||
let items2 = Arc::clone(&items);
|
||||
|
||||
let tx = context.tx.clone();
|
||||
|
|
@ -143,24 +254,26 @@ impl Module<gtk::Box> for LauncherModule {
|
|||
for info in handles {
|
||||
let mut items = lock!(items);
|
||||
let item = items.get_mut(&info.app_id);
|
||||
match item {
|
||||
Some(item) => {
|
||||
item.merge_toplevel(info.clone());
|
||||
}
|
||||
None => {
|
||||
items.insert(info.app_id.clone(), Item::from(info.clone()));
|
||||
}
|
||||
if let Some(item) = item {
|
||||
item.merge_toplevel(info.clone());
|
||||
} else {
|
||||
let item = Item::from(info.clone());
|
||||
items.insert(info.app_id.clone(), item);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let items = lock!(items);
|
||||
let items = items.iter();
|
||||
for (_, item) in items {
|
||||
try_send!(
|
||||
tx,
|
||||
ModuleUpdateEvent::Update(LauncherUpdate::AddItem(item.clone()))
|
||||
);
|
||||
let items = {
|
||||
let items = lock!(items);
|
||||
|
||||
items
|
||||
.iter()
|
||||
.map(|(_, item)| item.clone())
|
||||
.collect::<Vec<_>>() // need to collect to be able to drop lock
|
||||
};
|
||||
|
||||
for item in items {
|
||||
tx.send_update(LauncherUpdate::AddItem(item)).await;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -179,7 +292,6 @@ impl Module<gtk::Box> for LauncherModule {
|
|||
match item {
|
||||
None => {
|
||||
let item: Item = info.into();
|
||||
|
||||
items.insert(app_id.clone(), item.clone());
|
||||
|
||||
ItemOrWindow::Item(item)
|
||||
|
|
@ -256,7 +368,7 @@ impl Module<gtk::Box> for LauncherModule {
|
|||
.await?;
|
||||
}
|
||||
None => {}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -265,36 +377,29 @@ impl Module<gtk::Box> for LauncherModule {
|
|||
});
|
||||
|
||||
// listen to ui events
|
||||
let minimize_focused = self.minimize_focused;
|
||||
let wl = context.client::<wayland::Client>();
|
||||
|
||||
let desktop_files = context.ironbar.desktop_files();
|
||||
let launch_command_str: String = self.launch_command.clone();
|
||||
|
||||
spawn(async move {
|
||||
while let Some(event) = rx.recv().await {
|
||||
if let ItemEvent::OpenItem(app_id) = event {
|
||||
find_desktop_file(&app_id).map_or_else(
|
||||
|| error!("Could not find desktop file for {}", app_id),
|
||||
|file| {
|
||||
if let Err(err) = Command::new("gtk-launch")
|
||||
.arg(
|
||||
file.file_name()
|
||||
.expect("File segment missing from path to desktop file"),
|
||||
)
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
{
|
||||
error!(
|
||||
"{:?}",
|
||||
Report::new(err)
|
||||
.wrap_err("Failed to run gtk-launch command.")
|
||||
.suggestion("Perhaps the desktop file is invalid?")
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
match desktop_files.find(&app_id).await {
|
||||
Ok(Some(file)) => {
|
||||
open_program(&file.file_name, &launch_command_str);
|
||||
}
|
||||
Ok(None) => warn!("Could not find applications file for {}", app_id),
|
||||
Err(err) => error!("Failed to find parse file for {}: {}", app_id, err),
|
||||
}
|
||||
} else {
|
||||
send_async!(tx, ModuleUpdateEvent::ClosePopup);
|
||||
tx.send_expect(ModuleUpdateEvent::ClosePopup).await;
|
||||
|
||||
let minimize_window = matches!(event, ItemEvent::MinimizeItem(_));
|
||||
|
||||
let id = match event {
|
||||
ItemEvent::FocusItem(app_id) => {
|
||||
ItemEvent::FocusItem(app_id) | ItemEvent::MinimizeItem(app_id) => {
|
||||
lock!(items).get(&app_id).and_then(|item| {
|
||||
item.windows
|
||||
.iter()
|
||||
|
|
@ -313,7 +418,11 @@ impl Module<gtk::Box> for LauncherModule {
|
|||
.find_map(|(_, item)| item.windows.get(&id))
|
||||
{
|
||||
debug!("Focusing window {id}: {}", window.name);
|
||||
wl.toplevel_focus(window.id);
|
||||
if minimize_window && minimize_focused {
|
||||
wl.toplevel_minimize(window.id);
|
||||
} else {
|
||||
wl.toplevel_focus(window.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -328,20 +437,32 @@ impl Module<gtk::Box> for LauncherModule {
|
|||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> crate::Result<ModuleParts<gtk::Box>> {
|
||||
let icon_theme = info.icon_theme;
|
||||
let container = gtk::Box::new(self.layout.orientation(info), 0);
|
||||
let page_size = self.page_size;
|
||||
|
||||
let container = gtk::Box::new(info.bar_position.orientation(), 0);
|
||||
let image_provider = context.ironbar.image_provider();
|
||||
|
||||
let pagination = Pagination::new(
|
||||
&container,
|
||||
self.page_size,
|
||||
self.layout.orientation(info),
|
||||
&IconContext {
|
||||
back: &self.icons.page_back,
|
||||
fwd: &self.icons.page_forward,
|
||||
size: self.pagination_icon_size,
|
||||
},
|
||||
&image_provider,
|
||||
);
|
||||
|
||||
{
|
||||
let container = container.clone();
|
||||
let icon_theme = icon_theme.clone();
|
||||
|
||||
let controller_tx = context.controller_tx.clone();
|
||||
|
||||
let appearance_options = AppearanceOptions {
|
||||
show_names: self.show_names,
|
||||
show_icons: self.show_icons,
|
||||
icon_size: self.icon_size,
|
||||
truncate: self.truncate,
|
||||
orientation: self.layout.orientation(info),
|
||||
angle: self.layout.angle(info),
|
||||
justify: self.layout.justify.into(),
|
||||
};
|
||||
|
||||
let show_names = self.show_names;
|
||||
|
|
@ -349,93 +470,117 @@ impl Module<gtk::Box> for LauncherModule {
|
|||
|
||||
let mut buttons = IndexMap::<String, ItemButton>::new();
|
||||
|
||||
let tx = context.tx.clone();
|
||||
let rx = context.subscribe();
|
||||
glib_recv!(rx, event => {
|
||||
match event {
|
||||
LauncherUpdate::AddItem(item) => {
|
||||
debug!("Adding item with id '{}' to the bar: {item:?}", item.app_id);
|
||||
|
||||
if let Some(button) = buttons.get(&item.app_id) {
|
||||
button.set_open(true);
|
||||
button.set_focused(item.open_state.is_focused());
|
||||
} else {
|
||||
let button = ItemButton::new(
|
||||
&item,
|
||||
appearance_options,
|
||||
&icon_theme,
|
||||
bar_position,
|
||||
&tx,
|
||||
&controller_tx,
|
||||
);
|
||||
rx.recv_glib(
|
||||
(&container, &context.controller_tx, &context.tx),
|
||||
move |(container, controller_tx, tx), event: LauncherUpdate| {
|
||||
// all widgets show by default
|
||||
// so check if pagination should be shown
|
||||
// to ensure correct state on init.
|
||||
if buttons.len() <= page_size {
|
||||
pagination.hide();
|
||||
}
|
||||
|
||||
if self.reversed {
|
||||
container.pack_end(&button.button, false, false, 0);
|
||||
match event {
|
||||
LauncherUpdate::AddItem(item) => {
|
||||
debug!("Adding item with id '{}' to the bar: {item:?}", item.app_id);
|
||||
|
||||
if let Some(button) = buttons.get(&item.app_id) {
|
||||
button.set_open(true);
|
||||
button.set_focused(item.open_state.is_focused());
|
||||
} else {
|
||||
container.add(&button.button);
|
||||
}
|
||||
let button = ItemButton::new(
|
||||
&item,
|
||||
appearance_options,
|
||||
image_provider.clone(),
|
||||
bar_position,
|
||||
tx,
|
||||
controller_tx,
|
||||
);
|
||||
|
||||
buttons.insert(item.app_id, button);
|
||||
}
|
||||
}
|
||||
LauncherUpdate::AddWindow(app_id, win) => {
|
||||
if let Some(button) = buttons.get(&app_id) {
|
||||
button.set_open(true);
|
||||
button.set_focused(win.open_state.is_focused());
|
||||
|
||||
write_lock!(button.menu_state).num_windows += 1;
|
||||
}
|
||||
}
|
||||
LauncherUpdate::RemoveItem(app_id) => {
|
||||
debug!("Removing item with id {}", app_id);
|
||||
|
||||
if let Some(button) = buttons.get(&app_id) {
|
||||
if button.persistent {
|
||||
button.set_open(false);
|
||||
if button.show_names {
|
||||
button.button.set_label(&app_id);
|
||||
if self.reversed {
|
||||
container.pack_end(&*button.button, false, false, 0);
|
||||
} else {
|
||||
container.add(&*button.button);
|
||||
}
|
||||
} else {
|
||||
container.remove(&button.button);
|
||||
buttons.shift_remove(&app_id);
|
||||
|
||||
if buttons.len() + 1 >= pagination.offset() + page_size {
|
||||
button.button.set_visible(false);
|
||||
pagination.set_sensitive_fwd(true);
|
||||
}
|
||||
|
||||
if buttons.len() + 1 > page_size {
|
||||
pagination.show_all();
|
||||
}
|
||||
|
||||
buttons.insert(item.app_id, button);
|
||||
}
|
||||
}
|
||||
}
|
||||
LauncherUpdate::RemoveWindow(app_id, win_id) => {
|
||||
debug!("Removing window {win_id} with id {app_id}");
|
||||
|
||||
if let Some(button) = buttons.get(&app_id) {
|
||||
button.set_focused(false);
|
||||
|
||||
let mut menu_state = write_lock!(button.menu_state);
|
||||
menu_state.num_windows -= 1;
|
||||
}
|
||||
}
|
||||
LauncherUpdate::Focus(app_id, focus) => {
|
||||
debug!("Changing focus to {} on item with id {}", focus, app_id);
|
||||
|
||||
if let Some(button) = buttons.get(&app_id) {
|
||||
button.set_focused(focus);
|
||||
}
|
||||
}
|
||||
LauncherUpdate::Title(app_id, _, name) => {
|
||||
debug!("Updating title for item with id {}: {:?}", app_id, name);
|
||||
|
||||
if show_names {
|
||||
LauncherUpdate::AddWindow(app_id, win) => {
|
||||
if let Some(button) = buttons.get(&app_id) {
|
||||
button.button.set_label(&name);
|
||||
button.set_open(true);
|
||||
button.set_focused(win.open_state.is_focused());
|
||||
|
||||
write_lock!(button.menu_state).num_windows += 1;
|
||||
}
|
||||
}
|
||||
LauncherUpdate::RemoveItem(app_id) => {
|
||||
debug!("Removing item with id {}", app_id);
|
||||
|
||||
if let Some(button) = buttons.get(&app_id) {
|
||||
if button.persistent {
|
||||
button.set_open(false);
|
||||
if button.show_names {
|
||||
button.button.label.set_label(&app_id);
|
||||
}
|
||||
} else {
|
||||
container.remove(&button.button.button);
|
||||
buttons.shift_remove(&app_id);
|
||||
}
|
||||
}
|
||||
|
||||
if buttons.len() < pagination.offset() + page_size {
|
||||
pagination.set_sensitive_fwd(false);
|
||||
}
|
||||
|
||||
if buttons.len() <= page_size {
|
||||
pagination.hide();
|
||||
}
|
||||
}
|
||||
LauncherUpdate::RemoveWindow(app_id, win_id) => {
|
||||
debug!("Removing window {win_id} with id {app_id}");
|
||||
|
||||
if let Some(button) = buttons.get(&app_id) {
|
||||
button.set_focused(false);
|
||||
|
||||
let mut menu_state = write_lock!(button.menu_state);
|
||||
menu_state.num_windows -= 1;
|
||||
}
|
||||
}
|
||||
LauncherUpdate::Focus(app_id, focus) => {
|
||||
debug!("Changing focus to {} on item with id {}", focus, app_id);
|
||||
|
||||
if let Some(button) = buttons.get(&app_id) {
|
||||
button.set_focused(focus);
|
||||
}
|
||||
}
|
||||
LauncherUpdate::Title(app_id, _, name) => {
|
||||
debug!("Updating title for item with id {}: {:?}", app_id, name);
|
||||
|
||||
if show_names {
|
||||
if let Some(button) = buttons.get(&app_id) {
|
||||
button.button.label.set_label(&name);
|
||||
}
|
||||
}
|
||||
}
|
||||
LauncherUpdate::Hover(_) => {}
|
||||
}
|
||||
LauncherUpdate::Hover(_) => {}
|
||||
};
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let rx = context.subscribe();
|
||||
let popup = self
|
||||
.into_popup(context.controller_tx.clone(), rx, context, info)
|
||||
.into_popup_parts(vec![]); // since item buttons are dynamic, they pass their geometry directly
|
||||
let popup = self.into_popup(context, info).into_popup_parts(vec![]); // since item buttons are dynamic, they pass their geometry directly
|
||||
|
||||
Ok(ModuleParts {
|
||||
widget: container,
|
||||
|
|
@ -445,9 +590,7 @@ impl Module<gtk::Box> for LauncherModule {
|
|||
|
||||
fn into_popup(
|
||||
self,
|
||||
controller_tx: mpsc::Sender<Self::ReceiveMessage>,
|
||||
rx: broadcast::Receiver<Self::SendMessage>,
|
||||
_context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
_info: &ModuleInfo,
|
||||
) -> Option<gtk::Box> {
|
||||
const MAX_WIDTH: i32 = 250;
|
||||
|
|
@ -459,11 +602,11 @@ impl Module<gtk::Box> for LauncherModule {
|
|||
placeholder.set_width_request(MAX_WIDTH);
|
||||
container.add(&placeholder);
|
||||
|
||||
let mut buttons = IndexMap::<String, IndexMap<usize, Button>>::new();
|
||||
let mut buttons = IndexMap::<String, IndexMap<usize, ImageTextButton>>::new();
|
||||
|
||||
{
|
||||
let container = container.clone();
|
||||
glib_recv!(rx, event => {
|
||||
context
|
||||
.subscribe()
|
||||
.recv_glib(&container, move |container, event| {
|
||||
match event {
|
||||
LauncherUpdate::AddItem(item) => {
|
||||
let app_id = item.app_id.clone();
|
||||
|
|
@ -473,15 +616,16 @@ impl Module<gtk::Box> for LauncherModule {
|
|||
.windows
|
||||
.into_iter()
|
||||
.map(|(_, win)| {
|
||||
let button = Button::builder()
|
||||
.label(clamp(&win.name))
|
||||
.height_request(40)
|
||||
.build();
|
||||
// TODO: Currently has a useless image
|
||||
let button = ImageTextButton::new(Orientation::Horizontal);
|
||||
button.set_height_request(40);
|
||||
button.label.set_label(&win.name);
|
||||
button.label.truncate(self.truncate_popup);
|
||||
|
||||
{
|
||||
let tx = controller_tx.clone();
|
||||
let tx = context.controller_tx.clone();
|
||||
button.connect_clicked(move |_| {
|
||||
try_send!(tx, ItemEvent::FocusWindow(win.id));
|
||||
tx.send_spawn(ItemEvent::FocusWindow(win.id));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -498,15 +642,16 @@ impl Module<gtk::Box> for LauncherModule {
|
|||
);
|
||||
|
||||
if let Some(buttons) = buttons.get_mut(&app_id) {
|
||||
let button = Button::builder()
|
||||
.height_request(40)
|
||||
.label(clamp(&win.name))
|
||||
.build();
|
||||
// TODO: Currently has a useless image
|
||||
let button = ImageTextButton::new(Orientation::Horizontal);
|
||||
button.set_height_request(40);
|
||||
button.label.set_label(&win.name);
|
||||
button.label.truncate(self.truncate_popup);
|
||||
|
||||
{
|
||||
let tx = controller_tx.clone();
|
||||
let tx = context.controller_tx.clone();
|
||||
button.connect_clicked(move |_button| {
|
||||
try_send!(tx, ItemEvent::FocusWindow(win.id));
|
||||
tx.send_spawn(ItemEvent::FocusWindow(win.id));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -527,7 +672,7 @@ impl Module<gtk::Box> for LauncherModule {
|
|||
|
||||
if let Some(buttons) = buttons.get_mut(&app_id) {
|
||||
if let Some(button) = buttons.get(&win_id) {
|
||||
button.set_label(&title);
|
||||
button.label.set_label(&title);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -540,8 +685,8 @@ impl Module<gtk::Box> for LauncherModule {
|
|||
// add app's buttons
|
||||
if let Some(buttons) = buttons.get(&app_id) {
|
||||
for (_, button) in buttons {
|
||||
button.style_context().add_class("popup-item");
|
||||
container.add(button);
|
||||
button.add_class("popup-item");
|
||||
container.add(&button.button);
|
||||
}
|
||||
|
||||
container.show_all();
|
||||
|
|
@ -551,26 +696,7 @@ impl Module<gtk::Box> for LauncherModule {
|
|||
_ => {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Some(container)
|
||||
}
|
||||
}
|
||||
|
||||
/// Clamps a string at 24 characters.
|
||||
///
|
||||
/// This is a hacky number derived from
|
||||
/// "what fits inside the 250px popup"
|
||||
/// and probably won't hold up with wide fonts.
|
||||
///
|
||||
/// TODO: Migrate this to truncate system
|
||||
///
|
||||
fn clamp(str: &str) -> String {
|
||||
const MAX_CHARS: usize = 24;
|
||||
|
||||
if str.len() > MAX_CHARS {
|
||||
str.chars().take(MAX_CHARS - 3).collect::<String>() + "..."
|
||||
} else {
|
||||
str.to_string()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
135
src/modules/launcher/pagination.rs
Normal file
135
src/modules/launcher/pagination.rs
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
use crate::gtk_helpers::IronbarGtkExt;
|
||||
use crate::image;
|
||||
use crate::image::IconButton;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Button, Orientation};
|
||||
use std::cell::RefCell;
|
||||
use std::ops::Deref;
|
||||
use std::rc::Rc;
|
||||
|
||||
pub struct Pagination {
|
||||
offset: Rc<RefCell<usize>>,
|
||||
|
||||
controls_container: gtk::Box,
|
||||
btn_fwd: Button,
|
||||
}
|
||||
|
||||
pub struct IconContext<'a> {
|
||||
pub back: &'a str,
|
||||
pub fwd: &'a str,
|
||||
pub size: i32,
|
||||
}
|
||||
|
||||
impl Pagination {
|
||||
pub fn new(
|
||||
container: >k::Box,
|
||||
page_size: usize,
|
||||
orientation: Orientation,
|
||||
icon_context: &IconContext,
|
||||
image_provider: &image::Provider,
|
||||
) -> Self {
|
||||
let scroll_box = gtk::Box::new(orientation, 0);
|
||||
|
||||
let scroll_back =
|
||||
IconButton::new(icon_context.back, icon_context.size, image_provider.clone());
|
||||
|
||||
let scroll_fwd =
|
||||
IconButton::new(icon_context.fwd, icon_context.size, image_provider.clone());
|
||||
|
||||
scroll_back.set_sensitive(false);
|
||||
scroll_fwd.set_sensitive(false);
|
||||
|
||||
scroll_box.add_class("pagination");
|
||||
scroll_back.add_class("btn-back");
|
||||
scroll_fwd.add_class("btn-forward");
|
||||
|
||||
scroll_box.add(&*scroll_back);
|
||||
scroll_box.add(&*scroll_fwd);
|
||||
container.add(&scroll_box);
|
||||
|
||||
let offset = Rc::new(RefCell::new(1));
|
||||
|
||||
{
|
||||
let offset = offset.clone();
|
||||
let container = container.clone();
|
||||
let scroll_back = scroll_back.clone();
|
||||
|
||||
scroll_fwd.connect_clicked(move |btn| {
|
||||
let mut offset = offset.borrow_mut();
|
||||
let child_count = container.children().len();
|
||||
|
||||
*offset = std::cmp::min(child_count - 1, *offset + page_size);
|
||||
|
||||
Self::update_page(&container, *offset, page_size);
|
||||
|
||||
if *offset + page_size >= child_count {
|
||||
btn.set_sensitive(false);
|
||||
}
|
||||
|
||||
scroll_back.set_sensitive(true);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let offset = offset.clone();
|
||||
let container = container.clone();
|
||||
let scroll_fwd = scroll_fwd.clone();
|
||||
|
||||
scroll_back.connect_clicked(move |btn| {
|
||||
let mut offset = offset.borrow_mut();
|
||||
// avoid using std::cmp::max due to possible overflow
|
||||
if page_size < *offset {
|
||||
*offset -= page_size;
|
||||
} else {
|
||||
*offset = 1;
|
||||
}
|
||||
|
||||
Self::update_page(&container, *offset, page_size);
|
||||
|
||||
if *offset == 1 || *offset - page_size < 1 {
|
||||
btn.set_sensitive(false);
|
||||
}
|
||||
|
||||
scroll_fwd.set_sensitive(true);
|
||||
});
|
||||
}
|
||||
|
||||
Self {
|
||||
offset,
|
||||
|
||||
controls_container: scroll_box,
|
||||
btn_fwd: scroll_fwd.deref().clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn update_page(container: >k::Box, offset: usize, page_size: usize) {
|
||||
for (i, btn) in container.children().iter().enumerate() {
|
||||
// skip offset buttons
|
||||
if i == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
if i >= offset && i < offset + page_size {
|
||||
btn.show();
|
||||
} else {
|
||||
btn.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_sensitive_fwd(&self, sensitive: bool) {
|
||||
self.btn_fwd.set_sensitive(sensitive);
|
||||
}
|
||||
|
||||
pub fn offset(&self) -> usize {
|
||||
*self.offset.borrow()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Pagination {
|
||||
type Target = gtk::Box;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.controls_container
|
||||
}
|
||||
}
|
||||
279
src/modules/menu/config.rs
Normal file
279
src/modules/menu/config.rs
Normal file
|
|
@ -0,0 +1,279 @@
|
|||
use crate::config::default_launch_command;
|
||||
use crate::config::{CommonConfig, TruncateMode};
|
||||
use crate::modules::menu::{MenuEntry, XdgSection};
|
||||
use indexmap::IndexMap;
|
||||
use serde::Deserialize;
|
||||
|
||||
/// An individual entry in the main menu section.
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum MenuConfig {
|
||||
/// Contains all applications matching the configured `categories`.
|
||||
XdgEntry(XdgEntry),
|
||||
/// Contains all applications not covered by `xdg_entry` categories.
|
||||
XdgOther,
|
||||
/// Individual shell command entry.
|
||||
Custom(CustomEntry),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct XdgEntry {
|
||||
/// Text to display on the button.
|
||||
#[serde(default)]
|
||||
pub label: String,
|
||||
|
||||
/// Name of the image icon to show next to the label.
|
||||
#[serde(default)]
|
||||
pub icon: Option<String>,
|
||||
|
||||
/// XDG categories the associated submenu should contain.
|
||||
#[serde(default)]
|
||||
pub categories: Vec<String>,
|
||||
}
|
||||
|
||||
/// Individual shell command entry.
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct CustomEntry {
|
||||
/// Text to display on the button.
|
||||
#[serde(default)]
|
||||
pub label: String,
|
||||
|
||||
/// Name of the image icon to show next to the label.
|
||||
///
|
||||
/// **Default**: `null`
|
||||
pub icon: Option<String>,
|
||||
|
||||
/// Shell command to execute when the button is clicked.
|
||||
/// This is run using `sh -c`.
|
||||
#[serde(default)]
|
||||
pub on_click: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct MenuModule {
|
||||
/// Items to add to the start of the main menu.
|
||||
///
|
||||
/// **Default**: `[]`
|
||||
#[serde(default)]
|
||||
pub(super) start: Vec<MenuConfig>,
|
||||
|
||||
/// Items to add to the start of the main menu.
|
||||
///
|
||||
/// By default, this shows a number of XDG entries
|
||||
/// that should cover all common applications.
|
||||
///
|
||||
/// **Default**: See `examples/menu/default`
|
||||
#[serde(default = "default_menu")]
|
||||
pub(super) center: Vec<MenuConfig>,
|
||||
|
||||
/// Items to add to the end of the main menu.
|
||||
///
|
||||
/// **Default**: `[]`
|
||||
#[serde(default)]
|
||||
pub(super) end: Vec<MenuConfig>,
|
||||
|
||||
/// Fixed height of the menu.
|
||||
///
|
||||
/// When set, if the number of (sub)menu entries exceeds this value,
|
||||
/// a scrollbar will be shown.
|
||||
///
|
||||
/// Leave null to resize dynamically.
|
||||
///
|
||||
/// **Default**: `null`
|
||||
#[serde(default)]
|
||||
pub(super) height: Option<i32>,
|
||||
|
||||
/// Fixed width of the menu.
|
||||
///
|
||||
/// Can be used with `truncate` options
|
||||
/// to customise how item labels are truncated.
|
||||
///
|
||||
/// **Default**: `null`
|
||||
#[serde(default)]
|
||||
pub(super) width: Option<i32>,
|
||||
|
||||
/// Label to show on the menu button on the bar.
|
||||
///
|
||||
/// **Default**: `≡`
|
||||
#[serde(default = "default_menu_popup_label")]
|
||||
pub(super) label: Option<String>,
|
||||
|
||||
/// Icon to show on the menu button on the bar.
|
||||
///
|
||||
/// **Default**: `null`
|
||||
#[serde(default)]
|
||||
pub(super) label_icon: Option<String>,
|
||||
|
||||
/// Size of the `label_icon` image.
|
||||
#[serde(default = "default_menu_popup_icon_size")]
|
||||
pub(super) label_icon_size: i32,
|
||||
|
||||
// -- common --
|
||||
/// Truncate options to apply to (sub)menu item labels.
|
||||
///
|
||||
/// See [truncate options](module-level-options#truncate-mode).
|
||||
///
|
||||
/// **Default**: `Auto (end)`
|
||||
#[serde(default)]
|
||||
pub(super) truncate: TruncateMode,
|
||||
|
||||
/// See [common options](module-level-options#common-options).
|
||||
#[serde(flatten)]
|
||||
pub common: Option<CommonConfig>,
|
||||
|
||||
/// Command used to launch applications.
|
||||
///
|
||||
/// **Default**: `gtk-launch`
|
||||
#[serde(default = "default_launch_command")]
|
||||
pub launch_command: String,
|
||||
}
|
||||
|
||||
impl Default for MenuModule {
|
||||
fn default() -> Self {
|
||||
MenuModule {
|
||||
start: vec![],
|
||||
center: default_menu(),
|
||||
end: vec![],
|
||||
height: None,
|
||||
width: None,
|
||||
truncate: TruncateMode::default(),
|
||||
// max_label_length: default_length(),
|
||||
label: default_menu_popup_label(),
|
||||
label_icon: None,
|
||||
label_icon_size: default_menu_popup_icon_size(),
|
||||
common: Some(CommonConfig::default()),
|
||||
launch_command: default_launch_command(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_menu() -> Vec<MenuConfig> {
|
||||
vec![
|
||||
MenuConfig::XdgEntry(XdgEntry {
|
||||
label: "Accessories".to_string(),
|
||||
icon: Some("accessories".to_string()),
|
||||
categories: vec![
|
||||
"Accessibility".to_string(),
|
||||
"Core".to_string(),
|
||||
"Legacy".to_string(),
|
||||
"Utility".to_string(),
|
||||
],
|
||||
}),
|
||||
MenuConfig::XdgEntry(XdgEntry {
|
||||
label: "Development".to_string(),
|
||||
icon: Some("applications-development".to_string()),
|
||||
categories: vec!["Development".to_string()],
|
||||
}),
|
||||
MenuConfig::XdgEntry(XdgEntry {
|
||||
label: "Education".to_string(),
|
||||
icon: Some("applications-education".to_string()),
|
||||
categories: vec!["Education".to_string()],
|
||||
}),
|
||||
MenuConfig::XdgEntry(XdgEntry {
|
||||
label: "Games".to_string(),
|
||||
icon: Some("applications-games".to_string()),
|
||||
categories: vec!["Game".to_string()],
|
||||
}),
|
||||
MenuConfig::XdgEntry(XdgEntry {
|
||||
label: "Graphics".to_string(),
|
||||
icon: Some("applications-graphics".to_string()),
|
||||
categories: vec!["Graphics".to_string()],
|
||||
}),
|
||||
MenuConfig::XdgEntry(XdgEntry {
|
||||
label: "Multimedia".to_string(),
|
||||
icon: Some("applications-multimedia".to_string()),
|
||||
categories: vec![
|
||||
"Audio".to_string(),
|
||||
"Video".to_string(),
|
||||
"AudioVideo".to_string(),
|
||||
],
|
||||
}),
|
||||
MenuConfig::XdgEntry(XdgEntry {
|
||||
label: "Network".to_string(),
|
||||
icon: Some("applications-internet".to_string()),
|
||||
categories: vec!["Network".to_string()],
|
||||
}),
|
||||
MenuConfig::XdgEntry(XdgEntry {
|
||||
label: "Office".to_string(),
|
||||
icon: Some("applications-office".to_string()),
|
||||
categories: vec!["Office".to_string()],
|
||||
}),
|
||||
MenuConfig::XdgEntry(XdgEntry {
|
||||
label: "Science".to_string(),
|
||||
icon: Some("applications-science".to_string()),
|
||||
categories: vec!["Science".to_string()],
|
||||
}),
|
||||
MenuConfig::XdgEntry(XdgEntry {
|
||||
label: "System".to_string(),
|
||||
icon: Some("applications-system".to_string()),
|
||||
categories: vec!["Emulator".to_string(), "System".to_string()],
|
||||
}),
|
||||
MenuConfig::XdgOther,
|
||||
MenuConfig::XdgEntry(XdgEntry {
|
||||
label: "Settings".to_string(),
|
||||
icon: Some("preferences-system".to_string()),
|
||||
categories: vec!["Settings".to_string(), "Screensaver".to_string()],
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
fn default_menu_popup_label() -> Option<String> {
|
||||
Some("≡".to_string())
|
||||
}
|
||||
|
||||
const fn default_menu_popup_icon_size() -> i32 {
|
||||
16
|
||||
}
|
||||
|
||||
pub const OTHER_LABEL: &str = "Other";
|
||||
|
||||
pub fn parse_config(
|
||||
section_config: Vec<MenuConfig>,
|
||||
sections_by_cat: &mut IndexMap<String, Vec<String>>,
|
||||
) -> IndexMap<String, MenuEntry> {
|
||||
section_config
|
||||
.into_iter()
|
||||
.map(|entry_config| match entry_config {
|
||||
MenuConfig::XdgEntry(entry) => {
|
||||
entry.categories.into_iter().for_each(|cat| {
|
||||
let existing = sections_by_cat.get_mut(&cat);
|
||||
|
||||
if let Some(existing) = existing {
|
||||
existing.push(entry.label.clone());
|
||||
} else {
|
||||
sections_by_cat.insert(cat, vec![entry.label.clone()]);
|
||||
}
|
||||
});
|
||||
|
||||
(
|
||||
entry.label.clone(),
|
||||
MenuEntry::Xdg(XdgSection {
|
||||
label: entry.label,
|
||||
icon: entry.icon,
|
||||
applications: IndexMap::new(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
MenuConfig::XdgOther => (
|
||||
OTHER_LABEL.to_string(),
|
||||
MenuEntry::Xdg(XdgSection {
|
||||
label: OTHER_LABEL.to_string(),
|
||||
icon: Some("applications-other".to_string()),
|
||||
applications: IndexMap::new(),
|
||||
}),
|
||||
),
|
||||
MenuConfig::Custom(entry) => (
|
||||
entry.label.clone(),
|
||||
MenuEntry::Custom(CustomEntry {
|
||||
icon: entry.icon,
|
||||
label: entry.label,
|
||||
on_click: entry.on_click,
|
||||
}),
|
||||
),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
326
src/modules/menu/mod.rs
Normal file
326
src/modules/menu/mod.rs
Normal file
|
|
@ -0,0 +1,326 @@
|
|||
mod config;
|
||||
mod ui;
|
||||
|
||||
use color_eyre::Result;
|
||||
use color_eyre::eyre::Report;
|
||||
use config::*;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Align, Button, Orientation};
|
||||
use indexmap::IndexMap;
|
||||
use serde::Deserialize;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use super::{ModuleLocation, PopupButton};
|
||||
use crate::channels::{AsyncSenderExt, BroadcastReceiverExt};
|
||||
use crate::config::BarPosition;
|
||||
use crate::gtk_helpers::IronbarGtkExt;
|
||||
use crate::modules::{
|
||||
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, WidgetContext,
|
||||
};
|
||||
use crate::{module_impl, spawn};
|
||||
|
||||
pub use config::MenuModule;
|
||||
|
||||
/// XDG button and menu from parsed config.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct XdgSection {
|
||||
pub label: String,
|
||||
pub icon: Option<String>,
|
||||
pub applications: IndexMap<String, MenuApplication>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct MenuApplication {
|
||||
pub label: String,
|
||||
pub file_name: String,
|
||||
pub categories: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum MenuEntry {
|
||||
Xdg(XdgSection),
|
||||
Custom(CustomEntry),
|
||||
}
|
||||
|
||||
impl MenuEntry {
|
||||
pub fn label(&self) -> String {
|
||||
match self {
|
||||
Self::Xdg(entry) => entry.label.clone(),
|
||||
Self::Custom(entry) => entry.label.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn icon(&self) -> Option<String> {
|
||||
match self {
|
||||
Self::Xdg(entry) => entry.icon.clone(),
|
||||
Self::Custom(entry) => entry.icon.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Module<Button> for MenuModule {
|
||||
type SendMessage = Vec<MenuApplication>;
|
||||
type ReceiveMessage = ();
|
||||
|
||||
module_impl!("menu");
|
||||
|
||||
fn spawn_controller(
|
||||
&self,
|
||||
_info: &ModuleInfo,
|
||||
context: &WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
mut rx: mpsc::Receiver<Self::ReceiveMessage>,
|
||||
) -> Result<()> {
|
||||
let tx = context.tx.clone();
|
||||
// let max_label_length = self.max_label_length;
|
||||
|
||||
let desktop_files = context.ironbar.desktop_files();
|
||||
|
||||
spawn(async move {
|
||||
// parsing all desktop files is heavy so wait until the popup is first opened before loading
|
||||
rx.recv().await;
|
||||
|
||||
let apps = desktop_files
|
||||
.get_all()
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|file| {
|
||||
file.no_display != Some(true)
|
||||
&& file.app_type.as_deref().is_some_and(|v| v == "Application")
|
||||
})
|
||||
.map(|file| MenuApplication {
|
||||
label: file.name.unwrap_or_default(),
|
||||
file_name: file.file_name,
|
||||
categories: file.categories,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
tx.send_update_spawn(apps);
|
||||
Ok::<(), Report>(())
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn into_widget(
|
||||
self,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> Result<ModuleParts<Button>> {
|
||||
let button = Button::new();
|
||||
|
||||
if let Some(ref label) = self.label {
|
||||
button.set_label(label);
|
||||
}
|
||||
|
||||
if let Some(ref label_icon) = self.label_icon {
|
||||
let image_provider = context.ironbar.image_provider();
|
||||
|
||||
let gtk_image = gtk::Image::new();
|
||||
button.set_image(Some(>k_image));
|
||||
button.set_always_show_image(true);
|
||||
|
||||
let label_icon = label_icon.clone();
|
||||
|
||||
glib::spawn_future_local(async move {
|
||||
image_provider
|
||||
.load_into_image_silent(&label_icon, self.label_icon_size, true, >k_image)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
|
||||
let tx = context.tx.clone();
|
||||
let controller_tx = context.controller_tx.clone();
|
||||
button.connect_clicked(move |button| {
|
||||
tx.send_spawn(ModuleUpdateEvent::TogglePopup(button.popup_id()));
|
||||
|
||||
// channel will close after init event
|
||||
if !controller_tx.is_closed() {
|
||||
controller_tx.send_spawn(());
|
||||
}
|
||||
});
|
||||
|
||||
let popup = self
|
||||
.into_popup(context, info)
|
||||
.into_popup_parts(vec![&button]);
|
||||
|
||||
Ok(ModuleParts::new(button, popup))
|
||||
}
|
||||
|
||||
fn into_popup(
|
||||
self,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> Option<gtk::Box> {
|
||||
let image_provider = context.ironbar.image_provider();
|
||||
|
||||
let alignment = {
|
||||
match info.bar_position {
|
||||
// For fixed height menus always align to the top
|
||||
_ if self.height.is_some() => Align::Start,
|
||||
|
||||
// Otherwise alignment is based on menu position
|
||||
BarPosition::Top => Align::Start,
|
||||
BarPosition::Bottom => Align::End,
|
||||
|
||||
_ => match &info.location {
|
||||
&ModuleLocation::Left | &ModuleLocation::Center => Align::Start,
|
||||
&ModuleLocation::Right => Align::End,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
let mut sections_by_cat = IndexMap::<String, Vec<String>>::new();
|
||||
|
||||
let container = gtk::Box::new(Orientation::Horizontal, 4);
|
||||
|
||||
let main_menu = gtk::Box::new(Orientation::Vertical, 0);
|
||||
main_menu.set_valign(alignment);
|
||||
main_menu.set_vexpand(false);
|
||||
main_menu.add_class("main");
|
||||
|
||||
if let Some(width) = self.width {
|
||||
main_menu.set_width_request(width / 2);
|
||||
}
|
||||
|
||||
if let Some(max_height) = self.height {
|
||||
container.set_height_request(max_height);
|
||||
|
||||
let scrolled = gtk::ScrolledWindow::builder()
|
||||
.max_content_height(max_height)
|
||||
.hscrollbar_policy(gtk::PolicyType::Never)
|
||||
.build();
|
||||
|
||||
scrolled.add(&main_menu);
|
||||
container.add(&scrolled);
|
||||
} else {
|
||||
container.add(&main_menu);
|
||||
}
|
||||
container.show_all();
|
||||
|
||||
let mut start_entries = parse_config(self.start, &mut sections_by_cat);
|
||||
let mut center_entries = parse_config(self.center, &mut sections_by_cat);
|
||||
let mut end_entries = parse_config(self.end, &mut sections_by_cat);
|
||||
|
||||
let start_section = gtk::Box::new(Orientation::Vertical, 0);
|
||||
let center_section = gtk::Box::new(Orientation::Vertical, 0);
|
||||
let end_section = gtk::Box::new(Orientation::Vertical, 0);
|
||||
|
||||
start_section.add_class("main-start");
|
||||
center_section.add_class("main-center");
|
||||
end_section.add_class("main-end");
|
||||
|
||||
let truncate_mode = self.truncate;
|
||||
|
||||
context.subscribe().recv_glib(
|
||||
(
|
||||
&main_menu,
|
||||
&container,
|
||||
&start_section,
|
||||
¢er_section,
|
||||
&end_section,
|
||||
),
|
||||
move |(main_menu, container, start_section, center_section, end_section),
|
||||
applications| {
|
||||
for application in applications.iter() {
|
||||
let mut inserted = false;
|
||||
|
||||
for category in application.categories.iter() {
|
||||
if let Some(section_names) = sections_by_cat.get(category) {
|
||||
for section_name in section_names.iter() {
|
||||
[&mut start_entries, &mut center_entries, &mut end_entries]
|
||||
.into_iter()
|
||||
.for_each(|entries| {
|
||||
let existing = entries.get_mut(section_name);
|
||||
if let Some(MenuEntry::Xdg(existing)) = existing {
|
||||
existing.applications.insert_sorted(
|
||||
application.label.clone(),
|
||||
application.clone(),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
inserted = true;
|
||||
}
|
||||
}
|
||||
|
||||
if !inserted {
|
||||
let other = center_entries.get_mut(OTHER_LABEL);
|
||||
if let Some(MenuEntry::Xdg(other)) = other {
|
||||
let _ = other
|
||||
.applications
|
||||
.insert_sorted(application.label.clone(), application.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main_menu.foreach(|child| {
|
||||
main_menu.remove(child);
|
||||
});
|
||||
|
||||
macro_rules! add_entries {
|
||||
($entries:expr, $section:expr) => {
|
||||
for entry in $entries.values() {
|
||||
let container1 = container.clone();
|
||||
let tx = context.tx.clone();
|
||||
let (button, sub_menu) = ui::make_entry(
|
||||
entry,
|
||||
tx,
|
||||
&image_provider,
|
||||
truncate_mode,
|
||||
&self.launch_command,
|
||||
);
|
||||
|
||||
if let Some(sub_menu) = sub_menu.clone() {
|
||||
sub_menu.set_valign(alignment);
|
||||
sub_menu.add_class("sub-menu");
|
||||
if let Some(width) = self.width {
|
||||
sub_menu.set_width_request(width / 2);
|
||||
}
|
||||
}
|
||||
|
||||
ui::add_entries(
|
||||
entry,
|
||||
button,
|
||||
sub_menu.as_ref(),
|
||||
$section,
|
||||
&container1,
|
||||
self.height,
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
add_entries!(&start_entries, &start_section);
|
||||
add_entries!(¢er_entries, ¢er_section);
|
||||
add_entries!(&end_entries, &end_section);
|
||||
|
||||
main_menu.add(start_section);
|
||||
main_menu.add(center_section);
|
||||
main_menu.add(end_section);
|
||||
},
|
||||
);
|
||||
|
||||
{
|
||||
let container = container.clone();
|
||||
context.popup.window.connect_hide(move |_| {
|
||||
start_section.foreach(|child| {
|
||||
child.remove_class("open");
|
||||
});
|
||||
|
||||
center_section.foreach(|child| {
|
||||
child.remove_class("open");
|
||||
});
|
||||
|
||||
end_section.foreach(|child| {
|
||||
child.remove_class("open");
|
||||
});
|
||||
|
||||
container.children().iter().skip(1).for_each(|sub_menu| {
|
||||
sub_menu.hide();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Some(container)
|
||||
}
|
||||
}
|
||||
210
src/modules/menu/ui.rs
Normal file
210
src/modules/menu/ui.rs
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
use super::MenuEntry;
|
||||
use crate::channels::AsyncSenderExt;
|
||||
use crate::config::TruncateMode;
|
||||
use crate::desktop_file::open_program;
|
||||
use crate::gtk_helpers::{IronbarGtkExt, IronbarLabelExt};
|
||||
use crate::modules::ModuleUpdateEvent;
|
||||
use crate::script::Script;
|
||||
use crate::{image, spawn};
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Align, Button, Label, Orientation};
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{debug, error};
|
||||
|
||||
pub fn make_entry<R>(
|
||||
entry: &MenuEntry,
|
||||
tx: mpsc::Sender<ModuleUpdateEvent<R>>,
|
||||
image_provider: &image::Provider,
|
||||
truncate_mode: TruncateMode,
|
||||
launch_command_str: &str,
|
||||
) -> (Button, Option<gtk::Box>)
|
||||
where
|
||||
R: Send + Clone + 'static,
|
||||
{
|
||||
let button = Button::new();
|
||||
button.add_class("category");
|
||||
|
||||
let button_container = gtk::Box::new(Orientation::Horizontal, 4);
|
||||
button.add(&button_container);
|
||||
|
||||
let label = Label::new(Some(&entry.label()));
|
||||
label.set_halign(Align::Start);
|
||||
label.truncate(truncate_mode);
|
||||
|
||||
if let Some(icon_name) = entry.icon() {
|
||||
let gtk_image = gtk::Image::new();
|
||||
gtk_image.set_halign(Align::Start);
|
||||
button_container.add(>k_image);
|
||||
|
||||
let image_provider = image_provider.clone();
|
||||
glib::spawn_future_local(async move {
|
||||
image_provider
|
||||
.load_into_image_silent(&icon_name, 16, true, >k_image)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
|
||||
button_container.add(&label);
|
||||
button_container.foreach(|child| {
|
||||
child.set_halign(Align::Start);
|
||||
});
|
||||
|
||||
if let MenuEntry::Xdg(_) = entry {
|
||||
let right_arrow = Label::new(Some("🢒"));
|
||||
right_arrow.set_halign(Align::End);
|
||||
button_container.pack_end(&right_arrow, false, false, 0);
|
||||
}
|
||||
|
||||
button.show_all();
|
||||
|
||||
let sub_menu = match entry {
|
||||
MenuEntry::Xdg(entry) => {
|
||||
let sub_menu = gtk::Box::new(Orientation::Vertical, 0);
|
||||
|
||||
entry.applications.values().for_each(|sub_entry| {
|
||||
let button = Button::new();
|
||||
button.add_class("application");
|
||||
|
||||
let button_container = gtk::Box::new(Orientation::Horizontal, 4);
|
||||
button.add(&button_container);
|
||||
|
||||
let label = Label::new(Some(&sub_entry.label));
|
||||
label.set_halign(Align::Start);
|
||||
label.truncate(truncate_mode);
|
||||
|
||||
let icon_name = sub_entry.file_name.trim_end_matches(".desktop").to_string();
|
||||
let gtk_image = gtk::Image::new();
|
||||
gtk_image.set_halign(Align::Start);
|
||||
|
||||
button_container.add(>k_image);
|
||||
button_container.add(&label);
|
||||
|
||||
let image_provider = image_provider.clone();
|
||||
|
||||
glib::spawn_future_local(async move {
|
||||
image_provider
|
||||
.load_into_image_silent(&icon_name, 16, true, >k_image)
|
||||
.await;
|
||||
});
|
||||
|
||||
button.foreach(|child| {
|
||||
child.set_halign(Align::Start);
|
||||
});
|
||||
|
||||
sub_menu.add(&button);
|
||||
|
||||
{
|
||||
let sub_menu = sub_menu.clone();
|
||||
let file_name = sub_entry.file_name.clone();
|
||||
let command = launch_command_str.to_string();
|
||||
let tx = tx.clone();
|
||||
|
||||
button.connect_clicked(move |_button| {
|
||||
open_program(&file_name, &command);
|
||||
|
||||
sub_menu.hide();
|
||||
tx.send_spawn(ModuleUpdateEvent::ClosePopup);
|
||||
});
|
||||
}
|
||||
|
||||
button.show_all();
|
||||
});
|
||||
|
||||
Some(sub_menu)
|
||||
}
|
||||
MenuEntry::Custom(_) => None,
|
||||
};
|
||||
|
||||
(button, sub_menu)
|
||||
}
|
||||
|
||||
pub fn add_entries(
|
||||
entry: &MenuEntry,
|
||||
button: Button,
|
||||
sub_menu: Option<>k::Box>,
|
||||
main_menu: >k::Box,
|
||||
container: >k::Box,
|
||||
height: Option<i32>,
|
||||
) {
|
||||
let container1 = container.clone();
|
||||
main_menu.add(&button);
|
||||
|
||||
if let Some(sub_menu) = sub_menu {
|
||||
if let Some(height) = height {
|
||||
container.set_height_request(height);
|
||||
|
||||
let scrolled = gtk::ScrolledWindow::builder()
|
||||
.max_content_height(height)
|
||||
.hscrollbar_policy(gtk::PolicyType::Never)
|
||||
.build();
|
||||
|
||||
sub_menu.show();
|
||||
scrolled.add(sub_menu);
|
||||
container.add(&scrolled);
|
||||
|
||||
let sub_menu1 = scrolled.clone();
|
||||
let sub_menu_popup_container = sub_menu.clone();
|
||||
|
||||
button.connect_clicked(move |button| {
|
||||
container1.children().iter().skip(1).for_each(|sub_menu| {
|
||||
if sub_menu.get_visible() {
|
||||
sub_menu.hide();
|
||||
}
|
||||
});
|
||||
|
||||
button
|
||||
.parent()
|
||||
.expect("button parent should exist")
|
||||
.downcast::<gtk::Box>()
|
||||
.expect("button container should be gtk::Box")
|
||||
.children()
|
||||
.iter()
|
||||
.for_each(|child| child.remove_class("open"));
|
||||
|
||||
sub_menu1.show_all();
|
||||
button.add_class("open");
|
||||
|
||||
// Reset scroll to top.
|
||||
if let Some(w) = sub_menu_popup_container.children().first() {
|
||||
w.set_has_focus(true)
|
||||
}
|
||||
});
|
||||
} else {
|
||||
container.add(sub_menu);
|
||||
let sub_menu1 = sub_menu.clone();
|
||||
|
||||
button.connect_clicked(move |_button| {
|
||||
container1.children().iter().skip(1).for_each(|sub_menu| {
|
||||
if sub_menu.get_visible() {
|
||||
sub_menu.hide();
|
||||
}
|
||||
});
|
||||
sub_menu1.show();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let MenuEntry::Custom(entry) = entry {
|
||||
let label = entry.on_click.clone();
|
||||
let container = container.clone();
|
||||
|
||||
button.connect_clicked(move |_button| {
|
||||
container.children().iter().skip(1).for_each(|sub_menu| {
|
||||
sub_menu.hide();
|
||||
});
|
||||
|
||||
let script = Script::from(label.as_str());
|
||||
debug!("executing command: '{}'", script.cmd);
|
||||
|
||||
let args = Vec::new();
|
||||
|
||||
spawn(async move {
|
||||
if let Err(err) = script.get_output(Some(&args)).await {
|
||||
error!("{err:?}");
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
main_menu.show_all();
|
||||
}
|
||||
|
|
@ -6,16 +6,19 @@ use color_eyre::Result;
|
|||
use glib::IsA;
|
||||
use gtk::gdk::{EventMask, Monitor};
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Application, Button, EventBox, IconTheme, Orientation, Revealer, Widget};
|
||||
use gtk::{Application, Button, EventBox, Orientation, Revealer, Widget};
|
||||
use tokio::sync::{broadcast, mpsc};
|
||||
use tracing::debug;
|
||||
|
||||
use crate::Ironbar;
|
||||
use crate::channels::{MpscReceiverExt, SyncSenderExt};
|
||||
use crate::clients::{ClientResult, ProvidesClient, ProvidesFallibleClient};
|
||||
use crate::config::{BarPosition, CommonConfig, TransitionType};
|
||||
use crate::gtk_helpers::{IronbarGtkExt, WidgetGeometry};
|
||||
use crate::popup::Popup;
|
||||
use crate::{glib_recv_mpsc, send, Ironbar};
|
||||
|
||||
#[cfg(feature = "bindmode")]
|
||||
pub mod bindmode;
|
||||
#[cfg(feature = "cairo")]
|
||||
pub mod cairo;
|
||||
#[cfg(feature = "clipboard")]
|
||||
|
|
@ -28,18 +31,28 @@ pub mod clipboard;
|
|||
/// with second-level precision and a calendar.
|
||||
#[cfg(feature = "clock")]
|
||||
pub mod clock;
|
||||
|
||||
#[cfg(feature = "custom")]
|
||||
pub mod custom;
|
||||
#[cfg(feature = "focused")]
|
||||
pub mod focused;
|
||||
#[cfg(feature = "keyboard")]
|
||||
pub mod keyboard;
|
||||
|
||||
#[cfg(feature = "label")]
|
||||
pub mod label;
|
||||
#[cfg(feature = "launcher")]
|
||||
pub mod launcher;
|
||||
#[cfg(feature = "menu")]
|
||||
pub mod menu;
|
||||
#[cfg(feature = "music")]
|
||||
pub mod music;
|
||||
#[cfg(feature = "network_manager")]
|
||||
pub mod networkmanager;
|
||||
#[cfg(feature = "notifications")]
|
||||
pub mod notifications;
|
||||
|
||||
#[cfg(feature = "script")]
|
||||
pub mod script;
|
||||
#[cfg(feature = "sys_info")]
|
||||
pub mod sysinfo;
|
||||
|
|
@ -66,7 +79,6 @@ pub struct ModuleInfo<'a> {
|
|||
pub bar_position: BarPosition,
|
||||
pub monitor: &'a Monitor,
|
||||
pub output_name: &'a str,
|
||||
pub icon_theme: &'a IconTheme,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
@ -257,8 +269,6 @@ where
|
|||
|
||||
fn into_popup(
|
||||
self,
|
||||
_tx: mpsc::Sender<Self::ReceiveMessage>,
|
||||
_rx: broadcast::Receiver<Self::SendMessage>,
|
||||
_context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
_info: &ModuleInfo,
|
||||
) -> Option<gtk::Box>
|
||||
|
|
@ -378,38 +388,41 @@ impl ModuleFactory for BarModuleFactory {
|
|||
) where
|
||||
TSend: Debug + Clone + Send + 'static,
|
||||
{
|
||||
let popup = self.popup.clone();
|
||||
glib_recv_mpsc!(rx, ev => {
|
||||
match ev {
|
||||
ModuleUpdateEvent::Update(update) => {
|
||||
send!(tx, update);
|
||||
}
|
||||
ModuleUpdateEvent::TogglePopup(button_id) if !disable_popup => {
|
||||
debug!("Toggling popup for {} [#{}] (button id: {button_id})", name, id);
|
||||
if popup.visible() && popup.current_widget().unwrap_or_default() == id {
|
||||
popup.hide();
|
||||
} else {
|
||||
popup.show(id, button_id);
|
||||
}
|
||||
}
|
||||
ModuleUpdateEvent::OpenPopup(button_id) if !disable_popup => {
|
||||
debug!("Opening popup for {} [#{}] (button id: {button_id})", name, id);
|
||||
rx.recv_glib(&self.popup, move |popup, ev| match ev {
|
||||
ModuleUpdateEvent::Update(update) => {
|
||||
tx.send_expect(update);
|
||||
}
|
||||
ModuleUpdateEvent::TogglePopup(button_id) if !disable_popup => {
|
||||
debug!(
|
||||
"Toggling popup for {} [#{}] (button id: {button_id})",
|
||||
name, id
|
||||
);
|
||||
if popup.visible() && popup.current_widget().unwrap_or_default() == id {
|
||||
popup.hide();
|
||||
} else {
|
||||
popup.show(id, button_id);
|
||||
}
|
||||
#[cfg(feature = "launcher")]
|
||||
ModuleUpdateEvent::OpenPopupAt(geometry) if !disable_popup => {
|
||||
debug!("Opening popup for {} [#{}]", name, id);
|
||||
|
||||
popup.hide();
|
||||
popup.show_at(id, geometry);
|
||||
}
|
||||
ModuleUpdateEvent::ClosePopup if !disable_popup => {
|
||||
debug!("Closing popup for {} [#{}]", name, id);
|
||||
popup.hide();
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
ModuleUpdateEvent::OpenPopup(button_id) if !disable_popup => {
|
||||
debug!(
|
||||
"Opening popup for {} [#{}] (button id: {button_id})",
|
||||
name, id
|
||||
);
|
||||
popup.hide();
|
||||
popup.show(id, button_id);
|
||||
}
|
||||
#[cfg(feature = "launcher")]
|
||||
ModuleUpdateEvent::OpenPopupAt(geometry) if !disable_popup => {
|
||||
debug!("Opening popup for {} [#{}]", name, id);
|
||||
|
||||
popup.hide();
|
||||
popup.show_at(id, geometry);
|
||||
}
|
||||
ModuleUpdateEvent::ClosePopup if !disable_popup => {
|
||||
debug!("Closing popup for {} [#{}]", name, id);
|
||||
popup.hide();
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -450,39 +463,43 @@ impl ModuleFactory for PopupModuleFactory {
|
|||
) where
|
||||
TSend: Debug + Clone + Send + 'static,
|
||||
{
|
||||
let popup = self.popup.clone();
|
||||
let button_id = self.button_id;
|
||||
glib_recv_mpsc!(rx, ev => {
|
||||
match ev {
|
||||
ModuleUpdateEvent::Update(update) => {
|
||||
send!(tx, update);
|
||||
}
|
||||
ModuleUpdateEvent::TogglePopup(_) if !disable_popup => {
|
||||
debug!("Toggling popup for {} [#{}] (button id: {button_id})", name, id);
|
||||
if popup.visible() && popup.current_widget().unwrap_or_default() == id {
|
||||
popup.hide();
|
||||
} else {
|
||||
popup.show(id, button_id);
|
||||
}
|
||||
}
|
||||
ModuleUpdateEvent::OpenPopup(_) if !disable_popup => {
|
||||
debug!("Opening popup for {} [#{}] (button id: {button_id})", name, id);
|
||||
|
||||
rx.recv_glib(&self.popup, move |popup, ev| match ev {
|
||||
ModuleUpdateEvent::Update(update) => {
|
||||
tx.send_expect(update);
|
||||
}
|
||||
ModuleUpdateEvent::TogglePopup(_) if !disable_popup => {
|
||||
debug!(
|
||||
"Toggling popup for {} [#{}] (button id: {button_id})",
|
||||
name, id
|
||||
);
|
||||
if popup.visible() && popup.current_widget().unwrap_or_default() == id {
|
||||
popup.hide();
|
||||
} else {
|
||||
popup.show(id, button_id);
|
||||
}
|
||||
#[cfg(feature = "launcher")]
|
||||
ModuleUpdateEvent::OpenPopupAt(geometry) if !disable_popup => {
|
||||
debug!("Opening popup for {} [#{}]", name, id);
|
||||
|
||||
popup.hide();
|
||||
popup.show_at(id, geometry);
|
||||
}
|
||||
ModuleUpdateEvent::ClosePopup if !disable_popup => {
|
||||
debug!("Closing popup for {} [#{}]", name, id);
|
||||
popup.hide();
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
ModuleUpdateEvent::OpenPopup(_) if !disable_popup => {
|
||||
debug!(
|
||||
"Opening popup for {} [#{}] (button id: {button_id})",
|
||||
name, id
|
||||
);
|
||||
popup.hide();
|
||||
popup.show(id, button_id);
|
||||
}
|
||||
#[cfg(feature = "launcher")]
|
||||
ModuleUpdateEvent::OpenPopupAt(geometry) if !disable_popup => {
|
||||
debug!("Opening popup for {} [#{}]", name, id);
|
||||
|
||||
popup.hide();
|
||||
popup.show_at(id, geometry);
|
||||
}
|
||||
ModuleUpdateEvent::ClosePopup if !disable_popup => {
|
||||
debug!("Closing popup for {} [#{}]", name, id);
|
||||
popup.hide();
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -569,7 +586,7 @@ pub fn wrap_widget<W: IsA<Widget>>(
|
|||
let container = EventBox::new();
|
||||
container.add_class("widget-container");
|
||||
|
||||
container.add_events(EventMask::SCROLL_MASK);
|
||||
container.add_events(EventMask::SCROLL_MASK | EventMask::SMOOTH_SCROLL_MASK);
|
||||
container.add(&revealer);
|
||||
|
||||
common.install_events(&container, &revealer);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use crate::config::{CommonConfig, TruncateMode};
|
||||
use crate::config::{CommonConfig, LayoutConfig, TruncateMode};
|
||||
use dirs::{audio_dir, home_dir};
|
||||
use serde::Deserialize;
|
||||
use std::path::PathBuf;
|
||||
|
|
@ -74,13 +74,23 @@ impl Default for Icons {
|
|||
#[serde(rename_all = "snake_case")]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub enum PlayerType {
|
||||
#[cfg(feature = "music+mpd")]
|
||||
Mpd,
|
||||
#[cfg(feature = "music+mpris")]
|
||||
Mpris,
|
||||
}
|
||||
|
||||
impl Default for PlayerType {
|
||||
fn default() -> Self {
|
||||
Self::Mpris
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(feature = "music+mpris")] {
|
||||
Self::Mpris
|
||||
} else if #[cfg(feature = "music+mpd")] {
|
||||
Self::Mpd
|
||||
} else {
|
||||
compile_error!("No player type feature enabled")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -147,6 +157,25 @@ pub struct MusicModule {
|
|||
/// **Default**: `null`
|
||||
pub(crate) truncate: Option<TruncateMode>,
|
||||
|
||||
/// See [truncate options](module-level-options#truncate-mode).
|
||||
///
|
||||
/// **Default**: `null`
|
||||
pub(crate) truncate_popup_artist: Option<TruncateMode>,
|
||||
|
||||
/// See [truncate options](module-level-options#truncate-mode).
|
||||
///
|
||||
/// **Default**: `null`
|
||||
pub(crate) truncate_popup_album: Option<TruncateMode>,
|
||||
|
||||
/// See [truncate options](module-level-options#truncate-mode).
|
||||
///
|
||||
/// **Default**: `null`
|
||||
pub(crate) truncate_popup_title: Option<TruncateMode>,
|
||||
|
||||
/// See [layout options](module-level-options#layout)
|
||||
#[serde(default, flatten)]
|
||||
pub(crate) layout: LayoutConfig,
|
||||
|
||||
/// See [common options](module-level-options#common-options).
|
||||
#[serde(flatten)]
|
||||
pub common: Option<CommonConfig>,
|
||||
|
|
|
|||
|
|
@ -1,31 +1,31 @@
|
|||
use std::cell::RefMut;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::Duration;
|
||||
|
||||
use color_eyre::Result;
|
||||
use glib::{markup_escape_text, Propagation, PropertySet};
|
||||
use glib::{Propagation, PropertySet};
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Button, IconTheme, Label, Orientation, Scale};
|
||||
use gtk::{Button, Label, Orientation, Scale};
|
||||
use regex::Regex;
|
||||
use tokio::sync::{broadcast, mpsc};
|
||||
use tracing::error;
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{error, warn};
|
||||
|
||||
pub use self::config::MusicModule;
|
||||
use self::config::PlayerType;
|
||||
use crate::channels::{AsyncSenderExt, BroadcastReceiverExt};
|
||||
use crate::clients::Clients;
|
||||
use crate::clients::music::{
|
||||
self, MusicClient, PlayerState, PlayerUpdate, ProgressTick, Status, Track,
|
||||
};
|
||||
use crate::clients::Clients;
|
||||
use crate::gtk_helpers::IronbarGtkExt;
|
||||
use crate::image::{new_icon_button, new_icon_label, ImageProvider};
|
||||
use crate::gtk_helpers::{IronbarGtkExt, IronbarLabelExt};
|
||||
use crate::image::{IconButton, IconLabel};
|
||||
use crate::modules::PopupButton;
|
||||
use crate::modules::{
|
||||
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, WidgetContext,
|
||||
};
|
||||
use crate::{glib_recv, module_impl, send_async, spawn, try_send};
|
||||
|
||||
pub use self::config::MusicModule;
|
||||
use self::config::PlayerType;
|
||||
use crate::{image, module_impl, spawn};
|
||||
|
||||
mod config;
|
||||
|
||||
|
|
@ -40,13 +40,18 @@ pub enum PlayerCommand {
|
|||
}
|
||||
|
||||
/// Formats a duration given in seconds
|
||||
/// in hh:mm format
|
||||
/// in hh:mm:ss format
|
||||
fn format_time(duration: Duration) -> String {
|
||||
let time = duration.as_secs();
|
||||
let hours = time / (60 * 60);
|
||||
let minutes = (time / 60) % 60;
|
||||
let seconds = time % 60;
|
||||
|
||||
format!("{minutes:0>2}:{seconds:0>2}")
|
||||
if hours > 0 {
|
||||
format!("{hours}:{minutes:0>2}:{seconds:0>2}")
|
||||
} else {
|
||||
format!("{minutes:0>2}:{seconds:0>2}")
|
||||
}
|
||||
}
|
||||
|
||||
/// Extracts the formatting tokens from a formatting string
|
||||
|
|
@ -76,7 +81,9 @@ fn get_client(
|
|||
music_dir: PathBuf,
|
||||
) -> Arc<dyn MusicClient> {
|
||||
let client_type = match player_type {
|
||||
#[cfg(feature = "music+mpd")]
|
||||
PlayerType::Mpd => music::ClientType::Mpd { host, music_dir },
|
||||
#[cfg(feature = "music+mpris")]
|
||||
PlayerType::Mpris => music::ClientType::Mpris,
|
||||
};
|
||||
|
||||
|
|
@ -129,24 +136,14 @@ impl Module<Button> for MusicModule {
|
|||
display_string,
|
||||
};
|
||||
|
||||
send_async!(
|
||||
tx,
|
||||
ModuleUpdateEvent::Update(ControllerEvent::Update(Some(
|
||||
update
|
||||
)))
|
||||
);
|
||||
tx.send_update(ControllerEvent::Update(Some(update))).await;
|
||||
}
|
||||
None => send_async!(
|
||||
tx,
|
||||
ModuleUpdateEvent::Update(ControllerEvent::Update(None))
|
||||
),
|
||||
None => tx.send_update(ControllerEvent::Update(None)).await,
|
||||
},
|
||||
PlayerUpdate::ProgressTick(progress_tick) => send_async!(
|
||||
tx,
|
||||
ModuleUpdateEvent::Update(ControllerEvent::UpdateProgress(
|
||||
progress_tick
|
||||
))
|
||||
),
|
||||
PlayerUpdate::ProgressTick(progress_tick) => {
|
||||
tx.send_update(ControllerEvent::UpdateProgress(progress_tick))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -182,79 +179,83 @@ impl Module<Button> for MusicModule {
|
|||
info: &ModuleInfo,
|
||||
) -> Result<ModuleParts<Button>> {
|
||||
let button = Button::new();
|
||||
let button_contents = gtk::Box::new(Orientation::Horizontal, 5);
|
||||
let button_contents = gtk::Box::new(self.layout.orientation(info), 5);
|
||||
button_contents.add_class("contents");
|
||||
|
||||
button.add(&button_contents);
|
||||
|
||||
let icon_play = new_icon_label(&self.icons.play, info.icon_theme, self.icon_size);
|
||||
let icon_pause = new_icon_label(&self.icons.pause, info.icon_theme, self.icon_size);
|
||||
let label = Label::new(None);
|
||||
let image_provider = context.ironbar.image_provider();
|
||||
|
||||
label.set_use_markup(true);
|
||||
label.set_angle(info.bar_position.get_angle());
|
||||
let icon_play = IconLabel::new(&self.icons.play, self.icon_size, &image_provider);
|
||||
let icon_pause = IconLabel::new(&self.icons.pause, self.icon_size, &image_provider);
|
||||
|
||||
icon_play.label().set_angle(self.layout.angle(info));
|
||||
icon_play.label().set_justify(self.layout.justify.into());
|
||||
|
||||
icon_pause.label().set_angle(self.layout.angle(info));
|
||||
icon_pause.label().set_justify(self.layout.justify.into());
|
||||
|
||||
let label = Label::builder()
|
||||
.use_markup(true)
|
||||
.angle(self.layout.angle(info))
|
||||
.justify(self.layout.justify.into())
|
||||
.build();
|
||||
|
||||
if let Some(truncate) = self.truncate {
|
||||
truncate.truncate_label(&label);
|
||||
label.truncate(truncate);
|
||||
}
|
||||
|
||||
button_contents.add(&icon_pause);
|
||||
button_contents.add(&icon_play);
|
||||
button_contents.add(&*icon_pause);
|
||||
button_contents.add(&*icon_play);
|
||||
button_contents.add(&label);
|
||||
|
||||
{
|
||||
let tx = context.tx.clone();
|
||||
|
||||
button.connect_clicked(move |button| {
|
||||
try_send!(tx, ModuleUpdateEvent::TogglePopup(button.popup_id()));
|
||||
tx.send_spawn(ModuleUpdateEvent::TogglePopup(button.popup_id()));
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let button = button.clone();
|
||||
let rx = context.subscribe();
|
||||
|
||||
let tx = context.tx.clone();
|
||||
let rx = context.subscribe();
|
||||
rx.recv_glib((&button, &context.tx), move |(button, tx), event| {
|
||||
let ControllerEvent::Update(mut event) = event else {
|
||||
return;
|
||||
};
|
||||
|
||||
glib_recv!(rx, event => {
|
||||
let ControllerEvent::Update(mut event) = event else {
|
||||
continue;
|
||||
};
|
||||
if let Some(event) = event.take() {
|
||||
label.set_label_escaped(&event.display_string);
|
||||
|
||||
if let Some(event) = event.take() {
|
||||
label.set_label(&event.display_string);
|
||||
button.show();
|
||||
|
||||
button.show();
|
||||
|
||||
match event.status.state {
|
||||
PlayerState::Playing if self.show_status_icon => {
|
||||
icon_play.show();
|
||||
icon_pause.hide();
|
||||
}
|
||||
PlayerState::Paused if self.show_status_icon => {
|
||||
icon_pause.show();
|
||||
icon_play.hide();
|
||||
}
|
||||
PlayerState::Stopped => {
|
||||
button.hide();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if !self.show_status_icon {
|
||||
match event.status.state {
|
||||
PlayerState::Playing if self.show_status_icon => {
|
||||
icon_play.show();
|
||||
icon_pause.hide();
|
||||
}
|
||||
PlayerState::Paused if self.show_status_icon => {
|
||||
icon_pause.show();
|
||||
icon_play.hide();
|
||||
}
|
||||
} else {
|
||||
button.hide();
|
||||
try_send!(tx, ModuleUpdateEvent::ClosePopup);
|
||||
PlayerState::Stopped => {
|
||||
button.hide();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
let rx = context.subscribe();
|
||||
if !self.show_status_icon {
|
||||
icon_pause.hide();
|
||||
icon_play.hide();
|
||||
}
|
||||
} else {
|
||||
button.hide();
|
||||
tx.send_spawn(ModuleUpdateEvent::ClosePopup);
|
||||
}
|
||||
});
|
||||
|
||||
let popup = self
|
||||
.into_popup(context.controller_tx.clone(), rx, context, info)
|
||||
.into_popup(context, info)
|
||||
.into_popup_parts(vec![&button]);
|
||||
|
||||
Ok(ModuleParts::new(button, popup))
|
||||
|
|
@ -262,12 +263,10 @@ impl Module<Button> for MusicModule {
|
|||
|
||||
fn into_popup(
|
||||
self,
|
||||
tx: mpsc::Sender<Self::ReceiveMessage>,
|
||||
rx: broadcast::Receiver<Self::SendMessage>,
|
||||
_context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
info: &ModuleInfo,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
_info: &ModuleInfo,
|
||||
) -> Option<gtk::Box> {
|
||||
let icon_theme = info.icon_theme;
|
||||
let image_provider = context.ironbar.image_provider();
|
||||
|
||||
let container = gtk::Box::new(Orientation::Vertical, 10);
|
||||
let main_container = gtk::Box::new(Orientation::Horizontal, 10);
|
||||
|
|
@ -281,9 +280,21 @@ impl Module<Button> for MusicModule {
|
|||
let icons = self.icons;
|
||||
|
||||
let info_box = gtk::Box::new(Orientation::Vertical, 10);
|
||||
let title_label = IconLabel::new(&icons.track, None, icon_theme);
|
||||
let album_label = IconLabel::new(&icons.album, None, icon_theme);
|
||||
let artist_label = IconLabel::new(&icons.artist, None, icon_theme);
|
||||
|
||||
let title_label = IconPrefixedLabel::new(&icons.track, None, &image_provider);
|
||||
if let Some(truncate) = self.truncate_popup_title {
|
||||
title_label.label.truncate(truncate);
|
||||
}
|
||||
|
||||
let album_label = IconPrefixedLabel::new(&icons.album, None, &image_provider);
|
||||
if let Some(truncate) = self.truncate_popup_album {
|
||||
album_label.label.truncate(truncate);
|
||||
}
|
||||
|
||||
let artist_label = IconPrefixedLabel::new(&icons.artist, None, &image_provider);
|
||||
if let Some(truncate) = self.truncate_popup_artist {
|
||||
artist_label.label.truncate(truncate);
|
||||
}
|
||||
|
||||
title_label.container.add_class("title");
|
||||
album_label.container.add_class("album");
|
||||
|
|
@ -296,22 +307,22 @@ impl Module<Button> for MusicModule {
|
|||
let controls_box = gtk::Box::new(Orientation::Horizontal, 0);
|
||||
controls_box.add_class("controls");
|
||||
|
||||
let btn_prev = new_icon_button(&icons.prev, icon_theme, self.icon_size);
|
||||
let btn_prev = IconButton::new(&icons.prev, self.icon_size, image_provider.clone());
|
||||
btn_prev.add_class("btn-prev");
|
||||
|
||||
let btn_play = new_icon_button(&icons.play, icon_theme, self.icon_size);
|
||||
let btn_play = IconButton::new(&icons.play, self.icon_size, image_provider.clone());
|
||||
btn_play.add_class("btn-play");
|
||||
|
||||
let btn_pause = new_icon_button(&icons.pause, icon_theme, self.icon_size);
|
||||
let btn_pause = IconButton::new(&icons.pause, self.icon_size, image_provider.clone());
|
||||
btn_pause.add_class("btn-pause");
|
||||
|
||||
let btn_next = new_icon_button(&icons.next, icon_theme, self.icon_size);
|
||||
let btn_next = IconButton::new(&icons.next, self.icon_size, image_provider.clone());
|
||||
btn_next.add_class("btn-next");
|
||||
|
||||
controls_box.add(&btn_prev);
|
||||
controls_box.add(&btn_play);
|
||||
controls_box.add(&btn_pause);
|
||||
controls_box.add(&btn_next);
|
||||
controls_box.add(&*btn_prev);
|
||||
controls_box.add(&*btn_play);
|
||||
controls_box.add(&*btn_pause);
|
||||
controls_box.add(&*btn_next);
|
||||
|
||||
info_box.add(&controls_box);
|
||||
|
||||
|
|
@ -322,40 +333,40 @@ impl Module<Button> for MusicModule {
|
|||
volume_slider.set_inverted(true);
|
||||
volume_slider.add_class("slider");
|
||||
|
||||
let volume_icon = new_icon_label(&icons.volume, icon_theme, self.icon_size);
|
||||
let volume_icon = IconLabel::new(&icons.volume, self.icon_size, &image_provider);
|
||||
volume_icon.add_class("icon");
|
||||
|
||||
volume_box.pack_start(&volume_slider, true, true, 0);
|
||||
volume_box.pack_end(&volume_icon, false, false, 0);
|
||||
volume_box.pack_end(&*volume_icon, false, false, 0);
|
||||
|
||||
main_container.add(&album_image);
|
||||
main_container.add(&info_box);
|
||||
main_container.add(&volume_box);
|
||||
container.add(&main_container);
|
||||
|
||||
let tx_prev = tx.clone();
|
||||
let tx_prev = context.controller_tx.clone();
|
||||
btn_prev.connect_clicked(move |_| {
|
||||
try_send!(tx_prev, PlayerCommand::Previous);
|
||||
tx_prev.send_spawn(PlayerCommand::Previous);
|
||||
});
|
||||
|
||||
let tx_play = tx.clone();
|
||||
let tx_play = context.controller_tx.clone();
|
||||
btn_play.connect_clicked(move |_| {
|
||||
try_send!(tx_play, PlayerCommand::Play);
|
||||
tx_play.send_spawn(PlayerCommand::Play);
|
||||
});
|
||||
|
||||
let tx_pause = tx.clone();
|
||||
let tx_pause = context.controller_tx.clone();
|
||||
btn_pause.connect_clicked(move |_| {
|
||||
try_send!(tx_pause, PlayerCommand::Pause);
|
||||
tx_pause.send_spawn(PlayerCommand::Pause);
|
||||
});
|
||||
|
||||
let tx_next = tx.clone();
|
||||
let tx_next = context.controller_tx.clone();
|
||||
btn_next.connect_clicked(move |_| {
|
||||
try_send!(tx_next, PlayerCommand::Next);
|
||||
tx_next.send_spawn(PlayerCommand::Next);
|
||||
});
|
||||
|
||||
let tx_vol = tx.clone();
|
||||
let tx_vol = context.controller_tx.clone();
|
||||
volume_slider.connect_change_value(move |_, _, val| {
|
||||
try_send!(tx_vol, PlayerCommand::Volume(val as u8));
|
||||
tx_vol.send_spawn(PlayerCommand::Volume(val as u8));
|
||||
Propagation::Proceed
|
||||
});
|
||||
|
||||
|
|
@ -387,9 +398,10 @@ impl Module<Button> for MusicModule {
|
|||
|
||||
{
|
||||
let drag_lock = drag_lock.clone();
|
||||
let tx = context.controller_tx.clone();
|
||||
progress.connect_button_release_event(move |scale, _| {
|
||||
let value = scale.value();
|
||||
try_send!(tx, PlayerCommand::Seek(Duration::from_secs_f64(value)));
|
||||
tx.send_spawn(PlayerCommand::Seek(Duration::from_secs_f64(value)));
|
||||
|
||||
drag_lock.set(false);
|
||||
Propagation::Proceed
|
||||
|
|
@ -398,107 +410,123 @@ impl Module<Button> for MusicModule {
|
|||
|
||||
container.show_all();
|
||||
|
||||
{
|
||||
let icon_theme = icon_theme.clone();
|
||||
let image_size = self.cover_image_size;
|
||||
let image_size = self.cover_image_size;
|
||||
|
||||
let mut prev_cover = None;
|
||||
glib_recv!(rx, event => {
|
||||
match event {
|
||||
ControllerEvent::Update(Some(update)) => {
|
||||
// only update art when album changes
|
||||
let new_cover = update.song.cover_path;
|
||||
if prev_cover != new_cover {
|
||||
prev_cover.clone_from(&new_cover);
|
||||
let res = if let Some(image) = new_cover.and_then(|cover_path| {
|
||||
ImageProvider::parse(&cover_path, &icon_theme, false, image_size)
|
||||
}) {
|
||||
album_image.show();
|
||||
image.load_into_image(album_image.clone())
|
||||
} else {
|
||||
album_image.set_from_pixbuf(None);
|
||||
album_image.hide();
|
||||
Ok(())
|
||||
};
|
||||
let mut prev_cover = None;
|
||||
context.subscribe().recv_glib((), move |(), event| {
|
||||
match event {
|
||||
ControllerEvent::Update(Some(update)) => {
|
||||
// only update art when album changes
|
||||
let new_cover = update.song.cover_path;
|
||||
if prev_cover != new_cover {
|
||||
prev_cover.clone_from(&new_cover);
|
||||
|
||||
if let Err(err) = res {
|
||||
error!("{err:?}");
|
||||
}
|
||||
}
|
||||
if let Some(cover_path) = new_cover {
|
||||
let image_provider = image_provider.clone();
|
||||
let album_image = album_image.clone();
|
||||
|
||||
update_popup_metadata_label(update.song.title, &title_label);
|
||||
update_popup_metadata_label(update.song.album, &album_label);
|
||||
update_popup_metadata_label(update.song.artist, &artist_label);
|
||||
glib::spawn_future_local(async move {
|
||||
let success = match image_provider
|
||||
.load_into_image(&cover_path, image_size, false, &album_image)
|
||||
.await
|
||||
{
|
||||
Ok(true) => {
|
||||
album_image.show();
|
||||
true
|
||||
}
|
||||
Ok(false) => {
|
||||
warn!("failed to parse image: {}", cover_path);
|
||||
false
|
||||
}
|
||||
Err(err) => {
|
||||
error!("failed to load image: {}", err);
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
match update.status.state {
|
||||
PlayerState::Stopped => {
|
||||
btn_pause.hide();
|
||||
btn_play.show();
|
||||
btn_play.set_sensitive(false);
|
||||
}
|
||||
PlayerState::Playing => {
|
||||
btn_play.set_sensitive(false);
|
||||
btn_play.hide();
|
||||
|
||||
btn_pause.set_sensitive(true);
|
||||
btn_pause.show();
|
||||
}
|
||||
PlayerState::Paused => {
|
||||
btn_pause.set_sensitive(false);
|
||||
btn_pause.hide();
|
||||
|
||||
btn_play.set_sensitive(true);
|
||||
btn_play.show();
|
||||
}
|
||||
}
|
||||
|
||||
let enable_prev = update.status.playlist_position > 0;
|
||||
|
||||
let enable_next =
|
||||
update.status.playlist_position < update.status.playlist_length;
|
||||
|
||||
btn_prev.set_sensitive(enable_prev);
|
||||
btn_next.set_sensitive(enable_next);
|
||||
|
||||
if let Some(volume) = update.status.volume_percent {
|
||||
volume_slider.set_value(f64::from(volume));
|
||||
volume_box.show();
|
||||
if !success {
|
||||
album_image.set_from_pixbuf(None);
|
||||
album_image.hide();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
volume_box.hide();
|
||||
album_image.set_from_pixbuf(None);
|
||||
album_image.hide();
|
||||
}
|
||||
}
|
||||
ControllerEvent::UpdateProgress(progress_tick)
|
||||
if !drag_lock.load(Ordering::Relaxed) =>
|
||||
|
||||
update_popup_metadata_label(update.song.title, &title_label);
|
||||
update_popup_metadata_label(update.song.album, &album_label);
|
||||
update_popup_metadata_label(update.song.artist, &artist_label);
|
||||
|
||||
match update.status.state {
|
||||
PlayerState::Stopped => {
|
||||
btn_pause.hide();
|
||||
btn_play.show();
|
||||
btn_play.set_sensitive(false);
|
||||
}
|
||||
PlayerState::Playing => {
|
||||
btn_play.set_sensitive(false);
|
||||
btn_play.hide();
|
||||
|
||||
btn_pause.set_sensitive(true);
|
||||
btn_pause.show();
|
||||
}
|
||||
PlayerState::Paused => {
|
||||
btn_pause.set_sensitive(false);
|
||||
btn_pause.hide();
|
||||
|
||||
btn_play.set_sensitive(true);
|
||||
btn_play.show();
|
||||
}
|
||||
}
|
||||
|
||||
let enable_prev = update.status.playlist_position > 0;
|
||||
|
||||
let enable_next =
|
||||
update.status.playlist_position < update.status.playlist_length;
|
||||
|
||||
btn_prev.set_sensitive(enable_prev);
|
||||
btn_next.set_sensitive(enable_next);
|
||||
|
||||
if let Some(volume) = update.status.volume_percent {
|
||||
volume_slider.set_value(f64::from(volume));
|
||||
volume_box.show();
|
||||
} else {
|
||||
volume_box.hide();
|
||||
}
|
||||
}
|
||||
ControllerEvent::UpdateProgress(progress_tick)
|
||||
if !drag_lock.load(Ordering::Relaxed) =>
|
||||
{
|
||||
if let (Some(elapsed), Some(duration)) =
|
||||
(progress_tick.elapsed, progress_tick.duration)
|
||||
{
|
||||
if let (Some(elapsed), Some(duration)) =
|
||||
(progress_tick.elapsed, progress_tick.duration)
|
||||
{
|
||||
progress_label.set_label(&format!(
|
||||
"{}/{}",
|
||||
format_time(elapsed),
|
||||
format_time(duration)
|
||||
));
|
||||
progress_label.set_label_escaped(&format!(
|
||||
"{}/{}",
|
||||
format_time(elapsed),
|
||||
format_time(duration)
|
||||
));
|
||||
|
||||
progress.set_value(elapsed.as_secs_f64());
|
||||
progress.set_range(0.0, duration.as_secs_f64());
|
||||
progress_box.show_all();
|
||||
} else {
|
||||
progress_box.hide();
|
||||
}
|
||||
progress.set_value(elapsed.as_secs_f64());
|
||||
progress.set_range(0.0, duration.as_secs_f64());
|
||||
progress_box.show_all();
|
||||
} else {
|
||||
progress_box.hide();
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
|
||||
Some(container)
|
||||
}
|
||||
}
|
||||
|
||||
fn update_popup_metadata_label(text: Option<String>, label: &IconLabel) {
|
||||
fn update_popup_metadata_label(text: Option<String>, label: &IconPrefixedLabel) {
|
||||
match text {
|
||||
Some(value) => {
|
||||
label.label.set_text(&value);
|
||||
label.label.set_label_escaped(&value);
|
||||
label.container.show_all();
|
||||
}
|
||||
None => {
|
||||
|
|
@ -531,21 +559,20 @@ fn get_token_value(song: &Track, token: &str) -> String {
|
|||
"track" => song.track.map(|x| x.to_string()),
|
||||
_ => Some(token.to_string()),
|
||||
}
|
||||
.map(|str| markup_escape_text(str.as_str()).to_string())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct IconLabel {
|
||||
struct IconPrefixedLabel {
|
||||
label: Label,
|
||||
container: gtk::Box,
|
||||
}
|
||||
|
||||
impl IconLabel {
|
||||
fn new(icon_input: &str, label: Option<&str>, icon_theme: &IconTheme) -> Self {
|
||||
impl IconPrefixedLabel {
|
||||
fn new(icon_input: &str, label: Option<&str>, image_provider: &image::Provider) -> Self {
|
||||
let container = gtk::Box::new(Orientation::Horizontal, 5);
|
||||
|
||||
let icon = new_icon_label(icon_input, icon_theme, 24);
|
||||
let icon = IconLabel::new(icon_input, 24, image_provider);
|
||||
|
||||
let mut builder = Label::builder().use_markup(true);
|
||||
|
||||
|
|
@ -558,7 +585,7 @@ impl IconLabel {
|
|||
icon.add_class("icon-box");
|
||||
label.add_class("label");
|
||||
|
||||
container.add(&icon);
|
||||
container.add(&*icon);
|
||||
container.add(&label);
|
||||
|
||||
Self { label, container }
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
use crate::channels::{AsyncSenderExt, BroadcastReceiverExt};
|
||||
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, module_impl, send_async, spawn, try_send};
|
||||
use crate::modules::{Module, ModuleInfo, ModuleParts, WidgetContext};
|
||||
use crate::{module_impl, spawn};
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Align, Button, Label, Overlay};
|
||||
use serde::Deserialize;
|
||||
|
|
@ -153,12 +154,12 @@ impl Module<Overlay> for NotificationsModule {
|
|||
let initial_state = client.state().await;
|
||||
|
||||
match initial_state {
|
||||
Ok(ev) => send_async!(tx, ModuleUpdateEvent::Update(ev)),
|
||||
Ok(ev) => tx.send_update(ev).await,
|
||||
Err(err) => error!("{err:?}"),
|
||||
};
|
||||
}
|
||||
|
||||
while let Ok(ev) = rx.recv().await {
|
||||
send_async!(tx, ModuleUpdateEvent::Update(ev));
|
||||
tx.send_update(ev).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -200,20 +201,16 @@ impl Module<Overlay> for NotificationsModule {
|
|||
|
||||
let ctx = context.controller_tx.clone();
|
||||
button.connect_clicked(move |_| {
|
||||
try_send!(ctx, UiEvent::ToggleVisibility);
|
||||
ctx.send_spawn(UiEvent::ToggleVisibility);
|
||||
});
|
||||
|
||||
{
|
||||
let button = button.clone();
|
||||
context.subscribe().recv_glib(&button, move |button, ev| {
|
||||
let icon = self.icons.icon(ev);
|
||||
button.set_label(icon);
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
label.set_label(&ev.count.to_string());
|
||||
label.set_visible(self.show_count && ev.count > 0);
|
||||
});
|
||||
|
||||
Ok(ModuleParts {
|
||||
widget: overlay,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
use crate::config::CommonConfig;
|
||||
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
|
||||
use crate::channels::{AsyncSenderExt, BroadcastReceiverExt};
|
||||
use crate::config::{CommonConfig, LayoutConfig};
|
||||
use crate::gtk_helpers::IronbarLabelExt;
|
||||
use crate::modules::{Module, ModuleInfo, ModuleParts, WidgetContext};
|
||||
use crate::script::{OutputStream, Script, ScriptMode};
|
||||
use crate::{glib_recv, module_impl, spawn, try_send};
|
||||
use crate::{module_impl, spawn};
|
||||
use color_eyre::{Help, Report, Result};
|
||||
use gtk::prelude::*;
|
||||
use gtk::Label;
|
||||
use serde::Deserialize;
|
||||
use tokio::sync::mpsc;
|
||||
|
|
@ -35,6 +36,11 @@ pub struct ScriptModule {
|
|||
#[serde(default = "default_interval")]
|
||||
interval: u64,
|
||||
|
||||
// -- Common --
|
||||
/// See [layout options](module-level-options#layout)
|
||||
#[serde(default, flatten)]
|
||||
layout: LayoutConfig,
|
||||
|
||||
/// See [common options](module-level-options#common-options).
|
||||
#[serde(flatten)]
|
||||
pub common: Option<CommonConfig>,
|
||||
|
|
@ -78,7 +84,7 @@ impl Module<Label> for ScriptModule {
|
|||
spawn(async move {
|
||||
script.run(None, move |out, _| match out {
|
||||
OutputStream::Stdout(stdout) => {
|
||||
try_send!(tx, ModuleUpdateEvent::Update(stdout));
|
||||
tx.send_update_spawn(stdout);
|
||||
},
|
||||
OutputStream::Stderr(stderr) => {
|
||||
error!("{:?}", Report::msg(stderr)
|
||||
|
|
@ -98,13 +104,15 @@ impl Module<Label> for ScriptModule {
|
|||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> Result<ModuleParts<Label>> {
|
||||
let label = Label::builder().use_markup(true).build();
|
||||
label.set_angle(info.bar_position.get_angle());
|
||||
let label = Label::builder()
|
||||
.use_markup(true)
|
||||
.angle(self.layout.angle(info))
|
||||
.justify(self.layout.justify.into())
|
||||
.build();
|
||||
|
||||
{
|
||||
let label = label.clone();
|
||||
glib_recv!(context.subscribe(), s => label.set_markup(s.as_str()));
|
||||
}
|
||||
context
|
||||
.subscribe()
|
||||
.recv_glib(&label, |label, s| label.set_label_escaped(&s));
|
||||
|
||||
Ok(ModuleParts {
|
||||
widget: label,
|
||||
|
|
|
|||
|
|
@ -1,451 +0,0 @@
|
|||
use crate::config::{CommonConfig, ModuleOrientation};
|
||||
use crate::gtk_helpers::IronbarGtkExt;
|
||||
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
|
||||
use crate::{glib_recv, module_impl, send_async, spawn};
|
||||
use color_eyre::Result;
|
||||
use gtk::prelude::*;
|
||||
use gtk::Label;
|
||||
use regex::{Captures, Regex};
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
use sysinfo::{ComponentExt, CpuExt, DiskExt, NetworkExt, RefreshKind, System, SystemExt};
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::time::sleep;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct SysInfoModule {
|
||||
/// List of strings including formatting tokens.
|
||||
/// For available tokens, see [below](#formatting-tokens).
|
||||
///
|
||||
/// **Required**
|
||||
format: Vec<String>,
|
||||
|
||||
/// Number of seconds between refresh.
|
||||
///
|
||||
/// This can be set as a global interval,
|
||||
/// or passed as an object to customize the interval per-system.
|
||||
///
|
||||
/// **Default**: `5`
|
||||
#[serde(default = "Interval::default")]
|
||||
interval: Interval,
|
||||
|
||||
/// The orientation of text for the labels.
|
||||
///
|
||||
/// **Valid options**: `horizontal`, `vertical, `h`, `v`
|
||||
/// <br>
|
||||
/// **Default** : `horizontal`
|
||||
#[serde(default)]
|
||||
orientation: ModuleOrientation,
|
||||
|
||||
/// The orientation by which the labels are laid out.
|
||||
///
|
||||
/// **Valid options**: `horizontal`, `vertical, `h`, `v`
|
||||
/// <br>
|
||||
/// **Default** : `horizontal`
|
||||
direction: Option<ModuleOrientation>,
|
||||
|
||||
/// See [common options](module-level-options#common-options).
|
||||
#[serde(flatten)]
|
||||
pub common: Option<CommonConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Copy, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct Intervals {
|
||||
/// The number of seconds between refreshing memory data.
|
||||
///
|
||||
/// **Default**: `5`
|
||||
#[serde(default = "default_interval")]
|
||||
memory: u64,
|
||||
|
||||
/// The number of seconds between refreshing CPU data.
|
||||
///
|
||||
/// **Default**: `5`
|
||||
#[serde(default = "default_interval")]
|
||||
cpu: u64,
|
||||
|
||||
/// The number of seconds between refreshing temperature data.
|
||||
///
|
||||
/// **Default**: `5`
|
||||
#[serde(default = "default_interval")]
|
||||
temps: u64,
|
||||
|
||||
/// The number of seconds between refreshing disk data.
|
||||
///
|
||||
/// **Default**: `5`
|
||||
#[serde(default = "default_interval")]
|
||||
disks: u64,
|
||||
|
||||
/// The number of seconds between refreshing network data.
|
||||
///
|
||||
/// **Default**: `5`
|
||||
#[serde(default = "default_interval")]
|
||||
networks: u64,
|
||||
|
||||
/// The number of seconds between refreshing system data.
|
||||
///
|
||||
/// **Default**: `5`
|
||||
#[serde(default = "default_interval")]
|
||||
system: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Copy, Clone)]
|
||||
#[serde(untagged)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub enum Interval {
|
||||
All(u64),
|
||||
Individual(Intervals),
|
||||
}
|
||||
|
||||
impl Default for Interval {
|
||||
fn default() -> Self {
|
||||
Self::All(default_interval())
|
||||
}
|
||||
}
|
||||
|
||||
impl Interval {
|
||||
const fn memory(self) -> u64 {
|
||||
match self {
|
||||
Self::All(n) => n,
|
||||
Self::Individual(intervals) => intervals.memory,
|
||||
}
|
||||
}
|
||||
|
||||
const fn cpu(self) -> u64 {
|
||||
match self {
|
||||
Self::All(n) => n,
|
||||
Self::Individual(intervals) => intervals.cpu,
|
||||
}
|
||||
}
|
||||
|
||||
const fn temps(self) -> u64 {
|
||||
match self {
|
||||
Self::All(n) => n,
|
||||
Self::Individual(intervals) => intervals.temps,
|
||||
}
|
||||
}
|
||||
|
||||
const fn disks(self) -> u64 {
|
||||
match self {
|
||||
Self::All(n) => n,
|
||||
Self::Individual(intervals) => intervals.disks,
|
||||
}
|
||||
}
|
||||
|
||||
const fn networks(self) -> u64 {
|
||||
match self {
|
||||
Self::All(n) => n,
|
||||
Self::Individual(intervals) => intervals.networks,
|
||||
}
|
||||
}
|
||||
|
||||
const fn system(self) -> u64 {
|
||||
match self {
|
||||
Self::All(n) => n,
|
||||
Self::Individual(intervals) => intervals.system,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fn default_interval() -> u64 {
|
||||
5
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum RefreshType {
|
||||
Memory,
|
||||
Cpu,
|
||||
Temps,
|
||||
Disks,
|
||||
Network,
|
||||
System,
|
||||
}
|
||||
|
||||
impl Module<gtk::Box> for SysInfoModule {
|
||||
type SendMessage = HashMap<String, String>;
|
||||
type ReceiveMessage = ();
|
||||
|
||||
module_impl!("sysinfo");
|
||||
|
||||
fn spawn_controller(
|
||||
&self,
|
||||
_info: &ModuleInfo,
|
||||
context: &WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
_rx: mpsc::Receiver<Self::ReceiveMessage>,
|
||||
) -> Result<()> {
|
||||
let interval = self.interval;
|
||||
|
||||
let refresh_kind = RefreshKind::everything()
|
||||
.without_processes()
|
||||
.without_users_list();
|
||||
|
||||
let mut sys = System::new_with_specifics(refresh_kind);
|
||||
sys.refresh_components_list();
|
||||
sys.refresh_disks_list();
|
||||
sys.refresh_networks_list();
|
||||
|
||||
let (refresh_tx, mut refresh_rx) = mpsc::channel(16);
|
||||
|
||||
macro_rules! spawn_refresh {
|
||||
($refresh_type:expr, $func:ident) => {{
|
||||
let tx = refresh_tx.clone();
|
||||
spawn(async move {
|
||||
loop {
|
||||
send_async!(tx, $refresh_type);
|
||||
sleep(Duration::from_secs(interval.$func())).await;
|
||||
}
|
||||
});
|
||||
}};
|
||||
}
|
||||
|
||||
spawn_refresh!(RefreshType::Memory, memory);
|
||||
spawn_refresh!(RefreshType::Cpu, cpu);
|
||||
spawn_refresh!(RefreshType::Temps, temps);
|
||||
spawn_refresh!(RefreshType::Disks, disks);
|
||||
spawn_refresh!(RefreshType::Network, networks);
|
||||
spawn_refresh!(RefreshType::System, system);
|
||||
|
||||
let tx = context.tx.clone();
|
||||
spawn(async move {
|
||||
let mut format_info = HashMap::new();
|
||||
|
||||
while let Some(refresh) = refresh_rx.recv().await {
|
||||
match refresh {
|
||||
RefreshType::Memory => refresh_memory_tokens(&mut format_info, &mut sys),
|
||||
RefreshType::Cpu => refresh_cpu_tokens(&mut format_info, &mut sys),
|
||||
RefreshType::Temps => refresh_temp_tokens(&mut format_info, &mut sys),
|
||||
RefreshType::Disks => refresh_disk_tokens(&mut format_info, &mut sys),
|
||||
RefreshType::Network => {
|
||||
refresh_network_tokens(&mut format_info, &mut sys, interval.networks());
|
||||
}
|
||||
RefreshType::System => refresh_system_tokens(&mut format_info, &sys),
|
||||
};
|
||||
|
||||
send_async!(tx, ModuleUpdateEvent::Update(format_info.clone()));
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn into_widget(
|
||||
self,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
_info: &ModuleInfo,
|
||||
) -> Result<ModuleParts<gtk::Box>> {
|
||||
let re = Regex::new(r"\{([^}]+)}")?;
|
||||
|
||||
let layout = match self.direction {
|
||||
Some(orientation) => orientation,
|
||||
None => self.orientation,
|
||||
};
|
||||
|
||||
let container = gtk::Box::new(layout.into(), 10);
|
||||
|
||||
let mut labels = Vec::new();
|
||||
|
||||
for format in &self.format {
|
||||
let label = Label::builder().label(format).use_markup(true).build();
|
||||
|
||||
label.add_class("item");
|
||||
label.set_angle(self.orientation.to_angle());
|
||||
|
||||
container.add(&label);
|
||||
labels.push(label);
|
||||
}
|
||||
|
||||
{
|
||||
let formats = self.format;
|
||||
glib_recv!(context.subscribe(), info => {
|
||||
for (format, label) in formats.iter().zip(labels.clone()) {
|
||||
let format_compiled = re.replace_all(format, |caps: &Captures| {
|
||||
info.get(&caps[1])
|
||||
.unwrap_or(&caps[0].to_string())
|
||||
.to_string()
|
||||
});
|
||||
|
||||
label.set_markup(format_compiled.as_ref());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Ok(ModuleParts {
|
||||
widget: container,
|
||||
popup: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn refresh_memory_tokens(format_info: &mut HashMap<String, String>, sys: &mut System) {
|
||||
sys.refresh_memory();
|
||||
|
||||
let total_memory = sys.total_memory();
|
||||
let available_memory = sys.available_memory();
|
||||
|
||||
let actual_used_memory = total_memory - available_memory;
|
||||
let memory_percent = actual_used_memory as f64 / total_memory as f64 * 100.0;
|
||||
|
||||
format_info.insert(
|
||||
String::from("memory_free"),
|
||||
(bytes_to_gigabytes(available_memory)).to_string(),
|
||||
);
|
||||
format_info.insert(
|
||||
String::from("memory_used"),
|
||||
(bytes_to_gigabytes(actual_used_memory)).to_string(),
|
||||
);
|
||||
format_info.insert(
|
||||
String::from("memory_total"),
|
||||
(bytes_to_gigabytes(total_memory)).to_string(),
|
||||
);
|
||||
format_info.insert(
|
||||
String::from("memory_percent"),
|
||||
format!("{memory_percent:0>2.0}"),
|
||||
);
|
||||
|
||||
let used_swap = sys.used_swap();
|
||||
let total_swap = sys.total_swap();
|
||||
|
||||
format_info.insert(
|
||||
String::from("swap_free"),
|
||||
(bytes_to_gigabytes(sys.free_swap())).to_string(),
|
||||
);
|
||||
format_info.insert(
|
||||
String::from("swap_used"),
|
||||
(bytes_to_gigabytes(used_swap)).to_string(),
|
||||
);
|
||||
format_info.insert(
|
||||
String::from("swap_total"),
|
||||
(bytes_to_gigabytes(total_swap)).to_string(),
|
||||
);
|
||||
format_info.insert(
|
||||
String::from("swap_percent"),
|
||||
format!("{:0>2.0}", used_swap as f64 / total_swap as f64 * 100.0),
|
||||
);
|
||||
}
|
||||
|
||||
fn refresh_cpu_tokens(format_info: &mut HashMap<String, String>, sys: &mut System) {
|
||||
sys.refresh_cpu();
|
||||
|
||||
let cpu_info = sys.global_cpu_info();
|
||||
let cpu_percent = cpu_info.cpu_usage();
|
||||
|
||||
format_info.insert(String::from("cpu_percent"), format!("{cpu_percent:0>2.0}"));
|
||||
}
|
||||
|
||||
fn refresh_temp_tokens(format_info: &mut HashMap<String, String>, sys: &mut System) {
|
||||
sys.refresh_components();
|
||||
|
||||
let components = sys.components();
|
||||
for component in components {
|
||||
let key = component.label().replace(' ', "-");
|
||||
let temp = component.temperature();
|
||||
|
||||
format_info.insert(format!("temp_c:{key}"), format!("{temp:.0}"));
|
||||
format_info.insert(format!("temp_f:{key}"), format!("{:.0}", c_to_f(temp)));
|
||||
}
|
||||
}
|
||||
|
||||
fn refresh_disk_tokens(format_info: &mut HashMap<String, String>, sys: &mut System) {
|
||||
sys.refresh_disks();
|
||||
|
||||
for disk in sys.disks() {
|
||||
// replace braces to avoid conflict with regex
|
||||
let key = disk
|
||||
.mount_point()
|
||||
.to_str()
|
||||
.map(|s| s.replace(['{', '}'], ""));
|
||||
|
||||
if let Some(key) = key {
|
||||
let total = disk.total_space();
|
||||
let available = disk.available_space();
|
||||
let used = total - available;
|
||||
|
||||
format_info.insert(
|
||||
format!("disk_free:{key}"),
|
||||
bytes_to_gigabytes(available).to_string(),
|
||||
);
|
||||
|
||||
format_info.insert(
|
||||
format!("disk_used:{key}"),
|
||||
bytes_to_gigabytes(used).to_string(),
|
||||
);
|
||||
|
||||
format_info.insert(
|
||||
format!("disk_total:{key}"),
|
||||
bytes_to_gigabytes(total).to_string(),
|
||||
);
|
||||
|
||||
format_info.insert(
|
||||
format!("disk_percent:{key}"),
|
||||
format!("{:0>2.0}", used as f64 / total as f64 * 100.0),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn refresh_network_tokens(
|
||||
format_info: &mut HashMap<String, String>,
|
||||
sys: &mut System,
|
||||
interval: u64,
|
||||
) {
|
||||
sys.refresh_networks();
|
||||
|
||||
for (iface, network) in sys.networks() {
|
||||
format_info.insert(
|
||||
format!("net_down:{iface}"),
|
||||
format!("{:0>2.0}", bytes_to_megabits(network.received()) / interval),
|
||||
);
|
||||
|
||||
format_info.insert(
|
||||
format!("net_up:{iface}"),
|
||||
format!(
|
||||
"{:0>2.0}",
|
||||
bytes_to_megabits(network.transmitted()) / interval
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn refresh_system_tokens(format_info: &mut HashMap<String, String>, sys: &System) {
|
||||
// no refresh required for these tokens
|
||||
|
||||
let load_average = sys.load_average();
|
||||
format_info.insert(
|
||||
String::from("load_average:1"),
|
||||
format!("{:.2}", load_average.one),
|
||||
);
|
||||
|
||||
format_info.insert(
|
||||
String::from("load_average:5"),
|
||||
format!("{:.2}", load_average.five),
|
||||
);
|
||||
|
||||
format_info.insert(
|
||||
String::from("load_average:15"),
|
||||
format!("{:.2}", load_average.fifteen),
|
||||
);
|
||||
|
||||
let uptime = Duration::from_secs(sys.uptime()).as_secs();
|
||||
let hours = uptime / 3600;
|
||||
format_info.insert(
|
||||
String::from("uptime"),
|
||||
format!("{:0>2}:{:0>2}", hours, (uptime % 3600) / 60),
|
||||
);
|
||||
}
|
||||
|
||||
/// Converts celsius to fahrenheit.
|
||||
fn c_to_f(c: f32) -> f32 {
|
||||
c * 9.0 / 5.0 + 32.0
|
||||
}
|
||||
|
||||
const fn bytes_to_gigabytes(b: u64) -> u64 {
|
||||
const BYTES_IN_GIGABYTE: u64 = 1_000_000_000;
|
||||
b / BYTES_IN_GIGABYTE
|
||||
}
|
||||
|
||||
const fn bytes_to_megabits(b: u64) -> u64 {
|
||||
const BYTES_IN_MEGABIT: u64 = 125_000;
|
||||
b / BYTES_IN_MEGABIT
|
||||
}
|
||||
316
src/modules/sysinfo/mod.rs
Normal file
316
src/modules/sysinfo/mod.rs
Normal file
|
|
@ -0,0 +1,316 @@
|
|||
mod parser;
|
||||
mod renderer;
|
||||
mod token;
|
||||
|
||||
use crate::channels::{AsyncSenderExt, BroadcastReceiverExt};
|
||||
use crate::clients::sysinfo::TokenType;
|
||||
use crate::config::{CommonConfig, LayoutConfig, ModuleOrientation};
|
||||
use crate::gtk_helpers::{IronbarGtkExt, IronbarLabelExt};
|
||||
use crate::modules::sysinfo::token::Part;
|
||||
use crate::modules::{Module, ModuleInfo, ModuleParts, WidgetContext};
|
||||
use crate::{clients, module_impl, spawn};
|
||||
use color_eyre::Result;
|
||||
use gtk::Label;
|
||||
use gtk::prelude::*;
|
||||
use serde::Deserialize;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::time::sleep;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct SysInfoModule {
|
||||
/// List of strings including formatting tokens.
|
||||
/// For available tokens, see [below](#formatting-tokens).
|
||||
///
|
||||
/// **Required**
|
||||
format: Vec<String>,
|
||||
|
||||
/// Number of seconds between refresh.
|
||||
///
|
||||
/// This can be set as a global interval,
|
||||
/// or passed as an object to customize the interval per-system.
|
||||
///
|
||||
/// **Default**: `5`
|
||||
#[serde(default = "Interval::default")]
|
||||
interval: Interval,
|
||||
|
||||
/// The orientation by which the labels are laid out.
|
||||
///
|
||||
/// **Valid options**: `horizontal`, `vertical`, `h`, `v`
|
||||
/// <br>
|
||||
/// **Default** : `horizontal`
|
||||
direction: Option<ModuleOrientation>,
|
||||
|
||||
// -- common --
|
||||
/// See [layout options](module-level-options#layout)
|
||||
#[serde(default, flatten)]
|
||||
layout: LayoutConfig,
|
||||
|
||||
/// See [common options](module-level-options#common-options).
|
||||
#[serde(flatten)]
|
||||
pub common: Option<CommonConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Copy, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct Intervals {
|
||||
/// The number of seconds between refreshing memory data.
|
||||
///
|
||||
/// **Default**: `5`
|
||||
#[serde(default = "default_interval")]
|
||||
memory: u64,
|
||||
|
||||
/// The number of seconds between refreshing CPU data.
|
||||
///
|
||||
/// **Default**: `5`
|
||||
#[serde(default = "default_interval")]
|
||||
cpu: u64,
|
||||
|
||||
/// The number of seconds between refreshing temperature data.
|
||||
///
|
||||
/// **Default**: `5`
|
||||
#[serde(default = "default_interval")]
|
||||
temps: u64,
|
||||
|
||||
/// The number of seconds between refreshing disk data.
|
||||
///
|
||||
/// **Default**: `5`
|
||||
#[serde(default = "default_interval")]
|
||||
disks: u64,
|
||||
|
||||
/// The number of seconds between refreshing network data.
|
||||
///
|
||||
/// **Default**: `5`
|
||||
#[serde(default = "default_interval")]
|
||||
networks: u64,
|
||||
|
||||
/// The number of seconds between refreshing system data.
|
||||
///
|
||||
/// **Default**: `5`
|
||||
#[serde(default = "default_interval")]
|
||||
system: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Copy, Clone)]
|
||||
#[serde(untagged)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub enum Interval {
|
||||
All(u64),
|
||||
Individual(Intervals),
|
||||
}
|
||||
|
||||
impl Default for Interval {
|
||||
fn default() -> Self {
|
||||
Self::All(default_interval())
|
||||
}
|
||||
}
|
||||
|
||||
impl Interval {
|
||||
const fn memory(self) -> u64 {
|
||||
match self {
|
||||
Self::All(n) => n,
|
||||
Self::Individual(intervals) => intervals.memory,
|
||||
}
|
||||
}
|
||||
|
||||
const fn cpu(self) -> u64 {
|
||||
match self {
|
||||
Self::All(n) => n,
|
||||
Self::Individual(intervals) => intervals.cpu,
|
||||
}
|
||||
}
|
||||
|
||||
const fn temps(self) -> u64 {
|
||||
match self {
|
||||
Self::All(n) => n,
|
||||
Self::Individual(intervals) => intervals.temps,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn disks(self) -> u64 {
|
||||
match self {
|
||||
Self::All(n) => n,
|
||||
Self::Individual(intervals) => intervals.disks,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn networks(self) -> u64 {
|
||||
match self {
|
||||
Self::All(n) => n,
|
||||
Self::Individual(intervals) => intervals.networks,
|
||||
}
|
||||
}
|
||||
|
||||
const fn system(self) -> u64 {
|
||||
match self {
|
||||
Self::All(n) => n,
|
||||
Self::Individual(intervals) => intervals.system,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fn default_interval() -> u64 {
|
||||
5
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
enum RefreshType {
|
||||
Memory,
|
||||
Cpu,
|
||||
Temps,
|
||||
Disks,
|
||||
Network,
|
||||
System,
|
||||
}
|
||||
|
||||
impl TokenType {
|
||||
fn is_affected_by(self, refresh_type: RefreshType) -> bool {
|
||||
match self {
|
||||
Self::CpuFrequency | Self::CpuPercent => refresh_type == RefreshType::Cpu,
|
||||
Self::MemoryFree
|
||||
| Self::MemoryAvailable
|
||||
| Self::MemoryTotal
|
||||
| Self::MemoryUsed
|
||||
| Self::MemoryPercent
|
||||
| Self::SwapFree
|
||||
| Self::SwapTotal
|
||||
| Self::SwapUsed
|
||||
| Self::SwapPercent => refresh_type == RefreshType::Memory,
|
||||
Self::TempC | Self::TempF => refresh_type == RefreshType::Temps,
|
||||
Self::DiskFree
|
||||
| Self::DiskTotal
|
||||
| Self::DiskUsed
|
||||
| Self::DiskPercent
|
||||
| Self::DiskRead
|
||||
| Self::DiskWrite => refresh_type == RefreshType::Disks,
|
||||
Self::NetDown | Self::NetUp => refresh_type == RefreshType::Network,
|
||||
Self::LoadAverage1 | Self::LoadAverage5 | Self::LoadAverage15 => {
|
||||
refresh_type == RefreshType::System
|
||||
}
|
||||
Self::Uptime => refresh_type == RefreshType::System,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Module<gtk::Box> for SysInfoModule {
|
||||
type SendMessage = (usize, String);
|
||||
type ReceiveMessage = ();
|
||||
|
||||
module_impl!("sysinfo");
|
||||
|
||||
fn spawn_controller(
|
||||
&self,
|
||||
_info: &ModuleInfo,
|
||||
context: &WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
_rx: mpsc::Receiver<Self::ReceiveMessage>,
|
||||
) -> Result<()> {
|
||||
let interval = self.interval;
|
||||
|
||||
let client = context.client::<clients::sysinfo::Client>();
|
||||
|
||||
let format_tokens = self
|
||||
.format
|
||||
.iter()
|
||||
.map(|format| parser::parse_input(format.as_str()))
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
for (i, token_set) in format_tokens.iter().enumerate() {
|
||||
let rendered = Part::render_all(token_set, &client, interval);
|
||||
context.tx.send_update_spawn((i, rendered));
|
||||
}
|
||||
|
||||
let (refresh_tx, mut refresh_rx) = mpsc::channel(16);
|
||||
|
||||
macro_rules! spawn_refresh {
|
||||
($refresh_type:expr, $func:ident) => {{
|
||||
let tx = refresh_tx.clone();
|
||||
spawn(async move {
|
||||
loop {
|
||||
tx.send_expect($refresh_type).await;
|
||||
sleep(Duration::from_secs(interval.$func())).await;
|
||||
}
|
||||
});
|
||||
}};
|
||||
}
|
||||
|
||||
spawn_refresh!(RefreshType::Memory, memory);
|
||||
spawn_refresh!(RefreshType::Cpu, cpu);
|
||||
spawn_refresh!(RefreshType::Temps, temps);
|
||||
spawn_refresh!(RefreshType::Disks, disks);
|
||||
spawn_refresh!(RefreshType::Network, networks);
|
||||
spawn_refresh!(RefreshType::System, system);
|
||||
|
||||
let tx = context.tx.clone();
|
||||
spawn(async move {
|
||||
while let Some(refresh) = refresh_rx.recv().await {
|
||||
match refresh {
|
||||
RefreshType::Memory => client.refresh_memory(),
|
||||
RefreshType::Cpu => client.refresh_cpu(),
|
||||
RefreshType::Temps => client.refresh_temps(),
|
||||
RefreshType::Disks => client.refresh_disks(),
|
||||
RefreshType::Network => client.refresh_network(),
|
||||
RefreshType::System => client.refresh_load_average(),
|
||||
}
|
||||
|
||||
for (i, token_set) in format_tokens.iter().enumerate() {
|
||||
let is_affected = token_set
|
||||
.iter()
|
||||
.filter_map(|part| {
|
||||
if let Part::Token(token) = part {
|
||||
Some(token)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.any(|t| t.token.is_affected_by(refresh));
|
||||
|
||||
if is_affected {
|
||||
let rendered = Part::render_all(token_set, &client, interval);
|
||||
tx.send_update((i, rendered)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn into_widget(
|
||||
self,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> Result<ModuleParts<gtk::Box>> {
|
||||
let layout = match self.direction {
|
||||
Some(orientation) => orientation.into(),
|
||||
None => self.layout.orientation(info),
|
||||
};
|
||||
|
||||
let container = gtk::Box::new(layout, 10);
|
||||
|
||||
let mut labels = Vec::new();
|
||||
|
||||
for _ in &self.format {
|
||||
let label = Label::builder()
|
||||
.use_markup(true)
|
||||
.angle(self.layout.angle(info))
|
||||
.justify(self.layout.justify.into())
|
||||
.build();
|
||||
|
||||
label.add_class("item");
|
||||
|
||||
container.add(&label);
|
||||
labels.push(label);
|
||||
}
|
||||
|
||||
context.subscribe().recv_glib((), move |(), data| {
|
||||
let label = &labels[data.0];
|
||||
label.set_label_escaped(&data.1);
|
||||
});
|
||||
|
||||
Ok(ModuleParts {
|
||||
widget: container,
|
||||
popup: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
404
src/modules/sysinfo/parser.rs
Normal file
404
src/modules/sysinfo/parser.rs
Normal file
|
|
@ -0,0 +1,404 @@
|
|||
use crate::clients::sysinfo::{Function, Prefix, TokenType};
|
||||
use crate::modules::sysinfo::token::{Alignment, Formatting, Part, Token};
|
||||
use color_eyre::{Report, Result};
|
||||
use std::iter::Peekable;
|
||||
use std::str::{Chars, FromStr};
|
||||
|
||||
impl Function {
|
||||
pub(crate) fn default_for(token_type: TokenType) -> Self {
|
||||
match token_type {
|
||||
TokenType::CpuFrequency
|
||||
| TokenType::CpuPercent
|
||||
| TokenType::TempC
|
||||
| TokenType::DiskPercent => Self::Mean,
|
||||
TokenType::DiskFree
|
||||
| TokenType::DiskTotal
|
||||
| TokenType::DiskUsed
|
||||
| TokenType::DiskRead
|
||||
| TokenType::DiskWrite
|
||||
| TokenType::NetDown
|
||||
| TokenType::NetUp => Self::Sum,
|
||||
_ => Self::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Prefix {
|
||||
pub(crate) fn default_for(token_type: TokenType) -> Self {
|
||||
match token_type {
|
||||
TokenType::CpuFrequency
|
||||
| TokenType::MemoryFree
|
||||
| TokenType::MemoryAvailable
|
||||
| TokenType::MemoryTotal
|
||||
| TokenType::MemoryUsed
|
||||
| TokenType::SwapFree
|
||||
| TokenType::SwapTotal
|
||||
| TokenType::SwapUsed
|
||||
| TokenType::DiskFree
|
||||
| TokenType::DiskTotal
|
||||
| TokenType::DiskUsed => Self::Giga,
|
||||
TokenType::DiskRead | TokenType::DiskWrite => Self::Mega,
|
||||
TokenType::NetDown | TokenType::NetUp => Self::MegaBit,
|
||||
_ => Self::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Prefix {
|
||||
type Err = Report;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
match s {
|
||||
"k" => Ok(Prefix::Kilo),
|
||||
"M" => Ok(Prefix::Mega),
|
||||
"G" => Ok(Prefix::Giga),
|
||||
"T" => Ok(Prefix::Tera),
|
||||
"P" => Ok(Prefix::Peta),
|
||||
|
||||
"ki" => Ok(Prefix::Kibi),
|
||||
"Mi" => Ok(Prefix::Mebi),
|
||||
"Gi" => Ok(Prefix::Gibi),
|
||||
"Ti" => Ok(Prefix::Tebi),
|
||||
"Pi" => Ok(Prefix::Pebi),
|
||||
|
||||
"kb" => Ok(Prefix::KiloBit),
|
||||
"Mb" => Ok(Prefix::MegaBit),
|
||||
"Gb" => Ok(Prefix::GigaBit),
|
||||
|
||||
_ => Err(Report::msg(format!("invalid prefix: {s}"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<char> for Alignment {
|
||||
type Error = Report;
|
||||
|
||||
fn try_from(value: char) -> Result<Self> {
|
||||
match value {
|
||||
'<' => Ok(Self::Left),
|
||||
'^' => Ok(Self::Center),
|
||||
'>' => Ok(Self::Right),
|
||||
_ => Err(Report::msg(format!("Unknown alignment: {value}"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Formatting {
|
||||
fn default_for(token_type: TokenType) -> Self {
|
||||
match token_type {
|
||||
TokenType::CpuFrequency
|
||||
| TokenType::LoadAverage1
|
||||
| TokenType::LoadAverage5
|
||||
| TokenType::LoadAverage15 => Self {
|
||||
width: 0,
|
||||
fill: '0',
|
||||
align: Alignment::default(),
|
||||
precision: 2,
|
||||
},
|
||||
TokenType::CpuPercent => Self {
|
||||
width: 2,
|
||||
fill: '0',
|
||||
align: Alignment::default(),
|
||||
precision: 0,
|
||||
},
|
||||
TokenType::MemoryFree
|
||||
| TokenType::MemoryAvailable
|
||||
| TokenType::MemoryTotal
|
||||
| TokenType::MemoryUsed
|
||||
| TokenType::MemoryPercent
|
||||
| TokenType::SwapFree
|
||||
| TokenType::SwapTotal
|
||||
| TokenType::SwapUsed
|
||||
| TokenType::SwapPercent => Self {
|
||||
width: 4,
|
||||
fill: '0',
|
||||
align: Alignment::default(),
|
||||
precision: 1,
|
||||
},
|
||||
_ => Self {
|
||||
width: 0,
|
||||
fill: '0',
|
||||
align: Alignment::default(),
|
||||
precision: 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_input(input: &str) -> Result<Vec<Part>> {
|
||||
let mut tokens = vec![];
|
||||
|
||||
let mut chars = input.chars().peekable();
|
||||
|
||||
let mut next_char = chars.peek().copied();
|
||||
while let Some(char) = next_char {
|
||||
let token = if char == '{' {
|
||||
chars.next();
|
||||
parse_dynamic(&mut chars)?
|
||||
} else {
|
||||
parse_static(&mut chars)
|
||||
};
|
||||
|
||||
tokens.push(token);
|
||||
next_char = chars.peek().copied();
|
||||
}
|
||||
|
||||
Ok(tokens)
|
||||
}
|
||||
|
||||
fn parse_static(chars: &mut Peekable<Chars>) -> Part {
|
||||
let mut str = String::new();
|
||||
|
||||
let mut next_char = chars.next_if(|&c| c != '{');
|
||||
while let Some(char) = next_char {
|
||||
if char == '{' {
|
||||
break;
|
||||
}
|
||||
|
||||
str.push(char);
|
||||
next_char = chars.next_if(|&c| c != '{');
|
||||
}
|
||||
|
||||
Part::Static(str)
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
enum DynamicMode {
|
||||
Token,
|
||||
Name,
|
||||
Prefix,
|
||||
}
|
||||
|
||||
fn parse_dynamic(chars: &mut Peekable<Chars>) -> Result<Part> {
|
||||
let mut mode = DynamicMode::Token;
|
||||
|
||||
let mut token_str = String::new();
|
||||
let mut func_str = String::new();
|
||||
let mut prefix_str = String::new();
|
||||
|
||||
// we don't want to peek here as that would be the same char as the outer loop
|
||||
let mut next_char = chars.next();
|
||||
while let Some(char) = next_char {
|
||||
match char {
|
||||
'}' | ':' => break,
|
||||
'@' => mode = DynamicMode::Name,
|
||||
'#' => mode = DynamicMode::Prefix,
|
||||
_ => match mode {
|
||||
DynamicMode::Token => token_str.push(char),
|
||||
DynamicMode::Name => func_str.push(char),
|
||||
DynamicMode::Prefix => prefix_str.push(char),
|
||||
},
|
||||
}
|
||||
|
||||
next_char = chars.next();
|
||||
}
|
||||
|
||||
let token_type = token_str.parse()?;
|
||||
let mut formatting = Formatting::default_for(token_type);
|
||||
|
||||
if next_char == Some(':') {
|
||||
formatting = parse_formatting(chars, formatting)?;
|
||||
}
|
||||
|
||||
let token = Token {
|
||||
token: token_type,
|
||||
function: func_str
|
||||
.parse()
|
||||
.unwrap_or_else(|()| Function::default_for(token_type)),
|
||||
prefix: prefix_str
|
||||
.parse()
|
||||
.unwrap_or_else(|_| Prefix::default_for(token_type)),
|
||||
formatting,
|
||||
};
|
||||
|
||||
Ok(Part::Token(token))
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
enum FormattingMode {
|
||||
WidthFillAlign,
|
||||
Precision,
|
||||
}
|
||||
|
||||
fn parse_formatting(chars: &mut Peekable<Chars>, mut formatting: Formatting) -> Result<Formatting> {
|
||||
let mut width_string = String::new();
|
||||
let mut precision_string = String::new();
|
||||
|
||||
let mut mode = FormattingMode::WidthFillAlign;
|
||||
|
||||
let mut next_char = chars.next();
|
||||
while let Some(char) = next_char {
|
||||
match (char, mode) {
|
||||
('}', _) => break,
|
||||
('.', _) => mode = FormattingMode::Precision,
|
||||
(_, FormattingMode::Precision) => precision_string.push(char),
|
||||
('1'..='9', FormattingMode::WidthFillAlign) => width_string.push(char),
|
||||
('<' | '^' | '>', FormattingMode::WidthFillAlign) => {
|
||||
formatting.align = Alignment::try_from(char)?;
|
||||
}
|
||||
(_, FormattingMode::WidthFillAlign) => formatting.fill = char,
|
||||
}
|
||||
|
||||
next_char = chars.next();
|
||||
}
|
||||
|
||||
if !width_string.is_empty() {
|
||||
formatting.width = width_string.parse()?;
|
||||
}
|
||||
|
||||
if !precision_string.is_empty() {
|
||||
formatting.precision = precision_string.parse()?;
|
||||
}
|
||||
|
||||
Ok(formatting)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn static_only() {
|
||||
let tokens = parse_input("hello world").unwrap();
|
||||
println!("{tokens:?}");
|
||||
|
||||
assert_eq!(tokens.len(), 1);
|
||||
assert!(matches!(&tokens[0], Part::Static(str) if str == "hello world"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic() {
|
||||
let tokens = parse_input("{cpu_frequency}").unwrap();
|
||||
println!("{tokens:?}");
|
||||
|
||||
assert_eq!(tokens.len(), 1);
|
||||
|
||||
assert!(matches!(&tokens[0], Part::Token(_)));
|
||||
let Part::Token(token) = tokens.get(0).unwrap() else {
|
||||
return;
|
||||
};
|
||||
|
||||
assert_eq!(token.token, TokenType::CpuFrequency);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn named() {
|
||||
let tokens = parse_input("{cpu_frequency@cpu0}").unwrap();
|
||||
println!("{tokens:?}");
|
||||
|
||||
assert_eq!(tokens.len(), 1);
|
||||
|
||||
assert!(matches!(&tokens[0], Part::Token(_)));
|
||||
let Part::Token(token) = tokens.get(0).unwrap() else {
|
||||
return;
|
||||
};
|
||||
|
||||
assert_eq!(token.token, TokenType::CpuFrequency);
|
||||
assert!(matches!(&token.function, Function::Name(n) if n == "cpu0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conversion() {
|
||||
let tokens = parse_input("{cpu_frequency#G}").unwrap();
|
||||
println!("{tokens:?}");
|
||||
|
||||
assert_eq!(tokens.len(), 1);
|
||||
|
||||
assert!(matches!(&tokens[0], Part::Token(_)));
|
||||
let Part::Token(token) = tokens.get(0).unwrap() else {
|
||||
return;
|
||||
};
|
||||
|
||||
assert_eq!(token.token, TokenType::CpuFrequency);
|
||||
assert_eq!(token.prefix, Prefix::Giga);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formatting_basic() {
|
||||
let tokens = parse_input("{cpu_frequency:.2}").unwrap();
|
||||
println!("{tokens:?}");
|
||||
|
||||
assert_eq!(tokens.len(), 1);
|
||||
|
||||
assert!(matches!(&tokens[0], Part::Token(_)));
|
||||
let Part::Token(token) = tokens.get(0).unwrap() else {
|
||||
return;
|
||||
};
|
||||
|
||||
assert_eq!(token.token, TokenType::CpuFrequency);
|
||||
assert_eq!(token.formatting.precision, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formatting_complex() {
|
||||
let tokens = parse_input("{cpu_frequency:0<5.2}").unwrap();
|
||||
println!("{tokens:?}");
|
||||
|
||||
assert_eq!(tokens.len(), 1);
|
||||
|
||||
assert!(matches!(&tokens[0], Part::Token(_)));
|
||||
let Part::Token(token) = tokens.get(0).unwrap() else {
|
||||
return;
|
||||
};
|
||||
|
||||
assert_eq!(token.token, TokenType::CpuFrequency);
|
||||
assert_eq!(token.formatting.fill, '0');
|
||||
assert_eq!(token.formatting.align, Alignment::Left);
|
||||
assert_eq!(token.formatting.width, 5);
|
||||
assert_eq!(token.formatting.precision, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complex() {
|
||||
let tokens = parse_input("{cpu_frequency@cpu0#G:.2}").unwrap();
|
||||
println!("{tokens:?}");
|
||||
|
||||
assert_eq!(tokens.len(), 1);
|
||||
|
||||
assert!(matches!(&tokens[0], Part::Token(_)));
|
||||
let Part::Token(token) = tokens.get(0).unwrap() else {
|
||||
return;
|
||||
};
|
||||
|
||||
assert_eq!(token.token, TokenType::CpuFrequency);
|
||||
assert!(matches!(&token.function, Function::Name(n) if n == "cpu0"));
|
||||
assert_eq!(token.prefix, Prefix::Giga);
|
||||
assert_eq!(token.formatting.precision, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn static_then_token() {
|
||||
let tokens = parse_input("Freq: {cpu_frequency#G:.2}").unwrap();
|
||||
println!("{tokens:?}");
|
||||
|
||||
assert_eq!(tokens.len(), 2);
|
||||
|
||||
assert!(matches!(&tokens[0], Part::Static(str) if str == "Freq: "));
|
||||
|
||||
assert!(matches!(&tokens[1], Part::Token(_)));
|
||||
let Part::Token(token) = tokens.get(1).unwrap() else {
|
||||
return;
|
||||
};
|
||||
|
||||
assert_eq!(token.token, TokenType::CpuFrequency);
|
||||
assert_eq!(token.formatting.precision, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn token_then_static() {
|
||||
let tokens = parse_input("{cpu_frequency#G:.2} GHz").unwrap();
|
||||
println!("{tokens:?}");
|
||||
|
||||
assert_eq!(tokens.len(), 2);
|
||||
|
||||
assert!(matches!(&tokens[0], Part::Token(_)));
|
||||
let Part::Token(token) = tokens.get(0).unwrap() else {
|
||||
return;
|
||||
};
|
||||
|
||||
assert_eq!(token.token, TokenType::CpuFrequency);
|
||||
assert_eq!(token.formatting.precision, 2);
|
||||
|
||||
assert!(matches!(&tokens[1], Part::Static(str) if str == " GHz"));
|
||||
}
|
||||
}
|
||||
91
src/modules/sysinfo/renderer.rs
Normal file
91
src/modules/sysinfo/renderer.rs
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
use super::Interval;
|
||||
use super::token::{Alignment, Part, Token};
|
||||
use crate::clients;
|
||||
use crate::clients::sysinfo::{TokenType, Value, ValueSet};
|
||||
|
||||
pub enum TokenValue {
|
||||
Number(f64),
|
||||
String(String),
|
||||
}
|
||||
|
||||
impl Part {
|
||||
pub fn render_all(
|
||||
tokens: &[Self],
|
||||
client: &clients::sysinfo::Client,
|
||||
interval: Interval,
|
||||
) -> String {
|
||||
tokens
|
||||
.iter()
|
||||
.map(|part| part.render(client, interval))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn render(&self, client: &clients::sysinfo::Client, interval: Interval) -> String {
|
||||
match self {
|
||||
Part::Static(str) => str.clone(),
|
||||
Part::Token(token) => {
|
||||
match token.get(client, interval) {
|
||||
TokenValue::Number(value) => {
|
||||
let fmt = token.formatting;
|
||||
let mut str = format!("{value:.precision$}", precision = fmt.precision);
|
||||
|
||||
// fill/align doesn't support parameterization so we need our own impl
|
||||
let mut add_to_end = fmt.align == Alignment::Right;
|
||||
while str.len() < fmt.width {
|
||||
if add_to_end {
|
||||
str.push(fmt.fill);
|
||||
} else {
|
||||
str.insert(0, fmt.fill);
|
||||
}
|
||||
|
||||
if fmt.align == Alignment::Center {
|
||||
add_to_end = !add_to_end;
|
||||
}
|
||||
}
|
||||
|
||||
str
|
||||
}
|
||||
TokenValue::String(value) => value,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Token {
|
||||
pub fn get(&self, client: &clients::sysinfo::Client, interval: Interval) -> TokenValue {
|
||||
let get = |value: Value| TokenValue::Number(value.get(self.prefix));
|
||||
let apply = |set: ValueSet| TokenValue::Number(set.apply(&self.function, self.prefix));
|
||||
|
||||
match self.token {
|
||||
// Number tokens
|
||||
TokenType::CpuFrequency => apply(client.cpu_frequency()),
|
||||
TokenType::CpuPercent => apply(client.cpu_percent()),
|
||||
TokenType::MemoryFree => get(client.memory_free()),
|
||||
TokenType::MemoryAvailable => get(client.memory_available()),
|
||||
TokenType::MemoryTotal => get(client.memory_total()),
|
||||
TokenType::MemoryUsed => get(client.memory_used()),
|
||||
TokenType::MemoryPercent => get(client.memory_percent()),
|
||||
TokenType::SwapFree => get(client.swap_free()),
|
||||
TokenType::SwapTotal => get(client.swap_total()),
|
||||
TokenType::SwapUsed => get(client.swap_used()),
|
||||
TokenType::SwapPercent => get(client.swap_percent()),
|
||||
TokenType::TempC => apply(client.temp_c()),
|
||||
TokenType::TempF => apply(client.temp_f()),
|
||||
TokenType::DiskFree => apply(client.disk_free()),
|
||||
TokenType::DiskTotal => apply(client.disk_total()),
|
||||
TokenType::DiskUsed => apply(client.disk_used()),
|
||||
TokenType::DiskPercent => apply(client.disk_percent()),
|
||||
TokenType::DiskRead => apply(client.disk_read(interval)),
|
||||
TokenType::DiskWrite => apply(client.disk_write(interval)),
|
||||
TokenType::NetDown => apply(client.net_down(interval)),
|
||||
TokenType::NetUp => apply(client.net_up(interval)),
|
||||
TokenType::LoadAverage1 => get(client.load_average_1()),
|
||||
TokenType::LoadAverage5 => get(client.load_average_5()),
|
||||
TokenType::LoadAverage15 => get(client.load_average_15()),
|
||||
|
||||
// String tokens
|
||||
TokenType::Uptime => TokenValue::String(clients::sysinfo::Client::uptime()),
|
||||
}
|
||||
}
|
||||
}
|
||||
31
src/modules/sysinfo/token.rs
Normal file
31
src/modules/sysinfo/token.rs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
use crate::clients::sysinfo::{Function, Prefix, TokenType};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Token {
|
||||
pub token: TokenType,
|
||||
pub function: Function,
|
||||
pub prefix: Prefix,
|
||||
pub formatting: Formatting,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Part {
|
||||
Static(String),
|
||||
Token(Token),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Formatting {
|
||||
pub width: usize,
|
||||
pub fill: char,
|
||||
pub align: Alignment,
|
||||
pub precision: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub enum Alignment {
|
||||
#[default]
|
||||
Left,
|
||||
Center,
|
||||
Right,
|
||||
}
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
use system_tray::menu::{MenuItem, ToggleState};
|
||||
|
||||
/// Diff change type and associated info.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Diff {
|
||||
Add(MenuItem),
|
||||
Update(i32, MenuItemDiff),
|
||||
Remove(i32),
|
||||
}
|
||||
|
||||
/// Diff info to be applied to an existing menu item as an update.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MenuItemDiff {
|
||||
/// Text of the item,
|
||||
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.
|
||||
pub visible: Option<bool>,
|
||||
/// Icon name of the item, following the freedesktop.org icon spec.
|
||||
// pub icon_name: Option<Option<String>>,
|
||||
/// Describe the current state of a "togglable" item. Can be one of:
|
||||
/// - Some(true): on
|
||||
/// - Some(false): off
|
||||
/// - None: indeterminate
|
||||
pub toggle_state: Option<ToggleState>,
|
||||
/// A submenu for this item, typically this would ve revealed to the user by hovering the current item
|
||||
pub submenu: Vec<Diff>,
|
||||
}
|
||||
|
||||
impl MenuItemDiff {
|
||||
fn new(old: &MenuItem, new: &MenuItem) -> Self {
|
||||
macro_rules! diff {
|
||||
($field:ident) => {
|
||||
if old.$field == new.$field {
|
||||
None
|
||||
} else {
|
||||
Some(new.$field)
|
||||
}
|
||||
};
|
||||
|
||||
(&$field:ident) => {
|
||||
if &old.$field == &new.$field {
|
||||
None
|
||||
} else {
|
||||
Some(new.$field.clone())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Self {
|
||||
label: diff!(&label),
|
||||
enabled: diff!(enabled),
|
||||
visible: diff!(visible),
|
||||
// icon_name: diff!(&icon_name),
|
||||
toggle_state: diff!(toggle_state),
|
||||
submenu: get_diffs(&old.submenu, &new.submenu),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this diff contains any changes
|
||||
fn has_diff(&self) -> bool {
|
||||
self.label.is_some()
|
||||
|| self.enabled.is_some()
|
||||
|| self.visible.is_some()
|
||||
// || self.icon_name.is_some()
|
||||
|| self.toggle_state.is_some()
|
||||
|| !self.submenu.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets a diff set between old and new state.
|
||||
pub fn get_diffs(old: &[MenuItem], new: &[MenuItem]) -> Vec<Diff> {
|
||||
let mut diffs = vec![];
|
||||
|
||||
for new_item in new {
|
||||
let old_item = old.iter().find(|&item| item.id == new_item.id);
|
||||
|
||||
let diff = match old_item {
|
||||
Some(old_item) => {
|
||||
let item_diff = MenuItemDiff::new(old_item, new_item);
|
||||
if item_diff.has_diff() {
|
||||
Some(Diff::Update(old_item.id, item_diff))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
None => Some(Diff::Add(new_item.clone())),
|
||||
};
|
||||
|
||||
if let Some(diff) = diff {
|
||||
diffs.push(diff);
|
||||
}
|
||||
}
|
||||
|
||||
for old_item in old {
|
||||
let new_item = new.iter().find(|&item| item.id == old_item.id);
|
||||
if new_item.is_none() {
|
||||
diffs.push(Diff::Remove(old_item.id));
|
||||
}
|
||||
}
|
||||
|
||||
diffs
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
use crate::image::ImageProvider;
|
||||
use crate::image::create_and_load_surface;
|
||||
use crate::modules::tray::interface::TrayMenu;
|
||||
use color_eyre::{Report, Result};
|
||||
use glib::ffi::g_strfreev;
|
||||
|
|
@ -40,21 +40,21 @@ fn get_icon_theme_search_paths(icon_theme: &IconTheme) -> HashSet<String> {
|
|||
|
||||
pub fn get_image(
|
||||
item: &TrayMenu,
|
||||
icon_theme: &IconTheme,
|
||||
size: u32,
|
||||
prefer_icons: bool,
|
||||
icon_theme: &IconTheme,
|
||||
) -> 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)
|
||||
get_image_from_icon_name(item, size, icon_theme)
|
||||
.or_else(|_| get_image_from_pixmap(item, size))
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to get a GTK `Image` component
|
||||
/// for the status notifier item's icon.
|
||||
fn get_image_from_icon_name(item: &TrayMenu, icon_theme: &IconTheme, size: u32) -> Result<Image> {
|
||||
fn get_image_from_icon_name(item: &TrayMenu, size: u32, icon_theme: &IconTheme) -> 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);
|
||||
|
|
@ -68,7 +68,7 @@ fn get_image_from_icon_name(item: &TrayMenu, icon_theme: &IconTheme, size: u32)
|
|||
if let Some(icon_info) = icon_info {
|
||||
let pixbuf = icon_info.load_icon()?;
|
||||
let image = Image::new();
|
||||
ImageProvider::create_and_load_surface(&pixbuf, &image)?;
|
||||
create_and_load_surface(&pixbuf, &image)?;
|
||||
Ok(image)
|
||||
} else {
|
||||
Err(Report::msg("could not find icon"))
|
||||
|
|
@ -90,7 +90,11 @@ fn get_image_from_pixmap(item: &TrayMenu, size: u32) -> Result<Image> {
|
|||
.and_then(|pixmap| pixmap.first())
|
||||
.ok_or_else(|| Report::msg("Failed to get pixmap from tray icon"))?;
|
||||
|
||||
let mut pixels = pixmap.pixels.to_vec();
|
||||
if pixmap.width == 0 || pixmap.height == 0 {
|
||||
return Err(Report::msg("empty pixmap"));
|
||||
}
|
||||
|
||||
let mut pixels = pixmap.pixels.clone();
|
||||
|
||||
for i in (0..pixels.len()).step_by(4) {
|
||||
let alpha = pixels[i];
|
||||
|
|
@ -118,6 +122,6 @@ fn get_image_from_pixmap(item: &TrayMenu, size: u32) -> Result<Image> {
|
|||
.unwrap_or(pixbuf);
|
||||
|
||||
let image = Image::new();
|
||||
ImageProvider::create_and_load_surface(&pixbuf, &image)?;
|
||||
create_and_load_surface(&pixbuf, &image)?;
|
||||
Ok(image)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,114 +1,51 @@
|
|||
use super::diff::{Diff, MenuItemDiff};
|
||||
use crate::{spawn, try_send};
|
||||
use glib::Propagation;
|
||||
use glib::{Propagation, SignalHandlerId};
|
||||
use gtk::gdk::Gravity;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{CheckMenuItem, Image, Label, Menu, MenuItem, SeparatorMenuItem};
|
||||
use std::collections::HashMap;
|
||||
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,
|
||||
/// passing in a single argument.
|
||||
///
|
||||
/// This is useful to avoid matching on
|
||||
/// `TrayMenuWidget` constantly.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// call!(container, add, my_widget)
|
||||
/// ```
|
||||
/// is the same as:
|
||||
/// ```
|
||||
/// match &my_widget {
|
||||
/// TrayMenuWidget::Separator(w) => {
|
||||
/// container.add(w);
|
||||
/// }
|
||||
/// TrayMenuWidget::Standard(w) => {
|
||||
/// container.add(w);
|
||||
/// }
|
||||
/// TrayMenuWidget::Checkbox(w) => {
|
||||
/// container.add(w);
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
macro_rules! call {
|
||||
($parent:expr, $method:ident, $child:expr) => {
|
||||
match &$child {
|
||||
TrayMenuWidget::Separator(w) => {
|
||||
$parent.$method(w);
|
||||
}
|
||||
TrayMenuWidget::Standard(w) => {
|
||||
$parent.$method(w);
|
||||
}
|
||||
TrayMenuWidget::Checkbox(w) => {
|
||||
$parent.$method(w);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
use gtk::{EventBox, Image, Label, MenuItem};
|
||||
use system_tray::item::{IconPixmap, StatusNotifierItem, Tooltip};
|
||||
|
||||
/// Main tray icon to show on the bar
|
||||
pub(crate) struct TrayMenu {
|
||||
pub widget: MenuItem,
|
||||
menu_widget: Menu,
|
||||
pub event_box: EventBox,
|
||||
button_handler: Option<SignalHandlerId>,
|
||||
widget: MenuItem,
|
||||
image_widget: Option<Image>,
|
||||
label_widget: Option<Label>,
|
||||
|
||||
menu: HashMap<i32, TrayMenuItem>,
|
||||
state: Vec<MenuItemInfo>,
|
||||
|
||||
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<ActivateRequest>,
|
||||
address: String,
|
||||
item: StatusNotifierItem,
|
||||
) -> Self {
|
||||
pub fn new(address: &str, item: StatusNotifierItem) -> Self {
|
||||
let event_box = EventBox::new();
|
||||
|
||||
let widget = MenuItem::new();
|
||||
widget.style_context().add_class("item");
|
||||
event_box.add(&widget);
|
||||
|
||||
let (item_tx, mut item_rx) = mpsc::channel(8);
|
||||
event_box.show_all();
|
||||
|
||||
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));
|
||||
|
||||
Self {
|
||||
let mut slf = Self {
|
||||
event_box,
|
||||
button_handler: None,
|
||||
widget,
|
||||
menu_widget: menu,
|
||||
image_widget: None,
|
||||
label_widget: None,
|
||||
state: vec![],
|
||||
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,
|
||||
};
|
||||
|
||||
if let Some(menu) = item.menu {
|
||||
let menu = system_tray::gtk_menu::Menu::new(address, &menu);
|
||||
slf.set_menu_widget(menu);
|
||||
}
|
||||
|
||||
slf
|
||||
}
|
||||
|
||||
/// Updates the label text, and shows it in favour of the image.
|
||||
|
|
@ -153,42 +90,10 @@ impl TrayMenu {
|
|||
image.show();
|
||||
}
|
||||
|
||||
/// Applies a diff set to the submenu.
|
||||
pub fn apply_diffs(&mut self, diffs: Vec<Diff>) {
|
||||
for diff in diffs {
|
||||
match diff {
|
||||
Diff::Add(info) => {
|
||||
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) {
|
||||
item.apply_diff(info);
|
||||
}
|
||||
}
|
||||
Diff::Remove(id) => {
|
||||
if let Some(item) = self.menu.remove(&id) {
|
||||
call!(self.menu_widget, remove, item.widget);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn label_widget(&self) -> Option<&Label> {
|
||||
self.label_widget.as_ref()
|
||||
}
|
||||
|
||||
pub fn state(&self) -> &[MenuItemInfo] {
|
||||
&self.state
|
||||
}
|
||||
|
||||
pub fn set_state(&mut self, state: Vec<MenuItemInfo>) {
|
||||
self.state = state;
|
||||
}
|
||||
|
||||
pub fn icon_name(&self) -> Option<&String> {
|
||||
self.icon_name.as_ref()
|
||||
}
|
||||
|
|
@ -196,173 +101,28 @@ impl TrayMenu {
|
|||
pub fn set_icon_name(&mut self, icon_name: Option<String>) {
|
||||
self.icon_name = icon_name;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TrayMenuItem {
|
||||
id: i32,
|
||||
widget: TrayMenuWidget,
|
||||
menu_widget: Menu,
|
||||
submenu: HashMap<i32, TrayMenuItem>,
|
||||
tx: mpsc::Sender<i32>,
|
||||
}
|
||||
pub fn set_tooltip(&self, tooltip: Option<Tooltip>) {
|
||||
let title = tooltip.map(|t| t.title);
|
||||
|
||||
#[derive(Debug)]
|
||||
enum TrayMenuWidget {
|
||||
Separator(SeparatorMenuItem),
|
||||
Standard(MenuItem),
|
||||
Checkbox(CheckMenuItem),
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
};
|
||||
if let Some(widget) = &self.image_widget {
|
||||
widget.set_tooltip_text(title.as_deref());
|
||||
}
|
||||
|
||||
let widget = match (info.menu_type, info.toggle_type) {
|
||||
(MenuType::Separator, _) => TrayMenuWidget::Separator(SeparatorMenuItem::new()),
|
||||
(MenuType::Standard, ToggleType::Checkmark) => {
|
||||
let widget = CheckMenuItem::builder()
|
||||
.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_button_press_event(move |_item, _button| {
|
||||
try_send!(tx, id);
|
||||
Propagation::Proceed
|
||||
});
|
||||
}
|
||||
|
||||
TrayMenuWidget::Checkbox(widget)
|
||||
}
|
||||
(MenuType::Standard, _) => {
|
||||
let widget = MenuItem::builder()
|
||||
.visible(info.visible)
|
||||
.sensitive(info.enabled)
|
||||
.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| {
|
||||
try_send!(tx, id);
|
||||
});
|
||||
}
|
||||
|
||||
TrayMenuWidget::Standard(widget)
|
||||
}
|
||||
};
|
||||
|
||||
Self {
|
||||
id: info.id,
|
||||
widget,
|
||||
menu_widget: menu,
|
||||
submenu,
|
||||
tx,
|
||||
if let Some(widget) = &self.label_widget {
|
||||
widget.set_tooltip_text(title.as_deref());
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies a diff to this submenu item.
|
||||
///
|
||||
/// This is called recursively,
|
||||
/// 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),
|
||||
TrayMenuWidget::Checkbox(widget) => widget.set_label(&label),
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Image support
|
||||
// if let Some(icon_name) = diff.icon_name {
|
||||
//
|
||||
// }
|
||||
|
||||
if let Some(enabled) = diff.enabled {
|
||||
match &self.widget {
|
||||
TrayMenuWidget::Separator(widget) => widget.set_sensitive(enabled),
|
||||
TrayMenuWidget::Standard(widget) => widget.set_sensitive(enabled),
|
||||
TrayMenuWidget::Checkbox(widget) => widget.set_sensitive(enabled),
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(visible) = diff.visible {
|
||||
match &self.widget {
|
||||
TrayMenuWidget::Separator(widget) => widget.set_visible(visible),
|
||||
TrayMenuWidget::Standard(widget) => widget.set_visible(visible),
|
||||
TrayMenuWidget::Checkbox(widget) => widget.set_visible(visible),
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(toggle_state) = diff.toggle_state {
|
||||
if let TrayMenuWidget::Checkbox(widget) = &self.widget {
|
||||
widget.set_active(toggle_state == ToggleState::On);
|
||||
}
|
||||
}
|
||||
|
||||
for sub_diff in diff.submenu {
|
||||
match sub_diff {
|
||||
Diff::Add(info) => {
|
||||
let menu_item = TrayMenuItem::new(&info, self.tx.clone());
|
||||
call!(self.menu_widget, add, menu_item.widget);
|
||||
|
||||
if let TrayMenuWidget::Standard(widget) = &self.widget {
|
||||
widget.set_submenu(Some(&self.menu_widget));
|
||||
}
|
||||
|
||||
self.submenu.insert(menu_item.id, menu_item);
|
||||
}
|
||||
Diff::Update(id, diff) => {
|
||||
if let Some(sub) = self.submenu.get_mut(&id) {
|
||||
sub.apply_diff(diff);
|
||||
}
|
||||
}
|
||||
Diff::Remove(id) => {
|
||||
if let Some(sub) = self.submenu.remove(&id) {
|
||||
call!(self.menu_widget, remove, sub.widget);
|
||||
}
|
||||
if let TrayMenuWidget::Standard(widget) = &self.widget {
|
||||
if self.submenu.is_empty() {
|
||||
widget.set_submenu(None::<&Menu>);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn set_menu_widget(&mut self, menu: system_tray::gtk_menu::Menu) {
|
||||
let button_handler = self
|
||||
.event_box
|
||||
.connect_button_press_event(move |event_box, _event| {
|
||||
menu.popup_at_widget(event_box, Gravity::North, Gravity::South, None);
|
||||
Propagation::Proceed
|
||||
});
|
||||
if let Some(handler) = self.button_handler.replace(button_handler) {
|
||||
self.event_box.disconnect(handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,21 @@
|
|||
mod diff;
|
||||
mod icon;
|
||||
mod interface;
|
||||
|
||||
use crate::channels::{AsyncSenderExt, BroadcastReceiverExt};
|
||||
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, lock, module_impl, send_async, spawn};
|
||||
use crate::config::{CommonConfig, ModuleOrientation};
|
||||
use crate::modules::{Module, ModuleInfo, ModuleParts, WidgetContext};
|
||||
use crate::{lock, module_impl, spawn};
|
||||
use color_eyre::{Report, Result};
|
||||
use gtk::{prelude::*, PackDirection};
|
||||
use gtk::{IconTheme, MenuBar};
|
||||
use gtk::prelude::*;
|
||||
use gtk::{IconTheme, Orientation};
|
||||
use interface::TrayMenu;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use system_tray::client::Event;
|
||||
use system_tray::client::{ActivateRequest, UpdateEvent};
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{debug, error, warn};
|
||||
use tracing::{debug, error, trace, warn};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
|
|
@ -34,14 +33,13 @@ pub struct TrayModule {
|
|||
#[serde(default = "default_icon_size")]
|
||||
icon_size: u32,
|
||||
|
||||
/// Direction to display the tray items.
|
||||
/// The direction in which to pack tray icons.
|
||||
///
|
||||
/// **Valid options**: `top_to_bottom`, `bottom_to_top`, `left_to_right`, `right_to_left`
|
||||
/// **Valid options**: `horizontal`, `vertical`
|
||||
/// <br>
|
||||
/// **Default**: `left_to_right` if bar is horizontal, `top_to_bottom` if bar is vertical
|
||||
#[serde(default, deserialize_with = "deserialize_pack_direction")]
|
||||
#[cfg_attr(feature = "schema", schemars(schema_with = "schema_pack_direction"))]
|
||||
direction: Option<PackDirection>,
|
||||
/// **Default**: `horizontal` for horizontal bars, `vertical` for vertical bars
|
||||
#[serde(default)]
|
||||
direction: Option<ModuleOrientation>,
|
||||
|
||||
/// See [common options](module-level-options#common-options).
|
||||
#[serde(flatten)]
|
||||
|
|
@ -52,36 +50,7 @@ const fn default_icon_size() -> u32 {
|
|||
16
|
||||
}
|
||||
|
||||
fn deserialize_pack_direction<'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 direction")),
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
|
||||
#[cfg(feature = "schema")]
|
||||
fn schema_pack_direction(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
|
||||
use schemars::JsonSchema;
|
||||
let mut schema: schemars::schema::SchemaObject = <String>::json_schema(gen).into();
|
||||
schema.enum_values = Some(vec![
|
||||
"top_to_bottom".into(),
|
||||
"bottom_to_top".into(),
|
||||
"left_to_right".into(),
|
||||
"right_to_left".into(),
|
||||
]);
|
||||
schema.into()
|
||||
}
|
||||
|
||||
impl Module<MenuBar> for TrayModule {
|
||||
impl Module<gtk::Box> for TrayModule {
|
||||
type SendMessage = Event;
|
||||
type ReceiveMessage = ActivateRequest;
|
||||
|
||||
|
|
@ -98,33 +67,33 @@ impl Module<MenuBar> for TrayModule {
|
|||
let client = context.try_client::<tray::Client>()?;
|
||||
let mut tray_rx = client.subscribe();
|
||||
|
||||
let initial_items = lock!(client.items()).clone();
|
||||
let initial_items = {
|
||||
let items = client.items();
|
||||
lock!(items).clone()
|
||||
};
|
||||
|
||||
// listen to tray updates
|
||||
spawn(async move {
|
||||
for (key, (item, menu)) in initial_items {
|
||||
send_async!(
|
||||
tx,
|
||||
ModuleUpdateEvent::Update(Event::Add(key.clone(), item.into()))
|
||||
);
|
||||
tx.send_update(Event::Add(key.clone(), item.into())).await;
|
||||
|
||||
if let Some(menu) = menu.clone() {
|
||||
send_async!(
|
||||
tx,
|
||||
ModuleUpdateEvent::Update(Event::Update(key, UpdateEvent::Menu(menu)))
|
||||
);
|
||||
tx.send_update(Event::Update(key, UpdateEvent::Menu(menu)))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
while let Ok(message) = tray_rx.recv().await {
|
||||
send_async!(tx, ModuleUpdateEvent::Update(message));
|
||||
tx.send_update(message).await;
|
||||
}
|
||||
});
|
||||
|
||||
// send tray commands
|
||||
spawn(async move {
|
||||
while let Some(cmd) = rx.recv().await {
|
||||
client.activate(cmd).await?;
|
||||
if let Err(err) = client.activate(cmd).await {
|
||||
error!("{err:?}");
|
||||
}
|
||||
}
|
||||
|
||||
Ok::<_, Report>(())
|
||||
|
|
@ -137,30 +106,33 @@ impl Module<MenuBar> for TrayModule {
|
|||
self,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> Result<ModuleParts<MenuBar>> {
|
||||
let container = MenuBar::new();
|
||||
) -> Result<ModuleParts<gtk::Box>> {
|
||||
let orientation = self
|
||||
.direction
|
||||
.map_or(info.bar_position.orientation(), Orientation::from);
|
||||
|
||||
let direction = self.direction.unwrap_or(
|
||||
if info.bar_position.orientation() == gtk::Orientation::Vertical {
|
||||
PackDirection::Ttb
|
||||
} else {
|
||||
PackDirection::Ltr
|
||||
},
|
||||
);
|
||||
// We use a `Box` here instead of the (supposedly correct) `MenuBar`
|
||||
// as the latter has issues on Sway with menus focus-stealing from the bar.
|
||||
//
|
||||
// Each widget is wrapped in an EventBox, copying what Waybar does here.
|
||||
let container = gtk::Box::new(orientation, 10);
|
||||
|
||||
container.set_pack_direction(direction);
|
||||
container.set_child_pack_direction(direction);
|
||||
let mut menus = HashMap::new();
|
||||
let icon_theme = context.ironbar.image_provider().icon_theme();
|
||||
|
||||
{
|
||||
let container = container.clone();
|
||||
let mut menus = HashMap::new();
|
||||
let icon_theme = info.icon_theme.clone();
|
||||
|
||||
// listen for UI updates
|
||||
glib_recv!(context.subscribe(), update =>
|
||||
on_update(update, &container, &mut menus, &icon_theme, self.icon_size, self.prefer_theme_icons, &context.controller_tx)
|
||||
);
|
||||
};
|
||||
// listen for UI updates
|
||||
context
|
||||
.subscribe()
|
||||
.recv_glib(&container, move |container, update| {
|
||||
on_update(
|
||||
update,
|
||||
container,
|
||||
&mut menus,
|
||||
&icon_theme,
|
||||
self.icon_size,
|
||||
self.prefer_theme_icons,
|
||||
);
|
||||
});
|
||||
|
||||
Ok(ModuleParts {
|
||||
widget: container,
|
||||
|
|
@ -173,32 +145,32 @@ impl Module<MenuBar> for TrayModule {
|
|||
/// getting the diff since the previous update and applying it to the menu.
|
||||
fn on_update(
|
||||
update: Event,
|
||||
container: &MenuBar,
|
||||
container: >k::Box,
|
||||
menus: &mut HashMap<Box<str>, TrayMenu>,
|
||||
icon_theme: &IconTheme,
|
||||
icon_size: u32,
|
||||
prefer_icons: bool,
|
||||
tx: &mpsc::Sender<ActivateRequest>,
|
||||
) {
|
||||
match update {
|
||||
Event::Add(address, item) => {
|
||||
debug!("Received new tray item at '{address}': {item:?}");
|
||||
|
||||
let mut menu_item = TrayMenu::new(tx.clone(), address.clone(), *item);
|
||||
container.add(&menu_item.widget);
|
||||
let mut menu_item = TrayMenu::new(&address, *item);
|
||||
container.pack_start(&menu_item.event_box, true, true, 0);
|
||||
|
||||
if let Ok(image) = icon::get_image(&menu_item, icon_theme, icon_size, prefer_icons) {
|
||||
if let Ok(image) = icon::get_image(&menu_item, icon_size, prefer_icons, icon_theme) {
|
||||
menu_item.set_image(&image);
|
||||
} else {
|
||||
let label = menu_item.title.clone().unwrap_or(address.clone());
|
||||
menu_item.set_label(&label);
|
||||
};
|
||||
}
|
||||
|
||||
menu_item.widget.show();
|
||||
menu_item.event_box.show();
|
||||
menus.insert(address.into(), menu_item);
|
||||
}
|
||||
Event::Update(address, update) => {
|
||||
debug!("Received tray update for '{address}': {update:?}");
|
||||
debug!("Received tray update for '{address}'");
|
||||
trace!("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");
|
||||
|
|
@ -211,13 +183,12 @@ fn on_update(
|
|||
}
|
||||
UpdateEvent::Icon(icon) => {
|
||||
if icon.as_ref() != menu_item.icon_name() {
|
||||
match icon::get_image(menu_item, icon_theme, icon_size, prefer_icons) {
|
||||
menu_item.set_icon_name(icon);
|
||||
match icon::get_image(menu_item, icon_size, prefer_icons, icon_theme) {
|
||||
Ok(image) => menu_item.set_image(&image),
|
||||
Err(_) => menu_item.show_label(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
menu_item.set_icon_name(icon);
|
||||
}
|
||||
UpdateEvent::OverlayIcon(_icon) => {
|
||||
warn!("received unimplemented NewOverlayIcon event");
|
||||
|
|
@ -230,25 +201,22 @@ fn on_update(
|
|||
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);
|
||||
|
||||
let diffs = get_diffs(menu_item.state(), &menu.submenus);
|
||||
|
||||
menu_item.apply_diffs(diffs);
|
||||
menu_item.set_state(menu.submenus);
|
||||
UpdateEvent::Tooltip(tooltip) => {
|
||||
menu_item.set_tooltip(tooltip);
|
||||
}
|
||||
UpdateEvent::MenuConnect(menu) => {
|
||||
let menu = system_tray::gtk_menu::Menu::new(&address, &menu);
|
||||
menu_item.set_menu_widget(menu);
|
||||
}
|
||||
UpdateEvent::Menu(_) | UpdateEvent::MenuDiff(_) => {}
|
||||
}
|
||||
}
|
||||
Event::Remove(address) => {
|
||||
debug!("Removing tray item at '{address}'");
|
||||
|
||||
if let Some(menu) = menus.get(address.as_str()) {
|
||||
container.remove(&menu.widget);
|
||||
container.remove(&menu.event_box);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,22 @@
|
|||
use color_eyre::Result;
|
||||
use futures_lite::stream::StreamExt;
|
||||
use gtk::{prelude::*, Button};
|
||||
use gtk::{Button, prelude::*};
|
||||
use gtk::{Label, Orientation};
|
||||
use serde::Deserialize;
|
||||
use tokio::sync::{broadcast, mpsc};
|
||||
use upower_dbus::BatteryState;
|
||||
use std::fmt::Write;
|
||||
use tokio::sync::mpsc;
|
||||
use zbus;
|
||||
use zbus::fdo::PropertiesProxy;
|
||||
|
||||
use crate::config::CommonConfig;
|
||||
use crate::gtk_helpers::IronbarGtkExt;
|
||||
use crate::image::ImageProvider;
|
||||
use crate::channels::{AsyncSenderExt, BroadcastReceiverExt};
|
||||
use crate::clients::upower::BatteryState;
|
||||
use crate::config::{CommonConfig, LayoutConfig};
|
||||
use crate::gtk_helpers::{IronbarGtkExt, IronbarLabelExt};
|
||||
use crate::modules::PopupButton;
|
||||
use crate::modules::{
|
||||
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, WidgetContext,
|
||||
};
|
||||
use crate::{glib_recv, module_impl, send_async, spawn, try_send};
|
||||
use crate::{module_impl, spawn};
|
||||
|
||||
const DAY: i64 = 24 * 60 * 60;
|
||||
const HOUR: i64 = 60 * 60;
|
||||
|
|
@ -37,6 +38,11 @@ pub struct UpowerModule {
|
|||
#[serde(default = "default_icon_size")]
|
||||
icon_size: i32,
|
||||
|
||||
// -- Common --
|
||||
/// See [layout options](module-level-options#layout)
|
||||
#[serde(default, flatten)]
|
||||
layout: LayoutConfig,
|
||||
|
||||
/// See [common options](module-level-options#common-options).
|
||||
#[serde(flatten)]
|
||||
pub common: Option<CommonConfig>,
|
||||
|
|
@ -59,7 +65,7 @@ pub struct UpowerProperties {
|
|||
time_to_empty: i64,
|
||||
}
|
||||
|
||||
impl Module<gtk::Button> for UpowerModule {
|
||||
impl Module<Button> for UpowerModule {
|
||||
type SendMessage = UpowerProperties;
|
||||
type ReceiveMessage = ();
|
||||
|
||||
|
|
@ -73,7 +79,7 @@ impl Module<gtk::Button> for UpowerModule {
|
|||
) -> Result<()> {
|
||||
let tx = context.tx.clone();
|
||||
|
||||
let display_proxy = context.client::<PropertiesProxy>();
|
||||
let display_proxy = context.try_client::<PropertiesProxy>()?;
|
||||
|
||||
spawn(async move {
|
||||
let mut prop_changed_stream = display_proxy.receive_properties_changed().await?;
|
||||
|
|
@ -84,23 +90,23 @@ impl Module<gtk::Button> for UpowerModule {
|
|||
|
||||
let properties = display_proxy.get_all(device_interface_name.clone()).await?;
|
||||
|
||||
let percentage = *properties["Percentage"]
|
||||
let percentage = properties["Percentage"]
|
||||
.downcast_ref::<f64>()
|
||||
.expect("expected percentage: f64 in HashMap of all properties");
|
||||
let icon_name = properties["IconName"]
|
||||
.downcast_ref::<str>()
|
||||
.downcast_ref::<&str>()
|
||||
.expect("expected IconName: str in HashMap of all properties")
|
||||
.to_string();
|
||||
let state = u32_to_battery_state(
|
||||
*properties["State"]
|
||||
properties["State"]
|
||||
.downcast_ref::<u32>()
|
||||
.expect("expected State: u32 in HashMap of all properties"),
|
||||
)
|
||||
.unwrap_or(BatteryState::Unknown);
|
||||
let time_to_full = *properties["TimeToFull"]
|
||||
let time_to_full = properties["TimeToFull"]
|
||||
.downcast_ref::<i64>()
|
||||
.expect("expected TimeToFull: i64 in HashMap of all properties");
|
||||
let time_to_empty = *properties["TimeToEmpty"]
|
||||
let time_to_empty = properties["TimeToEmpty"]
|
||||
.downcast_ref::<i64>()
|
||||
.expect("expected TimeToEmpty: i64 in HashMap of all properties");
|
||||
let mut properties = UpowerProperties {
|
||||
|
|
@ -111,7 +117,7 @@ impl Module<gtk::Button> for UpowerModule {
|
|||
time_to_empty,
|
||||
};
|
||||
|
||||
send_async!(tx, ModuleUpdateEvent::Update(properties.clone()));
|
||||
tx.send_update(properties.clone()).await;
|
||||
|
||||
while let Some(signal) = prop_changed_stream.next().await {
|
||||
let args = signal.args().expect("Invalid signal arguments");
|
||||
|
|
@ -128,7 +134,7 @@ impl Module<gtk::Button> for UpowerModule {
|
|||
}
|
||||
"IconName" => {
|
||||
properties.icon_name = changed_value
|
||||
.downcast_ref::<str>()
|
||||
.downcast_ref::<&str>()
|
||||
.expect("expected IconName to be str")
|
||||
.to_string();
|
||||
}
|
||||
|
|
@ -151,7 +157,7 @@ impl Module<gtk::Button> for UpowerModule {
|
|||
}
|
||||
}
|
||||
|
||||
send_async!(tx, ModuleUpdateEvent::Update(properties.clone()));
|
||||
tx.send_update(properties.clone()).await;
|
||||
}
|
||||
|
||||
Result::<()>::Ok(())
|
||||
|
|
@ -165,17 +171,19 @@ impl Module<gtk::Button> for UpowerModule {
|
|||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> Result<ModuleParts<Button>> {
|
||||
let icon_theme = info.icon_theme.clone();
|
||||
let icon = gtk::Image::new();
|
||||
icon.add_class("icon");
|
||||
|
||||
let label = Label::builder()
|
||||
.label(&self.format)
|
||||
.use_markup(true)
|
||||
.angle(self.layout.angle(info))
|
||||
.justify(self.layout.justify.into())
|
||||
.build();
|
||||
|
||||
label.add_class("label");
|
||||
|
||||
let container = gtk::Box::new(Orientation::Horizontal, 5);
|
||||
let container = gtk::Box::new(self.layout.orientation(info), 5);
|
||||
container.add_class("contents");
|
||||
|
||||
let button = Button::new();
|
||||
|
|
@ -187,37 +195,48 @@ impl Module<gtk::Button> for UpowerModule {
|
|||
|
||||
let tx = context.tx.clone();
|
||||
button.connect_clicked(move |button| {
|
||||
try_send!(tx, ModuleUpdateEvent::TogglePopup(button.popup_id()));
|
||||
tx.send_spawn(ModuleUpdateEvent::TogglePopup(button.popup_id()));
|
||||
});
|
||||
|
||||
let format = self.format.clone();
|
||||
|
||||
let rx = context.subscribe();
|
||||
glib_recv!(rx, properties => {
|
||||
let provider = context.ironbar.image_provider();
|
||||
rx.recv_glib_async((), move |(), properties| {
|
||||
let state = properties.state;
|
||||
let is_charging = state == BatteryState::Charging || state == BatteryState::PendingCharge;
|
||||
|
||||
let is_charging =
|
||||
state == BatteryState::Charging || state == BatteryState::PendingCharge;
|
||||
|
||||
let time_remaining = if is_charging {
|
||||
seconds_to_string(properties.time_to_full)
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
seconds_to_string(properties.time_to_empty)
|
||||
};
|
||||
let format = format.replace("{percentage}", &properties.percentage.to_string())
|
||||
}
|
||||
.unwrap_or_default();
|
||||
|
||||
let format = format
|
||||
.replace("{percentage}", &properties.percentage.round().to_string())
|
||||
.replace("{time_remaining}", &time_remaining)
|
||||
.replace("{state}", battery_state_to_string(state));
|
||||
|
||||
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()));
|
||||
let provider = provider.clone();
|
||||
let icon = icon.clone();
|
||||
|
||||
label.set_markup(format.as_ref());
|
||||
label.set_label_escaped(&format);
|
||||
|
||||
async move {
|
||||
provider
|
||||
.load_into_image_silent(&icon_name, self.icon_size, false, &icon)
|
||||
.await;
|
||||
}
|
||||
});
|
||||
|
||||
let rx = context.subscribe();
|
||||
let popup = self
|
||||
.into_popup(context.controller_tx.clone(), rx, context, info)
|
||||
.into_popup(context, info)
|
||||
.into_popup_parts(vec![&button]);
|
||||
|
||||
Ok(ModuleParts::new(button, popup))
|
||||
|
|
@ -225,9 +244,7 @@ impl Module<gtk::Button> for UpowerModule {
|
|||
|
||||
fn into_popup(
|
||||
self,
|
||||
_tx: mpsc::Sender<Self::ReceiveMessage>,
|
||||
rx: broadcast::Receiver<Self::SendMessage>,
|
||||
_context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
_info: &ModuleInfo,
|
||||
) -> Option<gtk::Box>
|
||||
where
|
||||
|
|
@ -237,17 +254,17 @@ impl Module<gtk::Button> for UpowerModule {
|
|||
.orientation(Orientation::Horizontal)
|
||||
.build();
|
||||
|
||||
let label = Label::new(None);
|
||||
let label = Label::builder().use_markup(true).build();
|
||||
label.add_class("upower-details");
|
||||
container.add(&label);
|
||||
|
||||
glib_recv!(rx, properties => {
|
||||
context.subscribe().recv_glib((), move |(), properties| {
|
||||
let state = properties.state;
|
||||
let format = match state {
|
||||
BatteryState::Charging | BatteryState::PendingCharge => {
|
||||
let ttf = properties.time_to_full;
|
||||
if ttf > 0 {
|
||||
format!("Full in {}", seconds_to_string(ttf))
|
||||
format!("Full in {}", seconds_to_string(ttf).unwrap_or_default())
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
|
|
@ -255,7 +272,7 @@ impl Module<gtk::Button> for UpowerModule {
|
|||
BatteryState::Discharging | BatteryState::PendingDischarge => {
|
||||
let tte = properties.time_to_empty;
|
||||
if tte > 0 {
|
||||
format!("Empty in {}", seconds_to_string(tte))
|
||||
format!("Empty in {}", seconds_to_string(tte).unwrap_or_default())
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
|
|
@ -263,7 +280,7 @@ impl Module<gtk::Button> for UpowerModule {
|
|||
_ => String::new(),
|
||||
};
|
||||
|
||||
label.set_markup(&format);
|
||||
label.set_label_escaped(&format);
|
||||
});
|
||||
|
||||
container.show_all();
|
||||
|
|
@ -272,21 +289,22 @@ impl Module<gtk::Button> for UpowerModule {
|
|||
}
|
||||
}
|
||||
|
||||
fn seconds_to_string(seconds: i64) -> String {
|
||||
fn seconds_to_string(seconds: i64) -> Result<String> {
|
||||
let mut time_string = String::new();
|
||||
let days = seconds / (DAY);
|
||||
if days > 0 {
|
||||
time_string += &format!("{days}d");
|
||||
write!(time_string, "{days}d")?;
|
||||
}
|
||||
let hours = (seconds % DAY) / HOUR;
|
||||
if hours > 0 {
|
||||
time_string += &format!(" {hours}h");
|
||||
write!(time_string, " {hours}h")?;
|
||||
}
|
||||
let minutes = (seconds % HOUR) / MINUTE;
|
||||
if minutes > 0 {
|
||||
time_string += &format!(" {minutes}m");
|
||||
write!(time_string, " {minutes}m")?;
|
||||
}
|
||||
time_string.trim_start().to_string()
|
||||
|
||||
Ok(time_string.trim_start().to_string())
|
||||
}
|
||||
|
||||
const fn u32_to_battery_state(number: u32) -> Result<BatteryState, u32> {
|
||||
|
|
|
|||
|
|
@ -1,426 +0,0 @@
|
|||
use crate::clients::compositor::{Visibility, Workspace, WorkspaceClient, WorkspaceUpdate};
|
||||
use crate::config::CommonConfig;
|
||||
use crate::gtk_helpers::IronbarGtkExt;
|
||||
use crate::image::new_icon_button;
|
||||
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
|
||||
use crate::{glib_recv, module_impl, send_async, spawn, try_send, Ironbar};
|
||||
use color_eyre::{Report, Result};
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Button, IconTheme};
|
||||
use serde::Deserialize;
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
use tracing::{debug, trace, warn};
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Copy, Eq, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub enum SortOrder {
|
||||
/// Shows workspaces in the order they're added
|
||||
Added,
|
||||
/// Shows workspaces in numeric order.
|
||||
/// Named workspaces are added to the end in alphabetical order.
|
||||
Alphanumeric,
|
||||
}
|
||||
|
||||
impl Default for SortOrder {
|
||||
fn default() -> Self {
|
||||
Self::Alphanumeric
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[serde(untagged)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub enum Favorites {
|
||||
ByMonitor(HashMap<String, Vec<String>>),
|
||||
Global(Vec<String>),
|
||||
}
|
||||
|
||||
impl Default for Favorites {
|
||||
fn default() -> Self {
|
||||
Self::Global(vec![])
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct WorkspacesModule {
|
||||
/// Map of actual workspace names to custom names.
|
||||
///
|
||||
/// Custom names can be [images](images).
|
||||
///
|
||||
/// If a workspace is not present in the map,
|
||||
/// it will fall back to using its actual name.
|
||||
name_map: Option<HashMap<String, String>>,
|
||||
|
||||
/// Workspaces which should always be shown.
|
||||
/// This can either be an array of workspace names,
|
||||
/// or a map of monitor names to arrays of workspace names.
|
||||
///
|
||||
/// **Default**: `{}`
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```corn
|
||||
/// // array format
|
||||
/// {
|
||||
/// type = "workspaces"
|
||||
/// favorites = ["1", "2", "3"]
|
||||
/// }
|
||||
///
|
||||
/// // map format
|
||||
/// {
|
||||
/// type = "workspaces"
|
||||
/// favorites.DP-1 = ["1", "2", "3"]
|
||||
/// favorites.DP-2 = ["4", "5", "6"]
|
||||
/// }
|
||||
/// ```
|
||||
#[serde(default)]
|
||||
favorites: Favorites,
|
||||
|
||||
/// A list of workspace names to never show.
|
||||
///
|
||||
/// This may be useful for scratchpad/special workspaces, for example.
|
||||
///
|
||||
/// **Default**: `[]`
|
||||
#[serde(default)]
|
||||
hidden: Vec<String>,
|
||||
|
||||
/// Whether to display workspaces from all monitors.
|
||||
/// When false, only shows workspaces on the current monitor.
|
||||
///
|
||||
/// **Default**: `false`
|
||||
#[serde(default = "crate::config::default_false")]
|
||||
all_monitors: bool,
|
||||
|
||||
/// The method used for sorting workspaces.
|
||||
/// `added` always appends to the end, `alphanumeric` sorts by number/name.
|
||||
///
|
||||
/// **Valid options**: `added`, `alphanumeric`
|
||||
/// <br>
|
||||
/// **Default**: `alphanumeric`
|
||||
#[serde(default)]
|
||||
sort: SortOrder,
|
||||
|
||||
/// The size to render icons at (image icons only).
|
||||
///
|
||||
/// **Default**: `32`
|
||||
#[serde(default = "default_icon_size")]
|
||||
icon_size: i32,
|
||||
|
||||
/// See [common options](module-level-options#common-options).
|
||||
#[serde(flatten)]
|
||||
pub common: Option<CommonConfig>,
|
||||
}
|
||||
|
||||
const fn default_icon_size() -> i32 {
|
||||
32
|
||||
}
|
||||
|
||||
/// Creates a button from a workspace
|
||||
fn create_button(
|
||||
name: &str,
|
||||
visibility: Visibility,
|
||||
name_map: &HashMap<String, String>,
|
||||
icon_theme: &IconTheme,
|
||||
icon_size: i32,
|
||||
tx: &Sender<String>,
|
||||
) -> Button {
|
||||
let label = name_map.get(name).map_or(name, String::as_str);
|
||||
|
||||
let button = new_icon_button(label, icon_theme, icon_size);
|
||||
button.set_widget_name(name);
|
||||
|
||||
let style_context = button.style_context();
|
||||
style_context.add_class("item");
|
||||
|
||||
if visibility.is_visible() {
|
||||
style_context.add_class("visible");
|
||||
}
|
||||
|
||||
if visibility.is_focused() {
|
||||
style_context.add_class("focused");
|
||||
}
|
||||
|
||||
if !visibility.is_visible() {
|
||||
style_context.add_class("inactive");
|
||||
}
|
||||
|
||||
{
|
||||
let tx = tx.clone();
|
||||
let name = name.to_string();
|
||||
button.connect_clicked(move |_item| {
|
||||
try_send!(tx, name.clone());
|
||||
});
|
||||
}
|
||||
|
||||
button
|
||||
}
|
||||
|
||||
fn reorder_workspaces(container: >k::Box) {
|
||||
let mut buttons = container
|
||||
.children()
|
||||
.into_iter()
|
||||
.map(|child| (child.widget_name().to_string(), child))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
buttons.sort_by(|(label_a, _), (label_b, _a)| {
|
||||
match (label_a.parse::<i32>(), label_b.parse::<i32>()) {
|
||||
(Ok(a), Ok(b)) => a.cmp(&b),
|
||||
(Ok(_), Err(_)) => Ordering::Less,
|
||||
(Err(_), Ok(_)) => Ordering::Greater,
|
||||
(Err(_), Err(_)) => label_a.cmp(label_b),
|
||||
}
|
||||
});
|
||||
|
||||
for (i, (_, button)) in buttons.into_iter().enumerate() {
|
||||
container.reorder_child(&button, i as i32);
|
||||
}
|
||||
}
|
||||
|
||||
fn find_btn(map: &HashMap<i64, Button>, workspace: &Workspace) -> Option<Button> {
|
||||
map.get(&workspace.id)
|
||||
.or_else(|| {
|
||||
map.values()
|
||||
.find(|btn| btn.label().unwrap_or_default() == workspace.name)
|
||||
})
|
||||
.cloned()
|
||||
}
|
||||
|
||||
impl WorkspacesModule {
|
||||
fn show_workspace_check(&self, output: &String, work: &Workspace) -> bool {
|
||||
(work.visibility.is_focused() || !self.hidden.contains(&work.name))
|
||||
&& (self.all_monitors || output == &work.monitor)
|
||||
}
|
||||
}
|
||||
|
||||
impl Module<gtk::Box> for WorkspacesModule {
|
||||
type SendMessage = WorkspaceUpdate;
|
||||
type ReceiveMessage = String;
|
||||
|
||||
module_impl!("workspaces");
|
||||
|
||||
fn spawn_controller(
|
||||
&self,
|
||||
_info: &ModuleInfo,
|
||||
context: &WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
mut rx: Receiver<Self::ReceiveMessage>,
|
||||
) -> Result<()> {
|
||||
let tx = context.tx.clone();
|
||||
let client = context.ironbar.clients.borrow_mut().workspaces()?;
|
||||
// Subscribe & send events
|
||||
spawn(async move {
|
||||
let mut srx = client.subscribe_workspace_change();
|
||||
|
||||
trace!("Set up workspace subscription");
|
||||
|
||||
while let Ok(payload) = srx.recv().await {
|
||||
debug!("Received update: {payload:?}");
|
||||
send_async!(tx, ModuleUpdateEvent::Update(payload));
|
||||
}
|
||||
});
|
||||
|
||||
let client = context.try_client::<dyn WorkspaceClient>()?;
|
||||
|
||||
// Change workspace focus
|
||||
spawn(async move {
|
||||
trace!("Setting up UI event handler");
|
||||
|
||||
while let Some(name) = rx.recv().await {
|
||||
client.focus(name)?;
|
||||
}
|
||||
|
||||
Ok::<(), Report>(())
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn into_widget(
|
||||
self,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> Result<ModuleParts<gtk::Box>> {
|
||||
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();
|
||||
let mut fav_names: Vec<String> = vec![];
|
||||
|
||||
let mut button_map: HashMap<i64, Button> = HashMap::new();
|
||||
|
||||
{
|
||||
let container = container.clone();
|
||||
let output_name = info.output_name.to_string();
|
||||
let icon_theme = info.icon_theme.clone();
|
||||
let icon_size = self.icon_size;
|
||||
|
||||
// keep track of whether init event has fired previously
|
||||
// since it fires for every workspace subscriber
|
||||
let mut has_initialized = false;
|
||||
|
||||
glib_recv!(context.subscribe(), event => {
|
||||
match event {
|
||||
WorkspaceUpdate::Init(workspaces) => {
|
||||
if !has_initialized {
|
||||
trace!("Creating workspace buttons");
|
||||
|
||||
let mut added = HashSet::new();
|
||||
|
||||
let mut add_workspace = |id: i64, name: &str, visibility: Visibility| {
|
||||
let item = create_button(
|
||||
name,
|
||||
visibility,
|
||||
&name_map,
|
||||
&icon_theme,
|
||||
icon_size,
|
||||
&context.controller_tx,
|
||||
);
|
||||
|
||||
container.add(&item);
|
||||
button_map.insert(id, item);
|
||||
};
|
||||
|
||||
// add workspaces from client
|
||||
for workspace in &workspaces {
|
||||
if self.show_workspace_check(&output_name, workspace) {
|
||||
add_workspace(workspace.id, &workspace.name, workspace.visibility);
|
||||
added.insert(workspace.name.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let mut add_favourites = |names: &Vec<String>| {
|
||||
for name in names {
|
||||
fav_names.push(name.to_string());
|
||||
|
||||
if !added.contains(name) {
|
||||
// Favourites are added with the same name and ID
|
||||
// as Hyprland will initialize them this way.
|
||||
// Since existing workspaces are added above,
|
||||
// this means there shouldn't be any issues with renaming.
|
||||
add_workspace(-(Ironbar::unique_id() as i64), name, Visibility::Hidden);
|
||||
added.insert(name.to_string());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// add workspaces from favourites
|
||||
match &favs {
|
||||
Favorites::Global(names) => add_favourites(names),
|
||||
Favorites::ByMonitor(map) => {
|
||||
if let Some(to_add) = map.get(&output_name) {
|
||||
add_favourites(to_add);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.sort == SortOrder::Alphanumeric {
|
||||
reorder_workspaces(&container);
|
||||
}
|
||||
|
||||
container.show_all();
|
||||
has_initialized = true;
|
||||
}
|
||||
}
|
||||
WorkspaceUpdate::Focus { old, new } => {
|
||||
if let Some(btn) = old.as_ref().and_then(|w| find_btn(&button_map, w)) {
|
||||
if Some(new.monitor.as_str()) == old.as_ref().map(|w| w.monitor.as_str()) {
|
||||
btn.style_context().remove_class("visible");
|
||||
}
|
||||
|
||||
btn.style_context().remove_class("focused");
|
||||
}
|
||||
|
||||
if let Some(btn) = find_btn(&button_map, &new) {
|
||||
btn.add_class("visible");
|
||||
btn.add_class("focused");
|
||||
}
|
||||
}
|
||||
WorkspaceUpdate::Rename { id, name } => {
|
||||
if let Some(btn) = button_map.get(&id) {
|
||||
let name = name_map.get(&name).unwrap_or(&name);
|
||||
btn.set_label(name);
|
||||
}
|
||||
}
|
||||
WorkspaceUpdate::Add(workspace) => {
|
||||
if fav_names.contains(&workspace.name) {
|
||||
let btn = button_map.get(&workspace.id);
|
||||
if let Some(btn) = btn {
|
||||
btn.style_context().remove_class("inactive");
|
||||
}
|
||||
} else if self.show_workspace_check(&output_name, &workspace) {
|
||||
let name = workspace.name;
|
||||
let item = create_button(
|
||||
&name,
|
||||
workspace.visibility,
|
||||
&name_map,
|
||||
&icon_theme,
|
||||
icon_size,
|
||||
&context.controller_tx,
|
||||
);
|
||||
|
||||
container.add(&item);
|
||||
if self.sort == SortOrder::Alphanumeric {
|
||||
reorder_workspaces(&container);
|
||||
}
|
||||
|
||||
item.show();
|
||||
|
||||
if !name.is_empty() {
|
||||
button_map.insert(workspace.id, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
WorkspaceUpdate::Move(workspace) => {
|
||||
if !self.hidden.contains(&workspace.name) && !self.all_monitors {
|
||||
if workspace.monitor == output_name {
|
||||
let name = workspace.name;
|
||||
let item = create_button(
|
||||
&name,
|
||||
workspace.visibility,
|
||||
&name_map,
|
||||
&icon_theme,
|
||||
icon_size,
|
||||
&context.controller_tx,
|
||||
);
|
||||
|
||||
container.add(&item);
|
||||
|
||||
if self.sort == SortOrder::Alphanumeric {
|
||||
reorder_workspaces(&container);
|
||||
}
|
||||
|
||||
item.show();
|
||||
|
||||
if !name.is_empty() {
|
||||
button_map.insert(workspace.id, item);
|
||||
}
|
||||
} else if let Some(item) = button_map.get(&workspace.id) {
|
||||
container.remove(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
WorkspaceUpdate::Remove(workspace) => {
|
||||
let button = button_map.get(&workspace);
|
||||
if let Some(item) = button {
|
||||
if workspace < 0 {
|
||||
// if fav_names.contains(&workspace) {
|
||||
item.style_context().add_class("inactive");
|
||||
} else {
|
||||
container.remove(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
WorkspaceUpdate::Unknown => warn!("Received unknown type workspace event")
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
Ok(ModuleParts {
|
||||
widget: container,
|
||||
popup: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
77
src/modules/workspaces/button.rs
Normal file
77
src/modules/workspaces/button.rs
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
use super::open_state::OpenState;
|
||||
use crate::channels::AsyncSenderExt;
|
||||
use crate::gtk_helpers::IronbarGtkExt;
|
||||
use crate::image::IconButton;
|
||||
use crate::modules::workspaces::WorkspaceItemContext;
|
||||
use gtk::Button as GtkButton;
|
||||
use gtk::prelude::*;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Button {
|
||||
button: IconButton,
|
||||
workspace_id: i64,
|
||||
}
|
||||
|
||||
impl Button {
|
||||
pub fn new(id: i64, name: &str, open_state: OpenState, context: &WorkspaceItemContext) -> Self {
|
||||
let label = context.name_map.get(name).map_or(name, String::as_str);
|
||||
|
||||
let button = IconButton::new(label, context.icon_size, context.image_provider.clone());
|
||||
button.set_widget_name(name);
|
||||
button.add_class("item");
|
||||
|
||||
let tx = context.tx.clone();
|
||||
|
||||
button.connect_clicked(move |_item| {
|
||||
tx.send_spawn(id);
|
||||
});
|
||||
|
||||
let btn = Self {
|
||||
button,
|
||||
workspace_id: id,
|
||||
};
|
||||
|
||||
btn.set_open_state(open_state);
|
||||
btn
|
||||
}
|
||||
|
||||
pub fn button(&self) -> &GtkButton {
|
||||
&self.button
|
||||
}
|
||||
|
||||
pub fn set_open_state(&self, open_state: OpenState) {
|
||||
if open_state.is_visible() {
|
||||
self.button.add_class("visible");
|
||||
} else {
|
||||
self.button.remove_class("visible");
|
||||
}
|
||||
|
||||
if open_state == OpenState::Focused {
|
||||
self.button.add_class("focused");
|
||||
} else {
|
||||
self.button.remove_class("focused");
|
||||
}
|
||||
|
||||
if open_state == OpenState::Closed {
|
||||
self.button.add_class("inactive");
|
||||
} else {
|
||||
self.button.remove_class("inactive");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_urgent(&self, urgent: bool) {
|
||||
if urgent {
|
||||
self.button.add_class("urgent");
|
||||
} else {
|
||||
self.button.remove_class("urgent");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn workspace_id(&self) -> i64 {
|
||||
self.workspace_id
|
||||
}
|
||||
|
||||
pub fn set_workspace_id(&mut self, id: i64) {
|
||||
self.workspace_id = id;
|
||||
}
|
||||
}
|
||||
78
src/modules/workspaces/button_map.rs
Normal file
78
src/modules/workspaces/button_map.rs
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
use super::button::Button;
|
||||
use crate::clients::compositor::Workspace;
|
||||
use std::collections::HashMap;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum Identifier {
|
||||
Id(i64),
|
||||
Name(String),
|
||||
}
|
||||
|
||||
/// Wrapper around a hashmap of workspace buttons,
|
||||
/// which can be found using the workspace ID,
|
||||
/// or their name for favourites.
|
||||
#[derive(Debug)]
|
||||
pub struct ButtonMap {
|
||||
map: HashMap<Identifier, Button>,
|
||||
}
|
||||
|
||||
impl ButtonMap {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
map: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the button for a workspace,
|
||||
/// checking the map for both its ID and name.
|
||||
pub fn find_button_mut(&mut self, workspace: &Workspace) -> Option<&mut Button> {
|
||||
let id = Identifier::Id(workspace.id);
|
||||
|
||||
if self.map.contains_key(&id) {
|
||||
self.map.get_mut(&id)
|
||||
} else {
|
||||
self.map.get_mut(&Identifier::Name(workspace.name.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the button for a workspace,
|
||||
/// performing a search of all keys for the button
|
||||
/// with the associated workspace ID.
|
||||
pub fn find_button_by_id(&self, id: i64) -> Option<&Button> {
|
||||
self.map.iter().find_map(|(_, button)| {
|
||||
if button.workspace_id() == id {
|
||||
Some(button)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Gets the button for a workspace,
|
||||
/// performing a search of all keys for the button
|
||||
/// with the associated workspace ID.
|
||||
pub fn find_button_by_id_mut(&mut self, id: i64) -> Option<&mut Button> {
|
||||
self.map.iter_mut().find_map(|(_, button)| {
|
||||
if button.workspace_id() == id {
|
||||
Some(button)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for ButtonMap {
|
||||
type Target = HashMap<Identifier, Button>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.map
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for ButtonMap {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.map
|
||||
}
|
||||
}
|
||||
416
src/modules/workspaces/mod.rs
Normal file
416
src/modules/workspaces/mod.rs
Normal file
|
|
@ -0,0 +1,416 @@
|
|||
mod button;
|
||||
mod button_map;
|
||||
mod open_state;
|
||||
|
||||
use self::button::Button;
|
||||
use crate::channels::{AsyncSenderExt, BroadcastReceiverExt};
|
||||
use crate::clients::compositor::{Workspace, WorkspaceClient, WorkspaceUpdate};
|
||||
use crate::config::{CommonConfig, LayoutConfig};
|
||||
use crate::modules::workspaces::button_map::{ButtonMap, Identifier};
|
||||
use crate::modules::workspaces::open_state::OpenState;
|
||||
use crate::modules::{Module, ModuleInfo, ModuleParts, WidgetContext};
|
||||
use crate::{image, module_impl, spawn};
|
||||
use color_eyre::{Report, Result};
|
||||
use gtk::prelude::*;
|
||||
use serde::Deserialize;
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::HashMap;
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{debug, trace, warn};
|
||||
|
||||
#[derive(Debug, Deserialize, Default, Clone, Copy, Eq, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub enum SortOrder {
|
||||
/// Shows workspaces in the order they're added
|
||||
Added,
|
||||
|
||||
/// Shows workspaces in the order of their displayed labels,
|
||||
/// accounting for any mappings supplied in `name_map`.
|
||||
/// In most cases, this is likely their number.
|
||||
///
|
||||
/// Workspaces are sorted numerically first,
|
||||
/// and named workspaces are added to the end in alphabetical order.
|
||||
#[default]
|
||||
Label,
|
||||
|
||||
/// Shows workspaces in the order of their real names,
|
||||
/// as supplied by the compositor.
|
||||
/// In most cases, this is likely their number.
|
||||
///
|
||||
/// Workspaces are sorted numerically first,
|
||||
/// and named workspaces are added to the end in alphabetical order.
|
||||
Name,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[serde(untagged)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub enum Favorites {
|
||||
ByMonitor(HashMap<String, Vec<String>>),
|
||||
Global(Vec<String>),
|
||||
}
|
||||
|
||||
impl Default for Favorites {
|
||||
fn default() -> Self {
|
||||
Self::Global(vec![])
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct WorkspacesModule {
|
||||
/// Map of actual workspace names to custom names.
|
||||
///
|
||||
/// Custom names can be [images](images).
|
||||
///
|
||||
/// If a workspace is not present in the map,
|
||||
/// it will fall back to using its actual name.
|
||||
#[serde(default)]
|
||||
name_map: HashMap<String, String>,
|
||||
|
||||
/// Workspaces which should always be shown.
|
||||
/// This can either be an array of workspace names,
|
||||
/// or a map of monitor names to arrays of workspace names.
|
||||
///
|
||||
/// **Default**: `{}`
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```corn
|
||||
/// // array format
|
||||
/// {
|
||||
/// type = "workspaces"
|
||||
/// favorites = ["1", "2", "3"]
|
||||
/// }
|
||||
///
|
||||
/// // map format
|
||||
/// {
|
||||
/// type = "workspaces"
|
||||
/// favorites.DP-1 = ["1", "2", "3"]
|
||||
/// favorites.DP-2 = ["4", "5", "6"]
|
||||
/// }
|
||||
/// ```
|
||||
#[serde(default)]
|
||||
favorites: Favorites,
|
||||
|
||||
/// A list of workspace names to never show.
|
||||
///
|
||||
/// This may be useful for scratchpad/special workspaces, for example.
|
||||
///
|
||||
/// **Default**: `[]`
|
||||
#[serde(default)]
|
||||
hidden: Vec<String>,
|
||||
|
||||
/// Whether to display workspaces from all monitors.
|
||||
/// When false, only shows workspaces on the current monitor.
|
||||
///
|
||||
/// **Default**: `false`
|
||||
#[serde(default = "crate::config::default_false")]
|
||||
all_monitors: bool,
|
||||
|
||||
/// The method used for sorting workspaces.
|
||||
///
|
||||
/// - `added` always appends to the end.
|
||||
/// - `label` sorts by displayed value.
|
||||
/// - `name` sorts by workspace name.
|
||||
///
|
||||
/// **Valid options**: `added`, `label`, `name`.
|
||||
/// <br>
|
||||
/// **Default**: `label`
|
||||
#[serde(default)]
|
||||
sort: SortOrder,
|
||||
|
||||
/// The size to render icons at (image icons only).
|
||||
///
|
||||
/// **Default**: `32`
|
||||
#[serde(default = "default_icon_size")]
|
||||
icon_size: i32,
|
||||
|
||||
// -- Common --
|
||||
/// See [layout options](module-level-options#layout)
|
||||
#[serde(default, flatten)]
|
||||
layout: LayoutConfig,
|
||||
|
||||
/// See [common options](module-level-options#common-options).
|
||||
#[serde(flatten)]
|
||||
pub common: Option<CommonConfig>,
|
||||
}
|
||||
|
||||
const fn default_icon_size() -> i32 {
|
||||
32
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WorkspaceItemContext {
|
||||
name_map: HashMap<String, String>,
|
||||
icon_size: i32,
|
||||
image_provider: image::Provider,
|
||||
tx: mpsc::Sender<i64>,
|
||||
}
|
||||
|
||||
/// Re-orders the container children alphabetically,
|
||||
/// using their widget names.
|
||||
///
|
||||
/// Named workspaces are always sorted before numbered ones.
|
||||
fn reorder_workspaces(container: >k::Box, sort_order: SortOrder) {
|
||||
let mut buttons = container
|
||||
.children()
|
||||
.into_iter()
|
||||
.map(|child| {
|
||||
let label = if sort_order == SortOrder::Label {
|
||||
child
|
||||
.downcast_ref::<gtk::Button>()
|
||||
.and_then(ButtonExt::label)
|
||||
.unwrap_or_else(|| child.widget_name())
|
||||
} else {
|
||||
child.widget_name()
|
||||
}
|
||||
.to_string();
|
||||
|
||||
(label, child)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
buttons.sort_by(|(label_a, _), (label_b, _a)| {
|
||||
match (label_a.parse::<i32>(), label_b.parse::<i32>()) {
|
||||
(Ok(a), Ok(b)) => a.cmp(&b),
|
||||
(Ok(_), Err(_)) => Ordering::Less,
|
||||
(Err(_), Ok(_)) => Ordering::Greater,
|
||||
(Err(_), Err(_)) => label_a.cmp(label_b),
|
||||
}
|
||||
});
|
||||
|
||||
for (i, (_, button)) in buttons.into_iter().enumerate() {
|
||||
container.reorder_child(&button, i as i32);
|
||||
}
|
||||
}
|
||||
|
||||
impl Module<gtk::Box> for WorkspacesModule {
|
||||
type SendMessage = WorkspaceUpdate;
|
||||
type ReceiveMessage = i64;
|
||||
|
||||
module_impl!("workspaces");
|
||||
|
||||
fn spawn_controller(
|
||||
&self,
|
||||
_info: &ModuleInfo,
|
||||
context: &WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
mut rx: mpsc::Receiver<Self::ReceiveMessage>,
|
||||
) -> Result<()> {
|
||||
let tx = context.tx.clone();
|
||||
let client = context.ironbar.clients.borrow_mut().workspaces()?;
|
||||
// Subscribe & send events
|
||||
spawn(async move {
|
||||
let mut srx = client.subscribe();
|
||||
|
||||
trace!("Set up workspace subscription");
|
||||
|
||||
while let Ok(payload) = srx.recv().await {
|
||||
debug!("Received update: {payload:?}");
|
||||
tx.send_update(payload).await;
|
||||
}
|
||||
});
|
||||
|
||||
let client = context.try_client::<dyn WorkspaceClient>()?;
|
||||
|
||||
// Change workspace focus
|
||||
spawn(async move {
|
||||
trace!("Setting up UI event handler");
|
||||
|
||||
while let Some(id) = rx.recv().await {
|
||||
client.focus(id);
|
||||
}
|
||||
|
||||
Ok::<(), Report>(())
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn into_widget(
|
||||
self,
|
||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||
info: &ModuleInfo,
|
||||
) -> Result<ModuleParts<gtk::Box>> {
|
||||
let container = gtk::Box::new(self.layout.orientation(info), 0);
|
||||
|
||||
let mut button_map = ButtonMap::new();
|
||||
|
||||
let item_context = WorkspaceItemContext {
|
||||
name_map: self.name_map.clone(),
|
||||
icon_size: self.icon_size,
|
||||
image_provider: context.ironbar.image_provider(),
|
||||
tx: context.controller_tx.clone(),
|
||||
};
|
||||
|
||||
// setup favorites
|
||||
let favorites = match self.favorites {
|
||||
Favorites::ByMonitor(map) => map.get(info.output_name).cloned(),
|
||||
Favorites::Global(vec) => Some(vec),
|
||||
}
|
||||
.unwrap_or_default();
|
||||
|
||||
for favorite in &favorites {
|
||||
let btn = Button::new(-1, favorite, OpenState::Closed, &item_context);
|
||||
container.add(btn.button());
|
||||
button_map.insert(Identifier::Name(favorite.clone()), btn);
|
||||
}
|
||||
|
||||
{
|
||||
let container = container.clone();
|
||||
let output_name = info.output_name.to_string();
|
||||
|
||||
// keep track of whether init event has fired previously
|
||||
// since it fires for every workspace subscriber
|
||||
let mut has_initialized = false;
|
||||
|
||||
let add_workspace = {
|
||||
let container = container.clone();
|
||||
move |workspace: Workspace, button_map: &mut ButtonMap| {
|
||||
if favorites.contains(&workspace.name) {
|
||||
let btn = button_map
|
||||
.get_mut(&Identifier::Name(workspace.name))
|
||||
.expect("favorite to exist");
|
||||
|
||||
// set an ID to track the open workspace for the favourite
|
||||
btn.set_workspace_id(workspace.id);
|
||||
btn.set_open_state(workspace.visibility.into());
|
||||
} else {
|
||||
let btn = Button::new(
|
||||
workspace.id,
|
||||
&workspace.name,
|
||||
workspace.visibility.into(),
|
||||
&item_context,
|
||||
);
|
||||
container.add(btn.button());
|
||||
btn.button().show();
|
||||
|
||||
button_map.insert(Identifier::Id(workspace.id), btn);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let remove_workspace = {
|
||||
let container = container.clone();
|
||||
move |id: i64, button_map: &mut ButtonMap| {
|
||||
// since favourites use name identifiers,
|
||||
// we can safely remove using ID here and favourites will remain
|
||||
if let Some(button) = button_map.remove(&Identifier::Id(id)) {
|
||||
container.remove(button.button());
|
||||
} else {
|
||||
// otherwise we do a deep search and use the button's cached ID
|
||||
if let Some(button) = button_map.find_button_by_id_mut(id) {
|
||||
button.set_workspace_id(-1);
|
||||
button.set_open_state(OpenState::Closed);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
macro_rules! reorder {
|
||||
() => {
|
||||
if self.sort != SortOrder::Added {
|
||||
reorder_workspaces(&container, self.sort);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let name_map = self.name_map;
|
||||
context
|
||||
.subscribe()
|
||||
.recv_glib((), move |(), event| match event {
|
||||
WorkspaceUpdate::Init(workspaces) => {
|
||||
if has_initialized {
|
||||
return;
|
||||
}
|
||||
|
||||
trace!("Creating workspace buttons");
|
||||
|
||||
for workspace in workspaces
|
||||
.into_iter()
|
||||
.filter(|w| self.all_monitors || w.monitor == output_name)
|
||||
.filter(|w| !self.hidden.contains(&w.name))
|
||||
{
|
||||
add_workspace(workspace, &mut button_map);
|
||||
}
|
||||
|
||||
reorder!();
|
||||
|
||||
has_initialized = true;
|
||||
}
|
||||
WorkspaceUpdate::Add(workspace) => {
|
||||
if !self.hidden.contains(&workspace.name)
|
||||
&& (self.all_monitors || workspace.monitor == output_name)
|
||||
{
|
||||
add_workspace(workspace, &mut button_map);
|
||||
}
|
||||
|
||||
reorder!();
|
||||
}
|
||||
WorkspaceUpdate::Remove(id) => remove_workspace(id, &mut button_map),
|
||||
WorkspaceUpdate::Move(workspace) => {
|
||||
if self.all_monitors {
|
||||
return;
|
||||
}
|
||||
|
||||
if workspace.monitor == output_name
|
||||
&& !self.hidden.contains(&workspace.name)
|
||||
{
|
||||
add_workspace(workspace, &mut button_map);
|
||||
reorder!();
|
||||
} else {
|
||||
remove_workspace(workspace.id, &mut button_map);
|
||||
}
|
||||
}
|
||||
WorkspaceUpdate::Focus { old, new } => {
|
||||
// Open states are calculated here rather than using the workspace visibility
|
||||
// as that seems to come back wrong, at least on Hyprland.
|
||||
// Likely a deeper issue that needs exploring.
|
||||
|
||||
if let Some(old) = old {
|
||||
if let Some(button) = button_map.find_button_mut(&old) {
|
||||
let open_state = if new.monitor == old.monitor {
|
||||
OpenState::Hidden
|
||||
} else {
|
||||
OpenState::Visible
|
||||
};
|
||||
|
||||
button.set_open_state(open_state);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(button) = button_map.find_button_mut(&new) {
|
||||
button.set_open_state(OpenState::Focused);
|
||||
}
|
||||
}
|
||||
WorkspaceUpdate::Rename { id, name } => {
|
||||
if let Some(button) = button_map
|
||||
.get(&Identifier::Id(id))
|
||||
.or_else(|| button_map.get(&Identifier::Name(name.clone())))
|
||||
.map(Button::button)
|
||||
{
|
||||
let display_name = name_map.get(&name).unwrap_or(&name);
|
||||
|
||||
button.set_label(display_name);
|
||||
button.set_widget_name(&name);
|
||||
}
|
||||
}
|
||||
WorkspaceUpdate::Urgent { id, urgent } => {
|
||||
if let Some(button) = button_map
|
||||
.get(&Identifier::Id(id))
|
||||
.or_else(|| button_map.find_button_by_id(id))
|
||||
{
|
||||
button.set_urgent(urgent);
|
||||
}
|
||||
}
|
||||
WorkspaceUpdate::Unknown => warn!("received unknown type workspace event"),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(ModuleParts {
|
||||
widget: container,
|
||||
popup: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
30
src/modules/workspaces/open_state.rs
Normal file
30
src/modules/workspaces/open_state.rs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
use crate::clients::compositor::Visibility;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum OpenState {
|
||||
/// A favourite workspace, which is not currently open
|
||||
Closed,
|
||||
/// A workspace which is open but not visible on any monitors.
|
||||
Hidden,
|
||||
/// A workspace which is visible, but not focused.
|
||||
Visible,
|
||||
/// The currently active workspace.
|
||||
Focused,
|
||||
}
|
||||
|
||||
impl From<Visibility> for OpenState {
|
||||
fn from(value: Visibility) -> Self {
|
||||
match value {
|
||||
Visibility::Visible { focused: true } => Self::Focused,
|
||||
Visibility::Visible { focused: false } => Self::Visible,
|
||||
Visibility::Hidden => Self::Hidden,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenState {
|
||||
/// Whether the workspace is visible, including focused state.
|
||||
pub fn is_visible(self) -> bool {
|
||||
matches!(self, OpenState::Visible | OpenState::Focused)
|
||||
}
|
||||
}
|
||||
56
src/popup.rs
56
src/popup.rs
|
|
@ -3,17 +3,17 @@ use std::cell::RefCell;
|
|||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
use gtk::gdk::Monitor;
|
||||
use crate::channels::BroadcastReceiverExt;
|
||||
use crate::clients::wayland::{OutputEvent, OutputEventType};
|
||||
use crate::config::BarPosition;
|
||||
use crate::gtk_helpers::{IronbarGtkExt, WidgetGeometry};
|
||||
use crate::modules::{ModuleInfo, ModulePopupParts, PopupButton};
|
||||
use crate::{Ironbar, rc_mut};
|
||||
use gtk::prelude::*;
|
||||
use gtk::{ApplicationWindow, Button, Orientation};
|
||||
use gtk_layer_shell::LayerShell;
|
||||
use tracing::{debug, trace};
|
||||
|
||||
use crate::config::BarPosition;
|
||||
use crate::gtk_helpers::{IronbarGtkExt, WidgetGeometry};
|
||||
use crate::modules::{ModuleInfo, ModulePopupParts, PopupButton};
|
||||
use crate::rc_mut;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PopupCacheValue {
|
||||
pub name: String,
|
||||
|
|
@ -25,16 +25,21 @@ pub struct Popup {
|
|||
pub window: ApplicationWindow,
|
||||
pub container_cache: Rc<RefCell<HashMap<usize, PopupCacheValue>>>,
|
||||
pub button_cache: Rc<RefCell<Vec<Button>>>,
|
||||
monitor: Monitor,
|
||||
pos: BarPosition,
|
||||
current_widget: Rc<RefCell<Option<(usize, usize)>>>,
|
||||
output_size: Rc<RefCell<(i32, i32)>>,
|
||||
}
|
||||
|
||||
impl Popup {
|
||||
/// Creates a new popup window.
|
||||
/// This includes setting up gtk-layer-shell
|
||||
/// and an empty `gtk::Box` container.
|
||||
pub fn new(module_info: &ModuleInfo, gap: i32) -> Self {
|
||||
pub fn new(
|
||||
ironbar: &Ironbar,
|
||||
module_info: &ModuleInfo,
|
||||
output_size: (i32, i32),
|
||||
gap: i32,
|
||||
) -> Self {
|
||||
let pos = module_info.bar_position;
|
||||
let orientation = pos.orientation();
|
||||
|
||||
|
|
@ -105,13 +110,26 @@ impl Popup {
|
|||
Propagation::Proceed
|
||||
});
|
||||
|
||||
let output_size = rc_mut!(output_size);
|
||||
|
||||
// respond to resolution changes
|
||||
let rx = ironbar.clients.borrow_mut().wayland().subscribe_outputs();
|
||||
let output_name = module_info.output_name.to_string();
|
||||
rx.recv_glib(&output_size, move |output_size, event: OutputEvent| {
|
||||
if event.event_type == OutputEventType::Update
|
||||
&& event.output.name.unwrap_or_default() == output_name
|
||||
{
|
||||
*output_size.borrow_mut() = event.output.logical_size.unwrap_or_default();
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
window: win,
|
||||
container_cache: rc_mut!(HashMap::new()),
|
||||
button_cache: rc_mut!(vec![]),
|
||||
monitor: module_info.monitor.clone(),
|
||||
pos,
|
||||
current_widget: rc_mut!(None),
|
||||
output_size,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -123,13 +141,14 @@ impl Popup {
|
|||
}
|
||||
|
||||
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.container_cache.clone();
|
||||
let button_cache = self.button_cache.clone();
|
||||
|
||||
let output_size = self.output_size.clone();
|
||||
|
||||
content
|
||||
.container
|
||||
.connect_size_allocate(move |container, rect| {
|
||||
|
|
@ -142,8 +161,8 @@ impl Popup {
|
|||
&button_cache.borrow(),
|
||||
button_id,
|
||||
orientation,
|
||||
&monitor,
|
||||
&window,
|
||||
&output_size,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -175,8 +194,8 @@ impl Popup {
|
|||
&self.button_cache.borrow(),
|
||||
button_id,
|
||||
self.pos.orientation(),
|
||||
&self.monitor,
|
||||
&self.window,
|
||||
&self.output_size,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -193,8 +212,8 @@ impl Popup {
|
|||
Self::set_pos(
|
||||
geometry,
|
||||
self.pos.orientation(),
|
||||
&self.monitor,
|
||||
&self.window,
|
||||
*self.output_size.borrow(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -203,8 +222,8 @@ impl Popup {
|
|||
buttons: &[Button],
|
||||
button_id: usize,
|
||||
orientation: Orientation,
|
||||
monitor: &Monitor,
|
||||
window: &ApplicationWindow,
|
||||
output_size: &Rc<RefCell<(i32, i32)>>,
|
||||
) {
|
||||
let button = buttons
|
||||
.iter()
|
||||
|
|
@ -212,7 +231,7 @@ impl Popup {
|
|||
.expect("to find valid button");
|
||||
|
||||
let geometry = button.geometry(orientation);
|
||||
Self::set_pos(geometry, orientation, monitor, window);
|
||||
Self::set_pos(geometry, orientation, window, *output_size.borrow());
|
||||
}
|
||||
|
||||
fn clear_window(&self) {
|
||||
|
|
@ -242,14 +261,13 @@ impl Popup {
|
|||
fn set_pos(
|
||||
geometry: WidgetGeometry,
|
||||
orientation: Orientation,
|
||||
monitor: &Monitor,
|
||||
window: &ApplicationWindow,
|
||||
output_size: (i32, i32),
|
||||
) {
|
||||
let mon_workarea = monitor.workarea();
|
||||
let screen_size = if orientation == Orientation::Horizontal {
|
||||
mon_workarea.width()
|
||||
output_size.0
|
||||
} else {
|
||||
mon_workarea.height()
|
||||
output_size.1
|
||||
};
|
||||
|
||||
let (popup_width, popup_height) = window.size();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use crate::{send_async, spawn};
|
||||
use crate::channels::AsyncSenderExt;
|
||||
use crate::spawn;
|
||||
use color_eyre::eyre::WrapErr;
|
||||
use color_eyre::{Report, Result};
|
||||
use serde::Deserialize;
|
||||
|
|
@ -216,7 +217,7 @@ impl Script {
|
|||
}
|
||||
Err(err) => error!("{err:?}"),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
sleep(tokio::time::Duration::from_millis(self.interval)).await;
|
||||
}
|
||||
|
|
@ -302,11 +303,11 @@ impl Script {
|
|||
_ = handle.wait() => break,
|
||||
Ok(Some(line)) = stdout_lines.next_line() => {
|
||||
debug!("sending stdout line: '{line}'");
|
||||
send_async!(tx, OutputStream::Stdout(line));
|
||||
tx.send_expect(OutputStream::Stdout(line)).await;
|
||||
}
|
||||
Ok(Some(line)) = stderr_lines.next_line() => {
|
||||
debug!("sending stderr line: '{line}'");
|
||||
send_async!(tx, OutputStream::Stderr(line));
|
||||
tx.send_expect(OutputStream::Stderr(line)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
21
src/style.rs
21
src/style.rs
|
|
@ -1,10 +1,11 @@
|
|||
use crate::{glib_recv_mpsc, spawn, try_send};
|
||||
use crate::channels::{AsyncSenderExt, MpscReceiverExt};
|
||||
use crate::spawn;
|
||||
use color_eyre::{Help, Report};
|
||||
use gtk::ffi::GTK_STYLE_PROVIDER_PRIORITY_USER;
|
||||
use gtk::prelude::CssProviderExt;
|
||||
use gtk::{gdk, gio, CssProvider, StyleContext};
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Application, CssProvider, StyleContext, gdk, gio};
|
||||
use notify::event::ModifyKind;
|
||||
use notify::{recommended_watcher, Event, EventKind, RecursiveMode, Result, Watcher};
|
||||
use notify::{Event, EventKind, RecursiveMode, Result, Watcher, recommended_watcher};
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
|
@ -17,7 +18,7 @@ use tracing::{debug, error, info};
|
|||
///
|
||||
/// Installs a file watcher and reloads CSS when
|
||||
/// write changes are detected on the file.
|
||||
pub fn load_css(style_path: PathBuf) {
|
||||
pub fn load_css(style_path: PathBuf, application: Application) {
|
||||
// file watcher requires absolute path
|
||||
let style_path = if style_path.is_absolute() {
|
||||
style_path
|
||||
|
|
@ -34,7 +35,7 @@ pub fn load_css(style_path: PathBuf) {
|
|||
.suggestion("Check the CSS file for errors")
|
||||
.suggestion("GTK CSS uses a subset of the full CSS spec and many properties are not available. Ensure you are not using any unsupported property.")
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
let screen = gdk::Screen::default().expect("Failed to get default GTK screen");
|
||||
StyleContext::add_provider_for_screen(
|
||||
|
|
@ -51,7 +52,7 @@ pub fn load_css(style_path: PathBuf) {
|
|||
Ok(event) if matches!(event.kind, EventKind::Modify(ModifyKind::Data(_))) => {
|
||||
debug!("{event:?}");
|
||||
if event.paths.first().is_some_and(|p| p == &style_path2) {
|
||||
try_send!(tx, style_path2.clone());
|
||||
tx.send_spawn(style_path2.clone());
|
||||
}
|
||||
}
|
||||
Err(e) => error!("Error occurred when watching stylesheet: {:?}", e),
|
||||
|
|
@ -72,7 +73,7 @@ pub fn load_css(style_path: PathBuf) {
|
|||
}
|
||||
});
|
||||
|
||||
glib_recv_mpsc!(rx, path => {
|
||||
rx.recv_glib((), move |(), path| {
|
||||
info!("Reloading CSS");
|
||||
if let Err(err) = provider.load_from_file(&gio::File::for_path(path)) {
|
||||
error!("{:?}", Report::new(err)
|
||||
|
|
@ -80,6 +81,10 @@ pub fn load_css(style_path: PathBuf) {
|
|||
.suggestion("Check the CSS file for errors")
|
||||
.suggestion("GTK CSS uses a subset of the full CSS spec and many properties are not available. Ensure you are not using any unsupported property.")
|
||||
);
|
||||
} else {
|
||||
for win in application.windows() {
|
||||
win.queue_draw();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue