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", "litrs",
] ]
[[package]]
name = "downcast-rs"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
[[package]] [[package]]
name = "dunce" name = "dunce"
version = "1.0.5" version = "1.0.5"
@@ -2735,6 +2741,9 @@ dependencies = [
"thiserror 2.0.21", "thiserror 2.0.21",
"time-format", "time-format",
"tokio", "tokio",
"wayland-client",
"wayland-protocols",
"zbus",
] ]
[[package]] [[package]]
@@ -4249,6 +4258,15 @@ dependencies = [
"pulldown-cmark", "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]] [[package]]
name = "quinn" name = "quinn"
version = "0.11.12" version = "0.11.12"
@@ -6108,6 +6126,63 @@ dependencies = [
"unicode-ident", "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]] [[package]]
name = "web-sys" name = "web-sys"
version = "0.3.106" version = "0.3.106"

View File

@@ -13,7 +13,7 @@ glib = "0.22.10"
mpris = "2.1.0" mpris = "2.1.0"
time-format = "1.2.4" time-format = "1.2.4"
nmrs = "3.5.7" 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" futures-util = "0.3"
serde_json = "1.0.154" serde_json = "1.0.154"
crossbeam-channel = "0.5.17" crossbeam-channel = "0.5.17"
@@ -32,6 +32,10 @@ http-body-util = "0.1.5"
brightness = { version = "0.8.0", features = ["blocking"] } brightness = { version = "0.8.0", features = ["blocking"] }
battery = "0.7.8" battery = "0.7.8"
ddc-hi = { git = "https://github.com/jmb05/ddc-hi-rs" } 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] [build-dependencies]
glib-build-tools = "0.22.8" 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 audio;
pub mod brightness_control; pub mod brightness_control;
pub(crate) mod idle;
pub(crate) mod json_socket;
pub mod network; pub mod network;
pub mod perf_stats; pub mod perf_stats;
pub(crate) mod tailscale;
pub mod window_manager_hypr; pub mod window_manager_hypr;
use crate::LOGGER; use crate::LOGGER;
use crate::backend::audio::{MediaBackend, MediaCommand}; use crate::backend::audio::{MediaBackend, MediaCommand};
use crate::backend::brightness_control::{BrightnessManager, BrightnessMessage}; 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::network::{NetworkBackendHandle, NetworkCommand, NetworkState};
use crate::backend::perf_stats::{Stats, StatsBackend}; use crate::backend::perf_stats::{Stats, StatsBackend};
use crate::backend::tailscale::{TailscaleBackend, TailscaleState};
use crate::backend::window_manager_hypr::{HyprlandComms, WindowManagerEvent}; use crate::backend::window_manager_hypr::{HyprlandComms, WindowManagerEvent};
use crossbeam_channel::{Receiver, Sender}; use crossbeam_channel::{Receiver, Sender};
use jmb_logger_rs::*; use jmb_logger_rs::*;
use std::fmt::Debug; use std::fmt::Debug;
use std::process::{Child, Command, Stdio};
use std::sync::{Arc, Mutex};
use tokio::sync::mpsc::UnboundedSender; use tokio::sync::mpsc::UnboundedSender;
pub(crate) type Registrator<M> = Sender<Sender<M>>; 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)] #[derive(Clone)]
pub struct Backend { pub struct Backend {
hypr_registrator: Registrator<WindowManagerEvent>, hypr_registrator: Registrator<WindowManagerEvent>,
@@ -51,8 +72,8 @@ pub struct Backend {
br_registrator: Registrator<BrightnessMessage>, br_registrator: Registrator<BrightnessMessage>,
stats_registrator: Registrator<Stats>, stats_registrator: Registrator<Stats>,
media_registrator: Registrator<MediaCommand>, media_registrator: Registrator<MediaCommand>,
tailscale_registrator: Registrator<TailscaleState>, tailscale_registrator: Registrator<State>,
hypridle: Arc<Mutex<Option<Child>>>, idle_registrator: Registrator<IdleState>,
} }
impl Backend { impl Backend {
@@ -63,10 +84,8 @@ impl Backend {
let br_registrator = BrightnessManager::start(); let br_registrator = BrightnessManager::start();
let stats_registrator = StatsBackend::start(); let stats_registrator = StatsBackend::start();
let media_registrator = MediaBackend::start(); let media_registrator = MediaBackend::start();
let tailscale_registrator = TailscaleBackend::start(); let tailscale_registrator = JsonSocketBackend::start();
let hypridle = Arc::new(Mutex::new( let idle_registrator = IdleDaemon::start();
Command::new("hypridle").stdout(Stdio::null()).spawn().ok(),
));
debug!(&LOGGER, "Started Backend threads"); debug!(&LOGGER, "Started Backend threads");
Self { Self {
hypr_registrator, hypr_registrator,
@@ -75,7 +94,7 @@ impl Backend {
stats_registrator, stats_registrator,
media_registrator, media_registrator,
tailscale_registrator, tailscale_registrator,
hypridle, idle_registrator,
} }
} }
@@ -85,7 +104,8 @@ impl Backend {
let (br_rx, br_tx) = BrightnessManager::subscribe(self.br_registrator.clone()); let (br_rx, br_tx) = BrightnessManager::subscribe(self.br_registrator.clone());
let (stats_rx, _) = StatsBackend::subscribe(self.stats_registrator.clone()); let (stats_rx, _) = StatsBackend::subscribe(self.stats_registrator.clone());
let (media_rx, media_tx) = MediaBackend::subscribe(self.media_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 { BackendChannels {
net_rx: Some(net_rx), net_rx: Some(net_rx),
net_tx, net_tx,
@@ -96,13 +116,10 @@ impl Backend {
media_rx: Some(media_rx), media_rx: Some(media_rx),
media_tx, media_tx,
tailscale_rx: Some(tailscale_rx), 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 { pub struct BackendChannels {
@@ -114,8 +131,9 @@ pub struct BackendChannels {
stats_rx: Option<Receiver<Stats>>, stats_rx: Option<Receiver<Stats>>,
media_rx: Option<Receiver<MediaCommand>>, media_rx: Option<Receiver<MediaCommand>>,
media_tx: Sender<MediaCommand>, media_tx: Sender<MediaCommand>,
tailscale_rx: Option<Receiver<TailscaleState>>, tailscale_rx: Option<Receiver<State>>,
hypridle: Arc<Mutex<Option<Child>>>, idle_rx: Option<Receiver<IdleState>>,
idle_tx: Sender<IdleMessage>,
} }
impl BackendChannels { impl BackendChannels {
@@ -131,12 +149,15 @@ impl BackendChannels {
pub fn take_stats_rx(&mut self) -> Option<Receiver<Stats>> { pub fn take_stats_rx(&mut self) -> Option<Receiver<Stats>> {
self.stats_rx.take() 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() self.tailscale_rx.take()
} }
pub fn take_media_rx(&mut self) -> Option<Receiver<MediaCommand>> { pub fn take_media_rx(&mut self) -> Option<Receiver<MediaCommand>> {
self.media_rx.take() 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> { pub fn net_tx(&self) -> UnboundedSender<NetworkCommand> {
self.net_tx.clone() self.net_tx.clone()
} }
@@ -146,21 +167,7 @@ impl BackendChannels {
pub fn media_tx(&self) -> Sender<MediaCommand> { pub fn media_tx(&self) -> Sender<MediaCommand> {
self.media_tx.clone() self.media_tx.clone()
} }
pub fn hypridle(&self) -> Arc<Mutex<Option<Child>>> { pub fn idle_tx(&self) -> Sender<IdleMessage> {
self.hypridle.clone() self.idle_tx.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();
}
} }
} }

View File

@@ -4,8 +4,9 @@ use brightness::blocking::{Brightness, BrightnessDevice};
use crossbeam_channel::{Receiver, Sender}; use crossbeam_channel::{Receiver, Sender};
use ddc_hi::Ddc; use ddc_hi::Ddc;
use jmb_logger_rs::*; use jmb_logger_rs::*;
use std::collections::HashMap; use std::collections::{HashMap, HashSet};
use std::error::Error; use std::error::Error;
use std::fmt::{Debug, Formatter};
use std::os::unix::prelude::MetadataExt; use std::os::unix::prelude::MetadataExt;
use std::path::PathBuf; use std::path::PathBuf;
use std::process::Command; use std::process::Command;
@@ -18,6 +19,20 @@ pub enum DisplayBackend {
DdcCi(Box<ddc_hi::Display>), 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> { fn get_sysfs_dev(id: &str) -> Option<BrightnessDevice> {
brightness::blocking::brightness_devices() brightness::blocking::brightness_devices()
.find(|dev| { .find(|dev| {
@@ -62,26 +77,45 @@ impl DisplayBackend {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum BrightnessMessage { pub enum BrightnessMessage {
Init(Sender<BrightnessMessage>), Init(Sender<BrightnessMessage>),
Value(String, u8), Value(String, u8, bool),
Revert(String),
Request(String), Request(String),
} }
struct DisplayData(DisplayBackend, u8); #[derive(Debug)]
struct DisplayData {
backend: DisplayBackend,
value: u8,
saved_value: Option<u8>,
}
impl DisplayData { 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 { fn update(&mut self) -> bool {
if let Some(value) = self.0.get_brightness() if let Some(value) = self.backend.get_brightness()
&& self.1 != value && self.value != value
{ {
self.1 = value; self.value = value;
return true; return true;
} }
false false
} }
fn set_brightness(&mut self, percent: u8) { fn set_brightness(&mut self, percent: u8) {
self.0.set_brightness(percent); self.backend.set_brightness(percent);
self.1 = percent; self.value = percent;
} }
} }
@@ -119,7 +153,7 @@ impl BackendThread<BrightnessMessage, Sender<BrightnessMessage>> for BrightnessM
thread::spawn(move || { thread::spawn(move || {
while let Ok(tx) = registration_rx.recv() { while let Ok(tx) = registration_rx.recv() {
if tx.send(BrightnessMessage::Init(f2b_tx.clone())).is_ok() { if tx.send(BrightnessMessage::Init(f2b_tx.clone())).is_ok() {
trace!(&LOGGER, "Added Brightness Subscriber: {tx:?}"); debug!(&LOGGER, "Added Brightness Subscriber: {tx:?}");
instance_clone instance_clone
.lock() .lock()
.expect("Couldn't get lock") .expect("Couldn't get lock")
@@ -130,36 +164,53 @@ impl BackendThread<BrightnessMessage, Sender<BrightnessMessage>> for BrightnessM
}); });
let instance_clone = instance.clone(); let instance_clone = instance.clone();
thread::spawn(move || { thread::spawn(move || {
while let Ok(mut msg) = f2b_rx.recv() { loop {
while let Ok(newer) = f2b_rx.try_recv() { // Ensure all Messages are handled in the correct order
msg = newer; let mut operations: HashMap<String, Vec<Operation>> = HashMap::new();
} let mut requests: HashSet<String> = HashSet::new();
match msg { if let Ok(msg) = f2b_rx.recv() {
BrightnessMessage::Value(id, val) => { Self::insert_message_operation(&mut operations, &mut requests, msg);
instance_clone while let Ok(msg) = f2b_rx.try_recv() {
.lock() Self::insert_message_operation(&mut operations, &mut requests, msg);
.expect("Couldn't get lock")
.set_brightness(id, val);
} }
BrightnessMessage::Request(id) => { }
instance_clone
.lock() let mut manager = instance_clone.lock().unwrap();
.expect("Couldn't get lock") for (id, operations) in operations {
.register_display(id.clone()); let mut last = manager
if let Some(val) = instance_clone .display_cache
.lock() .get(&id)
.expect("Couldn't get lock") .and_then(|d| Some(d.value))
.display_cache .unwrap_or(60);
.get(&id) let mut revert_flag = false;
.map(|v| v.1) for operation in operations {
{ match operation {
instance_clone Operation::Set(val, save) => {
.lock() if save {
.expect("Couldn't get lock") manager.display_cache.get_mut(&id).and_then(|d| d.save());
.publish(BrightnessMessage::Value(id, val)); }
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)); thread::sleep(Duration::from_millis(10_000));
let mut messages = vec![]; let mut messages = vec![];
for monitor in &mut instance.lock().expect("Couldn't get lock").display_cache { for (monitor, data) in
if monitor.1.update() { &mut instance.lock().expect("Couldn't get lock").display_cache
messages.push(BrightnessMessage::Value(monitor.0.clone(), monitor.1.1)); {
if data.update() {
messages.push(BrightnessMessage::Value(monitor.clone(), data.value, false));
} }
} }
for message in messages { 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 { impl BrightnessManager {
pub fn set_brightness(&mut self, connector: String, percent: u8) { pub fn set_brightness(&mut self, connector: String, percent: u8) {
if !self.display_cache.contains_key(&connector) { 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) { if !self.display_cache.contains_key(&connector) {
self.register_display(connector.clone()); self.register_display(connector.clone());
} }
if let Some(display) = self.display_cache.get_mut(&connector) { if let Some(display) = self.display_cache.get_mut(&connector) {
display.0.get_brightness() display.backend.get_brightness()
} else { } else {
None None
} }
@@ -220,7 +279,7 @@ impl BrightnessManager {
.to_str() .to_str()
.is_some_and(|name| name.eq(&format!("card1-{connector}"))) .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 path = bl_path.file_name();
let Some(name) = path.to_str() else { continue }; let Some(name) = path.to_str() else { continue };
let Some(device) = get_sysfs_dev(name) else { let Some(device) = get_sysfs_dev(name) else {
@@ -229,7 +288,7 @@ impl BrightnessManager {
let mut backend = DisplayBackend::Sysfs(device); let mut backend = DisplayBackend::Sysfs(device);
let value = backend.get_brightness().unwrap_or(60); let value = backend.get_brightness().unwrap_or(60);
self.display_cache self.display_cache
.insert(connector.clone(), DisplayData(backend, value)); .insert(connector.clone(), DisplayData::new(backend, value));
return; return;
} }
} }
@@ -239,13 +298,13 @@ impl BrightnessManager {
&& let Ok(data) = fs::metadata(path) && let Ok(data) = fs::metadata(path)
{ {
let id = data.rdev(); let id = data.rdev();
println!("Found DDC/CI display {connector}"); debug!(&LOGGER, "Found DDC/CI display {connector}");
for display in ddc_hi::Display::enumerate() { for display in ddc_hi::Display::enumerate() {
if id.to_string().eq(&display.info.id) { if id.to_string().eq(&display.info.id) {
let mut backend = DisplayBackend::DdcCi(Box::new(display)); let mut backend = DisplayBackend::DdcCi(Box::new(display));
let value = backend.get_brightness().unwrap_or(60); let value = backend.get_brightness().unwrap_or(60);
self.display_cache self.display_cache
.insert(connector.clone(), DisplayData(backend, value)); .insert(connector.clone(), DisplayData::new(backend, value));
break; break;
} }
} }
@@ -318,4 +377,35 @@ impl BrightnessManager {
) )
.into()) .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::LOGGER;
use crate::backend::BackendThread; use crate::backend::{BackendThread, SimpleBackendThread};
use crossbeam_channel::{Receiver, Sender}; use crossbeam_channel::{Receiver, Sender};
use hyprland::data::{Client, Monitor, Workspaces}; use hyprland::data::{Client, Monitor, Workspaces};
use hyprland::event_listener::EventListener; use hyprland::event_listener::EventListener;
@@ -23,14 +23,7 @@ pub struct HyprlandComms {
subscribers: Vec<Sender<WindowManagerEvent>>, subscribers: Vec<Sender<WindowManagerEvent>>,
} }
impl BackendThread<WindowManagerEvent, ()> for HyprlandComms { impl SimpleBackendThread<WindowManagerEvent> for HyprlandComms {
fn subscribe(
registrator: Sender<Sender<WindowManagerEvent>>,
) -> (Receiver<WindowManagerEvent>, ()) {
let (tx, rx) = crossbeam_channel::unbounded::<WindowManagerEvent>();
let _ = registrator.send(tx);
(rx, ())
}
fn subscribers(&self) -> &Vec<Sender<WindowManagerEvent>> { fn subscribers(&self) -> &Vec<Sender<WindowManagerEvent>> {
&self.subscribers &self.subscribers

View File

@@ -13,23 +13,20 @@ use std::process::Command;
use std::rc::Rc; use std::rc::Rc;
pub const LOGGER: Logger = Logger::create(Level::DEBUG); pub const LOGGER: Logger = Logger::create(Level::DEBUG);
pub const APP_NAME: &str = "net.jmb19905.jmb-bar";
fn main() { fn main() {
info!(&LOGGER, "Starting jmb-bar"); info!(&LOGGER, "Starting jmb-bar");
gio::resources_register_include!("templates.gresource").expect("Failed to register resources"); gio::resources_register_include!("templates.gresource").expect("Failed to register resources");
let app = Application::builder() let app = Application::builder().application_id(APP_NAME).build();
.application_id("net.jmb19905.jmb-bar")
.build();
let watcher_slot: Rc<RefCell<Option<RecommendedWatcher>>> = Rc::new(RefCell::new(None)); let watcher_slot: Rc<RefCell<Option<RecommendedWatcher>>> = Rc::new(RefCell::new(None));
let _ = Command::new("pkill hypridle").output(); // Kill existing hypridle processes let _ = Command::new("pkill hypridle").output(); // Kill existing hypridle processes
let backend = Backend::start(); 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 |_| { app.connect_startup(move |_| {
let provider = CssProvider::new(); 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; let mut handled = false;
if let Some(button) = button_opt if let Some(button) = button_opt
&& let Some(player) = player_opt && let Some(player) = player_opt
&& let Ok(status) = player.get_playback_status()
&& let Ok(metadata) = player.get_metadata()
{ {
match status { //println!("{:?}", player.get_playback_status());
PlaybackStatus::Playing => { if let Ok(status) = player.get_playback_status()
button.set_visible(true); && let Ok(metadata) = player.get_metadata()
button {
.child() match status {
.and_downcast::<Label>() PlaybackStatus::Playing => {
.unwrap() button.set_visible(true);
.set_label(&format!("{}", metadata.title().unwrap_or(""))); button
button.add_css_class("media"); .child()
button.add_css_class("playing"); .and_downcast::<Label>()
handled = true; .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 { if !handled {
button.set_visible(true); button.set_visible(true);

View File

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

View File

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