diff --git a/Cargo.lock b/Cargo.lock
index d6cbca5..0fcb012 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1361,6 +1361,12 @@ dependencies = [
"litrs",
]
+[[package]]
+name = "downcast-rs"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
+
[[package]]
name = "dunce"
version = "1.0.5"
@@ -2735,6 +2741,9 @@ dependencies = [
"thiserror 2.0.21",
"time-format",
"tokio",
+ "wayland-client",
+ "wayland-protocols",
+ "zbus",
]
[[package]]
@@ -4249,6 +4258,15 @@ dependencies = [
"pulldown-cmark",
]
+[[package]]
+name = "quick-xml"
+version = "0.41.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1"
+dependencies = [
+ "memchr",
+]
+
[[package]]
name = "quinn"
version = "0.11.12"
@@ -6108,6 +6126,63 @@ dependencies = [
"unicode-ident",
]
+[[package]]
+name = "wayland-backend"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078"
+dependencies = [
+ "cc",
+ "downcast-rs",
+ "rustix",
+ "smallvec",
+ "wayland-sys",
+]
+
+[[package]]
+name = "wayland-client"
+version = "0.31.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073"
+dependencies = [
+ "bitflags 2.13.2",
+ "rustix",
+ "wayland-backend",
+ "wayland-scanner",
+]
+
+[[package]]
+name = "wayland-protocols"
+version = "0.32.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6"
+dependencies = [
+ "bitflags 2.13.2",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-scanner",
+]
+
+[[package]]
+name = "wayland-scanner"
+version = "0.31.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0"
+dependencies = [
+ "proc-macro2",
+ "quick-xml",
+ "quote",
+]
+
+[[package]]
+name = "wayland-sys"
+version = "0.31.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be"
+dependencies = [
+ "pkg-config",
+]
+
[[package]]
name = "web-sys"
version = "0.3.106"
diff --git a/Cargo.toml b/Cargo.toml
index 1f28e27..34d29df 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -13,7 +13,7 @@ glib = "0.22.10"
mpris = "2.1.0"
time-format = "1.2.4"
nmrs = "3.5.7"
-tokio = { version = "1.53.3", features = ["rt", "time", "net", "sync", "rt-multi-thread", "io-util", "macros"] }
+tokio = { version = "1.53.3", features = ["rt", "time", "net", "sync", "rt-multi-thread", "io-util", "macros", "signal"] }
futures-util = "0.3"
serde_json = "1.0.154"
crossbeam-channel = "0.5.17"
@@ -32,6 +32,10 @@ http-body-util = "0.1.5"
brightness = { version = "0.8.0", features = ["blocking"] }
battery = "0.7.8"
ddc-hi = { git = "https://github.com/jmb05/ddc-hi-rs" }
+wayland-protocols = { version = "0.32.13", features = ["staging", "client"] }
+wayland-client = "0.31.15"
+zbus = "5.19.0"
+#zbus = "6.1.0"
[build-dependencies]
glib-build-tools = "0.22.8"
diff --git a/icons/lan-symbolic.svg b/icons/lan-symbolic.svg
new file mode 100644
index 0000000..cc26d0c
--- /dev/null
+++ b/icons/lan-symbolic.svg
@@ -0,0 +1,5 @@
+
+
diff --git a/icons/plus-symbolic.svg b/icons/plus-symbolic.svg
new file mode 100644
index 0000000..79102c6
--- /dev/null
+++ b/icons/plus-symbolic.svg
@@ -0,0 +1,3 @@
+
+
diff --git a/src/backend.rs b/src/backend.rs
index 7648589..6517691 100644
--- a/src/backend.rs
+++ b/src/backend.rs
@@ -1,22 +1,22 @@
pub mod audio;
pub mod brightness_control;
+pub(crate) mod idle;
+pub(crate) mod json_socket;
pub mod network;
pub mod perf_stats;
-pub(crate) mod tailscale;
pub mod window_manager_hypr;
use crate::LOGGER;
use crate::backend::audio::{MediaBackend, MediaCommand};
use crate::backend::brightness_control::{BrightnessManager, BrightnessMessage};
+use crate::backend::idle::{IdleDaemon, IdleMessage, IdleState};
+use crate::backend::json_socket::{JsonSocketBackend, State};
use crate::backend::network::{NetworkBackendHandle, NetworkCommand, NetworkState};
use crate::backend::perf_stats::{Stats, StatsBackend};
-use crate::backend::tailscale::{TailscaleBackend, TailscaleState};
use crate::backend::window_manager_hypr::{HyprlandComms, WindowManagerEvent};
use crossbeam_channel::{Receiver, Sender};
use jmb_logger_rs::*;
use std::fmt::Debug;
-use std::process::{Child, Command, Stdio};
-use std::sync::{Arc, Mutex};
use tokio::sync::mpsc::UnboundedSender;
pub(crate) type Registrator = Sender>;
@@ -44,6 +44,27 @@ pub trait BackendThread {
}
}
+pub trait SimpleBackendThread: BackendThread {
+ fn subscribers(&self) -> &Vec>;
+ fn init(registration_rx: Receiver>);
+}
+
+impl> BackendThread for S {
+ fn subscribe(registrator: Registrator) -> (Receiver, ()) {
+ let (tx, rx) = crossbeam_channel::unbounded::();
+ let _ = registrator.send(tx);
+ (rx, ())
+ }
+
+ fn subscribers(&self) -> &Vec> {
+ SimpleBackendThread::subscribers(self)
+ }
+
+ fn init(registration_rx: Receiver>) {
+ >::init(registration_rx)
+ }
+}
+
#[derive(Clone)]
pub struct Backend {
hypr_registrator: Registrator,
@@ -51,8 +72,8 @@ pub struct Backend {
br_registrator: Registrator,
stats_registrator: Registrator,
media_registrator: Registrator,
- tailscale_registrator: Registrator,
- hypridle: Arc>>,
+ tailscale_registrator: Registrator,
+ idle_registrator: Registrator,
}
impl Backend {
@@ -63,10 +84,8 @@ impl Backend {
let br_registrator = BrightnessManager::start();
let stats_registrator = StatsBackend::start();
let media_registrator = MediaBackend::start();
- let tailscale_registrator = TailscaleBackend::start();
- let hypridle = Arc::new(Mutex::new(
- Command::new("hypridle").stdout(Stdio::null()).spawn().ok(),
- ));
+ let tailscale_registrator = JsonSocketBackend::start();
+ let idle_registrator = IdleDaemon::start();
debug!(&LOGGER, "Started Backend threads");
Self {
hypr_registrator,
@@ -75,7 +94,7 @@ impl Backend {
stats_registrator,
media_registrator,
tailscale_registrator,
- hypridle,
+ idle_registrator,
}
}
@@ -85,7 +104,8 @@ impl Backend {
let (br_rx, br_tx) = BrightnessManager::subscribe(self.br_registrator.clone());
let (stats_rx, _) = StatsBackend::subscribe(self.stats_registrator.clone());
let (media_rx, media_tx) = MediaBackend::subscribe(self.media_registrator.clone());
- let (tailscale_rx, _) = TailscaleBackend::subscribe(self.tailscale_registrator.clone());
+ let (tailscale_rx, _) = JsonSocketBackend::subscribe(self.tailscale_registrator.clone());
+ let (idle_rx, idle_tx) = IdleDaemon::subscribe(self.idle_registrator.clone());
BackendChannels {
net_rx: Some(net_rx),
net_tx,
@@ -96,13 +116,10 @@ impl Backend {
media_rx: Some(media_rx),
media_tx,
tailscale_rx: Some(tailscale_rx),
- hypridle: self.hypridle.clone(),
+ idle_rx: Some(idle_rx),
+ idle_tx,
}
}
-
- pub fn create_drop_guard(&self) -> DropGuard {
- DropGuard(self.hypridle.clone())
- }
}
pub struct BackendChannels {
@@ -114,8 +131,9 @@ pub struct BackendChannels {
stats_rx: Option>,
media_rx: Option>,
media_tx: Sender,
- tailscale_rx: Option>,
- hypridle: Arc>>,
+ tailscale_rx: Option>,
+ idle_rx: Option>,
+ idle_tx: Sender,
}
impl BackendChannels {
@@ -131,12 +149,15 @@ impl BackendChannels {
pub fn take_stats_rx(&mut self) -> Option> {
self.stats_rx.take()
}
- pub fn take_ts_rx(&mut self) -> Option> {
+ pub fn take_netbird_rx(&mut self) -> Option> {
self.tailscale_rx.take()
}
pub fn take_media_rx(&mut self) -> Option> {
self.media_rx.take()
}
+ pub fn take_idle_rx(&mut self) -> Option> {
+ self.idle_rx.take()
+ }
pub fn net_tx(&self) -> UnboundedSender {
self.net_tx.clone()
}
@@ -146,21 +167,7 @@ impl BackendChannels {
pub fn media_tx(&self) -> Sender {
self.media_tx.clone()
}
- pub fn hypridle(&self) -> Arc>> {
- self.hypridle.clone()
- }
-}
-
-pub struct DropGuard(Arc>>);
-impl Drop for DropGuard {
- fn drop(&mut self) {
- let mut hypridle = self
- .0
- .lock()
- .expect("Couldn't get lock on hypridle child process handle");
- if hypridle.is_some() {
- info!(&LOGGER, "Killing hypridle");
- let _ = hypridle.as_mut().unwrap().kill();
- }
+ pub fn idle_tx(&self) -> Sender {
+ self.idle_tx.clone()
}
}
diff --git a/src/backend/brightness_control.rs b/src/backend/brightness_control.rs
index 7d7c0c4..308cb24 100644
--- a/src/backend/brightness_control.rs
+++ b/src/backend/brightness_control.rs
@@ -4,8 +4,9 @@ use brightness::blocking::{Brightness, BrightnessDevice};
use crossbeam_channel::{Receiver, Sender};
use ddc_hi::Ddc;
use jmb_logger_rs::*;
-use std::collections::HashMap;
+use std::collections::{HashMap, HashSet};
use std::error::Error;
+use std::fmt::{Debug, Formatter};
use std::os::unix::prelude::MetadataExt;
use std::path::PathBuf;
use std::process::Command;
@@ -18,6 +19,20 @@ pub enum DisplayBackend {
DdcCi(Box),
}
+impl Debug for DisplayBackend {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ match self {
+ DisplayBackend::Sysfs(dev) => {
+ return dev.fmt(f);
+ }
+ DisplayBackend::DdcCi(displ) => {
+ f.write_str(&displ.info.id).expect("Error");
+ }
+ }
+ Ok(())
+ }
+}
+
fn get_sysfs_dev(id: &str) -> Option {
brightness::blocking::brightness_devices()
.find(|dev| {
@@ -62,26 +77,45 @@ impl DisplayBackend {
#[derive(Debug, Clone)]
pub enum BrightnessMessage {
Init(Sender),
- Value(String, u8),
+ Value(String, u8, bool),
+ Revert(String),
Request(String),
}
-struct DisplayData(DisplayBackend, u8);
+#[derive(Debug)]
+struct DisplayData {
+ backend: DisplayBackend,
+ value: u8,
+ saved_value: Option,
+}
impl DisplayData {
+ fn new(backend: DisplayBackend, value: u8) -> Self {
+ Self {
+ backend,
+ value,
+ saved_value: None,
+ }
+ }
+
+ fn save(&mut self) -> Option<&u8> {
+ self.saved_value = Some(self.value);
+ self.saved_value.as_ref()
+ }
+
fn update(&mut self) -> bool {
- if let Some(value) = self.0.get_brightness()
- && self.1 != value
+ if let Some(value) = self.backend.get_brightness()
+ && self.value != value
{
- self.1 = value;
+ self.value = value;
return true;
}
false
}
fn set_brightness(&mut self, percent: u8) {
- self.0.set_brightness(percent);
- self.1 = percent;
+ self.backend.set_brightness(percent);
+ self.value = percent;
}
}
@@ -119,7 +153,7 @@ impl BackendThread> for BrightnessM
thread::spawn(move || {
while let Ok(tx) = registration_rx.recv() {
if tx.send(BrightnessMessage::Init(f2b_tx.clone())).is_ok() {
- trace!(&LOGGER, "Added Brightness Subscriber: {tx:?}");
+ debug!(&LOGGER, "Added Brightness Subscriber: {tx:?}");
instance_clone
.lock()
.expect("Couldn't get lock")
@@ -130,36 +164,53 @@ impl BackendThread> for BrightnessM
});
let instance_clone = instance.clone();
thread::spawn(move || {
- while let Ok(mut msg) = f2b_rx.recv() {
- while let Ok(newer) = f2b_rx.try_recv() {
- msg = newer;
- }
- match msg {
- BrightnessMessage::Value(id, val) => {
- instance_clone
- .lock()
- .expect("Couldn't get lock")
- .set_brightness(id, val);
+ loop {
+ // Ensure all Messages are handled in the correct order
+ let mut operations: HashMap> = HashMap::new();
+ let mut requests: HashSet = HashSet::new();
+ if let Ok(msg) = f2b_rx.recv() {
+ Self::insert_message_operation(&mut operations, &mut requests, msg);
+ while let Ok(msg) = f2b_rx.try_recv() {
+ Self::insert_message_operation(&mut operations, &mut requests, msg);
}
- BrightnessMessage::Request(id) => {
- instance_clone
- .lock()
- .expect("Couldn't get lock")
- .register_display(id.clone());
- if let Some(val) = instance_clone
- .lock()
- .expect("Couldn't get lock")
- .display_cache
- .get(&id)
- .map(|v| v.1)
- {
- instance_clone
- .lock()
- .expect("Couldn't get lock")
- .publish(BrightnessMessage::Value(id, val));
+ }
+
+ let mut manager = instance_clone.lock().unwrap();
+ for (id, operations) in operations {
+ let mut last = manager
+ .display_cache
+ .get(&id)
+ .and_then(|d| Some(d.value))
+ .unwrap_or(60);
+ let mut revert_flag = false;
+ for operation in operations {
+ match operation {
+ Operation::Set(val, save) => {
+ if save {
+ manager.display_cache.get_mut(&id).and_then(|d| d.save());
+ }
+ last = val;
+ revert_flag = false;
+ }
+ Operation::Revert => {
+ last = manager
+ .display_cache
+ .get_mut(&id)
+ .and_then(|d| d.saved_value.take())
+ .unwrap_or(last);
+ revert_flag = true;
+ }
}
}
- _ => {}
+ if revert_flag {
+ manager.publish(BrightnessMessage::Value(id.clone(), last, false));
+ }
+ manager.set_brightness(id, last);
+ }
+ for id in requests {
+ if let Some(val) = manager.get_brightness(id.clone()) {
+ manager.publish(BrightnessMessage::Value(id, val, false));
+ }
}
}
});
@@ -168,9 +219,11 @@ impl BackendThread> for BrightnessM
thread::sleep(Duration::from_millis(10_000));
let mut messages = vec![];
- for monitor in &mut instance.lock().expect("Couldn't get lock").display_cache {
- if monitor.1.update() {
- messages.push(BrightnessMessage::Value(monitor.0.clone(), monitor.1.1));
+ for (monitor, data) in
+ &mut instance.lock().expect("Couldn't get lock").display_cache
+ {
+ if data.update() {
+ messages.push(BrightnessMessage::Value(monitor.clone(), data.value, false));
}
}
for message in messages {
@@ -181,6 +234,12 @@ impl BackendThread> for BrightnessM
}
}
+#[derive(Debug)]
+enum Operation {
+ Set(u8, bool),
+ Revert,
+}
+
impl BrightnessManager {
pub fn set_brightness(&mut self, connector: String, percent: u8) {
if !self.display_cache.contains_key(&connector) {
@@ -191,12 +250,12 @@ impl BrightnessManager {
}
}
- pub fn _get_brightness(&mut self, connector: String) -> Option {
+ pub fn get_brightness(&mut self, connector: String) -> Option {
if !self.display_cache.contains_key(&connector) {
self.register_display(connector.clone());
}
if let Some(display) = self.display_cache.get_mut(&connector) {
- display.0.get_brightness()
+ display.backend.get_brightness()
} else {
None
}
@@ -220,7 +279,7 @@ impl BrightnessManager {
.to_str()
.is_some_and(|name| name.eq(&format!("card1-{connector}")))
{
- println!("Found sysfs display {connector}");
+ debug!(&LOGGER, "Found sysfs display {connector}");
let path = bl_path.file_name();
let Some(name) = path.to_str() else { continue };
let Some(device) = get_sysfs_dev(name) else {
@@ -229,7 +288,7 @@ impl BrightnessManager {
let mut backend = DisplayBackend::Sysfs(device);
let value = backend.get_brightness().unwrap_or(60);
self.display_cache
- .insert(connector.clone(), DisplayData(backend, value));
+ .insert(connector.clone(), DisplayData::new(backend, value));
return;
}
}
@@ -239,13 +298,13 @@ impl BrightnessManager {
&& let Ok(data) = fs::metadata(path)
{
let id = data.rdev();
- println!("Found DDC/CI display {connector}");
+ debug!(&LOGGER, "Found DDC/CI display {connector}");
for display in ddc_hi::Display::enumerate() {
if id.to_string().eq(&display.info.id) {
let mut backend = DisplayBackend::DdcCi(Box::new(display));
let value = backend.get_brightness().unwrap_or(60);
self.display_cache
- .insert(connector.clone(), DisplayData(backend, value));
+ .insert(connector.clone(), DisplayData::new(backend, value));
break;
}
}
@@ -318,4 +377,35 @@ impl BrightnessManager {
)
.into())
}
+
+ fn insert_message_operation(
+ operations: &mut HashMap>,
+ requests: &mut HashSet,
+ msg: BrightnessMessage,
+ ) {
+ match msg {
+ BrightnessMessage::Value(id, val, save) => {
+ if !operations.contains_key(&id) {
+ operations.insert(id.clone(), Vec::new());
+ }
+ let ops = operations.get_mut(&id);
+ if let Some(ops) = ops {
+ ops.push(Operation::Set(val, save));
+ }
+ }
+ BrightnessMessage::Revert(id) => {
+ if !operations.contains_key(&id) {
+ operations.insert(id.clone(), Vec::new());
+ }
+ let ops = operations.get_mut(&id);
+ if let Some(ops) = ops {
+ ops.push(Operation::Revert);
+ }
+ }
+ BrightnessMessage::Request(id) => {
+ requests.insert(id);
+ }
+ _ => {}
+ }
+ }
}
diff --git a/src/backend/idle.rs b/src/backend/idle.rs
new file mode 100644
index 0000000..034102d
--- /dev/null
+++ b/src/backend/idle.rs
@@ -0,0 +1,454 @@
+use crate::backend::{BackendThread, Registrator};
+use crate::{APP_NAME, LOGGER};
+use crossbeam_channel::{Receiver, Sender};
+use jmb_logger_rs::*;
+use nmrs::raw::zbus::interface;
+use std::collections::HashMap;
+use std::error::Error;
+use std::sync::{Arc, Mutex};
+use std::time::Duration;
+use tokio::sync::watch;
+use tokio::time::sleep;
+use wayland_client::globals::registry_queue_init;
+use wayland_client::protocol::wl_registry::WlRegistry;
+use wayland_client::protocol::wl_seat::WlSeat;
+use wayland_client::{Connection, Dispatch, QueueHandle, delegate_noop};
+use wayland_protocols::ext::idle_notify::v1::client::ext_idle_notification_v1::{
+ Event, ExtIdleNotificationV1,
+};
+use wayland_protocols::ext::idle_notify::v1::client::ext_idle_notifier_v1::ExtIdleNotifierV1;
+use zbus;
+use zbus::conn::Builder;
+use zbus::message::Header;
+
+const SERVICE: &str = "org.freedesktop.ScreenSaver";
+const OBJECT_PATH: &str = "/org/freedesktop/ScreenSaver";
+const LEGACY_OBJECT_PATH: &str = "/ScreenSaver";
+
+struct WlState {
+ idle: Option,
+ tx: watch::Sender,
+}
+
+delegate_noop!(WlState: ignore WlSeat);
+
+impl Dispatch for WlState {
+ fn event(
+ _: &mut Self,
+ _: &WlRegistry,
+ _: wayland_client::protocol::wl_registry::Event,
+ _: &wayland_client::globals::GlobalListContents,
+ _: &Connection,
+ _: &QueueHandle,
+ ) {
+ }
+}
+
+impl Dispatch for WlState {
+ fn event(
+ state: &mut Self,
+ _: &ExtIdleNotificationV1,
+ event: Event,
+ _: &(),
+ _: &Connection,
+ _: &QueueHandle,
+ ) {
+ match event {
+ Event::Idled => state.idle = Some(true),
+ Event::Resumed => state.idle = Some(false),
+ _ => {}
+ }
+ if let Some(idle) = state.idle {
+ state.tx.send_modify(|state| {
+ state.wayland_idle = idle;
+ });
+ }
+ }
+}
+
+impl Dispatch for WlState {
+ fn event(
+ _: &mut WlState,
+ _: &ExtIdleNotifierV1,
+ event: ::Event,
+ _: &(),
+ _: &Connection,
+ _: &QueueHandle,
+ ) {
+ match event {
+ _ => {}
+ }
+ }
+}
+
+#[derive(Debug)]
+struct Inhibitor {
+ owner: String,
+ application: String,
+ reason: String,
+}
+
+#[derive(Clone, Copy, Debug)]
+struct IdleStateInner {
+ dimmed: bool,
+ wayland_idle: bool,
+ dbus_inhibited: bool,
+ caffeine: bool,
+}
+
+impl IdleStateInner {
+ fn should_dim(&self) -> bool {
+ self.wayland_idle && !self.dbus_inhibited
+ }
+}
+
+impl Into for IdleStateInner {
+ fn into(self) -> IdleState {
+ if self.should_dim() {
+ IdleState::Dimmed
+ } else if self.caffeine {
+ IdleState::Caffeine
+ } else if self.dbus_inhibited {
+ IdleState::Inhibited
+ } else {
+ IdleState::Active
+ }
+ }
+}
+
+#[derive(Debug, Clone)]
+pub enum IdleState {
+ Init(Sender),
+ Dimmed,
+ Inhibited,
+ Caffeine,
+ Active,
+}
+
+#[derive(Debug, Clone)]
+pub enum IdleMessage {
+ Inhibit,
+ Release,
+}
+
+#[derive(Debug)]
+pub struct IdleDaemon {
+ subscribers: Vec>,
+}
+
+impl BackendThread> for IdleDaemon {
+ fn subscribe(
+ registrator: Registrator,
+ ) -> (Receiver, Sender) {
+ let (tx, rx) = crossbeam_channel::unbounded::();
+ let _ = registrator.send(tx);
+ while let Ok(message) = rx.recv() {
+ return match message {
+ IdleState::Init(sender) => (rx, sender),
+ _ => continue,
+ };
+ }
+ panic!("Channel closed unexpectedly, without returning anything")
+ }
+
+ fn subscribers(&self) -> &Vec> {
+ &self.subscribers
+ }
+
+ fn init(registration_rx: Receiver>) {
+ let (tx, rx) = crossbeam_channel::unbounded::();
+ let tx_clone = tx.clone();
+ let instance = Arc::new(Mutex::new(Self {
+ subscribers: Vec::new(),
+ }));
+ let instance_clone = instance.clone();
+ std::thread::spawn(move || {
+ while let Ok(sender) = registration_rx.recv() {
+ if sender.send(IdleState::Init(tx_clone.clone())).is_ok() {
+ trace!(&LOGGER, "Added IdleDaemon Subscriber");
+ instance_clone
+ .lock()
+ .expect("Couldn't get lock")
+ .subscribers
+ .push(sender);
+ }
+ }
+ });
+ let (state_tx, state_rx) = watch::channel(IdleStateInner {
+ dimmed: false,
+ wayland_idle: false,
+ dbus_inhibited: false,
+ caffeine: false,
+ });
+ let tx_clone = state_tx.clone();
+ std::thread::spawn(move || {
+ if let Err(err) = Self::start_idle_notifier(tx_clone) {
+ error!(&LOGGER, "Idle Daemon unexpectedly quit, reason: {err}");
+ }
+ });
+ std::thread::spawn(move || {
+ let runtime = tokio::runtime::Builder::new_current_thread()
+ .enable_all()
+ .build()
+ .expect("Failed to create Tokio runtime");
+ runtime.block_on(async move {
+ let mut idle_inhibitor = None;
+ while let Ok(msg) = rx.recv() {
+ match msg {
+ IdleMessage::Inhibit => {
+ if idle_inhibitor.is_none()
+ && let Ok(ii) = IdleInhibitor::inhibit_idle().await
+ {
+ idle_inhibitor = Some(ii);
+ }
+ }
+ IdleMessage::Release => {
+ if let Some(ii) = idle_inhibitor.take()
+ && let Err(e) = ii.uninhibit_idle().await
+ {
+ warn!(&LOGGER, "Error uninhibiting {e}");
+ }
+ }
+ }
+ }
+ });
+ });
+ std::thread::spawn(move || {
+ let runtime = tokio::runtime::Builder::new_current_thread()
+ .enable_all()
+ .build()
+ .expect("Failed to create Tokio runtime");
+
+ runtime.block_on(async move {
+ let res = IdleDaemon::start_dbus(state_tx).await;
+ match res {
+ Ok((_, _)) => {
+ Self::run_idle_timer(instance, state_rx, Duration::from_secs(10)).await;
+ }
+ Err(err) => error!(&LOGGER, "Error running D-Bus Screensaver: {err}"),
+ }
+ });
+ });
+ }
+}
+
+impl IdleDaemon {
+ async fn run_idle_timer(
+ instance: Arc>,
+ mut state_rx: watch::Receiver,
+ timeout: Duration,
+ ) {
+ loop {
+ let mut state = *state_rx.borrow_and_update();
+ instance.lock().unwrap().publish(state.into());
+
+ if !state.should_dim() {
+ if state_rx.changed().await.is_err() {
+ return;
+ }
+
+ continue;
+ }
+
+ tokio::select! {
+ _ = sleep(timeout) => {
+ state.dimmed = true;
+ instance.lock().unwrap().publish(state.into());
+ }
+ result = state_rx.changed() => {
+ if result.is_err() {
+ return;
+ }
+ state.dimmed = false;
+ instance.lock().unwrap().publish(state.into());
+ }
+ }
+ }
+ }
+
+ fn start_idle_notifier(tx: watch::Sender) -> Result<(), Box> {
+ let conn = Connection::connect_to_env()?;
+ let (globals, mut event_queue) = registry_queue_init::(&conn)?;
+ let queue_handle = event_queue.handle();
+
+ let seat = globals.bind::(&queue_handle, 1..=9, ())?;
+
+ let notifier = globals.bind::(&queue_handle, 2..=2, ())?;
+
+ let mut state = WlState { idle: None, tx };
+
+ let _notification = notifier.get_idle_notification(10_000, &seat, &queue_handle, ());
+
+ loop {
+ event_queue.blocking_dispatch(&mut state)?;
+ }
+ }
+
+ async fn start_dbus(
+ tx: watch::Sender,
+ ) -> zbus::Result<(ScreenSaver, zbus::connection::Connection)> {
+ let service = ScreenSaver::new(tx);
+
+ let connection = Builder::session()?
+ .name(SERVICE)?
+ .serve_at(OBJECT_PATH, service.clone())?
+ .serve_at(LEGACY_OBJECT_PATH, service.clone())?
+ .build()
+ .await?;
+
+ info!(&LOGGER, "Providing {SERVICE}");
+
+ Ok((service, connection))
+ }
+}
+
+pub struct IdleInhibitor {
+ connection: zbus::Connection,
+ cookie: u32,
+}
+
+impl IdleInhibitor {
+ async fn inhibit_idle() -> zbus::Result {
+ let connection = zbus::Connection::session().await?;
+
+ let reply = connection
+ .call_method(
+ Some("org.freedesktop.ScreenSaver"),
+ "/org/freedesktop/ScreenSaver",
+ Some("org.freedesktop.ScreenSaver"),
+ "Inhibit",
+ &(APP_NAME, "Caffeine mode active"),
+ )
+ .await?;
+
+ let cookie = reply.body().deserialize::()?;
+
+ Ok(Self { connection, cookie })
+ }
+
+ async fn uninhibit_idle(self) -> zbus::Result<()> {
+ self.connection
+ .call_method(
+ Some("org.freedesktop.ScreenSaver"),
+ "/org/freedesktop/ScreenSaver",
+ Some("org.freedesktop.ScreenSaver"),
+ "UnInhibit",
+ &(self.cookie,),
+ )
+ .await?;
+
+ Ok(())
+ }
+}
+
+#[derive(Clone)]
+struct ScreenSaver {
+ inner: Arc>,
+}
+
+struct Inner {
+ next_cookie: u32,
+ inhibitors: HashMap,
+ tx: watch::Sender,
+}
+
+impl ScreenSaver {
+ fn new(tx: watch::Sender) -> Self {
+ Self {
+ inner: Arc::new(Mutex::new(Inner {
+ next_cookie: 1,
+ inhibitors: HashMap::new(),
+ tx,
+ })),
+ }
+ }
+}
+
+#[interface(name = "org.freedesktop.ScreenSaver")]
+impl ScreenSaver {
+ #[zbus(name = "Inhibit")]
+ fn inhibit(
+ &self,
+ #[zbus(header)] header: Header<'_>,
+ application_name: String,
+ reason_for_inhibit: String,
+ ) -> zbus::fdo::Result {
+ let owner = header
+ .sender()
+ .ok_or_else(|| zbus::fdo::Error::Failed("Missing D-Bus sender".into()))?
+ .to_string();
+
+ let mut inner = self.inner.lock().unwrap();
+
+ let start = inner.next_cookie;
+ let cookie = loop {
+ let candidate = inner.next_cookie;
+ inner.next_cookie = inner.next_cookie.wrapping_add(1).max(1);
+
+ if !inner.inhibitors.contains_key(&candidate) {
+ break candidate;
+ }
+
+ if inner.next_cookie == start {
+ return Err(zbus::fdo::Error::Failed(
+ "No available inhibition cookies".into(),
+ ));
+ }
+ };
+
+ let caffeine_flag = application_name.eq(APP_NAME);
+
+ debug!(&LOGGER, "Inhibitor added: {application_name} {cookie}");
+ inner.inhibitors.insert(
+ cookie,
+ Inhibitor {
+ owner,
+ application: application_name,
+ reason: reason_for_inhibit,
+ },
+ );
+
+ inner.tx.send_modify(|state| {
+ state.dbus_inhibited = !inner.inhibitors.is_empty();
+ state.caffeine = caffeine_flag;
+ });
+ Ok(cookie)
+ }
+
+ #[zbus(name = "UnInhibit")]
+ fn uninhibit(&self, #[zbus(header)] header: Header<'_>, cookie: u32) -> zbus::fdo::Result<()> {
+ let owner = header
+ .sender()
+ .ok_or_else(|| zbus::fdo::Error::Failed("Missing D-Bus sender".into()))?
+ .to_string();
+
+ let mut inner = self.inner.lock().unwrap();
+
+ match inner.inhibitors.get(&cookie) {
+ Some(inhibitor) if inhibitor.owner == owner => {
+ if let Some(removed) = inner.inhibitors.remove(&cookie) {
+ debug!(
+ &LOGGER,
+ "Inhibitor removed: {} {cookie}", removed.application
+ );
+ }
+ let mut caffeine_flag = false;
+ for inhibitor in inner.inhibitors.values() {
+ if inhibitor.application.eq(APP_NAME) {
+ caffeine_flag = true;
+ break;
+ }
+ }
+ inner.tx.send_modify(|state| {
+ state.dbus_inhibited = !inner.inhibitors.is_empty();
+ state.caffeine = caffeine_flag;
+ });
+ Ok(())
+ }
+ Some(_) => Err(zbus::fdo::Error::AccessDenied(
+ "Cookie belongs to another D-Bus client".into(),
+ )),
+ None => Err(zbus::fdo::Error::Failed("Unknown inhibition cookie".into())),
+ }
+ }
+}
diff --git a/src/backend/json_socket.rs b/src/backend/json_socket.rs
new file mode 100644
index 0000000..06d1275
--- /dev/null
+++ b/src/backend/json_socket.rs
@@ -0,0 +1,265 @@
+use crate::LOGGER;
+use crate::backend::{BackendThread, Registrator};
+use crossbeam_channel::{Receiver, Sender};
+use http::Request;
+use http_body_util::{BodyExt, Empty, Full};
+use hyper::body::{Bytes, Incoming};
+use hyper_util::rt::TokioIo;
+use jmb_logger_rs::*;
+use std::cmp::PartialEq;
+use std::error::Error;
+use std::process::Command;
+use std::sync::{Arc, Mutex};
+use tokio::net::UnixStream;
+
+pub struct JsonSocketBackend {
+ subscribers: Vec>,
+ state: State,
+ socket_address: String,
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum State {
+ Connected,
+ Connecting,
+ Disconnected,
+ NeedsLogin,
+}
+
+impl State {
+ fn try_from_value(value: i64) -> Option {
+ match value {
+ 5 => Some(State::Connecting),
+ 6 => Some(State::Connected),
+ _ => Some(State::Disconnected),
+ }
+ }
+}
+
+impl JsonSocketBackend {
+ async fn watch(instance: Arc>) -> Result<(), Box> {
+ let socket_address = instance
+ .lock()
+ .expect("Couldn't get lock")
+ .socket_address
+ .clone();
+ let socket = UnixStream::connect(&socket_address).await?;
+ let io = TokioIo::new(socket);
+
+ let (mut sender, connection) = hyper::client::conn::http1::handshake(io).await?;
+
+ tokio::spawn(async move {
+ if let Err(err) = connection.await {
+ warn!(&LOGGER, "Tailscale socket HTTP connection error: {err}");
+ }
+ });
+
+ let request = Request::builder()
+ .method("GET")
+ .uri("http://local-tailscaled.sock/localapi/v0/watch-ipn-bus")
+ .header("Host", "local-tailscaled.sock")
+ .header("Accept", "application/json")
+ .body(Empty::::new())?;
+
+ let response = sender.send_request(request).await?;
+
+ if !response.status().is_success() {
+ return Err(format!("Tailscale API returned {}", response.status()).into());
+ }
+
+ let mut body: Incoming = response.into_body();
+ let mut pending = Vec::::new();
+
+ while let Some(frame_result) = body.frame().await {
+ let frame = frame_result?;
+
+ if let Ok(data) = frame.into_data() {
+ pending.extend_from_slice(&data);
+
+ while let Some(newline) = pending.iter().position(|&b| b == b'\n') {
+ let line: Vec = pending.drain(..=newline).collect();
+ let line = &line[..line.len() - 1];
+
+ if line.iter().all(u8::is_ascii_whitespace) {
+ continue;
+ }
+
+ match serde_json::from_slice::(line) {
+ Ok(notification) => {
+ if let Some(state) = notification.get("State")
+ && !state.is_null()
+ {
+ let state = State::try_from_value(state.as_i64().unwrap_or(4))
+ .unwrap_or(State::Disconnected);
+ instance.lock().expect("Couldn't get lock").publish(state);
+ }
+ }
+ Err(err) => {
+ warn!(&LOGGER, "Invalid Tailscale notification: {err}");
+ }
+ }
+ }
+ }
+ }
+
+ Ok(())
+ }
+
+ async fn watch_netbird(instance: Arc>) -> Result<(), Box> {
+ let socket_address = instance
+ .lock()
+ .expect("Couldn't get lock")
+ .socket_address
+ .clone();
+
+ let socket = UnixStream::connect(&socket_address).await?;
+ let io = TokioIo::new(socket);
+
+ let (mut sender, connection) = hyper::client::conn::http1::handshake(io).await?;
+
+ tokio::spawn(async move {
+ if let Err(err) = connection.await {
+ warn!(&LOGGER, "Netbird socket HTTP connection error: {err}");
+ }
+ });
+
+ let request = Request::builder()
+ .method("POST")
+ .uri("http://localhost/daemon.DaemonService/SubscribeStatus")
+ .header("Host", "localhost")
+ .header("Content-Type", "application/json")
+ .header("Accept", "application/json")
+ .body(Full::new(Bytes::from_static(b"{}")))?;
+
+ let response = sender.send_request(request).await?;
+
+ if !response.status().is_success() {
+ return Err(format!("NetBird API returned {}", response.status()).into());
+ }
+
+ let mut body = response.into_body();
+ let mut pending = Vec::::new();
+
+ while let Some(frame_result) = body.frame().await {
+ let frame = frame_result?;
+
+ if let Ok(data) = frame.into_data() {
+ pending.extend_from_slice(&data);
+
+ while let Some(newline) = pending.iter().position(|&b| b == b'\n') {
+ let line: Vec = pending.drain(..=newline).collect();
+ let line = &line[..line.len() - 1];
+
+ if line.iter().all(u8::is_ascii_whitespace) {
+ continue;
+ }
+
+ match serde_json::from_slice::(line) {
+ Ok(notification) => {
+ let Some(status) = notification
+ .get("result")
+ .and_then(|v| v.get("status").and_then(|v| v.as_str()))
+ else {
+ continue;
+ };
+ let state = match status {
+ "Connected" => State::Connected,
+ "Connecting" => State::Connecting,
+ "Idle" => State::Disconnected,
+ "NeedsLogin" => State::NeedsLogin,
+ other => {
+ warn!(&LOGGER, "Unknown NetBird status: {other}");
+ continue;
+ }
+ };
+ instance.lock().expect("Couldn't get lock").publish(state);
+ }
+ Err(err) => {
+ warn!(&LOGGER, "Invalid NetBird notification: {err}");
+ }
+ }
+ }
+ }
+ }
+
+ Ok(())
+ }
+}
+
+impl BackendThread for JsonSocketBackend {
+ fn subscribe(registrator: Registrator) -> (Receiver, ()) {
+ let (tx, rx) = crossbeam_channel::unbounded::();
+ let _ = registrator.send(tx);
+ (rx, ())
+ }
+
+ fn subscribers(&self) -> &Vec> {
+ &self.subscribers
+ }
+
+ fn init(registration_rx: Receiver>) {
+ let state = initial_state_tailscale();
+
+ let instance = Arc::new(Mutex::new(Self {
+ subscribers: vec![],
+ state,
+ socket_address: String::from("/var/run/netbird-http.sock"),
+ }));
+
+ let instance_clone = instance.clone();
+ std::thread::spawn(move || {
+ while let Ok(tx) = registration_rx.recv() {
+ trace!(&LOGGER, "Added NetBird Subscriber: {tx:?}");
+
+ let _ = tx.send(
+ instance_clone
+ .lock()
+ .expect("Couldn't get lock")
+ .state
+ .clone(),
+ );
+
+ instance_clone
+ .lock()
+ .expect("Couldn't get lock")
+ .subscribers
+ .push(tx);
+ }
+ });
+
+ std::thread::spawn(move || {
+ let runtime = tokio::runtime::Builder::new_current_thread()
+ .enable_all()
+ .build()
+ .expect("Failed to create Tokio runtime");
+
+ runtime.block_on(async move {
+ loop {
+ if let Err(e) = Self::watch_netbird(instance.clone()).await {
+ warn!(&LOGGER, "Error: {e:?}");
+ }
+ }
+ });
+ });
+ }
+}
+
+fn initial_state_tailscale() -> State {
+ if let Ok(output) = Command::new("tailscale")
+ .arg("status")
+ .arg("--json")
+ .output()
+ {
+ let val: Result = serde_json::from_slice(&output.stdout);
+ if let Ok(val) = val {
+ if let Some(res) = val.get("BackendState").and_then(|v| v.as_str())
+ && res.eq("Running")
+ {
+ return State::Connected;
+ }
+ } else {
+ error!(&LOGGER, "{:?}", val.err());
+ }
+ }
+ State::Connecting
+}
diff --git a/src/backend/tailscale.rs b/src/backend/tailscale.rs
deleted file mode 100644
index 3bcb476..0000000
--- a/src/backend/tailscale.rs
+++ /dev/null
@@ -1,188 +0,0 @@
-use crate::LOGGER;
-use crate::backend::{BackendThread, Registrator};
-use crossbeam_channel::{Receiver, Sender};
-use http::Request;
-use http_body_util::BodyExt;
-use hyper::body::Incoming;
-use hyper_util::rt::TokioIo;
-use jmb_logger_rs::*;
-use std::error::Error;
-use std::process::Command;
-use std::sync::{Arc, Mutex};
-use tokio::net::UnixStream;
-
-pub struct TailscaleBackend {
- subscribers: Vec>,
- state: TailscaleState,
-}
-
-#[derive(Debug, Clone)]
-pub enum TailscaleState {
- NoState = 0,
- InUseOtherUser = 1,
- NeedsLogin = 2,
- NeedsMachineAuth = 3,
- Stopped = 4,
- Starting = 5,
- Running = 6,
-}
-
-impl TailscaleState {
- fn try_from_value(value: i64) -> Option {
- match value {
- 0 => Some(TailscaleState::NoState),
- 1 => Some(TailscaleState::InUseOtherUser),
- 2 => Some(TailscaleState::NeedsLogin),
- 3 => Some(TailscaleState::NeedsMachineAuth),
- 4 => Some(TailscaleState::Stopped),
- 5 => Some(TailscaleState::Starting),
- 6 => Some(TailscaleState::Running),
- _ => None,
- }
- }
-}
-
-impl TailscaleBackend {
- async fn watch_tailscale(
- instance: Arc>,
- ) -> Result<(), Box> {
- let socket = UnixStream::connect("/var/run/tailscale/tailscaled.sock").await?;
- let io = TokioIo::new(socket);
-
- let (mut sender, connection) = hyper::client::conn::http1::handshake(io).await?;
-
- tokio::spawn(async move {
- if let Err(err) = connection.await {
- warn!(&LOGGER, "Tailscale socket HTTP connection error: {err}");
- }
- });
-
- let request = Request::builder()
- .method("GET")
- .uri("http://local-tailscaled.sock/localapi/v0/watch-ipn-bus")
- .header("Host", "local-tailscaled.sock")
- .header("Accept", "application/json")
- .body(http_body_util::Empty::::new())?;
-
- let response = sender.send_request(request).await?;
-
- if !response.status().is_success() {
- return Err(format!("Tailscale API returned {}", response.status()).into());
- }
-
- let mut body: Incoming = response.into_body();
- let mut pending = Vec::::new();
-
- while let Some(frame_result) = body.frame().await {
- let frame = frame_result?;
-
- if let Ok(data) = frame.into_data() {
- pending.extend_from_slice(&data);
-
- while let Some(newline) = pending.iter().position(|&b| b == b'\n') {
- let line: Vec = pending.drain(..=newline).collect();
- let line = &line[..line.len() - 1];
-
- if line.iter().all(u8::is_ascii_whitespace) {
- continue;
- }
-
- match serde_json::from_slice::(line) {
- Ok(notification) => {
- if let Some(state) = notification.get("State")
- && !state.is_null()
- {
- let state =
- TailscaleState::try_from_value(state.as_i64().unwrap_or(4))
- .unwrap_or(TailscaleState::Stopped);
- instance.lock().expect("Couldn't get lock").publish(state);
- }
- }
- Err(err) => {
- eprintln!("Invalid Tailscale notification: {err}");
- }
- }
- }
- }
- }
-
- Ok(())
- }
-}
-
-impl BackendThread for TailscaleBackend {
- fn subscribe(registrator: Registrator) -> (Receiver, ()) {
- let (tx, rx) = crossbeam_channel::unbounded::();
- let _ = registrator.send(tx);
- (rx, ())
- }
-
- fn subscribers(&self) -> &Vec> {
- &self.subscribers
- }
-
- fn init(registration_rx: Receiver>) {
- let state = initial_state();
-
- let instance = Arc::new(Mutex::new(Self {
- subscribers: vec![],
- state,
- }));
-
- let instance_clone = instance.clone();
- std::thread::spawn(move || {
- while let Ok(tx) = registration_rx.recv() {
- trace!(&LOGGER, "Added Tailscale Subscriber: {tx:?}");
-
- let _ = tx.send(
- instance_clone
- .lock()
- .expect("Couldn't get lock")
- .state
- .clone(),
- );
-
- instance_clone
- .lock()
- .expect("Couldn't get lock")
- .subscribers
- .push(tx);
- }
- });
-
- std::thread::spawn(move || {
- let runtime = tokio::runtime::Builder::new_current_thread()
- .enable_all()
- .build()
- .expect("Failed to create Tokio runtime");
-
- runtime.block_on(async move {
- loop {
- if let Err(e) = Self::watch_tailscale(instance.clone()).await {
- warn!(&LOGGER, "Error: {e:?}");
- }
- }
- });
- });
- }
-}
-
-fn initial_state() -> TailscaleState {
- if let Ok(output) = Command::new("tailscale")
- .arg("status")
- .arg("--json")
- .output()
- {
- let val: Result = serde_json::from_slice(&output.stdout);
- if let Ok(val) = val {
- if let Some(res) = val.get("BackendState").and_then(|v| v.as_str())
- && res.eq("Running")
- {
- return TailscaleState::Running;
- }
- } else {
- error!(&LOGGER, "{:?}", val.err());
- }
- }
- TailscaleState::Stopped
-}
diff --git a/src/backend/window_manager_hypr.rs b/src/backend/window_manager_hypr.rs
index 5308ed0..43f7625 100644
--- a/src/backend/window_manager_hypr.rs
+++ b/src/backend/window_manager_hypr.rs
@@ -1,5 +1,5 @@
use crate::LOGGER;
-use crate::backend::BackendThread;
+use crate::backend::{BackendThread, SimpleBackendThread};
use crossbeam_channel::{Receiver, Sender};
use hyprland::data::{Client, Monitor, Workspaces};
use hyprland::event_listener::EventListener;
@@ -23,14 +23,7 @@ pub struct HyprlandComms {
subscribers: Vec>,
}
-impl BackendThread for HyprlandComms {
- fn subscribe(
- registrator: Sender>,
- ) -> (Receiver, ()) {
- let (tx, rx) = crossbeam_channel::unbounded::();
- let _ = registrator.send(tx);
- (rx, ())
- }
+impl SimpleBackendThread for HyprlandComms {
fn subscribers(&self) -> &Vec> {
&self.subscribers
diff --git a/src/main.rs b/src/main.rs
index bc95ae9..3a9d783 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -13,23 +13,20 @@ use std::process::Command;
use std::rc::Rc;
pub const LOGGER: Logger = Logger::create(Level::DEBUG);
+pub const APP_NAME: &str = "net.jmb19905.jmb-bar";
fn main() {
info!(&LOGGER, "Starting jmb-bar");
gio::resources_register_include!("templates.gresource").expect("Failed to register resources");
- let app = Application::builder()
- .application_id("net.jmb19905.jmb-bar")
- .build();
+ let app = Application::builder().application_id(APP_NAME).build();
let watcher_slot: Rc>> = Rc::new(RefCell::new(None));
let _ = Command::new("pkill hypridle").output(); // Kill existing hypridle processes
let backend = Backend::start();
- let _guard = backend.create_drop_guard(); // Make sure hypridle gets killed when the process exits
- trace!(&LOGGER, "Initialized DropGuard");
app.connect_startup(move |_| {
let provider = CssProvider::new();
diff --git a/src/ui/title_panel.rs b/src/ui/title_panel.rs
index 33b1ea2..b3adef4 100644
--- a/src/ui/title_panel.rs
+++ b/src/ui/title_panel.rs
@@ -42,33 +42,41 @@ pub fn manage_title(button_opt: &Option