gioser

tawasuyu/brahman

Rust 582TOML 222Markdown 73JSON 198635 Definiciones

feat(mirada): mirada-protocol — contrato Cerebro↔Cuerpo del compositor

BrainCommand/BodyEvent + WindowPlacement, marco postcard con prefijo u32
LE (write_frame/read_frame, guard MAX_FRAME) y el puente placements()
desde un Workspace de mirada-layout. 9 tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

e7365878574bfa3a89939281336557889961a6c0 · por sergio · (hace 5 meses) · padre b7181da7d · ver el árbol en este commit

Delta semántico · 20

crates/modules/mirada/mirada-protocol/src/lib.rs

4 archivos · +294 −0

diff --git a/Cargo.lock b/Cargo.lock
index 02fceaa..c5b854e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -7397,6 +7397,15 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "mirada-protocol"
+version = "0.1.0"
+dependencies = [
+ "mirada-layout",
+ "postcard",
+ "serde",
+]
+
 [[package]]
 name = "moka"
 version = "0.12.15"
diff --git a/Cargo.toml b/Cargo.toml
index 12d1ea8..334c019 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -161,6 +161,7 @@ members = [
     # modules/mirada/ — Compositor Wayland
     # ============================================================
     "crates/modules/mirada/mirada-layout",
+    "crates/modules/mirada/mirada-protocol",
 
     # ============================================================
     # modules/nakui/ — ERP matemático (categórico)
diff --git a/crates/modules/mirada/mirada-protocol/Cargo.toml b/crates/modules/mirada/mirada-protocol/Cargo.toml
new file mode 100644
index 0000000..73200ea
--- /dev/null
+++ b/crates/modules/mirada/mirada-protocol/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "mirada-protocol"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+authors.workspace = true
+publish.workspace = true
+description = "mirada — contrato Cerebro↔Cuerpo del compositor: comandos de geometría que el Cerebro (GPUI) envía y eventos de hardware/superficies que el Cuerpo (smithay) reporta. Marco postcard con prefijo de longitud."
+
+[dependencies]
+mirada-layout = { path = "../mirada-layout" }
+serde = { workspace = true }
+postcard = { workspace = true }
diff --git a/crates/modules/mirada/mirada-protocol/src/lib.rs b/crates/modules/mirada/mirada-protocol/src/lib.rs
new file mode 100644
index 0000000..7e0a9c2
--- /dev/null
+++ b/crates/modules/mirada/mirada-protocol/src/lib.rs
@@ -0,0 +1,271 @@
+//! `mirada-protocol` — el contrato Cerebro↔Cuerpo del compositor.
+//!
+//! mirada se parte en dos procesos:
+//!
+//! - **El Cuerpo** (`mirada-compositor`, sobre `smithay`): habla Wayland
+//!   con los clientes, posee el hardware (DRM/GPU/libinput) y compone las
+//!   superficies reales. Los píxeles nunca salen de él.
+//! - **El Cerebro** (una app GPUI sobre [`mirada-layout`]): decide *dónde*
+//!   va cada ventana — pura aritmética de rectángulos— y orquesta el
+//!   escritorio (layouts, atajos, focos).
+//!
+//! Este crate es el único lenguaje que comparten: un par de enums y un
+//! marco de cable. No depende de Wayland, ni de `smithay`, ni de GPUI —
+//! sólo de [`mirada-layout`] para reusar [`Rect`] y [`WindowId`].
+//!
+//! - El Cerebro emite [`BrainCommand`]; el Cuerpo los aplica.
+//! - El Cuerpo emite [`BodyEvent`]; el Cerebro reacciona y recalcula.
+//!
+//! El cable es [`postcard`] con prefijo de longitud `u32` little-endian
+//! (ver [`write_frame`] / [`read_frame`]).
+
+#![forbid(unsafe_code)]
+
+use std::io::{self, Read, Write};
+
+use serde::de::DeserializeOwned;
+use serde::{Deserialize, Serialize};
+
+pub use mirada_layout::geometry::Rect;
+pub use mirada_layout::workspace::WindowId;
+use mirada_layout::{LayoutMode, Workspace};
+
+/// Identificador de una salida física (un monitor).
+pub type OutputId = u32;
+
+/// Dónde y cómo debe colocarse una ventana en pantalla.
+///
+/// Es la unidad de geometría que el Cerebro calcula y el Cuerpo aplica a
+/// la superficie Wayland correspondiente.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+pub struct WindowPlacement {
+    pub id: WindowId,
+    /// Rectángulo en píxeles de pantalla.
+    pub rect: Rect,
+    /// `false` la oculta sin destruirla (p. ej. en modo `Monocle`).
+    pub visible: bool,
+    /// `true` si esta ventana tiene el foco del teclado.
+    pub focused: bool,
+}
+
+/// Una orden del Cerebro al Cuerpo.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub enum BrainCommand {
+    /// Geometría completa del escritorio: el Cuerpo mueve/redimensiona
+    /// cada superficie y oculta las que falten en la lista.
+    Place(Vec<WindowPlacement>),
+    /// Pide el cierre ordenado de una ventana (`xdg_toplevel.close`).
+    Close(WindowId),
+    /// Mata al cliente de una ventana que no responde.
+    Kill(WindowId),
+    /// Registra los atajos globales que el Cuerpo debe interceptar y
+    /// devolver como [`BodyEvent::Keybind`] en vez de pasarlos al cliente.
+    GrabKeys(Vec<String>),
+    /// Cambia el cursor del puntero al nombre dado (tema XCursor).
+    SetCursor(String),
+    /// Apaga el Cuerpo y libera el hardware.
+    Shutdown,
+}
+
+/// Un hecho del Cuerpo que el Cerebro debe conocer.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub enum BodyEvent {
+    /// Apareció un monitor (al arrancar o en caliente).
+    OutputAdded { id: OutputId, width: i32, height: i32 },
+    /// Desapareció un monitor.
+    OutputRemoved { id: OutputId },
+    /// Un cliente creó una ventana de nivel superior.
+    WindowOpened { id: WindowId, app_id: String, title: String },
+    /// Una ventana se cerró (por el cliente o tras un [`BrainCommand::Close`]).
+    WindowClosed { id: WindowId },
+    /// Una ventana cambió su título.
+    WindowRetitled { id: WindowId, title: String },
+    /// El usuario pulsó un atajo registrado con [`BrainCommand::GrabKeys`].
+    Keybind(String),
+    /// El puntero entró en una ventana — el Cerebro puede enfocar al pasar.
+    PointerEntered { id: WindowId },
+}
+
+/// Tamaño máximo de un marco, en bytes. Acota el búfer de [`read_frame`]
+/// para que un prefijo de longitud corrupto no reserve gigabytes.
+pub const MAX_FRAME: usize = 16 * 1024 * 1024;
+
+/// Escribe `value` como un marco: prefijo `u32` LE con la longitud + el
+/// cuerpo serializado con `postcard`.
+pub fn write_frame<W: Write, T: Serialize>(w: &mut W, value: &T) -> io::Result<()> {
+    let body = postcard::to_stdvec(value)
+        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
+    if body.len() > MAX_FRAME {
+        return Err(io::Error::new(
+            io::ErrorKind::InvalidData,
+            "marco mayor que MAX_FRAME",
+        ));
+    }
+    w.write_all(&(body.len() as u32).to_le_bytes())?;
+    w.write_all(&body)?;
+    w.flush()
+}
+
+/// Lee un marco escrito por [`write_frame`]. Devuelve `Ok(None)` en un
+/// EOF limpio (el otro extremo cerró sin datos a medias).
+pub fn read_frame<R: Read, T: DeserializeOwned>(r: &mut R) -> io::Result<Option<T>> {
+    let mut len = [0u8; 4];
+    match r.read_exact(&mut len) {
+        Ok(()) => {}
+        Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(None),
+        Err(e) => return Err(e),
+    }
+    let len = u32::from_le_bytes(len) as usize;
+    if len > MAX_FRAME {
+        return Err(io::Error::new(
+            io::ErrorKind::InvalidData,
+            "prefijo de longitud mayor que MAX_FRAME",
+        ));
+    }
+    let mut body = vec![0u8; len];
+    r.read_exact(&mut body)?;
+    let value = postcard::from_bytes(&body)
+        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
+    Ok(Some(value))
+}
+
+/// Traduce un [`Workspace`] de mirada-layout a la geometría de cable.
+///
+/// Es el puente del Cerebro: toma el estado abstracto (ventanas, foco,
+/// modo) y la pantalla física, y produce el [`Vec<WindowPlacement>`] que
+/// va dentro de un [`BrainCommand::Place`].
+///
+/// En modo [`LayoutMode::Monocle`] sólo la ventana enfocada queda
+/// `visible`; en el resto de modos todas lo están.
+pub fn placements(ws: &Workspace, screen: Rect) -> Vec<WindowPlacement> {
+    let monocle = ws.params().mode == LayoutMode::Monocle;
+    let focused = ws.focused();
+    ws.layout(screen)
+        .into_iter()
+        .map(|(id, rect)| {
+            let is_focused = focused == Some(id);
+            WindowPlacement {
+                id,
+                rect,
+                visible: !monocle || is_focused,
+                focused: is_focused,
+            }
+        })
+        .collect()
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use mirada_layout::LayoutParams;
+    use std::io::Cursor;
+
+    fn ws(mode: LayoutMode) -> Workspace {
+        let mut w = Workspace::new(LayoutParams { mode, ..LayoutParams::default() });
+        for id in [10, 20, 30] {
+            w.add(id);
+        }
+        w
+    }
+
+    const SCREEN: Rect = Rect { x: 0, y: 0, w: 1920, h: 1080 };
+
+    #[test]
+    fn frame_round_trips_a_brain_command() {
+        let cmd = BrainCommand::Place(vec![WindowPlacement {
+            id: 7,
+            rect: Rect::new(0, 0, 800, 600),
+            visible: true,
+            focused: true,
+        }]);
+        let mut buf = Vec::new();
+        write_frame(&mut buf, &cmd).unwrap();
+        let mut cur = Cursor::new(buf);
+        let back: BrainCommand = read_frame(&mut cur).unwrap().unwrap();
+        assert_eq!(back, cmd);
+    }
+
+    #[test]
+    fn frame_round_trips_a_body_event() {
+        let ev = BodyEvent::WindowOpened {
+            id: 42,
+            app_id: "org.brahman.shuma".into(),
+            title: "shell".into(),
+        };
+        let mut buf = Vec::new();
+        write_frame(&mut buf, &ev).unwrap();
+        let mut cur = Cursor::new(buf);
+        let back: BodyEvent = read_frame(&mut cur).unwrap().unwrap();
+        assert_eq!(back, ev);
+    }
+
+    #[test]
+    fn several_frames_stream_in_order() {
+        let evs = vec![
+            BodyEvent::OutputAdded { id: 0, width: 2560, height: 1440 },
+            BodyEvent::WindowOpened { id: 1, app_id: "a".into(), title: "t".into() },
+            BodyEvent::Keybind("Super+Return".into()),
+        ];
+        let mut buf = Vec::new();
+        for ev in &evs {
+            write_frame(&mut buf, ev).unwrap();
+        }
+        let mut cur = Cursor::new(buf);
+        for ev in &evs {
+            let back: BodyEvent = read_frame(&mut cur).unwrap().unwrap();
+            assert_eq!(&back, ev);
+        }
+        // Agotado el stream, un EOF limpio.
+        assert!(read_frame::<_, BodyEvent>(&mut cur).unwrap().is_none());
+    }
+
+    #[test]
+    fn empty_reader_is_a_clean_eof() {
+        let mut cur = Cursor::new(Vec::new());
+        assert!(read_frame::<_, BrainCommand>(&mut cur).unwrap().is_none());
+    }
+
+    #[test]
+    fn an_oversized_length_prefix_is_rejected() {
+        let mut buf = Vec::new();
+        buf.extend_from_slice(&(u32::MAX).to_le_bytes());
+        let mut cur = Cursor::new(buf);
+        assert!(read_frame::<_, BrainCommand>(&mut cur).is_err());
+    }
+
+    #[test]
+    fn placements_cover_every_window() {
+        let p = placements(&ws(LayoutMode::Columns), SCREEN);
+        assert_eq!(p.len(), 3);
+        assert!(p.iter().all(|w| w.visible));
+        // Sólo una enfocada — la última añadida.
+        assert_eq!(p.iter().filter(|w| w.focused).count(), 1);
+        assert!(p.iter().find(|w| w.id == 30).unwrap().focused);
+    }
+
+    #[test]
+    fn monocle_keeps_only_the_focused_window_visible() {
+        let p = placements(&ws(LayoutMode::Monocle), SCREEN);
+        assert_eq!(p.len(), 3);
+        assert_eq!(p.iter().filter(|w| w.visible).count(), 1);
+        let shown = p.iter().find(|w| w.visible).unwrap();
+        assert!(shown.focused);
+        assert_eq!(shown.id, 30);
+    }
+
+    #[test]
+    fn an_empty_workspace_places_nothing() {
+        let empty = Workspace::new(LayoutParams::default());
+        assert!(placements(&empty, SCREEN).is_empty());
+    }
+
+    #[test]
+    fn placements_fill_a_place_command_round_trip() {
+        let cmd = BrainCommand::Place(placements(&ws(LayoutMode::Grid), SCREEN));
+        let mut buf = Vec::new();
+        write_frame(&mut buf, &cmd).unwrap();
+        let mut cur = Cursor::new(buf);
+        let back: BrainCommand = read_frame(&mut cur).unwrap().unwrap();
+        assert_eq!(back, cmd);
+    }
+}