From 6da27dda3ca09b9cdb8d7258fa3721cb231dc288 Mon Sep 17 00:00:00 2001 From: LGUG2Z Date: Fri, 30 Oct 2026 23:47:49 -0700 Subject: [PATCH] refactor(bar-core): move the memory, date and update widgets into komorebi-bar-core Pure move of the three widget files that are byte-identical in the two bars since the previous revision. Each bar's widgets/mod.rs re-exports them (pub use komorebi_bar_core::widgets::memory / date / update). The core takes chrono, chrono-tz, reqwest (blocking) and serde_json (now a normal dependency, it was only a dev-dependency). Removed: 541 lines from each bar (138 + 245 + 158), 541 added once. Verified: Windows cargo clippy --workspace --locked --all-targets -D warnings clean; macOS fake-CC cargo check --locked --workspace --all-targets finished, 0 errors; native cargo test -p komorebi-bar-core 3 unit + 2 oracle passed; harness regeneration byte-identical for both (macOS 3,774 lines, Windows 9,091 lines). Not verified: rendering. --- Cargo.lock | 3 + crates/komorebi-bar-core/Cargo.toml | 7 +- .../komorebi-bar-core}/src/widgets/date.rs | 0 .../komorebi-bar-core}/src/widgets/memory.rs | 0 crates/komorebi-bar-core/src/widgets/mod.rs | 3 + .../komorebi-bar-core}/src/widgets/update.rs | 0 komorebi-bar/src/widgets/mod.rs | 6 +- komorebi-for-mac/Cargo.lock | 4 + .../komorebi-bar/src/widgets/date.rs | 246 ------------------ .../komorebi-bar/src/widgets/memory.rs | 138 ---------- .../komorebi-bar/src/widgets/mod.rs | 6 +- .../komorebi-bar/src/widgets/update.rs | 157 ----------- 12 files changed, 20 insertions(+), 550 deletions(-) rename {komorebi-bar => crates/komorebi-bar-core}/src/widgets/date.rs (100%) rename {komorebi-bar => crates/komorebi-bar-core}/src/widgets/memory.rs (100%) rename {komorebi-bar => crates/komorebi-bar-core}/src/widgets/update.rs (100%) delete mode 100644 komorebi-for-mac/komorebi-bar/src/widgets/date.rs delete mode 100644 komorebi-for-mac/komorebi-bar/src/widgets/memory.rs delete mode 100644 komorebi-for-mac/komorebi-bar/src/widgets/update.rs diff --git a/Cargo.lock b/Cargo.lock index c2999427..2d355ce7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3435,8 +3435,11 @@ dependencies = [ name = "komorebi-bar-core" version = "0.1.42" dependencies = [ + "chrono", + "chrono-tz", "eframe", "egui-phosphor", + "reqwest", "schemars 1.2.3", "serde", "serde_json_lenient", diff --git a/crates/komorebi-bar-core/Cargo.toml b/crates/komorebi-bar-core/Cargo.toml index 48a081e2..c339bcbe 100644 --- a/crates/komorebi-bar-core/Cargo.toml +++ b/crates/komorebi-bar-core/Cargo.toml @@ -5,18 +5,19 @@ description = "The status bar shared by the Windows and macOS komorebi bars: con edition = "2024" [dependencies] +chrono = { workspace = true } +chrono-tz = { workspace = true } eframe = { workspace = true } egui-phosphor = { git = "https://github.com/amPerl/egui-phosphor", rev = "d13688738478ecd12b426e3e74c59d6577a85b59" } +reqwest = { version = "0.12", features = ["blocking"] } serde = { workspace = true } +serde_json = { workspace = true } starship-battery = "0.11" sysinfo = { workspace = true } # Optional dependencies schemars = { workspace = true, optional = true } -[dev-dependencies] -serde_json = { workspace = true } - [features] default = ["schemars"] schemars = ["dep:schemars"] diff --git a/komorebi-bar/src/widgets/date.rs b/crates/komorebi-bar-core/src/widgets/date.rs similarity index 100% rename from komorebi-bar/src/widgets/date.rs rename to crates/komorebi-bar-core/src/widgets/date.rs diff --git a/komorebi-bar/src/widgets/memory.rs b/crates/komorebi-bar-core/src/widgets/memory.rs similarity index 100% rename from komorebi-bar/src/widgets/memory.rs rename to crates/komorebi-bar-core/src/widgets/memory.rs diff --git a/crates/komorebi-bar-core/src/widgets/mod.rs b/crates/komorebi-bar-core/src/widgets/mod.rs index 45e3fc65..42d16a57 100644 --- a/crates/komorebi-bar-core/src/widgets/mod.rs +++ b/crates/komorebi-bar-core/src/widgets/mod.rs @@ -1,3 +1,6 @@ pub mod battery; pub mod cpu; +pub mod date; +pub mod memory; +pub mod update; pub mod widget; diff --git a/komorebi-bar/src/widgets/update.rs b/crates/komorebi-bar-core/src/widgets/update.rs similarity index 100% rename from komorebi-bar/src/widgets/update.rs rename to crates/komorebi-bar-core/src/widgets/update.rs diff --git a/komorebi-bar/src/widgets/mod.rs b/komorebi-bar/src/widgets/mod.rs index e24b53f0..02250e4f 100644 --- a/komorebi-bar/src/widgets/mod.rs +++ b/komorebi-bar/src/widgets/mod.rs @@ -12,18 +12,18 @@ use std::sync::RwLock; pub mod applications; pub use komorebi_bar_core::widgets::battery; pub use komorebi_bar_core::widgets::cpu; -pub mod date; +pub use komorebi_bar_core::widgets::date; pub mod keyboard; pub mod komorebi; mod komorebi_layout; pub mod media; -pub mod memory; +pub use komorebi_bar_core::widgets::memory; pub mod network; pub mod storage; #[cfg(target_os = "windows")] pub mod systray; pub mod time; -pub mod update; +pub use komorebi_bar_core::widgets::update; pub mod widget; /// Global cache for icon images and their associated GPU textures. diff --git a/komorebi-for-mac/Cargo.lock b/komorebi-for-mac/Cargo.lock index e576e8d4..3bf67c72 100644 --- a/komorebi-for-mac/Cargo.lock +++ b/komorebi-for-mac/Cargo.lock @@ -3243,10 +3243,14 @@ dependencies = [ name = "komorebi-bar-core" version = "0.1.42" dependencies = [ + "chrono", + "chrono-tz", "eframe", "egui-phosphor", + "reqwest", "schemars 1.2.3", "serde", + "serde_json_lenient", "starship-battery", "sysinfo 0.39.6", ] diff --git a/komorebi-for-mac/komorebi-bar/src/widgets/date.rs b/komorebi-for-mac/komorebi-bar/src/widgets/date.rs deleted file mode 100644 index c51acebd..00000000 --- a/komorebi-for-mac/komorebi-bar/src/widgets/date.rs +++ /dev/null @@ -1,246 +0,0 @@ -use crate::config::LabelPrefix; -use crate::platform::platform; -use crate::render::RenderConfig; -use crate::selected_frame::SelectableFrame; -use crate::widgets::widget::BarWidget; -use chrono::Local; -use chrono_tz::Tz; -use eframe::egui::Align; -use eframe::egui::Context; -use eframe::egui::Label; -use eframe::egui::TextFormat; -use eframe::egui::Ui; -use eframe::egui::WidgetText; -use eframe::egui::text::LayoutJob; -use serde::Deserialize; -use serde::Serialize; -use std::sync::Arc; -use std::time::Duration; -use std::time::Instant; - -/// Custom format with additive modifiers for integer format specifiers -#[derive(Clone, Debug, Serialize, Deserialize)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct CustomModifiers { - /// Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html) - format: String, - /// Additive modifiers for integer format specifiers (e.g. { "%U": 1 } to increment the zero-indexed week number by 1) - modifiers: std::collections::HashMap, -} - -impl CustomModifiers { - fn apply(&self, output: &str) -> String { - let int_formatters = vec![ - "%Y", "%C", "%y", "%m", "%d", "%e", "%w", "%u", "%U", "%W", "%G", "%g", "%V", "%j", - "%H", "%k", "%I", "%l", "%M", "%S", "%f", - ]; - - let mut modified_output = output.to_string(); - - for (modifier, value) in &self.modifiers { - // check if formatter is integer type - if !int_formatters.contains(&modifier.as_str()) { - continue; - } - - // get the strftime value of modifier - let formatted_modifier = Local::now().format(modifier).to_string(); - - // find the gotten value in the original output - if let Some(pos) = modified_output.find(&formatted_modifier) { - let start = pos; - let end = start + formatted_modifier.len(); - // replace that value with the modified value - if let Ok(num) = formatted_modifier.parse::() { - modified_output.replace_range(start..end, &(num + value).to_string()); - } - } - } - - modified_output - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -/// Date widget configuration -pub struct DateConfig { - /// Enable the Date widget - pub enable: bool, - /// Set the Date format - pub format: DateFormat, - /// Display label prefix - pub label_prefix: Option, - /// TimeZone (https://docs.rs/chrono-tz/latest/chrono_tz/enum.Tz.html) - /// - /// Use a custom format to display additional information, i.e.: - /// ```json - /// { - /// "Date": { - /// "enable": true, - /// "format": { "Custom": "%D %Z (Tokyo)" }, - /// "timezone": "Asia/Tokyo" - /// } - ///} - /// ``` - pub timezone: Option, -} - -impl From for Date { - fn from(value: DateConfig) -> Self { - let data_refresh_interval = 1; - - Self { - enable: value.enable, - format: value.format, - label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon), - timezone: value.timezone, - data_refresh_interval, - last_state: String::new(), - last_updated: Instant::now() - .checked_sub(Duration::from_secs(data_refresh_interval)) - .unwrap(), - } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -/// Date widget format -pub enum DateFormat { - /// Month/Date/Year format (09/08/24) - MonthDateYear, - /// Year-Month-Date format (2024-09-08) - YearMonthDate, - /// Date-Month-Year format (8-Sep-2024) - DateMonthYear, - /// Day Date Month Year format (8 September 2024) - DayDateMonthYear, - /// Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html) - #[cfg_attr(feature = "schemars", schemars(title = "Custom"))] - Custom(String), - /// Custom format with modifiers - #[cfg_attr(feature = "schemars", schemars(title = "CustomModifiers"))] - CustomModifiers(CustomModifiers), -} - -impl DateFormat { - pub fn next(&mut self) { - match self { - DateFormat::MonthDateYear => *self = Self::YearMonthDate, - DateFormat::YearMonthDate => *self = Self::DateMonthYear, - DateFormat::DateMonthYear => *self = Self::DayDateMonthYear, - DateFormat::DayDateMonthYear => *self = Self::MonthDateYear, - _ => {} - }; - } - - pub fn fmt_string(&self) -> String { - match self { - DateFormat::MonthDateYear => String::from("%D"), - DateFormat::YearMonthDate => String::from("%F"), - DateFormat::DateMonthYear => String::from("%v"), - DateFormat::DayDateMonthYear => String::from("%A %e %B %Y"), - DateFormat::Custom(custom) => custom.to_string(), - DateFormat::CustomModifiers(custom) => custom.format.clone(), - } - } -} - -#[derive(Clone, Debug)] -pub struct Date { - pub enable: bool, - pub format: DateFormat, - label_prefix: LabelPrefix, - timezone: Option, - data_refresh_interval: u64, - last_state: String, - last_updated: Instant, -} - -impl Date { - fn output(&mut self) -> String { - let mut output = self.last_state.clone(); - let now = Instant::now(); - - if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) { - let formatted = match &self.timezone { - Some(timezone) => match timezone.parse::() { - Ok(tz) => Local::now() - .with_timezone(&tz) - .format(&self.format.fmt_string()) - .to_string() - .trim() - .to_string(), - Err(_) => format!("Invalid timezone: {timezone}"), - }, - None => Local::now() - .format(&self.format.fmt_string()) - .to_string() - .trim() - .to_string(), - }; - - // if custom modifiers are used, apply them - output = match &self.format { - DateFormat::CustomModifiers(custom) => custom.apply(&formatted), - _ => formatted, - }; - - self.last_state.clone_from(&output); - self.last_updated = now; - } - - output - } -} - -impl BarWidget for Date { - fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) { - if self.enable { - let mut output = self.output(); - if !output.is_empty() { - let mut layout_job = LayoutJob::simple( - match self.label_prefix { - LabelPrefix::Icon | LabelPrefix::IconAndText => { - platform().calendar_icon().to_string() - } - LabelPrefix::None | LabelPrefix::Text => String::new(), - }, - config.icon_font_id.clone(), - ctx.style().visuals.selection.stroke.color, - 100.0, - ); - - if let LabelPrefix::Text | LabelPrefix::IconAndText = self.label_prefix { - output.insert_str(0, "DATE: "); - } - - layout_job.append( - &output, - 10.0, - TextFormat { - font_id: config.text_font_id.clone(), - color: ctx.style().visuals.text_color(), - valign: Align::Center, - ..Default::default() - }, - ); - - config.apply_on_widget(false, ui, |ui| { - if SelectableFrame::new(false) - .show(ui, |ui| { - ui.add( - Label::new(WidgetText::LayoutJob(Arc::from(layout_job.clone()))) - .selectable(false), - ) - }) - .clicked() - { - self.format.next() - } - }); - } - } - } -} diff --git a/komorebi-for-mac/komorebi-bar/src/widgets/memory.rs b/komorebi-for-mac/komorebi-bar/src/widgets/memory.rs deleted file mode 100644 index 53e4f931..00000000 --- a/komorebi-for-mac/komorebi-bar/src/widgets/memory.rs +++ /dev/null @@ -1,138 +0,0 @@ -use crate::config::LabelPrefix; -use crate::platform::SystemTool; -use crate::platform::platform; -use crate::render::RenderConfig; -use crate::selected_frame::SelectableFrame; -use crate::widgets::widget::BarWidget; -use eframe::egui::Align; -use eframe::egui::Context; -use eframe::egui::Label; -use eframe::egui::TextFormat; -use eframe::egui::Ui; -use eframe::egui::text::LayoutJob; -use serde::Deserialize; -use serde::Serialize; -use std::time::Duration; -use std::time::Instant; -use sysinfo::RefreshKind; -use sysinfo::System; - -#[derive(Copy, Clone, Debug, Serialize, Deserialize)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -/// Memory widget configuration -pub struct MemoryConfig { - /// Enable the Memory widget - pub enable: bool, - /// Data refresh interval in seconds - #[cfg_attr(feature = "schemars", schemars(extend("default" = 10)))] - pub data_refresh_interval: Option, - /// Display label prefix - pub label_prefix: Option, - /// Select when the current percentage is over this value [[1-100]] - pub auto_select_over: Option, -} - -impl From for Memory { - fn from(value: MemoryConfig) -> Self { - let data_refresh_interval = value.data_refresh_interval.unwrap_or(10); - - Self { - enable: value.enable, - system: System::new_with_specifics( - RefreshKind::default().without_cpu().without_processes(), - ), - data_refresh_interval, - label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText), - auto_select_over: value.auto_select_over.map(|o| o.clamp(1, 100)), - last_updated: Instant::now() - .checked_sub(Duration::from_secs(data_refresh_interval)) - .unwrap(), - } - } -} - -#[derive(Clone, Debug)] -struct MemoryOutput { - label: String, - selected: bool, -} - -pub struct Memory { - pub enable: bool, - system: System, - data_refresh_interval: u64, - label_prefix: LabelPrefix, - auto_select_over: Option, - last_updated: Instant, -} - -impl Memory { - fn output(&mut self) -> MemoryOutput { - let now = Instant::now(); - if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) { - self.system.refresh_memory(); - self.last_updated = now; - } - - let used = self.system.used_memory(); - let total = self.system.total_memory(); - let usage = ((used * 100) / total) as u8; - let selected = self.auto_select_over.is_some_and(|o| usage >= o); - - MemoryOutput { - label: match self.label_prefix { - LabelPrefix::Text | LabelPrefix::IconAndText => { - format!("RAM: {usage}%") - } - LabelPrefix::None | LabelPrefix::Icon => format!("{usage}%"), - }, - selected, - } - } -} - -impl BarWidget for Memory { - fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) { - if self.enable { - let output = self.output(); - if !output.label.is_empty() { - let auto_text_color = config.auto_select_text.filter(|_| output.selected); - - let mut layout_job = LayoutJob::simple( - match self.label_prefix { - LabelPrefix::Icon | LabelPrefix::IconAndText => { - egui_phosphor::regular::MEMORY.to_string() - } - LabelPrefix::None | LabelPrefix::Text => String::new(), - }, - config.icon_font_id.clone(), - auto_text_color.unwrap_or(ctx.style().visuals.selection.stroke.color), - 100.0, - ); - - layout_job.append( - &output.label, - 10.0, - TextFormat { - font_id: config.text_font_id.clone(), - color: auto_text_color.unwrap_or(ctx.style().visuals.text_color()), - valign: Align::Center, - ..Default::default() - }, - ); - - let auto_focus_fill = config.auto_select_fill; - - config.apply_on_widget(false, ui, |ui| { - if SelectableFrame::new_auto(output.selected, auto_focus_fill) - .show(ui, |ui| ui.add(Label::new(layout_job).selectable(false))) - .clicked() - && let Err(error) = platform().open_system_tool(SystemTool::TaskManager) - { - eprintln!("{error}") - } - }); - } - } - } -} diff --git a/komorebi-for-mac/komorebi-bar/src/widgets/mod.rs b/komorebi-for-mac/komorebi-bar/src/widgets/mod.rs index 950fec55..ea5a5718 100644 --- a/komorebi-for-mac/komorebi-bar/src/widgets/mod.rs +++ b/komorebi-for-mac/komorebi-bar/src/widgets/mod.rs @@ -12,16 +12,16 @@ use std::sync::RwLock; pub mod applications; pub use komorebi_bar_core::widgets::battery; pub use komorebi_bar_core::widgets::cpu; -pub mod date; +pub use komorebi_bar_core::widgets::date; // pub mod keyboard; pub mod komorebi; mod komorebi_layout; // pub mod media; -pub mod memory; +pub use komorebi_bar_core::widgets::memory; pub mod network; pub mod storage; pub mod time; -pub mod update; +pub use komorebi_bar_core::widgets::update; pub mod widget; /// Global cache for icon images and their associated GPU textures. diff --git a/komorebi-for-mac/komorebi-bar/src/widgets/update.rs b/komorebi-for-mac/komorebi-bar/src/widgets/update.rs deleted file mode 100644 index 04c068ea..00000000 --- a/komorebi-for-mac/komorebi-bar/src/widgets/update.rs +++ /dev/null @@ -1,157 +0,0 @@ -use crate::config::LabelPrefix; -use crate::platform::SystemTool; -use crate::platform::platform; -use crate::render::RenderConfig; -use crate::selected_frame::SelectableFrame; -use crate::widgets::widget::BarWidget; -use eframe::egui::Align; -use eframe::egui::Context; -use eframe::egui::Label; -use eframe::egui::TextFormat; -use eframe::egui::Ui; -use eframe::egui::text::LayoutJob; -use serde::Deserialize; -use serde::Serialize; -use std::time::Duration; -use std::time::Instant; - -#[derive(Copy, Clone, Debug, Serialize, Deserialize)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -/// Update widget configuration -pub struct UpdateConfig { - /// Enable the Update widget - pub enable: bool, - /// Data refresh interval in hours - #[cfg_attr(feature = "schemars", schemars(extend("default" = 12)))] - pub data_refresh_interval: Option, - /// Display label prefix - pub label_prefix: Option, -} - -impl From for Update { - fn from(value: UpdateConfig) -> Self { - let data_refresh_interval = value.data_refresh_interval.unwrap_or(12); - - let mut latest_version = String::new(); - - let client = reqwest::blocking::Client::new(); - if let Ok(response) = client - .get(platform().latest_release_api_url()) - .header("User-Agent", "komorebi-bar-version-checker") - .send() - { - #[derive(Deserialize)] - struct Release { - tag_name: String, - } - - if let Ok(release) = - serde_json::from_str::(&response.text().unwrap_or_default()) - { - let trimmed = release.tag_name.trim_start_matches("v"); - latest_version = trimmed.to_string(); - } - } - - Self { - enable: value.enable, - data_refresh_interval, - installed_version: env!("CARGO_PKG_VERSION").to_string(), - latest_version, - label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText), - last_updated: Instant::now() - .checked_sub(Duration::from_secs(data_refresh_interval)) - .unwrap(), - } - } -} - -pub struct Update { - pub enable: bool, - data_refresh_interval: u64, - installed_version: String, - latest_version: String, - label_prefix: LabelPrefix, - last_updated: Instant, -} - -impl Update { - fn output(&mut self) -> String { - let now = Instant::now(); - if now.duration_since(self.last_updated) - > Duration::from_secs((self.data_refresh_interval * 60) * 60) - { - let client = reqwest::blocking::Client::new(); - if let Ok(response) = client - .get("https://api.github.com/repos/LGUG2Z/komorebi/releases/latest") - .header("User-Agent", "komorebi-bar-version-checker") - .send() - { - #[derive(Deserialize)] - struct Release { - tag_name: String, - } - - if let Ok(release) = - serde_json::from_str::(&response.text().unwrap_or_default()) - { - let trimmed = release.tag_name.trim_start_matches("v"); - self.latest_version = trimmed.to_string(); - } - } - - self.last_updated = now; - } - - if self.latest_version > self.installed_version { - format!("Update available! v{}", self.latest_version) - } else { - String::new() - } - } -} - -impl BarWidget for Update { - fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) { - if self.enable { - let output = self.output(); - if !output.is_empty() { - let mut layout_job = LayoutJob::simple( - match self.label_prefix { - LabelPrefix::Icon | LabelPrefix::IconAndText => { - egui_phosphor::regular::ROCKET_LAUNCH.to_string() - } - LabelPrefix::None | LabelPrefix::Text => String::new(), - }, - config.icon_font_id.clone(), - ctx.style().visuals.selection.stroke.color, - 100.0, - ); - - layout_job.append( - &output, - 10.0, - TextFormat { - font_id: config.text_font_id.clone(), - color: ctx.style().visuals.text_color(), - valign: Align::Center, - ..Default::default() - }, - ); - - config.apply_on_widget(false, ui, |ui| { - if SelectableFrame::new(false) - .show(ui, |ui| ui.add(Label::new(layout_job).selectable(false))) - .clicked() - && let Err(error) = platform().open_system_tool(SystemTool::Url(format!( - "https://github.com/LGUG2Z/komorebi/releases/v{}", - self.latest_version - ))) - { - eprintln!("{error}") - } - }); - } - } - } -}