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:
@@ -5,7 +5,8 @@ use std::collections::HashMap;
|
||||
use mirada_layout::{LayoutMode, LayoutParams, Rect, WindowId, Workspace};
|
||||
use mirada_protocol::{placements, BodyEvent, BrainCommand, OutputId};
|
||||
|
||||
use crate::action::{default_keymap, DesktopAction, WORKSPACE_COUNT};
|
||||
use crate::action::{DesktopAction, WORKSPACE_COUNT};
|
||||
use crate::keymap::Keymap;
|
||||
|
||||
/// Lo que el Cerebro sabe de una ventana: su identidad de aplicación.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
@@ -32,8 +33,8 @@ pub struct Desktop {
|
||||
active: usize,
|
||||
/// Identidad de cada ventana conocida.
|
||||
windows: HashMap<WindowId, WindowInfo>,
|
||||
/// Atajos globales → acción.
|
||||
keymap: Vec<(String, DesktopAction)>,
|
||||
/// Atajos globales → acción. Configurable, recargable en caliente.
|
||||
keymap: Keymap,
|
||||
}
|
||||
|
||||
impl Default for Desktop {
|
||||
@@ -46,6 +47,12 @@ impl Desktop {
|
||||
/// Escritorio recién arrancado: sin salidas ni ventanas, con los
|
||||
/// escritorios virtuales vacíos y el mapa de teclas por defecto.
|
||||
pub fn new() -> Self {
|
||||
Self::with_keymap(Keymap::default())
|
||||
}
|
||||
|
||||
/// Como [`Desktop::new`], pero con un keymap dado — el que la app
|
||||
/// cargó del archivo de configuración del usuario.
|
||||
pub fn with_keymap(keymap: Keymap) -> Self {
|
||||
let workspaces = (0..WORKSPACE_COUNT)
|
||||
.map(|_| Workspace::new(LayoutParams::default()))
|
||||
.collect();
|
||||
@@ -54,14 +61,26 @@ impl Desktop {
|
||||
workspaces,
|
||||
active: 0,
|
||||
windows: HashMap::new(),
|
||||
keymap: default_keymap(),
|
||||
keymap,
|
||||
}
|
||||
}
|
||||
|
||||
/// El comando que registra los atajos globales en el Cuerpo. La app
|
||||
/// GPUI lo envía una vez, al conectar.
|
||||
/// lo envía al conectar, y de nuevo tras cada recarga del keymap.
|
||||
pub fn grab_keys(&self) -> BrainCommand {
|
||||
BrainCommand::GrabKeys(self.keymap.iter().map(|(k, _)| k.clone()).collect())
|
||||
BrainCommand::GrabKeys(self.keymap.grab_list())
|
||||
}
|
||||
|
||||
/// Reemplaza el keymap en caliente. Devuelve el [`BrainCommand`] que
|
||||
/// el dueño debe enviar al Cuerpo para reajustar qué teclas intercepta.
|
||||
pub fn set_keymap(&mut self, keymap: Keymap) -> BrainCommand {
|
||||
self.keymap = keymap;
|
||||
self.grab_keys()
|
||||
}
|
||||
|
||||
/// El keymap vigente — para un HUD o un editor visual de atajos.
|
||||
pub fn keymap(&self) -> &Keymap {
|
||||
&self.keymap
|
||||
}
|
||||
|
||||
/// Geometría de la salida primaria, si hay alguna conectada.
|
||||
@@ -111,12 +130,10 @@ impl Desktop {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
BodyEvent::Keybind(key) => {
|
||||
match self.keymap.iter().find(|(k, _)| *k == key).map(|(_, a)| *a) {
|
||||
Some(action) => self.apply(action),
|
||||
None => Vec::new(),
|
||||
}
|
||||
}
|
||||
BodyEvent::Keybind(key) => match self.keymap.lookup(&key) {
|
||||
Some(action) => self.apply(action),
|
||||
None => Vec::new(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,6 +296,27 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_keymap_swaps_the_bindings_and_regrabs() {
|
||||
let mut d = desktop_with_screen();
|
||||
for id in [1, 2, 3] {
|
||||
open(&mut d, id);
|
||||
}
|
||||
// El keymap por defecto no usa Alt.
|
||||
assert!(d.on_event(BodyEvent::Keybind("Alt+x".into())).is_empty());
|
||||
// Cargamos un keymap a medida; el comando devuelto re-registra grabs.
|
||||
let custom = crate::Keymap::from_ron(r#"( bindings: { "Alt+x": "focus-prev" } )"#).unwrap();
|
||||
match d.set_keymap(custom) {
|
||||
BrainCommand::GrabKeys(keys) => assert_eq!(keys, vec!["Alt+x".to_string()]),
|
||||
other => panic!("se esperaba GrabKeys, no {other:?}"),
|
||||
}
|
||||
// Ahora «Alt+x» sí mueve el foco, y «Super+j» ya no.
|
||||
assert_eq!(d.focused_window(), Some(3));
|
||||
d.on_event(BodyEvent::Keybind("Alt+x".into()));
|
||||
assert_eq!(d.focused_window(), Some(2));
|
||||
assert!(d.on_event(BodyEvent::Keybind("Super+j".into())).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn without_a_screen_nothing_is_placed() {
|
||||
let mut d = Desktop::new();
|
||||
|
||||
Reference in New Issue
Block a user