From 3b424dd707fe07e60938a265d039459aea28ab5c Mon Sep 17 00:00:00 2001 From: LGUG2Z Date: Fri, 30 Oct 2026 11:39:00 -0700 Subject: [PATCH] fix(config): accept an integer for an initial window placement rule PlacementTarget deserialized through String, so the integer the docs promise ("initial_window_placement_rules": 2) made the daemon exit at start with an untagged enum error while "2" worked. Accept integers as well as strings. The derived schema described an externally tagged object that neither form matches, so describe the type by hand and regenerate both schemas. --- komorebi/src/core/mod.rs | 91 ++++++++++++++++++++++++++++++++++++++-- schema.bar.json | 43 ++++++++----------- schema.json | 43 ++++++++----------- 3 files changed, 124 insertions(+), 53 deletions(-) diff --git a/komorebi/src/core/mod.rs b/komorebi/src/core/mod.rs index 90a39adc..abf128e6 100644 --- a/komorebi/src/core/mod.rs +++ b/komorebi/src/core/mod.rs @@ -541,7 +541,6 @@ pub enum WindowPlacement { /// /// NOTE: Integer indices are 1-based in the config for user-friendliness. #[derive(Clone, Debug, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub enum PlacementTarget { /// A named placement strategy Placement(WindowPlacement), @@ -582,6 +581,39 @@ impl PartialOrd for PlacementTarget { } } +// Written by hand because the (de)serialization is: a `WindowPlacement` name or a container index, +// as a string or as an integer, never the externally tagged object a derive would describe +#[cfg(feature = "schemars")] +impl schemars::JsonSchema for PlacementTarget { + fn schema_name() -> std::borrow::Cow<'static, str> { + "PlacementTarget".into() + } + + fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema { + schemars::json_schema!({ + "description": "A target position for window placement, used as a key in `InitialWindowPlacementRules::Rules`. + +Can be either a `WindowPlacement` variant name (e.g. `\"Primary\"`, `\"Last\"`) +or a 1-based container index (e.g. `\"1\"`, `\"3\"`, or the integer `3`). + +NOTE: Integer indices are 1-based in the config for user-friendliness.", + "anyOf": [ + generator.subschema_for::(), + { + "description": "A 1-based container index as a string", + "type": "string", + "pattern": "^[0-9]+$" + }, + { + "description": "A 1-based container index", + "type": "integer", + "minimum": 0 + } + ] + }) + } +} + impl Ord for PlacementTarget { fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.to_string().cmp(&other.to_string()) @@ -596,8 +628,33 @@ impl Serialize for PlacementTarget { impl<'de> Deserialize<'de> for PlacementTarget { fn deserialize>(deserializer: D) -> Result { - let s = String::deserialize(deserializer)?; - s.parse().map_err(serde::de::Error::custom) + struct PlacementTargetVisitor; + + impl serde::de::Visitor<'_> for PlacementTargetVisitor { + type Value = PlacementTarget; + + fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.write_str("a WindowPlacement name or a non-negative container index") + } + + fn visit_str(self, s: &str) -> Result { + s.parse().map_err(E::custom) + } + + fn visit_u64(self, n: u64) -> Result { + usize::try_from(n) + .map(PlacementTarget::Index) + .map_err(E::custom) + } + + fn visit_i64(self, n: i64) -> Result { + usize::try_from(n) + .map(PlacementTarget::Index) + .map_err(E::custom) + } + } + + deserializer.deserialize_any(PlacementTargetVisitor) } } @@ -779,4 +836,32 @@ mod tests { assert_eq!(path, PathBuf::from("/path/VALUE/d")); } + + #[test] + fn placement_target_accepts_a_number_or_a_string() { + let parse = |json: &str| serde_json::from_str::(json); + + let index = InitialWindowPlacementRules::Target(PlacementTarget::Index(2)); + assert_eq!(parse("2").unwrap(), index); + assert_eq!(parse(r#""2""#).unwrap(), index); + assert_eq!( + parse(r#""Primary""#).unwrap(), + InitialWindowPlacementRules::Target(PlacementTarget::Placement( + WindowPlacement::Primary + )) + ); + + assert!(parse("-1").is_err()); + assert!(parse("1.5").is_err()); + assert!(parse("true").is_err()); + + // the keys of the map form are always strings + let Ok(InitialWindowPlacementRules::Rules(rules)) = + parse(r#"{"2": {"kind": "Exe", "id": "a.exe"}, "Primary": []}"#) + else { + panic!("expected the map form"); + }; + assert!(rules.contains_key(&PlacementTarget::Index(2))); + assert!(rules.contains_key(&PlacementTarget::Placement(WindowPlacement::Primary))); + } } diff --git a/schema.bar.json b/schema.bar.json index f83cb720..8d0b4929 100644 --- a/schema.bar.json +++ b/schema.bar.json @@ -3173,6 +3173,14 @@ "type": "object", "additionalProperties": { "$ref": "#/$defs/PlacementMatchingRules" + }, + "patternProperties": { + "^[0-9]+$": { + "$ref": "#/$defs/PlacementMatchingRules" + }, + "^\\d+$": { + "$ref": "#/$defs/PlacementMatchingRules" + } } } ] @@ -4974,35 +4982,20 @@ ] }, "PlacementTarget": { - "description": "A target position for window placement, used as a key in `InitialWindowPlacementRules::Rules`.\n\nCan be either a `WindowPlacement` variant name (e.g. `\"Primary\"`, `\"Last\"`)\nor a 1-based container index (e.g. `\"1\"`, `\"3\"`).\n\nNOTE: Integer indices are 1-based in the config for user-friendliness.", - "oneOf": [ + "description": "A target position for window placement, used as a key in `InitialWindowPlacementRules::Rules`.\n\nCan be either a `WindowPlacement` variant name (e.g. `\"Primary\"`, `\"Last\"`)\nor a 1-based container index (e.g. `\"1\"`, `\"3\"`, or the integer `3`).\n\nNOTE: Integer indices are 1-based in the config for user-friendliness.", + "anyOf": [ { - "description": "A named placement strategy", - "type": "object", - "properties": { - "Placement": { - "$ref": "#/$defs/WindowPlacement" - } - }, - "additionalProperties": false, - "required": [ - "Placement" - ] + "$ref": "#/$defs/WindowPlacement" + }, + { + "description": "A 1-based container index as a string", + "type": "string", + "pattern": "^[0-9]+$" }, { "description": "A 1-based container index", - "type": "object", - "properties": { - "Index": { - "type": "integer", - "format": "uint", - "minimum": 0 - } - }, - "additionalProperties": false, - "required": [ - "Index" - ] + "type": "integer", + "minimum": 0 } ] }, diff --git a/schema.json b/schema.json index 690bf470..cd1ae73c 100644 --- a/schema.json +++ b/schema.json @@ -2883,6 +2883,14 @@ "type": "object", "additionalProperties": { "$ref": "#/$defs/PlacementMatchingRules" + }, + "patternProperties": { + "^[0-9]+$": { + "$ref": "#/$defs/PlacementMatchingRules" + }, + "^\\d+$": { + "$ref": "#/$defs/PlacementMatchingRules" + } } } ] @@ -3748,35 +3756,20 @@ ] }, "PlacementTarget": { - "description": "A target position for window placement, used as a key in `InitialWindowPlacementRules::Rules`.\n\nCan be either a `WindowPlacement` variant name (e.g. `\"Primary\"`, `\"Last\"`)\nor a 1-based container index (e.g. `\"1\"`, `\"3\"`).\n\nNOTE: Integer indices are 1-based in the config for user-friendliness.", - "oneOf": [ + "description": "A target position for window placement, used as a key in `InitialWindowPlacementRules::Rules`.\n\nCan be either a `WindowPlacement` variant name (e.g. `\"Primary\"`, `\"Last\"`)\nor a 1-based container index (e.g. `\"1\"`, `\"3\"`, or the integer `3`).\n\nNOTE: Integer indices are 1-based in the config for user-friendliness.", + "anyOf": [ { - "description": "A named placement strategy", - "type": "object", - "properties": { - "Placement": { - "$ref": "#/$defs/WindowPlacement" - } - }, - "additionalProperties": false, - "required": [ - "Placement" - ] + "$ref": "#/$defs/WindowPlacement" + }, + { + "description": "A 1-based container index as a string", + "type": "string", + "pattern": "^[0-9]+$" }, { "description": "A 1-based container index", - "type": "object", - "properties": { - "Index": { - "type": "integer", - "format": "uint", - "minimum": 0 - } - }, - "additionalProperties": false, - "required": [ - "Index" - ] + "type": "integer", + "minimum": 0 } ] },