feat(bar): add first pass at configuration loader

This commit is contained in:
LGUG2Z
2024-09-08 18:29:44 -07:00
parent a4ef85859e
commit e99138a97e
17 changed files with 1531 additions and 458 deletions

82
Cargo.lock generated
View File

@@ -2121,7 +2121,7 @@ checksum = "9c08c1f623a8d0b722b8b99f821eb0ba672a1618f0d3b16ddbee1cedd2dd8557"
dependencies = [
"bitflags 2.6.0",
"gpu-descriptor-types",
"hashbrown",
"hashbrown 0.14.5",
]
[[package]]
@@ -2145,7 +2145,7 @@ dependencies = [
"futures-core",
"futures-sink",
"http",
"indexmap",
"indexmap 2.2.6",
"slab",
"tokio",
"tokio-util",
@@ -2162,6 +2162,12 @@ dependencies = [
"crunchy",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.14.5"
@@ -2450,6 +2456,16 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
[[package]]
name = "indexmap"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
"hashbrown 0.12.3",
]
[[package]]
name = "indexmap"
version = "2.2.6"
@@ -2457,7 +2473,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
dependencies = [
"equivalent",
"hashbrown",
"hashbrown 0.14.5",
]
[[package]]
@@ -2639,7 +2655,7 @@ dependencies = [
"schemars",
"serde",
"serde_json_lenient",
"serde_yaml",
"serde_yaml 0.9.34+deprecated",
"shadow-rs",
"strum",
"sysinfo 0.30.13",
@@ -2661,17 +2677,22 @@ dependencies = [
name = "komorebi-bar"
version = "0.1.0"
dependencies = [
"anyhow",
"catppuccin-egui",
"chrono",
"clap",
"color-eyre",
"crossbeam-channel",
"dirs",
"eframe",
"egui-phosphor",
"font-loader",
"image",
"komorebi-client",
"netdev",
"serde_json",
"schemars",
"serde",
"serde_json_lenient",
"serde_yaml 0.8.26",
"starship-battery",
"sysinfo 0.31.3",
"windows 0.54.0",
@@ -2717,7 +2738,7 @@ dependencies = [
"reqwest",
"serde",
"serde_json_lenient",
"serde_yaml",
"serde_yaml 0.9.34+deprecated",
"shadow-rs",
"sysinfo 0.30.13",
"thiserror",
@@ -2851,6 +2872,12 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "linked-hash-map"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "linux-raw-sys"
version = "0.3.8"
@@ -3079,7 +3106,7 @@ dependencies = [
"bitflags 2.6.0",
"codespan-reporting",
"hexf-parse",
"indexmap",
"indexmap 2.2.6",
"log",
"num-traits",
"rustc-hash",
@@ -3746,7 +3773,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db"
dependencies = [
"fixedbitset",
"indexmap",
"indexmap 2.2.6",
]
[[package]]
@@ -3805,7 +3832,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016"
dependencies = [
"base64",
"indexmap",
"indexmap 2.2.6",
"quick-xml 0.32.0",
"serde",
"time",
@@ -4539,13 +4566,25 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_yaml"
version = "0.8.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b"
dependencies = [
"indexmap 1.9.3",
"ryu",
"serde",
"yaml-rust",
]
[[package]]
name = "serde_yaml"
version = "0.9.34+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [
"indexmap",
"indexmap 2.2.6",
"itoa",
"ryu",
"serde",
@@ -4586,9 +4625,9 @@ dependencies = [
[[package]]
name = "shadow-rs"
version = "0.29.0"
version = "0.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a600f795d0894cda22235b44eea4b85c2a35b405f65523645ac8e35b306817a"
checksum = "69fe0bac8a8752586a618a1c80d01d8ca5d40fce4f6077fbc851e48dcbdb90df"
dependencies = [
"const_format",
"git2",
@@ -5188,7 +5227,7 @@ version = "0.19.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
dependencies = [
"indexmap",
"indexmap 2.2.6",
"toml_datetime",
"winnow 0.5.40",
]
@@ -5199,7 +5238,7 @@ version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1"
dependencies = [
"indexmap",
"indexmap 2.2.6",
"toml_datetime",
"winnow 0.5.40",
]
@@ -5210,7 +5249,7 @@ version = "0.22.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "278f3d518e152219c994ce877758516bca5e118eaed6996192a774fb9fbf0788"
dependencies = [
"indexmap",
"indexmap 2.2.6",
"serde",
"serde_spanned",
"toml_datetime",
@@ -5809,7 +5848,7 @@ dependencies = [
"cfg_aliases 0.1.1",
"codespan-reporting",
"document-features",
"indexmap",
"indexmap 2.2.6",
"log",
"naga",
"once_cell",
@@ -6495,6 +6534,15 @@ version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "791978798f0597cfc70478424c2b4fdc2b7a8024aaff78497ef00f24ef674193"
[[package]]
name = "yaml-rust"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
dependencies = [
"linked-hash-map",
]
[[package]]
name = "zbus"
version = "3.15.2"

View File

@@ -14,6 +14,7 @@ members = [
color-eyre = "0.6"
dirs = "5"
dunce = "1"
schemars = "0.8"
serde = { version = "1", features = ["derive"] }
serde_json = { package = "serde_json_lenient", version = "0.2" }
serde_yaml = "0.9"
@@ -22,7 +23,7 @@ uds_windows = "1"
win32-display-data = { git = "https://github.com/LGUG2Z/win32-display-data", rev = "32a45cebf132c3d651ee22c0c40033a6b7edc945" }
windows-implement = { version = "0.53" }
windows-interface = { version = "0.53" }
shadow-rs = "0.29"
shadow-rs = "0.34"
[workspace.dependencies.windows]
version = "0.54"

View File

@@ -8,17 +8,22 @@ edition = "2021"
[dependencies]
komorebi-client = { path = "../komorebi-client" }
anyhow = "1"
catppuccin-egui = { version = "5.1", default-features = false, features = ["egui28"] }
chrono = "0.4"
color-eyre = "0.6"
crossbeam-channel = "0.5"
dirs = { workspace = true }
eframe = "0.28"
egui-phosphor = "0.6.0"
font-loader = "0.11"
image = "0.25"
netdev = "0.30"
serde_json = "1"
schemars = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
serde_yaml = "0.8"
starship-battery = "0.10"
sysinfo = "0.31"
windows = { workspace = true }
windows-icons = "0.1"
egui-phosphor = "0.6.0"
clap = { version = "4", features = ["derive", "wrap_help"] }

184
komorebi-bar/src/bar.rs Normal file
View File

@@ -0,0 +1,184 @@
use crate::config::KomobarConfig;
use crate::config::Theme;
use crate::komorebi::Komorebi;
use crate::komorebi::KomorebiNotificationState;
use crate::widget::BarWidget;
use crate::widget::WidgetConfig;
use crossbeam_channel::Receiver;
use eframe::egui::Align;
use eframe::egui::CentralPanel;
use eframe::egui::Context;
use eframe::egui::FontData;
use eframe::egui::FontDefinitions;
use eframe::egui::FontFamily;
use eframe::egui::Frame;
use eframe::egui::Layout;
use eframe::egui::Margin;
use font_loader::system_fonts;
use font_loader::system_fonts::FontPropertyBuilder;
use std::cell::RefCell;
use std::ops::Deref;
use std::rc::Rc;
use std::sync::Arc;
pub struct Komobar {
pub config: KomobarConfig,
pub komorebi_notification_state: Option<Rc<RefCell<KomorebiNotificationState>>>,
pub left_widgets: Vec<Box<dyn BarWidget>>,
pub right_widgets: Vec<Box<dyn BarWidget>>,
pub rx_gui: Receiver<komorebi_client::Notification>,
}
impl Komobar {
pub fn new(
cc: &eframe::CreationContext<'_>,
rx_gui: Receiver<komorebi_client::Notification>,
config: Arc<KomobarConfig>,
) -> Self {
if let Some(font_family) = &config.font_family {
Self::add_custom_font(&cc.egui_ctx, font_family);
}
match config.theme {
Some(Theme::CatppuccinFrappe) => {
catppuccin_egui::set_theme(&cc.egui_ctx, catppuccin_egui::FRAPPE);
}
Some(Theme::CatppuccinMacchiato) => {
catppuccin_egui::set_theme(&cc.egui_ctx, catppuccin_egui::MACCHIATO);
}
Some(Theme::CatppuccinMocha) => {
catppuccin_egui::set_theme(&cc.egui_ctx, catppuccin_egui::MOCHA);
}
Some(Theme::Default) | None => {}
}
let mut komorebi_widget = None;
let mut komorebi_widget_idx = None;
let mut komorebi_notification_state = None;
let mut side = None;
for (idx, widget_config) in config.left_widgets.iter().enumerate() {
if let WidgetConfig::Komorebi(config) = widget_config {
komorebi_widget = Some(Komorebi::from(*config));
komorebi_widget_idx = Some(idx);
side = Some(Side::Left);
}
}
for (idx, widget_config) in config.right_widgets.iter().enumerate() {
if let WidgetConfig::Komorebi(config) = widget_config {
komorebi_widget = Some(Komorebi::from(*config));
komorebi_widget_idx = Some(idx);
side = Some(Side::Right);
}
}
let mut left_widgets = config
.left_widgets
.iter()
.map(|config| config.as_boxed_bar_widget())
.collect::<Vec<Box<dyn BarWidget>>>();
let mut right_widgets = config
.right_widgets
.iter()
.map(|config| config.as_boxed_bar_widget())
.collect::<Vec<Box<dyn BarWidget>>>();
if let (Some(idx), Some(widget), Some(side)) = (komorebi_widget_idx, komorebi_widget, side)
{
komorebi_notification_state = Some(widget.komorebi_notification_state.clone());
let boxed: Box<dyn BarWidget> = Box::new(widget);
match side {
Side::Left => left_widgets[idx] = boxed,
Side::Right => right_widgets[idx] = boxed,
}
}
right_widgets.reverse();
Self {
config: config.deref().clone(),
komorebi_notification_state,
left_widgets,
right_widgets,
rx_gui,
}
}
fn add_custom_font(ctx: &Context, name: &str) {
let mut fonts = FontDefinitions::default();
egui_phosphor::add_to_fonts(&mut fonts, egui_phosphor::Variant::Regular);
let property = FontPropertyBuilder::new().family(name).build();
if let Some((font, _)) = system_fonts::get(&property) {
fonts
.font_data
.insert(name.to_owned(), FontData::from_owned(font));
fonts
.families
.entry(FontFamily::Proportional)
.or_default()
.insert(0, name.to_owned());
fonts
.families
.entry(FontFamily::Monospace)
.or_default()
.push(name.to_owned());
// Tell egui to use these fonts:
ctx.set_fonts(fonts);
}
}
}
impl eframe::App for Komobar {
// TODO: I think this is needed for transparency??
// fn clear_color(&self, _visuals: &Visuals) -> [f32; 4] {
// egui::Rgba::TRANSPARENT.to_array()
// let mut background = Color32::from_gray(18).to_normalized_gamma_f32();
// background[3] = 0.9;
// background
// }
fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) {
if let Some(komorebi_notification_state) = &self.komorebi_notification_state {
komorebi_notification_state
.borrow_mut()
.handle_notification(self.config.monitor.index, self.rx_gui.clone());
}
let frame = if let Some(frame) = &self.config.frame {
Frame::none().outer_margin(Margin::symmetric(
frame.outer_margin.x,
frame.outer_margin.y,
))
} else {
Frame::none()
};
CentralPanel::default().frame(frame).show(ctx, |ui| {
ui.horizontal_centered(|ui| {
ui.with_layout(Layout::left_to_right(Align::Center), |ui| {
for w in &mut self.left_widgets {
w.render(ctx, ui);
}
});
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
for w in &mut self.right_widgets {
w.render(ctx, ui);
}
})
})
});
}
}
#[derive(Copy, Clone)]
enum Side {
Left,
Right,
}

View File

@@ -1,16 +1,21 @@
use crate::widget::BarWidget;
use crate::WIDGET_SPACING;
use eframe::egui::Context;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::Ui;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use starship_battery::units::ratio::percent;
use starship_battery::Manager;
use starship_battery::State;
use std::time::Duration;
use std::time::Instant;
#[derive(Copy, Clone, Debug)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct BatteryConfig {
/// Enable the Battery widget
pub enable: bool,
}
@@ -102,7 +107,7 @@ impl BarWidget for Battery {
);
}
ui.add_space(10.0);
ui.add_space(WIDGET_SPACING);
}
}
}

106
komorebi-bar/src/config.rs Normal file
View File

@@ -0,0 +1,106 @@
use crate::widget::WidgetConfig;
use eframe::egui::Pos2;
use eframe::egui::TextBuffer;
use eframe::egui::Vec2;
use komorebi_client::Rect;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::path::PathBuf;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct KomobarConfig {
/// Viewport options (see: https://docs.rs/egui/latest/egui/viewport/struct.ViewportBuilder.html)
pub viewport: Option<ViewportConfig>,
/// Frame options (see: https://docs.rs/egui/latest/egui/containers/struct.Frame.html)
pub frame: Option<FrameConfig>,
/// Monitor options
pub monitor: MonitorConfig,
/// Font family
pub font_family: Option<String>,
/// Theme
pub theme: Option<Theme>,
/// Left side widgets (ordered left-to-right)
pub left_widgets: Vec<WidgetConfig>,
/// Right side widgets (ordered left-to-right)
pub right_widgets: Vec<WidgetConfig>,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct ViewportConfig {
/// The desired starting position of the bar (0,0 = top left of the screen)
pub position: Option<Position>,
/// The desired size of the bar from the starting position (usually monitor width x desired height)
pub inner_size: Option<Position>,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct FrameConfig {
/// Margin outside the painted frame
pub outer_margin: Position,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct MonitorConfig {
/// Komorebi monitor index of the monitor on which to render the bar
pub index: usize,
/// Automatically apply a work area offset for this monitor to accommodate the bar
pub work_area_offset: Option<Rect>,
}
impl KomobarConfig {
pub fn read(path: &PathBuf) -> color_eyre::Result<Self> {
let content = std::fs::read_to_string(path)?;
let mut value: Self = match path.extension().unwrap().to_string_lossy().as_str() {
"yaml" => serde_yaml::from_str(&content)?,
"json" => serde_json::from_str(&content)?,
_ => panic!("unsupported format"),
};
if value.frame.is_none() {
value.frame = Some(FrameConfig {
outer_margin: Position { x: 10.0, y: 10.0 },
});
}
Ok(value)
}
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct Position {
/// X coordinate
pub x: f32,
/// Y coordinate
pub y: f32,
}
impl From<Position> for Vec2 {
fn from(value: Position) -> Self {
Self {
x: value.x,
y: value.y,
}
}
}
impl From<Position> for Pos2 {
fn from(value: Position) -> Self {
Self {
x: value.x,
y: value.y,
}
}
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub enum Theme {
/// Default egui theme
Default,
/// Catpuccin Frappe
CatppuccinFrappe,
/// Catpuccin Macchiato
CatppuccinMacchiato,
/// Catpuccin Mocha
CatppuccinMocha,
}

View File

@@ -1,12 +1,18 @@
use crate::widget::BarWidget;
use crate::WIDGET_SPACING;
use eframe::egui::Context;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::Ui;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
#[derive(Copy, Clone, Debug)]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct DateConfig {
/// Enable the Date widget
pub enable: bool,
/// Set the Date format
pub format: DateFormat,
}
@@ -19,12 +25,18 @@ impl From<DateConfig> for Date {
}
}
#[derive(Copy, Clone, Debug)]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
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)
Custom(String),
}
impl DateFormat {
@@ -34,6 +46,7 @@ impl DateFormat {
DateFormat::YearMonthDate => *self = Self::DateMonthYear,
DateFormat::DateMonthYear => *self = Self::DayDateMonthYear,
DateFormat::DayDateMonthYear => *self = Self::MonthDateYear,
_ => {}
};
}
@@ -43,11 +56,12 @@ impl DateFormat {
DateFormat::YearMonthDate => String::from("%F"),
DateFormat::DateMonthYear => String::from("%v"),
DateFormat::DayDateMonthYear => String::from("%A %e %B %Y"),
DateFormat::Custom(custom) => custom.to_string(),
}
}
}
#[derive(Copy, Clone, Debug)]
#[derive(Clone, Debug)]
pub struct Date {
pub enable: bool,
pub format: DateFormat,
@@ -82,7 +96,7 @@ impl BarWidget for Date {
}
// TODO: make spacing configurable
ui.add_space(10.0);
ui.add_space(WIDGET_SPACING);
}
}
}

View File

@@ -1,47 +1,59 @@
use crate::img_to_texture;
use crate::widget::BarWidget;
use crate::KomorebiNotificationState;
use crate::WIDGET_SPACING;
use crossbeam_channel::Receiver;
use eframe::egui::ColorImage;
use eframe::egui::Context;
use eframe::egui::Image;
use eframe::egui::Label;
use eframe::egui::SelectableLabel;
use eframe::egui::Sense;
use eframe::egui::TextureHandle;
use eframe::egui::TextureOptions;
use eframe::egui::Ui;
use image::RgbaImage;
use komorebi_client::CycleDirection;
use komorebi_client::SocketMessage;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Copy, Clone, Debug)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct KomorebiConfig {
pub enable: bool,
pub monitor_index: usize,
/// Configure the Workspaces widget
pub workspaces: KomorebiWorkspacesConfig,
/// Configure the Layout widget
pub layout: KomorebiLayoutConfig,
/// Configure the Focused Window widget
pub focused_window: KomorebiFocusedWindowConfig,
}
#[derive(Copy, Clone, Debug)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct KomorebiWorkspacesConfig {
/// Enable the Komorebi Workspaces widget
pub enable: bool,
/// Hide workspaces without any windows
pub hide_empty_workspaces: bool,
}
#[derive(Copy, Clone, Debug)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct KomorebiLayoutConfig {
/// Enable the Komorebi Layout widget
pub enable: bool,
}
#[derive(Copy, Clone, Debug)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct KomorebiFocusedWindowConfig {
/// Enable the Komorebi Focused Window widget
pub enable: bool,
/// Show the icon of the currently focused window
pub show_icon: bool,
}
impl From<KomorebiConfig> for Komorebi {
fn from(value: KomorebiConfig) -> Self {
Self {
enable: value.enable,
komorebi_notification_state: Rc::new(RefCell::new(KomorebiNotificationState {
selected_workspace: String::new(),
focused_window_title: String::new(),
@@ -49,7 +61,6 @@ impl From<KomorebiConfig> for Komorebi {
focused_window_icon: None,
layout: String::new(),
workspaces: vec![],
monitor_index: value.monitor_index,
hide_empty_workspaces: value.workspaces.hide_empty_workspaces,
})),
workspaces: value.workspaces,
@@ -61,7 +72,6 @@ impl From<KomorebiConfig> for Komorebi {
#[derive(Clone, Debug)]
pub struct Komorebi {
pub enable: bool,
pub komorebi_notification_state: Rc<RefCell<KomorebiNotificationState>>,
pub workspaces: KomorebiWorkspacesConfig,
pub layout: KomorebiLayoutConfig,
@@ -70,72 +80,150 @@ pub struct Komorebi {
impl BarWidget for Komorebi {
fn render(&mut self, ctx: &Context, ui: &mut Ui) {
if self.enable {
let mut komorebi_notification_state = self.komorebi_notification_state.borrow_mut();
let mut komorebi_notification_state = self.komorebi_notification_state.borrow_mut();
if self.workspaces.enable {
let mut update = None;
if self.workspaces.enable {
for (i, ws) in komorebi_notification_state.workspaces.iter().enumerate() {
if ui
.add(SelectableLabel::new(
komorebi_notification_state.selected_workspace.eq(ws),
ws.to_string(),
))
.clicked()
{
update = Some(ws.to_string());
komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(false))
.unwrap();
komorebi_client::send_message(&SocketMessage::FocusWorkspaceNumber(i))
.unwrap();
// TODO: store MFF value from state and restore that here instead of "true"
komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(true))
.unwrap();
komorebi_client::send_message(&SocketMessage::Retile).unwrap();
}
}
if let Some(update) = update {
komorebi_notification_state.selected_workspace = update;
}
ui.add_space(10.0);
}
if self.layout.enable {
for (i, ws) in komorebi_notification_state.workspaces.iter().enumerate() {
if ui
.add(
Label::new(&komorebi_notification_state.layout)
.selectable(false)
.sense(Sense::click()),
)
.add(SelectableLabel::new(
komorebi_notification_state.selected_workspace.eq(ws),
ws.to_string(),
))
.clicked()
{
komorebi_client::send_message(&SocketMessage::CycleLayout(
CycleDirection::Next,
))
.unwrap();
update = Some(ws.to_string());
komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(false))
.unwrap();
komorebi_client::send_message(&SocketMessage::FocusWorkspaceNumber(i)).unwrap();
// TODO: store MFF value from state and restore that here instead of "true"
komorebi_client::send_message(&SocketMessage::MouseFollowsFocus(true)).unwrap();
komorebi_client::send_message(&SocketMessage::Retile).unwrap();
}
ui.add_space(10.0);
}
if self.focused_window.enable {
if self.focused_window.show_icon {
if let Some(img) = &komorebi_notification_state.focused_window_icon {
ui.add(
Image::from(&img_to_texture(ctx, img))
.maintain_aspect_ratio(true)
.max_height(15.0),
);
if let Some(update) = update {
komorebi_notification_state.selected_workspace = update;
}
ui.add_space(WIDGET_SPACING);
}
if self.layout.enable {
if ui
.add(
Label::new(&komorebi_notification_state.layout)
.selectable(false)
.sense(Sense::click()),
)
.clicked()
{
komorebi_client::send_message(&SocketMessage::CycleLayout(CycleDirection::Next))
.unwrap();
}
ui.add_space(WIDGET_SPACING);
}
if self.focused_window.enable {
if self.focused_window.show_icon {
if let Some(img) = &komorebi_notification_state.focused_window_icon {
ui.add(
Image::from(&img_to_texture(ctx, img))
.maintain_aspect_ratio(true)
.max_height(15.0),
);
}
}
ui.add(Label::new(&komorebi_notification_state.focused_window_title).selectable(false));
ui.add_space(WIDGET_SPACING);
}
}
}
fn img_to_texture(ctx: &Context, rgba_image: &RgbaImage) -> TextureHandle {
let size = [rgba_image.width() as usize, rgba_image.height() as usize];
let pixels = rgba_image.as_flat_samples();
let color_image = ColorImage::from_rgba_unmultiplied(size, pixels.as_slice());
ctx.load_texture("icon", color_image, TextureOptions::default())
}
#[derive(Clone, Debug)]
pub struct KomorebiNotificationState {
pub workspaces: Vec<String>,
pub selected_workspace: String,
pub focused_window_title: String,
pub focused_window_pid: Option<u32>,
pub focused_window_icon: Option<RgbaImage>,
pub layout: String,
pub hide_empty_workspaces: bool,
}
impl KomorebiNotificationState {
pub fn handle_notification(
&mut self,
monitor_index: usize,
rx_gui: Receiver<komorebi_client::Notification>,
) {
if let Ok(notification) = rx_gui.try_recv() {
let monitor = &notification.state.monitors.elements()[monitor_index];
let focused_workspace_idx = monitor.focused_workspace_idx();
let mut workspaces = vec![];
self.selected_workspace = monitor.workspaces()[focused_workspace_idx]
.name()
.to_owned()
.unwrap_or_else(|| format!("{}", focused_workspace_idx + 1));
for (i, ws) in monitor.workspaces().iter().enumerate() {
let should_add = if self.hide_empty_workspaces {
focused_workspace_idx == i || !ws.containers().is_empty()
} else {
true
};
if should_add {
workspaces.push(ws.name().to_owned().unwrap_or_else(|| format!("{}", i + 1)));
}
}
self.workspaces = workspaces;
self.layout = match monitor.workspaces()[focused_workspace_idx].layout() {
komorebi_client::Layout::Default(layout) => layout.to_string(),
komorebi_client::Layout::Custom(_) => String::from("Custom"),
};
if let Some(container) = monitor.workspaces()[focused_workspace_idx].focused_container()
{
if let Some(window) = container.focused_window() {
if let Ok(title) = window.title() {
self.focused_window_title.clone_from(&title);
self.focused_window_pid = Some(window.process_id());
let img = windows_icons::get_icon_by_process_id(window.process_id());
self.focused_window_icon = Some(img);
}
}
} else {
self.focused_window_title.clear();
self.focused_window_icon = None;
}
ui.add(
Label::new(&komorebi_notification_state.focused_window_title).selectable(false),
);
if let Some(container) = monitor.workspaces()[focused_workspace_idx].monocle_container()
{
if let Some(window) = container.focused_window() {
if let Ok(title) = window.title() {
self.focused_window_title.clone_from(&title);
}
}
}
ui.add_space(10.0);
if let Some(window) = monitor.workspaces()[focused_workspace_idx].maximized_window() {
if let Ok(title) = window.title() {
self.focused_window_title.clone_from(&title);
}
}
}
}

View File

@@ -1,4 +1,6 @@
mod bar;
mod battery;
mod config;
mod date;
mod komorebi;
mod media;
@@ -8,161 +10,125 @@ mod storage;
mod time;
mod widget;
use crate::battery::Battery;
use crate::battery::BatteryConfig;
use crate::date::Date;
use crate::date::DateConfig;
use crate::date::DateFormat;
use crate::komorebi::Komorebi;
use crate::komorebi::KomorebiConfig;
use crate::komorebi::KomorebiFocusedWindowConfig;
use crate::komorebi::KomorebiLayoutConfig;
use crate::komorebi::KomorebiWorkspacesConfig;
use crate::media::Media;
use crate::media::MediaConfig;
use crate::memory::Memory;
use crate::memory::MemoryConfig;
use crate::network::Network;
use crate::network::NetworkConfig;
use crate::storage::Storage;
use crate::storage::StorageConfig;
use crate::time::TimeConfig;
use crate::time::TimeFormat;
use crate::widget::BarWidget;
use crossbeam_channel::Receiver;
use eframe::egui;
use eframe::egui::Align;
use eframe::egui::ColorImage;
use eframe::egui::Context;
use eframe::egui::Layout;
use eframe::egui::TextureHandle;
use crate::bar::Komobar;
use crate::config::KomobarConfig;
use crate::config::Position;
use clap::Parser;
use eframe::egui::ViewportBuilder;
use eframe::emath::Pos2;
use eframe::emath::Vec2;
use font_loader::system_fonts;
use font_loader::system_fonts::FontPropertyBuilder;
use image::RgbaImage;
use komorebi_client::SocketMessage;
use std::cell::RefCell;
use schemars::gen::SchemaSettings;
use std::io::BufReader;
use std::io::Read;
use std::ops::Deref;
use std::rc::Rc;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use time::Time;
#[derive(Copy, Clone, Debug)]
pub struct Position {
x: f32,
y: f32,
}
pub static WIDGET_SPACING: f32 = 10.0;
impl From<Position> for Vec2 {
fn from(value: Position) -> Self {
Self {
x: value.x,
y: value.y,
}
}
}
impl From<Position> for Pos2 {
fn from(value: Position) -> Self {
Self {
x: value.x,
y: value.y,
}
}
}
#[derive(Clone, Debug)]
pub struct Config {
inner_size: Position,
position: Position,
outer_margin: Position,
monitor_index: usize,
monitor_work_area_offset: Option<komorebi_client::Rect>,
font_family: Option<String>,
time: TimeConfig,
date: DateConfig,
storage: StorageConfig,
memory: MemoryConfig,
media: MediaConfig,
battery: BatteryConfig,
network: NetworkConfig,
komorebi: KomorebiConfig,
theme: Theme,
}
#[derive(Copy, Clone, Debug)]
pub enum Theme {
Default,
CatppuccinFrappe,
CatppuccinMacchiato,
CatppuccinMocha,
#[derive(Parser)]
#[clap(author, about, version)]
struct Opts {
/// Print the JSON schema of the configuration file and exit
#[clap(long)]
schema: bool,
/// Path to a JSON or YAML configuration file
#[clap(short, long)]
config: Option<PathBuf>,
}
fn main() -> eframe::Result<()> {
let config = Config {
inner_size: Position { x: 5120.0, y: 20.0 },
position: Position { x: 0.0, y: 0.0 },
outer_margin: Position { x: 10.0, y: 10.0 },
monitor_index: 0,
font_family: Some(String::from("JetBrains Mono")),
monitor_work_area_offset: Some(komorebi_client::Rect {
left: 0,
top: 40,
right: 0,
bottom: 40,
}),
time: TimeConfig {
enable: true,
format: TimeFormat::TwentyFourHour,
let opts: Opts = Opts::parse();
if opts.schema {
let settings = SchemaSettings::default().with(|s| {
s.option_nullable = false;
s.option_add_null_type = false;
s.inline_subschemas = true;
});
let gen = settings.into_generator();
let socket_message = gen.into_root_schema_for::<KomobarConfig>();
let schema = serde_json::to_string_pretty(&socket_message).unwrap();
println!("{schema}");
std::process::exit(0);
}
let home_dir: PathBuf = std::env::var("KOMOREBI_CONFIG_HOME").map_or_else(
|_| dirs::home_dir().expect("there is no home directory"),
|home_path| {
let home = PathBuf::from(&home_path);
if home.as_path().is_dir() {
home
} else {
panic!("$Env:KOMOREBI_CONFIG_HOME is set to '{home_path}', which is not a valid directory");
}
},
date: DateConfig {
enable: true,
format: DateFormat::DayDateMonthYear,
);
let config_path = opts.config.map_or_else(
|| {
let mut config = home_dir.join("komorebi.bar.json");
if !config.is_file() {
config.pop();
config.push("komorebi.bar.yaml");
}
if !config.is_file() {
None
} else {
Some(config)
}
},
storage: StorageConfig { enable: true },
memory: MemoryConfig { enable: true },
media: MediaConfig { enable: true },
battery: BatteryConfig { enable: true },
network: NetworkConfig {
enable: true,
show_data: true,
},
komorebi: KomorebiConfig {
enable: true,
monitor_index: 0,
workspaces: KomorebiWorkspacesConfig {
enable: true,
hide_empty_workspaces: true,
},
layout: KomorebiLayoutConfig { enable: true },
focused_window: KomorebiFocusedWindowConfig {
enable: true,
show_icon: true,
},
},
theme: Theme::CatppuccinMacchiato,
Option::from,
);
let config = match config_path {
None => panic!(
"no komorebi.bar.json or komorebi.bar.yaml found in {}",
home_dir.as_path().to_string_lossy()
),
Some(config) => KomobarConfig::read(&config).unwrap(),
};
// TODO: ensure that config.monitor_index represents a valid komorebi monitor index
let mut builder = ViewportBuilder::default()
.with_decorations(false)
// .with_transparent(config.transparent)
.with_taskbar(false)
.with_position(Position { x: 0.0, y: 0.0 })
.with_inner_size({
let state = serde_json::from_str::<komorebi_client::State>(
&komorebi_client::send_query(&SocketMessage::State).unwrap(),
)
.unwrap();
Position {
x: state.monitors.elements()[config.monitor.index].size().right as f32,
y: 20.0,
}
});
if let Some(viewport) = &config.viewport {
if let Some(position) = &viewport.position {
let b = builder.clone();
builder = b.with_position(*position);
}
if let Some(inner_size) = &viewport.inner_size {
let b = builder.clone();
builder = b.with_inner_size(*inner_size);
}
}
let native_options = eframe::NativeOptions {
viewport: ViewportBuilder::default()
.with_decorations(false)
// .with_transparent(config.transparent)
.with_position(config.position)
.with_taskbar(false)
.with_inner_size(config.inner_size),
viewport: builder,
..Default::default()
};
if let Some(rect) = &config.monitor_work_area_offset {
if let Some(rect) = &config.monitor.work_area_offset {
komorebi_client::send_message(&SocketMessage::MonitorWorkAreaOffset(
config.monitor_index,
config.monitor.index,
*rect,
))
.unwrap();
@@ -207,10 +173,10 @@ fn main() -> eframe::Result<()> {
}
// here we have reconnected
if let Some(rect) = &config_cl.monitor_work_area_offset {
if let Some(rect) = &config_cl.monitor.work_area_offset {
while komorebi_client::send_message(
&SocketMessage::MonitorWorkAreaOffset(
config_cl.monitor_index,
config_cl.monitor.index,
*rect,
),
)
@@ -241,214 +207,3 @@ fn main() -> eframe::Result<()> {
}),
)
}
struct Komobar {
config: Config,
komorebi_notification_state: Rc<RefCell<KomorebiNotificationState>>,
left_widgets: Vec<Box<dyn BarWidget>>,
right_widgets: Vec<Box<dyn BarWidget>>,
rx_gui: Receiver<komorebi_client::Notification>,
}
#[derive(Clone, Debug)]
struct KomorebiNotificationState {
monitor_index: usize,
workspaces: Vec<String>,
selected_workspace: String,
focused_window_title: String,
focused_window_pid: Option<u32>,
focused_window_icon: Option<RgbaImage>,
layout: String,
hide_empty_workspaces: bool,
}
impl KomorebiNotificationState {
fn handle_notification(&mut self, rx_gui: Receiver<komorebi_client::Notification>) {
if let Ok(notification) = rx_gui.try_recv() {
let monitor = &notification.state.monitors.elements()[self.monitor_index];
let focused_workspace_idx = monitor.focused_workspace_idx();
let mut workspaces = vec![];
self.selected_workspace = monitor.workspaces()[focused_workspace_idx]
.name()
.to_owned()
.unwrap_or_else(|| format!("{}", focused_workspace_idx + 1));
for (i, ws) in monitor.workspaces().iter().enumerate() {
let should_add = if self.hide_empty_workspaces {
focused_workspace_idx == i || !ws.containers().is_empty()
} else {
true
};
if should_add {
workspaces.push(ws.name().to_owned().unwrap_or_else(|| format!("{}", i + 1)));
}
}
self.workspaces = workspaces;
self.layout = match monitor.workspaces()[focused_workspace_idx].layout() {
komorebi_client::Layout::Default(layout) => layout.to_string(),
komorebi_client::Layout::Custom(_) => String::from("Custom"),
};
if let Some(container) = monitor.workspaces()[focused_workspace_idx].focused_container()
{
if let Some(window) = container.focused_window() {
if let Ok(title) = window.title() {
self.focused_window_title.clone_from(&title);
self.focused_window_pid = Some(window.process_id());
let img = windows_icons::get_icon_by_process_id(window.process_id());
self.focused_window_icon = Some(img);
}
}
} else {
self.focused_window_title.clear();
self.focused_window_icon = None;
}
if let Some(container) = monitor.workspaces()[focused_workspace_idx].monocle_container()
{
if let Some(window) = container.focused_window() {
if let Ok(title) = window.title() {
self.focused_window_title.clone_from(&title);
}
}
}
if let Some(window) = monitor.workspaces()[focused_workspace_idx].maximized_window() {
if let Ok(title) = window.title() {
self.focused_window_title.clone_from(&title);
}
}
}
}
}
fn add_custom_font(ctx: &Context, name: &str) {
let mut fonts = egui::FontDefinitions::default();
egui_phosphor::add_to_fonts(&mut fonts, egui_phosphor::Variant::Regular);
let property = FontPropertyBuilder::new().family(name).build();
if let Some((font, _)) = system_fonts::get(&property) {
// Install my own font (maybe supporting non-latin characters).
// .ttf and .otf files supported.
fonts
.font_data
.insert(name.to_owned(), egui::FontData::from_owned(font));
// Put my font first (highest priority) for proportional text:
fonts
.families
.entry(egui::FontFamily::Proportional)
.or_default()
.insert(0, name.to_owned());
// Put my font as last fallback for monospace:
fonts
.families
.entry(egui::FontFamily::Monospace)
.or_default()
.push(name.to_owned());
// Tell egui to use these fonts:
ctx.set_fonts(fonts);
}
}
impl Komobar {
fn new(
cc: &eframe::CreationContext<'_>,
rx: Receiver<komorebi_client::Notification>,
config: Arc<Config>,
) -> Self {
if let Some(font_family) = &config.font_family {
add_custom_font(&cc.egui_ctx, font_family);
}
match config.theme {
Theme::Default => {}
Theme::CatppuccinFrappe => {
catppuccin_egui::set_theme(&cc.egui_ctx, catppuccin_egui::FRAPPE);
}
Theme::CatppuccinMacchiato => {
catppuccin_egui::set_theme(&cc.egui_ctx, catppuccin_egui::MACCHIATO);
}
Theme::CatppuccinMocha => {
catppuccin_egui::set_theme(&cc.egui_ctx, catppuccin_egui::MOCHA);
}
}
// Customize egui here with cc.egui_ctx.set_fonts and cc.egui_ctx.set_visuals.
// Restore app state using cc.storage (requires the "persistence" feature).
// Use the cc.gl (a glow::Context) to create graphics shaders and buffers that you can use
// for e.g. egui::PaintCallback.
let komorebi_workspaces = Komorebi::from(config.komorebi);
let komorebi_notification_state = komorebi_workspaces.komorebi_notification_state.clone();
let left_widgets: Vec<Box<dyn BarWidget>> = vec![Box::new(komorebi_workspaces.clone())];
let mut right_widgets: Vec<Box<dyn BarWidget>> = vec![
Box::new(Media::from(config.media)),
Box::new(Storage::from(config.storage)),
Box::new(Memory::from(config.memory)),
Box::new(Network::from(config.network)),
Box::new(Date::from(config.date)),
Box::new(Time::from(config.time)),
Box::new(Battery::from(config.battery)),
];
right_widgets.reverse();
Self {
config: config.deref().clone(),
komorebi_notification_state,
left_widgets,
right_widgets,
rx_gui: rx,
}
}
}
fn img_to_texture(ctx: &Context, rgba_image: &RgbaImage) -> TextureHandle {
let size = [rgba_image.width() as usize, rgba_image.height() as usize];
let pixels = rgba_image.as_flat_samples();
let color_image = ColorImage::from_rgba_unmultiplied(size, pixels.as_slice());
ctx.load_texture("icon", color_image, egui::TextureOptions::default())
}
impl eframe::App for Komobar {
// TODO: I think this is needed for transparency??
// fn clear_color(&self, _visuals: &Visuals) -> [f32; 4] {
// egui::Rgba::TRANSPARENT.to_array()
// let mut background = Color32::from_gray(18).to_normalized_gamma_f32();
// background[3] = 0.9;
// background
// }
fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) {
self.komorebi_notification_state
.borrow_mut()
.handle_notification(self.rx_gui.clone());
egui::CentralPanel::default()
.frame(egui::Frame::none().outer_margin(egui::Margin::symmetric(
self.config.outer_margin.x,
self.config.outer_margin.y,
)))
.show(ctx, |ui| {
ui.horizontal_centered(|ui| {
ui.with_layout(Layout::left_to_right(Align::Center), |ui| {
for w in &mut self.left_widgets {
w.render(ctx, ui);
}
});
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
for w in &mut self.right_widgets {
w.render(ctx, ui);
}
})
})
});
}
}

View File

@@ -1,12 +1,17 @@
use crate::widget::BarWidget;
use crate::WIDGET_SPACING;
use eframe::egui::Context;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::Ui;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use windows::Media::Control::GlobalSystemMediaTransportControlsSessionManager;
#[derive(Copy, Clone, Debug)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct MediaConfig {
/// Enable the Media widget
pub enable: bool,
}
@@ -79,7 +84,7 @@ impl BarWidget for Media {
self.toggle();
}
ui.add_space(10.0);
ui.add_space(WIDGET_SPACING);
}
}
}

View File

@@ -1,22 +1,21 @@
use crate::widget::BarWidget;
use crate::WIDGET_SPACING;
use eframe::egui::Context;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::Ui;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::process::Command;
use std::time::Duration;
use std::time::Instant;
use sysinfo::RefreshKind;
use sysinfo::System;
pub struct Memory {
pub enable: bool,
system: System,
last_updated: Instant,
}
#[derive(Copy, Clone, Debug)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct MemoryConfig {
/// Enable the Memory widget
pub enable: bool,
}
@@ -35,6 +34,12 @@ impl From<MemoryConfig> for Memory {
}
}
pub struct Memory {
pub enable: bool,
system: System,
last_updated: Instant,
}
impl Memory {
fn output(&mut self) -> Vec<String> {
let now = Instant::now();
@@ -68,7 +73,7 @@ impl BarWidget for Memory {
}
}
ui.add_space(10.0);
ui.add_space(WIDGET_SPACING);
}
}
}

View File

@@ -1,16 +1,22 @@
use crate::widget::BarWidget;
use crate::WIDGET_SPACING;
use eframe::egui::Context;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::Ui;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::process::Command;
use std::time::Duration;
use std::time::Instant;
use sysinfo::Networks;
#[derive(Copy, Clone, Debug)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct NetworkConfig {
/// Enable the Network widget
pub enable: bool,
/// Show network transfer data
pub show_data: bool,
}
@@ -140,7 +146,7 @@ impl BarWidget for Network {
_ => {}
}
ui.add_space(10.0);
ui.add_space(WIDGET_SPACING);
}
}
}

View File

@@ -1,15 +1,20 @@
use crate::widget::BarWidget;
use crate::WIDGET_SPACING;
use eframe::egui::Context;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::Ui;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::process::Command;
use std::time::Duration;
use std::time::Instant;
use sysinfo::Disks;
#[derive(Copy, Clone, Debug)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct StorageConfig {
/// Enable the Storage widget
pub enable: bool,
}
@@ -87,7 +92,7 @@ impl BarWidget for Storage {
}
}
ui.add_space(10.0);
ui.add_space(WIDGET_SPACING);
}
}
}

