Compare commits

...

22 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
26 changed files with 860 additions and 3984 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

3633
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,11 +9,11 @@ path = "src/main.rs"
[[bin]] [[bin]]
name = "jmbctl" name = "jmbctl"
path = "src/ctl.rs" path = "src/bin/ctl.rs"
[dependencies] [dependencies]
grass = "0.13.4" 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" gtk4-layer-shell = "0.8.1"
hyprland = { git = "https://github.com/romanstingler/hyprland-rs-next", branch = "master" } hyprland = { git = "https://github.com/romanstingler/hyprland-rs-next", branch = "master" }
notify = "8.2.0" notify = "8.2.0"
@@ -26,7 +26,6 @@ serde_json = "1.0.154"
crossbeam-channel = "0.5.17" crossbeam-channel = "0.5.17"
jmb-logger-rs = { version = "0.2.2", git = "https://github.com/jmb05/jmb-logger-rs" } jmb-logger-rs = { version = "0.2.2", git = "https://github.com/jmb05/jmb-logger-rs" }
sysinfo = "0.40.0" sysinfo = "0.40.0"
all-smi = "0.28.1"
rustsni = "0.2.2" rustsni = "0.2.2"
thiserror = "2.0.21" thiserror = "2.0.21"
pulse = { package = "libpulse-binding", version = "2.30" } pulse = { package = "libpulse-binding", version = "2.30" }
@@ -45,8 +44,10 @@ zbus = "5.19.0" # Keep at this version for now because other dependencies also h
serde = { version = "1.0.229", features = ["derive"] } serde = { version = "1.0.229", features = ["derive"] }
freedesktop-desktop-entry = "0.8.4" freedesktop-desktop-entry = "0.8.4"
nucleo-matcher = "0.3.1" 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" toml = "1.1.8"
[build-dependencies] [build-dependencies]
glib-build-tools = "0.22.8" glib-build-tools = "0.22.8"
grass = "0.13.4"
regex = "1.13.1"

View File

