First Commit
This commit is contained in:
166
src/backend.rs
Normal file
166
src/backend.rs
Normal file
@@ -0,0 +1,166 @@
|
||||
pub mod audio;
|
||||
pub mod brightness_control;
|
||||
pub mod network;
|
||||
pub mod perf_stats;
|
||||
pub(crate) mod tailscale;
|
||||
pub mod window_manager_hypr;
|
||||
|
||||
use crate::LOGGER;
|
||||
use crate::backend::audio::{MediaBackend, MediaCommand};
|
||||
use crate::backend::brightness_control::{BrightnessManager, BrightnessMessage};
|
||||
use crate::backend::network::{NetworkBackendHandle, NetworkCommand, NetworkState};
|
||||
use crate::backend::perf_stats::{Stats, StatsBackend};
|
||||
use crate::backend::tailscale::{TailscaleBackend, TailscaleState};
|
||||
use crate::backend::window_manager_hypr::{HyprlandComms, WindowManagerEvent};
|
||||
use crossbeam_channel::{Receiver, Sender};
|
||||
use jmb_logger_rs::*;
|
||||
use std::fmt::Debug;
|
||||
use std::process::{Child, Command, Stdio};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
|
||||
pub(crate) type Registrator<M> = Sender<Sender<M>>;
|
||||
|
||||
pub trait BackendThread<M: Debug + Clone, D> {
|
||||
fn subscribe(registrator: Registrator<M>) -> (Receiver<M>, D);
|
||||
fn subscribers(&self) -> &Vec<Sender<M>>;
|
||||
fn init(registration_rx: Receiver<Sender<M>>);
|
||||
fn publish(&self, message: M) {
|
||||
trace!(&LOGGER, "Sending: {message:?} to {:?}", self.subscribers());
|
||||
for tx in self.subscribers() {
|
||||
if let Err(e) = tx.send(message.clone()) {
|
||||
warn!(
|
||||
&LOGGER,
|
||||
"Error sending message to frontend: {}",
|
||||
e.to_string()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn start() -> Registrator<M> {
|
||||
let (registration_tx, registration_rx) = crossbeam_channel::unbounded::<Sender<M>>();
|
||||
Self::init(registration_rx);
|
||||
registration_tx
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Backend {
|
||||
hypr_registrator: Registrator<WindowManagerEvent>,
|
||||
net_registrator: Registrator<NetworkState>,
|
||||
br_registrator: Registrator<BrightnessMessage>,
|
||||
stats_registrator: Registrator<Stats>,
|
||||
media_registrator: Registrator<MediaCommand>,
|
||||
tailscale_registrator: Registrator<TailscaleState>,
|
||||
hypridle: Arc<Mutex<Option<Child>>>,
|
||||
}
|
||||
|
||||
impl Backend {
|
||||
pub fn start() -> Self {
|
||||
debug!(&LOGGER, "Starting Backend threads...");
|
||||
let hypr_registrator = HyprlandComms::start();
|
||||
let net_registrator = NetworkBackendHandle::start();
|
||||
let br_registrator = BrightnessManager::start();
|
||||
let stats_registrator = StatsBackend::start();
|
||||
let media_registrator = MediaBackend::start();
|
||||
let tailscale_registrator = TailscaleBackend::start();
|
||||
let hypridle = Arc::new(Mutex::new(
|
||||
Command::new("hypridle").stdout(Stdio::null()).spawn().ok(),
|
||||
));
|
||||
debug!(&LOGGER, "Started Backend threads");
|
||||
Self {
|
||||
hypr_registrator,
|
||||
net_registrator,
|
||||
br_registrator,
|
||||
stats_registrator,
|
||||
media_registrator,
|
||||
tailscale_registrator,
|
||||
hypridle,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn subscribe(&self) -> BackendChannels {
|
||||
let (net_rx, net_tx) = NetworkBackendHandle::subscribe(self.net_registrator.clone());
|
||||
let (hypr_rx, _) = HyprlandComms::subscribe(self.hypr_registrator.clone());
|
||||
let (br_rx, br_tx) = BrightnessManager::subscribe(self.br_registrator.clone());
|
||||
let (stats_rx, _) = StatsBackend::subscribe(self.stats_registrator.clone());
|
||||
let (media_rx, media_tx) = MediaBackend::subscribe(self.media_registrator.clone());
|
||||
let (tailscale_rx, _) = TailscaleBackend::subscribe(self.tailscale_registrator.clone());
|
||||
BackendChannels {
|
||||
net_rx: Some(net_rx),
|
||||
net_tx,
|
||||
hypr_rx: Some(hypr_rx),
|
||||
br_rx: Some(br_rx),
|
||||
br_tx,
|
||||
stats_rx: Some(stats_rx),
|
||||
media_rx: Some(media_rx),
|
||||
media_tx,
|
||||
tailscale_rx: Some(tailscale_rx),
|
||||
hypridle: self.hypridle.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_drop_guard(&self) -> DropGuard {
|
||||
DropGuard(self.hypridle.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BackendChannels {
|
||||
net_rx: Option<Receiver<NetworkState>>,
|
||||
net_tx: UnboundedSender<NetworkCommand>,
|
||||
hypr_rx: Option<Receiver<WindowManagerEvent>>,
|
||||
br_rx: Option<Receiver<BrightnessMessage>>,
|
||||
br_tx: Sender<BrightnessMessage>,
|
||||
stats_rx: Option<Receiver<Stats>>,
|
||||
media_rx: Option<Receiver<MediaCommand>>,
|
||||
media_tx: Sender<MediaCommand>,
|
||||
tailscale_rx: Option<Receiver<TailscaleState>>,
|
||||
hypridle: Arc<Mutex<Option<Child>>>,
|
||||
}
|
||||
|
||||
impl BackendChannels {
|
||||
pub fn take_net_rx(&mut self) -> Option<Receiver<NetworkState>> {
|
||||
self.net_rx.take()
|
||||
}
|
||||
pub fn take_hypr_rx(&mut self) -> Option<Receiver<WindowManagerEvent>> {
|
||||
self.hypr_rx.take()
|
||||
}
|
||||
pub fn take_br_rx(&mut self) -> Option<Receiver<BrightnessMessage>> {
|
||||
self.br_rx.take()
|
||||
}
|
||||
pub fn take_stats_rx(&mut self) -> Option<Receiver<Stats>> {
|
||||
self.stats_rx.take()
|
||||
}
|
||||
pub fn take_ts_rx(&mut self) -> Option<Receiver<TailscaleState>> {
|
||||
self.tailscale_rx.take()
|
||||
}
|
||||
pub fn take_media_rx(&mut self) -> Option<Receiver<MediaCommand>> {
|
||||
self.media_rx.take()
|
||||
}
|
||||
pub fn net_tx(&self) -> UnboundedSender<NetworkCommand> {
|
||||
self.net_tx.clone()
|
||||
}
|
||||
pub fn br_tx(&self) -> Sender<BrightnessMessage> {
|
||||
self.br_tx.clone()
|
||||
}
|
||||
pub fn media_tx(&self) -> Sender<MediaCommand> {
|
||||
self.media_tx.clone()
|
||||
}
|
||||
pub fn hypridle(&self) -> Arc<Mutex<Option<Child>>> {
|
||||
self.hypridle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DropGuard(Arc<Mutex<Option<Child>>>);
|
||||
impl Drop for DropGuard {
|
||||
fn drop(&mut self) {
|
||||
let mut hypridle = self
|
||||
.0
|
||||
.lock()
|
||||
.expect("Couldn't get lock on hypridle child process handle");
|
||||
if hypridle.is_some() {
|
||||
info!(&LOGGER, "Killing hypridle");
|
||||
let _ = hypridle.as_mut().unwrap().kill();
|
||||
}
|
||||
}
|
||||
}
|
||||
423
src/backend/audio.rs
Normal file
423
src/backend/audio.rs
Normal 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",
|
||||
))
|
||||
}
|
||||
}
|
||||
323
src/backend/brightness_control.rs
Normal file
323
src/backend/brightness_control.rs
Normal 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
488
src/backend/network.rs
Normal 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
140
src/backend/perf_stats.rs
Normal 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
188
src/backend/tailscale.rs
Normal 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
|
||||
}
|
||||
207
src/backend/window_manager_hypr.rs
Normal file
207
src/backend/window_manager_hypr.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
56
src/css.rs
Normal file
56
src/css.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use glib::ControlFlow;
|
||||
use jmb_logger_rs::*;
|
||||
use notify::EventKind::{Create, Modify, Remove};
|
||||
use notify::{RecommendedWatcher, RecursiveMode, Watcher};
|
||||
use std::path::Path;
|
||||
use std::sync::mpsc;
|
||||
use std::time::Duration;
|
||||
use crate::LOGGER;
|
||||
|
||||
pub fn parse_css() -> Option<String> {
|
||||
match grass::from_path(
|
||||
"style/style.scss",
|
||||
&grass::Options::default(),
|
||||
) {
|
||||
Ok(css) => {
|
||||
debug!(&LOGGER, "(Re-)Loading CSS");
|
||||
Some(css)
|
||||
},
|
||||
Err(err) => {
|
||||
error!(&LOGGER, "SCSS compilation failed: {err}");
|
||||
None
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn watch_css<F>(reload: F) -> RecommendedWatcher
|
||||
where F: Fn(String) -> () + 'static {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
|
||||
let mut watcher = notify::recommended_watcher(tx).unwrap();
|
||||
|
||||
watcher
|
||||
.watch(
|
||||
Path::new("style"),
|
||||
RecursiveMode::Recursive,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
glib::timeout_add_local(Duration::from_millis(1000), move || {
|
||||
let mut changed = false;
|
||||
while let Ok(Ok(event)) = rx.try_recv() {
|
||||
match event.kind {
|
||||
Create(_) | Modify(_) | Remove(_) => changed = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if changed && let Some(css) = parse_css() {
|
||||
reload(css);
|
||||
}
|
||||
|
||||
ControlFlow::Continue
|
||||
});
|
||||
|
||||
watcher
|
||||
}
|
||||
58
src/main.rs
Normal file
58
src/main.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use crate::backend::Backend;
|
||||
mod backend;
|
||||
mod css;
|
||||
mod ui;
|
||||
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Application, CssProvider, gdk, gio};
|
||||
use jmb_logger_rs::*;
|
||||
use notify::RecommendedWatcher;
|
||||
use std::cell::RefCell;
|
||||
use std::process::Command;
|
||||
use std::rc::Rc;
|
||||
|
||||
pub const LOGGER: Logger = Logger::create(Level::DEBUG);
|
||||
|
||||
fn main() {
|
||||
info!(&LOGGER, "Starting jmb-bar");
|
||||
|
||||
gio::resources_register_include!("templates.gresource").expect("Failed to register resources");
|
||||
|
||||
let app = Application::builder()
|
||||
.application_id("net.jmb19905.jmb-bar")
|
||||
.build();
|
||||
|
||||
let watcher_slot: Rc<RefCell<Option<RecommendedWatcher>>> = Rc::new(RefCell::new(None));
|
||||
|
||||
let _ = Command::new("pkill hypridle").output(); // Kill existing hypridle processes
|
||||
|
||||
let backend = Backend::start();
|
||||
let _guard = backend.create_drop_guard(); // Make sure hypridle gets killed when the process exits
|
||||
trace!(&LOGGER, "Initialized DropGuard");
|
||||
|
||||
app.connect_startup(move |_| {
|
||||
let provider = CssProvider::new();
|
||||
if let Some(css) = css::parse_css() {
|
||||
provider.load_from_string(&css);
|
||||
}
|
||||
gtk::style_context_add_provider_for_display(
|
||||
&gdk::Display::default().expect("Could not connect to display"),
|
||||
&provider,
|
||||
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
|
||||
);
|
||||
let watcher = css::watch_css(move |css| provider.load_from_string(&css));
|
||||
*watcher_slot.borrow_mut() = Some(watcher);
|
||||
});
|
||||
|
||||
app.connect_activate(clone!(
|
||||
#[strong]
|
||||
backend,
|
||||
move |app| {
|
||||
trace!(&LOGGER, "Starting bars");
|
||||
ui::create_bars(app, backend.clone());
|
||||
}
|
||||
));
|
||||
|
||||
app.run();
|
||||
}
|
||||
236
src/ui.rs
Normal file
236
src/ui.rs
Normal file
@@ -0,0 +1,236 @@
|
||||
use crate::LOGGER;
|
||||
use crate::backend::window_manager_hypr::{HyprlandComms, WindowManagerEvent};
|
||||
use crate::backend::{Backend, BackendChannels};
|
||||
use crate::ui::action_centre::ActionCentre;
|
||||
use crossbeam_channel::TryRecvError;
|
||||
use glib::ControlFlow;
|
||||
use glib::prelude::Cast;
|
||||
use gtk::gdk::Display;
|
||||
use gtk::prelude::{BoxExt, ButtonExt, DisplayExt, GtkWindowExt, ListModelExt, WidgetExt};
|
||||
use gtk::{Align, Application, ApplicationWindow, Button, Orientation, gdk};
|
||||
use gtk4_layer_shell::{Edge, KeyboardMode, Layer, LayerShell};
|
||||
use jmb_logger_rs::*;
|
||||
use mpris::PlayerFinder;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
use std::time::Duration;
|
||||
|
||||
mod action_centre;
|
||||
mod systray;
|
||||
mod title_panel;
|
||||
mod utilities_panel;
|
||||
mod widgets;
|
||||
mod workspaces_panel;
|
||||
|
||||
pub fn create_bars(app: &Application, backend: Backend) {
|
||||
let display = Display::default().expect("Couldn't find GDK Display");
|
||||
let app_clone = app.clone();
|
||||
let bars: Mutex<HashMap<u32, ApplicationWindow>> = Mutex::new(HashMap::new());
|
||||
let ac_channels = backend.subscribe();
|
||||
let action_centre = ActionCentre::create_action_centre(app.clone(), ac_channels);
|
||||
for monitor in 0..display.monitors().n_items() {
|
||||
debug!(&LOGGER, "Opening bar {monitor}");
|
||||
let channels = backend.subscribe();
|
||||
bars.lock().expect("Couldn't get active bars").insert(
|
||||
monitor,
|
||||
create_bar(
|
||||
app_clone.clone(),
|
||||
&display,
|
||||
monitor,
|
||||
action_centre.clone(),
|
||||
channels,
|
||||
),
|
||||
);
|
||||
}
|
||||
display
|
||||
.monitors()
|
||||
.connect_items_changed(move |_list, pos, removed, added| {
|
||||
if removed == 1 {
|
||||
debug!(&LOGGER, "Detected Monitor disconnect {pos}");
|
||||
if let Some(bar) = bars.lock().expect("Couldn't get active bars").remove(&pos) {
|
||||
bar.close();
|
||||
}
|
||||
} else if added == 1 {
|
||||
debug!(&LOGGER, "Detected new monitor: {pos}");
|
||||
let channels = backend.subscribe();
|
||||
bars.lock().expect("Couldn't get active bars").insert(
|
||||
pos,
|
||||
create_bar(
|
||||
app_clone.clone(),
|
||||
&display,
|
||||
pos,
|
||||
action_centre.clone(),
|
||||
channels,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn create_bar(
|
||||
app: Application,
|
||||
display: &Display,
|
||||
monitor_idx: u32,
|
||||
action_centre: ActionCentre,
|
||||
mut channels: BackendChannels,
|
||||
) -> ApplicationWindow {
|
||||
// Take the Receiver and replace it with none so that it can be only used in one place
|
||||
let hypr_rx = channels
|
||||
.take_hypr_rx()
|
||||
.expect("Tried taking unavailable Hyprland Channel Receiver");
|
||||
|
||||
let player_opt = if let Ok(player_finder) = PlayerFinder::new() {
|
||||
if let Ok(player) = player_finder.find_active() {
|
||||
Some(player)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let window = ApplicationWindow::builder()
|
||||
.application(&app)
|
||||
.title("JmbBar")
|
||||
.default_width(500)
|
||||
.default_height(30)
|
||||
.css_classes(vec!["bar"])
|
||||
.build();
|
||||
|
||||
let monitors = display.monitors();
|
||||
|
||||
window.init_layer_shell();
|
||||
window.set_layer(Layer::Top);
|
||||
window.set_anchor(Edge::Top, true);
|
||||
window.set_anchor(Edge::Left, true);
|
||||
window.set_anchor(Edge::Right, true);
|
||||
window.set_margin(Edge::Top, 20);
|
||||
if let Some(monitor) = monitors.item(monitor_idx) {
|
||||
if let Ok(monitor) = monitor.downcast::<gdk::Monitor>() {
|
||||
window.set_monitor(Some(&monitor));
|
||||
}
|
||||
}
|
||||
|
||||
window.set_exclusive_zone(30);
|
||||
|
||||
let mut buttons: HashMap<i32, Button> = HashMap::new();
|
||||
let (workspaces, submap) = workspaces_panel::build(
|
||||
&mut buttons,
|
||||
window
|
||||
.monitor()
|
||||
.map(|monitor| monitor_index(&monitor))
|
||||
.flatten(),
|
||||
);
|
||||
|
||||
let title_button: Option<Button>;
|
||||
let title = title_panel::build();
|
||||
|
||||
let utilities = utilities_panel::build(&app, action_centre, channels, monitor_idx, display);
|
||||
|
||||
let dock = gtk::Box::new(Orientation::Horizontal, 10);
|
||||
dock.add_css_class("dock");
|
||||
dock.set_vexpand(true);
|
||||
dock.set_valign(Align::Fill);
|
||||
dock.append(&workspaces);
|
||||
dock.append(&title);
|
||||
dock.append(&utilities);
|
||||
|
||||
title_button = Some(title);
|
||||
title_panel::manage_title(&title_button, &player_opt);
|
||||
|
||||
window.set_keyboard_mode(KeyboardMode::None);
|
||||
window.set_child(Some(&dock));
|
||||
window.present();
|
||||
|
||||
let window_clone = window.clone();
|
||||
|
||||
glib::timeout_add_local(Duration::from_millis(50), move || {
|
||||
loop {
|
||||
match hypr_rx.try_recv() {
|
||||
Ok(event) => {
|
||||
handle_wm_event(
|
||||
&mut buttons,
|
||||
&submap,
|
||||
&title_button,
|
||||
event,
|
||||
window_clone.monitor(),
|
||||
);
|
||||
}
|
||||
Err(TryRecvError::Empty) => break,
|
||||
Err(TryRecvError::Disconnected) => {
|
||||
return ControlFlow::Break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
title_panel::manage_title(&title_button, &player_opt);
|
||||
|
||||
ControlFlow::Continue
|
||||
});
|
||||
window
|
||||
}
|
||||
|
||||
fn monitor_index(target: &gdk::Monitor) -> Option<u32> {
|
||||
let display = Display::default()?;
|
||||
let monitors = display.monitors();
|
||||
|
||||
(0..monitors.n_items()).find(|&i| {
|
||||
monitors
|
||||
.item(i)
|
||||
.and_then(|obj| obj.downcast::<gdk::Monitor>().ok())
|
||||
.is_some_and(|monitor| monitor == *target)
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_wm_event(
|
||||
buttons: &mut HashMap<i32, Button>,
|
||||
submap_label: &Button,
|
||||
title_button: &Option<Button>,
|
||||
event: WindowManagerEvent,
|
||||
current_monitor: Option<gdk::Monitor>,
|
||||
) {
|
||||
trace!(&LOGGER, "Received WM Event {event:?}");
|
||||
match event {
|
||||
WindowManagerEvent::WorkspaceChanged(workspace) => {
|
||||
for (&id, button) in buttons {
|
||||
if id == workspace {
|
||||
button.add_css_class("active");
|
||||
} else {
|
||||
button.remove_css_class("active");
|
||||
}
|
||||
}
|
||||
title_panel::set_title(&HyprlandComms::get_current_title(), title_button);
|
||||
}
|
||||
WindowManagerEvent::WorkspaceAdded(workspace, monitor) => {
|
||||
for (&id, button) in buttons {
|
||||
if id == workspace
|
||||
&& current_monitor.as_ref().is_none_or(|cur_mon| {
|
||||
monitor_index(&cur_mon).is_some_and(|idx| idx == monitor as u32)
|
||||
})
|
||||
{
|
||||
button.set_visible(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
WindowManagerEvent::WorkspaceRemoved(workspace) => {
|
||||
for (&id, button) in buttons {
|
||||
if id == workspace {
|
||||
button.set_visible(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
WindowManagerEvent::WindowFocusChanged(title) => {
|
||||
title_panel::set_title(&title, title_button);
|
||||
}
|
||||
WindowManagerEvent::SubmapChange(submap) => match submap {
|
||||
Some(submap) => {
|
||||
submap_label.set_label(&submap);
|
||||
submap_label.add_css_class("active");
|
||||
}
|
||||
None => {
|
||||
submap_label.set_label("");
|
||||
submap_label.remove_css_class("active");
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
383
src/ui/action_centre.rs
Normal file
383
src/ui/action_centre.rs
Normal file
@@ -0,0 +1,383 @@
|
||||
use crate::backend::BackendChannels;
|
||||
use crate::backend::network::{
|
||||
Auth, AuthType, ConnectError, NetworkCommand, NetworkInfo, NetworkState,
|
||||
};
|
||||
use crate::ui::widgets::{LabelEntry, WifiWidget};
|
||||
use glib::prelude::Cast;
|
||||
use glib::{ControlFlow, clone};
|
||||
use gtk::gdk::Display;
|
||||
use gtk::prelude::{
|
||||
BoxExt, ButtonExt, DisplayExt, EditableExt, GtkWindowExt, ListModelExt, WidgetExt,
|
||||
};
|
||||
use gtk::{
|
||||
Application, ApplicationWindow, Button, Image, Label, Orientation, PolicyType, ScrolledWindow,
|
||||
Stack, gdk,
|
||||
};
|
||||
use gtk4_layer_shell::{Edge, KeyboardMode, Layer, LayerShell};
|
||||
use std::cell::{Cell, RefCell, RefMut};
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
const WIFI_ICONS: [&str; 4] = [
|
||||
"xsi-network-wireless-signal-none-symbolic",
|
||||
"xsi-network-wireless-signal-weak-symbolic",
|
||||
"xsi-network-wireless-signal-ok-symbolic",
|
||||
"xsi-network-wireless-signal-good-symbolic",
|
||||
];
|
||||
const _WIFI_ICONS_SECURED: [&str; 4] = [
|
||||
"xsi-network-wireless-signal-none-secure-symbolic",
|
||||
"xsi-network-wireless-signal-weak-secure-symbolic",
|
||||
"xsi-network-wireless-signal-ok-secure-symbolic",
|
||||
"xsi-network-wireless-signal-good-secure-symbolic",
|
||||
];
|
||||
|
||||
const ANIMATION_DURATION: u64 = 200;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ActionCentre {
|
||||
pub window: ApplicationWindow,
|
||||
is_animating: Rc<Cell<bool>>,
|
||||
}
|
||||
|
||||
impl ActionCentre {
|
||||
pub fn create_action_centre(app: Application, mut channels: BackendChannels) -> Self {
|
||||
let window = ApplicationWindow::builder()
|
||||
.application(&app)
|
||||
.title("JmbBar")
|
||||
.default_width(400)
|
||||
.default_height(1080)
|
||||
.css_classes(vec!["action-centre"])
|
||||
.build();
|
||||
|
||||
window.init_layer_shell();
|
||||
window.set_layer(Layer::Overlay);
|
||||
window.set_anchor(Edge::Top, true);
|
||||
window.set_anchor(Edge::Bottom, true);
|
||||
window.set_anchor(Edge::Right, true);
|
||||
window.set_margin(Edge::Top, 20);
|
||||
window.set_margin(Edge::Right, 20);
|
||||
window.set_margin(Edge::Bottom, 20);
|
||||
window.set_keyboard_mode(KeyboardMode::OnDemand);
|
||||
|
||||
let instance = Self {
|
||||
window: window.clone(),
|
||||
is_animating: Rc::new(Cell::new(false)),
|
||||
};
|
||||
|
||||
let scrolled = ScrolledWindow::new();
|
||||
scrolled.set_policy(PolicyType::Never, PolicyType::Automatic);
|
||||
scrolled.set_vexpand(true);
|
||||
let content = gtk::Box::new(Orientation::Vertical, 6);
|
||||
|
||||
let add_net_stack = Stack::new();
|
||||
add_net_stack.set_vhomogeneous(false);
|
||||
|
||||
let add_network_button = Button::new();
|
||||
add_network_button.add_css_class("wifi-button");
|
||||
|
||||
let add_net_box = gtk::Box::new(Orientation::Horizontal, 4);
|
||||
let add_icon = Image::from_file("icons/plus-symbolic.svg");
|
||||
add_icon.add_css_class("list-add-icon");
|
||||
add_icon.add_css_class("icon");
|
||||
add_icon.set_pixel_size(24);
|
||||
add_net_box.append(&add_icon);
|
||||
add_net_stack.add_named(&add_network_button, Some("button"));
|
||||
|
||||
let add_net_input_box = gtk::Box::new(Orientation::Vertical, 4);
|
||||
let ssid = LabelEntry::new("SSID:");
|
||||
add_net_input_box.append(&ssid);
|
||||
let pw = LabelEntry::new("Passphrase:");
|
||||
add_net_input_box.append(&pw);
|
||||
add_net_stack.add_named(&add_net_input_box, Some("input"));
|
||||
|
||||
add_net_stack.set_visible_child_name("button");
|
||||
|
||||
add_network_button.connect_clicked(clone!(
|
||||
#[strong]
|
||||
add_net_stack,
|
||||
move |_button| {
|
||||
add_net_stack.set_visible_child_name("input");
|
||||
}
|
||||
));
|
||||
|
||||
let label = Label::new(Some("Add Network..."));
|
||||
add_net_box.append(&label);
|
||||
add_network_button.set_child(Some(&add_net_box));
|
||||
|
||||
content.append(&add_net_stack);
|
||||
|
||||
scrolled.set_child(Some(&content));
|
||||
|
||||
window.set_child(Some(&scrolled));
|
||||
|
||||
let instance_clone = instance.clone();
|
||||
let key_controller = gtk::EventControllerKey::new();
|
||||
key_controller.connect_key_pressed(move |_, key, _, _| {
|
||||
if key == gdk::Key::Escape {
|
||||
instance_clone.hide_action_centre();
|
||||
glib::Propagation::Stop
|
||||
} else {
|
||||
glib::Propagation::Proceed
|
||||
}
|
||||
});
|
||||
window.add_controller(key_controller);
|
||||
|
||||
let wifi_networks: Rc<RefCell<HashMap<String, WifiWidget>>> =
|
||||
Rc::new(RefCell::new(HashMap::new()));
|
||||
let wifi_networks_clone = wifi_networks.clone();
|
||||
window.connect_focus_widget_notify(move |window| {
|
||||
Self::focus_callback(&wifi_networks_clone, window)
|
||||
});
|
||||
|
||||
let net_rx = channels.take_net_rx().unwrap();
|
||||
let window_clone = window.clone();
|
||||
let wifi_networks_clone = wifi_networks.clone();
|
||||
glib::timeout_add_local(Duration::from_millis(100), move || {
|
||||
while let Ok(message) = net_rx.try_recv() {
|
||||
match message {
|
||||
NetworkState::ChangeNetwork(info) => match info {
|
||||
Some(info) => {
|
||||
for (name, widget) in wifi_networks_clone.borrow().iter() {
|
||||
if name.eq(&info.name) {
|
||||
widget.add_css_class("connected");
|
||||
} else {
|
||||
widget.remove_css_class("connected");
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
},
|
||||
NetworkState::Networks(networks) => {
|
||||
for network in &networks {
|
||||
let borrowed_wifi_networks_clone = wifi_networks_clone.borrow_mut();
|
||||
let widget = borrowed_wifi_networks_clone.get(&network.name);
|
||||
if widget.is_none() {
|
||||
let widget = Self::create_wifi_widget(
|
||||
&channels,
|
||||
network,
|
||||
borrowed_wifi_networks_clone,
|
||||
);
|
||||
content.append(&widget);
|
||||
} else {
|
||||
let idx = ((network.signal as f32 / 100.0)
|
||||
* (WIFI_ICONS.len() - 1) as f32
|
||||
+ 0.5) as usize;
|
||||
widget.unwrap().set_wifi_icon_from_name(WIFI_ICONS[idx]);
|
||||
}
|
||||
}
|
||||
let mut removal: Vec<String> = Vec::new();
|
||||
for (ssid, widget) in wifi_networks_clone.borrow().iter() {
|
||||
let network = &networks.iter().find(|n| n.name.eq(ssid));
|
||||
if network.is_none() {
|
||||
content.remove(widget);
|
||||
removal.push(ssid.clone());
|
||||
}
|
||||
}
|
||||
for ssid in removal {
|
||||
wifi_networks_clone.borrow_mut().remove(&ssid);
|
||||
}
|
||||
}
|
||||
NetworkState::Auth(ssid, auth) => {
|
||||
if let Some(widget) = Self::find_focused_widget((&window_clone).as_ref()) {
|
||||
match auth {
|
||||
AuthType::Password => {
|
||||
if widget.ssid().eq(&ssid) {
|
||||
widget.show_password_entry();
|
||||
widget.stop_spinner();
|
||||
widget.grab_focus_password_entry();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
NetworkState::ConnectFail(ssid, err) => {
|
||||
let mut networks = wifi_networks_clone.borrow_mut();
|
||||
if let Some(widget) = networks.get(&ssid) {
|
||||
widget.stop_spinner();
|
||||
match err {
|
||||
ConnectError::AuthFailed => widget.show_password_entry(),
|
||||
ConnectError::NotFound => {
|
||||
content.remove(widget);
|
||||
networks.remove(&ssid);
|
||||
}
|
||||
ConnectError::Unknown => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
NetworkState::ConnectSuccess(ssid) => {
|
||||
if let Some(widget) = wifi_networks_clone.borrow().get(&ssid) {
|
||||
widget.stop_spinner();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
ControlFlow::Continue
|
||||
});
|
||||
|
||||
instance
|
||||
}
|
||||
|
||||
fn create_wifi_widget(
|
||||
channels: &BackendChannels,
|
||||
network: &NetworkInfo,
|
||||
mut borrowed_wifi_networks_clone: RefMut<HashMap<String, WifiWidget>>,
|
||||
) -> WifiWidget {
|
||||
let ssid = network.name.clone();
|
||||
let widget = WifiWidget::new();
|
||||
widget.set_ssid(&ssid);
|
||||
widget.add_css_class("wifi-button");
|
||||
widget.set_secured(network.secured.unwrap());
|
||||
widget.set_known(network.known);
|
||||
let idx = ((network.signal as f32 / 100.0) * (WIFI_ICONS.len() - 1) as f32 + 0.5) as usize;
|
||||
widget.set_wifi_icon_from_name(WIFI_ICONS[idx]);
|
||||
borrowed_wifi_networks_clone.insert(ssid.clone(), widget.clone());
|
||||
let net_tx = channels.net_tx();
|
||||
widget.connect_clicked(clone!(
|
||||
#[strong]
|
||||
net_tx,
|
||||
#[strong]
|
||||
ssid,
|
||||
move |widget| {
|
||||
let _ = net_tx.send(NetworkCommand::Connect(ssid.clone(), Auth::Open));
|
||||
widget.start_spinner();
|
||||
}
|
||||
));
|
||||
widget.connect_activate_pass(clone!(
|
||||
#[strong]
|
||||
net_tx,
|
||||
#[weak]
|
||||
widget,
|
||||
move |entry| {
|
||||
let text = entry.text().to_string();
|
||||
if !text.is_empty() {
|
||||
let _ = net_tx.send(NetworkCommand::Connect(
|
||||
ssid.clone(),
|
||||
Auth::Password(entry.text().to_string()),
|
||||
));
|
||||
widget.start_spinner();
|
||||
widget.show_ssid();
|
||||
}
|
||||
}
|
||||
));
|
||||
widget
|
||||
}
|
||||
|
||||
fn focus_callback(
|
||||
wifi_networks: &Rc<RefCell<HashMap<String, WifiWidget>>>,
|
||||
window: &ApplicationWindow,
|
||||
) {
|
||||
for (_, widget) in wifi_networks.clone().borrow().iter() {
|
||||
let focused_inside = window.focus().is_some_and(|mut focus| {
|
||||
loop {
|
||||
if focus == *widget {
|
||||
break true;
|
||||
}
|
||||
|
||||
match focus.parent() {
|
||||
Some(parent) => focus = parent,
|
||||
None => break false,
|
||||
}
|
||||
}
|
||||
});
|
||||
if !focused_inside {
|
||||
widget.show_ssid();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn _has_focus_within(window: &ApplicationWindow) -> bool {
|
||||
let Some(mut widget) = window.focus() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
loop {
|
||||
if widget == *window {
|
||||
return true;
|
||||
}
|
||||
|
||||
match widget.parent() {
|
||||
Some(parent) => widget = parent,
|
||||
None => return false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn find_focused_widget(window: >k::Window) -> Option<WifiWidget> {
|
||||
let mut widget = window.focus()?;
|
||||
|
||||
loop {
|
||||
if let Ok(widget) = widget.clone().downcast::<WifiWidget>() {
|
||||
return Some(widget);
|
||||
}
|
||||
|
||||
widget = widget.parent()?;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show_action_centre(&self, monitor_idx: u32, display: &Display) {
|
||||
let window = &self.window;
|
||||
let monitors = display.monitors();
|
||||
let Some(monitor) = monitors.item(monitor_idx) else {
|
||||
return;
|
||||
};
|
||||
let Ok(monitor) = monitor.downcast::<gdk::Monitor>() else {
|
||||
return;
|
||||
};
|
||||
window.set_monitor(Some(&monitor));
|
||||
if !window.is_visible() {
|
||||
self.animate_margin(-window.width() - 20, 20, ANIMATION_DURATION, || {});
|
||||
window.present();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hide_action_centre(&self) {
|
||||
let clone = self.clone();
|
||||
self.animate_margin(
|
||||
20,
|
||||
-clone.window.width() - 20,
|
||||
ANIMATION_DURATION,
|
||||
move || clone.window.set_visible(false),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn animate_margin(
|
||||
&self,
|
||||
from: i32,
|
||||
to: i32,
|
||||
duration_ms: u64,
|
||||
on_finished: impl Fn() + 'static,
|
||||
) {
|
||||
let window = self.window.clone();
|
||||
let flag = self.is_animating.clone();
|
||||
let start = Instant::now();
|
||||
let duration = Duration::from_millis(duration_ms);
|
||||
|
||||
if flag.get() {
|
||||
return;
|
||||
}
|
||||
|
||||
glib::timeout_add_local(Duration::from_millis(10), move || {
|
||||
flag.set(true);
|
||||
let elapsed = start.elapsed();
|
||||
let t = (elapsed.as_secs_f64() / duration.as_secs_f64()).clamp(0.0, 1.0);
|
||||
// Ease-out cubic.
|
||||
let eased = 1.0 - (1.0 - t).powi(3);
|
||||
|
||||
let margin = from as f64 + (to - from) as f64 * eased;
|
||||
|
||||
window.set_margin(Edge::Right, margin.round() as i32);
|
||||
|
||||
if t >= 1.0 {
|
||||
on_finished();
|
||||
flag.set(false);
|
||||
ControlFlow::Break
|
||||
} else {
|
||||
ControlFlow::Continue
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
40
src/ui/systray.rs
Normal file
40
src/ui/systray.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
use crate::LOGGER;
|
||||
use glib::ControlFlow;
|
||||
use gtk::Orientation;
|
||||
use jmb_logger_rs::*;
|
||||
use rustsni::{TrayEvent, TrayHost};
|
||||
use std::time::Duration;
|
||||
|
||||
pub fn _build() -> gtk::Box {
|
||||
let systray = gtk::Box::new(Orientation::Horizontal, 0);
|
||||
let host = TrayHost::new();
|
||||
|
||||
if let Ok(mut host) = host {
|
||||
glib::timeout_add_local(
|
||||
Duration::from_millis(1_000),
|
||||
move || {
|
||||
for event in host.poll().unwrap() {
|
||||
match event {
|
||||
TrayEvent::ItemAdded(id) | TrayEvent::ItemChanged(id) => {
|
||||
if let Some(item) = host.items().get(&id) {
|
||||
println!("tray: {} — {}", item.title, item.status);
|
||||
println!(" category: {}", item.category);
|
||||
println!(" has menu: {}", item.has_menu());
|
||||
}
|
||||
}
|
||||
TrayEvent::ItemRemoved(id) => {
|
||||
println!("item gone: {id}");
|
||||
}
|
||||
TrayEvent::HostShutdown => break,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
ControlFlow::Continue
|
||||
}
|
||||
);
|
||||
} else {
|
||||
error!(&LOGGER, "Couldn't get SystemTray");
|
||||
}
|
||||
|
||||
systray
|
||||
}
|
||||
95
src/ui/title_panel.rs
Normal file
95
src/ui/title_panel.rs
Normal file
@@ -0,0 +1,95 @@
|
||||
use crate::backend::window_manager_hypr::HyprlandComms;
|
||||
use glib::prelude::CastNone;
|
||||
use gtk::pango::EllipsizeMode;
|
||||
use gtk::prelude::{ButtonExt, GestureSingleExt, WidgetExt};
|
||||
use gtk::{Align, Button, GestureClick, Label};
|
||||
use mpris::{PlaybackStatus, Player, PlayerFinder};
|
||||
|
||||
pub fn build() -> Button {
|
||||
let title_label = Label::new(None);
|
||||
title_label.set_ellipsize(EllipsizeMode::End);
|
||||
title_label.set_max_width_chars(50);
|
||||
|
||||
let title = Button::new();
|
||||
title.set_vexpand(true);
|
||||
title.set_valign(Align::Fill);
|
||||
title.set_child(Some(&title_label));
|
||||
title.set_visible(false);
|
||||
title.add_css_class("title");
|
||||
title.connect_clicked(|button| {
|
||||
if button.has_css_class("media") {
|
||||
if let Ok(player_finder) = PlayerFinder::new() {
|
||||
if let Ok(player) = player_finder.find_active() {
|
||||
let _ = player.play_pause();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let right_click = GestureClick::new();
|
||||
right_click.set_button(3);
|
||||
right_click.connect_pressed(|_gesture, _n_press, _, _| {
|
||||
if let Ok(player_finder) = PlayerFinder::new() {
|
||||
if let Ok(player) = player_finder.find_active() {
|
||||
let _ = player.stop();
|
||||
}
|
||||
}
|
||||
});
|
||||
title.add_controller(right_click);
|
||||
title
|
||||
}
|
||||
|
||||
pub fn manage_title(button_opt: &Option<Button>, player_opt: &Option<Player>) {
|
||||
let mut handled = false;
|
||||
if let Some(button) = button_opt {
|
||||
if let Some(player) = player_opt {
|
||||
if let Ok(status) = player.get_playback_status() {
|
||||
if let Ok(metadata) = player.get_metadata() {
|
||||
match status {
|
||||
PlaybackStatus::Playing => {
|
||||
button.set_visible(true);
|
||||
button.child()
|
||||
.and_downcast::<Label>()
|
||||
.unwrap()
|
||||
.set_label(&format!(" {}", metadata.title().unwrap_or("")));
|
||||
button.add_css_class("media");
|
||||
button.add_css_class("playing");
|
||||
handled = true;
|
||||
}
|
||||
PlaybackStatus::Paused => {
|
||||
button.set_visible(true);
|
||||
button.child()
|
||||
.and_downcast::<Label>()
|
||||
.unwrap()
|
||||
.set_label(&format!(" {}", metadata.title().unwrap_or("")));
|
||||
button.add_css_class("media");
|
||||
button.remove_css_class("playing");
|
||||
handled = true;
|
||||
}
|
||||
PlaybackStatus::Stopped => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !handled {
|
||||
button.set_visible(true);
|
||||
button.remove_css_class("media");
|
||||
button.remove_css_class("playing");
|
||||
set_title(&HyprlandComms::get_current_title(), button_opt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_title(title: &str, title_button: &Option<Button>) {
|
||||
if let Some(button) = title_button {
|
||||
if title.is_empty() {
|
||||
button.set_visible(false);
|
||||
} else {
|
||||
button.set_visible(true);
|
||||
}
|
||||
button.child()
|
||||
.and_downcast::<Label>()
|
||||
.unwrap()
|
||||
.set_label(&HyprlandComms::get_current_title());
|
||||
}
|
||||
}
|
||||
521
src/ui/utilities_panel.rs
Normal file
521
src/ui/utilities_panel.rs
Normal file
@@ -0,0 +1,521 @@
|
||||
use crate::LOGGER;
|
||||
use crate::backend::BackendChannels;
|
||||
use crate::backend::audio::MediaCommand;
|
||||
use crate::backend::brightness_control::BrightnessMessage;
|
||||
use crate::backend::network::{NetworkCommand, NetworkState, NetworkType};
|
||||
use crate::backend::tailscale::TailscaleState;
|
||||
use crate::ui::action_centre::ActionCentre;
|
||||
use crate::ui::widgets::{ExpandableBarItem, IconSlider};
|
||||
use battery::State;
|
||||
use glib::prelude::Cast;
|
||||
use glib::subclass::prelude::ObjectSubclassIsExt;
|
||||
use glib::{ControlFlow, clone};
|
||||
use gtk::prelude::{
|
||||
ApplicationExt, BoxExt, ButtonExt, DisplayExt, ListModelExt, MonitorExt, ToggleButtonExt,
|
||||
WidgetExt,
|
||||
};
|
||||
use gtk::{
|
||||
Adjustment, Align, Application, Button, Image, Label, Orientation, Revealer,
|
||||
RevealerTransitionType, gdk,
|
||||
};
|
||||
use jmb_logger_rs::*;
|
||||
use std::cell::Cell;
|
||||
use std::process::Command;
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
const BRIGHTNESS_ICONS: [&str; 3] = ["", "", ""];
|
||||
const BATTERY_ICONS: [&str; 5] = ["", "", "", "", ""];
|
||||
const WIFI_ICON_PATHS: [&str; 3] = [
|
||||
"icons/wifi_low.svg",
|
||||
"icons/wifi_medium.svg",
|
||||
"icons/wifi_full.svg",
|
||||
];
|
||||
const VOLUME_ICON_PATHS: [&str; 4] = [
|
||||
"icons/volume-off.svg",
|
||||
"icons/volume-low.svg",
|
||||
"icons/volume.svg",
|
||||
"icons/volume-high.svg",
|
||||
];
|
||||
|
||||
pub fn build(
|
||||
app: &Application,
|
||||
action_centre: ActionCentre,
|
||||
mut channels: BackendChannels,
|
||||
monitor_idx: u32,
|
||||
display: &gdk::Display,
|
||||
) -> gtk::Box {
|
||||
let utilities = gtk::Box::new(Orientation::Horizontal, 0);
|
||||
utilities.set_halign(Align::End);
|
||||
utilities.set_vexpand(true);
|
||||
utilities.set_hexpand(true);
|
||||
utilities.set_valign(Align::Fill);
|
||||
utilities.add_css_class("utilities");
|
||||
|
||||
let monitors = display.monitors();
|
||||
let monitor = if let Some(monitor) = monitors.item(monitor_idx)
|
||||
&& let Ok(monitor) = monitor.downcast::<gdk::Monitor>()
|
||||
&& let Some(connector) = monitor.connector()
|
||||
{
|
||||
String::from(connector)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let monitor_valid = !monitor.is_empty();
|
||||
if monitor_valid {
|
||||
let _ = channels
|
||||
.br_tx()
|
||||
.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 = IconSlider::new(Some(volume_adjustment));
|
||||
volume.imp().main_icon.set_icon_path(VOLUME_ICON_PATHS[2]);
|
||||
volume.imp().second_icon.set_icon_name("bluetooth-symbolic");
|
||||
volume.imp().second_icon.add_child_css_class("bt-icon");
|
||||
|
||||
let media_tx = channels.media_tx();
|
||||
let changed_progr = Rc::new(Cell::new(false));
|
||||
let changed_progr_clone = changed_progr.clone();
|
||||
volume.connect_value_changed(move |slider, button, _| {
|
||||
if !changed_progr_clone.get() {
|
||||
let _ = media_tx.send(MediaCommand::Cmd(
|
||||
slider.value() as u8,
|
||||
button.is_active(),
|
||||
false,
|
||||
));
|
||||
}
|
||||
});
|
||||
let media_tx = channels.media_tx();
|
||||
volume.connect_clicked(move |slider, button, _| {
|
||||
if button.is_active() {
|
||||
let _ = media_tx.send(MediaCommand::Cmd(slider.value() as u8, true, false));
|
||||
} else {
|
||||
let _ = media_tx.send(MediaCommand::Cmd(slider.value() as u8, false, false));
|
||||
}
|
||||
});
|
||||
|
||||
let brightness_adjustment = Adjustment::new(60.0, 1.0, 100.0, 1.0, 5.0, 0.0);
|
||||
let brightness = IconSlider::new(Some(brightness_adjustment));
|
||||
brightness.imp().main_icon.set_font_icon("");
|
||||
|
||||
let br_tx = channels.br_tx();
|
||||
brightness.connect_value_changed(clone!(
|
||||
#[strong]
|
||||
monitor,
|
||||
move |slider, _button, _| {
|
||||
if monitor_valid {
|
||||
let slider_val = slider.value().max(1.0);
|
||||
let _ = br_tx.send(BrightnessMessage::Value(monitor.clone(), slider_val as u8));
|
||||
let idx =
|
||||
((slider_val / 100.0) * (BRIGHTNESS_ICONS.len() - 1) as f64 + 0.5) as usize;
|
||||
slider.imp().main_icon.set_font_icon(BRIGHTNESS_ICONS[idx]);
|
||||
}
|
||||
}
|
||||
));
|
||||
let br_tx = channels.br_tx();
|
||||
brightness.connect_clicked(clone!(
|
||||
#[strong]
|
||||
monitor,
|
||||
move |slider, _button, _| {
|
||||
if monitor_valid {
|
||||
let _ = br_tx.send(BrightnessMessage::Value(monitor.clone(), 60));
|
||||
slider.set_value(60.0);
|
||||
let idx = ((60.0 / 100.0) * (BRIGHTNESS_ICONS.len() - 1) as f64 + 0.5) as usize;
|
||||
slider.imp().main_icon.set_font_icon(BRIGHTNESS_ICONS[idx]);
|
||||
}
|
||||
}
|
||||
));
|
||||
|
||||
let caffeine_button = Button::with_label(" ");
|
||||
caffeine_button.add_css_class("caffeine");
|
||||
let hypridle = channels.hypridle();
|
||||
caffeine_button.connect_clicked(move |button| {
|
||||
let mut child = hypridle.lock().expect("Couldn't get lock on hypridle");
|
||||
if child.is_none() {
|
||||
if let Ok(new_child) = Command::new("hypridle").spawn() {
|
||||
let _ = child.insert(new_child);
|
||||
button.remove_css_class("on")
|
||||
}
|
||||
} else if child.is_some()
|
||||
&& let Ok(exit) = child.as_mut().unwrap().try_wait()
|
||||
&& let Some(_) = exit
|
||||
{
|
||||
if let Ok(new_child) = Command::new("hypridle").spawn() {
|
||||
let _ = child.insert(new_child);
|
||||
button.remove_css_class("on")
|
||||
}
|
||||
} else {
|
||||
if let Ok(_) = child.as_mut().unwrap().kill() {
|
||||
let _ = child.take();
|
||||
button.add_css_class("on");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let stats = ExpandableBarItem::new("");
|
||||
stats.add_css_class("stats");
|
||||
stats.set_spacing(0);
|
||||
stats.set_vexpand(true);
|
||||
stats.set_valign(Align::Fill);
|
||||
|
||||
let cpu_box = gtk::Box::new(Orientation::Horizontal, 8);
|
||||
let cpu_icon = Label::new(Some(""));
|
||||
cpu_icon.add_css_class("stat-icon");
|
||||
cpu_box.append(&cpu_icon);
|
||||
let cpu_label = Label::new(None);
|
||||
cpu_box.append(&cpu_label);
|
||||
stats.set_button_child(Some(&cpu_box));
|
||||
|
||||
let stats_children = gtk::Box::new(Orientation::Horizontal, 6);
|
||||
stats_children.add_css_class("expandable-content");
|
||||
|
||||
let ram_box = gtk::Box::new(Orientation::Horizontal, 6);
|
||||
let ram_icon = Label::new(Some(""));
|
||||
ram_icon.add_css_class("stat-icon");
|
||||
ram_box.append(&ram_icon);
|
||||
let ram_label = Label::new(None);
|
||||
ram_box.append(&ram_label);
|
||||
stats_children.append(&ram_box);
|
||||
|
||||
let disk_box = gtk::Box::new(Orientation::Horizontal, 9);
|
||||
let disk_icon = Label::new(Some(""));
|
||||
disk_icon.add_css_class("stat-icon");
|
||||
disk_box.append(&disk_icon);
|
||||
let disk_label = Label::new(None);
|
||||
disk_box.append(&disk_label);
|
||||
stats_children.append(&disk_box);
|
||||
|
||||
let gpu_box = gtk::Box::new(Orientation::Horizontal, 9);
|
||||
let gpu_icon = Image::from_file("icons/gpu-card.svg");
|
||||
gpu_icon.add_css_class("stat-icon");
|
||||
gpu_box.append(&gpu_icon);
|
||||
let gpu_label = Label::new(None);
|
||||
gpu_box.append(&gpu_label);
|
||||
stats_children.append(&gpu_box);
|
||||
stats.set_second_child(stats_children);
|
||||
|
||||
let network_button = Button::new();
|
||||
network_button.add_css_class("network");
|
||||
let network_button_content = gtk::Box::new(Orientation::Horizontal, 0);
|
||||
let network_icon = Image::from_file("../../icons/disconnected.svg");
|
||||
network_icon.add_css_class("network-icon");
|
||||
network_button_content.append(&network_icon);
|
||||
let network_label = Label::new(None);
|
||||
network_label.add_css_class("network-label");
|
||||
network_label.set_visible(false);
|
||||
network_button_content.append(&network_label);
|
||||
network_button.set_child(Some(&network_button_content));
|
||||
|
||||
network_button.connect_clicked(clone!(
|
||||
#[weak]
|
||||
display,
|
||||
move |_| {
|
||||
if action_centre.window.is_visible() {
|
||||
action_centre.hide_action_centre();
|
||||
} else {
|
||||
action_centre.show_action_centre(monitor_idx, &display);
|
||||
}
|
||||
}
|
||||
));
|
||||
|
||||
let wg_button = Button::with_label("wg");
|
||||
wg_button.add_css_class("vpn");
|
||||
let net_tx = channels.net_tx();
|
||||
wg_button.connect_clicked(move |_| {
|
||||
let _ = net_tx.send(NetworkCommand::ToggleVpn(String::from("wg")));
|
||||
});
|
||||
|
||||
let ts_button = Button::with_label("ts");
|
||||
ts_button.add_css_class("vpn");
|
||||
ts_button.connect_clicked(move |button| {
|
||||
let _ = Command::new("tailscale")
|
||||
.arg(if button.has_css_class("on") {
|
||||
"down"
|
||||
} else {
|
||||
"up"
|
||||
})
|
||||
.output();
|
||||
});
|
||||
|
||||
let tud_button = Button::with_label("tud");
|
||||
tud_button.add_css_class("vpn");
|
||||
let mvd_button = Button::with_label("mvd");
|
||||
mvd_button.add_css_class("vpn");
|
||||
|
||||
let vpn_box = gtk::Box::new(Orientation::Horizontal, 0);
|
||||
vpn_box.set_valign(Align::Fill);
|
||||
vpn_box.set_vexpand(true);
|
||||
vpn_box.append(&wg_button);
|
||||
vpn_box.append(&ts_button);
|
||||
//vpn_box.append(&tud_button);
|
||||
//vpn_box.append(&mvd_button);
|
||||
|
||||
let vpn_revealer = Revealer::new();
|
||||
vpn_revealer.set_transition_type(RevealerTransitionType::SlideRight);
|
||||
vpn_revealer.set_transition_duration(500);
|
||||
vpn_revealer.set_reveal_child(false);
|
||||
vpn_revealer.set_child(Some(&vpn_box));
|
||||
vpn_revealer.add_css_class("revealer");
|
||||
|
||||
let vpn_button = Button::with_label("");
|
||||
vpn_button.add_css_class("vpn-icon");
|
||||
|
||||
let battery_widget = ExpandableBarItem::new("");
|
||||
battery_widget.add_css_class("battery");
|
||||
battery_widget.set_spacing(0);
|
||||
battery_widget.set_vexpand(true);
|
||||
battery_widget.set_valign(Align::Fill);
|
||||
|
||||
let battery_children = gtk::Box::new(Orientation::Horizontal, 0);
|
||||
|
||||
let wattage = Label::new(None);
|
||||
battery_children.append(&wattage);
|
||||
let remaining_time = Label::new(None);
|
||||
battery_children.append(&remaining_time);
|
||||
|
||||
battery_widget.set_second_child(battery_children);
|
||||
|
||||
let time = ExpandableBarItem::new("");
|
||||
time.add_css_class("time");
|
||||
time.set_spacing(0);
|
||||
time.set_vexpand(true);
|
||||
time.set_valign(Align::Fill);
|
||||
|
||||
let time_hidden = Label::new(None);
|
||||
time.set_second_child(time_hidden.clone());
|
||||
|
||||
let power_button = Button::with_label(" ");
|
||||
power_button.add_css_class("power-button");
|
||||
power_button.connect_clicked(clone!(
|
||||
#[strong]
|
||||
app,
|
||||
move |_button| {
|
||||
app.quit();
|
||||
//let _ = Command::new("wlogout").spawn();
|
||||
}
|
||||
));
|
||||
|
||||
utilities.append(&volume);
|
||||
utilities.append(&brightness);
|
||||
utilities.append(&caffeine_button);
|
||||
utilities.append(&stats);
|
||||
utilities.append(&network_button);
|
||||
utilities.append(&vpn_button);
|
||||
utilities.append(&vpn_revealer);
|
||||
//utilities.append(&systray_box);
|
||||
utilities.append(&battery_widget);
|
||||
utilities.append(&time);
|
||||
utilities.append(&power_button);
|
||||
|
||||
vpn_button.connect_clicked(move |_button| {
|
||||
vpn_revealer.set_reveal_child(!vpn_revealer.is_child_revealed());
|
||||
});
|
||||
|
||||
glib::timeout_add_local(Duration::from_millis(1000), move || {
|
||||
let timestamp = time_format::now().unwrap();
|
||||
let time_str = time_format::strftime_local("%H:%M", timestamp).unwrap_or(String::new());
|
||||
time.set_title(&time_str);
|
||||
let time_sec =
|
||||
time_format::strftime_local(":%S %Y-%m-%d", timestamp).unwrap_or(String::new());
|
||||
time_hidden.set_label(&time_sec);
|
||||
ControlFlow::Continue
|
||||
});
|
||||
|
||||
let media_rx = channels.take_media_rx().unwrap();
|
||||
let br_rx = channels.take_br_rx().unwrap();
|
||||
let net_rx = channels.take_net_rx().unwrap();
|
||||
let stats_rx = channels.take_stats_rx().unwrap();
|
||||
let ts_rx = channels.take_ts_rx().unwrap();
|
||||
let hypridle = channels.hypridle();
|
||||
glib::timeout_add_local(Duration::from_millis(100), move || {
|
||||
while let Ok(command) = media_rx.try_recv() {
|
||||
match command {
|
||||
MediaCommand::Cmd(vol, mute, bt) => {
|
||||
changed_progr.set(true);
|
||||
volume.set_value(vol as f64);
|
||||
changed_progr.set(false);
|
||||
volume.show_scale(!mute);
|
||||
volume.imp().button.set_active(mute);
|
||||
if mute {
|
||||
volume.imp().button.add_css_class("muted");
|
||||
volume.imp().second_icon.add_child_css_class("muted");
|
||||
volume
|
||||
.imp()
|
||||
.main_icon
|
||||
.set_icon_path("icons/volume-mute.svg");
|
||||
} else {
|
||||
volume.imp().button.remove_css_class("muted");
|
||||
volume.imp().second_icon.remove_child_css_class("muted");
|
||||
let idx = ((volume.value() / 100.0) * (VOLUME_ICON_PATHS.len() - 1) as f64
|
||||
+ 0.5) as usize;
|
||||
volume.imp().main_icon.set_icon_path(VOLUME_ICON_PATHS[idx]);
|
||||
}
|
||||
volume.imp().second_icon.set_visible(bt);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
while let Ok(message) = br_rx.try_recv() {
|
||||
match message {
|
||||
BrightnessMessage::Value(mon, value) => {
|
||||
if monitor.eq(&mon) {
|
||||
brightness.set_value(value as f64);
|
||||
let idx = ((value as f32 / 100.0) * (BRIGHTNESS_ICONS.len() - 1) as f32
|
||||
+ 0.5) as usize;
|
||||
brightness
|
||||
.imp()
|
||||
.main_icon
|
||||
.set_font_icon(BRIGHTNESS_ICONS[idx]);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if let Ok(mut hypridle) = hypridle.try_lock() {
|
||||
if hypridle.is_none() {
|
||||
caffeine_button.add_css_class("on");
|
||||
} else {
|
||||
if let Ok(exit) = hypridle.as_mut().unwrap().try_wait()
|
||||
&& exit.is_some()
|
||||
{
|
||||
caffeine_button.add_css_class("on");
|
||||
} else {
|
||||
caffeine_button.remove_css_class("on");
|
||||
}
|
||||
}
|
||||
}
|
||||
while let Ok(result) = net_rx.try_recv() {
|
||||
trace!(&LOGGER, "NetworkResult: {:?}", result);
|
||||
match result {
|
||||
NetworkState::ChangeNetwork(cfg) => {
|
||||
match cfg {
|
||||
Some(network) => {
|
||||
match network.network_type {
|
||||
NetworkType::Wifi => {
|
||||
//let idx = ((20.0 / 100.0) * (WIFI_ICON_PATHS.len() - 1) as f32 + 0.5) as usize;
|
||||
let idx = ((network.signal as f32 / 100.0)
|
||||
* (WIFI_ICON_PATHS.len() - 1) as f32
|
||||
+ 0.5)
|
||||
as usize;
|
||||
network_icon.set_from_file(Some(WIFI_ICON_PATHS[idx]));
|
||||
network_label.set_label(&network.name);
|
||||
network_label.set_visible(true);
|
||||
}
|
||||
NetworkType::Wired => {
|
||||
network_icon.set_from_file(Some("icons/ethernet.svg"));
|
||||
network_label.set_visible(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
network_icon.set_from_file(Some("icons/disconnected.svg"));
|
||||
network_label.set_visible(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
NetworkState::VpnState { connected, profile } => match profile.as_str() {
|
||||
"wg" => {
|
||||
if connected {
|
||||
wg_button.add_css_class("on");
|
||||
} else {
|
||||
wg_button.remove_css_class("on");
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
while let Ok(tailscale_state) = ts_rx.try_recv() {
|
||||
match tailscale_state {
|
||||
TailscaleState::Stopped => {
|
||||
ts_button.remove_css_class("on");
|
||||
ts_button.remove_css_class("starting");
|
||||
ts_button.remove_css_class("unknown");
|
||||
}
|
||||
TailscaleState::Starting => {
|
||||
ts_button.remove_css_class("on");
|
||||
ts_button.add_css_class("starting");
|
||||
ts_button.remove_css_class("unknown");
|
||||
}
|
||||
TailscaleState::Running => {
|
||||
ts_button.add_css_class("on");
|
||||
ts_button.remove_css_class("starting");
|
||||
ts_button.remove_css_class("unknown");
|
||||
}
|
||||
_ => {
|
||||
ts_button.remove_css_class("on");
|
||||
ts_button.remove_css_class("starting");
|
||||
ts_button.add_css_class("unknown");
|
||||
}
|
||||
}
|
||||
}
|
||||
if wg_button.has_css_class("on") || ts_button.has_css_class("on") {
|
||||
vpn_button.add_css_class("on");
|
||||
} else {
|
||||
vpn_button.remove_css_class("on");
|
||||
}
|
||||
while let Ok(stats) = stats_rx.try_recv() {
|
||||
cpu_label.set_label(&format!("{}%", stats.cpu));
|
||||
ram_label.set_label(&format!("{}%", stats.ram));
|
||||
for disk in stats.disks {
|
||||
if disk.0.eq("/") {
|
||||
disk_label.set_label(&format!("{}%", disk.1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for gpu in stats.gpus {
|
||||
gpu_label.set_label(&format!("{}%", gpu.1));
|
||||
break;
|
||||
}
|
||||
|
||||
set_battery_css_classes(&battery_widget, stats.battery_state, stats.battery_charge);
|
||||
let idx = ((stats.battery_charge as f32 / 100.0) * (BATTERY_ICONS.len() - 1) as f32
|
||||
+ 0.5) as usize;
|
||||
let idx = idx.min(BATTERY_ICONS.len() - 1);
|
||||
battery_widget.set_title(&format!("{} {}%", BATTERY_ICONS[idx], stats.battery_charge));
|
||||
wattage.set_label(&format!(" {:.1}W", stats.battery_draw));
|
||||
remaining_time.set_label(&format!(" {}h", format_remaining(stats.battery_time)));
|
||||
}
|
||||
ControlFlow::Continue
|
||||
});
|
||||
utilities
|
||||
}
|
||||
|
||||
fn format_remaining(ms: u64) -> String {
|
||||
let total_minutes = ms / 60_000;
|
||||
let hours = total_minutes / 60;
|
||||
let minutes = total_minutes % 60;
|
||||
|
||||
format!("{hours}:{minutes:02}")
|
||||
}
|
||||
|
||||
fn set_battery_css_classes(battery_widget: &ExpandableBarItem, state: State, percentage: u8) {
|
||||
if state == State::Charging {
|
||||
battery_widget.add_css_class("charging");
|
||||
} else {
|
||||
battery_widget.remove_css_class("charging");
|
||||
battery_widget.remove_css_class("critical");
|
||||
battery_widget.remove_css_class("low");
|
||||
battery_widget.remove_css_class("medium");
|
||||
match percentage {
|
||||
0..=10 => {
|
||||
battery_widget.add_css_class("critical");
|
||||
}
|
||||
11..=20 => {
|
||||
battery_widget.add_css_class("low");
|
||||
}
|
||||
21..=50 => {
|
||||
battery_widget.add_css_class("medium");
|
||||
}
|
||||
51..=100 => {}
|
||||
101.. => {
|
||||
battery_widget.set_title("<invalid>");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
646
src/ui/widgets.rs
Normal file
646
src/ui/widgets.rs
Normal file
@@ -0,0 +1,646 @@
|
||||
use glib::ParamSpec;
|
||||
use glib::object::IsA;
|
||||
use glib::prelude::ObjectExt;
|
||||
use glib::subclass::prelude::*;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Adjustment, Button, Label, Scale, ToggleButton, Widget};
|
||||
|
||||
mod imp_label_entry {
|
||||
use glib::subclass::object::ObjectImpl;
|
||||
use glib::subclass::prelude::*;
|
||||
use gtk::prelude::*;
|
||||
use gtk::subclass::box_::BoxImpl;
|
||||
use gtk::subclass::prelude::*;
|
||||
use gtk::{Entry, Label, Orientation};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct LabelEntry {
|
||||
pub label: std::cell::OnceCell<Label>,
|
||||
pub entry: std::cell::OnceCell<Entry>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for LabelEntry {
|
||||
const NAME: &'static str = "EntryLabel";
|
||||
type Type = super::LabelEntry;
|
||||
type ParentType = gtk::Box;
|
||||
}
|
||||
|
||||
impl ObjectImpl for LabelEntry {
|
||||
fn constructed(&self) {
|
||||
self.parent_constructed();
|
||||
let obj = self.obj();
|
||||
obj.set_orientation(Orientation::Horizontal);
|
||||
obj.set_spacing(4);
|
||||
|
||||
let label = Label::new(None);
|
||||
let entry = Entry::new();
|
||||
obj.append(&label);
|
||||
obj.append(&entry);
|
||||
|
||||
self.label.set(label.clone()).unwrap();
|
||||
self.entry.set(entry.clone()).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for LabelEntry {}
|
||||
impl BoxImpl for LabelEntry {}
|
||||
}
|
||||
|
||||
mod imp_icon_label {
|
||||
use crate::ui::widgets::Icon;
|
||||
use glib::Properties;
|
||||
use glib::property::PropertySet;
|
||||
use glib::subclass::object::ObjectImpl;
|
||||
use glib::subclass::prelude::*;
|
||||
use gtk::prelude::*;
|
||||
use gtk::subclass::box_::BoxImpl;
|
||||
use gtk::subclass::prelude::*;
|
||||
use gtk::{Label, Orientation};
|
||||
use std::cell::RefCell;
|
||||
|
||||
#[derive(Default, Properties)]
|
||||
#[properties(wrapper_type = super::IconLabel)]
|
||||
pub struct IconLabel {
|
||||
pub icon: std::cell::OnceCell<Icon>,
|
||||
pub label: std::cell::OnceCell<Label>,
|
||||
#[property(get, set = Self::set_icon_path, name = "icon-path")]
|
||||
icon_path: RefCell<String>,
|
||||
#[property(get, set = Self::set_icon_name, name = "icon-name")]
|
||||
icon_name: RefCell<String>,
|
||||
#[property(get, set = Self::set_font_icon, name = "font-icon")]
|
||||
font_icon: RefCell<String>,
|
||||
#[property(get, set = Self::set_label, name = "label")]
|
||||
label_text: RefCell<String>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for IconLabel {
|
||||
const NAME: &'static str = "JBIconLabel";
|
||||
type Type = super::IconLabel;
|
||||
type ParentType = gtk::Box;
|
||||
}
|
||||
|
||||
#[glib::derived_properties]
|
||||
impl ObjectImpl for IconLabel {
|
||||
fn constructed(&self) {
|
||||
self.parent_constructed();
|
||||
let obj = self.obj();
|
||||
obj.set_orientation(Orientation::Horizontal);
|
||||
obj.set_spacing(4);
|
||||
|
||||
let icon = Icon::new();
|
||||
obj.append(&icon);
|
||||
|
||||
let label = Label::new(None);
|
||||
obj.append(&label);
|
||||
|
||||
self.icon.set(icon.clone()).unwrap();
|
||||
self.label.set(label.clone()).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for IconLabel {}
|
||||
impl BoxImpl for IconLabel {}
|
||||
|
||||
impl IconLabel {
|
||||
fn set_icon_path(&self, path: String) {
|
||||
self.icon_path.set(path.clone());
|
||||
self.icon.get().unwrap().set_icon_path(path);
|
||||
}
|
||||
|
||||
pub fn set_icon_name(&self, name: String) {
|
||||
self.icon_name.set(name.clone());
|
||||
self.icon.get().unwrap().set_icon_name(name);
|
||||
}
|
||||
|
||||
pub fn set_font_icon(&self, icon: &str) {
|
||||
self.font_icon.set(icon.to_string());
|
||||
self.icon.get().unwrap().set_font_icon(icon);
|
||||
}
|
||||
|
||||
pub fn set_label(&self, text: &str) {
|
||||
self.label_text.set(text.to_string());
|
||||
self.label.get().unwrap().set_text(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod imp_icon {
|
||||
use glib::Properties;
|
||||
use glib::object::ObjectExt;
|
||||
use glib::property::PropertySet;
|
||||
use glib::subclass::InitializingObject;
|
||||
use glib::subclass::object::ObjectImpl;
|
||||
use glib::subclass::prelude::*;
|
||||
use gtk::prelude::WidgetExt;
|
||||
use gtk::subclass::box_::BoxImpl;
|
||||
use gtk::subclass::prelude::*;
|
||||
use gtk::{CompositeTemplate, Image, Label, TemplateChild};
|
||||
use std::cell::{Cell, RefCell};
|
||||
|
||||
#[derive(CompositeTemplate, Default, Properties)]
|
||||
#[properties(wrapper_type = super::Icon)]
|
||||
#[template(resource = "/net/jmb19905/jmb-bar/icon.ui")]
|
||||
pub struct Icon {
|
||||
#[template_child]
|
||||
pub image: TemplateChild<Image>,
|
||||
#[template_child]
|
||||
pub label: TemplateChild<Label>,
|
||||
#[property(get, set = Self::add_child_css_class, name = "child-css-class")]
|
||||
child_css_class: RefCell<String>,
|
||||
#[property(get, set = Self::set_icon_size, minimum = 1, maximum = 255, default = 16, name = "pixel-size")]
|
||||
pixel_size: Cell<i32>,
|
||||
#[property(get, set = Self::set_icon_path, name = "icon-path")]
|
||||
icon_path: RefCell<String>,
|
||||
#[property(get, set = Self::set_icon_name, name = "icon-name")]
|
||||
icon_name: RefCell<String>,
|
||||
#[property(get, set = Self::set_font_icon, name = "font-icon")]
|
||||
font_icon: RefCell<String>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for Icon {
|
||||
const NAME: &'static str = "JBIcon";
|
||||
type Type = super::Icon;
|
||||
type ParentType = gtk::Box;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
klass.bind_template();
|
||||
}
|
||||
|
||||
fn instance_init(obj: &InitializingObject<Self>) {
|
||||
obj.init_template();
|
||||
}
|
||||
}
|
||||
|
||||
#[glib::derived_properties]
|
||||
impl ObjectImpl for Icon {}
|
||||
impl WidgetImpl for Icon {}
|
||||
impl BoxImpl for Icon {}
|
||||
|
||||
impl Icon {
|
||||
fn add_child_css_class(&self, class: &str) {
|
||||
self.child_css_class.set(class.to_string());
|
||||
if self.label.is_bound() {
|
||||
self.label.add_css_class(class);
|
||||
}
|
||||
if self.image.is_bound() {
|
||||
self.image.add_css_class(class);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_icon_size(&self, size: i32) {
|
||||
self.pixel_size.set(size);
|
||||
|
||||
if self.image.is_bound() {
|
||||
self.image.set_pixel_size(size);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_icon_path(&self, path: String) {
|
||||
self.image.set_from_file(Some(path));
|
||||
self.label.set_visible(false);
|
||||
self.image.set_visible(true);
|
||||
}
|
||||
|
||||
pub fn set_icon_name(&self, name: String) {
|
||||
self.image.set_icon_name(Some(&name));
|
||||
self.label.set_visible(false);
|
||||
self.image.set_visible(true);
|
||||
}
|
||||
|
||||
pub fn set_font_icon(&self, icon: &str) {
|
||||
self.label.set_label(icon);
|
||||
self.label.set_visible(true);
|
||||
self.image.set_visible(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod imp_icon_slider {
|
||||
use glib::subclass::InitializingObject;
|
||||
use glib::subclass::object::ObjectImpl;
|
||||
use glib::subclass::prelude::ObjectSubclass;
|
||||
use gtk::subclass::box_::BoxImpl;
|
||||
use gtk::subclass::prelude::*;
|
||||
use gtk::{CompositeTemplate, TemplateChild, ToggleButton};
|
||||
|
||||
#[derive(CompositeTemplate, Default)]
|
||||
#[template(resource = "/net/jmb19905/jmb-bar/icon_slider.ui")]
|
||||
pub struct IconSlider {
|
||||
#[template_child]
|
||||
pub button: TemplateChild<ToggleButton>,
|
||||
#[template_child]
|
||||
pub main_icon: TemplateChild<super::Icon>,
|
||||
#[template_child]
|
||||
pub second_icon: TemplateChild<super::Icon>,
|
||||
#[template_child]
|
||||
pub slider: TemplateChild<gtk::Scale>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for IconSlider {
|
||||
const NAME: &'static str = "JBIconSlider";
|
||||
type Type = super::IconSlider;
|
||||
type ParentType = gtk::Box;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
klass.bind_template();
|
||||
}
|
||||
|
||||
fn instance_init(obj: &InitializingObject<Self>) {
|
||||
obj.init_template();
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectImpl for IconSlider {}
|
||||
|
||||
impl WidgetImpl for IconSlider {}
|
||||
|
||||
impl BoxImpl for IconSlider {}
|
||||
}
|
||||
|
||||
mod imp_bar_item {
|
||||
use glib::Properties;
|
||||
use glib::object::ObjectExt;
|
||||
use glib::property::PropertySet;
|
||||
use glib::subclass::InitializingObject;
|
||||
use glib::subclass::object::ObjectImpl;
|
||||
use glib::subclass::prelude::*;
|
||||
use gtk::prelude::{ButtonExt, WidgetExt};
|
||||
use gtk::subclass::box_::BoxImpl;
|
||||
use gtk::subclass::prelude::*;
|
||||
use gtk::{Button, CompositeTemplate, EventControllerMotion, Revealer, TemplateChild, Widget};
|
||||
use std::cell::RefCell;
|
||||
|
||||
#[derive(CompositeTemplate, Default, Properties)]
|
||||
#[properties(wrapper_type = super::ExpandableBarItem)]
|
||||
#[template(resource = "/net/jmb19905/jmb-bar/expandable_bar_item.ui")]
|
||||
pub struct ExpandableBarItem {
|
||||
#[template_child]
|
||||
pub main_button: TemplateChild<Button>,
|
||||
#[template_child]
|
||||
pub revealer: TemplateChild<Revealer>,
|
||||
#[property(get, set = Self::set_main_child, name = "main-child")]
|
||||
pub main_child: RefCell<Option<Widget>>,
|
||||
#[property(get, set = Self::set_second_child, name = "second-child")]
|
||||
pub second_child: RefCell<Option<Widget>>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for ExpandableBarItem {
|
||||
const NAME: &'static str = "JBExpandableBarItem";
|
||||
type Type = super::ExpandableBarItem;
|
||||
type ParentType = gtk::Box;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
klass.bind_template();
|
||||
}
|
||||
|
||||
fn instance_init(obj: &InitializingObject<Self>) {
|
||||
obj.init_template();
|
||||
}
|
||||
}
|
||||
|
||||
#[glib::derived_properties]
|
||||
impl ObjectImpl for ExpandableBarItem {
|
||||
fn constructed(&self) {
|
||||
self.parent_constructed();
|
||||
|
||||
let obj = self.obj();
|
||||
let motion = EventControllerMotion::new();
|
||||
|
||||
let revealer_enter = self.revealer.clone();
|
||||
let button_enter = self.main_button.clone();
|
||||
|
||||
motion.connect_enter(move |_, _, _| {
|
||||
revealer_enter.set_reveal_child(true);
|
||||
button_enter.add_css_class("hovered");
|
||||
});
|
||||
|
||||
let revealer_leave = self.revealer.clone();
|
||||
let button_leave = self.main_button.clone();
|
||||
|
||||
motion.connect_leave(move |_| {
|
||||
revealer_leave.set_reveal_child(false);
|
||||
button_leave.remove_css_class("hovered");
|
||||
});
|
||||
|
||||
obj.add_controller(motion);
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for ExpandableBarItem {}
|
||||
|
||||
impl BoxImpl for ExpandableBarItem {}
|
||||
|
||||
impl ExpandableBarItem {
|
||||
fn set_main_child(&self, widget: Option<&Widget>) {
|
||||
self.main_child.set(widget.cloned());
|
||||
self.main_button.set_child(widget);
|
||||
}
|
||||
|
||||
fn set_second_child(&self, widget: Option<&Widget>) {
|
||||
self.second_child.set(widget.cloned());
|
||||
self.revealer.set_child(widget);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod imp_wifi_widget {
|
||||
use glib::subclass::InitializingObject;
|
||||
use glib::subclass::prelude::ObjectSubclass;
|
||||
use gtk::subclass::prelude::*;
|
||||
use gtk::{Button, CompositeTemplate, Label, PasswordEntry, Spinner, Stack, TemplateChild};
|
||||
|
||||
#[derive(CompositeTemplate, Default)]
|
||||
#[template(resource = "/net/jmb19905/jmb-bar/wifi_widget.ui")]
|
||||
pub struct WifiWidget {
|
||||
#[template_child]
|
||||
pub wifi_icon: TemplateChild<super::Icon>,
|
||||
#[template_child]
|
||||
pub spinner: TemplateChild<Spinner>,
|
||||
#[template_child]
|
||||
pub stack: TemplateChild<Stack>,
|
||||
#[template_child]
|
||||
pub ssid: TemplateChild<Label>,
|
||||
#[template_child]
|
||||
pub password: TemplateChild<PasswordEntry>,
|
||||
#[template_child]
|
||||
pub known: TemplateChild<super::Icon>,
|
||||
#[template_child]
|
||||
pub secure: TemplateChild<super::Icon>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for WifiWidget {
|
||||
const NAME: &'static str = "JBWifiWidget";
|
||||
type Type = super::WifiWidget;
|
||||
type ParentType = Button;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
klass.bind_template();
|
||||
}
|
||||
|
||||
fn instance_init(obj: &InitializingObject<Self>) {
|
||||
obj.init_template();
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectImpl for WifiWidget {}
|
||||
impl WidgetImpl for WifiWidget {}
|
||||
impl ButtonImpl for WifiWidget {}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct LabelEntry(ObjectSubclass<imp_label_entry::LabelEntry>)
|
||||
@extends gtk::Box, Widget,
|
||||
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable;
|
||||
}
|
||||
|
||||
impl LabelEntry {
|
||||
pub fn new(title: &str) -> Self {
|
||||
let obj: Self = glib::Object::new();
|
||||
|
||||
if let Some(label) = obj.clone().downcast::<LabelEntry>().unwrap().label() {
|
||||
label.set_label(title);
|
||||
}
|
||||
|
||||
obj
|
||||
}
|
||||
|
||||
pub fn label(&self) -> Option<Label> {
|
||||
let imp = self.imp();
|
||||
|
||||
if let Some(label) = imp.label.get() {
|
||||
return Some(label.clone());
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct IconLabel(ObjectSubclass<imp_icon_label::IconLabel>)
|
||||
@extends gtk::Box, Widget,
|
||||
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable;
|
||||
}
|
||||
|
||||
impl IconLabel {
|
||||
pub fn new() -> Self {
|
||||
let obj: Self = glib::Object::new();
|
||||
obj
|
||||
}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct Icon(ObjectSubclass<imp_icon::Icon>)
|
||||
@extends gtk::Box, Widget,
|
||||
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable;
|
||||
}
|
||||
|
||||
impl Icon {
|
||||
pub fn new() -> Self {
|
||||
let obj: Self = glib::Object::new();
|
||||
obj
|
||||
}
|
||||
pub fn from_font_icon(icon: &str) -> Self {
|
||||
let obj = Self::new();
|
||||
obj.set_font_icon(icon);
|
||||
obj
|
||||
}
|
||||
|
||||
pub fn from_icon_path(
|
||||
path: Option<impl AsRef<std::path::Path> + std::borrow::Borrow<str>>,
|
||||
) -> Self {
|
||||
let obj = Self::new();
|
||||
if let Some(path) = path {
|
||||
obj.set_icon_path(path);
|
||||
}
|
||||
obj
|
||||
}
|
||||
|
||||
pub fn from_icon_name(name: &str) -> Self {
|
||||
let obj = Self::new();
|
||||
obj.set_icon_name(name);
|
||||
obj
|
||||
}
|
||||
|
||||
pub fn add_child_css_class(&self, class: &str) {
|
||||
self.imp().image.add_css_class(class);
|
||||
self.imp().label.add_css_class(class);
|
||||
}
|
||||
|
||||
pub fn remove_child_css_class(&self, class: &str) {
|
||||
self.imp().image.remove_css_class(class);
|
||||
self.imp().label.remove_css_class(class);
|
||||
}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct IconSlider(ObjectSubclass<imp_icon_slider::IconSlider>)
|
||||
@extends gtk::Box, Widget,
|
||||
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable;
|
||||
}
|
||||
|
||||
impl IconSlider {
|
||||
pub fn new(adjustment: Option<Adjustment>) -> Self {
|
||||
let obj: Self = glib::Object::new();
|
||||
if let Some(adj) = adjustment {
|
||||
obj.imp().slider.set_adjustment(&adj);
|
||||
}
|
||||
obj
|
||||
}
|
||||
|
||||
pub fn show_second_icon(&self, val: bool) {
|
||||
self.imp().second_icon.set_visible(val);
|
||||
}
|
||||
|
||||
pub fn connect_value_changed<A>(&self, action: A)
|
||||
where
|
||||
A: Fn(&IconSlider, &ToggleButton, &Scale) + 'static,
|
||||
{
|
||||
let clone = self.clone();
|
||||
self.imp().slider.connect_value_changed(move |scale| {
|
||||
action(&clone, &clone.imp().button, scale);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn connect_clicked<A>(&self, action: A)
|
||||
where
|
||||
A: Fn(&IconSlider, &ToggleButton, &Scale) + 'static,
|
||||
{
|
||||
let clone = self.clone();
|
||||
self.imp().button.connect_clicked(move |button| {
|
||||
action(&clone, button, &clone.imp().slider);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn value(&self) -> f64 {
|
||||
self.imp().slider.value()
|
||||
}
|
||||
|
||||
pub fn set_value(&self, val: f64) {
|
||||
self.imp().slider.set_value(val);
|
||||
}
|
||||
|
||||
pub fn show_scale(&self, val: bool) {
|
||||
self.imp().slider.set_visible(val);
|
||||
}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct ExpandableBarItem(ObjectSubclass<imp_bar_item::ExpandableBarItem>)
|
||||
@extends gtk::Box, Widget,
|
||||
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable;
|
||||
}
|
||||
|
||||
impl ExpandableBarItem {
|
||||
pub fn new(title: &str) -> Self {
|
||||
let obj: Self = glib::Object::new();
|
||||
|
||||
obj.set_title(title);
|
||||
|
||||
obj
|
||||
}
|
||||
|
||||
pub fn set_title(&self, title: &str) {
|
||||
self.imp().main_button.set_label(title);
|
||||
}
|
||||
|
||||
pub fn set_icon(&self, icon_name: &str) {
|
||||
self.imp().main_button.set_label(icon_name);
|
||||
}
|
||||
|
||||
pub fn set_button_child(&self, child: Option<&impl IsA<Widget>>) {
|
||||
self.imp().main_button.set_child(child);
|
||||
}
|
||||
|
||||
pub fn connect_clicked<F>(&self, action: F)
|
||||
where
|
||||
F: Fn(&Button) + 'static,
|
||||
{
|
||||
self.imp().main_button.connect_clicked(action);
|
||||
}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct WifiWidget(ObjectSubclass<imp_wifi_widget::WifiWidget>)
|
||||
@extends Button, Widget,
|
||||
@implements gtk::Actionable, gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
|
||||
}
|
||||
|
||||
impl WifiWidget {
|
||||
pub fn new() -> Self {
|
||||
let obj: Self = glib::Object::new();
|
||||
obj
|
||||
}
|
||||
|
||||
pub fn set_ssid(&self, ssid: &str) {
|
||||
self.imp().ssid.set_label(ssid);
|
||||
}
|
||||
|
||||
pub fn ssid(&self) -> glib::GString {
|
||||
self.imp().ssid.get().text()
|
||||
}
|
||||
|
||||
pub fn set_secured(&self, secured: bool) {
|
||||
if secured {
|
||||
let _ = &self.imp().secure.set_icon_name("channel-secure-symbolic");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_known(&self, known: bool) {
|
||||
if known {
|
||||
let _ = &self.imp().known.set_icon_name("user-bookmarks-symbolic");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn grab_focus_password_entry(&self) -> bool {
|
||||
self.imp().password.grab_focus()
|
||||
}
|
||||
|
||||
pub fn stop_spinner(&self) {
|
||||
self.imp().spinner.stop();
|
||||
}
|
||||
|
||||
pub fn start_spinner(&self) {
|
||||
self.imp().spinner.start();
|
||||
}
|
||||
|
||||
pub fn set_stack_child(&self, name: &str) {
|
||||
self.imp().stack.set_visible_child_name(name);
|
||||
}
|
||||
|
||||
pub fn show_password_entry(&self) {
|
||||
self.set_stack_child("pass");
|
||||
}
|
||||
|
||||
pub fn show_ssid(&self) {
|
||||
self.set_stack_child("ssid");
|
||||
}
|
||||
|
||||
pub fn set_wifi_icon_from_file(&self, icon_path: &str) {
|
||||
self.imp().wifi_icon.set_icon_path(icon_path);
|
||||
}
|
||||
|
||||
pub fn set_wifi_icon_from_name(&self, icon: &str) {
|
||||
self.imp().wifi_icon.set_icon_name(icon);
|
||||
}
|
||||
|
||||
pub fn connect_activate_pass<F>(&self, action: F)
|
||||
where
|
||||
F: Fn(>k::PasswordEntry) + 'static,
|
||||
{
|
||||
self.imp().password.connect_activate(action);
|
||||
}
|
||||
|
||||
pub fn connect_has_focus_pass<F>(&self, action: F)
|
||||
where
|
||||
F: Fn(>k::PasswordEntry, &ParamSpec) + 'static,
|
||||
{
|
||||
self.imp()
|
||||
.password
|
||||
.connect_notify_local(Some("has-focus"), action);
|
||||
}
|
||||
}
|
||||
62
src/ui/workspaces_panel.rs
Normal file
62
src/ui/workspaces_panel.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
use crate::LOGGER;
|
||||
use crate::backend::window_manager_hypr::HyprlandComms;
|
||||
use gtk::prelude::{BoxExt, ButtonExt, WidgetExt};
|
||||
use gtk::{Align, Button, Orientation};
|
||||
use hyprland::data::Workspace;
|
||||
use hyprland::prelude::HyprDataActive;
|
||||
use jmb_logger_rs::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub(crate) fn build(buttons: &mut HashMap<i32, Button>, monitor: Option<u32>) -> (gtk::Box, Button) {
|
||||
let workspaces = gtk::Box::new(Orientation::Horizontal, 0);
|
||||
workspaces.set_halign(Align::Start);
|
||||
workspaces.set_valign(Align::Center);
|
||||
workspaces.set_vexpand(true);
|
||||
workspaces.set_valign(Align::Fill);
|
||||
workspaces.set_hexpand(true);
|
||||
workspaces.add_css_class("workspaces");
|
||||
|
||||
for workspace in 1..10 {
|
||||
let button = Button::with_label(&format!("{workspace}"));
|
||||
button.set_vexpand(true);
|
||||
button.add_css_class("workspace-button");
|
||||
button.set_visible(false);
|
||||
|
||||
let initial_workspace = Workspace::get_active()
|
||||
.ok()
|
||||
.map(|workspace| workspace.id().unwrap_or(1) as i32)
|
||||
.unwrap_or(1);
|
||||
|
||||
let is_active_ws_on_current_monitor = Workspace::get_active()
|
||||
.ok()
|
||||
.map(|ws| ws.monitor_id)
|
||||
.flatten()
|
||||
.is_some_and(|id| monitor.is_some_and(|other_id| id == other_id as i128));
|
||||
|
||||
if workspace == initial_workspace && is_active_ws_on_current_monitor {
|
||||
button.add_css_class("active");
|
||||
button.set_visible(true);
|
||||
}
|
||||
if HyprlandComms::workspace_added(workspace, monitor.map(|monitor| monitor as i128)) {
|
||||
button.set_visible(true);
|
||||
}
|
||||
button.connect_clicked(move |_button| {
|
||||
if let Err(err) = HyprlandComms::dispatch(&format!("hl.dsp.focus({{ workspace = {workspace} }})")) {
|
||||
warn!(&LOGGER, "Couldn't change workspace focus {}", err.to_string());
|
||||
}
|
||||
});
|
||||
workspaces.append(&button);
|
||||
buttons.insert(workspace, button);
|
||||
}
|
||||
|
||||
let submap_button = Button::new();
|
||||
submap_button.add_css_class("submap");
|
||||
submap_button.connect_clicked(|_| {
|
||||
if let Err(err) = HyprlandComms::dispatch("hl.dsp.submap('reset')") {
|
||||
warn!(&LOGGER, "Couldn't reset submap {}", err.to_string());
|
||||
}
|
||||
});
|
||||
workspaces.append(&submap_button);
|
||||
|
||||
(workspaces, submap_button)
|
||||
}
|
||||
Reference in New Issue
Block a user