feat(carmen): modo greeter — mirada-compositor como DM
`mirada-compositor --greeter` arranca como gestor de login: lanza
mirada-greeter como proceso hijo, lee su stdout y, al recibir el
SessionTicket, muta de BodyMode::Greeter a BodyMode::Session sin
reiniciar el servidor Wayland — la «mutación atómica» del DM.
- BodyMode { Greeter, Session }: eje ortogonal a Brain (Embedded/Linked).
- modo greeter: sin atajos registrados, rechaza Spawn, sin autoarranque.
- traspaso (complete_greeter_handoff): registra los atajos y arranca la
sesión — el comando del tiquet, o el autoarranque del usuario.
- privilegios: el compositor corre como root; spawn_command baja a
setuid/setgid + grupos suplementarios del usuario autenticado.
- bandera ortogonal al backend (--greeter [--drm|--winit]); el tiquet
llega por un canal calloop en DRM y por mpsc en winit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
diff --git a/Cargo.lock b/Cargo.lock
index df4b287..2c439fa 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -7827,9 +7827,11 @@ dependencies = [
name = "mirada-compositor"
version = "0.1.0"
dependencies = [
+ "brahman-auth",
"mirada-body",
"mirada-brain",
"mirada-link",
+ "nix 0.29.0",
"smithay",
]
diff --git a/crates/apps/mirada-compositor/Cargo.toml b/crates/apps/mirada-compositor/Cargo.toml
index e7547a0..522de8b 100644
--- a/crates/apps/mirada-compositor/Cargo.toml
+++ b/crates/apps/mirada-compositor/Cargo.toml
@@ -16,4 +16,6 @@ path = "src/main.rs"
mirada-brain = { path = "../../modules/mirada/mirada-brain" }
mirada-body = { path = "../../modules/mirada/mirada-body" }
mirada-link = { path = "../../modules/mirada/mirada-link" }
+brahman-auth = { path = "../../protocol/brahman-auth" }
+nix = { workspace = true }
smithay = "0.7"
diff --git a/crates/apps/mirada-compositor/README.md b/crates/apps/mirada-compositor/README.md
index eaa1fcc..f38f757 100644
--- a/crates/apps/mirada-compositor/README.md
+++ b/crates/apps/mirada-compositor/README.md
@@ -18,6 +18,9 @@ Tiene **dos backends gráficos**:
Sin argumentos elige solo: con `DISPLAY`/`WAYLAND_DISPLAY` → `winit`;
sin ellos → `drm`. O fuérzalo: `mirada-compositor --winit` / `--drm`.
+La bandera `--greeter` (ortogonal al backend) arranca el compositor como
+gestor de login — ver **Modo greeter (DM)** más abajo.
+
## Backends
### winit — anidado
@@ -87,6 +90,35 @@ sólo probarlo:
Dentro de la sesión, `Ctrl+Alt+F1…F12` salta a otra TTY y vuelve sin
romper carmen.
+## Modo greeter (DM)
+
+`mirada-compositor --greeter` arranca el compositor como **gestor de
+login**: en vez de la sesión, compone el greeter (`mirada-greeter`),
+que lanza como proceso hijo. El usuario teclea sus credenciales; cuando
+el login es válido el greeter emite un `SessionTicket` por su stdout y
+el compositor **muta a modo sesión sin reiniciar el servidor Wayland**
+— el mismo proceso, la misma GPU, las mismas ventanas («mutación
+atómica»). Desde ahí baja privilegios al usuario autenticado
+(`setuid`/`setgid` + grupos) para todo lo que lanza.
+
+La bandera es ortogonal al backend: `--greeter` solo (auto), o
+`--greeter --drm` / `--greeter --winit`.
+
+```sh
+# DM real, sobre una TTY — el compositor corre como root: PAM lo exige
+sudo mirada-compositor --greeter --drm
+
+# iterar el greeter anidado, con credenciales de prueba
+MIRADA_GREETER_MOCK=demo:demo \
+ cargo run -p mirada-compositor -- --greeter --winit
+```
+
+En modo greeter no se registran atajos (todas las teclas van al
+greeter — que el usuario no pueda lanzar nada ni cerrar el compositor),
+se rechaza `spawn:` y no corre el autoarranque; los atajos y la sesión
+arrancan sólo tras el traspaso. `MIRADA_GREETER_BIN` apunta a otro
+binario de greeter (cómodo para señalar a `target/…` en desarrollo).
+
## Lanzador de aplicaciones
`mirada-launcher` escanea los `.desktop` del sistema y lanza el que
diff --git a/crates/apps/mirada-compositor/src/drm_backend.rs b/crates/apps/mirada-compositor/src/drm_backend.rs
index 4953de9..5be079f 100644
--- a/crates/apps/mirada-compositor/src/drm_backend.rs
+++ b/crates/apps/mirada-compositor/src/drm_backend.rs
@@ -44,6 +44,7 @@ use smithay::backend::udev;
use smithay::input::keyboard::FilterResult;
use smithay::input::pointer::{AxisFrame, ButtonEvent, CursorImageStatus, MotionEvent};
use smithay::output::OutputModeSource;
+use smithay::reexports::calloop::channel::{channel as ticket_channel, Event as TicketEvent};
use smithay::reexports::calloop::generic::Generic;
use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
use smithay::reexports::calloop::{EventLoop, Interest, Mode as CalloopMode, PostAction};
@@ -56,9 +57,13 @@ use smithay::utils::{
DeviceFd, IsAlive, Logical, Physical, Point, Rectangle, Scale, Size, Transform, SERIAL_COUNTER,
};
+use brahman_auth::SessionTicket;
use mirada_brain::{BodyEvent, CtlReply, Keymap, Rect};
-use crate::{combo_string, send_frames_surface_tree, App, Brain, ClientState, DragGrab, DragMode, Setup};
+use crate::{
+ combo_string, send_frames_surface_tree, App, BodyMode, Brain, ClientState, DragGrab, DragMode,
+ Setup,
+};
/// El `DrmCompositor` concreto para la salida (un solo GPU).
type Compositor =
@@ -604,8 +609,9 @@ impl DrmState {
}
}
-/// Arranca el Cuerpo sobre DRM/KMS — fases 1, 2a y 2b.
-pub fn run() -> Result<(), Box<dyn Error>> {
+/// Arranca el Cuerpo sobre DRM/KMS — fases 1, 2a y 2b. Con `greeter`,
+/// el compositor nace en modo DM: ver [`BodyMode`].
+pub fn run(greeter: bool) -> Result<(), Box<dyn Error>> {
println!("mirada-compositor · backend DRM.");
println!("──────────────────────────────────────────────────");
@@ -733,7 +739,8 @@ pub fn run() -> Result<(), Box<dyn Error>> {
// 7 · El estado Wayland (Cerebro, teclado, keymap, control).
println!("[7/8] armando el estado Wayland …");
- let Setup { mut display, mut app, keymap_path, keymap_watch, ctl } = crate::build_app()?;
+ let Setup { mut display, mut app, keymap_path, keymap_watch, ctl } =
+ crate::build_app(greeter)?;
// Con el renderer ya creado, anuncia dmabuf — sin esto las apps que
// pintan por GPU (GPUI, navegadores acelerados) no pueden conectarse.
crate::announce_dmabuf(&mut app, &display.handle(), &renderer);
@@ -762,14 +769,25 @@ pub fn run() -> Result<(), Box<dyn Error>> {
std::env::set_var("WAYLAND_DISPLAY", &socket_name);
println!(" escuchando en WAYLAND_DISPLAY={socket_name}");
- // Autoarranque: los programas de `~/.config/mirada/autostart`.
- crate::spawn_autostart();
-
- // App de arranque: si `MIRADA_STARTUP` trae un comando, se lanza como
- // hijo (hereda `WAYLAND_DISPLAY`) — cómodo para probar sin saltar de VT.
- if let Ok(cmd) = std::env::var("MIRADA_STARTUP") {
- crate::spawn_command(&cmd);
- }
+ // Modo DM: lanza el greeter y recibe su tiquet por un canal de
+ // `calloop`. Modo normal: autoarranque + `MIRADA_STARTUP`.
+ let greeter_rx = if app.mode == BodyMode::Greeter {
+ let (tx, rx) = ticket_channel::<SessionTicket>();
+ crate::spawn_greeter(move |ticket| {
+ let _ = tx.send(ticket);
+ })?;
+ Some(rx)
+ } else {
+ // Autoarranque: los programas de `~/.config/mirada/autostart`.
+ crate::spawn_autostart(None);
+ // App de arranque: si `MIRADA_STARTUP` trae un comando, se lanza
+ // como hijo (hereda `WAYLAND_DISPLAY`) — cómodo para probar sin
+ // saltar de VT.
+ if let Ok(cmd) = std::env::var("MIRADA_STARTUP") {
+ crate::spawn_command(&cmd, None);
+ }
+ None
+ };
// 8 · El bucle `calloop`: VBlank, teclado, clientes y un timer.
println!("[8/8] montando el bucle de eventos …");
@@ -853,6 +871,18 @@ pub fn run() -> Result<(), Box<dyn Error>> {
})
.map_err(|e| format!("insert timer: {e}"))?;
+ // Tiquet del greeter (modo DM): al llegar, el traspaso a la sesión.
+ // El hilo lector del greeter despierta el bucle por este canal.
+ if let Some(rx) = greeter_rx {
+ handle
+ .insert_source(rx, |event, _, state: &mut DrmState| {
+ if let TicketEvent::Msg(ticket) = event {
+ state.app.complete_greeter_handoff(ticket);
+ }
+ })
+ .map_err(|e| format!("insert greeter: {e}"))?;
+ }
+
// Tope de tiempo opcional: `MIRADA_DRM_TIMEOUT=<segundos>` cierra el
// compositor solo (0 o sin definir = sin tope). El teclado ya
// funciona — `Super+Shift+e` o `Ctrl+C` son la salida normal.
diff --git a/crates/apps/mirada-compositor/src/main.rs b/crates/apps/mirada-compositor/src/main.rs
index 4bb4602..02af6d1 100644
--- a/crates/apps/mirada-compositor/src/main.rs
+++ b/crates/apps/mirada-compositor/src/main.rs
@@ -69,6 +69,7 @@ use smithay::{
delegate_shm, delegate_xdg_decoration, delegate_xdg_shell,
};
+use brahman_auth::{SessionTicket, UserInfo};
use mirada_body::{BodyOp, BodyState};
use mirada_brain::{
BodyEvent, BrainCommand, CtlReply, CtlRequest, CtlServer, Desktop, Keymap, Rules,
@@ -89,6 +90,21 @@ enum Brain {
Linked(BodyLink),
}
+/// La fase del ciclo de vida del Cuerpo. Es un eje **ortogonal** a
+/// [`Brain`]: `Brain` dice de dónde sale la geometría; `BodyMode` dice
+/// si el compositor está pidiendo credenciales o sirviendo una sesión.
+/// Un arranque normal nace ya en [`BodyMode::Session`]; un arranque de
+/// DM (`--greeter`) nace en [`BodyMode::Greeter`] y muta una sola vez,
+/// al recibir el tiquet de un login válido — la «mutación atómica».
+#[derive(Clone, Copy, PartialEq, Eq)]
+enum BodyMode {
+ /// Pantalla de login: el único cliente es el greeter, no se
+ /// registran atajos, se rechaza `Spawn` y no hay autoarranque.
+ Greeter,
+ /// Sesión de usuario: el compositor funciona con normalidad.
+ Session,
+}
+
/// `app_id` que distingue a la ventana del shell del escritorio. carmen
/// no la tesela: la acopla a una franja al pie de la pantalla.
const SHELL_APP_ID: &str = "carmen.shell";
@@ -169,6 +185,12 @@ struct App {
body: BodyState,
/// El Cerebro: embebido o enlazado.
brain: Brain,
+ /// Fase del ciclo de vida — login o sesión (ver [`BodyMode`]).
+ mode: BodyMode,
+ /// Identidad a la que rebajar privilegios al lanzar procesos de
+ /// sesión. `None` salvo tras el traspaso del DM — entonces cada
+ /// `spawn` hace `setuid`/`setgid` a este usuario (si somos root).
+ session_user: Option<UserInfo>,
/// Atajos globales a interceptar (los registra el Cerebro).
grabs: Vec<String>,
/// Atajo capturado en el último evento de teclado, pendiente de enviar.
@@ -292,7 +314,15 @@ impl App {
}
BodyOp::SetGrabs(keys) => self.grabs = keys,
BodyOp::SetCursor(_) => {}
- BodyOp::Spawn(cmd) => spawn_command(&cmd),
+ BodyOp::Spawn(cmd) => {
+ // En modo greeter no se lanza nada: la pantalla de login
+ // no es un sitio desde donde abrir programas.
+ if self.mode == BodyMode::Greeter {
+ eprintln!("mirada-compositor · «{cmd}» rechazado — modo greeter.");
+ } else {
+ spawn_command(&cmd, self.session_user.as_ref());
+ }
+ }
BodyOp::Shutdown => self.running = false,
}
}
@@ -378,6 +408,45 @@ impl App {
self.brain_feed(ev);
}
}
+
+ /// El traspaso del DM — la «mutación atómica». Llega el tiquet de un
+ /// login válido y el compositor pasa de la pantalla de greeter a la
+ /// sesión del usuario **sin reiniciar el servidor Wayland**: el mismo
+ /// proceso, la misma GPU, las mismas ventanas. Idempotente — un
+ /// segundo tiquet (no debería llegar) se ignora.
+ fn complete_greeter_handoff(&mut self, ticket: SessionTicket) {
+ if self.mode == BodyMode::Session {
+ return; // ya en sesión — un tiquet de más, se ignora
+ }
+ println!(
+ "mirada-compositor · traspaso a la sesión de «{}» (uid {}).",
+ ticket.user.name, ticket.user.uid
+ );
+ if !nix::unistd::geteuid().is_root() {
+ eprintln!(
+ "mirada-compositor · aviso: no corro como root — la sesión \
+ heredará mis privilegios, sin setuid al usuario."
+ );
+ }
+ self.mode = BodyMode::Session;
+ self.session_user = Some(ticket.user.clone());
+
+ // Ya en sesión: registra los atajos del escritorio (en modo
+ // greeter se omitieron a propósito — ver `build_app`).
+ if let Brain::Embedded(desktop) = &self.brain {
+ let grab = desktop.grab_keys();
+ self.apply_commands(vec![grab]);
+ }
+
+ // Arranca la sesión: el comando del tiquet, o el autoarranque
+ // del usuario si el tiquet no trae ninguno.
+ let user = self.session_user.clone();
+ if ticket.session.trim().is_empty() {
+ spawn_autostart(user.as_ref());
+ } else {
+ spawn_command(&ticket.session, user.as_ref());
+ }
+ }
}
// ---------------------------------------------------------------------
@@ -738,33 +807,85 @@ const THEME_ENV: &[(&str, &str)] = &[
/// conecta a este compositor; además se le inyecta [`THEME_ENV`] para
/// que GTK y Qt adopten el tema del escritorio. Lo usan la acción
/// `spawn:…` del keymap, la variable `MIRADA_STARTUP` y el autoarranque.
-fn spawn_command(cmd: &str) {
+///
+/// `as_user`: si viene una identidad y el compositor corre como root
+/// (modo DM, tras el traspaso), el hijo baja a ese usuario — ver
+/// [`apply_user`]. Con `None`, o sin ser root, lanza con la identidad
+/// actual del compositor.
+fn spawn_command(cmd: &str, as_user: Option<&UserInfo>) {
let cmd = cmd.trim();
if cmd.is_empty() {
return;
}
- match std::process::Command::new("sh")
- .arg("-c")
- .arg(cmd)
- .envs(THEME_ENV.iter().copied())
- .spawn()
- {
+ let mut command = std::process::Command::new("sh");
+ command.arg("-c").arg(cmd).envs(THEME_ENV.iter().copied());
+ if let Some(user) = as_user {
+ if nix::unistd::geteuid().is_root() {
+ apply_user(&mut command, user);
+ }
+ }
+ match command.spawn() {
Ok(child) => println!("mirada-compositor · lanzado (pid {}): {cmd}", child.id()),
Err(e) => eprintln!("mirada-compositor · no pude lanzar «{cmd}»: {e}"),
}
}
-/// La ruta del archivo de autoarranque del usuario,
-/// `~/.config/mirada/autostart` — junto al keymap y las reglas.
-fn autostart_path() -> Option<std::path::PathBuf> {
- Keymap::default_path().and_then(|p| p.parent().map(|d| d.join("autostart")))
+/// Prepara un `Command` para que el hijo corra como `user`: fija grupos
+/// suplementarios, gid, uid y una sesión propia, hace `cd` a su home e
+/// inyecta las variables de identidad. Sólo se llama tras comprobar que
+/// el compositor es root.
+///
+/// La lista de grupos se calcula **en el padre**: `getgrouplist`
+/// consulta NSS (abre `/etc/group`), y eso no es seguro entre `fork` y
+/// `exec`; en `pre_exec` quedan sólo syscalls async-signal-safe.
+fn apply_user(command: &mut std::process::Command, user: &UserInfo) {
+ use nix::unistd::{setgid, setgroups, setuid, Gid, Uid};
+ use std::os::unix::process::CommandExt;
+
+ let uid = Uid::from_raw(user.uid);
+ let gid = Gid::from_raw(user.gid);
+ let groups: Vec<Gid> = std::ffi::CString::new(user.name.as_bytes())
+ .ok()
+ .and_then(|name| nix::unistd::getgrouplist(&name, gid).ok())
+ .unwrap_or_else(|| vec![gid]);
+
+ command
+ .env("HOME", &user.home)
+ .env("USER", &user.name)
+ .env("LOGNAME", &user.name)
+ .env("SHELL", &user.shell)
+ .current_dir(&user.home);
+
+ // SAFETY: corre en el hijo, entre `fork` y `exec`. Sólo syscalls
+ // async-signal-safe. El orden es obligatorio: grupos y gid ANTES que
+ // uid — al rebajar el uid se pierde el privilegio para fijarlos.
+ unsafe {
+ command.pre_exec(move || {
+ setgroups(&groups)?;
+ setgid(gid)?;
+ setuid(uid)?;
+ let _ = nix::unistd::setsid(); // sesión propia; no es crítico
+ Ok(())
+ });
+ }
+}
+
+/// La ruta del archivo de autoarranque, `…/mirada/autostart` — junto al
+/// keymap y las reglas. Con un usuario (tras el traspaso del DM) se
+/// resuelve bajo su home; sin él, bajo la config del proceso actual.
+fn autostart_path(user: Option<&UserInfo>) -> Option<std::path::PathBuf> {
+ match user {
+ Some(u) => Some(u.home.join(".config/mirada/autostart")),
+ None => Keymap::default_path().and_then(|p| p.parent().map(|d| d.join("autostart"))),
+ }
}
/// Lanza los programas del archivo de autoarranque: un comando por
/// línea, `#` comenta y las líneas en blanco se saltan. Sin archivo, no
-/// hace nada. Se llama una vez al arrancar, con el socket ya abierto.
-fn spawn_autostart() {
- let Some(path) = autostart_path() else {
+/// hace nada. Se llama una vez al arrancar (o tras el traspaso del DM),
+/// con el socket ya abierto. `as_user` se propaga a [`spawn_command`].
+fn spawn_autostart(as_user: Option<&UserInfo>) {
+ let Some(path) = autostart_path(as_user) else {
return;
};
let Ok(text) = std::fs::read_to_string(&path) else {
@@ -776,7 +897,7 @@ fn spawn_autostart() {
if line.is_empty() || line.starts_with('#') {
continue;
}
- spawn_command(line);
+ spawn_command(line, as_user);
n += 1;
}
if n > 0 {
@@ -784,6 +905,49 @@ fn spawn_autostart() {
}
}
+/// Nombre o ruta del binario del greeter. `MIRADA_GREETER_BIN` lo
+/// sobreescribe — cómodo en desarrollo para apuntar a `target/…`.
+fn greeter_bin() -> String {
+ std::env::var("MIRADA_GREETER_BIN").unwrap_or_else(|_| "mirada-greeter".to_string())
+}
+
+/// Lanza `mirada-greeter` como proceso hijo, en modo DM, con el stdout
+/// capturado. Un hilo lee sus líneas: la que sea un [`SessionTicket`] se
+/// entrega por `send` (el bucle de eventos hará el traspaso); el resto
+/// del stdout se reenvía a la consola con el prefijo `greeter ·`. El
+/// hilo es dueño del `Child` y lo cosecha cuando el greeter termina.
+fn spawn_greeter<S>(send: S) -> std::io::Result<()>
+where
+ S: Fn(SessionTicket) + Send + 'static,
+{
+ use std::io::{BufRead, BufReader};
+ use std::process::{Command, Stdio};
+
+ let mut child = Command::new(greeter_bin())
+ .envs(THEME_ENV.iter().copied())
+ .stdout(Stdio::piped())
+ .spawn()?;
+ let stdout = child.stdout.take().expect("stdout pedido con Stdio::piped");
+ println!("mirada-compositor · greeter lanzado (pid {}).", child.id());
+
+ std::thread::spawn(move || {
+ for line in BufReader::new(stdout).lines().map_while(Result::ok) {
+ match SessionTicket::from_line(&line) {
+ Some(ticket) => {
+ println!("mirada-compositor · tiquet de sesión recibido del greeter.");
+ send(ticket);
+ }
+ None => println!("greeter · {line}"),
+ }
+ }
+ match child.wait() {
+ Ok(status) => println!("mirada-compositor · el greeter terminó ({status})."),
+ Err(e) => eprintln!("mirada-compositor · wait(greeter): {e}"),
+ }
+ });
+ Ok(())
+}
+
/// Carga las reglas de ventana del usuario, o ninguna si no hay archivo.
fn load_user_rules() -> Rules {
match Rules::default_path() {
@@ -792,6 +956,20 @@ fn load_user_rules() -> Rules {
}
}
+/// Arma un Cerebro embebido: un `Desktop` con el keymap del usuario y
+/// sus reglas de ventana. Lo usan tanto el modo autónomo como el modo
+/// greeter (el DM es siempre autónomo — un Cerebro externo no tiene
+/// sentido en la pantalla de login).
+fn embedded_brain(keymap_path: &Option<std::path::PathBuf>) -> Brain {
+ let keymap = match keymap_path {
+ Some(p) => Keymap::load_or_init(p),
+ None => Keymap::default(),
+ };
+ let mut desktop = Desktop::with_keymap(keymap);
+ desktop.set_rules(load_user_rules());
+ Brain::Embedded(desktop)
+}
+
/// Crea y anuncia un `wl_output` (un monitor) en el protocolo Wayland —
/// muchos clientes (`foot` entre ellos) se niegan a arrancar sin uno.
/// Devuelve el [`Output`](smithay::output::Output); hay que mantenerlo
@@ -851,7 +1029,7 @@ struct Setup {
/// Arma el estado del compositor — todo lo independiente del backend
/// gráfico (Wayland, Cerebro, teclado, keymap, control). Cada backend
/// (winit o DRM) registra luego su propia salida y monta su bucle.
-fn build_app() -> Result<Setup, Box<dyn std::error::Error>> {
+fn build_app(greeter: bool) -> Result<Setup, Box<dyn std::error::Error>> {
let display: Display<App> = Display::new()?;
let dh = display.handle();
@@ -867,23 +1045,23 @@ fn build_app() -> Result<Setup, Box<dyn std::error::Error>> {
// el Cerebro embebido; con un Cerebro enlazado, el keymap es asunto suyo.
let keymap_path = Keymap::default_path();
- // Elige el Cerebro: enlazado si `MIRADA_SOCKET` está puesto.
- let brain = match std::env::var("MIRADA_SOCKET") {
- Ok(path) => {
- println!("mirada-compositor · esperando al Cerebro en {path} …");
- let link = BodyLink::listen(&path)?;
- println!("mirada-compositor · Cerebro conectado.");
- Brain::Linked(link)
- }
- Err(_) => {
- println!("mirada-compositor · modo autónomo (Cerebro embebido).");
- let keymap = match &keymap_path {
- Some(p) => Keymap::load_or_init(p),
- None => Keymap::default(),
- };
- let mut desktop = Desktop::with_keymap(keymap);
- desktop.set_rules(load_user_rules());
- Brain::Embedded(desktop)
+ // Elige el Cerebro. El modo greeter (DM) fuerza Cerebro embebido;
+ // si no, enlazado cuando `MIRADA_SOCKET` está puesto, autónomo si no.
+ let brain = if greeter {
+ println!("mirada-compositor · modo greeter (DM) — Cerebro embebido.");
+ embedded_brain(&keymap_path)
+ } else {
+ match std::env::var("MIRADA_SOCKET") {
+ Ok(path) => {
+ println!("mirada-compositor · esperando al Cerebro en {path} …");
+ let link = BodyLink::listen(&path)?;
+ println!("mirada-compositor · Cerebro conectado.");
+ Brain::Linked(link)
+ }
+ Err(_) => {
+ println!("mirada-compositor · modo autónomo (Cerebro embebido).");
+ embedded_brain(&keymap_path)
+ }
}
};
@@ -904,6 +1082,8 @@ fn build_app() -> Result<Setup, Box<dyn std::error::Error>> {
windows: Vec::new(),
body: BodyState::new(),
brain,
+ mode: if greeter { BodyMode::Greeter } else { BodyMode::Session },
+ session_user: None,
grabs: Vec::new(),
pending_keybind: None,
next_id: 1,
@@ -914,16 +1094,23 @@ fn build_app() -> Result<Setup, Box<dyn std::error::Error>> {
app.keyboard = Some(keyboard);
app.pointer = Some(app.seat.add_pointer());
- // En modo embebido, el propio Desktop dicta los atajos a interceptar.
- if let Brain::Embedded(desktop) = &app.brain {
- let grab = desktop.grab_keys();
- app.apply_commands(vec![grab]);
+ // En modo embebido, el propio Desktop dicta los atajos a
+ // interceptar — salvo en modo greeter: en la pantalla de login
+ // todas las teclas van al greeter (que el usuario no pueda lanzar
+ // nada ni cerrar el compositor). Los atajos se registran luego, en
+ // el traspaso a la sesión (`complete_greeter_handoff`).
+ if !greeter {
+ if let Brain::Embedded(desktop) = &app.brain {
+ let grab = desktop.grab_keys();
+ app.apply_commands(vec![grab]);
+ }
}
// Vigilancia del keymap para recargarlo en caliente — sólo tiene
- // sentido con el Cerebro embebido.
+ // sentido con el Cerebro embebido y fuera del modo greeter (donde
+ // no hay atajos registrados que recargar).
let keymap_watch = match (&app.brain, &keymap_path) {
- (Brain::Embedded(_), Some(p)) => Keymap::watch(p).ok(),
+ (Brain::Embedded(_), Some(p)) if !greeter => Keymap::watch(p).ok(),
_ => None,
};
if keymap_watch.is_some() {
@@ -953,14 +1140,14 @@ fn build_app() -> Result<Setup, Box<dyn std::error::Error>> {
}
/// El backend `winit`: corre anidado dentro de una sesión gráfica.
-fn run_winit() -> Result<(), Box<dyn std::error::Error>> {
+fn run_winit(greeter: bool) -> Result<(), Box<dyn std::error::Error>> {
let Setup {
mut display,
app: mut state,
keymap_path,
keymap_watch,
ctl,
- } = build_app()?;
+ } = build_app(greeter)?;
let keyboard = state.keyboard.clone().expect("teclado inicializado");
// El backend gráfico va primero. winit abre la ventana del compositor
@@ -1016,6 +1203,18 @@ fn run_winit() -> Result<(), Box<dyn std::error::Error>> {
state.output_size = (win_size.w, win_size.h);
}
+ // Modo greeter (DM anidado — útil para iterar la UI del login):
+ // lanza el greeter y recibe su tiquet por un canal que el bucle sondea.
+ let greeter_rx = if state.mode == BodyMode::Greeter {
+ let (tx, rx) = std::sync::mpsc::channel::<SessionTicket>();
+ spawn_greeter(move |ticket| {
+ let _ = tx.send(ticket);
+ })?;
+ Some(rx)
+ } else {
+ None
+ };
+
while state.running {
// 1 · Eventos del backend (teclado, redimensión, cierre).
let status = winit.dispatch_new_events(|event| match event {
@@ -1061,7 +1260,14 @@ fn run_winit() -> Result<(), Box<dyn std::error::Error>> {
// 2 · Comandos de un Cerebro enlazado.
state.brain_poll();
- // 2 bis · Recarga del keymap si el archivo cambió en disco.
+ // 2 bis · El tiquet del greeter (modo DM): dispara el traspaso.
+ if let Some(rx) = &greeter_rx {
+ while let Ok(ticket) = rx.try_recv() {
+ state.complete_greeter_handoff(ticket);
+ }
+ }
+
+ // 2 ter · Recarga del keymap si el archivo cambió en disco.
if keymap_watch.as_ref().is_some_and(|w| w.changed()) {
if let Some(path) = &keymap_path {
match Keymap::load(path) {
@@ -1083,7 +1289,7 @@ fn run_winit() -> Result<(), Box<dyn std::error::Error>> {
}
}
- // 2 ter · Peticiones del API de control (mirada-ctl).
+ // 2 quater · Peticiones del API de control (mirada-ctl).
if let Some(ctl) = &ctl {
while let Some(mut conn) = ctl.poll() {
let reply = match conn.read_request() {
@@ -1154,25 +1360,34 @@ fn run_winit() -> Result<(), Box<dyn std::error::Error>> {
}
fn main() {
- let arg = std::env::args().nth(1);
- let result = match arg.as_deref() {
- Some("--drm") => drm_backend::run(),
- Some("--winit") => run_winit(),
- Some(other) => {
- eprintln!("mirada-compositor: opción desconocida «{other}» — usa --drm o --winit");
+ // Banderas en cualquier orden: `--greeter` (modo DM) es ortogonal
+ // al backend (`--winit` anidado · `--drm` nativo · auto si falta).
+ let args: Vec<String> = std::env::args().skip(1).collect();
+ for a in &args {
+ if !matches!(a.as_str(), "--greeter" | "--winit" | "--drm") {
+ eprintln!(
+ "mirada-compositor: opción desconocida «{a}» — usa --greeter, --winit o --drm"
+ );
std::process::exit(2);
}
- None => {
+ }
+ let greeter = args.iter().any(|a| a == "--greeter");
+ let backend = args.iter().find(|a| matches!(a.as_str(), "--winit" | "--drm"));
+
+ let result = match backend.map(String::as_str) {
+ Some("--drm") => drm_backend::run(greeter),
+ Some("--winit") => run_winit(greeter),
+ _ => {
// Auto: con sesión gráfica anfitriona → winit (anidado);
// sin ella (una TTY pelada) → backend DRM.
let nested = std::env::var_os("WAYLAND_DISPLAY").is_some()
|| std::env::var_os("DISPLAY").is_some();
if nested {
println!("mirada-compositor · sesión gráfica detectada → backend winit.");
- run_winit()
+ run_winit(greeter)
} else {
println!("mirada-compositor · sin sesión gráfica → backend DRM.");
- drm_backend::run()
+ drm_backend::run(greeter)
}
}
};
diff --git a/crates/apps/mirada-greeter/README.md b/crates/apps/mirada-greeter/README.md
index fd1330b..ba844de 100644
--- a/crates/apps/mirada-greeter/README.md
+++ b/crates/apps/mirada-greeter/README.md
@@ -35,8 +35,10 @@ anidado dentro de otro escritorio:
MIRADA_GREETER_MOCK=demo:demo cargo run -p mirada-greeter
```
-## Pendiente
+## Integración con el compositor
-El consumo del tiquet en `mirada-compositor` (modo greeter +
-`BodyMode::Session` + arranque de la sesión con setuid) — siguiente
-slice del DM.
+El consumo del tiquet ya está cableado. `mirada-compositor --greeter`
+lanza este greeter, lee su stdout y, al recibir el `SessionTicket`,
+muta de `BodyMode::Greeter` a `BodyMode::Session` y arranca la sesión
+del usuario con `setuid`/`setgid` — sin reiniciar el servidor Wayland.
+Ver el README de `mirada-compositor`, sección **Modo greeter (DM)**.