diff --git a/Cargo.lock b/Cargo.lock index 2829349..f0933ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "ab_glyph" @@ -724,6 +724,12 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" +[[package]] +name = "bytesize" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e368af43e418a04d52505cf3dbc23dda4e3407ae2fa99fd0e4f308ce546acc" + [[package]] name = "cacache" version = "13.1.0" @@ -4323,6 +4329,7 @@ dependencies = [ "plotters", "reqwest", "reqwest-middleware", + "systemstat", "tokio", "which", ] @@ -4905,6 +4912,20 @@ dependencies = [ "version-compare", ] +[[package]] +name = "systemstat" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668a4db78b439df482c238f559e4ea869017f9e62ef0a059c8bfcd841a4df544" +dependencies = [ + "bytesize", + "lazy_static", + "libc", + "nom", + "time", + "winapi", +] + [[package]] name = "target-lexicon" version = "0.12.16" diff --git a/Cargo.toml b/Cargo.toml index 2b73045..31ed2ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ pipewire = "0.8.0" networkmanager = "0.4" dbus = "0.9" battery = "0.7.8" +systemstat = "0.2.4" [patch.crates-io] winit = { path = "../winit" } diff --git a/src/bar/battery.rs b/src/bar/battery.rs index 2f13e7d..9e5d5d3 100644 --- a/src/bar/battery.rs +++ b/src/bar/battery.rs @@ -1,13 +1,39 @@ -use battery::State; -use cushy::{styles::components::TextColor, widget::MakeWidget}; +use battery::{Battery, State}; +use cushy::{ + styles::components::TextColor, + value::{Destination, Dynamic}, + widget::MakeWidget, +}; -use crate::theme::{TEXT_BATTERY, WIDGET_PADDING}; +use crate::{ + rt::tokio_runtime, + theme::{TEXT_BATTERY, WIDGET_PADDING}, +}; const BATTERY_LOW: &str = "󱃍"; const BATTERY_CHARGING: [&str; 11] = ["󰢟", "󰢜", "󰂆", "󰂇", "󰂈", "󰢝", "󰂉", "󰢞", "󰂊", "󰂋", "󰂅"]; const BATTERY_NORMAL: [&str; 11] = ["󰂎", "󰁺", "󰁻", "󰁼", "󰁽", "󰁾", "󰁿", "󰂀", "󰂁", "󰂂", "󰁹"]; +const BATTERY_UNKNOWN: &str = "󰂑"; +const BATTERY_FULL: &str = "󱟢"; + +fn format_battery(battery: &Battery) -> String { + let state = battery.state(); + let charge = battery.state_of_charge(); + + let icon = match state { + State::Charging => BATTERY_CHARGING[(charge.value * 10.) as usize], + State::Discharging => BATTERY_NORMAL[(charge.value * 10.) as usize], + State::Empty => BATTERY_LOW, + State::Full => BATTERY_FULL, + State::Unknown | _ => BATTERY_UNKNOWN, + }; + + let percent = (charge.value * 100.) as u8; + format!(" {} {}% ", icon, percent) +} + pub fn battery() -> impl MakeWidget { let manager = battery::Manager::new(); let Ok(manager) = manager else { @@ -36,22 +62,33 @@ pub fn battery() -> impl MakeWidget { let Some(Ok(battery)) = batteries.next() else { return "".make_widget(); }; + let info = Dynamic::new(format_battery(&battery)); - let state = battery.state(); - let charge = battery.state_of_charge(); + tokio_runtime().spawn({ + let info = info.clone(); + async move { + let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(1)); + loop { + interval.tick().await; - let icon = match state { - State::Charging => BATTERY_CHARGING[(charge.value * 10.) as usize], - State::Discharging => BATTERY_NORMAL[(charge.value * 10.) as usize], - State::Empty => BATTERY_LOW, - State::Full => "󱟢", - State::Unknown | _ => "󰂑", - }; + let Ok(manager) = battery::Manager::new() else { + info.set(BATTERY_UNKNOWN.to_string()); + return; + }; + let Ok(mut batteries) = manager.batteries() else { + info.set(BATTERY_UNKNOWN.to_string()); + return; + }; + let Some(Ok(battery)) = batteries.next() else { + info.set(BATTERY_UNKNOWN.to_string()); + return; + }; + info.set(format_battery(&battery)); + } + } + }); - let percent = (charge.value * 100.) as u8; - - format!(" {} {}% ", icon, percent) - .with(&TextColor, TEXT_BATTERY) + info.with(&TextColor, TEXT_BATTERY) .pad_by(WIDGET_PADDING) .centered() .make_widget() diff --git a/src/bar/hypr.rs b/src/bar/hypr.rs new file mode 100644 index 0000000..597e001 --- /dev/null +++ b/src/bar/hypr.rs @@ -0,0 +1,155 @@ +use std::sync::Arc; + +use cushy::{ + styles::{ + components::{CornerRadius, TextColor, WidgetBackground}, + Styles, + }, + value::{Destination, Dynamic, Source}, + widget::{MakeWidget, WidgetList}, + widgets::Style, +}; +use hyprland::{ + data::{Clients, Devices, Monitors, Workspaces}, + event_listener::AsyncEventListener, + shared::{HyprData, HyprDataVec, HyprError}, +}; + +use crate::{ + rt::tokio_runtime, + theme::{ + BG_WORKSPACE_ACTIVE, TEXT_TOOL, TEXT_WORKSPACE_ACTIVE, WIDGET_PADDING, + WORKSPACE_CORNER_RADIUS, WORKSPACE_PADDING, + }, +}; + +use super::HyprlandState; + +pub fn get_hyprland_state_sync() -> Result { + let monitors = Monitors::get()?.to_vec(); + let mut workspaces = Workspaces::get()?.to_vec(); + workspaces.sort_by_key(|a| a.id); + let devices = Devices::get()?; + let client = Clients::get()?.to_vec(); + Ok(HyprlandState { + monitors, + workspaces, + devices, + clients: client, + }) +} + +pub async fn get_hyprland_state() -> Result { + let monitors = Monitors::get_async().await?.to_vec(); + let mut workspaces = Workspaces::get_async().await?.to_vec(); + workspaces.sort_by_key(|a| a.id); + let devices = Devices::get_async().await?; + let client = Clients::get_async().await?.to_vec(); + Ok(HyprlandState { + monitors, + workspaces, + devices, + clients: client, + }) +} + +pub fn init_callbacks(state: Dynamic) { + tokio_runtime().spawn({ + let state = state.clone(); + async move { + let mut event_listener = AsyncEventListener::new(); + + let update = Arc::new(move || { + let state = state.clone(); + Box::pin(async move { + if let Ok(unwrap) = get_hyprland_state().await { + state.set(unwrap); + } + }) + }); + + // I prefer winit-like event loop iterators/single callback with enum... + + event_listener.add_workspace_added_handler({ + let update = update.clone(); + move |_| update() + }); + event_listener.add_workspace_changed_handler({ + let update = update.clone(); + move |_| update() + }); + event_listener.add_workspace_deleted_handler({ + let update = update.clone(); + move |_| update() + }); + event_listener.add_window_opened_handler({ + let update = update.clone(); + move |_| update() + }); + event_listener.add_window_closed_handler({ + let update = update.clone(); + move |_| update() + }); + event_listener.add_window_title_changed_handler({ + let update = update.clone(); + move |_| update() + }); + event_listener.add_urgent_state_changed_handler({ + let update = update.clone(); + move |_| update() + }); + event_listener.add_screencast_handler({ + let update = update.clone(); + move |_| update() + }); + event_listener.add_layout_changed_handler({ + let update = update.clone(); + move |_| update() + }); + + event_listener.start_listener_async().await.unwrap(); + } + }); +} + +pub fn hyprland_workspaces(state: Dynamic) -> impl MakeWidget { + state.map_each(|state| { + let monitor = state.monitors.first().unwrap(); + let workspaces = &state.workspaces; + WidgetList::from_iter(workspaces.iter().map(|w| { + let active = monitor.active_workspace.id == w.id; + + let name = w.name.clone(); + + if active { + name.pad_by(WORKSPACE_PADDING) + .centered() + .with(&WidgetBackground, BG_WORKSPACE_ACTIVE) + .with(&TextColor, TEXT_WORKSPACE_ACTIVE) + .with(&CornerRadius, WORKSPACE_CORNER_RADIUS) + .make_widget() + } else { + name.pad_by(WORKSPACE_PADDING).centered().make_widget() + } + })) + .into_columns() + .make_widget() + }) +} + +pub fn hyprland_active_title(state: Dynamic) -> impl MakeWidget { + state.map_each(|state| { + let monitor = state.monitors.first().unwrap(); + let active_workspace = state + .workspaces + .iter() + .find(|w| w.id == monitor.active_workspace.id) + .unwrap(); + + format!("󱄅 {}", active_workspace.last_window_title.clone()) + .with(&TextColor, TEXT_TOOL) + .pad_by(WIDGET_PADDING) + .centered() + .make_widget() + }) +} diff --git a/src/bar/memory.rs b/src/bar/memory.rs new file mode 100644 index 0000000..6fc32f7 --- /dev/null +++ b/src/bar/memory.rs @@ -0,0 +1,45 @@ +use cushy::{ + styles::components::TextColor, + value::{Destination, Dynamic, Source}, + widget::MakeWidget, +}; +use systemstat::{saturating_sub_bytes, Platform, System}; + +use crate::{ + rt::tokio_runtime, + theme::{TEXT_MEM, WIDGET_PADDING}, +}; + +fn get_memory_usage() -> f64 { + let sys = System::new(); + sys.memory() + .map(|mem| { + let used = saturating_sub_bytes(mem.total, mem.free); + let used_percentage = (used.as_u64() as f64 / mem.total.as_u64() as f64) * 100.; + used_percentage + }) + .unwrap_or(0.) +} + +pub fn memory_widget() -> impl MakeWidget { + let percentage = Dynamic::new(get_memory_usage()); + tokio_runtime().spawn({ + let current_time = percentage.clone(); + async move { + let mut interval = tokio::time::interval(tokio::time::Duration::from_millis(1000)); + loop { + interval.tick().await; + current_time.set(get_memory_usage()); + } + } + }); + + percentage + .map_each(|percentage| { + let icon = "".to_string(); + format!(" {} {:.0}%", icon, percentage) + }) + .with(&TextColor, TEXT_MEM) + .pad_by(WIDGET_PADDING) + .centered() +} diff --git a/src/bar/mod.rs b/src/bar/mod.rs index 7bc2acc..a9a42c4 100644 --- a/src/bar/mod.rs +++ b/src/bar/mod.rs @@ -2,19 +2,22 @@ use battery::battery; use cushy::{ figures::{ units::{Lp, UPx}, - Size, + Size, Zero, }, - kludgine::app::winit::{platform::wayland::Anchor, window::WindowLevel}, + kludgine::app::winit::{error::EventLoopError, platform::wayland::Anchor, window::WindowLevel}, styles::{ components::{ BaseLineHeight, BaseTextSize, CornerRadius, DefaultBackgroundColor, FontWeight, + IntrinsicPadding, }, - FontFamilyList, + Dimension, FontFamilyList, }, value::Dynamic, widget::MakeWidget, Application, Open, }; +use hypr::{get_hyprland_state_sync, hyprland_active_title, hyprland_workspaces, init_callbacks}; +use hyprland::data::{Client, Devices, Monitor, Workspace}; use crate::{ theme::{BG_DEFAULT, CORNER_RADIUS, DEFAULT_FONT_WEIGHT, TEXT_FONT, TEXT_SIZE}, @@ -22,38 +25,70 @@ use crate::{ }; mod battery; +mod hypr; +mod memory; mod spotify; mod time; +#[derive(Debug, PartialEq)] +pub struct HyprlandState { + monitors: Vec, + workspaces: Vec, + devices: Devices, + clients: Vec, +} + pub fn start_bar(app: &mut impl Application) -> cushy::Result { + let state = Dynamic::new(get_hyprland_state_sync().map_err(|err| { + dbg!(err); + EventLoopError::ExitFailure(1) + })?); + + init_callbacks(state.clone()); + let monitors = (app.as_app().monitors()).unwrap(); let mut monitor_size: Size = monitors.available[0].size().into(); monitor_size.height = UPx::new(40); monitor_size.width = UPx::new((monitor_size.width.get() as f64 / 1.25) as _); let size = Dynamic::new(monitor_size); - let mut window = ((time::time_widget().bar_pill()) + + let left_part = (hyprland_workspaces(state.clone()) + .and(hyprland_active_title(state.clone())) + .into_columns() + .bar_pill() + .and(memory::memory_widget().bar_pill())) + .into_columns(); + + let middle_part = time::time_widget() + .bar_pill() .and(spotify::spotify_controls().bar_pill()) .into_columns() .centered() - .expand_horizontally()) - .and(battery().bar_pill()) - .into_columns() - .expand_horizontally() - .width(monitor_size.width) - .height(Lp::points(30)) - .with(&BaseTextSize, TEXT_SIZE) - .with(&BaseLineHeight, TEXT_SIZE) - .with(&DefaultBackgroundColor, BG_DEFAULT) - .with(&CornerRadius, CORNER_RADIUS) - .with(&FontWeight, DEFAULT_FONT_WEIGHT) - .into_window() - .inner_size(size.clone()) - .titled("rshell") - .transparent() - .app_name("rshell") - .decorated(false) - .resize_to_fit(false) - .window_level(WindowLevel::AlwaysOnTop); + .expand_horizontally(); + + let right_part = battery().bar_pill(); + + let mut window = left_part + .and(middle_part) + .and(right_part) + .into_columns() + .expand_horizontally() + .width(monitor_size.width) + .height(Lp::points(30)) + .with(&BaseTextSize, TEXT_SIZE) + .with(&BaseLineHeight, TEXT_SIZE) + .with(&DefaultBackgroundColor, BG_DEFAULT) + .with(&CornerRadius, CORNER_RADIUS) + .with(&FontWeight, DEFAULT_FONT_WEIGHT) + .with(&IntrinsicPadding, Dimension::ZERO) + .into_window() + .inner_size(size.clone()) + .titled("rshell") + .transparent() + .app_name("rshell") + .decorated(false) + .resize_to_fit(false) + .window_level(WindowLevel::AlwaysOnTop); let mut family = FontFamilyList::default(); for font in TEXT_FONT.iter() { diff --git a/src/bar/spotify.rs b/src/bar/spotify.rs index 73c4baf..ee771c3 100644 --- a/src/bar/spotify.rs +++ b/src/bar/spotify.rs @@ -125,32 +125,33 @@ fn get_texture_dynamic( let texture = texture.clone(); let vibrancy = vibrancy.clone(); let client = client.clone(); - let track_url = track.album_art.clone().unwrap(); - *prev_request_join = Some(tokio_runtime().spawn(async move { - let response = client.get(track_url).send().await.unwrap(); - let bytes = response.bytes().await.unwrap(); - let image = image::load_from_memory(&bytes).unwrap(); - let image = image.resize(128, 128, FilterType::Lanczos3); - let image_vibrancy = Vibrancy::new(&image); - vibrancy.set(ImageVibrancy { - primary: image_vibrancy.primary.map(|c| rgb_to_color(c)), - dark: image_vibrancy.dark.map(|c| rgb_to_color(c)), - light: image_vibrancy.light.map(|c| rgb_to_color(c)), - muted: image_vibrancy.muted.map(|c| rgb_to_color(c)), - dark_muted: image_vibrancy.dark_muted.map(|c| rgb_to_color(c)), - light_muted: image_vibrancy.light_muted.map(|c| rgb_to_color(c)), - }); - let image_texture = LazyTexture::from_image( - image, - cushy::kludgine::wgpu::FilterMode::Linear, - ); - let image_texture = AnyTexture::Lazy(image_texture); - texture.set(image_texture); - })); - } else { - vibrancy.set(ImageVibrancy::default()); - texture.set(get_empty_texture()); + if let Some(track_url) = track.album_art.clone() { + *prev_request_join = Some(tokio_runtime().spawn(async move { + let response = client.get(track_url).send().await.unwrap(); + let bytes = response.bytes().await.unwrap(); + let image = image::load_from_memory(&bytes).unwrap(); + let image = image.resize(128, 128, FilterType::Lanczos3); + let image_vibrancy = Vibrancy::new(&image); + vibrancy.set(ImageVibrancy { + primary: image_vibrancy.primary.map(|c| rgb_to_color(c)), + dark: image_vibrancy.dark.map(|c| rgb_to_color(c)), + light: image_vibrancy.light.map(|c| rgb_to_color(c)), + muted: image_vibrancy.muted.map(|c| rgb_to_color(c)), + dark_muted: image_vibrancy.dark_muted.map(|c| rgb_to_color(c)), + light_muted: image_vibrancy.light_muted.map(|c| rgb_to_color(c)), + }); + let image_texture = LazyTexture::from_image( + image, + cushy::kludgine::wgpu::FilterMode::Linear, + ); + let image_texture = AnyTexture::Lazy(image_texture); + texture.set(image_texture); + })); + return; + } } + vibrancy.set(ImageVibrancy::default()); + texture.set(get_empty_texture()); } }) .persist(); diff --git a/src/theme.rs b/src/theme.rs index a43e57b..ae451f4 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -12,19 +12,33 @@ pub const WIDGET_PADDING: Edges = Edges { bottom: Dimension::Px(Px::new(4)), }; +pub const WORKSPACE_PADDING: Edges = Edges { + left: Dimension::Px(Px::new(15)), + right: Dimension::Px(Px::new(15)), + top: Dimension::Px(Px::new(4)), + bottom: Dimension::Px(Px::new(4)), +}; +pub const WORKSPACE_CORNER_RADIUS: Dimension = Dimension::Px(Px::new(12)); + pub const DEFAULT_FONT_WEIGHT: Weight = Weight::MEDIUM; pub const BG_DEFAULT: Color = Color(0x191724FF); pub const TEXT_SPOTIFY: Color = Color(0x1DB954FF); pub const TEXT_CLOCK: Color = Color(0xF6C177FF); pub const TEXT_CPU: Color = Color(0xff671fFF); -pub const TEXT_MEM: Color = Color(0x1DB954FF); +pub const TEXT_MEM: Color = Color(0xFFFFFFFF); pub const TEXT_TEMP: Color = Color(0x97f993FF); +pub const TEXT_RED: Color = Color(0xD81E5BFF); + +pub const TEXT_TOOL: Color = Color(0x4e9dc2ff); pub const TEXT_AUDIO: Color = Color(0xF7E733FF); pub const TEXT_AUDIO_MUTED: Color = Color(0xD81E5BFF); pub const TEXT_BATTERY: Color = Color(0x6bb0d9FF); +pub const BG_WORKSPACE_ACTIVE: Color = Color(0x753a88ff); +pub const TEXT_WORKSPACE_ACTIVE: Color = Color(0xebbcbaff); + pub const TEXT_FONT: [&str; 5] = [ "Inter", "Iosevka", diff --git a/src/widgets.rs b/src/widgets.rs index 8b9d398..85b04f5 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -1,12 +1,12 @@ use cushy::{styles::components::WidgetBackground, widget::MakeWidget, widgets::Container}; -use crate::theme::BG_DEFAULT; +use crate::theme::{BG_DEFAULT, WIDGET_PADDING}; pub trait WidgetExt: MakeWidget { fn bar_pill(self) -> Container { self.expand_vertically() .with(&WidgetBackground, BG_DEFAULT) - .pad() + .pad_by(WIDGET_PADDING) } }