feat(mirada): reglas de ventana — escritorio y flotante por app_id
mirada-brain::rules — config declarativa que decide qué hacer con una ventana al abrirse, mismo patrón que el keymap. - Rule casa por subcadena de app_id y/o title (sin distinguir mayúsculas; vacío = cualquiera) y aplica un destino: workspace (1..9) y/o floating. Gana la primera regla que case. - Rules en RON (~/.config/mirada/rules.ron); la primera vez se escribe una plantilla con ejemplos comentados, si está corrupta se ignora. - Desktop consulta Rules::resolve en cada WindowOpened — el evento ya trae app_id/title — y abre la ventana en su escritorio, flotando si toca. set_rules en Desktop; las apps cargan rules.ron al arrancar. mirada-brain 42->51 tests. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -59,7 +59,9 @@ use smithay::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use mirada_body::{BodyOp, BodyState};
|
use mirada_body::{BodyOp, BodyState};
|
||||||
use mirada_brain::{BodyEvent, BrainCommand, CtlReply, CtlRequest, CtlServer, Desktop, Keymap};
|
use mirada_brain::{
|
||||||
|
BodyEvent, BrainCommand, CtlReply, CtlRequest, CtlServer, Desktop, Keymap, Rules,
|
||||||
|
};
|
||||||
use mirada_link::BodyLink;
|
use mirada_link::BodyLink;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------
|
// ---------------------------------------------------------------------
|
||||||
@@ -469,6 +471,14 @@ fn send_frames_surface_tree(surface: &WlSurface, time: u32) {
|
|||||||
// Bucle principal
|
// Bucle principal
|
||||||
// ---------------------------------------------------------------------
|
// ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Carga las reglas de ventana del usuario, o ninguna si no hay archivo.
|
||||||
|
fn load_user_rules() -> Rules {
|
||||||
|
match Rules::default_path() {
|
||||||
|
Some(p) => Rules::load_or_default(&p),
|
||||||
|
None => Rules::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn run() -> Result<(), Box<dyn std::error::Error>> {
|
fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let mut display: Display<App> = Display::new()?;
|
let mut display: Display<App> = Display::new()?;
|
||||||
let dh = display.handle();
|
let dh = display.handle();
|
||||||
@@ -494,7 +504,9 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
Some(p) => Keymap::load_or_init(p),
|
Some(p) => Keymap::load_or_init(p),
|
||||||
None => Keymap::default(),
|
None => Keymap::default(),
|
||||||
};
|
};
|
||||||
Brain::Embedded(Desktop::with_keymap(keymap))
|
let mut desktop = Desktop::with_keymap(keymap);
|
||||||
|
desktop.set_rules(load_user_rules());
|
||||||
|
Brain::Embedded(desktop)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ use gpui::{
|
|||||||
};
|
};
|
||||||
use mirada_brain::{
|
use mirada_brain::{
|
||||||
BodyEvent, BrainCommand, CtlConn, CtlReply, CtlRequest, CtlServer, Desktop, DesktopAction,
|
BodyEvent, BrainCommand, CtlConn, CtlReply, CtlRequest, CtlServer, Desktop, DesktopAction,
|
||||||
Keymap, KeymapWatch, LayoutMode, WindowId, WindowPlacement,
|
Keymap, KeymapWatch, LayoutMode, Rules, WindowId, WindowPlacement,
|
||||||
};
|
};
|
||||||
use mirada_link::BrainLink;
|
use mirada_link::BrainLink;
|
||||||
use nahual_launcher::launch_app;
|
use nahual_launcher::launch_app;
|
||||||
@@ -105,8 +105,13 @@ impl Mirada {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Reglas de ventana (~/.config/mirada/rules.ron): a qué
|
||||||
|
// escritorio va cada ventana, si flota.
|
||||||
|
let mut desktop = Desktop::with_keymap(keymap);
|
||||||
|
desktop.set_rules(load_user_rules());
|
||||||
|
|
||||||
let mut app = Self {
|
let mut app = Self {
|
||||||
desktop: Desktop::with_keymap(keymap),
|
desktop,
|
||||||
placements: Vec::new(),
|
placements: Vec::new(),
|
||||||
next_id: 1,
|
next_id: 1,
|
||||||
link,
|
link,
|
||||||
@@ -316,6 +321,14 @@ fn connect_body() -> Option<BrainLink> {
|
|||||||
BrainLink::connect(&path).ok()
|
BrainLink::connect(&path).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Carga las reglas de ventana del usuario, o ninguna si no hay archivo.
|
||||||
|
fn load_user_rules() -> Rules {
|
||||||
|
match Rules::default_path() {
|
||||||
|
Some(p) => Rules::load_or_default(&p),
|
||||||
|
None => Rules::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Nombre legible de un modo de teselado.
|
/// Nombre legible de un modo de teselado.
|
||||||
fn mode_name(m: LayoutMode) -> &'static str {
|
fn mode_name(m: LayoutMode) -> &'static str {
|
||||||
match m {
|
match m {
|
||||||
|
|||||||
@@ -61,8 +61,8 @@ ejecuta operaciones de geometría".
|
|||||||
`placements(&Workspace, Rect)`.
|
`placements(&Workspace, Rect)`.
|
||||||
- **`mirada-brain`** — `Desktop`: salidas, 9 escritorios virtuales,
|
- **`mirada-brain`** — `Desktop`: salidas, 9 escritorios virtuales,
|
||||||
registro de ventanas. `on_event(BodyEvent) -> Vec<BrainCommand>`;
|
registro de ventanas. `on_event(BodyEvent) -> Vec<BrainCommand>`;
|
||||||
`DesktopAction` + `Keymap` configurable. `set_keymap` lo cambia en
|
`DesktopAction` + `Keymap` configurable (`set_keymap` en caliente) +
|
||||||
caliente y devuelve el `GrabKeys` a reenviar.
|
`Rules` de ventana.
|
||||||
- **`mirada-link`** — `Link<Out,In>` sobre socket Unix; hilo lector de
|
- **`mirada-link`** — `Link<Out,In>` sobre socket Unix; hilo lector de
|
||||||
fondo + canal `mpsc` para sondeo no bloqueante. `BrainLink`/`BodyLink`,
|
fondo + canal `mpsc` para sondeo no bloqueante. `BrainLink`/`BodyLink`,
|
||||||
`connected_pair` (socketpair), `connect`/`listen` por ruta.
|
`connected_pair` (socketpair), `connect`/`listen` por ruta.
|
||||||
@@ -137,6 +137,20 @@ que un front-end (`Keybind` → `lookup` → `apply`); hay otros tres:
|
|||||||
`cargo run -p mirada-brain --example headless-ctl` levanta un Cerebro sin
|
`cargo run -p mirada-brain --example headless-ctl` levanta un Cerebro sin
|
||||||
gráficos para ejercitar `mirada-ctl` en modo desatendido.
|
gráficos para ejercitar `mirada-ctl` en modo desatendido.
|
||||||
|
|
||||||
|
## Reglas de ventana
|
||||||
|
|
||||||
|
`mirada-brain::rules` decide qué hacer con una ventana **al abrirse**:
|
||||||
|
config declarativa en RON (`~/.config/mirada/rules.ron`), mismo patrón
|
||||||
|
que el keymap. Cada `Rule` casa por subcadena de `app_id` y/o `title`
|
||||||
|
(sin distinguir mayúsculas; vacío = cualquiera) y aplica un destino:
|
||||||
|
`workspace` (1..9) y/o `floating`. Gana la primera regla que case.
|
||||||
|
|
||||||
|
El `Desktop` consulta `Rules::resolve` en cada `WindowOpened` — el evento
|
||||||
|
ya trae `app_id`/`title` — y manda la ventana a su escritorio, flotando
|
||||||
|
si toca. Se carga al arrancar (la primera vez se escribe una plantilla
|
||||||
|
con ejemplos comentados); las reglas afectan a las ventanas futuras, no
|
||||||
|
a las ya abiertas.
|
||||||
|
|
||||||
## Dependencias
|
## Dependencias
|
||||||
|
|
||||||
- Todos los `lib` con `#![forbid(unsafe_code)]`. Cero Wayland, cero
|
- Todos los `lib` con `#![forbid(unsafe_code)]`. Cero Wayland, cero
|
||||||
@@ -146,7 +160,7 @@ gráficos para ejercitar `mirada-ctl` en modo desatendido.
|
|||||||
## Estado
|
## Estado
|
||||||
|
|
||||||
Implementado y verde: `mirada-layout` (32 tests), `mirada-protocol`
|
Implementado y verde: `mirada-layout` (32 tests), `mirada-protocol`
|
||||||
(10), `mirada-brain` (42), `mirada-link` (7), `mirada-body` (14), las
|
(10), `mirada-brain` (51), `mirada-link` (7), `mirada-body` (14), las
|
||||||
apps `mirada` y `mirada-compositor` (compilan; verificación visual
|
apps `mirada` y `mirada-compositor` (compilan; verificación visual
|
||||||
manual) y `mirada-ctl` (CLI, probado vía el ejemplo `headless-ctl`).
|
manual) y `mirada-ctl` (CLI, probado vía el ejemplo `headless-ctl`).
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use mirada_protocol::{placements, BodyEvent, BrainCommand, OutputId};
|
|||||||
|
|
||||||
use crate::action::{DesktopAction, WORKSPACE_COUNT};
|
use crate::action::{DesktopAction, WORKSPACE_COUNT};
|
||||||
use crate::keymap::Keymap;
|
use crate::keymap::Keymap;
|
||||||
|
use crate::rules::Rules;
|
||||||
|
|
||||||
/// Lo que el Cerebro sabe de una ventana: su identidad de aplicación.
|
/// Lo que el Cerebro sabe de una ventana: su identidad de aplicación.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||||
@@ -35,6 +36,8 @@ pub struct Desktop {
|
|||||||
windows: HashMap<WindowId, WindowInfo>,
|
windows: HashMap<WindowId, WindowInfo>,
|
||||||
/// Atajos globales → acción. Configurable, recargable en caliente.
|
/// Atajos globales → acción. Configurable, recargable en caliente.
|
||||||
keymap: Keymap,
|
keymap: Keymap,
|
||||||
|
/// Reglas de ventana — escritorio/flotante por `app_id`/título.
|
||||||
|
rules: Rules,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Desktop {
|
impl Default for Desktop {
|
||||||
@@ -62,9 +65,16 @@ impl Desktop {
|
|||||||
active: 0,
|
active: 0,
|
||||||
windows: HashMap::new(),
|
windows: HashMap::new(),
|
||||||
keymap,
|
keymap,
|
||||||
|
rules: Rules::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reemplaza las reglas de ventana. Se aplican a las ventanas que se
|
||||||
|
/// abran a partir de ahora; las ya abiertas no se tocan.
|
||||||
|
pub fn set_rules(&mut self, rules: Rules) {
|
||||||
|
self.rules = rules;
|
||||||
|
}
|
||||||
|
|
||||||
/// El comando que registra los atajos globales en el Cuerpo. La app
|
/// El comando que registra los atajos globales en el Cuerpo. La app
|
||||||
/// lo envía al conectar, y de nuevo tras cada recarga del keymap.
|
/// lo envía al conectar, y de nuevo tras cada recarga del keymap.
|
||||||
pub fn grab_keys(&self) -> BrainCommand {
|
pub fn grab_keys(&self) -> BrainCommand {
|
||||||
@@ -103,8 +113,21 @@ impl Desktop {
|
|||||||
self.relayout()
|
self.relayout()
|
||||||
}
|
}
|
||||||
BodyEvent::WindowOpened { id, app_id, title } => {
|
BodyEvent::WindowOpened { id, app_id, title } => {
|
||||||
|
// Las reglas pueden mandarla a otro escritorio o hacerla flotar.
|
||||||
|
let outcome = self.rules.resolve(&app_id, &title);
|
||||||
self.windows.insert(id, WindowInfo { app_id, title });
|
self.windows.insert(id, WindowInfo { app_id, title });
|
||||||
self.workspaces[self.active].add(id);
|
let ws = outcome
|
||||||
|
.workspace
|
||||||
|
.filter(|&n| n < self.workspaces.len())
|
||||||
|
.unwrap_or(self.active);
|
||||||
|
self.workspaces[ws].add(id);
|
||||||
|
if outcome.floating {
|
||||||
|
let rect = self
|
||||||
|
.screen()
|
||||||
|
.map(centered_float_rect)
|
||||||
|
.unwrap_or_else(|| Rect::new(100, 100, 800, 600));
|
||||||
|
self.workspaces[ws].set_floating(id, Some(rect));
|
||||||
|
}
|
||||||
self.relayout()
|
self.relayout()
|
||||||
}
|
}
|
||||||
BodyEvent::WindowClosed { id } => {
|
BodyEvent::WindowClosed { id } => {
|
||||||
@@ -452,6 +475,24 @@ mod tests {
|
|||||||
assert!(!places(&cmds).iter().find(|x| x.id == 2).unwrap().floating);
|
assert!(!places(&cmds).iter().find(|x| x.id == 2).unwrap().floating);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_rule_sends_a_new_window_to_its_workspace() {
|
||||||
|
let mut d = desktop_with_screen();
|
||||||
|
d.set_rules(Rules::from_ron(r#"( rules: [ (app_id: "app2", workspace: 3) ] )"#).unwrap());
|
||||||
|
open(&mut d, 1); // app1 → sin regla → escritorio activo (1)
|
||||||
|
open(&mut d, 2); // app2 → regla → escritorio 3
|
||||||
|
assert_eq!(d.workspace_loads()[0], 1);
|
||||||
|
assert_eq!(d.workspace_loads()[2], 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_rule_can_open_a_window_floating() {
|
||||||
|
let mut d = desktop_with_screen();
|
||||||
|
d.set_rules(Rules::from_ron(r#"( rules: [ (app_id: "app1", floating: true) ] )"#).unwrap());
|
||||||
|
let cmds = open(&mut d, 1);
|
||||||
|
assert!(places(&cmds).iter().find(|p| p.id == 1).unwrap().floating);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn without_a_screen_nothing_is_placed() {
|
fn without_a_screen_nothing_is_placed() {
|
||||||
let mut d = Desktop::new();
|
let mut d = Desktop::new();
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
//! - [`action`] — las acciones de escritorio y el mapa de teclas.
|
//! - [`action`] — las acciones de escritorio y el mapa de teclas.
|
||||||
//! - [`desktop`] — el [`Desktop`]: el estado y el bucle `evento → comandos`.
|
//! - [`desktop`] — el [`Desktop`]: el estado y el bucle `evento → comandos`.
|
||||||
//! - [`keymap`] — el [`Keymap`] configurable en RON, recargable en caliente.
|
//! - [`keymap`] — el [`Keymap`] configurable en RON, recargable en caliente.
|
||||||
|
//! - [`rules`] — las [`Rules`] de ventana (escritorio/flotante por `app_id`).
|
||||||
//! - [`ctl`] — el API de control externo (`mirada-ctl`, taskbars, scripts).
|
//! - [`ctl`] — el API de control externo (`mirada-ctl`, taskbars, scripts).
|
||||||
|
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
@@ -21,11 +22,13 @@ pub mod action;
|
|||||||
pub mod ctl;
|
pub mod ctl;
|
||||||
pub mod desktop;
|
pub mod desktop;
|
||||||
pub mod keymap;
|
pub mod keymap;
|
||||||
|
pub mod rules;
|
||||||
|
|
||||||
pub use action::{default_keymap, DesktopAction, WORKSPACE_COUNT};
|
pub use action::{default_keymap, DesktopAction, WORKSPACE_COUNT};
|
||||||
pub use ctl::{CtlConn, CtlReply, CtlRequest, CtlServer, WindowLine};
|
pub use ctl::{CtlConn, CtlReply, CtlRequest, CtlServer, WindowLine};
|
||||||
pub use desktop::{Desktop, WindowInfo};
|
pub use desktop::{Desktop, WindowInfo};
|
||||||
pub use keymap::{Keymap, KeymapError, KeymapWatch};
|
pub use keymap::{Keymap, KeymapError, KeymapWatch};
|
||||||
|
pub use rules::{Rule, RuleOutcome, Rules};
|
||||||
|
|
||||||
pub use mirada_layout::{LayoutMode, LayoutParams, Rect, WindowId, Workspace};
|
pub use mirada_layout::{LayoutMode, LayoutParams, Rect, WindowId, Workspace};
|
||||||
pub use mirada_protocol::{BodyEvent, BrainCommand, OutputId, WindowPlacement};
|
pub use mirada_protocol::{BodyEvent, BrainCommand, OutputId, WindowPlacement};
|
||||||
|
|||||||
@@ -0,0 +1,211 @@
|
|||||||
|
//! Reglas de ventana — config declarativa que decide, al abrirse una
|
||||||
|
//! ventana, a qué escritorio va y si flota.
|
||||||
|
//!
|
||||||
|
//! Mismo patrón que [`crate::keymap`]: RON de texto en
|
||||||
|
//! `~/.config/mirada/rules.ron`, que el [`Desktop`](crate::Desktop)
|
||||||
|
//! consulta en cada `WindowOpened` — el evento ya trae `app_id` y
|
||||||
|
//! `title`. Una regla casa por subcadena (sin distinguir mayúsculas);
|
||||||
|
//! gana la primera que case.
|
||||||
|
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// Una regla: criterio de coincidencia + qué aplicar.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||||
|
pub struct Rule {
|
||||||
|
/// Subcadena que debe contener el `app_id`; vacía = casa con cualquiera.
|
||||||
|
#[serde(default)]
|
||||||
|
pub app_id: String,
|
||||||
|
/// Subcadena que debe contener el título; vacía = cualquiera.
|
||||||
|
#[serde(default)]
|
||||||
|
pub title: String,
|
||||||
|
/// Escritorio de destino (1-based); `0` = no moverla.
|
||||||
|
#[serde(default)]
|
||||||
|
pub workspace: usize,
|
||||||
|
/// Abrir la ventana flotando.
|
||||||
|
#[serde(default)]
|
||||||
|
pub floating: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Rule {
|
||||||
|
/// `true` si la regla casa con una ventana de este `app_id`/`title`.
|
||||||
|
fn matches(&self, app_id: &str, title: &str) -> bool {
|
||||||
|
let app_ok = self.app_id.is_empty() || contains_ci(app_id, &self.app_id);
|
||||||
|
let title_ok = self.title.is_empty() || contains_ci(title, &self.title);
|
||||||
|
app_ok && title_ok
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `true` si `haystack` contiene `needle`, sin distinguir mayúsculas.
|
||||||
|
fn contains_ci(haystack: &str, needle: &str) -> bool {
|
||||||
|
haystack.to_lowercase().contains(&needle.to_lowercase())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Qué hacer con una ventana recién abierta, según las reglas.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub struct RuleOutcome {
|
||||||
|
/// Escritorio de destino, ya como índice 0-based. `None` = el activo.
|
||||||
|
pub workspace: Option<usize>,
|
||||||
|
/// Abrir flotando.
|
||||||
|
pub floating: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// El conjunto de reglas de ventana del usuario.
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct Rules {
|
||||||
|
#[serde(default)]
|
||||||
|
rules: Vec<Rule>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Rules {
|
||||||
|
/// Construye un conjunto de reglas a partir de una lista.
|
||||||
|
pub fn new(rules: Vec<Rule>) -> Self {
|
||||||
|
Self { rules }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resuelve qué hacer con una ventana — gana la primera regla que case.
|
||||||
|
pub fn resolve(&self, app_id: &str, title: &str) -> RuleOutcome {
|
||||||
|
for r in &self.rules {
|
||||||
|
if r.matches(app_id, title) {
|
||||||
|
return RuleOutcome {
|
||||||
|
workspace: (r.workspace >= 1).then(|| r.workspace - 1),
|
||||||
|
floating: r.floating,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RuleOutcome::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cuántas reglas hay.
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.rules.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `true` si no hay ninguna regla.
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.rules.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parsea las reglas desde el texto RON de un archivo de config.
|
||||||
|
pub fn from_ron(text: &str) -> Result<Rules, String> {
|
||||||
|
ron::from_str(text).map_err(|e| format!("RON inválido: {e}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// La ruta canónica de las reglas: `~/.config/mirada/rules.ron`.
|
||||||
|
pub fn default_path() -> Option<PathBuf> {
|
||||||
|
directories::ProjectDirs::from("", "", "mirada")
|
||||||
|
.map(|d| d.config_dir().join("rules.ron"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Carga las reglas de un archivo RON.
|
||||||
|
pub fn load(path: &Path) -> Result<Rules, String> {
|
||||||
|
let text = std::fs::read_to_string(path).map_err(|e| format!("E/S: {e}"))?;
|
||||||
|
Rules::from_ron(&text)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Carga las reglas del usuario con un fallback amable: si el archivo
|
||||||
|
/// no existe, escribe una plantilla documentada y devuelve un
|
||||||
|
/// conjunto vacío; si está corrupto, avisa y devuelve vacío.
|
||||||
|
pub fn load_or_default(path: &Path) -> Rules {
|
||||||
|
if path.exists() {
|
||||||
|
match Rules::load(path) {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!(
|
||||||
|
"mirada · reglas «{}» inválidas ({e}); las ignoro.",
|
||||||
|
path.display()
|
||||||
|
);
|
||||||
|
Rules::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let Some(dir) = path.parent() {
|
||||||
|
let _ = std::fs::create_dir_all(dir);
|
||||||
|
}
|
||||||
|
match std::fs::write(path, RULES_TEMPLATE) {
|
||||||
|
Ok(()) => eprintln!("mirada · plantilla de reglas escrita en {}", path.display()),
|
||||||
|
Err(e) => eprintln!("mirada · no pude escribir la plantilla de reglas: {e}"),
|
||||||
|
}
|
||||||
|
Rules::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// La plantilla que se escribe la primera vez — sin reglas, con ejemplos
|
||||||
|
/// comentados para que el usuario los descubra.
|
||||||
|
const RULES_TEMPLATE: &str = "\
|
||||||
|
// Reglas de ventana de mirada — qué hacer con una ventana al abrirse.
|
||||||
|
//
|
||||||
|
// Cada regla casa por subcadena de `app_id` y/o `title` (sin distinguir
|
||||||
|
// mayúsculas; cadena vacía = cualquiera) y aplica un destino: `workspace`
|
||||||
|
// (1..9; 0 = no mover) y/o `floating`. Gana la primera regla que case.
|
||||||
|
//
|
||||||
|
// Descomenta y edita los ejemplos:
|
||||||
|
(
|
||||||
|
rules: [
|
||||||
|
// (app_id: \"pavucontrol\", floating: true),
|
||||||
|
// (app_id: \"firefox\", workspace: 2),
|
||||||
|
// (title: \"Picture-in-Picture\", floating: true),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
";
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_template_parses_to_an_empty_rule_set() {
|
||||||
|
assert!(Rules::from_ron(RULES_TEMPLATE).unwrap().is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rules_parse_from_ron_with_omitted_fields() {
|
||||||
|
let ron = r#"(
|
||||||
|
rules: [
|
||||||
|
(app_id: "pavucontrol", floating: true),
|
||||||
|
(app_id: "firefox", workspace: 2),
|
||||||
|
],
|
||||||
|
)"#;
|
||||||
|
assert_eq!(Rules::from_ron(ron).unwrap().len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_sends_a_match_to_its_workspace() {
|
||||||
|
let r = Rules::from_ron(r#"( rules: [ (app_id: "firefox", workspace: 3) ] )"#).unwrap();
|
||||||
|
let out = r.resolve("org.mozilla.firefox", "");
|
||||||
|
assert_eq!(out.workspace, Some(2)); // 3 (1-based) -> índice 2
|
||||||
|
assert!(!out.floating);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_matches_app_id_case_insensitively_by_substring() {
|
||||||
|
let r = Rules::from_ron(r#"( rules: [ (app_id: "FIREFOX", floating: true) ] )"#).unwrap();
|
||||||
|
assert!(r.resolve("org.mozilla.firefox", "").floating);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_matches_by_title() {
|
||||||
|
let r =
|
||||||
|
Rules::from_ron(r#"( rules: [ (title: "Picture-in-Picture", floating: true) ] )"#)
|
||||||
|
.unwrap();
|
||||||
|
assert!(r.resolve("cualquiera", "YouTube — Picture-in-Picture").floating);
|
||||||
|
assert!(!r.resolve("cualquiera", "ventana normal").floating);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_first_matching_rule_wins() {
|
||||||
|
let r = Rules::from_ron(
|
||||||
|
r#"( rules: [ (app_id: "term", workspace: 1), (app_id: "term", workspace: 5) ] )"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(r.resolve("term", "").workspace, Some(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_match_yields_the_default_outcome() {
|
||||||
|
let r = Rules::from_ron(r#"( rules: [ (app_id: "firefox", workspace: 2) ] )"#).unwrap();
|
||||||
|
assert_eq!(r.resolve("xterm", ""), RuleOutcome::default());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1056,5 +1056,14 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Reglas de ventana — mirada-brain::rules:
|
||||||
|
Config declarativa en RON (~/.config/mirada/rules.ron); el Desktop la consulta en cada
|
||||||
|
WindowOpened y manda la ventana a su escritorio / la hace flotar.
|
||||||
|
Cada regla casa por subcadena de app_id y/o title (sin mayúsculas); gana la primera.
|
||||||
|
Ejemplo: (app_id: "firefox", workspace: 2) · (title: "Picture-in-Picture", floating: true)
|
||||||
|
La primera vez se escribe una plantilla con ejemplos comentados.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user