Compare commits

..

25 Commits

Author SHA1 Message Date
jmb05
092f0eab76 Remove check formatting step, add missing dependency
All checks were successful
Build Project / Build Linux (push) Successful in 3m25s
2026-11-03 01:39:55 +01:00
jmb05
55ade087de Add upload binaries to workflow
Some checks failed
Build Project / Build Linux (push) Failing after 2m16s
2026-11-03 01:36:42 +01:00
jmb05
495210c229 Random commit
Some checks failed
Build Project / Build Linux (push) Failing after 1m2s
2026-11-03 01:33:33 +01:00
jmb05
3285353006 Add cache step and missing dependency
Some checks failed
Build Project / Build Linux (push) Failing after 1m40s
2026-11-03 01:31:23 +01:00
jmb05
1ec5d1e30d Fix typo
Some checks failed
Build Project / Build Linux (push) Failing after 1m5s
2026-11-03 01:28:43 +01:00
jmb05
0a3a93adf2 Dont build layer-shell in workflow
Some checks failed
Build Project / Build Linux (push) Failing after 5s
2026-11-03 01:28:07 +01:00
jmb05
0f67f4ffcf Build against gtk 4.22
Some checks failed
Build Project / Build Linux (push) Has been cancelled
2026-11-03 01:27:24 +01:00
jmb05
63f247a0dc Change runner version
Some checks failed
Build Project / Build Linux (push) Failing after 2m17s
2026-11-03 01:20:41 +01:00
jmb05
5ffed55e37 Fix systray formatting
Some checks failed
Build Project / Build Linux (push) Failing after 1m11s
2026-11-03 01:14:50 +01:00
jmb05
f88f3ba30d Change workflow
Some checks failed
Build Project / Build Linux (push) Failing after 50s
2026-11-03 01:12:23 +01:00
jmb05
2fa7b4a55c Change workflow to build gtk4-layer-shell
Some checks failed
Build Project / Build Linux (push) Failing after 34s
2026-11-03 01:10:44 +01:00
jmb05
0f19ff6213 Change workflow ubuntu-version
Some checks failed
Build Project / Build Linux (push) Failing after 56s
2026-11-03 01:06:08 +01:00
jmb05
61a4e136ed Change workflow
Some checks failed
Build Project / Build Linux (push) Failing after 6s
2026-11-03 01:03:48 +01:00
jmb05
daf865423f Fix gitea workflow
Some checks failed
Build Project / Build Linux (push) Failing after 7s
2026-11-03 01:02:24 +01:00
jmb05
e547fd5885 Add Gitea workflow
Some checks failed
Build Project / Build Linux (push) Failing after 58s
2026-11-03 00:58:16 +01:00
jmb05
c758268beb Fix config not loading 2026-11-02 22:02:37 +01:00
jmb05
541ce5ccb2 Launcher appears on correct display, Action centre can be triggered by control command 2026-11-02 19:55:43 +01:00
jmb05
f46c2408a5 Change meval to patched version 2026-11-02 19:35:36 +01:00
jmb05
c3ad6f38cf Add scss colors into rust via build-script, flesh out math evaluation in launcher, Restructure some code 2026-11-02 19:31:21 +01:00
jmb05
11fb03427c clippy 2026-11-02 17:26:51 +01:00
jmb05
cdfc5b8d36 Pass config from main to the relevant places 2026-11-02 17:06:42 +01:00
jmb05
de1f731e63 Replaced all-smi with own implementation 2026-11-02 16:20:51 +01:00
jmb05
00b2b1f64c Replace mpris crate with own implementation 2026-11-02 14:36:53 +01:00
jmb05
39f4263db5 Removed print statements 2026-11-02 09:58:47 +01:00
jmb05
87d27f9734 Fixed title now visible on first startup 2026-11-01 16:59:03 +01:00
28 changed files with 1406 additions and 4135 deletions

View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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"

View File

@@ -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.

View File

@@ -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");
}

View File

@@ -1,4 +1,7 @@
[idle]
idle_timeout = 20
dim_timeout = 60
lock_timeout = 300
sleep_timeout = 320
lock_command = 'hyprlock'
sleep_command = 'systemctl sleep'

View File

@@ -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>> {

View File

@@ -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;

View File

@@ -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());

View File

@@ -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);
}
}

View 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))
}
}

View File

@@ -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,
(),

View File

@@ -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 || {

View File

@@ -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)));
}
_ => {}
}
}
}

View File

@@ -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));
}

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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
View 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}")),
}
}
}

View File

@@ -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
View File

@@ -0,0 +1 @@
include!(concat!(env!("OUT_DIR"), "/theme.rs"));

View File

@@ -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
});

View File

@@ -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,

View File

@@ -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: &gtk::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: &gtk::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: &gtk::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;

View File

@@ -10,28 +10,25 @@ 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");
}

View File

@@ -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");

View File

@@ -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);

View File

@@ -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;