First Commit

This commit is contained in:
jmb05
2026-10-24 19:55:33 +02:00
commit d6a2c6afea
54 changed files with 12084 additions and 0 deletions

423
src/backend/audio.rs Normal file
View File

@@ -0,0 +1,423 @@
use crate::LOGGER;
use crate::backend::audio::ControllerMsg::Request;
use crate::backend::{BackendThread, Registrator};
use crossbeam_channel::{Receiver, Sender};
use eventfd::{EfdFlags, EventFD};
use jmb_logger_rs::*;
use mainloop::standard::Mainloop;
use pulse::callbacks::ListResult;
use pulse::context::subscribe::{Facility, InterestMaskSet};
use pulse::context::{Context, FlagSet, State};
use pulse::mainloop::api;
use pulse::mainloop::events::io;
use pulse::proplist::Proplist;
use pulse::volume::{ChannelVolumes, Volume};
use pulse::{mainloop, operation};
use std::boxed::Box;
use std::cell::Cell;
use std::os::fd::{AsRawFd, RawFd};
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use std::thread;
pub struct AudioController {
pub mainloop: Mainloop,
context: Context,
device_index: Option<u32>,
device_bt: Option<bool>,
current_vol: u8,
current_mute: bool,
}
#[derive(Debug)]
pub enum ControllerMsg {
NotifyServer,
NotifySink(u32),
Request,
Set(u8, bool),
}
impl AudioController {
pub fn new(tx: Sender<ControllerMsg>) -> Self {
let mut mainloop = Mainloop::new().unwrap();
let mut prop_list = Proplist::new().unwrap();
prop_list
.set_str(pulse::proplist::properties::APPLICATION_NAME, "jmb-bar")
.unwrap();
let mut context = Context::new_with_proplist(&mainloop, "jmb-bar", &prop_list).unwrap();
context.connect(None, FlagSet::NOFLAGS, None).unwrap();
loop {
match context.get_state() {
State::Ready => break,
State::Failed | State::Terminated => panic!("PulseAudio connection failed"),
_ => mainloop.iterate(false),
};
}
let device_list = Arc::new(Mutex::new(Vec::new()));
let clone = Arc::clone(&device_list);
let op = context
.introspect()
.get_sink_info_list(move |info| match info {
ListResult::Item(device) => {
if let Some(description) = device.description.as_ref()
&& let Some(name) = device.name.as_ref()
{
if let Ok(mut lock) = clone.lock() {
lock.push((name.to_string(), description.to_string()));
} else {
error!(&LOGGER, "Failed to unlock device list on device {}", name);
}
} else {
error!(&LOGGER, "Failed to access device description");
}
}
ListResult::End => {}
ListResult::Error => {}
});
while op.get_state() == operation::State::Running {
mainloop.iterate(false);
}
context.set_subscribe_callback(Some(Box::new(move |facility, _, index| {
if facility.is_some_and(|f| f == Facility::Sink) {
let _ = tx.send(ControllerMsg::NotifySink(index));
} else if facility.is_some_and(|f| f == Facility::Server) {
let _ = tx.send(ControllerMsg::NotifyServer);
}
})));
let _op = context.subscribe(InterestMaskSet::ALL, |success| {
trace!(&LOGGER, "Subscribed to pulseaudio events: {success}");
});
let mut controller = AudioController {
context,
mainloop,
device_index: None,
device_bt: None,
current_vol: 0,
current_mute: true,
};
controller.query_default_device_blocking();
controller
}
pub fn query_default_device_blocking(&mut self) {
let default_device = Arc::new(Mutex::new(None));
let dev_clone = default_device.clone();
let op = self.context.introspect().get_server_info(move |info| {
if let Some(name) = info.default_sink_name.as_ref() {
let _ = dev_clone
.lock()
.expect("Couldn't get lock on default dev")
.insert(name.to_string());
}
});
while op.get_state() == operation::State::Running {
self.mainloop.iterate(false);
}
let name = default_device
.lock()
.expect("Couldn't get lock on device name")
.take()
.unwrap_or(String::new());
let index = Arc::new(Mutex::new(None));
let index_clone = index.clone();
let op = self
.context
.introspect()
.get_sink_info_by_name(&name, move |res| match res {
ListResult::Item(item) => {
let _ = index_clone
.lock()
.expect("Couldn't get lock on device index")
.insert(item.index);
}
ListResult::End => {}
ListResult::Error => {}
});
while op.get_state() == operation::State::Running {
self.mainloop.iterate(false);
}
let val = index
.lock()
.expect("Couldn't get lock on device index")
.take();
self.device_index = val;
self.device_bt = Some(name.starts_with("bluez"));
debug!(
&LOGGER,
"Found default Audio Sink Idx: {:?} Bluetooth: {}",
self.device_index,
self.device_bt.unwrap()
);
}
pub fn query_state_blocking(&mut self) -> bool {
if let Some(idx) = self.device_index {
let volume = Rc::new(Cell::new(self.current_vol));
let vol_clone = volume.clone();
let mute = Rc::new(Cell::new(self.current_mute));
let mute_clone = mute.clone();
let op =
self.context
.introspect()
.get_sink_info_by_index(idx, move |info| match info {
ListResult::Item(info) => {
let percent =
info.volume.avg().0 as f64 * 100.0 / Volume::NORMAL.0 as f64;
trace!(
&LOGGER,
"State of default Sink changed: {percent} {}", info.mute
);
vol_clone.set(percent as u8);
mute_clone.set(info.mute);
}
ListResult::End => {}
ListResult::Error => {}
});
while op.get_state() == operation::State::Running {
self.mainloop.iterate(false);
}
let mut dirty = false;
let volume = volume.take();
if self.current_vol != volume {
self.current_vol = volume;
dirty = true;
}
let mute = mute.take();
if self.current_mute != mute {
self.current_mute = mute;
dirty = true;
}
return dirty;
}
false
}
pub fn set_state(&mut self, volume: u8, mute: bool) {
if let Some(idx) = self.device_index {
if self.current_vol != volume {
self.current_vol = volume;
let volume = Volume(
((volume.min(150) as f64 / 100.0) * Volume::NORMAL.0 as f64).round() as u32,
);
let mut channel_volumes = ChannelVolumes::default();
channel_volumes.set(2, volume);
self.context.introspect().set_sink_volume_by_index(
idx,
&channel_volumes,
Some(Box::new(|success| {
if !success {
warn!(&LOGGER, "Failed setting sink volume");
}
})),
);
}
if self.current_mute != mute {
self.current_mute = mute;
let a = self.context.introspect().set_sink_mute_by_index(
idx,
mute,
Some(Box::new(|success| {
if !success {
warn!(&LOGGER, "Failed setting sink mute");
}
})),
);
while a.get_state() == operation::State::Running {
self.mainloop.iterate(false);
}
}
}
}
}
#[derive(Clone, Debug)]
pub enum MediaCommand {
Init(Sender<MediaCommand>),
Request,
Cmd(u8, bool, bool),
}
pub struct MediaBackend {
subscribers: Vec<Sender<MediaCommand>>,
command_tx: Sender<MediaCommand>,
}
impl BackendThread<MediaCommand, Sender<MediaCommand>> for MediaBackend {
fn subscribe(
registrator: Registrator<MediaCommand>,
) -> (Receiver<MediaCommand>, Sender<MediaCommand>) {
let (result_tx, result_rx) = crossbeam_channel::unbounded::<MediaCommand>();
let _ = registrator.send(result_tx);
while let Ok(state) = result_rx.recv() {
return match state {
MediaCommand::Init(sender) => (result_rx, sender),
_ => continue,
};
}
panic!("Channel closed unexpectedly, without returning anything")
}
fn subscribers(&self) -> &Vec<Sender<MediaCommand>> {
&self.subscribers
}
fn init(registration_rx: Receiver<Sender<MediaCommand>>) {
let (command_tx, command_rx) = crossbeam_channel::unbounded::<MediaCommand>();
let instance = Arc::new(Mutex::new(Self {
subscribers: vec![],
command_tx,
}));
let instance_clone = instance.clone();
let (controller_tx, controller_rx) = crossbeam_channel::unbounded::<ControllerMsg>();
let controller_tx_clone = controller_tx.clone();
let notify = Arc::new(EventFD::new(0, EfdFlags::EFD_CLOEXEC).unwrap());
let notify_clone = notify.clone();
thread::spawn(move || {
let mut controller = AudioController::new(controller_tx_clone);
let _event = api::Mainloop::new_io_event(
&mut controller.mainloop,
notify_clone.as_raw_fd(),
io::FlagSet::INPUT,
Box::new(move |_, fd, _flags| match drain_eventfd(fd) {
Ok(_) => trace!(&LOGGER, "Woke pulseaudio loop using EventFD"),
Err(e) => warn!(&LOGGER, "Error draining EventFD {}", e.to_string()),
}),
)
.unwrap();
loop {
while let Ok(msg) = controller_rx.try_recv() {
match msg {
ControllerMsg::Request => {
let _ = instance_clone.lock().expect("Couldn't get lock").publish(
MediaCommand::Cmd(
controller.current_vol,
controller.current_mute,
controller.device_bt.unwrap_or(false),
),
);
}
ControllerMsg::NotifyServer => {
controller.query_default_device_blocking();
let _ = controller.query_state_blocking();
let _ = instance_clone.lock().expect("Couldn't get lock").publish(
MediaCommand::Cmd(
controller.current_vol,
controller.current_mute,
controller.device_bt.unwrap_or(false),
),
);
}
ControllerMsg::NotifySink(idx) => {
if controller.device_index.is_some_and(|i| i == idx) {
let changed = controller.query_state_blocking();
if changed {
let _ = instance_clone
.lock()
.expect("Couldn't get lock")
.publish(MediaCommand::Cmd(
controller.current_vol,
controller.current_mute,
controller.device_bt.unwrap_or(false),
));
trace!(
&LOGGER,
"Change: {} muted: {}",
controller.current_vol,
controller.current_mute
);
}
}
}
ControllerMsg::Set(volume, mute) => {
controller.set_state(volume, mute);
let _ = instance_clone.lock().expect("Couldn't get lock").publish(
MediaCommand::Cmd(
controller.current_vol,
controller.current_mute,
controller.device_bt.unwrap_or(false),
),
);
}
}
}
controller.mainloop.iterate(true);
}
});
let instance_clone = instance.clone();
thread::spawn(move || {
while let Ok(sender) = registration_rx.recv() {
if sender
.send(MediaCommand::Init(
instance_clone
.lock()
.expect("Couldn't get lock")
.command_tx
.clone(),
))
.is_ok()
{
let _ = sender.send(MediaCommand::Request);
instance_clone
.lock()
.expect("Couldn't get subscribers")
.subscribers
.push(sender);
}
}
});
thread::spawn(move || {
while let Ok(command) = command_rx.recv() {
match command {
MediaCommand::Cmd(vol, mute, _) => {
let _ = controller_tx.send(ControllerMsg::Set(vol, mute));
notify.write(1).expect("Error writing notify");
}
MediaCommand::Request => {
let _ = controller_tx.send(Request);
notify.write(1).expect("Error writing notify");
}
_ => {
warn!(&LOGGER, "Frontend sent invalid Command")
}
}
}
});
}
}
fn drain_eventfd(fd: RawFd) -> std::io::Result<u64> {
let mut value: u64 = 0;
let result = unsafe { libc::read(fd, (&mut value as *mut u64).cast(), size_of::<u64>()) };
if result == size_of::<u64>() as isize {
Ok(value)
} else if result == -1 {
Err(std::io::Error::last_os_error())
} else {
Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"short eventfd read",
))
}
}

