A Rust reimplementation of Hyprlang, the configuration language used by Hyprland.
Hyprlang is a powerful configuration language featuring variables, nested categories, expressions, custom handlers, and more. This library provides a complete parser and configuration manager with a clean, idiomatic Rust API.
This project is not endorsed by or affiliated with the Hyprland project/HyprWM Organization.
- 🎯 Complete Hyprlang Implementation - Full compatibility with the original C++ version
- 🚀 Fast PEG Parser - Built with pest 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
- 🧮 Expression Evaluation - Arithmetic expressions with
{{expr}}syntax - 🎨 Color Support - Multiple color formats:
rgba(),rgb(), and hex colors - 📐 Vec2 Coordinates - Built-in support for 2D coordinate pairs
- 🔌 Handler System - Extensible keyword handlers for custom syntax
- 🏷️ Special Categories - Keyed, static, and anonymous category types
- 📄 Source Directives - Include external configuration files
- 💬 Conditional Directives -
# hyprlang if/endif/noerrorsupport with negation - 🎨 Expression Escaping - Escape expressions with
\{{}}or{\{}}for literal braces - 🔄 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
sourcedirectives - 🎯 Windowrule v3 / Layerrule v2 - Full support for new special category syntax with 85+ registered properties
- ✅ Fully Tested - 177 tests covering all features
See BENCHMARKS.md for detailed performance metrics. Quick summary:
- Parsing: ~2.7 µs/line (370k lines/sec)
- Retrieval: 10-13 ns per operation
- Serialization: ~65 ns/line
Add this to your Cargo.toml:
[dependencies]
hyprlang = "0.3.0"Enable the hyprland feature to get a high-level Hyprland struct with pre-configured handlers and typed access to Hyprland configuration options:
[dependencies]
hyprlang = { version = "0.3.0", features = ["hyprland"] }This feature provides:
- Automatic registration of all Hyprland handlers (bind, monitor, env, etc.)
- Typed accessor methods for common Hyprland config values
- Convenient methods to access all binds, windowrules, animations, etc.
Enable the mutation feature to modify configuration values and serialize configs back to files:
[dependencies]
hyprlang = { version = "0.3.0", features = ["mutation"] }This feature provides:
- Mutation API - Modify config values, variables, handlers, and special categories
- Serialization - Save configurations back to files with clean formatting
- Multi-file tracking - Automatically track which values came from which source file
- Smart saving - Save changes only to the modified files when using
sourcedirectives - Two mutation styles - Direct setters and mutable references
- Round-trip support - Parse → modify → save → parse
use hyprlang::Config;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut config = Config::new();
// Parse a configuration string
config.parse(r#"
general {
gaps_in = 5
gaps_out = 20
border_size = 2
}
"#)?;
// Access values
let gaps_in = config.get_int("general:gaps_in")?;
let gaps_out = config.get_int("general:gaps_out")?;
println!("gaps_in: {}, gaps_out: {}", gaps_in, gaps_out);
Ok(())
}The hyprland feature provides a high-level, type-safe API specifically designed for working with Hyprland configurations. Instead of manually registering handlers and using string-based key access, you get a convenient Hyprland struct with pre-configured handlers and typed accessor methods.
Without the Hyprland feature (using low-level Config API):
use hyprlang::Config;
let mut config = Config::new();
// Manually register all handlers
config.register_handler_fn("bind", |_| Ok(()));
config.register_handler_fn("monitor", |_| Ok(()));
config.register_handler_fn("windowrule", |_| Ok(()));
// ... register 20+ more handlers
config.register_category_handler_fn("animations", "animation", |_| Ok(()));
config.register_category_handler_fn("animations", "bezier", |_| Ok(()));
// Access values with string keys and manual type conversion
let border_size = config.get_int("general:border_size")?;
let gaps_in = config.get_string("general:gaps_in")?; // Could be int or string
let binds = config.get_handler_calls("bind").unwrap_or(&vec![]);With the Hyprland feature (using high-level Hyprland API):
use hyprlang::Hyprland;
let mut hypr = Hyprland::new(); // All handlers pre-registered!
// Typed accessor methods
let border_size = hypr.general_border_size()?; // Returns i64
let gaps_in = hypr.general_gaps_in()?; // Returns String (CSS-style)
let active_border = hypr.general_active_border_color()?; // Returns Color
// Convenient array access
let binds = hypr.all_binds(); // Returns Vec<&String>use hyprlang::Hyprland;
use std::path::Path;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create Hyprland config (handlers auto-registered)
let mut hypr = Hyprland::new();
// Parse your Hyprland config
hypr.parse_file(Path::new("~/.config/hypr/hyprland.conf"))?;
// === Access General Settings ===
println!("Border: {}", hypr.general_border_size()?);
println!("Layout: {}", hypr.general_layout()?);
println!("Gaps: {} / {}", hypr.general_gaps_in()?, hypr.general_gaps_out()?);
let active = hypr.general_active_border_color()?;
println!("Active border: rgba({}, {}, {}, {})",
active.r, active.g, active.b, active.a);
// === Access Decoration Settings ===
println!("\nRounding: {}", hypr.decoration_rounding()?);
println!("Active opacity: {}", hypr.decoration_active_opacity()?);
println!("Blur enabled: {}", hypr.decoration_blur_enabled()?);
println!("Blur size: {}", hypr.decoration_blur_size()?);
// === Access Animation Settings ===
if hypr.animations_enabled()? {
println!("\nAnimations:");
for anim in hypr.all_animations() {
println!(" - {}", anim);
}
println!("\nBezier curves:");
for bezier in hypr.all_beziers() {
println!(" - {}", bezier);
}
}
// === Access Input Settings ===
println!("\nKeyboard layout: {}", hypr.input_kb_layout()?);
println!("Mouse sensitivity: {}", hypr.input_sensitivity()?);
println!("Natural scroll: {}", hypr.input_touchpad_natural_scroll()?);
// === Access Keybindings ===
println!("\nKeybindings ({}):", hypr.all_binds().len());
for (i, bind) in hypr.all_binds().iter().enumerate() {
println!(" [{}] {}", i + 1, bind);
}
// === Access Window Rules ===
println!("\nWindow rules ({}):", hypr.all_windowrules().len());
for rule in hypr.all_windowrules() {
println!(" - {}", rule);
}
// === Access Variables ===
println!("\nVariables:");
for (name, value) in hypr.variables() {
println!(" ${} = {}", name, value);
}
// === Access Monitors ===
for monitor in hypr.all_monitors() {
println!("Monitor: {}", monitor);
}
// === Access Environment Variables ===
for env in hypr.all_env() {
println!("Env: {}", env);
}
// === Access Autostart ===
for exec in hypr.all_exec_once() {
println!("Exec-once: {}", exec);
}
Ok(())
}hypr.general_border_size() -> Result<i64>
hypr.general_gaps_in() -> Result<String> // CSS-style: "5" or "5 10 15 20"
hypr.general_gaps_out() -> Result<String> // CSS-style: "20" or "5 10 15 20"
hypr.general_layout() -> Result<&str> // "dwindle" or "master"
hypr.general_allow_tearing() -> Result<bool>
hypr.general_active_border_color() -> Result<Color>
hypr.general_inactive_border_color() -> Result<Color>hypr.decoration_rounding() -> Result<i64>
hypr.decoration_active_opacity() -> Result<f64>
hypr.decoration_inactive_opacity() -> Result<f64>
hypr.decoration_blur_enabled() -> Result<bool>
hypr.decoration_blur_size() -> Result<i64>
hypr.decoration_blur_passes() -> Result<i64>hypr.animations_enabled() -> Result<bool>
hypr.all_animations() -> Vec<&String> // All animation definitions
hypr.all_beziers() -> Vec<&String> // All bezier curve definitionshypr.input_kb_layout() -> Result<&str>
hypr.input_follow_mouse() -> Result<i64>
hypr.input_sensitivity() -> Result<f64>
hypr.input_touchpad_natural_scroll() -> Result<bool>hypr.dwindle_pseudotile() -> Result<bool>
hypr.dwindle_preserve_split() -> Result<bool>
hypr.master_new_status() -> Result<&str>hypr.misc_disable_hyprland_logo() -> Result<bool>
hypr.misc_force_default_wallpaper() -> Result<i64>hypr.all_binds() -> Vec<&String> // All bind definitions
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_workspaces() -> Vec<&String> // All workspace definitions
hypr.all_monitors() -> Vec<&String> // All monitor definitions
hypr.all_env() -> Vec<&String> // All env definitions
hypr.all_exec() -> Vec<&String> // All exec definitions
hypr.all_exec_once() -> Vec<&String> // All exec-once definitions// New v3 syntax for windowrules
hypr.windowrule_names() -> Vec<String> // All windowrule names
hypr.get_windowrule(name: &str) -> Result<RuleInstance> // Get specific rule
// New v2 syntax for layerrules
hypr.layerrule_names() -> Vec<String> // All layerrule names
hypr.get_layerrule(name: &str) -> Result<RuleInstance> // Get specific rule
// RuleInstance helper provides typed access to properties:
rule.get(key: &str) -> Result<&ConfigValue> // Get any property
rule.get_string(key: &str) -> Result<String> // Get as string
rule.get_int(key: &str) -> Result<i64> // Get as integer
rule.get_float(key: &str) -> Result<f64> // Get as float
rule.get_color(key: &str) -> Result<Color> // Get as colorhypr.variables() -> &HashMap<String, String> // All variables
hypr.get_variable(name: &str) -> Option<&String> // Get specific variableIf you need access to the underlying low-level Config API:
let config: &Config = hypr.config(); // Immutable access
let config: &mut Config = hypr.config_mut(); // Mutable access
// Use all Config methods
let custom_value = config.get("custom:key")?;
config.register_handler_fn("custom", |ctx| { /* ... */ Ok(()) });The Hyprland struct automatically registers these handlers:
Root-level handlers:
monitor- Monitor configurationenv- Environment variablesbind,bindm,bindel,bindl,bindr,binde,bindn- Keybindingswindowrule,windowrulev2- Window ruleslayerrule- Layer rulesworkspace- Workspace configurationexec,exec-once- Commandssource- File inclusionblurls- Blur layer surfaceplugin- Plugin loading
Category-specific handlers:
animations:animation- Animation definitionsanimations:bezier- Bezier curve definitions
Special categories:
device[name]- Per-device input configuration (keyed category)monitor[name]- Per-monitor configuration (keyed category)windowrule[name]- Window rules v3 syntax (keyed category with 80+ properties)layerrule[name]- Layer rules v2 syntax (keyed category with 12 properties)
Use the Hyprland API when:
- ✅ You're working specifically with Hyprland configurations
- ✅ You want typed, convenient access to common config values
- ✅ You want all Hyprland handlers pre-registered automatically
- ✅ You're building tools for Hyprland users (config editors, validators, etc.)
Use the low-level Config API when:
- ✅ You're implementing a different config language
- ✅ You need full control over handler registration
- ✅ You're working with a custom config format
- ✅ You want minimal dependencies (no Hyprland-specific code)
use hyprlang::Config;
let mut config = Config::new();
config.parse(r#"
# Integers and floats
count = 42
opacity = 0.95
# Strings
terminal = kitty
shell = "zsh"
# Booleans
enabled = true
disabled = false
"#)?;
assert_eq!(config.get_int("count")?, 42);
assert_eq!(config.get_float("opacity")?, 0.95);
assert_eq!(config.get_string("terminal")?, "kitty");use hyprlang::Config;
let mut config = Config::new();
config.parse(r#"
$terminal = kitty
$mod = SUPER
# Variables are expanded when used
my_term = $terminal
modifier = $mod
"#)?;
// Access variables directly
let vars = config.variables();
assert_eq!(vars.get("terminal"), Some(&"kitty".to_string()));
// Or access expanded values
assert_eq!(config.get_string("my_term")?, "kitty");use hyprlang::Config;
let mut config = Config::new();
config.parse(r#"
color1 = rgba(33ccffee)
color2 = rgb(255, 128, 64)
color3 = 0xff8040ff
"#)?;
let color = config.get_color("color1")?;
println!("R: {}, G: {}, B: {}, A: {}", color.r, color.g, color.b, color.a);use hyprlang::Config;
let mut config = Config::new();
config.parse(r#"
position1 = 100, 200
position2 = (50, 75)
"#)?;
let pos = config.get_vec2("position1")?;
assert_eq!(pos.x, 100.0);
assert_eq!(pos.y, 200.0);use hyprlang::Config;
let mut config = Config::new();
config.parse(r#"
$base = 10
# Arithmetic expressions with {{}}
double = {{$base * 2}}
sum = {{5 + 3}}
complex = {{($base + 5) * 2}}
"#)?;
assert_eq!(config.get_int("double")?, 20);
assert_eq!(config.get_int("sum")?, 8);
assert_eq!(config.get_int("complex")?, 30);use hyprlang::Config;
let mut config = Config::new();
config.parse(r#"
general {
border_size = 2
gaps {
inner = 5
outer = 10
}
}
"#)?;
// Access with colon-separated paths
assert_eq!(config.get_int("general:border_size")?, 2);
assert_eq!(config.get_int("general:gaps:inner")?, 5);
assert_eq!(config.get_int("general:gaps:outer")?, 10);use hyprlang::Config;
let mut config = Config::new();
// Register a handler for custom keywords
config.register_handler_fn("bind", |ctx| {
println!("Bind: {}", ctx.value);
Ok(())
});
config.parse(r#"
bind = SUPER, Q, exec, kitty
bind = SUPER, C, killactive
"#)?;
// Access handler calls as arrays
let binds = config.get_handler_calls("bind").unwrap();
assert_eq!(binds.len(), 2);use hyprlang::Config;
let mut config = Config::new();
// Register handlers that only work in specific categories
config.register_category_handler_fn("animations", "animation", |ctx| {
println!("Animation: {}", ctx.value);
Ok(())
});
config.parse(r#"
animations {
animation = windows, 1, 4, default
animation = fade, 1, 3, quick
}
"#)?;
// Handler calls are namespaced by category
let anims = config.get_handler_calls("animations:animation").unwrap();
assert_eq!(anims.len(), 2);use hyprlang::{Config, SpecialCategoryDescriptor};
let mut config = Config::new();
// Register a special category
config.register_special_category(
SpecialCategoryDescriptor::keyed("device", "name")
);
config.parse(r#"
device[mouse] {
sensitivity = 0.5
accel_profile = flat
}
device[keyboard] {
repeat_rate = 50
repeat_delay = 300
}
"#)?;
// Access keyed category instances
let mouse = config.get_special_category("device", "mouse")?;
println!("Mouse sensitivity: {:?}", mouse.get("sensitivity"));The new windowrule v3 and layerrule v2 syntax uses special category blocks:
use hyprlang::Hyprland;
let mut hypr = Hyprland::new();
hypr.parse(r#"
# New v3 syntax - windowrule as special category
windowrule[float-terminals] {
# Match properties
match:class = ^(kitty|alacritty)$
match:floating = false
# Effect properties
float = true
size = 800 600
center = true
opacity = 0.95
rounding = 10
border_color = rgba(33ccffee)
}
# Layerrule v2 syntax
layerrule[blur-waybar] {
match:namespace = waybar
blur = true
ignorealpha = 0.5
}
"#)?;
// Access windowrules
let names = hypr.windowrule_names(); // vec!["float-terminals"]
let rule = hypr.get_windowrule("float-terminals")?;
// Get properties with type safety
let class_match = rule.get_string("match:class")?; // "^(kitty|alacritty)$"
let is_float = rule.get_int("float")?; // 1 (true)
let opacity = rule.get_float("opacity")?; // 0.95
let color = rule.get_color("border_color")?; // Color { r: 51, g: 204, b: 255, a: 238 }
// Old v2 handler syntax still works for backward compatibility
hypr.parse(r#"
windowrulev2 = float, class:^(kitty)$
"#)?;
let v2_rules = hypr.all_windowrulesv2();Windowrule v3 - Match Properties (19):
match:class,match:title,match:initial_class,match:initial_titlematch:floating,match:xwayland,match:fullscreen,match:pinnedmatch:focus,match:group,match:modal,match:tagmatch:fullscreenstate_internal,match:fullscreenstate_clientmatch:on_workspace,match:content,match:xdg_tagmatch:namespace,match:exec_token
Windowrule v3 - Effect Properties (60+ with aliases):
- Static:
float,tile,fullscreen,maximize,move,size,center,pseudo,monitor,workspace,pin,group, etc. - Dynamic:
rounding,opacity,border_color,border_size,max_size,min_size,animation,no_blur,no_shadow,xray, etc.
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
### Source Directive
```hyprlang
# .colors.conf
$borderSize = 3
use hyprlang::{Config, ConfigOptions};
use std::path::PathBuf;
let mut options = ConfigOptions::default();
options.base_dir = Some(PathBuf::from("/path/to/config"));
let mut config = Config::with_options(options);
// This will include another config file
config.parse(r#"
source = ./colors.conf
general {
border_size = $borderSize
}
"#)?;Enable the mutation feature to modify configurations and save them:
use hyprlang::{Config, ConfigValue};
let mut config = Config::new();
config.parse(r#"
$GAPS = 10
border_size = 3
opacity = 0.9
"#)?;
// ===== Mutate Values =====
config.set_int("border_size", 5);
config.set_float("opacity", 1.0);
config.set("new_key", ConfigValue::String("value".to_string()));
// Remove values
let old = config.remove("opacity")?;
// ===== Mutate Variables =====
// Method 1: Direct mutation
config.set_variable("GAPS".to_string(), "15".to_string());
// Method 2: Mutable reference
if let Some(mut gaps) = config.get_variable_mut("GAPS") {
gaps.set("20")?; // Now GAPS = 20
}
// ===== Mutate Handlers =====
config.register_handler_fn("bind", |_| Ok(()));
config.add_handler_call("bind", "SUPER, Q, exec, terminal".to_string())?;
config.remove_handler_call("bind", 0)?; // Remove first bind
// ===== Serialize & Save =====
let output = config.serialize(); // Get string representation
config.save_as("config_modified.conf")?; // Save to file
// Verify round-trip
let mut config2 = Config::new();
config2.parse_file(Path::new("config_modified.conf"))?;
assert_eq!(config2.get_int("border_size")?, 5);Run the comprehensive example:
cargo run --example mutation_example --features mutationWhen your configuration uses source directives to include other files, the mutation feature automatically tracks which values came from which file and saves changes only to the modified files:
use hyprlang::Config;
use std::path::Path;
// Create config files
// main.conf:
// source = ./vars.conf
// source = ./appearance.conf
// border_size = 2
//
// vars.conf:
// $GAPS = 10
//
// appearance.conf:
// decoration {
// rounding = 5
// }
let mut config = Config::new();
config.parse_file(Path::new("main.conf"))?;
// ===== Check which file defines a key =====
let source = config.get_key_source_file("decoration:rounding");
println!("rounding is defined in: {:?}", source);
// ===== Mutate a value from appearance.conf =====
config.set_int("decoration:rounding", 15);
// ===== Check which files were modified =====
let modified = config.get_modified_files();
println!("Modified files: {:?}", modified); // Only appearance.conf
// ===== Save only the modified files =====
let saved = config.save_all()?;
println!("Saved files: {:?}", saved); // Only appearance.conf was written
// ===== The master config preserves source directives =====
// main.conf still contains:
// source = ./vars.conf
// source = ./appearance.conf
// border_size = 2
//
// appearance.conf now contains:
// decoration {
// rounding = 15 # <-- Updated!
// }
//
// vars.conf remains unchanged
// ===== List all source files =====
let all_files = config.get_source_files();
println!("All source files: {:?}", all_files);
// ===== Serialize a specific file =====
let appearance_content = config.serialize_file(Path::new("./appearance.conf"))?;
println!("appearance.conf:\n{}", appearance_content);Key features:
- 🎯 Automatic tracking - No need to manually specify which file to update
- 💾 Selective saving - Only modified files are written to disk
- 🔒 Structure preservation - Source directives remain intact in the master config
- 🔍 File inspection - Query which file defines any key
use hyprlang::Config;
use std::path::Path;
let mut config = Config::new();
config.parse_file(Path::new("config.conf"))?;
// Access all keys
for key in config.keys() {
println!("Key: {}", key);
}When the hyprland feature is enabled, you can use the high-level Hyprland struct:
use hyprlang::Hyprland;
use std::path::Path;
// Create a new Hyprland config (automatically registers all handlers)
let mut hypr = Hyprland::new();
// Parse your Hyprland config
hypr.parse_file(Path::new("~/.config/hypr/hyprland.conf"))?;
// Access config with typed methods
let border_size = hypr.general_border_size()?;
let gaps_in = hypr.general_gaps_in()?;
let active_border = hypr.general_active_border_color()?;
// Get all binds as an array
let binds = hypr.all_binds();
for bind in binds {
println!("Bind: {}", bind);
}
// Get all animations
let animations = hypr.all_animations();
println!("Found {} animations", animations.len());
// Get all window rules
let rules = hypr.all_windowrules();
for rule in rules {
println!("Rule: {}", rule);
}
// Access variables
let terminal = hypr.get_variable("terminal");The Hyprland struct provides convenient typed access to:
- General settings: border_size, gaps, colors, layout, etc.
- Decoration: rounding, opacity, blur settings
- Animations: enabled status, all animations, all beziers
- Input: keyboard layout, mouse settings, touchpad
- Layout: dwindle and master layout settings
- Handlers: all binds, windowrules, monitors, env vars, exec-once, etc.
- Variables: all user-defined variables
use hyprlang::{Config, ConfigOptions};
use std::path::PathBuf;
let mut options = ConfigOptions::default();
// Collect all errors instead of stopping at the first one
options.throw_all_errors = false;
// Allow parsing after initial parse
options.allow_dynamic_parsing = true;
// Base directory for resolving source directives
options.base_dir = Some(PathBuf::from("/path/to/config"));
let config = Config::with_options(options);Config- Main configuration managerConfigValue- Enum representing all value typesInt(i64)- Integer valueFloat(f64)- Float valueString(String)- String valueVec2(Vec2)- 2D coordinateColor(Color)- RGBA colorCustom { type_name, value }- Custom value type
Color- RGBA color (r, g, b, a)Vec2- 2D coordinate (x, y)
// Parsing
config.parse(content: &str) -> Result<()>
config.parse_file(path: &Path) -> Result<()>
// Getting values
config.get(key: &str) -> Result<&ConfigValue>
config.get_int(key: &str) -> Result<i64>
config.get_float(key: &str) -> Result<f64>
config.get_string(key: &str) -> Result<&str>
config.get_vec2(key: &str) -> Result<Vec2>
config.get_color(key: &str) -> Result<Color>
// Setting values
config.set(key: impl Into<String>, value: ConfigValue)
config.set_variable(name: String, value: String)
// Mutation (requires `mutation` feature)
config.set_int(key, value: i64)
config.set_float(key, value: f64)
config.set_string(key, value: impl Into<String>)
config.remove(key: &str) -> Result<ConfigValue>
config.get_variable_mut(name: &str) -> Option<MutableVariable>
config.remove_variable(name: &str) -> Option<String>
config.add_handler_call(handler, value: String) -> Result<()>
config.remove_handler_call(handler: &str, index: usize) -> Result<String>
config.get_special_category_mut(category, key) -> Result<MutableCategoryInstance>
// Serialization (requires `mutation` feature)
config.serialize() -> String
config.save() -> Result<()>
config.save_as(path: impl AsRef<Path>) -> Result<()>
// Multi-file mutation (requires `mutation` feature)
config.save_all() -> Result<Vec<PathBuf>>
config.serialize_file(path: &Path) -> Result<String>
config.get_key_source_file(key: &str) -> Option<&Path>
config.get_source_files() -> Vec<&Path>
config.get_modified_files() -> Vec<&Path>
// Querying
config.keys() -> Vec<&str>
config.variables() -> &HashMap<String, String>
config.has(key: &str) -> bool
// Handlers
config.register_handler_fn(keyword, handler_fn)
config.register_category_handler_fn(category, keyword, handler_fn)
config.get_handler_calls(handler: &str) -> Option<&Vec<String>>
config.all_handler_calls() -> &HashMap<String, Vec<String>>
// Special categories
config.register_special_category(descriptor)
config.get_special_category(category: &str, key: &str) -> Result<HashMap<String, &ConfigValue>>The repository includes several examples demonstrating different features:
A comprehensive example showing a complete configuration with all features:
cargo run --example pretty_printParse and pretty-print a real Hyprland configuration file:
cargo run --example parse_hyprlandThis example demonstrates:
- Parsing complex real-world configs
- Variable handling
- Nested categories
- Handler calls (binds, windowrules, etc.)
- Beautiful formatted output
Demonstrate the high-level Hyprland API (requires hyprland feature):
cargo run --example hyprland_api --features hyprlandThis example demonstrates:
- Using the
Hyprlandstruct for typed config access - Accessing general, decoration, animation, and input settings
- Getting all binds, windowrules, and other handler calls
- Working with variables
- Comprehensive display of all Hyprland configuration options
Comprehensive example demonstrating mutation and serialization (requires mutation feature):
cargo run --example mutation_example --features mutationThis example demonstrates:
- Mutating configuration values (both direct setters and mutable references)
- Mutating variables using both API styles
- Adding and removing handler calls
- Serializing configurations to strings
- Saving configurations to files
- Round-trip verification (parse → mutate → save → parse)
Run the full test suite:
cargo test
# Run with all features enabled
cargo test --all-featuresThe project includes 177 tests with 100% pass rate:
- 52 unit tests covering core functionality
- 11 conditional directive tests
- 11 expression escaping tests
- 15 windowrule v3 / layerrule v2 tests
- 12 Hyprland config tests
- 10 mutation & round-trip serialization tests
- 6 multi-file mutation tests
- 19 parsing edge case tests
- 41 documentation tests
All tests from the original Hyprlang C++ implementation have been ported and pass successfully, plus additional tests for new features like expression escaping, negated conditionals, windowrule v3 syntax, and comprehensive edge case coverage.
The parser is implemented using pest with a PEG grammar. The grammar file is located at src/hyprlang.pest.
Key syntax features:
- Comments:
#for single-line,##for documentation - Variables:
$VAR = value,$env:PATH(environment variables) - Expressions:
{{expr}}with arithmetic operators (+, -, *, /) - Expression escaping:
\{{}}or{\{}}for literal braces - Categories:
category { ... }(nested supported) - Special categories:
category[key] { ... }(keyed, static, anonymous) - Assignments:
key = value - Handlers:
keyword = value(with optional flags:keyword[flag]) - Source directive:
source = path - Conditional directives:
# hyprlang if VAR,# hyprlang if !VAR,# hyprlang endif - Error suppression:
# hyprlang noerror true/false
This project is a reimplementation of Hyprlang in Rust, based on the original C++ implementation by the Hyprland team.
Contributions are welcome! Please ensure all tests pass before submitting a PR:
cargo test
cargo clippy
cargo fmt --check