feat(shuma): macros del shell — barra [RUN]
shuma-intent: + módulo macros.
- Macro — secuencia de intenciones nombrada, con tecla física opcional
(F1-F3...). Builder bind()/step(). Serializable: compartible entre
sesiones y usuarios (requisito de la spec).
- MacroBook — colección con lookup por tecla y por nombre; insert
reemplaza por nombre.
Completa el núcleo agnóstico del shell shuma: prompt de intenciones +
grafo de contexto + macros. 11 tests verdes. cargo check --workspace
verde.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
b9a6cd33fd60c1866e6f7b3fdffc6460f4681ca9 · por sergio · 2026-05-20T15:47:57Z (hace 5 meses) · padre 1da4ee11d · ver el árbol en este commit
Delta semántico · 17
crates/modules/shuma/shuma-intent/src/lib.rs
crates/modules/shuma/shuma-intent/src/macros.rs
diff --git a/crates/modules/shuma/shuma-intent/src/lib.rs b/crates/modules/shuma/shuma-intent/src/lib.rs
index 8b0ad23..d520c89 100644
--- a/crates/modules/shuma/shuma-intent/src/lib.rs
+++ b/crates/modules/shuma/shuma-intent/src/lib.rs
@@ -14,6 +14,8 @@
pub mod parse;
pub mod graph;
+pub mod macros;
pub use graph::{CommandNode, NodeStatus, SessionGraph};
+pub use macros::{Macro, MacroBook};
pub use parse::{Intention, Ref, Stage};
diff --git a/crates/modules/shuma/shuma-intent/src/macros.rs b/crates/modules/shuma/shuma-intent/src/macros.rs
new file mode 100644
index 0000000..a1f0eca
--- /dev/null
+++ b/crates/modules/shuma/shuma-intent/src/macros.rs
@@ -0,0 +1,114 @@
+//! Macros del shell — la barra de ejecución [RUN].
+//!
+//! Una macro es una secuencia de intenciones nombrada y opcionalmente
+//! mapeada a una tecla física (F1-F3...). Son serializables: la spec
+//! pide que sean compartibles entre sesiones y entre usuarios.
+
+use serde::{Deserialize, Serialize};
+
+/// Una macro: un nombre, una tecla opcional y las intenciones que dispara.
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+pub struct Macro {
+ pub name: String,
+ /// Tecla física que la dispara (`"F1"`, `"F2"`, ...). `None` = sin atajo.
+ pub key: Option<String>,
+ /// Líneas de prompt que ejecuta, en orden.
+ pub intentions: Vec<String>,
+}
+
+impl Macro {
+ pub fn new(name: impl Into<String>) -> Self {
+ Self { name: name.into(), key: None, intentions: Vec::new() }
+ }
+
+ /// Builder: asigna una tecla.
+ pub fn bind(mut self, key: impl Into<String>) -> Self {
+ self.key = Some(key.into());
+ self
+ }
+
+ /// Builder: agrega una intención.
+ pub fn step(mut self, intention: impl Into<String>) -> Self {
+ self.intentions.push(intention.into());
+ self
+ }
+}
+
+/// Colección de macros de la barra [RUN]. Serializable para compartir.
+#[derive(Debug, Clone, Default, Serialize, Deserialize)]
+pub struct MacroBook {
+ macros: Vec<Macro>,
+}
+
+impl MacroBook {
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ /// Agrega (o reemplaza por nombre) una macro.
+ pub fn insert(&mut self, m: Macro) {
+ if let Some(slot) = self.macros.iter_mut().find(|x| x.name == m.name) {
+ *slot = m;
+ } else {
+ self.macros.push(m);
+ }
+ }
+
+ pub fn len(&self) -> usize {
+ self.macros.len()
+ }
+
+ pub fn is_empty(&self) -> bool {
+ self.macros.is_empty()
+ }
+
+ pub fn all(&self) -> &[Macro] {
+ &self.macros
+ }
+
+ /// Macro mapeada a una tecla física dada.
+ pub fn by_key(&self, key: &str) -> Option<&Macro> {
+ self.macros.iter().find(|m| m.key.as_deref() == Some(key))
+ }
+
+ /// Macro por nombre exacto.
+ pub fn by_name(&self, name: &str) -> Option<&Macro> {
+ self.macros.iter().find(|m| m.name == name)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn macro_builder_composes() {
+ let m = Macro::new("deploy")
+ .bind("F2")
+ .step("cargo build --release")
+ .step("scp target/release/app host:/srv");
+ assert_eq!(m.key.as_deref(), Some("F2"));
+ assert_eq!(m.intentions.len(), 2);
+ }
+
+ #[test]
+ fn book_lookup_by_key_and_name() {
+ let mut book = MacroBook::new();
+ book.insert(Macro::new("build").bind("F1").step("cargo build"));
+ book.insert(Macro::new("clean").bind("F3").step("cargo clean"));
+ assert_eq!(book.len(), 2);
+ assert_eq!(book.by_key("F1").unwrap().name, "build");
+ assert_eq!(book.by_key("F3").unwrap().name, "clean");
+ assert!(book.by_key("F9").is_none());
+ assert!(book.by_name("clean").is_some());
+ }
+
+ #[test]
+ fn insert_replaces_by_name() {
+ let mut book = MacroBook::new();
+ book.insert(Macro::new("x").step("v1"));
+ book.insert(Macro::new("x").step("v2"));
+ assert_eq!(book.len(), 1);
+ assert_eq!(book.by_name("x").unwrap().intentions, vec!["v2"]);
+ }
+}