use crate::DEFAULT_PADDING; use crate::bar::exec_powershell; use crate::render::Grouping; use crate::widgets::widget::WidgetConfig; use eframe::egui::Pos2; use eframe::egui::TextBuffer; use eframe::egui::Vec2; use komorebi_client::PathExt; use komorebi_client::Rect; use komorebi_client::SocketMessage; use serde::Deserialize; use serde::Serialize; use std::collections::HashMap; use std::path::PathBuf; #[derive(Clone, Debug, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] /// The `komorebi.bar.json` configuration file reference for `v0.1.42` pub struct KomobarConfig { /// Bar height #[cfg_attr(feature = "schemars", schemars(extend("default" = 50)))] pub height: Option, /// Bar padding. Use one value for all sides or use a grouped padding for horizontal and/or /// vertical definition which can each take a single value for a symmetric padding or two /// values for each side, i.e.: /// ```json /// "padding": { /// "horizontal": 10 /// } /// ``` /// or: /// ```json /// "padding": { /// "horizontal": [left, right] /// } /// ``` /// You can also set individual padding on each side like this: /// ```json /// "padding": { /// "top": 10, /// "bottom": 10, /// "left": 10, /// "right": 10, /// } /// ``` /// By default, padding is set to 10 on all sides. pub padding: Option, /// Bar margin. Use one value for all sides or use a grouped margin for horizontal and/or /// vertical definition which can each take a single value for a symmetric margin or two /// values for each side, i.e.: /// ```json /// "margin": { /// "horizontal": 10 /// } /// ``` /// or: /// ```json /// "margin": { /// "vertical": [top, bottom] /// } /// ``` /// You can also set individual margin on each side like this: /// ```json /// "margin": { /// "top": 10, /// "bottom": 10, /// "left": 10, /// "right": 10, /// } /// ``` /// By default, margin is set to 0 on all sides. pub margin: Option, /// Bar positioning options #[serde(alias = "viewport")] pub position: Option, /// Frame options (see: https://docs.rs/egui/latest/egui/containers/frame/struct.Frame.html) pub frame: Option, /// The monitor index or the full monitor options #[cfg_attr(feature = "schemars", schemars(extend("default" = MonitorConfigOrIndex::Index(0))))] pub monitor: Option, /// Font family pub font_family: Option, /// Font size #[cfg_attr(feature = "schemars", schemars(extend("default" = 12.5)))] pub font_size: Option, /// Scale of the icons relative to the font_size [[1.0-2.0]] #[cfg_attr(feature = "schemars", schemars(extend("default" = 1.4)))] pub icon_scale: Option, /// Max label width before text truncation #[cfg_attr(feature = "schemars", schemars(extend("default" = 400.0)))] pub max_label_width: Option, /// Theme pub theme: Option, /// Alpha value for the color transparency [[0-255]] #[cfg_attr(feature = "schemars", schemars(extend("default" = 200)))] pub transparency_alpha: Option, /// Spacing between widgets #[cfg_attr(feature = "schemars", schemars(extend("default" = 10.0)))] pub widget_spacing: Option, /// Visual grouping for widgets pub grouping: Option, /// Options for mouse interaction on the bar pub mouse: Option, /// Left side widgets (ordered left-to-right) pub left_widgets: Vec, /// Center widgets (ordered left-to-right) pub center_widgets: Option>, /// Right side widgets (ordered left-to-right) pub right_widgets: Vec, } impl KomobarConfig { pub fn aliases(raw: &str) { let mut map = HashMap::new(); map.insert("position", ["viewport"]); map.insert("end", ["inner_frame"]); let mut display = false; for aliases in map.values() { for a in aliases { if raw.contains(a) { display = true; } } } if display { println!( "\nYour bar configuration file contains some options that have been renamed or deprecated:\n" ); for (canonical, aliases) in map { for alias in aliases { if raw.contains(alias) { println!(r#""{alias}" is now "{canonical}""#); } } } } } pub fn show_all_icons_on_komorebi_workspace(widgets: &[WidgetConfig]) -> bool { widgets .iter() .any(|w| matches!(w, WidgetConfig::Komorebi(config) if config.workspaces.is_some_and(|w| w.enable && w.display.is_some_and(|s| matches!(s, WorkspacesDisplayFormat::AllIcons | WorkspacesDisplayFormat::AllIconsAndText | WorkspacesDisplayFormat::AllIconsAndTextOnSelected))))) } } #[derive(Clone, Debug, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] /// Position configuration pub struct PositionConfig { /// The desired starting position of the bar (0,0 = top left of the screen) #[serde(alias = "position")] pub start: Option, /// The desired size of the bar from the starting position (usually monitor width x desired height) #[serde(alias = "inner_size")] pub end: Option, } #[derive(Clone, Debug, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] /// Frame configuration pub struct FrameConfig { /// Margin inside the painted frame pub inner_margin: Position, } #[derive(Clone, Debug, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(untagged)] /// Monitor configuration or monitor index pub enum MonitorConfigOrIndex { /// The monitor index where you want the bar to show Index(usize), /// The full monitor options with the index and an optional work_area_offset MonitorConfig(MonitorConfig), } #[derive(Clone, Debug, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] /// Monitor configuration 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, } pub type Padding = SpacingKind; pub type Margin = SpacingKind; #[derive(Clone, Debug, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(untagged)] // WARNING: To any developer messing with this code in the future: The order here matters! // `Grouped` needs to come last, otherwise serde might mistaken an `IndividualSpacingConfig` for a // `GroupedSpacingConfig` with both `vertical` and `horizontal` set to `None` ignoring the // individual values. /// Spacing kind pub enum SpacingKind { /// Spacing applied to all sides All(f32), /// Individual spacing applied to each side Individual(IndividualSpacingConfig), /// Grouped vertical and horizontal spacing Grouped(GroupedSpacingConfig), } impl SpacingKind { pub fn to_individual(&self, default: f32) -> IndividualSpacingConfig { match self { SpacingKind::All(m) => IndividualSpacingConfig::all(*m), SpacingKind::Grouped(grouped_spacing_config) => { let vm = grouped_spacing_config.vertical.as_ref().map_or( IndividualSpacingConfig::vertical(default), |vm| match vm { GroupedSpacingOptions::Symmetrical(m) => { IndividualSpacingConfig::vertical(*m) } GroupedSpacingOptions::Split(tm, bm) => { IndividualSpacingConfig::vertical(*tm).bottom(*bm) } }, ); let hm = grouped_spacing_config.horizontal.as_ref().map_or( IndividualSpacingConfig::horizontal(default), |hm| match hm { GroupedSpacingOptions::Symmetrical(m) => { IndividualSpacingConfig::horizontal(*m) } GroupedSpacingOptions::Split(lm, rm) => { IndividualSpacingConfig::horizontal(*lm).right(*rm) } }, ); IndividualSpacingConfig { top: vm.top, bottom: vm.bottom, left: hm.left, right: hm.right, } } SpacingKind::Individual(m) => *m, } } } #[derive(Clone, Debug, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] /// Grouped vertical and horizontal spacing pub struct GroupedSpacingConfig { /// Vertical grouped spacing pub vertical: Option, /// Horizontal grouped spacing pub horizontal: Option, } #[derive(Clone, Debug, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(untagged)] /// Grouped spacing options pub enum GroupedSpacingOptions { /// Symmetrical grouped spacing Symmetrical(f32), /// Split grouped spacing Split(f32, f32), } #[derive(Copy, Clone, Debug, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] /// Individual spacing configuration pub struct IndividualSpacingConfig { /// Spacing for the top pub top: f32, /// Spacing for the bottom pub bottom: f32, /// Spacing for the left pub left: f32, /// Spacing for the right pub right: f32, } #[allow(dead_code)] impl IndividualSpacingConfig { pub const ZERO: Self = IndividualSpacingConfig { top: 0.0, bottom: 0.0, left: 0.0, right: 0.0, }; pub fn all(value: f32) -> Self { IndividualSpacingConfig { top: value, bottom: value, left: value, right: value, } } pub fn horizontal(value: f32) -> Self { IndividualSpacingConfig { top: 0.0, bottom: 0.0, left: value, right: value, } } pub fn vertical(value: f32) -> Self { IndividualSpacingConfig { top: value, bottom: value, left: 0.0, right: 0.0, } } pub fn top(self, value: f32) -> Self { IndividualSpacingConfig { top: value, ..self } } pub fn bottom(self, value: f32) -> Self { IndividualSpacingConfig { bottom: value, ..self } } pub fn left(self, value: f32) -> Self { IndividualSpacingConfig { left: value, ..self } } pub fn right(self, value: f32) -> Self { IndividualSpacingConfig { right: value, ..self } } } pub fn get_individual_spacing( default: f32, spacing: &Option, ) -> IndividualSpacingConfig { spacing .as_ref() .map_or(IndividualSpacingConfig::all(default), |s| { s.to_individual(default) }) } #[derive(Clone, Debug, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(untagged)] /// Mouse message pub enum MouseMessage { /// Send a message to the komorebi client. /// By default, a batch of messages are sent in the following order: /// FocusMonitorAtCursor => /// MouseFollowsFocus(false) => /// {message} => /// MouseFollowsFocus({original.value}) /// /// Example: /// ```json /// "on_extra2_click": { /// "message": { /// "type": "NewWorkspace" /// } /// }, /// ``` /// or: /// ```json /// "on_middle_click": { /// "focus_monitor_at_cursor": false, /// "ignore_mouse_follows_focus": false, /// "message": { /// "type": "TogglePause" /// } /// } /// ``` /// or: /// ```json /// "on_scroll_up": { /// "message": { /// "type": "CycleFocusWorkspace", /// "content": "Previous" /// } /// } /// ``` Komorebi(KomorebiMouseMessage), /// Execute a custom command. /// CMD (%variable%), Bash ($variable) and PowerShell ($Env:variable) variables will be resolved. /// Example: `komorebic toggle-pause` Command(String), } #[derive(Clone, Debug, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] /// Komorebi socket mouse message pub struct KomorebiMouseMessage { /// Send the FocusMonitorAtCursor message #[cfg_attr(feature = "schemars", schemars(extend("default" = true)))] pub focus_monitor_at_cursor: Option, /// Wrap the {message} with a MouseFollowsFocus(false) and MouseFollowsFocus({original.value}) message #[cfg_attr(feature = "schemars", schemars(extend("default" = true)))] pub ignore_mouse_follows_focus: Option, /// The message to send to the komorebi client pub message: komorebi_client::SocketMessage, } #[derive(Clone, Debug, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] /// Mouse configuration pub struct MouseConfig { /// Command to send on primary/left double button click pub on_primary_double_click: Option, /// Command to send on secondary/right button click pub on_secondary_click: Option, /// Command to send on middle button click pub on_middle_click: Option, /// Command to send on extra1/back button click pub on_extra1_click: Option, /// Command to send on extra2/forward button click pub on_extra2_click: Option, /// Defines how many points a user needs to scroll vertically to make a "tick" on a mouse/touchpad/touchscreen #[cfg_attr(feature = "schemars", schemars(extend("default" = 30.0)))] pub vertical_scroll_threshold: Option, /// Command to send on scrolling up (every tick) pub on_scroll_up: Option, /// Command to send on scrolling down (every tick) pub on_scroll_down: Option, /// Defines how many points a user needs to scroll horizontally to make a "tick" on a mouse/touchpad/touchscreen #[cfg_attr(feature = "schemars", schemars(extend("default" = 30.0)))] pub horizontal_scroll_threshold: Option, /// Command to send on scrolling left (every tick) pub on_scroll_left: Option, /// Command to send on scrolling right (every tick) pub on_scroll_right: Option, } impl MouseConfig { pub fn has_command(&self) -> bool { [ &self.on_primary_double_click, &self.on_secondary_click, &self.on_middle_click, &self.on_extra1_click, &self.on_extra2_click, &self.on_scroll_up, &self.on_scroll_down, &self.on_scroll_left, &self.on_scroll_right, ] .iter() .any(|opt| matches!(opt, Some(MouseMessage::Command(_)))) } } impl MouseMessage { pub fn execute(&self, mouse_follows_focus: bool) { match self { MouseMessage::Komorebi(config) => { let mut messages = Vec::new(); if config.focus_monitor_at_cursor.unwrap_or(true) { messages.push(SocketMessage::FocusMonitorAtCursor); } if config.ignore_mouse_follows_focus.unwrap_or(true) { messages.push(SocketMessage::MouseFollowsFocus(false)); messages.push(config.message.clone()); messages.push(SocketMessage::MouseFollowsFocus(mouse_follows_focus)); } else { messages.push(config.message.clone()); } tracing::debug!("Sending messages: {messages:?}"); if komorebi_client::send_batch(messages).is_err() { tracing::error!("could not send commands"); } } MouseMessage::Command(cmd) => { tracing::debug!("Executing command: {}", cmd); let cmd_no_env = cmd.replace_env(); if exec_powershell(cmd_no_env.to_str().expect("Invalid command")).is_err() { tracing::error!("Failed to execute '{}'", cmd); } } }; } } impl KomobarConfig { pub fn read(path: &PathBuf) -> color_eyre::Result { let content = std::fs::read_to_string(path)?; let mut value: Self = match path.extension().unwrap().to_string_lossy().as_str() { "json" => serde_json::from_str(&content)?, _ => panic!("unsupported format"), }; if value.frame.is_none() { value.frame = Some(FrameConfig { inner_margin: Position { x: DEFAULT_PADDING, y: DEFAULT_PADDING, }, }); } Ok(value) } } #[derive(Copy, Clone, Debug, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] /// Position pub struct Position { /// X coordinate pub x: f32, /// Y coordinate pub y: f32, } impl From for Vec2 { fn from(value: Position) -> Self { Self { x: value.x, y: value.y, } } } impl From for Pos2 { fn from(value: Position) -> Self { Self { x: value.x, y: value.y, } } } pub use komorebi_themes::KomobarTheme; #[derive(Copy, Clone, Debug, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] /// Label prefix pub enum LabelPrefix { /// Show no prefix None, /// Show an icon Icon, /// Show text Text, /// Show an icon and text IconAndText, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] /// Display format pub enum DisplayFormat { /// Show only icon Icon, /// Show only text Text, /// Show an icon and text for the selected element, and text on the rest TextAndIconOnSelected, /// Show both icon and text IconAndText, /// Show an icon and text for the selected element, and icons on the rest IconAndTextOnSelected, } macro_rules! extend_enum { ($(#[$type_meta:meta])* $existing_enum:ident, $new_enum:ident, { $($(#[$meta:meta])* $variant:ident),* $(,)? }) => { #[derive(Copy, Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] $(#[$type_meta])* pub enum $new_enum { // Add new variants $( $(#[$meta])* $variant, )* // Include a variant that wraps the existing enum and flatten it when deserializing #[serde(untagged)] Existing($existing_enum), } // Implement From for the existing enum impl From<$existing_enum> for $new_enum { fn from(value: $existing_enum) -> Self { $new_enum::Existing(value) } } }; } extend_enum!( /// Workspaces display format DisplayFormat, WorkspacesDisplayFormat, { /// Show all icons only AllIcons, /// Show both all icons and text AllIconsAndText, /// Show all icons and text for the selected element, and all icons on the rest AllIconsAndTextOnSelected, }); #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] /// Media widget display format pub enum MediaDisplayFormat { /// Show only the media info icon Icon, /// Show only the media info text (artist - title) Text, /// Show both icon and text IconAndText, /// Show only the control buttons (previous, play/pause, next) ControlsOnly, /// Show icon with control buttons IconAndControls, /// Show text with control buttons TextAndControls, /// Show icon, text, and control buttons Full, } #[cfg(test)] mod tests { use serde::Deserialize; use serde::Serialize; use serde_json::json; #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub enum OriginalDisplayFormat { /// Show None Of The Things NoneOfTheThings, } extend_enum!(OriginalDisplayFormat, ExtendedDisplayFormat, { /// Show Some Of The Things SomeOfTheThings, }); #[derive(serde::Deserialize)] struct ExampleConfig { #[allow(unused)] format: ExtendedDisplayFormat, } #[test] pub fn extend_new_variant() { let raw = json!({ "format": "SomeOfTheThings", }) .to_string(); assert!(serde_json::from_str::(&raw).is_ok()) } #[test] pub fn extend_existing_variant() { let raw = json!({ "format": "NoneOfTheThings", }) .to_string(); assert!(serde_json::from_str::(&raw).is_ok()) } #[test] pub fn extend_invalid_variant() { let raw = json!({ "format": "ALLOFTHETHINGS", }) .to_string(); assert!(serde_json::from_str::(&raw).is_err()) } }