feat(mirada): mirada-body — contabilidad del Cuerpo del compositor
BodyState agnóstico de smithay: lleva salidas + superficies, traduce BrainCommand a BodyOp (sólo lo que cambia) y emite BodyEvent desde los mutadores del backend. Ejemplo headless: Cuerpo sin gráficos guiado por stdin para ejercitar el bucle Cerebro↔Cuerpo. 13 tests. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Generated
+8
@@ -7401,6 +7401,14 @@ dependencies = [
|
|||||||
"nahual-theme",
|
"nahual-theme",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mirada-body"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"mirada-link",
|
||||||
|
"mirada-protocol",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mirada-brain"
|
name = "mirada-brain"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ members = [
|
|||||||
"crates/modules/mirada/mirada-protocol",
|
"crates/modules/mirada/mirada-protocol",
|
||||||
"crates/modules/mirada/mirada-brain",
|
"crates/modules/mirada/mirada-brain",
|
||||||
"crates/modules/mirada/mirada-link",
|
"crates/modules/mirada/mirada-link",
|
||||||
|
"crates/modules/mirada/mirada-body",
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# modules/nakui/ — ERP matemático (categórico)
|
# modules/nakui/ — ERP matemático (categórico)
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "mirada-body"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
description = "mirada — estado del Cuerpo del compositor: lleva la cuenta de salidas y superficies, traduce los BrainCommand a operaciones de backend y emite los BodyEvent. Agnóstico de smithay."
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
mirada-protocol = { path = "../mirada-protocol" }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
mirada-link = { path = "../mirada-link" }
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
//! `headless` — un Cuerpo de carmen sin gráficos, guiado por stdin.
|
||||||
|
//!
|
||||||
|
//! Es el banco de pruebas del Cerebro: implementa el lado del Cuerpo del
|
||||||
|
//! protocolo (escucha en un socket, lleva un [`BodyState`], manda
|
||||||
|
//! [`BodyEvent`]s y ejecuta —imprimiéndolas— las [`BodyOp`]s) sin tocar
|
||||||
|
//! `smithay` ni el hardware. Así se ejercita el bucle entero
|
||||||
|
//! Cerebro↔Cuerpo desde una terminal.
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! # terminal 1 — el Cuerpo escucha
|
||||||
|
//! cargo run -p mirada-body --example headless -- /tmp/mirada.sock
|
||||||
|
//! # terminal 2 — el Cerebro se conecta
|
||||||
|
//! MIRADA_SOCKET=/tmp/mirada.sock cargo run -p mirada
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! Órdenes de stdin: `output <w> <h>`, `open <app>`, `close <id>`,
|
||||||
|
//! `title <id> <texto>`, `key <combo>`, `pointer <id>`, `tick`, `quit`.
|
||||||
|
|
||||||
|
use std::io::BufRead;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use mirada_body::BodyState;
|
||||||
|
use mirada_link::BodyLink;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let path = std::env::args()
|
||||||
|
.nth(1)
|
||||||
|
.unwrap_or_else(|| "/tmp/mirada.sock".to_string());
|
||||||
|
let _ = std::fs::remove_file(&path);
|
||||||
|
|
||||||
|
println!("Cuerpo headless · escuchando en {path} — esperando al Cerebro…");
|
||||||
|
let mut link: BodyLink = match BodyLink::listen(&path) {
|
||||||
|
Ok(l) => l,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("no se pudo escuchar en {path}: {e}");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
println!("Cerebro conectado. Órdenes: output / open / close / title / key / pointer / tick / quit");
|
||||||
|
|
||||||
|
let mut body = BodyState::new();
|
||||||
|
let mut next_id: u64 = 1;
|
||||||
|
let stdin = std::io::stdin();
|
||||||
|
|
||||||
|
for line in stdin.lock().lines() {
|
||||||
|
let line = match line {
|
||||||
|
Ok(l) => l,
|
||||||
|
Err(_) => break,
|
||||||
|
};
|
||||||
|
let mut parts = line.split_whitespace();
|
||||||
|
let cmd = parts.next().unwrap_or("");
|
||||||
|
let rest: Vec<&str> = parts.collect();
|
||||||
|
|
||||||
|
// Cada orden o bien manda un evento al Cerebro, o no manda nada.
|
||||||
|
let event = match cmd {
|
||||||
|
"output" if rest.len() == 2 => {
|
||||||
|
match (rest[0].parse(), rest[1].parse()) {
|
||||||
|
(Ok(w), Ok(h)) => Some(body.add_output(0, w, h)),
|
||||||
|
_ => {
|
||||||
|
eprintln!("uso: output <ancho> <alto>");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"open" if !rest.is_empty() => {
|
||||||
|
let app = rest[0];
|
||||||
|
let id = next_id;
|
||||||
|
next_id += 1;
|
||||||
|
println!(" → ventana {id} ({app})");
|
||||||
|
Some(body.open_surface(id, format!("org.brahman.{app}"), format!("{app} {id}")))
|
||||||
|
}
|
||||||
|
"close" if rest.len() == 1 => match rest[0].parse() {
|
||||||
|
Ok(id) => body.close_surface(id),
|
||||||
|
Err(_) => {
|
||||||
|
eprintln!("uso: close <id>");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title" if rest.len() >= 2 => match rest[0].parse() {
|
||||||
|
Ok(id) => body.retitle_surface(id, rest[1..].join(" ")),
|
||||||
|
Err(_) => {
|
||||||
|
eprintln!("uso: title <id> <texto>");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"key" if rest.len() == 1 => Some(body.keybind(rest[0])),
|
||||||
|
"pointer" if rest.len() == 1 => match rest[0].parse() {
|
||||||
|
Ok(id) => Some(body.pointer_enter(id)),
|
||||||
|
Err(_) => {
|
||||||
|
eprintln!("uso: pointer <id>");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tick" => None,
|
||||||
|
"quit" | "exit" => break,
|
||||||
|
"" => None,
|
||||||
|
other => {
|
||||||
|
eprintln!("orden desconocida: {other}");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(ev) = event {
|
||||||
|
if link.send(&ev).is_err() {
|
||||||
|
eprintln!("el Cerebro cerró la conexión.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deja que el Cerebro responda y ejecuta lo que ordene.
|
||||||
|
std::thread::sleep(Duration::from_millis(40));
|
||||||
|
for command in link.drain() {
|
||||||
|
for op in body.apply(command) {
|
||||||
|
println!(" · op: {op:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Cuerpo headless · adiós.");
|
||||||
|
let _ = std::fs::remove_file(&path);
|
||||||
|
}
|
||||||
@@ -0,0 +1,379 @@
|
|||||||
|
//! `mirada-body` — el estado del Cuerpo del compositor.
|
||||||
|
//!
|
||||||
|
//! El "Cuerpo" de carmen (`mirada-compositor`, sobre `smithay`) tiene
|
||||||
|
//! dos mitades: el *backend*, que habla Wayland y posee el hardware, y
|
||||||
|
//! esta *contabilidad* — qué salidas y superficies existen y con qué
|
||||||
|
//! geometría. Aislarla deja el backend reducido a "ejecuta estas
|
||||||
|
//! [`BodyOp`]" y la hace testeable sin un servidor gráfico.
|
||||||
|
//!
|
||||||
|
//! El flujo es simétrico al del Cerebro:
|
||||||
|
//!
|
||||||
|
//! - El backend avisa de cambios de hardware/clientes con los mutadores
|
||||||
|
//! ([`BodyState::open_surface`], [`BodyState::add_output`], …), que
|
||||||
|
//! devuelven el [`BodyEvent`] a mandar al Cerebro.
|
||||||
|
//! - El Cerebro responde con [`BrainCommand`]s; [`BodyState::apply`] los
|
||||||
|
//! traduce a [`BodyOp`]s concretas que el backend ejecuta.
|
||||||
|
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
use mirada_protocol::{BodyEvent, BrainCommand, OutputId, Rect, WindowId};
|
||||||
|
|
||||||
|
/// Una superficie Wayland desde la óptica del Cuerpo.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Surface {
|
||||||
|
pub app_id: String,
|
||||||
|
pub title: String,
|
||||||
|
/// Geometría aplicada — `None` hasta la primera [`BodyOp::Configure`].
|
||||||
|
pub geometry: Option<Rect>,
|
||||||
|
pub visible: bool,
|
||||||
|
pub focused: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Surface {
|
||||||
|
fn new(app_id: String, title: String) -> Self {
|
||||||
|
Self { app_id, title, geometry: None, visible: false, focused: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Una orden concreta para el backend (smithay, headless, …).
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum BodyOp {
|
||||||
|
/// Recoloca una superficie y la muestra u oculta.
|
||||||
|
Configure { id: WindowId, rect: Rect, visible: bool },
|
||||||
|
/// Da el foco del teclado a una superficie.
|
||||||
|
Focus(WindowId),
|
||||||
|
/// Quita el foco a todas las superficies.
|
||||||
|
Unfocus,
|
||||||
|
/// Pide el cierre ordenado de un cliente.
|
||||||
|
CloseClient(WindowId),
|
||||||
|
/// Mata a un cliente que no responde.
|
||||||
|
KillClient(WindowId),
|
||||||
|
/// Registra los atajos globales a interceptar.
|
||||||
|
SetGrabs(Vec<String>),
|
||||||
|
/// Cambia el cursor del puntero.
|
||||||
|
SetCursor(String),
|
||||||
|
/// Apaga el compositor y libera el hardware.
|
||||||
|
Shutdown,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// La contabilidad del Cuerpo: salidas y superficies.
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct BodyState {
|
||||||
|
outputs: Vec<(OutputId, Rect)>,
|
||||||
|
/// `BTreeMap` para que el orden de las `BodyOp` sea determinista.
|
||||||
|
surfaces: BTreeMap<WindowId, Surface>,
|
||||||
|
focused: Option<WindowId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BodyState {
|
||||||
|
/// Cuerpo recién arrancado: sin salidas ni superficies.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Traducción de comandos del Cerebro --------------------------
|
||||||
|
|
||||||
|
/// Traduce un comando del Cerebro a las operaciones de backend que lo
|
||||||
|
/// materializan. Sólo emite lo que de verdad cambia: un `Place`
|
||||||
|
/// idéntico al estado actual no produce ninguna `BodyOp`.
|
||||||
|
pub fn apply(&mut self, cmd: BrainCommand) -> Vec<BodyOp> {
|
||||||
|
match cmd {
|
||||||
|
BrainCommand::Place(placements) => {
|
||||||
|
let mut ops = Vec::new();
|
||||||
|
let listed: Vec<WindowId> = placements.iter().map(|p| p.id).collect();
|
||||||
|
let mut new_focus = None;
|
||||||
|
|
||||||
|
// Reconfigura las superficies que aparecen en la lista.
|
||||||
|
for p in &placements {
|
||||||
|
if p.focused {
|
||||||
|
new_focus = Some(p.id);
|
||||||
|
}
|
||||||
|
if let Some(s) = self.surfaces.get_mut(&p.id) {
|
||||||
|
if s.geometry != Some(p.rect) || s.visible != p.visible {
|
||||||
|
s.geometry = Some(p.rect);
|
||||||
|
s.visible = p.visible;
|
||||||
|
ops.push(BodyOp::Configure {
|
||||||
|
id: p.id,
|
||||||
|
rect: p.rect,
|
||||||
|
visible: p.visible,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Oculta lo que el Cerebro ya no coloca.
|
||||||
|
for (id, s) in &mut self.surfaces {
|
||||||
|
if !listed.contains(id) && s.visible {
|
||||||
|
s.visible = false;
|
||||||
|
let rect = s.geometry.unwrap_or(Rect::new(0, 0, 0, 0));
|
||||||
|
ops.push(BodyOp::Configure { id: *id, rect, visible: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reasigna el foco sólo si cambió.
|
||||||
|
if new_focus != self.focused {
|
||||||
|
self.focused = new_focus;
|
||||||
|
for (id, s) in &mut self.surfaces {
|
||||||
|
s.focused = Some(*id) == new_focus;
|
||||||
|
}
|
||||||
|
ops.push(match new_focus {
|
||||||
|
Some(id) => BodyOp::Focus(id),
|
||||||
|
None => BodyOp::Unfocus,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ops
|
||||||
|
}
|
||||||
|
BrainCommand::Close(id) => vec![BodyOp::CloseClient(id)],
|
||||||
|
BrainCommand::Kill(id) => vec![BodyOp::KillClient(id)],
|
||||||
|
BrainCommand::GrabKeys(keys) => vec![BodyOp::SetGrabs(keys)],
|
||||||
|
BrainCommand::SetCursor(name) => vec![BodyOp::SetCursor(name)],
|
||||||
|
BrainCommand::Shutdown => vec![BodyOp::Shutdown],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Mutadores del backend → eventos para el Cerebro -------------
|
||||||
|
|
||||||
|
/// Registra una salida recién conectada.
|
||||||
|
pub fn add_output(&mut self, id: OutputId, width: i32, height: i32) -> BodyEvent {
|
||||||
|
self.outputs.push((id, Rect::new(0, 0, width, height)));
|
||||||
|
BodyEvent::OutputAdded { id, width, height }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Da de baja una salida desconectada.
|
||||||
|
pub fn remove_output(&mut self, id: OutputId) -> BodyEvent {
|
||||||
|
self.outputs.retain(|(o, _)| *o != id);
|
||||||
|
BodyEvent::OutputRemoved { id }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Registra una superficie recién creada por un cliente.
|
||||||
|
pub fn open_surface(
|
||||||
|
&mut self,
|
||||||
|
id: WindowId,
|
||||||
|
app_id: impl Into<String>,
|
||||||
|
title: impl Into<String>,
|
||||||
|
) -> BodyEvent {
|
||||||
|
let app_id = app_id.into();
|
||||||
|
let title = title.into();
|
||||||
|
self.surfaces
|
||||||
|
.insert(id, Surface::new(app_id.clone(), title.clone()));
|
||||||
|
BodyEvent::WindowOpened { id, app_id, title }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Da de baja una superficie destruida. `None` si no se conocía.
|
||||||
|
pub fn close_surface(&mut self, id: WindowId) -> Option<BodyEvent> {
|
||||||
|
self.surfaces.remove(&id)?;
|
||||||
|
if self.focused == Some(id) {
|
||||||
|
self.focused = None;
|
||||||
|
}
|
||||||
|
Some(BodyEvent::WindowClosed { id })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Actualiza el título de una superficie. `None` si no se conocía.
|
||||||
|
pub fn retitle_surface(&mut self, id: WindowId, title: impl Into<String>) -> Option<BodyEvent> {
|
||||||
|
let title = title.into();
|
||||||
|
let s = self.surfaces.get_mut(&id)?;
|
||||||
|
s.title = title.clone();
|
||||||
|
Some(BodyEvent::WindowRetitled { id, title })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construye un evento de puntero entrando en una superficie.
|
||||||
|
pub fn pointer_enter(&self, id: WindowId) -> BodyEvent {
|
||||||
|
BodyEvent::PointerEntered { id }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construye un evento de atajo pulsado.
|
||||||
|
pub fn keybind(&self, combo: impl Into<String>) -> BodyEvent {
|
||||||
|
BodyEvent::Keybind(combo.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Accesores de sólo lectura -----------------------------------
|
||||||
|
|
||||||
|
/// Las salidas conectadas.
|
||||||
|
pub fn outputs(&self) -> &[(OutputId, Rect)] {
|
||||||
|
&self.outputs
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Una superficie conocida.
|
||||||
|
pub fn surface(&self, id: WindowId) -> Option<&Surface> {
|
||||||
|
self.surfaces.get(&id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Número de superficies registradas.
|
||||||
|
pub fn surface_count(&self) -> usize {
|
||||||
|
self.surfaces.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Las superficies visibles, en orden de id.
|
||||||
|
pub fn visible(&self) -> impl Iterator<Item = (WindowId, &Surface)> {
|
||||||
|
self.surfaces.iter().filter(|(_, s)| s.visible).map(|(id, s)| (*id, s))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// La superficie enfocada.
|
||||||
|
pub fn focused(&self) -> Option<WindowId> {
|
||||||
|
self.focused
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use mirada_protocol::WindowPlacement;
|
||||||
|
|
||||||
|
fn placement(id: WindowId, visible: bool, focused: bool) -> WindowPlacement {
|
||||||
|
WindowPlacement {
|
||||||
|
id,
|
||||||
|
rect: Rect::new(0, 0, 800, 600),
|
||||||
|
visible,
|
||||||
|
focused,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cuerpo con dos superficies abiertas.
|
||||||
|
fn body_with_two() -> BodyState {
|
||||||
|
let mut b = BodyState::new();
|
||||||
|
b.add_output(0, 1920, 1080);
|
||||||
|
b.open_surface(1, "app1", "uno");
|
||||||
|
b.open_surface(2, "app2", "dos");
|
||||||
|
b
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn opening_a_surface_yields_a_window_opened_event() {
|
||||||
|
let mut b = BodyState::new();
|
||||||
|
let ev = b.open_surface(7, "org.brahman.shuma", "shell");
|
||||||
|
assert_eq!(
|
||||||
|
ev,
|
||||||
|
BodyEvent::WindowOpened {
|
||||||
|
id: 7,
|
||||||
|
app_id: "org.brahman.shuma".into(),
|
||||||
|
title: "shell".into()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(b.surface_count(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn placing_surfaces_configures_and_focuses_them() {
|
||||||
|
let mut b = body_with_two();
|
||||||
|
let ops = b.apply(BrainCommand::Place(vec![
|
||||||
|
placement(1, true, false),
|
||||||
|
placement(2, true, true),
|
||||||
|
]));
|
||||||
|
// Dos Configure + un Focus.
|
||||||
|
let configures = ops.iter().filter(|o| matches!(o, BodyOp::Configure { .. })).count();
|
||||||
|
assert_eq!(configures, 2);
|
||||||
|
assert!(ops.contains(&BodyOp::Focus(2)));
|
||||||
|
assert_eq!(b.focused(), Some(2));
|
||||||
|
assert!(b.surface(2).unwrap().focused);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_identical_place_produces_no_ops() {
|
||||||
|
let mut b = body_with_two();
|
||||||
|
let cmd = BrainCommand::Place(vec![placement(1, true, true), placement(2, true, false)]);
|
||||||
|
assert!(!b.apply(cmd.clone()).is_empty());
|
||||||
|
// Repetir el mismo Place no cambia nada.
|
||||||
|
assert!(b.apply(cmd).is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dropping_a_surface_from_the_list_hides_it() {
|
||||||
|
let mut b = body_with_two();
|
||||||
|
b.apply(BrainCommand::Place(vec![placement(1, true, true), placement(2, true, false)]));
|
||||||
|
// El Cerebro deja de colocar la 2 (p. ej. cambió de escritorio).
|
||||||
|
let ops = b.apply(BrainCommand::Place(vec![placement(1, true, true)]));
|
||||||
|
assert!(ops.contains(&BodyOp::Configure {
|
||||||
|
id: 2,
|
||||||
|
rect: Rect::new(0, 0, 800, 600),
|
||||||
|
visible: false,
|
||||||
|
}));
|
||||||
|
assert!(!b.surface(2).unwrap().visible);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn placement_for_an_unknown_surface_is_ignored() {
|
||||||
|
let mut b = body_with_two();
|
||||||
|
// La 99 no existe — no debe producir Configure.
|
||||||
|
let ops = b.apply(BrainCommand::Place(vec![placement(99, true, true)]));
|
||||||
|
assert!(!ops.iter().any(|o| matches!(o, BodyOp::Configure { id: 99, .. })));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn close_and_kill_map_to_client_ops() {
|
||||||
|
let mut b = body_with_two();
|
||||||
|
assert_eq!(b.apply(BrainCommand::Close(1)), vec![BodyOp::CloseClient(1)]);
|
||||||
|
assert_eq!(b.apply(BrainCommand::Kill(2)), vec![BodyOp::KillClient(2)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grab_keys_cursor_and_shutdown_pass_through() {
|
||||||
|
let mut b = BodyState::new();
|
||||||
|
assert_eq!(
|
||||||
|
b.apply(BrainCommand::GrabKeys(vec!["Super+q".into()])),
|
||||||
|
vec![BodyOp::SetGrabs(vec!["Super+q".into()])]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
b.apply(BrainCommand::SetCursor("crosshair".into())),
|
||||||
|
vec![BodyOp::SetCursor("crosshair".into())]
|
||||||
|
);
|
||||||
|
assert_eq!(b.apply(BrainCommand::Shutdown), vec![BodyOp::Shutdown]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn closing_a_surface_clears_its_focus() {
|
||||||
|
let mut b = body_with_two();
|
||||||
|
b.apply(BrainCommand::Place(vec![placement(1, true, true)]));
|
||||||
|
assert_eq!(b.focused(), Some(1));
|
||||||
|
let ev = b.close_surface(1);
|
||||||
|
assert_eq!(ev, Some(BodyEvent::WindowClosed { id: 1 }));
|
||||||
|
assert_eq!(b.focused(), None);
|
||||||
|
assert_eq!(b.surface_count(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn closing_an_unknown_surface_yields_nothing() {
|
||||||
|
let mut b = body_with_two();
|
||||||
|
assert!(b.close_surface(404).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn retitling_updates_the_surface() {
|
||||||
|
let mut b = body_with_two();
|
||||||
|
let ev = b.retitle_surface(1, "uno bis");
|
||||||
|
assert_eq!(ev, Some(BodyEvent::WindowRetitled { id: 1, title: "uno bis".into() }));
|
||||||
|
assert_eq!(b.surface(1).unwrap().title, "uno bis");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn outputs_are_tracked() {
|
||||||
|
let mut b = BodyState::new();
|
||||||
|
b.add_output(0, 2560, 1440);
|
||||||
|
b.add_output(1, 1920, 1080);
|
||||||
|
assert_eq!(b.outputs().len(), 2);
|
||||||
|
b.remove_output(0);
|
||||||
|
assert_eq!(b.outputs().len(), 1);
|
||||||
|
assert_eq!(b.outputs()[0].0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn moving_focus_emits_a_single_focus_op() {
|
||||||
|
let mut b = body_with_two();
|
||||||
|
b.apply(BrainCommand::Place(vec![placement(1, true, true), placement(2, true, false)]));
|
||||||
|
// Cambia el foco a la 2; geometría igual → sólo un Focus.
|
||||||
|
let ops = b.apply(BrainCommand::Place(vec![
|
||||||
|
placement(1, true, false),
|
||||||
|
placement(2, true, true),
|
||||||
|
]));
|
||||||
|
assert_eq!(ops, vec![BodyOp::Focus(2)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn visible_iterates_only_shown_surfaces() {
|
||||||
|
let mut b = body_with_two();
|
||||||
|
b.apply(BrainCommand::Place(vec![placement(1, true, true), placement(2, false, false)]));
|
||||||
|
let shown: Vec<_> = b.visible().map(|(id, _)| id).collect();
|
||||||
|
assert_eq!(shown, vec![1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user