diff --git a/Cargo.lock b/Cargo.lock index 8c06d0a..af0133a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloca" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4" +dependencies = [ + "cc", +] + [[package]] name = "anes" version = "0.1.6" @@ -50,6 +59,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" +[[package]] +name = "cc" +version = "1.2.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -119,25 +138,24 @@ dependencies = [ [[package]] name = "criterion" -version = "0.5.1" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +checksum = "4d883447757bb0ee46f233e9dc22eb84d93a9508c9b868687b274fc431d886bf" dependencies = [ + "alloca", "anes", "cast", "ciborium", "clap", "criterion-plot", - "is-terminal", "itertools", "num-traits", - "once_cell", "oorandom", + "page_size", "plotters", "rayon", "regex", "serde", - "serde_derive", "serde_json", "tinytemplate", "walkdir", @@ -145,9 +163,9 @@ dependencies = [ [[package]] name = "criterion-plot" -version = "0.5.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +checksum = "ed943f81ea2faa8dcecbbfa50164acf95d555afec96a27871663b300e387b2e4" dependencies = [ "cast", "itertools", @@ -210,6 +228,12 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "find-msvc-tools" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" + [[package]] name = "generic-array" version = "0.14.9" @@ -231,37 +255,20 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - [[package]] name = "hyprlang" -version = "0.3.0" +version = "0.4.0" dependencies = [ "criterion", "pest", "pest_derive", ] -[[package]] -name = "is-terminal" -version = "0.4.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys", -] - [[package]] name = "itertools" -version = "0.10.5" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] @@ -315,6 +322,16 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "pest" version = "2.8.4" @@ -530,6 +547,12 @@ dependencies = [ "digest", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "syn" version = "2.0.110" @@ -640,6 +663,22 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.11" @@ -649,6 +688,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-link" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index 9360ac3..310e6bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hyprlang" -version = "0.3.0" +version = "0.4.0" edition = "2024" authors = ["Alex Spinu"] description = "A scripting language interpreter and parser for Hyprlang and Hyprland configuration files." @@ -25,7 +25,7 @@ name = "hyprlang" path = "src/lib.rs" [dev-dependencies] -criterion = { version = "0.5", features = ["html_reports"] } +criterion = { version = "0.8.1", features = ["html_reports"] } [[bench]] name = "parsing" diff --git a/README.md b/README.md index a3f6765..f0407ab 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,13 @@ This project is not endorsed by or affiliated with the Hyprland project/HyprWM O ![Crates.io Size](https://img.shields.io/crates/size/hyprlang) [![GitHub Sponsors](https://img.shields.io/github/sponsors/spinualexandru)](https://github.com/sponsors/spinualexandru) +Parity: +- For Hyprland < 0.53.0, use hyprlang-rs v0.3.x +- For Hyprland >= 0.53.0, use hyprlang-rs v0.4.x + ## Features -- 🎯 **Complete Hyprlang Implementation** - Full compatibility with the original C++ version +- 🎯 **Complete Hyprlang Implementation** - Full compatibility with the original C++ version and Hyprland 0.53.0 - 🚀 **Fast PEG Parser** - Built with [pest](https://pest.rs/) for efficient parsing - 🔧 **Type-Safe API** - Strongly-typed configuration values (Int, Float, String, Vec2, Color) - 📦 **Variable System** - Support for user-defined and environment variables with cycle detection @@ -31,7 +35,7 @@ This project is not endorsed by or affiliated with the Hyprland project/HyprWM O - 🔄 **Mutation & Serialization** - Modify config values and save back to files (optional) - 📁 **Multi-File Mutation Tracking** - Track and save changes to the correct source file when using `source` directives - 🎯 **Windowrule v3 / Layerrule v2** - Full support for new special category syntax with 85+ registered properties -- ✅ **Fully Tested** - 177 tests covering all features +- ✅ **Fully Tested** - 207 tests covering all features ## Benchmarks @@ -47,7 +51,7 @@ Add this to your `Cargo.toml`: ```toml [dependencies] -hyprlang = "0.3.0" +hyprlang = "0.4.0" ``` ### Optional Features @@ -58,7 +62,7 @@ Enable the `hyprland` feature to get a high-level `Hyprland` struct with pre-con ```toml [dependencies] -hyprlang = { version = "0.3.0", features = ["hyprland"] } +hyprlang = { version = "0.4.0", features = ["hyprland"] } ``` This feature provides: @@ -72,7 +76,7 @@ Enable the `mutation` feature to modify configuration values and serialize confi ```toml [dependencies] -hyprlang = { version = "0.3.0", features = ["mutation"] } +hyprlang = { version = "0.4.0", features = ["mutation"] } ``` This feature provides: @@ -246,6 +250,7 @@ hypr.general_layout() -> Result<&str> // "dwindle" or "master" hypr.general_allow_tearing() -> Result hypr.general_active_border_color() -> Result hypr.general_inactive_border_color() -> Result +hypr.general_locale() -> Result<&str> // Locale override (new in 0.53.0) ``` #### Decoration Settings @@ -286,15 +291,31 @@ hypr.misc_disable_hyprland_logo() -> Result hypr.misc_force_default_wallpaper() -> Result ``` +#### Quirks Settings (new in 0.53.0) +```rust +hypr.quirks_prefer_hdr() -> Result // 0=off, 1=always HDR, 2=gamescope only +``` + +#### Cursor Settings (new in 0.53.0) +```rust +hypr.cursor_hide_on_tablet() -> Result // Hide cursor on tablet input +``` + +#### Group Settings (new in 0.53.0) +```rust +hypr.group_groupbar_blur() -> Result // Groupbar blur effect +``` + #### Handler Calls (Arrays) ```rust hypr.all_binds() -> Vec<&String> // All bind definitions +hypr.all_bindu() -> Vec<&String> // All universal submap binds (new in 0.53.0) hypr.all_bindm() -> Vec<&String> // All mouse bindings hypr.all_bindel() -> Vec<&String> // All bindel definitions hypr.all_bindl() -> Vec<&String> // All bindl definitions -hypr.all_windowrules() -> Vec<&String> // All windowrule v1 definitions (deprecated) -hypr.all_windowrulesv2() -> Vec<&String> // All windowrule v2 definitions (deprecated) -hypr.all_layerrules() -> Vec<&String> // All layerrule v1 definitions (deprecated) +hypr.all_windowrules() -> Vec<&String> // All windowrule v1 definitions (DEPRECATED) +hypr.all_windowrulesv2() -> Vec<&String> // All windowrule v2 definitions (DEPRECATED) +hypr.all_layerrules() -> Vec<&String> // All layerrule v1 definitions (DEPRECATED) hypr.all_workspaces() -> Vec<&String> // All workspace definitions hypr.all_monitors() -> Vec<&String> // All monitor definitions hypr.all_env() -> Vec<&String> // All env definitions @@ -346,9 +367,9 @@ The `Hyprland` struct automatically registers these handlers: **Root-level handlers:** - `monitor` - Monitor configuration - `env` - Environment variables -- `bind`, `bindm`, `bindel`, `bindl`, `bindr`, `binde`, `bindn` - Keybindings -- `windowrule`, `windowrulev2` - Window rules -- `layerrule` - Layer rules +- `bind`, `bindu`, `bindm`, `bindel`, `bindl`, `bindr`, `binde`, `bindn` - Keybindings (`bindu` for universal submap binds, new in 0.53.0) +- `windowrule`, `windowrulev2` - Window rules (deprecated, use v3 special category syntax) +- `layerrule` - Layer rules (deprecated, use v2 special category syntax) - `workspace` - Workspace configuration - `exec`, `exec-once` - Commands - `source` - File inclusion @@ -649,10 +670,55 @@ let v2_rules = hypr.all_windowrulesv2(); **Layerrule v2 - Match Properties (6):** - `match:namespace`, `match:address`, `match:class`, `match:title`, `match:monitor`, `match:layer` -**Layerrule v2 - Effect Properties (6):** -- `blur`, `ignorealpha`, `ignorezero`, `animation`, `noanim`, `xray` +**Layerrule v2 - Effect Properties (14, updated in 0.53.0):** +- `blur`, `blur_popups` (new), `ignorealpha`/`ignore_alpha`, `ignorezero`, `animation`, `noanim`/`no_anim`, `xray` +- `dim_around` (new), `order` (new), `above_lock` (new), `no_screen_share`/`noscreenshare` (new) ``` +#### Migration from Deprecated v1/v2 Methods + +The old handler-based methods (`all_windowrules()`, `all_windowrulesv2()`, `all_layerrules()`) are deprecated in hyprlang-rs 0.4.0. Here's how to migrate: + +**Old approach (deprecated):** +```rust +// Returns raw handler strings like "float, class:^(kitty)$" +let rules = hypr.all_windowrulesv2(); +for rule_str in rules { + // Manual parsing required + println!("{}", rule_str); +} +``` + +**New approach (v3 special categories):** +```rust +// Iterate all windowrules with structured access +for name in hypr.windowrule_names() { + let rule = hypr.get_windowrule(&name)?; + + // Type-safe property access + if let Ok(class) = rule.get_string("match:class") { + println!("Rule '{}' matches class: {}", name, class); + } + if let Ok(is_float) = rule.get_int("float") { + println!(" float = {}", is_float == 1); + } +} + +// Same for layerrules +for name in hypr.layerrule_names() { + let rule = hypr.get_layerrule(&name)?; + if let Ok(ns) = rule.get_string("match:namespace") { + println!("Layerrule '{}' matches: {}", name, ns); + } +} +``` + +The new v3 syntax provides: +- **Named rules** - Each rule has an identifier for easy lookup +- **Structured properties** - Access individual properties by key +- **Type safety** - Get values as the correct type (string, int, float, color) +- **Better organization** - Match conditions and effects in one block + ### Source Directive ```hyprlang diff --git a/src/hyprland.rs b/src/hyprland.rs index b2453d8..f66bc05 100644 --- a/src/hyprland.rs +++ b/src/hyprland.rs @@ -363,7 +363,40 @@ use crate::types::{Color, ConfigValue}; use std::collections::HashMap; use std::path::Path; -/// Wrapper around a special category instance with convenient value accessors +/// Wrapper around a windowrule or layerrule instance with type-safe value accessors. +/// +/// This struct provides convenient methods to access properties from windowrule v3 +/// and layerrule v2 special category blocks. +/// +/// # Example +/// +/// ```rust +/// use hyprlang::Hyprland; +/// +/// let mut hypr = Hyprland::new(); +/// hypr.parse(r#" +/// windowrule[float-kitty] { +/// match:class = ^(kitty)$ +/// float = true +/// size = 800 600 +/// opacity = 0.9 +/// border_color = rgba(33ccffee) +/// } +/// "#).unwrap(); +/// +/// let rule = hypr.get_windowrule("float-kitty").unwrap(); +/// +/// // Access different value types +/// let class_pattern = rule.get_string("match:class").unwrap(); +/// let is_floating = rule.get_int("float").unwrap(); // 1 for true +/// let opacity = rule.get_float("opacity").unwrap(); +/// let color = rule.get_color("border_color").unwrap(); +/// +/// assert_eq!(class_pattern, "^(kitty)$"); +/// assert_eq!(is_floating, 1); +/// assert_eq!(opacity, 0.9); +/// assert_eq!(color.r, 51); // 0x33 +/// ``` pub struct RuleInstance<'a> { values: HashMap, } @@ -489,6 +522,7 @@ impl Hyprland { "monitor", "env", "bind", + "bindu", // Universal bind flag for submaps (new in 0.53.0) "bindm", "bindel", "bindl", @@ -569,6 +603,24 @@ impl Hyprland { ); } + // Match property aliases for Hyprland v3 naming (new in 0.53.0) + // These provide alternative names that match Hyprland's actual property names + let match_aliases = [ + "float", // Alias for "floating" + "pin", // Alias for "pinned" + "workspace", // Alias for "on_workspace" + "fullscreen_state_internal", // Alias for "fullscreenstate_internal" + "fullscreen_state_client", // Alias for "fullscreenstate_client" + ]; + + for alias in match_aliases { + config.register_special_category_value( + "windowrule", + format!("match:{}", alias), + ConfigValue::String(String::new()), + ); + } + // Effect properties (60+ from WindowRuleEffectContainer.hpp) // Note: Many properties have aliases (e.g., border_color / bordercolor) let effect_props = [ @@ -578,6 +630,7 @@ impl Hyprland { "fullscreen", "maximize", "fullscreenstate", + "fullscreen_state", // Alias for fullscreenstate (new in 0.53.0) "move", "size", "center", @@ -585,11 +638,14 @@ impl Hyprland { "monitor", "workspace", "noinitialfocus", + "no_initial_focus", // Alias for noinitialfocus (new in 0.53.0) "pin", "group", "suppressevent", + "suppress_event", // Alias for suppressevent (new in 0.53.0) "content", "noclosefor", + "no_close_for", // Alias for noclosefor (new in 0.53.0) // Dynamic effects (continuously applied) "rounding", "rounding_power", @@ -687,12 +743,20 @@ impl Hyprland { // Effect properties for layer surfaces let effect_props = [ - "blur", // Enable blur - "ignorealpha", // Ignore alpha - "ignorezero", // Ignore zero alpha - "animation", // Animation style - "noanim", // Disable animations - "xray", // X-ray mode + "blur", // Enable blur + "blur_popups", // Blur popups (new in 0.53.0) + "ignorealpha", // Ignore alpha + "ignore_alpha", // Alias for ignorealpha (new in 0.53.0) + "ignorezero", // Ignore zero alpha + "animation", // Animation style + "noanim", // Disable animations + "no_anim", // Alias for noanim (new in 0.53.0) + "xray", // X-ray mode + "dim_around", // Dim around layer (new in 0.53.0) + "order", // Layer order (new in 0.53.0) + "above_lock", // Display above lock screen (new in 0.53.0) + "no_screen_share", // Exclude from screen share (new in 0.53.0) + "noscreenshare", // Alias for no_screen_share ]; for prop in effect_props { @@ -753,6 +817,13 @@ impl Hyprland { } } + /// Get general:locale - overrides system locale (new in 0.53.0) + /// + /// Example: "en_US", "es", "de_DE" + pub fn general_locale(&self) -> ParseResult<&str> { + self.config.get_string("general:locale") + } + // ==================== Decoration Config ==================== /// Get decoration:rounding @@ -858,6 +929,37 @@ impl Hyprland { self.config.get_int("misc:force_default_wallpaper") } + // ==================== Quirks Config (new in 0.53.0) ==================== + + /// Get quirks:prefer_hdr - HDR preference (new in 0.53.0) + /// + /// Returns: 0 = off (default), 1 = always report HDR, 2 = gamescope only + pub fn quirks_prefer_hdr(&self) -> ParseResult { + self.config.get_int("quirks:prefer_hdr") + } + + // ==================== Cursor Config ==================== + + /// Get cursor:hide_on_tablet - hides cursor when last input was tablet (new in 0.53.0) + pub fn cursor_hide_on_tablet(&self) -> ParseResult { + match self.config.get("cursor:hide_on_tablet")? { + ConfigValue::Int(i) => Ok(*i != 0), + ConfigValue::String(s) => Ok(s == "true" || s == "yes" || s == "on" || s == "1"), + _ => Ok(false), + } + } + + // ==================== Group Config ==================== + + /// Get group:groupbar:blur - applies blur to groupbar (new in 0.53.0) + pub fn group_groupbar_blur(&self) -> ParseResult { + match self.config.get("group:groupbar:blur")? { + ConfigValue::Int(i) => Ok(*i != 0), + ConfigValue::String(s) => Ok(s == "true" || s == "yes" || s == "on" || s == "1"), + _ => Ok(false), + } + } + // ==================== Dwindle Layout ==================== /// Get dwindle:pseudotile @@ -919,7 +1021,26 @@ impl Hyprland { .unwrap_or_default() } - /// Get all windowrule definitions (v1/v2 handler-based syntax, deprecated) + /// Get all bindu definitions (universal submap bindings, new in 0.53.0) + /// + /// Universal binds remain active across all submaps. + pub fn all_bindu(&self) -> Vec<&String> { + self.config + .get_handler_calls("bindu") + .map(|calls| calls.iter().collect()) + .unwrap_or_default() + } + + /// Get all windowrule definitions (v1 handler-based syntax) + /// + /// **DEPRECATED in Hyprland 0.53.0**: The `windowrule` handler syntax is deprecated. + /// Use the new v3 special category syntax instead: + /// ```conf + /// windowrule[rule-name] { + /// match:class = ^(kitty)$ + /// float = true + /// } + /// ``` /// /// This returns windowrule handler calls from old configs using: /// ```conf @@ -928,6 +1049,10 @@ impl Hyprland { /// /// For new v3 syntax, use [`windowrule_names()`](Self::windowrule_names) and /// [`get_windowrule()`](Self::get_windowrule) instead. + #[deprecated( + since = "0.4.0", + note = "Use windowrule v3 syntax via windowrule_names() and get_windowrule() instead" + )] pub fn all_windowrules(&self) -> Vec<&String> { self.config .get_handler_calls("windowrule") @@ -935,7 +1060,16 @@ impl Hyprland { .unwrap_or_default() } - /// Get all windowrulev2 definitions (v2 handler-based syntax, deprecated) + /// Get all windowrulev2 definitions (v2 handler-based syntax) + /// + /// **DEPRECATED in Hyprland 0.53.0**: The `windowrulev2` handler syntax is deprecated. + /// Use the new v3 special category syntax instead: + /// ```conf + /// windowrule[rule-name] { + /// match:class = ^(kitty)$ + /// float = true + /// } + /// ``` /// /// This returns windowrulev2 handler calls from old configs using: /// ```conf @@ -944,6 +1078,10 @@ impl Hyprland { /// /// For new v3 syntax, use [`windowrule_names()`](Self::windowrule_names) and /// [`get_windowrule()`](Self::get_windowrule) instead. + #[deprecated( + since = "0.4.0", + note = "Use windowrule v3 syntax via windowrule_names() and get_windowrule() instead" + )] pub fn all_windowrulesv2(&self) -> Vec<&String> { self.config .get_handler_calls("windowrulev2") @@ -955,43 +1093,82 @@ impl Hyprland { /// /// Returns the names of all windowrule blocks defined in the config: /// ```conf - /// windowrule { - /// name = my-float-rule + /// windowrule[my-float-rule] { /// match:class = ^(kitty)$ /// float = true /// } + /// + /// windowrule[center-dialogs] { + /// match:title = ^(Open File)$ + /// center = true + /// } /// ``` /// - /// Returns `vec!["my-float-rule"]` + /// Returns `vec!["my-float-rule", "center-dialogs"]` + /// + /// Use with [`get_windowrule()`](Self::get_windowrule) to iterate all rules: + /// ```rust + /// use hyprlang::Hyprland; + /// + /// let mut hypr = Hyprland::new(); + /// hypr.parse(r#" + /// windowrule[test] { + /// match:class = test + /// } + /// "#).unwrap(); + /// + /// for name in hypr.windowrule_names() { + /// let rule = hypr.get_windowrule(&name).unwrap(); + /// // Access rule properties... + /// } + /// ``` pub fn windowrule_names(&self) -> Vec { self.config.list_special_category_keys("windowrule") } /// Get a specific windowrule by name (v3 special category syntax) /// - /// Returns all properties of a windowrule block: + /// Returns a [`RuleInstance`] with all properties of a windowrule block: /// ```conf - /// windowrule { - /// name = my-rule + /// windowrule[my-rule] { /// match:class = ^(kitty)$ /// float = true /// size = 800 600 + /// opacity = 0.95 + /// border_color = rgba(33ccffee) /// } /// ``` /// - /// Access properties: + /// Access properties with type-safe methods: /// ```rust - /// # use hyprlang::{Hyprland, ConfigValue}; + /// # use hyprlang::Hyprland; /// # let mut hypr = Hyprland::new(); /// # hypr.parse(r#" /// # windowrule[my-rule] { /// # match:class = ^(kitty)$ - /// # float = yes_please + /// # float = true + /// # size = 800 600 + /// # opacity = 0.95 + /// # border_color = rgba(33ccffee) /// # } /// # "#).unwrap(); /// let rule = hypr.get_windowrule("my-rule").unwrap(); + /// + /// // String values /// let class_match = rule.get_string("match:class").unwrap(); - /// let is_float = rule.get_string("float").unwrap(); + /// assert_eq!(class_match, "^(kitty)$"); + /// + /// // Integer values (booleans become 0/1) + /// let is_float = rule.get_int("float").unwrap(); + /// assert_eq!(is_float, 1); + /// + /// // Float values + /// let opacity = rule.get_float("opacity").unwrap(); + /// assert_eq!(opacity, 0.95); + /// + /// // Color values + /// let color = rule.get_color("border_color").unwrap(); + /// assert_eq!(color.r, 51); // 0x33 /// ``` pub fn get_windowrule(&self, name: &str) -> ParseResult> { self.config @@ -999,12 +1176,25 @@ impl Hyprland { .map(RuleInstance::new) } - /// Get all layerrule definitions (v1 handler-based syntax, deprecated) + /// Get all layerrule definitions (v1 handler-based syntax) + /// + /// **DEPRECATED in Hyprland 0.53.0**: The `layerrule` handler syntax is deprecated. + /// Use the new v2 special category syntax instead: + /// ```conf + /// layerrule[rule-name] { + /// match:namespace = waybar + /// blur = true + /// } + /// ``` /// /// This returns layerrule handler calls from old configs. /// /// For new v2 syntax, use [`layerrule_names()`](Self::layerrule_names) and /// [`get_layerrule()`](Self::get_layerrule) instead. + #[deprecated( + since = "0.4.0", + note = "Use layerrule v2 syntax via layerrule_names() and get_layerrule() instead" + )] pub fn all_layerrules(&self) -> Vec<&String> { self.config .get_handler_calls("layerrule") @@ -1016,19 +1206,66 @@ impl Hyprland { /// /// Returns the names of all layerrule blocks defined in the config: /// ```conf - /// layerrule { - /// name = blur-waybar + /// layerrule[blur-waybar] { /// match:namespace = waybar /// blur = true /// } + /// + /// layerrule[dim-notifications] { + /// match:namespace = ^(mako|dunst)$ + /// dim_around = true + /// } /// ``` /// - /// Returns `vec!["blur-waybar"]` + /// Returns `vec!["blur-waybar", "dim-notifications"]` + /// + /// Use with [`get_layerrule()`](Self::get_layerrule) to iterate all rules: + /// ```rust + /// use hyprlang::Hyprland; + /// + /// let mut hypr = Hyprland::new(); + /// hypr.parse(r#" + /// layerrule[test] { + /// match:namespace = test + /// } + /// "#).unwrap(); + /// + /// for name in hypr.layerrule_names() { + /// let rule = hypr.get_layerrule(&name).unwrap(); + /// // Access rule properties... + /// } + /// ``` pub fn layerrule_names(&self) -> Vec { self.config.list_special_category_keys("layerrule") } /// Get a specific layerrule by name (v2 special category syntax) + /// + /// Returns a [`RuleInstance`] with all properties of a layerrule block: + /// ```conf + /// layerrule[blur-waybar] { + /// match:namespace = waybar + /// blur = true + /// ignorealpha = 0.5 + /// } + /// ``` + /// + /// Access properties: + /// ```rust + /// # use hyprlang::Hyprland; + /// # let mut hypr = Hyprland::new(); + /// # hypr.parse(r#" + /// # layerrule[blur-waybar] { + /// # match:namespace = waybar + /// # blur = true + /// # ignorealpha = 0.5 + /// # } + /// # "#).unwrap(); + /// let rule = hypr.get_layerrule("blur-waybar").unwrap(); + /// let namespace = rule.get_string("match:namespace").unwrap(); + /// let is_blur = rule.get_int("blur").unwrap(); + /// let alpha = rule.get_float("ignorealpha").unwrap(); + /// ``` pub fn get_layerrule(&self, name: &str) -> ParseResult> { self.config .get_special_category("layerrule", name) diff --git a/tests/hyprland_parity_test.rs b/tests/hyprland_parity_test.rs new file mode 100644 index 0000000..18a1f82 --- /dev/null +++ b/tests/hyprland_parity_test.rs @@ -0,0 +1,503 @@ +//! Hyprland parity tests - based on real Hyprland test configs from hyprtester +//! +//! These tests ensure hyprlang-rs can parse actual Hyprland configuration patterns +//! from the official test suite at: Hyprland/hyprtester/test.conf +//! +//! Note: hyprlang-rs uses `windowrule[name] { ... }` syntax (bracket-keyed), +//! while Hyprland's test.conf uses `windowrule { name = rule-name ... }`. +//! Both are equivalent; these tests use hyprlang-rs's bracket syntax. + +#![cfg(feature = "hyprland")] + +use hyprlang::Hyprland; + +/// Test parsing the exact windowrule v3 syntax from Hyprland's test.conf +#[test] +fn test_hyprland_test_conf_suppress_maximize() { + let mut hypr = Hyprland::new(); + + // From hyprtester/test.conf lines 321-327 + // Using hyprlang-rs bracket syntax + hypr.parse( + r#" + windowrule[suppress-maximize-events] { + # Ignore maximize requests from apps. You'll probably like this. + match:class = .* + + suppress_event = maximize + } + "#, + ) + .unwrap(); + + let names = hypr.windowrule_names(); + assert!(names.contains(&"suppress-maximize-events".to_string())); + + let rule = hypr.get_windowrule("suppress-maximize-events").unwrap(); + assert_eq!(rule.get_string("match:class").unwrap(), ".*"); + assert_eq!(rule.get_string("suppress_event").unwrap(), "maximize"); +} + +/// Test complex multi-condition windowrule from Hyprland's test.conf +#[test] +fn test_hyprland_test_conf_fix_xwayland_drags() { + let mut hypr = Hyprland::new(); + + // From hyprtester/test.conf lines 329-340 + hypr.parse( + r#" + windowrule[fix-xwayland-drags] { + # Fix some dragging issues with XWayland + match:class = ^$ + match:title = ^$ + match:xwayland = true + match:float = true + match:fullscreen = false + match:pin = false + + no_focus = true + } + "#, + ) + .unwrap(); + + let rule = hypr.get_windowrule("fix-xwayland-drags").unwrap(); + + // Verify all match conditions + assert_eq!(rule.get_string("match:class").unwrap(), "^$"); + assert_eq!(rule.get_string("match:title").unwrap(), "^$"); + assert_eq!(rule.get_int("match:xwayland").unwrap(), 1); + assert_eq!(rule.get_int("match:float").unwrap(), 1); + assert_eq!(rule.get_int("match:fullscreen").unwrap(), 0); + assert_eq!(rule.get_int("match:pin").unwrap(), 0); + + // Verify effect + assert_eq!(rule.get_int("no_focus").unwrap(), 1); +} + +/// Test smart gaps windowrule with workspace expression +#[test] +fn test_hyprland_test_conf_smart_gaps() { + let mut hypr = Hyprland::new(); + + // From hyprtester/test.conf lines 345-361 + hypr.parse( + r#" + windowrule[smart-gaps-1] { + match:float = false + match:workspace = n[s:window] w[tv1] + + border_size = 0 + rounding = 0 + } + + windowrule[smart-gaps-2] { + match:float = false + match:workspace = n[s:window] f[1] + + border_size = 0 + rounding = 0 + } + "#, + ) + .unwrap(); + + let rule1 = hypr.get_windowrule("smart-gaps-1").unwrap(); + assert_eq!(rule1.get_int("match:float").unwrap(), 0); + // The workspace expression is stored as-is + assert_eq!( + rule1.get_string("match:workspace").unwrap(), + "n[s:window] w[tv1]" + ); + assert_eq!(rule1.get_int("border_size").unwrap(), 0); + assert_eq!(rule1.get_int("rounding").unwrap(), 0); + + let rule2 = hypr.get_windowrule("smart-gaps-2").unwrap(); + assert_eq!( + rule2.get_string("match:workspace").unwrap(), + "n[s:window] f[1]" + ); +} + +/// Test windowrule with basic float, size, and pin +#[test] +fn test_hyprland_test_conf_wr_kitty_stuff() { + let mut hypr = Hyprland::new(); + + // From hyprtester/test.conf lines 363-370 + hypr.parse( + r#" + windowrule[wr-kitty-stuff] { + match:class = wr_kitty + + float = true + size = 200 200 + pin = false + } + "#, + ) + .unwrap(); + + let rule = hypr.get_windowrule("wr-kitty-stuff").unwrap(); + assert_eq!(rule.get_string("match:class").unwrap(), "wr_kitty"); + assert_eq!(rule.get_int("float").unwrap(), 1); + assert_eq!(rule.get_string("size").unwrap(), "200 200"); + assert_eq!(rule.get_int("pin").unwrap(), 0); +} + +/// Test tag-based matching and tag assignment +#[test] +fn test_hyprland_test_conf_tags() { + let mut hypr = Hyprland::new(); + + // From hyprtester/test.conf lines 372-384 + hypr.parse( + r#" + windowrule[tagged-kitty-floats] { + match:tag = tag_kitty + + float = true + } + + windowrule[static-kitty-tag] { + match:class = tag_kitty + + tag = +tag_kitty + } + "#, + ) + .unwrap(); + + let float_rule = hypr.get_windowrule("tagged-kitty-floats").unwrap(); + assert_eq!(float_rule.get_string("match:tag").unwrap(), "tag_kitty"); + assert_eq!(float_rule.get_int("float").unwrap(), 1); + + let tag_rule = hypr.get_windowrule("static-kitty-tag").unwrap(); + assert_eq!(tag_rule.get_string("match:class").unwrap(), "tag_kitty"); + // Tag with + prefix for adding + assert_eq!(tag_rule.get_string("tag").unwrap(), "+tag_kitty"); +} + +/// Test windowrule with opacity override (from window.cpp tests) +#[test] +fn test_hyprland_opacity_override() { + let mut hypr = Hyprland::new(); + + // From hyprtester/src/tests/main/window.cpp line 694 + hypr.parse( + r#" + windowrule[wr-kitty-stuff] { + match:class = wr_kitty + opacity = 0.5 0.5 override + } + "#, + ) + .unwrap(); + + let rule = hypr.get_windowrule("wr-kitty-stuff").unwrap(); + // Opacity with override is stored as full string + assert_eq!(rule.get_string("opacity").unwrap(), "0.5 0.5 override"); +} + +/// Test minsize/maxsize rules (from window.cpp tests) +#[test] +fn test_hyprland_minmax_size_rules() { + let mut hypr = Hyprland::new(); + + // From hyprtester/src/tests/main/window.cpp lines 583-585 + hypr.parse( + r#" + windowrule[kitty-max-rule] { + match:class = kitty_maxsize + max_size = 1500 500 + min_size = 1200 500 + } + "#, + ) + .unwrap(); + + let rule = hypr.get_windowrule("kitty-max-rule").unwrap(); + assert_eq!(rule.get_string("match:class").unwrap(), "kitty_maxsize"); + assert_eq!(rule.get_string("max_size").unwrap(), "1500 500"); + assert_eq!(rule.get_string("min_size").unwrap(), "1200 500"); +} + +/// Test workspace assignment to special workspace (from window.cpp tests) +#[test] +fn test_hyprland_special_workspace_rule() { + let mut hypr = Hyprland::new(); + + // From hyprtester/src/tests/main/window.cpp lines 701-702 + hypr.parse( + r#" + windowrule[special-magic-kitty] { + match:class = magic_kitty + workspace = special:magic + } + "#, + ) + .unwrap(); + + let rule = hypr.get_windowrule("special-magic-kitty").unwrap(); + assert_eq!(rule.get_string("match:class").unwrap(), "magic_kitty"); + assert_eq!(rule.get_string("workspace").unwrap(), "special:magic"); +} + +/// Test persistent_size rule (from window.cpp tests) +#[test] +fn test_hyprland_persistent_size() { + let mut hypr = Hyprland::new(); + + // From hyprtester/src/tests/main/window.cpp line 746 + hypr.parse( + r#" + windowrule[persistent-float] { + match:class = persistent_size_kitty + persistent_size = true + float = true + } + "#, + ) + .unwrap(); + + let rule = hypr.get_windowrule("persistent-float").unwrap(); + assert_eq!( + rule.get_string("match:class").unwrap(), + "persistent_size_kitty" + ); + assert_eq!(rule.get_int("persistent_size").unwrap(), 1); + assert_eq!(rule.get_int("float").unwrap(), 1); +} + +/// Test expression-based rules (from window.cpp tests) +#[test] +fn test_hyprland_expression_rules() { + let mut hypr = Hyprland::new(); + + // From hyprtester/src/tests/main/window.cpp line 802 + // Note: hyprlang-rs parses these as strings, actual evaluation is done by Hyprland + hypr.parse( + r#" + windowrule[expr-rule] { + match:class = expr_kitty + float = true + size = monitor_w*0.5 monitor_h*0.5 + move = 20+(monitor_w*0.1) monitor_h*0.5 + } + "#, + ) + .unwrap(); + + let rule = hypr.get_windowrule("expr-rule").unwrap(); + assert_eq!(rule.get_string("match:class").unwrap(), "expr_kitty"); + // "yes" gets parsed as boolean true (1) + assert_eq!(rule.get_int("float").unwrap(), 1); + assert_eq!(rule.get_string("size").unwrap(), "monitor_w*0.5 monitor_h*0.5"); + assert_eq!( + rule.get_string("move").unwrap(), + "20+(monitor_w*0.1) monitor_h*0.5" + ); +} + +/// Test dynamic match:float rule (from window.cpp tests) +#[test] +fn test_hyprland_dynamic_float_match() { + let mut hypr = Hyprland::new(); + + // From hyprtester/src/tests/main/window.cpp line 774 + hypr.parse( + r#" + windowrule[float-border] { + match:float = true + border_size = 10 + } + "#, + ) + .unwrap(); + + let rule = hypr.get_windowrule("float-border").unwrap(); + assert_eq!(rule.get_int("match:float").unwrap(), 1); + assert_eq!(rule.get_int("border_size").unwrap(), 10); +} + +/// Test group rules with workspace expressions (from window.cpp tests) +#[test] +fn test_hyprland_group_workspace_rules() { + let mut hypr = Hyprland::new(); + + // From hyprtester/src/tests/main/window.cpp lines 144-145 + hypr.parse( + r#" + windowrule[ws-tv1-border] { + match:workspace = w[tv1] + border_size = 0 + } + + windowrule[ws-f1-border] { + match:workspace = f[1] + border_size = 0 + } + "#, + ) + .unwrap(); + + let rule1 = hypr.get_windowrule("ws-tv1-border").unwrap(); + assert_eq!(rule1.get_string("match:workspace").unwrap(), "w[tv1]"); + assert_eq!(rule1.get_int("border_size").unwrap(), 0); + + let rule2 = hypr.get_windowrule("ws-f1-border").unwrap(); + assert_eq!(rule2.get_string("match:workspace").unwrap(), "f[1]"); +} + +/// Test parsing the full Hyprland test.conf structure +#[test] +fn test_hyprland_full_config_structure() { + let mut hypr = Hyprland::new(); + + // Simplified version of hyprtester/test.conf + hypr.parse( + r#" + # Variables + $terminal = kitty + $mainMod = SUPER + + # Monitors + monitor = HEADLESS-1, 1920x1080@60, auto-right, 1 + + # Environment + env = XCURSOR_SIZE, 24 + + # General settings + general { + gaps_in = 5 + gaps_out = 20 + border_size = 2 + col.active_border = rgba(33ccffee) rgba(00ff99ee) 45deg + col.inactive_border = rgba(595959aa) + layout = dwindle + } + + # Decoration + decoration { + rounding = 10 + rounding_power = 2 + + blur { + enabled = true + size = 3 + passes = 1 + } + } + + # Animations + animations { + enabled = 0 + bezier = easeOutQuint, 0.23, 1, 0.32, 1 + animation = global, 1, 10, default + } + + # Device config + device { + name = test-mouse-1 + enabled = true + } + + # Layout + dwindle { + pseudotile = true + preserve_split = true + } + + master { + new_status = master + } + + # Input + input { + kb_layout = us + follow_mouse = 1 + sensitivity = 0 + } + + # Keybindings + bind = $mainMod, Q, exec, $terminal + bind = $mainMod, C, killactive, + bindm = $mainMod, mouse:272, movewindow + + # Windowrules + windowrule[test-rule] { + match:class = .* + suppress_event = maximize + } + "#, + ) + .unwrap(); + + // Verify various config values + assert_eq!(hypr.general_border_size().unwrap(), 2); + assert_eq!(hypr.general_layout().unwrap(), "dwindle"); + assert_eq!(hypr.decoration_rounding().unwrap(), 10); + assert!(hypr.dwindle_pseudotile().unwrap()); + assert_eq!(hypr.master_new_status().unwrap(), "master"); + + // Verify handlers + let binds = hypr.all_binds(); + assert_eq!(binds.len(), 2); + + let bindm = hypr.all_bindm(); + assert_eq!(bindm.len(), 1); + + // Verify windowrule + let rule = hypr.get_windowrule("test-rule").unwrap(); + assert_eq!(rule.get_string("suppress_event").unwrap(), "maximize"); +} + +/// Test that alternate bracket syntax works: windowrule[name] { ... } +#[test] +fn test_hyprland_bracket_name_syntax() { + let mut hypr = Hyprland::new(); + + hypr.parse( + r#" + windowrule[my-bracket-rule] { + match:class = test + float = true + } + "#, + ) + .unwrap(); + + let rule = hypr.get_windowrule("my-bracket-rule").unwrap(); + assert_eq!(rule.get_string("match:class").unwrap(), "test"); + assert_eq!(rule.get_int("float").unwrap(), 1); +} + +/// Test overlapping rules (from window.cpp tests) +#[test] +fn test_hyprland_overlapping_rules() { + let mut hypr = Hyprland::new(); + + // From window.cpp lines 731-732 - rules that overlap effects but not props + hypr.parse( + r#" + windowrule[class-overlap] { + match:class = overlap_kitty + border_size = 0 + } + + windowrule[fullscreen-overlap] { + match:fullscreen = false + border_size = 10 + } + "#, + ) + .unwrap(); + + let rule1 = hypr.get_windowrule("class-overlap").unwrap(); + assert_eq!(rule1.get_string("match:class").unwrap(), "overlap_kitty"); + assert_eq!(rule1.get_int("border_size").unwrap(), 0); + + let rule2 = hypr.get_windowrule("fullscreen-overlap").unwrap(); + assert_eq!(rule2.get_int("match:fullscreen").unwrap(), 0); + assert_eq!(rule2.get_int("border_size").unwrap(), 10); +} diff --git a/tests/windowrule_v3_test.rs b/tests/windowrule_v3_test.rs index c9f83ae..7f3aa06 100644 --- a/tests/windowrule_v3_test.rs +++ b/tests/windowrule_v3_test.rs @@ -541,3 +541,161 @@ fn test_layerrule_default_values() { assert_eq!(rule.get_string("blur").unwrap(), ""); assert_eq!(rule.get_string("animation").unwrap(), ""); } + +// ==================== Hyprland 0.53.0 Tests ==================== + +#[test] +fn test_bindu_handler() { + let mut hypr = Hyprland::new(); + + hypr.parse( + r#" + bindu = SUPER, Y, exec, kitty + bindu = SUPER SHIFT, Y, killactive + "#, + ) + .unwrap(); + + let binds = hypr.all_bindu(); + assert_eq!(binds.len(), 2); + assert_eq!(binds[0], "SUPER, Y, exec, kitty"); + assert_eq!(binds[1], "SUPER SHIFT, Y, killactive"); +} + +#[test] +fn test_new_config_accessors() { + let mut hypr = Hyprland::new(); + + hypr.parse( + r#" + general { + locale = en_US + } + + quirks { + prefer_hdr = 2 + } + + cursor { + hide_on_tablet = true + } + + group { + groupbar { + blur = true + } + } + "#, + ) + .unwrap(); + + assert_eq!(hypr.general_locale().unwrap(), "en_US"); + assert_eq!(hypr.quirks_prefer_hdr().unwrap(), 2); + assert!(hypr.cursor_hide_on_tablet().unwrap()); + assert!(hypr.group_groupbar_blur().unwrap()); +} + +#[test] +fn test_windowrule_match_property_aliases() { + let mut hypr = Hyprland::new(); + + // Test Hyprland v3 naming with aliases + hypr.parse( + r#" + windowrule[v3-aliases] { + match:float = true + match:pin = false + match:workspace = 5 + match:fullscreen_state_internal = 1 + match:fullscreen_state_client = 2 + } + "#, + ) + .unwrap(); + + let rule = hypr.get_windowrule("v3-aliases").unwrap(); + + // Test the Hyprland v3 naming aliases work + assert_eq!(rule.get_int("match:float").unwrap(), 1); + assert_eq!(rule.get_int("match:pin").unwrap(), 0); + assert_eq!(rule.get_int("match:workspace").unwrap(), 5); + assert_eq!(rule.get_int("match:fullscreen_state_internal").unwrap(), 1); + assert_eq!(rule.get_int("match:fullscreen_state_client").unwrap(), 2); +} + +#[test] +fn test_windowrule_effect_property_aliases() { + let mut hypr = Hyprland::new(); + + // Test v3 naming with underscore aliases + hypr.parse( + r#" + windowrule[v3-effect-aliases] { + match:class = test + fullscreen_state = 2 + no_initial_focus = true + suppress_event = maximize + no_close_for = 5000 + } + "#, + ) + .unwrap(); + + let rule = hypr.get_windowrule("v3-effect-aliases").unwrap(); + + assert_eq!(rule.get_int("fullscreen_state").unwrap(), 2); + assert_eq!(rule.get_int("no_initial_focus").unwrap(), 1); + assert_eq!(rule.get_string("suppress_event").unwrap(), "maximize"); + assert_eq!(rule.get_int("no_close_for").unwrap(), 5000); +} + +#[test] +fn test_layerrule_new_effects() { + let mut hypr = Hyprland::new(); + + hypr.parse( + r#" + layerrule[new-effects] { + match:namespace = test-layer + blur_popups = true + dim_around = 0.5 + order = 10 + above_lock = true + no_screen_share = true + } + "#, + ) + .unwrap(); + + let rule = hypr.get_layerrule("new-effects").unwrap(); + + assert_eq!(rule.get_int("blur_popups").unwrap(), 1); + assert_eq!(rule.get_float("dim_around").unwrap(), 0.5); + assert_eq!(rule.get_int("order").unwrap(), 10); + assert_eq!(rule.get_int("above_lock").unwrap(), 1); + assert_eq!(rule.get_int("no_screen_share").unwrap(), 1); +} + +#[test] +fn test_layerrule_effect_aliases() { + let mut hypr = Hyprland::new(); + + hypr.parse( + r#" + layerrule[effect-aliases] { + match:namespace = test + ignore_alpha = 0.3 + no_anim = true + noscreenshare = true + } + "#, + ) + .unwrap(); + + let rule = hypr.get_layerrule("effect-aliases").unwrap(); + + // Test underscore aliases work + assert_eq!(rule.get_float("ignore_alpha").unwrap(), 0.3); + assert_eq!(rule.get_int("no_anim").unwrap(), 1); + assert_eq!(rule.get_int("noscreenshare").unwrap(), 1); +}