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
+51 -7
View File
@@ -25,6 +25,7 @@
//! Shift+j / k mueve la enfocada Ctrl+1..9 enviar a escritorio
//! ```
use std::path::PathBuf;
use std::time::Duration;
use gpui::{
@@ -32,7 +33,8 @@ use gpui::{
SharedString, Window,
};
use mirada_brain::{
BodyEvent, BrainCommand, Desktop, DesktopAction, LayoutMode, WindowId, WindowPlacement,
BodyEvent, BrainCommand, Desktop, DesktopAction, Keymap, KeymapWatch, LayoutMode, WindowId,
WindowPlacement,
};
use mirada_link::BrainLink;
use nahual_launcher::launch_app;
@@ -62,18 +64,40 @@ struct Mirada {
note: SharedString,
focus: FocusHandle,
focused_once: bool,
/// Ruta del keymap del usuario, para recargarlo en caliente.
keymap_path: Option<PathBuf>,
/// Vigía del keymap; `None` en simulación o si no hay archivo.
keymap_watch: Option<KeymapWatch>,
}
impl Mirada {
fn new(cx: &mut Context<Self>) -> Self {
// Keymap del usuario (~/.config/mirada/keymap.ron): define los
// atajos que el Cuerpo intercepta y nos devuelve como `Keybind`.
let keymap_path = Keymap::default_path();
let keymap = match &keymap_path {
Some(p) => Keymap::load_or_init(p),
None => Keymap::default(),
};
let link = connect_body();
// Vigilar el keymap sólo tiene sentido con un Cuerpo conectado;
// en simulación, mirada usa las teclas de su propia ventana.
let keymap_watch = if link.is_some() {
keymap_path.as_deref().and_then(|p| Keymap::watch(p).ok())
} else {
None
};
let mut app = Self {
desktop: Desktop::new(),
desktop: Desktop::with_keymap(keymap),
placements: Vec::new(),
next_id: 1,
link: connect_body(),
link,
note: SharedString::from("listo"),
focus: cx.focus_handle(),
focused_once: false,
keymap_path,
keymap_watch,
};
if let Some(link) = app.link.as_mut() {
// Registra los atajos globales en el Cuerpo.
@@ -102,10 +126,15 @@ impl Mirada {
Some(link) => link.drain(),
None => Vec::new(),
};
if !events.is_empty() {
for ev in events {
app.feed(ev);
}
let had_events = !events.is_empty();
let keymap_changed = app.keymap_watch.as_ref().is_some_and(|w| w.changed());
if keymap_changed {
app.reload_keymap();
}
for ev in events {
app.feed(ev);
}
if had_events || keymap_changed {
cx.notify();
}
});
@@ -141,6 +170,21 @@ impl Mirada {
self.dispatch(cmds);
}
/// Recarga el keymap del disco y re-registra los atajos en el Cuerpo.
fn reload_keymap(&mut self) {
let Some(path) = self.keymap_path.clone() else {
return;
};
match Keymap::load(&path) {
Ok(km) => {
let cmd = self.desktop.set_keymap(km);
self.dispatch(vec![cmd]);
self.note = SharedString::from("keymap recargado");
}
Err(e) => self.note = SharedString::from(format!("keymap inválido: {e}")),
}
}
/// Reparte los comandos del Cerebro: actualiza lo pintado y, o bien
/// los manda al Cuerpo, o bien —en simulación— cierra las ventanas
/// por su cuenta (no hay nadie que devuelva el `WindowClosed`).