atagen

monolith

public
Star0
Watch0
783f8b1b03c5bfb6f8d42f01112fbf7048725180
1
ssh://brewery@pls.lobotomise.me:33/atagen/monolith.git

Blame: crates/planner/src/plan.rs

b563d2da1c80atagen2026-10-16 08:25 UTC1
use crate::geom::{Change, Rect};
b563d2da1c80atagen2026-10-16 08:25 UTC2
use crate::strategy::plan_group;
b563d2da1c80atagen2026-10-16 08:25 UTC3
b563d2da1c80atagen2026-10-16 08:25 UTC4
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
b563d2da1c80atagen2026-10-16 08:25 UTC5
pub enum Style {
b563d2da1c80atagen2026-10-16 08:25 UTC6
    /// Every window moves or resizes along one axis per phase.
b563d2da1c80atagen2026-10-16 08:25 UTC7
    Orthogonal,
b563d2da1c80atagen2026-10-16 08:25 UTC8
    /// Windows may change both axes in one phase when no pair loses its separation.
b563d2da1c80atagen2026-10-16 08:25 UTC9
    Direct,
b563d2da1c80atagen2026-10-16 08:25 UTC10
}
b563d2da1c80atagen2026-10-16 08:25 UTC11
b563d2da1c80atagen2026-10-16 08:25 UTC12
#[derive(Clone, Copy, Debug, PartialEq)]
b563d2da1c80atagen2026-10-16 08:25 UTC13
pub struct Config {
b563d2da1c80atagen2026-10-16 08:25 UTC14
    pub style: Style,
b563d2da1c80atagen2026-10-16 08:25 UTC15
    /// Gap between tiled windows. Neighbours closer than this are adjacent.
b563d2da1c80atagen2026-10-16 08:25 UTC16
    pub gap: f64,
b563d2da1c80atagen2026-10-16 08:25 UTC17
    /// Tolerance for separation and adjacency tests.
b563d2da1c80atagen2026-10-16 08:25 UTC18
    pub eps: f64,
b563d2da1c80atagen2026-10-16 08:25 UTC19
    /// Lanes thinner than this are not used.
b563d2da1c80atagen2026-10-16 08:25 UTC20
    pub min_lane: f64,
b563d2da1c80atagen2026-10-16 08:25 UTC21
}
b563d2da1c80atagen2026-10-16 08:25 UTC22
b563d2da1c80atagen2026-10-16 08:25 UTC23
impl Default for Config {
b563d2da1c80atagen2026-10-16 08:25 UTC24
    fn default() -> Self {
b563d2da1c80atagen2026-10-16 08:25 UTC25
        Self {
b563d2da1c80atagen2026-10-16 08:25 UTC26
            style: Style::Orthogonal,
b563d2da1c80atagen2026-10-16 08:25 UTC27
            gap: 10.0,
b563d2da1c80atagen2026-10-16 08:25 UTC28
            eps: 0.5,
b563d2da1c80atagen2026-10-16 08:25 UTC29
            min_lane: 6.0,
b563d2da1c80atagen2026-10-16 08:25 UTC30
        }
b563d2da1c80atagen2026-10-16 08:25 UTC31
    }
b563d2da1c80atagen2026-10-16 08:25 UTC32
}
b563d2da1c80atagen2026-10-16 08:25 UTC33
b563d2da1c80atagen2026-10-16 08:25 UTC34
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
b563d2da1c80atagen2026-10-16 08:25 UTC35
pub enum Strategy {
b563d2da1c80atagen2026-10-16 08:25 UTC36
    Unchanged,
b563d2da1c80atagen2026-10-16 08:25 UTC37
    Direct,
b563d2da1c80atagen2026-10-16 08:25 UTC38
    AxisOrder,
b563d2da1c80atagen2026-10-16 08:25 UTC39
    Lanes { lanes: usize },
b563d2da1c80atagen2026-10-16 08:25 UTC40
    Grid { cells: usize },
b563d2da1c80atagen2026-10-16 08:25 UTC41
    Pinch,
b563d2da1c80atagen2026-10-16 08:25 UTC42
}
b563d2da1c80atagen2026-10-16 08:25 UTC43
b563d2da1c80atagen2026-10-16 08:25 UTC44
/// Windows that plan together. Groups never touch each other while moving.
b563d2da1c80atagen2026-10-16 08:25 UTC45
#[derive(Clone, Debug, PartialEq)]
b563d2da1c80atagen2026-10-16 08:25 UTC46
pub struct Group {
b563d2da1c80atagen2026-10-16 08:25 UTC47
    pub members: Vec<usize>,
b563d2da1c80atagen2026-10-16 08:25 UTC48
    pub strategy: Strategy,
b563d2da1c80atagen2026-10-16 08:25 UTC49
    pub phases: usize,
b563d2da1c80atagen2026-10-16 08:25 UTC50
    /// Whether the plan briefly separates two neighbours that touch at both ends.
b563d2da1c80atagen2026-10-16 08:25 UTC51
    pub opens_hole: bool,
b563d2da1c80atagen2026-10-16 08:25 UTC52
}
b563d2da1c80atagen2026-10-16 08:25 UTC53
b563d2da1c80atagen2026-10-16 08:25 UTC54
/// Keyframe layouts indexed like the input. Every phase lasts the same time
b563d2da1c80atagen2026-10-16 08:25 UTC55
/// and every window in a group moves on that group's curve.
b563d2da1c80atagen2026-10-16 08:25 UTC56
#[derive(Clone, Debug, PartialEq)]
b563d2da1c80atagen2026-10-16 08:25 UTC57
pub struct Plan {
b563d2da1c80atagen2026-10-16 08:25 UTC58
    pub keyframes: Vec<Vec<Rect>>,
b563d2da1c80atagen2026-10-16 08:25 UTC59
    pub groups: Vec<Group>,
b563d2da1c80atagen2026-10-16 08:25 UTC60
}
b563d2da1c80atagen2026-10-16 08:25 UTC61
b563d2da1c80atagen2026-10-16 08:25 UTC62
impl Plan {
b563d2da1c80atagen2026-10-16 08:25 UTC63
    pub fn phases(&self) -> usize {
b563d2da1c80atagen2026-10-16 08:25 UTC64
        self.keyframes.len().saturating_sub(1)
b563d2da1c80atagen2026-10-16 08:25 UTC65
    }
b563d2da1c80atagen2026-10-16 08:25 UTC66
b563d2da1c80atagen2026-10-16 08:25 UTC67
    /// Window positions at `phase` with eased progress `s`.
b563d2da1c80atagen2026-10-16 08:25 UTC68
    pub fn sample(&self, phase: usize, s: f64) -> Vec<Rect> {
b563d2da1c80atagen2026-10-16 08:25 UTC69
        match (self.keyframes.get(phase), self.keyframes.get(phase + 1)) {
b563d2da1c80atagen2026-10-16 08:25 UTC70
            (Some(a), Some(b)) => a.iter().zip(b).map(|(a, b)| a.lerp(b, s)).collect(),
b563d2da1c80atagen2026-10-16 08:25 UTC71
            _ => self.keyframes.last().cloned().unwrap_or_default(),
b563d2da1c80atagen2026-10-16 08:25 UTC72
        }
b563d2da1c80atagen2026-10-16 08:25 UTC73
    }
b563d2da1c80atagen2026-10-16 08:25 UTC74
}
b563d2da1c80atagen2026-10-16 08:25 UTC75
b563d2da1c80atagen2026-10-16 08:25 UTC76
pub(crate) struct GroupPlan {
b563d2da1c80atagen2026-10-16 08:25 UTC77
    pub keys: Vec<Vec<Rect>>,
b563d2da1c80atagen2026-10-16 08:25 UTC78
    pub strategy: Strategy,
b563d2da1c80atagen2026-10-16 08:25 UTC79
    pub opens_hole: bool,
b563d2da1c80atagen2026-10-16 08:25 UTC80
}
b563d2da1c80atagen2026-10-16 08:25 UTC81
b563d2da1c80atagen2026-10-16 08:25 UTC82
impl GroupPlan {
b563d2da1c80atagen2026-10-16 08:25 UTC83
    pub fn reversed(mut self) -> Self {
b563d2da1c80atagen2026-10-16 08:25 UTC84
        self.keys.reverse();
b563d2da1c80atagen2026-10-16 08:25 UTC85
        self
b563d2da1c80atagen2026-10-16 08:25 UTC86
    }
b563d2da1c80atagen2026-10-16 08:25 UTC87
}
b563d2da1c80atagen2026-10-16 08:25 UTC88
b563d2da1c80atagen2026-10-16 08:25 UTC89
/// Plans the transition from `from` to `to`. Both slices index the same windows.
ab9ba6872885atagen2026-10-19 10:38 UTC90
/// When some windows appear while others vanish in the same place, no single
ab9ba6872885atagen2026-10-19 10:38 UTC91
/// set of phases keeps them apart, so the vanishing ones finish first.
ab9ba6872885atagen2026-10-19 10:38 UTC92
pub fn plan(from: &[Rect], to: &[Rect], cfg: &Config) -> Plan {
ab9ba6872885atagen2026-10-19 10:38 UTC93
    let direct = plan_once(from, to, cfg);
ab9ba6872885atagen2026-10-19 10:38 UTC94
    if !direct.groups.iter().any(|g| g.strategy == Strategy::Pinch) {
ab9ba6872885atagen2026-10-19 10:38 UTC95
        return direct;
ab9ba6872885atagen2026-10-19 10:38 UTC96
    }
ab9ba6872885atagen2026-10-19 10:38 UTC97
    staged(from, to, cfg).unwrap_or(direct)
ab9ba6872885atagen2026-10-19 10:38 UTC98
}
ab9ba6872885atagen2026-10-19 10:38 UTC99
ab9ba6872885atagen2026-10-19 10:38 UTC100
/// Plans everything but the windows that appear, then lets those grow.
ab9ba6872885atagen2026-10-19 10:38 UTC101
/// A window that appears from a seed sits inside the space a vanishing one
ab9ba6872885atagen2026-10-19 10:38 UTC102
/// still fills, and the exact overlap check rejects growing into it.
ab9ba6872885atagen2026-10-19 10:38 UTC103
fn staged(from: &[Rect], to: &[Rect], cfg: &Config) -> Option<Plan> {
ab9ba6872885atagen2026-10-19 10:38 UTC104
    let appears = |i: usize| is_empty(&from[i], cfg) && !is_empty(&to[i], cfg);
ab9ba6872885atagen2026-10-19 10:38 UTC105
    if !(0..from.len()).any(appears) {
ab9ba6872885atagen2026-10-19 10:38 UTC106
        return None;
ab9ba6872885atagen2026-10-19 10:38 UTC107
    }
ab9ba6872885atagen2026-10-19 10:38 UTC108
    let mid: Vec<Rect> = (0..from.len())
ab9ba6872885atagen2026-10-19 10:38 UTC109
        .map(|i| if appears(i) { from[i] } else { to[i] })
ab9ba6872885atagen2026-10-19 10:38 UTC110
        .collect();
ab9ba6872885atagen2026-10-19 10:38 UTC111
    let (first, second) = (plan_once(from, &mid, cfg), plan_once(&mid, to, cfg));
ab9ba6872885atagen2026-10-19 10:38 UTC112
    let groups: Vec<Group> = first.groups.into_iter().chain(second.groups).collect();
ab9ba6872885atagen2026-10-19 10:38 UTC113
    if groups.iter().any(|g| g.strategy == Strategy::Pinch) {
ab9ba6872885atagen2026-10-19 10:38 UTC114
        return None;
ab9ba6872885atagen2026-10-19 10:38 UTC115
    }
ab9ba6872885atagen2026-10-19 10:38 UTC116
    let mut keyframes = first.keyframes;
ab9ba6872885atagen2026-10-19 10:38 UTC117
    keyframes.extend(second.keyframes.into_iter().skip(1));
ab9ba6872885atagen2026-10-19 10:38 UTC118
    Some(Plan { keyframes, groups })
ab9ba6872885atagen2026-10-19 10:38 UTC119
}
ab9ba6872885atagen2026-10-19 10:38 UTC120
ab9ba6872885atagen2026-10-19 10:38 UTC121
fn is_empty(r: &Rect, cfg: &Config) -> bool {
ab9ba6872885atagen2026-10-19 10:38 UTC122
    r.w <= cfg.eps || r.h <= cfg.eps
ab9ba6872885atagen2026-10-19 10:38 UTC123
}
ab9ba6872885atagen2026-10-19 10:38 UTC124
1a10e81ae5c1atagen2026-10-17 13:31 UTC125
/// Windows with no area at either end, like hidden ones held at their seed,
1a10e81ae5c1atagen2026-10-17 13:31 UTC126
/// jump straight to `to`. Planning them would let an invisible point inside
1a10e81ae5c1atagen2026-10-17 13:31 UTC127
/// a visible window join its group, or block every strategy and pinch.
ab9ba6872885atagen2026-10-19 10:38 UTC128
fn plan_once(from: &[Rect], to: &[Rect], cfg: &Config) -> Plan {
b563d2da1c80atagen2026-10-16 08:25 UTC129
    assert_eq!(
b563d2da1c80atagen2026-10-16 08:25 UTC130
        from.len(),
b563d2da1c80atagen2026-10-16 08:25 UTC131
        to.len(),
b563d2da1c80atagen2026-10-16 08:25 UTC132
        "layouts must describe the same windows"
b563d2da1c80atagen2026-10-16 08:25 UTC133
    );
1a10e81ae5c1atagen2026-10-17 13:31 UTC134
    let live: Vec<usize> = (0..from.len())
ab9ba6872885atagen2026-10-19 10:38 UTC135
        .filter(|&i| !(is_empty(&from[i], cfg) && is_empty(&to[i], cfg)))
1a10e81ae5c1atagen2026-10-17 13:31 UTC136
        .collect();
1a10e81ae5c1atagen2026-10-17 13:31 UTC137
    if live.len() == from.len() {
1a10e81ae5c1atagen2026-10-17 13:31 UTC138
        return plan_live(from, to, cfg);
1a10e81ae5c1atagen2026-10-17 13:31 UTC139
    }
1a10e81ae5c1atagen2026-10-17 13:31 UTC140
    let pick = |rects: &[Rect]| live.iter().map(|&i| rects[i]).collect::<Vec<_>>();
1a10e81ae5c1atagen2026-10-17 13:31 UTC141
    let inner = plan_live(&pick(from), &pick(to), cfg);
1a10e81ae5c1atagen2026-10-17 13:31 UTC142
    let keyframes = inner
1a10e81ae5c1atagen2026-10-17 13:31 UTC143
        .keyframes
1a10e81ae5c1atagen2026-10-17 13:31 UTC144
        .iter()
1a10e81ae5c1atagen2026-10-17 13:31 UTC145
        .map(|key| {
1a10e81ae5c1atagen2026-10-17 13:31 UTC146
            let mut frame = to.to_vec();
1a10e81ae5c1atagen2026-10-17 13:31 UTC147
            for (k, &i) in live.iter().enumerate() {
1a10e81ae5c1atagen2026-10-17 13:31 UTC148
                frame[i] = key[k];
1a10e81ae5c1atagen2026-10-17 13:31 UTC149
            }
1a10e81ae5c1atagen2026-10-17 13:31 UTC150
            frame
1a10e81ae5c1atagen2026-10-17 13:31 UTC151
        })
1a10e81ae5c1atagen2026-10-17 13:31 UTC152
        .collect();
1a10e81ae5c1atagen2026-10-17 13:31 UTC153
    let groups = inner
1a10e81ae5c1atagen2026-10-17 13:31 UTC154
        .groups
1a10e81ae5c1atagen2026-10-17 13:31 UTC155
        .into_iter()
1a10e81ae5c1atagen2026-10-17 13:31 UTC156
        .map(|g| Group {
1a10e81ae5c1atagen2026-10-17 13:31 UTC157
            members: g.members.iter().map(|&k| live[k]).collect(),
1a10e81ae5c1atagen2026-10-17 13:31 UTC158
            ..g
1a10e81ae5c1atagen2026-10-17 13:31 UTC159
        })
1a10e81ae5c1atagen2026-10-17 13:31 UTC160
        .collect();
1a10e81ae5c1atagen2026-10-17 13:31 UTC161
    Plan { keyframes, groups }
1a10e81ae5c1atagen2026-10-17 13:31 UTC162
}
1a10e81ae5c1atagen2026-10-17 13:31 UTC163
1a10e81ae5c1atagen2026-10-17 13:31 UTC164
fn plan_live(from: &[Rect], to: &[Rect], cfg: &Config) -> Plan {
b563d2da1c80atagen2026-10-16 08:25 UTC165
    let mut groups = Vec::new();
b563d2da1c80atagen2026-10-16 08:25 UTC166
    let mut plans = Vec::new();
b563d2da1c80atagen2026-10-16 08:25 UTC167
    for members in motion_groups(from, to, cfg) {
b563d2da1c80atagen2026-10-16 08:25 UTC168
        let f: Vec<Rect> = members.iter().map(|&i| from[i]).collect();
b563d2da1c80atagen2026-10-16 08:25 UTC169
        let t: Vec<Rect> = members.iter().map(|&i| to[i]).collect();
b563d2da1c80atagen2026-10-16 08:25 UTC170
        let gp = plan_canonical(&f, &t, cfg);
b563d2da1c80atagen2026-10-16 08:25 UTC171
        groups.push(Group {
b563d2da1c80atagen2026-10-16 08:25 UTC172
            members: members.clone(),
b563d2da1c80atagen2026-10-16 08:25 UTC173
            strategy: gp.strategy,
b563d2da1c80atagen2026-10-16 08:25 UTC174
            phases: gp.keys.len() - 1,
b563d2da1c80atagen2026-10-16 08:25 UTC175
            opens_hole: gp.opens_hole,
b563d2da1c80atagen2026-10-16 08:25 UTC176
        });
b563d2da1c80atagen2026-10-16 08:25 UTC177
        plans.push((members, gp));
b563d2da1c80atagen2026-10-16 08:25 UTC178
    }
b563d2da1c80atagen2026-10-16 08:25 UTC179
    let phases = groups.iter().map(|g| g.phases).max().unwrap_or(0);
b563d2da1c80atagen2026-10-16 08:25 UTC180
    let mut keyframes = vec![to.to_vec(); phases + 1];
b563d2da1c80atagen2026-10-16 08:25 UTC181
    for (members, gp) in &plans {
b563d2da1c80atagen2026-10-16 08:25 UTC182
        for (k, frame) in keyframes.iter_mut().enumerate() {
b563d2da1c80atagen2026-10-16 08:25 UTC183
            let key = &gp.keys[k.min(gp.keys.len() - 1)];
b563d2da1c80atagen2026-10-16 08:25 UTC184
            for (local, &i) in members.iter().enumerate() {
b563d2da1c80atagen2026-10-16 08:25 UTC185
                frame[i] = key[local];
b563d2da1c80atagen2026-10-16 08:25 UTC186
            }
b563d2da1c80atagen2026-10-16 08:25 UTC187
        }
b563d2da1c80atagen2026-10-16 08:25 UTC188
    }
b563d2da1c80atagen2026-10-16 08:25 UTC189
    Plan { keyframes, groups }
b563d2da1c80atagen2026-10-16 08:25 UTC190
}
b563d2da1c80atagen2026-10-16 08:25 UTC191
b563d2da1c80atagen2026-10-16 08:25 UTC192
/// Plans in one fixed direction per pair of layouts, so planning back plays
b563d2da1c80atagen2026-10-16 08:25 UTC193
/// the same plan in reverse.
b563d2da1c80atagen2026-10-16 08:25 UTC194
pub(crate) fn plan_canonical(from: &[Rect], to: &[Rect], cfg: &Config) -> GroupPlan {
b563d2da1c80atagen2026-10-16 08:25 UTC195
    if canonical_order(from, to).is_le() {
b563d2da1c80atagen2026-10-16 08:25 UTC196
        plan_group(from, to, cfg)
b563d2da1c80atagen2026-10-16 08:25 UTC197
    } else {
b563d2da1c80atagen2026-10-16 08:25 UTC198
        plan_group(to, from, cfg).reversed()
b563d2da1c80atagen2026-10-16 08:25 UTC199
    }
b563d2da1c80atagen2026-10-16 08:25 UTC200
}
b563d2da1c80atagen2026-10-16 08:25 UTC201
b563d2da1c80atagen2026-10-16 08:25 UTC202
fn canonical_order(a: &[Rect], b: &[Rect]) -> std::cmp::Ordering {
b563d2da1c80atagen2026-10-16 08:25 UTC203
    let q = |v: f64| (v * 10.0).round() as i64;
b563d2da1c80atagen2026-10-16 08:25 UTC204
    let key = |r: &Rect| [q(r.x), q(r.y), q(r.w), q(r.h)];
b563d2da1c80atagen2026-10-16 08:25 UTC205
    a.iter().map(key).cmp(b.iter().map(key))
b563d2da1c80atagen2026-10-16 08:25 UTC206
}
b563d2da1c80atagen2026-10-16 08:25 UTC207
b563d2da1c80atagen2026-10-16 08:25 UTC208
/// Partitions windows so that windows in different groups can never meet.
b563d2da1c80atagen2026-10-16 08:25 UTC209
/// A moving window joins every window its swept area comes within a gap of.
b563d2da1c80atagen2026-10-16 08:25 UTC210
fn motion_groups(from: &[Rect], to: &[Rect], cfg: &Config) -> Vec<Vec<usize>> {
b563d2da1c80atagen2026-10-16 08:25 UTC211
    let n = from.len();
b563d2da1c80atagen2026-10-16 08:25 UTC212
    let swept: Vec<Rect> = (0..n)
b563d2da1c80atagen2026-10-16 08:25 UTC213
        .map(|i| Rect::bounding([&from[i], &to[i]]).expanded(cfg.gap / 2.0 + cfg.eps))
b563d2da1c80atagen2026-10-16 08:25 UTC214
        .collect();
b563d2da1c80atagen2026-10-16 08:25 UTC215
    let moves: Vec<bool> = (0..n)
b563d2da1c80atagen2026-10-16 08:25 UTC216
        .map(|i| Change::between(&from[i], &to[i]).moves())
b563d2da1c80atagen2026-10-16 08:25 UTC217
        .collect();
b563d2da1c80atagen2026-10-16 08:25 UTC218
    let mut parent: Vec<usize> = (0..n).collect();
b563d2da1c80atagen2026-10-16 08:25 UTC219
    fn find(parent: &mut [usize], mut i: usize) -> usize {
b563d2da1c80atagen2026-10-16 08:25 UTC220
        while parent[i] != i {
b563d2da1c80atagen2026-10-16 08:25 UTC221
            parent[i] = parent[parent[i]];
b563d2da1c80atagen2026-10-16 08:25 UTC222
            i = parent[i];
b563d2da1c80atagen2026-10-16 08:25 UTC223
        }
b563d2da1c80atagen2026-10-16 08:25 UTC224
        i
b563d2da1c80atagen2026-10-16 08:25 UTC225
    }
b563d2da1c80atagen2026-10-16 08:25 UTC226
    for i in 0..n {
b563d2da1c80atagen2026-10-16 08:25 UTC227
        for j in i + 1..n {
b563d2da1c80atagen2026-10-16 08:25 UTC228
            if (moves[i] || moves[j]) && swept[i].intersects(&swept[j]) {
b563d2da1c80atagen2026-10-16 08:25 UTC229
                let (a, b) = (find(&mut parent, i), find(&mut parent, j));
b563d2da1c80atagen2026-10-16 08:25 UTC230
                parent[a] = b;
b563d2da1c80atagen2026-10-16 08:25 UTC231
            }
b563d2da1c80atagen2026-10-16 08:25 UTC232
        }
b563d2da1c80atagen2026-10-16 08:25 UTC233
    }
b563d2da1c80atagen2026-10-16 08:25 UTC234
    let mut groups: Vec<Vec<usize>> = Vec::new();
b563d2da1c80atagen2026-10-16 08:25 UTC235
    let mut slot = vec![usize::MAX; n];
b563d2da1c80atagen2026-10-16 08:25 UTC236
    for i in 0..n {
b563d2da1c80atagen2026-10-16 08:25 UTC237
        let root = find(&mut parent, i);
b563d2da1c80atagen2026-10-16 08:25 UTC238
        if slot[root] == usize::MAX {
b563d2da1c80atagen2026-10-16 08:25 UTC239
            slot[root] = groups.len();
b563d2da1c80atagen2026-10-16 08:25 UTC240
            groups.push(Vec::new());
b563d2da1c80atagen2026-10-16 08:25 UTC241
        }
b563d2da1c80atagen2026-10-16 08:25 UTC242
        groups[slot[root]].push(i);
b563d2da1c80atagen2026-10-16 08:25 UTC243
    }
b563d2da1c80atagen2026-10-16 08:25 UTC244
    groups
b563d2da1c80atagen2026-10-16 08:25 UTC245
}