Tailscale -> Netbird

Start Implement Idle Daemon with Dimming Display
This commit is contained in:
jmb05
2026-10-28 01:09:56 +01:00
parent 4ca6596b1d
commit 1be6e51d02
14 changed files with 1114 additions and 370 deletions

75
Cargo.lock generated
View File

@@ -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"

View File

@@ -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"

5
icons/lan-symbolic.svg Normal file
View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:gpa="https://www.gtk.org/grappa" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" gpa:rendering="symbolic" width="16" height="16" gpa:version="1" gpa:state="0" gpa:state-names="outline state1 empty"><path id="cables" fill="none" stroke="url(&quot;#gpa:foreground&quot;) rgb(0,0,0)" stroke-width="2" stroke-linecap="butt" stroke-linejoin="round" d="M 13 8 L 13 12 M 3 8 L 3 12 M 8 8 L 8 4 M 16 8 L 0 8" class="transparent-fill foreground-stroke" gpa:stroke="foreground" gpa:fill="none" gpa:states="outline state1" gpa:stroke-minwidth="25%" gpa:stroke-maxwidth="150%">
</path><path id="outline" fill="none" stroke="url(&quot;#gpa:foreground&quot;) rgb(0,0,0)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M 1 13 L 1 14 C 1 14.5522852, 1.44771528 15, 2 15 L 4 15 C 4.55228472 15, 5 14.5522852, 5 14 L 5 13 C 5 12.4477148, 4.55228472 12, 4 12 L 2 12 C 1.44771528 12, 1 12.4477148, 1 13 Z M 6 2 L 6 3 C 6 3.55228472, 6.44771528 4, 7 4 L 9 4 C 9.55228519 4, 10 3.55228472, 10 3 L 10 2 C 10 1.44771528, 9.55228519 1, 9 1 L 7 1 C 6.44771528 1, 6 1.44771528, 6 2 Z M 11 13 L 11 14 C 11 14.5522852, 11.4477148 15, 12 15 L 14 15 C 14.5522852 15, 15 14.5522852, 15 14 L 15 13 C 15 12.4477148, 14.5522852 12, 14 12 L 12 12 C 11.4477148 12, 11 12.4477148, 11 13 Z" class="transparent-fill foreground-stroke" gpa:stroke="foreground" gpa:fill="none" gpa:states="outline" gpa:stroke-minwidth="25%" gpa:stroke-maxwidth="150%">
</path><path visibility="hidden" id="fill" fill="url(&quot;#gpa:foreground&quot;) rgb(0,0,0)" fill-rule="nonzero" stroke="url(&quot;#gpa:foreground&quot;) rgb(0,0,0)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M 1 13 L 1 14 C 1 14.5522852, 1.44771528 15, 2 15 L 4 15 C 4.55228472 15, 5 14.5522852, 5 14 L 5 13 C 5 12.4477148, 4.55228472 12, 4 12 L 2 12 C 1.44771528 12, 1 12.4477148, 1 13 Z M 6 2 L 6 3 C 6 3.55228472, 6.44771528 4, 7 4 L 9 4 C 9.55228519 4, 10 3.55228472, 10 3 L 10 2 C 10 1.44771528, 9.55228519 1, 9 1 L 7 1 C 6.44771528 1, 6 1.44771528, 6 2 Z M 11 13 L 11 14 C 11 14.5522852, 11.4477148 15, 12 15 L 14 15 C 14.5522852 15, 15 14.5522852, 15 14 L 15 13 C 15 12.4477148, 14.5522852 12, 14 12 L 12 12 C 11.4477148 12, 11 12.4477148, 11 13 Z" class="foreground foreground-fill foreground-stroke" gpa:stroke="foreground" gpa:states="state1" gpa:fill="foreground" gpa:stroke-minwidth="25%" gpa:stroke-maxwidth="150%">
</path><metadata><rdf:RDF><cc:Work><cc:license rdf:resource="https://creativecommons.org/publicdomain/zero/1.0/"/><dc:subject><rdf:Bag><rdf:li>network-wired lan ethernet nic</rdf:li></rdf:Bag></dc:subject><dc:creator><cc:Agent><dc:title>Jakub Steiner</dc:title></cc:Agent></dc:creator></cc:Work></rdf:RDF></metadata></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

3
icons/plus-symbolic.svg Normal file
View File

@@ -0,0 +1,3 @@
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:gpa="https://www.gtk.org/grappa" gpa:rendering="symbolic" gpa:version="1" gpa:state-names="outline empty" gpa:state="0" width="16" height="16"><metadata><rdf:RDF><cc:Work><cc:license rdf:resource="https://creativecommons.org/publicdomain/zero/1.0/"/><dc:creator><cc:Agent><dc:title>Jakub Steiner</dc:title></cc:Agent></dc:creator><dc:subject><rdf:Bag><rdf:li>list-add add plus create</rdf:li></rdf:Bag></dc:subject></cc:Work></rdf:RDF></metadata><path id="plus" fill="none" stroke="url(#gpa:foreground) rgb(0,0,0)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M 2 8 L 14 8 M 8 2 L 8 14" gpa:stroke-minwidth="25%" gpa:stroke-maxwidth="150%" class="transparent-fill foreground-stroke" gpa:stroke="foreground" gpa:states="outline">
</path></svg>

After

Width:  |  Height:  |  Size: 1016 B

View File

@@ -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<M> = Sender<Sender<M>>;
@@ -44,6 +44,27 @@ pub trait BackendThread<M: Debug + Clone, D> {
}
}
pub trait SimpleBackendThread<M: Debug + Clone>: BackendThread<M, ()> {
fn subscribers(&self) -> &Vec<Sender<M>>;
fn init(registration_rx: Receiver<Sender<M>>);
}
impl<M: Debug + Clone, S: SimpleBackendThread<M>> BackendThread<M, ()> for S {
fn subscribe(registrator: Registrator<M>) -> (Receiver<M>, ()) {
let (tx, rx) = crossbeam_channel::unbounded::<M>();
let _ = registrator.send(tx);
(rx, ())
}
fn subscribers(&self) -> &Vec<Sender<M>> {
SimpleBackendThread::subscribers(self)
}
fn init(registration_rx: Receiver<Sender<M>>) {
<S as SimpleBackendThread<M>>::init(registration_rx)
}
}
#[derive(Clone)]
pub struct Backend {
hypr_registrator: Registrator<WindowManagerEvent>,
@@ -51,8 +72,8 @@ pub struct Backend {
br_registrator: Registrator<BrightnessMessage>,
stats_registrator: Registrator<Stats>,
media_registrator: Registrator<MediaCommand>,
tailscale_registrator: Registrator<TailscaleState>,
hypridle: Arc<Mutex<Option<Child>>>,
tailscale_registrator: Registrator<State>,
idle_registrator: Registrator<IdleState>,
}
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<Receiver<Stats>>,
media_rx: Option<Receiver<MediaCommand>>,
media_tx: Sender<MediaCommand>,
tailscale_rx: Option<Receiver<TailscaleState>>,
hypridle: Arc<Mutex<Option<Child>>>,
tailscale_rx: Option<Receiver<State>>,
idle_rx: Option<Receiver<IdleState>>,
idle_tx: Sender<IdleMessage>,
}
impl BackendChannels {
@@ -131,12 +149,15 @@ impl BackendChannels {
pub fn take_stats_rx(&mut self) -> Option<Receiver<Stats>> {
self.stats_rx.take()
}
pub fn take_ts_rx(&mut self) -> Option<Receiver<TailscaleState>> {
pub fn take_netbird_rx(&mut self) -> Option<Receiver<State>> {
self.tailscale_rx.take()
}
pub fn take_media_rx(&mut self) -> Option<Receiver<MediaCommand>> {
self.media_rx.take()
}
pub fn take_idle_rx(&mut self) -> Option<Receiver<IdleState>> {
self.idle_rx.take()
}
pub fn net_tx(&self) -> UnboundedSender<NetworkCommand> {
self.net_tx.clone()
}
@@ -146,21 +167,7 @@ impl BackendChannels {
pub fn media_tx(&self) -> Sender<MediaCommand> {
self.media_tx.clone()
}
pub fn hypridle(&self) -> Arc<Mutex<Option<Child>>> {
self.hypridle.clone()
}
}
pub struct DropGuard(Arc<Mutex<Option<Child>>>);
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<IdleMessage> {
self.idle_tx.clone()
}
}

View File

@@ -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<ddc_hi::Display>),
}
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<BrightnessDevice> {
brightness::blocking::brightness_devices()
.find(|dev| {
@@ -62,26 +77,45 @@ impl DisplayBackend {
#[derive(Debug, Clone)]
pub enum BrightnessMessage {
Init(Sender<BrightnessMessage>),
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<u8>,
}
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<BrightnessMessage, Sender<BrightnessMessage>> 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<BrightnessMessage, Sender<BrightnessMessage>> 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<String, Vec<Operation>> = HashMap::new();
let mut requests: HashSet<String> = 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<BrightnessMessage, Sender<BrightnessMessage>> 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<BrightnessMessage, Sender<BrightnessMessage>> 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<u8> {
pub fn get_brightness(&mut self, connector: String) -> Option<u8> {
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<String, Vec<Operation>>,
requests: &mut HashSet<String>,
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);
}
_ => {}
}
}
}

454
src/backend/idle.rs Normal file
View File

@@ -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<bool>,
tx: watch::Sender<IdleStateInner>,
}
delegate_noop!(WlState: ignore WlSeat);
impl Dispatch<WlRegistry, wayland_client::globals::GlobalListContents> for WlState {
fn event(
_: &mut Self,
_: &WlRegistry,
_: wayland_client::protocol::wl_registry::Event,
_: &wayland_client::globals::GlobalListContents,
_: &Connection,
_: &QueueHandle<Self>,
) {
}
}
impl Dispatch<ExtIdleNotificationV1, ()> for WlState {
fn event(
state: &mut Self,
_: &ExtIdleNotificationV1,
event: Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
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<ExtIdleNotifierV1, (), WlState> for WlState {
fn event(
_: &mut WlState,
_: &ExtIdleNotifierV1,
event: <ExtIdleNotifierV1 as wayland_client::Proxy>::Event,
_: &(),
_: &Connection,
_: &QueueHandle<WlState>,
) {
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<IdleState> 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<IdleMessage>),
Dimmed,
Inhibited,
Caffeine,
Active,
}
#[derive(Debug, Clone)]
pub enum IdleMessage {
Inhibit,
Release,
}
#[derive(Debug)]
pub struct IdleDaemon {
subscribers: Vec<Sender<IdleState>>,
}
impl BackendThread<IdleState, Sender<IdleMessage>> for IdleDaemon {
fn subscribe(
registrator: Registrator<IdleState>,
) -> (Receiver<IdleState>, Sender<IdleMessage>) {
let (tx, rx) = crossbeam_channel::unbounded::<IdleState>();
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<Sender<IdleState>> {
&self.subscribers
}
fn init(registration_rx: Receiver<Sender<IdleState>>) {
let (tx, rx) = crossbeam_channel::unbounded::<IdleMessage>();
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<Mutex<Self>>,
mut state_rx: watch::Receiver<IdleStateInner>,
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<IdleStateInner>) -> Result<(), Box<dyn Error>> {
let conn = Connection::connect_to_env()?;
let (globals, mut event_queue) = registry_queue_init::<WlState>(&conn)?;
let queue_handle = event_queue.handle();
let seat = globals.bind::<WlSeat, _, _>(&queue_handle, 1..=9, ())?;
let notifier = globals.bind::<ExtIdleNotifierV1, _, _>(&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<IdleStateInner>,
) -> 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<IdleInhibitor> {
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::<u32>()?;
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<Mutex<Inner>>,
}
struct Inner {
next_cookie: u32,
inhibitors: HashMap<u32, Inhibitor>,
tx: watch::Sender<IdleStateInner>,
}
impl ScreenSaver {
fn new(tx: watch::Sender<IdleStateInner>) -> 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<u32> {
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())),
}
}
}

265
src/backend/json_socket.rs Normal file
View File

@@ -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<Sender<State>>,
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<State> {
match value {
5 => Some(State::Connecting),
6 => Some(State::Connected),
_ => Some(State::Disconnected),
}
}
}
impl JsonSocketBackend {
async fn watch(instance: Arc<Mutex<Self>>) -> Result<(), Box<dyn Error + Send + Sync>> {
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::<Bytes>::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::<u8>::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<u8> = pending.drain(..=newline).collect();
let line = &line[..line.len() - 1];
if line.iter().all(u8::is_ascii_whitespace) {
continue;
}
match serde_json::from_slice::<serde_json::Value>(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<Mutex<Self>>) -> Result<(), Box<dyn Error + Send + Sync>> {
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::<u8>::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<u8> = pending.drain(..=newline).collect();
let line = &line[..line.len() - 1];
if line.iter().all(u8::is_ascii_whitespace) {
continue;
}
match serde_json::from_slice::<serde_json::Value>(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<State, ()> for JsonSocketBackend {
fn subscribe(registrator: Registrator<State>) -> (Receiver<State>, ()) {
let (tx, rx) = crossbeam_channel::unbounded::<State>();
let _ = registrator.send(tx);
(rx, ())
}
fn subscribers(&self) -> &Vec<Sender<State>> {
&self.subscribers
}
fn init(registration_rx: Receiver<Sender<State>>) {
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::Value, _> = 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
}

View File

@@ -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<Sender<TailscaleState>>,
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<TailscaleState> {
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<Mutex<Self>>,
) -> Result<(), Box<dyn Error + Send + Sync>> {
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::<hyper::body::Bytes>::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::<u8>::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<u8> = pending.drain(..=newline).collect();
let line = &line[..line.len() - 1];
if line.iter().all(u8::is_ascii_whitespace) {
continue;
}
match serde_json::from_slice::<serde_json::Value>(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<TailscaleState, ()> for TailscaleBackend {
fn subscribe(registrator: Registrator<TailscaleState>) -> (Receiver<TailscaleState>, ()) {
let (tx, rx) = crossbeam_channel::unbounded::<TailscaleState>();
let _ = registrator.send(tx);
(rx, ())
}
fn subscribers(&self) -> &Vec<Sender<TailscaleState>> {
&self.subscribers
}
fn init(registration_rx: Receiver<Sender<TailscaleState>>) {
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::Value, _> = 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
}

View File

@@ -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<Sender<WindowManagerEvent>>,
}
impl BackendThread<WindowManagerEvent, ()> for HyprlandComms {
fn subscribe(
registrator: Sender<Sender<WindowManagerEvent>>,
) -> (Receiver<WindowManagerEvent>, ()) {
let (tx, rx) = crossbeam_channel::unbounded::<WindowManagerEvent>();
let _ = registrator.send(tx);
(rx, ())
}
impl SimpleBackendThread<WindowManagerEvent> for HyprlandComms {
fn subscribers(&self) -> &Vec<Sender<WindowManagerEvent>> {
&self.subscribers

View File

@@ -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<RefCell<Option<RecommendedWatcher>>> = 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();

View File

@@ -42,33 +42,41 @@ pub fn manage_title(button_opt: &Option<Button>, player_opt: &Option<Player>) {
let mut handled = false;
if let Some(button) = button_opt
&& let Some(player) = player_opt
&& let Ok(status) = player.get_playback_status()
&& let Ok(metadata) = player.get_metadata()
{
match status {
PlaybackStatus::Playing => {
button.set_visible(true);
button
.child()
.and_downcast::<Label>()
.unwrap()
.set_label(&format!("{}", metadata.title().unwrap_or("")));
button.add_css_class("media");
button.add_css_class("playing");
handled = true;
//println!("{:?}", player.get_playback_status());
if let Ok(status) = player.get_playback_status()
&& let Ok(metadata) = player.get_metadata()
{
match status {
PlaybackStatus::Playing => {
button.set_visible(true);
button
.child()
.and_downcast::<Label>()
.unwrap()
.set_label(&format!("{}", metadata.title().unwrap_or("")));
button.add_css_class("media");
button.add_css_class("playing");
handled = true;
}
PlaybackStatus::Paused => {
button.set_visible(true);
button
.child()
.and_downcast::<Label>()
.unwrap()
.set_label(&format!("{}", metadata.title().unwrap_or("")));
button.add_css_class("media");
button.remove_css_class("playing");
handled = true;
}
PlaybackStatus::Stopped => {
button.set_visible(true);
button.remove_css_class("media");
button.remove_css_class("playing");
set_title(&HyprlandComms::get_current_title(), button_opt);
}
}
PlaybackStatus::Paused => {
button.set_visible(true);
button
.child()
.and_downcast::<Label>()
.unwrap()
.set_label(&format!("{}", metadata.title().unwrap_or("")));
button.add_css_class("media");
button.remove_css_class("playing");
handled = true;
}
PlaybackStatus::Stopped => {}
}
if !handled {
button.set_visible(true);

View File

@@ -1,9 +1,9 @@
use crate::LOGGER;
use crate::backend::BackendChannels;
use crate::backend::audio::MediaCommand;
use crate::backend::brightness_control::BrightnessMessage;
use crate::backend::idle::{IdleMessage, IdleState};
use crate::backend::network::{NetworkCommand, NetworkState, NetworkType};
use crate::backend::tailscale::TailscaleState;
use crate::backend::{BackendChannels, json_socket};
use crate::ui::action_centre::ActionCentre;
use crate::ui::widgets::{ExpandableBarItem, IconSlider};
use battery::State;
@@ -65,8 +65,7 @@ pub fn build(
if monitor_valid {
let _ = channels
.br_tx()
.send(BrightnessMessage::Request(monitor.clone()))
.is_ok();
.send(BrightnessMessage::Request(monitor.clone()));
}
let volume_adjustment = Adjustment::new(60.0, 1.0, 100.0, 1.0, 5.0, 0.0);
@@ -108,7 +107,11 @@ pub fn build(
move |slider, _button, _| {
if monitor_valid {
let slider_val = slider.value().max(1.0);
let _ = br_tx.send(BrightnessMessage::Value(monitor.clone(), slider_val as u8));
let _ = br_tx.send(BrightnessMessage::Value(
monitor.clone(),
slider_val as u8,
false,
));
let idx =
((slider_val / 100.0) * (BRIGHTNESS_ICONS.len() - 1) as f64 + 0.5) as usize;
slider.imp().main_icon.set_font_icon(BRIGHTNESS_ICONS[idx]);
@@ -121,7 +124,7 @@ pub fn build(
monitor,
move |slider, _button, _| {
if monitor_valid {
let _ = br_tx.send(BrightnessMessage::Value(monitor.clone(), 60));
let _ = br_tx.send(BrightnessMessage::Value(monitor.clone(), 60, false));
slider.set_value(60.0);
let idx = ((60.0 / 100.0) * (BRIGHTNESS_ICONS.len() - 1) as f64 + 0.5) as usize;
slider.imp().main_icon.set_font_icon(BRIGHTNESS_ICONS[idx]);
@@ -131,27 +134,12 @@ pub fn build(
let caffeine_button = Button::with_label("");
caffeine_button.add_css_class("caffeine");
let hypridle = channels.hypridle();
let idle_tx = channels.idle_tx();
caffeine_button.connect_clicked(move |button| {
let mut child = hypridle.lock().expect("Couldn't get lock on hypridle");
if child.is_none() {
if let Ok(new_child) = Command::new("hypridle").spawn() {
let _ = child.insert(new_child);
button.remove_css_class("on")
}
} else if child.is_some()
&& let Ok(exit) = child.as_mut().unwrap().try_wait()
&& let Some(_) = exit
{
if let Ok(new_child) = Command::new("hypridle").spawn() {
let _ = child.insert(new_child);
button.remove_css_class("on")
}
if button.has_css_class("on") {
let _ = idle_tx.send(IdleMessage::Release);
} else {
if child.as_mut().unwrap().kill().is_ok() {
let _ = child.take();
button.add_css_class("on");
}
let _ = idle_tx.send(IdleMessage::Inhibit);
}
});
@@ -228,10 +216,10 @@ pub fn build(
let _ = net_tx.send(NetworkCommand::ToggleVpn(String::from("wg")));
});
let ts_button = Button::with_label("ts");
ts_button.add_css_class("vpn");
ts_button.connect_clicked(move |button| {
let _ = Command::new("tailscale")
let netbird_button = Button::with_label("nb");
netbird_button.add_css_class("vpn");
netbird_button.connect_clicked(move |button| {
let _ = Command::new("netbird")
.arg(if button.has_css_class("on") {
"down"
} else {
@@ -249,7 +237,7 @@ pub fn build(
vpn_box.set_valign(Align::Fill);
vpn_box.set_vexpand(true);
vpn_box.append(&wg_button);
vpn_box.append(&ts_button);
vpn_box.append(&netbird_button);
//vpn_box.append(&tud_button);
//vpn_box.append(&mvd_button);
@@ -327,8 +315,10 @@ pub fn build(
let br_rx = channels.take_br_rx().unwrap();
let net_rx = channels.take_net_rx().unwrap();
let stats_rx = channels.take_stats_rx().unwrap();
let ts_rx = channels.take_ts_rx().unwrap();
let hypridle = channels.hypridle();
let netbird_rx = channels.take_netbird_rx().unwrap();
let idle_rx = channels.take_idle_rx().unwrap();
let monitor_clone = monitor.clone();
let br_tx = channels.br_tx();
glib::timeout_add_local(Duration::from_millis(100), move || {
while let Ok(command) = media_rx.try_recv() {
if let MediaCommand::Cmd(vol, mute, bt) = command {
@@ -355,7 +345,7 @@ pub fn build(
}
}
while let Ok(message) = br_rx.try_recv() {
if let BrightnessMessage::Value(mon, value) = message
if let BrightnessMessage::Value(mon, value, _) = message
&& monitor.eq(&mon)
{
brightness.set_value(value as f64);
@@ -367,17 +357,33 @@ pub fn build(
.set_font_icon(BRIGHTNESS_ICONS[idx]);
}
}
if let Ok(mut hypridle) = hypridle.try_lock() {
if hypridle.is_none() {
caffeine_button.add_css_class("on");
} else {
if let Ok(exit) = hypridle.as_mut().unwrap().try_wait()
&& exit.is_some()
{
caffeine_button.add_css_class("on");
} else {
while let Ok(state) = idle_rx.try_recv() {
match state {
IdleState::Dimmed => {
if !caffeine_button.has_css_class("idle") && monitor_valid {
let _ =
br_tx.send(BrightnessMessage::Value(monitor_clone.clone(), 20, true));
brightness.set_value(20.0);
println!("Dimmed: {monitor_clone}");
}
caffeine_button.add_css_class("idle");
caffeine_button.remove_css_class("inhibit");
caffeine_button.remove_css_class("on");
}
other => {
caffeine_button.remove_css_class("idle");
caffeine_button.remove_css_class("inhibit");
caffeine_button.remove_css_class("on");
if monitor_valid {
let _ = br_tx.send(BrightnessMessage::Revert(monitor_clone.clone()));
}
match other {
IdleState::Caffeine => caffeine_button.add_css_class("on"),
IdleState::Inhibited => caffeine_button.add_css_class("inhibit"),
IdleState::Active => {}
_ => {}
}
}
}
}
while let Ok(result) = net_rx.try_recv() {
@@ -419,34 +425,43 @@ pub fn build(
_ => {}
}
}
while let Ok(tailscale_state) = ts_rx.try_recv() {
while let Ok(tailscale_state) = netbird_rx.try_recv() {
match tailscale_state {
TailscaleState::Stopped => {
ts_button.remove_css_class("on");
ts_button.remove_css_class("starting");
ts_button.remove_css_class("unknown");
json_socket::State::Disconnected => {
netbird_button.remove_css_class("on");
netbird_button.remove_css_class("starting");
netbird_button.remove_css_class("unknown");
netbird_button.remove_css_class("login");
}
TailscaleState::Starting => {
ts_button.remove_css_class("on");
ts_button.add_css_class("starting");
ts_button.remove_css_class("unknown");
json_socket::State::Connecting => {
netbird_button.remove_css_class("on");
netbird_button.add_css_class("starting");
netbird_button.remove_css_class("unknown");
netbird_button.remove_css_class("login");
}
TailscaleState::Running => {
ts_button.add_css_class("on");
ts_button.remove_css_class("starting");
ts_button.remove_css_class("unknown");
json_socket::State::Connected => {
netbird_button.add_css_class("on");
netbird_button.remove_css_class("starting");
netbird_button.remove_css_class("unknown");
netbird_button.remove_css_class("login");
}
_ => {
ts_button.remove_css_class("on");
ts_button.remove_css_class("starting");
ts_button.add_css_class("unknown");
json_socket::State::NeedsLogin => {
netbird_button.remove_css_class("on");
netbird_button.remove_css_class("starting");
netbird_button.remove_css_class("unknown");
netbird_button.add_css_class("login");
}
}
}
if wg_button.has_css_class("on") || ts_button.has_css_class("on") {
if wg_button.has_css_class("on") || netbird_button.has_css_class("on") {
vpn_button.add_css_class("on");
vpn_button.remove_css_class("login");
} else if wg_button.has_css_class("login") || netbird_button.has_css_class("login") {
vpn_button.add_css_class("login");
vpn_button.remove_css_class("on");
} else {
vpn_button.remove_css_class("on");
vpn_button.remove_css_class("login");
}
while let Ok(stats) = stats_rx.try_recv() {
cpu_label.set_label(&format!("{}%", stats.cpu));

View File

@@ -129,6 +129,14 @@ button {
background: $red;
}
.caffeine.idle {
background: $yellow;
}
.caffeine.inhibit {
background: $green;
}
.stats {
padding: 0 6px;
}
@@ -176,6 +184,14 @@ button {
color: $lightgreen;
}
.vpn-icon.login {
color: $yellow;
}
.vpn-icon.login:hover {
color: $yellow;
}
.vpn {
color: $background;
background: $red;
@@ -192,7 +208,7 @@ button {
background: $green;
}
.vpn.starting {
.vpn.login {
background: $yellow;
}