feat(carmen): modo greeter — mirada-compositor como DM
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::allocator::dmabuf::Dmabuf;
24use smithay::backend::input::{InputEvent, KeyState, KeyboardKeyEvent};
25use smithay::backend::renderer::element::surface::{
26 render_elements_from_surface_tree, WaylandSurfaceRenderElement,
27};
28use smithay::backend::renderer::element::solid::SolidColorBuffer;
29use smithay::backend::renderer::element::Kind;
30use smithay::backend::renderer::gles::GlesRenderer;
31use smithay::backend::renderer::utils::{
32 draw_render_elements, on_commit_buffer_handler, with_renderer_surface_state,
33};
34use smithay::backend::renderer::{Color32F, Frame, ImportDma, Renderer};
35use smithay::backend::winit::{self, WinitEvent};
36use smithay::input::keyboard::{xkb, FilterResult, KeyboardHandle, Keysym, ModifiersState};
37use smithay::input::pointer::{CursorImageStatus, CursorImageSurfaceData, PointerHandle};
38use smithay::input::{Seat, SeatHandler, SeatState};
39use smithay::reexports::wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode as DecorationMode;
40use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
41use smithay::reexports::wayland_server::backend::{ClientData, ClientId, DisconnectReason};
42use smithay::reexports::wayland_server::protocol::wl_buffer;
43use smithay::reexports::wayland_server::protocol::wl_output;
44use smithay::reexports::wayland_server::protocol::wl_seat;
45use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
46use smithay::reexports::wayland_server::{Client, Display, DisplayHandle, ListeningSocket};
47use smithay::reexports::winit::platform::pump_events::PumpStatus;
48use smithay::utils::{Rectangle, SERIAL_COUNTER};
49use smithay::utils::{Serial, Transform};
50use smithay::wayland::buffer::BufferHandler;
51use smithay::wayland::dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier};
52use smithay::wayland::compositor::{
53 with_states, with_surface_tree_downward, CompositorClientState, CompositorHandler,
54 CompositorState, SurfaceAttributes, TraversalAction,
55};
56use smithay::wayland::selection::data_device::{
57 ClientDndGrabHandler, DataDeviceHandler, DataDeviceState, ServerDndGrabHandler,
58};
59use smithay::wayland::selection::SelectionHandler;
60use smithay::wayland::shell::xdg::decoration::{XdgDecorationHandler, XdgDecorationState};
61use smithay::wayland::shell::xdg::{
62 PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState,
63 XdgToplevelSurfaceData,
64};
65use smithay::wayland::output::OutputHandler;
66use smithay::wayland::shm::{ShmHandler, ShmState};
67use smithay::{
68 delegate_compositor, delegate_data_device, delegate_dmabuf, delegate_output, delegate_seat,
69 delegate_shm, delegate_xdg_decoration, delegate_xdg_shell,
70};
71
72use brahman_auth::{SessionTicket, UserInfo};
73use mirada_body::{BodyOp, BodyState};
74use mirada_brain::{
75 BodyEvent, BrainCommand, CtlReply, CtlRequest, CtlServer, Desktop, Keymap, Rules,
76};
77use mirada_link::BodyLink;
78
79mod drm_backend;
80
81// ---------------------------------------------------------------------
82// Estado
83// ---------------------------------------------------------------------
84
85/// De dónde salen las decisiones de geometría.
86enum Brain {
87 /// El compositor lleva su propio `Desktop` — proceso único.
88 Embedded(Desktop),
89 /// Un Cerebro externo (la app `mirada`) por socket.
90 Linked(BodyLink),
91}
92
93/// La fase del ciclo de vida del Cuerpo. Es un eje **ortogonal** a
94/// [`Brain`]: `Brain` dice de dónde sale la geometría; `BodyMode` dice
95/// si el compositor está pidiendo credenciales o sirviendo una sesión.
96/// Un arranque normal nace ya en [`BodyMode::Session`]; un arranque de
97/// DM (`--greeter`) nace en [`BodyMode::Greeter`] y muta una sola vez,
98/// al recibir el tiquet de un login válido — la «mutación atómica».
99#[derive(Clone, Copy, PartialEq, Eq)]
100enum BodyMode {
101 /// Pantalla de login: el único cliente es el greeter, no se
102 /// registran atajos, se rechaza `Spawn` y no hay autoarranque.
103 Greeter,
104 /// Sesión de usuario: el compositor funciona con normalidad.
105 Session,
106}
107
108/// `app_id` que distingue a la ventana del shell del escritorio. carmen
109/// no la tesela: la acopla a una franja al pie de la pantalla.
110const SHELL_APP_ID: &str = "carmen.shell";
111
112/// Alto en píxeles de la franja del shell, al pie de la salida.
113const SHELL_DOCK_HEIGHT: i32 = 40;
114
115/// Una ventana de cliente que el compositor gestiona.
116struct ManagedWindow {
117 id: u64,
118 toplevel: ToplevelSurface,
119 surface: WlSurface,
120 /// Esquina superior-izquierda de la celda asignada, según el Cerebro.
121 loc: (i32, i32),
122 /// Tamaño de la celda asignada — para centrar la ventana si el
123 /// cliente presenta una superficie más pequeña.
124 size: (i32, i32),
125 visible: bool,
126 /// `true` si flota: se compone por encima de las teseladas.
127 floating: bool,
128 /// `true` si tiene el foco del teclado — pinta el marco resaltado.
129 focused: bool,
130 /// `true` si es la ventana del shell — acoplada al pie, sin teselar.
131 is_shell: bool,
132 /// Búferes de los 4 lados del marco (arriba, abajo, izq., der.) —
133 /// cada uno con su `Id` estable para el seguimiento de daño.
134 borders: [SolidColorBuffer; 4],
135}
136
137/// Un arrastre de ratón en curso: mueve o redimensiona una ventana.
138struct DragGrab {
139 /// La ventana que se arrastra.
140 id: u64,
141 /// Mover (`Super`+botón izquierdo) o redimensionar (`Super`+derecho).
142 mode: DragMode,
143 /// Posición del puntero al empezar el arrastre.
144 start_pointer: (f64, f64),
145 /// Rectángulo `(x, y, w, h)` de la ventana al empezar.
146 start_rect: (i32, i32, i32, i32),
147}
148
149/// Qué le hace un arrastre a la ventana.
150#[derive(Clone, Copy)]
151enum DragMode {
152 /// Reubicar la ventana — la esquina la sigue al puntero.
153 Move,
154 /// Redimensionarla — la esquina inferior-derecha sigue al puntero.
155 Resize,
156}
157
158/// El estado global del compositor.
159struct App {
160 compositor_state: CompositorState,
161 xdg_shell_state: XdgShellState,
162 shm_state: ShmState,
163 /// Estado de `zwp_linux_dmabuf` — deja que los clientes con GPU
164 /// (apps GPUI, navegadores acelerados) compartan búferes de vídeo.
165 dmabuf_state: DmabufState,
166 seat_state: SeatState<Self>,
167 data_device_state: DataDeviceState,
168 seat: Seat<Self>,
169 keyboard: Option<KeyboardHandle<Self>>,
170 pointer: Option<PointerHandle<Self>>,
171 /// Posición del puntero en coordenadas globales.
172 pointer_loc: (f64, f64),
173 /// Qué cursor pide el cliente enfocado — una superficie suya, un
174 /// cursor con nombre, u oculto. El backend lo pinta en consecuencia.
175 cursor_status: CursorImageStatus,
176 /// Arrastre de ventana en curso (mover o redimensionar con el ratón).
177 drag: Option<DragGrab>,
178 /// Tamaño real de la salida (con la franja del shell incluida) — lo
179 /// fija el backend; sirve para acoplar la ventana del shell.
180 output_size: (i32, i32),
181
182 /// Ventanas gestionadas, en orden de aparición.
183 windows: Vec<ManagedWindow>,
184 /// La contabilidad del Cuerpo (mirada-body).
185 body: BodyState,
186 /// El Cerebro: embebido o enlazado.
187 brain: Brain,
188 /// Fase del ciclo de vida — login o sesión (ver [`BodyMode`]).
189 mode: BodyMode,
190 /// Identidad a la que rebajar privilegios al lanzar procesos de
191 /// sesión. `None` salvo tras el traspaso del DM — entonces cada
192 /// `spawn` hace `setuid`/`setgid` a este usuario (si somos root).
193 session_user: Option<UserInfo>,
194 /// Atajos globales a interceptar (los registra el Cerebro).
195 grabs: Vec<String>,
196 /// Atajo capturado en el último evento de teclado, pendiente de enviar.
197 pending_keybind: Option<String>,
198 next_id: u64,
199 running: bool,
200}
201
202impl App {
203 /// Inyecta un evento del Cuerpo en el Cerebro y aplica su respuesta.
204 fn brain_feed(&mut self, event: BodyEvent) {
205 let cmds = match &mut self.brain {
206 Brain::Embedded(desktop) => desktop.on_event(event),
207 Brain::Linked(link) => {
208 let _ = link.send(&event);
209 Vec::new()
210 }
211 };
212 self.apply_commands(cmds);
213 }
214
215 /// Drena los comandos de un Cerebro enlazado (no hace nada si es embebido).
216 fn brain_poll(&mut self) {
217 let cmds = match &self.brain {
218 Brain::Linked(link) => link.drain(),
219 Brain::Embedded(_) => Vec::new(),
220 };
221 if !cmds.is_empty() {
222 self.apply_commands(cmds);
223 }
224 }
225
226 /// Atiende una petición del API de control (`mirada-ctl`).
227 fn serve_ctl(&mut self, req: CtlRequest) -> CtlReply {
228 match req {
229 CtlRequest::Do(action) => {
230 let cmds = match &mut self.brain {
231 Brain::Embedded(d) => Some(d.apply(action)),
232 Brain::Linked(_) => None,
233 };
234 match cmds {
235 Some(cmds) => {
236 self.apply_commands(cmds);
237 CtlReply::Ok
238 }
239 None => CtlReply::Error(
240 "el Cerebro es externo; usa mirada-ctl contra la app mirada".into(),
241 ),
242 }
243 }
244 CtlRequest::ListWindows => match &self.brain {
245 Brain::Embedded(d) => CtlReply::Windows(d.window_lines()),
246 Brain::Linked(_) => CtlReply::Error("el Cerebro es externo".into()),
247 },
248 }
249 }
250
251 /// Traduce los comandos del Cerebro a operaciones y las ejecuta.
252 fn apply_commands(&mut self, cmds: Vec<BrainCommand>) {
253 for cmd in cmds {
254 let ops = self.body.apply(cmd);
255 for op in ops {
256 self.exec_op(op);
257 }
258 }
259 }
260
261 /// Ejecuta una operación concreta sobre las superficies reales.
262 fn exec_op(&mut self, op: BodyOp) {
263 match op {
264 BodyOp::Configure { id, rect, visible, floating, fullscreen } => {
265 if let Some(w) = self.windows.iter_mut().find(|w| w.id == id) {
266 w.loc = (rect.x, rect.y);
267 w.size = (rect.w, rect.h);
268 w.visible = visible;
269 w.floating = floating;
270 w.toplevel.with_pending_state(|s| {
271 s.size = Some((rect.w.max(1), rect.h.max(1)).into());
272 if fullscreen {
273 s.states.set(xdg_toplevel::State::Fullscreen);
274 } else {
275 s.states.unset(xdg_toplevel::State::Fullscreen);
276 }
277 });
278 w.toplevel.send_pending_configure();
279 }
280 }
281 BodyOp::Focus(id) => {
282 let mut target = None;
283 for w in &mut self.windows {
284 let active = w.id == id;
285 w.focused = active;
286 if active {
287 target = Some(w.surface.clone());
288 }
289 w.toplevel.with_pending_state(|s| {
290 if active {
291 s.states.set(xdg_toplevel::State::Activated);
292 } else {
293 s.states.unset(xdg_toplevel::State::Activated);
294 }
295 });
296 w.toplevel.send_pending_configure();
297 }
298 if let Some(kb) = self.keyboard.clone() {
299 kb.set_focus(self, target, SERIAL_COUNTER.next_serial());
300 }
301 }
302 BodyOp::Unfocus => {
303 for w in &mut self.windows {
304 w.focused = false;
305 }
306 if let Some(kb) = self.keyboard.clone() {
307 kb.set_focus(self, Option::<WlSurface>::None, SERIAL_COUNTER.next_serial());
308 }
309 }
310 BodyOp::CloseClient(id) | BodyOp::KillClient(id) => {
311 if let Some(w) = self.windows.iter().find(|w| w.id == id) {
312 w.toplevel.send_close();
313 }
314 }
315 BodyOp::SetGrabs(keys) => self.grabs = keys,
316 BodyOp::SetCursor(_) => {}
317 BodyOp::Spawn(cmd) => {
318 // En modo greeter no se lanza nada: la pantalla de login
319 // no es un sitio desde donde abrir programas.
320 if self.mode == BodyMode::Greeter {
321 eprintln!("mirada-compositor · «{cmd}» rechazado — modo greeter.");
322 } else {
323 spawn_command(&cmd, self.session_user.as_ref());
324 }
325 }
326 BodyOp::Shutdown => self.running = false,
327 }
328 }
329
330 /// Registra un toplevel recién creado y avisa al Cerebro.
331 fn register_toplevel(&mut self, toplevel: ToplevelSurface) {
332 let surface = toplevel.wl_surface().clone();
333 let id = self.next_id;
334 self.next_id += 1;
335
336 let (app_id, title) = with_states(&surface, |states| {
337 states
338 .data_map
339 .get::<XdgToplevelSurfaceData>()
340 .and_then(|d| d.lock().ok())
341 .map(|d| {
342 (
343 d.app_id.clone().unwrap_or_default(),
344 d.title.clone().unwrap_or_default(),
345 )
346 })
347 .unwrap_or_default()
348 });
349 // La ventana del shell no se tesela: carmen la acopla al pie.
350 let is_shell = app_id == SHELL_APP_ID;
351
352 self.windows.push(ManagedWindow {
353 id,
354 toplevel,
355 surface,
356 loc: (0, 0),
357 size: (0, 0),
358 visible: false,
359 floating: false,
360 focused: false,
361 is_shell,
362 borders: std::array::from_fn(|_| SolidColorBuffer::default()),
363 });
364
365 if is_shell {
366 self.dock_shell();
367 } else {
368 let app_id = if app_id.is_empty() { "cliente".into() } else { app_id };
369 let title = if title.is_empty() { format!("ventana {id}") } else { title };
370 let ev = self.body.open_surface(id, app_id, title);
371 self.brain_feed(ev);
372 }
373 }
374
375 /// Acopla la ventana del shell: le reserva una franja al pie de la
376 /// salida —el Cerebro tesela el área que queda— y la dimensiona y
377 /// coloca ahí. Se llama al registrarla y al cambiar el tamaño de la
378 /// salida.
379 fn dock_shell(&mut self) {
380 let (ow, oh) = self.output_size;
381 if ow == 0 || oh == 0 {
382 return; // la salida todavía no está lista
383 }
384 // Reserva la franja: el Cerebro tesela en el alto que queda.
385 let ev = self.body.resize_output(0, ow, oh - SHELL_DOCK_HEIGHT);
386 self.brain_feed(ev);
387 // Dimensiona la ventana del shell y la fija en la franja.
388 if let Some(w) = self.windows.iter_mut().find(|w| w.is_shell) {
389 w.loc = (0, oh - SHELL_DOCK_HEIGHT);
390 w.size = (ow, SHELL_DOCK_HEIGHT);
391 w.visible = true;
392 w.toplevel.with_pending_state(|s| {
393 s.size = Some((ow.max(1), SHELL_DOCK_HEIGHT.max(1)).into());
394 });
395 w.toplevel.send_pending_configure();
396 }
397 }
398
399 /// El backend informa de un tamaño de salida nuevo (arranque o
400 /// redimensión). Si hay shell acoplado, recoloca su franja; si no,
401 /// le pasa el área entera al Cerebro.
402 fn output_changed(&mut self, width: i32, height: i32) {
403 self.output_size = (width, height);
404 if self.windows.iter().any(|w| w.is_shell) {
405 self.dock_shell();
406 } else {
407 let ev = self.body.resize_output(0, width, height);
408 self.brain_feed(ev);
409 }
410 }
411
412 /// El traspaso del DM — la «mutación atómica». Llega el tiquet de un
413 /// login válido y el compositor pasa de la pantalla de greeter a la
414 /// sesión del usuario **sin reiniciar el servidor Wayland**: el mismo
415 /// proceso, la misma GPU, las mismas ventanas. Idempotente — un
416 /// segundo tiquet (no debería llegar) se ignora.
417 fn complete_greeter_handoff(&mut self, ticket: SessionTicket) {
418 if self.mode == BodyMode::Session {
419 return; // ya en sesión — un tiquet de más, se ignora
420 }
421 println!(
422 "mirada-compositor · traspaso a la sesión de «{}» (uid {}).",
423 ticket.user.name, ticket.user.uid
424 );
425 if !nix::unistd::geteuid().is_root() {
426 eprintln!(
427 "mirada-compositor · aviso: no corro como root — la sesión \
428 heredará mis privilegios, sin setuid al usuario."
429 );
430 }
431 self.mode = BodyMode::Session;
432 self.session_user = Some(ticket.user.clone());
433
434 // Ya en sesión: registra los atajos del escritorio (en modo
435 // greeter se omitieron a propósito — ver `build_app`).
436 if let Brain::Embedded(desktop) = &self.brain {
437 let grab = desktop.grab_keys();
438 self.apply_commands(vec![grab]);
439 }
440
441 // Arranca la sesión: el comando del tiquet, o el autoarranque
442 // del usuario si el tiquet no trae ninguno.
443 let user = self.session_user.clone();
444 if ticket.session.trim().is_empty() {
445 spawn_autostart(user.as_ref());
446 } else {
447 spawn_command(&ticket.session, user.as_ref());
448 }
449 }
450}
451
452// ---------------------------------------------------------------------
453// Handlers de protocolo
454// ---------------------------------------------------------------------
455
456impl CompositorHandler for App {
457 fn compositor_state(&mut self) -> &mut CompositorState {
458 &mut self.compositor_state
459 }
460
461 fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState {
462 &client.get_data::<ClientState>().unwrap().compositor_state
463 }
464
465 fn commit(&mut self, surface: &WlSurface) {
466 on_commit_buffer_handler::<Self>(surface);
467 }
468}
469
470impl BufferHandler for App {
471 fn buffer_destroyed(&mut self, _buffer: &wl_buffer::WlBuffer) {}
472}
473
474impl DmabufHandler for App {
475 fn dmabuf_state(&mut self) -> &mut DmabufState {
476 &mut self.dmabuf_state
477 }
478
479 /// Un cliente importó un DMA-BUF. El `GlesRenderer` lo importará de
480 /// verdad al componer; aquí basta con aceptarlo — un búfer inválido
481 /// sólo dejará en blanco ese cuadro de esa ventana.
482 fn dmabuf_imported(
483 &mut self,
484 _global: &DmabufGlobal,
485 _dmabuf: Dmabuf,
486 notifier: ImportNotifier,
487 ) {
488 let _ = notifier.successful::<App>();
489 }
490}
491
492impl ShmHandler for App {
493 fn shm_state(&self) -> &ShmState {
494 &self.shm_state
495 }
496}
497
498impl XdgShellHandler for App {
499 fn xdg_shell_state(&mut self) -> &mut XdgShellState {
500 &mut self.xdg_shell_state
501 }
502
503 fn new_toplevel(&mut self, surface: ToplevelSurface) {
504 surface.with_pending_state(|s| {
505 s.states.set(xdg_toplevel::State::Activated);
506 });
507 surface.send_configure();
508 self.register_toplevel(surface);
509 }
510
511 fn toplevel_destroyed(&mut self, surface: ToplevelSurface) {
512 let pos = self
513 .windows
514 .iter()
515 .position(|w| w.surface == *surface.wl_surface());
516 if let Some(pos) = pos {
517 let w = self.windows.remove(pos);
518 if w.is_shell {
519 // El shell se cerró: libera su franja, el Cerebro vuelve
520 // a teselar en la salida entera.
521 let (ow, oh) = self.output_size;
522 if ow != 0 && oh != 0 {
523 let ev = self.body.resize_output(0, ow, oh);
524 self.brain_feed(ev);
525 }
526 } else if let Some(ev) = self.body.close_surface(w.id) {
527 self.brain_feed(ev);
528 }
529 }
530 }
531
532 fn title_changed(&mut self, surface: ToplevelSurface) {
533 let id = self
534 .windows
535 .iter()
536 .find(|w| w.surface == *surface.wl_surface())
537 .map(|w| w.id);
538 let Some(id) = id else { return };
539 let title = with_states(surface.wl_surface(), |states| {
540 states
541 .data_map
542 .get::<XdgToplevelSurfaceData>()
543 .and_then(|d| d.lock().ok())
544 .and_then(|d| d.title.clone())
545 .unwrap_or_default()
546 });
547 if let Some(ev) = self.body.retitle_surface(id, title) {
548 self.brain_feed(ev);
549 }
550 }
551
552 fn fullscreen_request(
553 &mut self,
554 surface: ToplevelSurface,
555 _output: Option<wl_output::WlOutput>,
556 ) {
557 let id = self
558 .windows
559 .iter()
560 .find(|w| w.surface == *surface.wl_surface())
561 .map(|w| w.id);
562 if let Some(id) = id {
563 self.brain_feed(BodyEvent::FullscreenRequest { id, fullscreen: true });
564 }
565 }
566
567 fn unfullscreen_request(&mut self, surface: ToplevelSurface) {
568 let id = self
569 .windows
570 .iter()
571 .find(|w| w.surface == *surface.wl_surface())
572 .map(|w| w.id);
573 if let Some(id) = id {
574 self.brain_feed(BodyEvent::FullscreenRequest { id, fullscreen: false });
575 }
576 }
577
578 fn new_popup(&mut self, surface: PopupSurface, _positioner: PositionerState) {
579 let _ = surface.send_configure();
580 }
581
582 fn grab(&mut self, _surface: PopupSurface, _seat: wl_seat::WlSeat, _serial: Serial) {}
583
584 fn reposition_request(
585 &mut self,
586 _surface: PopupSurface,
587 _positioner: PositionerState,
588 _token: u32,
589 ) {
590 }
591}
592
593/// Decoración de ventana: carmen tesela, así que las ventanas no llevan
594/// barra de título. Le decimos a todo cliente que la decoración la pone
595/// el servidor (`ServerSide`) — y como el servidor no dibuja ninguna, la
596/// ventana queda sin marco. Sin esto, clientes como `foot` se dibujan su
597/// propia barra (CSD), que estorba en un escritorio teselante.
598impl XdgDecorationHandler for App {
599 fn new_decoration(&mut self, toplevel: ToplevelSurface) {
600 toplevel.with_pending_state(|s| s.decoration_mode = Some(DecorationMode::ServerSide));
601 toplevel.send_configure();
602 }
603
604 fn request_mode(&mut self, toplevel: ToplevelSurface, _mode: DecorationMode) {
605 toplevel.with_pending_state(|s| s.decoration_mode = Some(DecorationMode::ServerSide));
606 toplevel.send_configure();
607 }
608
609 fn unset_mode(&mut self, toplevel: ToplevelSurface) {
610 toplevel.with_pending_state(|s| s.decoration_mode = Some(DecorationMode::ServerSide));
611 toplevel.send_configure();
612 }
613}
614
615impl SelectionHandler for App {
616 type SelectionUserData = ();
617}
618
619impl DataDeviceHandler for App {
620 fn data_device_state(&self) -> &DataDeviceState {
621 &self.data_device_state
622 }
623}
624impl ClientDndGrabHandler for App {}
625impl ServerDndGrabHandler for App {
626 fn send(&mut self, _mime_type: String, _fd: std::os::unix::io::OwnedFd, _seat: Seat<Self>) {}
627}
628
629impl SeatHandler for App {
630 type KeyboardFocus = WlSurface;
631 type PointerFocus = WlSurface;
632 type TouchFocus = WlSurface;
633
634 fn seat_state(&mut self) -> &mut SeatState<Self> {
635 &mut self.seat_state
636 }
637
638 fn focus_changed(&mut self, _seat: &Seat<Self>, _focused: Option<&WlSurface>) {}
639
640 /// El cliente enfocado pidió un cursor — guardamos su petición; el
641 /// backend la pinta (su superficie, o el cuadrado si es con nombre).
642 fn cursor_image(&mut self, _seat: &Seat<Self>, image: CursorImageStatus) {
643 self.cursor_status = image;
644 }
645}
646
647/// El protocolo `wl_output` no necesita estado propio — basta con
648/// anunciar el global para que los clientes vean que hay un monitor.
649impl OutputHandler for App {}
650
651delegate_compositor!(App);
652delegate_xdg_shell!(App);
653delegate_xdg_decoration!(App);
654delegate_dmabuf!(App);
655delegate_shm!(App);
656delegate_seat!(App);
657delegate_data_device!(App);
658delegate_output!(App);
659
660// ---------------------------------------------------------------------
661// Datos por cliente
662// ---------------------------------------------------------------------
663
664#[derive(Default)]
665struct ClientState {
666 compositor_state: CompositorClientState,
667}
668impl ClientData for ClientState {
669 fn initialized(&self, _id: ClientId) {}
670 fn disconnected(&self, _id: ClientId, _reason: DisconnectReason) {}
671}
672
673// ---------------------------------------------------------------------
674// Utilidades
675// ---------------------------------------------------------------------
676
677/// Construye la cadena de un atajo (`"Super+Shift+j"`) desde el estado de
678/// modificadores y el keysym, con el mismo formato que el mapa de teclas
679/// de [`mirada_brain`]. `None` si no es una tecla mapeable.
680fn combo_string(mods: &ModifiersState, sym: Keysym) -> Option<String> {
681 let utf = xkb::keysym_to_utf8(sym);
682 let key = utf.trim_end_matches('\0');
683 let name = if key == " " {
684 "space".to_string()
685 } else {
686 // ¿Es un único carácter imprimible? Entonces la tecla es ese carácter.
687 let mut chars = key.chars();
688 match (chars.next(), chars.next()) {
689 (Some(c), None) if c.is_ascii_graphic() => c.to_ascii_lowercase().to_string(),
690 // Si no, una tecla con nombre: Return, Tab, Up, F5…
691 _ => named_key(sym)?,
692 }
693 };
694 let mut combo = String::new();
695 if mods.logo {
696 combo.push_str("Super+");
697 }
698 if mods.ctrl {
699 combo.push_str("Ctrl+");
700 }
701 if mods.shift {
702 combo.push_str("Shift+");
703 }
704 if mods.alt {
705 combo.push_str("Alt+");
706 }
707 combo.push_str(&name);
708 Some(combo)
709}
710
711/// El nombre canónico de una tecla especial — `Return`, `Tab`, `Up`,
712/// `F5`… `None` si xkb no le da un nombre razonable.
713fn named_key(sym: Keysym) -> Option<String> {
714 let name = xkb::keysym_get_name(sym);
715 if name.is_empty() || name == "NoSymbol" || name.starts_with("0x") {
716 None
717 } else {
718 Some(name)
719 }
720}
721
722/// Despacha los callbacks de frame de un árbol de superficies: avisa a
723/// cada cliente de que puede dibujar el siguiente cuadro.
724fn send_frames_surface_tree(surface: &WlSurface, time: u32) {
725 with_surface_tree_downward(
726 surface,
727 (),
728 |_, _, &()| TraversalAction::DoChildren(()),
729 |_surf, states, &()| {
730 for callback in states
731 .cached_state
732 .get::<SurfaceAttributes>()
733 .current()
734 .frame_callbacks
735 .drain(..)
736 {
737 callback.done(time);
738 }
739 },
740 |_, _, &()| true,
741 );
742}
743
744// ---------------------------------------------------------------------
745// Bucle principal
746// ---------------------------------------------------------------------
747
748/// Dónde pintar una ventana. La del shell se ancla al pie de la salida
749/// y crece hacia arriba (su cajón de resultados se despliega sobre las
750/// ventanas). Una ventana normal va en su celda; si el cliente presenta
751/// una superficie más pequeña que la celda (p. ej. un terminal que
752/// redondea su tamaño a celdas de texto), se centra en el hueco.
753fn render_loc(w: &ManagedWindow, output_h: i32) -> (i32, i32) {
754 if w.is_shell {
755 let h = surface_px_size(w).map(|(_, h)| h).unwrap_or(SHELL_DOCK_HEIGHT);
756 return (0, output_h - h);
757 }
758 match with_renderer_surface_state(&w.surface, |s| s.surface_size()) {
759 Some(Some(size)) => {
760 let dx = ((w.size.0 - size.w) / 2).max(0);
761 let dy = ((w.size.1 - size.h) / 2).max(0);
762 (w.loc.0 + dx, w.loc.1 + dy)
763 }
764 _ => w.loc,
765 }
766}
767
768/// El tamaño en píxeles de la superficie de una ventana, si el cliente
769/// ya presentó un buffer. `None` mientras no haya dibujado nada — la usa
770/// el backend DRM para acertar el rectángulo en el test de impacto del
771/// puntero.
772fn surface_px_size(w: &ManagedWindow) -> Option<(i32, i32)> {
773 with_renderer_surface_state(&w.surface, |s| s.surface_size())
774 .flatten()
775 .map(|s| (s.w, s.h))
776}
777
778/// El punto caliente (hotspot) de una superficie de cursor: el píxel de
779/// la imagen que debe quedar bajo la posición real del puntero. `(0, 0)`
780/// si el cliente no lo declaró.
781fn cursor_hotspot(surface: &WlSurface) -> (i32, i32) {
782 with_states(surface, |states| {
783 states
784 .data_map
785 .get::<CursorImageSurfaceData>()
786 .map(|m| {
787 let h = m.lock().unwrap().hotspot;
788 (h.x, h.y)
789 })
790 .unwrap_or((0, 0))
791 })
792}
793
794/// Variables de entorno de tema que el compositor inyecta a cada hijo,
795/// para uniformizar GTK y Qt:
796/// - `XDG_CURRENT_DESKTOP=mirada` hace que `xdg-desktop-portal` enrute
797/// hacia `mirada-portal` (el backend de `org.freedesktop.appearance`).
798/// - `QT_QPA_PLATFORMTHEME=gtk3` hace que las apps Qt sigan el tema GTK,
799/// y por tanto el `gtk.css` que genera `nahual-theme`.
800const THEME_ENV: &[(&str, &str)] = &[
801 ("XDG_CURRENT_DESKTOP", "mirada"),
802 ("QT_QPA_PLATFORMTHEME", "gtk3"),
803];
804
805/// Lanza un comando como proceso hijo, vía `sh -c`. El hijo hereda el
806/// entorno —`WAYLAND_DISPLAY` incluido—, así que el cliente que abra se
807/// conecta a este compositor; además se le inyecta [`THEME_ENV`] para
808/// que GTK y Qt adopten el tema del escritorio. Lo usan la acción
809/// `spawn:…` del keymap, la variable `MIRADA_STARTUP` y el autoarranque.
810///
811/// `as_user`: si viene una identidad y el compositor corre como root
812/// (modo DM, tras el traspaso), el hijo baja a ese usuario — ver
813/// [`apply_user`]. Con `None`, o sin ser root, lanza con la identidad
814/// actual del compositor.
815fn spawn_command(cmd: &str, as_user: Option<&UserInfo>) {
816 let cmd = cmd.trim();
817 if cmd.is_empty() {
818 return;
819 }
820 let mut command = std::process::Command::new("sh");
821 command.arg("-c").arg(cmd).envs(THEME_ENV.iter().copied());
822 if let Some(user) = as_user {
823 if nix::unistd::geteuid().is_root() {
824 apply_user(&mut command, user);
825 }
826 }
827 match command.spawn() {
828 Ok(child) => println!("mirada-compositor · lanzado (pid {}): {cmd}", child.id()),
829 Err(e) => eprintln!("mirada-compositor · no pude lanzar «{cmd}»: {e}"),
830 }
831}
832
833/// Prepara un `Command` para que el hijo corra como `user`: fija grupos
834/// suplementarios, gid, uid y una sesión propia, hace `cd` a su home e
835/// inyecta las variables de identidad. Sólo se llama tras comprobar que
836/// el compositor es root.
837///
838/// La lista de grupos se calcula **en el padre**: `getgrouplist`
839/// consulta NSS (abre `/etc/group`), y eso no es seguro entre `fork` y
840/// `exec`; en `pre_exec` quedan sólo syscalls async-signal-safe.
841fn apply_user(command: &mut std::process::Command, user: &UserInfo) {
842 use nix::unistd::{setgid, setgroups, setuid, Gid, Uid};
843 use std::os::unix::process::CommandExt;
844
845 let uid = Uid::from_raw(user.uid);
846 let gid = Gid::from_raw(user.gid);
847 let groups: Vec<Gid> = std::ffi::CString::new(user.name.as_bytes())
848 .ok()
849 .and_then(|name| nix::unistd::getgrouplist(&name, gid).ok())
850 .unwrap_or_else(|| vec![gid]);
851
852 command
853 .env("HOME", &user.home)
854 .env("USER", &user.name)
855 .env("LOGNAME", &user.name)
856 .env("SHELL", &user.shell)
857 .current_dir(&user.home);
858
859 // SAFETY: corre en el hijo, entre `fork` y `exec`. Sólo syscalls
860 // async-signal-safe. El orden es obligatorio: grupos y gid ANTES que
861 // uid — al rebajar el uid se pierde el privilegio para fijarlos.
862 unsafe {
863 command.pre_exec(move || {
864 setgroups(&groups)?;
865 setgid(gid)?;
866 setuid(uid)?;
867 let _ = nix::unistd::setsid(); // sesión propia; no es crítico
868 Ok(())
869 });
870 }
871}
872
873/// La ruta del archivo de autoarranque, `…/mirada/autostart` — junto al
874/// keymap y las reglas. Con un usuario (tras el traspaso del DM) se
875/// resuelve bajo su home; sin él, bajo la config del proceso actual.
876fn autostart_path(user: Option<&UserInfo>) -> Option<std::path::PathBuf> {
877 match user {
878 Some(u) => Some(u.home.join(".config/mirada/autostart")),
879 None => Keymap::default_path().and_then(|p| p.parent().map(|d| d.join("autostart"))),
880 }
881}
882
883/// Lanza los programas del archivo de autoarranque: un comando por
884/// línea, `#` comenta y las líneas en blanco se saltan. Sin archivo, no
885/// hace nada. Se llama una vez al arrancar (o tras el traspaso del DM),
886/// con el socket ya abierto. `as_user` se propaga a [`spawn_command`].
887fn spawn_autostart(as_user: Option<&UserInfo>) {
888 let Some(path) = autostart_path(as_user) else {
889 return;
890 };
891 let Ok(text) = std::fs::read_to_string(&path) else {
892 return; // no hay archivo de autoarranque
893 };
894 let mut n = 0;
895 for line in text.lines() {
896 let line = line.trim();
897 if line.is_empty() || line.starts_with('#') {
898 continue;
899 }
900 spawn_command(line, as_user);
901 n += 1;
902 }
903 if n > 0 {
904 println!("mirada-compositor · autoarranque: {n} programa(s) desde {}", path.display());
905 }
906}
907
908/// Nombre o ruta del binario del greeter. `MIRADA_GREETER_BIN` lo
909/// sobreescribe — cómodo en desarrollo para apuntar a `target/…`.
910fn greeter_bin() -> String {
911 std::env::var("MIRADA_GREETER_BIN").unwrap_or_else(|_| "mirada-greeter".to_string())
912}
913
914/// Lanza `mirada-greeter` como proceso hijo, en modo DM, con el stdout
915/// capturado. Un hilo lee sus líneas: la que sea un [`SessionTicket`] se
916/// entrega por `send` (el bucle de eventos hará el traspaso); el resto
917/// del stdout se reenvía a la consola con el prefijo `greeter ·`. El
918/// hilo es dueño del `Child` y lo cosecha cuando el greeter termina.
919fn spawn_greeter<S>(send: S) -> std::io::Result<()>
920where
921 S: Fn(SessionTicket) + Send + 'static,
922{
923 use std::io::{BufRead, BufReader};
924 use std::process::{Command, Stdio};
925
926 let mut child = Command::new(greeter_bin())
927 .envs(THEME_ENV.iter().copied())
928 .stdout(Stdio::piped())
929 .spawn()?;
930 let stdout = child.stdout.take().expect("stdout pedido con Stdio::piped");
931 println!("mirada-compositor · greeter lanzado (pid {}).", child.id());
932
933 std::thread::spawn(move || {
934 for line in BufReader::new(stdout).lines().map_while(Result::ok) {
935 match SessionTicket::from_line(&line) {
936 Some(ticket) => {
937 println!("mirada-compositor · tiquet de sesión recibido del greeter.");
938 send(ticket);
939 }
940 None => println!("greeter · {line}"),
941 }
942 }
943 match child.wait() {
944 Ok(status) => println!("mirada-compositor · el greeter terminó ({status})."),
945 Err(e) => eprintln!("mirada-compositor · wait(greeter): {e}"),
946 }
947 });
948 Ok(())
949}
950
951/// Carga las reglas de ventana del usuario, o ninguna si no hay archivo.
952fn load_user_rules() -> Rules {
953 match Rules::default_path() {
954 Some(p) => Rules::load_or_default(&p),
955 None => Rules::default(),
956 }
957}
958
959/// Arma un Cerebro embebido: un `Desktop` con el keymap del usuario y
960/// sus reglas de ventana. Lo usan tanto el modo autónomo como el modo
961/// greeter (el DM es siempre autónomo — un Cerebro externo no tiene
962/// sentido en la pantalla de login).
963fn embedded_brain(keymap_path: &Option<std::path::PathBuf>) -> Brain {
964 let keymap = match keymap_path {
965 Some(p) => Keymap::load_or_init(p),
966 None => Keymap::default(),
967 };
968 let mut desktop = Desktop::with_keymap(keymap);
969 desktop.set_rules(load_user_rules());
970 Brain::Embedded(desktop)
971}
972
973/// Crea y anuncia un `wl_output` (un monitor) en el protocolo Wayland —
974/// muchos clientes (`foot` entre ellos) se niegan a arrancar sin uno.
975/// Devuelve el [`Output`](smithay::output::Output); hay que mantenerlo
976/// vivo mientras el compositor corra.
977fn announce_output(
978 dh: &DisplayHandle,
979 name: &str,
980 width: i32,
981 height: i32,
982 refresh_mhz: i32,
983) -> smithay::output::Output {
984 use smithay::output::{Mode, Output, PhysicalProperties, Scale, Subpixel};
985 let output = Output::new(
986 name.to_string(),
987 PhysicalProperties {
988 size: (0, 0).into(),
989 subpixel: Subpixel::Unknown,
990 make: "mirada".into(),
991 model: name.to_string(),
992 },
993 );
994 output.create_global::<App>(dh);
995 let mode = Mode { size: (width, height).into(), refresh: refresh_mhz };
996 output.change_current_state(
997 Some(mode),
998 Some(Transform::Normal),
999 Some(Scale::Integer(1)),
1000 Some((0, 0).into()),
1001 );
1002 output.set_preferred(mode);
1003 output
1004}
1005
1006/// Anuncia el global `zwp_linux_dmabuf` con los formatos que el
1007/// `GlesRenderer` admite. Hay que llamarlo una vez creado el renderer
1008/// (no antes: los formatos salen de él) — así las apps que pintan por
1009/// GPU (GPUI, navegadores acelerados) pueden ser clientes del compositor.
1010fn announce_dmabuf(app: &mut App, dh: &DisplayHandle, renderer: &GlesRenderer) {
1011 let formats: Vec<_> = renderer.dmabuf_formats().into_iter().collect();
1012 println!(
1013 "mirada-compositor · dmabuf: {} formato(s) anunciado(s).",
1014 formats.len()
1015 );
1016 app.dmabuf_state.create_global::<App>(dh, formats);
1017}
1018
1019/// Lo que comparten los dos backends gráficos: el `Display` de Wayland,
1020/// el `App` ya armado y la maquinaria de keymap y control.
1021struct Setup {
1022 display: Display<App>,
1023 app: App,
1024 keymap_path: Option<std::path::PathBuf>,
1025 keymap_watch: Option<mirada_brain::KeymapWatch>,
1026 ctl: Option<CtlServer>,
1027}
1028
1029/// Arma el estado del compositor — todo lo independiente del backend
1030/// gráfico (Wayland, Cerebro, teclado, keymap, control). Cada backend
1031/// (winit o DRM) registra luego su propia salida y monta su bucle.
1032fn build_app(greeter: bool) -> Result<Setup, Box<dyn std::error::Error>> {
1033 let display: Display<App> = Display::new()?;
1034 let dh = display.handle();
1035
1036 let mut seat_state = SeatState::new();
1037 let seat = seat_state.new_wl_seat(&dh, "mirada");
1038
1039 // Anuncia el gestor de decoración: las ventanas van sin marco (ver
1040 // `XdgDecorationHandler`). El `XdgDecorationState` sólo serviría para
1041 // retirar el global más tarde, cosa que nunca hacemos.
1042 let _ = XdgDecorationState::new::<App>(&dh);
1043
1044 // El keymap del usuario (`~/.config/mirada/keymap.ron`). Sólo lo usa
1045 // el Cerebro embebido; con un Cerebro enlazado, el keymap es asunto suyo.
1046 let keymap_path = Keymap::default_path();
1047
1048 // Elige el Cerebro. El modo greeter (DM) fuerza Cerebro embebido;
1049 // si no, enlazado cuando `MIRADA_SOCKET` está puesto, autónomo si no.
1050 let brain = if greeter {
1051 println!("mirada-compositor · modo greeter (DM) — Cerebro embebido.");
1052 embedded_brain(&keymap_path)
1053 } else {
1054 match std::env::var("MIRADA_SOCKET") {
1055 Ok(path) => {
1056 println!("mirada-compositor · esperando al Cerebro en {path} …");
1057 let link = BodyLink::listen(&path)?;
1058 println!("mirada-compositor · Cerebro conectado.");
1059 Brain::Linked(link)
1060 }
1061 Err(_) => {
1062 println!("mirada-compositor · modo autónomo (Cerebro embebido).");
1063 embedded_brain(&keymap_path)
1064 }
1065 }
1066 };
1067
1068 let mut app = App {
1069 compositor_state: CompositorState::new::<App>(&dh),
1070 xdg_shell_state: XdgShellState::new::<App>(&dh),
1071 shm_state: ShmState::new::<App>(&dh, Vec::new()),
1072 dmabuf_state: DmabufState::new(),
1073 seat_state,
1074 data_device_state: DataDeviceState::new::<App>(&dh),
1075 seat,
1076 keyboard: None,
1077 pointer: None,
1078 pointer_loc: (0.0, 0.0),
1079 cursor_status: CursorImageStatus::default_named(),
1080 drag: None,
1081 output_size: (0, 0),
1082 windows: Vec::new(),
1083 body: BodyState::new(),
1084 brain,
1085 mode: if greeter { BodyMode::Greeter } else { BodyMode::Session },
1086 session_user: None,
1087 grabs: Vec::new(),
1088 pending_keybind: None,
1089 next_id: 1,
1090 running: true,
1091 };
1092
1093 let keyboard = app.seat.add_keyboard(Default::default(), 200, 25)?;
1094 app.keyboard = Some(keyboard);
1095 app.pointer = Some(app.seat.add_pointer());
1096
1097 // En modo embebido, el propio Desktop dicta los atajos a
1098 // interceptar — salvo en modo greeter: en la pantalla de login
1099 // todas las teclas van al greeter (que el usuario no pueda lanzar
1100 // nada ni cerrar el compositor). Los atajos se registran luego, en
1101 // el traspaso a la sesión (`complete_greeter_handoff`).
1102 if !greeter {
1103 if let Brain::Embedded(desktop) = &app.brain {
1104 let grab = desktop.grab_keys();
1105 app.apply_commands(vec![grab]);
1106 }
1107 }
1108
1109 // Vigilancia del keymap para recargarlo en caliente — sólo tiene
1110 // sentido con el Cerebro embebido y fuera del modo greeter (donde
1111 // no hay atajos registrados que recargar).
1112 let keymap_watch = match (&app.brain, &keymap_path) {
1113 (Brain::Embedded(_), Some(p)) if !greeter => Keymap::watch(p).ok(),
1114 _ => None,
1115 };
1116 if keymap_watch.is_some() {
1117 println!("mirada-compositor · vigilando el keymap (recarga en caliente).");
1118 }
1119
1120 // API de control (mirada-ctl) — sólo con el Cerebro embebido; si es
1121 // externo, el socket de control lo abre él.
1122 let ctl = match &app.brain {
1123 Brain::Embedded(_) => {
1124 let path = mirada_brain::ctl::default_socket_path();
1125 match CtlServer::bind(&path) {
1126 Ok(s) => {
1127 println!("mirada-compositor · API de control en {}", path.display());
1128 Some(s)
1129 }
1130 Err(e) => {
1131 eprintln!("mirada-compositor · sin API de control: {e}");
1132 None
1133 }
1134 }
1135 }
1136 Brain::Linked(_) => None,
1137 };
1138
1139 Ok(Setup { display, app, keymap_path, keymap_watch, ctl })
1140}
1141
1142/// El backend `winit`: corre anidado dentro de una sesión gráfica.
1143fn run_winit(greeter: bool) -> Result<(), Box<dyn std::error::Error>> {
1144 let Setup {
1145 mut display,
1146 app: mut state,
1147 keymap_path,
1148 keymap_watch,
1149 ctl,
1150 } = build_app(greeter)?;
1151 let keyboard = state.keyboard.clone().expect("teclado inicializado");
1152
1153 // El backend gráfico va primero. winit abre la ventana del compositor
1154 // dentro de tu sesión gráfica anfitriona, y para encontrarla lee
1155 // `WAYLAND_DISPLAY` / `DISPLAY` del entorno. Si publicáramos antes
1156 // nuestro propio socket en `WAYLAND_DISPLAY`, winit intentaría
1157 // anidarse en nosotros mismos —un socket que aún no atiende a nadie—
1158 // y se quedaría colgado para siempre.
1159 let (mut backend, mut winit) = match winit::init::<GlesRenderer>() {
1160 Ok(pair) => pair,
1161 Err(e) => {
1162 eprintln!("mirada-compositor · no pude abrir la ventana: {e}");
1163 eprintln!(
1164 " El backend `winit` necesita una sesión gráfica anfitriona\n \
1165 (X11 o Wayland) donde dibujar la ventana del compositor.\n \
1166 Aquí no hay ninguna: DISPLAY='{}', WAYLAND_DISPLAY='{}',\n \
1167 XDG_SESSION_TYPE='{}'.\n \
1168 Lánzalo desde un escritorio gráfico, o desde un servidor X\n \
1169 virtual (Xvfb) al que te conectes por VNC.",
1170 std::env::var("DISPLAY").unwrap_or_default(),
1171 std::env::var("WAYLAND_DISPLAY").unwrap_or_default(),
1172 std::env::var("XDG_SESSION_TYPE").unwrap_or_else(|_| "tty".into()),
1173 );
1174 return Err(e.into());
1175 }
1176 };
1177
1178 // Ahora sí, nuestro propio socket Wayland — y `WAYLAND_DISPLAY` se
1179 // publica *después* de winit, sólo para los clientes que lancemos
1180 // como procesos hijos.
1181 let listener = ListeningSocket::bind_auto("wayland", 1..32)?;
1182 let socket_name = listener
1183 .socket_name()
1184 .and_then(|s| s.to_str())
1185 .unwrap_or("wayland-?")
1186 .to_string();
1187 std::env::set_var("WAYLAND_DISPLAY", &socket_name);
1188 println!("mirada-compositor · escuchando en WAYLAND_DISPLAY={socket_name}");
1189 println!(" lanza un cliente: WAYLAND_DISPLAY={socket_name} foot");
1190
1191 let start = Instant::now();
1192 let mut clients = Vec::new();
1193
1194 // Con el renderer ya creado, anuncia dmabuf (clientes con GPU).
1195 announce_dmabuf(&mut state, &display.handle(), backend.renderer());
1196
1197 // Salida inicial = el tamaño de la ventana winit.
1198 let win_size = backend.window_size();
1199 let _wl_output = announce_output(&display.handle(), "winit", win_size.w, win_size.h, 60_000);
1200 {
1201 let ev = state.body.add_output(0, win_size.w, win_size.h);
1202 state.brain_feed(ev);
1203 state.output_size = (win_size.w, win_size.h);
1204 }
1205
1206 // Modo greeter (DM anidado — útil para iterar la UI del login):
1207 // lanza el greeter y recibe su tiquet por un canal que el bucle sondea.
1208 let greeter_rx = if state.mode == BodyMode::Greeter {
1209 let (tx, rx) = std::sync::mpsc::channel::<SessionTicket>();
1210 spawn_greeter(move |ticket| {
1211 let _ = tx.send(ticket);
1212 })?;
1213 Some(rx)
1214 } else {
1215 None
1216 };
1217
1218 while state.running {
1219 // 1 · Eventos del backend (teclado, redimensión, cierre).
1220 let status = winit.dispatch_new_events(|event| match event {
1221 WinitEvent::CloseRequested => state.running = false,
1222 WinitEvent::Resized { size, .. } => {
1223 state.output_changed(size.w, size.h);
1224 }
1225 WinitEvent::Input(InputEvent::Keyboard { event }) => {
1226 let code = event.key_code();
1227 let key_state = event.state();
1228 let pressed = key_state == KeyState::Pressed;
1229 let time = start.elapsed().as_millis() as u32;
1230 keyboard.clone().input::<(), _>(
1231 &mut state,
1232 code,
1233 key_state,
1234 SERIAL_COUNTER.next_serial(),
1235 time,
1236 |st, mods, handle| {
1237 if !pressed {
1238 return FilterResult::Forward;
1239 }
1240 if let Some(combo) = combo_string(mods, handle.modified_sym()) {
1241 if st.grabs.contains(&combo) {
1242 st.pending_keybind = Some(combo);
1243 return FilterResult::Intercept(());
1244 }
1245 }
1246 FilterResult::Forward
1247 },
1248 );
1249 if let Some(combo) = state.pending_keybind.take() {
1250 let ev = state.body.keybind(combo);
1251 state.brain_feed(ev);
1252 }
1253 }
1254 _ => {}
1255 });
1256 if let PumpStatus::Exit(_) = status {
1257 break;
1258 }
1259
1260 // 2 · Comandos de un Cerebro enlazado.
1261 state.brain_poll();
1262
1263 // 2 bis · El tiquet del greeter (modo DM): dispara el traspaso.
1264 if let Some(rx) = &greeter_rx {
1265 while let Ok(ticket) = rx.try_recv() {
1266 state.complete_greeter_handoff(ticket);
1267 }
1268 }
1269
1270 // 2 ter · Recarga del keymap si el archivo cambió en disco.
1271 if keymap_watch.as_ref().is_some_and(|w| w.changed()) {
1272 if let Some(path) = &keymap_path {
1273 match Keymap::load(path) {
1274 Ok(km) => {
1275 let cmd = if let Brain::Embedded(d) = &mut state.brain {
1276 Some(d.set_keymap(km))
1277 } else {
1278 None
1279 };
1280 if let Some(cmd) = cmd {
1281 state.apply_commands(vec![cmd]);
1282 }
1283 println!("mirada-compositor · keymap recargado.");
1284 }
1285 Err(e) => eprintln!(
1286 "mirada-compositor · keymap inválido, conservo el anterior: {e}"
1287 ),
1288 }
1289 }
1290 }
1291
1292 // 2 quater · Peticiones del API de control (mirada-ctl).
1293 if let Some(ctl) = &ctl {
1294 while let Some(mut conn) = ctl.poll() {
1295 let reply = match conn.read_request() {
1296 Ok(Some(req)) => state.serve_ctl(req),
1297 Ok(None) => continue,
1298 Err(e) => CtlReply::Error(format!("{e}")),
1299 };
1300 let _ = conn.reply(&reply);
1301 }
1302 }
1303
1304 // 3 · Composición de las superficies en sus rectángulos.
1305 let size = backend.window_size();
1306 let damage: Rectangle<i32, smithay::utils::Physical> = Rectangle::from_size(size);
1307 {
1308 let (renderer, mut framebuffer) = backend.bind().unwrap();
1309 // Orden de pintado: la lista de elementos va front-to-back
1310 // (índice 0 = encima): el shell primero —va sobre todo—, luego
1311 // las flotantes, luego las teseladas. `sort_by_key` es estable:
1312 // dentro de cada grupo se respeta el orden de apertura.
1313 let output_h = state.output_size.1;
1314 let mut shown: Vec<&ManagedWindow> =
1315 state.windows.iter().filter(|w| w.visible).collect();
1316 shown.sort_by_key(|w| (!w.is_shell, !w.floating));
1317 let elements: Vec<WaylandSurfaceRenderElement<GlesRenderer>> = shown
1318 .iter()
1319 .flat_map(|w| {
1320 render_elements_from_surface_tree(
1321 renderer,
1322 &w.surface,
1323 render_loc(w, output_h),
1324 1.0,
1325 1.0,
1326 Kind::Unspecified,
1327 )
1328 })
1329 .collect();
1330 let mut frame = renderer
1331 .render(&mut framebuffer, size, Transform::Flipped180)
1332 .unwrap();
1333 frame
1334 .clear(Color32F::new(0.05, 0.05, 0.08, 1.0), &[damage])
1335 .unwrap();
1336 draw_render_elements(&mut frame, 1.0, &elements, &[damage]).unwrap();
1337 let _ = frame.finish().unwrap();
1338 }
1339
1340 // 4 · Callbacks de frame + clientes nuevos + flush.
1341 let time = start.elapsed().as_millis() as u32;
1342 for w in &state.windows {
1343 send_frames_surface_tree(&w.surface, time);
1344 }
1345 if let Some(stream) = listener.accept()? {
1346 let client = display
1347 .handle()
1348 .insert_client(stream, Arc::new(ClientState::default()))
1349 .unwrap();
1350 clients.push(client);
1351 }
1352 display.dispatch_clients(&mut state)?;
1353 display.flush_clients()?;
1354
1355 backend.submit(Some(&[damage])).unwrap();
1356 }
1357
1358 println!("mirada-compositor · adiós.");
1359 Ok(())
1360}
1361
1362fn main() {
1363 // Banderas en cualquier orden: `--greeter` (modo DM) es ortogonal
1364 // al backend (`--winit` anidado · `--drm` nativo · auto si falta).
1365 let args: Vec<String> = std::env::args().skip(1).collect();
1366 for a in &args {
1367 if !matches!(a.as_str(), "--greeter" | "--winit" | "--drm") {
1368 eprintln!(
1369 "mirada-compositor: opción desconocida «{a}» — usa --greeter, --winit o --drm"
1370 );
1371 std::process::exit(2);
1372 }
1373 }
1374 let greeter = args.iter().any(|a| a == "--greeter");
1375 let backend = args.iter().find(|a| matches!(a.as_str(), "--winit" | "--drm"));
1376
1377 let result = match backend.map(String::as_str) {
1378 Some("--drm") => drm_backend::run(greeter),
1379 Some("--winit") => run_winit(greeter),
1380 _ => {
1381 // Auto: con sesión gráfica anfitriona → winit (anidado);
1382 // sin ella (una TTY pelada) → backend DRM.
1383 let nested = std::env::var_os("WAYLAND_DISPLAY").is_some()
1384 || std::env::var_os("DISPLAY").is_some();
1385 if nested {
1386 println!("mirada-compositor · sesión gráfica detectada → backend winit.");
1387 run_winit(greeter)
1388 } else {
1389 println!("mirada-compositor · sin sesión gráfica → backend DRM.");
1390 drm_backend::run(greeter)
1391 }
1392 }
1393 };
1394 if let Err(e) = result {
1395 eprintln!("mirada-compositor · error: {e}");
1396 std::process::exit(1);
1397 }
1398}