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]]
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"
@@ -26,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" }
@@ -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"] }
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"
[build-dependencies]
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() {
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;
@@ -13,11 +14,12 @@ 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)
}
}
@@ -81,6 +93,7 @@ pub struct Backend {
control_registrator: Registrator<ControlCommand>,
}
#[derive(PartialEq)]
pub enum ChannelType {
Net,
Hypr,
@@ -94,17 +107,17 @@ 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 audio_registrator = AudioBackend::start();
let media_registration = MediaBackend::start();
let netbird_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,
@@ -124,7 +137,7 @@ 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);
}
@@ -169,8 +182,8 @@ impl Backend {
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,
@@ -180,7 +193,9 @@ impl Backend {
ChannelType::Audio,
ChannelType::Media,
ChannelType::Hypr,
])
];
types.retain(|i| !channel_types.contains(i));
self.subscribe(&types)
}
}

View File

@@ -266,7 +266,16 @@ pub enum AudioMessage {
pub struct AudioBackend {
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 {
@@ -288,12 +297,10 @@ impl BackendThread<AudioMessage, Sender<AudioMessage>> for AudioBackend {
&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 instance = Arc::new(Mutex::new(Self {
subscribers: vec![],
command_tx,
}));
self.command_tx = Some(command_tx);
let instance = Arc::new(Mutex::new(self));
let (controller_tx, controller_rx) = crossbeam_channel::unbounded::<ControllerMsg>();
let notify = Arc::new(EventFD::new(0, EfdFlags::EFD_CLOEXEC).unwrap());
thread::spawn({
@@ -381,7 +388,8 @@ impl BackendThread<AudioMessage, Sender<AudioMessage>> for AudioBackend {
.lock()
.expect("Couldn't get lock")
.command_tx
.clone(),
.clone()
.unwrap(),
))
.is_ok()
{
@@ -460,18 +468,14 @@ impl BackendThread<MediaMessage, Sender<MediaMessage>> for MediaBackend {
&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 instance = Arc::new(Mutex::new(Self {
subscribers: vec![],
players: HashMap::new(),
active_player: None,
}));
let instance = Arc::new(Mutex::new(self));
thread::spawn({
let instance = instance.clone();
move || {
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();
if let Some(name) = &instance_guard.active_player
&& let Some(status) = instance_guard.players.get(name)
@@ -512,39 +516,48 @@ impl BackendThread<MediaMessage, Sender<MediaMessage>> for MediaBackend {
return;
};
while let Ok(msg) = rx.recv() {
let mut instance_guard = instance.lock().unwrap();
if let Some(name) = &instance_guard.active_player {
if let Some(status) = instance_guard.players.get(name) {
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 {
instance_guard.active_player_play(&connection).await;
MediaBackend::player_play(name, &connection).await;
}
}
MediaMessage::PlayerPause => {
if let PlayerStatus::Playing(_) = status {
instance_guard.active_player_pause(&connection).await;
MediaBackend::player_pause(name, &connection).await;
instance.lock().unwrap().update_active_player();
}
}
MediaMessage::PlayerStop => {
if let PlayerStatus::Playing(_) | PlayerStatus::Paused(_) =
status
{
instance_guard.active_player_stop(&connection).await;
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>>,
@@ -564,7 +577,7 @@ impl MediaBackend {
.collect();
for player in players {
Self::spawn_new_player_watcher(&connection, &instance, &player);
Self::spawn_new_player_watcher(connection, &instance, &player);
}
Ok(())
@@ -669,13 +682,13 @@ impl MediaBackend {
continue;
}
if let Some(value) = args.changed_properties().get("PlaybackStatus") {
if let Ok(status) = <String>::try_from(value.clone()) {
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);
@@ -688,7 +701,7 @@ impl MediaBackend {
player_name: 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) => {
let mut instance_guard = instance.lock().unwrap();
instance_guard.players.insert(player_name.clone(), status);
@@ -745,61 +758,36 @@ impl MediaBackend {
}
}
async fn active_player_play(&self, connection: &zbus::Connection) {
if let Some(player_name) = &self.active_player {
if let Ok(proxy) = zbus::Proxy::new(
&connection,
player_name.as_str(),
MPRIS_PATH,
MPRIS_PLAYER_INTERFACE,
)
.await
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
{
if let Err(e) = proxy.call_method("Play", &()).await {
warn!(
&LOGGER,
"Error starting play for player: {player_name}, {e}"
);
}
}
warn!(&LOGGER, "Error starting play for player: {name}, {e}");
}
}
async fn active_player_pause(&mut self, connection: &zbus::Connection) {
if let Some(player_name) = &self.active_player {
if let Ok(proxy) = zbus::Proxy::new(
&connection,
player_name.as_str(),
MPRIS_PATH,
MPRIS_PLAYER_INTERFACE,
)
.await
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
{
if let Err(e) = proxy.call_method("Pause", &()).await {
warn!(&LOGGER, "Error pausing player: {player_name}, {e}");
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());
}
}
async fn active_player_stop(&mut self, connection: &zbus::Connection) {
if let Some(player_name) = &self.active_player {
if let Ok(proxy) = zbus::Proxy::new(
&connection,
player_name.as_str(),
MPRIS_PATH,
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 select_active_player(&mut self, changed_player: Option<String>) {
let mut changed_also_playing = false;

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,12 +35,12 @@ 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) {
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 {
error!(&LOGGER, "Error creating socket {}", socket.err().unwrap());
@@ -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,18 +439,35 @@ 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 {
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()
let mut iter = connections.into_iter();
loop {
let Some(conn) = iter.next() else {
break None;
};
match connection {
Some(conn) => {
match conn {
ActiveConnection::Other(_) | ActiveConnection::Vpn(_) => continue,
other => {
break Some(other);
}
}
}
}) 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(),
@@ -451,6 +476,7 @@ impl NetworkBackendHandle {
secured: None,
known: true,
};
debug!(&LOGGER, "Connected to wired network");
self.publish(NetworkState::ChangeNetwork(Some(info)));
}
ActiveConnection::Wifi(c) => {
@@ -461,16 +487,12 @@ impl NetworkBackendHandle {
secured: None, // Not relevant here
known: true,
};
debug!(&LOGGER, "Connected to wireless network");
self.publish(NetworkState::ChangeNetwork(Some(info)));
}
_ => {}
}
}
None => {
self.publish(NetworkState::ChangeNetwork(None));
}
}
}
}
async fn toggle_vpn(&self, profile: String, nm: &NetworkManager) {

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

@@ -2,7 +2,6 @@ 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 glib::ControlFlow;
@@ -11,6 +10,7 @@ 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 std::collections::HashMap;
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 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(
@@ -46,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) {
@@ -58,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");
@@ -68,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 || {
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.show();
}
ControlCommand::OpenRunMenu => menu.toggle(),
ControlCommand::OpenNetMenu => action_centre.toggle_active_monitor(),
}
}
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,17 +67,14 @@ 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);
let top_margin = (geometry.height()) / 2;
window.set_margin(Edge::Top, top_margin - 100);
} else {
@@ -94,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);
}
}
@@ -132,10 +150,17 @@ impl RunMenu {
#[strong]
content_box,
move |_| {
if let Some(child) = content_box.first_child()
&& let Ok(button) = child.downcast::<gtk::Button>()
{
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
));
}
}
}
));
@@ -164,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);
@@ -198,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,9 +10,7 @@ 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 || {
glib::timeout_add_local(Duration::from_millis(1_000), move || {
for event in host.poll().unwrap() {
match event {
TrayEvent::ItemAdded(id) | TrayEvent::ItemChanged(id) => {
@@ -30,8 +28,7 @@ pub fn _build() -> gtk::Box {
}
}
ControlFlow::Continue
}
);
});
} else {
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::widgets::{ExpandableBarItem, IconSlider};
use battery::State;
use crossbeam_channel::TryRecvError;
use glib::prelude::Cast;
use glib::subclass::prelude::ObjectSubclassIsExt;
use glib::{ControlFlow, clone};
@@ -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);
}
}
));
@@ -390,10 +391,13 @@ pub fn build(
}
}
}
while let Ok(result) = net_rx.try_recv() {
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 {
@@ -403,12 +407,14 @@ pub fn build(
* (WIFI_ICON_PATHS.len() - 1) as f32
+ 0.5)
as usize;
network_icon.set_from_file(Some(WIFI_ICON_PATHS[idx]));
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_icon
.set_from_file(Some("icons/ethernet.svg"));
network_label.set_visible(false);
}
}
@@ -419,7 +425,9 @@ pub fn build(
}
}
}
NetworkState::VpnState { connected, profile } if profile.as_str() == "wg" => {
NetworkState::VpnState { connected, profile }
if profile.as_str() == "wg" =>
{
if connected {
wg_button.add_css_class("on");
} else {
@@ -429,6 +437,17 @@ pub fn build(
_ => {}
}
}
Err(e) => {
match e {
TryRecvError::Empty => {}
TryRecvError::Disconnected => {
warn!(&LOGGER, "Network channel disconnected")
}
}
break;
}
}
}
while let Ok(tailscale_state) = netbird_rx.try_recv() {
match tailscale_state {
json_socket::State::Disconnected => {
@@ -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;