feat(mirada): sesión de escritorio — autostart y conmutación de VT

Dos piezas para usar carmen como tu escritorio de verdad.

Conmutación de VT — `Ctrl+Alt+Fn` salta a otra TTY y vuelve sin romper
la sesión. El `SessionEvent` de `libseat` ahora hace trabajo de verdad:
- al ceder la VT, pausa el `DrmDevice` y suspende `libinput`; `render()`
  no vuelve a tocar la GPU mientras la sesión esté cedida (`active`).
- al recuperarla, reanuda `libinput`, reactiva el `DrmDevice`, llama a
  `DrmCompositor::reset_state` y repinta.
`DrmState` conserva ahora `drm` y un clon del contexto `libinput`.

Sesión — `~/.config/mirada/autostart` (un comando por línea, `#`
comenta) se lanza al arrancar el backend DRM, vía un `spawn_autostart`
que reusa `spawn_command`. Y `session/`: el script `mirada-session`
(fija el entorno XDG y exec del compositor) y `carmen.desktop` para
registrarlo en un gestor de login, más un `autostart.example`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-21 04:31:55 +00:00
parent 58e72c3d08
commit 5ede927d34
8 changed files with 172 additions and 8 deletions
@@ -116,8 +116,15 @@ const BTN_RIGHT: u32 = 0x111;
struct DrmState {
app: App,
display: Display<App>,
/// El dispositivo DRM — se conserva para pausarlo y reactivarlo al
/// conmutar de VT.
drm: DrmDevice,
compositor: Compositor,
renderer: GlesRenderer,
/// Contexto `libinput` — se suspende y reanuda al conmutar de VT.
libinput: Libinput,
/// `false` mientras la sesión está cedida a otra VT — no se compone.
active: bool,
/// `true` entre que se encola un page-flip y llega su VBlank.
pending_flip: bool,
keymap_path: Option<std::path::PathBuf>,
@@ -139,6 +146,9 @@ struct DrmState {
impl DrmState {
/// Compone el cursor y las ventanas y, si hubo cambios, encola el cuadro.
fn render(&mut self) {
if !self.active {
return; // la sesión está en otra VT — no tocamos la GPU
}
if self.pending_flip {
return; // aún esperamos el VBlank del cuadro anterior
}
@@ -258,6 +268,34 @@ impl DrmState {
}
}
/// La sesión se cede a otra VT (`Ctrl+Alt+Fn`): suelta la GPU y deja
/// de leer el ratón y el teclado, para no chocar con quien ahora
/// manda en la pantalla.
fn pause_session(&mut self) {
self.active = false;
self.drm.pause();
self.libinput.suspend();
println!("mirada-compositor · sesión cedida a otra VT.");
}
/// La sesión vuelve a esta VT: recupera la GPU y la entrada, reinicia
/// el estado del compositor y repinta.
fn resume_session(&mut self) {
if self.libinput.resume().is_err() {
eprintln!("mirada-compositor · libinput.resume falló.");
}
if let Err(e) = self.drm.activate(false) {
eprintln!("mirada-compositor · drm.activate falló: {e}");
}
if let Err(e) = self.compositor.reset_state() {
eprintln!("mirada-compositor · compositor.reset_state falló: {e}");
}
self.active = true;
self.pending_flip = false;
self.render();
println!("mirada-compositor · sesión recuperada.");
}
/// Tarea periódica: Cerebro enlazado, recarga del keymap, API de
/// control, composición y vaciado hacia los clientes.
fn tick(&mut self) {
@@ -696,6 +734,9 @@ 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") {
@@ -708,11 +749,11 @@ pub fn run() -> Result<(), Box<dyn Error>> {
EventLoop::try_new().map_err(|e| format!("calloop falló: {e}"))?;
let handle = event_loop.handle();
// Sesión: pausa/activación al cambiar de VT.
// Sesión: pausa/activación al conmutar de VT.
handle
.insert_source(session_notifier, |event, _, _state| match event {
SessionEvent::PauseSession => println!("mirada-compositor · sesión en pausa."),
SessionEvent::ActivateSession => println!("mirada-compositor · sesión activa."),
.insert_source(session_notifier, |event, _, state: &mut DrmState| match event {
SessionEvent::PauseSession => state.pause_session(),
SessionEvent::ActivateSession => state.resume_session(),
})
.map_err(|e| format!("insert session: {e}"))?;
@@ -729,11 +770,14 @@ pub fn run() -> Result<(), Box<dyn Error>> {
})
.map_err(|e| format!("insert drm: {e}"))?;
// Teclado y ratón vía libinput.
// Teclado y ratón vía libinput. Guardamos un clon del contexto (es
// un manejador con contador de referencias) para suspenderlo y
// reanudarlo al conmutar de VT.
let mut libinput = Libinput::new_with_udev(LibinputSessionInterface::from(session.clone()));
libinput
.udev_assign_seat(&seat_name)
.map_err(|()| "libinput: no pude asignar el seat")?;
let libinput_handle = libinput.clone();
handle
.insert_source(LibinputInputBackend::new(libinput), |event, _meta, state| {
state.handle_input(event);
@@ -800,8 +844,11 @@ pub fn run() -> Result<(), Box<dyn Error>> {
let mut state = DrmState {
app,
display,
drm,
compositor,
renderer,
libinput: libinput_handle,
active: true,
pending_flip: false,
keymap_path,
keymap_watch,
+32 -2
View File
@@ -630,8 +630,8 @@ fn cursor_hotspot(surface: &WlSurface) -> (i32, i32) {
/// 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. Lo usan la acción `spawn:…` del keymap y
/// la variable `MIRADA_STARTUP`.
/// conecta a este compositor. Lo usan la acción `spawn:…` del keymap, la
/// variable `MIRADA_STARTUP` y el autoarranque.
fn spawn_command(cmd: &str) {
let cmd = cmd.trim();
if cmd.is_empty() {
@@ -643,6 +643,36 @@ fn spawn_command(cmd: &str) {
}
}
/// 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")))
}
/// 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 {
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);
n += 1;
}
if n > 0 {
println!("mirada-compositor · autoarranque: {n} programa(s) desde {}", path.display());
}
}
/// Carga las reglas de ventana del usuario, o ninguna si no hay archivo.
fn load_user_rules() -> Rules {
match Rules::default_path() {