bread-theme: validate surfaces.*.anchor like its width/layer siblings

anchor was the one enum-shaped schema field taken via
unwrap_or_default() with no manifest-time validation, while width and
layer both bail! on an unrecognized value (as does bar.window.anchors).
A typo'd anchor silently fell through to breadbar's own runtime
eprintln + unanchored-window fallback instead of the load-time hard
error every other typo'd key gets. Validate against the three shapes
breadbar/src/surface.rs actually implements.
This commit is contained in:
Breadway 2026-08-25 16:14:46 +08:00
parent 470d2aa7e9
commit a313eb518c
3 changed files with 81 additions and 1 deletions

View file

@ -414,6 +414,18 @@ fn resolve_surfaces(
let mut out = BTreeMap::new();
let Some(raw) = raw else { return Ok(out) };
for (namespace, s) in raw {
let anchor = match s.anchor.as_deref() {
Some(a @ ("top_right" | "bottom_centre" | "fill")) => a.to_string(),
Some(other) => bail!(
"theme '{theme_id}': surfaces.{namespace}.anchor = \"{other}\" is not \
top_right|bottom_centre|fill (the only shapes breadbar's satellite \
windows implement see breadbar/src/surface.rs)"
),
None => bail!(
"theme '{theme_id}': surfaces.{namespace} has no anchor set \
(expected one of top_right|bottom_centre|fill)"
),
};
let offset = match &s.offset {
None => vec![],
Some(RawOffset::Single(v)) => vec![*v],
@ -439,7 +451,7 @@ fn resolve_surfaces(
out.insert(
namespace.clone(),
Surface {
anchor: s.anchor.clone().unwrap_or_default(),
anchor,
offset,
width,
layer,

View file

@ -1035,6 +1035,67 @@ mod tests {
assert!(format!("{err:#}").contains("hexagon"));
}
#[test]
fn unknown_surface_anchor_is_a_hard_error_not_a_silent_default() {
// Before the fix, `surfaces.*.anchor` was the one enum-shaped
// field in the whole schema that skipped manifest-time validation
// entirely (`unwrap_or_default()`), unlike its siblings `width` and
// `layer` which both `bail!`. A typo here must fail exactly like
// any other typo'd enum value.
let xdg = isolated_xdg();
write_theme(
&xdg,
"badanchor",
r#"
id = "badanchor"
[surfaces."breadbar-notif"]
anchor = "top_lft"
width = 320
layer = "overlay"
"#,
);
let err = load_named("badanchor").expect_err("unknown anchor must be a hard error");
assert!(format!("{err:#}").contains("top_lft"));
}
#[test]
fn missing_surface_anchor_is_a_hard_error() {
let xdg = isolated_xdg();
write_theme(
&xdg,
"noanchor",
r#"
id = "noanchor"
[surfaces."breadbar-notif"]
width = 320
layer = "overlay"
"#,
);
let err = load_named("noanchor").expect_err("missing anchor must be a hard error");
assert!(format!("{err:#}").contains("no anchor set"));
}
#[test]
fn every_known_surface_anchor_shape_resolves() {
for anchor in ["top_right", "bottom_centre", "fill"] {
let xdg = isolated_xdg();
write_theme(
&xdg,
"okanchor",
&format!(
r#"
id = "okanchor"
[surfaces."breadbar-notif"]
anchor = "{anchor}"
"#
),
);
let theme = load_named("okanchor")
.unwrap_or_else(|e| panic!("anchor {anchor} should resolve: {e:#}"));
assert_eq!(theme.surfaces()["breadbar-notif"].anchor, anchor);
}
}
// ---- fallback on broken theme -----------------------------------------
#[test]

View file

@ -200,6 +200,13 @@ pub struct Launcher {
/// one lookup.
#[derive(Debug, Clone, PartialEq)]
pub struct Surface {
/// One of `top_right`/`bottom_centre`/`fill` — validated in
/// `manifest.rs::resolve_surfaces` against the shapes
/// `breadbar/src/surface.rs::apply` actually implements, the same
/// "typo'd key is a hard error" policy this field's siblings
/// (`width`, `layer`) already got. Required, not defaulted: a surface
/// entry with no anchor at all is as much a hard `theme.toml` error as
/// an unrecognized one.
pub anchor: String,
pub offset: Vec<f64>,
pub width: SurfaceWidth,