View File

@@ -0,0 +1,323 @@
use crate::LOGGER;
use crate::backend::{BackendThread, Registrator};
use brightness::blocking::{Brightness, BrightnessDevice};
use crossbeam_channel::{Receiver, Sender};
use ddc_hi::Ddc;
use jmb_logger_rs::*;
use std::collections::HashMap;
use std::error::Error;
use std::os::unix::prelude::MetadataExt;
use std::path::PathBuf;
use std::process::Command;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use std::{fs, thread};
pub enum DisplayBackend {
Sysfs(BrightnessDevice),
DdcCi(ddc_hi::Display),
}
fn get_sysfs_dev(id: &str) -> Option<BrightnessDevice> {
brightness::blocking::brightness_devices()
.find(|dev| {
if let Ok(dev) = dev
&& let Ok(name) = dev.device_name()
&& name.eq(id)
{
true
} else {
false
}
})
.and_then(|b| b.ok())
}
impl DisplayBackend {
pub fn set_brightness(&mut self, percent: u8) {
match self {
DisplayBackend::Sysfs(dev) => {
let _ = dev.set(percent as u32);
}
DisplayBackend::DdcCi(display) => {
let _ = display.handle.set_vcp_feature(0x10, percent as u16);
}
}
}
pub fn get_brightness(&mut self) -> Option<u8> {
match self {
DisplayBackend::Sysfs(dev) => dev.get().and_then(|val| Ok(val as u8)).ok(),
DisplayBackend::DdcCi(display) => {
if let Ok(val) = display.handle.get_vcp_feature(0x10) {
Some(val.value() as u8)
} else {
None
}
}
}
}
}
#[derive(Debug, Clone)]
pub enum BrightnessMessage {
Init(Sender<BrightnessMessage>),
Value(String, u8),
Request(String),
}
struct DisplayData(DisplayBackend, u8);
impl DisplayData {
fn update(&mut self) -> bool {
if let Some(value) = self.0.get_brightness() {
if self.1 != value {
self.1 = value;
return true;
}
}
false
}
fn set_brightness(&mut self, percent: u8) {
self.0.set_brightness(percent);
self.1 = percent;
}
}
pub struct BrightnessManager {
display_cache: HashMap<String, DisplayData>,
subscribers: Vec<Sender<BrightnessMessage>>,
}
impl BackendThread<BrightnessMessage, Sender<BrightnessMessage>> for BrightnessManager {
fn subscribe(
registrator: Registrator<BrightnessMessage>,
) -> (Receiver<BrightnessMessage>, Sender<BrightnessMessage>) {
let (b2f_tx, b2f_rx) = crossbeam_channel::unbounded::<BrightnessMessage>();
let _ = registrator.send(b2f_tx);
while let Ok(message) = b2f_rx.recv() {
return match message {
BrightnessMessage::Init(sender) => (b2f_rx, sender),
_ => continue,
};
}
panic!("Channel closed unexpectedly, without returning anything")
}
fn subscribers(&self) -> &Vec<Sender<BrightnessMessage>> {
&self.subscribers
}
fn init(registration_rx: Receiver<Sender<BrightnessMessage>>) {
let (f2b_tx, f2b_rx) = crossbeam_channel::unbounded::<BrightnessMessage>();
let instance = Arc::new(Mutex::new(Self {
display_cache: HashMap::new(),
subscribers: Vec::new(),
}));
let instance_clone = instance.clone();
std::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:?}");
instance_clone
.lock()
.expect("Couldn't get lock")
.subscribers
.push(tx);
}
}
});
let instance_clone = instance.clone();
std::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);
}
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)
.and_then(|v| Some(v.1))
{
instance_clone
.lock()
.expect("Couldn't get lock")
.publish(BrightnessMessage::Value(id, val));
}
}
_ => {}
}
}
});
thread::spawn(move || {
loop {
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 message in messages {
instance.lock().expect("Couldn't get lock").publish(message);
}
}
});
}
}
impl BrightnessManager {
pub fn set_brightness(&mut self, connector: String, percent: u8) {
if !self.display_cache.contains_key(&connector) {
self.register_display(connector.clone());
}
if let Some(display) = self.display_cache.get_mut(&connector) {
display.set_brightness(percent);
}
}
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()
} else {
None
}
}
fn register_display(&mut self, connector: String) {
let Ok(dir) = fs::read_dir("/sys/class/backlight/") else {
return;
};
for path in dir {
let Ok(bl_path) = path else { continue };
let Ok(dir) = fs::read_dir(bl_path.path()) else {
continue;
};
for path in dir {
let Ok(path) = path else { continue };
if path.file_name().eq("device")
&& let Ok(link) = fs::read_link(path.path())
&& let Some(name) = link.file_name()
{
if name
.to_str()
.is_some_and(|name| name.eq(&format!("card1-{connector}")))
{
println!("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 {
continue;
};
let mut backend = DisplayBackend::Sysfs(device);
let value = backend.get_brightness().unwrap_or(60);
self.display_cache
.insert(connector.clone(), DisplayData(backend, value));
return;
}
}
}
}
let res = Self::ddcutil_bus_for_connector(&connector);
if let Ok(path) = res
&& let Ok(data) = fs::metadata(path)
{
let id = data.rdev();
println!("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(display);
let value = backend.get_brightness().unwrap_or(60);
self.display_cache
.insert(connector.clone(), DisplayData(backend, value));
break;
}
}
}
}
fn ddcutil_bus_for_connector(connector: &str) -> Result<PathBuf, Box<dyn Error>> {
let output = Command::new("ddcutil")
.args(["detect", "--verbose"])
.output()?;
if !output.status.success() {
return Err(std::io::Error::other("ddcutil detect --verbose failed").into());
}
// Split the output into display blocks.
let mut blocks: Vec<Vec<&str>> = Vec::new();
for line in std::str::from_utf8(&output.stdout)?.lines() {
if line.starts_with("Display ") || line.starts_with("Invalid display") {
blocks.push(vec![line]);
} else if let Some(block) = blocks.last_mut() {
block.push(line);
}
}
for block in blocks {
// Skip displays that ddcutil identifies as invalid.
if block[0].starts_with("Invalid display") {
continue;
}
let mut bus: Option<PathBuf> = None;
let mut drm_connector: Option<String> = None;
for line in block.iter().skip(1) {
let Some((key, value)) = line.split_once(':') else {
continue;
};
// Accept both "DRM connector" and "DRM_connector".
let key = key.trim().replace('_', " ");
let value = value.trim();
if key.eq_ignore_ascii_case("I2C bus") {
bus = Some(PathBuf::from(value));
} else if key.eq_ignore_ascii_case("DRM connector") {
// Convert "card1-HDMI-A-1" to "HDMI-A-1".
drm_connector = Some(
value
.strip_prefix("card")
.and_then(|s| s.split_once('-'))
.map(|(_, name)| name)
.unwrap_or(value)
.to_owned(),
);
}
}
if drm_connector.as_deref() == Some(connector) {
if let Some(bus) = bus {
return Ok(bus);
}
}
}
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("No DDC-capable display found for {connector}"),
)
.into())
}
}

