Inline some clones to make code more readable, add config, implement dimming -> locking -> sleeping into idle daemon, fix waiting on network thread, implement run menu with application launching and math evaluation,

This commit is contained in:
jmb05
2026-11-01 16:21:46 +01:00
parent 80b5cafb8c
commit a8fe99b345
14 changed files with 975 additions and 484 deletions

View File

@@ -1,12 +1,13 @@
use crate::backend::{BackendThread, Registrator};
use crate::config::Config;
use crate::{APP_NAME, LOGGER};
use crossbeam_channel::{Receiver, Sender};
use jmb_logger_rs::*;
use nmrs::raw::zbus::interface;
use std::collections::HashMap;
use std::error::Error;
use std::process::Command;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use std::time::Duration;
use tokio::sync::watch;
use tokio::time::sleep;
use wayland_client::globals::registry_queue_init;
@@ -17,12 +18,11 @@ use wayland_protocols::ext::idle_notify::v1::client::ext_idle_notification_v1::{
Event, ExtIdleNotificationV1,
};
use wayland_protocols::ext::idle_notify::v1::client::ext_idle_notifier_v1::ExtIdleNotifierV1;
use zbus::conn::Builder;
use zbus::interface;
use zbus::message::Header;
const SERVICE: &str = "org.freedesktop.ScreenSaver";
const OBJECT_PATH: &str = "/org/freedesktop/ScreenSaver";
const LEGACY_OBJECT_PATH: &str = "/ScreenSaver";
struct WlState {
idle: Option<bool>,
@@ -53,14 +53,8 @@ impl Dispatch<ExtIdleNotificationV1, ()> for WlState {
_: &QueueHandle<Self>,
) {
match event {
Event::Idled => {
println!("Idled: {:?}", Instant::now());
state.idle = Some(true);
}
Event::Resumed => {
println!("Resumed: {:?}", Instant::now());
state.idle = Some(false);
}
Event::Idled => state.idle = Some(true),
Event::Resumed => state.idle = Some(false),
_ => {}
}
if let Some(idle) = state.idle {
@@ -85,15 +79,15 @@ impl Dispatch<ExtIdleNotifierV1, (), WlState> for WlState {
struct Inhibitor {
owner: String,
application: String,
_reason: String,
reason: String,
}
#[derive(Debug, Clone)]
pub enum IdleState {
Init(Sender<IdleMessage>),
Dimmed,
Inhibited,
Caffeine,
Inhibited(bool),
Caffeine(bool),
Active,
}
@@ -128,73 +122,92 @@ impl BackendThread<IdleState, Sender<IdleMessage>> for IdleDaemon {
}
fn init(registration_rx: Receiver<Sender<IdleState>>) {
let config = Config::load();
let (tx, rx) = crossbeam_channel::unbounded::<IdleMessage>();
let tx_clone = tx.clone();
let instance = Arc::new(Mutex::new(Self {
subscribers: Vec::new(),
}));
let instance_clone = instance.clone();
std::thread::spawn(move || {
while let Ok(sender) = registration_rx.recv() {
if sender.send(IdleState::Init(tx_clone.clone())).is_ok() {
trace!(&LOGGER, "Added IdleDaemon Subscriber");
instance_clone
.lock()
.expect("Couldn't get lock")
.subscribers
.push(sender);
std::thread::spawn({
let instance = instance.clone();
let tx = tx.clone();
move || {
while let Ok(sender) = registration_rx.recv() {
if sender.send(IdleState::Init(tx.clone())).is_ok() {
trace!(&LOGGER, "Added IdleDaemon Subscriber");
instance
.lock()
.expect("Couldn't get lock")
.subscribers
.push(sender);
}
}
}
});
let (wl_tx, wl_rx) = watch::channel(false);
let (dbus_tx, dbus_rx) = watch::channel((false, false));
std::thread::spawn(move || {
if let Err(err) = Self::start_idle_notifier(wl_tx) {
error!(&LOGGER, "Idle Daemon unexpectedly quit, reason: {err}");
let (dbus_tx, dbus_rx) = watch::channel(false);
std::thread::spawn({
let config = config.clone();
move || {
if let Err(err) = Self::start_idle_notifier(wl_tx, &config) {
error!(&LOGGER, "Idle Daemon unexpectedly quit, reason: {err}");
}
}
});
std::thread::spawn(move || {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("Failed to create Tokio runtime");
runtime.block_on(async move {
let mut idle_inhibitor = None;
while let Ok(msg) = rx.recv() {
match msg {
IdleMessage::Inhibit => {
if idle_inhibitor.is_none()
&& let Ok(ii) = IdleInhibitor::inhibit_idle().await
{
idle_inhibitor = Some(ii);
std::thread::spawn({
let instance = instance.clone();
move || {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("Failed to create Tokio runtime");
runtime.block_on(async move {
let mut idle_inhibitor = None;
while let Ok(msg) = rx.recv() {
match msg {
IdleMessage::Inhibit => {
if idle_inhibitor.is_none()
&& let Ok(ii) = IdleInhibitor::inhibit_idle().await
{
instance.lock().unwrap().publish(IdleState::Caffeine(true));
idle_inhibitor = Some(ii);
}
}
}
IdleMessage::Release => {
if let Some(ii) = idle_inhibitor.take()
&& let Err(e) = ii.uninhibit_idle().await
{
warn!(&LOGGER, "Error uninhibiting {e}");
IdleMessage::Release => {
if let Some(ii) = idle_inhibitor.take() {
match ii.uninhibit_idle().await {
Ok(_) => {
instance
.lock()
.unwrap()
.publish(IdleState::Caffeine(false));
}
Err(e) => warn!(&LOGGER, "Error uninhibiting {e}"),
}
}
}
}
}
}
});
});
}
});
std::thread::spawn(move || {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("Failed to create Tokio runtime");
std::thread::spawn({
let instance_clone = instance.clone();
move || {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("Failed to create Tokio runtime");
runtime.block_on(async move {
let res = IdleDaemon::start_dbus(dbus_tx).await;
match res {
Ok((_, _)) => {
Self::run_idle_timer(instance, wl_rx, dbus_rx).await;
runtime.block_on(async move {
let res = IdleDaemon::start_dbus(dbus_tx, instance_clone).await;
match res {
Ok((_, _)) => {
Self::run_idle_timer(instance, wl_rx, dbus_rx, &config).await;
}
Err(err) => error!(&LOGGER, "Error running D-Bus Screensaver: {err}"),
}
Err(err) => error!(&LOGGER, "Error running D-Bus Screensaver: {err}"),
}
});
});
}
});
}
}
@@ -215,20 +228,21 @@ impl IdleStage {
}
}
fn timeout(self) -> Duration {
fn timeout(self, cfg: &Config) -> Duration {
match self {
Self::Dim => Duration::from_secs(20),
Self::TurnOff => Duration::from_secs(20),
Self::Sleep => Duration::from_secs(20),
Self::Dim => Duration::from_secs(cfg.dim_timeout_delta()),
Self::TurnOff => Duration::from_secs(cfg.lock_timeout_delta()),
Self::Sleep => Duration::from_secs(cfg.sleep_timeout_delta()),
}
}
}
impl IdleDaemon {
async fn run_idle_timer(
_instance: Arc<Mutex<Self>>,
instance: Arc<Mutex<Self>>,
mut wl_rx: watch::Receiver<bool>,
mut dbus_rx: watch::Receiver<(bool, bool)>,
mut dbus_rx: watch::Receiver<bool>,
config: &Config,
) {
loop {
if let Err(e) = wl_rx.changed().await {
@@ -237,22 +251,26 @@ impl IdleDaemon {
}
let wl_idle = *wl_rx.borrow_and_update();
let dbus_state = *dbus_rx.borrow_and_update();
println!("Wayland idle state changed {wl_idle} (DBus state: {dbus_state:?})");
if wl_idle && !dbus_state.0 {
println!("Going into idle cycle");
if wl_idle && !dbus_state {
let mut stage = IdleStage::Dim;
loop {
tokio::select! {
_ = sleep(stage.timeout()) => {
_ = sleep(stage.timeout(config)) => {
match stage {
IdleStage::Dim => instance.lock().unwrap().publish(IdleState::Dimmed),
IdleStage::TurnOff => _ = Command::new("hyprlock").spawn(),
IdleStage::Sleep => _ = Command::new("systemctl").arg("sleep").spawn(),
}
stage = stage.next();
println!("Reached next stage {stage:?}");
}
res = wl_rx.changed() => {
if let Err(e) = res {
warn!(&LOGGER, "Error getting Wayland idle state {e}");
return;
}
println!("WL state change");
if !*wl_rx.borrow_and_update() {
instance.lock().unwrap().publish(IdleState::Active);
}
break;
}
res = dbus_rx.changed() => {
@@ -260,7 +278,6 @@ impl IdleDaemon {
warn!(&LOGGER, "Error getting D-Bus idle state {e}");
return;
}
println!("D-Bus state change");
break;
}
}
@@ -269,7 +286,7 @@ impl IdleDaemon {
}
}
fn start_idle_notifier(tx: watch::Sender<bool>) -> Result<(), Box<dyn Error>> {
fn start_idle_notifier(tx: watch::Sender<bool>, config: &Config) -> Result<(), Box<dyn Error>> {
let conn = Connection::connect_to_env()?;
let (globals, mut event_queue) = registry_queue_init::<WlState>(&conn)?;
let queue_handle = event_queue.handle();
@@ -280,7 +297,12 @@ impl IdleDaemon {
let mut state = WlState { idle: None, tx };
let _notification = notifier.get_idle_notification(10_000, &seat, &queue_handle, ());
let _notification = notifier.get_idle_notification(
(config.idle_timeout() * 1000) as u32,
&seat,
&queue_handle,
(),
);
loop {
event_queue.blocking_dispatch(&mut state)?;
@@ -288,16 +310,18 @@ impl IdleDaemon {
}
async fn start_dbus(
tx: watch::Sender<(bool, bool)>,
tx: watch::Sender<bool>,
daemon: Arc<Mutex<IdleDaemon>>,
) -> zbus::Result<(ScreenSaver, zbus::connection::Connection)> {
let service = ScreenSaver::new(tx);
let service = ScreenSaver::new(tx, daemon);
let connection = Builder::session()?
.name(SERVICE)?
.serve_at(OBJECT_PATH, service.clone())?
.serve_at(LEGACY_OBJECT_PATH, service.clone())?
.build()
let connection = zbus::Connection::session().await?;
connection
.object_server()
.at(OBJECT_PATH, service.clone())
.await?;
connection.request_name(SERVICE).await?;
info!(&LOGGER, "Providing {SERVICE}");
@@ -347,22 +371,24 @@ impl IdleInhibitor {
#[derive(Clone)]
struct ScreenSaver {
inner: Arc<Mutex<Inner>>,
daemon: Arc<Mutex<IdleDaemon>>,
}
struct Inner {
next_cookie: u32,
inhibitors: HashMap<u32, Inhibitor>,
tx: watch::Sender<(bool, bool)>,
tx: watch::Sender<bool>,
}
impl ScreenSaver {
fn new(tx: watch::Sender<(bool, bool)>) -> Self {
fn new(tx: watch::Sender<bool>, daemon: Arc<Mutex<IdleDaemon>>) -> Self {
Self {
inner: Arc::new(Mutex::new(Inner {
next_cookie: 1,
inhibitors: HashMap::new(),
tx,
})),
daemon,
}
}
}
@@ -399,21 +425,26 @@ impl ScreenSaver {
}
};
let caffeine_flag = application_name.eq(APP_NAME);
debug!(&LOGGER, "Inhibitor added: {application_name} {cookie}");
debug!(
&LOGGER,
"Inhibitor added - Name: '{application_name}' Reason: '{reason_for_inhibit}' Value: {cookie}"
);
inner.inhibitors.insert(
cookie,
Inhibitor {
owner,
application: application_name,
_reason: reason_for_inhibit,
reason: reason_for_inhibit,
},
);
self.daemon
.lock()
.unwrap()
.publish(IdleState::Inhibited(true));
inner.tx.send_modify(|state| {
state.0 = !inner.inhibitors.is_empty();
state.1 = caffeine_flag;
*state = !inner.inhibitors.is_empty();
});
Ok(cookie)
}
@@ -432,25 +463,25 @@ impl ScreenSaver {
if let Some(removed) = inner.inhibitors.remove(&cookie) {
debug!(
&LOGGER,
"Inhibitor removed: {} {cookie}", removed.application
"Inhibitor removed - Name: '{}' Reason: '{}' Value: {cookie}",
removed.application,
removed.reason
);
}
let mut caffeine_flag = false;
for inhibitor in inner.inhibitors.values() {
if inhibitor.application.eq(APP_NAME) {
caffeine_flag = true;
break;
}
}
self.daemon
.lock()
.unwrap()
.publish(IdleState::Inhibited(!inner.inhibitors.is_empty()));
inner.tx.send_modify(|state| {
state.0 = !inner.inhibitors.is_empty();
state.1 = caffeine_flag;
*state = !inner.inhibitors.is_empty();
});
Ok(())
}
Some(_) => Err(zbus::fdo::Error::AccessDenied(
"Cookie belongs to another D-Bus client".into(),
)),
Some(_) => Err(zbus::fdo::Error::AccessDenied(format!(
"Cookie {cookie} belongs to another D-Bus client"
))),
None => Err(zbus::fdo::Error::Failed("Unknown inhibition cookie".into())),
}
}

View File

@@ -161,6 +161,26 @@ impl BackendThread<NetworkState, UnboundedSender<NetworkCommand>> for NetworkBac
}
};
std::thread::spawn({
let nm = nm.clone();
let mut instance = instance.clone();
move || {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("Failed to create Tokio runtime");
runtime.block_on(async move {
while let Ok(tx) = registration_rx.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;
}
}
});
}});
let mut events = match nm.network_events().await {
Ok(events) => events,
Err(err) => {
@@ -169,7 +189,6 @@ impl BackendThread<NetworkState, UnboundedSender<NetworkCommand>> for NetworkBac
}
};
let mut debouncer = EventDebouncer {
duration: Duration::from_millis(200),
deadline: None,
@@ -177,13 +196,6 @@ impl BackendThread<NetworkState, UnboundedSender<NetworkCommand>> for NetworkBac
};
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 {

View File

@@ -1,10 +1,10 @@
use crate::LOGGER;
use crate::backend::{BackendThread, SimpleBackendThread};
use crossbeam_channel::{Receiver, Sender};
use hyprland::data::{Client, Monitor, Workspaces};
use hyprland::data::{Client, CursorPosition, Monitor, Monitors, Workspaces};
use hyprland::event_listener::EventListener;
use hyprland::prelude::*;
use hyprland::shared::{MonitorId, WorkspaceId, WorkspaceType};
use hyprland::shared::{MonitorId, WorkspaceId};
use jmb_logger_rs::*;
use std::io;
use std::process::{Command, Output};
@@ -24,7 +24,6 @@ pub struct HyprlandComms {
}
impl SimpleBackendThread<WindowManagerEvent> for HyprlandComms {
fn subscribers(&self) -> &Vec<Sender<WindowManagerEvent>> {
&self.subscribers
}
@@ -38,9 +37,7 @@ impl SimpleBackendThread<WindowManagerEvent> for HyprlandComms {
let inst_clone = instance.clone();
listener.add_workspace_changed_handler(move |workspace| {
if let WorkspaceType::Regular(name) = workspace.name
&& let Ok(id) = name.parse::<i32>()
{
if let Ok(id) = workspace.name.parse::<i32>() {
inst_clone
.lock()
.expect("Couldn't get Subscribers")
@@ -50,26 +47,24 @@ impl SimpleBackendThread<WindowManagerEvent> for HyprlandComms {
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(),
));
}
//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().unwrap())
{
//find the current workspace Object from its id
if let Ok(id) = workspace.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(),
));
}
}
}
@@ -77,9 +72,7 @@ impl SimpleBackendThread<WindowManagerEvent> for HyprlandComms {
let inst_clone = instance.clone();
listener.add_workspace_deleted_handler(move |workspace| {
if let WorkspaceType::Regular(name) = workspace.name
&& let Ok(id) = name.parse::<i32>()
{
if let Ok(id) = workspace.name.parse::<i32>() {
inst_clone
.lock()
.expect("Couldn't get Subscribers")
@@ -92,7 +85,9 @@ impl SimpleBackendThread<WindowManagerEvent> for HyprlandComms {
inst_clone
.lock()
.expect("Couldn't get Subscribers")
.publish(WindowManagerEvent::WorkspaceChanged(evt.workspace_id));
.publish(WindowManagerEvent::WorkspaceChanged(
evt.workspace.id().unwrap(),
));
});
let inst_clone = instance.clone();
@@ -106,15 +101,22 @@ impl SimpleBackendThread<WindowManagerEvent> for HyprlandComms {
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) {
if let Some(monitor_id) =
Self::monitor_id_from_workspace_id(event.workspace.id().unwrap())
{
inst_clone
.lock()
.expect("Couldn't get Subscribers")
.publish(WindowManagerEvent::WorkspaceRemoved(event.id));
.publish(WindowManagerEvent::WorkspaceRemoved(
event.workspace.id().unwrap(),
));
inst_clone
.lock()
.expect("Couldn't get Subscribers")
.publish(WindowManagerEvent::WorkspaceAdded(event.id, monitor_id));
.publish(WindowManagerEvent::WorkspaceAdded(
event.workspace.id().unwrap(),
monitor_id,
));
}
});
@@ -160,7 +162,7 @@ impl HyprlandComms {
None
}
pub fn _current_monitor() -> MonitorId {
pub fn current_monitor() -> MonitorId {
if let Ok(monitor) = Monitor::get_active() {
return monitor.id;
}
@@ -197,4 +199,22 @@ impl HyprlandComms {
.arg(format!("hyprctl dispatch \"{dispatcher}\""))
.output()
}
pub fn cursor_monitor() -> Option<MonitorId> {
if let Ok(pos) = CursorPosition::get()
&& let Ok(monitors) = Monitors::get()
{
for monitor in monitors {
if pos.x > monitor.x as i64
&& pos.x < (monitor.x as i64 + monitor.width as i64)
&& pos.y > monitor.y as i64
&& pos.y < (monitor.y as i64 + monitor.height as i64)
{
return Some(monitor.id);
}
}
}
None
}
}

59
src/config.rs Normal file
View File

@@ -0,0 +1,59 @@
use serde::Deserialize;
use std::fs;
#[derive(Deserialize, Debug, Clone)]
pub(crate) struct Config {
idle_timeout: u64,
dim_timeout: u64,
lock_timeout: u64,
sleep_timeout: u64,
}
impl Default for Config {
fn default() -> Self {
Self {
idle_timeout: 20,
dim_timeout: 60,
lock_timeout: 300,
sleep_timeout: 320,
}
}
}
impl Config {
pub fn load() -> Config {
fs::read_to_string("jmb-bar.conf")
.ok()
.map(|s| toml::from_str::<Config>(&s).ok())
.flatten()
.unwrap_or_default()
}
pub fn idle_timeout(&self) -> u64 {
self.idle_timeout
}
pub fn dim_timeout(&self) -> u64 {
self.dim_timeout
}
pub fn lock_timeout(&self) -> u64 {
self.lock_timeout
}
pub fn sleep_timeout(&self) -> u64 {
self.sleep_timeout
}
pub fn dim_timeout_delta(&self) -> u64 {
self.dim_timeout - self.idle_timeout
}
pub fn lock_timeout_delta(&self) -> u64 {
self.lock_timeout - self.dim_timeout_delta()
}
pub fn sleep_timeout_delta(&self) -> u64 {
self.sleep_timeout - self.lock_timeout_delta()
}
}

View File

@@ -1,9 +1,11 @@
use crate::backend::Backend;
mod backend;
mod config;
mod css;
mod ctl;
mod ui;
use crate::config::Config;
use glib::clone;
use gtk::prelude::*;
use gtk::{Application, CssProvider, gdk, gio};

View File

@@ -39,7 +39,7 @@ pub fn create_bars(app: &Application, backend: Backend) {
monitor,
create_and_show_bar(
app_clone.clone(),
&display,
display.clone(),
monitor,
action_centre.clone(),
channels,
@@ -61,11 +61,11 @@ pub fn create_bars(app: &Application, backend: Backend) {
let action_centre = action_centre.clone();
let channels = backend_clone.subscribe_all();
let bars = bars.clone();
let display = display.clone();
glib::idle_add_local_once(move || {
let display = Display::default().expect("Couldn't find GDK Display");
bars.lock().expect("Couldn't get active bars").insert(
pos,
create_and_show_bar(app_clone, &display, pos, action_centre, channels),
create_and_show_bar(app_clone, display, pos, action_centre, channels),
);
});
}
@@ -74,7 +74,7 @@ pub fn create_bars(app: &Application, backend: Backend) {
let mut ctl_channel = backend.subscribe(&[ChannelType::Control]);
let ctl_rx = ctl_channel.take_ctl_rx().unwrap();
let app_clone = app.clone();
let menu = RunMenu::create(app_clone);
let mut menu = RunMenu::create(app_clone);
glib::timeout_add_local(Duration::from_millis(100), move || {
while let Ok(cmd) = ctl_rx.try_recv() {
match cmd {
@@ -89,7 +89,7 @@ pub fn create_bars(app: &Application, backend: Backend) {
fn create_and_show_bar(
app: Application,
display: &Display,
display: Display,
monitor_idx: u32,
action_centre: ActionCentre,
mut channels: BackendChannels,
@@ -138,7 +138,7 @@ fn create_and_show_bar(
let title_button: Option<Button>;
let title = title_panel::build();
let utilities = utilities_panel::build(&app, action_centre, channels, monitor_idx, display);
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");

View File

@@ -111,106 +111,111 @@ impl ActionCentre {
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
key_controller.connect_key_pressed({
let instance = instance.clone();
move |_, key, _, _| {
if key == gdk::Key::Escape {
instance.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)
window.connect_focus_widget_notify({
let wifi_networks = wifi_networks.clone();
move |window| Self::focus_callback(&wifi_networks, 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(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");
glib::timeout_add_local(Duration::from_millis(100), {
let window = window.clone();
let wifi_networks = wifi_networks.clone();
let net_rx = channels.take_net_rx().unwrap();
move || {
while let Ok(message) = net_rx.try_recv() {
match message {
NetworkState::ChangeNetwork(Some(info)) => {
for (name, widget) in wifi_networks.borrow().iter() {
if name.eq(&info.name) {
widget.add_css_class("connected");
} else {
widget.remove_css_class("connected");
}
}
}
}
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 let Some(widget) = widget {
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]);
} else {
let widget = Self::create_wifi_widget(
&channels,
network,
borrowed_wifi_networks_clone,
);
content.append(&widget);
NetworkState::Networks(networks) => {
for network in &networks {
let borrowed_wifi_networks = wifi_networks.borrow_mut();
let widget = borrowed_wifi_networks.get(&network.name);
if let Some(widget) = widget {
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]);
} else {
let widget = Self::create_wifi_widget(
&channels,
network,
borrowed_wifi_networks,
);
content.append(&widget);
}
}
let mut removal: Vec<String> = Vec::new();
for (ssid, widget) in wifi_networks.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.borrow_mut().remove(&ssid);
}
}
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::Auth(ssid, auth) => {
if let Some(widget) = Self::find_focused_widget(window.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);
NetworkState::ConnectFail(ssid, err) => {
let mut networks = wifi_networks.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 => {}
}
ConnectError::Unknown => {}
}
}
}
NetworkState::ConnectSuccess(ssid) => {
if let Some(widget) = wifi_networks_clone.borrow().get(&ssid) {
widget.stop_spinner();
NetworkState::ConnectSuccess(ssid) => {
if let Some(widget) = wifi_networks.borrow().get(&ssid) {
widget.stop_spinner();
}
}
_ => {}
}
_ => {}
}
ControlFlow::Continue
}
ControlFlow::Continue
});
instance
@@ -219,7 +224,7 @@ impl ActionCentre {
fn create_wifi_widget(
channels: &BackendChannels,
network: &NetworkInfo,
mut borrowed_wifi_networks_clone: RefMut<HashMap<String, WifiWidget>>,
mut borrowed_wifi_networks: RefMut<HashMap<String, WifiWidget>>,
) -> WifiWidget {
let ssid = network.name.clone();
let widget = WifiWidget::new();
@@ -229,7 +234,7 @@ impl ActionCentre {
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());
borrowed_wifi_networks.insert(ssid.clone(), widget.clone());
let net_tx = channels.net_tx().unwrap();
widget.connect_clicked(clone!(
#[strong]

View File

@@ -1,27 +1,44 @@
use gtk::prelude::{BoxExt, GtkWindowExt, WidgetExt};
use crate::LOGGER;
use crate::backend::window_manager_hypr::HyprlandComms;
use freedesktop_desktop_entry::{default_paths, get_languages_from_env};
use glib::clone;
use glib::prelude::Cast;
use gtk::gdk::{Display, Monitor};
use gtk::prelude::{
BoxExt, ButtonExt, DisplayExt, EditableExt, EntryExt, GtkWindowExt, ListModelExt, MonitorExt,
WidgetExt,
};
use gtk::{Align, Application, ApplicationWindow, Orientation, PolicyType, gdk};
use gtk4_layer_shell::{KeyboardMode, Layer, LayerShell};
use gtk4_layer_shell::{Edge, KeyboardMode, Layer, LayerShell};
use jmb_logger_rs::*;
use nucleo_matcher::pattern::{CaseMatching, Normalization, Pattern};
use nucleo_matcher::{Config, Matcher};
use std::cell::RefCell;
use std::collections::HashMap;
use std::process::Command;
use std::rc::Rc;
#[derive(Clone)]
pub struct RunMenu {
window: ApplicationWindow,
entry: gtk::Entry,
entries: Rc<RefCell<HashMap<String, Vec<String>>>>,
}
impl RunMenu {
pub fn create(application: Application) -> Self {
let window = gtk::ApplicationWindow::builder()
let window = ApplicationWindow::builder()
.application(&application)
.title("JmbRunMenu")
.default_width(500)
.default_height(300)
.css_classes(["run-menu"])
.build();
window.init_layer_shell();
window.set_layer(Layer::Overlay);
window.set_halign(Align::Center);
window.set_valign(Align::Center);
window.set_size_request(500, 300);
window.set_width_request(500);
window.set_anchor(Edge::Top, true);
window.set_keyboard_mode(KeyboardMode::Exclusive);
let v_box = gtk::Box::new(Orientation::Vertical, 0);
@@ -31,28 +48,109 @@ impl RunMenu {
let scrolled = gtk::ScrolledWindow::new();
scrolled.set_policy(PolicyType::Never, PolicyType::Automatic);
scrolled.set_vexpand(true);
scrolled.set_valign(Align::Fill);
scrolled.set_vexpand(false);
scrolled.set_propagate_natural_height(true);
scrolled.set_min_content_height(0);
scrolled.set_max_content_height(300);
let content_box = gtk::Box::new(Orientation::Vertical, 0);
content_box.add_css_class("content-box");
for _ in 0..10 {
let label = gtk::Label::new(Some("Test"));
content_box.append(&label);
}
content_box.set_hexpand(true);
scrolled.set_child(Some(&content_box));
v_box.set_vexpand(false);
v_box.set_valign(Align::Start);
v_box.append(&entry);
v_box.append(&scrolled);
window.set_child(Some(&v_box));
let display = Display::default().expect("Couldn't get display");
let monitor = display
.monitors()
.item(HyprlandComms::current_monitor() as u32)
.expect("Couldn't get monitor")
.downcast::<Monitor>()
.ok();
window.set_monitor(monitor.as_ref());
if let Some(monitor) = monitor {
let geometry = monitor.geometry();
let (_, natural_height, _, _) =
window.child().unwrap().measure(Orientation::Vertical, -1);
println!("Monitor height: {}", geometry.height());
let top_margin = (geometry.height()) / 2;
window.set_margin(Edge::Top, top_margin - 100);
} else {
window.set_valign(Align::Center);
}
content_box
.observe_children()
.connect_items_changed(move |model, _, _, _| {
if model.n_items() == 0 {
println!("Hide");
} else {
println!("Reveal");
}
});
let window_clone = window.clone();
let instance = Self {
window: window_clone,
entry: entry.clone(),
entries: Rc::new(RefCell::new(HashMap::new())),
};
let instance_clone = instance.clone();
let matcher_rc = Rc::new(RefCell::new(Matcher::new(Config::DEFAULT)));
entry.connect_changed(clone!(
#[strong]
content_box,
move |e| {
while let Some(child) = content_box.first_child() {
content_box.remove(&child);
}
if e.text_length() > 0 {
let text = e.text();
if text.starts_with("=") {
if let Ok(eval) = meval::eval_str(&text[1..]) {
let label = gtk::Label::new(Some(&format!("{eval}")));
label.add_css_class("exec-button");
label.set_halign(Align::Start);
label.set_hexpand(true);
content_box.append(&label);
}
} else {
instance_clone.list_matching(e, &content_box, &mut matcher_rc.borrow_mut());
instance_clone.window.set_default_size(500, -1);
}
}
}
));
entry.connect_has_focus_notify(clone!(
#[strong]
content_box,
move |e| {
if let Some(child) = content_box.first_child() {
if e.has_focus() {
child.add_css_class("suggested");
} else {
child.remove_css_class("suggested");
}
}
}
));
entry.connect_activate(clone!(
#[strong]
content_box,
move |_| {
if let Some(child) = content_box.first_child()
&& let Ok(button) = child.downcast::<gtk::Button>()
{
button.activate();
}
}
));
let instance_clone = instance.clone();
let key_controller = gtk::EventControllerKey::new();
key_controller.connect_key_pressed(move |_, key, _, _| {
@@ -69,9 +167,82 @@ impl RunMenu {
pub fn hide(&self) {
self.window.set_visible(false);
self.entry.set_text("");
}
pub fn show(&self) {
pub fn show(&mut self) {
self.load_desktop_files();
self.window.present();
}
fn create_entry(&self, content_box: &gtk::Box, entry: &String) -> gtk::Button {
let entry_clone = entry.clone();
let button = gtk::Button::new();
button.set_halign(Align::Fill);
button.set_hexpand(true);
let label = gtk::Label::new(Some(entry));
label.set_halign(Align::Start);
button.set_child(Some(&label));
button.add_css_class("exec-button");
let entry_map = self.entries.clone();
let instance = self.clone();
button.connect_clicked(move |_| {
let map = entry_map.borrow();
let exec = map.get(&entry_clone);
if let Some(exec) = exec {
let executable = &exec[0];
if let Err(err) = Command::new(executable).args(&exec[1..]).spawn() {
warn!(&LOGGER, "Couldn't execute app: {err}");
}
instance.hide();
}
});
content_box.append(&button);
button
}
fn list_matching(
&self,
entry: &gtk::Entry,
content_box: &gtk::Box,
matcher: &mut Matcher,
) -> bool {
let keys: Vec<String> = self.entries.borrow().keys().map(|i| i.clone()).collect();
let mut first = true;
let mut added = false;
for (entry, _) in Pattern::parse(&entry.text(), CaseMatching::Ignore, Normalization::Smart)
.match_list(keys, matcher)
.iter()
{
let button = self.create_entry(content_box, &entry);
if first {
button.add_css_class("suggested");
first = false;
}
added = true;
}
added
}
fn load_desktop_files(&mut self) {
let locales = get_languages_from_env();
let entries = freedesktop_desktop_entry::Iter::new(default_paths())
.entries(Some(&locales))
.collect::<Vec<_>>();
let mut entry_map = self.entries.borrow_mut();
entry_map.clear();
for entry in entries {
if let Ok(exec) = entry.parse_exec() {
let name = entry.full_name(&locales);
if let Some(name) = name {
entry_map.insert(name.to_string(), exec);
}
}
}
}
}

View File

@@ -78,14 +78,16 @@ pub fn build(
let media_tx = channels.media_tx().unwrap();
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,
));
volume.connect_value_changed({
let changed_progr = changed_progr.clone();
move |slider, button, _| {
if !changed_progr.get() {
let _ = media_tx.send(MediaCommand::Cmd(
slider.value() as u8,
button.is_active(),
false,
));
}
}
});
let media_tx = channels.media_tx().unwrap();
@@ -189,7 +191,7 @@ pub fn build(
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");
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);
@@ -312,184 +314,188 @@ pub fn build(
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 netbird_rx = channels.take_netbird_rx().unwrap();
let idle_rx = channels.take_idle_rx().unwrap();
let monitor_clone = monitor.clone();
let br_tx = channels.br_tx().unwrap();
glib::timeout_add_local(Duration::from_millis(100), move || {
while let Ok(command) = media_rx.try_recv() {
if let MediaCommand::Cmd(vol, mute, bt) = command {
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
glib::timeout_add_local(Duration::from_millis(100), {
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 netbird_rx = channels.take_netbird_rx().unwrap();
let idle_rx = channels.take_idle_rx().unwrap();
let monitor = monitor.clone();
let br_tx = channels.br_tx().unwrap();
move || {
while let Ok(command) = media_rx.try_recv() {
if let MediaCommand::Cmd(vol, mute, bt) = command {
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() {
if let BrightnessMessage::Value(mon, value, _) = message
&& 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_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]);
.set_font_icon(BRIGHTNESS_ICONS[idx]);
}
volume.imp().second_icon.set_visible(bt);
}
}
while let Ok(message) = br_rx.try_recv() {
if let BrightnessMessage::Value(mon, value, _) = message
&& 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]);
}
}
while let Ok(state) = idle_rx.try_recv() {
match state {
IdleState::Dimmed => {
if !caffeine_button.has_css_class("idle") && monitor_valid {
let _ =
br_tx.send(BrightnessMessage::Value(monitor_clone.clone(), 20, true));
brightness.set_value(20.0);
while let Ok(state) = idle_rx.try_recv() {
match state {
IdleState::Dimmed => {
if !caffeine_button.has_css_class("idle") && monitor_valid {
let _ = br_tx.send(BrightnessMessage::Value(monitor.clone(), 20, true));
brightness.set_value(20.0);
}
caffeine_button.add_css_class("idle");
caffeine_button.remove_css_class("inhibit");
caffeine_button.remove_css_class("on");
}
caffeine_button.add_css_class("idle");
caffeine_button.remove_css_class("inhibit");
caffeine_button.remove_css_class("on");
}
other => {
caffeine_button.remove_css_class("idle");
caffeine_button.remove_css_class("inhibit");
caffeine_button.remove_css_class("on");
if monitor_valid {
let _ = br_tx.send(BrightnessMessage::Revert(monitor_clone.clone()));
}
match other {
IdleState::Caffeine => caffeine_button.add_css_class("on"),
IdleState::Inhibited => caffeine_button.add_css_class("inhibit"),
IdleState::Active => {}
_ => {}
other => {
caffeine_button.remove_css_class("idle");
if monitor_valid {
let _ = br_tx.send(BrightnessMessage::Revert(monitor.clone()));
}
match other {
IdleState::Caffeine(true) => caffeine_button.add_css_class("on"),
IdleState::Inhibited(true) => caffeine_button.add_css_class("inhibit"),
IdleState::Caffeine(false) => caffeine_button.remove_css_class("on"),
IdleState::Inhibited(false) => {
caffeine_button.remove_css_class("inhibit")
}
IdleState::Active => {}
_ => {}
}
}
}
}
}
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);
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);
None => {
network_icon.set_from_file(Some("icons/disconnected.svg"));
network_label.set_visible(false);
}
}
}
NetworkState::VpnState { connected, profile } if profile.as_str() == "wg" => {
if connected {
wg_button.add_css_class("on");
} else {
wg_button.remove_css_class("on");
}
}
_ => {}
}
NetworkState::VpnState { connected, profile } if profile.as_str() == "wg" => {
if connected {
wg_button.add_css_class("on");
} else {
wg_button.remove_css_class("on");
}
while let Ok(tailscale_state) = netbird_rx.try_recv() {
match tailscale_state {
json_socket::State::Disconnected => {
netbird_button.remove_css_class("on");
netbird_button.remove_css_class("starting");
netbird_button.remove_css_class("unknown");
netbird_button.remove_css_class("login");
}
json_socket::State::Connecting => {
netbird_button.remove_css_class("on");
netbird_button.add_css_class("starting");
netbird_button.remove_css_class("unknown");
netbird_button.remove_css_class("login");
}
json_socket::State::Connected => {
netbird_button.add_css_class("on");
netbird_button.remove_css_class("starting");
netbird_button.remove_css_class("unknown");
netbird_button.remove_css_class("login");
}
json_socket::State::NeedsLogin => {
netbird_button.remove_css_class("on");
netbird_button.remove_css_class("starting");
netbird_button.remove_css_class("unknown");
netbird_button.add_css_class("login");
}
}
_ => {}
}
}
while let Ok(tailscale_state) = netbird_rx.try_recv() {
match tailscale_state {
json_socket::State::Disconnected => {
netbird_button.remove_css_class("on");
netbird_button.remove_css_class("starting");
netbird_button.remove_css_class("unknown");
netbird_button.remove_css_class("login");
}
json_socket::State::Connecting => {
netbird_button.remove_css_class("on");
netbird_button.add_css_class("starting");
netbird_button.remove_css_class("unknown");
netbird_button.remove_css_class("login");
}
json_socket::State::Connected => {
netbird_button.add_css_class("on");
netbird_button.remove_css_class("starting");
netbird_button.remove_css_class("unknown");
netbird_button.remove_css_class("login");
}
json_socket::State::NeedsLogin => {
netbird_button.remove_css_class("on");
netbird_button.remove_css_class("starting");
netbird_button.remove_css_class("unknown");
netbird_button.add_css_class("login");
}
if wg_button.has_css_class("on") || netbird_button.has_css_class("on") {
vpn_button.add_css_class("on");
vpn_button.remove_css_class("login");
} else if wg_button.has_css_class("login") || netbird_button.has_css_class("login") {
vpn_button.add_css_class("login");
vpn_button.remove_css_class("on");
} else {
vpn_button.remove_css_class("on");
vpn_button.remove_css_class("login");
}
}
if wg_button.has_css_class("on") || netbird_button.has_css_class("on") {
vpn_button.add_css_class("on");
vpn_button.remove_css_class("login");
} else if wg_button.has_css_class("login") || netbird_button.has_css_class("login") {
vpn_button.add_css_class("login");
vpn_button.remove_css_class("on");
} else {
vpn_button.remove_css_class("on");
vpn_button.remove_css_class("login");
}
while let Ok(stats) = stats_rx.try_recv() {
if let Some(cpu) = stats.cpu {
cpu_label.set_label(&format!("{}%", cpu));
}
if let Some(ram) = stats.ram {
ram_label.set_label(&format!("{}%", ram));
}
for disk in stats.disks {
if disk.0.eq("/") {
disk_label.set_label(&format!("{}%", disk.1));
break;
while let Ok(stats) = stats_rx.try_recv() {
if let Some(cpu) = stats.cpu {
cpu_label.set_label(&format!("{}%", cpu));
}
if let Some(ram) = stats.ram {
ram_label.set_label(&format!("{}%", ram));
}
for disk in stats.disks {
if disk.0.eq("/") {
disk_label.set_label(&format!("{}%", disk.1));
break;
}
}
}
if let Some(gpu) = stats.gpus.into_iter().next() {
gpu_label.set_label(&format!("{}%", gpu.1));
}
if let Some(gpu) = stats.gpus.into_iter().next() {
gpu_label.set_label(&format!("{}%", gpu.1));
}
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)));
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
}
ControlFlow::Continue
});
utilities
}