Files
feresaria/crates/ferese-ipc/src/events.rs
T
Samuel Onoja a6f24e8188 Harden desktop recovery and pre-release stability checks
Bound IPC transport deadlines while preserving portal leases, supervise desktop helper and agent recovery, and reserve final floating window geometry during placement.

Stabilize CLI output and isolated tests, expand workspace CI, resolve formatting and lint failures, and bundle the MIT license with installer compatibility.
2026-10-07 09:08:23 +01:00

135 lines
3.8 KiB
Rust

use serde::{Deserialize, Serialize};
use serde_json::Value;
/// Event schema version, independent of command framing versions.
pub const VERSION: u32 = 1;
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
pub struct ConfigState {
pub revision: u64,
pub error: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct LockState {
pub phase: String,
pub sleeping: bool,
}
/// A coherent logical desktop view. Desktop objects are redacted while locked.
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct Snapshot {
pub outputs: Value,
pub workspaces: Value,
pub windows: Value,
pub focus: Value,
pub config: ConfigState,
pub theme: Value,
pub lock: LockState,
}
/// Replacement values for each changed domain; not animation frames or client damage.
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Change {
Snapshot { desktop: Box<Snapshot> },
OutputsChanged { outputs: Value },
WorkspacesChanged { workspaces: Value },
WindowsChanged { windows: Value },
FocusChanged { focus: Value },
ConfigChanged { config: ConfigState },
ThemeChanged { theme: Value },
LockChanged { lock: LockState },
}
/// All changes from one dispatch share a generation. The last marks its end.
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct Event {
pub version: u32,
pub generation: u64,
pub last: bool,
#[serde(flatten)]
pub change: Change,
}
impl Event {
pub fn validate(&self) -> Result<(), String> {
if self.version == VERSION {
Ok(())
} else {
Err(format!("Unsupported IPC event version {}", self.version))
}
}
}
impl Snapshot {
pub fn changes_since(&self, previous: &Self) -> Vec<Change> {
let mut changes = Vec::new();
if self.outputs != previous.outputs {
changes.push(Change::OutputsChanged {
outputs: self.outputs.clone(),
});
}
if self.workspaces != previous.workspaces {
changes.push(Change::WorkspacesChanged {
workspaces: self.workspaces.clone(),
});
}
if self.windows != previous.windows {
changes.push(Change::WindowsChanged {
windows: self.windows.clone(),
});
}
if self.focus != previous.focus {
changes.push(Change::FocusChanged {
focus: self.focus.clone(),
});
}
if self.config != previous.config {
changes.push(Change::ConfigChanged {
config: self.config.clone(),
});
}
if self.theme != previous.theme {
changes.push(Change::ThemeChanged {
theme: self.theme.clone(),
});
}
if self.lock != previous.lock {
changes.push(Change::LockChanged {
lock: self.lock.clone(),
});
}
changes
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn events_round_trip_with_an_independent_schema_version() {
let event = Event {
version: VERSION,
generation: 42,
last: true,
change: Change::FocusChanged {
focus: serde_json::json!({"window": 7}),
},
};
let mut bytes = Vec::new();
crate::write_frame(&mut bytes, &event).unwrap();
let decoded: Event = crate::read_frame(&mut bytes.as_slice()).unwrap();
assert_eq!(decoded, event);
decoded.validate().unwrap();
assert!(
Event {
version: VERSION + 1,
..event
}
.validate()
.is_err()
);
}
}