workspace and memory support

This commit is contained in:
Daniel Bulant 2025-01-11 14:40:07 +01:00
parent 715527b138
commit ecf63c0e3b
No known key found for this signature in database
9 changed files with 377 additions and 68 deletions

23
Cargo.lock generated
View file

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

View file

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

View file

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

155
src/bar/hypr.rs Normal file
View file

@ -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<HyprlandState, HyprError> {
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<HyprlandState, HyprError> {
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<HyprlandState>) {
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<HyprlandState>) -> 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<HyprlandState>) -> 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()
})
}

45
src/bar/memory.rs Normal file
View file

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

View file

@ -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<Monitor>,
workspaces: Vec<Workspace>,
devices: Devices,
clients: Vec<Client>,
}
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<UPx> = 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() {

View file

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

View file

@ -12,19 +12,33 @@ pub const WIDGET_PADDING: Edges<Dimension> = Edges {
bottom: Dimension::Px(Px::new(4)),
};
pub const WORKSPACE_PADDING: Edges<Dimension> = 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",

View file

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