From 280eea6fecb6d91a15c727e3fdec5bccf5e8fa9b Mon Sep 17 00:00:00 2001 From: LGUG2Z Date: Sat, 31 Oct 2026 10:00:04 -0700 Subject: [PATCH] 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); + } +}