mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-11-14 21:04:04 +01:00
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:
commit
1b3b33fb2c
59 files changed
+3566
-3722
No files matched your search
Generated
+19
@@ -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"
|
||||
|
||||
@@ -8,6 +8,7 @@ members = [
|
||||
"crates/komorebi-backend",
|
||||
"crates/komorebi-collections",
|
||||
"crates/komorebi-config",
|
||||
"crates/komorebi-settings",
|
||||
"komorebi-layouts",
|
||||
"komorebi-scrolling",
|
||||
"komorebic",
|
||||
|
||||
@@ -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::*;
|
||||
@@ -18,3 +18,5 @@ pub mod render;
|
||||
pub mod widgets;
|
||||
|
||||
pub mod platform;
|
||||
|
||||
pub mod schema;
|
||||
@@ -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")),
|
||||
];
|
||||
@@ -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)
|
||||
}
|
||||
+4
-34
@@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
+46
-85
@@ -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>,
|
||||
}
|
||||
+5
-9
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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"))])
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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`).
|
||||
@@ -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.
|
||||
@@ -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
@@ -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::*;
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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);
|
||||
|
||||
@@ -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,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(
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+12
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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
|
||||
///
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
Generated
+20
@@ -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"
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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 +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::*;
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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" }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user