tawasuyu

tawasuyu/brahman

Rust 582TOML 222Markdown 73JSON 198630 Definitions

⎇ f9c4bf594e361ce3a59435f2b263840ae81ac87a crates/modules/mirada/mirada-protocol/src/lib.rs Rust · 319 lines · 11.7 KB History Who touched what raw

feat(mirada): fullscreen iniciado por el cliente + HUD multi-salida f9c4bf594 · sergio ·

1//! `mirada-protocol` — el contrato Cerebro↔Cuerpo del compositor.
2//!
3//! mirada se parte en dos procesos:
4//!
5//! - **El Cuerpo** (`mirada-compositor`, sobre `smithay`): habla Wayland
6//!   con los clientes, posee el hardware (DRM/GPU/libinput) y compone las
7//!   superficies reales. Los píxeles nunca salen de él.
8//! - **El Cerebro** (una app GPUI sobre [`mirada-layout`]): decide *dónde*
9//!   va cada ventana — pura aritmética de rectángulos— y orquesta el
10//!   escritorio (layouts, atajos, focos).
11//!
12//! Este crate es el único lenguaje que comparten: un par de enums y un
13//! marco de cable. No depende de Wayland, ni de `smithay`, ni de GPUI —
14//! sólo de [`mirada-layout`] para reusar [`Rect`] y [`WindowId`].
15//!
16//! - El Cerebro emite [`BrainCommand`]; el Cuerpo los aplica.
17//! - El Cuerpo emite [`BodyEvent`]; el Cerebro reacciona y recalcula.
18//!
19//! El cable es [`postcard`] con prefijo de longitud `u32` little-endian
20//! (ver [`write_frame`] / [`read_frame`]).
21
22#![forbid(unsafe_code)]
23
24use std::io::{self, Read, Write};
25
26use serde::de::DeserializeOwned;
27use serde::{Deserialize, Serialize};
28
29pub use mirada_layout::geometry::Rect;
30pub use mirada_layout::workspace::WindowId;
31use mirada_layout::{LayoutMode, Workspace};
32
33/// Identificador de una salida física (un monitor).
34pub type OutputId = u32;
35
36/// Dónde y cómo debe colocarse una ventana en pantalla.
37///
38/// Es la unidad de geometría que el Cerebro calcula y el Cuerpo aplica a
39/// la superficie Wayland correspondiente.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
41pub struct WindowPlacement {
42    pub id: WindowId,
43    /// Rectángulo en píxeles de pantalla.
44    pub rect: Rect,
45    /// `false` la oculta sin destruirla (p. ej. en modo `Monocle`).
46    pub visible: bool,
47    /// `true` si esta ventana tiene el foco del teclado.
48    pub focused: bool,
49    /// `true` si flota (fuera del teselado): el Cuerpo la pinta encima.
50    pub floating: bool,
51    /// `true` si está en pantalla completa: cubre toda la salida.
52    pub fullscreen: bool,
53}
54
55/// Una orden del Cerebro al Cuerpo.
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
57pub enum BrainCommand {
58    /// Geometría completa del escritorio: el Cuerpo mueve/redimensiona
59    /// cada superficie y oculta las que falten en la lista.
60    Place(Vec<WindowPlacement>),
61    /// Pide el cierre ordenado de una ventana (`xdg_toplevel.close`).
62    Close(WindowId),
63    /// Mata al cliente de una ventana que no responde.
64    Kill(WindowId),
65    /// Registra los atajos globales que el Cuerpo debe interceptar y
66    /// devolver como [`BodyEvent::Keybind`] en vez de pasarlos al cliente.
67    GrabKeys(Vec<String>),
68    /// Cambia el cursor del puntero al nombre dado (tema XCursor).
69    SetCursor(String),
70    /// Apaga el Cuerpo y libera el hardware.
71    Shutdown,
72}
73
74/// Un hecho del Cuerpo que el Cerebro debe conocer.
75#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
76pub enum BodyEvent {
77    /// Apareció un monitor (al arrancar o en caliente).
78    OutputAdded { id: OutputId, width: i32, height: i32 },
79    /// Desapareció un monitor.
80    OutputRemoved { id: OutputId },
81    /// Un cliente creó una ventana de nivel superior.
82    WindowOpened { id: WindowId, app_id: String, title: String },
83    /// Una ventana se cerró (por el cliente o tras un [`BrainCommand::Close`]).
84    WindowClosed { id: WindowId },
85    /// Una ventana cambió su título.
86    WindowRetitled { id: WindowId, title: String },
87    /// El usuario pulsó un atajo registrado con [`BrainCommand::GrabKeys`].
88    Keybind(String),
89    /// El puntero entró en una ventana — el Cerebro puede enfocar al pasar.
90    PointerEntered { id: WindowId },
91    /// Un cliente pidió pantalla completa para su ventana (`true`), o la
92    /// soltó (`false`) — `xdg_toplevel.set_fullscreen`.
93    FullscreenRequest { id: WindowId, fullscreen: bool },
94}
95
96/// Tamaño máximo de un marco, en bytes. Acota el búfer de [`read_frame`]
97/// para que un prefijo de longitud corrupto no reserve gigabytes.
98pub const MAX_FRAME: usize = 16 * 1024 * 1024;
99
100/// Escribe `value` como un marco: prefijo `u32` LE con la longitud + el
101/// cuerpo serializado con `postcard`.
102pub fn write_frame<W: Write, T: Serialize>(w: &mut W, value: &T) -> io::Result<()> {
103    let body = postcard::to_stdvec(value)
104        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
105    if body.len() > MAX_FRAME {
106        return Err(io::Error::new(
107            io::ErrorKind::InvalidData,
108            "marco mayor que MAX_FRAME",
109        ));
110    }
111    w.write_all(&(body.len() as u32).to_le_bytes())?;
112    w.write_all(&body)?;
113    w.flush()
114}
115
116/// Lee un marco escrito por [`write_frame`]. Devuelve `Ok(None)` en un
117/// EOF limpio (el otro extremo cerró sin datos a medias).
118pub fn read_frame<R: Read, T: DeserializeOwned>(r: &mut R) -> io::Result<Option<T>> {
119    let mut len = [0u8; 4];
120    match r.read_exact(&mut len) {
121        Ok(()) => {}
122        Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(None),
123        Err(e) => return Err(e),
124    }
125    let len = u32::from_le_bytes(len) as usize;
126    if len > MAX_FRAME {
127        return Err(io::Error::new(
128            io::ErrorKind::InvalidData,
129            "prefijo de longitud mayor que MAX_FRAME",
130        ));
131    }
132    let mut body = vec![0u8; len];
133    r.read_exact(&mut body)?;
134    let value = postcard::from_bytes(&body)
135        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
136    Ok(Some(value))
137}
138
139/// Traduce un [`Workspace`] de mirada-layout a la geometría de cable.
140///
141/// Es el puente del Cerebro: toma el estado abstracto (ventanas, foco,
142/// modo) y la pantalla física, y produce el [`Vec<WindowPlacement>`] que
143/// va dentro de un [`BrainCommand::Place`].
144///
145/// En modo [`LayoutMode::Monocle`] sólo la ventana enfocada queda
146/// `visible`; en el resto de modos todas lo están.
147pub fn placements(ws: &Workspace, screen: Rect) -> Vec<WindowPlacement> {
148    let fullscreen = ws.fullscreen();
149    let monocle = ws.params().mode == LayoutMode::Monocle;
150    let focused = ws.focused();
151    ws.layout(screen)
152        .into_iter()
153        .map(|(id, rect)| {
154            let floating = ws.is_floating(id);
155            let is_fs = fullscreen == Some(id);
156            // Con una ventana en pantalla completa manda ella: ocupa toda
157            // la salida, es la única visible y se lleva el foco.
158            let (rect, visible, is_focused) = match fullscreen {
159                Some(_) => (if is_fs { screen } else { rect }, is_fs, is_fs),
160                None => {
161                    let f = focused == Some(id);
162                    // Una flotante siempre se ve; en `Monocle`, sólo la enfocada.
163                    (rect, floating || !monocle || f, f)
164                }
165            };
166            WindowPlacement {
167                id,
168                rect,
169                visible,
170                focused: is_focused,
171                floating,
172                fullscreen: is_fs,
173            }
174        })
175        .collect()
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    use mirada_layout::LayoutParams;
182    use std::io::Cursor;
183
184    fn ws(mode: LayoutMode) -> Workspace {
185        let mut w = Workspace::new(LayoutParams { mode, ..LayoutParams::default() });
186        for id in [10, 20, 30] {
187            w.add(id);
188        }
189        w
190    }
191
192    const SCREEN: Rect = Rect { x: 0, y: 0, w: 1920, h: 1080 };
193
194    #[test]
195    fn frame_round_trips_a_brain_command() {
196        let cmd = BrainCommand::Place(vec![WindowPlacement {
197            id: 7,
198            rect: Rect::new(0, 0, 800, 600),
199            visible: true,
200            focused: true,
201            floating: false,
202            fullscreen: false,
203        }]);
204        let mut buf = Vec::new();
205        write_frame(&mut buf, &cmd).unwrap();
206        let mut cur = Cursor::new(buf);
207        let back: BrainCommand = read_frame(&mut cur).unwrap().unwrap();
208        assert_eq!(back, cmd);
209    }
210
211    #[test]
212    fn frame_round_trips_a_body_event() {
213        let ev = BodyEvent::WindowOpened {
214            id: 42,
215            app_id: "org.brahman.shuma".into(),
216            title: "shell".into(),
217        };
218        let mut buf = Vec::new();
219        write_frame(&mut buf, &ev).unwrap();
220        let mut cur = Cursor::new(buf);
221        let back: BodyEvent = read_frame(&mut cur).unwrap().unwrap();
222        assert_eq!(back, ev);
223    }
224
225    #[test]
226    fn several_frames_stream_in_order() {
227        let evs = vec![
228            BodyEvent::OutputAdded { id: 0, width: 2560, height: 1440 },
229            BodyEvent::WindowOpened { id: 1, app_id: "a".into(), title: "t".into() },
230            BodyEvent::Keybind("Super+Return".into()),
231        ];
232        let mut buf = Vec::new();
233        for ev in &evs {
234            write_frame(&mut buf, ev).unwrap();
235        }
236        let mut cur = Cursor::new(buf);
237        for ev in &evs {
238            let back: BodyEvent = read_frame(&mut cur).unwrap().unwrap();
239            assert_eq!(&back, ev);
240        }
241        // Agotado el stream, un EOF limpio.
242        assert!(read_frame::<_, BodyEvent>(&mut cur).unwrap().is_none());
243    }
244
245    #[test]
246    fn empty_reader_is_a_clean_eof() {
247        let mut cur = Cursor::new(Vec::new());
248        assert!(read_frame::<_, BrainCommand>(&mut cur).unwrap().is_none());
249    }
250
251    #[test]
252    fn an_oversized_length_prefix_is_rejected() {
253        let mut buf = Vec::new();
254        buf.extend_from_slice(&(u32::MAX).to_le_bytes());
255        let mut cur = Cursor::new(buf);
256        assert!(read_frame::<_, BrainCommand>(&mut cur).is_err());
257    }
258
259    #[test]
260    fn placements_cover_every_window() {
261        let p = placements(&ws(LayoutMode::Columns), SCREEN);
262        assert_eq!(p.len(), 3);
263        assert!(p.iter().all(|w| w.visible));
264        // Sólo una enfocada — la última añadida.
265        assert_eq!(p.iter().filter(|w| w.focused).count(), 1);
266        assert!(p.iter().find(|w| w.id == 30).unwrap().focused);
267    }
268
269    #[test]
270    fn monocle_keeps_only_the_focused_window_visible() {
271        let p = placements(&ws(LayoutMode::Monocle), SCREEN);
272        assert_eq!(p.len(), 3);
273        assert_eq!(p.iter().filter(|w| w.visible).count(), 1);
274        let shown = p.iter().find(|w| w.visible).unwrap();
275        assert!(shown.focused);
276        assert_eq!(shown.id, 30);
277    }
278
279    #[test]
280    fn an_empty_workspace_places_nothing() {
281        let empty = Workspace::new(LayoutParams::default());
282        assert!(placements(&empty, SCREEN).is_empty());
283    }
284
285    #[test]
286    fn a_floating_window_is_marked_and_stays_visible_in_monocle() {
287        let mut w = ws(LayoutMode::Monocle); // Monocle oculta las no enfocadas
288        w.set_floating(10, Some(Rect::new(0, 0, 200, 200)));
289        let p = placements(&w, SCREEN);
290        let f = p.iter().find(|x| x.id == 10).unwrap();
291        assert!(f.floating);
292        assert!(f.visible, "una flotante se ve aunque el modo sea Monocle");
293        // Y conserva su rectángulo flotante.
294        assert_eq!(f.rect, Rect::new(0, 0, 200, 200));
295    }
296
297    #[test]
298    fn a_fullscreen_window_covers_the_screen_and_hides_the_rest() {
299        let mut w = ws(LayoutMode::Columns);
300        w.set_fullscreen(Some(20));
301        let p = placements(&w, SCREEN);
302        let fs = p.iter().find(|x| x.id == 20).unwrap();
303        assert!(fs.fullscreen);
304        assert!(fs.focused, "la ventana en pantalla completa se lleva el foco");
305        assert_eq!(fs.rect, SCREEN);
306        // El resto queda oculto.
307        assert!(p.iter().filter(|x| x.id != 20).all(|x| !x.visible));
308    }
309
310    #[test]
311    fn placements_fill_a_place_command_round_trip() {
312        let cmd = BrainCommand::Place(placements(&ws(LayoutMode::Grid), SCREEN));
313        let mut buf = Vec::new();
314        write_frame(&mut buf, &cmd).unwrap();
315        let mut cur = Cursor::new(buf);
316        let back: BrainCommand = read_frame(&mut cur).unwrap().unwrap();
317        assert_eq!(back, cmd);
318    }
319}