Compare commits
25 Commits
957a53fd8c
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
092f0eab76 | ||
|
|
55ade087de | ||
|
|
495210c229 | ||
|
|
3285353006 | ||
|
|
1ec5d1e30d | ||
|
|
0a3a93adf2 | ||
|
|
0f67f4ffcf | ||
|
|
63f247a0dc | ||
|
|
5ffed55e37 | ||
|
|
f88f3ba30d | ||
|
|
2fa7b4a55c | ||
|
|
0f19ff6213 | ||
|
|
61a4e136ed | ||
|
|
daf865423f | ||
|
|
e547fd5885 | ||
|
|
c758268beb | ||
|
|
541ce5ccb2 | ||
|
|
f46c2408a5 | ||
|
|
c3ad6f38cf | ||
|
|
11fb03427c | ||
|
|
cdfc5b8d36 | ||
|
|
de1f731e63 | ||
|
|
00b2b1f64c | ||
|
|
39f4263db5 | ||
|
|
87d27f9734 |
72
.gitea/workflows/build.yaml
Normal file
72
.gitea/workflows/build.yaml
Normal file
@@ -0,0 +1,72 @@
|
||||
name: Build Project
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Linux
|
||||
runs-on: ubuntu-26.04
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Print Os release
|
||||
run: cat /etc/os-release
|
||||
|
||||
- name: Cache Cargo
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
target/
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cargo-
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
build-essential \
|
||||
pkg-config \
|
||||
libgtk-4-dev \
|
||||
libgtk4-layer-shell-dev \
|
||||
libudev-dev \
|
||||
libpulse-dev
|
||||
|
||||
- name: Install Rust
|
||||
run: |
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
|
||||
| sh -s -- -y --profile minimal --default-toolchain stable
|
||||
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Install Rust components
|
||||
run: |
|
||||
rustup component add clippy
|
||||
|
||||
- name: Run Clippy
|
||||
run: cargo clippy --all-targets --all-features -- -D warnings
|
||||
|
||||
- name: Run tests
|
||||
run: cargo test --locked
|
||||
|
||||
- name: Build release binaries
|
||||
run: cargo build --locked --release --bins
|
||||
|
||||
- name: Upload binaries
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: jmb-bar-linux-x86_64
|
||||
path: |
|
||||
target/release/jmbbar
|
||||
target/release/jmbctl
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
3676
Cargo.lock
generated
3676
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
13
Cargo.toml
13
Cargo.toml
@@ -9,16 +9,15 @@ path = "src/main.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "jmbctl"
|
||||
path = "src/ctl.rs"
|
||||
path = "src/bin/ctl.rs"
|
||||
|
||||
[dependencies]
|
||||
grass = "0.13.4"
|
||||
gtk = { version = "0.11.5", package = "gtk4", features = ["v4_24"] }
|
||||
gtk = { version = "0.11.5", package = "gtk4", features = ["v4_22"] }
|
||||
gtk4-layer-shell = "0.8.1"
|
||||
hyprland = { git = "https://github.com/romanstingler/hyprland-rs-next", branch = "master" }
|
||||
notify = "8.2.0"
|
||||
glib = "0.22.10"
|
||||
mpris = "2.1.0"
|
||||
time-format = "1.2.4"
|
||||
nmrs = "3.5.7"
|
||||
tokio = { version = "1.53.3", features = ["rt", "time", "net", "sync", "rt-multi-thread", "io-util", "macros", "signal"] }
|
||||
@@ -27,7 +26,6 @@ serde_json = "1.0.154"
|
||||
crossbeam-channel = "0.5.17"
|
||||
jmb-logger-rs = { version = "0.2.2", git = "https://github.com/jmb05/jmb-logger-rs" }
|
||||
sysinfo = "0.40.0"
|
||||
all-smi = "0.28.1"
|
||||
rustsni = "0.2.2"
|
||||
thiserror = "2.0.21"
|
||||
pulse = { package = "libpulse-binding", version = "2.30" }
|
||||
@@ -42,13 +40,14 @@ battery = "0.7.8"
|
||||
ddc-hi = { git = "https://github.com/jmb05/ddc-hi-rs" }
|
||||
wayland-protocols = { version = "0.32.13", features = ["staging", "client"] }
|
||||
wayland-client = "0.31.15"
|
||||
zbus = "5.19.0"
|
||||
zbus = "5.19.0" # Keep at this version for now because other dependencies also have it
|
||||
serde = { version = "1.0.229", features = ["derive"] }
|
||||
freedesktop-desktop-entry = "0.8.4"
|
||||
nucleo-matcher = "0.3.1"
|
||||
meval = "0.2.0"
|
||||
meval = { version = "0.3.0", git = "https://github.com/UncleAndy/meval-rs" }
|
||||
toml = "1.1.8"
|
||||
#zbus = "6.1.0"
|
||||
|
||||
[build-dependencies]
|
||||
glib-build-tools = "0.22.8"
|
||||
grass = "0.13.4"
|
||||
regex = "1.13.1"
|
||||
|
||||
@@ -4,7 +4,8 @@ Additionally, it acts as an idle daemon. It is written in rust and uses Gtk 4 an
|
||||
gtk-layer-shell.
|
||||
**This is a hobby project and for my own personal use. It might be unoptimized in
|
||||
some places and naively implemented. It is also most definitely not very customizable
|
||||
and highly opinionated. Use at your own risk.**
|
||||
and highly opinionated. It is built with the specifics of my system in mind and
|
||||
will probably not work on your machine. Use at your own risk.**
|
||||
|
||||
### Building and Running
|
||||
To build the project you need the rust compiler and cargo installed.
|
||||
|
||||
28
build.rs
28
build.rs
@@ -1,7 +1,35 @@
|
||||
use regex::Regex;
|
||||
use std::path::PathBuf;
|
||||
use std::{env, fs};
|
||||
|
||||
fn main() {
|
||||
glib_build_tools::compile_resources(
|
||||
&["resources"],
|
||||
"resources/resources.gresource.xml",
|
||||
"templates.gresource",
|
||||
);
|
||||
|
||||
let colors_path = "style/_colors.scss";
|
||||
|
||||
println!("cargo:rerun-if-changed={colors_path}");
|
||||
|
||||
let scss = fs::read_to_string(colors_path).expect("Failed to read SCSS colors");
|
||||
|
||||
let regex =
|
||||
Regex::new(r"(?m)^\s*\$([a-zA-Z_][a-zA-Z0-9_-]*)\s*:\s*(#[0-9a-fA-F]{3,8})\s*;").unwrap();
|
||||
|
||||
let mut generated = String::from("// Generated by build.rs. Do not edit\n\n");
|
||||
|
||||
for captures in regex.captures_iter(&scss) {
|
||||
let name = captures.get(1).unwrap().as_str();
|
||||
let value = captures.get(2).unwrap().as_str();
|
||||
|
||||
let constant_name = name.replace('-', "_").to_uppercase();
|
||||
|
||||
generated.push_str(&format!("pub const {constant_name}: &str = \"{value}\";\n"));
|
||||
}
|
||||
|
||||
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
|
||||
|
||||
fs::write(out_dir.join("theme.rs"), generated).expect("Failed to generate theme.rs");
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
[idle]
|
||||
idle_timeout = 20
|
||||
dim_timeout = 60
|
||||
lock_timeout = 300
|
||||
sleep_timeout = 320
|
||||
sleep_timeout = 320
|
||||
lock_command = 'hyprlock'
|
||||
sleep_command = 'systemctl sleep'
|
||||
107
src/backend.rs
107
src/backend.rs
@@ -1,6 +1,7 @@
|
||||
pub mod audio;
|
||||
pub mod brightness_control;
|
||||
mod commands;
|
||||
mod gpu_utilization;
|
||||
pub(crate) mod idle;
|
||||
pub(crate) mod json_socket;
|
||||
pub mod network;
|
||||
@@ -8,16 +9,17 @@ pub mod perf_stats;
|
||||
pub mod window_manager_hypr;
|
||||
|
||||
use crate::LOGGER;
|
||||
use crate::backend::audio::{MediaBackend, MediaCommand};
|
||||
use crate::backend::audio::{AudioBackend, AudioMessage, MediaBackend, MediaMessage};
|
||||
use crate::backend::brightness_control::{BrightnessManager, BrightnessMessage};
|
||||
use crate::backend::commands::ControlSocket;
|
||||
use crate::backend::idle::{IdleDaemon, IdleMessage, IdleState};
|
||||
use crate::backend::json_socket::{JsonSocketBackend, State};
|
||||
use crate::backend::network::{NetworkBackendHandle, NetworkCommand, NetworkState};
|
||||
use crate::backend::network::{NetworkBackend, NetworkCommand, NetworkState};
|
||||
use crate::backend::perf_stats::{Stats, StatsBackend};
|
||||
use crate::backend::window_manager_hypr::{HyprlandComms, WindowManagerEvent};
|
||||
use crate::ctl::ControlCommand;
|
||||
use crossbeam_channel::{Receiver, Sender};
|
||||
use jmb_bar::ControlCommand;
|
||||
use jmb_bar::config::Config;
|
||||
use jmb_logger_rs::*;
|
||||
use std::fmt::Debug;
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
@@ -27,8 +29,15 @@ 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 init(self, registration_rx: Receiver<Sender<M>>);
|
||||
fn publish(&self, message: M) {
|
||||
if self.subscribers().is_empty() {
|
||||
warn!(
|
||||
&LOGGER,
|
||||
"Tried sending message: {message:?} to no subscribers"
|
||||
);
|
||||
return;
|
||||
}
|
||||
trace!(&LOGGER, "Sending: {message:?} to {:?}", self.subscribers());
|
||||
for tx in self.subscribers() {
|
||||
if let Err(e) = tx.send(message.clone()) {
|
||||
@@ -40,16 +49,19 @@ pub trait BackendThread<M: Debug + Clone, D> {
|
||||
}
|
||||
}
|
||||
}
|
||||
fn start() -> Registrator<M> {
|
||||
fn start(self) -> Registrator<M>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let (registration_tx, registration_rx) = crossbeam_channel::unbounded::<Sender<M>>();
|
||||
Self::init(registration_rx);
|
||||
Self::init(self, registration_rx);
|
||||
registration_tx
|
||||
}
|
||||
}
|
||||
|
||||
pub trait SimpleBackendThread<M: Debug + Clone>: BackendThread<M, ()> {
|
||||
fn subscribers(&self) -> &Vec<Sender<M>>;
|
||||
fn init(registration_rx: Receiver<Sender<M>>);
|
||||
fn init(self, registration_rx: Receiver<Sender<M>>);
|
||||
}
|
||||
|
||||
impl<M: Debug + Clone, S: SimpleBackendThread<M>> BackendThread<M, ()> for S {
|
||||
@@ -63,8 +75,8 @@ impl<M: Debug + Clone, S: SimpleBackendThread<M>> BackendThread<M, ()> for S {
|
||||
SimpleBackendThread::subscribers(self)
|
||||
}
|
||||
|
||||
fn init(registration_rx: Receiver<Sender<M>>) {
|
||||
<S as SimpleBackendThread<M>>::init(registration_rx)
|
||||
fn init(self, registration_rx: Receiver<Sender<M>>) {
|
||||
<S as SimpleBackendThread<M>>::init(self, registration_rx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,17 +86,20 @@ pub struct Backend {
|
||||
net_registrator: Registrator<NetworkState>,
|
||||
br_registrator: Registrator<BrightnessMessage>,
|
||||
stats_registrator: Registrator<Stats>,
|
||||
media_registrator: Registrator<MediaCommand>,
|
||||
audio_registrator: Registrator<AudioMessage>,
|
||||
media_registration: Registrator<MediaMessage>,
|
||||
netbird_registrator: Registrator<State>,
|
||||
idle_registrator: Registrator<IdleState>,
|
||||
control_registrator: Registrator<ControlCommand>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub enum ChannelType {
|
||||
Net,
|
||||
Hypr,
|
||||
Br,
|
||||
Stats,
|
||||
Audio,
|
||||
Media,
|
||||
Idle,
|
||||
Netbird,
|
||||
@@ -92,24 +107,26 @@ pub enum ChannelType {
|
||||
}
|
||||
|
||||
impl Backend {
|
||||
pub fn start() -> Self {
|
||||
pub fn start(config: &Config) -> 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 = JsonSocketBackend::start();
|
||||
let idle_registrator = IdleDaemon::start();
|
||||
let control_registrator = ControlSocket::start();
|
||||
let hypr_registrator = HyprlandComms::new().start();
|
||||
let net_registrator = NetworkBackend::new().start();
|
||||
let br_registrator = BrightnessManager::new().start();
|
||||
let stats_registrator = StatsBackend::new().start();
|
||||
let audio_registrator = AudioBackend::new().start();
|
||||
let media_registration = MediaBackend::new().start();
|
||||
let netbird_registrator = JsonSocketBackend::new().start();
|
||||
let idle_registrator = IdleDaemon::new(config.idle.clone()).start();
|
||||
let control_registrator = ControlSocket::new().start();
|
||||
debug!(&LOGGER, "Started Backend threads");
|
||||
Self {
|
||||
hypr_registrator,
|
||||
net_registrator,
|
||||
br_registrator,
|
||||
stats_registrator,
|
||||
media_registrator,
|
||||
netbird_registrator: tailscale_registrator,
|
||||
audio_registrator,
|
||||
media_registration,
|
||||
netbird_registrator,
|
||||
idle_registrator,
|
||||
control_registrator,
|
||||
}
|
||||
@@ -120,25 +137,22 @@ impl Backend {
|
||||
for ch_type in channel_types {
|
||||
match ch_type {
|
||||
ChannelType::Net => {
|
||||
let (rx, tx) = NetworkBackendHandle::subscribe(self.net_registrator.clone());
|
||||
let (rx, tx) = NetworkBackend::subscribe(self.net_registrator.clone());
|
||||
channels.net_tx = Some(tx);
|
||||
channels.net_rx = Some(rx);
|
||||
}
|
||||
ChannelType::Hypr => {
|
||||
channels.hypr_rx =
|
||||
Some(HyprlandComms::subscribe(self.hypr_registrator.clone()).0);
|
||||
}
|
||||
ChannelType::Br => {
|
||||
let (rx, tx) = BrightnessManager::subscribe(self.br_registrator.clone());
|
||||
channels.br_rx = Some(rx);
|
||||
channels.br_tx = Some(tx);
|
||||
}
|
||||
ChannelType::Stats => {
|
||||
channels.stats_rx =
|
||||
Some(StatsBackend::subscribe(self.stats_registrator.clone()).0);
|
||||
ChannelType::Audio => {
|
||||
let (rx, tx) = AudioBackend::subscribe(self.audio_registrator.clone());
|
||||
channels.audio_rx = Some(rx);
|
||||
channels.audio_tx = Some(tx);
|
||||
}
|
||||
ChannelType::Media => {
|
||||
let (rx, tx) = MediaBackend::subscribe(self.media_registrator.clone());
|
||||
let (rx, tx) = MediaBackend::subscribe(self.media_registration.clone());
|
||||
channels.media_rx = Some(rx);
|
||||
channels.media_tx = Some(tx);
|
||||
}
|
||||
@@ -155,22 +169,33 @@ impl Backend {
|
||||
channels.ctl_rx =
|
||||
Some(ControlSocket::subscribe(self.control_registrator.clone()).0);
|
||||
}
|
||||
ChannelType::Stats => {
|
||||
channels.stats_rx =
|
||||
Some(StatsBackend::subscribe(self.stats_registrator.clone()).0);
|
||||
}
|
||||
ChannelType::Hypr => {
|
||||
channels.hypr_rx =
|
||||
Some(HyprlandComms::subscribe(self.hypr_registrator.clone()).0);
|
||||
}
|
||||
}
|
||||
}
|
||||
channels
|
||||
}
|
||||
|
||||
pub fn subscribe_all(&self) -> BackendChannels {
|
||||
self.subscribe(&[
|
||||
pub fn subscribe_except(&self, channel_types: &[ChannelType]) -> BackendChannels {
|
||||
let mut types = vec![
|
||||
ChannelType::Net,
|
||||
ChannelType::Idle,
|
||||
ChannelType::Netbird,
|
||||
ChannelType::Br,
|
||||
ChannelType::Stats,
|
||||
ChannelType::Control,
|
||||
ChannelType::Audio,
|
||||
ChannelType::Media,
|
||||
ChannelType::Hypr,
|
||||
])
|
||||
];
|
||||
types.retain(|i| !channel_types.contains(i));
|
||||
self.subscribe(&types)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,8 +207,10 @@ pub struct BackendChannels {
|
||||
br_rx: Option<Receiver<BrightnessMessage>>,
|
||||
br_tx: Option<Sender<BrightnessMessage>>,
|
||||
stats_rx: Option<Receiver<Stats>>,
|
||||
media_rx: Option<Receiver<MediaCommand>>,
|
||||
media_tx: Option<Sender<MediaCommand>>,
|
||||
audio_rx: Option<Receiver<AudioMessage>>,
|
||||
audio_tx: Option<Sender<AudioMessage>>,
|
||||
media_rx: Option<Receiver<MediaMessage>>,
|
||||
media_tx: Option<Sender<MediaMessage>>,
|
||||
netbird_rx: Option<Receiver<State>>,
|
||||
idle_rx: Option<Receiver<IdleState>>,
|
||||
idle_tx: Option<Sender<IdleMessage>>,
|
||||
@@ -206,7 +233,10 @@ impl BackendChannels {
|
||||
pub fn take_netbird_rx(&mut self) -> Option<Receiver<State>> {
|
||||
self.netbird_rx.take()
|
||||
}
|
||||
pub fn take_media_rx(&mut self) -> Option<Receiver<MediaCommand>> {
|
||||
pub fn take_audio_rx(&mut self) -> Option<Receiver<AudioMessage>> {
|
||||
self.audio_rx.take()
|
||||
}
|
||||
pub fn take_media_rx(&mut self) -> Option<Receiver<MediaMessage>> {
|
||||
self.media_rx.take()
|
||||
}
|
||||
pub fn take_idle_rx(&mut self) -> Option<Receiver<IdleState>> {
|
||||
@@ -221,7 +251,10 @@ impl BackendChannels {
|
||||
pub fn br_tx(&self) -> Option<Sender<BrightnessMessage>> {
|
||||
self.br_tx.clone()
|
||||
}
|
||||
pub fn media_tx(&self) -> Option<Sender<MediaCommand>> {
|
||||
pub fn audio_tx(&self) -> Option<Sender<AudioMessage>> {
|
||||
self.audio_tx.clone()
|
||||
}
|
||||
pub fn media_tx(&self) -> Option<Sender<MediaMessage>> {
|
||||
self.media_tx.clone()
|
||||
}
|
||||
pub fn idle_tx(&self) -> Option<Sender<IdleMessage>> {
|
||||
|
||||
@@ -15,12 +15,19 @@ use pulse::volume::{ChannelVolumes, Volume};
|
||||
use pulse::{mainloop, operation};
|
||||
use std::boxed::Box;
|
||||
use std::cell::Cell;
|
||||
use std::collections::HashMap;
|
||||
use std::os::fd::{AsRawFd, RawFd};
|
||||
use std::rc::Rc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
use zbus::export::ordered_stream::OrderedStreamExt;
|
||||
use zbus::fdo::{NameOwnerChanged, PropertiesProxy};
|
||||
use zbus::message::Type;
|
||||
use zbus::names::InterfaceName;
|
||||
use zbus::zvariant::OwnedValue;
|
||||
use zbus::{MatchRule, MessageStream};
|
||||
|
||||
pub struct AudioController {
|
||||
pub struct PulseController {
|
||||
pub mainloop: Mainloop,
|
||||
context: Context,
|
||||
device_index: Option<u32>,
|
||||
@@ -37,7 +44,7 @@ pub enum ControllerMsg {
|
||||
Set(u8, bool),
|
||||
}
|
||||
|
||||
impl AudioController {
|
||||
impl PulseController {
|
||||
pub fn new(tx: Sender<ControllerMsg>) -> Self {
|
||||
let mut mainloop = Mainloop::new().unwrap();
|
||||
let mut prop_list = Proplist::new().unwrap();
|
||||
@@ -95,7 +102,7 @@ impl AudioController {
|
||||
trace!(&LOGGER, "Subscribed to pulseaudio events: {success}");
|
||||
});
|
||||
|
||||
let mut controller = AudioController {
|
||||
let mut controller = PulseController {
|
||||
context,
|
||||
mainloop,
|
||||
device_index: None,
|
||||
@@ -251,147 +258,159 @@ impl AudioController {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum MediaCommand {
|
||||
Init(Sender<MediaCommand>),
|
||||
pub enum AudioMessage {
|
||||
Init(Sender<AudioMessage>),
|
||||
Request,
|
||||
Cmd(u8, bool, bool),
|
||||
}
|
||||
|
||||
pub struct MediaBackend {
|
||||
subscribers: Vec<Sender<MediaCommand>>,
|
||||
command_tx: Sender<MediaCommand>,
|
||||
pub struct AudioBackend {
|
||||
subscribers: Vec<Sender<AudioMessage>>,
|
||||
command_tx: Option<Sender<AudioMessage>>,
|
||||
}
|
||||
|
||||
impl BackendThread<MediaCommand, Sender<MediaCommand>> for MediaBackend {
|
||||
impl AudioBackend {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
subscribers: vec![],
|
||||
command_tx: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BackendThread<AudioMessage, Sender<AudioMessage>> for AudioBackend {
|
||||
fn subscribe(
|
||||
registrator: Registrator<MediaCommand>,
|
||||
) -> (Receiver<MediaCommand>, Sender<MediaCommand>) {
|
||||
let (result_tx, result_rx) = crossbeam_channel::unbounded::<MediaCommand>();
|
||||
registrator: Registrator<AudioMessage>,
|
||||
) -> (Receiver<AudioMessage>, Sender<AudioMessage>) {
|
||||
let (result_tx, result_rx) = crossbeam_channel::unbounded::<AudioMessage>();
|
||||
let _ = registrator.send(result_tx);
|
||||
while let Ok(state) = result_rx.recv() {
|
||||
return match state {
|
||||
MediaCommand::Init(sender) => (result_rx, sender),
|
||||
AudioMessage::Init(sender) => (result_rx, sender),
|
||||
_ => continue,
|
||||
};
|
||||
}
|
||||
panic!("Channel closed unexpectedly, without returning anything")
|
||||
}
|
||||
|
||||
fn subscribers(&self) -> &Vec<Sender<MediaCommand>> {
|
||||
fn subscribers(&self) -> &Vec<Sender<AudioMessage>> {
|
||||
&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();
|
||||
fn init(mut self, registration_rx: Receiver<Sender<AudioMessage>>) {
|
||||
let (command_tx, command_rx) = crossbeam_channel::unbounded::<AudioMessage>();
|
||||
self.command_tx = Some(command_tx);
|
||||
let instance = Arc::new(Mutex::new(self));
|
||||
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 {
|
||||
Request => {
|
||||
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();
|
||||
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 {
|
||||
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: {}",
|
||||
thread::spawn({
|
||||
let instance = instance.clone();
|
||||
let controller_tx = controller_tx.clone();
|
||||
let notify = notify.clone();
|
||||
move || {
|
||||
let mut controller = PulseController::new(controller_tx);
|
||||
let _event = api::Mainloop::new_io_event(
|
||||
&mut controller.mainloop,
|
||||
notify.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 {
|
||||
Request => {
|
||||
instance.lock().expect("Couldn't get lock").publish(
|
||||
AudioMessage::Cmd(
|
||||
controller.current_vol,
|
||||
controller.current_mute
|
||||
);
|
||||
controller.current_mute,
|
||||
controller.device_bt.unwrap_or(false),
|
||||
),
|
||||
);
|
||||
}
|
||||
ControllerMsg::NotifyServer => {
|
||||
controller.query_default_device_blocking();
|
||||
let _ = controller.query_state_blocking();
|
||||
instance.lock().expect("Couldn't get lock").publish(
|
||||
AudioMessage::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 {
|
||||
instance.lock().expect("Couldn't get lock").publish(
|
||||
AudioMessage::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);
|
||||
instance_clone.lock().expect("Couldn't get lock").publish(
|
||||
MediaCommand::Cmd(
|
||||
controller.current_vol,
|
||||
controller.current_mute,
|
||||
controller.device_bt.unwrap_or(false),
|
||||
),
|
||||
);
|
||||
ControllerMsg::Set(volume, mute) => {
|
||||
controller.set_state(volume, mute);
|
||||
instance.lock().expect("Couldn't get lock").publish(
|
||||
AudioMessage::Cmd(
|
||||
controller.current_vol,
|
||||
controller.current_mute,
|
||||
controller.device_bt.unwrap_or(false),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
controller.mainloop.iterate(true);
|
||||
}
|
||||
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
|
||||
thread::spawn({
|
||||
let instance = instance.clone();
|
||||
move || {
|
||||
while let Ok(sender) = registration_rx.recv() {
|
||||
if sender
|
||||
.send(AudioMessage::Init(
|
||||
instance
|
||||
.lock()
|
||||
.expect("Couldn't get lock")
|
||||
.command_tx
|
||||
.clone()
|
||||
.unwrap(),
|
||||
))
|
||||
.is_ok()
|
||||
{
|
||||
let _ = sender.send(AudioMessage::Request);
|
||||
instance
|
||||
.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);
|
||||
.expect("Couldn't get subscribers")
|
||||
.subscribers
|
||||
.push(sender);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
thread::spawn(move || {
|
||||
while let Ok(command) = command_rx.recv() {
|
||||
match command {
|
||||
MediaCommand::Cmd(vol, mute, _) => {
|
||||
AudioMessage::Cmd(vol, mute, _) => {
|
||||
let _ = controller_tx.send(ControllerMsg::Set(vol, mute));
|
||||
notify.write(1).expect("Error writing notify");
|
||||
}
|
||||
MediaCommand::Request => {
|
||||
AudioMessage::Request => {
|
||||
let _ = controller_tx.send(Request);
|
||||
notify.write(1).expect("Error writing notify");
|
||||
}
|
||||
@@ -404,6 +423,418 @@ impl BackendThread<MediaCommand, Sender<MediaCommand>> for MediaBackend {
|
||||
}
|
||||
}
|
||||
|
||||
const MPRIS_PREFIX: &str = "org.mpris.MediaPlayer2";
|
||||
const MPRIS_PATH: &str = "/org/mpris/MediaPlayer2";
|
||||
const MPRIS_PLAYER_INTERFACE: &str = "org.mpris.MediaPlayer2.Player";
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum PlayerStatus {
|
||||
Playing(String),
|
||||
Paused(String),
|
||||
Stopped,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum MediaMessage {
|
||||
Init(Sender<MediaMessage>),
|
||||
PlayerStatus(PlayerStatus),
|
||||
PlayerPlay,
|
||||
PlayerPause,
|
||||
PlayerStop,
|
||||
}
|
||||
|
||||
pub struct MediaBackend {
|
||||
subscribers: Vec<Sender<MediaMessage>>,
|
||||
players: HashMap<String, PlayerStatus>,
|
||||
active_player: Option<String>,
|
||||
}
|
||||
|
||||
impl BackendThread<MediaMessage, Sender<MediaMessage>> for MediaBackend {
|
||||
fn subscribe(
|
||||
registrator: Registrator<MediaMessage>,
|
||||
) -> (Receiver<MediaMessage>, Sender<MediaMessage>) {
|
||||
let (result_tx, result_rx) = crossbeam_channel::unbounded::<MediaMessage>();
|
||||
let _ = registrator.send(result_tx);
|
||||
while let Ok(state) = result_rx.recv() {
|
||||
return match state {
|
||||
MediaMessage::Init(sender) => (result_rx, sender),
|
||||
_ => continue,
|
||||
};
|
||||
}
|
||||
panic!("Channel closed unexpectedly, without returning anything")
|
||||
}
|
||||
|
||||
fn subscribers(&self) -> &Vec<Sender<MediaMessage>> {
|
||||
&self.subscribers
|
||||
}
|
||||
|
||||
fn init(self, registration_rx: Receiver<Sender<MediaMessage>>) {
|
||||
let (tx, rx) = crossbeam_channel::unbounded::<MediaMessage>();
|
||||
let instance = Arc::new(Mutex::new(self));
|
||||
thread::spawn({
|
||||
let instance = instance.clone();
|
||||
move || {
|
||||
while let Ok(sender) = registration_rx.recv() {
|
||||
if sender.send(MediaMessage::Init(tx.clone())).is_ok() {
|
||||
let mut instance_guard = instance.lock().unwrap();
|
||||
if let Some(name) = &instance_guard.active_player
|
||||
&& let Some(status) = instance_guard.players.get(name)
|
||||
{
|
||||
let _ = sender.send(MediaMessage::PlayerStatus(status.clone()));
|
||||
}
|
||||
instance_guard.subscribers.push(sender);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
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 {
|
||||
if let Err(e) = Self::wait_for_players(instance).await {
|
||||
error!(&LOGGER, "Error waiting for players: {e}");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
thread::spawn(move || {
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("Failed to create Tokio runtime");
|
||||
|
||||
runtime.block_on({
|
||||
let instance = instance.clone();
|
||||
async move {
|
||||
let Ok(connection) = zbus::Connection::session().await else {
|
||||
error!(&LOGGER, "Couldn't create D-Bus connection");
|
||||
return;
|
||||
};
|
||||
while let Ok(msg) = rx.recv() {
|
||||
let Some(name) = &instance.lock().unwrap().active_player.clone() else {
|
||||
continue;
|
||||
};
|
||||
let Some(status) = instance.lock().unwrap().players.get(name).cloned()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
match msg {
|
||||
MediaMessage::PlayerPlay => {
|
||||
if let PlayerStatus::Paused(_) = status {
|
||||
MediaBackend::player_play(name, &connection).await;
|
||||
}
|
||||
}
|
||||
MediaMessage::PlayerPause => {
|
||||
if let PlayerStatus::Playing(_) = status {
|
||||
MediaBackend::player_pause(name, &connection).await;
|
||||
instance.lock().unwrap().update_active_player();
|
||||
}
|
||||
}
|
||||
MediaMessage::PlayerStop => {
|
||||
if let PlayerStatus::Playing(_) | PlayerStatus::Paused(_) = status {
|
||||
MediaBackend::player_stop(name, &connection).await;
|
||||
instance.lock().unwrap().select_active_player(None);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl MediaBackend {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
subscribers: vec![],
|
||||
players: HashMap::new(),
|
||||
active_player: None,
|
||||
}
|
||||
}
|
||||
async fn find_existing_players(
|
||||
connection: &zbus::Connection,
|
||||
instance: Arc<Mutex<Self>>,
|
||||
) -> zbus::fdo::Result<()> {
|
||||
let dbus = zbus::Proxy::new(
|
||||
connection,
|
||||
"org.freedesktop.DBus",
|
||||
"/org/freedesktop/DBus",
|
||||
"org.freedesktop.DBus",
|
||||
)
|
||||
.await?;
|
||||
|
||||
let names: Vec<String> = dbus.call("ListNames", &()).await?;
|
||||
let players: Vec<String> = names
|
||||
.into_iter()
|
||||
.filter(|name| name.starts_with(&format!("{MPRIS_PREFIX}.")))
|
||||
.collect();
|
||||
|
||||
for player in players {
|
||||
Self::spawn_new_player_watcher(connection, &instance, &player);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn wait_for_players(instance: Arc<Mutex<Self>>) -> zbus::fdo::Result<()> {
|
||||
let connection = zbus::Connection::session().await?;
|
||||
|
||||
if let Err(e) = Self::find_existing_players(&connection, instance.clone()).await {
|
||||
warn!(&LOGGER, "Error finding existing Media Players: {e}");
|
||||
}
|
||||
|
||||
let rule = MatchRule::builder()
|
||||
.msg_type(Type::Signal)
|
||||
.sender("org.freedesktop.DBus")?
|
||||
.interface("org.freedesktop.DBus")?
|
||||
.member("NameOwnerChanged")?
|
||||
.build();
|
||||
|
||||
let mut stream = MessageStream::for_match_rule(rule, &connection, Some(32)).await?;
|
||||
|
||||
while let Some(Ok(message)) = stream.next().await {
|
||||
let Some(signal) = NameOwnerChanged::from_message(message) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Ok(args) = signal.args() else {
|
||||
continue;
|
||||
};
|
||||
let name = args.name.as_str();
|
||||
|
||||
if !name.starts_with(&format!("{MPRIS_PREFIX}.")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let old_owner = args.old_owner();
|
||||
let new_owner = args.new_owner();
|
||||
|
||||
let map = &mut instance.lock().unwrap().players;
|
||||
if old_owner.is_none() && new_owner.is_some() {
|
||||
Self::spawn_new_player_watcher(&connection, &instance, name);
|
||||
} else if old_owner.is_some() && new_owner.is_none() {
|
||||
map.remove(name);
|
||||
debug!(&LOGGER, "(1) Player removed: {name}");
|
||||
} else {
|
||||
debug!(&LOGGER, "Player owner changed: {name}");
|
||||
}
|
||||
}
|
||||
warn!(&LOGGER, "Player Finder ended prematurely");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn spawn_new_player_watcher(
|
||||
connection: &zbus::Connection,
|
||||
instance: &Arc<Mutex<Self>>,
|
||||
name: &str,
|
||||
) {
|
||||
tokio::spawn({
|
||||
let instance = instance.clone();
|
||||
let connection = connection.clone();
|
||||
let name = name.to_string();
|
||||
async move {
|
||||
let _ = Self::watch_player_status(&connection, instance, &name).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn watch_player_status(
|
||||
connection: &zbus::Connection,
|
||||
instance: Arc<Mutex<Self>>,
|
||||
name: &str,
|
||||
) -> zbus::fdo::Result<()> {
|
||||
let props = PropertiesProxy::builder(connection)
|
||||
.destination(name)?
|
||||
.path(MPRIS_PATH)?
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let mut changes = props.receive_properties_changed().await?;
|
||||
|
||||
let try_string: Result<String, _> = props
|
||||
.get(
|
||||
InterfaceName::from_static_str_unchecked(MPRIS_PLAYER_INTERFACE),
|
||||
"PlaybackStatus",
|
||||
)
|
||||
.await?
|
||||
.try_into();
|
||||
|
||||
let initial: String = if let Ok(s) = try_string {
|
||||
s
|
||||
} else {
|
||||
String::from("Stopped")
|
||||
};
|
||||
|
||||
debug!(&LOGGER, "Player added: {name} {initial}");
|
||||
let _ = Self::insert_or_update_player(&instance, &props, name.into(), initial).await;
|
||||
|
||||
while let Some(change) = changes.next().await {
|
||||
let args = change.args()?;
|
||||
|
||||
if !args.interface_name().eq(MPRIS_PLAYER_INTERFACE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(value) = args.changed_properties().get("PlaybackStatus")
|
||||
&& let Ok(status) = <String>::try_from(value.clone())
|
||||
{
|
||||
debug!(&LOGGER, "Player ({name}) status has changed to: '{status}'");
|
||||
Self::insert_or_update_player(&instance, &props, name.into(), status).await;
|
||||
}
|
||||
}
|
||||
debug!(&LOGGER, "(2) Removing player: {name}");
|
||||
instance.lock().unwrap().players.remove(name);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn insert_or_update_player(
|
||||
instance: &Arc<Mutex<Self>>,
|
||||
props: &PropertiesProxy<'_>,
|
||||
player_name: String,
|
||||
player_status: String,
|
||||
) {
|
||||
match Self::get_player_status_from_string(props, player_status).await {
|
||||
Ok(status) => {
|
||||
let mut instance_guard = instance.lock().unwrap();
|
||||
instance_guard.players.insert(player_name.clone(), status);
|
||||
instance_guard.select_active_player(Some(player_name));
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(&LOGGER, "Error parsing player status: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_title(props: &PropertiesProxy<'_>) -> zbus::fdo::Result<String> {
|
||||
let metadata = props
|
||||
.get(
|
||||
InterfaceName::from_static_str_unchecked(MPRIS_PLAYER_INTERFACE),
|
||||
"Metadata",
|
||||
)
|
||||
.await?;
|
||||
let metadata = <HashMap<String, OwnedValue>>::try_from(metadata);
|
||||
match metadata {
|
||||
Ok(metadata) => {
|
||||
if let Some(title) = metadata.get("xesam:title") {
|
||||
<String>::try_from(title.clone())
|
||||
.map_err(|e| zbus::fdo::Error::Failed(e.to_string()))
|
||||
} else {
|
||||
Err(zbus::fdo::Error::Failed("Couldn't parse title".into()))
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(&LOGGER, "Error parsing metadata {}", err);
|
||||
Err(zbus::fdo::Error::Failed("Couldn't parse metadata".into()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_player_status_from_string(
|
||||
props: &PropertiesProxy<'_>,
|
||||
status: String,
|
||||
) -> zbus::fdo::Result<PlayerStatus> {
|
||||
match status.as_str() {
|
||||
"Playing" => match Self::get_title(props).await {
|
||||
Ok(title) => Ok(PlayerStatus::Playing(title)),
|
||||
Err(e) => Err(e),
|
||||
},
|
||||
"Paused" => match Self::get_title(props).await {
|
||||
Ok(title) => Ok(PlayerStatus::Paused(title)),
|
||||
Err(e) => Err(e),
|
||||
},
|
||||
"Stopped" => Ok(PlayerStatus::Stopped),
|
||||
other => Err(zbus::fdo::Error::Failed(format!(
|
||||
"Invalid status string: '{other}' {:?}",
|
||||
other.as_bytes()
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
async fn player_play(name: &str, connection: &zbus::Connection) {
|
||||
if let Ok(proxy) =
|
||||
zbus::Proxy::new(connection, name, MPRIS_PATH, MPRIS_PLAYER_INTERFACE).await
|
||||
&& let Err(e) = proxy.call_method("Play", &()).await
|
||||
{
|
||||
warn!(&LOGGER, "Error starting play for player: {name}, {e}");
|
||||
}
|
||||
}
|
||||
|
||||
async fn player_pause(name: &str, connection: &zbus::Connection) {
|
||||
if let Ok(proxy) =
|
||||
zbus::Proxy::new(connection, name, MPRIS_PATH, MPRIS_PLAYER_INTERFACE).await
|
||||
&& let Err(e) = proxy.call_method("Pause", &()).await
|
||||
{
|
||||
warn!(&LOGGER, "Error pausing player: {name}, {e}");
|
||||
}
|
||||
}
|
||||
|
||||
async fn player_stop(name: &str, connection: &zbus::Connection) {
|
||||
if let Ok(proxy) =
|
||||
zbus::Proxy::new(connection, name, MPRIS_PATH, MPRIS_PLAYER_INTERFACE).await
|
||||
&& let Err(e) = proxy.call_method("Stop", &()).await
|
||||
{
|
||||
warn!(&LOGGER, "Error stopping player: {name}, {e}");
|
||||
}
|
||||
}
|
||||
|
||||
fn update_active_player(&mut self) {
|
||||
self.select_active_player(self.active_player.clone());
|
||||
}
|
||||
|
||||
fn select_active_player(&mut self, changed_player: Option<String>) {
|
||||
let mut changed_also_playing = false;
|
||||
let mut new_active: Option<String> = None;
|
||||
let mut send_status = PlayerStatus::Stopped;
|
||||
if let Some(player) = changed_player
|
||||
&& let Some(status) = self.players.get(&player)
|
||||
{
|
||||
match status {
|
||||
PlayerStatus::Playing(_) => {
|
||||
new_active = Some(player);
|
||||
send_status = status.clone();
|
||||
changed_also_playing = true;
|
||||
}
|
||||
PlayerStatus::Paused(_) => {
|
||||
new_active = Some(player);
|
||||
send_status = status.clone();
|
||||
}
|
||||
PlayerStatus::Stopped => {} // Stopped players shouldn't be active
|
||||
}
|
||||
}
|
||||
|
||||
if !changed_also_playing {
|
||||
for (name, status) in &self.players {
|
||||
match status {
|
||||
PlayerStatus::Playing(_) => {
|
||||
// Found a player that is playing media -> prioritize this one
|
||||
new_active = Some(name.clone());
|
||||
send_status = status.clone();
|
||||
break;
|
||||
}
|
||||
PlayerStatus::Paused(_) => {
|
||||
if new_active.is_none() {
|
||||
new_active = Some(name.clone());
|
||||
send_status = status.clone();
|
||||
}
|
||||
}
|
||||
PlayerStatus::Stopped => {} // Stopped players shouldn't be active
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.active_player = new_active;
|
||||
self.publish(MediaMessage::PlayerStatus(send_status));
|
||||
}
|
||||
}
|
||||
|
||||
fn drain_eventfd(fd: RawFd) -> std::io::Result<u64> {
|
||||
let mut value: u64 = 0;
|
||||
|
||||
|
||||
@@ -143,12 +143,9 @@ impl BackendThread<BrightnessMessage, Sender<BrightnessMessage>> for BrightnessM
|
||||
&self.subscribers
|
||||
}
|
||||
|
||||
fn init(registration_rx: Receiver<Sender<BrightnessMessage>>) {
|
||||
fn init(self, 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 = Arc::new(Mutex::new(self));
|
||||
let instance_clone = instance.clone();
|
||||
thread::spawn(move || {
|
||||
while let Ok(tx) = registration_rx.recv() {
|
||||
@@ -246,6 +243,12 @@ enum Operation {
|
||||
}
|
||||
|
||||
impl BrightnessManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
subscribers: vec![],
|
||||
display_cache: HashMap::new(),
|
||||
}
|
||||
}
|
||||
pub fn set_brightness(&mut self, connector: String, percent: u8) {
|
||||
if !self.display_cache.contains_key(&connector) {
|
||||
self.register_display(connector.clone());
|
||||
|
||||
@@ -1,26 +1,33 @@
|
||||
use crate::LOGGER;
|
||||
use crate::backend::{BackendThread, SimpleBackendThread};
|
||||
use crate::ctl::{ControlCommand, SOCKET_PATH};
|
||||
use crossbeam_channel::{Receiver, Sender};
|
||||
use jmb_bar::{ControlCommand, SOCKET_PATH};
|
||||
use jmb_logger_rs::*;
|
||||
use std::io::Read;
|
||||
use std::net::Shutdown;
|
||||
use std::os::unix::net::UnixListener;
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
pub struct ControlSocket {
|
||||
subscribers: Vec<Sender<ControlCommand>>,
|
||||
}
|
||||
|
||||
impl ControlSocket {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
subscribers: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SimpleBackendThread<ControlCommand> for ControlSocket {
|
||||
fn subscribers(&self) -> &Vec<Sender<ControlCommand>> {
|
||||
&self.subscribers
|
||||
}
|
||||
|
||||
fn init(registration_rx: Receiver<Sender<ControlCommand>>) {
|
||||
let instance = Arc::new(Mutex::new(Self {
|
||||
subscribers: Vec::new(),
|
||||
}));
|
||||
fn init(self, registration_rx: Receiver<Sender<ControlCommand>>) {
|
||||
let instance = Arc::new(Mutex::new(self));
|
||||
let instance_clone = instance.clone();
|
||||
std::thread::spawn(move || {
|
||||
while let Ok(sender) = registration_rx.recv() {
|
||||
@@ -28,11 +35,11 @@ impl SimpleBackendThread<ControlCommand> for ControlSocket {
|
||||
}
|
||||
});
|
||||
std::thread::spawn(move || {
|
||||
if std::fs::metadata(SOCKET_PATH).is_ok() {
|
||||
if let Err(e) = std::fs::remove_file(SOCKET_PATH) {
|
||||
error!(&LOGGER, "Error deleting old socket file: {e}");
|
||||
return;
|
||||
}
|
||||
if std::fs::metadata(SOCKET_PATH).is_ok()
|
||||
&& let Err(e) = std::fs::remove_file(SOCKET_PATH)
|
||||
{
|
||||
error!(&LOGGER, "Error deleting old socket file: {e}");
|
||||
return;
|
||||
}
|
||||
let socket = UnixListener::bind(SOCKET_PATH);
|
||||
let Ok(socket) = socket else {
|
||||
@@ -52,7 +59,7 @@ impl SimpleBackendThread<ControlCommand> for ControlSocket {
|
||||
let _ = stream.shutdown(Shutdown::Both);
|
||||
continue;
|
||||
}
|
||||
if let Some(command) = ControlCommand::from_str(&read) {
|
||||
if let Ok(command) = ControlCommand::from_str(&read) {
|
||||
instance.lock().unwrap().publish(command);
|
||||
}
|
||||
}
|
||||
|
||||
194
src/backend/gpu_utilization.rs
Normal file
194
src/backend/gpu_utilization.rs
Normal file
@@ -0,0 +1,194 @@
|
||||
use std::collections::HashMap;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
|
||||
use std::{fs, io, mem};
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Default)]
|
||||
struct PerfEventAttr {
|
||||
type_: u32,
|
||||
size: u32,
|
||||
config: u64,
|
||||
sample_period: u64,
|
||||
sample_type: u64,
|
||||
read_format: u64,
|
||||
flags: u64,
|
||||
wakeup_events: u32,
|
||||
bp_type: u32,
|
||||
config1: u64,
|
||||
config2: u64,
|
||||
branch_sample_type: u64,
|
||||
sample_regs_user: u64,
|
||||
sample_stack_user: u32,
|
||||
clockid: i32,
|
||||
sample_regs_intr: u64,
|
||||
aux_watermark: u32,
|
||||
sample_max_stack: u16,
|
||||
reserved2: u16,
|
||||
aux_sample_size: u32,
|
||||
reserved3: u32,
|
||||
sig_data: u64,
|
||||
}
|
||||
|
||||
const PERF_FORMAT_TOTAL_TIME_ENABLED: u64 = 1;
|
||||
const PERF_FORMAT_TOTAL_TIME_RUNNING: u64 = 2;
|
||||
|
||||
#[repr(C)]
|
||||
struct ReadFormat {
|
||||
value: u64,
|
||||
time_enabled: u64,
|
||||
time_running: u64,
|
||||
}
|
||||
|
||||
impl ReadFormat {
|
||||
fn calculate_utilization(&self, previous: &ReadFormat) -> f64 {
|
||||
let busy_delta = self.value.saturating_sub(previous.value);
|
||||
let elapsed_delta = self.time_enabled.saturating_sub(previous.time_enabled);
|
||||
|
||||
if elapsed_delta == 0 {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
(busy_delta as f64 / elapsed_delta as f64 * 100.0).clamp(0.0, 100.0)
|
||||
}
|
||||
}
|
||||
|
||||
fn read_number(path: &str) -> io::Result<u64> {
|
||||
fs::read_to_string(path)?
|
||||
.trim()
|
||||
.parse()
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
|
||||
}
|
||||
|
||||
fn read_config(path: &str) -> io::Result<u64> {
|
||||
let contents = fs::read_to_string(path)?;
|
||||
let value = contents
|
||||
.trim()
|
||||
.strip_prefix("config=")
|
||||
.unwrap_or(contents.trim());
|
||||
|
||||
u64::from_str_radix(value.trim_start_matches("0x"), 16)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
|
||||
}
|
||||
|
||||
struct I915Engine {
|
||||
name: String,
|
||||
fd: OwnedFd,
|
||||
}
|
||||
|
||||
impl Hash for I915Engine {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.name.hash(state)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<Self> for I915Engine {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.name.eq(&other.name)
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for I915Engine {}
|
||||
|
||||
impl I915Engine {
|
||||
fn open(name: &str) -> io::Result<Self> {
|
||||
let base = "/sys/bus/event_source/devices/i915";
|
||||
|
||||
let pmu_type = read_number(&format!("{base}/type"))? as u32;
|
||||
let config = read_config(&format!("{base}/events/{name}"))?;
|
||||
|
||||
let attr = PerfEventAttr {
|
||||
type_: pmu_type,
|
||||
size: size_of::<PerfEventAttr>() as u32,
|
||||
config,
|
||||
read_format: PERF_FORMAT_TOTAL_TIME_ENABLED | PERF_FORMAT_TOTAL_TIME_RUNNING,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// pid = -1 and cpu = -1: system-wide event, if permitted.
|
||||
let fd = unsafe {
|
||||
libc::syscall(
|
||||
libc::SYS_perf_event_open,
|
||||
&attr as *const PerfEventAttr,
|
||||
-1i32, // all processes
|
||||
0i32, // CPU 0
|
||||
-1i32, // no event group
|
||||
0u64,
|
||||
) as i32
|
||||
};
|
||||
|
||||
if fd < 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
|
||||
let fd = unsafe { OwnedFd::from_raw_fd(fd) };
|
||||
|
||||
Ok(Self {
|
||||
name: name.to_owned(),
|
||||
fd,
|
||||
})
|
||||
}
|
||||
|
||||
fn read(&self) -> io::Result<ReadFormat> {
|
||||
let mut result = ReadFormat {
|
||||
value: 0,
|
||||
time_enabled: 0,
|
||||
time_running: 0,
|
||||
};
|
||||
|
||||
let size = mem::size_of::<ReadFormat>();
|
||||
let read = unsafe {
|
||||
libc::read(
|
||||
self.fd.as_raw_fd(),
|
||||
&mut result as *mut _ as *mut libc::c_void,
|
||||
size,
|
||||
)
|
||||
};
|
||||
|
||||
if read != size as isize {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct I915UtilizationMonitor {
|
||||
engines: HashMap<I915Engine, Option<ReadFormat>>,
|
||||
}
|
||||
|
||||
impl I915UtilizationMonitor {
|
||||
pub fn new() -> Option<Self> {
|
||||
let mut engines: HashMap<_, Option<ReadFormat>> = HashMap::new();
|
||||
let Ok(dir) = fs::read_dir("/sys/bus/event_source/devices/i915/events") else {
|
||||
return None;
|
||||
};
|
||||
for entry in dir {
|
||||
let Ok(entry) = entry else { continue };
|
||||
let Ok(name) = String::from_utf8(entry.file_name().into_encoded_bytes()) else {
|
||||
continue;
|
||||
};
|
||||
if name.ends_with("-busy") {
|
||||
let Ok(engine) = I915Engine::open(&name) else {
|
||||
continue;
|
||||
};
|
||||
engines.insert(engine, None);
|
||||
}
|
||||
}
|
||||
Some(Self { engines })
|
||||
}
|
||||
|
||||
pub fn calc_utilization(&mut self) -> u8 {
|
||||
let mut values: Vec<u8> = Vec::new();
|
||||
for (engine, sample) in &mut self.engines {
|
||||
let new_sample = engine.read().unwrap();
|
||||
if let Some(last) = sample {
|
||||
values.push(new_sample.calculate_utilization(last) as u8);
|
||||
}
|
||||
let _ = sample.insert(new_sample);
|
||||
}
|
||||
values
|
||||
.iter()
|
||||
.fold(0u8, |arg0: u8, other: &u8| arg0.max(*other))
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::backend::{BackendThread, Registrator};
|
||||
use crate::config::Config;
|
||||
use crate::{APP_NAME, LOGGER};
|
||||
use crossbeam_channel::{Receiver, Sender};
|
||||
use jmb_bar::config::IdleConfig;
|
||||
use jmb_logger_rs::*;
|
||||
use std::collections::HashMap;
|
||||
use std::error::Error;
|
||||
@@ -100,6 +100,7 @@ pub enum IdleMessage {
|
||||
#[derive(Debug)]
|
||||
pub struct IdleDaemon {
|
||||
subscribers: Vec<Sender<IdleState>>,
|
||||
config: IdleConfig,
|
||||
}
|
||||
|
||||
impl BackendThread<IdleState, Sender<IdleMessage>> for IdleDaemon {
|
||||
@@ -121,12 +122,9 @@ impl BackendThread<IdleState, Sender<IdleMessage>> for IdleDaemon {
|
||||
&self.subscribers
|
||||
}
|
||||
|
||||
fn init(registration_rx: Receiver<Sender<IdleState>>) {
|
||||
let config = Config::load();
|
||||
fn init(self, registration_rx: Receiver<Sender<IdleState>>) {
|
||||
let (tx, rx) = crossbeam_channel::unbounded::<IdleMessage>();
|
||||
let instance = Arc::new(Mutex::new(Self {
|
||||
subscribers: Vec::new(),
|
||||
}));
|
||||
let instance = Arc::new(Mutex::new(self));
|
||||
std::thread::spawn({
|
||||
let instance = instance.clone();
|
||||
let tx = tx.clone();
|
||||
@@ -146,9 +144,9 @@ impl BackendThread<IdleState, Sender<IdleMessage>> for IdleDaemon {
|
||||
let (wl_tx, wl_rx) = watch::channel(false);
|
||||
let (dbus_tx, dbus_rx) = watch::channel(false);
|
||||
std::thread::spawn({
|
||||
let config = config.clone();
|
||||
let instance = instance.clone();
|
||||
move || {
|
||||
if let Err(err) = Self::start_idle_notifier(wl_tx, &config) {
|
||||
if let Err(err) = Self::start_idle_notifier(wl_tx, instance) {
|
||||
error!(&LOGGER, "Idle Daemon unexpectedly quit, reason: {err}");
|
||||
}
|
||||
}
|
||||
@@ -202,7 +200,7 @@ impl BackendThread<IdleState, Sender<IdleMessage>> for IdleDaemon {
|
||||
let res = IdleDaemon::start_dbus(dbus_tx, instance_clone).await;
|
||||
match res {
|
||||
Ok((_, _)) => {
|
||||
Self::run_idle_timer(instance, wl_rx, dbus_rx, &config).await;
|
||||
Self::run_idle_timer(instance, wl_rx, dbus_rx).await;
|
||||
}
|
||||
Err(err) => error!(&LOGGER, "Error running D-Bus Screensaver: {err}"),
|
||||
}
|
||||
@@ -228,7 +226,7 @@ impl IdleStage {
|
||||
}
|
||||
}
|
||||
|
||||
fn timeout(self, cfg: &Config) -> Duration {
|
||||
fn timeout(self, cfg: &IdleConfig) -> Duration {
|
||||
match self {
|
||||
Self::Dim => Duration::from_secs(cfg.dim_timeout_delta()),
|
||||
Self::TurnOff => Duration::from_secs(cfg.lock_timeout_delta()),
|
||||
@@ -238,11 +236,16 @@ impl IdleStage {
|
||||
}
|
||||
|
||||
impl IdleDaemon {
|
||||
pub fn new(config: IdleConfig) -> Self {
|
||||
Self {
|
||||
subscribers: vec![],
|
||||
config,
|
||||
}
|
||||
}
|
||||
async fn run_idle_timer(
|
||||
instance: Arc<Mutex<Self>>,
|
||||
mut wl_rx: watch::Receiver<bool>,
|
||||
mut dbus_rx: watch::Receiver<bool>,
|
||||
config: &Config,
|
||||
) {
|
||||
loop {
|
||||
if let Err(e) = wl_rx.changed().await {
|
||||
@@ -254,12 +257,13 @@ impl IdleDaemon {
|
||||
if wl_idle && !dbus_state {
|
||||
let mut stage = IdleStage::Dim;
|
||||
loop {
|
||||
let timeout = stage.timeout(&instance.lock().unwrap().config);
|
||||
tokio::select! {
|
||||
_ = sleep(stage.timeout(config)) => {
|
||||
_ = sleep(timeout) => {
|
||||
match stage {
|
||||
IdleStage::Dim => instance.lock().unwrap().publish(IdleState::Dimmed),
|
||||
IdleStage::TurnOff => _ = Command::new("hyprlock").spawn(),
|
||||
IdleStage::Sleep => _ = Command::new("systemctl").arg("sleep").spawn(),
|
||||
IdleStage::TurnOff => _ = Command::new("bash").arg("-c").arg(format!("'{}'", instance.lock().unwrap().config.lock_command())).spawn(),
|
||||
IdleStage::Sleep => _ = Command::new("bash").arg("-c").arg(format!("'{}'", instance.lock().unwrap().config.sleep_command())).spawn(),
|
||||
}
|
||||
stage = stage.next();
|
||||
}
|
||||
@@ -286,7 +290,10 @@ impl IdleDaemon {
|
||||
}
|
||||
}
|
||||
|
||||
fn start_idle_notifier(tx: watch::Sender<bool>, config: &Config) -> Result<(), Box<dyn Error>> {
|
||||
fn start_idle_notifier(
|
||||
tx: watch::Sender<bool>,
|
||||
instance: Arc<Mutex<IdleDaemon>>,
|
||||
) -> 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();
|
||||
@@ -298,7 +305,7 @@ impl IdleDaemon {
|
||||
let mut state = WlState { idle: None, tx };
|
||||
|
||||
let _notification = notifier.get_idle_notification(
|
||||
(config.idle_timeout() * 1000) as u32,
|
||||
(instance.lock().unwrap().config.idle_timeout() * 1000) as u32,
|
||||
&seat,
|
||||
&queue_handle,
|
||||
(),
|
||||
|
||||
@@ -37,6 +37,13 @@ impl State {
|
||||
}
|
||||
|
||||
impl JsonSocketBackend {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
subscribers: vec![],
|
||||
state: initial_state_tailscale(),
|
||||
socket_address: String::from("/var/run/netbird-http.sock"),
|
||||
}
|
||||
}
|
||||
async fn _watch(instance: Arc<Mutex<Self>>) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
let socket_address = instance
|
||||
.lock()
|
||||
@@ -197,14 +204,8 @@ impl BackendThread<State, ()> for JsonSocketBackend {
|
||||
&self.subscribers
|
||||
}
|
||||
|
||||
fn init(registration_rx: Receiver<Sender<State>>) {
|
||||
let state = initial_state_tailscale();
|
||||
|
||||
let instance = Arc::new(Mutex::new(Self {
|
||||
subscribers: vec![],
|
||||
state,
|
||||
socket_address: String::from("/var/run/netbird-http.sock"),
|
||||
}));
|
||||
fn init(self, registration_rx: Receiver<Sender<State>>) {
|
||||
let instance = Arc::new(Mutex::new(self));
|
||||
|
||||
let instance_clone = instance.clone();
|
||||
std::thread::spawn(move || {
|
||||
|
||||
@@ -10,7 +10,9 @@ use nmrs::raw::zvariant::OwnedObjectPath;
|
||||
use nmrs::{ActiveConnection, ConnectionError, NetworkEvent, NetworkManager, WifiSecurity};
|
||||
use std::cmp::Ordering;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
use tokio::time::{Instant, Sleep};
|
||||
|
||||
@@ -109,10 +111,10 @@ pub enum NetworkState {
|
||||
ConnectSuccess(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NetworkBackendHandle {
|
||||
#[derive(Debug)]
|
||||
pub struct NetworkBackend {
|
||||
subscribers: Vec<Sender<NetworkState>>,
|
||||
command_tx: UnboundedSender<NetworkCommand>,
|
||||
command_tx: Option<UnboundedSender<NetworkCommand>>,
|
||||
}
|
||||
|
||||
struct EventDebouncer {
|
||||
@@ -121,7 +123,7 @@ struct EventDebouncer {
|
||||
sleep: Pin<Box<Sleep>>,
|
||||
}
|
||||
|
||||
impl BackendThread<NetworkState, UnboundedSender<NetworkCommand>> for NetworkBackendHandle {
|
||||
impl BackendThread<NetworkState, UnboundedSender<NetworkCommand>> for NetworkBackend {
|
||||
fn subscribe(
|
||||
registrator: Sender<Sender<NetworkState>>,
|
||||
) -> (Receiver<NetworkState>, UnboundedSender<NetworkCommand>) {
|
||||
@@ -140,12 +142,10 @@ impl BackendThread<NetworkState, UnboundedSender<NetworkCommand>> for NetworkBac
|
||||
&self.subscribers
|
||||
}
|
||||
|
||||
fn init(registration_rx: Receiver<Sender<NetworkState>>) {
|
||||
fn init(mut self, 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,
|
||||
};
|
||||
self.command_tx = Some(command_tx);
|
||||
let instance = Arc::new(Mutex::new(self));
|
||||
std::thread::spawn(move || {
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
@@ -162,8 +162,8 @@ impl BackendThread<NetworkState, UnboundedSender<NetworkCommand>> for NetworkBac
|
||||
};
|
||||
|
||||
std::thread::spawn({
|
||||
let instance = instance.clone();
|
||||
let nm = nm.clone();
|
||||
let mut instance = instance.clone();
|
||||
move || {
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
@@ -172,10 +172,12 @@ impl BackendThread<NetworkState, UnboundedSender<NetworkCommand>> for NetworkBac
|
||||
|
||||
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 instance_guard = instance.lock().await;
|
||||
if tx.send(NetworkState::Init(instance_guard.command_tx.clone().unwrap())).is_ok() {
|
||||
instance_guard.subscribers.push(tx);
|
||||
println!("Added subscriber: {:?}", instance_guard.subscribers);
|
||||
let _ = instance_guard.send_all_networks(&nm).await;
|
||||
let _ = instance_guard.send_active_connection(&nm).await;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -205,15 +207,16 @@ impl BackendThread<NetworkState, UnboundedSender<NetworkCommand>> for NetworkBac
|
||||
// Run the command without stopping event processing
|
||||
// while a potentially slow connection operation runs.
|
||||
let nm = nm.clone();
|
||||
let instance_clone = instance.clone();
|
||||
let instance = instance.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
tokio::spawn(
|
||||
async move {
|
||||
match command {
|
||||
NetworkCommand::ToggleVpn(profile) => instance_clone.toggle_vpn(profile, &nm).await,
|
||||
NetworkCommand::ToggleVpn(profile) => instance.lock().await.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;
|
||||
instance.lock().await.connect(&nm, ssid, WifiSecurity::Open).await;
|
||||
} else {
|
||||
match auth {
|
||||
Auth::Open => {
|
||||
@@ -221,19 +224,18 @@ impl BackendThread<NetworkState, UnboundedSender<NetworkCommand>> for NetworkBac
|
||||
&& let Some(ap) = networks.iter().find(|net| net.ssid.eq(&ssid))
|
||||
{
|
||||
if ap.security.is_open() {
|
||||
instance_clone.connect(&nm, ssid, WifiSecurity::Open).await;
|
||||
instance.lock().await.connect(&nm, ssid, WifiSecurity::Open).await;
|
||||
} else {
|
||||
instance_clone.publish(NetworkState::Auth(ssid, AuthType::Password));
|
||||
instance.lock().await.publish(NetworkState::Auth(ssid, AuthType::Password));
|
||||
}
|
||||
}
|
||||
}
|
||||
Auth::Password(password) => instance_clone.connect(&nm, ssid, WifiSecurity::WpaPsk { psk: password }).await,
|
||||
Auth::Password(password) => instance.lock().await.connect(&nm, ssid, WifiSecurity::WpaPsk { psk: password }).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let _ = nm;
|
||||
});
|
||||
}
|
||||
@@ -241,7 +243,7 @@ impl BackendThread<NetworkState, UnboundedSender<NetworkCommand>> for NetworkBac
|
||||
match event {
|
||||
Some(event_res) => {
|
||||
match event_res {
|
||||
Ok(event) => instance.handle_event(&nm, event, &mut debouncer).await,
|
||||
Ok(event) => instance.lock().await.handle_event(&nm, event, &mut debouncer).await,
|
||||
Err(err) => error!(&LOGGER, "Error: {err:?}"),
|
||||
}
|
||||
}
|
||||
@@ -250,7 +252,7 @@ impl BackendThread<NetworkState, UnboundedSender<NetworkCommand>> for NetworkBac
|
||||
}
|
||||
_ = &mut debouncer.sleep, if debouncer.deadline.is_some() => {
|
||||
debouncer.deadline = None;
|
||||
let _ = instance.send_all_networks(&nm).await;
|
||||
let _ = instance.lock().await.send_all_networks(&nm).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -259,7 +261,13 @@ impl BackendThread<NetworkState, UnboundedSender<NetworkCommand>> for NetworkBac
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkBackendHandle {
|
||||
impl NetworkBackend {
|
||||
pub fn new() -> NetworkBackend {
|
||||
Self {
|
||||
subscribers: vec![],
|
||||
command_tx: None,
|
||||
}
|
||||
}
|
||||
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;
|
||||
@@ -431,44 +439,58 @@ impl NetworkBackendHandle {
|
||||
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,
|
||||
})
|
||||
let Some(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,
|
||||
})
|
||||
.cloned()
|
||||
} else {
|
||||
connections.first()
|
||||
};
|
||||
match connection {
|
||||
Some(conn) => {
|
||||
let mut iter = connections.into_iter();
|
||||
loop {
|
||||
let Some(conn) = iter.next() else {
|
||||
break None;
|
||||
};
|
||||
match conn {
|
||||
ActiveConnection::Wired(c) => {
|
||||
let info = NetworkInfo {
|
||||
name: c.id.clone(),
|
||||
network_type: NetworkType::Wired,
|
||||
signal: 0,
|
||||
secured: None,
|
||||
known: true,
|
||||
};
|
||||
self.publish(NetworkState::ChangeNetwork(Some(info)));
|
||||
ActiveConnection::Other(_) | ActiveConnection::Vpn(_) => continue,
|
||||
other => {
|
||||
break Some(other);
|
||||
}
|
||||
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,
|
||||
};
|
||||
self.publish(NetworkState::ChangeNetwork(Some(info)));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
self.publish(NetworkState::ChangeNetwork(None));
|
||||
}) else {
|
||||
debug!(&LOGGER, "Disconnected from all Networks");
|
||||
self.publish(NetworkState::ChangeNetwork(None));
|
||||
return;
|
||||
};
|
||||
debug!(&LOGGER, "Found active connection: {connection:#?}");
|
||||
match connection {
|
||||
ActiveConnection::Wired(c) => {
|
||||
let info = NetworkInfo {
|
||||
name: c.id.clone(),
|
||||
network_type: NetworkType::Wired,
|
||||
signal: 0,
|
||||
secured: None,
|
||||
known: true,
|
||||
};
|
||||
debug!(&LOGGER, "Connected to wired network");
|
||||
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,
|
||||
};
|
||||
debug!(&LOGGER, "Connected to wireless network");
|
||||
self.publish(NetworkState::ChangeNetwork(Some(info)));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::LOGGER;
|
||||
use crate::backend::gpu_utilization::I915UtilizationMonitor;
|
||||
use crate::backend::{BackendThread, Registrator};
|
||||
use all_smi::AllSmi;
|
||||
use battery::State;
|
||||
use battery::units::Time;
|
||||
use battery::units::power::watt;
|
||||
@@ -8,6 +8,7 @@ use battery::units::ratio::percent;
|
||||
use battery::units::time::millisecond;
|
||||
use crossbeam_channel::{Receiver, Sender};
|
||||
use jmb_logger_rs::*;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use sysinfo::{Disks, System};
|
||||
@@ -19,7 +20,7 @@ pub struct Stats {
|
||||
pub cpu: Option<u8>,
|
||||
pub ram: Option<u8>,
|
||||
pub disks: Vec<(String, u8)>,
|
||||
pub gpus: Vec<(String, u8)>,
|
||||
pub gpu: Option<u8>,
|
||||
pub battery_charge: u8,
|
||||
pub battery_state: State,
|
||||
pub battery_draw: f32,
|
||||
@@ -28,8 +29,14 @@ pub struct Stats {
|
||||
|
||||
pub struct StatsBackend {
|
||||
subscribers: Vec<Sender<Stats>>,
|
||||
system: Option<System>,
|
||||
disks: Option<Disks>,
|
||||
}
|
||||
|
||||
impl StatsBackend {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
subscribers: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BackendThread<Stats, ()> for StatsBackend {
|
||||
@@ -43,24 +50,26 @@ impl BackendThread<Stats, ()> for StatsBackend {
|
||||
&self.subscribers
|
||||
}
|
||||
|
||||
fn init(registration_rx: Receiver<Sender<Stats>>) {
|
||||
thread::spawn(move || {
|
||||
let mut instance = Self {
|
||||
subscribers: Vec::new(),
|
||||
system: System::new().ok(),
|
||||
disks: Disks::new().ok(),
|
||||
};
|
||||
let manager = battery::Manager::new();
|
||||
let smi = AllSmi::new();
|
||||
loop {
|
||||
while let Ok(tx) = registration_rx.try_recv() {
|
||||
fn init(self, registration_rx: Receiver<Sender<Stats>>) {
|
||||
let instance = Arc::new(Mutex::new(self));
|
||||
thread::spawn({
|
||||
let instance = instance.clone();
|
||||
move || {
|
||||
while let Ok(tx) = registration_rx.recv() {
|
||||
trace!(&LOGGER, "Added Stats Subscriber: {tx:?}");
|
||||
instance.subscribers.push(tx);
|
||||
instance.lock().unwrap().subscribers.push(tx);
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
thread::spawn(move || {
|
||||
let manager = battery::Manager::new();
|
||||
let mut system = System::new().ok();
|
||||
let mut disks = Disks::new().ok();
|
||||
let mut gpu_monitor = I915UtilizationMonitor::new();
|
||||
loop {
|
||||
let mut cpu = None;
|
||||
let mut ram = None;
|
||||
if let Some(system) = instance.system.as_mut() {
|
||||
if let Some(system) = system.as_mut() {
|
||||
system.refresh_cpu_usage();
|
||||
system.refresh_memory();
|
||||
|
||||
@@ -69,7 +78,7 @@ impl BackendThread<Stats, ()> for StatsBackend {
|
||||
}
|
||||
|
||||
let mut disk_stats = Vec::new();
|
||||
if let Some(disks) = instance.disks.as_mut() {
|
||||
if let Some(disks) = disks.as_mut() {
|
||||
disks.refresh(true);
|
||||
disk_stats = disks
|
||||
.iter()
|
||||
@@ -82,14 +91,7 @@ impl BackendThread<Stats, ()> for StatsBackend {
|
||||
.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 gpu = gpu_monitor.as_mut().map(|v| v.calc_utilization());
|
||||
|
||||
let mut battery_charge: u8 = 255; // Use > 100 as invalid value
|
||||
let mut battery_state = State::Unknown;
|
||||
@@ -128,13 +130,13 @@ impl BackendThread<Stats, ()> for StatsBackend {
|
||||
cpu,
|
||||
ram,
|
||||
disks: disk_stats,
|
||||
gpus,
|
||||
gpu,
|
||||
battery_charge,
|
||||
battery_state,
|
||||
battery_draw,
|
||||
battery_time,
|
||||
};
|
||||
instance.publish(msg);
|
||||
instance.lock().unwrap().publish(msg);
|
||||
|
||||
thread::sleep(Duration::from_millis(POLL_INTERVAL));
|
||||
}
|
||||
|
||||
@@ -28,11 +28,9 @@ impl SimpleBackendThread<WindowManagerEvent> for HyprlandComms {
|
||||
&self.subscribers
|
||||
}
|
||||
|
||||
fn init(registration_rx: Receiver<Sender<WindowManagerEvent>>) {
|
||||
fn init(self, registration_rx: Receiver<Sender<WindowManagerEvent>>) {
|
||||
std::thread::spawn(move || {
|
||||
let instance = Arc::new(Mutex::new(Self {
|
||||
subscribers: vec![],
|
||||
}));
|
||||
let instance = Arc::new(Mutex::new(self));
|
||||
let mut listener = EventListener::new();
|
||||
|
||||
let inst_clone = instance.clone();
|
||||
@@ -153,6 +151,12 @@ impl SimpleBackendThread<WindowManagerEvent> for HyprlandComms {
|
||||
}
|
||||
|
||||
impl HyprlandComms {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
subscribers: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn monitor_id_from_workspace_id(id: WorkspaceId) -> Option<MonitorId> {
|
||||
if let Ok(wss) = Workspaces::get()
|
||||
&& let Some(ws) = wss.to_vec().iter().find(|ws| ws.id().unwrap() == id)
|
||||
@@ -162,7 +166,7 @@ impl HyprlandComms {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn current_monitor() -> MonitorId {
|
||||
pub fn _current_monitor() -> MonitorId {
|
||||
if let Ok(monitor) = Monitor::get_active() {
|
||||
return monitor.id;
|
||||
}
|
||||
|
||||
@@ -1,30 +1,11 @@
|
||||
use jmb_bar::{ControlCommand, SOCKET_PATH};
|
||||
use jmb_logger_rs::*;
|
||||
use std::env;
|
||||
use std::io::Write;
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::str::FromStr;
|
||||
|
||||
const CTL_LOGGER: Logger = Logger::create_with_custom_format(Level::DEBUG, String::new());
|
||||
pub const SOCKET_PATH: &str = "/run/user/1000/jmb-bar.sock";
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ControlCommand {
|
||||
OpenRunMenu,
|
||||
}
|
||||
|
||||
impl ControlCommand {
|
||||
pub fn from_str(s: &str) -> Option<Self> {
|
||||
match s {
|
||||
"menu" => Some(ControlCommand::OpenRunMenu),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_string(self) -> String {
|
||||
match self {
|
||||
ControlCommand::OpenRunMenu => String::from("menu"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
info!(&CTL_LOGGER, "Hello, this is mission control");
|
||||
@@ -43,7 +24,7 @@ fn main() {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
|
||||
match args.get(1) {
|
||||
Some(str) if let Some(cmd) = ControlCommand::from_str(str) => stream
|
||||
Some(str) if let Ok(cmd) = ControlCommand::from_str(str) => stream
|
||||
.write(cmd.to_string().as_bytes())
|
||||
.expect("Error sending message"),
|
||||
_ => 0usize,
|
||||
@@ -1,34 +1,44 @@
|
||||
use serde::Deserialize;
|
||||
use std::default::Default;
|
||||
use std::fs;
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, Default)]
|
||||
pub struct Config {
|
||||
pub idle: IdleConfig,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn load(file_path: &str) -> Option<Config> {
|
||||
fs::read_to_string(file_path)
|
||||
.ok()
|
||||
.and_then(|s| toml::from_str::<Config>(&s).ok())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub(crate) struct Config {
|
||||
pub struct IdleConfig {
|
||||
idle_timeout: u64,
|
||||
dim_timeout: u64,
|
||||
lock_timeout: u64,
|
||||
sleep_timeout: u64,
|
||||
lock_command: String,
|
||||
sleep_command: String,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
impl Default for IdleConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
idle_timeout: 20,
|
||||
dim_timeout: 60,
|
||||
lock_timeout: 300,
|
||||
sleep_timeout: 320,
|
||||
lock_command: String::default(),
|
||||
sleep_command: String::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
impl IdleConfig {
|
||||
pub fn idle_timeout(&self) -> u64 {
|
||||
self.idle_timeout
|
||||
}
|
||||
@@ -56,4 +66,12 @@ impl Config {
|
||||
pub fn sleep_timeout_delta(&self) -> u64 {
|
||||
self.sleep_timeout - self.lock_timeout_delta()
|
||||
}
|
||||
|
||||
pub fn lock_command(&self) -> &str {
|
||||
&self.lock_command
|
||||
}
|
||||
|
||||
pub fn sleep_command(&self) -> &str {
|
||||
&self.sleep_command
|
||||
}
|
||||
}
|
||||
|
||||
34
src/lib.rs
Normal file
34
src/lib.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::str::FromStr;
|
||||
|
||||
pub mod config;
|
||||
pub mod theme;
|
||||
|
||||
pub const SOCKET_PATH: &str = "/run/user/1000/jmb-bar.sock";
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ControlCommand {
|
||||
OpenRunMenu,
|
||||
OpenNetMenu,
|
||||
}
|
||||
|
||||
impl Display for ControlCommand {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ControlCommand::OpenRunMenu => f.write_str("menu"),
|
||||
ControlCommand::OpenNetMenu => f.write_str("net"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for ControlCommand {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"menu" => Ok(ControlCommand::OpenRunMenu),
|
||||
"net" => Ok(ControlCommand::OpenNetMenu),
|
||||
other => Err(format!("Unknown command: {other}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
16
src/main.rs
16
src/main.rs
@@ -1,17 +1,15 @@
|
||||
use crate::backend::Backend;
|
||||
mod backend;
|
||||
mod config;
|
||||
mod css;
|
||||
mod ctl;
|
||||
mod ui;
|
||||
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use gtk::{Application, CssProvider, gdk, gio};
|
||||
use jmb_bar::config::Config;
|
||||
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);
|
||||
@@ -20,16 +18,18 @@ pub const APP_NAME: &str = "net.jmb19905.jmb-bar";
|
||||
fn main() {
|
||||
info!(&LOGGER, "Starting jmb-bar");
|
||||
|
||||
gio::resources_register_include!("templates.gresource").expect("Failed to register resources");
|
||||
let config = Config::load("jmb-bar.conf");
|
||||
let config = config.unwrap_or_else(|| {
|
||||
warn!(&LOGGER, "Couldn't load config file, using default");
|
||||
Config::default()
|
||||
});
|
||||
let backend = Backend::start(&config);
|
||||
|
||||
gio::resources_register_include!("templates.gresource").expect("Failed to register resources");
|
||||
let app = Application::builder().application_id(APP_NAME).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();
|
||||
|
||||
app.connect_startup(move |_| {
|
||||
debug!(&LOGGER, "Connect startup");
|
||||
let provider = CssProvider::new();
|
||||
|
||||
1
src/theme.rs
Normal file
1
src/theme.rs
Normal file
@@ -0,0 +1 @@
|
||||
include!(concat!(env!("OUT_DIR"), "/theme.rs"));
|
||||
80
src/ui.rs
80
src/ui.rs
@@ -1,18 +1,17 @@
|
||||
use crate::LOGGER;
|
||||
use crate::backend::audio::MediaMessage;
|
||||
use crate::backend::window_manager_hypr::{HyprlandComms, WindowManagerEvent};
|
||||
use crate::backend::{Backend, BackendChannels, ChannelType};
|
||||
use crate::ctl::ControlCommand;
|
||||
use crate::ui::action_centre::ActionCentre;
|
||||
use crate::ui::run_menu::RunMenu;
|
||||
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_bar::ControlCommand;
|
||||
use jmb_logger_rs::*;
|
||||
use mpris::PlayerFinder;
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Mutex;
|
||||
@@ -30,11 +29,11 @@ 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: Rc<Mutex<HashMap<u32, ApplicationWindow>>> = Rc::new(Mutex::new(HashMap::new()));
|
||||
let ac_channels = backend.subscribe_all();
|
||||
let ac_channels = backend.subscribe(&[ChannelType::Net]);
|
||||
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_all();
|
||||
let channels = backend.subscribe_except(&[ChannelType::Control]);
|
||||
bars.lock().expect("Couldn't get active bars").insert(
|
||||
monitor,
|
||||
create_and_show_bar(
|
||||
@@ -47,9 +46,9 @@ pub fn create_bars(app: &Application, backend: Backend) {
|
||||
);
|
||||
}
|
||||
let backend_clone = backend.clone();
|
||||
display
|
||||
.monitors()
|
||||
.connect_items_changed(move |_list, pos, removed, added| {
|
||||
display.monitors().connect_items_changed({
|
||||
let action_centre = action_centre.clone();
|
||||
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) {
|
||||
@@ -59,7 +58,7 @@ pub fn create_bars(app: &Application, backend: Backend) {
|
||||
debug!(&LOGGER, "Detected new monitor: {pos}");
|
||||
let app_clone = app_clone.clone();
|
||||
let action_centre = action_centre.clone();
|
||||
let channels = backend_clone.subscribe_all();
|
||||
let channels = backend_clone.subscribe_except(&[ChannelType::Control]);
|
||||
let bars = bars.clone();
|
||||
glib::idle_add_local_once(move || {
|
||||
let display = Display::default().expect("Couldn't find GDK Display");
|
||||
@@ -69,21 +68,24 @@ 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 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 {
|
||||
ControlCommand::OpenRunMenu => {
|
||||
menu.show();
|
||||
glib::timeout_add_local(Duration::from_millis(100), {
|
||||
let action_centre = action_centre.clone();
|
||||
move || {
|
||||
while let Ok(cmd) = ctl_rx.try_recv() {
|
||||
match cmd {
|
||||
ControlCommand::OpenRunMenu => menu.toggle(),
|
||||
ControlCommand::OpenNetMenu => action_centre.toggle_active_monitor(),
|
||||
}
|
||||
}
|
||||
ControlFlow::Continue
|
||||
}
|
||||
ControlFlow::Continue
|
||||
});
|
||||
}
|
||||
|
||||
@@ -98,12 +100,10 @@ fn create_and_show_bar(
|
||||
let hypr_rx = channels
|
||||
.take_hypr_rx()
|
||||
.expect("Tried taking unavailable Hyprland Channel Receiver");
|
||||
|
||||
let player_opt = if let Ok(player_finder) = PlayerFinder::new() {
|
||||
player_finder.find_active().ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let media_rx = channels
|
||||
.take_media_rx()
|
||||
.expect("Tried taking unavailable Media Channel Receiver");
|
||||
let media_tx = channels.media_tx().unwrap();
|
||||
|
||||
let window = ApplicationWindow::builder()
|
||||
.application(&app)
|
||||
@@ -136,7 +136,7 @@ fn create_and_show_bar(
|
||||
);
|
||||
|
||||
let title_button: Option<Button>;
|
||||
let title = title_panel::build();
|
||||
let title = title_panel::build(media_tx);
|
||||
|
||||
let utilities = utilities_panel::build(&app, action_centre, channels, monitor_idx, &display);
|
||||
|
||||
@@ -149,34 +149,30 @@ fn create_and_show_bar(
|
||||
dock.append(&utilities);
|
||||
|
||||
title_button = Some(title);
|
||||
title_panel::manage_title(&title_button, &player_opt);
|
||||
title_panel::manage_title(&title_button, &None);
|
||||
|
||||
window.set_keyboard_mode(KeyboardMode::None);
|
||||
window.set_child(Some(&dock));
|
||||
window.present();
|
||||
|
||||
let window_clone = window.clone();
|
||||
|
||||
let mut status = None;
|
||||
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;
|
||||
}
|
||||
}
|
||||
while let Ok(event) = hypr_rx.try_recv() {
|
||||
handle_wm_event(
|
||||
&mut buttons,
|
||||
&submap,
|
||||
&title_button,
|
||||
event,
|
||||
window_clone.monitor(),
|
||||
);
|
||||
}
|
||||
|
||||
title_panel::manage_title(&title_button, &player_opt);
|
||||
while let Ok(MediaMessage::PlayerStatus(recv_status)) = media_rx.try_recv() {
|
||||
status = Some(recv_status);
|
||||
}
|
||||
|
||||
title_panel::manage_title(&title_button, &status);
|
||||
|
||||
ControlFlow::Continue
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::backend::BackendChannels;
|
||||
use crate::backend::network::{
|
||||
Auth, AuthType, ConnectError, NetworkCommand, NetworkInfo, NetworkState,
|
||||
};
|
||||
use crate::backend::window_manager_hypr::HyprlandComms;
|
||||
use crate::ui::widgets::{LabelEntry, WifiWidget};
|
||||
use glib::prelude::Cast;
|
||||
use glib::{ControlFlow, clone};
|
||||
@@ -58,7 +59,7 @@ impl ActionCentre {
|
||||
window.set_margin(Edge::Top, 20);
|
||||
window.set_margin(Edge::Right, 20);
|
||||
window.set_margin(Edge::Bottom, 20);
|
||||
window.set_keyboard_mode(KeyboardMode::OnDemand);
|
||||
window.set_keyboard_mode(KeyboardMode::Exclusive);
|
||||
|
||||
let instance = Self {
|
||||
window: window.clone(),
|
||||
@@ -116,7 +117,7 @@ impl ActionCentre {
|
||||
let instance = instance.clone();
|
||||
move |_, key, _, _| {
|
||||
if key == gdk::Key::Escape {
|
||||
instance.hide_action_centre();
|
||||
instance.hide();
|
||||
glib::Propagation::Stop
|
||||
} else {
|
||||
glib::Propagation::Proceed
|
||||
@@ -318,7 +319,25 @@ impl ActionCentre {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show_action_centre(&self, monitor_idx: u32, display: &Display) {
|
||||
pub fn toggle_active_monitor(&self) {
|
||||
if self.window.is_visible() {
|
||||
self.hide();
|
||||
} else {
|
||||
self.show_active_monitor();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show_active_monitor(&self) {
|
||||
let Some(display) = Display::default() else {
|
||||
return;
|
||||
};
|
||||
let Some(monitor) = HyprlandComms::cursor_monitor() else {
|
||||
return;
|
||||
};
|
||||
self.show(monitor as u32, &display);
|
||||
}
|
||||
|
||||
pub fn show(&self, monitor_idx: u32, display: &Display) {
|
||||
let window = &self.window;
|
||||
let monitors = display.monitors();
|
||||
let Some(monitor) = monitors.item(monitor_idx) else {
|
||||
@@ -334,7 +353,7 @@ impl ActionCentre {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hide_action_centre(&self) {
|
||||
pub fn hide(&self) {
|
||||
let clone = self.clone();
|
||||
self.animate_margin(
|
||||
20,
|
||||
|
||||
@@ -10,6 +10,7 @@ use gtk::prelude::{
|
||||
};
|
||||
use gtk::{Align, Application, ApplicationWindow, Orientation, PolicyType, gdk};
|
||||
use gtk4_layer_shell::{Edge, KeyboardMode, Layer, LayerShell};
|
||||
use jmb_bar::theme;
|
||||
use jmb_logger_rs::*;
|
||||
use nucleo_matcher::pattern::{CaseMatching, Normalization, Pattern};
|
||||
use nucleo_matcher::{Config, Matcher};
|
||||
@@ -66,34 +67,20 @@ impl RunMenu {
|
||||
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)
|
||||
let monitor = HyprlandComms::cursor_monitor()
|
||||
.and_then(|id| display.monitors().item(id 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,
|
||||
@@ -105,22 +92,42 @@ impl RunMenu {
|
||||
entry.connect_changed(clone!(
|
||||
#[strong]
|
||||
content_box,
|
||||
move |e| {
|
||||
move |entry| {
|
||||
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}")));
|
||||
if entry.text_length() > 0 {
|
||||
let text = entry.text();
|
||||
if let Some(eval) = text.strip_prefix("=") {
|
||||
let new_eval = eval.replace("**", "^");
|
||||
if new_eval != eval {
|
||||
let cursor = entry.position() as usize;
|
||||
let removed_before_cursor = text[..cursor].matches("**").count();
|
||||
let new_cursor = cursor - removed_before_cursor;
|
||||
|
||||
glib::idle_add_local_once(clone!(
|
||||
#[weak]
|
||||
entry,
|
||||
move || {
|
||||
entry.set_text(&format!("={new_eval}"));
|
||||
entry.set_position(new_cursor as i32);
|
||||
}
|
||||
));
|
||||
}
|
||||
if let Ok(result) = meval::eval_str(eval) {
|
||||
let label = gtk::Label::new(Some(&format!("{}", result)));
|
||||
label.add_css_class("exec-button");
|
||||
label.set_halign(Align::Start);
|
||||
label.set_hexpand(true);
|
||||
content_box.append(&label);
|
||||
instance_clone.window.set_default_size(500, -1);
|
||||
}
|
||||
} else {
|
||||
instance_clone.list_matching(e, &content_box, &mut matcher_rc.borrow_mut());
|
||||
instance_clone.list_matching(
|
||||
entry,
|
||||
&content_box,
|
||||
&mut matcher_rc.borrow_mut(),
|
||||
);
|
||||
instance_clone.window.set_default_size(500, -1);
|
||||
}
|
||||
}
|
||||
@@ -143,10 +150,17 @@ impl RunMenu {
|
||||
#[strong]
|
||||
content_box,
|
||||
move |_| {
|
||||
if let Some(child) = content_box.first_child()
|
||||
&& let Ok(button) = child.downcast::<gtk::Button>()
|
||||
{
|
||||
button.activate();
|
||||
if let Some(child) = content_box.first_child() {
|
||||
if let Ok(button) = child.clone().downcast::<gtk::Button>() {
|
||||
button.activate();
|
||||
} else if let Ok(label) = child.downcast::<gtk::Label>() {
|
||||
display.clipboard().set_text(&label.text());
|
||||
label.set_markup(&format!(
|
||||
"{}<span foreground=\"{}\"><i> - Copied</i></span>",
|
||||
label.text(),
|
||||
theme::LIGHTGRAY
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
));
|
||||
@@ -175,8 +189,16 @@ impl RunMenu {
|
||||
self.window.present();
|
||||
}
|
||||
|
||||
fn create_entry(&self, content_box: >k::Box, entry: &String) -> gtk::Button {
|
||||
let entry_clone = entry.clone();
|
||||
pub fn toggle(&mut self) {
|
||||
if self.window.is_visible() {
|
||||
self.hide();
|
||||
} else {
|
||||
self.show();
|
||||
}
|
||||
}
|
||||
|
||||
fn create_entry(&self, content_box: >k::Box, entry: &str) -> gtk::Button {
|
||||
let entry_clone = entry.to_owned();
|
||||
let button = gtk::Button::new();
|
||||
button.set_halign(Align::Fill);
|
||||
button.set_hexpand(true);
|
||||
@@ -209,14 +231,14 @@ impl RunMenu {
|
||||
content_box: >k::Box,
|
||||
matcher: &mut Matcher,
|
||||
) -> bool {
|
||||
let keys: Vec<String> = self.entries.borrow().keys().map(|i| i.clone()).collect();
|
||||
let keys: Vec<String> = self.entries.borrow().keys().cloned().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);
|
||||
let button = self.create_entry(content_box, entry);
|
||||
if first {
|
||||
button.add_css_class("suggested");
|
||||
first = false;
|
||||
|
||||
@@ -10,31 +10,28 @@ pub fn _build() -> gtk::Box {
|
||||
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());
|
||||
}
|
||||
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,
|
||||
_ => {}
|
||||
}
|
||||
TrayEvent::ItemRemoved(id) => {
|
||||
println!("item gone: {id}");
|
||||
}
|
||||
TrayEvent::HostShutdown => break,
|
||||
_ => {}
|
||||
}
|
||||
ControlFlow::Continue
|
||||
}
|
||||
);
|
||||
ControlFlow::Continue
|
||||
});
|
||||
} else {
|
||||
error!(&LOGGER, "Couldn't get SystemTray");
|
||||
}
|
||||
|
||||
systray
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use crate::backend::audio::{MediaMessage, PlayerStatus};
|
||||
use crate::backend::window_manager_hypr::HyprlandComms;
|
||||
use crossbeam_channel::Sender;
|
||||
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 {
|
||||
pub fn build(media_tx: Sender<MediaMessage>) -> Button {
|
||||
let title_label = Label::new(None);
|
||||
title_label.set_ellipsize(EllipsizeMode::End);
|
||||
title_label.set_max_width_chars(50);
|
||||
@@ -16,61 +17,56 @@ pub fn build() -> Button {
|
||||
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")
|
||||
&& let Ok(player_finder) = PlayerFinder::new()
|
||||
&& let Ok(player) = player_finder.find_active()
|
||||
{
|
||||
let _ = player.play_pause();
|
||||
title.connect_clicked({
|
||||
let media_tx = media_tx.clone();
|
||||
move |button| {
|
||||
if button.has_css_class("media") {
|
||||
if button.has_css_class("playing") {
|
||||
let _ = media_tx.send(MediaMessage::PlayerPause);
|
||||
} else {
|
||||
let _ = media_tx.send(MediaMessage::PlayerPlay);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let right_click = GestureClick::new();
|
||||
right_click.set_button(3);
|
||||
right_click.connect_pressed(|_gesture, _n_press, _, _| {
|
||||
if let Ok(player_finder) = PlayerFinder::new()
|
||||
&& let Ok(player) = player_finder.find_active()
|
||||
{
|
||||
let _ = player.stop();
|
||||
}
|
||||
right_click.connect_pressed(move |_gesture, _n_press, _, _| {
|
||||
let _ = media_tx.send(MediaMessage::PlayerStop);
|
||||
});
|
||||
title.add_controller(right_click);
|
||||
title
|
||||
}
|
||||
|
||||
pub fn manage_title(button_opt: &Option<Button>, player_opt: &Option<Player>) {
|
||||
pub fn manage_title(button_opt: &Option<Button>, status: &Option<PlayerStatus>) {
|
||||
let mut handled = false;
|
||||
if let Some(button) = button_opt
|
||||
&& let Some(player) = player_opt
|
||||
{
|
||||
//println!("{:?}", player.get_playback_status());
|
||||
if let Ok(status) = player.get_playback_status()
|
||||
&& let Ok(metadata) = player.get_metadata()
|
||||
{
|
||||
if let Some(button) = button_opt {
|
||||
if let Some(status) = status {
|
||||
match status {
|
||||
PlaybackStatus::Playing => {
|
||||
PlayerStatus::Playing(title) => {
|
||||
button.set_visible(true);
|
||||
button
|
||||
.child()
|
||||
.and_downcast::<Label>()
|
||||
.unwrap()
|
||||
.set_label(&format!(" {}", metadata.title().unwrap_or("")));
|
||||
.set_label(&format!(" {}", title));
|
||||
button.add_css_class("media");
|
||||
button.add_css_class("playing");
|
||||
handled = true;
|
||||
}
|
||||
PlaybackStatus::Paused => {
|
||||
PlayerStatus::Paused(title) => {
|
||||
button.set_visible(true);
|
||||
button
|
||||
.child()
|
||||
.and_downcast::<Label>()
|
||||
.unwrap()
|
||||
.set_label(&format!(" {}", metadata.title().unwrap_or("")));
|
||||
.set_label(&format!(" {}", title));
|
||||
button.add_css_class("media");
|
||||
button.remove_css_class("playing");
|
||||
handled = true;
|
||||
}
|
||||
PlaybackStatus::Stopped => {
|
||||
PlayerStatus::Stopped => {
|
||||
button.set_visible(true);
|
||||
button.remove_css_class("media");
|
||||
button.remove_css_class("playing");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::LOGGER;
|
||||
use crate::backend::audio::MediaCommand;
|
||||
use crate::backend::audio::AudioMessage;
|
||||
use crate::backend::brightness_control::BrightnessMessage;
|
||||
use crate::backend::idle::{IdleMessage, IdleState};
|
||||
use crate::backend::network::{NetworkCommand, NetworkState, NetworkType};
|
||||
@@ -7,6 +7,7 @@ use crate::backend::{BackendChannels, json_socket};
|
||||
use crate::ui::action_centre::ActionCentre;
|
||||
use crate::ui::widgets::{ExpandableBarItem, IconSlider};
|
||||
use battery::State;
|
||||
use crossbeam_channel::TryRecvError;
|
||||
use glib::prelude::Cast;
|
||||
use glib::subclass::prelude::ObjectSubclassIsExt;
|
||||
use glib::{ControlFlow, clone};
|
||||
@@ -76,13 +77,13 @@ pub fn build(
|
||||
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().unwrap();
|
||||
let media_tx = channels.audio_tx().unwrap();
|
||||
let changed_progr = Rc::new(Cell::new(false));
|
||||
volume.connect_value_changed({
|
||||
let changed_progr = changed_progr.clone();
|
||||
move |slider, button, _| {
|
||||
if !changed_progr.get() {
|
||||
let _ = media_tx.send(MediaCommand::Cmd(
|
||||
let _ = media_tx.send(AudioMessage::Cmd(
|
||||
slider.value() as u8,
|
||||
button.is_active(),
|
||||
false,
|
||||
@@ -90,12 +91,12 @@ pub fn build(
|
||||
}
|
||||
}
|
||||
});
|
||||
let media_tx = channels.media_tx().unwrap();
|
||||
let media_tx = channels.audio_tx().unwrap();
|
||||
volume.connect_clicked(move |slider, button, _| {
|
||||
if button.is_active() {
|
||||
let _ = media_tx.send(MediaCommand::Cmd(slider.value() as u8, true, false));
|
||||
let _ = media_tx.send(AudioMessage::Cmd(slider.value() as u8, true, false));
|
||||
} else {
|
||||
let _ = media_tx.send(MediaCommand::Cmd(slider.value() as u8, false, false));
|
||||
let _ = media_tx.send(AudioMessage::Cmd(slider.value() as u8, false, false));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -205,9 +206,9 @@ pub fn build(
|
||||
display,
|
||||
move |_| {
|
||||
if action_centre.window.is_visible() {
|
||||
action_centre.hide_action_centre();
|
||||
action_centre.hide();
|
||||
} else {
|
||||
action_centre.show_action_centre(monitor_idx, &display);
|
||||
action_centre.show(monitor_idx, &display);
|
||||
}
|
||||
}
|
||||
));
|
||||
@@ -315,7 +316,7 @@ pub fn build(
|
||||
});
|
||||
|
||||
glib::timeout_add_local(Duration::from_millis(100), {
|
||||
let media_rx = channels.take_media_rx().unwrap();
|
||||
let media_rx = channels.take_audio_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();
|
||||
@@ -325,7 +326,7 @@ pub fn build(
|
||||
let br_tx = channels.br_tx().unwrap();
|
||||
move || {
|
||||
while let Ok(command) = media_rx.try_recv() {
|
||||
if let MediaCommand::Cmd(vol, mute, bt) = command {
|
||||
if let AudioMessage::Cmd(vol, mute, bt) = command {
|
||||
changed_progr.set(true);
|
||||
volume.set_value(vol as f64);
|
||||
changed_progr.set(false);
|
||||
@@ -390,43 +391,61 @@ pub fn build(
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
loop {
|
||||
match net_rx.try_recv() {
|
||||
Ok(result) => {
|
||||
trace!(&LOGGER, "NetworkResult: {:?}", result);
|
||||
match result {
|
||||
NetworkState::ChangeNetwork(cfg) => {
|
||||
println!("Change network: {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);
|
||||
}
|
||||
}
|
||||
}
|
||||
NetworkType::Wired => {
|
||||
network_icon.set_from_file(Some("icons/ethernet.svg"));
|
||||
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");
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
match e {
|
||||
TryRecvError::Empty => {}
|
||||
TryRecvError::Disconnected => {
|
||||
warn!(&LOGGER, "Network channel disconnected")
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
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() {
|
||||
@@ -481,8 +500,8 @@ pub fn build(
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(gpu) = stats.gpus.into_iter().next() {
|
||||
gpu_label.set_label(&format!("{}%", gpu.1));
|
||||
if let Some(gpu) = stats.gpu {
|
||||
gpu_label.set_label(&format!("{}%", gpu));
|
||||
}
|
||||
|
||||
set_battery_css_classes(&battery_widget, stats.battery_state, stats.battery_charge);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Changing colors here will not apply to the constants in theme.rs until the app is recompiled
|
||||
$background: #232320;
|
||||
$background1: #3c3836;
|
||||
|
||||
Reference in New Issue
Block a user