488
src/backend/network.rs Normal file
View File

@@ -0,0 +1,488 @@
use crate::LOGGER;
use crate::backend::BackendThread;
use crossbeam_channel::{Receiver, Sender};
use futures_util::StreamExt;
use jmb_logger_rs::*;
use nmrs::raw::zbus;
use nmrs::raw::zbus::Error;
use nmrs::raw::zbus::names::{ErrorName, InterfaceName};
use nmrs::raw::zvariant::OwnedObjectPath;
use nmrs::{ActiveConnection, ConnectionError, NetworkEvent, NetworkManager, WifiSecurity};
use std::cmp::Ordering;
use std::pin::Pin;
use std::time::Duration;
use tokio::sync::mpsc::UnboundedSender;
use tokio::time::{Instant, Sleep};
#[derive(Debug, Clone)]
pub enum NetworkType {
Wifi,
Wired,
}
#[derive(Debug, Clone)]
pub struct NetworkInfo {
pub name: String,
pub network_type: NetworkType,
pub signal: u8,
pub secured: Option<bool>,
pub known: bool,
}
impl Eq for NetworkInfo {}
impl PartialEq<Self> for NetworkInfo {
fn eq(&self, other: &Self) -> bool {
other.name.eq(&self.name)
}
}
impl PartialOrd<Self> for NetworkInfo {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(other.cmp(self))
}
}
impl Ord for NetworkInfo {
fn cmp(&self, other: &Self) -> Ordering {
self.signal
.cmp(&other.signal)
.then_with(|| self.name.cmp(&other.name))
}
}
pub enum NetworkCommand {
ToggleVpn(String),
Connect(String, Auth),
}
#[derive(Clone, Debug)]
pub enum Auth {
Open,
Password(String),
}
#[derive(Clone, Debug)]
pub enum AuthType {
Password,
}
#[derive(Clone, Debug, thiserror::Error)]
pub enum ConnectError {
#[error("Authentication Failure")]
AuthFailed,
#[error("Network not found")]
NotFound,
#[error("Unknown error")]
Unknown,
}
impl From<ConnectionError> for ConnectError {
fn from(value: ConnectionError) -> Self {
match value {
ConnectionError::AuthFailed => ConnectError::AuthFailed,
ConnectionError::NotFound => ConnectError::NotFound,
ConnectionError::Dbus(err) => match err {
Error::MethodError(name, ..) => {
if name.eq(&ErrorName::try_from(
"org.freedesktop.NetworkManager.Device.SpecificObjectNotFound",
)
.unwrap())
{
ConnectError::NotFound
} else {
ConnectError::Unknown
}
}
_ => ConnectError::Unknown,
},
_ => ConnectError::Unknown,
}
}
}
#[derive(Debug, Clone)]
pub enum NetworkState {
Init(UnboundedSender<NetworkCommand>),
ChangeNetwork(Option<NetworkInfo>),
VpnState { profile: String, connected: bool },
Networks(Vec<NetworkInfo>),
Auth(String, AuthType),
ConnectFail(String, ConnectError),
ConnectSuccess(String),
}
#[derive(Debug, Clone)]
pub struct NetworkBackendHandle {
subscribers: Vec<Sender<NetworkState>>,
command_tx: UnboundedSender<NetworkCommand>,
}
struct EventDebouncer {
duration: Duration,
deadline: Option<Instant>,
sleep: Pin<Box<Sleep>>,
}
impl BackendThread<NetworkState, UnboundedSender<NetworkCommand>> for NetworkBackendHandle {
fn subscribe(
registrator: Sender<Sender<NetworkState>>,
) -> (Receiver<NetworkState>, UnboundedSender<NetworkCommand>) {
let (result_tx, result_rx) = crossbeam_channel::unbounded::<NetworkState>();
let _ = registrator.send(result_tx);
while let Ok(state) = result_rx.recv() {
return match state {
NetworkState::Init(sender) => (result_rx, sender),
_ => continue,
};
}
panic!("Channel closed unexpectedly, without returning anything")
}
fn subscribers(&self) -> &Vec<Sender<NetworkState>> {
&self.subscribers
}
fn init(registration_rx: Receiver<Sender<NetworkState>>) {
let (command_tx, mut command_rx) = tokio::sync::mpsc::unbounded_channel::<NetworkCommand>();
let mut instance = Self {
subscribers: vec![],
command_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 {
let nm = match NetworkManager::new().await {
Ok(nm) => nm,
Err(err) => {
error!(&LOGGER, "Error getting NetworkManager: {}", err.to_string());
return;
}
};
let mut events = match nm.network_events().await {
Ok(events) => events,
Err(err) => {
error!(&LOGGER, "Error getting NetworkManager events: {}", err.to_string());
return;
}
};
let mut debouncer = EventDebouncer {
duration: Duration::from_millis(200),
deadline: None,
sleep: Box::pin(tokio::time::sleep(Duration::from_secs(86400))),
};
debouncer.sleep.as_mut().reset(Instant::now() + Duration::from_secs(86400));
loop {
while let Ok(tx) = registration_rx.try_recv() {
if tx.send(NetworkState::Init(instance.command_tx.clone())).is_ok() {
instance.subscribers.push(tx);
let _ = instance.send_all_networks(&nm).await;
let _ = instance.send_active_connection(&nm).await;
}
}
tokio::select! {
command = command_rx.recv() => {
let Some(command) = command else {
break;
};
// Run the command without stopping event processing
// while a potentially slow connection operation runs.
let nm = nm.clone();
let instance_clone = instance.clone();
tokio::spawn(async move {
match command {
NetworkCommand::ToggleVpn(profile) => instance_clone.toggle_vpn(profile, &nm).await,
NetworkCommand::Connect(ssid, auth) => {
if let Ok(saved) = nm.list_saved_connections().await {
if saved.iter().find(|c| c.id.eq(&ssid)).is_some() {
instance_clone.connect(&nm, ssid, WifiSecurity::Open).await;
} else {
match auth {
Auth::Open => {
if let Ok(networks) = nm.list_access_points(None).await {
if let Some(ap) = networks.iter().find(|net| net.ssid.eq(&ssid)) {
if ap.security.is_open() {
instance_clone.connect(&nm, ssid, WifiSecurity::Open).await;
} else {
instance_clone.publish(NetworkState::Auth(ssid, AuthType::Password));
}
}
}
}
Auth::Password(password) => instance_clone.connect(&nm, ssid, WifiSecurity::WpaPsk { psk: password }).await,
}
}
}
}
};
let _ = nm;
});
}
event = events.next() => {
match event {
Some(event_res) => {
match event_res {
Ok(event) => instance.handle_event(&nm, event, &mut debouncer).await,
Err(err) => error!(&LOGGER, "Error: {err:?}"),
}
}
None => break,
}
}
_ = &mut debouncer.sleep, if debouncer.deadline.is_some() => {
debouncer.deadline = None;
let _ = instance.send_all_networks(&nm).await;
}
}
}
});
});
}
}
impl NetworkBackendHandle {
async fn connect(&self, nm: &NetworkManager, ssid: String, creds: WifiSecurity) {
let previous_ssid = nm.current_ssid().await;
let res = nm.connect(&ssid, None, creds).await;
if res.is_err() {
let err = res.err().unwrap();
warn!(&LOGGER, "Error connecting to network {ssid}: {:?}", err);
self.print_network_info(nm, &ssid, Level::WARN).await;
self.publish(NetworkState::ConnectFail(ssid, ConnectError::from(err)));
if let Some(prev) = previous_ssid {
let _ = nm.connect(&prev, None, WifiSecurity::Open).await;
}
self.send_all_networks(nm).await;
} else {
self.publish(NetworkState::ConnectSuccess(ssid));
}
}
async fn print_network_info(&self, nm: &NetworkManager, ssid: &str, level: Level) {
if let Ok(nets) = nm.list_networks(None).await {
if let Some(net) = nets.iter().find(|net| net.ssid.eq(ssid)) {
log!(&LOGGER, level, "Network:");
log!(&LOGGER, level, " SSID: {}", net.ssid);
log!(&LOGGER, level, " Active: {}", net.is_active);
log!(&LOGGER, level, " Known: {}", net.known);
log!(&LOGGER, level, " Secured: {}", net.secured);
if net.secured {
log!(&LOGGER, level, " Features: {:?}", net.security_features);
log!(&LOGGER, level, " EAP: {}", net.is_eap);
log!(&LOGGER, level, " PSK: {}", net.is_psk);
}
log!(&LOGGER, level, " Strength: {:?}", net.strength);
log!(&LOGGER, level, " Device: {}", net.device);
log!(&LOGGER, level, " Addr(4): {:?}", net.ip4_address);
log!(&LOGGER, level, " Addr(6): {:?}", net.ip6_address);
}
}
}
async fn handle_event(
&self,
nm: &NetworkManager,
event: NetworkEvent,
debouncer: &mut EventDebouncer,
) {
match event {
NetworkEvent::AccessPointsChanged => {
let deadline = Instant::now() + debouncer.duration;
debouncer.deadline = Some(deadline);
debouncer.sleep.as_mut().reset(deadline);
}
NetworkEvent::WirelessEnabledChanged => {
if let Ok(state) = nm.wifi_state().await {
if !state.enabled {
let _ = self.publish(NetworkState::ChangeNetwork(None));
} else {
let _ = self.send_active_connection(nm).await;
}
}
}
NetworkEvent::ActiveConnectionsChanged => {
let _ = self.send_active_connection(nm).await;
}
NetworkEvent::DeviceChanged { interface } => match interface {
Some(interface) => match nm.active_vpn_connections().await {
Ok(connections) => {
let mut connected = false;
for conn in connections {
if conn.interface.is_some_and(|s| s.eq(&interface)) {
connected = true;
}
}
let _ = self.publish(NetworkState::VpnState {
profile: interface,
connected,
});
}
Err(err) => error!(&LOGGER, "Error getting active vpns: {}", err.to_string()),
},
None => {}
},
_ => {}
}
}
async fn primary_connection(nm: &NetworkManager) -> Option<String> {
let manager = zbus::fdo::PropertiesProxy::new(
nm.dbus_connection(),
"org.freedesktop.NetworkManager",
"/org/freedesktop/NetworkManager",
)
.await
.ok()?;
let path = OwnedObjectPath::try_from(
manager
.get(
InterfaceName::from_static_str("org.freedesktop.NetworkManager").unwrap(),
"PrimaryConnection",
)
.await
.ok()?,
)
.ok()?;
let props = zbus::fdo::PropertiesProxy::new(
nm.dbus_connection(),
"org.freedesktop.NetworkManager",
path.as_str(),
)
.await
.ok()?;
let is_default = bool::try_from(
props
.get(
InterfaceName::from_static_str(
"org.freedesktop.NetworkManager.Connection.Active",
)
.unwrap(),
"Default",
)
.await
.ok()?,
)
.ok()?;
if is_default {
let uuid: String = String::try_from(
props
.get(
InterfaceName::from_static_str(
"org.freedesktop.NetworkManager.Connection.Active",
)
.unwrap(),
"Uuid",
)
.await
.ok()?,
)
.ok()?;
return Some(uuid);
}
None
}
async fn send_all_networks(&self, nm: &NetworkManager) {
let _ = nm.scan_networks(None).await;
if let Ok(connections) = nm.list_access_points(None).await {
let mut networks: Vec<NetworkInfo> = Vec::new();
for conn in connections {
if conn.is_hidden() {
continue;
}
let ssid = conn.ssid;
let known = nm.has_saved_connection(&ssid).await.is_ok_and(|b| b);
let network = NetworkInfo {
name: ssid,
network_type: NetworkType::Wifi,
signal: conn.strength,
secured: Some(!conn.security.is_open()),
known,
};
networks.push(network);
}
networks.sort();
self.publish(NetworkState::Networks(networks));
}
}
async fn send_active_connection(&self, nm: &NetworkManager) {
trace!(&LOGGER, "Sending Active Connection");
if let Ok(connections) = nm.list_active_connections().await {
let connection = if let Some(primary) = Self::primary_connection(nm).await {
connections.iter().find(|c| match c {
ActiveConnection::Wired(c) => c.uuid.eq(&primary),
ActiveConnection::Wifi(c) => c.uuid.eq(&primary),
_ => false,
})
} else {
connections.first()
};
match connection {
Some(conn) => {
match conn {
ActiveConnection::Wired(c) => {
let info = NetworkInfo {
name: c.id.clone(),
network_type: NetworkType::Wired,
signal: 0,
secured: None,
known: true,
};
let _ = self.publish(NetworkState::ChangeNetwork(Some(info)));
}
ActiveConnection::Wifi(c) => {
let info = NetworkInfo {
name: c.ssid.clone(),
network_type: NetworkType::Wifi,
signal: c.strength.unwrap_or(0),
secured: None, // Not relevant here
known: true,
};
let _ = self.publish(NetworkState::ChangeNetwork(Some(info)));
}
_ => {}
}
}
None => {
let _ = self.publish(NetworkState::ChangeNetwork(None));
}
}
}
}
async fn toggle_vpn(&self, profile: String, nm: &NetworkManager) {
let operation = async {
let active = nm.active_vpn_connections().await?;
if active.iter().any(|vpn| vpn.name == profile) {
nm.disconnect_vpn(&profile).await?;
} else {
nm.connect_vpn_by_id(&profile).await?;
};
let active = nm.active_vpn_connections().await?;
Ok::<bool, Box<dyn std::error::Error>>(active.iter().any(|vpn| vpn.name == profile))
};
match operation.await {
Ok(connected) => {
let _ = self.publish(NetworkState::VpnState { profile, connected });
}
Err(_) => {}
}
}
}

