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:
@@ -60,10 +60,26 @@ WAYLAND_DISPLAY=wayland-1 foot # o weston-terminal, alacritty, …
|
||||
```
|
||||
|
||||
Las ventanas se teselan solas. El teclado, con la ventana del compositor
|
||||
enfocada, maneja el escritorio con atajos `Super+…` (los que registra el
|
||||
Cerebro: foco `Super+j/k`, layout `Super+Tab`, escritorios `Super+1..9`).
|
||||
enfocada, maneja el escritorio con atajos `Super+…`: foco `Super+j/k`,
|
||||
ciclar layout `Super+space`, escritorios `Super+1..9`, cerrar `Super+q`.
|
||||
Cierra la ventana del compositor para salir.
|
||||
|
||||
## Atajos de teclado
|
||||
|
||||
Los atajos son configurables en RON: `~/.config/mirada/keymap.ron`. En
|
||||
modo autónomo, el Cuerpo lo carga al arrancar (si no existe, escribe uno
|
||||
por defecto documentado) y lo **recarga en caliente** — edita el archivo,
|
||||
guarda, y los atajos cambian sin reiniciar. En modo enlazado el keymap es
|
||||
asunto del Cerebro (la app `mirada`).
|
||||
|
||||
```sh
|
||||
cargo run -p mirada-brain --example keymap-default # ver el formato
|
||||
```
|
||||
|
||||
El compositor en sí no interpreta atajos: sólo intercepta las
|
||||
combinaciones que el Cerebro le pide (`GrabKeys`) y le devuelve la
|
||||
pulsada. *Qué significa* cada una lo decide `mirada-brain`. Ver el SDD.
|
||||
|
||||
## Qué implementa
|
||||
|
||||
`wl_compositor`, `xdg_shell` (toplevels y popups), `wl_shm`, `wl_seat`
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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`).
|
||||
|
||||
Reference in New Issue
Block a user