refactor(bar-core): move config.rs (KomobarConfig and the mouse options) into the core

The (now identical) config.rs of both bars becomes
crates/komorebi-bar-core/src/config/komobar.rs; each shell config.rs is one
line, `pub use komorebi_bar_core::config::*;`. Import edits only: statics
from crate::statics, PathExt/SocketMessage from komorebi-protocol, Rect from
komorebi-layouts, the platform hooks and WidgetConfig as in the core. The
core gains komorebi-themes (KomobarTheme, Colour; schemars forwarded): one
line in each Cargo.lock. About 590 lines per bar are gone.

Because the configuration types are now all in the core, the oracle test
regenerates the macOS schema natively: the macOS view of
`schema_for!(KomobarConfig)` is byte-identical to
komorebi-for-mac/schema.bar.json (new test `generated`). Windows is still
compared by the out-of-tree harness: three descriptions in komorebi-protocol
and komorebi-themes read differently when compiled for Windows, which a
Linux test cannot do.

Verified: native bar-core tests 3 unit + 6 oracle (incl. the generated macOS
view); schema harness macOS and Windows byte-identical (3,774 / 9,091
lines); Windows clippy -D warnings --locked clean; macOS fake-CC check
--locked 0 errors (core files touched first). Not run: the bars.
This commit is contained in:
LGUG2Z committed 2026-10-31 10:54:51 -07:00
1 parent afa7fcd219
commit ef89899bd9
8 files changed
+638 -1211

No files matched your search

