* Add Live Activities as configurable bar items * Fix activity owner monitoring and Morph list interactions * Keep activity rows stable during updates and tighten their layout * Use subtle theme tint for activity cards * Tighten activity title and progress spacing * Use theme accent for active progress rings * Fit compact activities to measured titles and badge * Expose Live Activities display controls in Settings * Open activity terminals and streamline the Morph header * Give Morph continuous icons and prioritize workspace navigation * Show activity progress inside its ring * Inset activity progress rings and strengthen their stroke * Fix activity lifecycle, command output handling and list navigation * Reduce activity update work and harden publisher lifetimes * Set up Live Activities for the detected user shell during installation * Document Activities setup, daily use and troubleshooting * Preserve Morph idle width across empty activity updates * Fix dimming flicker, hide inactive music tab and consolidate Activities docs * Fix panel clearance across styles and match previews to theme settings * Prevent panel blur from repainting neighboring islands --------- Co-authored-by: Samuel Onoja <[email protected]>
265 lines
12 KiB
Rust
265 lines
12 KiB
Rust
use ferese_config::PanelStyle;
|
|
use ferese_config::panel::*;
|
|
|
|
#[test]
|
|
fn removed_panel_surface_fields_are_rejected() {
|
|
for source in [
|
|
"surface solid;",
|
|
"group-surface island;",
|
|
"background islands;",
|
|
"style continuous;",
|
|
"style islands;",
|
|
] {
|
|
let document = ferese_config::Document::parse(&format!("panel main {{ {source} }}")).unwrap();
|
|
assert!(
|
|
serde_json::from_value::<Vec<Panel>>(document.get("panels").unwrap().clone()).is_err(),
|
|
"{source}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn panel_surfaces_round_trip_without_group_overrides() {
|
|
let source = "// keep\npanel main { style floating; start { group nav { item overview kind=overview; }; }; center { group title { item title kind=focused-window; }; }; end { group media { item media kind=media; }; }; }";
|
|
let mut doc = ferese_config::Document::parse(source).unwrap();
|
|
let panels: Vec<Panel> = serde_json::from_value(doc.get("panels").unwrap().clone()).unwrap();
|
|
let panel = &panels[0];
|
|
assert_eq!(panel.group_surface(), GroupSurface::Island);
|
|
assert_eq!(panel.preset(), Some(PanelStyle::Floating));
|
|
doc.set("panels", serde_json::to_value(&panels).unwrap()).unwrap();
|
|
let restored = ferese_config::Document::parse(&doc.to_string()).unwrap();
|
|
let restored: Vec<Panel> = serde_json::from_value(restored.get("panels").unwrap().clone()).unwrap();
|
|
assert_eq!(restored, panels);
|
|
assert!(doc.to_string().contains("// keep"));
|
|
let mut preset = panel.clone();
|
|
let groups = [&panel.start, &panel.center, &panel.end].map(|zone| zone.groups.clone());
|
|
preset.apply_preset(PanelStyle::Flat);
|
|
assert_eq!(preset.preset(), Some(PanelStyle::Flat));
|
|
assert_eq!(
|
|
[&preset.start, &preset.center, &preset.end].map(|zone| zone.groups.clone()),
|
|
groups
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn group_surface_options_are_rejected_instead_of_inherited_or_ignored() {
|
|
for surface in ["none", "inset", "island", "default", "unset"] {
|
|
let doc = ferese_config::Document::parse(&format!(
|
|
"panel main {{ start {{ group nav {{ surface {surface}; item clock kind=clock; }} }} }}"
|
|
))
|
|
.unwrap();
|
|
let error = serde_json::from_value::<Vec<Panel>>(doc.get("panels").unwrap().clone())
|
|
.unwrap_err()
|
|
.to_string();
|
|
assert!(error.contains("unknown field `surface`"), "{error}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn composition_round_trips_kdl_and_edits_keep_ids_order_and_comments() {
|
|
let source = r#"// panel comment
|
|
panel "main" {
|
|
style "floating"
|
|
end {
|
|
group "status" {
|
|
padding 2 3
|
|
// network comment
|
|
item "network" kind="network" overflow="always"
|
|
item "local-clock" kind="clock"
|
|
item "second-clock" kind="clock" representation="compact"
|
|
}
|
|
}
|
|
}
|
|
animations { speed 0.8; }
|
|
"#;
|
|
let mut doc = ferese_config::Document::parse(source).unwrap();
|
|
let panels: Vec<Panel> = serde_json::from_value(doc.get("panels").unwrap().clone()).unwrap();
|
|
validate(&panels).unwrap();
|
|
assert_eq!(panels[0].end.groups[0].padding, [2, 3]);
|
|
assert!(panels[0].background_opacity.is_none());
|
|
assert_eq!(panels[0].end.groups[0].items[0].overflow, OverflowPolicy::Always);
|
|
doc.set("panels.0.end.groups.0.items.0.overflow", "never".into())
|
|
.unwrap();
|
|
let path = "panels.0.end.groups.0.items";
|
|
let mut items = doc.get(path).unwrap().as_array().unwrap().clone();
|
|
items.swap(0, 2);
|
|
doc.set(path, items.into()).unwrap();
|
|
let formatted = doc.to_string();
|
|
assert!(formatted.contains("// panel comment"));
|
|
assert!(formatted.contains("// network comment"));
|
|
let parsed = ferese_config::Document::parse(&formatted).unwrap();
|
|
let panels: Vec<Panel> = serde_json::from_value(parsed.get("panels").unwrap().clone()).unwrap();
|
|
validate(&panels).unwrap();
|
|
assert_eq!(
|
|
panels[0].end.groups[0]
|
|
.items
|
|
.iter()
|
|
.map(|item| item.id.0.as_str())
|
|
.collect::<Vec<_>>(),
|
|
["second-clock", "local-clock", "network"]
|
|
);
|
|
assert_eq!(
|
|
panels[0].item(&ItemId("network".into())).unwrap().overflow,
|
|
OverflowPolicy::Never
|
|
);
|
|
assert_eq!(parsed.get("animations.speed").unwrap(), &serde_json::json!(0.8));
|
|
let value = serde_json::to_value(&panels).unwrap();
|
|
doc.set("panels", value.clone()).unwrap();
|
|
let round_trip = ferese_config::Document::parse(&doc.to_string()).unwrap();
|
|
let restored: Vec<Panel> = serde_json::from_value(round_trip.get("panels").unwrap().clone()).unwrap();
|
|
assert_eq!(restored, panels);
|
|
doc.set(path, Vec::<serde_json::Value>::new().into()).unwrap();
|
|
assert!(doc.to_string().contains("// network comment"));
|
|
let restored: Vec<Panel> = serde_json::from_value(doc.get("panels").unwrap().clone()).unwrap();
|
|
assert!(restored[0].end.groups[0].items.is_empty());
|
|
assert_eq!(restored[0].end.spacing, 8.0);
|
|
}
|
|
|
|
#[test]
|
|
fn panel_border_defaults_off_and_round_trips_an_explicit_enabled_value() {
|
|
let mut doc =
|
|
ferese_config::Document::parse("panel main { end { group status { item clock kind=clock; }; }; }\n").unwrap();
|
|
let panels: Vec<Panel> = serde_json::from_value(doc.get("panels").unwrap().clone()).unwrap();
|
|
assert!(!panels[0].border);
|
|
doc.set("panels.0.border", true.into()).unwrap();
|
|
let doc = ferese_config::Document::parse(&doc.to_string()).unwrap();
|
|
let panels: Vec<Panel> = serde_json::from_value(doc.get("panels").unwrap().clone()).unwrap();
|
|
assert!(panels[0].border);
|
|
assert_eq!(panels[0].end.groups[0].items[0].id.0, "clock");
|
|
}
|
|
|
|
#[test]
|
|
fn panel_background_opacity_inherits_until_overridden_and_validates_bounds() {
|
|
let doc = ferese_config::Document::parse("panel main { end { group status { }; }; }").unwrap();
|
|
let mut panels: Vec<Panel> = serde_json::from_value(doc.get("panels").unwrap().clone()).unwrap();
|
|
assert_eq!(panels[0].background_opacity, None);
|
|
assert!(
|
|
serde_json::to_value(&panels[0]).unwrap()["end"]["groups"][0]
|
|
.get("background_opacity")
|
|
.is_none()
|
|
);
|
|
for value in [0., 0.5, 1.] {
|
|
panels[0].background_opacity = Some(value);
|
|
validate(&panels).unwrap();
|
|
}
|
|
for value in [-0.1, 1.1, f32::NAN, f32::INFINITY] {
|
|
panels[0].background_opacity = Some(value);
|
|
assert!(validate(&panels).is_err());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn panel_edge_defaults_to_top_and_round_trips_both_positions() {
|
|
for (source, expected) in [
|
|
("", Edge::Top),
|
|
("edge top;", Edge::Top),
|
|
("edge bottom;", Edge::Bottom),
|
|
] {
|
|
let document = ferese_config::Document::parse(&format!("panel main {{ {source} }}")).unwrap();
|
|
let panels: Vec<Panel> = serde_json::from_value(document.get("panels").unwrap().clone()).unwrap();
|
|
validate(&panels).unwrap();
|
|
assert_eq!(panels[0].edge, expected);
|
|
let restored: Vec<Panel> = serde_json::from_value(serde_json::to_value(&panels).unwrap()).unwrap();
|
|
assert_eq!(restored[0].edge, expected);
|
|
let mut moved = panels[0].clone();
|
|
moved.edge = if expected == Edge::Top { Edge::Bottom } else { Edge::Top };
|
|
assert!(!moved.same_composition(&panels[0]));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn validation_rejects_duplicate_ids_and_unsupported_surface_configuration() {
|
|
let mut panel = Panel::default();
|
|
validate(std::slice::from_ref(&panel)).unwrap();
|
|
panel.end.groups[0].items[1].id = panel.start.groups[0].items[0].id.clone();
|
|
assert!(validate(std::slice::from_ref(&panel)).is_err());
|
|
panel.end.groups[0].items[1].id = ItemId("_overflow".into());
|
|
assert!(validate(std::slice::from_ref(&panel)).is_err());
|
|
assert!(validate(&[]).is_err());
|
|
assert!(validate(&[panel.clone(), panel]).is_err());
|
|
assert!(ferese_config::from_str::<serde_json::Value>("panel \"main\" { edge \"bottom\"; }").is_ok());
|
|
let doc = ferese_config::Document::parse("panel \"main\" { edge \"left\"; }").unwrap();
|
|
assert!(serde_json::from_value::<Vec<Panel>>(doc.get("panels").unwrap().clone()).is_err());
|
|
}
|
|
#[test]
|
|
fn workspace_styles_round_trip_and_only_apply_to_workspaces() {
|
|
for style in WorkspaceStyle::ALL {
|
|
let source = format!(
|
|
"panel main {{ start {{ group nav {{ item spaces kind=\"workspaces\" style=\"{}\"; }} }} }}",
|
|
style.key()
|
|
);
|
|
let document = ferese_config::Document::parse(&source).unwrap();
|
|
let panels: Vec<Panel> = serde_json::from_value(document.get("panels").unwrap().clone()).unwrap();
|
|
validate(&panels).unwrap();
|
|
assert_eq!(panels[0].start.groups[0].items[0].kind, ItemKind::Workspaces { style });
|
|
let mut wrong_kind = serde_json::to_value(&panels[0].start.groups[0].items[0]).unwrap();
|
|
wrong_kind["kind"] = "clock".into();
|
|
assert!(serde_json::from_value::<Item>(wrong_kind).is_err());
|
|
let restored: Vec<Panel> = serde_json::from_value(serde_json::to_value(&panels).unwrap()).unwrap();
|
|
assert_eq!(restored, panels);
|
|
}
|
|
let mut item = serde_json::json!({"id": "spaces", "kind": "workspaces"});
|
|
assert_eq!(
|
|
serde_json::from_value::<Item>(item.clone()).unwrap().kind,
|
|
ItemKind::Workspaces {
|
|
style: WorkspaceStyle::Dots
|
|
}
|
|
);
|
|
item["style"] = "unsupported".into();
|
|
assert!(serde_json::from_value::<Item>(item).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn item_options_are_strict_and_metadata_is_explicit() {
|
|
for key in ["ovreflow", "visibile", "custom_note"] {
|
|
let mut value = serde_json::json!({"id":"network", "kind":"network"});
|
|
value[key] = "always".into();
|
|
let error = serde_json::from_value::<Item>(value).unwrap_err().to_string();
|
|
assert!(error.contains(key) && error.contains("unknown field"), "{error}");
|
|
}
|
|
let source = "panel main { start { group nav { item network kind=\"network\" { metadata { custom-note \"keep\"; overflow \"authored note\"; }; }; }; }; }";
|
|
let panel: Vec<Panel> = serde_json::from_value(
|
|
ferese_config::Document::parse(source)
|
|
.unwrap()
|
|
.get("panels")
|
|
.unwrap()
|
|
.clone(),
|
|
)
|
|
.unwrap();
|
|
let item = &panel[0].start.groups[0].items[0];
|
|
assert_eq!(item.metadata["custom_note"], "keep");
|
|
assert_eq!(item.overflow, OverflowPolicy::Auto);
|
|
assert_eq!(
|
|
serde_json::from_value::<Item>(serde_json::to_value(item).unwrap()).unwrap(),
|
|
*item
|
|
);
|
|
assert!(
|
|
serde_json::from_value::<Item>(serde_json::json!({"id":"network", "kind":"network", "metadata":"wrong"}))
|
|
.is_err()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn activities_are_first_class_items_in_every_zone_and_keep_overflow_settings() {
|
|
for style in ["flat", "floating", "morph", "adaptive"] {
|
|
for zone in ["start", "center", "end"] {
|
|
let source = format!(
|
|
"panel main {{ style {style}; {zone} {{ group tasks {{ item builds kind=activities priority=20 overflow=always; }}; }}; }}"
|
|
);
|
|
let mut doc = ferese_config::Document::parse(&source).unwrap();
|
|
let panels: Vec<Panel> = serde_json::from_value(doc.get("panels").unwrap().clone()).unwrap();
|
|
ferese_panel::validate(&panels).unwrap();
|
|
let parsed = match zone {
|
|
"start" => &panels[0].start,
|
|
"center" => &panels[0].center,
|
|
_ => &panels[0].end,
|
|
};
|
|
assert_eq!(parsed.groups[0].items[0].kind, ferese_panel::ItemKind::Activities);
|
|
assert_eq!(parsed.groups[0].items[0].overflow, ferese_panel::OverflowPolicy::Always);
|
|
doc.set("panels", serde_json::to_value(&panels).unwrap()).unwrap();
|
|
let restored = ferese_config::Document::parse(&doc.to_string()).unwrap();
|
|
assert_eq!(restored.get("panels"), doc.get("panels"));
|
|
}
|
|
}
|
|
}
|