use async_mach_ports::Result;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
pub use async_mach_ports::{Codec, Error, RecvPort, SendPort};
pub const SERVICE_NAME: &str = "com.github.karinushka.paneru";
pub const SERVICE_ENV: &str = "PANERU_MACH_SERVICE";
#[must_use]
pub fn service_name() -> String {
std::env::var(SERVICE_ENV).unwrap_or_else(|_| SERVICE_NAME.to_string())
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct MessagePack;
impl async_mach_ports::Codec for MessagePack {
fn encode<T: Serialize + ?Sized>(&self, value: &T) -> Result<Vec<u8>> {
rmp_serde::to_vec_named(value).map_err(|_| Error::Encode)
}
fn decode<T: DeserializeOwned>(&self, bytes: &[u8]) -> Result<T> {
rmp_serde::from_slice(bytes).map_err(|_| Error::Decode)
}
}
pub type Sender<T> = async_mach_ports::Sender<T, MessagePack>;
pub type Receiver<T> = async_mach_ports::Receiver<T, MessagePack>;
pub type Delivery<T> = async_mach_ports::Delivery<T, MessagePack>;
pub type Reply = async_mach_ports::Reply<MessagePack>;
pub type Subscriber = async_mach_ports::Subscriber<MessagePack>;
pub fn connect<T: Serialize>(service: &str) -> Result<Sender<T>> {
async_mach_ports::Sender::connect(service, MessagePack)
}
pub fn bind<T: DeserializeOwned>(service: &str) -> Result<Receiver<T>> {
async_mach_ports::Receiver::bind(service, MessagePack)
}
use crate::types::commands::Command;
pub use crate::types::script_state::WriteOutcome;
use crate::types::script_state::ScriptStateWrite;
use crate::types::script_value::ScriptValue;
use crate::types::state::{
ActiveState, QueryState, StateQueryKind, VirtualWorkspaceState, WindowState,
};
use crate::types::windowset::{LayoutOp, WindowSet};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum Request {
Command(Command),
Query(StateQueryKind),
WindowSet,
WindowSetApply(Vec<LayoutOp>),
ScriptState(ScriptStateRequest),
Subscribe,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum ScriptStateRequest {
Get { key: String },
Write(ScriptStateWrite),
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum Response {
Query(QueryPayload),
WindowSet(Box<WindowSet>),
ScriptState(ScriptStateResponse),
Error(String),
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum QueryPayload {
State(Box<QueryState>),
VirtualWorkspaces(Vec<VirtualWorkspaceState>),
Active(Box<ActiveState>),
OnScreen(Vec<WindowState>),
}
impl QueryPayload {
pub fn to_json(&self) -> serde_json::Result<serde_json::Value> {
match self {
Self::State(state) => serde_json::to_value(state),
Self::VirtualWorkspaces(rows) => serde_json::to_value(rows),
Self::Active(active) => serde_json::to_value(active),
Self::OnScreen(windows) => serde_json::to_value(windows),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum ScriptStateResponse {
Value(Option<ScriptValue>),
Write(WriteOutcome),
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::commands::{Command, Direction, Operation};
use crate::types::state::Frame;
use std::sync::Arc;
fn round_trip<T>(value: &T)
where
T: serde::Serialize + serde::de::DeserializeOwned + PartialEq + std::fmt::Debug,
{
let bytes = MessagePack.encode(value).expect("encodes");
let decoded: T = MessagePack.decode(&bytes).expect("decodes");
assert_eq!(&decoded, value);
}
#[test]
fn every_request_survives_the_wire() {
round_trip(&Request::Command(Command::Window(Operation::Focus(
Direction::East,
))));
round_trip(&Request::Query(StateQueryKind::Active));
round_trip(&Request::WindowSet);
round_trip(&Request::WindowSetApply(vec![LayoutOp::Focus(7)]));
round_trip(&Request::Subscribe);
round_trip(&Request::ScriptState(ScriptStateRequest::Get {
key: "pads.term".to_string(),
}));
round_trip(&Request::ScriptState(ScriptStateRequest::Write(
ScriptStateWrite::set("count".to_string(), ScriptValue::Int(7)),
)));
}
#[test]
fn every_response_survives_the_wire() {
round_trip(&Response::Query(QueryPayload::Active(Box::default())));
round_trip(&Response::Query(
QueryPayload::VirtualWorkspaces(Vec::new()),
));
round_trip(&Response::Query(QueryPayload::OnScreen(Vec::new())));
round_trip(&Response::ScriptState(ScriptStateResponse::Value(Some(
ScriptValue::Str("hello".to_string()),
))));
round_trip(&Response::ScriptState(ScriptStateResponse::Write(
WriteOutcome::Applied { changed: true },
)));
round_trip(&Response::Error("no such window".to_string()));
}
#[test]
fn the_window_set_survives_the_wire() {
use crate::types::windowset::{ColumnSet, DisplaySet, WindowRec, WorkspaceSet};
let window = |id| WindowRec {
id,
app_name: "Test App".to_string(),
bundle_id: "com.example.test".to_string(),
title: format!("Window {id}"),
frame: Some(Frame {
x: 0,
y: 0,
width: 400,
height: 600,
}),
floating: false,
managed: true,
visible: true,
focused: id == 1,
};
let set = WindowSet::new(
vec![DisplaySet {
id: 1,
frame: Frame {
x: 0,
y: 0,
width: 1024,
height: 768,
},
active: true,
workspaces: Arc::new(vec![WorkspaceSet {
number: 1,
native_id: 10,
active: true,
columns: Arc::new(vec![
ColumnSet::single(window(1), 0.5),
ColumnSet::single(window(2), 0.5),
]),
floating: Arc::new(Vec::new()),
}]),
}],
Some(1),
);
let bytes = MessagePack
.encode(&Response::WindowSet(Box::new(set)))
.expect("encodes");
let Response::WindowSet(decoded) = MessagePack.decode(&bytes).expect("decodes") else {
panic!("expected a window set");
};
assert_eq!(decoded.focused(), Some(1));
assert_eq!(decoded.east(1), Some(2));
assert!(decoded.ops().is_empty());
}
#[test]
fn a_request_is_small() {
let bytes = MessagePack
.encode(&Request::Query(StateQueryKind::Active))
.expect("encodes");
assert!(
bytes.len() <= 32,
"a query request took {} bytes",
bytes.len()
);
}
}