use std::cmp; use std::collections::HashMap; use std::fmt::Display; use std::fs::read_to_string; use std::path::PathBuf; use chrono::prelude::*; use freedesktop_icons::lookup; use hyprland::data::{Client, Workspace, Workspaces}; use hyprland::event_listener::{self, AsyncEventListener}; use hyprland::shared::{HyprData, HyprDataActiveOptional, HyprDataVec}; use iced::font::Weight; use iced::futures::SinkExt; use iced::stream; use iced::widget::image::Handle; use iced::widget::{container, horizontal_space, image, row, svg, text}; use iced::{ time, Background, Border, Color, Element, Event, Font, Length, Shadow, Subscription, Task, Vector, }; use iced_layershell::build_pattern::{application, MainSettings}; use iced_layershell::reexport::{Anchor, KeyboardInteractivity}; use iced_layershell::settings::LayerShellSettings; use iced_layershell::to_layer_message; use iced_runtime::futures::event; use miette::{IntoDiagnostic, Result}; use sysinfo::{Disks, System}; use system_tray::item::StatusNotifierItem; use system_tray::menu::TrayMenu; use tracing::level_filters::LevelFilter; use tracing::{debug, error}; use tracing_subscriber::EnvFilter; fn main() -> iced_layershell::Result { let timer = tracing_subscriber::fmt::time::ChronoLocal::new("%Y-%m-%d_%I:%M:%S%.6f %P".to_owned()); let filter = EnvFilter::builder() .with_default_directive(LevelFilter::WARN.into()) .parse_lossy("shelly=debug"); tracing_subscriber::FmtSubscriber::builder() .pretty() .with_line_number(true) .with_level(true) .with_target(true) .with_env_filter(filter) .with_target(true) .with_timer(timer) .init(); debug!("Starting shelly"); let mut font = Font::with_name("VictorMono Nerd Font"); font.weight = Weight::Bold; let settings = MainSettings { layer_settings: LayerShellSettings { size: Some((1400, 60)), anchor: Anchor::Bottom | Anchor::Left | Anchor::Right, margin: (0, 0, 0, 0), exclusive_zone: 60, keyboard_interactivity: KeyboardInteractivity::None, ..Default::default() }, default_font: font, // antialiasing: true, ..Default::default() }; application(Panel::namespace, Panel::update, Panel::view) .subscription(Panel::subscription) .settings(settings) .style(Panel::style) .run_with(Panel::new) } #[derive(Debug)] struct Panel { time: String, cpu: String, memory: String, battery: Option, disk: String, system: System, disks: Disks, workspaces: Vec, active_workspace: i32, active_window: String, active_window_icon_svg: Option, active_window_icon_raster: Option, system_tray: Option, apps: Vec, } #[to_layer_message] #[derive(Debug, Clone)] enum Message { Launch(usize), Time(DateTime), IcedEvent(Event), WorkspaceChange(Vec), ActiveWorkspaceChange(event_listener::WorkspaceEventData), ActiveWindowChange((String, String)), } impl Panel { fn new() -> (Self, Task) { let workspaces = Workspaces::get().map_err(|e| { error!("Couldn't get hyprland info: {}", e); e }); let client = Client::get_active().unwrap().unwrap(); let icon = lookup(&client.class) .with_theme("Papirus Dark") .with_cache() .find(); let active_window_icon_svg = icon .clone() .filter(|icon| icon.ends_with("svg")) .map(|icon| svg::Handle::from_path(icon)); let active_window_icon_raster = icon.map(|icon| image::Handle::from_path(icon)); let active_window = client.title; let battery = match Battery::get() { Ok(b) => Some(b), Err(e) => { error!("{e}"); None } }; ( Self { system: System::new_all(), disks: Disks::new_with_refreshed_list(), workspaces: workspaces.unwrap().to_vec(), active_workspace: 1, time: String::new(), cpu: String::new(), memory: String::new(), disk: String::new(), apps: vec![String::new()], active_window, active_window_icon_svg, active_window_icon_raster, battery, system_tray: None, }, Task::none(), ) } fn namespace(&self) -> String { String::from("panel") } fn update(&mut self, message: Message) -> Task { match message { Message::Launch(index) => {} Message::Time(instant) => { self.system.refresh_memory(); self.system.refresh_cpu_usage(); // disk.refresh(); // let used_space = disk.available_space() - disk.total_space(); let disk = self.disks.first_mut(); if let Some(disk) = disk { disk.refresh(); self.disk = format!( "󰋊 {}", convert((disk.total_space() - disk.available_space()) as f64) ); }; self.battery = match Battery::get() { Ok(b) => Some(b), Err(e) => { error!("{e}"); None } }; // let used_space = 393; // self.disk = format!("󰋊 {}", used_space); self.cpu = format!(" {}%", self.system.global_cpu_usage().round()); let memory = ((self.system.used_memory() as f64 / self.system.total_memory() as f64) .fract() * 100.0) .trunc(); self.memory = format!(" {}%", memory); self.time = instant.format("%a %b %d, %I:%M %p").to_string(); } Message::AnchorChange(anchor) => todo!(), Message::AnchorSizeChange(anchor, _) => todo!(), Message::LayerChange(layer) => todo!(), Message::MarginChange(_) => todo!(), Message::SizeChange(_) => todo!(), Message::VirtualKeyboardPressed { time, key } => todo!(), Message::IcedEvent(event) => { debug!(?event) } Message::WorkspaceChange(workspaces) => { // debug!(?workspaces); self.workspaces = workspaces; } Message::ActiveWorkspaceChange(data) => { // debug!(?data); self.active_workspace = data.id; } Message::ActiveWindowChange(w) => { debug!(?w); self.active_window = w.0; let icon = lookup(&w.1) .with_theme("Papirus-Dark") .force_svg() .with_size(16) .find(); debug!(?icon); self.active_window_icon_raster = icon .clone() .filter(|icon| !icon.ends_with("svg")) .map(|icon| image::Handle::from_path(icon)); let svg = icon.map(|icon| svg::Handle::from_path(icon)); debug!(?svg); self.active_window_icon_svg = svg; } _ => unreachable!(), } Task::none() } fn subscription(&self) -> Subscription { let iced_events = event::listen().map(Message::IcedEvent); let timed_events = time::every(time::Duration::from_millis(1000)).map(|_| Message::Time(Local::now())); let hyprland_events = self.hyprland_subscription(); Subscription::batch([iced_events, timed_events, hyprland_events]) } fn hyprland_subscription(&self) -> Subscription { Subscription::run(|| { stream::channel(1, |mut sender| async move { let mut workspaces = Workspaces::get_async() .await .map_err(|e| { error!("Couldn't get hyprland info: {}", e); e }) .unwrap() .to_vec(); workspaces.sort_by(|a, b| a.id.cmp(&b.id)); sender .send(Message::WorkspaceChange(workspaces)) .await .unwrap_or_else(|err| { eprintln!("Trying to send workspaces failed with err: {err}"); }); if let Ok(window) = Client::get_active_async().await { if let Some(w) = window { sender .send(Message::ActiveWindowChange(( w.title.trim().to_owned(), w.initial_class.trim().to_owned(), ))) .await .unwrap_or_else(|e| { error!("Trying to send window failed with err: {e}"); }); } } let mut listener = AsyncEventListener::new(); let senderx = sender.clone(); listener.add_active_window_changed_handler(move |data| { let mut sender = senderx.clone(); Box::pin(async move { if let Some(data) = data { let client = Client::get_active().unwrap().unwrap(); sender .send(Message::ActiveWindowChange(( client.title, client.initial_class, ))) .await .unwrap(); } }) }); let senderx = sender.clone(); listener.add_workspace_changed_handler(move |data| { let mut sender = senderx.clone(); Box::pin(async move { sender .send(Message::ActiveWorkspaceChange(data)) .await .unwrap_or_else(|e| error!("Error in sending: {e}")); let mut workspaces = Workspaces::get_async() .await .map_err(|e| { error!("Couldn't get hyprland info: {}", e); e }) .unwrap() .to_vec(); workspaces.sort_by(|a, b| a.id.cmp(&b.id)); sender .send(Message::WorkspaceChange(workspaces)) .await .unwrap_or_else(|err| { eprintln!("Trying to send workspaces failed with err: {err}"); }); }) }); listener .start_listener_async() .await .expect("Failed to listen for hyprland events"); }) }) } fn view(&self) -> Element { let workspaces: Vec> = self .workspaces .iter() .filter(|w| w.name != "special:special") .map(|w| { text!("{}", { match w.id { 1 => "".to_owned(), 2 => "".to_owned(), 3 => "󰈙".to_owned(), 4 => "󰭹".to_owned(), 5 => "".to_owned(), 6 => "".to_owned(), 7 => "󰕧".to_owned(), 8 => "󰭹".to_owned(), 9 => "".to_owned(), _ => w.name.clone(), } }) .color({ if w.id == self.active_workspace { Color::parse("#ff6ac1").unwrap() } else { Color::parse("#57c7ff").unwrap() } }) .into() }) .collect(); let workspaces = row(workspaces).spacing(20).padding([0, 5]); let icon: Element; // if let Some(handle) = &self.active_window_icon_raster { // icon = image(handle).into(); // } else { // if let Some(handle) = &self.active_window_icon_svg { // icon = svg(handle.clone()).width(24).height(24).into(); // } else { // icon = image("/home/chris/pics/wojaks/reddit_the_xenomorph_s bigass wojak folder/Chads/ChristianChad.png").into(); // } // } if let Some(handle) = &self.active_window_icon_svg { icon = svg(handle.clone()).width(16).height(16).into(); } else { icon = image("/home/chris/pics/wojaks/reddit_the_xenomorph_s bigass wojak folder/Chads/ChristianChad.png").into(); } let icon = container(icon).center(Length::Shrink).padding(4); let window = text!("{}", self.active_window).color(Color::parse("#57c7ff").unwrap()); let disk = text!("{}", self.disk).color(Color::parse("#ff9f43").unwrap()); let mem = text!("{}", self.memory).color(Color::parse("#f3f99d").unwrap()); let cpu = text!("{}", self.cpu).color(Color::parse("#57c7ff").unwrap()); let clock = text!("{}", self.time); let clock = clock.color(Color::parse("#5af78e").unwrap()); let battery_icon = if let Some(battery) = &self.battery { match battery.status { BatteryStatus::Charging => "󱟦", BatteryStatus::Charged => "󱟢", BatteryStatus::Draining => "󱟤", } } else { "" }; let battery = text!( "{} {}", battery_icon, if let Some(battery) = &self.battery { battery.capacity.to_string() } else { "".to_string() } ) .color(Color::parse("#ff6ac1").unwrap()); let row = row!( container(row!(workspaces, icon, window, horizontal_space()).spacing(10)), container(clock), container(row!(horizontal_space(), disk, cpu, mem, battery).spacing(5)) ) .spacing(5) .padding([0, 20]); let mut shadow = Shadow::default(); shadow.color = Color::BLACK; shadow.offset = Vector::new(4.5, 4.5); shadow.blur_radius = 18.5; container( container(row) .style(move |t: &iced::Theme| { container::rounded_box(t) .border(Border::default().rounded(20)) .background( Background::Color(Color::parse("#282a36").unwrap()).scale_alpha(0.95), ) .shadow(shadow) }) .center(Length::Fill), ) .padding([15, 20]) .center(Length::Fill) .style(move |t: &iced::Theme| { container::rounded_box(t) .border(Border::default().rounded(20)) .background(Background::Color(Color::TRANSPARENT)) }) .into() } fn style(&self, theme: &iced::Theme) -> iced_layershell::Appearance { use iced_layershell::Appearance; Appearance { background_color: Color::TRANSPARENT, text_color: theme.palette().text, } } } pub fn convert(num: f64) -> String { let negative = if num.is_sign_positive() { "" } else { "-" }; let num = num.abs(); let units = ["B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; if num < 1_f64 { return format!("{}{} {}", negative, num, "B"); } let delimiter = 1000_f64; let exponent = cmp::min( (num.ln() / delimiter.ln()).floor() as i32, (units.len() - 1) as i32, ); let pretty_bytes = format!("{:.2}", num / delimiter.powi(exponent)) .parse::() .unwrap() * 1_f64; let unit = units[exponent as usize]; format!("{}{} {}", negative, pretty_bytes, unit) } #[derive(Debug)] struct Battery { capacity: u8, status: BatteryStatus, } impl Battery { fn get() -> Result { let capacity = read_to_string("/sys/class/power_supply/BAT1/capacity") .into_diagnostic()? .trim() .parse() .into_diagnostic()?; let status = match read_to_string("/sys/class/power_supply/BAT1/status") .into_diagnostic()? .trim() { "Not charging" => BatteryStatus::Charged, "Discharging" => BatteryStatus::Draining, "Charging" => BatteryStatus::Charging, _ => BatteryStatus::Draining, }; Ok(Self { capacity, status }) } } #[derive(Debug)] enum BatteryStatus { Charging, Charged, Draining, } #[derive(Debug)] struct SystemTray { client: system_tray::client::Client, items: HashMap)>, } impl SystemTray { async fn new() -> Result { let client = system_tray::client::Client::new().await.into_diagnostic()?; let items = client.items(); let items = items .lock() .map_err(|_| SystemTrayError::SomeError) .into_diagnostic()? .clone(); Ok(Self { client, items }) } } #[derive(Debug)] enum SystemTrayError { SomeError, } impl std::error::Error for SystemTrayError {} impl Display for SystemTrayError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "SystemTrayError::SomeError") } }