Add scss colors into rust via build-script, flesh out math evaluation in launcher, Restructure some code

This commit is contained in:
jmb05
2026-11-02 19:31:21 +01:00
parent 11fb03427c
commit c3ad6f38cf
15 changed files with 167 additions and 114 deletions

1
Cargo.lock generated
View File

@@ -1598,6 +1598,7 @@ dependencies = [
"nmrs", "nmrs",
"notify", "notify",
"nucleo-matcher", "nucleo-matcher",
"regex",
"rustsni", "rustsni",
"serde", "serde",
"serde_json", "serde_json",

View File

@@ -9,7 +9,7 @@ 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"
@@ -49,3 +49,5 @@ 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

@@ -17,9 +17,9 @@ use crate::backend::json_socket::{JsonSocketBackend, State};
use crate::backend::network::{NetworkBackendHandle, NetworkCommand, NetworkState}; use crate::backend::network::{NetworkBackendHandle, 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::config::Config;
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;
@@ -86,6 +86,7 @@ pub struct Backend {
control_registrator: Registrator<ControlCommand>, control_registrator: Registrator<ControlCommand>,
} }
#[derive(PartialEq)]
pub enum ChannelType { pub enum ChannelType {
Net, Net,
Hypr, Hypr,
@@ -174,8 +175,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,
@@ -185,7 +186,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

@@ -516,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;
&& let Some(status) = instance_guard.players.get(name) };
{ let Some(status) = instance.lock().unwrap().players.get(name).cloned()
match msg { else {
MediaMessage::PlayerPlay => { continue;
if let PlayerStatus::Paused(_) = status { };
instance_guard.active_player_play(&connection).await; match msg {
} MediaMessage::PlayerPlay => {
if let PlayerStatus::Paused(_) = status {
MediaBackend::player_play(name, &connection).await;
} }
MediaMessage::PlayerPause => {
if let PlayerStatus::Playing(_) = status {
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);
}
}
_ => {}
} }
} }
} }
@@ -756,54 +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) =
&& let Ok(proxy) = zbus::Proxy::new( zbus::Proxy::new(connection, name, MPRIS_PATH, MPRIS_PLAYER_INTERFACE).await
connection,
player_name.as_str(),
MPRIS_PATH,
MPRIS_PLAYER_INTERFACE,
)
.await
&& let Err(e) = proxy.call_method("Play", &()).await && let Err(e) = proxy.call_method("Play", &()).await
{ {
warn!( warn!(&LOGGER, "Error starting play for player: {name}, {e}");
&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) =
&& let Ok(proxy) = zbus::Proxy::new( zbus::Proxy::new(connection, name, MPRIS_PATH, MPRIS_PLAYER_INTERFACE).await
connection,
player_name.as_str(),
MPRIS_PATH,
MPRIS_PLAYER_INTERFACE,
)
.await
&& let Err(e) = proxy.call_method("Pause", &()).await && let Err(e) = proxy.call_method("Pause", &()).await
{ {
warn!(&LOGGER, "Error pausing player: {player_name}, {e}"); warn!(&LOGGER, "Error pausing 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) =
&& let Ok(proxy) = zbus::Proxy::new( zbus::Proxy::new(connection, name, MPRIS_PATH, MPRIS_PLAYER_INTERFACE).await
connection,
player_name.as_str(),
MPRIS_PATH,
MPRIS_PLAYER_INTERFACE,
)
.await
&& let Err(e) = proxy.call_method("Stop", &()).await && let Err(e) = proxy.call_method("Stop", &()).await
{ {
warn!(&LOGGER, "Error stopping player: {player_name}, {e}"); warn!(&LOGGER, "Error stopping 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

@@ -1,7 +1,7 @@
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;

View File

@@ -1,7 +1,7 @@
use crate::backend::{BackendThread, Registrator}; use crate::backend::{BackendThread, Registrator};
use crate::config::IdleConfig;
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;

View File

@@ -1,36 +1,11 @@
use jmb_bar::{ControlCommand, SOCKET_PATH};
use jmb_logger_rs::*; use jmb_logger_rs::*;
use std::env; use std::env;
use std::fmt::{Display, Formatter};
use std::io::Write; use std::io::Write;
use std::os::unix::net::UnixStream; use std::os::unix::net::UnixStream;
use std::str::FromStr; 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 Display for ControlCommand {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ControlCommand::OpenRunMenu => f.write_str("menu"),
}
}
}
impl FromStr for ControlCommand {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"menu" => Ok(ControlCommand::OpenRunMenu),
other => Err(format!("Unknown command: {other}")),
}
}
}
fn main() { fn main() {
info!(&CTL_LOGGER, "Hello, this is mission control"); info!(&CTL_LOGGER, "Hello, this is mission control");

View File

@@ -3,13 +3,13 @@ use std::default::Default;
use std::fs; use std::fs;
#[derive(Deserialize, Debug, Clone, Default)] #[derive(Deserialize, Debug, Clone, Default)]
pub(crate) struct Config { pub struct Config {
pub idle: IdleConfig, pub idle: IdleConfig,
} }
impl Config { impl Config {
pub fn load() -> Config { pub fn load() -> Config {
fs::read_to_string("jmb-bar.conf") fs::read_to_string("../jmb-bar.conf")
.ok() .ok()
.and_then(|s| toml::from_str::<Config>(&s).ok()) .and_then(|s| toml::from_str::<Config>(&s).ok())
.unwrap_or_default() .unwrap_or_default()
@@ -17,7 +17,7 @@ impl Config {
} }
#[derive(Deserialize, Debug, Clone)] #[derive(Deserialize, Debug, Clone)]
pub(crate) struct IdleConfig { pub struct IdleConfig {
idle_timeout: u64, idle_timeout: u64,
dim_timeout: u64, dim_timeout: u64,
lock_timeout: u64, lock_timeout: u64,

31
src/lib.rs Normal file
View File

@@ -0,0 +1,31 @@
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,
}
impl Display for ControlCommand {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ControlCommand::OpenRunMenu => f.write_str("menu"),
}
}
}
impl FromStr for ControlCommand {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"menu" => Ok(ControlCommand::OpenRunMenu),
other => Err(format!("Unknown command: {other}")),
}
}
}

View File

@@ -1,14 +1,12 @@
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 crate::config::Config;
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;

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

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};
@@ -92,20 +93,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 let Some(eval) = text.strip_prefix("=") { if let Some(eval) = text.strip_prefix("=") {
let label = gtk::Label::new(Some(eval)); let new_eval = eval.replace("**", "^");
label.add_css_class("exec-button"); if new_eval != eval {
label.set_halign(Align::Start); let cursor = entry.position() as usize;
label.set_hexpand(true); let removed_before_cursor = text[..cursor].matches("**").count();
content_box.append(&label); 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 { } 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);
} }
} }
@@ -128,10 +151,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
));
}
} }
} }
)); ));

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;