chore(repo): combine the kfde, bar and settings stacks (staging)

Staging merge of three stacks that make no runtime change on Windows or KDE: the KDE Backend/Container/process_command refactors (wip-u-kfde), the shared bar core moves (wip-u-bar) and komorebi-settings with the S1/S2 pins (wip-u-settings). Conflicts, if any, are resolved here. Not yet gated: the KDE VM suites and the Windows bar check are owed.
This commit is contained in:
LGUG2Z committed 2026-10-31 11:25:59 -07:00
59 files changed
+3566 -3722

No files matched your search

Generated
+19
View File
@@ -3445,14 +3445,22 @@ version = "0.1.42"
dependencies = [
"chrono",
"chrono-tz",
"color-eyre",
"eframe",
"egui-phosphor",
"image 0.25.10",
"komorebi-config",
"komorebi-layouts",
"komorebi-protocol",
"komorebi-themes",
"lazy_static",
"reqwest",
"schemars 1.2.3",
"serde",
"serde_json_lenient",
"starship-battery",
"sysinfo 0.39.6",
"tracing",
]
[[package]]
@@ -3548,6 +3556,17 @@ dependencies = [
name = "komorebi-scrolling"
version = "0.1.42"
[[package]]
name = "komorebi-settings"
version = "0.1.42"
dependencies = [
"komorebi-config",
"komorebi-layouts",
"komorebi-protocol",
"komorebi-themes",
"parking_lot",
]
[[package]]
name = "komorebi-shortcuts"
version = "0.1.0"
+1
View File
@@ -8,6 +8,7 @@ members = [
"crates/komorebi-backend",
"crates/komorebi-collections",
"crates/komorebi-config",
"crates/komorebi-settings",
"komorebi-layouts",
"komorebi-scrolling",
"komorebic",
+10 -2
View File
@@ -9,15 +9,23 @@ chrono = { workspace = true }
chrono-tz = { workspace = true }
eframe = { workspace = true }
egui-phosphor = { git = "https://github.com/amPerl/egui-phosphor", rev = "d13688738478ecd12b426e3e74c59d6577a85b59" }
lazy_static = { workspace = true }
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 }
serde_json = { workspace = true }
color-eyre = { workspace = true }
serde_json = { workspace = true, features = ["preserve_order"] }
starship-battery = "0.11"
sysinfo = { workspace = true }
tracing = { workspace = true }
# Optional dependencies
schemars = { workspace = true, optional = true }
[features]
default = ["schemars"]
schemars = ["dep: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::*;
+2
View File
@@ -18,3 +18,5 @@ pub mod render;
pub mod widgets;
pub mod platform;
pub mod schema;
+18
View File
@@ -3,6 +3,8 @@
//! A shell implements [`BarPlatform`] and calls [`install`] first thing in `main`; the widgets
//! reach it through [`platform`] when they are built or clicked.
use crate::widgets::icons::ImageIcon;
use komorebi_protocol::message::SocketMessage;
use std::io;
use std::sync::OnceLock;
@@ -17,6 +19,9 @@ pub enum SystemTool {
NetworkSettings,
/// A URL, in the default browser
Url(String),
/// The location a storage widget label names (the label text, as shown: the platform
/// knows how its labels are built), in the file manager
StorageLocation(String),
}
pub trait BarPlatform: Send + Sync {
@@ -26,6 +31,19 @@ pub trait BarPlatform: Send + Sync {
/// The GitHub API URL that says which release of this platform's komorebi is the latest.
fn latest_release_api_url(&self) -> &'static str;
/// The icon of the program a command string would start (Windows: the executable's icon;
/// macOS: none).
fn command_icon(&self, command: &str) -> Option<ImageIcon>;
/// Runs a command line in the bar's shell session (Windows: its PowerShell session).
fn exec_shell_command(&self, command: &str) -> color_eyre::Result<()>;
/// Sends one message to the komorebi daemon.
fn send_komorebi_message(&self, message: &SocketMessage) -> io::Result<()>;
/// Sends messages to the komorebi daemon as one batch.
fn send_komorebi_batch(&self, messages: Vec<SocketMessage>) -> io::Result<()>;
/// The glyph the date widget shows as its calendar icon.
fn calendar_icon(&self) -> &'static str;
}
@@ -0,0 +1,99 @@
//! What the macOS bar's schema says differently from the union, as data.
//!
//! The union is written once: it is the Windows text of `schema.bar.json` (every widget, the
//! mouse options, Windows' spelling of the documentation). The macOS view removes what the macOS
//! bar does not have (the keyboard, media and systray widgets, the mouse options and their
//! definitions, which pruning of the unreachable definitions finishes) and sets the text and the
//! defaults that read differently. Each entry is a convergence candidate: dropping one changes
//! one line of `komorebi-for-mac/schema.bar.json` and of `nix/komorebi-bar-options.nix`.
//!
//! Generated from the difference between the two checked-in files; `tests/schema_oracle.rs`
//! proves that the view of the Windows file is byte-identical to the macOS file.
/// One change of a view
#[derive(Clone, Copy, Debug)]
pub enum Change {
/// Replace the value at the JSON pointer with this JSON text
Set(&'static str),
/// Remove the key at the JSON pointer
Remove,
/// Remove the elements of the array at the JSON pointer whose `title` is this
RemoveTitled(&'static str),
}
/// The changes of the macOS view, in order.
pub const MACOS: &[(&str, Change)] = &[
(
"/description",
Change::Set(
r####""The `komorebi.bar.json` static configuration file reference for komorebi for Mac `v0.1.0`\n\n`\"$schema\": \"https://komorebi-for-mac.lgug2z.com/komorebi.bar.0.1.0.schema.json\"`""####,
),
),
(
"/properties/height",
Change::Set(
r####"{"description":"Bar height","type":["number","null"],"format":"float","default":50.0}"####,
),
),
(
"/properties/margin",
Change::Set(
r####"{"description":"Bar margin. Use one value for all sides or use a grouped margin for horizontal and/or\nvertical definition which can each take a single value for a symmetric margin or two\nvalues for each side, i.e.:\n```json\n\"margin\": {\n \"horizontal\": 10.0\n}\n```\nor:\n```json\n\"margin\": {\n \"vertical\": [top, bottom]\n}\n```\nYou can also set individual margin on each side like this:\n```json\n\"margin\": {\n \"top\": 10.0,\n \"bottom\": 10.0,\n \"left\": 10.0,\n \"right\": 10.0\n}\n```","anyOf":[{"$ref":"#/$defs/SpacingKind"},{"type":"null"}]}"####,
),
),
(
"/properties/padding",
Change::Set(
r####"{"description":"Bar padding. Use one value for all sides or use a grouped padding for horizontal and/or\nvertical definition which can each take a single value for a symmetric padding or two\nvalues for each side, i.e.:\n```json\n\"padding\": {\n \"horizontal\": 10.0\n}\n```\nor:\n```json\n\"padding\": {\n \"horizontal\": [left, right]\n}\n```\nYou can also set individual padding on each side like this:\n```json\n\"padding\": {\n \"top\": 10.0,\n \"bottom\": 10.0,\n \"left\": 10.0,\n \"right\": 10.0\n}\n```","anyOf":[{"$ref":"#/$defs/SpacingKind"},{"type":"null"}]}"####,
),
),
("/properties/mouse", Change::Remove),
(
"/$defs/KomorebiFocusedContainerConfig/properties/show_icon",
Change::Set(
r####"{"description":"DEPRECATED: use 'display' instead (Show the icon of the currently focused container)","type":["boolean","null"]}"####,
),
),
(
"/$defs/KomorebiLayout",
Change::Set(
r####"{"description":"Komorebi layout kind","oneOf":[{"title":"Default","description":"Predefined layout","type":"object","properties":{"Default":{"$ref":"#/$defs/DefaultLayout"}},"additionalProperties":false,"required":["Default"]},{"description":"Monocle mode","type":"string","const":"Monocle"},{"description":"Floating layer","type":"string","const":"Floating"},{"description":"Paused","type":"string","const":"Paused"},{"description":"Custom layout","type":"string","const":"Custom"}]}"####,
),
),
(
"/$defs/NetworkConfig/properties/default_interface_options",
Change::Remove,
),
(
"/$defs/StorageDisplayName/oneOf/0/description",
Change::Set(r####""Display label as mount point eg. /""####),
),
(
"/$defs/StorageDisplayName/oneOf/1/description",
Change::Set(r####""Display label as name eg. Macintosh HD""####),
),
(
"/$defs/StorageDisplayName/oneOf/2/description",
Change::Set(r####""Display label as mount then name eg. / Macintosh HD""####),
),
(
"/$defs/StorageDisplayName/oneOf/3/description",
Change::Set(r####""Display label as name then mount eg. Macintosh HD /""####),
),
(
"/$defs/TimeConfig/properties/changing_icon",
Change::Set(
r####"{"description":"Change the icon depending on the time. The default icon is used between 8:30 and 12:00","type":["boolean","null"],"default":false}"####,
),
),
(
"/$defs/TimeFormat/oneOf/6/title",
Change::Set(r####""Default""####),
),
(
"/$defs/WidgetConfig/oneOf",
Change::RemoveTitled("Keyboard"),
),
("/$defs/WidgetConfig/oneOf", Change::RemoveTitled("Media")),
("/$defs/WidgetConfig/oneOf", Change::RemoveTitled("Systray")),
];
+128
View File
@@ -0,0 +1,128 @@
//! The JSON Schema of `komorebi.bar.json`, one per platform, as views of one union.
//!
//! The union is the Windows schema: every widget and option any bar reads, in Windows' spelling.
//! [`view_of`] turns it into the schema a platform checks in: it applies the platform's
//! [`changes`] and removes the definitions nothing refers to any more. The Windows view is the
//! union itself.
//!
//! The changes are idempotent (setting text that is already there, or removing what is not
//! there, does nothing), so a bar can print its own generated schema through its view.
pub mod changes;
use changes::Change;
use serde_json::Map;
use serde_json::Value;
/// A platform that has a bar
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SchemaPlatform {
Windows,
MacOs,
}
/// The changes a platform's view applies, in order
#[must_use]
pub const fn changes_of(platform: SchemaPlatform) -> &'static [(&'static str, Change)] {
match platform {
SchemaPlatform::Windows => &[],
SchemaPlatform::MacOs => changes::MACOS,
}
}
/// The text a bar prints for `--schema`: pretty JSON followed by a newline, of the view of the
/// union of the bar configuration.
#[must_use]
pub fn view_text(union: Value, platform: SchemaPlatform) -> String {
let mut text =
serde_json::to_string_pretty(&view_of(union, platform)).expect("a schema is JSON");
text.push('\n');
text
}
/// The view of `schema` for `platform`.
#[must_use]
pub fn view_of(mut schema: Value, platform: SchemaPlatform) -> Value {
for (pointer, change) in changes_of(platform) {
apply(&mut schema, pointer, *change);
}
prune_unreachable(&mut schema);
schema
}
fn apply(schema: &mut Value, pointer: &str, change: Change) {
match change {
Change::Set(json) => {
if let Some(target) = schema.pointer_mut(pointer) {
*target = serde_json::from_str(json).expect("a change is JSON");
}
}
Change::Remove => {
if let Some((parent, key)) = pointer.rsplit_once('/')
&& let Some(Value::Object(map)) = schema.pointer_mut(parent)
{
map.retain(|k, _| k != key);
}
}
Change::RemoveTitled(title) => {
if let Some(Value::Array(items)) = schema.pointer_mut(pointer) {
items.retain(|item| item.get("title").and_then(Value::as_str) != Some(title));
}
}
}
}
/// Keep the definitions that the schema refers to, directly or through another definition.
fn prune_unreachable(schema: &mut Value) {
let mut reachable = Vec::new();
collect_refs(schema, &mut reachable);
let Some(defs) = schema.get("$defs").and_then(Value::as_object).cloned() else {
return;
};
let mut next = 0;
while next < reachable.len() {
if let Some(def) = defs.get(&reachable[next]) {
let mut found = Vec::new();
collect_refs(def, &mut found);
for name in found {
if !reachable.contains(&name) {
reachable.push(name);
}
}
}
next += 1;
}
let kept: Map<String, Value> = defs
.into_iter()
.filter(|(name, _)| reachable.contains(name))
.collect();
schema["$defs"] = Value::Object(kept);
}
fn collect_refs(value: &Value, found: &mut Vec<String>) {
match value {
Value::Object(map) => {
if let Some(Value::String(target)) = map.get("$ref")
&& let Some(name) = target.strip_prefix("#/$defs/")
&& !found.iter().any(|n| n == name)
{
found.push(name.to_string());
}
for (key, child) in map {
if key != "$defs" {
collect_refs(child, found);
}
}
}
Value::Array(items) => items.iter().for_each(|item| collect_refs(item, found)),
_ => {}
}
}
/// The text `--schema` prints for the bar configuration type `T`, as `platform` checks it in.
#[cfg(feature = "schemars")]
#[must_use]
pub fn generated_text<T: schemars::JsonSchema>(platform: SchemaPlatform) -> String {
let union = serde_json::to_value(schemars::schema_for!(T)).expect("a schema is JSON");
view_text(union, platform)
}
@@ -1,4 +1,5 @@
use super::ImageIcon;
use super::icons::ImageIcon;
use crate::platform::platform;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::widgets::widget::BarWidget;
@@ -17,17 +18,14 @@ use eframe::egui::StrokeKind;
use eframe::egui::Ui;
use eframe::egui::Vec2;
use eframe::egui::vec2;
use komorebi_client::PathExt;
use komorebi_protocol::pathext::PathExt;
use serde::Deserialize;
use serde::Serialize;
use std::borrow::Cow;
use std::path::Path;
use std::process::Command;
use std::sync::Arc;
use std::time::Duration;
use std::time::Instant;
use tracing;
use which::which;
/// Minimum interval between consecutive application launches to prevent accidental spamming.
const MIN_LAUNCH_INTERVAL: Duration = Duration::from_millis(800);
@@ -273,13 +271,7 @@ impl Icon {
/// - Returns `None` if the executable path is unavailable or icon extraction fails.
#[inline]
pub fn try_from_command(command: &UserCommand) -> Option<Self> {
let path = command.get_executable()?;
let image_icon = ImageIcon::try_load(path.as_ref(), || {
let path_str = path.to_str()?;
windows_icons::get_icon_by_path(path_str)
.or_else(|| windows_icons_fallback::get_icon_by_path(path_str))
})?;
Some(Icon::Image(image_icon))
platform().command_icon(&command.command).map(Icon::Image)
}
/// Renders the icon in the given [`Ui`] using the provided [`IconConfig`].
@@ -364,28 +356,6 @@ impl UserCommand {
}
}
/// Attempts to resolve the executable path from the command string.
///
/// Resolution logic:
/// - Splits the command by ".exe" and checks if the first part is an existing file.
/// - If not, attempts to locate the binary using [`which`] on this name.
/// - If still unresolved, takes the first word (separated by whitespace) and attempts
/// to find it in the system `PATH` using [`which`].
///
/// Returns `None` if no executable path can be determined.
#[inline]
pub fn get_executable(&self) -> Option<Cow<'_, Path>> {
if let Some(binary) = self.command.split(".exe").next().map(Path::new) {
if binary.is_file() {
return Some(Cow::Borrowed(binary));
} else if let Ok(binary) = which(binary) {
return Some(Cow::Owned(binary));
}
}
which(self.command.split(' ').next()?).ok().map(Cow::Owned)
}
/// Attempts to launch the specified command in a separate thread if enough time has passed
/// since the last launch. This prevents repeated launches from rapid consecutive clicks.
///
@@ -0,0 +1,181 @@
use eframe::egui::ColorImage;
use eframe::egui::Context;
use eframe::egui::TextureHandle;
use eframe::egui::TextureOptions;
use image::RgbaImage;
use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
use std::sync::LazyLock;
use std::sync::RwLock;
/// Global cache for icon images and their associated GPU textures.
pub static ICONS_CACHE: IconsCache = IconsCache::new();
/// In-memory cache for icon images and their associated GPU textures.
///
/// Stores raw [`ColorImage`]s and [`TextureHandle`]s keyed by [`ImageIconId`].
/// Texture entries are context-dependent and automatically invalidated when the [`Context`] changes.
#[allow(clippy::type_complexity)]
pub struct IconsCache {
textures: LazyLock<RwLock<(Option<Context>, HashMap<ImageIconId, TextureHandle>)>>,
images: LazyLock<RwLock<HashMap<ImageIconId, Arc<ColorImage>>>>,
}
impl IconsCache {
/// Creates a new empty IconsCache instance.
#[inline]
pub const fn new() -> Self {
Self {
textures: LazyLock::new(|| RwLock::new((None, HashMap::new()))),
images: LazyLock::new(|| RwLock::new(HashMap::new())),
}
}
/// Retrieves or creates a texture handle for the given icon ID and image.
///
/// If a texture for the given ID already exists for the current [`Context`], it is reused.
/// Otherwise, a new texture is created, inserted into the cache, and returned.
/// The cache is reset if the [`Context`] has changed.
#[inline]
pub fn texture(&self, ctx: &Context, id: &ImageIconId, img: &Arc<ColorImage>) -> TextureHandle {
if let Some(texture) = self.get_texture(ctx, id) {
return texture;
}
let texture_handle = ctx.load_texture("icon", img.clone(), TextureOptions::default());
self.insert_texture(ctx, id.clone(), texture_handle.clone());
texture_handle
}
/// Returns the cached texture for the given icon ID if it exists and matches the current [`Context`].
pub fn get_texture(&self, ctx: &Context, id: &ImageIconId) -> Option<TextureHandle> {
let textures_lock = self.textures.read().unwrap();
if textures_lock.0.as_ref() == Some(ctx) {
return textures_lock.1.get(id).cloned();
}
None
}
/// Inserts a texture handle, resetting the cache if the [`Context`] has changed.
pub fn insert_texture(&self, ctx: &Context, id: ImageIconId, texture: TextureHandle) {
let mut textures_lock = self.textures.write().unwrap();
if textures_lock.0.as_ref() != Some(ctx) {
textures_lock.0 = Some(ctx.clone());
textures_lock.1.clear();
}
textures_lock.1.insert(id, texture);
}
/// Returns the cached image for the given icon ID, if available.
pub fn get_image(&self, id: &ImageIconId) -> Option<Arc<ColorImage>> {
self.images.read().unwrap().get(id).cloned()
}
/// Caches a raw [`ColorImage`] associated with the given icon ID.
pub fn insert_image(&self, id: ImageIconId, image: Arc<ColorImage>) {
self.images.write().unwrap().insert(id, image);
}
/// Removes the cached image and texture for the given icon ID.
pub fn remove(&self, id: &ImageIconId) {
self.images.write().unwrap().remove(id);
self.textures.write().unwrap().1.remove(id);
}
}
#[inline]
pub fn rgba_to_color_image(rgba_image: &RgbaImage) -> ColorImage {
let size = [rgba_image.width() as usize, rgba_image.height() as usize];
let pixels = rgba_image.as_flat_samples();
ColorImage::from_rgba_unmultiplied(size, pixels.as_slice())
}
/// Represents an image-based icon with a unique ID and pixel data.
#[derive(Clone, Debug)]
pub struct ImageIcon {
/// Unique identifier for the image icon, used for texture caching.
pub id: ImageIconId,
/// Shared pixel data of the icon in `ColorImage` format.
pub image: Arc<ColorImage>,
}
impl ImageIcon {
/// Creates a new [`ImageIcon`] from the given ID and image data.
#[inline]
pub fn new(id: ImageIconId, image: Arc<ColorImage>) -> Self {
Self { id, image }
}
/// Loads an [`ImageIcon`] from [`ICONS_CACHE`] or calls `loader` if not cached.
/// The loaded image is converted to a [`ColorImage`], cached, and returned.
#[inline]
pub fn try_load<F, I>(id: impl Into<ImageIconId>, loader: F) -> Option<Self>
where
F: FnOnce() -> Option<I>,
I: Into<RgbaImage>,
{
let id = id.into();
let image = ICONS_CACHE.get_image(&id).or_else(|| {
let img = loader()?;
let img = Arc::new(rgba_to_color_image(&img.into()));
ICONS_CACHE.insert_image(id.clone(), img.clone());
Some(img)
})?;
Some(ImageIcon::new(id, image))
}
/// Returns a texture handle for the icon, using the given [`Context`].
///
/// If the texture is already cached in [`ICONS_CACHE`], it is reused.
/// Otherwise, a new texture is created from the [`ColorImage`] and cached.
#[inline]
pub fn texture(&self, ctx: &Context) -> TextureHandle {
ICONS_CACHE.texture(ctx, &self.id, &self.image)
}
}
/// Unique identifier for an image-based icon.
///
/// Used to distinguish cached images and textures by either a file path
/// or a Windows window handle.
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum ImageIconId {
/// Identifier based on a file system path.
Path(Arc<Path>),
/// Windows HWND handle.
Hwnd(isize),
/// System tray icon identifier.
SystrayIcon(String),
/// macOS window ID.
WindowId(u32),
}
impl From<&Path> for ImageIconId {
#[inline]
fn from(value: &Path) -> Self {
Self::Path(value.into())
}
}
impl From<isize> for ImageIconId {
#[inline]
fn from(value: isize) -> Self {
Self::Hwnd(value)
}
}
impl From<u32> for ImageIconId {
#[inline]
fn from(value: u32) -> Self {
Self::WindowId(value)
}
}
impl Default for IconsCache {
fn default() -> Self {
Self::new()
}
}
@@ -0,0 +1,158 @@
//! The configuration of the komorebi widgets (workspaces, layout, focused container, ...) and
//! the layout kind the layout widget shows: data only, shared by both bars.
use crate::config::DisplayFormat;
use crate::config::WorkspacesDisplayFormat;
use color_eyre::eyre;
use serde::Deserialize;
use serde::Deserializer;
use serde::Serialize;
use serde::de::Error;
use serde_json::from_str;
use std::collections::BTreeMap;
use std::fmt::Display;
use std::fmt::Formatter;
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Komorebi widget configuration
pub struct KomorebiConfig {
/// Configure the Workspaces widget
pub workspaces: Option<KomorebiWorkspacesConfig>,
/// Configure the Layout widget
pub layout: Option<KomorebiLayoutConfig>,
/// Configure the Workspace Layer widget
pub workspace_layer: Option<KomorebiWorkspaceLayerConfig>,
/// Configure the Focused Container widget
#[serde(alias = "focused_window")]
pub focused_container: Option<KomorebiFocusedContainerConfig>,
/// Configure the Locked Container widget
pub locked_container: Option<KomorebiLockedContainerConfig>,
/// Configure the Configuration Switcher widget
pub configuration_switcher: Option<KomorebiConfigurationSwitcherConfig>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Komorebi widget workspaces configuration
pub struct KomorebiWorkspacesConfig {
/// Enable the Komorebi Workspaces widget
pub enable: bool,
/// Hide workspaces without any windows
pub hide_empty_workspaces: bool,
/// Display format of the workspace
pub display: Option<WorkspacesDisplayFormat>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Komorebi widget layout configuration
pub struct KomorebiLayoutConfig {
/// Enable the Komorebi Layout widget
pub enable: bool,
/// List of layout options
pub options: Option<Vec<KomorebiLayout>>,
/// Display format of the current layout
pub display: Option<DisplayFormat>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Komorebi widget workspace layer configuration
pub struct KomorebiWorkspaceLayerConfig {
/// Enable the Komorebi Workspace Layer widget
pub enable: bool,
/// Display format of the current layer
pub display: Option<DisplayFormat>,
/// Show the widget event if the layer is Tiling
pub show_when_tiling: Option<bool>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Komorebi widget focused container configuration
pub struct KomorebiFocusedContainerConfig {
/// Enable the Komorebi Focused Container widget
pub enable: bool,
/// DEPRECATED: use `display` instead (Show the icon of the currently focused container)
#[deprecated(note = "Use `display` instead")]
pub show_icon: Option<bool>,
/// Display format of the currently focused container
pub display: Option<DisplayFormat>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Komorebi widget locked container configuration
pub struct KomorebiLockedContainerConfig {
/// Enable the Komorebi Locked Container widget
pub enable: bool,
/// Display format of the current locked state
pub display: Option<DisplayFormat>,
/// Show the widget event if the layer is unlocked
pub show_when_unlocked: Option<bool>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Komorebi widget configuration switcher configuration
pub struct KomorebiConfigurationSwitcherConfig {
/// Enable the Komorebi Configurations widget
pub enable: bool,
/// A map of display friendly name => path to configuration.json
pub configurations: BTreeMap<String, String>,
}
#[derive(Copy, Clone, Debug, Serialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)]
/// Komorebi layout kind
pub enum KomorebiLayout {
/// Predefined layout
#[cfg_attr(feature = "schemars", schemars(title = "Default"))]
Default(komorebi_layouts::DefaultLayout),
/// Monocle mode
Monocle,
/// Floating layer
Floating,
/// Paused
Paused,
/// Custom layout
Custom,
}
impl<'de> Deserialize<'de> for KomorebiLayout {
fn deserialize<D>(deserializer: D) -> eyre::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s: String = String::deserialize(deserializer)?;
// Attempt to deserialize the string as a DefaultLayout
if let Ok(default_layout) = from_str::<komorebi_layouts::DefaultLayout>(&format!("\"{s}\""))
{
return Ok(KomorebiLayout::Default(default_layout));
}
// Handle other cases
match s.as_str() {
"Monocle" => Ok(KomorebiLayout::Monocle),
"Floating" => Ok(KomorebiLayout::Floating),
"Paused" => Ok(KomorebiLayout::Paused),
"Custom" => Ok(KomorebiLayout::Custom),
_ => Err(Error::custom(format!("Invalid layout: {s}"))),
}
}
}
impl Display for KomorebiLayout {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
KomorebiLayout::Default(layout) => write!(f, "{layout}"),
KomorebiLayout::Monocle => write!(f, "Monocle"),
KomorebiLayout::Floating => write!(f, "Floating"),
KomorebiLayout::Paused => write!(f, "Paused"),
KomorebiLayout::Custom => write!(f, "Custom"),
}
}
}
@@ -1,8 +1,9 @@
use crate::config::DisplayFormat;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::widgets::komorebi::KomorebiLayoutConfig;
use color_eyre::eyre;
use crate::platform::platform;
use crate::widgets::komorebi_config::KomorebiLayout;
use crate::widgets::komorebi_config::KomorebiLayoutConfig;
use eframe::egui::Context;
use eframe::egui::CornerRadius;
use eframe::egui::FontId;
@@ -14,70 +15,30 @@ use eframe::egui::StrokeKind;
use eframe::egui::Ui;
use eframe::egui::Vec2;
use eframe::egui::vec2;
use komorebi_client::SocketMessage;
use serde::Deserialize;
use serde::Deserializer;
use serde::Serialize;
use serde::de::Error;
use serde_json::from_str;
use std::fmt::Display;
use std::fmt::Formatter;
use komorebi_protocol::message::SocketMessage;
#[derive(Copy, Clone, Debug, Serialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)]
/// Komorebi layout kind
pub enum KomorebiLayout {
/// Predefined layout
#[cfg_attr(feature = "schemars", schemars(title = "Default"))]
Default(komorebi_client::DefaultLayout),
/// Monocle mode
Monocle,
/// Floating layer
Floating,
/// Paused
Paused,
/// Custom layout
Custom,
/// What the layout widget does with a layout kind (the kind itself lives in the core).
pub trait KomorebiLayoutExt {
fn is_default(&mut self) -> bool;
fn on_click(
&mut self,
show_options: &bool,
monitor_idx: usize,
workspace_idx: Option<usize>,
) -> bool;
fn on_click_option(&mut self, monitor_idx: usize, workspace_idx: Option<usize>);
fn show_icon(&mut self, is_selected: bool, font_id: FontId, ctx: &Context, ui: &mut Ui);
fn show(
&mut self,
ctx: &Context,
ui: &mut Ui,
render_config: &mut RenderConfig,
layout_config: &KomorebiLayoutConfig,
workspace_idx: Option<usize>,
);
}
impl<'de> Deserialize<'de> for KomorebiLayout {
fn deserialize<D>(deserializer: D) -> eyre::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s: String = String::deserialize(deserializer)?;
// Attempt to deserialize the string as a DefaultLayout
if let Ok(default_layout) = from_str::<komorebi_client::DefaultLayout>(&format!("\"{s}\""))
{
return Ok(KomorebiLayout::Default(default_layout));
}
// Handle other cases
match s.as_str() {
"Monocle" => Ok(KomorebiLayout::Monocle),
"Floating" => Ok(KomorebiLayout::Floating),
"Paused" => Ok(KomorebiLayout::Paused),
"Custom" => Ok(KomorebiLayout::Custom),
_ => Err(Error::custom(format!("Invalid layout: {s}"))),
}
}
}
impl Display for KomorebiLayout {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
KomorebiLayout::Default(layout) => write!(f, "{layout}"),
KomorebiLayout::Monocle => write!(f, "Monocle"),
KomorebiLayout::Floating => write!(f, "Floating"),
KomorebiLayout::Paused => write!(f, "Paused"),
KomorebiLayout::Custom => write!(f, "Custom"),
}
}
}
impl KomorebiLayout {
impl KomorebiLayoutExt for KomorebiLayout {
fn is_default(&mut self) -> bool {
matches!(self, KomorebiLayout::Default(_))
}
@@ -100,7 +61,7 @@ impl KomorebiLayout {
match self {
KomorebiLayout::Default(option) => {
if let Some(ws_idx) = workspace_idx
&& komorebi_client::send_message(&SocketMessage::WorkspaceLayout(
&& platform().send_komorebi_message(&SocketMessage::WorkspaceLayout(
monitor_idx,
ws_idx,
*option,
@@ -111,7 +72,7 @@ impl KomorebiLayout {
}
}
KomorebiLayout::Monocle => {
if komorebi_client::send_batch([
if platform().send_komorebi_batch(vec![
SocketMessage::FocusMonitorAtCursor,
SocketMessage::ToggleMonocle,
])
@@ -121,7 +82,7 @@ impl KomorebiLayout {
}
}
KomorebiLayout::Floating => {
if komorebi_client::send_batch([
if platform().send_komorebi_batch(vec![
SocketMessage::FocusMonitorAtCursor,
SocketMessage::ToggleTiling,
])
@@ -131,7 +92,7 @@ impl KomorebiLayout {
}
}
KomorebiLayout::Paused => {
if komorebi_client::send_message(&SocketMessage::TogglePause).is_err() {
if platform().send_komorebi_message(&SocketMessage::TogglePause).is_err() {
tracing::error!("could not send message to komorebi: TogglePause");
}
}
@@ -158,44 +119,44 @@ impl KomorebiLayout {
match self {
KomorebiLayout::Default(layout) => match layout {
komorebi_client::DefaultLayout::BSP => {
komorebi_layouts::DefaultLayout::BSP => {
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
painter.line_segment([c, c + vec2(r, 0.0)], stroke);
painter.line_segment([c + vec2(r / 2.0, 0.0), c + vec2(r / 2.0, r)], stroke);
}
komorebi_client::DefaultLayout::Columns => {
komorebi_layouts::DefaultLayout::Columns => {
painter.line_segment([c - vec2(r / 2.0, r), c + vec2(-r / 2.0, r)], stroke);
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
painter.line_segment([c - vec2(-r / 2.0, r), c + vec2(r / 2.0, r)], stroke);
}
komorebi_client::DefaultLayout::Rows => {
komorebi_layouts::DefaultLayout::Rows => {
painter.line_segment([c - vec2(r, r / 2.0), c + vec2(r, -r / 2.0)], stroke);
painter.line_segment([c - vec2(r, 0.0), c + vec2(r, 0.0)], stroke);
painter.line_segment([c - vec2(r, -r / 2.0), c + vec2(r, r / 2.0)], stroke);
}
komorebi_client::DefaultLayout::VerticalStack => {
komorebi_layouts::DefaultLayout::VerticalStack => {
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
painter.line_segment([c, c + vec2(r, 0.0)], stroke);
}
komorebi_client::DefaultLayout::RightMainVerticalStack => {
komorebi_layouts::DefaultLayout::RightMainVerticalStack => {
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
painter.line_segment([c - vec2(r, 0.0), c], stroke);
}
komorebi_client::DefaultLayout::HorizontalStack => {
komorebi_layouts::DefaultLayout::HorizontalStack => {
painter.line_segment([c - vec2(r, 0.0), c + vec2(r, 0.0)], stroke);
painter.line_segment([c, c + vec2(0.0, r)], stroke);
}
komorebi_client::DefaultLayout::UltrawideVerticalStack => {
komorebi_layouts::DefaultLayout::UltrawideVerticalStack => {
painter.line_segment([c - vec2(r / 2.0, r), c + vec2(-r / 2.0, r)], stroke);
painter.line_segment([c + vec2(r / 2.0, 0.0), c + vec2(r, 0.0)], stroke);
painter.line_segment([c - vec2(-r / 2.0, r), c + vec2(r / 2.0, r)], stroke);
}
komorebi_client::DefaultLayout::Grid => {
komorebi_layouts::DefaultLayout::Grid => {
painter.line_segment([c - vec2(r, 0.0), c + vec2(r, 0.0)], stroke);
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
}
// TODO: @CtByte can you think of a nice icon to draw here?
komorebi_client::DefaultLayout::Scrolling => {
komorebi_layouts::DefaultLayout::Scrolling => {
painter.line_segment([c - vec2(r / 2.0, r), c + vec2(-r / 2.0, r)], stroke);
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
painter.line_segment([c - vec2(-r / 2.0, r), c + vec2(r / 2.0, r)], stroke);
@@ -242,7 +203,7 @@ impl KomorebiLayout {
}
}
pub fn show(
fn show(
&mut self,
ctx: &Context,
ui: &mut Ui,
@@ -284,18 +245,18 @@ impl KomorebiLayout {
);
let mut layout_options = layout_config.options.clone().unwrap_or(vec![
KomorebiLayout::Default(komorebi_client::DefaultLayout::BSP),
KomorebiLayout::Default(komorebi_client::DefaultLayout::Columns),
KomorebiLayout::Default(komorebi_client::DefaultLayout::Rows),
KomorebiLayout::Default(komorebi_client::DefaultLayout::VerticalStack),
KomorebiLayout::Default(komorebi_layouts::DefaultLayout::BSP),
KomorebiLayout::Default(komorebi_layouts::DefaultLayout::Columns),
KomorebiLayout::Default(komorebi_layouts::DefaultLayout::Rows),
KomorebiLayout::Default(komorebi_layouts::DefaultLayout::VerticalStack),
KomorebiLayout::Default(
komorebi_client::DefaultLayout::RightMainVerticalStack,
komorebi_layouts::DefaultLayout::RightMainVerticalStack,
),
KomorebiLayout::Default(komorebi_client::DefaultLayout::HorizontalStack),
KomorebiLayout::Default(komorebi_layouts::DefaultLayout::HorizontalStack),
KomorebiLayout::Default(
komorebi_client::DefaultLayout::UltrawideVerticalStack,
komorebi_layouts::DefaultLayout::UltrawideVerticalStack,
),
KomorebiLayout::Default(komorebi_client::DefaultLayout::Grid),
KomorebiLayout::Default(komorebi_layouts::DefaultLayout::Grid),
//KomorebiLayout::Custom,
KomorebiLayout::Monocle,
KomorebiLayout::Floating,
@@ -1,6 +1,14 @@
pub mod applications;
pub mod battery;
pub mod cpu;
pub mod date;
pub mod icons;
pub mod komorebi_config;
pub mod komorebi_layout;
pub mod memory;
pub mod network_config;
pub mod platform_config;
pub mod storage;
pub mod time;
pub mod update;
pub mod widget;
@@ -0,0 +1,66 @@
//! The configuration of the network widget: data only, shared by both bars. The Windows bar
//! reads `default_interface_options`; the macOS bar accepts the key and ignores it.
use crate::config::LabelPrefix;
use serde::Deserialize;
use serde::Serialize;
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Network widget configuration
pub struct NetworkConfig {
/// Enable the Network widget
pub enable: bool,
/// Show total received and transmitted activity
#[serde(alias = "show_total_data_transmitted")]
pub show_total_activity: bool,
/// Show received and transmitted activity
#[serde(alias = "show_network_activity")]
pub show_activity: bool,
/// Show default interface
pub show_default_interface: Option<bool>,
/// Default interface options: whether to show the friendly name, IPv4 or IPv6 address
pub default_interface_options: Option<DefaultInterfaceConfig>,
/// Characters to reserve for received and transmitted activity
#[serde(alias = "network_activity_fill_characters")]
pub activity_left_padding: Option<usize>,
/// Data refresh interval in seconds
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10)))]
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
/// Select when the value is over a limit (1MiB is 1048576 bytes (1024*1024))
pub auto_select: Option<NetworkSelectConfig>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Network select configuration
pub struct NetworkSelectConfig {
/// Select the total received data when it's over this value
pub total_received_over: Option<u64>,
/// Select the total transmitted data when it's over this value
pub total_transmitted_over: Option<u64>,
/// Select the received data when it's over this value
pub received_over: Option<u64>,
/// Select the transmitted data when it's over this value
pub transmitted_over: Option<u64>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Which IP address to show for the default interface
pub enum InterfaceDisplayType {
#[default]
FriendlyName,
Ipv4,
Ipv6,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Default interface configuration
pub struct DefaultInterfaceConfig {
#[serde(default)]
pub display_type: InterfaceDisplayType,
}
@@ -0,0 +1,131 @@
//! The configuration types of the widgets only the Windows bar runs (keyboard, media, system
//! tray). They are data without any platform code, so that `WidgetConfig` can name every widget
//! and the schema is one union that each platform's view prunes (see `schema`).
use crate::config::LabelPrefix;
use crate::config::MediaDisplayFormat;
use komorebi_config::matching::MatchingStrategy;
use serde::Deserialize;
use serde::Serialize;
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Keyboard widget configuration
pub struct KeyboardConfig {
/// Enable the Input widget
pub enable: bool,
/// Data refresh interval
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10)))]
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Media widget configuration
pub struct MediaConfig {
/// Enable the Media widget
pub enable: bool,
/// Display format of the media widget (defaults to IconAndText)
pub display: Option<MediaDisplayFormat>,
}
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Position of the overflow toggle button
pub enum OverflowTogglePosition {
/// Toggle button appears on the left side (before visible icons)
Left,
/// Toggle button appears on the right side (after visible icons)
#[default]
Right,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Where to place a systray button (refresh, info, shortcuts, etc.)
pub enum ButtonPosition {
/// Show in the main visible area
Visible,
/// Show in the overflow/hidden section
Overflow,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)]
/// A field value with an optional matching strategy.
///
/// A plain string uses exact (case-insensitive) matching.
/// An object with `value` and `matching_strategy` uses the specified strategy.
pub enum FieldMatch {
/// Exact case-insensitive match
Exact(String),
/// Match using a specific strategy
WithStrategy {
/// The value to match against
value: String,
/// How to match (Equals, StartsWith, EndsWith, Contains, Regex, etc.)
matching_strategy: MatchingStrategy,
},
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)]
/// Rule for matching a systray icon to hide
///
/// A plain string matches the exe name (backward compatible).
/// An object with optional `exe`, `tooltip`, and/or `guid` fields
/// uses AND logic: all specified fields must match.
/// Each field can be a plain string (exact match) or an object with
/// `value` and `matching_strategy` for advanced matching.
pub enum HiddenIconRule {
/// Match by exe name (case-insensitive, exact)
Exe(String),
/// Match by one or more properties (all specified fields must match)
Match {
/// Exe name to match
exe: Option<FieldMatch>,
/// Tooltip text to match
tooltip: Option<FieldMatch>,
/// Icon GUID to match (most stable identifier across restarts)
guid: Option<FieldMatch>,
},
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// System tray widget configuration
pub struct SystrayConfig {
/// Enable the System Tray widget
pub enable: bool,
/// A list of rules for icons to hide from the system tray
///
/// Each entry can be a plain string (matches exe name, case-insensitive)
/// or an object with optional `exe`, `tooltip`, and/or `guid` fields
/// (all specified fields must match, AND logic, case-insensitive).
///
/// Run komorebi-bar with RUST_LOG=info to see the properties of all
/// systray icons in the log output.
pub hidden_icons: Option<Vec<HiddenIconRule>>,
/// Position of the overflow toggle button (default: Right)
pub overflow_toggle_position: Option<OverflowTogglePosition>,
/// Show an info button that opens a floating panel listing all systray icons
/// with their exe, tooltip, and GUID. Set to "Visible" to show it in the main
/// area, or "Overflow" to show it in the hidden/overflow section.
pub info_button: Option<ButtonPosition>,
/// Interval in seconds to automatically check for and remove stale icons
/// whose owning process has exited. Clamped between 30 and 600 seconds.
/// Defaults to 60. Set to 0 to disable.
pub stale_icons_check_interval: Option<u64>,
/// Show a refresh button that manually triggers stale icon cleanup.
/// Set to "Visible" to show it in the main area, or "Overflow" to
/// show it in the hidden/overflow section.
pub refresh_button: Option<ButtonPosition>,
/// Show a button that toggles komorebi-shortcuts (kills the process if
/// running, starts it otherwise). Set to "Visible" to show it in the main
/// area, or "Overflow" to show it in the hidden/overflow section.
pub shortcuts_button: Option<ButtonPosition>,
}
@@ -1,4 +1,4 @@
use crate::bar::Alignment;
use crate::alignment::Alignment;
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
@@ -11,7 +11,8 @@ use eframe::egui::Ui;
use eframe::egui::text::LayoutJob;
use serde::Deserialize;
use serde::Serialize;
use std::process::Command;
use crate::platform::SystemTool;
use crate::platform::platform;
use std::time::Duration;
use std::time::Instant;
use sysinfo::Disks;
@@ -190,13 +191,8 @@ impl BarWidget for Storage {
if SelectableFrame::new_auto(output.selected, auto_focus_fill)
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
.clicked()
&& let Err(error) = Command::new("cmd.exe")
.args([
"/C",
"explorer.exe",
output.label.split(' ').collect::<Vec<&str>>()[0],
])
.spawn()
&& let Err(error) = platform()
.open_system_tool(SystemTool::StorageLocation(output.label.clone()))
{
eprintln!("{error}")
}
@@ -1,4 +1,4 @@
use crate::bar::Alignment;
use crate::alignment::Alignment;
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
@@ -1,7 +1,134 @@
use crate::render::RenderConfig;
use crate::widgets::applications::ApplicationsConfig;
use crate::widgets::battery::BatteryConfig;
use crate::widgets::cpu::CpuConfig;
use crate::widgets::date::DateConfig;
use crate::widgets::komorebi_config::KomorebiConfig;
use crate::widgets::memory::MemoryConfig;
use crate::widgets::network_config::NetworkConfig;
use crate::widgets::platform_config::KeyboardConfig;
use crate::widgets::platform_config::MediaConfig;
use crate::widgets::platform_config::SystrayConfig;
use crate::widgets::storage::StorageConfig;
use crate::widgets::time::TimeConfig;
use crate::widgets::update::UpdateConfig;
use eframe::egui::Context;
use eframe::egui::Ui;
use serde::Deserialize;
use serde::Serialize;
pub trait BarWidget {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig);
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Widget configuration
pub enum WidgetConfig {
/// Applications widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Applications"))]
Applications(ApplicationsConfig),
/// Battery widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Battery"))]
Battery(BatteryConfig),
/// CPU widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Cpu"))]
Cpu(CpuConfig),
/// Date widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Date"))]
Date(DateConfig),
/// Keyboard widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Keyboard"))]
Keyboard(KeyboardConfig),
/// Komorebi widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Komorebi"))]
Komorebi(KomorebiConfig),
/// Media widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Media"))]
Media(MediaConfig),
/// Memory widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Memory"))]
Memory(MemoryConfig),
/// Network widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Network"))]
Network(NetworkConfig),
/// Storage widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Storage"))]
Storage(StorageConfig),
/// System Tray widget configuration (Windows only)
#[cfg_attr(feature = "schemars", schemars(title = "Systray"))]
Systray(SystrayConfig),
/// Time widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Time"))]
Time(TimeConfig),
/// Update widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Update"))]
Update(UpdateConfig),
}
impl WidgetConfig {
/// The widget a configuration key names, if the platform this bar runs on has it. The
/// keyboard, media and system tray widgets exist on Windows only: elsewhere the key is
/// accepted and skipped (see [`WidgetConfig::unsupported`]).
#[must_use]
pub const fn supported(&self) -> bool {
match self {
WidgetConfig::Keyboard(_) | WidgetConfig::Media(_) | WidgetConfig::Systray(_) => {
cfg!(target_os = "windows")
}
_ => true,
}
}
/// The name of this widget when the platform cannot run it.
#[must_use]
pub const fn unsupported(&self) -> Option<&'static str> {
if self.supported() {
return None;
}
match self {
WidgetConfig::Keyboard(_) => Some("Keyboard"),
WidgetConfig::Media(_) => Some("Media"),
WidgetConfig::Systray(_) => Some("Systray"),
_ => None,
}
}
/// Whether the widget is enabled and can run on this platform.
#[must_use]
pub fn enabled(&self) -> bool {
self.supported()
&& match self {
WidgetConfig::Applications(config) => config.enable,
WidgetConfig::Battery(config) => config.enable,
WidgetConfig::Cpu(config) => config.enable,
WidgetConfig::Date(config) => config.enable,
WidgetConfig::Keyboard(config) => config.enable,
WidgetConfig::Komorebi(config) => {
config.workspaces.as_ref().is_some_and(|w| w.enable)
|| config.layout.as_ref().is_some_and(|w| w.enable)
|| config.focused_container.as_ref().is_some_and(|w| w.enable)
|| config
.configuration_switcher
.as_ref()
.is_some_and(|w| w.enable)
}
WidgetConfig::Media(config) => config.enable,
WidgetConfig::Memory(config) => config.enable,
WidgetConfig::Network(config) => config.enable,
WidgetConfig::Storage(config) => config.enable,
WidgetConfig::Systray(config) => config.enable,
WidgetConfig::Time(config) => config.enable,
WidgetConfig::Update(config) => config.enable,
}
}
}
/// Logs a warning for every widget in `widgets` that this platform cannot run; they are accepted
/// in the configuration and skipped.
pub fn warn_unsupported<'a>(widgets: impl IntoIterator<Item = &'a WidgetConfig>) {
for name in widgets.into_iter().filter_map(WidgetConfig::unsupported) {
tracing::warn!("the {name} widget is not supported on this platform and is skipped");
}
}
@@ -76,3 +76,73 @@ fn the_checked_in_bar_schemas_are_json_schemas_of_komobar_config() {
assert!(text.ends_with("}\n"), "{}", pin.file);
}
}
mod views {
use komorebi_bar_core::schema::SchemaPlatform;
use komorebi_bar_core::schema::view_text;
fn text(file: &str) -> String {
String::from_utf8(super::read(file)).expect("utf-8")
}
/// The Windows file is the union; its Windows view is itself.
#[test]
fn the_windows_view_of_the_union_is_the_windows_file() {
let union = serde_json::from_str(&text("schema.bar.json")).expect("json");
assert!(view_text(union, SchemaPlatform::Windows) == text("schema.bar.json"));
}
/// The macOS view of the union is the macOS file, byte for byte.
#[test]
fn the_macos_view_of_the_union_is_the_macos_file() {
let union = serde_json::from_str(&text("schema.bar.json")).expect("json");
let view = view_text(union, SchemaPlatform::MacOs);
let file = text("komorebi-for-mac/schema.bar.json");
if view != file {
let first = view.lines().zip(file.lines()).position(|(a, b)| a != b);
panic!(
"the views differ, first at line {first:?} (view {} lines, file {})",
view.lines().count(),
file.lines().count()
);
}
}
/// A view applied to its own output changes nothing, so a bar can print its generated
/// schema through it.
#[test]
fn a_view_is_idempotent() {
let union = serde_json::from_str(&text("schema.bar.json")).expect("json");
let once = view_text(union, SchemaPlatform::MacOs);
let again = view_text(
serde_json::from_str(&once).expect("json"),
SchemaPlatform::MacOs,
);
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()
);
}
}
}
+13
View File
@@ -0,0 +1,13 @@
[package]
name = "komorebi-settings"
version = "0.1.42"
edition = "2024"
description = "The settings every komorebi daemon reads while it runs: one immutable snapshot, replaced as a whole, instead of process-wide statics"
[dependencies]
komorebi-config = { path = "../komorebi-config", default-features = false }
komorebi-layouts = { path = "../../komorebi-layouts" }
komorebi-protocol = { path = "../komorebi-protocol", default-features = false }
komorebi-themes = { path = "../../komorebi-themes", default-features = false }
parking_lot = "0.12"
+263
View File
@@ -0,0 +1,263 @@
use std::sync::Arc;
use parking_lot::Mutex;
use parking_lot::RwLock;
use crate::Settings;
/// The current [`Settings`] of a daemon.
///
/// Reading costs one uncontended read lock and an `Arc` clone; the lock is held for the clone
/// only, never while the caller uses the snapshot. The representation (an `RwLock<Arc<_>>`) is
/// private: it can become an `ArcSwap` later without touching a caller.
pub struct Handle<P> {
current: RwLock<Arc<Settings<P>>>,
// Serialises writers. Readers never take it.
writers: Mutex<()>,
}
impl<P> Handle<P> {
pub fn new(settings: Settings<P>) -> Self {
Self {
current: RwLock::new(Arc::new(settings)),
writers: Mutex::new(()),
}
}
/// The settings as they are now. Take it once per unit of work (one border render, one
/// layout pass, one rule check) and use it for all of it.
pub fn snapshot(&self) -> Arc<Settings<P>> {
Arc::clone(&self.current.read())
}
/// Publish a whole new value. Readers that already hold a snapshot keep the old one.
pub fn replace(&self, settings: Settings<P>) {
let _writer = self.writers.lock();
*self.current.write() = Arc::new(settings);
}
}
impl<P: Clone> Handle<P> {
/// Copy the current value, let `edit` change the copy and publish it once.
///
/// Writers are serialised for the whole call, so an `update` never overwrites a change made by
/// another `update` that ran at the same time. If `edit` panics nothing is published.
pub fn update<R>(&self, edit: impl FnOnce(&mut Settings<P>) -> R) -> R {
let _writer = self.writers.lock();
let mut copy = (**self.current.read()).clone();
let result = edit(&mut copy);
*self.current.write() = Arc::new(copy);
result
}
}
impl<P: Clone + 'static> Handle<P> {
/// Set one field now and put its previous value back when the guard is dropped.
///
/// The previous value is restored only if the field still holds the value this call set. If a
/// reload or a command changed the field meanwhile, that change wins. (The statics did the
/// opposite: the saved value overwrote whatever had been written in between.)
pub fn scoped<T>(&self, field: fn(&mut Settings<P>) -> &mut T, value: T) -> Scoped<'_, P, T>
where
T: Clone + PartialEq,
{
let previous = self.update(|settings| std::mem::replace(field(settings), value.clone()));
Scoped {
handle: self,
field,
set: value,
previous: Some(previous),
}
}
}
/// The guard returned by [`Handle::scoped`].
#[must_use = "the previous value is put back when the guard is dropped"]
pub struct Scoped<'a, P: Clone + 'static, T: Clone + PartialEq> {
handle: &'a Handle<P>,
field: fn(&mut Settings<P>) -> &mut T,
set: T,
previous: Option<T>,
}
impl<P: Clone + 'static, T: Clone + PartialEq> Drop for Scoped<'_, P, T> {
fn drop(&mut self) {
if let Some(previous) = self.previous.take() {
let field = self.field;
let set = &self.set;
self.handle.update(|settings| {
let slot = field(settings);
if slot == set {
*slot = previous;
}
});
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::shared::PlatformDefaults;
#[derive(Clone, Debug, PartialEq, Default)]
struct Platform {
hiding: u8,
flag: bool,
}
fn handle() -> Handle<Platform> {
Handle::new(Settings::new(
crate::Shared::defaults_for(PlatformDefaults::KDE),
Platform::default(),
))
}
#[test]
fn a_snapshot_does_not_change_when_the_handle_does() {
let handle = handle();
let before = handle.snapshot();
handle.update(|s| s.shared.border.width = 99);
assert_eq!(before.shared.border.width, 10);
assert_eq!(handle.snapshot().shared.border.width, 99);
}
#[test]
fn replace_publishes_the_whole_value() {
let handle = handle();
let mut next = (*handle.snapshot()).clone();
next.platform.hiding = 3;
next.shared.border.enabled = false;
handle.replace(next);
let now = handle.snapshot();
assert_eq!(now.platform.hiding, 3);
assert!(!now.shared.border.enabled);
}
#[test]
fn a_panicking_edit_publishes_nothing() {
let handle = handle();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
handle.update(|s| {
s.shared.border.width = 77;
panic!("edit failed");
});
}));
assert!(result.is_err());
assert_eq!(handle.snapshot().shared.border.width, 10);
// and the writer lock is free again
handle.update(|s| s.shared.border.width = 5);
assert_eq!(handle.snapshot().shared.border.width, 5);
}
#[test]
fn concurrent_updates_lose_nothing() {
let handle = Arc::new(handle());
let threads: Vec<_> = (0..8)
.map(|_| {
let handle = Arc::clone(&handle);
std::thread::spawn(move || {
for _ in 0..500 {
handle.update(|s| s.shared.border.width += 1);
}
})
})
.collect();
for thread in threads {
thread.join().unwrap();
}
assert_eq!(handle.snapshot().shared.border.width, 10 + 8 * 500);
}
#[test]
fn readers_never_see_half_an_update() {
// The writer keeps width and offset equal; a torn read would show them different.
let handle = Arc::new(handle());
handle.update(|s| {
s.shared.border.width = 0;
s.shared.border.offset = 0;
});
let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
let writer = {
let (handle, stop) = (Arc::clone(&handle), Arc::clone(&stop));
std::thread::spawn(move || {
let mut n = 0;
while !stop.load(std::sync::atomic::Ordering::Relaxed) {
n += 1;
handle.update(|s| {
s.shared.border.width = n;
s.shared.border.offset = n;
});
}
})
};
for _ in 0..50_000 {
let seen = handle.snapshot();
assert_eq!(seen.shared.border.width, seen.shared.border.offset);
}
stop.store(true, std::sync::atomic::Ordering::Relaxed);
writer.join().unwrap();
}
#[test]
fn scoped_puts_the_value_back() {
let handle = handle();
{
let _guard = handle.scoped(|s| &mut s.platform.hiding, 9);
assert_eq!(handle.snapshot().platform.hiding, 9);
}
assert_eq!(handle.snapshot().platform.hiding, 0);
}
#[test]
fn scoped_leaves_a_change_made_meanwhile() {
let handle = handle();
{
let _guard = handle.scoped(|s| &mut s.platform.hiding, 9);
// a reload writes the field while the override is held
handle.update(|s| s.platform.hiding = 4);
}
assert_eq!(handle.snapshot().platform.hiding, 4);
}
#[test]
fn scoped_touches_only_its_own_field() {
let handle = handle();
{
let _guard = handle.scoped(|s| &mut s.platform.flag, true);
handle.update(|s| s.shared.border.width = 31);
}
let now = handle.snapshot();
assert!(!now.platform.flag);
assert_eq!(now.shared.border.width, 31);
}
#[test]
fn nested_scopes_unwind_in_order() {
let handle = handle();
let outer = handle.scoped(|s| &mut s.platform.hiding, 1);
let inner = handle.scoped(|s| &mut s.platform.hiding, 2);
assert_eq!(handle.snapshot().platform.hiding, 2);
drop(inner);
assert_eq!(handle.snapshot().platform.hiding, 1);
drop(outer);
assert_eq!(handle.snapshot().platform.hiding, 0);
}
}
+41
View File
@@ -0,0 +1,41 @@
//! The settings a daemon reads while it runs.
//!
//! Today each daemon keeps them in dozens of process-wide statics that `apply_globals` and the
//! `SocketMessage` handlers write one at a time while the border, animator and reaper threads
//! read them (see `docs/dev/settings.md`). This crate holds the replacement and nothing else: no
//! daemon uses it yet.
//!
//! * [`Settings`] is a value: a [`Shared`] part that is the same struct on every platform and a
//! platform part `P` that each daemon defines for its own subsystems.
//! * [`Handle`] holds the current value. A reader takes a [`Handle::snapshot`] once per unit of
//! work and sees one consistent configuration for all of it. A writer uses [`Handle::update`],
//! which serialises writers, so two writers never lose each other's change, and publishes the
//! result once.
//! * [`Handle::scoped`] replaces the "store a value, do work, store the old value back" pattern
//! without clobbering a change made in between.
#![warn(clippy::all)]
#![allow(clippy::missing_errors_doc, clippy::use_self, clippy::doc_markdown)]
mod handle;
pub mod shared;
pub use handle::Handle;
pub use handle::Scoped;
pub use shared::Shared;
/// Every setting of one running daemon.
///
/// `P` is the platform part: a plain type the daemon defines (the Windows stackbar and hiding
/// behaviour, the macOS tabbed-application list, ...). Shared code never looks inside it, so it is
/// a type parameter and not a trait; it needs to be `Clone` so that an update can copy the value.
#[derive(Clone, Debug, PartialEq)]
pub struct Settings<P> {
pub shared: Shared,
pub platform: P,
}
impl<P> Settings<P> {
pub fn new(shared: Shared, platform: P) -> Self {
Self { shared, platform }
}
}
+648
View File
@@ -0,0 +1,648 @@
//! The part of the settings that is the same struct on every platform.
//!
//! Every field here has a counterpart static in each daemon (`docs/dev/settings.md`, section 2.1),
//! and [`SharedDelta::apply`] reproduces what `StaticConfig::apply_globals` does with it, including
//! which absent keys keep the old value and which are reset.
use std::collections::HashMap;
use komorebi_config::animation::PerAnimationPrefixConfig;
use komorebi_config::aspect_ratio::AspectRatio;
use komorebi_config::aspect_ratio::PredefinedAspectRatio;
use komorebi_layouts::DefaultLayout;
use komorebi_layouts::LayoutDefaultEntry;
use komorebi_protocol::animation::AnimationStyle;
use komorebi_protocol::animation::prefix::AnimationPrefix;
use komorebi_themes::colour::Colour;
use komorebi_themes::colour::Rgb;
/// What `ANIMATION_FPS` becomes when `animation` is configured without `fps`
/// (`DEFAULT_ANIMATION_FPS` in each daemon).
pub const DEFAULT_ANIMATION_FPS: u64 = 60;
/// `DEFAULT_ANIMATION_ENABLED`, `DEFAULT_ANIMATION_STYLE` and `DEFAULT_ANIMATION_DURATION`.
pub const DEFAULT_ANIMATION_ENABLED: bool = false;
pub const DEFAULT_ANIMATION_STYLE: AnimationStyle = AnimationStyle::Linear;
pub const DEFAULT_ANIMATION_DURATION: u64 = 250;
/// `DEFAULT_RESIZE_DELTA` and `DEFAULT_MOUSE_FOLLOWS_FOCUS` (equal on all three platforms).
pub const DEFAULT_RESIZE_DELTA: i32 = 50;
pub const DEFAULT_MOUSE_FOLLOWS_FOCUS: bool = true;
/// The defaults a daemon starts with. They differ per platform today (border width 8 / 10 / 10,
/// padding 10 / 5 / 5, ...) and this step does not change any of them.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PlatformDefaults {
pub border_width: i32,
pub border_offset: i32,
pub border_radius: i32,
pub container_padding: i32,
pub workspace_padding: i32,
}
impl PlatformDefaults {
/// `komorebi/src/border_manager/mod.rs` and `komorebi/src/lib.rs`. Windows has no border
/// radius setting; the unused value is the one the other platforms use.
pub const WINDOWS: Self = Self {
border_width: 8,
border_offset: -1,
border_radius: 10,
container_padding: 10,
workspace_padding: 10,
};
/// `komorebi-for-kde/komorebi/src/border_manager/mod.rs` and `.../lib.rs`
pub const KDE: Self = Self {
border_width: 10,
border_offset: 0,
border_radius: 10,
container_padding: 5,
workspace_padding: 5,
};
/// `komorebi-for-mac/komorebi/src/border_manager/mod.rs` and `.../lib.rs`
pub const MACOS: Self = Self {
border_width: 10,
border_offset: 0,
border_radius: 10,
container_padding: 5,
workspace_padding: 5,
};
}
/// Colours are `u32` exactly as the border statics hold them (`u32::from(Colour)`).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BorderColours {
pub single: u32,
pub stack: u32,
pub monocle: u32,
pub floating: u32,
pub unfocused: u32,
pub unfocused_locked: u32,
}
/// Whether and how the borders are drawn; the colours are the same on all three platforms.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Border {
pub enabled: bool,
pub width: i32,
pub offset: i32,
/// Used by KDE and macOS; Windows keeps it but never reads it.
pub radius: i32,
pub colours: BorderColours,
}
/// The animation settings: a global value for each of enabled, style and duration, and a map of
/// per-prefix values that wins over the global one.
#[derive(Clone, Debug, PartialEq)]
pub struct Animation {
pub enabled: bool,
pub style: AnimationStyle,
pub duration: u64,
pub fps: u64,
pub enabled_per_animation: HashMap<AnimationPrefix, bool>,
pub style_per_animation: HashMap<AnimationPrefix, AnimationStyle>,
pub duration_per_animation: HashMap<AnimationPrefix, u64>,
}
/// How a window is moved for one kind of animation: the three values `motion_for` reads today as
/// three separate statics.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Motion {
pub duration_ms: u64,
pub style: AnimationStyle,
}
impl Animation {
/// `None` if `prefix` is not animated, else how. The same rule as `motion_for` (without the
/// suspension counter, which is runtime state and stays with the animation module), now read
/// from one snapshot.
pub fn motion_for(&self, prefix: AnimationPrefix) -> Option<Motion> {
let enabled = self
.enabled_per_animation
.get(&prefix)
.copied()
.unwrap_or(self.enabled);
enabled.then(|| Motion {
duration_ms: self
.duration_per_animation
.get(&prefix)
.copied()
.unwrap_or(self.duration),
style: self
.style_per_animation
.get(&prefix)
.copied()
.unwrap_or(self.style),
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Defaults {
pub container_padding: i32,
pub workspace_padding: i32,
pub resize_delta: i32,
pub mouse_follows_focus: bool,
}
/// `transparency` and `transparency_alpha` (Windows and KDE; macOS never reads them).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Transparency {
pub enabled: bool,
pub alpha: u8,
}
/// Every setting that is not tied to one platform.
#[derive(Clone, Debug, PartialEq)]
pub struct Shared {
pub border: Border,
pub animation: Animation,
pub defaults: Defaults,
pub transparency: Transparency,
pub floating_window_aspect_ratio: AspectRatio,
pub layout_defaults: HashMap<DefaultLayout, LayoutDefaultEntry>,
pub display_index_preferences: HashMap<usize, String>,
}
impl Shared {
/// The values the statics hold before any configuration is read.
pub fn defaults_for(defaults: PlatformDefaults) -> Self {
Self {
border: Border {
enabled: true,
width: defaults.border_width,
offset: defaults.border_offset,
radius: defaults.border_radius,
colours: BorderColours {
single: rgb(66, 165, 245),
stack: rgb(0, 165, 66),
monocle: rgb(255, 51, 153),
floating: rgb(245, 245, 165),
unfocused: rgb(128, 128, 128),
unfocused_locked: rgb(158, 8, 8),
},
},
animation: Animation {
enabled: DEFAULT_ANIMATION_ENABLED,
style: DEFAULT_ANIMATION_STYLE,
duration: DEFAULT_ANIMATION_DURATION,
fps: DEFAULT_ANIMATION_FPS,
enabled_per_animation: HashMap::new(),
style_per_animation: HashMap::new(),
duration_per_animation: HashMap::new(),
},
defaults: Defaults {
container_padding: defaults.container_padding,
workspace_padding: defaults.workspace_padding,
resize_delta: DEFAULT_RESIZE_DELTA,
mouse_follows_focus: DEFAULT_MOUSE_FOLLOWS_FOCUS,
},
transparency: Transparency {
enabled: false,
alpha: 200,
},
// The static starts at Widescreen; a load without the key writes Standard
// (`unwrap_or`), see `SharedDelta::floating_window_aspect_ratio`.
floating_window_aspect_ratio: AspectRatio::Predefined(
PredefinedAspectRatio::Widescreen,
),
layout_defaults: HashMap::new(),
display_index_preferences: HashMap::new(),
}
}
}
fn rgb(r: u32, g: u32, b: u32) -> u32 {
u32::from(Colour::Rgb(Rgb::new(r, g, b)))
}
/// The colours a configuration file or theme names; a missing one keeps the current value.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct BorderColoursDelta {
pub single: Option<u32>,
pub stack: Option<u32>,
pub monocle: Option<u32>,
pub floating: Option<u32>,
pub unfocused: Option<u32>,
pub unfocused_locked: Option<u32>,
}
/// The animation block of a configuration file (`enabled` is required in the file, the rest is
/// optional).
#[derive(Clone, Debug, PartialEq)]
pub struct AnimationDelta {
pub enabled: PerAnimationPrefixConfig<bool>,
pub style: Option<PerAnimationPrefixConfig<AnimationStyle>>,
pub duration: Option<PerAnimationPrefixConfig<u64>>,
pub fps: Option<u64>,
}
/// What one configuration load says about the [`Shared`] settings. `None` means "the key is not in
/// the file", and [`apply`](Self::apply) treats each `None` exactly as
/// `StaticConfig::apply_globals` does.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct SharedDelta {
pub border: Option<bool>,
pub border_width: Option<i32>,
pub border_offset: Option<i32>,
pub border_radius: Option<i32>,
pub border_colours: Option<BorderColoursDelta>,
pub animation: Option<AnimationDelta>,
pub default_container_padding: Option<i32>,
pub default_workspace_padding: Option<i32>,
pub transparency: Option<bool>,
pub transparency_alpha: Option<u8>,
pub floating_window_aspect_ratio: Option<AspectRatio>,
pub layout_defaults: Option<HashMap<DefaultLayout, LayoutDefaultEntry>>,
pub display_index_preferences: Option<HashMap<usize, String>>,
}
impl SharedDelta {
/// Merge this load into `shared`.
///
/// * Keeps the old value when the key is absent: the border (enabled, width, offset, radius,
/// each colour), both paddings, transparency and its alpha, the display index preferences,
/// and the animation block as a whole.
/// * Resets when the key is absent: `floating_window_aspect_ratio` (to Standard, not to the
/// initial Widescreen), `layout_defaults` (cleared), and, inside a present animation block,
/// `fps` (to 60).
/// * A global animation value clears that setting's per-prefix map; a per-prefix map replaces
/// the whole map and leaves the global value alone.
pub fn apply(&self, shared: &mut Shared) {
shared.floating_window_aspect_ratio = self
.floating_window_aspect_ratio
.unwrap_or(AspectRatio::Predefined(PredefinedAspectRatio::Standard));
if let Some(preferences) = &self.display_index_preferences {
shared.display_index_preferences.clone_from(preferences);
}
if let Some(animation) = &self.animation {
let target = &mut shared.animation;
match &animation.enabled {
PerAnimationPrefixConfig::Prefix(enabled) => {
target.enabled_per_animation.clone_from(enabled);
}
PerAnimationPrefixConfig::Global(enabled) => {
target.enabled = *enabled;
target.enabled_per_animation.clear();
}
}
match &animation.style {
Some(PerAnimationPrefixConfig::Prefix(style)) => {
target.style_per_animation.clone_from(style);
}
Some(PerAnimationPrefixConfig::Global(style)) => {
target.style = *style;
target.style_per_animation.clear();
}
None => {}
}
match &animation.duration {
Some(PerAnimationPrefixConfig::Prefix(duration)) => {
target.duration_per_animation.clone_from(duration);
}
Some(PerAnimationPrefixConfig::Global(duration)) => {
target.duration = *duration;
target.duration_per_animation.clear();
}
None => {}
}
target.fps = animation.fps.unwrap_or(DEFAULT_ANIMATION_FPS);
}
if let Some(container) = self.default_container_padding {
shared.defaults.container_padding = container;
}
if let Some(workspace) = self.default_workspace_padding {
shared.defaults.workspace_padding = workspace;
}
match &self.layout_defaults {
Some(defaults) => shared.layout_defaults.clone_from(defaults),
None => shared.layout_defaults.clear(),
}
if let Some(width) = self.border_width {
shared.border.width = width;
}
if let Some(offset) = self.border_offset {
shared.border.offset = offset;
}
if let Some(radius) = self.border_radius {
shared.border.radius = radius;
}
if let Some(enabled) = self.border {
shared.border.enabled = enabled;
}
if let Some(colours) = &self.border_colours {
let target = &mut shared.border.colours;
if let Some(colour) = colours.single {
target.single = colour;
}
if let Some(colour) = colours.stack {
target.stack = colour;
}
if let Some(colour) = colours.monocle {
target.monocle = colour;
}
if let Some(colour) = colours.floating {
target.floating = colour;
}
if let Some(colour) = colours.unfocused {
target.unfocused = colour;
}
if let Some(colour) = colours.unfocused_locked {
target.unfocused_locked = colour;
}
}
if let Some(enabled) = self.transparency {
shared.transparency.enabled = enabled;
}
if let Some(alpha) = self.transparency_alpha {
shared.transparency.alpha = alpha;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn shared() -> Shared {
Shared::defaults_for(PlatformDefaults::KDE)
}
#[test]
fn the_platform_defaults_are_the_ones_the_statics_start_with() {
let windows = Shared::defaults_for(PlatformDefaults::WINDOWS);
assert_eq!(
(windows.border.width, windows.border.offset),
(8, -1),
"komorebi/src/border_manager/mod.rs"
);
assert_eq!(windows.defaults.container_padding, 10);
assert_eq!(windows.defaults.workspace_padding, 10);
for platform in [PlatformDefaults::KDE, PlatformDefaults::MACOS] {
let other = Shared::defaults_for(platform);
assert_eq!((other.border.width, other.border.offset), (10, 0));
assert_eq!(other.defaults.container_padding, 5);
assert_eq!(other.defaults.workspace_padding, 5);
}
}
#[test]
fn the_default_colours_are_the_theme_conversion_of_the_rgb_values() {
// Rgb packs as r | g << 8 | b << 16 (komorebi-themes), e.g. FOCUSED = (66, 165, 245)
let colours = shared().border.colours;
assert_eq!(colours.single, 66 | (165 << 8) | (245 << 16));
assert_eq!(colours.unfocused_locked, 158 | (8 << 8) | (8 << 16));
}
#[test]
fn an_empty_load_keeps_everything_but_the_two_reset_keys() {
let mut shared = shared();
shared.border.width = 33;
shared.defaults.container_padding = 7;
shared.transparency.alpha = 12;
shared.animation.fps = 144;
shared
.display_index_preferences
.insert(0, String::from("serial"));
shared.layout_defaults.insert(
DefaultLayout::BSP,
LayoutDefaultEntry {
layout_options: None,
layout_options_rules: None,
},
);
let before = shared.clone();
SharedDelta::default().apply(&mut shared);
// kept
assert_eq!(shared.border, before.border);
assert_eq!(shared.defaults, before.defaults);
assert_eq!(shared.transparency, before.transparency);
assert_eq!(shared.animation, before.animation);
assert_eq!(
shared.display_index_preferences,
before.display_index_preferences
);
// reset
assert!(shared.layout_defaults.is_empty());
assert_eq!(
shared.floating_window_aspect_ratio,
AspectRatio::Predefined(PredefinedAspectRatio::Standard),
"absent key writes Standard, not the initial Widescreen"
);
}
#[test]
fn present_keys_replace_and_absent_colours_stay() {
let mut shared = shared();
let before = shared.border.colours;
SharedDelta {
border: Some(false),
border_width: Some(3),
border_offset: Some(-2),
border_radius: Some(0),
border_colours: Some(BorderColoursDelta {
single: Some(1),
unfocused_locked: Some(2),
..BorderColoursDelta::default()
}),
default_workspace_padding: Some(1),
transparency: Some(true),
transparency_alpha: Some(9),
..SharedDelta::default()
}
.apply(&mut shared);
assert!(!shared.border.enabled);
assert_eq!(
(
shared.border.width,
shared.border.offset,
shared.border.radius
),
(3, -2, 0)
);
assert_eq!(shared.border.colours.single, 1);
assert_eq!(shared.border.colours.unfocused_locked, 2);
assert_eq!(shared.border.colours.stack, before.stack);
assert_eq!(shared.defaults.workspace_padding, 1);
assert_eq!(shared.defaults.container_padding, 5, "absent: kept");
assert_eq!(
shared.transparency,
Transparency {
enabled: true,
alpha: 9
}
);
}
fn block(
enabled: PerAnimationPrefixConfig<bool>,
style: Option<PerAnimationPrefixConfig<AnimationStyle>>,
duration: Option<PerAnimationPrefixConfig<u64>>,
fps: Option<u64>,
) -> SharedDelta {
SharedDelta {
animation: Some(AnimationDelta {
enabled,
style,
duration,
fps,
}),
..SharedDelta::default()
}
}
#[test]
fn a_global_animation_value_clears_its_per_prefix_map_and_a_map_keeps_the_global() {
let mut shared = shared();
shared
.animation
.enabled_per_animation
.insert(AnimationPrefix::Movement, true);
shared
.animation
.duration_per_animation
.insert(AnimationPrefix::Workspace, 50);
// global enabled clears the enabled map; style absent keeps; duration map replaces the
// map and leaves the global duration alone
block(
PerAnimationPrefixConfig::Global(true),
None,
Some(PerAnimationPrefixConfig::Prefix(HashMap::from([(
AnimationPrefix::Movement,
10,
)]))),
Some(30),
)
.apply(&mut shared);
assert!(shared.animation.enabled);
assert!(shared.animation.enabled_per_animation.is_empty());
assert_eq!(shared.animation.style, DEFAULT_ANIMATION_STYLE);
assert_eq!(shared.animation.duration, DEFAULT_ANIMATION_DURATION);
assert_eq!(
shared.animation.duration_per_animation,
HashMap::from([(AnimationPrefix::Movement, 10)])
);
assert_eq!(shared.animation.fps, 30);
}
#[test]
fn an_animation_block_without_fps_resets_fps_and_no_block_keeps_it() {
let mut shared = shared();
shared.animation.fps = 144;
SharedDelta::default().apply(&mut shared);
assert_eq!(shared.animation.fps, 144);
block(PerAnimationPrefixConfig::Global(false), None, None, None).apply(&mut shared);
assert_eq!(shared.animation.fps, DEFAULT_ANIMATION_FPS);
}
#[test]
fn motion_for_reads_one_consistent_configuration() {
let mut shared = shared();
shared.animation.enabled = true;
shared.animation.duration = 250;
shared.animation.style = AnimationStyle::Linear;
shared
.animation
.duration_per_animation
.insert(AnimationPrefix::Workspace, 400);
shared
.animation
.style_per_animation
.insert(AnimationPrefix::Workspace, AnimationStyle::EaseInSine);
shared
.animation
.enabled_per_animation
.insert(AnimationPrefix::Transparency, false);
assert_eq!(
shared.animation.motion_for(AnimationPrefix::Movement),
Some(Motion {
duration_ms: 250,
style: AnimationStyle::Linear
})
);
assert_eq!(
shared.animation.motion_for(AnimationPrefix::Workspace),
Some(Motion {
duration_ms: 400,
style: AnimationStyle::EaseInSine
})
);
assert_eq!(
shared.animation.motion_for(AnimationPrefix::Transparency),
None,
"a per-prefix false wins over a global true"
);
}
#[test]
fn layout_defaults_are_replaced_when_given() {
let mut shared = shared();
let entry = LayoutDefaultEntry {
layout_options: None,
layout_options_rules: None,
};
SharedDelta {
layout_defaults: Some(HashMap::from([(DefaultLayout::BSP, entry.clone())])),
..SharedDelta::default()
}
.apply(&mut shared);
assert_eq!(
shared.layout_defaults.get(&DefaultLayout::BSP),
Some(&entry)
);
SharedDelta {
layout_defaults: Some(HashMap::new()),
..SharedDelta::default()
}
.apply(&mut shared);
assert!(shared.layout_defaults.is_empty());
}
#[test]
fn display_index_preferences_are_replaced_not_merged() {
let mut shared = shared();
shared
.display_index_preferences
.insert(0, String::from("a"));
shared
.display_index_preferences
.insert(1, String::from("b"));
SharedDelta {
display_index_preferences: Some(HashMap::from([(2, String::from("c"))])),
..SharedDelta::default()
}
.apply(&mut shared);
assert_eq!(
shared.display_index_preferences,
HashMap::from([(2, String::from("c"))])
);
}
}
+180
View File
@@ -0,0 +1,180 @@
# One `komorebi-bar` for Windows and macOS
Status at the revision that adds this file. Everything below was measured on this tree with
`diff` (counts are lines that differ, `<`/`>` lines of `diff`, hunks in parentheses). Nothing
here was rendered: the bars cannot run on Linux. Gates that did run are named; the ones that
did not are listed at the end.
KDE has no bar: `komorebi-for-kde` has no `komorebi-bar` package and no `schema.bar.json`.
The plan below is for two platforms; a KDE bar would add a third shell and a third schema view.
## What exists
- `crates/komorebi-bar-core` (4,592 lines): the code both bars share, one copy each.
`selected_frame`, `ui`, `statics`, `alignment`, `render`, `config` (the leaf types and the whole
`KomobarConfig` with the mouse options), the widgets `applications`, `battery`, `cpu`, `date`,
`memory`, `storage`, `time`, `update`, `komorebi_layout`, the icon cache (`widgets/icons.rs`),
all widget configuration types (`komorebi_config`, `network_config`, `platform_config` for
keyboard, media and systray), `WidgetConfig` with a variant for every widget, the `BarWidget`
trait, `platform` (`BarPlatform`, `SystemTool`, a once-installed global) and `schema`
(per-platform views, see below).
- `BarPlatform` hooks: `open_system_tool`, `latest_release_api_url`, `calendar_icon`,
`command_icon`, `exec_shell_command`, `send_komorebi_message`, `send_komorebi_batch`.
- Two shells, `komorebi-bar` (Windows, root workspace, version 0.1.42) and
`komorebi-for-mac/komorebi-bar` (own workspace, own `Cargo.lock`, own flake, version 0.1.1).
Each keeps `main.rs`, `bar.rs`, `os.rs`, the one-line `config.rs`, and the widgets whose
runtime differs: `komorebi.rs`, `network.rs`, and on Windows `keyboard.rs`, `media.rs`,
`systray.rs`. `widgets/widget.rs` is only `WidgetConfigExt` (config -> widget).
- The schema oracle regenerates the macOS file natively
(`tests/schema_oracle.rs`, module `generated`); the Windows file is still checked by the
out-of-tree harness because three descriptions read differently when compiled for Windows.
## Why one package is not possible yet (the structural blocker)
The two trees are two Cargo workspaces with two lockfiles and two flakes. A package belongs to
one workspace, so a single `komorebi-bar` that builds on both platforms needs one workspace
first: the Windows root `Cargo.toml` and `komorebi-for-mac/Cargo.toml` merged, one `Cargo.lock`,
one version (0.1.42 vs 0.1.0), one `komorebi-client` (the two `lib.rs` differ in 150 lines:
Windows re-exports the daemon crate and `State`/`Window`/`Workspace`, macOS has its own
types) and `windows`/`objc2` dependencies under `[target.'cfg(...)'.dependencies]`. That is a
repository-level cutover (the plan's "final naming of the outputs and the unified app/devShell
is a cutover decision"), not a bar change. Until then the bars share the crate and converge,
they do not merge. Everything in this stack is ordered so the merge later is a package move.
## The schema is solved: views of one union
`schema.bar.json` differs between the platforms in 40 hunks (5,381 lines of 9,091 / 3,774).
Analysed by definition (`jq` over `$defs`):
- 42 definitions exist only on Windows: all reachable only through `MouseConfig` (which holds a
`SocketMessage`, hence the closure) and the `Keyboard`/`Media`/`Systray` widgets. Removing the
`mouse` property and the three widget variants and pruning unreachable definitions removes
all 42.
- 14 differences remain in the 55 shared definitions, all text or default: root description
(macOS names its own schema URL), `height` default `50` vs `50.0`, the margin and padding
examples (`10` vs `10.0`, a sentence about defaults, a trailing comma), `show_icon`
(deprecated flag and backticks), `KomorebiLayout` (untagged `anyOf` on Windows, externally
tagged `oneOf` on macOS), `NetworkConfig.default_interface_options` (Windows only),
four storage descriptions (`C:\` vs `/`), `TimeConfig.changing_icon` (`default: false` only
on macOS), `TimeFormat` title of `Custom` (macOS says `Default`, a mislabel), the
`WidgetConfig` variants above.
`komorebi_bar_core::schema` holds this as data (`schema/changes.rs`, 16 changes) in the same
design as `komorebi-config`: the union is the Windows text, `view_of` applies a platform's
changes and prunes. `tests/schema_oracle.rs` proves that the macOS view of the checked-in Windows
file is byte-identical to `komorebi-for-mac/schema.bar.json`, that the Windows view is the
identity, and that views are idempotent. Both bars print `--schema` through their view
(`schema::generated_text`), so the files and `nix/komorebi-bar-options.nix` do not change when
code is converged onto the Windows spelling.
The out-of-tree harness (`S/bar2/regen.sh`, real bar sources over the real
`komorebi-protocol`/`komorebi-config`) regenerates both files on Linux. Since `config.rs` moved, the
in-repo oracle regenerates the macOS file itself (`generated` tests); only Windows needs the harness.
## What is still per platform (measured at this tip)
| file | win / mac lines | differing | what differs and why it is not moved |
|---|---|---|---|
| `widgets/komorebi.rs` | 940 / 986 | 136 (17) | Two things. (1) Real rendering differences: macOS wraps `show_label` and the "all icons" painter in a `Frame` with a margin and a minimum height (`bar.show_label(...)`, a method, vs an associated function on Windows), `1.0` vs `1.0_f32`. Taking either spelling is a visual change on the other platform: needs a screen. (2) The conversion from the daemon state: Windows uses `komorebi_client::{State, Window, Workspace, Container}` (`hwnd`, `title()`, `process_id()`, windows-icons), macOS its own client types (`details.title`, `details.icon_path`, `icns_to_rgba`, `device`, no `Layout::Custom`). A State-to-snapshot split (`WindowInfo`/`MonitorInfo` of `komorebi-backend` as the carrier) is not clean yet: the macOS and Windows `komorebi_client` `State` are different types, the bar subscribes to the daemons with different notification shapes, and I cannot test either subscription here. Not attempted. |
| `widgets/network.rs` | 598 / 578 | 82 (19) | Windows reads `default_interface_options` (friendly name, IPv4, IPv6), macOS does not (the config type is shared and macOS logs a warning); `std::sync::Mutex` (poisoning handled) vs `parking_lot`; `netdev` 0.46 vs 0.40 and a different interface match key (friendly name vs `name`); `cmd.exe /C ncpa` vs `open x-apple...`. Functional on macOS: BEHAVIOUR, needs a Mac. Known difference, left as is. |
| `bar.rs` | 1,399 / 1,105 | 444 (35) | Windows: PowerShell session (`exec_powershell`, behind the hook), `hwnd`, virtual-desktop restore/minimize, `position_bar` through `komorebi_client::Window`, CJK font fallbacks, `scale_factor`, the Windows mouse handling. macOS: unboxed notification type, window shadow and level. Not behaviour-preserving without a screen. |
| `main.rs` | 402 / 388 | 92 (14) | `process_hwnd`/`enum_window`, DPI awareness, `set_read_timeout`; macOS activation policy and window level (AppKit), config dir `~/.config/komorebi`. Window and viewport setup belongs behind `BarPlatform` hooks (`init_process`, `viewport_builder`, `after_creation`), not written: it cannot be exercised here. |
| `os.rs` | 83 / 60 | 71 (14) | the platform implementations themselves; stays per platform (`cfg(target_os)` modules after the merge). |
| `widgets/widget.rs`, `widgets/mod.rs` | 41 / 49, 22 / 19 | 20, 5 | `WidgetConfigExt` (macOS builds a no-op `Unsupported` for the three Windows-only widgets) and the module lists. |
Windows-only and not shared: `widgets/keyboard.rs` (167), `media.rs` (322), `systray.rs` (1,205);
their config types are in the core. The macOS copies of keyboard and media were dead and are deleted.
### Known macOS/Windows differences that were kept (D-coord-1)
- `network.rs`: see above. The macOS bar ignores `default_interface_options`.
- Windows-only config keys are accepted on macOS and skipped with a logged warning (BEHAVIOUR on
macOS, as for `StaticConfig`): `Keyboard`, `Media` and `Systray` widgets
(`widgets::widget::warn_unsupported`), the `mouse` options and the network
`default_interface_options`. Before, the first group failed to deserialize and rejected the whole
config, the other two were ignored silently.
- macOS commands that spawn programs that do not exist there are preserved, not fixed:
`SystemTool::PowerSettings` (battery click) spawns `cmd.exe /C start ms-settings:...`,
`SystemTool::Url` (update click) spawns `explorer.exe`, and `UserCommand::launch_if_ready` of the
applications widget spawns `cmd /C <command>` in both bars. `update.rs` still has a second GitHub
API URL for the Windows repository. The macOS spawns live in
`komorebi-for-mac/komorebi-bar/src/os.rs`; the storage click is `SystemTool::StorageLocation` and
each shell spawns what it spawned before.
- `KomorebiLayout` is `#[serde(untagged)]` in both bars now (the macOS source converged; the macOS
schema view restores the macOS text). Its `Deserialize` is hand-written and nothing in the bar
serializes it, so this changes only the derived schema and `Serialize`. The coordinator said to
leave it alone; it is a separate revision (`mac-bar: converge komorebi.rs config text and
komorebi_layout.rs`) and can be dropped, at the price of keeping `KomorebiLayout` and its
widget in the shells.
## The merged package layout (for the coordinator's workspace merge)
Not done and not prepared in the tree (no workspace or lockfile structure was changed). When the
root workspace and `komorebi-for-mac` are merged the bar should look like this:
- Package `komorebi-bar` (one, in `komorebi-bar/`) plus `crates/komorebi-bar-core` as today.
Version: the workspace version. The macOS tree's `komorebi-bar` 0.1.1 and the `v0.1.0` text in
its schema description are a view concern (`schema/changes.rs`), not a package one.
- `src/main.rs` (shared: the argument parsing, config read, `--schema`, the event loop shell)
and `src/bar.rs` split into `src/bar/mod.rs` (shared `Komobar`) with
`#[cfg(target_os = "windows")] mod windows;` and `#[cfg(target_os = "macos")] mod macos;`
for the window handle, DPI, activation policy, window level, position and the PowerShell
session. `src/os.rs` becomes `src/os/{windows,macos}.rs` (the `BarPlatform` implementations,
selected by `cfg(target_os)` and installed first thing in `main`). Windows-only widgets
(`keyboard`, `media`, `systray`) are `#[cfg(target_os = "windows")]` modules; `komorebi.rs` and
`network.rs` stay two files (`komorebi/{windows,macos}.rs` behind one `widgets::komorebi`
facade) until the snapshot split.
- `Cargo.toml`: `windows`, `windows-core`, `windows-icons*`, `systray-util`, `font-loader` under
`[target.'cfg(windows)'.dependencies]`; `objc2`, `objc2-app-kit`, `raw-window-handle`, `icns`
under `[target.'cfg(target_os = "macos")'.dependencies]`; `netdev` has two versions (0.46,
0.40): pick one when `network.rs` converges, until then a target-specific rename
(`netdev-win`, `netdev-mac`). `komorebi-client`: one crate (the two differ in 150 lines).
Features: `schemars` as now (it forwards to the core). No new feature per platform: the
platform is `cfg(target_os)`.
- Schema: the checked-in files stay (`schema.bar.json`, `komorebi-for-mac/schema.bar.json`, or one
directory for both after the merge). `--schema` prints the view of the running platform
(`SchemaPlatform` from `cfg(target_os)`); a `--schema-for <platform>` flag would let one host
regenerate both, as the Windows union is the Windows text. Windows' three
platform-dependent descriptions (protocol `PathBuf`, themes) need the Windows target to
regenerate, so the Windows file is only verifiable on Windows or with the harness.
- Lock files: the macOS `Cargo.lock` folds into the root one; the lines this stack added to both
(`image`, `komorebi-config`, `komorebi-layouts`, `komorebi-protocol`, `komorebi-themes`,
`tracing`, `lazy_static`, `color-eyre` for bar-core) are the same crates in both, so the merge
adds no new versions beyond `netdev` and `starship-battery`/`sysinfo` (already unified here).
- Nix: `komorebi-for-mac/nix/komorebi-bar-options.nix` is generated from the macOS file and does
not change; the two flakes' `extractBinary "komorebi-bar"` become one derivation per system.
## The plan, in order
1. Done: schema views and oracle; storage, time, applications, komorebi layout widget, icon
cache, all widget config types, `WidgetConfig`, `config.rs` moved; hooks for platform
spawns, the shell session and the komorebi client.
2. Not done, needs a screen or a Mac: `bar.rs`/`main.rs` window code behind `BarPlatform`
(`init_process`, `viewport_builder`, `after_creation`, position/DPI/activation), then
`Komobar<P>` and the shared `app` body into the core.
3. Not done, needs the snapshot split: `komorebi.rs`. It needs `State`-to-snapshot conversion
per platform and one renderer; the frame/margin difference must be decided by looking at both.
4. Decision for `network.rs`: converge on the Windows widget (macOS gains friendly-name,
IPv4/IPv6 and the `netdev` bump) or keep two.
5. Workspace merge (above), then the two shells collapse into one package.
6. The oracle then compares generated text with the files for both platforms.
## What to check by hand
Windows (coordinator, screenshot e2e): the bar starts; the storage, time, applications (icons
from executables), komorebi layout (click opens the options, choosing one changes the layout,
Monocle/Floating toggles, paused) and komorebi workspaces widgets render; a click on storage
opens Explorer on the drive; a config with `mouse` options and a `Systray`/`Keyboard`/`Media`
widget still works (this is the Windows path through the moved `config.rs`, `WidgetConfig` and
the `exec_shell_command` hook: a `Command` mouse message must still reach the PowerShell session);
`komorebi-bar --schema` equals `schema.bar.json`.
macOS (also in the section for `docs/dev/macos-handcheck.md`): the bar starts with your real
config; the time widget renders (format variants, `changing_icon`); clicking a storage widget
opens the mounted volume in Finder (`open <path>`); the komorebi layout widget click works as
before; a config that contains a `Keyboard`, `Media` or `Systray` widget, a `mouse` block, or
`default_interface_options` under `Network` starts the bar and logs one warning per item (before:
the widget case failed to load the config); `komorebi-bar --schema` equals
`komorebi-for-mac/schema.bar.json`; the battery and update clicks still fail the way they did
(preserved, one-function fix in `os.rs`).
+320
View File
@@ -0,0 +1,320 @@
# Settings: replacing the process-wide statics
Status: design (plan step 10, phase 1). Measured on `wip-main` 633e7453 (the Windows tree
`komorebi/src`, KDE `komorebi-for-kde/komorebi/src`, macOS `komorebi-for-mac/komorebi/src`). Nothing
here was run: counts come from `grep` and reading, and macOS cannot run on this host. The shared type
is landed as the crate `crates/komorebi-settings` (phase 2) and **no daemon uses it yet**.
Abbreviations: `W/`, `K/`, `M/` are the three `src` trees. `apply_globals` is
`StaticConfig::apply_globals` (`W/static_config.rs:396`, `K/static_config.rs:414`,
`M/static_config.rs:392`).
## 1. What the statics are
Declarations of `static`, `static ref` and `lazy_static` (thread locals and per-call counters left out):
**Windows 126, KDE 53, macOS 74.** Most are not settings. Four kinds:
| kind | examples | goes into `Settings`? |
|---|---|---|
| **config**: written by `komorebi.json` load and/or a `SocketMessage`, read by other threads | `BORDER_*`, `ANIMATION_*`, paddings, rule lists, stackbar | yes |
| **session state**: changes while running, is not configuration | `SESSION_FLOATING_APPLICATIONS`, `UNMANAGED_WINDOW_IDS`, `HIDDEN_HWNDS`, `WORKSPACE_MATCHING_RULES`, `INITIAL_CONFIGURATION_LOADED`, `CUSTOM_FFM` (command line) | no; stays where it is (core step 9) |
| **plumbing**: channels, `OnceLock<Sender>`, caches of OS handles, per-window tables | `CHANNEL`, `BORDER_STATE`, `MONITOR_CACHE`, `HWNDS_CACHE`, `PARKED_WINDOWS`, `live_resize.rs` flags | no |
| **environment**: computed once | `HOME_DIR`, `DATA_DIR`, `AHK_EXE`, `WINDOWS_11` | no |
Config-class statics by my count: **Windows 61, KDE 35, macOS 36** (these include the session-state
rule lists that sit next to them; see 2.3). What `apply_globals` itself writes (distinct statics,
counted on the function body): **Windows 51, KDE 29, macOS 28.** The plan's "19-27 written per
platform" counted scalar settings only; the 11 rule lists, the 7 per-animation/global animation
statics and the stackbar block make the totals larger.
## 2. The inventory (config class)
"Writers": `load` is `apply_globals` (runs on start-up through `preload`, and again on every
`ReloadConfiguration` through `reload`), `cmd` is a `SocketMessage` arm of `process_command.rs`,
`theme` is `theme_manager`, `thr` is another thread. "Readers" lists only threads that are not the
window-manager thread; "wm" means code that runs under the window-manager lock. Match is the name
on the other platforms (`=` same name and type, `~` same name, different default or type, `-`
absent).
### 2.1 Scalars and small values
| static | type | writers | readers | W / K / M | shared? | note |
|---|---|---|---|---|---|---|
| `BORDER_ENABLED` | AtomicBool | load, cmd `Border` | border thread (all 3), wm | = = = | shared | K reads it to decide whether borders take room in the layout (`BORDER_DRAWN` is its derived twin, K only) |
| `BORDER_WIDTH` | AtomicI32 | load, cmd | border thread, wm (offsets), stackbar (W) | ~ default 8 / 10 / 10 | shared | |
| `BORDER_OFFSET` | AtomicI32 | load, cmd | border thread, wm | ~ default -1 / 0 / 0 | shared | |
| `BORDER_RADIUS` | AtomicI32 | load | K effect, M `ns_window` | - K M | K+M | Windows draws square or via DWM (`STYLE`) |
| `BORDER_OFFSET_ADJUSTMENT` | `static i32` = 7 | nobody | M border | - - M | platform constant | stays a constant |
| `FOCUSED UNFOCUSED UNFOCUSED_LOCKED MONOCLE STACK FLOATING` | 6 x AtomicU32 | load, cmd `BorderColour`, theme | border thread | = = = | shared | `UNFOCUSED_LOCKED` is written by theme only (load never writes it); one theme change writes 6 border and (W) 3 stackbar values one by one |
| `STYLE` | AtomicCell<BorderStyle> | load, cmd | border thread, wm | W only | W platform | load resets it to default when the file has none |
| `IMPLEMENTATION` | AtomicCell<BorderImplementation> | load, cmd | border thread | W only | W platform | |
| `ANIMATION_ENABLED_GLOBAL` | Arc<AtomicBool> | load, cmd, **scoped override**, shutdown | W animation engine, M animator and lane threads, wm | = = = | shared | see 4 (override) |
| `ANIMATION_STYLE_GLOBAL` | Arc<Mutex<AnimationStyle>> | load, cmd | same | = = = | shared | |
| `ANIMATION_DURATION_GLOBAL` | Arc<AtomicU64> | load, cmd | same | = = = | shared | |
| `ANIMATION_{ENABLED,STYLE,DURATION}_PER_ANIMATION` | Arc<Mutex<HashMap<AnimationPrefix, _>>> x3 | load, cmd (2 sites for style), shutdown (enabled) | same | = = = | shared | `load` clears the map when a global value is given, replaces it when a prefix map is given |
| `ANIMATION_FPS` | AtomicU64 | load, cmd | W engine, M animator (`frame_period`) | = = = | shared | load writes the default when `animation` is present but `fps` is not |
| `GHOST_MOVEMENT_ENABLED` | AtomicBool | load | W ghost dispatcher, live resize | W only | W platform | |
| `TRANSPARENCY_ALPHA` | AtomicU8 | load, cmd | W/K transparency manager | = = - | shared (W, K) | default 200 |
| `TRANSPARENCY_ENABLED` | AtomicBool | load (`transparency`), cmd (set, toggle) | transparency manager, W process_event | = = - | shared (W, K) | a command toggles what the file set; a reload overwrites it only when the key is present |
| `DEFAULT_WORKSPACE_PADDING`, `DEFAULT_CONTAINER_PADDING` | AtomicI32 | load | wm (every layout), W stackbar | ~ default 10 / 5 / 5 | shared | load keeps the old value when the key is absent |
| `DEFAULT_RESIZE_DELTA`, `DEFAULT_MOUSE_FOLLOWS_FOCUS` | `static i32 / bool` | nobody | wm | = = = | shared constants | become part of the platform defaults |
| `FLOATING_WINDOW_TOGGLE_ASPECT_RATIO` | Arc<Mutex<AspectRatio>> | load (2 sites) | wm | = = = | shared | load writes a default when absent |
| `LAYOUT_DEFAULTS` | Arc<Mutex<HashMap<DefaultLayout, LayoutDefaultEntry>>> | load (replace or clear) | wm | = = = | shared | |
| `DISPLAY_INDEX_PREFERENCES` | Arc<RwLock<HashMap<usize, String>>> | load, cmd | monitor reconciliator thread, wm | = = = | shared | |
| `MONITOR_INDEX_PREFERENCES` | Arc<Mutex<HashMap<usize, Rect>>> | load, cmd | W windows_api, reconciliator | W only | W platform | commented out of macOS and KDE load |
| `DUPLICATE_MONITOR_SERIAL_IDS` | Arc<RwLock<Vec<String>>> | reconciliator, windows_api | windows_api | W only | **not config** | derived from the monitor list |
| `HIDING_BEHAVIOUR` | Arc<Mutex<HidingBehaviour>> | load, cmd, (state loader) | **reaper thread** (every 20 ms), wm | W only | W platform | reaper reads it without the wm lock |
| `WINDOW_HANDLING_BEHAVIOUR` | AtomicCell<WindowHandlingBehaviour> | load, **scoped override** in `border.rs:831-851` | W windows_api (every position call), ghost dispatcher, scrolling | W only | W platform | see 4 |
| `MINIMUM_WIDTH`, `MINIMUM_HEIGHT` | AtomicI32 x2 | load | wm | W only | W platform | commented out of the macOS load |
| `SLOW_APPLICATION_COMPENSATION_TIME` | AtomicU64 | nobody after init | W window | W only | W platform | effectively constant 20 |
| `REMOVE_TITLEBARS` | AtomicBool | cmd | wm, workspace | W only | W platform | runtime toggle, not in `apply_globals` |
| `STACKBAR_*` (9: font size/family, focused/unfocused text, tab background, height, width, label, mode) | AtomicI32/U32/Cell, `Mutex<Option<String>>` | load, cmd, theme (3 colours) | stackbar threads, wm | W only | W platform | load sets font size and family unconditionally (0 / None) when `stackbar.tabs` is present |
| `STACKBAR_TEMPORARILY_DISABLED` | AtomicBool | ghost dispatcher | stackbar manager | W only | session state | |
| `CURRENT_THEME` | AtomicCell<Option<Theme>> | theme thread | theme thread only | = = = | not config | read nowhere else |
| `TABBED_APPLICATIONS`, `TITLELESS_APPLICATIONS`, `DO_NOT_OBSERVE_APPLICATIONS` | Arc<Mutex<Vec<String>>> | load (first two) | M window, process_event | M only | M platform | |
### 2.2 Rule lists (the lock-ordered set)
| static | writers | readers | W / K / M |
|---|---|---|---|
| `FLOATING_APPLICATIONS`, `IGNORE_IDENTIFIERS`, `MANAGE_IDENTIFIERS` | load (all three held at once) | wm; W also `winevent` and `window_manager_event` threads through `window_is_eligible` | = = = |
| `TRANSPARENCY_BLACKLIST` | load | transparency manager | = = - (K: pub) |
| `LAYERED_WHITELIST`, `TRAY_AND_MULTI_WINDOW_IDENTIFIERS`, `OBJECT_NAME_CHANGE_ON_LAUNCH`, `OBJECT_NAME_CHANGE_TITLE_IGNORE_LIST`, `SLOW_APPLICATION_IDENTIFIERS`, `NO_TITLEBAR` | load | wm, event threads | W only |
| `REGEX_IDENTIFIERS` | load (clears), every rule check (compiles on demand) | every rule check | = = = |
| `PERMAIGNORE_CLASSES` (W also `WSL2_UI_PROCESSES`) | nobody after init | window eligibility | = = = (W: +1) |
| `SESSION_FLOATING_APPLICATIONS` | cmd (add/remove at run time) | window eligibility | = = = |
| `WORKSPACE_MATCHING_RULES` | cmd, **reload (clears, then refills after `apply_globals`)** | wm, reconciliator, M process_event | = = = |
### 2.3 Lock-order relations
The documented order (comment above the statics in `komorebi/src/lib.rs` on `wip-u-kfw`; it is the
only one: KDE and macOS declare no order and take the same lists in whatever order the code does):
```
FLOATING_APPLICATIONS, SESSION_FLOATING_APPLICATIONS, TRANSPARENCY_BLACKLIST, IGNORE_IDENTIFIERS,
MANAGE_IDENTIFIERS, LAYERED_WHITELIST, TRAY_AND_MULTI_WINDOW_IDENTIFIERS, OBJECT_NAME_CHANGE_ON_LAUNCH,
OBJECT_NAME_CHANGE_TITLE_IGNORE_LIST, SLOW_APPLICATION_IDENTIFIERS, NO_TITLEBAR,
WORKSPACE_MATCHING_RULES, REGEX_IDENTIFIERS
```
`REGEX_IDENTIFIERS` is last (every rule check holds a list and then the regexes); `PERMAIGNORE_CLASSES`
and `WSL2_UI_PROCESSES` are leaves. The window-manager lock is above all of them: `apply_globals` runs
with it held (the `reload` signature takes `&mut WindowManager`), but the event threads of Windows
(`winevent_listener` to `window_manager_event`) and the transparency manager also lock single lists
without it.
Two findings:
1. `apply_globals` on Windows takes **eleven** list locks in a fixed order and keeps them for the whole
function (`W/static_config.rs:562-572`), so the rules change atomically with respect to each other,
but not with respect to the scalars (written before and after).
2. `reload` clears `WORKSPACE_MATCHING_RULES` and refills it per monitor afterwards (`M/static_config.rs`
after `apply_globals`, same on W and K): for the duration of the loop a reader that does not hold the
wm lock sees an **empty** list. This is a live partial-update window today.
With `Settings` the immutable lists are fields of one `Arc` snapshot: readers take no lock, there is no
order to follow, and the lock order shrinks to: wm lock, then the two session mutexes
(`SESSION_FLOATING_APPLICATIONS`, the regex cache), never nested the other way round.
## 3. The design
### 3.1 Crates
New crate `crates/komorebi-settings` (std plus `parking_lot`; types from `komorebi-config`,
`komorebi-protocol` and `komorebi-layouts`; no daemon dependency, so it tests natively):
```rust
pub struct Settings<P> {
pub shared: Shared, // the same struct on all three platforms
pub platform: P, // Windows: border style, stackbar, hiding/handling behaviour, ...
} // KDE: nothing yet (the compositor owns look); macOS: tabbed/titleless apps
pub struct Shared {
pub border: Border, // enabled, width, offset, six colours
pub animation: Animation, // global + per-prefix enabled/style/duration, fps; motion_for(prefix)
pub defaults: Defaults, // container/workspace padding, resize delta, mouse-follows-focus
pub transparency: Transparency, // enabled, alpha (Windows and KDE; macOS keeps the default)
pub floating_window_aspect_ratio: AspectRatio,
pub layout_defaults: HashMap<DefaultLayout, LayoutDefaultEntry>,
pub display_index_preferences: HashMap<usize, String>,
}
pub struct Handle<P> { /* RwLock<Arc<Settings<P>>> + a writer mutex */ }
impl<P: Clone> Handle<P> {
pub fn snapshot(&self) -> Arc<Settings<P>>; // readers
pub fn replace(&self, s: Settings<P>); // whole new value
pub fn update<R>(&self, f: impl FnOnce(&mut Settings<P>) -> R) -> R; // copy, edit, swap; writers serialised
pub fn scoped<T>(&self, field: fn(&mut Settings<P>) -> &mut T, value: T) -> Scoped<'_, P, T>;
}
```
The **platform part** is a plain type parameter `P` (not a trait): each daemon names its struct
(`WindowsSettings`, `MacosSettings`, `KdeSettings`) and the handle is `static SETTINGS: LazyLock<Handle<P>>`
behind a `settings()` function of the daemon crate. `P` needs only `Clone + Debug`. A trait would add
nothing, because the shared code never calls into `P`: only the platform's own subsystems read it. When
the daemons become one crate this is `#[cfg(target_os)] type Platform = ...`.
The lists of rules and the stackbar block move into the platform parts first (they are Windows only or
differ); the rule lists join `Shared` in step G7 below once the lock order is retired.
### 3.2 How readers get a consistent snapshot
* A reader calls `settings().snapshot()` **once per unit of work** (one render pass of a border, one
`motion_for` call, one layout pass, one window eligibility check) and uses that `Arc` for the whole
unit. Cost: one uncontended `RwLock` read and an `Arc` clone; no atomics are needed on the hot
paths I found (border thread loops once per notification, animator once per frame, rule checks once per
event). If a measurement shows one needs it, add an `AtomicBool` *mirror* of that single field written by
the same `update`; do not read the mirror and the snapshot for the same decision.
* `ArcSwap` was the first idea. It is **not in any of the three lockfiles** (checked), so it would add a
dependency to three lockfiles and the flake hashes for no gain at this read rate. `Handle` hides the
representation: swapping `RwLock<Arc<_>>` for `ArcSwap<_>` later changes one file.
* Writers go through `update`, which holds a mutex while it clones, edits and publishes, so two writers
(a `SocketMessage` and a config reload, a theme change and a command) never lose each other's field. The
statics lose such updates today only when both write the same field, but a theme change writes nine
separate atomics and a command can interleave.
* A config load builds the new value and publishes it **once**; a theme change is one `update`.
### 3.3 Semantics that must survive: reload merges, it does not replace
`apply_globals` is a field-by-field merge: `if let Some(x) = self.border_width { store }`. A key absent
from a reloaded `komorebi.json` **keeps the previous value** (and a command-set value); a few fields are
instead reset (listed in section 4, "reset on reload"). A "build a fresh Settings from the file and swap"
would reset every absent key to its default. So the migration keeps the merge: `Settings::apply(delta)`
is written as the same sequence of `if let Some` over a copy of the **current** snapshot, published once.
Replacing instead of merging is a separate, optional BEHAVIOUR change, last in the list.
### 3.4 The scoped override
Two places write a setting, do work and put it back: Windows `border.rs:831-851`
(`WINDOW_HANDLING_BEHAVIOUR`: load, store `handling`, ..., store `previous`) and KDE
`process_command.rs:607-609` (`ANIMATION_ENABLED_GLOBAL.swap(false)`, `add_window_to_container`,
`store(restore)`). Both clobber a concurrent change (a reload or a command in between is overwritten with the old value). macOS
already has the right pattern: `animation::suspend()` returns a guard that counts and `motion_for` returns
`None` while it is held (`M/animation/mod.rs:36-71`). `Handle::scoped` gives the same for any field:
it sets the value and, on drop, puts the previous value back **only if the field still holds the
override value**, so a change made meanwhile wins. For animations KDE and Windows should adopt
macOS's `suspend` counter instead (a field of `Shared::animation`, not a scoped value).
## 4. Statics whose behaviour would change
Everything here is a **BEHAVIOUR** change if it is not made to keep today's semantics. Flagged items
say what the migration does about each.
**Partial updates visible mid-reload (today).** `apply_globals` publishes about 28 to 51 values
one at a time while the border, animator, reaper and transparency threads keep reading. A border
thread can draw once with the new width and the old colours; `motion_for` (macOS) reads enabled,
duration and style as three separate lock/atomic reads and can mix two configurations. With `Handle` all
readers see the old or the new `Settings`, never a mix: **BEHAVIOUR (intended, can only remove
tears)**, on every static of the table. The e2e that would show it is `config_reload`.
**Reset on reload (must be kept as written):** `LAYOUT_DEFAULTS` (cleared when absent),
`FLOATING_WINDOW_TOGGLE_ASPECT_RATIO` (default when absent), `ANIMATION_FPS` (default when `animation` is present
without `fps`), `GHOST_MOVEMENT_ENABLED` (W), `STYLE` and `IMPLEMENTATION` (W: default when absent),
`STACKBAR_FONT_SIZE` / `STACKBAR_FONT_FAMILY` (W: 0 / None when `stackbar.tabs` is present),
`ANIMATION_*_PER_ANIMATION` (cleared when the global form is used) and the rule lists (`apply_globals`
clears each only when the new file gives that list; check per list in G7).
**Keeps the old value when absent (must be kept):** borders (width, offset, radius, enabled, colours),
paddings, transparency alpha, hiding and handling behaviour, minimum size, stackbar scalars, display index
preferences.
**Scoped override (3.4):** KDE `ANIMATION_ENABLED_GLOBAL.swap/store` pair and Windows `WINDOW_HANDLING_BEHAVIOUR`
pair become guards. A concurrent reload no longer gets clobbered: BEHAVIOUR.
**Shutdown writes** (`main.rs:367-368`, `window_manager.rs:1445-1446` on W; the Stop handler on K and M): they
disable animation permanently before restoring windows. These are plain `update`s, not scoped.
**`WORKSPACE_MATCHING_RULES` empty window** (2.3): stays in the core until step 9 (session state); not part
of `Settings`, so not changed here.
**Defaults differ per platform** (border width 8/10/10, offset -1/0/0, padding 10/5/5, KDE border radius):
`Shared::defaults_for(PlatformDefaults::{WINDOWS, KDE, MACOS})`. Changing any default is user visible and is
not part of this step.
**Threads read the statics without the wm lock:** border thread (all three), W reaper
(`HIDING_BEHAVIOUR`), W winevent and window_manager_event threads (rule lists), W transparency manager,
macOS animator thread and the per-pid lane threads, macOS border thread. These are the readers whose behaviour a snapshot
changes (from "latest value on each read" to "value of the snapshot taken at the start of the unit"); the
gap is at most one unit of work.
## 5. Migration order (least to most risky)
Each group is one revision per platform tree (or per platform file set), moving the writers and **all**
readers of that group together, never dual-writing a static and a field. P = pure refactor (same
observable behaviour); B = BEHAVIOUR (marked in the revision body, not landed until its gate ran).
| step | group | kind | gate |
|---|---|---|---|
| S0 | the crate and its tests (landed, phase 2) | P | native `cargo test -p komorebi-settings` |
| S1 | `DEFAULT_RESIZE_DELTA`, `DEFAULT_MOUSE_FOLLOWS_FOCUS`, `BORDER_OFFSET_ADJUSTMENT`: constants into `PlatformDefaults` | P | KDE `cargo test -p komorebi --lib`, Windows clippy, macOS fake-CC check |
| S2 | install `settings()` in each daemon with the platform defaults (no reader yet) | P | same |
| S3 | paddings, aspect ratio, layout defaults, display index preferences (wm-lock readers and the reconciliator) | P for the readers; the load writes the new place once: B (tear) | KDE lib tests; Windows `config_reload`, `layouts`, `workspaces` e2e; macOS: reload with changed padding |
| S4 | transparency enabled and alpha (W, K) | P | Windows `config_reload` plus the transparency scenario if it exists; KDE lib tests |
| S5 | animation (global, per-prefix, fps): `motion_for`, engine, animator; KDE/Windows adopt `suspend()` guards | **B** (tear removed, override semantics) | KDE VM suites that stack windows (`stacks`); Windows animation e2e; **macOS hand-check**: animation on, switch workspace, change `animation.duration` by `komorebic` mid-animation, stack a window |
| S6 | border scalars and colours, theme colours as one `update` | **B** (the 6 colour atomics change together) | Windows border e2e (screenshot) and `config_reload`; KDE VM `borders`/look suites; **macOS hand-check**: `border-colour`, `border-width`, theme change, reload with changed border |
| S7 | Windows platform part: `HIDING_BEHAVIOUR`, `WINDOW_HANDLING_BEHAVIOUR` (with `scoped`), min size, stackbar block, border style/implementation, ghost movement | **B** (reaper reads a snapshot; override guard) | Windows: `commands`, `config_reload`, `lifecycle`, `lifecycle_hidden`, stackbars, border e2e |
| S8 | rule lists into `Shared` (immutable `Arc<RuleSet>`), session floating and regex cache stay mutexes; retire the lock-order comment | **B** (hot path of every window event; deadlock class removed) | Windows `config_reload` (lock order), `commands`, `focus`, `lifecycle`; KDE lib tests plus VM suites; macOS: manage/ignore rules by hand |
| S9 | `apply_globals` as one `update` (single publish), and one decision on merge versus replace | **B** | Windows `config_reload`; macOS reload by hand |
Risk grows with the number of threads that read without the wm lock and with how hot the read is:
S3 has none, S5 and S6 have the macOS threads the plan names, S7 has the reaper, S8 is every event.
Do S8 only after the Windows lock-order e2e from the `wip-u-kfw` stack has run once on the current
tree.
## 6. What is not in `Settings`
`SESSION_FLOATING_APPLICATIONS`, `UNMANAGED_WINDOW_IDS`, `HIDDEN_HWNDS`/`HIDDEN_WINDOWS`,
`WORKSPACE_MATCHING_RULES`, `REMOVE_TITLEBARS`, `CUSTOM_FFM`,
`INITIAL_CONFIGURATION_LOADED`, subscriptions, `*_CHANNEL`s, caches, `BORDER_STATE`: these are session or
plumbing state and belong to the model (step 9) or the subsystem. `Settings` is only what the user
configures.
## 7. What landed (phase 2) and findings from writing it
`crates/komorebi-settings` (native tests, 18 passed): `Settings<P>`, `Handle<P>` (`snapshot`, `replace`,
`update`, `scoped`), `Shared` (border, animation with `motion_for`, paddings and constants, transparency,
aspect ratio, layout defaults, display index preferences), `PlatformDefaults::{WINDOWS, KDE, MACOS}` and
`SharedDelta::apply`, which is `apply_globals` for the shared keys written as a function of the current
value. Not yet in the crate: rule lists, theme application as one update, anything platform specific.
Findings that only showed up while encoding the semantics:
* `FLOATING_WINDOW_TOGGLE_ASPECT_RATIO` starts at **Widescreen** on all three platforms, but a load
whose file has no `floating_window_aspect_ratio` writes **Standard** (`unwrap_or`). So the first
reload of a file without the key changes the ratio. `SharedDelta::apply` keeps this; fixing it is a
separate user-visible change.
* `TRANSPARENCY_ENABLED` is written by the load (`transparency`) and by the toggle command, so it is a
setting, not session state (corrected from the first draft of this document's table).
* `komorebi-config` warns about an unused import (`crate::defaults`, `static_config.rs:39`) when it is
built with `default-features = false`; the settings crate builds it that way. Not touched.
* Windows `reload` runs `apply_globals` twice per reload: in `preload` of the new window manager
(while the old one is paused) and again in `reload`; both writes become visible to the threads that
read without the wm lock. One `update` per load keeps the two visible publishes.
## 8. S1 and S2 as landed for KDE and macOS, and the Windows patch (not applied)
KDE and macOS (`wip-u-settings`): S1 (the `DEFAULT_*` statics take their values from
`komorebi_settings::shared`, paddings from `PlatformDefaults::{KDE, MACOS}`; the statics keep their
names and types) and S2 (`src/settings.rs` with `KdeSettings` / `MacosSettings` and `settings()`;
a test pins that the starting settings equal the starting values of the border and default
statics). The new crate is a path dependency of both workspaces (manifest, package manifest,
`Cargo.lock` +1 package). S3 was **not** started: it moves the writers (`apply_globals`) and the
readers in `monitor.rs`, `workspace.rs`, `window.rs`, `process_command.rs`, which the KDE and macOS
stacks are editing, and the load would publish once instead of storing key by key, which is the tear
removal marked BEHAVIOUR in section 5. It needs the rebase onto the merged stacks and its gate.
Windows S1/S2, to apply when told (all in the files kfw is changing, so not done):
1. `komorebi/Cargo.toml`: `komorebi-settings = { path = "../crates/komorebi-settings" }` (the crate is
already a root workspace member; `Cargo.lock` gets the dependency edge, no new package).
2. `komorebi/src/lib.rs`: `DEFAULT_WORKSPACE_PADDING` / `DEFAULT_CONTAINER_PADDING` start from
`PlatformDefaults::WINDOWS.workspace_padding` / `.container_padding` (10, 10);
`DEFAULT_RESIZE_DELTA` and `DEFAULT_MOUSE_FOLLOWS_FOCUS` are `komorebi_settings::shared::...`
(50, true); add `pub mod settings;`.
3. New `komorebi/src/settings.rs`, a copy of the KDE one with `WindowsSettings` (empty) and
`PlatformDefaults::WINDOWS`; its pin test compares the paddings, resize delta, mouse-follows-focus,
`BORDER_ENABLED`, `BORDER_WIDTH` (8), `BORDER_OFFSET` (-1) and the six colours, and leaves out
`border.radius` (Windows has no `BORDER_RADIUS`).
4. Gate: Windows clippy (`-D warnings`) and `cargo test -p komorebi --lib`; `tools/e2e-needed` will
say YES (the crate is in the closure of `komorebi.exe`) so one e2e run is owed, but behaviour is
unchanged.
+1
View File
@@ -23,6 +23,7 @@ use crate::widgets::komorebi::Komorebi;
use crate::widgets::komorebi::MonitorInfo;
use crate::widgets::widget::BarWidget;
use crate::widgets::widget::WidgetConfig;
use crate::widgets::widget::WidgetConfigExt;
use color_eyre::eyre;
use crossbeam_channel::Receiver;
use crossbeam_channel::TryRecvError;
+1 -590
View File
@@ -1,590 +1 @@
use crate::AUTO_SELECT_FILL_COLOUR;
use crate::AUTO_SELECT_TEXT_COLOUR;
use crate::DEFAULT_PADDING;
use crate::bar::exec_powershell;
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 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<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"),
};
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::*;
+6 -4
View File
@@ -106,10 +106,12 @@ fn main() -> color_eyre::Result<()> {
#[cfg(feature = "schemars")]
if opts.schema {
let bar_config = schemars::schema_for!(KomobarConfig);
let schema = serde_json::to_string_pretty(&bar_config)?;
println!("{schema}");
print!(
"{}",
komorebi_bar_core::schema::generated_text::<KomobarConfig>(
komorebi_bar_core::schema::SchemaPlatform::Windows
)
);
std::process::exit(0);
}
+54
View File
@@ -1,8 +1,13 @@
use crate::widgets::ImageIcon;
use egui_phosphor::regular::CALENDAR_DOTS;
use komorebi_bar_core::platform::BarPlatform;
use komorebi_bar_core::platform::SystemTool;
use std::borrow::Cow;
use komorebi_client::SocketMessage;
use std::io;
use std::path::Path;
use std::process::Command;
use which::which;
pub struct WindowsBar;
@@ -15,6 +20,13 @@ impl BarPlatform for WindowsBar {
.spawn(),
SystemTool::NetworkSettings => Command::new("cmd.exe").args(["/C", "ncpa"]).spawn(),
SystemTool::Url(url) => Command::new("explorer.exe").args([url]).spawn(),
SystemTool::StorageLocation(label) => Command::new("cmd.exe")
.args([
"/C",
"explorer.exe",
label.split(' ').collect::<Vec<&str>>()[0],
])
.spawn(),
}
.map(|_| ())
}
@@ -26,4 +38,46 @@ impl BarPlatform for WindowsBar {
fn calendar_icon(&self) -> &'static str {
CALENDAR_DOTS
}
fn exec_shell_command(&self, command: &str) -> color_eyre::Result<()> {
crate::bar::exec_powershell(command)
}
fn send_komorebi_message(&self, message: &SocketMessage) -> io::Result<()> {
komorebi_client::send_message(message)
}
fn send_komorebi_batch(&self, messages: Vec<SocketMessage>) -> io::Result<()> {
komorebi_client::send_batch(messages)
}
fn command_icon(&self, command: &str) -> Option<ImageIcon> {
let path = executable_of(command)?;
ImageIcon::try_load(path.as_ref(), || {
let path_str = path.to_str()?;
windows_icons::get_icon_by_path(path_str)
.or_else(|| windows_icons_fallback::get_icon_by_path(path_str))
})
}
}
/// Attempts to resolve the executable path from the command string.
///
/// Resolution logic:
/// - Splits the command by ".exe" and checks if the first part is an existing file.
/// - If not, attempts to locate the binary using [`which`] on this name.
/// - If still unresolved, takes the first word (separated by whitespace) and attempts
/// to find it in the system `PATH` using [`which`].
///
/// Returns `None` if no executable path can be determined.
fn executable_of(command: &str) -> Option<Cow<'_, Path>> {
if let Some(binary) = command.split(".exe").next().map(Path::new) {
if binary.is_file() {
return Some(Cow::Borrowed(binary));
} else if let Ok(binary) = which(binary) {
return Some(Cow::Owned(binary));
}
}
which(command.split(' ').next()?).ok().map(Cow::Owned)
}
+1 -15
View File
@@ -1,4 +1,5 @@
use crate::config::LabelPrefix;
pub use komorebi_bar_core::widgets::platform_config::KeyboardConfig;
use crate::render::RenderConfig;
use crate::widgets::widget::BarWidget;
use color_eyre::eyre;
@@ -9,8 +10,6 @@ use eframe::egui::TextFormat;
use eframe::egui::Ui;
use eframe::egui::WidgetText;
use eframe::egui::text::LayoutJob;
use serde::Deserialize;
use serde::Serialize;
use std::sync::Arc;
use std::time::Duration;
use std::time::Instant;
@@ -23,19 +22,6 @@ use windows::Win32::UI::WindowsAndMessaging::GetWindowThreadProcessId;
const ERROR_TEXT: &str = "Error";
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Keyboard widget configuration
pub struct KeyboardConfig {
/// Enable the Input widget
pub enable: bool,
/// Data refresh interval
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10)))]
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
}
impl From<KeyboardConfig> for Keyboard {
fn from(value: KeyboardConfig) -> Self {
let data_refresh_interval = value.data_refresh_interval.unwrap_or(10);
+9 -94
View File
@@ -7,7 +7,7 @@ use crate::config::WorkspacesDisplayFormat;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::ui::CustomUi;
use crate::widgets::komorebi_layout::KomorebiLayout;
use crate::widgets::komorebi_layout::KomorebiLayoutExt;
use crate::widgets::widget::BarWidget;
use eframe::egui::Align;
use eframe::egui::Color32;
@@ -28,6 +28,14 @@ use eframe::egui::Ui;
use eframe::egui::Vec2;
use eframe::egui::text::LayoutJob;
use eframe::egui::vec2;
pub use komorebi_bar_core::widgets::komorebi_config::KomorebiConfig;
pub use komorebi_bar_core::widgets::komorebi_config::KomorebiConfigurationSwitcherConfig;
pub use komorebi_bar_core::widgets::komorebi_config::KomorebiFocusedContainerConfig;
pub use komorebi_bar_core::widgets::komorebi_config::KomorebiLayout;
pub use komorebi_bar_core::widgets::komorebi_config::KomorebiLayoutConfig;
pub use komorebi_bar_core::widgets::komorebi_config::KomorebiLockedContainerConfig;
pub use komorebi_bar_core::widgets::komorebi_config::KomorebiWorkspaceLayerConfig;
pub use komorebi_bar_core::widgets::komorebi_config::KomorebiWorkspacesConfig;
use komorebi_client::Container;
use komorebi_client::PathExt;
use komorebi_client::Rect;
@@ -37,106 +45,13 @@ use komorebi_client::State;
use komorebi_client::Window;
use komorebi_client::Workspace;
use komorebi_client::WorkspaceLayer;
use serde::Deserialize;
use serde::Serialize;
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::io::Result as IoResult;
use std::path::Path;
use std::rc::Rc;
use std::sync::atomic::Ordering;
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Komorebi widget configuration
pub struct KomorebiConfig {
/// Configure the Workspaces widget
pub workspaces: Option<KomorebiWorkspacesConfig>,
/// Configure the Layout widget
pub layout: Option<KomorebiLayoutConfig>,
/// Configure the Workspace Layer widget
pub workspace_layer: Option<KomorebiWorkspaceLayerConfig>,
/// Configure the Focused Container widget
#[serde(alias = "focused_window")]
pub focused_container: Option<KomorebiFocusedContainerConfig>,
/// Configure the Locked Container widget
pub locked_container: Option<KomorebiLockedContainerConfig>,
/// Configure the Configuration Switcher widget
pub configuration_switcher: Option<KomorebiConfigurationSwitcherConfig>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Komorebi widget workspaces configuration
pub struct KomorebiWorkspacesConfig {
/// Enable the Komorebi Workspaces widget
pub enable: bool,
/// Hide workspaces without any windows
pub hide_empty_workspaces: bool,
/// Display format of the workspace
pub display: Option<WorkspacesDisplayFormat>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Komorebi widget layout configuration
pub struct KomorebiLayoutConfig {
/// Enable the Komorebi Layout widget
pub enable: bool,
/// List of layout options
pub options: Option<Vec<KomorebiLayout>>,
/// Display format of the current layout
pub display: Option<DisplayFormat>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Komorebi widget workspace layer configuration
pub struct KomorebiWorkspaceLayerConfig {
/// Enable the Komorebi Workspace Layer widget
pub enable: bool,
/// Display format of the current layer
pub display: Option<DisplayFormat>,
/// Show the widget event if the layer is Tiling
pub show_when_tiling: Option<bool>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Komorebi widget focused container configuration
pub struct KomorebiFocusedContainerConfig {
/// Enable the Komorebi Focused Container widget
pub enable: bool,
/// DEPRECATED: use `display` instead (Show the icon of the currently focused container)
#[deprecated(note = "Use `display` instead")]
pub show_icon: Option<bool>,
/// Display format of the currently focused container
pub display: Option<DisplayFormat>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Komorebi widget locked container configuration
pub struct KomorebiLockedContainerConfig {
/// Enable the Komorebi Locked Container widget
pub enable: bool,
/// Display format of the current locked state
pub display: Option<DisplayFormat>,
/// Show the widget event if the layer is unlocked
pub show_when_unlocked: Option<bool>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Komorebi widget configuration switcher configuration
pub struct KomorebiConfigurationSwitcherConfig {
/// Enable the Komorebi Configurations widget
pub enable: bool,
/// A map of display friendly name => path to configuration.json
pub configurations: BTreeMap<String, String>,
}
impl From<&KomorebiConfig> for Komorebi {
fn from(cfg: &KomorebiConfig) -> Self {
let configuration_switcher = cfg.configuration_switcher.clone().map(|mut cs| {
+1 -12
View File
@@ -1,6 +1,7 @@
use crate::MAX_LABEL_WIDTH;
use crate::bar::Alignment;
use crate::config::MediaDisplayFormat;
pub use komorebi_bar_core::widgets::platform_config::MediaConfig;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::ui::CustomUi;
@@ -12,22 +13,10 @@ use eframe::egui::TextFormat;
use eframe::egui::Ui;
use eframe::egui::Vec2;
use eframe::egui::text::LayoutJob;
use serde::Deserialize;
use serde::Serialize;
use std::sync::atomic::Ordering;
use windows::Media::Control::GlobalSystemMediaTransportControlsSessionManager;
use windows::Media::Control::GlobalSystemMediaTransportControlsSessionPlaybackStatus;
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Media widget configuration
pub struct MediaConfig {
/// Enable the Media widget
pub enable: bool,
/// Display format of the media widget (defaults to IconAndText)
pub display: Option<MediaDisplayFormat>,
}
impl From<MediaConfig> for Media {
fn from(value: MediaConfig) -> Self {
Self::new(
+8 -169
View File
@@ -1,183 +1,22 @@
use eframe::egui::ColorImage;
use eframe::egui::Context;
use eframe::egui::TextureHandle;
use eframe::egui::TextureOptions;
use image::RgbaImage;
use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
use std::sync::LazyLock;
use std::sync::RwLock;
pub mod applications;
pub use komorebi_bar_core::widgets::applications;
pub use komorebi_bar_core::widgets::battery;
pub use komorebi_bar_core::widgets::cpu;
pub use komorebi_bar_core::widgets::date;
pub mod keyboard;
pub mod komorebi;
mod komorebi_layout;
pub use komorebi_bar_core::widgets::komorebi_layout;
pub mod media;
pub use komorebi_bar_core::widgets::memory;
pub mod network;
pub mod storage;
pub use komorebi_bar_core::widgets::storage;
#[cfg(target_os = "windows")]
pub mod systray;
pub mod time;
pub use komorebi_bar_core::widgets::time;
pub use komorebi_bar_core::widgets::update;
pub mod widget;
/// Global cache for icon images and their associated GPU textures.
pub static ICONS_CACHE: IconsCache = IconsCache::new();
/// In-memory cache for icon images and their associated GPU textures.
///
/// Stores raw [`ColorImage`]s and [`TextureHandle`]s keyed by [`ImageIconId`].
/// Texture entries are context-dependent and automatically invalidated when the [`Context`] changes.
#[allow(clippy::type_complexity)]
pub struct IconsCache {
textures: LazyLock<RwLock<(Option<Context>, HashMap<ImageIconId, TextureHandle>)>>,
images: LazyLock<RwLock<HashMap<ImageIconId, Arc<ColorImage>>>>,
}
impl IconsCache {
/// Creates a new empty IconsCache instance.
#[inline]
pub const fn new() -> Self {
Self {
textures: LazyLock::new(|| RwLock::new((None, HashMap::new()))),
images: LazyLock::new(|| RwLock::new(HashMap::new())),
}
}
/// Retrieves or creates a texture handle for the given icon ID and image.
///
/// If a texture for the given ID already exists for the current [`Context`], it is reused.
/// Otherwise, a new texture is created, inserted into the cache, and returned.
/// The cache is reset if the [`Context`] has changed.
#[inline]
pub fn texture(&self, ctx: &Context, id: &ImageIconId, img: &Arc<ColorImage>) -> TextureHandle {
if let Some(texture) = self.get_texture(ctx, id) {
return texture;
}
let texture_handle = ctx.load_texture("icon", img.clone(), TextureOptions::default());
self.insert_texture(ctx, id.clone(), texture_handle.clone());
texture_handle
}
/// Returns the cached texture for the given icon ID if it exists and matches the current [`Context`].
pub fn get_texture(&self, ctx: &Context, id: &ImageIconId) -> Option<TextureHandle> {
let textures_lock = self.textures.read().unwrap();
if textures_lock.0.as_ref() == Some(ctx) {
return textures_lock.1.get(id).cloned();
}
None
}
/// Inserts a texture handle, resetting the cache if the [`Context`] has changed.
pub fn insert_texture(&self, ctx: &Context, id: ImageIconId, texture: TextureHandle) {
let mut textures_lock = self.textures.write().unwrap();
if textures_lock.0.as_ref() != Some(ctx) {
textures_lock.0 = Some(ctx.clone());
textures_lock.1.clear();
}
textures_lock.1.insert(id, texture);
}
/// Returns the cached image for the given icon ID, if available.
pub fn get_image(&self, id: &ImageIconId) -> Option<Arc<ColorImage>> {
self.images.read().unwrap().get(id).cloned()
}
/// Caches a raw [`ColorImage`] associated with the given icon ID.
pub fn insert_image(&self, id: ImageIconId, image: Arc<ColorImage>) {
self.images.write().unwrap().insert(id, image);
}
/// Removes the cached image and texture for the given icon ID.
pub fn remove(&self, id: &ImageIconId) {
self.images.write().unwrap().remove(id);
self.textures.write().unwrap().1.remove(id);
}
}
#[inline]
pub(crate) fn rgba_to_color_image(rgba_image: &RgbaImage) -> ColorImage {
let size = [rgba_image.width() as usize, rgba_image.height() as usize];
let pixels = rgba_image.as_flat_samples();
ColorImage::from_rgba_unmultiplied(size, pixels.as_slice())
}
/// Represents an image-based icon with a unique ID and pixel data.
#[derive(Clone, Debug)]
pub struct ImageIcon {
/// Unique identifier for the image icon, used for texture caching.
pub id: ImageIconId,
/// Shared pixel data of the icon in `ColorImage` format.
pub image: Arc<ColorImage>,
}
impl ImageIcon {
/// Creates a new [`ImageIcon`] from the given ID and image data.
#[inline]
pub fn new(id: ImageIconId, image: Arc<ColorImage>) -> Self {
Self { id, image }
}
/// Loads an [`ImageIcon`] from [`ICONS_CACHE`] or calls `loader` if not cached.
/// The loaded image is converted to a [`ColorImage`], cached, and returned.
#[inline]
pub fn try_load<F, I>(id: impl Into<ImageIconId>, loader: F) -> Option<Self>
where
F: FnOnce() -> Option<I>,
I: Into<RgbaImage>,
{
let id = id.into();
let image = ICONS_CACHE.get_image(&id).or_else(|| {
let img = loader()?;
let img = Arc::new(rgba_to_color_image(&img.into()));
ICONS_CACHE.insert_image(id.clone(), img.clone());
Some(img)
})?;
Some(ImageIcon::new(id, image))
}
/// Returns a texture handle for the icon, using the given [`Context`].
///
/// If the texture is already cached in [`ICONS_CACHE`], it is reused.
/// Otherwise, a new texture is created from the [`ColorImage`] and cached.
#[inline]
pub fn texture(&self, ctx: &Context) -> TextureHandle {
ICONS_CACHE.texture(ctx, &self.id, &self.image)
}
}
/// Unique identifier for an image-based icon.
///
/// Used to distinguish cached images and textures by either a file path
/// or a Windows window handle.
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum ImageIconId {
/// Identifier based on a file system path.
Path(Arc<Path>),
/// Windows HWND handle.
Hwnd(isize),
/// System tray icon identifier.
SystrayIcon(String),
}
impl From<&Path> for ImageIconId {
#[inline]
fn from(value: &Path) -> Self {
Self::Path(value.into())
}
}
impl From<isize> for ImageIconId {
#[inline]
fn from(value: isize) -> Self {
Self::Hwnd(value)
}
}
pub use komorebi_bar_core::widgets::icons::ICONS_CACHE;
pub use komorebi_bar_core::widgets::icons::ImageIcon;
pub use komorebi_bar_core::widgets::icons::ImageIconId;
pub use komorebi_bar_core::widgets::icons::rgba_to_color_image;
+4 -62
View File
@@ -1,4 +1,8 @@
use crate::bar::Alignment;
pub use komorebi_bar_core::widgets::network_config::DefaultInterfaceConfig;
pub use komorebi_bar_core::widgets::network_config::InterfaceDisplayType;
pub use komorebi_bar_core::widgets::network_config::NetworkConfig;
pub use komorebi_bar_core::widgets::network_config::NetworkSelectConfig;
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
@@ -11,8 +15,6 @@ use eframe::egui::TextFormat;
use eframe::egui::Ui;
use eframe::egui::text::LayoutJob;
use num_derive::FromPrimitive;
use serde::Deserialize;
use serde::Serialize;
use std::fmt;
use std::process::Command;
use std::sync::Arc;
@@ -24,66 +26,6 @@ use std::time::Duration;
use std::time::Instant;
use sysinfo::Networks;
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Network widget configuration
pub struct NetworkConfig {
/// Enable the Network widget
pub enable: bool,
/// Show total received and transmitted activity
#[serde(alias = "show_total_data_transmitted")]
pub show_total_activity: bool,
/// Show received and transmitted activity
#[serde(alias = "show_network_activity")]
pub show_activity: bool,
/// Show default interface
pub show_default_interface: Option<bool>,
/// Default interface options: whether to show the friendly name, IPv4 or IPv6 address
pub default_interface_options: Option<DefaultInterfaceConfig>,
/// Characters to reserve for received and transmitted activity
#[serde(alias = "network_activity_fill_characters")]
pub activity_left_padding: Option<usize>,
/// Data refresh interval in seconds
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10)))]
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
/// Select when the value is over a limit (1MiB is 1048576 bytes (1024*1024))
pub auto_select: Option<NetworkSelectConfig>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Network select configuration
pub struct NetworkSelectConfig {
/// Select the total received data when it's over this value
pub total_received_over: Option<u64>,
/// Select the total transmitted data when it's over this value
pub total_transmitted_over: Option<u64>,
/// Select the received data when it's over this value
pub received_over: Option<u64>,
/// Select the transmitted data when it's over this value
pub transmitted_over: Option<u64>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Which IP address to show for the default interface
pub enum InterfaceDisplayType {
#[default]
FriendlyName,
Ipv4,
Ipv6,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Default interface configuration
pub struct DefaultInterfaceConfig {
#[serde(default)]
pub display_type: InterfaceDisplayType,
}
impl From<NetworkConfig> for Network {
fn from(value: NetworkConfig) -> Self {
let default_refresh_interval = 10;
+5 -101
View File
@@ -26,10 +26,13 @@ use eframe::egui::ViewportId;
use eframe::egui::Window as EguiWindow;
use egui_extras::Column;
use egui_extras::TableBuilder;
pub use komorebi_bar_core::widgets::platform_config::ButtonPosition;
pub use komorebi_bar_core::widgets::platform_config::FieldMatch;
pub use komorebi_bar_core::widgets::platform_config::HiddenIconRule;
pub use komorebi_bar_core::widgets::platform_config::OverflowTogglePosition;
pub use komorebi_bar_core::widgets::platform_config::SystrayConfig;
use komorebi_client::MatchingStrategy;
use parking_lot::Mutex;
use serde::Deserialize;
use serde::Serialize;
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::LazyLock;
@@ -61,105 +64,6 @@ static SHOW_HIDDEN_ICONS: AtomicBool = AtomicBool::new(false);
/// Whether the systray info panel is currently shown
static SHOW_SYSTRAY_INFO: AtomicBool = AtomicBool::new(false);
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Position of the overflow toggle button
pub enum OverflowTogglePosition {
/// Toggle button appears on the left side (before visible icons)
Left,
/// Toggle button appears on the right side (after visible icons)
#[default]
Right,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Where to place a systray button (refresh, info, shortcuts, etc.)
pub enum ButtonPosition {
/// Show in the main visible area
Visible,
/// Show in the overflow/hidden section
Overflow,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)]
/// A field value with an optional matching strategy.
///
/// A plain string uses exact (case-insensitive) matching.
/// An object with `value` and `matching_strategy` uses the specified strategy.
pub enum FieldMatch {
/// Exact case-insensitive match
Exact(String),
/// Match using a specific strategy
WithStrategy {
/// The value to match against
value: String,
/// How to match (Equals, StartsWith, EndsWith, Contains, Regex, etc.)
matching_strategy: MatchingStrategy,
},
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(untagged)]
/// Rule for matching a systray icon to hide
///
/// A plain string matches the exe name (backward compatible).
/// An object with optional `exe`, `tooltip`, and/or `guid` fields
/// uses AND logic: all specified fields must match.
/// Each field can be a plain string (exact match) or an object with
/// `value` and `matching_strategy` for advanced matching.
pub enum HiddenIconRule {
/// Match by exe name (case-insensitive, exact)
Exe(String),
/// Match by one or more properties (all specified fields must match)
Match {
/// Exe name to match
exe: Option<FieldMatch>,
/// Tooltip text to match
tooltip: Option<FieldMatch>,
/// Icon GUID to match (most stable identifier across restarts)
guid: Option<FieldMatch>,
},
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// System tray widget configuration
pub struct SystrayConfig {
/// Enable the System Tray widget
pub enable: bool,
/// A list of rules for icons to hide from the system tray
///
/// Each entry can be a plain string (matches exe name, case-insensitive)
/// or an object with optional `exe`, `tooltip`, and/or `guid` fields
/// (all specified fields must match, AND logic, case-insensitive).
///
/// Run komorebi-bar with RUST_LOG=info to see the properties of all
/// systray icons in the log output.
pub hidden_icons: Option<Vec<HiddenIconRule>>,
/// Position of the overflow toggle button (default: Right)
pub overflow_toggle_position: Option<OverflowTogglePosition>,
/// Show an info button that opens a floating panel listing all systray icons
/// with their exe, tooltip, and GUID. Set to "Visible" to show it in the main
/// area, or "Overflow" to show it in the hidden/overflow section.
pub info_button: Option<ButtonPosition>,
/// Interval in seconds to automatically check for and remove stale icons
/// whose owning process has exited. Clamped between 30 and 600 seconds.
/// Defaults to 60. Set to 0 to disable.
pub stale_icons_check_interval: Option<u64>,
/// Show a refresh button that manually triggers stale icon cleanup.
/// Set to "Visible" to show it in the main area, or "Overflow" to
/// show it in the hidden/overflow section.
pub refresh_button: Option<ButtonPosition>,
/// Show a button that toggles komorebi-shortcuts (kills the process if
/// running, starts it otherwise). Set to "Visible" to show it in the main
/// area, or "Overflow" to show it in the hidden/overflow section.
pub shortcuts_button: Option<ButtonPosition>,
}
/// Command sent from UI to background thread
#[derive(Debug)]
enum SystrayCommand {
+6 -91
View File
@@ -1,84 +1,27 @@
use crate::widgets::applications::Applications;
use crate::widgets::applications::ApplicationsConfig;
use crate::widgets::battery::Battery;
use crate::widgets::battery::BatteryConfig;
use crate::widgets::cpu::Cpu;
use crate::widgets::cpu::CpuConfig;
use crate::widgets::date::Date;
use crate::widgets::date::DateConfig;
use crate::widgets::keyboard::Keyboard;
use crate::widgets::keyboard::KeyboardConfig;
use crate::widgets::komorebi::Komorebi;
use crate::widgets::komorebi::KomorebiConfig;
use crate::widgets::media::Media;
use crate::widgets::media::MediaConfig;
use crate::widgets::memory::Memory;
use crate::widgets::memory::MemoryConfig;
use crate::widgets::network::Network;
use crate::widgets::network::NetworkConfig;
use crate::widgets::storage::Storage;
use crate::widgets::storage::StorageConfig;
#[cfg(target_os = "windows")]
use crate::widgets::systray::Systray;
#[cfg(target_os = "windows")]
use crate::widgets::systray::SystrayConfig;
use crate::widgets::time::Time;
use crate::widgets::time::TimeConfig;
use crate::widgets::update::Update;
use crate::widgets::update::UpdateConfig;
use serde::Deserialize;
use serde::Serialize;
pub use komorebi_bar_core::widgets::widget::BarWidget;
pub use komorebi_bar_core::widgets::widget::WidgetConfig;
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Widget configuration
pub enum WidgetConfig {
/// Applications widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Applications"))]
Applications(ApplicationsConfig),
/// Battery widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Battery"))]
Battery(BatteryConfig),
/// CPU widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Cpu"))]
Cpu(CpuConfig),
/// Date widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Date"))]
Date(DateConfig),
/// Keyboard widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Keyboard"))]
Keyboard(KeyboardConfig),
/// Komorebi widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Komorebi"))]
Komorebi(KomorebiConfig),
/// Media widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Media"))]
Media(MediaConfig),
/// Memory widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Memory"))]
Memory(MemoryConfig),
/// Network widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Network"))]
Network(NetworkConfig),
/// Storage widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Storage"))]
Storage(StorageConfig),
/// System Tray widget configuration (Windows only)
#[cfg(target_os = "windows")]
#[cfg_attr(feature = "schemars", schemars(title = "Systray"))]
Systray(SystrayConfig),
/// Time widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Time"))]
Time(TimeConfig),
/// Update widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Update"))]
Update(UpdateConfig),
/// Turns a widget configuration into the widget that renders it.
pub trait WidgetConfigExt {
fn as_boxed_bar_widget(&self) -> Box<dyn BarWidget>;
}
impl WidgetConfig {
pub fn as_boxed_bar_widget(&self) -> Box<dyn BarWidget> {
impl WidgetConfigExt for WidgetConfig {
fn as_boxed_bar_widget(&self) -> Box<dyn BarWidget> {
match self {
WidgetConfig::Applications(config) => Box::new(Applications::from(config)),
WidgetConfig::Battery(config) => Box::new(Battery::from(*config)),
@@ -90,37 +33,9 @@ impl WidgetConfig {
WidgetConfig::Memory(config) => Box::new(Memory::from(*config)),
WidgetConfig::Network(config) => Box::new(Network::from(*config)),
WidgetConfig::Storage(config) => Box::new(Storage::from(*config)),
#[cfg(target_os = "windows")]
WidgetConfig::Systray(config) => Box::new(Systray::from(config)),
WidgetConfig::Time(config) => Box::new(Time::from(config.clone())),
WidgetConfig::Update(config) => Box::new(Update::from(*config)),
}
}
pub fn enabled(&self) -> bool {
match self {
WidgetConfig::Applications(config) => config.enable,
WidgetConfig::Battery(config) => config.enable,
WidgetConfig::Cpu(config) => config.enable,
WidgetConfig::Date(config) => config.enable,
WidgetConfig::Keyboard(config) => config.enable,
WidgetConfig::Komorebi(config) => {
config.workspaces.as_ref().is_some_and(|w| w.enable)
|| config.layout.as_ref().is_some_and(|w| w.enable)
|| config.focused_container.as_ref().is_some_and(|w| w.enable)
|| config
.configuration_switcher
.as_ref()
.is_some_and(|w| w.enable)
}
WidgetConfig::Media(config) => config.enable,
WidgetConfig::Memory(config) => config.enable,
WidgetConfig::Network(config) => config.enable,
WidgetConfig::Storage(config) => config.enable,
#[cfg(target_os = "windows")]
WidgetConfig::Systray(config) => config.enable,
WidgetConfig::Time(config) => config.enable,
WidgetConfig::Update(config) => config.enable,
}
}
}
+12
View File
@@ -2508,6 +2508,7 @@ dependencies = [
"komorebi-layouts",
"komorebi-protocol",
"komorebi-scrolling",
"komorebi-settings",
"komorebi-themes",
"komorebi-transport",
"lazy_static",
@@ -2612,6 +2613,17 @@ dependencies = [
name = "komorebi-scrolling"
version = "0.1.42"
[[package]]
name = "komorebi-settings"
version = "0.1.42"
dependencies = [
"komorebi-config",
"komorebi-layouts",
"komorebi-protocol",
"komorebi-themes",
"parking_lot 0.12.5",
]
[[package]]
name = "komorebi-themes"
version = "0.1.42"
+1
View File
@@ -27,6 +27,7 @@ komorebi-layouts = { path = "../komorebi-layouts", features = [
komorebi-scrolling = { path = "../komorebi-scrolling" }
# the wire types shared with the other komorebi daemons
komorebi-config = { path = "../crates/komorebi-config", default-features = false, features = ["schemars"] }
komorebi-settings = { path = "../crates/komorebi-settings" }
komorebi-protocol = { path = "../crates/komorebi-protocol", default-features = false, features = ["schemars"] }
komorebi-transport = { path = "../crates/komorebi-transport" }
# the themes (and the palettes made from pictures), from the same upstream revision
+1
View File
@@ -12,6 +12,7 @@ komorebi-collections = { workspace = true }
komorebi-layouts = { workspace = true }
komorebi-scrolling = { workspace = true }
komorebi-config = { workspace = true }
komorebi-settings = { workspace = true }
komorebi-protocol = { workspace = true }
komorebi-transport = { workspace = true }
version = { path = "../version" }
+7 -4
View File
@@ -85,6 +85,7 @@ pub mod process_command;
pub mod process_event;
pub mod reconcile;
pub mod scrolling;
pub mod settings;
pub mod state;
pub mod static_config;
pub mod theme_manager;
@@ -208,16 +209,18 @@ lazy_static! {
/// The padding, in pixels, between the edge of a monitor's work area and the tiles of its
/// workspaces, for a monitor that sets none of its own. The default for this is 5.
pub static DEFAULT_WORKSPACE_PADDING: AtomicI32 = AtomicI32::new(5);
pub static DEFAULT_WORKSPACE_PADDING: AtomicI32 =
AtomicI32::new(komorebi_settings::shared::PlatformDefaults::KDE.workspace_padding);
/// The padding, in pixels, around each tile, for a monitor that sets none of its own. The default
/// for this is 5.
pub static DEFAULT_CONTAINER_PADDING: AtomicI32 = AtomicI32::new(5);
pub static DEFAULT_CONTAINER_PADDING: AtomicI32 =
AtomicI32::new(komorebi_settings::shared::PlatformDefaults::KDE.container_padding);
/// How many pixels `ResizeWindowEdge` and `ResizeWindowAxis` resize by, when `komorebi.json` does
/// not say
pub static DEFAULT_RESIZE_DELTA: i32 = 50;
pub static DEFAULT_RESIZE_DELTA: i32 = komorebi_settings::shared::DEFAULT_RESIZE_DELTA;
/// Whether the pointer goes to the window that is given the focus, when `komorebi.json` does not
/// say
pub static DEFAULT_MOUSE_FOLLOWS_FOCUS: bool = true;
pub static DEFAULT_MOUSE_FOLLOWS_FOCUS: bool = komorebi_settings::shared::DEFAULT_MOUSE_FOLLOWS_FOCUS;
/// The event, the message or the change of the monitors a [`Notification`] is about
///
+97
View File
@@ -0,0 +1,97 @@
//! The settings of this daemon, as one snapshot (plan step 10, `docs/dev/settings.md`).
//!
//! Nothing reads this yet: the process-wide statics are still the source of truth, and each
//! migration step moves one group of them (writers and readers together) into
//! [`settings()`]. Until then the value is the starting values of those statics.
use std::sync::LazyLock;
use komorebi_settings::Handle;
use komorebi_settings::Shared;
use komorebi_settings::shared::PlatformDefaults;
/// The part of the settings that only this platform has.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct KdeSettings {}
pub type Settings = komorebi_settings::Settings<KdeSettings>;
static SETTINGS: LazyLock<Handle<KdeSettings>> = LazyLock::new(|| {
Handle::new(Settings::new(
Shared::defaults_for(PlatformDefaults::KDE),
KdeSettings::default(),
))
});
/// The settings of the running daemon: take a [`Handle::snapshot`] once per unit of work.
pub fn settings() -> &'static Handle<KdeSettings> {
&SETTINGS
}
#[cfg(test)]
mod tests {
use std::sync::atomic::Ordering;
use super::*;
use crate::border_manager;
// The statics are still the source of truth. These pin that the starting values of the
// settings are the starting values of the statics, so that moving a group over changes nothing.
// (They read the statics without any test writing them first: no test here sets them.)
#[test]
fn the_starting_settings_are_the_starting_values_of_the_statics() {
let shared = &settings().snapshot().shared;
assert_eq!(
shared.defaults.container_padding,
crate::DEFAULT_CONTAINER_PADDING.load(Ordering::SeqCst)
);
assert_eq!(
shared.defaults.workspace_padding,
crate::DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst)
);
assert_eq!(shared.defaults.resize_delta, crate::DEFAULT_RESIZE_DELTA);
assert_eq!(
shared.defaults.mouse_follows_focus,
crate::DEFAULT_MOUSE_FOLLOWS_FOCUS
);
assert_eq!(
shared.border.enabled,
border_manager::BORDER_ENABLED.load(Ordering::SeqCst)
);
assert_eq!(
shared.border.width,
border_manager::BORDER_WIDTH.load(Ordering::SeqCst)
);
assert_eq!(
shared.border.offset,
border_manager::BORDER_OFFSET.load(Ordering::SeqCst)
);
assert_eq!(
shared.border.radius,
border_manager::BORDER_RADIUS.load(Ordering::SeqCst)
);
let colours = shared.border.colours;
assert_eq!(
colours.single,
border_manager::FOCUSED.load(Ordering::SeqCst)
);
assert_eq!(colours.stack, border_manager::STACK.load(Ordering::SeqCst));
assert_eq!(
colours.monocle,
border_manager::MONOCLE.load(Ordering::SeqCst)
);
assert_eq!(
colours.floating,
border_manager::FLOATING.load(Ordering::SeqCst)
);
assert_eq!(
colours.unfocused,
border_manager::UNFOCUSED.load(Ordering::SeqCst)
);
assert_eq!(
colours.unfocused_locked,
border_manager::UNFOCUSED_LOCKED.load(Ordering::SeqCst)
);
}
}
+20
View File
@@ -3170,6 +3170,7 @@ dependencies = [
"komorebi-config",
"komorebi-layouts",
"komorebi-protocol",
"komorebi-settings",
"komorebi-themes",
"komorebi-transport",
"lazy_static",
@@ -3253,14 +3254,22 @@ version = "0.1.42"
dependencies = [
"chrono",
"chrono-tz",
"color-eyre",
"eframe",
"egui-phosphor",
"image 0.25.10",
"komorebi-config",
"komorebi-layouts",
"komorebi-protocol",
"komorebi-themes",
"lazy_static",
"reqwest",
"schemars 1.2.3",
"serde",
"serde_json_lenient",
"starship-battery",
"sysinfo 0.39.6",
"tracing",
]
[[package]]
@@ -3332,6 +3341,17 @@ dependencies = [
name = "komorebi-scrolling"
version = "0.1.42"
[[package]]
name = "komorebi-settings"
version = "0.1.42"
dependencies = [
"komorebi-config",
"komorebi-layouts",
"komorebi-protocol",
"komorebi-themes",
"parking_lot",
]
[[package]]
name = "komorebi-themes"
version = "0.1.42"
+1
View File
@@ -17,6 +17,7 @@ komorebi-themes = { path = "../komorebi-themes" }
komorebi-backend = { path = "../crates/komorebi-backend" }
komorebi-collections = { path = "../crates/komorebi-collections" }
komorebi-config = { path = "../crates/komorebi-config", default-features = false, features = ["schemars"] }
komorebi-settings = { path = "../crates/komorebi-settings" }
komorebi-protocol = { path = "../crates/komorebi-protocol", default-features = false, features = ["schemars"] }
komorebi-transport = { path = "../crates/komorebi-transport" }
komorebi-layouts = { path = "../komorebi-layouts", features = [
+1
View File
@@ -20,6 +20,7 @@ use crate::widgets::komorebi::Komorebi;
use crate::widgets::komorebi::MonitorInfo;
use crate::widgets::widget::BarWidget;
use crate::widgets::widget::WidgetConfig;
use crate::widgets::widget::WidgetConfigExt;
use crossbeam_channel::Receiver;
use crossbeam_channel::TryRecvError;
use eframe::egui::Align;
+1 -582
View File
@@ -1,582 +1 @@
use crate::AUTO_SELECT_FILL_COLOUR;
use crate::AUTO_SELECT_TEXT_COLOUR;
use crate::DEFAULT_PADDING;
use crate::render::RenderConfig;
use crate::render::RenderExt;
use eframe::egui::Color32;
use eframe::egui::Context;
use eframe::egui::FontId;
use eframe::egui::TextStyle;
use komorebi_themes::colour::Colour;
use komorebi_themes::colour::Rgb;
use std::num::NonZeroU32;
use std::sync::atomic::Ordering;
// use crate::bar::exec_powershell;
use crate::render::Grouping;
use crate::widgets::widget::WidgetConfig;
use color_eyre::eyre;
use eframe::egui::TextBuffer;
use komorebi_client::Rect;
use komorebi_themes::KomobarTheme;
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` static configuration file reference for komorebi for Mac `v0.1.0`
///
/// `"$schema": "https://komorebi-for-mac.lgug2z.com/komorebi.bar.0.1.0.schema.json"`
pub struct KomobarConfig {
/// Bar height
#[cfg_attr(feature = "schemars", schemars(extend("default" = 50.0)))]
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.0
/// }
/// ```
/// or:
/// ```json
/// "padding": {
/// "horizontal": [left, right]
/// }
/// ```
/// You can also set individual padding on each side like this:
/// ```json
/// "padding": {
/// "top": 10.0,
/// "bottom": 10.0,
/// "left": 10.0,
/// "right": 10.0
/// }
/// ```
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.0
/// }
/// ```
/// or:
/// ```json
/// "margin": {
/// "vertical": [top, bottom]
/// }
/// ```
/// You can also set individual margin on each side like this:
/// ```json
/// "margin": {
/// "top": 10.0,
/// "bottom": 10.0,
/// "left": 10.0,
/// "right": 10.0
/// }
/// ```
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)]
// 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))]
// pub struct KomorebiMouseMessage {
// /// Send the FocusMonitorAtCursor message (default:true)
// pub focus_monitor_at_cursor: Option<bool>,
// /// Wrap the {message} with a MouseFollowsFocus(false) and MouseFollowsFocus({original.value}) message (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))]
// 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 (default: 30)
// 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 (default: 30)
// 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 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) -> 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"),
};
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_bar_core::config::*;
+6 -4
View File
@@ -71,10 +71,12 @@ fn main() -> eyre::Result<()> {
#[cfg(feature = "schemars")]
if opts.schema {
let bar_config = schemars::schema_for!(KomobarConfig);
let schema = serde_json::to_string_pretty(&bar_config)?;
println!("{schema}");
print!(
"{}",
komorebi_bar_core::schema::generated_text::<KomobarConfig>(
komorebi_bar_core::schema::SchemaPlatform::MacOs
)
);
std::process::exit(0);
}
+25
View File
@@ -1,6 +1,8 @@
use crate::widgets::ImageIcon;
use egui_phosphor::regular::CALENDAR;
use komorebi_bar_core::platform::BarPlatform;
use komorebi_bar_core::platform::SystemTool;
use komorebi_client::SocketMessage;
use std::io;
use std::process::Command;
@@ -21,6 +23,12 @@ impl BarPlatform for MacBar {
.spawn(),
// explorer.exe does not exist on macOS: this is what the update widget does today
SystemTool::Url(url) => Command::new("explorer.exe").args([url]).spawn(),
SystemTool::StorageLocation(label) => {
let parts: Vec<&str> = label.split(' ').collect();
let path = parts[..parts.len() - 1].join(" ");
Command::new("open").args([path]).spawn()
}
}
.map(|_| ())
}
@@ -32,4 +40,21 @@ impl BarPlatform for MacBar {
fn calendar_icon(&self) -> &'static str {
CALENDAR
}
fn exec_shell_command(&self, _command: &str) -> color_eyre::Result<()> {
// the macOS bar has no shell session: the mouse commands are not supported here
Err(io::Error::new(io::ErrorKind::Unsupported, "no shell session on macOS").into())
}
fn send_komorebi_message(&self, message: &SocketMessage) -> io::Result<()> {
komorebi_client::send_message(message)
}
fn send_komorebi_batch(&self, messages: Vec<SocketMessage>) -> io::Result<()> {
komorebi_client::send_batch(messages)
}
fn command_icon(&self, _command: &str) -> Option<ImageIcon> {
None
}
}
@@ -1,407 +0,0 @@
use super::ImageIcon;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::widgets::widget::BarWidget;
use eframe::egui::Color32;
use eframe::egui::Context;
use eframe::egui::CornerRadius;
use eframe::egui::FontId;
use eframe::egui::Frame;
use eframe::egui::Image;
use eframe::egui::Label;
use eframe::egui::Margin;
use eframe::egui::RichText;
use eframe::egui::Sense;
use eframe::egui::Stroke;
use eframe::egui::StrokeKind;
use eframe::egui::Ui;
use eframe::egui::Vec2;
use eframe::egui::vec2;
use komorebi_client::PathExt;
use serde::Deserialize;
use serde::Serialize;
use std::process::Command;
use std::sync::Arc;
use std::time::Duration;
use std::time::Instant;
use tracing;
/// Minimum interval between consecutive application launches to prevent accidental spamming.
const MIN_LAUNCH_INTERVAL: Duration = Duration::from_millis(800);
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Applications widget configuration
pub struct ApplicationsConfig {
/// Enables or disables the applications widget.
pub enable: bool,
/// Whether to show the launch command on hover (optional).
/// Could be overridden per application. Defaults to `false` if not set.
pub show_command_on_hover: Option<bool>,
/// Horizontal spacing between application buttons.
pub spacing: Option<f32>,
/// Default display format for all applications (optional).
/// Could be overridden per application. Defaults to `Icon`.
pub display: Option<ApplicationsDisplayFormat>,
/// List of configured applications to display.
pub items: Vec<AppConfig>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Application button configuration
pub struct AppConfig {
/// Whether to enable this application button (optional).
/// Inherits from the global `Applications` setting if omitted.
pub enable: Option<bool>,
/// Whether to show the launch command on hover (optional).
/// Inherits from the global `Applications` setting if omitted.
pub show_command_on_hover: Option<bool>,
/// Display name of the application.
pub name: String,
/// Optional icon: a path to an image or a text-based glyph (e.g., from Nerd Fonts).
/// If not set, and if the `command` is a path to an executable, an icon might be extracted from it.
/// Note: glyphs require a compatible `font_family`.
pub icon: Option<String>,
/// Command to execute (e.g. path to the application or shell command).
pub command: String,
/// Display format for this application button (optional). Overrides global format if set.
pub display: Option<ApplicationsDisplayFormat>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Applications widget display format
pub enum ApplicationsDisplayFormat {
/// Show only the application icon.
#[default]
Icon,
/// Show only the application name as text.
Text,
/// Show both the application icon and name.
IconAndText,
}
#[derive(Clone, Debug)]
pub struct Applications {
/// Whether the applications widget is enabled.
pub enable: bool,
/// Horizontal spacing between application buttons.
pub spacing: Option<f32>,
/// Applications to be rendered in the UI.
pub items: Vec<App>,
}
impl BarWidget for Applications {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
if !self.enable {
return;
}
let icon_config = IconConfig {
font_id: config.icon_font_id.clone(),
size: config.icon_font_id.size,
color: ctx.style().visuals.selection.stroke.color,
};
if let Some(spacing) = self.spacing {
ui.spacing_mut().item_spacing.x = spacing;
}
config.apply_on_widget(false, ui, |ui| {
for app in &mut self.items {
app.render(ctx, ui, &icon_config);
}
});
}
}
impl From<&ApplicationsConfig> for Applications {
fn from(applications_config: &ApplicationsConfig) -> Self {
let items = applications_config
.items
.iter()
.enumerate()
.map(|(index, config)| {
let command = UserCommand::new(&config.command);
App {
enable: config.enable.unwrap_or(applications_config.enable),
#[allow(clippy::obfuscated_if_else)]
name: config
.name
.is_empty()
.then(|| format!("App {}", index + 1))
.unwrap_or_else(|| config.name.clone()),
icon: Icon::try_from_path(config.icon.as_deref())
.or_else(|| Icon::try_from_command(&command)),
command,
display: config
.display
.or(applications_config.display)
.unwrap_or_default(),
show_command_on_hover: config
.show_command_on_hover
.or(applications_config.show_command_on_hover)
.unwrap_or(false),
}
})
.collect();
Self {
enable: applications_config.enable,
items,
spacing: applications_config.spacing,
}
}
}
/// A single resolved application entry used at runtime.
#[derive(Clone, Debug)]
pub struct App {
/// Whether this application is enabled.
pub enable: bool,
/// Display name of the application. Defaults to "App N" if not set.
pub name: String,
/// Icon to display for this application, if available.
pub icon: Option<Icon>,
/// Command to execute when the application is launched.
pub command: UserCommand,
/// Display format (icon, text, or both).
pub display: ApplicationsDisplayFormat,
/// Whether to show the launch command on hover.
pub show_command_on_hover: bool,
}
impl App {
/// Renders the application button in the provided `Ui` context with a given icon size.
#[inline]
pub fn render(&mut self, ctx: &Context, ui: &mut Ui, icon_config: &IconConfig) {
if self.enable
&& SelectableFrame::new(false)
.show(ui, |ui| {
ui.spacing_mut().item_spacing = Vec2::splat(4.0);
match self.display {
ApplicationsDisplayFormat::Icon => self.draw_icon(ctx, ui, icon_config),
ApplicationsDisplayFormat::Text => self.draw_name(ui),
ApplicationsDisplayFormat::IconAndText => {
self.draw_icon(ctx, ui, icon_config);
self.draw_name(ui);
}
}
// Add hover text with command information
let response = ui.response();
if self.show_command_on_hover {
response.on_hover_text(format!("Launch: {}", self.command.as_ref()));
}
})
.clicked()
{
// Launch the application when clicked
self.command.launch_if_ready();
}
}
/// Draws the application's icon within the UI if available,
/// or falls back to a default placeholder icon.
#[inline]
fn draw_icon(&self, ctx: &Context, ui: &mut Ui, icon_config: &IconConfig) {
if let Some(icon) = &self.icon {
icon.draw(ctx, ui, icon_config);
} else {
Icon::draw_fallback(ui, Vec2::splat(icon_config.size));
}
}
/// Displays the application's name as a non-selectable label within the UI.
#[inline]
fn draw_name(&self, ui: &mut Ui) {
ui.add(Label::new(&self.name).selectable(false));
}
}
/// Holds image/text data to be used as an icon in the UI.
/// This represents source icon data before rendering.
#[derive(Clone, Debug)]
pub enum Icon {
/// RGBA image used for rendering the icon.
Image(ImageIcon),
/// Text-based icon, e.g. from a font like Nerd Fonts.
Text(String),
}
impl Icon {
/// Attempts to create an [`Icon`] from a string path or text glyph/glyphs.
///
/// - Environment variables in the path are resolved using [`PathExt::replace_env`].
/// - Uses [`ImageIcon::try_load`] to load and cache the icon image based on the resolved path.
/// - If the path is invalid but the string is non-empty, it is interpreted as a text-based icon and
/// returned as [`Icon::Text`].
/// - Returns `None` if the input is empty, `None`, or image loading fails.
#[inline]
pub fn try_from_path(icon: Option<&str>) -> Option<Self> {
let icon = icon.map(str::trim)?;
if icon.is_empty() {
return None;
}
let path = icon.replace_env();
if !path.is_file() {
return Some(Icon::Text(icon.to_owned()));
}
let image_icon = ImageIcon::try_load(path.as_ref(), || match image::open(&path) {
Ok(img) => Some(img),
Err(err) => {
tracing::error!("Failed to load icon from {:?}, error: {}", path, err);
None
}
})?;
Some(Icon::Image(image_icon))
}
/// Attempts to create an [`Icon`] by extracting an image from the executable path of a [`UserCommand`].
///
/// - Uses [`ImageIcon::try_load`] to load and cache the icon image based on the resolved executable path.
/// - Returns [`Icon::Image`] if an icon is successfully extracted.
/// - Returns `None` if the executable path is unavailable or icon extraction fails.
#[inline]
pub fn try_from_command(_command: &UserCommand) -> Option<Self> {
// let path = command.get_executable()?;
// let image_icon = ImageIcon::try_load(path.as_ref(), || {
// let path_str = path.to_str()?;
// windows_icons::get_icon_by_path(path_str)
// .or_else(|| windows_icons_fallback::get_icon_by_path(path_str))
// })?;
// Some(Icon::Image(image_icon))
None
}
/// Renders the icon in the given [`Ui`] using the provided [`IconConfig`].
#[inline]
pub fn draw(&self, ctx: &Context, ui: &mut Ui, icon_config: &IconConfig) {
match self {
Icon::Image(image_icon) => {
Frame::NONE
.inner_margin(Margin::same(ui.style().spacing.button_padding.y as i8))
.show(ui, |ui| {
ui.add(
Image::from_texture(&image_icon.texture(ctx))
.maintain_aspect_ratio(true)
.fit_to_exact_size(Vec2::splat(icon_config.size)),
);
});
}
Icon::Text(icon) => {
let rich_text = RichText::new(icon)
.font(icon_config.font_id.clone())
.size(icon_config.size)
.color(icon_config.color);
ui.add(Label::new(rich_text).selectable(false));
}
}
}
/// Draws a fallback icon when the specified icon cannot be loaded.
/// Displays a simple crossed-out rectangle as a placeholder.
#[inline]
pub fn draw_fallback(ui: &mut Ui, icon_size: Vec2) {
let (response, painter) = ui.allocate_painter(icon_size, Sense::hover());
let stroke = Stroke::new(1.0, ui.style().visuals.text_color());
let mut rect = response.rect;
let rounding = CornerRadius::same((rect.width() * 0.1) as u8);
rect = rect.shrink(stroke.width);
let c = rect.center();
let r = rect.width() / 2.0;
painter.rect_stroke(rect, rounding, stroke, StrokeKind::Outside);
painter.line_segment([c - vec2(r, r), c + vec2(r, r)], stroke);
}
}
/// Configuration structure for icon rendering
#[derive(Clone, Debug)]
pub struct IconConfig {
/// Font used for text-based icons
pub font_id: FontId,
/// Size of the icon
pub size: f32,
/// Color of the icon used for text-based icons
pub color: Color32,
}
/// A structure to manage command execution with cooldown prevention.
#[derive(Clone, Debug)]
pub struct UserCommand {
/// The command string to execute
pub command: Arc<str>,
/// Last time this command was executed (used for cooldown control)
pub last_launch: Instant,
}
impl AsRef<str> for UserCommand {
#[inline]
fn as_ref(&self) -> &str {
&self.command
}
}
impl UserCommand {
/// Creates a new [`UserCommand`] with environment variables in the command path
/// resolved using [`PathExt::replace_env`].
#[inline]
pub fn new(command: &str) -> Self {
// Allow immediate launch by initializing last_launch in the past
let last_launch = Instant::now() - 2 * MIN_LAUNCH_INTERVAL;
Self {
command: Arc::from(command.replace_env().to_str().unwrap_or_default()),
last_launch,
}
}
// /// Attempts to resolve the executable path from the command string.
// ///
// /// Resolution logic:
// /// - Splits the command by ".exe" and checks if the first part is an existing file.
// /// - If not, attempts to locate the binary using [`which`] on this name.
// /// - If still unresolved, takes the first word (separated by whitespace) and attempts
// /// to find it in the system `PATH` using [`which`].
// ///
// /// Returns `None` if no executable path can be determined.
// #[inline]
// pub fn get_executable(&self) -> Option<Cow<'_, Path>> {
// if let Some(binary) = self.command.split(".exe").next().map(Path::new) {
// if binary.is_file() {
// return Some(Cow::Borrowed(binary));
// } else if let Ok(binary) = which(binary) {
// return Some(Cow::Owned(binary));
// }
// }
//
// which(self.command.split(' ').next()?).ok().map(Cow::Owned)
// }
/// Attempts to launch the specified command in a separate thread if enough time has passed
/// since the last launch. This prevents repeated launches from rapid consecutive clicks.
///
/// Errors during launch are logged using the `tracing` crate.
pub fn launch_if_ready(&mut self) {
let now = Instant::now();
// Check if enough time has passed since the last launch
if now.duration_since(self.last_launch) < MIN_LAUNCH_INTERVAL {
return;
}
self.last_launch = now;
let command_string = self.command.clone();
// Launch the application in a separate thread to avoid blocking the UI
std::thread::spawn(move || {
if let Err(e) = Command::new("cmd").args(["/C", &command_string]).spawn() {
tracing::error!("Failed to launch command '{}': {}", command_string, e);
}
});
}
}
@@ -7,7 +7,15 @@ use crate::config::WorkspacesDisplayFormat;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::ui::CustomUi;
use crate::widgets::komorebi_layout::KomorebiLayout;
pub use komorebi_bar_core::widgets::komorebi_config::KomorebiConfig;
pub use komorebi_bar_core::widgets::komorebi_config::KomorebiConfigurationSwitcherConfig;
pub use komorebi_bar_core::widgets::komorebi_config::KomorebiFocusedContainerConfig;
pub use komorebi_bar_core::widgets::komorebi_config::KomorebiLayoutConfig;
pub use komorebi_bar_core::widgets::komorebi_config::KomorebiLockedContainerConfig;
pub use komorebi_bar_core::widgets::komorebi_config::KomorebiWorkspaceLayerConfig;
pub use komorebi_bar_core::widgets::komorebi_config::KomorebiWorkspacesConfig;
pub use komorebi_bar_core::widgets::komorebi_config::KomorebiLayout;
use crate::widgets::komorebi_layout::KomorebiLayoutExt;
use crate::widgets::widget::BarWidget;
use eframe::egui::Align;
use eframe::egui::Color32;
@@ -40,10 +48,7 @@ use komorebi_client::State;
use komorebi_client::Window;
use komorebi_client::Workspace;
use komorebi_client::WorkspaceLayer;
use serde::Deserialize;
use serde::Serialize;
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::fs::File;
use std::io::BufReader;
@@ -52,95 +57,6 @@ use std::path::Path;
use std::rc::Rc;
use std::sync::atomic::Ordering;
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Komorebi widget configuration
pub struct KomorebiConfig {
/// Configure the Workspaces widget
pub workspaces: Option<KomorebiWorkspacesConfig>,
/// Configure the Layout widget
pub layout: Option<KomorebiLayoutConfig>,
/// Configure the Workspace Layer widget
pub workspace_layer: Option<KomorebiWorkspaceLayerConfig>,
/// Configure the Focused Container widget
#[serde(alias = "focused_window")]
pub focused_container: Option<KomorebiFocusedContainerConfig>,
/// Configure the Locked Container widget
pub locked_container: Option<KomorebiLockedContainerConfig>,
/// Configure the Configuration Switcher widget
pub configuration_switcher: Option<KomorebiConfigurationSwitcherConfig>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Komorebi widget workspaces configuration
pub struct KomorebiWorkspacesConfig {
/// Enable the Komorebi Workspaces widget
pub enable: bool,
/// Hide workspaces without any windows
pub hide_empty_workspaces: bool,
/// Display format of the workspace
pub display: Option<WorkspacesDisplayFormat>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Komorebi widget layout configuration
pub struct KomorebiLayoutConfig {
/// Enable the Komorebi Layout widget
pub enable: bool,
/// List of layout options
pub options: Option<Vec<KomorebiLayout>>,
/// Display format of the current layout
pub display: Option<DisplayFormat>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Komorebi widget workspace layer configuration
pub struct KomorebiWorkspaceLayerConfig {
/// Enable the Komorebi Workspace Layer widget
pub enable: bool,
/// Display format of the current layer
pub display: Option<DisplayFormat>,
/// Show the widget event if the layer is Tiling
pub show_when_tiling: Option<bool>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Komorebi widget focused container configuration
pub struct KomorebiFocusedContainerConfig {
/// Enable the Komorebi Focused Container widget
pub enable: bool,
/// DEPRECATED: use 'display' instead (Show the icon of the currently focused container)
pub show_icon: Option<bool>,
/// Display format of the currently focused container
pub display: Option<DisplayFormat>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Komorebi widget locked container configuration
pub struct KomorebiLockedContainerConfig {
/// Enable the Komorebi Locked Container widget
pub enable: bool,
/// Display format of the current locked state
pub display: Option<DisplayFormat>,
/// Show the widget event if the layer is unlocked
pub show_when_unlocked: Option<bool>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Komorebi widget configuration switcher configuration
pub struct KomorebiConfigurationSwitcherConfig {
/// Enable the Komorebi Configurations widget
pub enable: bool,
/// A map of display friendly name => path to configuration.json
pub configurations: BTreeMap<String, String>,
}
impl From<&KomorebiConfig> for Komorebi {
fn from(cfg: &KomorebiConfig) -> Self {
let configuration_switcher = cfg.configuration_switcher.clone().map(|mut cs| {
@@ -530,7 +446,9 @@ impl FocusedContainerBar {
if !value.enable {
return None;
}
// Handle legacy setting - convert show_icon to display format
#[allow(deprecated)]
let format = value
.display
.unwrap_or(if value.show_icon.unwrap_or(false) {
@@ -1,329 +0,0 @@
use crate::config::DisplayFormat;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::widgets::komorebi::KomorebiLayoutConfig;
use eframe::egui::Context;
use eframe::egui::CornerRadius;
use eframe::egui::FontId;
use eframe::egui::Frame;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::Stroke;
use eframe::egui::StrokeKind;
use eframe::egui::Ui;
use eframe::egui::Vec2;
use eframe::egui::vec2;
use komorebi_client::SocketMessage;
use serde::Deserialize;
use serde::Deserializer;
use serde::Serialize;
use serde::de::Error;
use serde_json::from_str;
use std::fmt::Display;
use std::fmt::Formatter;
#[derive(Copy, Clone, Debug, Serialize, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Komorebi layout kind
pub enum KomorebiLayout {
/// Predefined layout
#[cfg_attr(feature = "schemars", schemars(title = "Default"))]
Default(komorebi_client::DefaultLayout),
/// Monocle mode
Monocle,
/// Floating layer
Floating,
/// Paused
Paused,
/// Custom layout
Custom,
}
impl<'de> Deserialize<'de> for KomorebiLayout {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s: String = String::deserialize(deserializer)?;
// Attempt to deserialize the string as a DefaultLayout
if let Ok(default_layout) = from_str::<komorebi_client::DefaultLayout>(&format!("\"{s}\""))
{
return Ok(KomorebiLayout::Default(default_layout));
}
// Handle other cases
match s.as_str() {
"Monocle" => Ok(KomorebiLayout::Monocle),
"Floating" => Ok(KomorebiLayout::Floating),
"Paused" => Ok(KomorebiLayout::Paused),
"Custom" => Ok(KomorebiLayout::Custom),
_ => Err(Error::custom(format!("Invalid layout: {s}"))),
}
}
}
impl Display for KomorebiLayout {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
KomorebiLayout::Default(layout) => write!(f, "{layout}"),
KomorebiLayout::Monocle => write!(f, "Monocle"),
KomorebiLayout::Floating => write!(f, "Floating"),
KomorebiLayout::Paused => write!(f, "Paused"),
KomorebiLayout::Custom => write!(f, "Custom"),
}
}
}
impl KomorebiLayout {
fn is_default(&mut self) -> bool {
matches!(self, KomorebiLayout::Default(_))
}
fn on_click(
&mut self,
show_options: &bool,
monitor_idx: usize,
workspace_idx: Option<usize>,
) -> bool {
if self.is_default() {
!show_options
} else {
self.on_click_option(monitor_idx, workspace_idx);
false
}
}
fn on_click_option(&mut self, monitor_idx: usize, workspace_idx: Option<usize>) {
match self {
KomorebiLayout::Default(option) => {
if let Some(ws_idx) = workspace_idx
&& komorebi_client::send_message(&SocketMessage::WorkspaceLayout(
monitor_idx,
ws_idx,
*option,
))
.is_err()
{
tracing::error!("could not send message to komorebi: WorkspaceLayout");
}
}
KomorebiLayout::Monocle => {
if komorebi_client::send_batch([
SocketMessage::FocusMonitorAtCursor,
SocketMessage::ToggleMonocle,
])
.is_err()
{
tracing::error!("could not send message to komorebi: ToggleMonocle");
}
}
KomorebiLayout::Floating => {
if komorebi_client::send_batch([
SocketMessage::FocusMonitorAtCursor,
SocketMessage::ToggleTiling,
])
.is_err()
{
tracing::error!("could not send message to komorebi: ToggleTiling");
}
}
KomorebiLayout::Paused => {
if komorebi_client::send_message(&SocketMessage::TogglePause).is_err() {
tracing::error!("could not send message to komorebi: TogglePause");
}
}
KomorebiLayout::Custom => {}
}
}
fn show_icon(&mut self, is_selected: bool, font_id: FontId, ctx: &Context, ui: &mut Ui) {
// paint custom icons for the layout
let size = Vec2::splat(font_id.size);
let (response, painter) = ui.allocate_painter(size, Sense::hover());
let color = if is_selected {
ctx.style().visuals.selection.stroke.color
} else {
ui.style().visuals.text_color()
};
let stroke = Stroke::new(1.0, color);
let mut rect = response.rect;
let rounding = CornerRadius::same((rect.width() * 0.1) as u8);
rect = rect.shrink(stroke.width);
let c = rect.center();
let r = rect.width() / 2.0;
painter.rect_stroke(rect, rounding, stroke, StrokeKind::Outside);
match self {
KomorebiLayout::Default(layout) => match layout {
komorebi_client::DefaultLayout::BSP => {
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
painter.line_segment([c, c + vec2(r, 0.0)], stroke);
painter.line_segment([c + vec2(r / 2.0, 0.0), c + vec2(r / 2.0, r)], stroke);
}
komorebi_client::DefaultLayout::Columns => {
painter.line_segment([c - vec2(r / 2.0, r), c + vec2(-r / 2.0, r)], stroke);
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
painter.line_segment([c - vec2(-r / 2.0, r), c + vec2(r / 2.0, r)], stroke);
}
komorebi_client::DefaultLayout::Rows => {
painter.line_segment([c - vec2(r, r / 2.0), c + vec2(r, -r / 2.0)], stroke);
painter.line_segment([c - vec2(r, 0.0), c + vec2(r, 0.0)], stroke);
painter.line_segment([c - vec2(r, -r / 2.0), c + vec2(r, r / 2.0)], stroke);
}
komorebi_client::DefaultLayout::VerticalStack => {
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
painter.line_segment([c, c + vec2(r, 0.0)], stroke);
}
komorebi_client::DefaultLayout::RightMainVerticalStack => {
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
painter.line_segment([c - vec2(r, 0.0), c], stroke);
}
komorebi_client::DefaultLayout::HorizontalStack => {
painter.line_segment([c - vec2(r, 0.0), c + vec2(r, 0.0)], stroke);
painter.line_segment([c, c + vec2(0.0, r)], stroke);
}
komorebi_client::DefaultLayout::UltrawideVerticalStack => {
painter.line_segment([c - vec2(r / 2.0, r), c + vec2(-r / 2.0, r)], stroke);
painter.line_segment([c + vec2(r / 2.0, 0.0), c + vec2(r, 0.0)], stroke);
painter.line_segment([c - vec2(-r / 2.0, r), c + vec2(r / 2.0, r)], stroke);
}
komorebi_client::DefaultLayout::Grid => {
painter.line_segment([c - vec2(r, 0.0), c + vec2(r, 0.0)], stroke);
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
}
// TODO: @CtByte can you think of a nice icon to draw here?
komorebi_client::DefaultLayout::Scrolling => {
painter.line_segment([c - vec2(r / 2.0, r), c + vec2(-r / 2.0, r)], stroke);
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
painter.line_segment([c - vec2(-r / 2.0, r), c + vec2(r / 2.0, r)], stroke);
}
},
KomorebiLayout::Monocle => {}
KomorebiLayout::Floating => {
let mut rect_left = response.rect;
rect_left.set_width(rect.width() * 0.5);
rect_left.set_height(rect.height() * 0.5);
let mut rect_right = rect_left;
rect_left = rect_left.translate(Vec2::new(
rect.width() * 0.1 + stroke.width,
rect.width() * 0.1 + stroke.width,
));
rect_right = rect_right.translate(Vec2::new(
rect.width() * 0.35 + stroke.width,
rect.width() * 0.35 + stroke.width,
));
painter.rect_filled(rect_left, rounding, color);
painter.rect_stroke(rect_right, rounding, stroke, StrokeKind::Outside);
}
KomorebiLayout::Paused => {
let mut rect_left = response.rect;
rect_left.set_width(rect.width() * 0.25);
rect_left.set_height(rect.height() * 0.8);
let mut rect_right = rect_left;
rect_left = rect_left.translate(Vec2::new(
rect.width() * 0.2 + stroke.width,
rect.width() * 0.1 + stroke.width,
));
rect_right = rect_right.translate(Vec2::new(
rect.width() * 0.55 + stroke.width,
rect.width() * 0.1 + stroke.width,
));
painter.rect_filled(rect_left, rounding, color);
painter.rect_filled(rect_right, rounding, color);
}
KomorebiLayout::Custom => {
painter.line_segment([c - vec2(0.0, r), c + vec2(0.0, r)], stroke);
painter.line_segment([c + vec2(0.0, r / 2.0), c + vec2(r, r / 2.0)], stroke);
painter.line_segment([c - vec2(0.0, r / 3.0), c - vec2(r, r / 3.0)], stroke);
}
}
}
pub fn show(
&mut self,
ctx: &Context,
ui: &mut Ui,
render_config: &mut RenderConfig,
layout_config: &KomorebiLayoutConfig,
workspace_idx: Option<usize>,
) {
let monitor_idx = render_config.monitor_idx;
let font_id = render_config.icon_font_id.clone();
let mut show_options = RenderConfig::load_show_komorebi_layout_options();
let format = layout_config.display.unwrap_or(DisplayFormat::IconAndText);
if !self.is_default() {
show_options = false;
}
render_config.apply_on_widget(false, ui, |ui| {
let layout_frame = SelectableFrame::new(false)
.show(ui, |ui| {
if let DisplayFormat::Icon | DisplayFormat::IconAndText = format {
self.show_icon(true, font_id.clone(), ctx, ui);
}
if let DisplayFormat::Text | DisplayFormat::IconAndText = format {
ui.add(Label::new(self.to_string()).selectable(false));
}
})
.on_hover_text(self.to_string());
if layout_frame.clicked() {
show_options = self.on_click(&show_options, monitor_idx, workspace_idx);
}
if show_options && let Some(workspace_idx) = workspace_idx {
Frame::NONE.show(ui, |ui| {
ui.add(
Label::new(egui_phosphor::regular::ARROW_FAT_LINES_RIGHT.to_string())
.selectable(false),
);
let mut layout_options = layout_config.options.clone().unwrap_or(vec![
KomorebiLayout::Default(komorebi_client::DefaultLayout::BSP),
KomorebiLayout::Default(komorebi_client::DefaultLayout::Columns),
KomorebiLayout::Default(komorebi_client::DefaultLayout::Rows),
KomorebiLayout::Default(komorebi_client::DefaultLayout::VerticalStack),
KomorebiLayout::Default(
komorebi_client::DefaultLayout::RightMainVerticalStack,
),
KomorebiLayout::Default(komorebi_client::DefaultLayout::HorizontalStack),
KomorebiLayout::Default(
komorebi_client::DefaultLayout::UltrawideVerticalStack,
),
KomorebiLayout::Default(komorebi_client::DefaultLayout::Grid),
//KomorebiLayout::Custom,
KomorebiLayout::Monocle,
KomorebiLayout::Floating,
KomorebiLayout::Paused,
]);
for layout_option in &mut layout_options {
let is_selected = self == layout_option;
if SelectableFrame::new(is_selected)
.show(ui, |ui| {
layout_option.show_icon(is_selected, font_id.clone(), ctx, ui)
})
.on_hover_text(match layout_option {
KomorebiLayout::Default(layout) => layout.to_string(),
KomorebiLayout::Monocle => "Toggle monocle".to_string(),
KomorebiLayout::Floating => "Toggle tiling".to_string(),
KomorebiLayout::Paused => "Toggle pause".to_string(),
KomorebiLayout::Custom => "Custom".to_string(),
})
.clicked()
{
layout_option.on_click_option(monitor_idx, Some(workspace_idx));
show_options = false;
};
}
});
}
});
RenderConfig::store_show_komorebi_layout_options(show_options);
}
}
@@ -1,171 +1,19 @@
use eframe::egui::ColorImage;
use eframe::egui::Context;
use eframe::egui::TextureHandle;
use eframe::egui::TextureOptions;
use image::RgbaImage;
use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
use std::sync::LazyLock;
use std::sync::RwLock;
pub mod applications;
pub use komorebi_bar_core::widgets::applications;
pub use komorebi_bar_core::widgets::battery;
pub use komorebi_bar_core::widgets::cpu;
pub use komorebi_bar_core::widgets::date;
pub mod komorebi;
mod komorebi_layout;
pub use komorebi_bar_core::widgets::komorebi_layout;
pub use komorebi_bar_core::widgets::memory;
pub mod network;
pub mod storage;
pub mod time;
pub use komorebi_bar_core::widgets::storage;
pub use komorebi_bar_core::widgets::time;
pub use komorebi_bar_core::widgets::update;
pub mod widget;
/// Global cache for icon images and their associated GPU textures.
pub static ICONS_CACHE: IconsCache = IconsCache::new();
/// In-memory cache for icon images and their associated GPU textures.
///
/// Stores raw [`ColorImage`]s and [`TextureHandle`]s keyed by [`ImageIconId`].
/// Texture entries are context-dependent and automatically invalidated when the [`Context`] changes.
#[allow(clippy::type_complexity)]
pub struct IconsCache {
textures: LazyLock<RwLock<(Option<Context>, HashMap<ImageIconId, TextureHandle>)>>,
images: LazyLock<RwLock<HashMap<ImageIconId, Arc<ColorImage>>>>,
}
impl IconsCache {
/// Creates a new empty IconsCache instance.
#[inline]
pub const fn new() -> Self {
Self {
textures: LazyLock::new(|| RwLock::new((None, HashMap::new()))),
images: LazyLock::new(|| RwLock::new(HashMap::new())),
}
}
/// Retrieves or creates a texture handle for the given icon ID and image.
///
/// If a texture for the given ID already exists for the current [`Context`], it is reused.
/// Otherwise, a new texture is created, inserted into the cache, and returned.
/// The cache is reset if the [`Context`] has changed.
#[inline]
pub fn texture(&self, ctx: &Context, id: &ImageIconId, img: &Arc<ColorImage>) -> TextureHandle {
if let Some(texture) = self.get_texture(ctx, id) {
return texture;
}
let texture_handle = ctx.load_texture("icon", img.clone(), TextureOptions::default());
self.insert_texture(ctx, id.clone(), texture_handle.clone());
texture_handle
}
/// Returns the cached texture for the given icon ID if it exists and matches the current [`Context`].
pub fn get_texture(&self, ctx: &Context, id: &ImageIconId) -> Option<TextureHandle> {
let textures_lock = self.textures.read().unwrap();
if textures_lock.0.as_ref() == Some(ctx) {
return textures_lock.1.get(id).cloned();
}
None
}
/// Inserts a texture handle, resetting the cache if the [`Context`] has changed.
pub fn insert_texture(&self, ctx: &Context, id: ImageIconId, texture: TextureHandle) {
let mut textures_lock = self.textures.write().unwrap();
if textures_lock.0.as_ref() != Some(ctx) {
textures_lock.0 = Some(ctx.clone());
textures_lock.1.clear();
}
textures_lock.1.insert(id, texture);
}
/// Returns the cached image for the given icon ID, if available.
pub fn get_image(&self, id: &ImageIconId) -> Option<Arc<ColorImage>> {
self.images.read().unwrap().get(id).cloned()
}
/// Caches a raw [`ColorImage`] associated with the given icon ID.
pub fn insert_image(&self, id: ImageIconId, image: Arc<ColorImage>) {
self.images.write().unwrap().insert(id, image);
}
}
#[inline]
fn rgba_to_color_image(rgba_image: &RgbaImage) -> ColorImage {
let size = [rgba_image.width() as usize, rgba_image.height() as usize];
let pixels = rgba_image.as_flat_samples();
ColorImage::from_rgba_unmultiplied(size, pixels.as_slice())
}
/// Represents an image-based icon with a unique ID and pixel data.
#[derive(Clone, Debug)]
pub struct ImageIcon {
/// Unique identifier for the image icon, used for texture caching.
pub id: ImageIconId,
/// Shared pixel data of the icon in `ColorImage` format.
pub image: Arc<ColorImage>,
}
impl ImageIcon {
/// Creates a new [`ImageIcon`] from the given ID and image data.
#[inline]
pub fn new(id: ImageIconId, image: Arc<ColorImage>) -> Self {
Self { id, image }
}
/// Loads an [`ImageIcon`] from [`ICONS_CACHE`] or calls `loader` if not cached.
/// The loaded image is converted to a [`ColorImage`], cached, and returned.
#[inline]
pub fn try_load<F, I>(id: impl Into<ImageIconId>, loader: F) -> Option<Self>
where
F: FnOnce() -> Option<I>,
I: Into<RgbaImage>,
{
let id = id.into();
let image = ICONS_CACHE.get_image(&id).or_else(|| {
let img = loader()?;
let img = Arc::new(rgba_to_color_image(&img.into()));
ICONS_CACHE.insert_image(id.clone(), img.clone());
Some(img)
})?;
Some(ImageIcon::new(id, image))
}
/// Returns a texture handle for the icon, using the given [`Context`].
///
/// If the texture is already cached in [`ICONS_CACHE`], it is reused.
/// Otherwise, a new texture is created from the [`ColorImage`] and cached.
#[inline]
pub fn texture(&self, ctx: &Context) -> TextureHandle {
ICONS_CACHE.texture(ctx, &self.id, &self.image)
}
}
/// Unique identifier for an image-based icon.
///
/// Used to distinguish cached images and textures by either a file path
/// or a Windows window handle.
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum ImageIconId {
/// Identifier based on a file system path.
Path(Arc<Path>),
/// Window ID
WindowId(u32),
}
impl From<&Path> for ImageIconId {
#[inline]
fn from(value: &Path) -> Self {
Self::Path(value.into())
}
}
impl From<u32> for ImageIconId {
#[inline]
fn from(value: u32) -> Self {
Self::WindowId(value)
}
}
pub use komorebi_bar_core::widgets::icons::ICONS_CACHE;
pub use komorebi_bar_core::widgets::icons::ImageIcon;
pub use komorebi_bar_core::widgets::icons::ImageIconId;
#[allow(unused_imports)]
pub use komorebi_bar_core::widgets::icons::rgba_to_color_image;
@@ -1,4 +1,6 @@
use crate::bar::Alignment;
pub use komorebi_bar_core::widgets::network_config::NetworkConfig;
pub use komorebi_bar_core::widgets::network_config::NetworkSelectConfig;
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
@@ -12,8 +14,6 @@ use eframe::egui::Ui;
use eframe::egui::text::LayoutJob;
use num_derive::FromPrimitive;
use parking_lot::Mutex;
use serde::Deserialize;
use serde::Serialize;
use std::fmt;
use std::process::Command;
use std::sync::Arc;
@@ -24,48 +24,14 @@ use std::time::Duration;
use std::time::Instant;
use sysinfo::Networks;
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Network widget configuration
pub struct NetworkConfig {
/// Enable the Network widget
pub enable: bool,
/// Show total received and transmitted activity
#[serde(alias = "show_total_data_transmitted")]
pub show_total_activity: bool,
/// Show received and transmitted activity
#[serde(alias = "show_network_activity")]
pub show_activity: bool,
/// Show default interface
pub show_default_interface: Option<bool>,
/// Characters to reserve for received and transmitted activity
#[serde(alias = "network_activity_fill_characters")]
pub activity_left_padding: Option<usize>,
/// Data refresh interval in seconds
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10)))]
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
/// Select when the value is over a limit (1MiB is 1048576 bytes (1024*1024))
pub auto_select: Option<NetworkSelectConfig>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Network select configuration
pub struct NetworkSelectConfig {
/// Select the total received data when it's over this value
pub total_received_over: Option<u64>,
/// Select the total transmitted data when it's over this value
pub total_transmitted_over: Option<u64>,
/// Select the received data when it's over this value
pub received_over: Option<u64>,
/// Select the transmitted data when it's over this value
pub transmitted_over: Option<u64>,
}
impl From<NetworkConfig> for Network {
fn from(value: NetworkConfig) -> Self {
if value.default_interface_options.is_some() {
tracing::warn!(
"the network widget option `default_interface_options` is not supported on macOS and is ignored"
);
}
let default_refresh_interval = 10;
let data_refresh_interval = value.data_refresh_interval.unwrap_or(10);
@@ -1,205 +0,0 @@
use crate::bar::Alignment;
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::widgets::widget::BarWidget;
use eframe::egui::Align;
use eframe::egui::Context;
use eframe::egui::Label;
use eframe::egui::TextFormat;
use eframe::egui::Ui;
use eframe::egui::text::LayoutJob;
use serde::Deserialize;
use serde::Serialize;
use std::process::Command;
use std::time::Duration;
use std::time::Instant;
use sysinfo::Disks;
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Storage widget configuration
pub struct StorageConfig {
/// Enable the Storage widget
pub enable: bool,
/// Data refresh interval in seconds
#[cfg_attr(feature = "schemars", schemars(extend("default" = 10)))]
pub data_refresh_interval: Option<u64>,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
/// Show disks that are read only
#[cfg_attr(feature = "schemars", schemars(extend("default" = false)))]
pub show_read_only_disks: Option<bool>,
/// Show removable disks
#[cfg_attr(feature = "schemars", schemars(extend("default" = true)))]
pub show_removable_disks: Option<bool>,
/// Storage display name
pub storage_display_name: Option<StorageDisplayName>,
/// Select when the current percentage is over this value [[1-100]]
pub auto_select_over: Option<u8>,
/// Hide when the current percentage is under this value [[1-100]]
pub auto_hide_under: Option<u8>,
}
impl From<StorageConfig> for Storage {
fn from(value: StorageConfig) -> Self {
Self {
enable: value.enable,
disks: Disks::new_with_refreshed_list(),
data_refresh_interval: value.data_refresh_interval.unwrap_or(10),
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::IconAndText),
show_read_only_disks: value.show_read_only_disks.unwrap_or(false),
show_removable_disks: value.show_removable_disks.unwrap_or(true),
storage_display_name: value
.storage_display_name
.unwrap_or(StorageDisplayName::Mount),
auto_select_over: value.auto_select_over.map(|o| o.clamp(1, 100)),
auto_hide_under: value.auto_hide_under.map(|o| o.clamp(1, 100)),
last_updated: Instant::now(),
}
}
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum StorageDisplayName {
/// Display label as mount point eg. /
Mount,
/// Display label as name eg. Macintosh HD
Name,
/// Display label as mount then name eg. / Macintosh HD
MountAndName,
/// Display label as name then mount eg. Macintosh HD /
NameAndMount,
}
struct StorageDisk {
label: String,
selected: bool,
}
pub struct Storage {
pub enable: bool,
disks: Disks,
data_refresh_interval: u64,
label_prefix: LabelPrefix,
show_read_only_disks: bool,
show_removable_disks: bool,
storage_display_name: StorageDisplayName,
auto_select_over: Option<u8>,
auto_hide_under: Option<u8>,
last_updated: Instant,
}
impl Storage {
fn output(&mut self) -> Vec<StorageDisk> {
let now = Instant::now();
if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) {
self.disks.refresh(true);
self.last_updated = now;
}
let mut disks = vec![];
for disk in &self.disks {
if disk.is_read_only() && !self.show_read_only_disks {
continue;
}
if disk.is_removable() && !self.show_removable_disks {
continue;
}
let mount = disk.mount_point();
let name = disk.name();
let display_name = match self.storage_display_name {
StorageDisplayName::Mount => mount.to_string_lossy(),
StorageDisplayName::Name => name.to_string_lossy(),
StorageDisplayName::MountAndName => {
mount.to_string_lossy() + name.to_string_lossy()
}
StorageDisplayName::NameAndMount => {
name.to_string_lossy() + mount.to_string_lossy()
}
};
let total = disk.total_space();
let available = disk.available_space();
let used = total - available;
let percentage = ((used * 100) / total) as u8;
let hide = self.auto_hide_under.is_some_and(|u| percentage <= u);
if !hide {
let selected = self.auto_select_over.is_some_and(|o| percentage >= o);
disks.push(StorageDisk {
label: match self.label_prefix {
LabelPrefix::Text | LabelPrefix::IconAndText => {
format!("{} {}%", display_name, percentage)
}
LabelPrefix::None | LabelPrefix::Icon => format!("{percentage}%"),
},
selected,
})
}
}
disks.sort_by(|a, b| a.label.cmp(&b.label));
disks
}
}
impl BarWidget for Storage {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
if self.enable {
let mut output = self.output();
let is_reversed = matches!(config.alignment, Some(Alignment::Right));
if is_reversed {
output.reverse();
}
for output in output {
let auto_text_color = config.auto_select_text.filter(|_| output.selected);
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => {
egui_phosphor::regular::HARD_DRIVES.to_string()
}
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
config.icon_font_id.clone(),
auto_text_color.unwrap_or(ctx.style().visuals.selection.stroke.color),
100.0,
);
layout_job.append(
&output.label,
10.0,
TextFormat {
font_id: config.text_font_id.clone(),
color: auto_text_color.unwrap_or(ctx.style().visuals.text_color()),
valign: Align::Center,
..Default::default()
},
);
let auto_focus_fill = config.auto_select_fill;
config.apply_on_widget(false, ui, |ui| {
if SelectableFrame::new_auto(output.selected, auto_focus_fill)
.show(ui, |ui| ui.add(Label::new(layout_job).selectable(false)))
.clicked()
{
let parts: Vec<&str> = output.label.split(' ').collect();
let path = parts[..parts.len() - 1].join(" ");
if let Err(error) = Command::new("open").args([path]).spawn() {
eprintln!("{error}")
}
}
});
}
}
}
}
@@ -1,542 +0,0 @@
use crate::bar::Alignment;
use crate::config::LabelPrefix;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::widgets::widget::BarWidget;
use chrono::Local;
use chrono::NaiveTime;
use chrono_tz::Tz;
use eframe::egui::Align;
use eframe::egui::Context;
use eframe::egui::CornerRadius;
use eframe::egui::Label;
use eframe::egui::Sense;
use eframe::egui::Stroke;
use eframe::egui::TextFormat;
use eframe::egui::Ui;
use eframe::egui::Vec2;
use eframe::egui::text::LayoutJob;
use eframe::epaint::StrokeKind;
use lazy_static::lazy_static;
use serde::Deserialize;
use serde::Serialize;
use std::time::Duration;
use std::time::Instant;
lazy_static! {
static ref TIME_RANGES: Vec<(&'static str, NaiveTime)> = {
vec![
(
egui_phosphor::regular::MOON,
NaiveTime::from_hms_opt(0, 0, 0).expect("invalid"),
),
(
egui_phosphor::regular::ALARM,
NaiveTime::from_hms_opt(6, 0, 0).expect("invalid"),
),
(
egui_phosphor::regular::BREAD,
NaiveTime::from_hms_opt(6, 1, 0).expect("invalid"),
),
(
egui_phosphor::regular::BARBELL,
NaiveTime::from_hms_opt(6, 30, 0).expect("invalid"),
),
(
egui_phosphor::regular::COFFEE,
NaiveTime::from_hms_opt(8, 0, 0).expect("invalid"),
),
(
egui_phosphor::regular::CLOCK,
NaiveTime::from_hms_opt(8, 30, 0).expect("invalid"),
),
(
egui_phosphor::regular::HAMBURGER,
NaiveTime::from_hms_opt(12, 0, 0).expect("invalid"),
),
(
egui_phosphor::regular::CLOCK_AFTERNOON,
NaiveTime::from_hms_opt(12, 30, 0).expect("invalid"),
),
(
egui_phosphor::regular::FORK_KNIFE,
NaiveTime::from_hms_opt(18, 0, 0).expect("invalid"),
),
(
egui_phosphor::regular::MOON_STARS,
NaiveTime::from_hms_opt(18, 30, 0).expect("invalid"),
),
]
};
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Time widget configuration
pub struct TimeConfig {
/// Enable the Time widget
pub enable: bool,
/// Set the Time format
pub format: TimeFormat,
/// Display label prefix
pub label_prefix: Option<LabelPrefix>,
/// TimeZone (https://docs.rs/chrono-tz/latest/chrono_tz/enum.Tz.html)
///
/// Use a custom format to display additional information, i.e.:
/// ```json
/// {
/// "Time": {
/// "enable": true,
/// "format": { "Custom": "%T %Z (Tokyo)" },
/// "timezone": "Asia/Tokyo"
/// }
///}
/// ```
pub timezone: Option<String>,
/// Change the icon depending on the time. The default icon is used between 8:30 and 12:00
#[cfg_attr(feature = "schemars", schemars(extend("default" = false)))]
pub changing_icon: Option<bool>,
}
impl From<TimeConfig> for Time {
fn from(value: TimeConfig) -> Self {
// using 1 second made the widget look "less accurate" and lagging (especially having multiple with seconds).
// This is still better than getting an update every frame
let data_refresh_interval = 500;
Self {
enable: value.enable,
format: value.format,
label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon),
timezone: value.timezone,
changing_icon: value.changing_icon.unwrap_or_default(),
data_refresh_interval_millis: data_refresh_interval,
last_state: TimeOutput::new(),
last_updated: Instant::now()
.checked_sub(Duration::from_millis(data_refresh_interval))
.unwrap(),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Time format
pub enum TimeFormat {
/// Twelve-hour format (with seconds)
TwelveHour,
/// Twelve-hour format (without seconds)
TwelveHourWithoutSeconds,
/// Twenty-four-hour format (with seconds)
TwentyFourHour,
/// Twenty-four-hour format (without seconds)
TwentyFourHourWithoutSeconds,
/// Twenty-four-hour format displayed as a binary clock with circles (with seconds) (https://en.wikipedia.org/wiki/Binary_clock)
BinaryCircle,
/// Twenty-four-hour format displayed as a binary clock with rectangles (with seconds) (https://en.wikipedia.org/wiki/Binary_clock)
BinaryRectangle,
/// Custom format (https://docs.rs/chrono/latest/chrono/format/strftime/index.html)
#[cfg_attr(feature = "schemars", schemars(title = "Default"))]
Custom(String),
}
impl TimeFormat {
pub fn toggle(&mut self) {
match self {
TimeFormat::TwelveHour => *self = TimeFormat::TwelveHourWithoutSeconds,
TimeFormat::TwelveHourWithoutSeconds => *self = TimeFormat::TwentyFourHour,
TimeFormat::TwentyFourHour => *self = TimeFormat::TwentyFourHourWithoutSeconds,
TimeFormat::TwentyFourHourWithoutSeconds => *self = TimeFormat::BinaryCircle,
TimeFormat::BinaryCircle => *self = TimeFormat::BinaryRectangle,
TimeFormat::BinaryRectangle => *self = TimeFormat::TwelveHour,
_ => {}
};
}
fn fmt_string(&self) -> String {
match self {
TimeFormat::TwelveHour => String::from("%l:%M:%S %p"),
TimeFormat::TwelveHourWithoutSeconds => String::from("%l:%M %p"),
TimeFormat::TwentyFourHour => String::from("%T"),
TimeFormat::TwentyFourHourWithoutSeconds => String::from("%H:%M"),
TimeFormat::BinaryCircle => String::from("c%T"),
TimeFormat::BinaryRectangle => String::from("r%T"),
TimeFormat::Custom(format) => format.to_string(),
}
}
}
#[derive(Clone, Debug)]
struct TimeOutput {
label: String,
icon: String,
}
impl TimeOutput {
fn new() -> Self {
Self {
label: String::new(),
icon: String::new(),
}
}
}
#[derive(Clone, Debug)]
pub struct Time {
pub enable: bool,
pub format: TimeFormat,
label_prefix: LabelPrefix,
timezone: Option<String>,
changing_icon: bool,
data_refresh_interval_millis: u64,
last_state: TimeOutput,
last_updated: Instant,
}
impl Time {
fn output(&mut self) -> TimeOutput {
let mut output = self.last_state.clone();
let now = Instant::now();
if now.duration_since(self.last_updated)
> Duration::from_millis(self.data_refresh_interval_millis)
{
let (formatted, current_time) = match &self.timezone {
Some(timezone) => match timezone.parse::<Tz>() {
Ok(tz) => {
let dt = Local::now().with_timezone(&tz);
(
dt.format(&self.format.fmt_string())
.to_string()
.trim()
.to_string(),
Some(dt.time()),
)
}
Err(_) => (format!("Invalid timezone: {timezone:?}"), None),
},
None => {
let dt = Local::now();
(
dt.format(&self.format.fmt_string())
.to_string()
.trim()
.to_string(),
Some(dt.time()),
)
}
};
if current_time.is_none() {
return TimeOutput {
label: formatted,
icon: egui_phosphor::regular::WARNING_CIRCLE.to_string(),
};
}
let current_range = match &self.changing_icon {
true => TIME_RANGES
.iter()
.rev()
.find(|&(_, start)| current_time.unwrap() > *start)
.cloned(),
false => None,
}
.unwrap_or((egui_phosphor::regular::CLOCK, NaiveTime::default()));
output = TimeOutput {
label: formatted,
icon: current_range.0.to_string(),
};
self.last_state.clone_from(&output);
self.last_updated = now;
}
output
}
fn paint_binary_circle(
&mut self,
size: f32,
number: u32,
max_power: usize,
ctx: &Context,
ui: &mut Ui,
) {
let full_height = size;
let height = full_height / 4.0;
let width = height;
let offset = height / 2.0 - height / 8.0;
let (response, painter) =
ui.allocate_painter(Vec2::new(width, full_height + offset * 2.0), Sense::hover());
let color = ctx.style().visuals.text_color();
let c = response.rect.center();
let r = height / 2.0 - 0.5;
if number == 1 || number == 3 || number == 5 || number == 7 || number == 9 {
painter.circle_filled(c + Vec2::new(0.0, height * 1.50 + offset), r, color);
} else {
painter.circle_filled(c + Vec2::new(0.0, height * 1.50 + offset), r / 2.5, color);
}
if number == 2 || number == 3 || number == 6 || number == 7 {
painter.circle_filled(c + Vec2::new(0.0, height * 0.50 + offset), r, color);
} else {
painter.circle_filled(c + Vec2::new(0.0, height * 0.50 + offset), r / 2.5, color);
}
if number == 4 || number == 5 || number == 6 || number == 7 {
painter.circle_filled(c + Vec2::new(0.0, -height * 0.50 + offset), r, color);
} else if max_power > 2 {
painter.circle_filled(c + Vec2::new(0.0, -height * 0.50 + offset), r / 2.5, color);
}
if number == 8 || number == 9 {
painter.circle_filled(c + Vec2::new(0.0, -height * 1.50 + offset), r, color);
} else if max_power > 3 {
painter.circle_filled(c + Vec2::new(0.0, -height * 1.50 + offset), r / 2.5, color);
}
}
fn paint_binary_rect(
&mut self,
size: f32,
number: u32,
max_power: usize,
ctx: &Context,
ui: &mut Ui,
) {
let full_height = size;
let height = full_height / 4.0;
let width = height * 1.5;
let offset = height / 2.0 - height / 8.0;
let (response, painter) =
ui.allocate_painter(Vec2::new(width, full_height + offset * 2.0), Sense::hover());
let color = ctx.style().visuals.text_color();
let stroke = Stroke::new(1.0, color);
let round_all = CornerRadius::same((response.rect.width() * 0.1) as u8);
let round_top = CornerRadius {
nw: round_all.nw,
ne: round_all.ne,
..Default::default()
};
let round_none = CornerRadius::ZERO;
let round_bottom = CornerRadius {
sw: round_all.nw,
se: round_all.ne,
..Default::default()
};
if max_power == 2 {
let mut rect = response
.rect
.shrink2(Vec2::new(stroke.width, stroke.width + offset));
rect.set_height(rect.height() - height * 2.0);
rect = rect.translate(Vec2::new(0.0, height * 2.0 + offset));
painter.rect_stroke(rect, round_all, stroke, StrokeKind::Outside);
} else if max_power == 3 {
let mut rect = response
.rect
.shrink2(Vec2::new(stroke.width, stroke.width + offset));
rect.set_height(rect.height() - height);
rect = rect.translate(Vec2::new(0.0, height + offset));
painter.rect_stroke(rect, round_all, stroke, StrokeKind::Outside);
} else {
let mut rect = response
.rect
.shrink2(Vec2::new(stroke.width, stroke.width + offset));
rect = rect.translate(Vec2::new(0.0, 0.0 + offset));
painter.rect_stroke(rect, round_all, stroke, StrokeKind::Outside);
}
let mut rect_bin = response.rect;
rect_bin.set_width(width);
if number == 1 || number == 5 || number == 9 {
rect_bin.set_height(height);
painter.rect_filled(
rect_bin.translate(Vec2::new(stroke.width, height * 3.0 + offset * 2.0)),
round_bottom,
color,
);
}
if number == 2 {
rect_bin.set_height(height);
painter.rect_filled(
rect_bin.translate(Vec2::new(stroke.width, height * 2.0 + offset * 2.0)),
if max_power == 2 {
round_top
} else {
round_none
},
color,
);
}
if number == 3 {
rect_bin.set_height(height * 2.0);
painter.rect_filled(
rect_bin.translate(Vec2::new(stroke.width, height * 2.0 + offset * 2.0)),
round_bottom,
color,
);
}
if number == 4 || number == 5 {
rect_bin.set_height(height);
painter.rect_filled(
rect_bin.translate(Vec2::new(stroke.width, height * 1.0 + offset * 2.0)),
if max_power == 3 {
round_top
} else {
round_none
},
color,
);
}
if number == 6 {
rect_bin.set_height(height * 2.0);
painter.rect_filled(
rect_bin.translate(Vec2::new(stroke.width, height * 1.0 + offset * 2.0)),
if max_power == 3 {
round_top
} else {
round_none
},
color,
);
}
if number == 7 {
rect_bin.set_height(height * 3.0);
painter.rect_filled(
rect_bin.translate(Vec2::new(stroke.width, height + offset * 2.0)),
if max_power == 3 {
round_all
} else {
round_bottom
},
color,
);
}
if number == 8 || number == 9 {
rect_bin.set_height(height);
painter.rect_filled(
rect_bin.translate(Vec2::new(stroke.width, 0.0 + offset * 2.0)),
round_top,
color,
);
}
}
}
impl BarWidget for Time {
fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
if self.enable {
let mut output = self.output();
if !output.label.is_empty() {
let use_binary_circle = output.label.starts_with('c');
let use_binary_rectangle = output.label.starts_with('r');
let mut layout_job = LayoutJob::simple(
match self.label_prefix {
LabelPrefix::Icon | LabelPrefix::IconAndText => output.icon,
LabelPrefix::None | LabelPrefix::Text => String::new(),
},
config.icon_font_id.clone(),
ctx.style().visuals.selection.stroke.color,
100.0,
);
if let LabelPrefix::Text | LabelPrefix::IconAndText = self.label_prefix {
output.label.insert_str(0, "TIME: ");
}
if !use_binary_circle && !use_binary_rectangle {
layout_job.append(
&output.label,
10.0,
TextFormat {
font_id: config.text_font_id.clone(),
color: ctx.style().visuals.text_color(),
valign: Align::Center,
..Default::default()
},
);
}
let font_id = config.icon_font_id.clone();
let is_reversed = matches!(config.alignment, Some(Alignment::Right));
config.apply_on_widget(false, ui, |ui| {
if SelectableFrame::new(false)
.show(ui, |ui| {
if !is_reversed {
ui.add(Label::new(layout_job.clone()).selectable(false));
}
if use_binary_circle || use_binary_rectangle {
let ordered_output = if is_reversed {
output.label.chars().rev().collect()
} else {
output.label
};
for (section_index, section) in
ordered_output.split(':').enumerate()
{
ui.scope(|ui| {
ui.spacing_mut().item_spacing = Vec2::splat(2.0);
for (number_index, number_char) in
section.chars().enumerate()
{
if let Some(number) = number_char.to_digit(10) {
// the hour is the second char in the first section (in reverse, it's in the last section)
let max_power = match (
is_reversed,
section_index,
number_index,
) {
(true, 2, 1) | (false, 0, 1) => 2,
(true, _, 1) | (false, _, 0) => 3,
_ => 4,
};
if use_binary_circle {
self.paint_binary_circle(
font_id.size,
number,
max_power,
ctx,
ui,
);
} else if use_binary_rectangle {
self.paint_binary_rect(
font_id.size,
number,
max_power,
ctx,
ui,
);
}
}
}
});
}
}
if is_reversed {
ui.add(Label::new(layout_job.clone()).selectable(false));
}
})
.clicked()
{
self.format.toggle()
}
});
}
}
}
}
@@ -1,110 +1,49 @@
use crate::widgets::applications::Applications;
use crate::widgets::applications::ApplicationsConfig;
use crate::widgets::battery::Battery;
use crate::widgets::battery::BatteryConfig;
use crate::widgets::cpu::Cpu;
use crate::widgets::cpu::CpuConfig;
use crate::widgets::date::Date;
use crate::widgets::date::DateConfig;
// use crate::widgets::keyboard::Keyboard;
// use crate::widgets::keyboard::KeyboardConfig;
use crate::widgets::komorebi::Komorebi;
use crate::widgets::komorebi::KomorebiConfig;
// use crate::widgets::media::Media;
// use crate::widgets::media::MediaConfig;
use crate::widgets::memory::Memory;
use crate::widgets::memory::MemoryConfig;
use crate::widgets::network::Network;
use crate::widgets::network::NetworkConfig;
use crate::widgets::storage::Storage;
use crate::widgets::storage::StorageConfig;
use crate::widgets::time::Time;
use crate::widgets::time::TimeConfig;
use crate::widgets::update::Update;
use crate::widgets::update::UpdateConfig;
use serde::Deserialize;
use serde::Serialize;
use eframe::egui::Context;
use eframe::egui::Ui;
pub use komorebi_bar_core::widgets::widget::BarWidget;
pub use komorebi_bar_core::widgets::widget::WidgetConfig;
use komorebi_bar_core::render::RenderConfig;
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Widget configuration
pub enum WidgetConfig {
/// Applications widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Applications"))]
Applications(ApplicationsConfig),
/// Battery widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Battery"))]
Battery(BatteryConfig),
/// CPU widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Cpu"))]
Cpu(CpuConfig),
/// Date widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Date"))]
Date(DateConfig),
// Keyboard(KeyboardConfig),
/// Komorebi widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Komorebi"))]
Komorebi(KomorebiConfig),
// Media(MediaConfig),
/// Memory widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Memory"))]
Memory(MemoryConfig),
/// Network widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Network"))]
Network(NetworkConfig),
/// Storage widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Storage"))]
Storage(StorageConfig),
/// Time widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Time"))]
Time(TimeConfig),
/// Update widget configuration
#[cfg_attr(feature = "schemars", schemars(title = "Update"))]
Update(UpdateConfig),
/// Stands in for a widget that macOS cannot run; `WidgetConfig::enabled` is false for those, so
/// this is never built from a configuration that the bar loads.
struct Unsupported;
impl BarWidget for Unsupported {
fn render(&mut self, _ctx: &Context, _ui: &mut Ui, _config: &mut RenderConfig) {}
}
impl WidgetConfig {
pub fn as_boxed_bar_widget(&self) -> Box<dyn BarWidget> {
/// Turns a widget configuration into the widget that renders it.
pub trait WidgetConfigExt {
fn as_boxed_bar_widget(&self) -> Box<dyn BarWidget>;
}
impl WidgetConfigExt for WidgetConfig {
fn as_boxed_bar_widget(&self) -> Box<dyn BarWidget> {
match self {
WidgetConfig::Applications(config) => Box::new(Applications::from(config)),
WidgetConfig::Battery(config) => Box::new(Battery::from(*config)),
WidgetConfig::Cpu(config) => Box::new(Cpu::from(*config)),
WidgetConfig::Date(config) => Box::new(Date::from(config.clone())),
// WidgetConfig::Keyboard(config) => Box::new(Keyboard::from(*config)),
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())),
WidgetConfig::Update(config) => Box::new(Update::from(*config)),
}
}
pub fn enabled(&self) -> bool {
match self {
WidgetConfig::Applications(config) => config.enable,
WidgetConfig::Battery(config) => config.enable,
WidgetConfig::Cpu(config) => config.enable,
WidgetConfig::Date(config) => config.enable,
// WidgetConfig::Keyboard(config) => config.enable,
WidgetConfig::Komorebi(config) => {
config.workspaces.as_ref().is_some_and(|w| w.enable)
|| config.layout.as_ref().is_some_and(|w| w.enable)
|| config.focused_container.as_ref().is_some_and(|w| w.enable)
|| config
.configuration_switcher
.as_ref()
.is_some_and(|w| w.enable)
WidgetConfig::Keyboard(_) | WidgetConfig::Media(_) | WidgetConfig::Systray(_) => {
Box::new(Unsupported)
}
// WidgetConfig::Media(config) => config.enable,
WidgetConfig::Memory(config) => config.enable,
WidgetConfig::Network(config) => config.enable,
WidgetConfig::Storage(config) => config.enable,
WidgetConfig::Time(config) => config.enable,
WidgetConfig::Update(config) => config.enable,
}
}
}
+1
View File
@@ -10,6 +10,7 @@ komorebi-backend = { workspace = true }
komorebi-collections = { workspace = true }
komorebi-layouts = { workspace = true }
komorebi-config = { workspace = true }
komorebi-settings = { workspace = true }
komorebi-protocol = { workspace = true }
komorebi-transport = { workspace = true }
quickxml_to_serde = { git = "https://github.com/LGUG2Z/quickxml_to_serde.git", version = "0.6.1" }
+7 -4
View File
@@ -72,6 +72,7 @@ pub mod process_command;
pub mod process_event;
pub mod reaper;
pub mod scrolling;
pub mod settings;
pub mod settle;
pub mod skylight;
pub mod splash;
@@ -159,10 +160,12 @@ lazy_static! {
Arc::new(Mutex::new(HashMap::new()));
}
pub static DEFAULT_WORKSPACE_PADDING: AtomicI32 = AtomicI32::new(5);
pub static DEFAULT_CONTAINER_PADDING: AtomicI32 = AtomicI32::new(5);
pub static DEFAULT_RESIZE_DELTA: i32 = 50;
pub static DEFAULT_MOUSE_FOLLOWS_FOCUS: bool = true;
pub static DEFAULT_WORKSPACE_PADDING: AtomicI32 =
AtomicI32::new(komorebi_settings::shared::PlatformDefaults::MACOS.workspace_padding);
pub static DEFAULT_CONTAINER_PADDING: AtomicI32 =
AtomicI32::new(komorebi_settings::shared::PlatformDefaults::MACOS.container_padding);
pub static DEFAULT_RESIZE_DELTA: i32 = komorebi_settings::shared::DEFAULT_RESIZE_DELTA;
pub static DEFAULT_MOUSE_FOLLOWS_FOCUS: bool = komorebi_settings::shared::DEFAULT_MOUSE_FOLLOWS_FOCUS;
pub const PUBLIC_KEY: [u8; 32] = [
0x5a, 0x69, 0x4a, 0xe1, 0x3c, 0x4b, 0xc8, 0x4e, 0xc3, 0x79, 0x0f, 0xab, 0x27, 0x6b, 0x7e, 0xdd,
+97
View File
@@ -0,0 +1,97 @@
//! The settings of this daemon, as one snapshot (plan step 10, `docs/dev/settings.md`).
//!
//! Nothing reads this yet: the process-wide statics are still the source of truth, and each
//! migration step moves one group of them (writers and readers together) into
//! [`settings()`]. Until then the value is the starting values of those statics.
use std::sync::LazyLock;
use komorebi_settings::Handle;
use komorebi_settings::Shared;
use komorebi_settings::shared::PlatformDefaults;
/// The part of the settings that only this platform has.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct MacosSettings {}
pub type Settings = komorebi_settings::Settings<MacosSettings>;
static SETTINGS: LazyLock<Handle<MacosSettings>> = LazyLock::new(|| {
Handle::new(Settings::new(
Shared::defaults_for(PlatformDefaults::MACOS),
MacosSettings::default(),
))
});
/// The settings of the running daemon: take a [`Handle::snapshot`] once per unit of work.
pub fn settings() -> &'static Handle<MacosSettings> {
&SETTINGS
}
#[cfg(test)]
mod tests {
use std::sync::atomic::Ordering;
use super::*;
use crate::border_manager;
// The statics are still the source of truth. These pin that the starting values of the
// settings are the starting values of the statics, so that moving a group over changes nothing.
// (They read the statics without any test writing them first: no test here sets them.)
#[test]
fn the_starting_settings_are_the_starting_values_of_the_statics() {
let shared = &settings().snapshot().shared;
assert_eq!(
shared.defaults.container_padding,
crate::DEFAULT_CONTAINER_PADDING.load(Ordering::SeqCst)
);
assert_eq!(
shared.defaults.workspace_padding,
crate::DEFAULT_WORKSPACE_PADDING.load(Ordering::SeqCst)
);
assert_eq!(shared.defaults.resize_delta, crate::DEFAULT_RESIZE_DELTA);
assert_eq!(
shared.defaults.mouse_follows_focus,
crate::DEFAULT_MOUSE_FOLLOWS_FOCUS
);
assert_eq!(
shared.border.enabled,
border_manager::BORDER_ENABLED.load(Ordering::SeqCst)
);
assert_eq!(
shared.border.width,
border_manager::BORDER_WIDTH.load(Ordering::SeqCst)
);
assert_eq!(
shared.border.offset,
border_manager::BORDER_OFFSET.load(Ordering::SeqCst)
);
assert_eq!(
shared.border.radius,
border_manager::BORDER_RADIUS.load(Ordering::SeqCst)
);
let colours = shared.border.colours;
assert_eq!(
colours.single,
border_manager::FOCUSED.load(Ordering::SeqCst)
);
assert_eq!(colours.stack, border_manager::STACK.load(Ordering::SeqCst));
assert_eq!(
colours.monocle,
border_manager::MONOCLE.load(Ordering::SeqCst)
);
assert_eq!(
colours.floating,
border_manager::FLOATING.load(Ordering::SeqCst)
);
assert_eq!(
colours.unfocused,
border_manager::UNFOCUSED.load(Ordering::SeqCst)
);
assert_eq!(
colours.unfocused_locked,
border_manager::UNFOCUSED_LOCKED.load(Ordering::SeqCst)
);
}
}