536 lines
20 KiB
Rust
536 lines
20 KiB
Rust
use crate::LOGGER;
|
|
use crate::backend::audio::MediaCommand;
|
|
use crate::backend::brightness_control::BrightnessMessage;
|
|
use crate::backend::idle::{IdleMessage, IdleState};
|
|
use crate::backend::network::{NetworkCommand, NetworkState, NetworkType};
|
|
use crate::backend::{BackendChannels, json_socket};
|
|
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();
|
|
println!(
|
|
"{:?}",
|
|
monitors
|
|
.item(monitor_idx)
|
|
.unwrap()
|
|
.downcast::<gdk::Monitor>()
|
|
.unwrap()
|
|
.connector()
|
|
);
|
|
if monitor_valid {
|
|
let _ = channels
|
|
.br_tx()
|
|
.send(BrightnessMessage::Request(monitor.clone()));
|
|
println!("req");
|
|
}
|
|
|
|
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,
|
|
false,
|
|
));
|
|
let idx =
|
|
((slider_val / 100.0) * (BRIGHTNESS_ICONS.len() - 1) as f64 + 0.5) as usize;
|
|
slider.imp().main_icon.set_font_icon(BRIGHTNESS_ICONS[idx]);
|
|
}
|
|
}
|
|
));
|
|
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, false));
|
|
slider.set_value(60.0);
|
|
let idx = ((60.0 / 100.0) * (BRIGHTNESS_ICONS.len() - 1) as f64 + 0.5) as usize;
|
|
slider.imp().main_icon.set_font_icon(BRIGHTNESS_ICONS[idx]);
|
|
}
|
|
}
|
|
));
|
|
|
|
let caffeine_button = Button::with_label(" ");
|
|
caffeine_button.add_css_class("caffeine");
|
|
let idle_tx = channels.idle_tx();
|
|
caffeine_button.connect_clicked(move |button| {
|
|
if button.has_css_class("on") {
|
|
let _ = idle_tx.send(IdleMessage::Release);
|
|
} else {
|
|
let _ = idle_tx.send(IdleMessage::Inhibit);
|
|
}
|
|
});
|
|
|
|
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 netbird_button = Button::with_label("nb");
|
|
netbird_button.add_css_class("vpn");
|
|
netbird_button.connect_clicked(move |button| {
|
|
let _ = Command::new("netbird")
|
|
.arg(if button.has_css_class("on") {
|
|
"down"
|
|
} else {
|
|
"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(&netbird_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_default();
|
|
time.set_title(&time_str);
|
|
let time_sec = time_format::strftime_local(":%S %Y-%m-%d", timestamp).unwrap_or_default();
|
|
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 netbird_rx = channels.take_netbird_rx().unwrap();
|
|
let idle_rx = channels.take_idle_rx().unwrap();
|
|
let monitor_clone = monitor.clone();
|
|
let br_tx = channels.br_tx();
|
|
glib::timeout_add_local(Duration::from_millis(100), move || {
|
|
while let Ok(command) = media_rx.try_recv() {
|
|
if let MediaCommand::Cmd(vol, mute, bt) = command {
|
|
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_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);
|
|
println!("Dimmed: {monitor_clone}");
|
|
}
|
|
caffeine_button.add_css_class("idle");
|
|
caffeine_button.remove_css_class("inhibit");
|
|
caffeine_button.remove_css_class("on");
|
|
}
|
|
other => {
|
|
caffeine_button.remove_css_class("idle");
|
|
caffeine_button.remove_css_class("inhibit");
|
|
caffeine_button.remove_css_class("on");
|
|
if monitor_valid {
|
|
let _ = br_tx.send(BrightnessMessage::Revert(monitor_clone.clone()));
|
|
}
|
|
match other {
|
|
IdleState::Caffeine => caffeine_button.add_css_class("on"),
|
|
IdleState::Inhibited => caffeine_button.add_css_class("inhibit"),
|
|
IdleState::Active => {}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
while let Ok(result) = net_rx.try_recv() {
|
|
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 } 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");
|
|
}
|
|
}
|
|
}
|
|
if wg_button.has_css_class("on") || netbird_button.has_css_class("on") {
|
|
vpn_button.add_css_class("on");
|
|
vpn_button.remove_css_class("login");
|
|
} else if wg_button.has_css_class("login") || netbird_button.has_css_class("login") {
|
|
vpn_button.add_css_class("login");
|
|
vpn_button.remove_css_class("on");
|
|
} else {
|
|
vpn_button.remove_css_class("on");
|
|
vpn_button.remove_css_class("login");
|
|
}
|
|
while let Ok(stats) = stats_rx.try_recv() {
|
|
cpu_label.set_label(&format!("{}%", stats.cpu));
|
|
ram_label.set_label(&format!("{}%", stats.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));
|
|
}
|
|
|
|
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>");
|
|
}
|
|
}
|
|
}
|
|
}
|