View File

@@ -1,12 +1,18 @@
use crate::widget::BarWidget;
use crate::WIDGET_SPACING;
use eframe::egui::Context;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::Ui;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
#[derive(Copy, Clone, Debug)]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct TimeConfig {
/// Enable the Time widget
pub enable: bool,
/// Set the Time format
pub format: TimeFormat,
}
@@ -19,10 +25,14 @@ impl From<TimeConfig> for Time {
}
}
#[derive(Copy, Clone, Debug)]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub enum TimeFormat {
/// Twelve-hour format (with seconds)
TwelveHour,
/// Twenty-four-hour format (with seconds)
TwentyFourHour,
/// Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)
Custom(String),
}
impl TimeFormat {
@@ -30,6 +40,7 @@ impl TimeFormat {
match self {
TimeFormat::TwelveHour => *self = TimeFormat::TwentyFourHour,
TimeFormat::TwentyFourHour => *self = TimeFormat::TwelveHour,
_ => {}
};
}
@@ -37,11 +48,12 @@ impl TimeFormat {
match self {
TimeFormat::TwelveHour => String::from("%l:%M:%S %p"),
TimeFormat::TwentyFourHour => String::from("%T"),
TimeFormat::Custom(format) => format.to_string(),
}
}
}
#[derive(Copy, Clone, Debug)]
#[derive(Clone, Debug)]
pub struct Time {
pub enable: bool,
pub format: TimeFormat,
@@ -72,7 +84,7 @@ impl BarWidget for Time {
}
// TODO: make spacing configurable
ui.add_space(10.0);
ui.add_space(WIDGET_SPACING);
}
}
}