Generated
+1
View File
@@ -3452,6 +3452,7 @@ dependencies = [
"komorebi-config",
"komorebi-layouts",
"komorebi-protocol",
"komorebi-themes",
"lazy_static",
"reqwest",
"schemars 1.2.3",
+2 -1
View File
@@ -14,6 +14,7 @@ image = "0.25"
komorebi-config = { path = "../komorebi-config", default-features = false }
reqwest = { version = "0.12", features = ["blocking"] }
komorebi-layouts = { path = "../../komorebi-layouts" }
komorebi-themes = { path = "../../komorebi-themes", default-features = false }
komorebi-protocol = { path = "../komorebi-protocol", default-features = false }
serde = { workspace = true }
color-eyre = { workspace = true }
@@ -27,4 +28,4 @@ schemars = { workspace = true, optional = true }
[features]
default = ["schemars"]
schemars = ["dep:schemars", "komorebi-config/schemars", "komorebi-layouts/schemars", "komorebi-protocol/schemars"]
schemars = ["dep:schemars", "komorebi-config/schemars", "komorebi-layouts/schemars", "komorebi-themes/schemars", "komorebi-protocol/schemars"]
@@ -0,0 +1,605 @@
use crate::config::types::*;
use crate::statics::AUTO_SELECT_FILL_COLOUR;
use crate::statics::AUTO_SELECT_TEXT_COLOUR;
use crate::statics::DEFAULT_PADDING;
use crate::platform::platform;
use crate::render::Grouping;
use crate::render::RenderConfig;
use crate::render::RenderExt;
use crate::widgets::widget::WidgetConfig;
use eframe::egui::Color32;
use eframe::egui::Context;
use eframe::egui::FontId;
use eframe::egui::TextBuffer;
use eframe::egui::TextStyle;
use komorebi_protocol::pathext::PathExt;
use komorebi_layouts::Rect;
use komorebi_protocol::message::SocketMessage;
use komorebi_themes::colour::Colour;
use komorebi_themes::colour::Rgb;
use serde::Deserialize;
use serde::Serialize;
use std::collections::HashMap;
use std::num::NonZeroU32;
use std::path::PathBuf;
use std::sync::atomic::Ordering;
#[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<f32>,
/// 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<Padding>,
/// 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<Margin>,
/// Bar positioning options
#[serde(alias = "viewport")]
pub position: Option<PositionConfig>,
/// Frame options (see: https://docs.rs/egui/latest/egui/containers/frame/struct.Frame.html)
pub frame: Option<FrameConfig>,
/// The monitor index or the full monitor options
#[cfg_attr(feature = "schemars", schemars(extend("default" = MonitorConfigOrIndex::Index(0))))]
pub monitor: Option<MonitorConfigOrIndex>,
/// Font family
pub font_family: Option<String>,
/// Font size
#[cfg_attr(feature = "schemars", schemars(extend("default" = 12.5)))]
pub font_size: Option<f32>,
/// 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<f32>,
/// Max label width before text truncation
#[cfg_attr(feature = "schemars", schemars(extend("default" = 400.0)))]
pub max_label_width: Option<f32>,
/// Theme
pub theme: Option<KomobarTheme>,
/// Alpha value for the color transparency [[0-255]]
#[cfg_attr(feature = "schemars", schemars(extend("default" = 200)))]
pub transparency_alpha: Option<u8>,
/// Spacing between widgets
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10.0)))]
pub widget_spacing: Option<f32>,
/// Visual grouping for widgets
pub grouping: Option<Grouping>,
/// Options for mouse interaction on the bar
pub mouse: Option<MouseConfig>,
/// Left side widgets (ordered left-to-right)
pub left_widgets: Vec<WidgetConfig>,
/// Center widgets (ordered left-to-right)
pub center_widgets: Option<Vec<WidgetConfig>>,
/// Right side widgets (ordered left-to-right)
pub right_widgets: Vec<WidgetConfig>,
}
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<Position>,
/// The desired size of the bar from the starting position (usually monitor width x desired height)
#[serde(alias = "inner_size")]
pub end: Option<Position>,
}
#[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<Rect>,
}
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<GroupedSpacingOptions>,
/// Horizontal grouped spacing
pub horizontal: Option<GroupedSpacingOptions>,
}
#[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<SpacingKind>,
) -> 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<bool>,
/// 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<bool>,
/// The message to send to the komorebi client
pub message: komorebi_protocol::message::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<MouseMessage>,
/// Command to send on secondary/right button click
pub on_secondary_click: Option<MouseMessage>,
/// Command to send on middle button click
pub on_middle_click: Option<MouseMessage>,
/// Command to send on extra1/back button click
pub on_extra1_click: Option<MouseMessage>,
/// Command to send on extra2/forward button click
pub on_extra2_click: Option<MouseMessage>,
/// 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<f32>,
/// Command to send on scrolling up (every tick)
pub on_scroll_up: Option<MouseMessage>,
/// Command to send on scrolling down (every tick)
pub on_scroll_down: Option<MouseMessage>,
/// 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<f32>,
/// Command to send on scrolling left (every tick)
pub on_scroll_left: Option<MouseMessage>,
/// Command to send on scrolling right (every tick)
pub on_scroll_right: Option<MouseMessage>,
}
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 platform().send_komorebi_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 platform()
.exec_shell_command(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<Self> {
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"),
};
crate::widgets::widget::warn_unsupported(
value
.left_widgets
.iter()
.chain(value.center_widgets.iter().flatten())
.chain(value.right_widgets.iter()),
);
if value.mouse.is_some() && !cfg!(target_os = "windows") {
tracing::warn!("the `mouse` options are not supported on this platform and are ignored");
}
if value.frame.is_none() {
value.frame = Some(FrameConfig {
inner_margin: Position {
x: DEFAULT_PADDING,
y: DEFAULT_PADDING,
},
});
}
Ok(value)
}
}
impl RenderExt for &KomobarConfig {
fn new_renderconfig(
&self,
ctx: &Context,
background_color: Color32,
icon_scale: Option<f32>,
) -> RenderConfig {
let text_font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
let mut icon_font_id = text_font_id.clone();
icon_font_id.size *= icon_scale.unwrap_or(1.4).clamp(1.0, 2.0);
let monitor_idx = match &self.monitor {
Some(MonitorConfigOrIndex::MonitorConfig(monitor_config)) => monitor_config.index,
Some(MonitorConfigOrIndex::Index(idx)) => *idx,
None => 0,
};
// check if any of the alignments have a komorebi widget with the workspace set to show all icons
let show_all_icons =
KomobarConfig::show_all_icons_on_komorebi_workspace(&self.left_widgets)
|| self
.center_widgets
.as_ref()
.is_some_and(|list| KomobarConfig::show_all_icons_on_komorebi_workspace(list))
|| KomobarConfig::show_all_icons_on_komorebi_workspace(&self.right_widgets);
RenderConfig {
monitor_idx,
spacing: self.widget_spacing.unwrap_or(10.0),
grouping: self.grouping.unwrap_or(Grouping::None),
background_color,
alignment: None,
more_inner_margin: false,
applied_on_widget: false,
text_font_id,
icon_font_id,
show_all_icons,
auto_select_fill: NonZeroU32::new(AUTO_SELECT_FILL_COLOUR.load(Ordering::SeqCst))
.map(|c| Colour::Rgb(Rgb::from(c.get())).into()),
auto_select_text: NonZeroU32::new(AUTO_SELECT_TEXT_COLOUR.load(Ordering::SeqCst))
.map(|c| Colour::Rgb(Rgb::from(c.get())).into()),
}
}
}
pub use komorebi_themes::KomobarTheme;
@@ -1,3 +1,5 @@
pub mod komobar;
pub mod types;
pub use komobar::*;
pub use types::*;
@@ -121,3 +121,28 @@ mod views {
assert!(once == again);
}
}
mod generated {
use komorebi_bar_core::config::KomobarConfig;
use komorebi_bar_core::schema::SchemaPlatform;
use komorebi_bar_core::schema::generated_text;
/// The schema the core generates from its own types, through the macOS view, is the macOS
/// file. (Not tested for Windows: three descriptions in komorebi-protocol and the themes
/// crate read differently when compiled for Windows, which a Linux test host cannot do; the
/// out-of-tree harness covers it.)
#[test]
fn the_generated_macos_view_is_the_macos_file() {
let file =
String::from_utf8(super::read("komorebi-for-mac/schema.bar.json")).expect("utf-8");
let text = generated_text::<KomobarConfig>(SchemaPlatform::MacOs);
if text != file {
let first = text.lines().zip(file.lines()).position(|(a, b)| a != b);
panic!(
"differs, first at line {first:?} ({} lines, file {})",
text.lines().count(),
file.lines().count()
);
}
}
}
+1 -605
View File
@@ -1,605 +1 @@
use crate::AUTO_SELECT_FILL_COLOUR;
use crate::AUTO_SELECT_TEXT_COLOUR;
use crate::DEFAULT_PADDING;
use crate::platform::platform;
use crate::render::Grouping;
use crate::render::RenderConfig;
use crate::render::RenderExt;
use crate::widgets::widget::WidgetConfig;
use eframe::egui::Color32;
use eframe::egui::Context;
use eframe::egui::FontId;
use eframe::egui::TextBuffer;
use eframe::egui::TextStyle;
use komorebi_client::PathExt;
use komorebi_client::Rect;
use komorebi_client::SocketMessage;
use komorebi_themes::colour::Colour;
use komorebi_themes::colour::Rgb;
use serde::Deserialize;
use serde::Serialize;
use std::collections::HashMap;
use std::num::NonZeroU32;
use std::path::PathBuf;
use std::sync::atomic::Ordering;
#[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<f32>,
/// 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<Padding>,
/// 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<Margin>,
/// Bar positioning options
#[serde(alias = "viewport")]
pub position: Option<PositionConfig>,
/// Frame options (see: https://docs.rs/egui/latest/egui/containers/frame/struct.Frame.html)
pub frame: Option<FrameConfig>,
/// The monitor index or the full monitor options
#[cfg_attr(feature = "schemars", schemars(extend("default" = MonitorConfigOrIndex::Index(0))))]
pub monitor: Option<MonitorConfigOrIndex>,
/// Font family
pub font_family: Option<String>,
/// Font size
#[cfg_attr(feature = "schemars", schemars(extend("default" = 12.5)))]
pub font_size: Option<f32>,
/// 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<f32>,
/// Max label width before text truncation
#[cfg_attr(feature = "schemars", schemars(extend("default" = 400.0)))]
pub max_label_width: Option<f32>,
/// Theme
pub theme: Option<KomobarTheme>,
/// Alpha value for the color transparency [[0-255]]
#[cfg_attr(feature = "schemars", schemars(extend("default" = 200)))]
pub transparency_alpha: Option<u8>,
/// Spacing between widgets
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10.0)))]
pub widget_spacing: Option<f32>,
/// Visual grouping for widgets
pub grouping: Option<Grouping>,
/// Options for mouse interaction on the bar
pub mouse: Option<MouseConfig>,
/// Left side widgets (ordered left-to-right)
pub left_widgets: Vec<WidgetConfig>,
/// Center widgets (ordered left-to-right)
pub center_widgets: Option<Vec<WidgetConfig>>,
/// Right side widgets (ordered left-to-right)
pub right_widgets: Vec<WidgetConfig>,
}
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<Position>,
/// The desired size of the bar from the starting position (usually monitor width x desired height)
#[serde(alias = "inner_size")]
pub end: Option<Position>,
}
#[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<Rect>,
}
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<GroupedSpacingOptions>,
/// Horizontal grouped spacing
pub horizontal: Option<GroupedSpacingOptions>,
}
#[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<SpacingKind>,
) -> 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<bool>,
/// 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<bool>,
/// 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<MouseMessage>,
/// Command to send on secondary/right button click
pub on_secondary_click: Option<MouseMessage>,
/// Command to send on middle button click
pub on_middle_click: Option<MouseMessage>,
/// Command to send on extra1/back button click
pub on_extra1_click: Option<MouseMessage>,
/// Command to send on extra2/forward button click
pub on_extra2_click: Option<MouseMessage>,
/// 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<f32>,
/// Command to send on scrolling up (every tick)
pub on_scroll_up: Option<MouseMessage>,
/// Command to send on scrolling down (every tick)
pub on_scroll_down: Option<MouseMessage>,
/// 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<f32>,
/// Command to send on scrolling left (every tick)
pub on_scroll_left: Option<MouseMessage>,
/// Command to send on scrolling right (every tick)
pub on_scroll_right: Option<MouseMessage>,
}
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 platform().send_komorebi_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 platform()
.exec_shell_command(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<Self> {
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"),
};
komorebi_bar_core::widgets::widget::warn_unsupported(
value
.left_widgets
.iter()
.chain(value.center_widgets.iter().flatten())
.chain(value.right_widgets.iter()),
);
if value.mouse.is_some() && !cfg!(target_os = "windows") {
tracing::warn!("the `mouse` options are not supported on this platform and are ignored");
}
if value.frame.is_none() {
value.frame = Some(FrameConfig {
inner_margin: Position {
x: DEFAULT_PADDING,
y: DEFAULT_PADDING,
},
});
}
Ok(value)
}
}
impl RenderExt for &KomobarConfig {
fn new_renderconfig(
&self,
ctx: &Context,
background_color: Color32,
icon_scale: Option<f32>,
) -> RenderConfig {
let text_font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
let mut icon_font_id = text_font_id.clone();
icon_font_id.size *= icon_scale.unwrap_or(1.4).clamp(1.0, 2.0);
let monitor_idx = match &self.monitor {
Some(MonitorConfigOrIndex::MonitorConfig(monitor_config)) => monitor_config.index,
Some(MonitorConfigOrIndex::Index(idx)) => *idx,
None => 0,
};
// check if any of the alignments have a komorebi widget with the workspace set to show all icons
let show_all_icons =
KomobarConfig::show_all_icons_on_komorebi_workspace(&self.left_widgets)
|| self
.center_widgets
.as_ref()
.is_some_and(|list| KomobarConfig::show_all_icons_on_komorebi_workspace(list))
|| KomobarConfig::show_all_icons_on_komorebi_workspace(&self.right_widgets);
RenderConfig {
monitor_idx,
spacing: self.widget_spacing.unwrap_or(10.0),
grouping: self.grouping.unwrap_or(Grouping::None),
background_color,
alignment: None,
more_inner_margin: false,
applied_on_widget: false,
text_font_id,
icon_font_id,
show_all_icons,
auto_select_fill: NonZeroU32::new(AUTO_SELECT_FILL_COLOUR.load(Ordering::SeqCst))
.map(|c| Colour::Rgb(Rgb::from(c.get())).into()),
auto_select_text: NonZeroU32::new(AUTO_SELECT_TEXT_COLOUR.load(Ordering::SeqCst))
.map(|c| Colour::Rgb(Rgb::from(c.get())).into()),
}
}
}
pub use komorebi_bar_core::config::types::*;
pub use komorebi_themes::KomobarTheme;
pub use komorebi_bar_core::config::*;
+1
View File
@@ -3260,6 +3260,7 @@ dependencies = [
"komorebi-config",
"komorebi-layouts",
"komorebi-protocol",
"komorebi-themes",
"lazy_static",
"reqwest",
"schemars 1.2.3",
+1 -605
View File
@@ -1,605 +1 @@
use crate::AUTO_SELECT_FILL_COLOUR;
use crate::AUTO_SELECT_TEXT_COLOUR;
use crate::DEFAULT_PADDING;
use crate::platform::platform;
use crate::render::Grouping;
use crate::render::RenderConfig;
use crate::render::RenderExt;
use crate::widgets::widget::WidgetConfig;
use eframe::egui::Color32;
use eframe::egui::Context;
use eframe::egui::FontId;
use eframe::egui::TextBuffer;
use eframe::egui::TextStyle;
use komorebi_client::PathExt;
use komorebi_client::Rect;
use komorebi_client::SocketMessage;
use komorebi_themes::colour::Colour;
use komorebi_themes::colour::Rgb;
use serde::Deserialize;
use serde::Serialize;
use std::collections::HashMap;
use std::num::NonZeroU32;
use std::path::PathBuf;
use std::sync::atomic::Ordering;
#[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<f32>,
/// 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<Padding>,
/// 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<Margin>,
/// Bar positioning options
#[serde(alias = "viewport")]
pub position: Option<PositionConfig>,
/// Frame options (see: https://docs.rs/egui/latest/egui/containers/frame/struct.Frame.html)
pub frame: Option<FrameConfig>,
/// The monitor index or the full monitor options
#[cfg_attr(feature = "schemars", schemars(extend("default" = MonitorConfigOrIndex::Index(0))))]
pub monitor: Option<MonitorConfigOrIndex>,
/// Font family
pub font_family: Option<String>,
/// Font size
#[cfg_attr(feature = "schemars", schemars(extend("default" = 12.5)))]
pub font_size: Option<f32>,
/// 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<f32>,
/// Max label width before text truncation
#[cfg_attr(feature = "schemars", schemars(extend("default" = 400.0)))]
pub max_label_width: Option<f32>,
/// Theme
pub theme: Option<KomobarTheme>,
/// Alpha value for the color transparency [[0-255]]
#[cfg_attr(feature = "schemars", schemars(extend("default" = 200)))]
pub transparency_alpha: Option<u8>,
/// Spacing between widgets
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10.0)))]
pub widget_spacing: Option<f32>,
/// Visual grouping for widgets
pub grouping: Option<Grouping>,
/// Options for mouse interaction on the bar
pub mouse: Option<MouseConfig>,
/// Left side widgets (ordered left-to-right)
pub left_widgets: Vec<WidgetConfig>,
/// Center widgets (ordered left-to-right)
pub center_widgets: Option<Vec<WidgetConfig>>,
/// Right side widgets (ordered left-to-right)
pub right_widgets: Vec<WidgetConfig>,
}
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<Position>,
/// The desired size of the bar from the starting position (usually monitor width x desired height)
#[serde(alias = "inner_size")]
pub end: Option<Position>,
}
#[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<Rect>,
}
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<GroupedSpacingOptions>,
/// Horizontal grouped spacing
pub horizontal: Option<GroupedSpacingOptions>,
}
#[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<SpacingKind>,
) -> 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<bool>,
/// 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<bool>,
/// 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<MouseMessage>,
/// Command to send on secondary/right button click
pub on_secondary_click: Option<MouseMessage>,
/// Command to send on middle button click
pub on_middle_click: Option<MouseMessage>,
/// Command to send on extra1/back button click
pub on_extra1_click: Option<MouseMessage>,
/// Command to send on extra2/forward button click
pub on_extra2_click: Option<MouseMessage>,
/// 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<f32>,
/// Command to send on scrolling up (every tick)
pub on_scroll_up: Option<MouseMessage>,
/// Command to send on scrolling down (every tick)
pub on_scroll_down: Option<MouseMessage>,
/// 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<f32>,
/// Command to send on scrolling left (every tick)
pub on_scroll_left: Option<MouseMessage>,
/// Command to send on scrolling right (every tick)
pub on_scroll_right: Option<MouseMessage>,
}
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 platform().send_komorebi_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 platform()
.exec_shell_command(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<Self> {
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"),
};
komorebi_bar_core::widgets::widget::warn_unsupported(
value
.left_widgets
.iter()
.chain(value.center_widgets.iter().flatten())
.chain(value.right_widgets.iter()),
);
if value.mouse.is_some() && !cfg!(target_os = "windows") {
tracing::warn!("the `mouse` options are not supported on this platform and are ignored");
}
if value.frame.is_none() {
value.frame = Some(FrameConfig {
inner_margin: Position {
x: DEFAULT_PADDING,
y: DEFAULT_PADDING,
},
});
}
Ok(value)
}
}
impl RenderExt for &KomobarConfig {
fn new_renderconfig(
&self,
ctx: &Context,
background_color: Color32,
icon_scale: Option<f32>,
) -> RenderConfig {
let text_font_id = ctx
.style()
.text_styles
.get(&TextStyle::Body)
.cloned()
.unwrap_or_else(FontId::default);
let mut icon_font_id = text_font_id.clone();
icon_font_id.size *= icon_scale.unwrap_or(1.4).clamp(1.0, 2.0);
let monitor_idx = match &self.monitor {
Some(MonitorConfigOrIndex::MonitorConfig(monitor_config)) => monitor_config.index,
Some(MonitorConfigOrIndex::Index(idx)) => *idx,
None => 0,
};
// check if any of the alignments have a komorebi widget with the workspace set to show all icons
let show_all_icons =
KomobarConfig::show_all_icons_on_komorebi_workspace(&self.left_widgets)
|| self
.center_widgets
.as_ref()
.is_some_and(|list| KomobarConfig::show_all_icons_on_komorebi_workspace(list))
|| KomobarConfig::show_all_icons_on_komorebi_workspace(&self.right_widgets);
RenderConfig {
monitor_idx,
spacing: self.widget_spacing.unwrap_or(10.0),
grouping: self.grouping.unwrap_or(Grouping::None),
background_color,
alignment: None,
more_inner_margin: false,
applied_on_widget: false,
text_font_id,
icon_font_id,
show_all_icons,
auto_select_fill: NonZeroU32::new(AUTO_SELECT_FILL_COLOUR.load(Ordering::SeqCst))
.map(|c| Colour::Rgb(Rgb::from(c.get())).into()),
auto_select_text: NonZeroU32::new(AUTO_SELECT_TEXT_COLOUR.load(Ordering::SeqCst))
.map(|c| Colour::Rgb(Rgb::from(c.get())).into()),
}
}
}
pub use komorebi_bar_core::config::types::*;
pub use komorebi_themes::KomobarTheme;
pub use komorebi_bar_core::config::*;