use crate::config::{Config, ConfigOptions};
use crate::error::{ConfigError, ParseResult};
use crate::special_categories::SpecialCategoryDescriptor;
use crate::types::{Color, ConfigValue};
use std::collections::HashMap;
use std::path::Path;
pub struct RuleInstance<'a> {
values: HashMap<String, &'a ConfigValue>,
}
impl<'a> RuleInstance<'a> {
fn new(values: HashMap<String, &'a ConfigValue>) -> Self {
Self { values }
}
fn parse_color_string(key: &str, value: &str) -> ParseResult<Color> {
if value.starts_with("rgba(") && value.ends_with(')') {
let inner = &value[5..value.len() - 1];
if !inner.contains(',') {
return Color::from_hex(inner);
}
let parts: Vec<&str> = inner.split(',').map(|part| part.trim()).collect();
if parts.len() != 4 {
return Err(ConfigError::invalid_color(value, "rgba needs 4 components"));
}
let r = parts[0]
.parse::<u8>()
.map_err(|_| ConfigError::invalid_color(value, "invalid r"))?;
let g = parts[1]
.parse::<u8>()
.map_err(|_| ConfigError::invalid_color(value, "invalid g"))?;
let b = parts[2]
.parse::<u8>()
.map_err(|_| ConfigError::invalid_color(value, "invalid b"))?;
let a = if parts[3].contains('.') {
let alpha = parts[3]
.parse::<f64>()
.map_err(|_| ConfigError::invalid_color(value, "invalid a"))?;
(alpha * 255.0).round() as u8
} else {
parts[3]
.parse::<u8>()
.map_err(|_| ConfigError::invalid_color(value, "invalid a"))?
};
return Ok(Color::from_rgba(r, g, b, a));
}
if value.starts_with("rgb(") && value.ends_with(')') {
let inner = &value[4..value.len() - 1];
let parts: Vec<&str> = inner.split(',').map(|part| part.trim()).collect();
if parts.len() != 3 {
return Err(ConfigError::invalid_color(value, "rgb needs 3 components"));
}
let r = parts[0]
.parse::<u8>()
.map_err(|_| ConfigError::invalid_color(value, "invalid r"))?;
let g = parts[1]
.parse::<u8>()
.map_err(|_| ConfigError::invalid_color(value, "invalid g"))?;
let b = parts[2]
.parse::<u8>()
.map_err(|_| ConfigError::invalid_color(value, "invalid b"))?;
return Ok(Color::from_rgb(r, g, b));
}
Color::from_hex(value).map_err(|_| ConfigError::type_error(key, "Color", "String"))
}
pub fn get(&self, key: &str) -> ParseResult<&ConfigValue> {
self.values
.get(key)
.copied()
.ok_or_else(|| ConfigError::key_not_found(key))
}
pub fn get_string(&self, key: &str) -> ParseResult<String> {
match self.get(key)? {
ConfigValue::String(s) => Ok(s.clone()),
v => Err(ConfigError::type_error(key, "String", v.type_name())),
}
}
pub fn get_int(&self, key: &str) -> ParseResult<i64> {
match self.get(key)? {
ConfigValue::Int(i) => Ok(*i),
ConfigValue::String(s) => ConfigValue::parse_bool(s)
.map(|b| if b { 1 } else { 0 })
.or_else(|_| ConfigValue::parse_int(s))
.map_err(|_| ConfigError::type_error(key, "Int", "String")),
v => Err(ConfigError::type_error(key, "Int", v.type_name())),
}
}
pub fn get_float(&self, key: &str) -> ParseResult<f64> {
match self.get(key)? {
ConfigValue::Float(f) => Ok(*f),
ConfigValue::Int(i) => Ok(*i as f64),
ConfigValue::String(s) => ConfigValue::parse_float(s)
.or_else(|_| ConfigValue::parse_int(s).map(|i| i as f64))
.map_err(|_| ConfigError::type_error(key, "Float", "String")),
v => Err(ConfigError::type_error(key, "Float", v.type_name())),
}
}
pub fn get_color(&self, key: &str) -> ParseResult<Color> {
match self.get(key)? {
ConfigValue::Color(c) => Ok(*c),
ConfigValue::String(s) => Self::parse_color_string(key, s),
v => Err(ConfigError::type_error(key, "Color", v.type_name())),
}
}
}
pub struct Hyprland {
config: Config,
}
impl Hyprland {
pub fn new() -> Self {
let mut config = Config::new();
Self::register_all_handlers(&mut config);
Self::register_all_special_categories(&mut config);
Self { config }
}
pub fn with_options(options: ConfigOptions) -> Self {
let mut config = Config::with_options(options);
Self::register_all_handlers(&mut config);
Self::register_all_special_categories(&mut config);
Self { config }
}
pub fn config(&self) -> &Config {
&self.config
}
pub fn config_mut(&mut self) -> &mut Config {
&mut self.config
}
pub fn parse(&mut self, content: &str) -> ParseResult<()> {
self.config.parse(content)
}
pub fn parse_file(&mut self, path: &Path) -> ParseResult<()> {
self.config.parse_file(path)
}
fn register_all_handlers(config: &mut Config) {
let root_handlers = [
"monitor",
"env",
"bind",
"bindu", "bindm",
"bindel",
"bindl",
"bindr",
"binde",
"bindn",
"windowrule",
"windowrulev2",
"layerrule",
"workspace",
"exec",
"exec-once",
"source",
"blurls",
"plugin",
];
for handler in root_handlers {
config.register_handler_fn(handler, |_ctx| Ok(()));
}
config.register_category_handler_fn("animations", "animation", |_ctx| Ok(()));
config.register_category_handler_fn("animations", "bezier", |_ctx| Ok(()));
}
fn register_all_special_categories(config: &mut Config) {
config.register_special_category(
SpecialCategoryDescriptor::keyed("device", "name").with_ignore_missing(),
);
config.register_special_category_value("device", "enabled", ConfigValue::Int(0));
config.register_special_category_value("device", "sensitivity", ConfigValue::Float(0.0));
config.register_special_category(
SpecialCategoryDescriptor::keyed("monitor", "name").with_ignore_missing(),
);
config.register_special_category(SpecialCategoryDescriptor::keyed("windowrule", "name"));
Self::register_windowrule_properties(config);
config.register_special_category(SpecialCategoryDescriptor::keyed("layerrule", "name"));
Self::register_layerrule_properties(config);
}
fn register_windowrule_properties(config: &mut Config) {
config.register_special_category_value("windowrule", "enable", ConfigValue::Int(1));
let match_props = [
"class", "title", "initial_class", "initial_title", "floating", "tag", "xwayland", "fullscreen", "pinned", "focus", "group", "modal", "fullscreenstate_internal", "fullscreenstate_client", "on_workspace", "content", "xdg_tag", "namespace", "exec_token", ];
for prop in match_props {
config.register_special_category_value(
"windowrule",
format!("match:{}", prop),
ConfigValue::String(String::new()),
);
}
let match_aliases = [
"float", "pin", "workspace", "fullscreen_state_internal", "fullscreen_state_client", ];
for alias in match_aliases {
config.register_special_category_value(
"windowrule",
format!("match:{}", alias),
ConfigValue::String(String::new()),
);
}
let effect_props = [
"float",
"tile",
"fullscreen",
"maximize",
"fullscreenstate",
"fullscreen_state", "move",
"size",
"center",
"pseudo",
"monitor",
"workspace",
"noinitialfocus",
"no_initial_focus", "pin",
"group",
"suppressevent",
"suppress_event", "content",
"noclosefor",
"no_close_for", "rounding",
"rounding_power",
"persistent_size",
"animation",
"border_color",
"bordercolor", "idle_inhibit",
"idleinhibit", "opacity",
"tag",
"max_size",
"maxsize", "min_size",
"minsize", "border_size",
"bordersize", "allows_input",
"dim_around",
"decorate",
"focus_on_activate",
"keep_aspect_ratio",
"keepaspectratio", "nearest_neighbor",
"nearestneighbor", "no_anim",
"noanim", "no_blur",
"noblur", "no_dim",
"nodim", "no_focus",
"nofocus", "no_follow_mouse",
"nofollowmouse", "no_max_size",
"nomaxsize", "no_shadow",
"noshadow", "no_shortcuts_inhibit",
"noshortcutsinhibit", "opaque",
"force_rgbx",
"forcergbx", "sync_fullscreen",
"syncfullscreen", "immediate",
"xray",
"render_unfocused",
"renderunfocused", "no_screen_share",
"noscreenshare", "no_vrr",
"novrr", "scroll_mouse",
"scrollmouse", "scroll_touchpad",
"scrolltouchpad", "stay_focused",
"stayfocused", ];
for prop in effect_props {
config.register_special_category_value(
"windowrule",
prop,
ConfigValue::String(String::new()),
);
}
}
fn register_layerrule_properties(config: &mut Config) {
config.register_special_category_value("layerrule", "enable", ConfigValue::Int(1));
let match_props = [
"namespace", "address", "class", "title", "monitor", "layer", ];
for prop in match_props {
config.register_special_category_value(
"layerrule",
format!("match:{}", prop),
ConfigValue::String(String::new()),
);
}
let effect_props = [
"blur", "blur_popups", "ignorealpha", "ignore_alpha", "ignorezero", "animation", "noanim", "no_anim", "xray", "dim_around", "order", "above_lock", "no_screen_share", "noscreenshare", ];
for prop in effect_props {
config.register_special_category_value(
"layerrule",
prop,
ConfigValue::String(String::new()),
);
}
}
pub fn general_border_size(&self) -> ParseResult<i64> {
self.config.get_int("general:border_size")
}
pub fn general_gaps_in(&self) -> ParseResult<String> {
match self.config.get("general:gaps_in")? {
ConfigValue::Int(i) => Ok(i.to_string()),
ConfigValue::String(s) => Ok(s.clone()),
_ => Ok("5".to_string()),
}
}
pub fn general_gaps_out(&self) -> ParseResult<String> {
match self.config.get("general:gaps_out")? {
ConfigValue::Int(i) => Ok(i.to_string()),
ConfigValue::String(s) => Ok(s.clone()),
_ => Ok("20".to_string()),
}
}
pub fn general_active_border_color(&self) -> ParseResult<Color> {
self.config.get_color("general:col.active_border")
}
pub fn general_inactive_border_color(&self) -> ParseResult<Color> {
self.config.get_color("general:col.inactive_border")
}
pub fn general_layout(&self) -> ParseResult<&str> {
self.config.get_string("general:layout")
}
pub fn general_allow_tearing(&self) -> ParseResult<bool> {
match self.config.get("general:allow_tearing")? {
ConfigValue::Int(i) => Ok(*i != 0),
ConfigValue::String(s) => Ok(s == "true" || s == "yes" || s == "on" || s == "1"),
_ => Ok(false),
}
}
pub fn general_locale(&self) -> ParseResult<&str> {
self.config.get_string("general:locale")
}
pub fn decoration_rounding(&self) -> ParseResult<i64> {
self.config.get_int("decoration:rounding")
}
pub fn decoration_active_opacity(&self) -> ParseResult<f64> {
self.config.get_float("decoration:active_opacity")
}
pub fn decoration_inactive_opacity(&self) -> ParseResult<f64> {
self.config.get_float("decoration:inactive_opacity")
}
pub fn decoration_blur_enabled(&self) -> ParseResult<bool> {
match self.config.get("decoration:blur:enabled")? {
ConfigValue::Int(i) => Ok(*i != 0),
ConfigValue::String(s) => Ok(s == "true" || s == "yes" || s == "on" || s == "1"),
_ => Ok(false),
}
}
pub fn decoration_blur_size(&self) -> ParseResult<i64> {
self.config.get_int("decoration:blur:size")
}
pub fn decoration_blur_passes(&self) -> ParseResult<i64> {
self.config.get_int("decoration:blur:passes")
}
pub fn animations_enabled(&self) -> ParseResult<bool> {
match self.config.get("animations:enabled")? {
ConfigValue::Int(i) => Ok(*i != 0),
ConfigValue::String(s) => Ok(s == "true" || s == "yes" || s == "on" || s == "1"),
_ => Ok(false),
}
}
pub fn all_animations(&self) -> Vec<&String> {
self.config
.get_handler_calls("animations:animation")
.map(|calls| calls.iter().collect())
.unwrap_or_default()
}
pub fn all_beziers(&self) -> Vec<&String> {
self.config
.get_handler_calls("animations:bezier")
.map(|calls| calls.iter().collect())
.unwrap_or_default()
}
pub fn input_kb_layout(&self) -> ParseResult<&str> {
self.config.get_string("input:kb_layout")
}
pub fn input_follow_mouse(&self) -> ParseResult<i64> {
self.config.get_int("input:follow_mouse")
}
pub fn input_sensitivity(&self) -> ParseResult<f64> {
self.config.get_float("input:sensitivity")
}
pub fn input_touchpad_natural_scroll(&self) -> ParseResult<bool> {
match self.config.get("input:touchpad:natural_scroll")? {
ConfigValue::Int(i) => Ok(*i != 0),
ConfigValue::String(s) => Ok(s == "true" || s == "yes" || s == "on" || s == "1"),
_ => Ok(false),
}
}
pub fn misc_disable_hyprland_logo(&self) -> ParseResult<bool> {
match self.config.get("misc:disable_hyprland_logo")? {
ConfigValue::Int(i) => Ok(*i != 0),
ConfigValue::String(s) => Ok(s == "true" || s == "yes" || s == "on" || s == "1"),
_ => Ok(false),
}
}
pub fn misc_force_default_wallpaper(&self) -> ParseResult<i64> {
self.config.get_int("misc:force_default_wallpaper")
}
pub fn quirks_prefer_hdr(&self) -> ParseResult<i64> {
self.config.get_int("quirks:prefer_hdr")
}
pub fn cursor_hide_on_tablet(&self) -> ParseResult<bool> {
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),
}
}
pub fn group_groupbar_blur(&self) -> ParseResult<bool> {
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),
}
}
pub fn dwindle_pseudotile(&self) -> ParseResult<bool> {
match self.config.get("dwindle:pseudotile")? {
ConfigValue::Int(i) => Ok(*i != 0),
ConfigValue::String(s) => Ok(s == "true" || s == "yes" || s == "on" || s == "1"),
_ => Ok(false),
}
}
pub fn dwindle_preserve_split(&self) -> ParseResult<bool> {
match self.config.get("dwindle:preserve_split")? {
ConfigValue::Int(i) => Ok(*i != 0),
ConfigValue::String(s) => Ok(s == "true" || s == "yes" || s == "on" || s == "1"),
_ => Ok(false),
}
}
pub fn master_new_status(&self) -> ParseResult<&str> {
self.config.get_string("master:new_status")
}
pub fn all_binds(&self) -> Vec<&String> {
self.config
.get_handler_calls("bind")
.map(|calls| calls.iter().collect())
.unwrap_or_default()
}
pub fn all_bindm(&self) -> Vec<&String> {
self.config
.get_handler_calls("bindm")
.map(|calls| calls.iter().collect())
.unwrap_or_default()
}
pub fn all_bindel(&self) -> Vec<&String> {
self.config
.get_handler_calls("bindel")
.map(|calls| calls.iter().collect())
.unwrap_or_default()
}
pub fn all_bindl(&self) -> Vec<&String> {
self.config
.get_handler_calls("bindl")
.map(|calls| calls.iter().collect())
.unwrap_or_default()
}
pub fn all_bindu(&self) -> Vec<&String> {
self.config
.get_handler_calls("bindu")
.map(|calls| calls.iter().collect())
.unwrap_or_default()
}
#[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")
.map(|calls| calls.iter().collect())
.unwrap_or_default()
}
#[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")
.map(|calls| calls.iter().collect())
.unwrap_or_default()
}
pub fn windowrule_names(&self) -> Vec<String> {
self.config.list_special_category_keys("windowrule")
}
pub fn get_windowrule(&self, name: &str) -> ParseResult<RuleInstance<'_>> {
self.config
.get_special_category("windowrule", name)
.map(RuleInstance::new)
}
#[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")
.map(|calls| calls.iter().collect())
.unwrap_or_default()
}
pub fn layerrule_names(&self) -> Vec<String> {
self.config.list_special_category_keys("layerrule")
}
pub fn get_layerrule(&self, name: &str) -> ParseResult<RuleInstance<'_>> {
self.config
.get_special_category("layerrule", name)
.map(RuleInstance::new)
}
pub fn all_workspaces(&self) -> Vec<&String> {
self.config
.get_handler_calls("workspace")
.map(|calls| calls.iter().collect())
.unwrap_or_default()
}
pub fn all_monitors(&self) -> Vec<&String> {
self.config
.get_handler_calls("monitor")
.map(|calls| calls.iter().collect())
.unwrap_or_default()
}
pub fn all_env(&self) -> Vec<&String> {
self.config
.get_handler_calls("env")
.map(|calls| calls.iter().collect())
.unwrap_or_default()
}
pub fn all_exec_once(&self) -> Vec<&String> {
self.config
.get_handler_calls("exec-once")
.map(|calls| calls.iter().collect())
.unwrap_or_default()
}
pub fn all_exec(&self) -> Vec<&String> {
self.config
.get_handler_calls("exec")
.map(|calls| calls.iter().collect())
.unwrap_or_default()
}
pub fn variables(&self) -> &std::collections::HashMap<String, String> {
self.config.variables()
}
pub fn get_variable(&self, name: &str) -> Option<&String> {
self.variables().get(name)
}
}
impl Default for Hyprland {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hyprland_basic_config() {
let mut hypr = Hyprland::new();
hypr.parse(
r#"
general {
border_size = 2
gaps_in = 5
gaps_out = 20
layout = dwindle
}
"#,
)
.unwrap();
assert_eq!(hypr.general_border_size().unwrap(), 2);
assert_eq!(hypr.general_gaps_in().unwrap(), "5".to_string());
assert_eq!(hypr.general_gaps_out().unwrap(), "20".to_string());
assert_eq!(hypr.general_layout().unwrap(), "dwindle");
}
#[test]
fn test_hyprland_binds() {
let mut hypr = Hyprland::new();
hypr.parse(
r#"
bind = SUPER, Q, exec, kitty
bind = SUPER, C, killactive
"#,
)
.unwrap();
let binds = hypr.all_binds();
assert_eq!(binds.len(), 2);
assert_eq!(binds[0], "SUPER, Q, exec, kitty");
assert_eq!(binds[1], "SUPER, C, killactive");
}
#[test]
fn test_hyprland_animations() {
let mut hypr = Hyprland::new();
hypr.parse(
r#"
animations {
enabled = true
animation = windows, 1, 4, default
animation = fade, 1, 3, quick
bezier = easeOut, 0.23, 1, 0.32, 1
}
"#,
)
.unwrap();
assert!(hypr.animations_enabled().unwrap());
let animations = hypr.all_animations();
assert_eq!(animations.len(), 2);
let beziers = hypr.all_beziers();
assert_eq!(beziers.len(), 1);
}
#[test]
fn test_hyprland_variables() {
let mut hypr = Hyprland::new();
hypr.parse(
r#"
$terminal = kitty
$mod = SUPER
"#,
)
.unwrap();
let vars = hypr.variables();
assert_eq!(vars.get("terminal"), Some(&"kitty".to_string()));
assert_eq!(vars.get("mod"), Some(&"SUPER".to_string()));
}
#[test]
fn test_hyprland_decoration() {
let mut hypr = Hyprland::new();
hypr.parse(
r#"
decoration {
rounding = 10
active_opacity = 1.0
inactive_opacity = 0.8
blur {
enabled = true
size = 3
passes = 1
}
}
"#,
)
.unwrap();
assert_eq!(hypr.decoration_rounding().unwrap(), 10);
assert_eq!(hypr.decoration_active_opacity().unwrap(), 1.0);
assert_eq!(hypr.decoration_inactive_opacity().unwrap(), 0.8);
assert!(hypr.decoration_blur_enabled().unwrap());
assert_eq!(hypr.decoration_blur_size().unwrap(), 3);
assert_eq!(hypr.decoration_blur_passes().unwrap(), 1);
}
}