@@ -1,7 +1,35 @@
use regex::Regex;
use std::path::PathBuf;
use std::{env, fs};
fn main() { fn main() {
glib_build_tools::compile_resources( glib_build_tools::compile_resources(
&["resources"], &["resources"],
"resources/resources.gresource.xml", "resources/resources.gresource.xml",
"templates.gresource", "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 idle_timeout = 20
dim_timeout = 60 dim_timeout = 60
lock_timeout = 300 lock_timeout = 300
sleep_timeout = 320 sleep_timeout = 320
lock_command = 'hyprlock'
sleep_command = 'systemctl sleep'

View File

@@ -1,6 +1,7 @@
pub mod audio; pub mod audio;
pub mod brightness_control; pub mod brightness_control;
mod commands; mod commands;
mod gpu_utilization;
pub(crate) mod idle; pub(crate) mod idle;
pub(crate) mod json_socket; pub(crate) mod json_socket;
pub mod network; pub mod network;
@@ -13,11 +14,12 @@ use crate::backend::brightness_control::{BrightnessManager, BrightnessMessage};
use crate::backend::commands::ControlSocket; use crate::backend::commands::ControlSocket;
use crate::backend::idle::{IdleDaemon, IdleMessage, IdleState}; use crate::backend::idle::{IdleDaemon, IdleMessage, IdleState};
use crate::backend::json_socket::{JsonSocketBackend, State}; 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::perf_stats::{Stats, StatsBackend};
use crate::backend::window_manager_hypr::{HyprlandComms, WindowManagerEvent}; use crate::backend::window_manager_hypr::{HyprlandComms, WindowManagerEvent};
use crate::ctl::ControlCommand;
use crossbeam_channel::{Receiver, Sender}; use crossbeam_channel::{Receiver, Sender};
use jmb_bar::ControlCommand;
use jmb_bar::config::Config;
use jmb_logger_rs::*; use jmb_logger_rs::*;
use std::fmt::Debug; use std::fmt::Debug;
use tokio::sync::mpsc::UnboundedSender; use tokio::sync::mpsc::UnboundedSender;
@@ -27,8 +29,15 @@ pub(crate) type Registrator<M> = Sender<Sender<M>>;
pub trait BackendThread<M: Debug + Clone, D> { pub trait BackendThread<M: Debug + Clone, D> {
fn subscribe(registrator: Registrator<M>) -> (Receiver<M>, D); fn subscribe(registrator: Registrator<M>) -> (Receiver<M>, D);
fn subscribers(&self) -> &Vec<Sender<M>>; 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) { 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()); trace!(&LOGGER, "Sending: {message:?} to {:?}", self.subscribers());
for tx in self.subscribers() { for tx in self.subscribers() {
if let Err(e) = tx.send(message.clone()) { 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>>(); let (registration_tx, registration_rx) = crossbeam_channel::unbounded::<Sender<M>>();
Self::init(registration_rx); Self::init(self, registration_rx);
registration_tx registration_tx
} }
} }
pub trait SimpleBackendThread<M: Debug + Clone>: BackendThread<M, ()> { pub trait SimpleBackendThread<M: Debug + Clone>: BackendThread<M, ()> {
fn subscribers(&self) -> &Vec<Sender<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 { 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) SimpleBackendThread::subscribers(self)
} }
fn init(registration_rx: Receiver<Sender<M>>) { fn init(self, registration_rx: Receiver<Sender<M>>) {
<S as SimpleBackendThread<M>>::init(registration_rx) <S as SimpleBackendThread<M>>::init(self, registration_rx)
} }
} }
@@ -81,6 +93,7 @@ pub struct Backend {
control_registrator: Registrator<ControlCommand>, control_registrator: Registrator<ControlCommand>,
} }
#[derive(PartialEq)]
pub enum ChannelType { pub enum ChannelType {
Net, Net,
Hypr, Hypr,
@@ -94,17 +107,17 @@ pub enum ChannelType {
} }
impl Backend { impl Backend {
pub fn start() -> Self { pub fn start(config: &Config) -> Self {
debug!(&LOGGER, "Starting Backend threads..."); debug!(&LOGGER, "Starting Backend threads...");
let hypr_registrator = HyprlandComms::start(); let hypr_registrator = HyprlandComms::new().start();
let net_registrator = NetworkBackendHandle::start(); let net_registrator = NetworkBackend::new().start();
let br_registrator = BrightnessManager::start(); let br_registrator = BrightnessManager::new().start();
let stats_registrator = StatsBackend::start(); let stats_registrator = StatsBackend::new().start();
let audio_registrator = AudioBackend::start(); let audio_registrator = AudioBackend::new().start();
let media_registration = MediaBackend::start(); let media_registration = MediaBackend::new().start();
let netbird_registrator = JsonSocketBackend::start(); let netbird_registrator = JsonSocketBackend::new().start();
let idle_registrator = IdleDaemon::start(); let idle_registrator = IdleDaemon::new(config.idle.clone()).start();
let control_registrator = ControlSocket::start(); let control_registrator = ControlSocket::new().start();
debug!(&LOGGER, "Started Backend threads"); debug!(&LOGGER, "Started Backend threads");
Self { Self {
hypr_registrator, hypr_registrator,
@@ -124,7 +137,7 @@ impl Backend {
for ch_type in channel_types { for ch_type in channel_types {
match ch_type { match ch_type {
ChannelType::Net => { 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_tx = Some(tx);
channels.net_rx = Some(rx); channels.net_rx = Some(rx);
} }
@@ -169,8 +182,8 @@ impl Backend {
channels channels
} }
pub fn subscribe_all(&self) -> BackendChannels { pub fn subscribe_except(&self, channel_types: &[ChannelType]) -> BackendChannels {
self.subscribe(&[ let mut types = vec![
ChannelType::Net, ChannelType::Net,
ChannelType::Idle, ChannelType::Idle,
ChannelType::Netbird, ChannelType::Netbird,
@@ -180,7 +193,9 @@ impl Backend {
ChannelType::Audio, ChannelType::Audio,
ChannelType::Media, ChannelType::Media,
ChannelType::Hypr, ChannelType::Hypr,
]) ];
types.retain(|i| !channel_types.contains(i));
self.subscribe(&types)
} }
} }

View File

@@ -266,7 +266,16 @@ pub enum AudioMessage {
pub struct AudioBackend { pub struct AudioBackend {
subscribers: Vec<Sender<AudioMessage>>, subscribers: Vec<Sender<AudioMessage>>,
command_tx: Sender<AudioMessage>, command_tx: Option<Sender<AudioMessage>>,
}
impl AudioBackend {
pub fn new() -> Self {
Self {
subscribers: vec![],
command_tx: None,
}
}
} }
impl BackendThread<AudioMessage, Sender<AudioMessage>> for AudioBackend { impl BackendThread<AudioMessage, Sender<AudioMessage>> for AudioBackend {
@@ -288,12 +297,10 @@ impl BackendThread<AudioMessage, Sender<AudioMessage>> for AudioBackend {
&self.subscribers &self.subscribers
} }
fn init(registration_rx: Receiver<Sender<AudioMessage>>) { fn init(mut self, registration_rx: Receiver<Sender<AudioMessage>>) {
let (command_tx, command_rx) = crossbeam_channel::unbounded::<AudioMessage>(); let (command_tx, command_rx) = crossbeam_channel::unbounded::<AudioMessage>();
let instance = Arc::new(Mutex::new(Self { self.command_tx = Some(command_tx);
subscribers: vec![], let instance = Arc::new(Mutex::new(self));
command_tx,
}));
let (controller_tx, controller_rx) = crossbeam_channel::unbounded::<ControllerMsg>(); let (controller_tx, controller_rx) = crossbeam_channel::unbounded::<ControllerMsg>();
let notify = Arc::new(EventFD::new(0, EfdFlags::EFD_CLOEXEC).unwrap()); let notify = Arc::new(EventFD::new(0, EfdFlags::EFD_CLOEXEC).unwrap());
thread::spawn({ thread::spawn({
@@ -381,7 +388,8 @@ impl BackendThread<AudioMessage, Sender<AudioMessage>> for AudioBackend {
.lock() .lock()
.expect("Couldn't get lock") .expect("Couldn't get lock")
.command_tx .command_tx
.clone(), .clone()
.unwrap(),
)) ))
.is_ok() .is_ok()
{ {
@@ -460,18 +468,14 @@ impl BackendThread<MediaMessage, Sender<MediaMessage>> for MediaBackend {
&self.subscribers &self.subscribers
} }
fn init(registration_rx: Receiver<Sender<MediaMessage>>) { fn init(self, registration_rx: Receiver<Sender<MediaMessage>>) {
let (tx, rx) = crossbeam_channel::unbounded::<MediaMessage>(); let (tx, rx) = crossbeam_channel::unbounded::<MediaMessage>();
let instance = Arc::new(Mutex::new(Self { let instance = Arc::new(Mutex::new(self));
subscribers: vec![],
players: HashMap::new(),
active_player: None,
}));
thread::spawn({ thread::spawn({
let instance = instance.clone(); let instance = instance.clone();
move || { move || {
while let Ok(sender) = registration_rx.recv() { while let Ok(sender) = registration_rx.recv() {
if let Ok(_) = sender.send(MediaMessage::Init(tx.clone())) { if sender.send(MediaMessage::Init(tx.clone())).is_ok() {
let mut instance_guard = instance.lock().unwrap(); let mut instance_guard = instance.lock().unwrap();
if let Some(name) = &instance_guard.active_player if let Some(name) = &instance_guard.active_player
&& let Some(status) = instance_guard.players.get(name) && let Some(status) = instance_guard.players.get(name)
@@ -512,30 +516,32 @@ impl BackendThread<MediaMessage, Sender<MediaMessage>> for MediaBackend {
return; return;
}; };
while let Ok(msg) = rx.recv() { while let Ok(msg) = rx.recv() {
let mut instance_guard = instance.lock().unwrap(); let Some(name) = &instance.lock().unwrap().active_player.clone() else {
if let Some(name) = &instance_guard.active_player { continue;
if let Some(status) = instance_guard.players.get(name) { };
match msg { let Some(status) = instance.lock().unwrap().players.get(name).cloned()
MediaMessage::PlayerPlay => { else {
if let PlayerStatus::Paused(_) = status { continue;
instance_guard.active_player_play(&connection).await; };
} match msg {
} MediaMessage::PlayerPlay => {
MediaMessage::PlayerPause => { if let PlayerStatus::Paused(_) = status {
if let PlayerStatus::Playing(_) = status { MediaBackend::player_play(name, &connection).await;
instance_guard.active_player_pause(&connection).await;
}
}
MediaMessage::PlayerStop => {
if let PlayerStatus::Playing(_) | PlayerStatus::Paused(_) =
status
{
instance_guard.active_player_stop(&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);
}
}
_ => {}
} }
} }
} }
@@ -545,6 +551,13 @@ impl BackendThread<MediaMessage, Sender<MediaMessage>> for MediaBackend {
} }
impl MediaBackend { impl MediaBackend {
pub fn new() -> Self {
Self {
subscribers: vec![],
players: HashMap::new(),
active_player: None,
}
}
async fn find_existing_players( async fn find_existing_players(
connection: &zbus::Connection, connection: &zbus::Connection,
instance: Arc<Mutex<Self>>, instance: Arc<Mutex<Self>>,
@@ -564,7 +577,7 @@ impl MediaBackend {
.collect(); .collect();
for player in players { for player in players {
Self::spawn_new_player_watcher(&connection, &instance, &player); Self::spawn_new_player_watcher(connection, &instance, &player);
} }
Ok(()) Ok(())
@@ -669,11 +682,11 @@ impl MediaBackend {
continue; continue;
} }
if let Some(value) = args.changed_properties().get("PlaybackStatus") { if let Some(value) = args.changed_properties().get("PlaybackStatus")
if let Ok(status) = <String>::try_from(value.clone()) { && 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, "Player ({name}) status has changed to: '{status}'");
} Self::insert_or_update_player(&instance, &props, name.into(), status).await;
} }
} }
debug!(&LOGGER, "(2) Removing player: {name}"); debug!(&LOGGER, "(2) Removing player: {name}");
@@ -688,7 +701,7 @@ impl MediaBackend {
player_name: String, player_name: String,
player_status: String, player_status: String,
) { ) {
match Self::get_player_status_from_string(&props, player_status).await { match Self::get_player_status_from_string(props, player_status).await {
Ok(status) => { Ok(status) => {
let mut instance_guard = instance.lock().unwrap(); let mut instance_guard = instance.lock().unwrap();
instance_guard.players.insert(player_name.clone(), status); instance_guard.players.insert(player_name.clone(), status);
@@ -745,60 +758,35 @@ impl MediaBackend {
} }
} }
async fn active_player_play(&self, connection: &zbus::Connection) { async fn player_play(name: &str, connection: &zbus::Connection) {
if let Some(player_name) = &self.active_player { if let Ok(proxy) =
if let Ok(proxy) = zbus::Proxy::new( zbus::Proxy::new(connection, name, MPRIS_PATH, MPRIS_PLAYER_INTERFACE).await
&connection, && let Err(e) = proxy.call_method("Play", &()).await
player_name.as_str(), {
MPRIS_PATH, warn!(&LOGGER, "Error starting play for player: {name}, {e}");
MPRIS_PLAYER_INTERFACE,
)
.await
{
if let Err(e) = proxy.call_method("Play", &()).await {
warn!(
&LOGGER,
"Error starting play for player: {player_name}, {e}"
);
}
}
} }
} }
async fn active_player_pause(&mut self, connection: &zbus::Connection) { async fn player_pause(name: &str, connection: &zbus::Connection) {
if let Some(player_name) = &self.active_player { if let Ok(proxy) =
if let Ok(proxy) = zbus::Proxy::new( zbus::Proxy::new(connection, name, MPRIS_PATH, MPRIS_PLAYER_INTERFACE).await
&connection, && let Err(e) = proxy.call_method("Pause", &()).await
player_name.as_str(), {
MPRIS_PATH, warn!(&LOGGER, "Error pausing player: {name}, {e}");
MPRIS_PLAYER_INTERFACE,
)
.await
{
if let Err(e) = proxy.call_method("Pause", &()).await {
warn!(&LOGGER, "Error pausing player: {player_name}, {e}");
}
}
self.select_active_player(self.active_player.clone());
} }
} }
async fn active_player_stop(&mut self, connection: &zbus::Connection) { async fn player_stop(name: &str, connection: &zbus::Connection) {
if let Some(player_name) = &self.active_player { if let Ok(proxy) =
if let Ok(proxy) = zbus::Proxy::new( zbus::Proxy::new(connection, name, MPRIS_PATH, MPRIS_PLAYER_INTERFACE).await
&connection, && let Err(e) = proxy.call_method("Stop", &()).await
player_name.as_str(), {
MPRIS_PATH, warn!(&LOGGER, "Error stopping player: {name}, {e}");
MPRIS_PLAYER_INTERFACE,
)
.await
{
if let Err(e) = proxy.call_method("Stop", &()).await {
warn!(&LOGGER, "Error stopping player: {player_name}, {e}");
}
}
} }
self.select_active_player(None); }
fn update_active_player(&mut self) {
self.select_active_player(self.active_player.clone());
} }
fn select_active_player(&mut self, changed_player: Option<String>) { fn select_active_player(&mut self, changed_player: Option<String>) {

View File

@@ -143,12 +143,9 @@ impl BackendThread<BrightnessMessage, Sender<BrightnessMessage>> for BrightnessM
&self.subscribers &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 (f2b_tx, f2b_rx) = crossbeam_channel::unbounded::<BrightnessMessage>();
let instance = Arc::new(Mutex::new(Self { let instance = Arc::new(Mutex::new(self));
display_cache: HashMap::new(),
subscribers: Vec::new(),
}));
let instance_clone = instance.clone(); let instance_clone = instance.clone();
thread::spawn(move || { thread::spawn(move || {
while let Ok(tx) = registration_rx.recv() { while let Ok(tx) = registration_rx.recv() {
@@ -246,6 +243,12 @@ enum Operation {
} }
impl BrightnessManager { impl BrightnessManager {
pub fn new() -> Self {
Self {
subscribers: vec![],
display_cache: HashMap::new(),
}
}
pub fn set_brightness(&mut self, connector: String, percent: u8) { pub fn set_brightness(&mut self, connector: String, percent: u8) {
if !self.display_cache.contains_key(&connector) { if !self.display_cache.contains_key(&connector) {
self.register_display(connector.clone()); self.register_display(connector.clone());

View File

@@ -1,26 +1,33 @@
use crate::LOGGER; use crate::LOGGER;
use crate::backend::{BackendThread, SimpleBackendThread}; use crate::backend::{BackendThread, SimpleBackendThread};
use crate::ctl::{ControlCommand, SOCKET_PATH};
use crossbeam_channel::{Receiver, Sender}; use crossbeam_channel::{Receiver, Sender};
use jmb_bar::{ControlCommand, SOCKET_PATH};
use jmb_logger_rs::*; use jmb_logger_rs::*;
use std::io::Read; use std::io::Read;
use std::net::Shutdown; use std::net::Shutdown;
use std::os::unix::net::UnixListener; use std::os::unix::net::UnixListener;
use std::str::FromStr;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
pub struct ControlSocket { pub struct ControlSocket {
subscribers: Vec<Sender<ControlCommand>>, subscribers: Vec<Sender<ControlCommand>>,
} }
impl ControlSocket {
pub fn new() -> Self {
Self {
subscribers: vec![],
}
}
}
impl SimpleBackendThread<ControlCommand> for ControlSocket { impl SimpleBackendThread<ControlCommand> for ControlSocket {
fn subscribers(&self) -> &Vec<Sender<ControlCommand>> { fn subscribers(&self) -> &Vec<Sender<ControlCommand>> {
&self.subscribers &self.subscribers
} }
fn init(registration_rx: Receiver<Sender<ControlCommand>>) { fn init(self, registration_rx: Receiver<Sender<ControlCommand>>) {
let instance = Arc::new(Mutex::new(Self { let instance = Arc::new(Mutex::new(self));
subscribers: Vec::new(),
}));
let instance_clone = instance.clone(); let instance_clone = instance.clone();
std::thread::spawn(move || { std::thread::spawn(move || {
while let Ok(sender) = registration_rx.recv() { while let Ok(sender) = registration_rx.recv() {
@@ -28,11 +35,11 @@ impl SimpleBackendThread<ControlCommand> for ControlSocket {
} }
}); });
std::thread::spawn(move || { std::thread::spawn(move || {
if std::fs::metadata(SOCKET_PATH).is_ok() { if std::fs::metadata(SOCKET_PATH).is_ok()
if let Err(e) = std::fs::remove_file(SOCKET_PATH) { && let Err(e) = std::fs::remove_file(SOCKET_PATH)
error!(&LOGGER, "Error deleting old socket file: {e}"); {
return; error!(&LOGGER, "Error deleting old socket file: {e}");
} return;
} }
let socket = UnixListener::bind(SOCKET_PATH); let socket = UnixListener::bind(SOCKET_PATH);
let Ok(socket) = socket else { let Ok(socket) = socket else {
@@ -52,7 +59,7 @@ impl SimpleBackendThread<ControlCommand> for ControlSocket {
let _ = stream.shutdown(Shutdown::Both); let _ = stream.shutdown(Shutdown::Both);
continue; continue;
} }
if let Some(command) = ControlCommand::from_str(&read) { if let Ok(command) = ControlCommand::from_str(&read) {
instance.lock().unwrap().publish(command); 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::backend::{BackendThread, Registrator};
use crate::config::Config;
use crate::{APP_NAME, LOGGER}; use crate::{APP_NAME, LOGGER};
use crossbeam_channel::{Receiver, Sender}; use crossbeam_channel::{Receiver, Sender};
use jmb_bar::config::IdleConfig;
use jmb_logger_rs::*; use jmb_logger_rs::*;
use std::collections::HashMap; use std::collections::HashMap;
use std::error::Error; use std::error::Error;
@@ -100,6 +100,7 @@ pub enum IdleMessage {
#[derive(Debug)] #[derive(Debug)]
pub struct IdleDaemon { pub struct IdleDaemon {
subscribers: Vec<Sender<IdleState>>, subscribers: Vec<Sender<IdleState>>,
config: IdleConfig,
} }
impl BackendThread<IdleState, Sender<IdleMessage>> for IdleDaemon { impl BackendThread<IdleState, Sender<IdleMessage>> for IdleDaemon {
@@ -121,12 +122,9 @@ impl BackendThread<IdleState, Sender<IdleMessage>> for IdleDaemon {
&self.subscribers &self.subscribers
} }
fn init(registration_rx: Receiver<Sender<IdleState>>) { fn init(self, registration_rx: Receiver<Sender<IdleState>>) {
let config = Config::load();
let (tx, rx) = crossbeam_channel::unbounded::<IdleMessage>(); let (tx, rx) = crossbeam_channel::unbounded::<IdleMessage>();
let instance = Arc::new(Mutex::new(Self { let instance = Arc::new(Mutex::new(self));
subscribers: Vec::new(),
}));
std::thread::spawn({ std::thread::spawn({
let instance = instance.clone(); let instance = instance.clone();
let tx = tx.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 (wl_tx, wl_rx) = watch::channel(false);
let (dbus_tx, dbus_rx) = watch::channel(false); let (dbus_tx, dbus_rx) = watch::channel(false);
std::thread::spawn({ std::thread::spawn({
let config = config.clone(); let instance = instance.clone();
move || { 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}"); 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; let res = IdleDaemon::start_dbus(dbus_tx, instance_clone).await;
match res { match res {
Ok((_, _)) => { 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}"), 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 { match self {
Self::Dim => Duration::from_secs(cfg.dim_timeout_delta()), Self::Dim => Duration::from_secs(cfg.dim_timeout_delta()),
Self::TurnOff => Duration::from_secs(cfg.lock_timeout_delta()), Self::TurnOff => Duration::from_secs(cfg.lock_timeout_delta()),
@@ -238,11 +236,16 @@ impl IdleStage {
} }
impl IdleDaemon { impl IdleDaemon {
pub fn new(config: IdleConfig) -> Self {
Self {
subscribers: vec![],
config,
}
}
async fn run_idle_timer( async fn run_idle_timer(
instance: Arc<Mutex<Self>>, instance: Arc<Mutex<Self>>,
mut wl_rx: watch::Receiver<bool>, mut wl_rx: watch::Receiver<bool>,
mut dbus_rx: watch::Receiver<bool>, mut dbus_rx: watch::Receiver<bool>,
config: &Config,
) { ) {
loop { loop {
if let Err(e) = wl_rx.changed().await { if let Err(e) = wl_rx.changed().await {
@@ -254,12 +257,13 @@ impl IdleDaemon {
if wl_idle && !dbus_state { if wl_idle && !dbus_state {
let mut stage = IdleStage::Dim; let mut stage = IdleStage::Dim;
loop { loop {
let timeout = stage.timeout(&instance.lock().unwrap().config);
tokio::select! { tokio::select! {
_ = sleep(stage.timeout(config)) => { _ = sleep(timeout) => {
match stage { match stage {
IdleStage::Dim => instance.lock().unwrap().publish(IdleState::Dimmed), IdleStage::Dim => instance.lock().unwrap().publish(IdleState::Dimmed),
IdleStage::TurnOff => _ = Command::new("hyprlock").spawn(), IdleStage::TurnOff => _ = Command::new("bash").arg("-c").arg(format!("'{}'", instance.lock().unwrap().config.lock_command())).spawn(),
IdleStage::Sleep => _ = Command::new("systemctl").arg("sleep").spawn(), IdleStage::Sleep => _ = Command::new("bash").arg("-c").arg(format!("'{}'", instance.lock().unwrap().config.sleep_command())).spawn(),
} }
stage = stage.next(); 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 conn = Connection::connect_to_env()?;
let (globals, mut event_queue) = registry_queue_init::<WlState>(&conn)?; let (globals, mut event_queue) = registry_queue_init::<WlState>(&conn)?;
let queue_handle = event_queue.handle(); let queue_handle = event_queue.handle();
@@ -298,7 +305,7 @@ impl IdleDaemon {
let mut state = WlState { idle: None, tx }; let mut state = WlState { idle: None, tx };
let _notification = notifier.get_idle_notification( let _notification = notifier.get_idle_notification(
(config.idle_timeout() * 1000) as u32, (instance.lock().unwrap().config.idle_timeout() * 1000) as u32,
&seat, &seat,
&queue_handle, &queue_handle,
(), (),

View File

@@ -37,6 +37,13 @@ impl State {
} }
impl JsonSocketBackend { 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>> { async fn _watch(instance: Arc<Mutex<Self>>) -> Result<(), Box<dyn Error + Send + Sync>> {
let socket_address = instance let socket_address = instance
.lock() .lock()
@@ -197,14 +204,8 @@ impl BackendThread<State, ()> for JsonSocketBackend {
&self.subscribers &self.subscribers
} }
fn init(registration_rx: Receiver<Sender<State>>) { fn init(self, registration_rx: Receiver<Sender<State>>) {
let state = initial_state_tailscale(); let instance = Arc::new(Mutex::new(self));
let instance = Arc::new(Mutex::new(Self {
subscribers: vec![],
state,
socket_address: String::from("/var/run/netbird-http.sock"),
}));
let instance_clone = instance.clone(); let instance_clone = instance.clone();
std::thread::spawn(move || { std::thread::spawn(move || {

View File

@@ -10,7 +10,9 @@ use nmrs::raw::zvariant::OwnedObjectPath;
use nmrs::{ActiveConnection, ConnectionError, NetworkEvent, NetworkManager, WifiSecurity}; use nmrs::{ActiveConnection, ConnectionError, NetworkEvent, NetworkManager, WifiSecurity};
use std::cmp::Ordering; use std::cmp::Ordering;
use std::pin::Pin; use std::pin::Pin;
use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use tokio::sync::Mutex;
use tokio::sync::mpsc::UnboundedSender; use tokio::sync::mpsc::UnboundedSender;
use tokio::time::{Instant, Sleep}; use tokio::time::{Instant, Sleep};
@@ -109,10 +111,10 @@ pub enum NetworkState {
ConnectSuccess(String), ConnectSuccess(String),
} }
#[derive(Debug, Clone)] #[derive(Debug)]
pub struct NetworkBackendHandle { pub struct NetworkBackend {
subscribers: Vec<Sender<NetworkState>>, subscribers: Vec<Sender<NetworkState>>,
command_tx: UnboundedSender<NetworkCommand>, command_tx: Option<UnboundedSender<NetworkCommand>>,
} }
struct EventDebouncer { struct EventDebouncer {
@@ -121,7 +123,7 @@ struct EventDebouncer {
sleep: Pin<Box<Sleep>>, sleep: Pin<Box<Sleep>>,
} }
impl BackendThread<NetworkState, UnboundedSender<NetworkCommand>> for NetworkBackendHandle { impl BackendThread<NetworkState, UnboundedSender<NetworkCommand>> for NetworkBackend {
fn subscribe( fn subscribe(
registrator: Sender<Sender<NetworkState>>, registrator: Sender<Sender<NetworkState>>,
) -> (Receiver<NetworkState>, UnboundedSender<NetworkCommand>) { ) -> (Receiver<NetworkState>, UnboundedSender<NetworkCommand>) {
@@ -140,12 +142,10 @@ impl BackendThread<NetworkState, UnboundedSender<NetworkCommand>> for NetworkBac
&self.subscribers &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 (command_tx, mut command_rx) = tokio::sync::mpsc::unbounded_channel::<NetworkCommand>();
let mut instance = Self { self.command_tx = Some(command_tx);
subscribers: vec![], let instance = Arc::new(Mutex::new(self));
command_tx,
};
std::thread::spawn(move || { std::thread::spawn(move || {
let runtime = tokio::runtime::Builder::new_current_thread() let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all() .enable_all()
@@ -162,8 +162,8 @@ impl BackendThread<NetworkState, UnboundedSender<NetworkCommand>> for NetworkBac
}; };
std::thread::spawn({ std::thread::spawn({
let instance = instance.clone();
let nm = nm.clone(); let nm = nm.clone();
let mut instance = instance.clone();
move || { move || {
let runtime = tokio::runtime::Builder::new_current_thread() let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all() .enable_all()
@@ -172,10 +172,12 @@ impl BackendThread<NetworkState, UnboundedSender<NetworkCommand>> for NetworkBac
runtime.block_on(async move { runtime.block_on(async move {
while let Ok(tx) = registration_rx.recv() { while let Ok(tx) = registration_rx.recv() {
if tx.send(NetworkState::Init(instance.command_tx.clone())).is_ok() { let mut instance_guard = instance.lock().await;
instance.subscribers.push(tx); if tx.send(NetworkState::Init(instance_guard.command_tx.clone().unwrap())).is_ok() {
let _ = instance.send_all_networks(&nm).await; instance_guard.subscribers.push(tx);
let _ = instance.send_active_connection(&nm).await; 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 // Run the command without stopping event processing
// while a potentially slow connection operation runs. // while a potentially slow connection operation runs.
let nm = nm.clone(); let nm = nm.clone();
let instance_clone = instance.clone(); let instance = instance.clone();
tokio::spawn(async move { tokio::spawn(
async move {
match command { 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) => { NetworkCommand::Connect(ssid, auth) => {
if let Ok(saved) = nm.list_saved_connections().await { if let Ok(saved) = nm.list_saved_connections().await {
if saved.iter().find(|c| c.id.eq(&ssid)).is_some() { 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 { } else {
match auth { match auth {
Auth::Open => { Auth::Open => {
@@ -221,19 +224,18 @@ impl BackendThread<NetworkState, UnboundedSender<NetworkCommand>> for NetworkBac
&& let Some(ap) = networks.iter().find(|net| net.ssid.eq(&ssid)) && let Some(ap) = networks.iter().find(|net| net.ssid.eq(&ssid))
{ {
if ap.security.is_open() { if ap.security.is_open() {
instance_clone.connect(&nm, ssid, WifiSecurity::Open).await; instance.lock().await.connect(&nm, ssid, WifiSecurity::Open).await;
} else { } 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; let _ = nm;
}); });
} }
@@ -241,7 +243,7 @@ impl BackendThread<NetworkState, UnboundedSender<NetworkCommand>> for NetworkBac
match event { match event {
Some(event_res) => { Some(event_res) => {
match 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:?}"), 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() => { _ = &mut debouncer.sleep, if debouncer.deadline.is_some() => {
debouncer.deadline = None; 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) { async fn connect(&self, nm: &NetworkManager, ssid: String, creds: WifiSecurity) {
let previous_ssid = nm.current_ssid().await; let previous_ssid = nm.current_ssid().await;
let res = nm.connect(&ssid, None, creds).await; let res = nm.connect(&ssid, None, creds).await;
@@ -431,44 +439,58 @@ impl NetworkBackendHandle {
async fn send_active_connection(&self, nm: &NetworkManager) { async fn send_active_connection(&self, nm: &NetworkManager) {
trace!(&LOGGER, "Sending Active Connection"); trace!(&LOGGER, "Sending Active Connection");
if let Ok(connections) = nm.list_active_connections().await { if let Ok(connections) = nm.list_active_connections().await {
let connection = if let Some(primary) = Self::primary_connection(nm).await { let Some(connection) = (if let Some(primary) = Self::primary_connection(nm).await {
connections.iter().find(|c| match c { connections
ActiveConnection::Wired(c) => c.uuid.eq(&primary), .iter()
ActiveConnection::Wifi(c) => c.uuid.eq(&primary), .find(|c| match c {
_ => false, ActiveConnection::Wired(c) => c.uuid.eq(&primary),
}) ActiveConnection::Wifi(c) => c.uuid.eq(&primary),
_ => false,
})
.cloned()
} else { } else {
connections.first() let mut iter = connections.into_iter();
}; loop {
match connection { let Some(conn) = iter.next() else {
Some(conn) => { break None;
};
match conn { match conn {
ActiveConnection::Wired(c) => { ActiveConnection::Other(_) | ActiveConnection::Vpn(_) => continue,
let info = NetworkInfo { other => {
name: c.id.clone(), break Some(other);
network_type: NetworkType::Wired,
signal: 0,
secured: None,
known: true,
};
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,
};
self.publish(NetworkState::ChangeNetwork(Some(info)));
}
_ => {}
} }
} }
None => { }) else {
self.publish(NetworkState::ChangeNetwork(None)); 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::LOGGER;
use crate::backend::gpu_utilization::I915UtilizationMonitor;
use crate::backend::{BackendThread, Registrator}; use crate::backend::{BackendThread, Registrator};
use all_smi::AllSmi;
use battery::State; use battery::State;
use battery::units::Time; use battery::units::Time;
use battery::units::power::watt; use battery::units::power::watt;
@@ -8,6 +8,7 @@ use battery::units::ratio::percent;
use battery::units::time::millisecond; use battery::units::time::millisecond;
use crossbeam_channel::{Receiver, Sender}; use crossbeam_channel::{Receiver, Sender};
use jmb_logger_rs::*; use jmb_logger_rs::*;
use std::sync::{Arc, Mutex};
use std::thread; use std::thread;
use std::time::Duration; use std::time::Duration;
use sysinfo::{Disks, System}; use sysinfo::{Disks, System};
@@ -19,7 +20,7 @@ pub struct Stats {
pub cpu: Option<u8>, pub cpu: Option<u8>,
pub ram: Option<u8>, pub ram: Option<u8>,
pub disks: Vec<(String, u8)>, pub disks: Vec<(String, u8)>,
pub gpus: Vec<(String, u8)>, pub gpu: Option<u8>,
pub battery_charge: u8, pub battery_charge: u8,
pub battery_state: State, pub battery_state: State,
pub battery_draw: f32, pub battery_draw: f32,
@@ -28,8 +29,14 @@ pub struct Stats {
pub struct StatsBackend { pub struct StatsBackend {
subscribers: Vec<Sender<Stats>>, subscribers: Vec<Sender<Stats>>,
system: Option<System>, }
disks: Option<Disks>,
impl StatsBackend {
pub fn new() -> Self {
Self {
subscribers: vec![],
}
}
} }
impl BackendThread<Stats, ()> for StatsBackend { impl BackendThread<Stats, ()> for StatsBackend {
@@ -43,24 +50,26 @@ impl BackendThread<Stats, ()> for StatsBackend {
&self.subscribers &self.subscribers
} }
fn init(registration_rx: Receiver<Sender<Stats>>) { fn init(self, registration_rx: Receiver<Sender<Stats>>) {
thread::spawn(move || { let instance = Arc::new(Mutex::new(self));
let mut instance = Self { thread::spawn({
subscribers: Vec::new(), let instance = instance.clone();
system: System::new().ok(), move || {
disks: Disks::new().ok(), while let Ok(tx) = registration_rx.recv() {
};
let manager = battery::Manager::new();
let smi = AllSmi::new();
loop {
while let Ok(tx) = registration_rx.try_recv() {
trace!(&LOGGER, "Added Stats Subscriber: {tx:?}"); 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 cpu = None;
let mut ram = 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_cpu_usage();
system.refresh_memory(); system.refresh_memory();
@@ -69,7 +78,7 @@ impl BackendThread<Stats, ()> for StatsBackend {
} }
let mut disk_stats = Vec::new(); let mut disk_stats = Vec::new();
if let Some(disks) = instance.disks.as_mut() { if let Some(disks) = disks.as_mut() {
disks.refresh(true); disks.refresh(true);
disk_stats = disks disk_stats = disks
.iter() .iter()
@@ -82,14 +91,7 @@ impl BackendThread<Stats, ()> for StatsBackend {
.collect(); .collect();
} }
let mut gpus: Vec<(String, u8)> = Vec::new(); let gpu = gpu_monitor.as_mut().map(|v| v.calc_utilization());
if let Ok(smi) = &smi {
gpus = smi
.get_gpu_info()
.iter()
.map(|info| (info.name.clone(), info.utilization as u8))
.collect();
}
let mut battery_charge: u8 = 255; // Use > 100 as invalid value let mut battery_charge: u8 = 255; // Use > 100 as invalid value
let mut battery_state = State::Unknown; let mut battery_state = State::Unknown;
@@ -128,13 +130,13 @@ impl BackendThread<Stats, ()> for StatsBackend {
cpu, cpu,
ram, ram,
disks: disk_stats, disks: disk_stats,
gpus, gpu,
battery_charge, battery_charge,
battery_state, battery_state,
battery_draw, battery_draw,
battery_time, battery_time,
}; };
instance.publish(msg); instance.lock().unwrap().publish(msg);
thread::sleep(Duration::from_millis(POLL_INTERVAL)); thread::sleep(Duration::from_millis(POLL_INTERVAL));
} }

View File

@@ -28,11 +28,9 @@ impl SimpleBackendThread<WindowManagerEvent> for HyprlandComms {
&self.subscribers &self.subscribers
} }
fn init(registration_rx: Receiver<Sender<WindowManagerEvent>>) { fn init(self, registration_rx: Receiver<Sender<WindowManagerEvent>>) {
std::thread::spawn(move || { std::thread::spawn(move || {
let instance = Arc::new(Mutex::new(Self { let instance = Arc::new(Mutex::new(self));
subscribers: vec![],
}));
let mut listener = EventListener::new(); let mut listener = EventListener::new();
let inst_clone = instance.clone(); let inst_clone = instance.clone();
@@ -153,6 +151,12 @@ impl SimpleBackendThread<WindowManagerEvent> for HyprlandComms {
} }
impl HyprlandComms { impl HyprlandComms {
pub fn new() -> Self {
Self {
subscribers: vec![],
}
}
fn monitor_id_from_workspace_id(id: WorkspaceId) -> Option<MonitorId> { fn monitor_id_from_workspace_id(id: WorkspaceId) -> Option<MonitorId> {
if let Ok(wss) = Workspaces::get() if let Ok(wss) = Workspaces::get()
&& let Some(ws) = wss.to_vec().iter().find(|ws| ws.id().unwrap() == id) && let Some(ws) = wss.to_vec().iter().find(|ws| ws.id().unwrap() == id)
@@ -162,7 +166,7 @@ impl HyprlandComms {
None None
} }
pub fn current_monitor() -> MonitorId { pub fn _current_monitor() -> MonitorId {
if let Ok(monitor) = Monitor::get_active() { if let Ok(monitor) = Monitor::get_active() {
return monitor.id; return monitor.id;
} }

View File

@@ -1,30 +1,11 @@
use jmb_bar::{ControlCommand, SOCKET_PATH};
use jmb_logger_rs::*; use jmb_logger_rs::*;
use std::env; use std::env;
use std::io::Write; use std::io::Write;
use std::os::unix::net::UnixStream; use std::os::unix::net::UnixStream;
use std::str::FromStr;
const CTL_LOGGER: Logger = Logger::create_with_custom_format(Level::DEBUG, String::new()); 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() { fn main() {
info!(&CTL_LOGGER, "Hello, this is mission control"); info!(&CTL_LOGGER, "Hello, this is mission control");
@@ -43,7 +24,7 @@ fn main() {
let args: Vec<String> = env::args().collect(); let args: Vec<String> = env::args().collect();
match args.get(1) { 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()) .write(cmd.to_string().as_bytes())
.expect("Error sending message"), .expect("Error sending message"),
_ => 0usize, _ => 0usize,

View File

@@ -1,34 +1,44 @@
use serde::Deserialize; use serde::Deserialize;
use std::default::Default;
use std::fs; 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)] #[derive(Deserialize, Debug, Clone)]
pub(crate) struct Config { pub struct IdleConfig {
idle_timeout: u64, idle_timeout: u64,
dim_timeout: u64, dim_timeout: u64,
lock_timeout: u64, lock_timeout: u64,
sleep_timeout: u64, sleep_timeout: u64,
lock_command: String,
sleep_command: String,
} }
impl Default for Config { impl Default for IdleConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
idle_timeout: 20, idle_timeout: 20,
dim_timeout: 60, dim_timeout: 60,
lock_timeout: 300, lock_timeout: 300,
sleep_timeout: 320, sleep_timeout: 320,
lock_command: String::default(),
sleep_command: String::default(),
} }
} }
} }
impl Config { impl IdleConfig {
pub fn load() -> Config {
fs::read_to_string("jmb-bar.conf")
.ok()
.map(|s| toml::from_str::<Config>(&s).ok())
.flatten()
.unwrap_or_default()
}
pub fn idle_timeout(&self) -> u64 { pub fn idle_timeout(&self) -> u64 {
self.idle_timeout self.idle_timeout
} }
@@ -56,4 +66,12 @@ impl Config {
pub fn sleep_timeout_delta(&self) -> u64 { pub fn sleep_timeout_delta(&self) -> u64 {
self.sleep_timeout - self.lock_timeout_delta() 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; use crate::backend::Backend;
mod backend; mod backend;
mod config;
mod css; mod css;
mod ctl;
mod ui; mod ui;
use glib::clone; use glib::clone;
use gtk::prelude::*; use gtk::prelude::*;
use gtk::{Application, CssProvider, gdk, gio}; use gtk::{Application, CssProvider, gdk, gio};
use jmb_bar::config::Config;
use jmb_logger_rs::*; use jmb_logger_rs::*;
use notify::RecommendedWatcher; use notify::RecommendedWatcher;
use std::cell::RefCell; use std::cell::RefCell;
use std::process::Command;
use std::rc::Rc; use std::rc::Rc;
pub const LOGGER: Logger = Logger::create(Level::DEBUG); pub const LOGGER: Logger = Logger::create(Level::DEBUG);
@@ -20,16 +18,18 @@ pub const APP_NAME: &str = "net.jmb19905.jmb-bar";
fn main() { fn main() {
info!(&LOGGER, "Starting jmb-bar"); 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 app = Application::builder().application_id(APP_NAME).build();
let watcher_slot: Rc<RefCell<Option<RecommendedWatcher>>> = Rc::new(RefCell::new(None)); 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 |_| { app.connect_startup(move |_| {
debug!(&LOGGER, "Connect startup"); debug!(&LOGGER, "Connect startup");
let provider = CssProvider::new(); let provider = CssProvider::new();

1
src/theme.rs Normal file
View File

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

View File

@@ -2,7 +2,6 @@ use crate::LOGGER;
use crate::backend::audio::MediaMessage; use crate::backend::audio::MediaMessage;
use crate::backend::window_manager_hypr::{HyprlandComms, WindowManagerEvent}; use crate::backend::window_manager_hypr::{HyprlandComms, WindowManagerEvent};
use crate::backend::{Backend, BackendChannels, ChannelType}; use crate::backend::{Backend, BackendChannels, ChannelType};
use crate::ctl::ControlCommand;
use crate::ui::action_centre::ActionCentre; use crate::ui::action_centre::ActionCentre;
use crate::ui::run_menu::RunMenu; use crate::ui::run_menu::RunMenu;
use glib::ControlFlow; use glib::ControlFlow;
@@ -11,6 +10,7 @@ use gtk::gdk::Display;
use gtk::prelude::{BoxExt, ButtonExt, DisplayExt, GtkWindowExt, ListModelExt, WidgetExt}; use gtk::prelude::{BoxExt, ButtonExt, DisplayExt, GtkWindowExt, ListModelExt, WidgetExt};
use gtk::{Align, Application, ApplicationWindow, Button, Orientation, gdk}; use gtk::{Align, Application, ApplicationWindow, Button, Orientation, gdk};
use gtk4_layer_shell::{Edge, KeyboardMode, Layer, LayerShell}; use gtk4_layer_shell::{Edge, KeyboardMode, Layer, LayerShell};
use jmb_bar::ControlCommand;
use jmb_logger_rs::*; use jmb_logger_rs::*;
use std::collections::HashMap; use std::collections::HashMap;
use std::rc::Rc; use std::rc::Rc;
@@ -29,11 +29,11 @@ pub fn create_bars(app: &Application, backend: Backend) {
let display = Display::default().expect("Couldn't find GDK Display"); let display = Display::default().expect("Couldn't find GDK Display");
let app_clone = app.clone(); let app_clone = app.clone();
let bars: Rc<Mutex<HashMap<u32, ApplicationWindow>>> = Rc::new(Mutex::new(HashMap::new())); 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); let action_centre = ActionCentre::create_action_centre(app.clone(), ac_channels);
for monitor in 0..display.monitors().n_items() { for monitor in 0..display.monitors().n_items() {
debug!(&LOGGER, "Opening bar {monitor}"); 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( bars.lock().expect("Couldn't get active bars").insert(
monitor, monitor,
create_and_show_bar( create_and_show_bar(
@@ -46,9 +46,9 @@ pub fn create_bars(app: &Application, backend: Backend) {
); );
} }
let backend_clone = backend.clone(); let backend_clone = backend.clone();
display display.monitors().connect_items_changed({
.monitors() let action_centre = action_centre.clone();
.connect_items_changed(move |_list, pos, removed, added| { move |_list, pos, removed, added| {
if removed == 1 { if removed == 1 {
debug!(&LOGGER, "Detected Monitor disconnect {pos}"); debug!(&LOGGER, "Detected Monitor disconnect {pos}");
if let Some(bar) = bars.lock().expect("Couldn't get active bars").remove(&pos) { if let Some(bar) = bars.lock().expect("Couldn't get active bars").remove(&pos) {
@@ -58,7 +58,7 @@ pub fn create_bars(app: &Application, backend: Backend) {
debug!(&LOGGER, "Detected new monitor: {pos}"); debug!(&LOGGER, "Detected new monitor: {pos}");
let app_clone = app_clone.clone(); let app_clone = app_clone.clone();
let action_centre = action_centre.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(); let bars = bars.clone();
glib::idle_add_local_once(move || { glib::idle_add_local_once(move || {
let display = Display::default().expect("Couldn't find GDK Display"); let display = Display::default().expect("Couldn't find GDK Display");
@@ -68,21 +68,24 @@ pub fn create_bars(app: &Application, backend: Backend) {
); );
}); });
} }
}); }
});
let mut ctl_channel = backend.subscribe(&[ChannelType::Control]); let mut ctl_channel = backend.subscribe(&[ChannelType::Control]);
let ctl_rx = ctl_channel.take_ctl_rx().unwrap(); let ctl_rx = ctl_channel.take_ctl_rx().unwrap();
let app_clone = app.clone(); let app_clone = app.clone();
let mut menu = RunMenu::create(app_clone); let mut menu = RunMenu::create(app_clone);
glib::timeout_add_local(Duration::from_millis(100), move || { glib::timeout_add_local(Duration::from_millis(100), {
while let Ok(cmd) = ctl_rx.try_recv() { let action_centre = action_centre.clone();
match cmd { move || {
ControlCommand::OpenRunMenu => { while let Ok(cmd) = ctl_rx.try_recv() {
menu.show(); match cmd {
ControlCommand::OpenRunMenu => menu.toggle(),
ControlCommand::OpenNetMenu => action_centre.toggle_active_monitor(),
} }
} }
ControlFlow::Continue
} }
ControlFlow::Continue
}); });
} }

View File

@@ -2,6 +2,7 @@ use crate::backend::BackendChannels;
use crate::backend::network::{ use crate::backend::network::{
Auth, AuthType, ConnectError, NetworkCommand, NetworkInfo, NetworkState, Auth, AuthType, ConnectError, NetworkCommand, NetworkInfo, NetworkState,
}; };
use crate::backend::window_manager_hypr::HyprlandComms;
use crate::ui::widgets::{LabelEntry, WifiWidget}; use crate::ui::widgets::{LabelEntry, WifiWidget};
use glib::prelude::Cast; use glib::prelude::Cast;
use glib::{ControlFlow, clone}; use glib::{ControlFlow, clone};
@@ -58,7 +59,7 @@ impl ActionCentre {
window.set_margin(Edge::Top, 20); window.set_margin(Edge::Top, 20);
window.set_margin(Edge::Right, 20); window.set_margin(Edge::Right, 20);
window.set_margin(Edge::Bottom, 20); window.set_margin(Edge::Bottom, 20);
window.set_keyboard_mode(KeyboardMode::OnDemand); window.set_keyboard_mode(KeyboardMode::Exclusive);
let instance = Self { let instance = Self {
window: window.clone(), window: window.clone(),
@@ -116,7 +117,7 @@ impl ActionCentre {
let instance = instance.clone(); let instance = instance.clone();
move |_, key, _, _| { move |_, key, _, _| {
if key == gdk::Key::Escape { if key == gdk::Key::Escape {
instance.hide_action_centre(); instance.hide();
glib::Propagation::Stop glib::Propagation::Stop
} else { } else {
glib::Propagation::Proceed 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 window = &self.window;
let monitors = display.monitors(); let monitors = display.monitors();
let Some(monitor) = monitors.item(monitor_idx) else { 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(); let clone = self.clone();
self.animate_margin( self.animate_margin(
20, 20,

View File

@@ -10,6 +10,7 @@ use gtk::prelude::{
}; };
use gtk::{Align, Application, ApplicationWindow, Orientation, PolicyType, gdk}; use gtk::{Align, Application, ApplicationWindow, Orientation, PolicyType, gdk};
use gtk4_layer_shell::{Edge, KeyboardMode, Layer, LayerShell}; use gtk4_layer_shell::{Edge, KeyboardMode, Layer, LayerShell};
use jmb_bar::theme;
use jmb_logger_rs::*; use jmb_logger_rs::*;
use nucleo_matcher::pattern::{CaseMatching, Normalization, Pattern}; use nucleo_matcher::pattern::{CaseMatching, Normalization, Pattern};
use nucleo_matcher::{Config, Matcher}; use nucleo_matcher::{Config, Matcher};
@@ -66,17 +67,14 @@ impl RunMenu {
window.set_child(Some(&v_box)); window.set_child(Some(&v_box));
let display = Display::default().expect("Couldn't get display"); let display = Display::default().expect("Couldn't get display");
let monitor = display let monitor = HyprlandComms::cursor_monitor()
.monitors() .and_then(|id| display.monitors().item(id as u32))
.item(HyprlandComms::current_monitor() as u32)
.expect("Couldn't get monitor") .expect("Couldn't get monitor")
.downcast::<Monitor>() .downcast::<Monitor>()
.ok(); .ok();
window.set_monitor(monitor.as_ref()); window.set_monitor(monitor.as_ref());
if let Some(monitor) = monitor { if let Some(monitor) = monitor {
let geometry = monitor.geometry(); let geometry = monitor.geometry();
let (_, natural_height, _, _) =
window.child().unwrap().measure(Orientation::Vertical, -1);
let top_margin = (geometry.height()) / 2; let top_margin = (geometry.height()) / 2;
window.set_margin(Edge::Top, top_margin - 100); window.set_margin(Edge::Top, top_margin - 100);
} else { } else {
@@ -94,22 +92,42 @@ impl RunMenu {
entry.connect_changed(clone!( entry.connect_changed(clone!(
#[strong] #[strong]
content_box, content_box,
move |e| { move |entry| {
while let Some(child) = content_box.first_child() { while let Some(child) = content_box.first_child() {
content_box.remove(&child); content_box.remove(&child);
} }
if e.text_length() > 0 { if entry.text_length() > 0 {
let text = e.text(); let text = entry.text();
if text.starts_with("=") { if let Some(eval) = text.strip_prefix("=") {
if let Ok(eval) = meval::eval_str(&text[1..]) { let new_eval = eval.replace("**", "^");
let label = gtk::Label::new(Some(&format!("{eval}"))); 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.add_css_class("exec-button");
label.set_halign(Align::Start); label.set_halign(Align::Start);
label.set_hexpand(true); label.set_hexpand(true);
content_box.append(&label); content_box.append(&label);
instance_clone.window.set_default_size(500, -1);
} }
} else { } 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); instance_clone.window.set_default_size(500, -1);
} }
} }
@@ -132,10 +150,17 @@ impl RunMenu {
#[strong] #[strong]
content_box, content_box,
move |_| { move |_| {
if let Some(child) = content_box.first_child() if let Some(child) = content_box.first_child() {
&& let Ok(button) = child.downcast::<gtk::Button>() if let Ok(button) = child.clone().downcast::<gtk::Button>() {
{ button.activate();
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
));
}
} }
} }
)); ));
@@ -164,8 +189,16 @@ impl RunMenu {
self.window.present(); self.window.present();
} }
fn create_entry(&self, content_box: &gtk::Box, entry: &String) -> gtk::Button { pub fn toggle(&mut self) {
let entry_clone = entry.clone(); 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(); let button = gtk::Button::new();
button.set_halign(Align::Fill); button.set_halign(Align::Fill);
button.set_hexpand(true); button.set_hexpand(true);
@@ -198,14 +231,14 @@ impl RunMenu {
content_box: &gtk::Box, content_box: &gtk::Box,
matcher: &mut Matcher, matcher: &mut Matcher,
) -> bool { ) -> 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 first = true;
let mut added = false; let mut added = false;
for (entry, _) in Pattern::parse(&entry.text(), CaseMatching::Ignore, Normalization::Smart) for (entry, _) in Pattern::parse(&entry.text(), CaseMatching::Ignore, Normalization::Smart)
.match_list(keys, matcher) .match_list(keys, matcher)
.iter() .iter()
{ {
let button = self.create_entry(content_box, &entry); let button = self.create_entry(content_box, entry);
if first { if first {
button.add_css_class("suggested"); button.add_css_class("suggested");
first = false; first = false;

View File

@@ -10,28 +10,25 @@ pub fn _build() -> gtk::Box {
let host = TrayHost::new(); let host = TrayHost::new();
if let Ok(mut host) = host { if let Ok(mut host) = host {
glib::timeout_add_local( glib::timeout_add_local(Duration::from_millis(1_000), move || {
Duration::from_millis(1_000), for event in host.poll().unwrap() {
move || { match event {
for event in host.poll().unwrap() { TrayEvent::ItemAdded(id) | TrayEvent::ItemChanged(id) => {
match event { if let Some(item) = host.items().get(&id) {
TrayEvent::ItemAdded(id) | TrayEvent::ItemChanged(id) => { println!("tray: {} — {}", item.title, item.status);
if let Some(item) = host.items().get(&id) { println!(" category: {}", item.category);
println!("tray: {} — {}", item.title, item.status); println!(" has menu: {}", item.has_menu());
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 { } else {
error!(&LOGGER, "Couldn't get SystemTray"); error!(&LOGGER, "Couldn't get SystemTray");
} }

View File

@@ -7,6 +7,7 @@ use crate::backend::{BackendChannels, json_socket};
use crate::ui::action_centre::ActionCentre; use crate::ui::action_centre::ActionCentre;
use crate::ui::widgets::{ExpandableBarItem, IconSlider}; use crate::ui::widgets::{ExpandableBarItem, IconSlider};
use battery::State; use battery::State;
use crossbeam_channel::TryRecvError;
use glib::prelude::Cast; use glib::prelude::Cast;
use glib::subclass::prelude::ObjectSubclassIsExt; use glib::subclass::prelude::ObjectSubclassIsExt;
use glib::{ControlFlow, clone}; use glib::{ControlFlow, clone};
@@ -205,9 +206,9 @@ pub fn build(
display, display,
move |_| { move |_| {
if action_centre.window.is_visible() { if action_centre.window.is_visible() {
action_centre.hide_action_centre(); action_centre.hide();
} else { } else {
action_centre.show_action_centre(monitor_idx, &display); action_centre.show(monitor_idx, &display);
} }
} }
)); ));
@@ -390,43 +391,61 @@ pub fn build(
} }
} }
} }
while let Ok(result) = net_rx.try_recv() { loop {
trace!(&LOGGER, "NetworkResult: {:?}", result); match net_rx.try_recv() {
match result { Ok(result) => {
NetworkState::ChangeNetwork(cfg) => { trace!(&LOGGER, "NetworkResult: {:?}", result);
match cfg { match result {
Some(network) => { NetworkState::ChangeNetwork(cfg) => {
match network.network_type { println!("Change network: {cfg:?}");
NetworkType::Wifi => { match cfg {
//let idx = ((20.0 / 100.0) * (WIFI_ICON_PATHS.len() - 1) as f32 + 0.5) as usize; Some(network) => {
let idx = ((network.signal as f32 / 100.0) match network.network_type {
* (WIFI_ICON_PATHS.len() - 1) as f32 NetworkType::Wifi => {
+ 0.5) //let idx = ((20.0 / 100.0) * (WIFI_ICON_PATHS.len() - 1) as f32 + 0.5) as usize;
as usize; let idx = ((network.signal as f32 / 100.0)
network_icon.set_from_file(Some(WIFI_ICON_PATHS[idx])); * (WIFI_ICON_PATHS.len() - 1) as f32
network_label.set_label(&network.name); + 0.5)
network_label.set_visible(true); 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 => { None => {
network_icon.set_from_file(Some("icons/ethernet.svg")); network_icon.set_from_file(Some("icons/disconnected.svg"));
network_label.set_visible(false); network_label.set_visible(false);
} }
} }
} }
None => { NetworkState::VpnState { connected, profile }
network_icon.set_from_file(Some("icons/disconnected.svg")); if profile.as_str() == "wg" =>
network_label.set_visible(false); {
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() { 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() { if let Some(gpu) = stats.gpu {
gpu_label.set_label(&format!("{}%", gpu.1)); gpu_label.set_label(&format!("{}%", gpu));
} }
set_battery_css_classes(&battery_widget, stats.battery_state, stats.battery_charge); 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; $background: #232320;
$background1: #3c3836; $background1: #3c3836;