758f61f52a
`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>
1399 lines
53 KiB
Rust
1399 lines
53 KiB
Rust
//! `mirada-compositor` — el Cuerpo del compositor carmen.
|
|
//!
|
|
//! Un compositor Wayland teselante real, sobre `smithay`, con backend
|
|
//! `winit`: corre **anidado** como una ventana dentro de tu sesión
|
|
//! gráfica actual (X11 o Wayland). Habla el protocolo Wayland con los
|
|
//! clientes, compone sus superficies y aplica la geometría que decide el
|
|
//! Cerebro.
|
|
//!
|
|
//! Dos modos:
|
|
//!
|
|
//! - **Autónomo** (por defecto): lleva un [`Desktop`] embebido — es un
|
|
//! compositor teselante completo en un solo proceso. Lánzalo y abre
|
|
//! clientes; el teclado (`Super+…`) maneja el escritorio.
|
|
//! - **Enlazado** (`MIRADA_SOCKET=/ruta`): el Cuerpo escucha ahí y la
|
|
//! app `mirada` (el Cerebro GPUI) se conecta; la geometría viaja por
|
|
//! [`mirada_link`].
|
|
//!
|
|
//! Cómo probarlo en un Linux real: ver `crates/apps/mirada-compositor/README.md`.
|
|
|
|
use std::sync::Arc;
|
|
use std::time::Instant;
|
|
|
|
use smithay::backend::allocator::dmabuf::Dmabuf;
|
|
use smithay::backend::input::{InputEvent, KeyState, KeyboardKeyEvent};
|
|
use smithay::backend::renderer::element::surface::{
|
|
render_elements_from_surface_tree, WaylandSurfaceRenderElement,
|
|
};
|
|
use smithay::backend::renderer::element::solid::SolidColorBuffer;
|
|
use smithay::backend::renderer::element::Kind;
|
|
use smithay::backend::renderer::gles::GlesRenderer;
|
|
use smithay::backend::renderer::utils::{
|
|
draw_render_elements, on_commit_buffer_handler, with_renderer_surface_state,
|
|
};
|
|
use smithay::backend::renderer::{Color32F, Frame, ImportDma, Renderer};
|
|
use smithay::backend::winit::{self, WinitEvent};
|
|
use smithay::input::keyboard::{xkb, FilterResult, KeyboardHandle, Keysym, ModifiersState};
|
|
use smithay::input::pointer::{CursorImageStatus, CursorImageSurfaceData, PointerHandle};
|
|
use smithay::input::{Seat, SeatHandler, SeatState};
|
|
use smithay::reexports::wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode as DecorationMode;
|
|
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
|
|
use smithay::reexports::wayland_server::backend::{ClientData, ClientId, DisconnectReason};
|
|
use smithay::reexports::wayland_server::protocol::wl_buffer;
|
|
use smithay::reexports::wayland_server::protocol::wl_output;
|
|
use smithay::reexports::wayland_server::protocol::wl_seat;
|
|
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
|
use smithay::reexports::wayland_server::{Client, Display, DisplayHandle, ListeningSocket};
|
|
use smithay::reexports::winit::platform::pump_events::PumpStatus;
|
|
use smithay::utils::{Rectangle, SERIAL_COUNTER};
|
|
use smithay::utils::{Serial, Transform};
|
|
use smithay::wayland::buffer::BufferHandler;
|
|
use smithay::wayland::dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier};
|
|
use smithay::wayland::compositor::{
|
|
with_states, with_surface_tree_downward, CompositorClientState, CompositorHandler,
|
|
CompositorState, SurfaceAttributes, TraversalAction,
|
|
};
|
|
use smithay::wayland::selection::data_device::{
|
|
ClientDndGrabHandler, DataDeviceHandler, DataDeviceState, ServerDndGrabHandler,
|
|
};
|
|
use smithay::wayland::selection::SelectionHandler;
|
|
use smithay::wayland::shell::xdg::decoration::{XdgDecorationHandler, XdgDecorationState};
|
|
use smithay::wayland::shell::xdg::{
|
|
PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState,
|
|
XdgToplevelSurfaceData,
|
|
};
|
|
use smithay::wayland::output::OutputHandler;
|
|
use smithay::wayland::shm::{ShmHandler, ShmState};
|
|
use smithay::{
|
|
delegate_compositor, delegate_data_device, delegate_dmabuf, delegate_output, delegate_seat,
|
|
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,
|
|
};
|
|
use mirada_link::BodyLink;
|
|
|
|
mod drm_backend;
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Estado
|
|
// ---------------------------------------------------------------------
|
|
|
|
/// De dónde salen las decisiones de geometría.
|
|
enum Brain {
|
|
/// El compositor lleva su propio `Desktop` — proceso único.
|
|
Embedded(Desktop),
|
|
/// Un Cerebro externo (la app `mirada`) por socket.
|
|
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";
|
|
|
|
/// Alto en píxeles de la franja del shell, al pie de la salida.
|
|
const SHELL_DOCK_HEIGHT: i32 = 40;
|
|
|
|
/// Una ventana de cliente que el compositor gestiona.
|
|
struct ManagedWindow {
|
|
id: u64,
|
|
toplevel: ToplevelSurface,
|
|
surface: WlSurface,
|
|
/// Esquina superior-izquierda de la celda asignada, según el Cerebro.
|
|
loc: (i32, i32),
|
|
/// Tamaño de la celda asignada — para centrar la ventana si el
|
|
/// cliente presenta una superficie más pequeña.
|
|
size: (i32, i32),
|
|
visible: bool,
|
|
/// `true` si flota: se compone por encima de las teseladas.
|
|
floating: bool,
|
|
/// `true` si tiene el foco del teclado — pinta el marco resaltado.
|
|
focused: bool,
|
|
/// `true` si es la ventana del shell — acoplada al pie, sin teselar.
|
|
is_shell: bool,
|
|
/// Búferes de los 4 lados del marco (arriba, abajo, izq., der.) —
|
|
/// cada uno con su `Id` estable para el seguimiento de daño.
|
|
borders: [SolidColorBuffer; 4],
|
|
}
|
|
|
|
/// Un arrastre de ratón en curso: mueve o redimensiona una ventana.
|
|
struct DragGrab {
|
|
/// La ventana que se arrastra.
|
|
id: u64,
|
|
/// Mover (`Super`+botón izquierdo) o redimensionar (`Super`+derecho).
|
|
mode: DragMode,
|
|
/// Posición del puntero al empezar el arrastre.
|
|
start_pointer: (f64, f64),
|
|
/// Rectángulo `(x, y, w, h)` de la ventana al empezar.
|
|
start_rect: (i32, i32, i32, i32),
|
|
}
|
|
|
|
/// Qué le hace un arrastre a la ventana.
|
|
#[derive(Clone, Copy)]
|
|
enum DragMode {
|
|
/// Reubicar la ventana — la esquina la sigue al puntero.
|
|
Move,
|
|
/// Redimensionarla — la esquina inferior-derecha sigue al puntero.
|
|
Resize,
|
|
}
|
|
|
|
/// El estado global del compositor.
|
|
struct App {
|
|
compositor_state: CompositorState,
|
|
xdg_shell_state: XdgShellState,
|
|
shm_state: ShmState,
|
|
/// Estado de `zwp_linux_dmabuf` — deja que los clientes con GPU
|
|
/// (apps GPUI, navegadores acelerados) compartan búferes de vídeo.
|
|
dmabuf_state: DmabufState,
|
|
seat_state: SeatState<Self>,
|
|
data_device_state: DataDeviceState,
|
|
seat: Seat<Self>,
|
|
keyboard: Option<KeyboardHandle<Self>>,
|
|
pointer: Option<PointerHandle<Self>>,
|
|
/// Posición del puntero en coordenadas globales.
|
|
pointer_loc: (f64, f64),
|
|
/// Qué cursor pide el cliente enfocado — una superficie suya, un
|
|
/// cursor con nombre, u oculto. El backend lo pinta en consecuencia.
|
|
cursor_status: CursorImageStatus,
|
|
/// Arrastre de ventana en curso (mover o redimensionar con el ratón).
|
|
drag: Option<DragGrab>,
|
|
/// Tamaño real de la salida (con la franja del shell incluida) — lo
|
|
/// fija el backend; sirve para acoplar la ventana del shell.
|
|
output_size: (i32, i32),
|
|
|
|
/// Ventanas gestionadas, en orden de aparición.
|
|
windows: Vec<ManagedWindow>,
|
|
/// La contabilidad del Cuerpo (mirada-body).
|
|
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.
|
|
pending_keybind: Option<String>,
|
|
next_id: u64,
|
|
running: bool,
|
|
}
|
|
|
|
impl App {
|
|
/// Inyecta un evento del Cuerpo en el Cerebro y aplica su respuesta.
|
|
fn brain_feed(&mut self, event: BodyEvent) {
|
|
let cmds = match &mut self.brain {
|
|
Brain::Embedded(desktop) => desktop.on_event(event),
|
|
Brain::Linked(link) => {
|
|
let _ = link.send(&event);
|
|
Vec::new()
|
|
}
|
|
};
|
|
self.apply_commands(cmds);
|
|
}
|
|
|
|
/// Drena los comandos de un Cerebro enlazado (no hace nada si es embebido).
|
|
fn brain_poll(&mut self) {
|
|
let cmds = match &self.brain {
|
|
Brain::Linked(link) => link.drain(),
|
|
Brain::Embedded(_) => Vec::new(),
|
|
};
|
|
if !cmds.is_empty() {
|
|
self.apply_commands(cmds);
|
|
}
|
|
}
|
|
|
|
/// Atiende una petición del API de control (`mirada-ctl`).
|
|
fn serve_ctl(&mut self, req: CtlRequest) -> CtlReply {
|
|
match req {
|
|
CtlRequest::Do(action) => {
|
|
let cmds = match &mut self.brain {
|
|
Brain::Embedded(d) => Some(d.apply(action)),
|
|
Brain::Linked(_) => None,
|
|
};
|
|
match cmds {
|
|
Some(cmds) => {
|
|
self.apply_commands(cmds);
|
|
CtlReply::Ok
|
|
}
|
|
None => CtlReply::Error(
|
|
"el Cerebro es externo; usa mirada-ctl contra la app mirada".into(),
|
|
),
|
|
}
|
|
}
|
|
CtlRequest::ListWindows => match &self.brain {
|
|
Brain::Embedded(d) => CtlReply::Windows(d.window_lines()),
|
|
Brain::Linked(_) => CtlReply::Error("el Cerebro es externo".into()),
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Traduce los comandos del Cerebro a operaciones y las ejecuta.
|
|
fn apply_commands(&mut self, cmds: Vec<BrainCommand>) {
|
|
for cmd in cmds {
|
|
let ops = self.body.apply(cmd);
|
|
for op in ops {
|
|
self.exec_op(op);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Ejecuta una operación concreta sobre las superficies reales.
|
|
fn exec_op(&mut self, op: BodyOp) {
|
|
match op {
|
|
BodyOp::Configure { id, rect, visible, floating, fullscreen } => {
|
|
if let Some(w) = self.windows.iter_mut().find(|w| w.id == id) {
|
|
w.loc = (rect.x, rect.y);
|
|
w.size = (rect.w, rect.h);
|
|
w.visible = visible;
|
|
w.floating = floating;
|
|
w.toplevel.with_pending_state(|s| {
|
|
s.size = Some((rect.w.max(1), rect.h.max(1)).into());
|
|
if fullscreen {
|
|
s.states.set(xdg_toplevel::State::Fullscreen);
|
|
} else {
|
|
s.states.unset(xdg_toplevel::State::Fullscreen);
|
|
}
|
|
});
|
|
w.toplevel.send_pending_configure();
|
|
}
|
|
}
|
|
BodyOp::Focus(id) => {
|
|
let mut target = None;
|
|
for w in &mut self.windows {
|
|
let active = w.id == id;
|
|
w.focused = active;
|
|
if active {
|
|
target = Some(w.surface.clone());
|
|
}
|
|
w.toplevel.with_pending_state(|s| {
|
|
if active {
|
|
s.states.set(xdg_toplevel::State::Activated);
|
|
} else {
|
|
s.states.unset(xdg_toplevel::State::Activated);
|
|
}
|
|
});
|
|
w.toplevel.send_pending_configure();
|
|
}
|
|
if let Some(kb) = self.keyboard.clone() {
|
|
kb.set_focus(self, target, SERIAL_COUNTER.next_serial());
|
|
}
|
|
}
|
|
BodyOp::Unfocus => {
|
|
for w in &mut self.windows {
|
|
w.focused = false;
|
|
}
|
|
if let Some(kb) = self.keyboard.clone() {
|
|
kb.set_focus(self, Option::<WlSurface>::None, SERIAL_COUNTER.next_serial());
|
|
}
|
|
}
|
|
BodyOp::CloseClient(id) | BodyOp::KillClient(id) => {
|
|
if let Some(w) = self.windows.iter().find(|w| w.id == id) {
|
|
w.toplevel.send_close();
|
|
}
|
|
}
|
|
BodyOp::SetGrabs(keys) => self.grabs = keys,
|
|
BodyOp::SetCursor(_) => {}
|
|
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,
|
|
}
|
|
}
|
|
|
|
/// Registra un toplevel recién creado y avisa al Cerebro.
|
|
fn register_toplevel(&mut self, toplevel: ToplevelSurface) {
|
|
let surface = toplevel.wl_surface().clone();
|
|
let id = self.next_id;
|
|
self.next_id += 1;
|
|
|
|
let (app_id, title) = with_states(&surface, |states| {
|
|
states
|
|
.data_map
|
|
.get::<XdgToplevelSurfaceData>()
|
|
.and_then(|d| d.lock().ok())
|
|
.map(|d| {
|
|
(
|
|
d.app_id.clone().unwrap_or_default(),
|
|
d.title.clone().unwrap_or_default(),
|
|
)
|
|
})
|
|
.unwrap_or_default()
|
|
});
|
|
// La ventana del shell no se tesela: carmen la acopla al pie.
|
|
let is_shell = app_id == SHELL_APP_ID;
|
|
|
|
self.windows.push(ManagedWindow {
|
|
id,
|
|
toplevel,
|
|
surface,
|
|
loc: (0, 0),
|
|
size: (0, 0),
|
|
visible: false,
|
|
floating: false,
|
|
focused: false,
|
|
is_shell,
|
|
borders: std::array::from_fn(|_| SolidColorBuffer::default()),
|
|
});
|
|
|
|
if is_shell {
|
|
self.dock_shell();
|
|
} else {
|
|
let app_id = if app_id.is_empty() { "cliente".into() } else { app_id };
|
|
let title = if title.is_empty() { format!("ventana {id}") } else { title };
|
|
let ev = self.body.open_surface(id, app_id, title);
|
|
self.brain_feed(ev);
|
|
}
|
|
}
|
|
|
|
/// Acopla la ventana del shell: le reserva una franja al pie de la
|
|
/// salida —el Cerebro tesela el área que queda— y la dimensiona y
|
|
/// coloca ahí. Se llama al registrarla y al cambiar el tamaño de la
|
|
/// salida.
|
|
fn dock_shell(&mut self) {
|
|
let (ow, oh) = self.output_size;
|
|
if ow == 0 || oh == 0 {
|
|
return; // la salida todavía no está lista
|
|
}
|
|
// Reserva la franja: el Cerebro tesela en el alto que queda.
|
|
let ev = self.body.resize_output(0, ow, oh - SHELL_DOCK_HEIGHT);
|
|
self.brain_feed(ev);
|
|
// Dimensiona la ventana del shell y la fija en la franja.
|
|
if let Some(w) = self.windows.iter_mut().find(|w| w.is_shell) {
|
|
w.loc = (0, oh - SHELL_DOCK_HEIGHT);
|
|
w.size = (ow, SHELL_DOCK_HEIGHT);
|
|
w.visible = true;
|
|
w.toplevel.with_pending_state(|s| {
|
|
s.size = Some((ow.max(1), SHELL_DOCK_HEIGHT.max(1)).into());
|
|
});
|
|
w.toplevel.send_pending_configure();
|
|
}
|
|
}
|
|
|
|
/// El backend informa de un tamaño de salida nuevo (arranque o
|
|
/// redimensión). Si hay shell acoplado, recoloca su franja; si no,
|
|
/// le pasa el área entera al Cerebro.
|
|
fn output_changed(&mut self, width: i32, height: i32) {
|
|
self.output_size = (width, height);
|
|
if self.windows.iter().any(|w| w.is_shell) {
|
|
self.dock_shell();
|
|
} else {
|
|
let ev = self.body.resize_output(0, width, height);
|
|
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());
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Handlers de protocolo
|
|
// ---------------------------------------------------------------------
|
|
|
|
impl CompositorHandler for App {
|
|
fn compositor_state(&mut self) -> &mut CompositorState {
|
|
&mut self.compositor_state
|
|
}
|
|
|
|
fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState {
|
|
&client.get_data::<ClientState>().unwrap().compositor_state
|
|
}
|
|
|
|
fn commit(&mut self, surface: &WlSurface) {
|
|
on_commit_buffer_handler::<Self>(surface);
|
|
}
|
|
}
|
|
|
|
impl BufferHandler for App {
|
|
fn buffer_destroyed(&mut self, _buffer: &wl_buffer::WlBuffer) {}
|
|
}
|
|
|
|
impl DmabufHandler for App {
|
|
fn dmabuf_state(&mut self) -> &mut DmabufState {
|
|
&mut self.dmabuf_state
|
|
}
|
|
|
|
/// Un cliente importó un DMA-BUF. El `GlesRenderer` lo importará de
|
|
/// verdad al componer; aquí basta con aceptarlo — un búfer inválido
|
|
/// sólo dejará en blanco ese cuadro de esa ventana.
|
|
fn dmabuf_imported(
|
|
&mut self,
|
|
_global: &DmabufGlobal,
|
|
_dmabuf: Dmabuf,
|
|
notifier: ImportNotifier,
|
|
) {
|
|
let _ = notifier.successful::<App>();
|
|
}
|
|
}
|
|
|
|
impl ShmHandler for App {
|
|
fn shm_state(&self) -> &ShmState {
|
|
&self.shm_state
|
|
}
|
|
}
|
|
|
|
impl XdgShellHandler for App {
|
|
fn xdg_shell_state(&mut self) -> &mut XdgShellState {
|
|
&mut self.xdg_shell_state
|
|
}
|
|
|
|
fn new_toplevel(&mut self, surface: ToplevelSurface) {
|
|
surface.with_pending_state(|s| {
|
|
s.states.set(xdg_toplevel::State::Activated);
|
|
});
|
|
surface.send_configure();
|
|
self.register_toplevel(surface);
|
|
}
|
|
|
|
fn toplevel_destroyed(&mut self, surface: ToplevelSurface) {
|
|
let pos = self
|
|
.windows
|
|
.iter()
|
|
.position(|w| w.surface == *surface.wl_surface());
|
|
if let Some(pos) = pos {
|
|
let w = self.windows.remove(pos);
|
|
if w.is_shell {
|
|
// El shell se cerró: libera su franja, el Cerebro vuelve
|
|
// a teselar en la salida entera.
|
|
let (ow, oh) = self.output_size;
|
|
if ow != 0 && oh != 0 {
|
|
let ev = self.body.resize_output(0, ow, oh);
|
|
self.brain_feed(ev);
|
|
}
|
|
} else if let Some(ev) = self.body.close_surface(w.id) {
|
|
self.brain_feed(ev);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn title_changed(&mut self, surface: ToplevelSurface) {
|
|
let id = self
|
|
.windows
|
|
.iter()
|
|
.find(|w| w.surface == *surface.wl_surface())
|
|
.map(|w| w.id);
|
|
let Some(id) = id else { return };
|
|
let title = with_states(surface.wl_surface(), |states| {
|
|
states
|
|
.data_map
|
|
.get::<XdgToplevelSurfaceData>()
|
|
.and_then(|d| d.lock().ok())
|
|
.and_then(|d| d.title.clone())
|
|
.unwrap_or_default()
|
|
});
|
|
if let Some(ev) = self.body.retitle_surface(id, title) {
|
|
self.brain_feed(ev);
|
|
}
|
|
}
|
|
|
|
fn fullscreen_request(
|
|
&mut self,
|
|
surface: ToplevelSurface,
|
|
_output: Option<wl_output::WlOutput>,
|
|
) {
|
|
let id = self
|
|
.windows
|
|
.iter()
|
|
.find(|w| w.surface == *surface.wl_surface())
|
|
.map(|w| w.id);
|
|
if let Some(id) = id {
|
|
self.brain_feed(BodyEvent::FullscreenRequest { id, fullscreen: true });
|
|
}
|
|
}
|
|
|
|
fn unfullscreen_request(&mut self, surface: ToplevelSurface) {
|
|
let id = self
|
|
.windows
|
|
.iter()
|
|
.find(|w| w.surface == *surface.wl_surface())
|
|
.map(|w| w.id);
|
|
if let Some(id) = id {
|
|
self.brain_feed(BodyEvent::FullscreenRequest { id, fullscreen: false });
|
|
}
|
|
}
|
|
|
|
fn new_popup(&mut self, surface: PopupSurface, _positioner: PositionerState) {
|
|
let _ = surface.send_configure();
|
|
}
|
|
|
|
fn grab(&mut self, _surface: PopupSurface, _seat: wl_seat::WlSeat, _serial: Serial) {}
|
|
|
|
fn reposition_request(
|
|
&mut self,
|
|
_surface: PopupSurface,
|
|
_positioner: PositionerState,
|
|
_token: u32,
|
|
) {
|
|
}
|
|
}
|
|
|
|
/// Decoración de ventana: carmen tesela, así que las ventanas no llevan
|
|
/// barra de título. Le decimos a todo cliente que la decoración la pone
|
|
/// el servidor (`ServerSide`) — y como el servidor no dibuja ninguna, la
|
|
/// ventana queda sin marco. Sin esto, clientes como `foot` se dibujan su
|
|
/// propia barra (CSD), que estorba en un escritorio teselante.
|
|
impl XdgDecorationHandler for App {
|
|
fn new_decoration(&mut self, toplevel: ToplevelSurface) {
|
|
toplevel.with_pending_state(|s| s.decoration_mode = Some(DecorationMode::ServerSide));
|
|
toplevel.send_configure();
|
|
}
|
|
|
|
fn request_mode(&mut self, toplevel: ToplevelSurface, _mode: DecorationMode) {
|
|
toplevel.with_pending_state(|s| s.decoration_mode = Some(DecorationMode::ServerSide));
|
|
toplevel.send_configure();
|
|
}
|
|
|
|
fn unset_mode(&mut self, toplevel: ToplevelSurface) {
|
|
toplevel.with_pending_state(|s| s.decoration_mode = Some(DecorationMode::ServerSide));
|
|
toplevel.send_configure();
|
|
}
|
|
}
|
|
|
|
impl SelectionHandler for App {
|
|
type SelectionUserData = ();
|
|
}
|
|
|
|
impl DataDeviceHandler for App {
|
|
fn data_device_state(&self) -> &DataDeviceState {
|
|
&self.data_device_state
|
|
}
|
|
}
|
|
impl ClientDndGrabHandler for App {}
|
|
impl ServerDndGrabHandler for App {
|
|
fn send(&mut self, _mime_type: String, _fd: std::os::unix::io::OwnedFd, _seat: Seat<Self>) {}
|
|
}
|
|
|
|
impl SeatHandler for App {
|
|
type KeyboardFocus = WlSurface;
|
|
type PointerFocus = WlSurface;
|
|
type TouchFocus = WlSurface;
|
|
|
|
fn seat_state(&mut self) -> &mut SeatState<Self> {
|
|
&mut self.seat_state
|
|
}
|
|
|
|
fn focus_changed(&mut self, _seat: &Seat<Self>, _focused: Option<&WlSurface>) {}
|
|
|
|
/// El cliente enfocado pidió un cursor — guardamos su petición; el
|
|
/// backend la pinta (su superficie, o el cuadrado si es con nombre).
|
|
fn cursor_image(&mut self, _seat: &Seat<Self>, image: CursorImageStatus) {
|
|
self.cursor_status = image;
|
|
}
|
|
}
|
|
|
|
/// El protocolo `wl_output` no necesita estado propio — basta con
|
|
/// anunciar el global para que los clientes vean que hay un monitor.
|
|
impl OutputHandler for App {}
|
|
|
|
delegate_compositor!(App);
|
|
delegate_xdg_shell!(App);
|
|
delegate_xdg_decoration!(App);
|
|
delegate_dmabuf!(App);
|
|
delegate_shm!(App);
|
|
delegate_seat!(App);
|
|
delegate_data_device!(App);
|
|
delegate_output!(App);
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Datos por cliente
|
|
// ---------------------------------------------------------------------
|
|
|
|
#[derive(Default)]
|
|
struct ClientState {
|
|
compositor_state: CompositorClientState,
|
|
}
|
|
impl ClientData for ClientState {
|
|
fn initialized(&self, _id: ClientId) {}
|
|
fn disconnected(&self, _id: ClientId, _reason: DisconnectReason) {}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Utilidades
|
|
// ---------------------------------------------------------------------
|
|
|
|
/// Construye la cadena de un atajo (`"Super+Shift+j"`) desde el estado de
|
|
/// modificadores y el keysym, con el mismo formato que el mapa de teclas
|
|
/// de [`mirada_brain`]. `None` si no es una tecla mapeable.
|
|
fn combo_string(mods: &ModifiersState, sym: Keysym) -> Option<String> {
|
|
let utf = xkb::keysym_to_utf8(sym);
|
|
let key = utf.trim_end_matches('\0');
|
|
let name = if key == " " {
|
|
"space".to_string()
|
|
} else {
|
|
// ¿Es un único carácter imprimible? Entonces la tecla es ese carácter.
|
|
let mut chars = key.chars();
|
|
match (chars.next(), chars.next()) {
|
|
(Some(c), None) if c.is_ascii_graphic() => c.to_ascii_lowercase().to_string(),
|
|
// Si no, una tecla con nombre: Return, Tab, Up, F5…
|
|
_ => named_key(sym)?,
|
|
}
|
|
};
|
|
let mut combo = String::new();
|
|
if mods.logo {
|
|
combo.push_str("Super+");
|
|
}
|
|
if mods.ctrl {
|
|
combo.push_str("Ctrl+");
|
|
}
|
|
if mods.shift {
|
|
combo.push_str("Shift+");
|
|
}
|
|
if mods.alt {
|
|
combo.push_str("Alt+");
|
|
}
|
|
combo.push_str(&name);
|
|
Some(combo)
|
|
}
|
|
|
|
/// El nombre canónico de una tecla especial — `Return`, `Tab`, `Up`,
|
|
/// `F5`… `None` si xkb no le da un nombre razonable.
|
|
fn named_key(sym: Keysym) -> Option<String> {
|
|
let name = xkb::keysym_get_name(sym);
|
|
if name.is_empty() || name == "NoSymbol" || name.starts_with("0x") {
|
|
None
|
|
} else {
|
|
Some(name)
|
|
}
|
|
}
|
|
|
|
/// Despacha los callbacks de frame de un árbol de superficies: avisa a
|
|
/// cada cliente de que puede dibujar el siguiente cuadro.
|
|
fn send_frames_surface_tree(surface: &WlSurface, time: u32) {
|
|
with_surface_tree_downward(
|
|
surface,
|
|
(),
|
|
|_, _, &()| TraversalAction::DoChildren(()),
|
|
|_surf, states, &()| {
|
|
for callback in states
|
|
.cached_state
|
|
.get::<SurfaceAttributes>()
|
|
.current()
|
|
.frame_callbacks
|
|
.drain(..)
|
|
{
|
|
callback.done(time);
|
|
}
|
|
},
|
|
|_, _, &()| true,
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Bucle principal
|
|
// ---------------------------------------------------------------------
|
|
|
|
/// Dónde pintar una ventana. La del shell se ancla al pie de la salida
|
|
/// y crece hacia arriba (su cajón de resultados se despliega sobre las
|
|
/// ventanas). Una ventana normal va en su celda; si el cliente presenta
|
|
/// una superficie más pequeña que la celda (p. ej. un terminal que
|
|
/// redondea su tamaño a celdas de texto), se centra en el hueco.
|
|
fn render_loc(w: &ManagedWindow, output_h: i32) -> (i32, i32) {
|
|
if w.is_shell {
|
|
let h = surface_px_size(w).map(|(_, h)| h).unwrap_or(SHELL_DOCK_HEIGHT);
|
|
return (0, output_h - h);
|
|
}
|
|
match with_renderer_surface_state(&w.surface, |s| s.surface_size()) {
|
|
Some(Some(size)) => {
|
|
let dx = ((w.size.0 - size.w) / 2).max(0);
|
|
let dy = ((w.size.1 - size.h) / 2).max(0);
|
|
(w.loc.0 + dx, w.loc.1 + dy)
|
|
}
|
|
_ => w.loc,
|
|
}
|
|
}
|
|
|
|
/// El tamaño en píxeles de la superficie de una ventana, si el cliente
|
|
/// ya presentó un buffer. `None` mientras no haya dibujado nada — la usa
|
|
/// el backend DRM para acertar el rectángulo en el test de impacto del
|
|
/// puntero.
|
|
fn surface_px_size(w: &ManagedWindow) -> Option<(i32, i32)> {
|
|
with_renderer_surface_state(&w.surface, |s| s.surface_size())
|
|
.flatten()
|
|
.map(|s| (s.w, s.h))
|
|
}
|
|
|
|
/// El punto caliente (hotspot) de una superficie de cursor: el píxel de
|
|
/// la imagen que debe quedar bajo la posición real del puntero. `(0, 0)`
|
|
/// si el cliente no lo declaró.
|
|
fn cursor_hotspot(surface: &WlSurface) -> (i32, i32) {
|
|
with_states(surface, |states| {
|
|
states
|
|
.data_map
|
|
.get::<CursorImageSurfaceData>()
|
|
.map(|m| {
|
|
let h = m.lock().unwrap().hotspot;
|
|
(h.x, h.y)
|
|
})
|
|
.unwrap_or((0, 0))
|
|
})
|
|
}
|
|
|
|
/// Variables de entorno de tema que el compositor inyecta a cada hijo,
|
|
/// para uniformizar GTK y Qt:
|
|
/// - `XDG_CURRENT_DESKTOP=mirada` hace que `xdg-desktop-portal` enrute
|
|
/// hacia `mirada-portal` (el backend de `org.freedesktop.appearance`).
|
|
/// - `QT_QPA_PLATFORMTHEME=gtk3` hace que las apps Qt sigan el tema GTK,
|
|
/// y por tanto el `gtk.css` que genera `nahual-theme`.
|
|
const THEME_ENV: &[(&str, &str)] = &[
|
|
("XDG_CURRENT_DESKTOP", "mirada"),
|
|
("QT_QPA_PLATFORMTHEME", "gtk3"),
|
|
];
|
|
|
|
/// Lanza un comando como proceso hijo, vía `sh -c`. El hijo hereda el
|
|
/// entorno —`WAYLAND_DISPLAY` incluido—, así que el cliente que abra se
|
|
/// 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.
|
|
///
|
|
/// `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;
|
|
}
|
|
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}"),
|
|
}
|
|
}
|
|
|
|
/// 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 (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 {
|
|
return; // no hay archivo de autoarranque
|
|
};
|
|
let mut n = 0;
|
|
for line in text.lines() {
|
|
let line = line.trim();
|
|
if line.is_empty() || line.starts_with('#') {
|
|
continue;
|
|
}
|
|
spawn_command(line, as_user);
|
|
n += 1;
|
|
}
|
|
if n > 0 {
|
|
println!("mirada-compositor · autoarranque: {n} programa(s) desde {}", path.display());
|
|
}
|
|
}
|
|
|
|
/// 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() {
|
|
Some(p) => Rules::load_or_default(&p),
|
|
None => Rules::default(),
|
|
}
|
|
}
|
|
|
|
/// 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
|
|
/// vivo mientras el compositor corra.
|
|
fn announce_output(
|
|
dh: &DisplayHandle,
|
|
name: &str,
|
|
width: i32,
|
|
height: i32,
|
|
refresh_mhz: i32,
|
|
) -> smithay::output::Output {
|
|
use smithay::output::{Mode, Output, PhysicalProperties, Scale, Subpixel};
|
|
let output = Output::new(
|
|
name.to_string(),
|
|
PhysicalProperties {
|
|
size: (0, 0).into(),
|
|
subpixel: Subpixel::Unknown,
|
|
make: "mirada".into(),
|
|
model: name.to_string(),
|
|
},
|
|
);
|
|
output.create_global::<App>(dh);
|
|
let mode = Mode { size: (width, height).into(), refresh: refresh_mhz };
|
|
output.change_current_state(
|
|
Some(mode),
|
|
Some(Transform::Normal),
|
|
Some(Scale::Integer(1)),
|
|
Some((0, 0).into()),
|
|
);
|
|
output.set_preferred(mode);
|
|
output
|
|
}
|
|
|
|
/// Anuncia el global `zwp_linux_dmabuf` con los formatos que el
|
|
/// `GlesRenderer` admite. Hay que llamarlo una vez creado el renderer
|
|
/// (no antes: los formatos salen de él) — así las apps que pintan por
|
|
/// GPU (GPUI, navegadores acelerados) pueden ser clientes del compositor.
|
|
fn announce_dmabuf(app: &mut App, dh: &DisplayHandle, renderer: &GlesRenderer) {
|
|
let formats: Vec<_> = renderer.dmabuf_formats().into_iter().collect();
|
|
println!(
|
|
"mirada-compositor · dmabuf: {} formato(s) anunciado(s).",
|
|
formats.len()
|
|
);
|
|
app.dmabuf_state.create_global::<App>(dh, formats);
|
|
}
|
|
|
|
/// Lo que comparten los dos backends gráficos: el `Display` de Wayland,
|
|
/// el `App` ya armado y la maquinaria de keymap y control.
|
|
struct Setup {
|
|
display: Display<App>,
|
|
app: App,
|
|
keymap_path: Option<std::path::PathBuf>,
|
|
keymap_watch: Option<mirada_brain::KeymapWatch>,
|
|
ctl: Option<CtlServer>,
|
|
}
|
|
|
|
/// 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(greeter: bool) -> Result<Setup, Box<dyn std::error::Error>> {
|
|
let display: Display<App> = Display::new()?;
|
|
let dh = display.handle();
|
|
|
|
let mut seat_state = SeatState::new();
|
|
let seat = seat_state.new_wl_seat(&dh, "mirada");
|
|
|
|
// Anuncia el gestor de decoración: las ventanas van sin marco (ver
|
|
// `XdgDecorationHandler`). El `XdgDecorationState` sólo serviría para
|
|
// retirar el global más tarde, cosa que nunca hacemos.
|
|
let _ = XdgDecorationState::new::<App>(&dh);
|
|
|
|
// El keymap del usuario (`~/.config/mirada/keymap.ron`). Sólo lo usa
|
|
// el Cerebro embebido; con un Cerebro enlazado, el keymap es asunto suyo.
|
|
let keymap_path = Keymap::default_path();
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
};
|
|
|
|
let mut app = App {
|
|
compositor_state: CompositorState::new::<App>(&dh),
|
|
xdg_shell_state: XdgShellState::new::<App>(&dh),
|
|
shm_state: ShmState::new::<App>(&dh, Vec::new()),
|
|
dmabuf_state: DmabufState::new(),
|
|
seat_state,
|
|
data_device_state: DataDeviceState::new::<App>(&dh),
|
|
seat,
|
|
keyboard: None,
|
|
pointer: None,
|
|
pointer_loc: (0.0, 0.0),
|
|
cursor_status: CursorImageStatus::default_named(),
|
|
drag: None,
|
|
output_size: (0, 0),
|
|
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,
|
|
running: true,
|
|
};
|
|
|
|
let keyboard = app.seat.add_keyboard(Default::default(), 200, 25)?;
|
|
app.keyboard = Some(keyboard);
|
|
app.pointer = Some(app.seat.add_pointer());
|
|
|
|
// 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 y fuera del modo greeter (donde
|
|
// no hay atajos registrados que recargar).
|
|
let keymap_watch = match (&app.brain, &keymap_path) {
|
|
(Brain::Embedded(_), Some(p)) if !greeter => Keymap::watch(p).ok(),
|
|
_ => None,
|
|
};
|
|
if keymap_watch.is_some() {
|
|
println!("mirada-compositor · vigilando el keymap (recarga en caliente).");
|
|
}
|
|
|
|
// API de control (mirada-ctl) — sólo con el Cerebro embebido; si es
|
|
// externo, el socket de control lo abre él.
|
|
let ctl = match &app.brain {
|
|
Brain::Embedded(_) => {
|
|
let path = mirada_brain::ctl::default_socket_path();
|
|
match CtlServer::bind(&path) {
|
|
Ok(s) => {
|
|
println!("mirada-compositor · API de control en {}", path.display());
|
|
Some(s)
|
|
}
|
|
Err(e) => {
|
|
eprintln!("mirada-compositor · sin API de control: {e}");
|
|
None
|
|
}
|
|
}
|
|
}
|
|
Brain::Linked(_) => None,
|
|
};
|
|
|
|
Ok(Setup { display, app, keymap_path, keymap_watch, ctl })
|
|
}
|
|
|
|
/// El backend `winit`: corre anidado dentro de una sesión gráfica.
|
|
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(greeter)?;
|
|
let keyboard = state.keyboard.clone().expect("teclado inicializado");
|
|
|
|
// El backend gráfico va primero. winit abre la ventana del compositor
|
|
// dentro de tu sesión gráfica anfitriona, y para encontrarla lee
|
|
// `WAYLAND_DISPLAY` / `DISPLAY` del entorno. Si publicáramos antes
|
|
// nuestro propio socket en `WAYLAND_DISPLAY`, winit intentaría
|
|
// anidarse en nosotros mismos —un socket que aún no atiende a nadie—
|
|
// y se quedaría colgado para siempre.
|
|
let (mut backend, mut winit) = match winit::init::<GlesRenderer>() {
|
|
Ok(pair) => pair,
|
|
Err(e) => {
|
|
eprintln!("mirada-compositor · no pude abrir la ventana: {e}");
|
|
eprintln!(
|
|
" El backend `winit` necesita una sesión gráfica anfitriona\n \
|
|
(X11 o Wayland) donde dibujar la ventana del compositor.\n \
|
|
Aquí no hay ninguna: DISPLAY='{}', WAYLAND_DISPLAY='{}',\n \
|
|
XDG_SESSION_TYPE='{}'.\n \
|
|
Lánzalo desde un escritorio gráfico, o desde un servidor X\n \
|
|
virtual (Xvfb) al que te conectes por VNC.",
|
|
std::env::var("DISPLAY").unwrap_or_default(),
|
|
std::env::var("WAYLAND_DISPLAY").unwrap_or_default(),
|
|
std::env::var("XDG_SESSION_TYPE").unwrap_or_else(|_| "tty".into()),
|
|
);
|
|
return Err(e.into());
|
|
}
|
|
};
|
|
|
|
// Ahora sí, nuestro propio socket Wayland — y `WAYLAND_DISPLAY` se
|
|
// publica *después* de winit, sólo para los clientes que lancemos
|
|
// como procesos hijos.
|
|
let listener = ListeningSocket::bind_auto("wayland", 1..32)?;
|
|
let socket_name = listener
|
|
.socket_name()
|
|
.and_then(|s| s.to_str())
|
|
.unwrap_or("wayland-?")
|
|
.to_string();
|
|
std::env::set_var("WAYLAND_DISPLAY", &socket_name);
|
|
println!("mirada-compositor · escuchando en WAYLAND_DISPLAY={socket_name}");
|
|
println!(" lanza un cliente: WAYLAND_DISPLAY={socket_name} foot");
|
|
|
|
let start = Instant::now();
|
|
let mut clients = Vec::new();
|
|
|
|
// Con el renderer ya creado, anuncia dmabuf (clientes con GPU).
|
|
announce_dmabuf(&mut state, &display.handle(), backend.renderer());
|
|
|
|
// Salida inicial = el tamaño de la ventana winit.
|
|
let win_size = backend.window_size();
|
|
let _wl_output = announce_output(&display.handle(), "winit", win_size.w, win_size.h, 60_000);
|
|
{
|
|
let ev = state.body.add_output(0, win_size.w, win_size.h);
|
|
state.brain_feed(ev);
|
|
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 {
|
|
WinitEvent::CloseRequested => state.running = false,
|
|
WinitEvent::Resized { size, .. } => {
|
|
state.output_changed(size.w, size.h);
|
|
}
|
|
WinitEvent::Input(InputEvent::Keyboard { event }) => {
|
|
let code = event.key_code();
|
|
let key_state = event.state();
|
|
let pressed = key_state == KeyState::Pressed;
|
|
let time = start.elapsed().as_millis() as u32;
|
|
keyboard.clone().input::<(), _>(
|
|
&mut state,
|
|
code,
|
|
key_state,
|
|
SERIAL_COUNTER.next_serial(),
|
|
time,
|
|
|st, mods, handle| {
|
|
if !pressed {
|
|
return FilterResult::Forward;
|
|
}
|
|
if let Some(combo) = combo_string(mods, handle.modified_sym()) {
|
|
if st.grabs.contains(&combo) {
|
|
st.pending_keybind = Some(combo);
|
|
return FilterResult::Intercept(());
|
|
}
|
|
}
|
|
FilterResult::Forward
|
|
},
|
|
);
|
|
if let Some(combo) = state.pending_keybind.take() {
|
|
let ev = state.body.keybind(combo);
|
|
state.brain_feed(ev);
|
|
}
|
|
}
|
|
_ => {}
|
|
});
|
|
if let PumpStatus::Exit(_) = status {
|
|
break;
|
|
}
|
|
|
|
// 2 · Comandos de un Cerebro enlazado.
|
|
state.brain_poll();
|
|
|
|
// 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) {
|
|
Ok(km) => {
|
|
let cmd = if let Brain::Embedded(d) = &mut state.brain {
|
|
Some(d.set_keymap(km))
|
|
} else {
|
|
None
|
|
};
|
|
if let Some(cmd) = cmd {
|
|
state.apply_commands(vec![cmd]);
|
|
}
|
|
println!("mirada-compositor · keymap recargado.");
|
|
}
|
|
Err(e) => eprintln!(
|
|
"mirada-compositor · keymap inválido, conservo el anterior: {e}"
|
|
),
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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() {
|
|
Ok(Some(req)) => state.serve_ctl(req),
|
|
Ok(None) => continue,
|
|
Err(e) => CtlReply::Error(format!("{e}")),
|
|
};
|
|
let _ = conn.reply(&reply);
|
|
}
|
|
}
|
|
|
|
// 3 · Composición de las superficies en sus rectángulos.
|
|
let size = backend.window_size();
|
|
let damage: Rectangle<i32, smithay::utils::Physical> = Rectangle::from_size(size);
|
|
{
|
|
let (renderer, mut framebuffer) = backend.bind().unwrap();
|
|
// Orden de pintado: la lista de elementos va front-to-back
|
|
// (índice 0 = encima): el shell primero —va sobre todo—, luego
|
|
// las flotantes, luego las teseladas. `sort_by_key` es estable:
|
|
// dentro de cada grupo se respeta el orden de apertura.
|
|
let output_h = state.output_size.1;
|
|
let mut shown: Vec<&ManagedWindow> =
|
|
state.windows.iter().filter(|w| w.visible).collect();
|
|
shown.sort_by_key(|w| (!w.is_shell, !w.floating));
|
|
let elements: Vec<WaylandSurfaceRenderElement<GlesRenderer>> = shown
|
|
.iter()
|
|
.flat_map(|w| {
|
|
render_elements_from_surface_tree(
|
|
renderer,
|
|
&w.surface,
|
|
render_loc(w, output_h),
|
|
1.0,
|
|
1.0,
|
|
Kind::Unspecified,
|
|
)
|
|
})
|
|
.collect();
|
|
let mut frame = renderer
|
|
.render(&mut framebuffer, size, Transform::Flipped180)
|
|
.unwrap();
|
|
frame
|
|
.clear(Color32F::new(0.05, 0.05, 0.08, 1.0), &[damage])
|
|
.unwrap();
|
|
draw_render_elements(&mut frame, 1.0, &elements, &[damage]).unwrap();
|
|
let _ = frame.finish().unwrap();
|
|
}
|
|
|
|
// 4 · Callbacks de frame + clientes nuevos + flush.
|
|
let time = start.elapsed().as_millis() as u32;
|
|
for w in &state.windows {
|
|
send_frames_surface_tree(&w.surface, time);
|
|
}
|
|
if let Some(stream) = listener.accept()? {
|
|
let client = display
|
|
.handle()
|
|
.insert_client(stream, Arc::new(ClientState::default()))
|
|
.unwrap();
|
|
clients.push(client);
|
|
}
|
|
display.dispatch_clients(&mut state)?;
|
|
display.flush_clients()?;
|
|
|
|
backend.submit(Some(&[damage])).unwrap();
|
|
}
|
|
|
|
println!("mirada-compositor · adiós.");
|
|
Ok(())
|
|
}
|
|
|
|
fn main() {
|
|
// 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);
|
|
}
|
|
}
|
|
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(greeter)
|
|
} else {
|
|
println!("mirada-compositor · sin sesión gráfica → backend DRM.");
|
|
drm_backend::run(greeter)
|
|
}
|
|
}
|
|
};
|
|
if let Err(e) = result {
|
|
eprintln!("mirada-compositor · error: {e}");
|
|
std::process::exit(1);
|
|
}
|
|
}
|