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.
This commit is contained in:
LGUG2Z committed 2026-10-30 12:13:42 -07:00
1 parent 837ca257d4
commit 3b424dd707
3 files changed
+124 -53

No files matched your search

+88 -3
View File
@@ -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::<WindowPlacement>(),
{
"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<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
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<E: serde::de::Error>(self, s: &str) -> Result<Self::Value, E> {
s.parse().map_err(E::custom)
}
fn visit_u64<E: serde::de::Error>(self, n: u64) -> Result<Self::Value, E> {
usize::try_from(n)
.map(PlacementTarget::Index)
.map_err(E::custom)
}
fn visit_i64<E: serde::de::Error>(self, n: i64) -> Result<Self::Value, E> {
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::<InitialWindowPlacementRules>(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)));
}
}
+18 -25
View File
@@ -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
}
]
},
+18 -25
View File
@@ -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
}
]
},