feat(mirada): mirada-brain — orquestador de escritorio del compositor
Desktop agnóstico de GPUI/smithay: salidas, 9 escritorios virtuales,
registro de ventanas y foco. on_event(BodyEvent) -> Vec<BrainCommand>;
DesktopAction + mapa de teclas estilo tiling WM (Super). 17 tests.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
3e2777540f992e839c6bbf93e21452062d183f66 · ruwaq sergio · 2026-05-20T21:00:24Z (5 killa ñawpaq) · tayta e73658785 · kay commitpi sach'ata qhaway
Semántico sayk'uy · 43
crates/modules/mirada/mirada-brain/src/action.rs
crates/modules/mirada/mirada-brain/src/desktop.rs
crates/modules/mirada/mirada-brain/src/lib.rs
diff --git a/Cargo.lock b/Cargo.lock
index c5b854e..38d5968 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -7390,6 +7390,14 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "mirada-brain"
+version = "0.1.0"
+dependencies = [
+ "mirada-layout",
+ "mirada-protocol",
+]
+
[[package]]
name = "mirada-layout"
version = "0.1.0"
diff --git a/Cargo.toml b/Cargo.toml
index 334c019..c1f2ea7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -162,6 +162,7 @@ members = [
# ============================================================
"crates/modules/mirada/mirada-layout",
"crates/modules/mirada/mirada-protocol",
+ "crates/modules/mirada/mirada-brain",
# ============================================================
# modules/nakui/ — ERP matemático (categórico)
diff --git a/crates/modules/mirada/mirada-brain/Cargo.toml b/crates/modules/mirada/mirada-brain/Cargo.toml
new file mode 100644
index 0000000..2c06d6a
--- /dev/null
+++ b/crates/modules/mirada/mirada-brain/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "mirada-brain"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+authors.workspace = true
+publish.workspace = true
+description = "mirada — orquestador de escritorio del compositor: mantiene salidas, escritorios virtuales, ventanas y foco; consume BodyEvent y produce BrainCommand. Agnóstico de GPUI y de smithay."
+
+[dependencies]
+mirada-layout = { path = "../mirada-layout" }
+mirada-protocol = { path = "../mirada-protocol" }
diff --git a/crates/modules/mirada/mirada-brain/src/action.rs b/crates/modules/mirada/mirada-brain/src/action.rs
new file mode 100644
index 0000000..2b54d26
--- /dev/null
+++ b/crates/modules/mirada/mirada-brain/src/action.rs
@@ -0,0 +1,95 @@
+//! Acciones de escritorio y su mapa de teclas por defecto.
+//!
+//! Una [`DesktopAction`] es una orden de alto nivel del usuario, ya
+//! desligada de la tecla concreta: el [`Desktop`](crate::Desktop) las
+//! aplica sin saber qué combinación las disparó.
+
+use mirada_layout::LayoutMode;
+
+/// Número de escritorios virtuales que mantiene el `Desktop`.
+pub const WORKSPACE_COUNT: usize = 9;
+
+/// Una orden de escritorio de alto nivel.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum DesktopAction {
+ /// Mueve el foco a la ventana siguiente del escritorio activo.
+ FocusNext,
+ /// Mueve el foco a la ventana anterior.
+ FocusPrev,
+ /// Adelanta la ventana enfocada en el orden de teselado.
+ MoveForward,
+ /// Atrasa la ventana enfocada en el orden de teselado.
+ MoveBackward,
+ /// Cierra la ventana enfocada (cierre ordenado).
+ CloseFocused,
+ /// Pasa al siguiente modo de teselado.
+ CycleLayout,
+ /// Fija un modo de teselado concreto.
+ SetLayout(LayoutMode),
+ /// Activa el escritorio virtual `n` (índice 0-based).
+ SwitchWorkspace(usize),
+ /// Manda la ventana enfocada al escritorio virtual `n`.
+ SendToWorkspace(usize),
+ /// Apaga el compositor.
+ Quit,
+}
+
+/// Mapa de teclas por defecto, estilo *tiling WM* (modificador `Super`).
+///
+/// Las cadenas deben coincidir literalmente con las que el Cuerpo emite
+/// en [`BodyEvent::Keybind`](mirada_protocol::BodyEvent::Keybind); son
+/// también las que se registran con
+/// [`BrainCommand::GrabKeys`](mirada_protocol::BrainCommand::GrabKeys).
+pub fn default_keymap() -> Vec<(String, DesktopAction)> {
+ let mut map = vec![
+ ("Super+j".into(), DesktopAction::FocusNext),
+ ("Super+k".into(), DesktopAction::FocusPrev),
+ ("Super+Shift+j".into(), DesktopAction::MoveForward),
+ ("Super+Shift+k".into(), DesktopAction::MoveBackward),
+ ("Super+q".into(), DesktopAction::CloseFocused),
+ ("Super+space".into(), DesktopAction::CycleLayout),
+ ("Super+t".into(), DesktopAction::SetLayout(LayoutMode::MasterStack)),
+ ("Super+m".into(), DesktopAction::SetLayout(LayoutMode::Monocle)),
+ ("Super+g".into(), DesktopAction::SetLayout(LayoutMode::Grid)),
+ ("Super+c".into(), DesktopAction::SetLayout(LayoutMode::Columns)),
+ ("Super+Shift+e".into(), DesktopAction::Quit),
+ ];
+ // Un escritorio por dígito: `Super+1`..`Super+9` lo activan,
+ // `Super+Shift+1`.. mandan la ventana enfocada allí.
+ for n in 0..WORKSPACE_COUNT {
+ map.push((format!("Super+{}", n + 1), DesktopAction::SwitchWorkspace(n)));
+ map.push((
+ format!("Super+Shift+{}", n + 1),
+ DesktopAction::SendToWorkspace(n),
+ ));
+ }
+ map
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn keymap_has_no_duplicate_bindings() {
+ let map = default_keymap();
+ let mut keys: Vec<_> = map.iter().map(|(k, _)| k.clone()).collect();
+ keys.sort();
+ let unique = keys.len();
+ keys.dedup();
+ assert_eq!(keys.len(), unique, "hay un atajo repetido");
+ }
+
+ #[test]
+ fn keymap_covers_every_virtual_workspace() {
+ let map = default_keymap();
+ for n in 0..WORKSPACE_COUNT {
+ assert!(map
+ .iter()
+ .any(|(_, a)| *a == DesktopAction::SwitchWorkspace(n)));
+ assert!(map
+ .iter()
+ .any(|(_, a)| *a == DesktopAction::SendToWorkspace(n)));
+ }
+ }
+}
diff --git a/crates/modules/mirada/mirada-brain/src/desktop.rs b/crates/modules/mirada/mirada-brain/src/desktop.rs
new file mode 100644
index 0000000..526bd29
--- /dev/null
+++ b/crates/modules/mirada/mirada-brain/src/desktop.rs
@@ -0,0 +1,431 @@
+//! El [`Desktop`] — el estado del escritorio y el bucle `evento → comandos`.
+
+use std::collections::HashMap;
+
+use mirada_layout::{LayoutMode, LayoutParams, Rect, WindowId, Workspace};
+use mirada_protocol::{placements, BodyEvent, BrainCommand, OutputId};
+
+use crate::action::{default_keymap, DesktopAction, WORKSPACE_COUNT};
+
+/// Lo que el Cerebro sabe de una ventana: su identidad de aplicación.
+#[derive(Debug, Clone, PartialEq, Eq, Default)]
+pub struct WindowInfo {
+ pub app_id: String,
+ pub title: String,
+}
+
+/// El estado completo del escritorio.
+///
+/// Mantiene las salidas físicas, [`WORKSPACE_COUNT`] escritorios
+/// virtuales, el registro de ventanas y el mapa de atajos. El único
+/// punto de entrada es [`Desktop::on_event`]: traga un [`BodyEvent`],
+/// muta el estado y devuelve los [`BrainCommand`]s a enviar al Cuerpo.
+///
+/// Limitación de v1: el teselado se calcula sobre la salida primaria
+/// (la primera conectada). El multi-monitor real llegará después.
+pub struct Desktop {
+ /// Salidas físicas, en fila horizontal y en orden de aparición.
+ outputs: Vec<(OutputId, Rect)>,
+ /// Escritorios virtuales — `WORKSPACE_COUNT` fijos.
+ workspaces: Vec<Workspace>,
+ /// Índice del escritorio activo.
+ active: usize,
+ /// Identidad de cada ventana conocida.
+ windows: HashMap<WindowId, WindowInfo>,
+ /// Atajos globales → acción.
+ keymap: Vec<(String, DesktopAction)>,
+}
+
+impl Default for Desktop {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl Desktop {
+ /// Escritorio recién arrancado: sin salidas ni ventanas, con los
+ /// escritorios virtuales vacíos y el mapa de teclas por defecto.
+ pub fn new() -> Self {
+ let workspaces = (0..WORKSPACE_COUNT)
+ .map(|_| Workspace::new(LayoutParams::default()))
+ .collect();
+ Self {
+ outputs: Vec::new(),
+ workspaces,
+ active: 0,
+ windows: HashMap::new(),
+ keymap: default_keymap(),
+ }
+ }
+
+ /// El comando que registra los atajos globales en el Cuerpo. La app
+ /// GPUI lo envía una vez, al conectar.
+ pub fn grab_keys(&self) -> BrainCommand {
+ BrainCommand::GrabKeys(self.keymap.iter().map(|(k, _)| k.clone()).collect())
+ }
+
+ /// Geometría de la salida primaria, si hay alguna conectada.
+ pub fn screen(&self) -> Option<Rect> {
+ self.outputs.first().map(|(_, r)| *r)
+ }
+
+ /// Procesa un evento del Cuerpo: muta el estado y devuelve los
+ /// comandos a enviar de vuelta.
+ pub fn on_event(&mut self, event: BodyEvent) -> Vec<BrainCommand> {
+ match event {
+ BodyEvent::OutputAdded { id, width, height } => {
+ // Las salidas se alinean en fila a la derecha de las previas.
+ let x: i32 = self.outputs.iter().map(|(_, r)| r.w).sum();
+ self.outputs.push((id, Rect::new(x, 0, width, height)));
+ self.relayout()
+ }
+ BodyEvent::OutputRemoved { id } => {
+ self.outputs.retain(|(o, _)| *o != id);
+ self.relayout()
+ }
+ BodyEvent::WindowOpened { id, app_id, title } => {
+ self.windows.insert(id, WindowInfo { app_id, title });
+ self.workspaces[self.active].add(id);
+ self.relayout()
+ }
+ BodyEvent::WindowClosed { id } => {
+ self.windows.remove(&id);
+ for ws in &mut self.workspaces {
+ ws.remove(id);
+ }
+ self.relayout()
+ }
+ BodyEvent::WindowRetitled { id, title } => {
+ if let Some(info) = self.windows.get_mut(&id) {
+ info.title = title;
+ }
+ // Un cambio de título no altera la geometría.
+ Vec::new()
+ }
+ BodyEvent::PointerEntered { id } => {
+ // Foco al pasar el puntero, sólo si la ventana está en el
+ // escritorio activo.
+ if self.workspaces[self.active].focus_window(id) {
+ self.relayout()
+ } else {
+ Vec::new()
+ }
+ }
+ BodyEvent::Keybind(key) => {
+ match self.keymap.iter().find(|(k, _)| *k == key).map(|(_, a)| *a) {
+ Some(action) => self.apply(action),
+ None => Vec::new(),
+ }
+ }
+ }
+ }
+
+ /// Aplica una acción de escritorio directamente (sin pasar por una
+ /// tecla). Útil para disparar acciones desde un HUD.
+ pub fn apply(&mut self, action: DesktopAction) -> Vec<BrainCommand> {
+ match action {
+ DesktopAction::FocusNext => {
+ self.workspaces[self.active].focus_next();
+ self.relayout()
+ }
+ DesktopAction::FocusPrev => {
+ self.workspaces[self.active].focus_prev();
+ self.relayout()
+ }
+ DesktopAction::MoveForward => {
+ self.workspaces[self.active].move_focused_forward();
+ self.relayout()
+ }
+ DesktopAction::MoveBackward => {
+ self.workspaces[self.active].move_focused_backward();
+ self.relayout()
+ }
+ DesktopAction::CloseFocused => {
+ // Pedimos el cierre; el estado se actualiza al recibir el
+ // `WindowClosed` de vuelta, no antes.
+ match self.workspaces[self.active].focused() {
+ Some(id) => vec![BrainCommand::Close(id)],
+ None => Vec::new(),
+ }
+ }
+ DesktopAction::CycleLayout => {
+ let next = cycle_mode(self.workspaces[self.active].params().mode);
+ self.workspaces[self.active].set_mode(next);
+ self.relayout()
+ }
+ DesktopAction::SetLayout(mode) => {
+ self.workspaces[self.active].set_mode(mode);
+ self.relayout()
+ }
+ DesktopAction::SwitchWorkspace(n) => {
+ if n < self.workspaces.len() && n != self.active {
+ self.active = n;
+ self.relayout()
+ } else {
+ Vec::new()
+ }
+ }
+ DesktopAction::SendToWorkspace(n) => {
+ if n >= self.workspaces.len() || n == self.active {
+ return Vec::new();
+ }
+ match self.workspaces[self.active].focused() {
+ Some(id) => {
+ self.workspaces[self.active].remove(id);
+ self.workspaces[n].add(id);
+ self.relayout()
+ }
+ None => Vec::new(),
+ }
+ }
+ DesktopAction::Quit => vec![BrainCommand::Shutdown],
+ }
+ }
+
+ /// Recalcula la geometría del escritorio activo y la empaqueta en un
+ /// [`BrainCommand::Place`]. Sin salida conectada, no hay nada que
+ /// colocar.
+ fn relayout(&self) -> Vec<BrainCommand> {
+ match self.screen() {
+ Some(screen) => {
+ vec![BrainCommand::Place(placements(
+ &self.workspaces[self.active],
+ screen,
+ ))]
+ }
+ None => Vec::new(),
+ }
+ }
+
+ // --- Accesores de sólo lectura, para el HUD de la app GPUI ---------
+
+ /// Índice del escritorio activo.
+ pub fn active_index(&self) -> usize {
+ self.active
+ }
+
+ /// El escritorio activo.
+ pub fn active_workspace(&self) -> &Workspace {
+ &self.workspaces[self.active]
+ }
+
+ /// Las salidas conectadas, en orden.
+ pub fn outputs(&self) -> &[(OutputId, Rect)] {
+ &self.outputs
+ }
+
+ /// Identidad de una ventana conocida.
+ pub fn window_info(&self, id: WindowId) -> Option<&WindowInfo> {
+ self.windows.get(&id)
+ }
+
+ /// La ventana enfocada en el escritorio activo.
+ pub fn focused_window(&self) -> Option<WindowId> {
+ self.workspaces[self.active].focused()
+ }
+
+ /// Cuántas ventanas hay en cada escritorio virtual.
+ pub fn workspace_loads(&self) -> Vec<usize> {
+ self.workspaces.iter().map(Workspace::len).collect()
+ }
+}
+
+/// El siguiente modo en el ciclo de [`DesktopAction::CycleLayout`].
+fn cycle_mode(mode: LayoutMode) -> LayoutMode {
+ match mode {
+ LayoutMode::MasterStack => LayoutMode::Monocle,
+ LayoutMode::Monocle => LayoutMode::Grid,
+ LayoutMode::Grid => LayoutMode::Columns,
+ LayoutMode::Columns => LayoutMode::MasterStack,
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ /// Un escritorio con una salida 1920×1080 ya conectada.
+ fn desktop_with_screen() -> Desktop {
+ let mut d = Desktop::new();
+ d.on_event(BodyEvent::OutputAdded { id: 0, width: 1920, height: 1080 });
+ d
+ }
+
+ fn open(d: &mut Desktop, id: WindowId) -> Vec<BrainCommand> {
+ d.on_event(BodyEvent::WindowOpened {
+ id,
+ app_id: format!("app{id}"),
+ title: format!("win {id}"),
+ })
+ }
+
+ /// Extrae las colocaciones de un único `Place`.
+ fn places(cmds: &[BrainCommand]) -> &[mirada_protocol::WindowPlacement] {
+ match cmds {
+ [BrainCommand::Place(p)] => p,
+ other => panic!("se esperaba un solo Place, no {other:?}"),
+ }
+ }
+
+ #[test]
+ fn grab_keys_lists_the_whole_keymap() {
+ let d = Desktop::new();
+ match d.grab_keys() {
+ BrainCommand::GrabKeys(keys) => {
+ assert!(keys.contains(&"Super+j".to_string()));
+ assert!(keys.contains(&"Super+Shift+e".to_string()));
+ }
+ other => panic!("se esperaba GrabKeys, no {other:?}"),
+ }
+ }
+
+ #[test]
+ fn without_a_screen_nothing_is_placed() {
+ let mut d = Desktop::new();
+ assert!(open(&mut d, 1).is_empty());
+ }
+
+ #[test]
+ fn opening_a_window_places_it() {
+ let mut d = desktop_with_screen();
+ let cmds = open(&mut d, 1);
+ assert_eq!(places(&cmds).len(), 1);
+ assert_eq!(d.focused_window(), Some(1));
+ }
+
+ #[test]
+ fn closing_a_window_removes_it_everywhere() {
+ let mut d = desktop_with_screen();
+ open(&mut d, 1);
+ open(&mut d, 2);
+ let cmds = d.on_event(BodyEvent::WindowClosed { id: 1 });
+ assert_eq!(places(&cmds).len(), 1);
+ assert!(d.window_info(1).is_none());
+ assert_eq!(d.focused_window(), Some(2));
+ }
+
+ #[test]
+ fn focus_keybind_cycles_within_the_active_workspace() {
+ let mut d = desktop_with_screen();
+ for id in [1, 2, 3] {
+ open(&mut d, id);
+ }
+ assert_eq!(d.focused_window(), Some(3));
+ d.on_event(BodyEvent::Keybind("Super+j".into())); // next, da la vuelta
+ assert_eq!(d.focused_window(), Some(1));
+ d.on_event(BodyEvent::Keybind("Super+k".into())); // prev
+ assert_eq!(d.focused_window(), Some(3));
+ }
+
+ #[test]
+ fn close_focused_keybind_asks_to_close_the_focused_window() {
+ let mut d = desktop_with_screen();
+ open(&mut d, 7);
+ let cmds = d.on_event(BodyEvent::Keybind("Super+q".into()));
+ assert_eq!(cmds, vec![BrainCommand::Close(7)]);
+ // No se elimina hasta que el Cuerpo confirme con WindowClosed.
+ assert!(d.window_info(7).is_some());
+ }
+
+ #[test]
+ fn cycle_layout_walks_the_four_modes() {
+ let mut d = desktop_with_screen();
+ open(&mut d, 1);
+ assert_eq!(d.active_workspace().params().mode, LayoutMode::MasterStack);
+ for expected in [
+ LayoutMode::Monocle,
+ LayoutMode::Grid,
+ LayoutMode::Columns,
+ LayoutMode::MasterStack,
+ ] {
+ d.on_event(BodyEvent::Keybind("Super+space".into()));
+ assert_eq!(d.active_workspace().params().mode, expected);
+ }
+ }
+
+ #[test]
+ fn monocle_keybind_hides_all_but_the_focused_window() {
+ let mut d = desktop_with_screen();
+ for id in [1, 2, 3] {
+ open(&mut d, id);
+ }
+ let cmds = d.on_event(BodyEvent::Keybind("Super+m".into()));
+ let visible = places(&cmds).iter().filter(|p| p.visible).count();
+ assert_eq!(visible, 1);
+ }
+
+ #[test]
+ fn switching_workspace_changes_what_is_placed() {
+ let mut d = desktop_with_screen();
+ open(&mut d, 1);
+ open(&mut d, 2);
+ // Escritorio 2 (índice 1) está vacío.
+ let cmds = d.on_event(BodyEvent::Keybind("Super+2".into()));
+ assert!(places(&cmds).is_empty());
+ assert_eq!(d.active_index(), 1);
+ // Volver al 1 reaparece las dos ventanas.
+ let cmds = d.on_event(BodyEvent::Keybind("Super+1".into()));
+ assert_eq!(places(&cmds).len(), 2);
+ }
+
+ #[test]
+ fn send_to_workspace_moves_the_focused_window() {
+ let mut d = desktop_with_screen();
+ open(&mut d, 1);
+ open(&mut d, 2); // enfocada
+ d.on_event(BodyEvent::Keybind("Super+Shift+3".into()));
+ assert_eq!(d.workspace_loads()[0], 1); // sólo queda la 1
+ assert_eq!(d.workspace_loads()[2], 1); // la 2 viajó al escritorio 3
+ // La ventana 2 sigue registrada — sólo cambió de escritorio.
+ assert!(d.window_info(2).is_some());
+ }
+
+ #[test]
+ fn pointer_focuses_a_window_in_the_active_workspace() {
+ let mut d = desktop_with_screen();
+ open(&mut d, 1);
+ open(&mut d, 2); // enfocada
+ d.on_event(BodyEvent::PointerEntered { id: 1 });
+ assert_eq!(d.focused_window(), Some(1));
+ }
+
+ #[test]
+ fn retitling_updates_the_registry_without_relayout() {
+ let mut d = desktop_with_screen();
+ open(&mut d, 1);
+ let cmds = d.on_event(BodyEvent::WindowRetitled {
+ id: 1,
+ title: "nuevo".into(),
+ });
+ assert!(cmds.is_empty());
+ assert_eq!(d.window_info(1).unwrap().title, "nuevo");
+ }
+
+ #[test]
+ fn an_unknown_keybind_does_nothing() {
+ let mut d = desktop_with_screen();
+ open(&mut d, 1);
+ assert!(d.on_event(BodyEvent::Keybind("Super+F12".into())).is_empty());
+ }
+
+ #[test]
+ fn quit_emits_a_shutdown() {
+ let mut d = desktop_with_screen();
+ assert_eq!(
+ d.on_event(BodyEvent::Keybind("Super+Shift+e".into())),
+ vec![BrainCommand::Shutdown]
+ );
+ }
+
+ #[test]
+ fn outputs_lay_side_by_side() {
+ let mut d = Desktop::new();
+ d.on_event(BodyEvent::OutputAdded { id: 0, width: 1920, height: 1080 });
+ d.on_event(BodyEvent::OutputAdded { id: 1, width: 2560, height: 1440 });
+ assert_eq!(d.outputs().len(), 2);
+ // La segunda salida arranca donde acaba la primera.
+ assert_eq!(d.outputs()[1].1.x, 1920);
+ // El teselado sigue sobre la salida primaria.
+ assert_eq!(d.screen().unwrap().w, 1920);
+ }
+}
diff --git a/crates/modules/mirada/mirada-brain/src/lib.rs b/crates/modules/mirada/mirada-brain/src/lib.rs
new file mode 100644
index 0000000..0d3696b
--- /dev/null
+++ b/crates/modules/mirada/mirada-brain/src/lib.rs
@@ -0,0 +1,25 @@
+//! `mirada-brain` — el orquestador de escritorio del compositor.
+//!
+//! Es el "Cerebro" de mirada sin pantalla: mantiene el estado del
+//! escritorio (salidas, escritorios virtuales, ventanas, foco), consume
+//! los [`BodyEvent`]s que reporta el Cuerpo y produce los
+//! [`BrainCommand`]s que el Cuerpo aplica.
+//!
+//! Es agnóstico de GPUI y de `smithay`: una app GPUI sólo lo *envuelve*
+//! para pintar un HUD y para mover los bytes por el cable de
+//! [`mirada_protocol`]. Toda la lógica vive aquí y es determinista —
+//! la misma secuencia de eventos da siempre el mismo estado.
+//!
+//! - [`action`] — las acciones de escritorio y el mapa de teclas.
+//! - [`desktop`] — el [`Desktop`]: el estado y el bucle `evento → comandos`.
+
+#![forbid(unsafe_code)]
+
+pub mod action;
+pub mod desktop;
+
+pub use action::{default_keymap, DesktopAction, WORKSPACE_COUNT};
+pub use desktop::{Desktop, WindowInfo};
+
+pub use mirada_layout::{LayoutMode, LayoutParams, Rect, WindowId, Workspace};
+pub use mirada_protocol::{BodyEvent, BrainCommand, OutputId, WindowPlacement};