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>
This commit is contained in:
sergio
2026-05-22 00:06:59 +00:00
parent 634a43006a
commit 758f61f52a
6 changed files with 352 additions and 69 deletions
+2
View File
@@ -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"
+32
View File
@@ -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
@@ -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.
+268 -53
View File
@@ -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)
}
}
};
+6 -4
View File
@@ -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)**.