1
0
Fork 0
forked from wry/wry
wry/src/ipc.rs
2026-06-08 19:56:17 -04:00

959 lines
32 KiB
Rust

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<u64>,
clients: RefCell<AHashMap<u64, SpawnedFuture<()>>>,
}
struct AllocatedSocket {
path: Ustring,
socket: Rc<OwnedFd>,
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<State>,
) -> Result<(Rc<IpcAcceptor>, Vec<SpawnedFuture<()>>), 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<AllocatedSocket, IpcAcceptorError> {
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<IpcAcceptor>, state: Rc<State>) {
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<OwnedFd>) -> 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<OwnedFd>, state: Rc<State>, acc: Rc<IpcAcceptor>) {
handle_client_(fd, state).await;
acc.clients.borrow_mut().remove(&id);
}
async fn handle_client_(fd: Rc<OwnedFd>, state: Rc<State>) {
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::<Vec<_>>();
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::<Request>(&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<OwnedFd>,
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<State>, req: Request) -> Response {
match handle_request_(state, req) {
Ok(value) => Response::Ok { result: value },
Err(message) => Response::Error { message },
}
}
fn handle_request_(state: &Rc<State>, req: Request) -> Result<Value, String> {
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<T: Serialize>(value: T) -> Result<Value, String> {
serde_json::to_value(value).map_err(|e| format!("Could not serialize response: {e}"))
}
fn get_connector(state: &State, name: &str) -> Result<Rc<ConnectorData>, 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<Rc<OutputNode>, 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<Rc<OutputNode>, 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<Rc<crate::tree::WorkspaceNode>, 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<State>, 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<State>,
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<Transform, String> {
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<OutputNode>) -> 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(&current_mode))
.iter()
.map(|mode| Mode {
width: mode.width,
height: mode.height,
refresh_rate_millihz: mode.refresh_rate_millihz,
current: mode == &current_mode,
})
.collect::<Vec<_>>();
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<Workspace> {
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::<Vec<_>>();
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<u64>) -> Vec<Client> {
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<String>,
},
#[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<u64>,
},
#[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<DrmDevice>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub unbound_connectors: Vec<Connector>,
}
#[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<Connector>,
}
#[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<Output>,
}
#[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<Mode>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub modes: Vec<Mode>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub format: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub formats: Vec<String>,
#[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<f64>,
pub tearing_mode: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub flip_margin_ns: Option<u64>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub supported_color_spaces: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub current_color_space: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub supported_eotfs: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub current_eotf: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min_brightness: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_brightness: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub brightness: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub blend_space: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub native_gamut: Option<Primaries>,
#[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<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sandbox_app_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sandbox_instance_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub uid: Option<c::uid_t>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pid: Option<c::pid_t>,
#[serde(default, skip_serializing_if = "is_false")]
pub is_xwayland: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub comm: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub exe: Option<String>,
}
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<T>(request: &Request) -> Result<T, IpcClientError>
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<Response, IpcClientError> {
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<PathBuf, IpcClientError> {
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())))
}