feat(mirada): keymap configurable en RON, recargable en caliente

Los atajos de teclado dejan de estar cableados: ahora son un Keymap
configurable que vive sólo en el Cerebro. El Cuerpo nunca lo ve — sólo
recibe la lista de cadenas a interceptar (GrabKeys) y devuelve la
pulsada; es Desktop quien la traduce. Esa separación (qué interceptar
vs. qué significa) hace innecesario cualquier candado o Arc.

mirada-brain:
- keymap.rs — Keymap: from_ron/to_ron, load/save, load_or_init (escribe
  un archivo por defecto documentado si falta; default sin pisar si está
  corrupto), default_path (~/.config/mirada/keymap.ron), y watch sobre
  notify para la recarga en caliente (KeymapWatch).
- DesktopAction: Display + FromStr — vocabulario textual estable
  ("focus-next", "layout:grid", "workspace:3"); evita los guiones que
  romperían el RON de un enum.
- Desktop: with_keymap, set_keymap (cambio en caliente -> nuevo GrabKeys).
- Ejemplo keymap-default: imprime el archivo por defecto en RON.

Apps: mirada y mirada-compositor (modo embebido) cargan el keymap del
usuario al arrancar y lo recargan en caliente cuando el archivo cambia.

Disco RON, cable postcard (sólo la lista de cadenas), sin ejecutable
configurador. mirada-brain: 17 -> 29 tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-21 00:04:11 +00:00
parent 51398f89cf
commit 8204852e3a
13 changed files with 713 additions and 25 deletions
+42 -2
View File
@@ -59,7 +59,7 @@ use smithay::{
};
use mirada_body::{BodyOp, BodyState};
use mirada_brain::{BodyEvent, BrainCommand, Desktop};
use mirada_brain::{BodyEvent, BrainCommand, Desktop, Keymap};
use mirada_link::BodyLink;
// ---------------------------------------------------------------------
@@ -435,6 +435,10 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
let mut seat_state = SeatState::new();
let seat = seat_state.new_wl_seat(&dh, "mirada");
// 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: enlazado si `MIRADA_SOCKET` está puesto.
let brain = match std::env::var("MIRADA_SOCKET") {
Ok(path) => {
@@ -445,7 +449,11 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
}
Err(_) => {
println!("mirada-compositor · modo autónomo (Cerebro embebido).");
Brain::Embedded(Desktop::new())
let keymap = match &keymap_path {
Some(p) => Keymap::load_or_init(p),
None => Keymap::default(),
};
Brain::Embedded(Desktop::with_keymap(keymap))
}
};
@@ -475,6 +483,16 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
state.apply_commands(vec![grab]);
}
// Vigilancia del keymap para recargarlo en caliente — sólo tiene
// sentido con el Cerebro embebido.
let keymap_watch = match (&state.brain, &keymap_path) {
(Brain::Embedded(_), Some(p)) => Keymap::watch(p).ok(),
_ => None,
};
if keymap_watch.is_some() {
println!("mirada-compositor · vigilando el keymap (recarga en caliente).");
}
// 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
@@ -571,6 +589,28 @@ fn run() -> 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.
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}"
),
}
}
}
// 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);