140
src/backend/perf_stats.rs Normal file
View File

@@ -0,0 +1,140 @@
use crate::LOGGER;
use crate::backend::{BackendThread, Registrator};
use all_smi::AllSmi;
use battery::State;
use battery::units::Time;
use battery::units::power::watt;
use battery::units::ratio::percent;
use battery::units::time::millisecond;
use crossbeam_channel::{Receiver, Sender};
use jmb_logger_rs::*;
use std::thread;
use std::time::Duration;
use sysinfo::{Disks, System};
const POLL_INTERVAL: u64 = 10_000;
#[derive(Clone, Debug)]
pub struct Stats {
pub cpu: u8,
pub ram: u8,
pub disks: Vec<(String, u8)>,
pub gpus: Vec<(String, u8)>,
pub battery_charge: u8,
pub battery_state: State,
pub battery_draw: f32,
pub battery_time: u64,
}
pub struct StatsBackend {
subscribers: Vec<Sender<Stats>>,
system: System,
disks: Disks,
}
impl BackendThread<Stats, ()> for StatsBackend {
fn subscribe(registrator: Registrator<Stats>) -> (Receiver<Stats>, ()) {
let (tx, rx) = crossbeam_channel::unbounded::<Stats>();
let _ = registrator.send(tx);
(rx, ())
}
fn subscribers(&self) -> &Vec<Sender<Stats>> {
&self.subscribers
}
fn init(registration_rx: Receiver<Sender<Stats>>) {
thread::spawn(move || {
let mut instance = Self {
subscribers: Vec::new(),
system: System::new(),
disks: Disks::new(),
};
let manager = battery::Manager::new();
let smi = AllSmi::new();
loop {
while let Ok(tx) = registration_rx.try_recv() {
trace!(&LOGGER, "Added Stats Subscriber: {tx:?}");
instance.subscribers.push(tx);
}
instance.system.refresh_cpu_usage();
instance.system.refresh_memory();
instance.disks.refresh(true);
let cpu = instance.system.global_cpu_usage() as u8;
let ram =
(instance.system.used_memory() * 100 / instance.system.total_memory()) as u8;
let disk_stats: Vec<(String, u8)> = instance
.disks
.iter()
.map(|disk| {
let mount_point = disk.mount_point().display().to_string();
let used_space = disk.total_space() - disk.available_space();
let percentage = (used_space * 100 / disk.total_space()) as u8;
(mount_point, percentage)
})
.collect();
let mut gpus: Vec<(String, u8)> = Vec::new();
if let Ok(smi) = &smi {
gpus = smi
.get_gpu_info()
.iter()
.map(|info| (info.name.clone(), info.utilization as u8))
.collect();
}
let mut battery_charge: u8 = 255; // Use > 100 as invalid value
let mut battery_state = State::Unknown;
let mut battery_draw: f32 = 0.0;
let mut battery_time: u64 = 0;
if let Ok(manager) = &manager {
if let Ok(mut batteries) = manager.batteries() {
match batteries.next() {
Some(Ok(battery)) => {
battery_charge =
battery.state_of_charge().get::<percent>().round() as u8;
battery_state = battery.state();
battery_draw = battery.energy_rate().get::<watt>();
battery_time = if battery_state == State::Charging {
battery
.time_to_full()
.unwrap_or(Time::new::<millisecond>(0.0))
.get::<millisecond>()
as u64
} else if battery_state == State::Discharging {
battery
.time_to_empty()
.unwrap_or(Time::new::<millisecond>(0.0))
.get::<millisecond>()
as u64
} else {
0
};
}
Some(Err(e)) => error!(&LOGGER, "Error accessing battery: {e:?}"),
None => {}
}
}
}
let msg = Stats {
cpu,
ram,
disks: disk_stats,
gpus,
battery_charge,
battery_state,
battery_draw,
battery_time,
};
instance.publish(msg);
thread::sleep(Duration::from_millis(POLL_INTERVAL));
}
});
}
}

