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:
@@ -51,6 +51,42 @@ fino: azul la que tiene el foco, gris las demás.
|
|||||||
- `MIRADA_DRM_TIMEOUT=<s>` — cierra el compositor solo tras N segundos
|
- `MIRADA_DRM_TIMEOUT=<s>` — cierra el compositor solo tras N segundos
|
||||||
(0 o sin definir = sin tope).
|
(0 o sin definir = sin tope).
|
||||||
|
|
||||||
|
## Como sesión de escritorio
|
||||||
|
|
||||||
|
Para usar carmen como tu escritorio de verdad — entrar a una sesión, no
|
||||||
|
sólo probarlo:
|
||||||
|
|
||||||
|
1. Compila e instala los binarios en el `PATH`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo build --release -p mirada-compositor -p mirada-ctl
|
||||||
|
sudo install -m755 target/release/mirada-compositor \
|
||||||
|
target/release/mirada-ctl /usr/local/bin/
|
||||||
|
sudo install -m755 session/mirada-session /usr/local/bin/
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Arranca desde una TTY:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mirada-session
|
||||||
|
```
|
||||||
|
|
||||||
|
O regístralo en un gestor de login copiando `session/carmen.desktop`
|
||||||
|
a `/usr/share/wayland-sessions/` — aparecerá carmen como sesión.
|
||||||
|
|
||||||
|
3. **Autoarranque** — los programas que quieras al iniciar van en
|
||||||
|
`~/.config/mirada/autostart`, uno por línea (`#` comenta). Tienes un
|
||||||
|
ejemplo en `session/autostart.example`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mkdir -p ~/.config/mirada
|
||||||
|
cp crates/apps/mirada-compositor/session/autostart.example \
|
||||||
|
~/.config/mirada/autostart
|
||||||
|
```
|
||||||
|
|
||||||
|
Dentro de la sesión, `Ctrl+Alt+F1…F12` salta a otra TTY y vuelve sin
|
||||||
|
romper carmen.
|
||||||
|
|
||||||
## Dos modos
|
## Dos modos
|
||||||
|
|
||||||
- **Autónomo** (por defecto) — lleva un `Desktop` (de `mirada-brain`)
|
- **Autónomo** (por defecto) — lleva un `Desktop` (de `mirada-brain`)
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Autoarranque de carmen — cópialo a ~/.config/mirada/autostart
|
||||||
|
#
|
||||||
|
# Un comando por línea; se lanza al arrancar el compositor, con
|
||||||
|
# WAYLAND_DISPLAY ya puesto. Las líneas en blanco y las que empiezan
|
||||||
|
# por # se ignoran. Cada línea se pasa a `sh -c`, así que valen las
|
||||||
|
# variables, las tuberías y el `&` final no hace falta.
|
||||||
|
|
||||||
|
# Una terminal para empezar.
|
||||||
|
foot
|
||||||
|
|
||||||
|
# Ejemplos (descoméntalos si los quieres):
|
||||||
|
# mirada-ctl layout spiral
|
||||||
|
# wbg ~/fondo.png
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Name=carmen
|
||||||
|
Comment=Compositor Wayland teselante (mirada)
|
||||||
|
Exec=mirada-session
|
||||||
|
Type=Application
|
||||||
|
DesktopNames=carmen
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# mirada-session — arranca carmen (el compositor mirada) como una sesión
|
||||||
|
# de escritorio. Pensado para lanzarse desde una TTY o desde un gestor de
|
||||||
|
# login (greetd, ly, …).
|
||||||
|
#
|
||||||
|
# Instálalo en el PATH (p. ej. /usr/local/bin/mirada-session) junto al
|
||||||
|
# binario `mirada-compositor`.
|
||||||
|
|
||||||
|
# Carmen es un compositor Wayland.
|
||||||
|
export XDG_SESSION_TYPE=wayland
|
||||||
|
export XDG_CURRENT_DESKTOP=carmen
|
||||||
|
export XDG_SESSION_DESKTOP=carmen
|
||||||
|
|
||||||
|
# Que las apps GUI prefieran sus backends Wayland.
|
||||||
|
export MOZ_ENABLE_WAYLAND=1
|
||||||
|
export QT_QPA_PLATFORM="wayland;xcb"
|
||||||
|
export SDL_VIDEODRIVER=wayland
|
||||||
|
export _JAVA_AWT_WM_NONREPARENTING=1
|
||||||
|
|
||||||
|
# El backend DRM toma la TTY entera. Los programas de arranque van en
|
||||||
|
# ~/.config/mirada/autostart (uno por línea).
|
||||||
|
exec mirada-compositor --drm
|
||||||
@@ -116,8 +116,15 @@ const BTN_RIGHT: u32 = 0x111;
|
|||||||
struct DrmState {
|
struct DrmState {
|
||||||
app: App,
|
app: App,
|
||||||
display: Display<App>,
|
display: Display<App>,
|
||||||
|
/// El dispositivo DRM — se conserva para pausarlo y reactivarlo al
|
||||||
|
/// conmutar de VT.
|
||||||
|
drm: DrmDevice,
|
||||||
compositor: Compositor,
|
compositor: Compositor,
|
||||||
renderer: GlesRenderer,
|
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.
|
/// `true` entre que se encola un page-flip y llega su VBlank.
|
||||||
pending_flip: bool,
|
pending_flip: bool,
|
||||||
keymap_path: Option<std::path::PathBuf>,
|
keymap_path: Option<std::path::PathBuf>,
|
||||||
@@ -139,6 +146,9 @@ struct DrmState {
|
|||||||
impl DrmState {
|
impl DrmState {
|
||||||
/// Compone el cursor y las ventanas y, si hubo cambios, encola el cuadro.
|
/// Compone el cursor y las ventanas y, si hubo cambios, encola el cuadro.
|
||||||
fn render(&mut self) {
|
fn render(&mut self) {
|
||||||
|
if !self.active {
|
||||||
|
return; // la sesión está en otra VT — no tocamos la GPU
|
||||||
|
}
|
||||||
if self.pending_flip {
|
if self.pending_flip {
|
||||||
return; // aún esperamos el VBlank del cuadro anterior
|
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
|
/// Tarea periódica: Cerebro enlazado, recarga del keymap, API de
|
||||||
/// control, composición y vaciado hacia los clientes.
|
/// control, composición y vaciado hacia los clientes.
|
||||||
fn tick(&mut self) {
|
fn tick(&mut self) {
|
||||||
@@ -696,6 +734,9 @@ pub fn run() -> Result<(), Box<dyn Error>> {
|
|||||||
std::env::set_var("WAYLAND_DISPLAY", &socket_name);
|
std::env::set_var("WAYLAND_DISPLAY", &socket_name);
|
||||||
println!(" escuchando en 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
|
// App de arranque: si `MIRADA_STARTUP` trae un comando, se lanza como
|
||||||
// hijo (hereda `WAYLAND_DISPLAY`) — cómodo para probar sin saltar de VT.
|
// hijo (hereda `WAYLAND_DISPLAY`) — cómodo para probar sin saltar de VT.
|
||||||
if let Ok(cmd) = std::env::var("MIRADA_STARTUP") {
|
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}"))?;
|
EventLoop::try_new().map_err(|e| format!("calloop falló: {e}"))?;
|
||||||
let handle = event_loop.handle();
|
let handle = event_loop.handle();
|
||||||
|
|
||||||
// Sesión: pausa/activación al cambiar de VT.
|
// Sesión: pausa/activación al conmutar de VT.
|
||||||
handle
|
handle
|
||||||
.insert_source(session_notifier, |event, _, _state| match event {
|
.insert_source(session_notifier, |event, _, state: &mut DrmState| match event {
|
||||||
SessionEvent::PauseSession => println!("mirada-compositor · sesión en pausa."),
|
SessionEvent::PauseSession => state.pause_session(),
|
||||||
SessionEvent::ActivateSession => println!("mirada-compositor · sesión activa."),
|
SessionEvent::ActivateSession => state.resume_session(),
|
||||||
})
|
})
|
||||||
.map_err(|e| format!("insert session: {e}"))?;
|
.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}"))?;
|
.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()));
|
let mut libinput = Libinput::new_with_udev(LibinputSessionInterface::from(session.clone()));
|
||||||
libinput
|
libinput
|
||||||
.udev_assign_seat(&seat_name)
|
.udev_assign_seat(&seat_name)
|
||||||
.map_err(|()| "libinput: no pude asignar el seat")?;
|
.map_err(|()| "libinput: no pude asignar el seat")?;
|
||||||
|
let libinput_handle = libinput.clone();
|
||||||
handle
|
handle
|
||||||
.insert_source(LibinputInputBackend::new(libinput), |event, _meta, state| {
|
.insert_source(LibinputInputBackend::new(libinput), |event, _meta, state| {
|
||||||
state.handle_input(event);
|
state.handle_input(event);
|
||||||
@@ -800,8 +844,11 @@ pub fn run() -> Result<(), Box<dyn Error>> {
|
|||||||
let mut state = DrmState {
|
let mut state = DrmState {
|
||||||
app,
|
app,
|
||||||
display,
|
display,
|
||||||
|
drm,
|
||||||
compositor,
|
compositor,
|
||||||
renderer,
|
renderer,
|
||||||
|
libinput: libinput_handle,
|
||||||
|
active: true,
|
||||||
pending_flip: false,
|
pending_flip: false,
|
||||||
keymap_path,
|
keymap_path,
|
||||||
keymap_watch,
|
keymap_watch,
|
||||||
|
|||||||
@@ -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
|
/// 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
|
/// entorno —`WAYLAND_DISPLAY` incluido—, así que el cliente que abra se
|
||||||
/// conecta a este compositor. Lo usan la acción `spawn:…` del keymap y
|
/// conecta a este compositor. Lo usan la acción `spawn:…` del keymap, la
|
||||||
/// la variable `MIRADA_STARTUP`.
|
/// variable `MIRADA_STARTUP` y el autoarranque.
|
||||||
fn spawn_command(cmd: &str) {
|
fn spawn_command(cmd: &str) {
|
||||||
let cmd = cmd.trim();
|
let cmd = cmd.trim();
|
||||||
if cmd.is_empty() {
|
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.
|
/// Carga las reglas de ventana del usuario, o ninguna si no hay archivo.
|
||||||
fn load_user_rules() -> Rules {
|
fn load_user_rules() -> Rules {
|
||||||
match Rules::default_path() {
|
match Rules::default_path() {
|
||||||
|
|||||||
@@ -214,13 +214,21 @@ Cerebro: **autónomo** (`Desktop` embebido) o **enlazado** (`MIRADA_SOCKET`
|
|||||||
la ventana ahí (`Workspace::set_floating`). Cada ventana lleva un marco
|
la ventana ahí (`Workspace::set_floating`). Cada ventana lleva un marco
|
||||||
de 2 px (4 `SolidColorBuffer` por ventana, `Id` estable): azul si tiene
|
de 2 px (4 `SolidColorBuffer` por ventana, `Id` estable): azul si tiene
|
||||||
el foco, gris si no.
|
el foco, gris si no.
|
||||||
|
- **Conmutación de VT** — `Ctrl+Alt+Fn` salta a otra TTY: el
|
||||||
|
`SessionEvent` de `libseat` pausa el `DrmDevice` y suspende `libinput`;
|
||||||
|
al volver, los reactiva, llama a `DrmCompositor::reset_state` y
|
||||||
|
repinta. Mientras está cedida, `render()` no toca la GPU.
|
||||||
|
- **Sesión** — `~/.config/mirada/autostart` (un comando por línea) se
|
||||||
|
lanza al arrancar el backend DRM; el script `session/mirada-session` y
|
||||||
|
`session/carmen.desktop` integran carmen con un gestor de login.
|
||||||
|
|
||||||
**Pendiente** — refinamientos del Cuerpo:
|
**Pendiente** — refinamientos del Cuerpo:
|
||||||
|
|
||||||
| capa pendiente | rol |
|
| capa pendiente | rol |
|
||||||
| ---------------- | ------------------------------------------------------------ |
|
| ---------------- | ------------------------------------------------------------ |
|
||||||
| puntero en `winit` | ratón en el backend anidado (hoy sólo el backend DRM) |
|
| puntero en `winit` | ratón en el backend anidado (hoy sólo el backend DRM) |
|
||||||
| `mirada-input` | repetición de teclas, gestos; conmutación de VT, hotplug |
|
| `mirada-input` | repetición de teclas, gestos; hotplug de monitores |
|
||||||
|
| barra de estado | `wlr-layer-shell` + un cliente que dibuje la barra |
|
||||||
| `mirada-sandbox` | aislamiento de clientes sobre `arje-incarnate` |
|
| `mirada-sandbox` | aislamiento de clientes sobre `arje-incarnate` |
|
||||||
|
|
||||||
CRIU (congelar/restaurar ventanas) queda anotado como futuro.
|
CRIU (congelar/restaurar ventanas) queda anotado como futuro.
|
||||||
|
|||||||
@@ -1006,6 +1006,8 @@
|
|||||||
Super+arrastre mueve la ventana (botón izq.) o la redimensiona (der.) — al arrastrarla pasa a flotar.
|
Super+arrastre mueve la ventana (botón izq.) o la redimensiona (der.) — al arrastrarla pasa a flotar.
|
||||||
Fuerza xdg-decoration ServerSide y no dibuja marco: las ventanas teseladas van sin barra de título.
|
Fuerza xdg-decoration ServerSide y no dibuja marco: las ventanas teseladas van sin barra de título.
|
||||||
Lanzar programas: acción spawn:<comando> del keymap (Super+Shift+Return → spawn:foot por defecto).
|
Lanzar programas: acción spawn:<comando> del keymap (Super+Shift+Return → spawn:foot por defecto).
|
||||||
|
Conmutación de VT: Ctrl+Alt+Fn salta a otra TTY y vuelve sin romper la sesión (pausa DRM + libinput).
|
||||||
|
Sesión: ~/.config/mirada/autostart (un comando por línea) + script session/mirada-session + carmen.desktop.
|
||||||
Ver crates/apps/mirada-compositor/README.md.
|
Ver crates/apps/mirada-compositor/README.md.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user