View File

@@ -1,6 +1,52 @@
use crate::battery::Battery;
use crate::battery::BatteryConfig;
use crate::date::Date;
use crate::date::DateConfig;
use crate::komorebi::Komorebi;
use crate::komorebi::KomorebiConfig;
use crate::media::Media;
use crate::media::MediaConfig;
use crate::memory::Memory;
use crate::memory::MemoryConfig;
use crate::network::Network;
use crate::network::NetworkConfig;
use crate::storage::Storage;
use crate::storage::StorageConfig;
use crate::time::Time;
use crate::time::TimeConfig;
use eframe::egui::Context;
use eframe::egui::Ui;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
pub trait BarWidget {
fn render(&mut self, ctx: &Context, ui: &mut Ui);
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub enum WidgetConfig {
Battery(BatteryConfig),
Date(DateConfig),
Komorebi(KomorebiConfig),
Media(MediaConfig),
Memory(MemoryConfig),
Network(NetworkConfig),
Storage(StorageConfig),
Time(TimeConfig),
}
impl WidgetConfig {
pub fn as_boxed_bar_widget(&self) -> Box<dyn BarWidget> {
match self {
WidgetConfig::Battery(config) => Box::new(Battery::from(*config)),
WidgetConfig::Date(config) => Box::new(Date::from(config.clone())),
WidgetConfig::Komorebi(config) => Box::new(Komorebi::from(*config)),
WidgetConfig::Media(config) => Box::new(Media::from(*config)),
WidgetConfig::Memory(config) => Box::new(Memory::from(*config)),
WidgetConfig::Network(config) => Box::new(Network::from(*config)),
WidgetConfig::Storage(config) => Box::new(Storage::from(*config)),
WidgetConfig::Time(config) => Box::new(Time::from(config.clone())),
}
}
}

View File

@@ -30,7 +30,7 @@ os_info = "3.8"
parking_lot = "0.12"
paste = "1"
regex = "1"
schemars = "0.8"
schemars = { workspace = true }
serde = { version = "1", features = ["derive"] }
serde_json = { workspace = true }
serde_yaml = { workspace = true }

788
schema.bar.json Normal file
View File

@@ -0,0 +1,788 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "KomobarConfig",
"type": "object",
"required": [
"left_widgets",
"monitor",
"right_widgets",
"theme"
],
"properties": {
"font_family": {
"description": "Font family",
"type": "string"
},
"frame": {
"description": "Frame options (see: https://docs.rs/egui/latest/egui/containers/struct.Frame.html)",
"type": "object",
"required": [
"outer_margin"
],
"properties": {
"outer_margin": {
"description": "Margin outside the painted frame",
"type": "object",
"required": [
"x",
"y"
],
"properties": {
"x": {
"description": "X coordinate",
"type": "number",
"format": "float"
},
"y": {
"description": "Y coordinate",
"type": "number",
"format": "float"
}
}
}
}
},
"left_widgets": {
"description": "Left side widgets (ordered left-to-right)",
"type": "array",
"items": {
"oneOf": [
{
"type": "object",
"required": [
"Battery"
],
"properties": {
"Battery": {
"type": "object",
"required": [
"enable"
],
"properties": {
"enable": {
"description": "Enable the Battery widget",
"type": "boolean"
}
}
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"Date"
],
"properties": {
"Date": {
"type": "object",
"required": [
"enable",
"format"
],
"properties": {
"enable": {
"description": "Enable the Date widget",
"type": "boolean"
},
"format": {
"description": "Set the Date format",
"oneOf": [
{
"description": "Month/Date/Year format (09/08/24)",
"type": "string",
"enum": [
"MonthDateYear"
]
},
{
"description": "Year-Month-Date format (2024-09-08)",
"type": "string",
"enum": [
"YearMonthDate"
]
},
{
"description": "Date-Month-Year format (8-Sep-2024)",
"type": "string",
"enum": [
"DateMonthYear"
]
},
{
"description": "Day Date Month Year format (8 September 2024)",
"type": "string",
"enum": [
"DayDateMonthYear"
]
},
{
"description": "Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)",
"type": "object",
"required": [
"Custom"
],
"properties": {
"Custom": {
"type": "string"
}
},
"additionalProperties": false
}
]
}
}
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"Komorebi"
],
"properties": {
"Komorebi": {
"type": "object",
"required": [
"focused_window",
"layout",
"workspaces"
],
"properties": {
"focused_window": {
"description": "Configure the Focused Window widget",
"type": "object",
"required": [
"enable",
"show_icon"
],
"properties": {
"enable": {
"description": "Enable the Komorebi Focused Window widget",
"type": "boolean"
},
"show_icon": {
"description": "Show the icon of the currently focused window",
"type": "boolean"
}
}
},
"layout": {
"description": "Configure the Layout widget",
"type": "object",
"required": [
"enable"
],
"properties": {
"enable": {
"description": "Enable the Komorebi Layout widget",
"type": "boolean"
}
}
},
"workspaces": {
"description": "Configure the Workspaces widget",
"type": "object",
"required": [
"enable",
"hide_empty_workspaces"
],
"properties": {
"enable": {
"description": "Enable the Komorebi Workspaces widget",
"type": "boolean"
},
"hide_empty_workspaces": {
"description": "Hide workspaces without any windows",
"type": "boolean"
}
}
}
}
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"Media"
],
"properties": {
"Media": {
"type": "object",
"required": [
"enable"
],
"properties": {
"enable": {
"description": "Enable the Media widget",
"type": "boolean"
}
}
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"Memory"
],
"properties": {
"Memory": {
"type": "object",
"required": [
"enable"
],
"properties": {
"enable": {
"description": "Enable the Memory widget",
"type": "boolean"
}
}
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"Network"
],
"properties": {
"Network": {
"type": "object",
"required": [
"enable",
"show_data"
],
"properties": {
"enable": {
"description": "Enable the Network widget",
"type": "boolean"
},
"show_data": {
"description": "Show network transfer data",
"type": "boolean"
}
}
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"Storage"
],
"properties": {
"Storage": {
"type": "object",
"required": [
"enable"
],
"properties": {
"enable": {
"description": "Enable the Storage widget",
"type": "boolean"
}
}
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"Time"
],
"properties": {
"Time": {
"type": "object",
"required": [
"enable",
"format"
],
"properties": {
"enable": {
"description": "Enable the Time widget",
"type": "boolean"
},
"format": {
"description": "Set the Time format",
"oneOf": [
{
"description": "Twelve-hour format (with seconds)",
"type": "string",
"enum": [
"TwelveHour"
]
},
{
"description": "Twenty-four-hour format (with seconds)",
"type": "string",
"enum": [
"TwentyFourHour"
]
},
{
"description": "Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)",
"type": "object",
"required": [
"Custom"
],
"properties": {
"Custom": {
"type": "string"
}
},
"additionalProperties": false
}
]
}
}
}
},
"additionalProperties": false
}
]
}
},
"monitor": {
"description": "Monitor options",
"type": "object",
"required": [
"index"
],
"properties": {
"index": {
"description": "Komorebi monitor index of the monitor on which to render the bar",
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"work_area_offset": {
"description": "Automatically apply a work area offset for this monitor to accommodate the bar",
"type": "object",
"required": [
"bottom",
"left",
"right",
"top"
],
"properties": {
"bottom": {
"description": "The bottom point in a Win32 Rect",
"type": "integer",
"format": "int32"
},
"left": {
"description": "The left point in a Win32 Rect",
"type": "integer",
"format": "int32"
},
"right": {
"description": "The right point in a Win32 Rect",
"type": "integer",
"format": "int32"
},
"top": {
"description": "The top point in a Win32 Rect",
"type": "integer",
"format": "int32"
}
}
}
}
},
"right_widgets": {
"description": "Right side widgets (ordered left-to-right)",
"type": "array",
"items": {
"oneOf": [
{
"type": "object",
"required": [
"Battery"
],
"properties": {
"Battery": {
"type": "object",
"required": [
"enable"
],
"properties": {
"enable": {
"description": "Enable the Battery widget",
"type": "boolean"
}
}
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"Date"
],
"properties": {
"Date": {
"type": "object",
"required": [
"enable",
"format"
],
"properties": {
"enable": {
"description": "Enable the Date widget",
"type": "boolean"
},
"format": {
"description": "Set the Date format",
"oneOf": [
{
"description": "Month/Date/Year format (09/08/24)",
"type": "string",
"enum": [
"MonthDateYear"
]
},
{
"description": "Year-Month-Date format (2024-09-08)",
"type": "string",
"enum": [
"YearMonthDate"
]
},
{
"description": "Date-Month-Year format (8-Sep-2024)",
"type": "string",
"enum": [
"DateMonthYear"
]
},
{
"description": "Day Date Month Year format (8 September 2024)",
"type": "string",
"enum": [
"DayDateMonthYear"
]
},
{
"description": "Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)",
"type": "object",
"required": [
"Custom"
],
"properties": {
"Custom": {
"type": "string"
}
},
"additionalProperties": false
}
]
}
}
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"Komorebi"
],
"properties": {
"Komorebi": {
"type": "object",
"required": [
"focused_window",
"layout",
"workspaces"
],
"properties": {
"focused_window": {
"description": "Configure the Focused Window widget",
"type": "object",
"required": [
"enable",
"show_icon"
],
"properties": {
"enable": {
"description": "Enable the Komorebi Focused Window widget",
"type": "boolean"
},
"show_icon": {
"description": "Show the icon of the currently focused window",
"type": "boolean"
}
}
},
"layout": {
"description": "Configure the Layout widget",
"type": "object",
"required": [
"enable"
],
"properties": {
"enable": {
"description": "Enable the Komorebi Layout widget",
"type": "boolean"
}
}
},
"workspaces": {
"description": "Configure the Workspaces widget",
"type": "object",
"required": [
"enable",
"hide_empty_workspaces"
],
"properties": {
"enable": {
"description": "Enable the Komorebi Workspaces widget",
"type": "boolean"
},
"hide_empty_workspaces": {
"description": "Hide workspaces without any windows",
"type": "boolean"
}
}
}
}
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"Media"
],
"properties": {
"Media": {
"type": "object",
"required": [
"enable"
],
"properties": {
"enable": {
"description": "Enable the Media widget",
"type": "boolean"
}
}
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"Memory"
],
"properties": {
"Memory": {
"type": "object",
"required": [
"enable"
],
"properties": {
"enable": {
"description": "Enable the Memory widget",
"type": "boolean"
}
}
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"Network"
],
"properties": {
"Network": {
"type": "object",
"required": [
"enable",
"show_data"
],
"properties": {
"enable": {
"description": "Enable the Network widget",
"type": "boolean"
},
"show_data": {
"description": "Show network transfer data",
"type": "boolean"
}
}
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"Storage"
],
"properties": {
"Storage": {
"type": "object",
"required": [
"enable"
],
"properties": {
"enable": {
"description": "Enable the Storage widget",
"type": "boolean"
}
}
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"Time"
],
"properties": {
"Time": {
"type": "object",
"required": [
"enable",
"format"
],
"properties": {
"enable": {
"description": "Enable the Time widget",
"type": "boolean"
},
"format": {
"description": "Set the Time format",
"oneOf": [
{
"description": "Twelve-hour format (with seconds)",
"type": "string",
"enum": [
"TwelveHour"
]
},
{
"description": "Twenty-four-hour format (with seconds)",
"type": "string",
"enum": [
"TwentyFourHour"
]
},
{
"description": "Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)",
"type": "object",
"required": [
"Custom"
],
"properties": {
"Custom": {
"type": "string"
}
},
"additionalProperties": false
}
]
}
}
}
},
"additionalProperties": false
}
]
}
},
"theme": {
"description": "Theme",
"oneOf": [
{
"description": "Default egui theme",
"type": "string",
"enum": [
"Default"
]
},
{
"description": "Catpuccin Frappe",
"type": "string",
"enum": [
"CatppuccinFrappe"
]
},
{
"description": "Catpuccin Macchiato",
"type": "string",
"enum": [
"CatppuccinMacchiato"
]
},
{
"description": "Catpuccin Mocha",
"type": "string",
"enum": [
"CatppuccinMocha"
]
}
]
},
"viewport": {
"description": "Viewport options (see: https://docs.rs/egui/latest/egui/viewport/struct.ViewportBuilder.html)",
"type": "object",
"properties": {
"inner_size": {
"description": "The desired size of the bar from the starting position (usually monitor width x desired height)",
"type": "object",
"required": [
"x",
"y"
],
"properties": {
"x": {
"description": "X coordinate",
"type": "number",
"format": "float"
},
"y": {
"description": "Y coordinate",
"type": "number",
"format": "float"
}
}
},
"position": {
"description": "The desired starting position of the bar (0,0 = top left of the screen)",
"type": "object",
"required": [
"x",
"y"
],
"properties": {
"x": {
"description": "X coordinate",
"type": "number",
"format": "float"
},
"y": {
"description": "Y coordinate",
"type": "number",
"format": "float"
}
}
}
}
}
}
}