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 @@ + + + + + network-wired lan ethernet nicJakub Steiner 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 @@ + +Jakub Steinerlist-add add plus create + 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