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.
This commit is contained in:
LGUG2Z committed 2026-10-31 10:00:37 -07:00
1 parent 633e7453cd
commit 280eea6fec
5 files changed
+267 -1

No files matched your search

+1 -1
View File
@@ -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 }
+2
View File
@@ -18,3 +18,5 @@ pub mod render;
pub mod widgets;
pub mod platform;
pub mod schema;
@@ -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")),
];
+120
View File
@@ -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<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)),
_ => {}
}
}
@@ -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);
}
}