use { crate::{ backend, client::ClientId, compositor::MAX_EXTENTS, state::{ConnectorData, State}, tree::{OutputNode, Transform, WsMoveConfig, move_ws_to_output}, utils::{ buf::Buf, errorfmt::ErrorFmt, numcell::NumCell, oserror::{OsError, OsErrorExt, OsErrorExt2}, pid_info::get_socket_creds, static_text::StaticText, xrd::xrd, }, }, ahash::AHashMap, jay_async_engine::SpawnedFuture, jay_units::scale::Scale, serde::{Deserialize, Serialize, de::DeserializeOwned}, serde_json::Value, std::{ cell::RefCell, collections::VecDeque, io::{BufRead, BufReader, Write}, os::unix::net::UnixStream, path::PathBuf, rc::Rc, }, thiserror::Error, uapi::{OwnedFd, Ustr, Ustring, c, format_ustr}, }; pub const JAY_IPC_SOCKET: &str = "JAY_IPC_SOCKET"; const PROTOCOL_VERSION: u32 = 1; const MAX_LINE_LEN: usize = 1024 * 1024; const COMMANDS: &[&str] = &[ "hello", "outputs.get", "outputs.set_enabled", "outputs.set_mode", "outputs.set_position", "outputs.set_scale", "outputs.set_transform", "workspaces.get", "workspaces.show", "workspaces.create", "workspaces.move_to_output", "clients.get", "clients.kill", "dpms.set", ]; #[derive(Debug, Error)] pub enum IpcAcceptorError { #[error("XDG_RUNTIME_DIR is not set")] XrdNotSet, #[error("XDG_RUNTIME_DIR ({0:?}) is too long to form a unix socket address")] XrdTooLong(String), #[error("Could not create an IPC socket")] SocketFailed(#[source] OsError), #[error("Could not stat the existing IPC socket")] SocketStat(#[source] OsError), #[error("Could not start listening for incoming IPC connections")] ListenFailed(#[source] OsError), #[error("Could not open the IPC lock file")] OpenLockFile(#[source] OsError), #[error("Could not lock the IPC lock file")] LockLockFile(#[source] OsError), #[error("Could not bind the IPC socket to an address")] BindFailed(#[source] OsError), } pub struct IpcAcceptor { socket: AllocatedSocket, next_client_id: NumCell, clients: RefCell>>, } struct AllocatedSocket { path: Ustring, socket: Rc, lock_path: Ustring, _lock_fd: OwnedFd, } impl Drop for AllocatedSocket { fn drop(&mut self) { let _ = uapi::unlink(&self.path); let _ = uapi::unlink(&self.lock_path); } } impl IpcAcceptor { pub fn install( state: &Rc, ) -> Result<(Rc, Vec>), IpcAcceptorError> { let socket = allocate_socket()?; log::info!("bound IPC socket {}", socket.path.display()); uapi::listen(socket.socket.raw(), 128).map_os_err(IpcAcceptorError::ListenFailed)?; let acc = Rc::new(IpcAcceptor { socket, next_client_id: NumCell::new(1), clients: Default::default(), }); let futures = vec![ state .eng .spawn("IPC acceptor", accept(acc.clone(), state.clone())), ]; state.ipc_acceptor.set(Some(acc.clone())); Ok((acc, futures)) } pub fn socket_path(&self) -> &Ustr { self.socket.path.as_ustr() } } fn allocate_socket() -> Result { let xrd = match xrd() { Some(d) => d, _ => return Err(IpcAcceptorError::XrdNotSet), }; let socket = uapi::socket(c::AF_UNIX, c::SOCK_STREAM | c::SOCK_CLOEXEC, 0) .map(Rc::new) .map_os_err(IpcAcceptorError::SocketFailed)?; let mut addr: c::sockaddr_un = uapi::pod_zeroed(); addr.sun_family = c::AF_UNIX as _; let path = format_ustr!("{}/jay-ipc-{}.sock", xrd, uapi::geteuid()); let lock_path = format_ustr!("{}.lock", path.display()); if path.len() + 1 > addr.sun_path.len() { return Err(IpcAcceptorError::XrdTooLong(xrd.to_string())); } let lock_fd = uapi::open(&*lock_path, c::O_CREAT | c::O_CLOEXEC | c::O_RDWR, 0o600) .map_os_err(IpcAcceptorError::OpenLockFile)?; uapi::flock(lock_fd.raw(), c::LOCK_EX | c::LOCK_NB) .map_os_err(IpcAcceptorError::LockLockFile)?; match uapi::lstat(&path).to_os_error() { Ok(_) => { log::info!("Unlinking {}", path.display()); let _ = uapi::unlink(&path); } Err(OsError(c::ENOENT)) => {} Err(e) => return Err(IpcAcceptorError::SocketStat(e)), } let sun_path = uapi::as_bytes_mut(&mut addr.sun_path[..]); sun_path[..path.len()].copy_from_slice(path.as_bytes()); sun_path[path.len()] = 0; uapi::bind(socket.raw(), &addr).map_os_err(IpcAcceptorError::BindFailed)?; Ok(AllocatedSocket { path, socket, lock_path, _lock_fd: lock_fd, }) } async fn accept(acc: Rc, state: Rc) { loop { let fd = match state.ring.accept(&acc.socket.socket, c::SOCK_CLOEXEC).await { Ok(fd) => fd, Err(e) => { log::error!("Could not accept IPC client: {}", ErrorFmt(e)); break; } }; if !peer_is_allowed(&fd) { continue; } let id = acc.next_client_id.fetch_add(1); let future = state .eng .spawn("IPC client", handle_client(id, fd, state.clone(), acc.clone())); acc.clients.borrow_mut().insert(id, future); } } fn peer_is_allowed(fd: &Rc) -> bool { match get_socket_creds(fd) { Some((uid, _)) if uid == uapi::geteuid() => true, Some((uid, pid)) => { log::warn!("Rejecting IPC client pid {pid} with uid {uid}"); false } _ => false, } } async fn handle_client(id: u64, fd: Rc, state: Rc, acc: Rc) { handle_client_(fd, state).await; acc.clients.borrow_mut().remove(&id); } async fn handle_client_(fd: Rc, state: Rc) { let mut pending = Vec::new(); loop { let mut bufs = [Buf::new(4096)]; let mut fds = VecDeque::new(); let n = match state.ring.recvmsg(&fd, &mut bufs, &mut fds).await { Ok(0) => return, Ok(n) => n, Err(e) => { log::debug!("Could not read IPC request: {}", ErrorFmt(e)); return; } }; pending.extend_from_slice(&bufs[0][..n]); if pending.len() > MAX_LINE_LEN { let _ = write_response( &state, &fd, &Response::Error { message: "IPC request is too large".to_string(), }, ) .await; return; } while let Some(pos) = pending.iter().position(|&b| b == b'\n') { let mut line = pending.drain(..=pos).collect::>(); while line.last().is_some_and(|b| *b == b'\n' || *b == b'\r') { line.pop(); } if line.is_empty() { continue; } let response = match serde_json::from_slice::(&line) { Ok(req) => handle_request(&state, req), Err(e) => Response::Error { message: format!("Could not parse request: {e}"), }, }; if let Err(e) = write_response(&state, &fd, &response).await { log::debug!("Could not write IPC response: {}", ErrorFmt(e)); return; } } } } async fn write_response( state: &State, fd: &Rc, response: &Response, ) -> Result<(), jay_io_uring::IoUringError> { let mut bytes = serde_json::to_vec(response).unwrap(); bytes.push(b'\n'); let mut offset = 0; while offset < bytes.len() { let buf = Buf::from_slice(&bytes[offset..]); let n = state.ring.write(fd, buf, None).await?; if n == 0 { return Ok(()); } offset += n; } Ok(()) } fn handle_request(state: &Rc, req: Request) -> Response { match handle_request_(state, req) { Ok(value) => Response::Ok { result: value }, Err(message) => Response::Error { message }, } } fn handle_request_(state: &Rc, req: Request) -> Result { match req { Request::Hello => to_value(Hello { protocol_version: PROTOCOL_VERSION, pid: state.pid, commands: COMMANDS, }), Request::OutputsGet => to_value(outputs(state)), Request::OutputsSetEnabled { output, enabled } => { set_output_enabled(state, &output, enabled)?; Ok(Value::Null) } Request::OutputsSetMode { output, width, height, refresh_rate, } => { set_output_mode(state, &output, width, height, refresh_rate)?; Ok(Value::Null) } Request::OutputsSetPosition { output, x, y } => { set_output_position(state, &output, x, y)?; Ok(Value::Null) } Request::OutputsSetScale { output, scale, round_to_float, } => { let output = get_output_node(state, &output)?; let scale = match round_to_float { true => Scale::from_f64_as_float(scale), false => Scale::from_f64(scale), }; output.set_preferred_scale(scale); Ok(Value::Null) } Request::OutputsSetTransform { output, transform } => { let output = get_output_node(state, &output)?; let transform = parse_transform(&transform)?; output.update_transform(transform); Ok(Value::Null) } Request::WorkspacesGet => to_value(workspaces(state)), Request::WorkspacesShow { name, output } => { let output = match output { Some(output) => get_output_node(state, &output)?, None => default_output(state)?, }; let ws = output .find_workspace(&name) .unwrap_or_else(|| output.create_workspace(&name)); state.show_workspace2(None, &output, &ws); Ok(Value::Null) } Request::WorkspacesCreate { name, output } => { let output = get_output_node(state, &output)?; if output.find_workspace(&name).is_none() { output.create_workspace(&name); } Ok(Value::Null) } Request::WorkspacesMoveToOutput { name, output } => { let ws = get_workspace_by_name(state, &name)?; let output = get_output_node(state, &output)?; let link = match &*ws.output_link.borrow() { Some(link) => link.to_ref(), None => return Err(format!("Workspace `{name}` is not linked to an output")), }; let config = WsMoveConfig { make_visible_always: false, make_visible_if_empty: true, source_is_destroyed: false, before: None, }; move_ws_to_output(&link, &output, config); ws.desired_output.set(output.global.output_id.clone()); state.tree_changed(); Ok(Value::Null) } Request::ClientsGet { id } => to_value(clients(state, id)), Request::ClientsKill { id } => { state.clients.kill(ClientId::from_raw(id)); Ok(Value::Null) } Request::DpmsSet { active } => { state .set_dpms_active(active) .map_err(|e| format!("Could not set DPMS state: {}", ErrorFmt(e)))?; Ok(Value::Null) } } } fn to_value(value: T) -> Result { serde_json::to_value(value).map_err(|e| format!("Could not serialize response: {e}")) } fn get_connector(state: &State, name: &str) -> Result, String> { for connector in state.connectors.lock().values() { if connector.name.as_str() == name { return Ok(connector.clone()); } } let namelc = name.to_ascii_lowercase(); for connector in state.connectors.lock().values() { if connector.name.to_ascii_lowercase() == namelc { return Ok(connector.clone()); } } Err(format!("Found no connector matching `{name}`")) } fn get_output_node(state: &State, name: &str) -> Result, String> { let connector = get_connector(state, name)?; let output = state .outputs .get(&connector.connector.id()) .ok_or_else(|| format!("Connector {} is not connected", connector.name))?; output.node.clone().ok_or_else(|| { format!( "Display connected to {} is not a desktop display", connector.name ) }) } fn default_output(state: &State) -> Result, String> { if let Some(seat) = state.seat_queue.last() { let output = seat.get_fallback_output(); if !output.is_dummy { return Ok(output); } } if let Some(output) = state.root.outputs.lock().values().next().cloned() { return Ok(output); } if let Some(output) = state.dummy_output.get() { return Ok(output); } Err("No output is available".to_string()) } fn get_workspace_by_name(state: &State, name: &str) -> Result, String> { let mut result = None; for ws in state.workspaces.lock().values() { if !ws.is_dummy && ws.name == name { if result.is_some() { return Err(format!("Workspace name `{name}` is ambiguous")); } result = Some(ws.clone()); } } result.ok_or_else(|| format!("Workspace `{name}` does not exist")) } fn set_output_enabled(state: &Rc, name: &str, enabled: bool) -> Result<(), String> { let connector = get_connector(state, name)?; connector .modify_state(state, |s| s.enabled = enabled) .map_err(|e| format!("Could not en/disable connector: {}", ErrorFmt(e))) } fn set_output_mode( state: &Rc, name: &str, width: i32, height: i32, refresh_rate: f64, ) -> Result<(), String> { let connector = get_connector(state, name)?; let refresh_rate_millihz = (refresh_rate * 1_000.0).round() as u32; if let Some(output) = state.outputs.get(&connector.connector.id()) && let Some(node) = &output.node && node.global.modes.is_some() && !node.global.modes.as_ref().unwrap().iter().any(|mode| { mode.width == width && mode.height == height && mode.refresh_rate_millihz == refresh_rate_millihz }) { return Err(format!("Output {} does not support this mode", connector.name)); } connector .modify_state(state, |s| { s.mode = backend::Mode { width, height, refresh_rate_millihz, }; }) .map_err(|e| format!("Could not modify connector mode: {}", ErrorFmt(e))) } fn set_output_position(state: &State, name: &str, x: i32, y: i32) -> Result<(), String> { if x < 0 || y < 0 { return Err("x and y cannot be less than 0".to_string()); } if x > MAX_EXTENTS || y > MAX_EXTENTS { return Err(format!("x and y cannot be greater than {MAX_EXTENTS}")); } let output = get_output_node(state, name)?; output.set_position(x, y); Ok(()) } fn parse_transform(transform: &str) -> Result { match transform { "none" => Ok(Transform::None), "rotate-90" => Ok(Transform::Rotate90), "rotate-180" => Ok(Transform::Rotate180), "rotate-270" => Ok(Transform::Rotate270), "flip" => Ok(Transform::Flip), "flip-rotate-90" => Ok(Transform::FlipRotate90), "flip-rotate-180" => Ok(Transform::FlipRotate180), "flip-rotate-270" => Ok(Transform::FlipRotate270), _ => Err(format!("Unknown transform `{transform}`")), } } fn outputs(state: &State) -> Outputs { let mut drm_devices = vec![]; for dev in state.drm_devs.lock().values() { let dev_id = dev.dev.id().raw() as u64; let mut connectors = vec![]; for connector in state.connectors.lock().values() { let connector_dev_id = connector.drm_dev.as_ref().map(|d| d.dev.id().raw() as u64); if connector_dev_id == Some(dev_id) { connectors.push(output_connector(state, connector)); } } connectors.sort_by(|l, r| l.name.cmp(&r.name)); drm_devices.push(DrmDevice { devnode: dev.devnode.as_deref().unwrap_or_default().to_string(), syspath: dev.syspath.as_deref().unwrap_or_default().to_string(), vendor: dev.pci_id.map(|p| p.vendor).unwrap_or_default(), vendor_name: dev.vendor.as_deref().unwrap_or_default().to_string(), model: dev.pci_id.map(|p| p.model).unwrap_or_default(), model_name: dev.model.as_deref().unwrap_or_default().to_string(), gfx_api: dev.dev.gtx_api().to_str().to_string(), render_device: dev.dev.is_render_device(), connectors, }); } drm_devices.sort_by(|l, r| l.devnode.cmp(&r.devnode)); let mut unbound_connectors = vec![]; for connector in state.connectors.lock().values() { if connector.drm_dev.is_none() { unbound_connectors.push(output_connector(state, connector)); } } unbound_connectors.sort_by(|l, r| l.name.cmp(&r.name)); Outputs { drm_devices, unbound_connectors, } } fn output_connector(state: &State, connector: &ConnectorData) -> Connector { let state_enabled = connector.state.borrow().enabled; let output = state .outputs .get(&connector.connector.id()) .map(|output| match &output.node { Some(node) => Output::from_node(output.as_ref(), node), None => Output { product: output.monitor_info.output_id.model.clone(), manufacturer: output.monitor_info.output_id.manufacturer.clone(), serial_number: output.monitor_info.output_id.serial_number.clone(), width_mm: output.monitor_info.width_mm, height_mm: output.monitor_info.height_mm, non_desktop: true, scale: 1.0, ..Default::default() }, }); Connector { name: connector.name.to_string(), enabled: state_enabled, output, } } impl Output { fn from_node(output: &crate::state::OutputData, node: &Rc) -> Self { let global = &node.global; let pos = global.pos.get(); let current_mode = global.mode.get(); let modes = global .modes .as_deref() .unwrap_or(std::slice::from_ref(¤t_mode)) .iter() .map(|mode| Mode { width: mode.width, height: mode.height, refresh_rate_millihz: mode.refresh_rate_millihz, current: mode == ¤t_mode, }) .collect::>(); let current_format = node.global.format.get(); let mut formats = vec![current_format.name.to_string()]; for &format in &*node.global.formats.get() { if format != current_format { formats.push(format.name.to_string()); } } let p = &node.global.primaries; Output { product: output.monitor_info.output_id.model.clone(), manufacturer: output.monitor_info.output_id.manufacturer.clone(), serial_number: output.monitor_info.output_id.serial_number.clone(), width_mm: global.width_mm, height_mm: global.height_mm, non_desktop: false, scale: global.persistent.scale.get().to_f64(), x: pos.x1(), y: pos.y1(), width: pos.width(), height: pos.height(), transform: global.persistent.transform.get().text().to_string(), mode: modes.iter().copied().find(|mode| mode.current), modes, format: Some(current_format.name.to_string()), formats, vrr_capable: output.monitor_info.vrr_capable, vrr_enabled: node.schedule.vrr_enabled(), vrr_mode: node.global.persistent.vrr_mode.get().to_config().0, vrr_cursor_hz: node.global.persistent.vrr_cursor_hz.get(), tearing_mode: node.global.persistent.tearing_mode.get().to_config().0, flip_margin_ns: node.flip_margin_ns.get(), supported_color_spaces: node .global .color_spaces .iter() .map(|cs| cs.name().to_string()) .collect(), current_color_space: Some(node.global.bcs.get().name().to_string()), supported_eotfs: node .global .eotfs .iter() .map(|eotf| eotf.name().to_string()) .collect(), current_eotf: Some(node.global.btf.get().name().to_string()), min_brightness: node.global.luminance.map(|lum| lum.min), max_brightness: node.global.luminance.map(|lum| lum.max), brightness: node.global.persistent.brightness.get(), blend_space: Some(node.global.persistent.blend_space.get().name().to_string()), native_gamut: Some(Primaries { r_x: p.r.0.0, r_y: p.r.1.0, g_x: p.g.0.0, g_y: p.g.1.0, b_x: p.b.0.0, b_y: p.b.1.0, w_x: p.wp.0.0, w_y: p.wp.1.0, }), use_native_gamut: node.global.persistent.use_native_gamut.get(), arbitrary_modes: global.modes.is_none(), } } } fn workspaces(state: &State) -> Vec { let mut workspaces = state .workspaces .lock() .values() .filter(|ws| !ws.is_dummy) .map(|ws| Workspace { id: ws.id.raw(), name: ws.name.clone(), output: ws.output.get().global.connector.name.to_string(), visible: ws.visible.get(), }) .collect::>(); workspaces.sort_by(|l, r| (&l.output, &l.name, l.id).cmp(&(&r.output, &r.name, r.id))); workspaces } fn clients(state: &State, id: Option) -> Vec { let mut clients = vec![]; for holder in state.clients.clients.borrow().values() { let client = &holder.data; if id.is_some_and(|id| id != client.id.raw()) { continue; } clients.push(Client { client_id: client.id.raw(), sandboxed: client.metadata.sandboxed, sandbox_engine: client.metadata.sandbox_engine.clone(), sandbox_app_id: client.metadata.app_id.clone(), sandbox_instance_id: client.metadata.instance_id.clone(), uid: (!client.is_xwayland).then_some(client.pid_info.uid), pid: (!client.is_xwayland).then_some(client.pid_info.pid), is_xwayland: client.is_xwayland, comm: (!client.is_xwayland).then(|| client.pid_info.comm.clone()), exe: (!client.is_xwayland).then(|| client.pid_info.exe.clone()), }); } clients.sort_by_key(|client| client.client_id); clients } #[derive(Debug, Serialize, Deserialize)] #[serde(tag = "method", rename_all = "snake_case")] pub enum Request { #[serde(rename = "hello")] Hello, #[serde(rename = "outputs.get")] OutputsGet, #[serde(rename = "outputs.set_enabled")] OutputsSetEnabled { output: String, enabled: bool }, #[serde(rename = "outputs.set_mode")] OutputsSetMode { output: String, width: i32, height: i32, refresh_rate: f64, }, #[serde(rename = "outputs.set_position")] OutputsSetPosition { output: String, x: i32, y: i32 }, #[serde(rename = "outputs.set_scale")] OutputsSetScale { output: String, scale: f64, round_to_float: bool, }, #[serde(rename = "outputs.set_transform")] OutputsSetTransform { output: String, transform: String }, #[serde(rename = "workspaces.get")] WorkspacesGet, #[serde(rename = "workspaces.show")] WorkspacesShow { name: String, #[serde(skip_serializing_if = "Option::is_none")] output: Option, }, #[serde(rename = "workspaces.create")] WorkspacesCreate { name: String, output: String }, #[serde(rename = "workspaces.move_to_output")] WorkspacesMoveToOutput { name: String, output: String }, #[serde(rename = "clients.get")] ClientsGet { #[serde(skip_serializing_if = "Option::is_none")] id: Option, }, #[serde(rename = "clients.kill")] ClientsKill { id: u64 }, #[serde(rename = "dpms.set")] DpmsSet { active: bool }, } #[derive(Debug, Serialize, Deserialize)] #[serde(tag = "status", rename_all = "snake_case")] pub enum Response { Ok { result: Value }, Error { message: String }, } #[derive(Serialize)] struct Hello<'a> { protocol_version: u32, pid: c::pid_t, commands: &'a [&'a str], } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Outputs { #[serde(default, skip_serializing_if = "Vec::is_empty")] pub drm_devices: Vec, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub unbound_connectors: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DrmDevice { pub devnode: String, pub syspath: String, pub vendor: u32, pub vendor_name: String, pub model: u32, pub model_name: String, pub gfx_api: String, #[serde(default, skip_serializing_if = "is_false")] pub render_device: bool, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub connectors: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Connector { pub name: String, pub enabled: bool, #[serde(default, skip_serializing_if = "Option::is_none")] pub output: Option, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct Output { pub product: String, pub manufacturer: String, pub serial_number: String, #[serde(default, skip_serializing_if = "is_zero_i32")] pub width_mm: i32, #[serde(default, skip_serializing_if = "is_zero_i32")] pub height_mm: i32, #[serde(default, skip_serializing_if = "is_false")] pub non_desktop: bool, pub scale: f64, pub x: i32, pub y: i32, pub width: i32, pub height: i32, pub transform: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub mode: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub modes: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] pub format: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub formats: Vec, #[serde(default, skip_serializing_if = "is_false")] pub vrr_capable: bool, #[serde(default, skip_serializing_if = "is_false")] pub vrr_enabled: bool, pub vrr_mode: u32, #[serde(default, skip_serializing_if = "Option::is_none")] pub vrr_cursor_hz: Option, pub tearing_mode: u32, #[serde(default, skip_serializing_if = "Option::is_none")] pub flip_margin_ns: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub supported_color_spaces: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] pub current_color_space: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub supported_eotfs: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] pub current_eotf: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub min_brightness: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub max_brightness: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub brightness: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub blend_space: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub native_gamut: Option, #[serde(default, skip_serializing_if = "is_false")] pub use_native_gamut: bool, #[serde(default, skip_serializing_if = "is_false")] pub arbitrary_modes: bool, } #[derive(Debug, Copy, Clone, Serialize, Deserialize)] pub struct Mode { pub width: i32, pub height: i32, pub refresh_rate_millihz: u32, #[serde(default, skip_serializing_if = "is_false")] pub current: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Primaries { pub r_x: f64, pub r_y: f64, pub g_x: f64, pub g_y: f64, pub b_x: f64, pub b_y: f64, pub w_x: f64, pub w_y: f64, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Workspace { pub id: u32, pub name: String, pub output: String, pub visible: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Client { pub client_id: u64, #[serde(default, skip_serializing_if = "is_false")] pub sandboxed: bool, #[serde(default, skip_serializing_if = "Option::is_none")] pub sandbox_engine: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub sandbox_app_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub sandbox_instance_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub uid: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub pid: Option, #[serde(default, skip_serializing_if = "is_false")] pub is_xwayland: bool, #[serde(default, skip_serializing_if = "Option::is_none")] pub comm: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub exe: Option, } fn is_zero_i32(v: &i32) -> bool { *v == 0 } fn is_false(v: &bool) -> bool { !*v } #[derive(Debug, Error)] pub enum IpcClientError { #[error("XDG_RUNTIME_DIR is not set")] XrdNotSet, #[error("Could not connect to the IPC socket {path}")] Connect { path: PathBuf, #[source] error: std::io::Error, }, #[error("Could not write the IPC request")] Write(#[source] std::io::Error), #[error("Could not read the IPC response")] Read(#[source] std::io::Error), #[error("Could not serialize the IPC request")] Serialize(#[source] serde_json::Error), #[error("Could not parse the IPC response")] Deserialize(#[source] serde_json::Error), #[error("IPC request failed: {0}")] Server(String), } impl IpcClientError { pub fn can_fallback(&self) -> bool { matches!(self, IpcClientError::XrdNotSet | IpcClientError::Connect { .. }) } } pub fn request(request: &Request) -> Result where T: DeserializeOwned, { match raw_request(request)? { Response::Ok { result } => { serde_json::from_value(result).map_err(IpcClientError::Deserialize) } Response::Error { message } => Err(IpcClientError::Server(message)), } } pub fn request_unit(req: &Request) -> Result<(), IpcClientError> { let _: Value = request(req)?; Ok(()) } fn raw_request(request: &Request) -> Result { let path = client_socket_path()?; let mut stream = UnixStream::connect(&path).map_err(|error| IpcClientError::Connect { path: path.clone(), error, })?; let mut bytes = serde_json::to_vec(request).map_err(IpcClientError::Serialize)?; bytes.push(b'\n'); stream.write_all(&bytes).map_err(IpcClientError::Write)?; let mut reader = BufReader::new(stream); let mut line = Vec::new(); reader.read_until(b'\n', &mut line).map_err(IpcClientError::Read)?; serde_json::from_slice(&line).map_err(IpcClientError::Deserialize) } fn client_socket_path() -> Result { if let Some(path) = std::env::var_os(JAY_IPC_SOCKET) { return Ok(PathBuf::from(path)); } let xrd = match std::env::var_os("XDG_RUNTIME_DIR") { Some(xrd) => xrd, None => return Err(IpcClientError::XrdNotSet), }; Ok(PathBuf::from(xrd).join(format!("jay-ipc-{}.sock", uapi::geteuid()))) }