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
    }
}