From 280eea6fecb6d91a15c727e3fdec5bccf5e8fa9b Mon Sep 17 00:00:00 2001 From: LGUG2Z Date: Sat, 31 Oct 2026 10:00:04 -0700 Subject: [PATCH 01/27] feat(bar-core): per-platform views of one union bar schema The Windows schema.bar.json is the union of the bar configuration (every widget, the mouse options, Windows spelling). New module `schema` holds the macOS view as data: 16 changes (set text/defaults that read differently, remove the mouse property, the Keyboard/Media/Systray widget variants and the NetworkConfig default_interface_options) followed by pruning of the definitions nothing refers to any more. Same design as komorebi-config (schema.rs, changes.rs); the changes are idempotent so a bar can print its own generated schema through its view. Nothing uses the views yet; no bar source changed. Verified: native `cargo test -p komorebi-bar-core` 3 unit + 5 oracle passed (new: the macOS view of the checked-in Windows file is byte-identical to komorebi-for-mac/schema.bar.json; the Windows view is the Windows file; a view is idempotent). Windows clippy -D warnings clean; macOS fake-CC check 0 errors. Not verified: nothing rendered. --- crates/komorebi-bar-core/Cargo.toml | 2 +- crates/komorebi-bar-core/src/lib.rs | 2 + .../komorebi-bar-core/src/schema/changes.rs | 99 +++++++++++++++ crates/komorebi-bar-core/src/schema/mod.rs | 120 ++++++++++++++++++ .../komorebi-bar-core/tests/schema_oracle.rs | 45 +++++++ 5 files changed, 267 insertions(+), 1 deletion(-) create mode 100644 crates/komorebi-bar-core/src/schema/changes.rs create mode 100644 crates/komorebi-bar-core/src/schema/mod.rs diff --git a/crates/komorebi-bar-core/Cargo.toml b/crates/komorebi-bar-core/Cargo.toml index c339bcbe..9b5eb2d5 100644 --- a/crates/komorebi-bar-core/Cargo.toml +++ b/crates/komorebi-bar-core/Cargo.toml @@ -11,7 +11,7 @@ eframe = { workspace = true } egui-phosphor = { git = "https://github.com/amPerl/egui-phosphor", rev = "d13688738478ecd12b426e3e74c59d6577a85b59" } reqwest = { version = "0.12", features = ["blocking"] } serde = { workspace = true } -serde_json = { workspace = true } +serde_json = { workspace = true, features = ["preserve_order"] } starship-battery = "0.11" sysinfo = { workspace = true } diff --git a/crates/komorebi-bar-core/src/lib.rs b/crates/komorebi-bar-core/src/lib.rs index 9088d849..fcf4bf3a 100644 --- a/crates/komorebi-bar-core/src/lib.rs +++ b/crates/komorebi-bar-core/src/lib.rs @@ -18,3 +18,5 @@ pub mod render; pub mod widgets; pub mod platform; + +pub mod schema; diff --git a/crates/komorebi-bar-core/src/schema/changes.rs b/crates/komorebi-bar-core/src/schema/changes.rs new file mode 100644 index 00000000..8175560b --- /dev/null +++ b/crates/komorebi-bar-core/src/schema/changes.rs @@ -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")), +]; diff --git a/crates/komorebi-bar-core/src/schema/mod.rs b/crates/komorebi-bar-core/src/schema/mod.rs new file mode 100644 index 00000000..b22c3982 --- /dev/null +++ b/crates/komorebi-bar-core/src/schema/mod.rs @@ -0,0 +1,120 @@ +//! 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 = defs + .into_iter() + .filter(|(name, _)| reachable.contains(name)) + .collect(); + schema["$defs"] = Value::Object(kept); +} + +fn collect_refs(value: &Value, found: &mut Vec) { + 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)), + _ => {} + } +} diff --git a/crates/komorebi-bar-core/tests/schema_oracle.rs b/crates/komorebi-bar-core/tests/schema_oracle.rs index 510057e8..0a255c09 100644 --- a/crates/komorebi-bar-core/tests/schema_oracle.rs +++ b/crates/komorebi-bar-core/tests/schema_oracle.rs @@ -76,3 +76,48 @@ 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); + } +} From 352cda3be3ef5b1969b26428f31d717d7746add8 Mon Sep 17 00:00:00 2001 From: LGUG2Z Date: Sat, 31 Oct 2026 10:00:38 -0700 Subject: [PATCH 02/27] refactor(bar): print --schema through the platform view Both bars now print `schema.bar.json` as the view of the generated union schema (komorebi_bar_core::schema::generated_text): Windows through the Windows view (the identity), macOS through the macOS view. The sources of both bars are still separate, so the generated text is what it was and the macOS changes set what is already there (they are idempotent); this is the switch that lets the next revisions move code that differs only in schema text into the core in the Windows spelling. Verified: out-of-tree schema harness (S/bar2/regen.sh, real sources, bins updated to call generated_text): macOS byte-identical to komorebi-for-mac/schema.bar.json (3,774 lines), Windows byte-identical to schema.bar.json (9,091 lines). Native bar-core tests 3+5 passed, Windows clippy -D warnings clean, macOS fake-CC check 0 errors. The binaries were not run (cannot on Linux). --- crates/komorebi-bar-core/src/schema/mod.rs | 8 ++++++++ komorebi-bar/src/main.rs | 10 ++++++---- komorebi-for-mac/komorebi-bar/src/main.rs | 10 ++++++---- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/crates/komorebi-bar-core/src/schema/mod.rs b/crates/komorebi-bar-core/src/schema/mod.rs index b22c3982..ee284574 100644 --- a/crates/komorebi-bar-core/src/schema/mod.rs +++ b/crates/komorebi-bar-core/src/schema/mod.rs @@ -118,3 +118,11 @@ fn collect_refs(value: &Value, found: &mut Vec) { _ => {} } } + +/// The text `--schema` prints for the bar configuration type `T`, as `platform` checks it in. +#[cfg(feature = "schemars")] +#[must_use] +pub fn generated_text(platform: SchemaPlatform) -> String { + let union = serde_json::to_value(schemars::schema_for!(T)).expect("a schema is JSON"); + view_text(union, platform) +} diff --git a/komorebi-bar/src/main.rs b/komorebi-bar/src/main.rs index 89b25c77..b76e22a3 100644 --- a/komorebi-bar/src/main.rs +++ b/komorebi-bar/src/main.rs @@ -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::( + komorebi_bar_core::schema::SchemaPlatform::Windows + ) + ); std::process::exit(0); } diff --git a/komorebi-for-mac/komorebi-bar/src/main.rs b/komorebi-for-mac/komorebi-bar/src/main.rs index c08f444c..57e31bea 100644 --- a/komorebi-for-mac/komorebi-bar/src/main.rs +++ b/komorebi-for-mac/komorebi-bar/src/main.rs @@ -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::( + komorebi_bar_core::schema::SchemaPlatform::MacOs + ) + ); std::process::exit(0); } From bfddb6f0c3aa6869177a6c9c4d7dba822decf588 Mon Sep 17 00:00:00 2001 From: LGUG2Z Date: Sat, 31 Oct 2026 10:09:34 -0700 Subject: [PATCH 03/27] refactor(bar): converge storage.rs of both bars behind a platform hook The two storage.rs differed in the click handler and in four doc strings. The click now asks the platform (new SystemTool::StorageLocation(label)) and each shell keeps exactly what it spawned: Windows `cmd.exe /C explorer.exe `, macOS `open