use bevy::ecs::resource::Resource;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Resource)]
pub enum SnippetDialect {
#[default]
Toml,
Lua,
}
#[derive(Clone, Copy, Debug)]
pub struct RuleSubject<'a> {
pub app_name: &'a str,
pub bundle_id: &'a str,
pub title: &'a str,
pub role: &'a str,
pub subrole: &'a str,
}
#[must_use]
pub fn window_rule_snippet(dialect: SnippetDialect, subject: &RuleSubject<'_>) -> String {
let key = rule_key(subject.app_name);
let title = title_pattern(subject.title);
let wildcard_alternative = !subject.title.is_empty();
let identity = identity_comment(subject);
let bundle_id = quote(subject.bundle_id);
let mut lines = Vec::new();
match dialect {
SnippetDialect::Toml => {
lines.push(format!("[windows.{key}]"));
if subject.bundle_id.is_empty() {
lines.push("# bundle_id = \"\" # unknown; rule matches every app".to_owned());
} else {
lines.push(format!("bundle_id = \"{bundle_id}\""));
}
lines.push(format!("title = \"{title}\""));
if wildcard_alternative {
lines.push("# title = \".*\" # all windows of this app".to_owned());
}
lines.push(format!("# {identity}"));
lines.push("# floating = true".to_owned());
lines.push("# manage = true".to_owned());
lines.push("# width = 0.5".to_owned());
}
SnippetDialect::Lua => {
lines.push("windows = {".to_owned());
lines.push(format!(" {key} = {{"));
if subject.bundle_id.is_empty() {
lines.push(
" -- bundle_id = \"\", -- unknown; rule matches every app".to_owned(),
);
} else {
lines.push(format!(" bundle_id = \"{bundle_id}\","));
}
lines.push(format!(" title = \"{title}\","));
if wildcard_alternative {
lines.push(" -- title = \".*\", -- all windows of this app".to_owned());
}
lines.push(format!(" -- {identity}"));
lines.push(" -- floating = true,".to_owned());
lines.push(" -- manage = true,".to_owned());
lines.push(" -- width = 0.5,".to_owned());
lines.push(" },".to_owned());
lines.push("}".to_owned());
}
}
lines.push(String::new());
lines.join("\n")
}
fn rule_key(app_name: &str) -> String {
let mut key = String::with_capacity(app_name.len() + 4);
let mut first_alnum = true;
for character in app_name.chars() {
if first_alnum {
if character.is_whitespace() || !character.is_alphanumeric() {
continue;
}
first_alnum = false;
match character {
'0' => key.push_str("zero"),
'1' => key.push_str("one"),
'2' => key.push_str("two"),
'3' => key.push_str("three"),
'4' => key.push_str("four"),
'5' => key.push_str("five"),
'6' => key.push_str("six"),
'7' => key.push_str("seven"),
'8' => key.push_str("eight"),
'9' => key.push_str("nine"),
c if c.is_ascii_alphanumeric() => key.push(c.to_ascii_lowercase()),
_ => {}
}
continue;
}
if character.is_ascii_alphanumeric() {
key.push(character.to_ascii_lowercase());
} else if !key.ends_with('_') {
key.push('_');
}
}
let trimmed = key.trim_matches('_');
if trimmed.is_empty() {
"window".to_owned()
} else {
trimmed.to_owned()
}
}
fn title_pattern(title: &str) -> String {
if title.is_empty() {
return ".*".to_owned();
}
quote(&format!("^{}$", regex::escape(title)))
}
fn quote(text: &str) -> String {
let mut out = String::with_capacity(text.len());
for character in text.chars() {
match character {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
'\t' => out.push_str("\\t"),
'\r' => out.push_str("\\r"),
_ => out.push(character),
}
}
out
}
fn identity_comment(subject: &RuleSubject<'_>) -> String {
let name = if subject.app_name.is_empty() {
"?"
} else {
subject.app_name
};
let role = if subject.role.is_empty() {
"?"
} else {
subject.role
};
let subrole = if subject.subrole.is_empty() {
"?"
} else {
subject.subrole
};
format!("app: {name} role: {role} subrole: {subrole}")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::InnerConfig;
fn subject<'a>(app_name: &'a str, bundle_id: &'a str, title: &'a str) -> RuleSubject<'a> {
RuleSubject {
app_name,
bundle_id,
title,
role: "AXWindow",
subrole: "AXStandardWindow",
}
}
#[test]
fn toml_snippet_has_both_matchers() {
let snippet = window_rule_snippet(
SnippetDialect::Toml,
&subject("Ghostty", "com.mitchellh.ghostty", "paneru"),
);
assert_eq!(
snippet,
concat!(
"[windows.ghostty]\n",
"bundle_id = \"com.mitchellh.ghostty\"\n",
"title = \"^paneru$\"\n",
"# title = \".*\" # all windows of this app\n",
"# app: Ghostty role: AXWindow subrole: AXStandardWindow\n",
"# floating = true\n",
"# manage = true\n",
"# width = 0.5\n",
)
);
}
#[test]
fn lua_snippet_has_both_matchers() {
let snippet = window_rule_snippet(
SnippetDialect::Lua,
&subject("Ghostty", "com.mitchellh.ghostty", "paneru"),
);
assert_eq!(
snippet,
concat!(
"windows = {\n",
" ghostty = {\n",
" bundle_id = \"com.mitchellh.ghostty\",\n",
" title = \"^paneru$\",\n",
" -- title = \".*\", -- all windows of this app\n",
" -- app: Ghostty role: AXWindow subrole: AXStandardWindow\n",
" -- floating = true,\n",
" -- manage = true,\n",
" -- width = 0.5,\n",
" },\n",
"}\n",
)
);
}
#[test]
fn generated_toml_matches_the_window_it_came_from() {
let title = r#"Find "a.b" (2) \ [x]"#;
let bundle_id = "com.apple.Terminal";
let snippet = window_rule_snippet(SnippetDialect::Toml, &subject("Term", bundle_id, title));
let config = InnerConfig::new(&snippet).expect("snippet parses as TOML config");
let rules = config
.windows
.as_ref()
.expect("snippet defines a windows table");
let rule = rules.values().next().expect("exactly one rule");
assert!(rule.title.is_match(title));
assert_eq!(rule.bundle_id.as_deref(), Some(bundle_id));
}
#[test]
fn anchored_pattern_rejects_a_longer_title() {
let snippet = window_rule_snippet(SnippetDialect::Toml, &subject("Term", "com.x", "build"));
let config = InnerConfig::new(&snippet).expect("snippet parses as TOML config");
let rules = config.windows.as_ref().expect("windows table");
let rule = rules.values().next().expect("exactly one rule");
assert!(rule.title.is_match("build"));
assert!(!rule.title.is_match("rebuild all"));
}
#[test]
fn empty_title_falls_back_to_wildcard() {
let snippet = window_rule_snippet(SnippetDialect::Toml, &subject("Term", "com.x", ""));
assert!(snippet.contains("title = \".*\"\n"));
assert!(!snippet.contains("# title = \".*\""));
}
#[test]
fn empty_bundle_id_is_commented_out() {
let snippet = window_rule_snippet(SnippetDialect::Toml, &subject("Term", "", "hello"));
assert!(snippet.contains("# bundle_id = \"\" # unknown; rule matches every app\n"));
let config = InnerConfig::new(&snippet).expect("snippet parses as TOML config");
let rules = config.windows.as_ref().expect("windows table");
assert!(rules.values().next().expect("one rule").bundle_id.is_none());
}
#[test]
fn rule_keys_are_sanitized() {
assert_eq!(rule_key("Ghostty"), "ghostty");
assert_eq!(rule_key("Visual Studio Code"), "visual_studio_code");
assert_eq!(rule_key("1Password 8"), "onepassword_8");
assert_eq!(rule_key("7-Zip"), "seven_zip");
assert_eq!(rule_key(" — "), "window");
assert_eq!(rule_key(""), "window");
}
}