lib.rs
raw
//! Wire protocol shared between the `quibble` compositor daemon and the
//! `quibblectl` CLI.
//!
//! The transport is newline-delimited JSON over a Unix-domain socket: the
//! client writes one [`Request`] as a single JSON line and reads back one
//! [`Response`] line. Keeping the types here makes this module the single
//! source of truth for the format used by both binaries.
use serde::{Deserialize, Serialize};
/// Stable identifier assigned to a toplevel window when the client creates it.
///
/// This is deliberately independent of any Wayland object id so that the CLI
/// has a value it can hold onto across requests. Ids increase with creation
/// order, which is also the order the compositor lays windows out in.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct WindowId(pub u64);
/// An axis-aligned rectangle in output-logical coordinates.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Rect {
pub x: i32,
pub y: i32,
pub w: i32,
pub h: i32,
}
/// The compositor's window-arrangement policy.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Layout {
/// One near-fullscreen toplevel at a time, popups floating on top.
Tiled,
/// Freely overlapping toplevels, z-order following focus.
Floating,
}
/// Press/release state for a pointer or touch button.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ButtonState {
Pressed,
Released,
}
/// Press/release state for a keyboard key.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum KeyState {
Pressed,
Released,
}
/// Scroll axis for a pointer axis (wheel) event.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Axis {
Vertical,
Horizontal,
}
/// Metadata describing a toplevel, returned by introspection requests.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WindowInfo {
pub id: WindowId,
#[serde(default)]
pub app_id: Option<String>,
#[serde(default)]
pub title: Option<String>,
pub geometry: Rect,
pub focused: bool,
/// Whether the client has presented a buffer. A toplevel exists from the
/// moment the client creates it, but is not on screen — and has no
/// geometry — until it draws into it for the first time.
pub mapped: bool,
}
/// A command sent from the CLI to the compositor.
///
/// Coordinates in pointer/touch requests are absolute, in output-logical
/// space; the compositor resolves which surface they land on.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Request {
/// Liveness check.
Ping,
/// Composite the current scene and return it as a PNG.
Screenshot {
/// Restrict the capture to this region; `None` captures the whole output.
#[serde(default)]
region: Option<Rect>,
},
/// Move the pointer to an absolute location.
PointerMotion { x: f64, y: f64 },
/// Press or release a pointer button (evdev button code, e.g. `0x110` = left).
PointerButton { button: u32, state: ButtonState },
/// Emit a scroll event.
PointerAxis { axis: Axis, value: f64 },
/// Press or release a key (raw evdev keycode, i.e. xkb keycode minus 8).
Key { keycode: u32, state: KeyState },
/// Type a UTF-8 string; the compositor maps characters to key events using
/// its active keymap, pressing/releasing modifiers as needed.
Text { text: String },
/// Begin a touch contact in a given slot.
TouchDown { slot: u32, x: f64, y: f64 },
/// Move an existing touch contact.
TouchMotion { slot: u32, x: f64, y: f64 },
/// End a touch contact.
TouchUp { slot: u32 },
/// Cancel all active touch contacts.
TouchCancel,
/// List every mapped toplevel.
ListWindows,
/// Block until a toplevel matching the given filters is mapped, or the
/// timeout elapses. An empty filter matches the next window to map.
WaitForWindow {
#[serde(default)]
app_id: Option<String>,
#[serde(default)]
title: Option<String>,
timeout_ms: u64,
},
/// Give keyboard focus to a specific window and raise it.
FocusWindow { id: WindowId },
/// Move a window (floating layout only).
MoveWindow { id: WindowId, x: i32, y: i32 },
/// Resize a window (floating layout only).
ResizeWindow { id: WindowId, w: i32, h: i32 },
/// Switch the arrangement policy and relayout existing windows.
SetLayout { layout: Layout },
/// Resize/rescale the virtual output.
SetOutput { width: i32, height: i32, scale: f64 },
/// Read the current clipboard (primary selection text).
ClipboardGet,
/// Set the clipboard selection to the given text.
ClipboardSet { text: String },
/// Open (`true`) or close (`false`) the monitor window.
Monitor { on: bool },
/// Ask the compositor to shut down.
Quit,
}
/// The compositor's reply to a [`Request`].
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Response {
/// Generic success for requests without a payload.
Ok,
/// Reply to [`Request::Ping`].
Pong,
/// A captured screenshot.
Screenshot { png: Png },
/// Reply to [`Request::ListWindows`].
Windows(Vec<WindowInfo>),
/// The window matched by [`Request::WaitForWindow`].
Window(WindowInfo),
/// Reply to [`Request::ClipboardGet`]; `None` when the selection is empty.
Clipboard { text: Option<String> },
/// A [`Request::WaitForWindow`] whose timeout elapsed with no match.
Timeout,
/// The request could not be satisfied.
Error { message: String },
}
/// A binary blob (a PNG image) transported as base64 so it stays compact in
/// the JSON envelope instead of exploding into an array of integers.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Png(pub Vec<u8>);
impl Serialize for Png {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use base64::Engine as _;
serializer.serialize_str(&base64::engine::general_purpose::STANDARD.encode(&self.0))
}
}
impl<'de> Deserialize<'de> for Png {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
use base64::Engine as _;
let s = String::deserialize(deserializer)?;
base64::engine::general_purpose::STANDARD
.decode(s.as_bytes())
.map(Png)
.map_err(serde::de::Error::custom)
}
}
impl Request {
/// Serialize to a single newline-terminated JSON line.
pub fn to_line(&self) -> String {
let mut s = serde_json::to_string(self).expect("Request serialization is infallible");
s.push('\n');
s
}
}
impl Response {
/// Serialize to a single newline-terminated JSON line.
pub fn to_line(&self) -> String {
let mut s = serde_json::to_string(self).expect("Response serialization is infallible");
s.push('\n');
s
}
}