diff --git a/docs/wiki/Configuration:-Include.md b/docs/wiki/Configuration:-Include.md index db4817cc..bbf6a955 100644 --- a/docs/wiki/Configuration:-Include.md +++ b/docs/wiki/Configuration:-Include.md @@ -114,6 +114,30 @@ window-rule { } ``` +### Optional includes + +Since: next release + +By default, including a nonexistent file will cause an error. +You can allow nonexistent includes by setting `optional=true`: + +```kdl,must-fail +// Won't fail if this file doesn't exist. +include optional=true "optional-config.kdl" + +// Regular include, will fail if the file doesn't exist. +include "required-config.kdl" +``` + +When an optional include file is missing, niri will emit a warning in the logs on every config reload. +This reminds you that the file is missing while still loading the config successfully. + +The optional file is still watched for changes, so if you create it later, the config will automatically reload and apply the new settings. + +Note that `optional` only affects whether a missing file causes an error. +If the file exists but contains invalid syntax or other errors, those errors will still cause a parsing failure. + + ### Merging Most config sections are merged between includes, meaning that you can set only a few properties, and only those properties will change. diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index 545ae5a4..b61fe1c1 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -291,7 +291,51 @@ where } "include" => { - let path: PathBuf = utils::parse_arg_node("include", node, ctx)?; + // Parse the path argument + let mut iter_args = node.arguments.iter(); + let path_val = iter_args.next().ok_or_else(|| { + DecodeError::missing( + node, + "additional argument for include path is required", + ) + })?; + let path: PathBuf = knuffel::traits::DecodeScalar::decode(path_val, ctx)?; + + // Check for extra arguments + if let Some(val) = iter_args.next() { + ctx.emit_error(DecodeError::unexpected( + &val.literal, + "argument", + "unexpected argument", + )); + } + + // Parse the optional property + let mut optional = false; + for (name, val) in &node.properties { + match &***name { + "optional" => { + optional = knuffel::traits::DecodeScalar::decode(val, ctx)?; + } + name_str => { + ctx.emit_error(DecodeError::unexpected( + name, + "property", + format!("unexpected property `{}`", name_str.escape_default()), + )); + } + } + } + + // Check for unexpected children + for child in node.children() { + ctx.emit_error(DecodeError::unexpected( + child, + "node", + format!("unexpected node `{}`", child.node_name.escape_default()), + )); + } + let base = ctx.get::().unwrap(); let path = base.0.join(path); @@ -369,10 +413,16 @@ where } } Err(err) => { - ctx.emit_error(DecodeError::missing( - node, - format!("failed to read included config from {path:?}: {err}"), - )); + if optional && err.kind() == std::io::ErrorKind::NotFound { + // Warn about missing optional includes + warn!("optional include not found: {path:?}"); + } else { + // Report all other errors normally + ctx.emit_error(DecodeError::missing( + node, + format!("failed to read included config from {path:?}: {err}"), + )); + } } } }