⎇ 8204852e3aa72d653aa41e941fc3a310bd11493f
crates/apps/mirada-compositor/src/main.rs
Historia
Quién tocó qué
crudo
feat(mirada): keymap configurable en RON, recargable en caliente
1//! `mirada-compositor` — el Cuerpo del compositor carmen.
2//!
3//! Un compositor Wayland teselante real, sobre `smithay`, con backend
4//! `winit`: corre **anidado** como una ventana dentro de tu sesión
5//! gráfica actual (X11 o Wayland). Habla el protocolo Wayland con los
6//! clientes, compone sus superficies y aplica la geometría que decide el
7//! Cerebro.
8//!
9//! Dos modos:
10//!
11//! - **Autónomo** (por defecto): lleva un [`Desktop`] embebido — es un
12//! compositor teselante completo en un solo proceso. Lánzalo y abre
13//! clientes; el teclado (`Super+…`) maneja el escritorio.
14//! - **Enlazado** (`MIRADA_SOCKET=/ruta`): el Cuerpo escucha ahí y la
15//! app `mirada` (el Cerebro GPUI) se conecta; la geometría viaja por
16//! [`mirada_link`].
17//!
18//! Cómo probarlo en un Linux real: ver `crates/apps/mirada-compositor/README.md`.
19
20use std::sync::Arc;
21use std::time::Instant;
22
23use smithay::backend::input::{InputEvent, KeyState, KeyboardKeyEvent};
24use smithay::backend::renderer::element::surface::{
25 render_elements_from_surface_tree, WaylandSurfaceRenderElement,
26};
27use smithay::backend::renderer::element::Kind;
28use smithay::backend::renderer::gles::GlesRenderer;
29use smithay::backend::renderer::utils::{draw_render_elements, on_commit_buffer_handler};
30use smithay::backend::renderer::{Color32F, Frame, Renderer};
31use smithay::backend::winit::{self, WinitEvent};
32use smithay::input::keyboard::{xkb, FilterResult, KeyboardHandle, Keysym, ModifiersState};
33use smithay::input::{Seat, SeatHandler, SeatState};
34use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
35use smithay::reexports::wayland_server::backend::{ClientData, ClientId, DisconnectReason};
36use smithay::reexports::wayland_server::protocol::wl_buffer;
37use smithay::reexports::wayland_server::protocol::wl_seat;
38use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
39use smithay::reexports::wayland_server::{Client, Display, ListeningSocket};
40use smithay::reexports::winit::platform::pump_events::PumpStatus;
41use smithay::utils::{Rectangle, SERIAL_COUNTER};
42use smithay::utils::{Serial, Transform};
43use smithay::wayland::buffer::BufferHandler;
44use smithay::wayland::compositor::{
45 with_states, with_surface_tree_downward, CompositorClientState, CompositorHandler,
46 CompositorState, SurfaceAttributes, TraversalAction,
47};
48use smithay::wayland::selection::data_device::{
49 ClientDndGrabHandler, DataDeviceHandler, DataDeviceState, ServerDndGrabHandler,
50};
51use smithay::wayland::selection::SelectionHandler;
52use smithay::wayland::shell::xdg::{
53 PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState,
54 XdgToplevelSurfaceData,
55};
56use smithay::wayland::shm::{ShmHandler, ShmState};
57use smithay::{
58 delegate_compositor, delegate_data_device, delegate_seat, delegate_shm, delegate_xdg_shell,
59};
60
61use mirada_body::{BodyOp, BodyState};
62use mirada_brain::{BodyEvent, BrainCommand, Desktop, Keymap};
63use mirada_link::BodyLink;
64
65// ---------------------------------------------------------------------
66// Estado
67// ---------------------------------------------------------------------
68
69/// De dónde salen las decisiones de geometría.
70enum Brain {
71 /// El compositor lleva su propio `Desktop` — proceso único.
72 Embedded(Desktop),
73 /// Un Cerebro externo (la app `mirada`) por socket.
74 Linked(BodyLink),
75}
76
77/// Una ventana de cliente que el compositor gestiona.
78struct ManagedWindow {
79 id: u64,
80 toplevel: ToplevelSurface,
81 surface: WlSurface,
82 /// Esquina superior-izquierda en píxeles, según el Cerebro.
83 loc: (i32, i32),
84 visible: bool,
85}
86
87/// El estado global del compositor.
88struct App {
89 compositor_state: CompositorState,
90 xdg_shell_state: XdgShellState,
91 shm_state: ShmState,
92 seat_state: SeatState<Self>,
93 data_device_state: DataDeviceState,
94 seat: Seat<Self>,
95 keyboard: Option<KeyboardHandle<Self>>,
96
97 /// Ventanas gestionadas, en orden de aparición.
98 windows: Vec<ManagedWindow>,
99 /// La contabilidad del Cuerpo (mirada-body).
100 body: BodyState,
101 /// El Cerebro: embebido o enlazado.
102 brain: Brain,
103 /// Atajos globales a interceptar (los registra el Cerebro).
104 grabs: Vec<String>,
105 /// Atajo capturado en el último evento de teclado, pendiente de enviar.
106 pending_keybind: Option<String>,
107 next_id: u64,
108 running: bool,
109}
110
111impl App {
112 /// Inyecta un evento del Cuerpo en el Cerebro y aplica su respuesta.
113 fn brain_feed(&mut self, event: BodyEvent) {
114 let cmds = match &mut self.brain {
115 Brain::Embedded(desktop) => desktop.on_event(event),
116 Brain::Linked(link) => {
117 let _ = link.send(&event);
118 Vec::new()
119 }
120 };
121 self.apply_commands(cmds);
122 }
123
124 /// Drena los comandos de un Cerebro enlazado (no hace nada si es embebido).
125 fn brain_poll(&mut self) {
126 let cmds = match &self.brain {
127 Brain::Linked(link) => link.drain(),
128 Brain::Embedded(_) => Vec::new(),
129 };
130 if !cmds.is_empty() {
131 self.apply_commands(cmds);
132 }
133 }
134
135 /// Traduce los comandos del Cerebro a operaciones y las ejecuta.
136 fn apply_commands(&mut self, cmds: Vec<BrainCommand>) {
137 for cmd in cmds {
138 let ops = self.body.apply(cmd);
139 for op in ops {
140 self.exec_op(op);
141 }
142 }
143 }
144
145 /// Ejecuta una operación concreta sobre las superficies reales.
146 fn exec_op(&mut self, op: BodyOp) {
147 match op {
148 BodyOp::Configure { id, rect, visible } => {
149 if let Some(w) = self.windows.iter_mut().find(|w| w.id == id) {
150 w.loc = (rect.x, rect.y);
151 w.visible = visible;
152 w.toplevel.with_pending_state(|s| {
153 s.size = Some((rect.w.max(1), rect.h.max(1)).into());
154 });
155 w.toplevel.send_pending_configure();
156 }
157 }
158 BodyOp::Focus(id) => {
159 let mut target = None;
160 for w in &self.windows {
161 let active = w.id == id;
162 if active {
163 target = Some(w.surface.clone());
164 }
165 w.toplevel.with_pending_state(|s| {
166 if active {
167 s.states.set(xdg_toplevel::State::Activated);
168 } else {
169 s.states.unset(xdg_toplevel::State::Activated);
170 }
171 });
172 w.toplevel.send_pending_configure();
173 }
174 if let Some(kb) = self.keyboard.clone() {
175 kb.set_focus(self, target, SERIAL_COUNTER.next_serial());
176 }
177 }
178 BodyOp::Unfocus => {
179 if let Some(kb) = self.keyboard.clone() {
180 kb.set_focus(self, Option::<WlSurface>::None, SERIAL_COUNTER.next_serial());
181 }
182 }
183 BodyOp::CloseClient(id) | BodyOp::KillClient(id) => {
184 if let Some(w) = self.windows.iter().find(|w| w.id == id) {
185 w.toplevel.send_close();
186 }
187 }
188 BodyOp::SetGrabs(keys) => self.grabs = keys,
189 BodyOp::SetCursor(_) => {}
190 BodyOp::Shutdown => self.running = false,
191 }
192 }
193
194 /// Registra un toplevel recién creado y avisa al Cerebro.
195 fn register_toplevel(&mut self, toplevel: ToplevelSurface) {
196 let surface = toplevel.wl_surface().clone();
197 let id = self.next_id;
198 self.next_id += 1;
199
200 let (app_id, title) = with_states(&surface, |states| {
201 states
202 .data_map
203 .get::<XdgToplevelSurfaceData>()
204 .and_then(|d| d.lock().ok())
205 .map(|d| {
206 (
207 d.app_id.clone().unwrap_or_default(),
208 d.title.clone().unwrap_or_default(),
209 )
210 })
211 .unwrap_or_default()
212 });
213 let app_id = if app_id.is_empty() { "cliente".into() } else { app_id };
214 let title = if title.is_empty() { format!("ventana {id}") } else { title };
215
216 self.windows.push(ManagedWindow {
217 id,
218 toplevel,
219 surface,
220 loc: (0, 0),
221 visible: false,
222 });
223 let ev = self.body.open_surface(id, app_id, title);
224 self.brain_feed(ev);
225 }
226}
227
228// ---------------------------------------------------------------------
229// Handlers de protocolo
230// ---------------------------------------------------------------------
231
232impl CompositorHandler for App {
233 fn compositor_state(&mut self) -> &mut CompositorState {
234 &mut self.compositor_state
235 }
236
237 fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState {
238 &client.get_data::<ClientState>().unwrap().compositor_state
239 }
240
241 fn commit(&mut self, surface: &WlSurface) {
242 on_commit_buffer_handler::<Self>(surface);
243 }
244}
245
246impl BufferHandler for App {
247 fn buffer_destroyed(&mut self, _buffer: &wl_buffer::WlBuffer) {}
248}
249
250impl ShmHandler for App {
251 fn shm_state(&self) -> &ShmState {
252 &self.shm_state
253 }
254}
255
256impl XdgShellHandler for App {
257 fn xdg_shell_state(&mut self) -> &mut XdgShellState {
258 &mut self.xdg_shell_state
259 }
260
261 fn new_toplevel(&mut self, surface: ToplevelSurface) {
262 surface.with_pending_state(|s| {
263 s.states.set(xdg_toplevel::State::Activated);
264 });
265 surface.send_configure();
266 self.register_toplevel(surface);
267 }
268
269 fn toplevel_destroyed(&mut self, surface: ToplevelSurface) {
270 let pos = self
271 .windows
272 .iter()
273 .position(|w| w.surface == *surface.wl_surface());
274 if let Some(pos) = pos {
275 let id = self.windows.remove(pos).id;
276 if let Some(ev) = self.body.close_surface(id) {
277 self.brain_feed(ev);
278 }
279 }
280 }
281
282 fn title_changed(&mut self, surface: ToplevelSurface) {
283 let id = self
284 .windows
285 .iter()
286 .find(|w| w.surface == *surface.wl_surface())
287 .map(|w| w.id);
288 let Some(id) = id else { return };
289 let title = with_states(surface.wl_surface(), |states| {
290 states
291 .data_map
292 .get::<XdgToplevelSurfaceData>()
293 .and_then(|d| d.lock().ok())
294 .and_then(|d| d.title.clone())
295 .unwrap_or_default()
296 });
297 if let Some(ev) = self.body.retitle_surface(id, title) {
298 self.brain_feed(ev);
299 }
300 }
301
302 fn new_popup(&mut self, surface: PopupSurface, _positioner: PositionerState) {
303 let _ = surface.send_configure();
304 }
305
306 fn grab(&mut self, _surface: PopupSurface, _seat: wl_seat::WlSeat, _serial: Serial) {}
307
308 fn reposition_request(
309 &mut self,
310 _surface: PopupSurface,
311 _positioner: PositionerState,
312 _token: u32,
313 ) {
314 }
315}
316
317impl SelectionHandler for App {
318 type SelectionUserData = ();
319}
320
321impl DataDeviceHandler for App {
322 fn data_device_state(&self) -> &DataDeviceState {
323 &self.data_device_state
324 }
325}
326impl ClientDndGrabHandler for App {}
327impl ServerDndGrabHandler for App {
328 fn send(&mut self, _mime_type: String, _fd: std::os::unix::io::OwnedFd, _seat: Seat<Self>) {}
329}
330
331impl SeatHandler for App {
332 type KeyboardFocus = WlSurface;
333 type PointerFocus = WlSurface;
334 type TouchFocus = WlSurface;
335
336 fn seat_state(&mut self) -> &mut SeatState<Self> {
337 &mut self.seat_state
338 }
339
340 fn focus_changed(&mut self, _seat: &Seat<Self>, _focused: Option<&WlSurface>) {}
341 fn cursor_image(
342 &mut self,
343 _seat: &Seat<Self>,
344 _image: smithay::input::pointer::CursorImageStatus,
345 ) {
346 }
347}
348
349delegate_compositor!(App);
350delegate_xdg_shell!(App);
351delegate_shm!(App);
352delegate_seat!(App);
353delegate_data_device!(App);
354
355// ---------------------------------------------------------------------
356// Datos por cliente
357// ---------------------------------------------------------------------
358
359#[derive(Default)]
360struct ClientState {
361 compositor_state: CompositorClientState,
362}
363impl ClientData for ClientState {
364 fn initialized(&self, _id: ClientId) {}
365 fn disconnected(&self, _id: ClientId, _reason: DisconnectReason) {}
366}
367
368// ---------------------------------------------------------------------
369// Utilidades
370// ---------------------------------------------------------------------
371
372/// Construye la cadena de un atajo (`"Super+Shift+j"`) desde el estado de
373/// modificadores y el keysym, con el mismo formato que el mapa de teclas
374/// de [`mirada_brain`]. `None` si no es una tecla mapeable.
375fn combo_string(mods: &ModifiersState, sym: Keysym) -> Option<String> {
376 let utf = xkb::keysym_to_utf8(sym);
377 let key = utf.trim_end_matches('\0');
378 let name = if key == " " {
379 "space".to_string()
380 } else {
381 let mut chars = key.chars();
382 let c = chars.next()?;
383 if chars.next().is_some() || !c.is_ascii_graphic() {
384 return None;
385 }
386 c.to_ascii_lowercase().to_string()
387 };
388 let mut combo = String::new();
389 if mods.logo {
390 combo.push_str("Super+");
391 }
392 if mods.ctrl {
393 combo.push_str("Ctrl+");
394 }
395 if mods.shift {
396 combo.push_str("Shift+");
397 }
398 if mods.alt {
399 combo.push_str("Alt+");
400 }
401 combo.push_str(&name);
402 Some(combo)
403}
404
405/// Despacha los callbacks de frame de un árbol de superficies: avisa a
406/// cada cliente de que puede dibujar el siguiente cuadro.
407fn send_frames_surface_tree(surface: &WlSurface, time: u32) {
408 with_surface_tree_downward(
409 surface,
410 (),
411 |_, _, &()| TraversalAction::DoChildren(()),
412 |_surf, states, &()| {
413 for callback in states
414 .cached_state
415 .get::<SurfaceAttributes>()
416 .current()
417 .frame_callbacks
418 .drain(..)
419 {
420 callback.done(time);
421 }
422 },
423 |_, _, &()| true,
424 );
425}
426
427// ---------------------------------------------------------------------
428// Bucle principal
429// ---------------------------------------------------------------------
430
431fn run() -> Result<(), Box<dyn std::error::Error>> {
432 let mut display: Display<App> = Display::new()?;
433 let dh = display.handle();
434
435 let mut seat_state = SeatState::new();
436 let seat = seat_state.new_wl_seat(&dh, "mirada");
437
438 // El keymap del usuario (`~/.config/mirada/keymap.ron`). Sólo lo usa
439 // el Cerebro embebido; con un Cerebro enlazado, el keymap es asunto suyo.
440 let keymap_path = Keymap::default_path();
441
442 // Elige el Cerebro: enlazado si `MIRADA_SOCKET` está puesto.
443 let brain = match std::env::var("MIRADA_SOCKET") {
444 Ok(path) => {
445 println!("mirada-compositor · esperando al Cerebro en {path} …");
446 let link = BodyLink::listen(&path)?;
447 println!("mirada-compositor · Cerebro conectado.");
448 Brain::Linked(link)
449 }
450 Err(_) => {
451 println!("mirada-compositor · modo autónomo (Cerebro embebido).");
452 let keymap = match &keymap_path {
453 Some(p) => Keymap::load_or_init(p),
454 None => Keymap::default(),
455 };
456 Brain::Embedded(Desktop::with_keymap(keymap))
457 }
458 };
459
460 let mut state = App {
461 compositor_state: CompositorState::new::<App>(&dh),
462 xdg_shell_state: XdgShellState::new::<App>(&dh),
463 shm_state: ShmState::new::<App>(&dh, Vec::new()),
464 seat_state,
465 data_device_state: DataDeviceState::new::<App>(&dh),
466 seat,
467 keyboard: None,
468 windows: Vec::new(),
469 body: BodyState::new(),
470 brain,
471 grabs: Vec::new(),
472 pending_keybind: None,
473 next_id: 1,
474 running: true,
475 };
476
477 let keyboard = state.seat.add_keyboard(Default::default(), 200, 25)?;
478 state.keyboard = Some(keyboard.clone());
479
480 // En modo embebido, el propio Desktop dicta los atajos a interceptar.
481 if let Brain::Embedded(desktop) = &state.brain {
482 let grab = desktop.grab_keys();
483 state.apply_commands(vec![grab]);
484 }
485
486 // Vigilancia del keymap para recargarlo en caliente — sólo tiene
487 // sentido con el Cerebro embebido.
488 let keymap_watch = match (&state.brain, &keymap_path) {
489 (Brain::Embedded(_), Some(p)) => Keymap::watch(p).ok(),
490 _ => None,
491 };
492 if keymap_watch.is_some() {
493 println!("mirada-compositor · vigilando el keymap (recarga en caliente).");
494 }
495
496 // El backend gráfico va primero. winit abre la ventana del compositor
497 // dentro de tu sesión gráfica anfitriona, y para encontrarla lee
498 // `WAYLAND_DISPLAY` / `DISPLAY` del entorno. Si publicáramos antes
499 // nuestro propio socket en `WAYLAND_DISPLAY`, winit intentaría
500 // anidarse en nosotros mismos —un socket que aún no atiende a nadie—
501 // y se quedaría colgado para siempre.
502 let (mut backend, mut winit) = match winit::init::<GlesRenderer>() {
503 Ok(pair) => pair,
504 Err(e) => {
505 eprintln!("mirada-compositor · no pude abrir la ventana: {e}");
506 eprintln!(
507 " El backend `winit` necesita una sesión gráfica anfitriona\n \
508 (X11 o Wayland) donde dibujar la ventana del compositor.\n \
509 Aquí no hay ninguna: DISPLAY='{}', WAYLAND_DISPLAY='{}',\n \
510 XDG_SESSION_TYPE='{}'.\n \
511 Lánzalo desde un escritorio gráfico, o desde un servidor X\n \
512 virtual (Xvfb) al que te conectes por VNC.",
513 std::env::var("DISPLAY").unwrap_or_default(),
514 std::env::var("WAYLAND_DISPLAY").unwrap_or_default(),
515 std::env::var("XDG_SESSION_TYPE").unwrap_or_else(|_| "tty".into()),
516 );
517 return Err(e.into());
518 }
519 };
520
521 // Ahora sí, nuestro propio socket Wayland — y `WAYLAND_DISPLAY` se
522 // publica *después* de winit, sólo para los clientes que lancemos
523 // como procesos hijos.
524 let listener = ListeningSocket::bind_auto("wayland", 1..32)?;
525 let socket_name = listener
526 .socket_name()
527 .and_then(|s| s.to_str())
528 .unwrap_or("wayland-?")
529 .to_string();
530 std::env::set_var("WAYLAND_DISPLAY", &socket_name);
531 println!("mirada-compositor · escuchando en WAYLAND_DISPLAY={socket_name}");
532 println!(" lanza un cliente: WAYLAND_DISPLAY={socket_name} foot");
533
534 let start = Instant::now();
535 let mut clients = Vec::new();
536
537 // Salida inicial = el tamaño de la ventana winit.
538 {
539 let size = backend.window_size();
540 let ev = state.body.add_output(0, size.w, size.h);
541 state.brain_feed(ev);
542 }
543
544 while state.running {
545 // 1 · Eventos del backend (teclado, redimensión, cierre).
546 let status = winit.dispatch_new_events(|event| match event {
547 WinitEvent::CloseRequested => state.running = false,
548 WinitEvent::Resized { size, .. } => {
549 let ev = state.body.remove_output(0);
550 state.brain_feed(ev);
551 let ev = state.body.add_output(0, size.w, size.h);
552 state.brain_feed(ev);
553 }
554 WinitEvent::Input(InputEvent::Keyboard { event }) => {
555 let code = event.key_code();
556 let key_state = event.state();
557 let pressed = key_state == KeyState::Pressed;
558 let time = start.elapsed().as_millis() as u32;
559 keyboard.clone().input::<(), _>(
560 &mut state,
561 code,
562 key_state,
563 SERIAL_COUNTER.next_serial(),
564 time,
565 |st, mods, handle| {
566 if !pressed {
567 return FilterResult::Forward;
568 }
569 if let Some(combo) = combo_string(mods, handle.modified_sym()) {
570 if st.grabs.contains(&combo) {
571 st.pending_keybind = Some(combo);
572 return FilterResult::Intercept(());
573 }
574 }
575 FilterResult::Forward
576 },
577 );
578 if let Some(combo) = state.pending_keybind.take() {
579 let ev = state.body.keybind(combo);
580 state.brain_feed(ev);
581 }
582 }
583 _ => {}
584 });
585 if let PumpStatus::Exit(_) = status {
586 break;
587 }
588
589 // 2 · Comandos de un Cerebro enlazado.
590 state.brain_poll();
591
592 // 2 bis · Recarga del keymap si el archivo cambió en disco.
593 if keymap_watch.as_ref().is_some_and(|w| w.changed()) {
594 if let Some(path) = &keymap_path {
595 match Keymap::load(path) {
596 Ok(km) => {
597 let cmd = if let Brain::Embedded(d) = &mut state.brain {
598 Some(d.set_keymap(km))
599 } else {
600 None
601 };
602 if let Some(cmd) = cmd {
603 state.apply_commands(vec![cmd]);
604 }
605 println!("mirada-compositor · keymap recargado.");
606 }
607 Err(e) => eprintln!(
608 "mirada-compositor · keymap inválido, conservo el anterior: {e}"
609 ),
610 }
611 }
612 }
613
614 // 3 · Composición de las superficies en sus rectángulos.
615 let size = backend.window_size();
616 let damage: Rectangle<i32, smithay::utils::Physical> = Rectangle::from_size(size);
617 {
618 let (renderer, mut framebuffer) = backend.bind().unwrap();
619 let elements: Vec<WaylandSurfaceRenderElement<GlesRenderer>> = state
620 .windows
621 .iter()
622 .filter(|w| w.visible)
623 .flat_map(|w| {
624 render_elements_from_surface_tree(
625 renderer,
626 &w.surface,
627 w.loc,
628 1.0,
629 1.0,
630 Kind::Unspecified,
631 )
632 })
633 .collect();
634 let mut frame = renderer
635 .render(&mut framebuffer, size, Transform::Flipped180)
636 .unwrap();
637 frame
638 .clear(Color32F::new(0.05, 0.05, 0.08, 1.0), &[damage])
639 .unwrap();
640 draw_render_elements(&mut frame, 1.0, &elements, &[damage]).unwrap();
641 let _ = frame.finish().unwrap();
642 }
643
644 // 4 · Callbacks de frame + clientes nuevos + flush.
645 let time = start.elapsed().as_millis() as u32;
646 for w in &state.windows {
647 send_frames_surface_tree(&w.surface, time);
648 }
649 if let Some(stream) = listener.accept()? {
650 let client = display
651 .handle()
652 .insert_client(stream, Arc::new(ClientState::default()))
653 .unwrap();
654 clients.push(client);
655 }
656 display.dispatch_clients(&mut state)?;
657 display.flush_clients()?;
658
659 backend.submit(Some(&[damage])).unwrap();
660 }
661
662 println!("mirada-compositor · adiós.");
663 Ok(())
664}
665
666fn main() {
667 if let Err(e) = run() {
668 eprintln!("mirada-compositor · error: {e}");
669 std::process::exit(1);
670 }
671}