188
src/backend/tailscale.rs Normal file
View File

@@ -0,0 +1,188 @@
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") {
if !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 val.is_ok() {
if let Some(res) = val.unwrap().get("BackendState").and_then(|v| v.as_str()) {
if res.eq("Running") {
return TailscaleState::Running;
}
}
} else {
error!(&LOGGER, "{:?}", val.err());
}
}
TailscaleState::Stopped
}

View File

@@ -0,0 +1,207 @@
use crate::LOGGER;
use crate::backend::BackendThread;
use crossbeam_channel::{Receiver, Sender};
use hyprland::data::{Client, Monitor, Workspaces};
use hyprland::event_listener::EventListener;
use hyprland::prelude::*;
use hyprland::shared::{MonitorId, WorkspaceId, WorkspaceType};
use jmb_logger_rs::*;
use std::io;
use std::process::{Command, Output};
use std::sync::{Arc, Mutex};
#[derive(Debug, Clone)]
pub enum WindowManagerEvent {
WorkspaceChanged(i32),
WorkspaceAdded(i32, MonitorId),
WorkspaceRemoved(i32),
WindowFocusChanged(String),
SubmapChange(Option<String>),
}
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, ())
}
fn subscribers(&self) -> &Vec<Sender<WindowManagerEvent>> {
&self.subscribers
}
fn init(registration_rx: Receiver<Sender<WindowManagerEvent>>) {
std::thread::spawn(move || {
let instance = Arc::new(Mutex::new(Self {
subscribers: vec![],
}));
let mut listener = EventListener::new();
let inst_clone = instance.clone();
listener.add_workspace_changed_handler(move |workspace| {
if let WorkspaceType::Regular(name) = workspace.name {
if let Ok(id) = name.parse::<i32>() {
inst_clone
.lock()
.expect("Couldn't get Subscribers")
.publish(WindowManagerEvent::WorkspaceChanged(id));
}
}
});
let inst_clone = instance.clone();
listener.add_workspace_added_handler(move |workspace| {
if let WorkspaceType::Regular(name) = workspace.name {
//unpacks the name of the workspace
if let Ok(wss) = Workspaces::get() {
//gets all workspaces
if let Some(ws) = wss
.to_vec()
.iter()
.find(|ws| ws.id().unwrap() == workspace.id)
{
//find the current workspace Object from its id
if let Ok(id) = name.parse::<i32>() {
// parse the id as an int
inst_clone
.lock()
.expect("Couldn't get Subscribers")
.publish(WindowManagerEvent::WorkspaceAdded(
id,
ws.monitor_id.unwrap(),
));
}
}
}
}
});
let inst_clone = instance.clone();
listener.add_workspace_deleted_handler(move |workspace| {
if let WorkspaceType::Regular(name) = workspace.name {
if let Ok(id) = name.parse::<i32>() {
inst_clone
.lock()
.expect("Couldn't get Subscribers")
.publish(WindowManagerEvent::WorkspaceRemoved(id));
}
}
});
let inst_clone = instance.clone();
listener.add_active_monitor_changed_v2_handler(move |evt| {
inst_clone
.lock()
.expect("Couldn't get Subscribers")
.publish(WindowManagerEvent::WorkspaceChanged(evt.workspace_id));
});
let inst_clone = instance.clone();
listener.add_active_window_changed_handler(move |data_opt| {
let title = data_opt.map(|data| data.title).unwrap_or(String::new());
inst_clone
.lock()
.expect("Couldn't get Subscribers")
.publish(WindowManagerEvent::WindowFocusChanged(title));
});
let inst_clone = instance.clone();
listener.add_workspace_moved_handler(move |event| {
if let Some(monitor_id) = Self::monitor_id_from_workspace_id(event.id) {
inst_clone
.lock()
.expect("Couldn't get Subscribers")
.publish(WindowManagerEvent::WorkspaceRemoved(event.id));
inst_clone
.lock()
.expect("Couldn't get Subscribers")
.publish(WindowManagerEvent::WorkspaceAdded(event.id, monitor_id));
}
});
let inst_clone = instance.clone();
listener.add_sub_map_changed_handler(move |submap| {
let submap = if submap.is_empty() {
None
} else {
Some(submap.clone())
};
inst_clone
.lock()
.expect("Couldn't get Subscribers")
.publish(WindowManagerEvent::SubmapChange(submap))
});
std::thread::spawn(move || {
let inst_clone = instance.clone();
while let Ok(tx) = registration_rx.recv() {
trace!(&LOGGER, "Added WM Hyprland Subscriber: {tx:?}");
inst_clone
.lock()
.expect("Couldn't get Subscribers")
.subscribers
.push(tx);
}
});
if let Err(err) = listener.start_listener() {
error!(&LOGGER, "Hyprland event listener failed {:?}", err);
}
});
}
}
impl HyprlandComms {
fn monitor_id_from_workspace_id(id: WorkspaceId) -> Option<MonitorId> {
if let Ok(wss) = Workspaces::get() {
if let Some(ws) = wss.to_vec().iter().find(|ws| ws.id().unwrap() == id) {
return ws.monitor_id;
}
}
None
}
pub fn _current_monitor() -> MonitorId {
if let Ok(monitor) = Monitor::get_active() {
return monitor.id;
}
0
}
pub fn workspace_added(id: WorkspaceId, monitor: Option<MonitorId>) -> bool {
if let Ok(wss) = Workspaces::get() {
wss.to_vec()
.iter()
.find(|ws| {
let b1 = ws.id().unwrap() == id;
let b2 =
monitor.is_some_and(|id| ws.monitor_id.is_some_and(|mon_id| id == mon_id));
b1 && b2
})
.is_some()
} else {
false
}
}
pub fn get_current_title() -> String {
if let Ok(Some(client)) = Client::get_active() {
client.title
} else {
String::new()
}
}
pub fn dispatch(dispatcher: &str) -> io::Result<Output> {
Command::new("sh")
.arg("-c")
.arg(format!("hyprctl dispatch \"{dispatcher}\""))
.output()
}
}