aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIvan Molodetskikh <yalterz@gmail.com>2025-09-27 11:20:43 +0300
committerIvan Molodetskikh <yalterz@gmail.com>2025-10-02 09:38:17 +0300
commitb3ae3adbb77c4111366cb59b80d757f361c70237 (patch)
treeaf60f8397ae9971b3ab2889c0925c705abe02a25
parent264289cd41069bd2c85c523a1b0c687eab0a89d7 (diff)
downloadniri-b3ae3adbb77c4111366cb59b80d757f361c70237.tar.gz
niri-b3ae3adbb77c4111366cb59b80d757f361c70237.tar.bz2
niri-b3ae3adbb77c4111366cb59b80d757f361c70237.zip
Partially implement config includes
Subsequent commits will add merging for all leftover sections.
-rw-r--r--niri-config/src/appearance.rs10
-rw-r--r--niri-config/src/error.rs99
-rw-r--r--niri-config/src/lib.rs883
-rw-r--r--niri-config/src/misc.rs11
-rw-r--r--niri-config/tests/wiki-parses.rs4
-rw-r--r--src/layer/mapped.rs4
-rw-r--r--src/layout/mod.rs2
-rw-r--r--src/layout/tests.rs10
-rw-r--r--src/main.rs13
-rw-r--r--src/tests/animations.rs4
-rw-r--r--src/tests/floating.rs2
-rw-r--r--src/tests/window_opening.rs4
-rw-r--r--src/ui/hotkey_overlay.rs2
-rw-r--r--src/utils/mod.rs2
-rw-r--r--src/utils/watcher.rs2
15 files changed, 694 insertions, 358 deletions
diff --git a/niri-config/src/appearance.rs b/niri-config/src/appearance.rs
index bdcbb318..77ee5894 100644
--- a/niri-config/src/appearance.rs
+++ b/niri-config/src/appearance.rs
@@ -1103,8 +1103,7 @@ mod tests {
#[test]
fn rule_color_can_override_base_gradient() {
- let config = Config::parse(
- "test.kdl",
+ let config = Config::parse_mem(
r##"
// Start with gradient set.
layout {
@@ -1127,7 +1126,7 @@ mod tests {
)
.unwrap();
- let mut border = config.resolve_layout().border;
+ let mut border = config.layout.border;
for rule in &config.window_rules {
border.merge_with(&rule.border);
}
@@ -1151,8 +1150,7 @@ mod tests {
#[test]
fn rule_color_can_override_rule_gradient() {
- let config = Config::parse(
- "test.kdl",
+ let config = Config::parse_mem(
r##"
// Start with gradient set.
layout {
@@ -1196,7 +1194,7 @@ mod tests {
)
.unwrap();
- let mut border = config.resolve_layout().border;
+ let mut border = config.layout.border;
let mut tab_indicator_rule = TabIndicatorRule::default();
for rule in &config.window_rules {
border.merge_with(&rule.border);
diff --git a/niri-config/src/error.rs b/niri-config/src/error.rs
new file mode 100644
index 00000000..0210c9c1
--- /dev/null
+++ b/niri-config/src/error.rs
@@ -0,0 +1,99 @@
+use std::error::Error;
+use std::fmt;
+use std::path::PathBuf;
+
+use miette::Diagnostic;
+
+#[derive(Debug)]
+pub struct ConfigParseResult<T, E> {
+ pub config: Result<T, E>,
+
+ // We always try to return includes for the file watcher.
+ //
+ // If the main config is valid, but an included file fails to parse, config will be an Err(),
+ // but includes will still be filled, so that fixing just the included file is enough to
+ // trigger a reload.
+ pub includes: Vec<PathBuf>,
+}
+
+/// Error type that chains main errors with include errors.
+///
+/// Allows miette's Report formatting to have main + include errors all in one.
+#[derive(Debug)]
+pub struct ConfigIncludeError {
+ pub main: knuffel::Error,
+ pub includes: Vec<knuffel::Error>,
+}
+
+impl<T, E> ConfigParseResult<T, E> {
+ pub fn from_err(err: E) -> Self {
+ Self {
+ config: Err(err),
+ includes: Vec::new(),
+ }
+ }
+
+ pub fn map_config_res<U, V>(
+ self,
+ f: impl FnOnce(Result<T, E>) -> Result<U, V>,
+ ) -> ConfigParseResult<U, V> {
+ ConfigParseResult {
+ config: f(self.config),
+ includes: self.includes,
+ }
+ }
+}
+
+impl fmt::Display for ConfigIncludeError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ fmt::Display::fmt(&self.main, f)
+ }
+}
+
+impl Error for ConfigIncludeError {
+ fn source(&self) -> Option<&(dyn Error + 'static)> {
+ self.main.source()
+ }
+}
+
+impl Diagnostic for ConfigIncludeError {
+ fn code<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
+ self.main.code()
+ }
+
+ fn severity(&self) -> Option<miette::Severity> {
+ self.main.severity()
+ }
+
+ fn help<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
+ self.main.help()
+ }
+
+ fn url<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
+ self.main.url()
+ }
+
+ fn source_code(&self) -> Option<&dyn miette::SourceCode> {
+ self.main.source_code()
+ }
+
+ fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
+ self.main.labels()
+ }
+
+ fn diagnostic_source(&self) -> Option<&dyn Diagnostic> {
+ self.main.diagnostic_source()
+ }
+
+ fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
+ let main_related = self.main.related();
+ let includes_iter = self.includes.iter().map(|err| err as &'a dyn Diagnostic);
+
+ let iter: Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a> = match main_related {
+ Some(main) => Box::new(main.chain(includes_iter)),
+ None => Box::new(includes_iter),
+ };
+
+ Some(iter)
+ }
+}
diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs
index 4f2cf455..14a93332 100644
--- a/niri-config/src/lib.rs
+++ b/niri-config/src/lib.rs
@@ -1,12 +1,29 @@
+//! niri config parsing.
+//!
+//! The config can be constructed from multiple files (includes). To support this, many types are
+//! split into two. For example, `Layout` and `LayoutPart` where `Layout` is the final config and
+//! `LayoutPart` is one part parsed from one config file.
+//!
+//! The convention for `Default` impls is to set the initial values before the parsing occurs.
+//! Then, parsing will update the values with those parsed from the config.
+//!
+//! The `Default` values match those from `default-config.kdl` in almost all cases, with a notable
+//! exception of `binds {}` and some window rules.
+
#[macro_use]
extern crate tracing;
+use std::cell::RefCell;
+use std::collections::HashSet;
use std::ffi::OsStr;
use std::fs::{self, File};
use std::io::Write as _;
use std::path::{Path, PathBuf};
+use std::rc::Rc;
-use miette::{Context as _, IntoDiagnostic as _};
+use knuffel::errors::DecodeError;
+use knuffel::Decode as _;
+use miette::{miette, Context as _, IntoDiagnostic as _};
#[macro_use]
pub mod macros;
@@ -15,6 +32,7 @@ pub mod animations;
pub mod appearance;
pub mod binds;
pub mod debug;
+pub mod error;
pub mod gestures;
pub mod input;
pub mod layer_rule;
@@ -29,6 +47,7 @@ pub use crate::animations::{Animation, Animations};
pub use crate::appearance::*;
pub use crate::binds::*;
pub use crate::debug::Debug;
+pub use crate::error::{ConfigIncludeError, ConfigParseResult};
pub use crate::gestures::Gestures;
pub use crate::input::{Input, ModKey, ScrollMethod, TrackLayout, WarpMouseToFocusMode, Xkb};
pub use crate::layer_rule::LayerRule;
@@ -36,61 +55,35 @@ pub use crate::layout::*;
pub use crate::misc::*;
pub use crate::output::{Output, OutputName, Outputs, Position, Vrr};
pub use crate::utils::FloatOrInt;
-use crate::utils::MergeWith as _;
+use crate::utils::{Flag, MergeWith as _};
pub use crate::window_rule::{FloatingPosition, RelativeTo, WindowRule};
pub use crate::workspace::{Workspace, WorkspaceLayoutPart};
-#[derive(knuffel::Decode, Debug, PartialEq)]
+const RECURSION_LIMIT: u8 = 10;
+
+#[derive(Debug, Default, PartialEq)]
pub struct Config {
- #[knuffel(child, default)]
pub input: Input,
- #[knuffel(children(name = "output"))]
pub outputs: Outputs,
- #[knuffel(children(name = "spawn-at-startup"))]
pub spawn_at_startup: Vec<SpawnAtStartup>,
- #[knuffel(children(name = "spawn-sh-at-startup"))]
pub spawn_sh_at_startup: Vec<SpawnShAtStartup>,
- #[knuffel(child, default)]
- pub layout: LayoutPart,
- #[knuffel(child, default)]
+ pub layout: Layout,
pub prefer_no_csd: bool,
- #[knuffel(child, default)]
pub cursor: Cursor,
- #[knuffel(
- child,
- unwrap(argument),
- default = Some(String::from(
- "~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png"
- )))
- ]
- pub screenshot_path: Option<String>,
- #[knuffel(child, default)]
+ pub screenshot_path: ScreenshotPath,
pub clipboard: Clipboard,
- #[knuffel(child, default)]
pub hotkey_overlay: HotkeyOverlay,
- #[knuffel(child, default)]
pub config_notification: ConfigNotification,
- #[knuffel(child, default)]
pub animations: Animations,
- #[knuffel(child, default)]
pub gestures: Gestures,
- #[knuffel(child, default)]
pub overview: Overview,
- #[knuffel(child, default)]
pub environment: Environment,
- #[knuffel(child, default)]
pub xwayland_satellite: XwaylandSatellite,
- #[knuffel(children(name = "window-rule"))]
pub window_rules: Vec<WindowRule>,
- #[knuffel(children(name = "layer-rule"))]
pub layer_rules: Vec<LayerRule>,
- #[knuffel(child, default)]
pub binds: Binds,
- #[knuffel(child, default)]
pub switch_events: SwitchBinds,
- #[knuffel(child, default)]
pub debug: Debug,
- #[knuffel(children(name = "workspace"))]
pub workspaces: Vec<Workspace>,
}
@@ -113,79 +106,347 @@ pub enum ConfigPath {
},
}
-impl Config {
- pub fn load(path: &Path) -> miette::Result<Self> {
- let contents = fs::read_to_string(path)
- .into_diagnostic()
- .with_context(|| format!("error reading {path:?}"))?;
-
- let config = Self::parse(
- path.file_name()
- .and_then(OsStr::to_str)
- .unwrap_or("config.kdl"),
- &contents,
- )
- .context("error parsing")?;
- debug!("loaded config from {path:?}");
- Ok(config)
- }
+// Newtypes for putting information into the knuffel context.
+struct BasePath(PathBuf);
+struct RootBase(PathBuf);
+struct Recursion(u8);
+#[derive(Default)]
+struct Includes(Vec<PathBuf>);
+#[derive(Default)]
+struct IncludeErrors(Vec<knuffel::Error>);
+
+// Rather than listing all fields and deriving knuffel::Decode, we implement
+// knuffel::DecodeChildren by hand, since we need custom logic for every field anyway: we want to
+// merge the values into the config from the context as we go to support the positionality of
+// includes. The reason we need this type at all is because knuffel's only entry point that allows
+// setting default values on a context is `parse_with_context()` that needs a type to parse.
+pub struct ConfigPart;
+
+impl<S> knuffel::DecodeChildren<S> for ConfigPart
+where
+ S: knuffel::traits::ErrorSpan,
+{
+ fn decode_children(
+ nodes: &[knuffel::ast::SpannedNode<S>],
+ ctx: &mut knuffel::decode::Context<S>,
+ ) -> Result<Self, DecodeError<S>> {
+ let _span = tracy_client::span!("parse config file");
+
+ let config = ctx.get::<Rc<RefCell<Config>>>().unwrap().clone();
+ let includes = ctx.get::<Rc<RefCell<Includes>>>().unwrap().clone();
+ let include_errors = ctx.get::<Rc<RefCell<IncludeErrors>>>().unwrap().clone();
+ let recursion = ctx.get::<Recursion>().unwrap().0;
+
+ let mut seen = HashSet::new();
+
+ for node in nodes {
+ let name = &**node.node_name;
+
+ // Within one config file, splitting sections into multiple parts is not allowed to
+ // reduce confusion. The exceptions here aren't multipart; they all add new values.
+ if !matches!(
+ name,
+ "output"
+ | "spawn-at-startup"
+ | "spawn-sh-at-startup"
+ | "window-rule"
+ | "layer-rule"
+ | "workspace"
+ | "include"
+ ) && !seen.insert(name)
+ {
+ ctx.emit_error(DecodeError::unexpected(
+ &node.node_name,
+ "node",
+ format!("duplicate node `{name}`, single node expected"),
+ ));
+ continue;
+ }
- pub fn parse(filename: &str, text: &str) -> Result<Self, knuffel::Error> {
- let _span = tracy_client::span!("Config::parse");
- knuffel::parse(filename, text)
- }
+ macro_rules! m_replace {
+ ($field:ident) => {{
+ let part = knuffel::Decode::decode_node(node, ctx)?;
+ config.borrow_mut().$field = part;
+ }};
+ }
+
+ macro_rules! m_merge {
+ ($field:ident) => {{
+ let part = knuffel::Decode::decode_node(node, ctx)?;
+ config.borrow_mut().$field.merge_with(&part);
+ }};
+ }
+
+ macro_rules! m_push {
+ ($field:ident) => {{
+ let part = knuffel::Decode::decode_node(node, ctx)?;
+ config.borrow_mut().$field.push(part);
+ }};
+ }
+
+ match name {
+ // TODO: most (all?) of these need to be merged instead
+ "input" => m_replace!(input),
+ "cursor" => m_replace!(cursor),
+ "clipboard" => m_replace!(clipboard),
+ "hotkey-overlay" => m_replace!(hotkey_overlay),
+ "config-notification" => m_replace!(config_notification),
+ "animations" => m_replace!(animations),
+ "gestures" => m_replace!(gestures),
+ "overview" => m_replace!(overview),
+ "xwayland-satellite" => m_replace!(xwayland_satellite),
+ "switch-events" => m_replace!(switch_events),
+ "debug" => m_replace!(debug),
+
+ // Multipart sections.
+ "output" => {
+ let part = Output::decode_node(node, ctx)?;
+ config.borrow_mut().outputs.0.push(part);
+ }
+ "spawn-at-startup" => m_push!(spawn_at_startup),
+ "spawn-sh-at-startup" => m_push!(spawn_sh_at_startup),
+ "window-rule" => m_push!(window_rules),
+ "layer-rule" => m_push!(layer_rules),
+ "workspace" => m_push!(workspaces),
+
+ // Single-part sections.
+ "binds" => {
+ let part = Binds::decode_node(node, ctx)?;
+
+ // We replace conflicting binds, rather than error, to support the use-case
+ // where you import some preconfigured-dots.kdl, then override some binds with
+ // your own.
+ let mut config = config.borrow_mut();
+ let binds = &mut config.binds.0;
+ // Remove existing binds matching any new bind.
+ binds.retain(|bind| !part.0.iter().any(|new| new.key == bind.key));
+ // Add all new binds.
+ binds.extend(part.0);
+ }
+ "environment" => {
+ let part = Environment::decode_node(node, ctx)?;
+ config.borrow_mut().environment.0.extend(part.0);
+ }
+
+ "prefer-no-csd" => {
+ config.borrow_mut().prefer_no_csd = Flag::decode_node(node, ctx)?.0
+ }
+
+ "screenshot-path" => {
+ let part = knuffel::Decode::decode_node(node, ctx)?;
+ config.borrow_mut().screenshot_path = part;
+ }
+
+ "layout" => {
+ let mut part = LayoutPart::decode_node(node, ctx)?;
+
+ // Preserve the behavior we'd always had for the border section:
+ // - `layout {}` gives border = off
+ // - `layout { border {} }` gives border = on
+ // - `layout { border { off } }` gives border = off
+ //
+ // This behavior is inconsistent with the rest of the config where adding an
+ // empty section generally doesn't change the outcome. Particularly, shadows
+ // are also disabled by default (like borders), and they always had an `on`
+ // instead of an `off` for this reason, so that writing `layout { shadow {} }`
+ // still results in shadow = off, as it should.
+ //
+ // Unfortunately, the default config has always had wording that heavily
+ // implies that `layout { border {} }` enables the borders. This wording is
+ // sure to be present in a lot of users' configs by now, which we can't change.
+ //
+ // Another way to make things consistent would be to default borders to on.
+ // However, that is annoying because it would mean changing many tests that
+ // rely on borders being off by default. This would also contradict the
+ // intended default borders value (off).
+ //
+ // So, let's just work around the problem here, preserving the original
+ // behavior.
+ if recursion == 0 {
+ if let Some(border) = part.border.as_mut() {
+ if !border.on && !border.off {
+ border.on = true;
+ }
+ }
+ }
+
+ config.borrow_mut().layout.merge_with(&part);
+ }
+
+ "include" => {
+ let path: PathBuf = utils::parse_arg_node("include", node, ctx)?;
+ let base = ctx.get::<BasePath>().unwrap();
+ let path = base.0.join(path);
+
+ // We use DecodeError::Missing throughout this block because it results in the
+ // least confusing error messages while still allowing to provide a span.
+
+ let recursion = ctx.get::<Recursion>().unwrap().0 + 1;
+ if recursion == RECURSION_LIMIT {
+ ctx.emit_error(DecodeError::missing(
+ node,
+ format!(
+ "reached the recursion limit; \
+ includes cannot be {RECURSION_LIMIT} levels deep"
+ ),
+ ));
+ continue;
+ }
- pub fn resolve_layout(&self) -> Layout {
- let mut rv = Layout::from_part(&self.layout);
-
- // Preserve the behavior we'd always had for the border section:
- // - `layout {}` gives border = off
- // - `layout { border {} }` gives border = on
- // - `layout { border { off } }` gives border = off
- //
- // This behavior is inconsistent with the rest of the config where adding an empty section
- // generally doesn't change the outcome. Particularly, shadows are also disabled by default
- // (like borders), and they always had an `on` instead of an `off` for this reason, so that
- // writing `layout { shadow {} }` still results in shadow = off, as it should.
- //
- // Unfortunately, the default config has always had wording that heavily implies that
- // `layout { border {} }` enables the borders. This wording is sure to be present in a lot
- // of users' configs by now, which we can't change.
- //
- // Another way to make things consistent would be to default borders to on. However, that
- // is annoying because it would mean changing many tests that rely on borders being off by
- // default. This would also contradict the intended default borders value (off).
- //
- // So, let's just work around the problem here, preserving the original behavior.
- if self.layout.border.is_some_and(|x| !x.on && !x.off) {
- rv.border.off = false;
+ let Some(filename) = path.file_name().and_then(OsStr::to_str) else {
+ ctx.emit_error(DecodeError::missing(
+ node,
+ "include path doesn't have a valid file name",
+ ));
+ continue;
+ };
+ let base = path.parent().map(Path::to_path_buf).unwrap_or_default();
+
+ // Store even if the include fails to read or parse, so it gets watched.
+ includes.borrow_mut().0.push(path.to_path_buf());
+
+ match fs::read_to_string(&path) {
+ Ok(text) => {
+ // Try to get filename relative to the root base config folder for
+ // clearer error messages.
+ let root_base = &ctx.get::<RootBase>().unwrap().0;
+ // Failing to strip prefix usually means absolute path; show it in full.
+ let relative_path = path.strip_prefix(root_base).ok().unwrap_or(&path);
+ let filename = relative_path.to_str().unwrap_or(filename);
+
+ let part = knuffel::parse_with_context::<
+ ConfigPart,
+ knuffel::span::Span,
+ _,
+ >(filename, &text, |ctx| {
+ ctx.set(BasePath(base));
+ ctx.set(RootBase(root_base.clone()));
+ ctx.set(Recursion(recursion));
+ ctx.set(includes.clone());
+ ctx.set(include_errors.clone());
+ ctx.set(config.clone());
+ });
+
+ match part {
+ Ok(_) => {}
+ Err(err) => {
+ include_errors.borrow_mut().0.push(err);
+
+ ctx.emit_error(DecodeError::missing(
+ node,
+ "failed to parse included config",
+ ));
+ }
+ }
+ }
+ Err(err) => {
+ ctx.emit_error(DecodeError::missing(
+ node,
+ format!("failed to read included config from {path:?}: {err}"),
+ ));
+ }
+ }
+ }
+
+ name => {
+ ctx.emit_error(DecodeError::unexpected(
+ node,
+ "node",
+ format!("unexpected node `{}`", name.escape_default()),
+ ));
+ }
+ }
}
- rv
+ Ok(Self)
}
}
-impl Default for Config {
- fn default() -> Self {
- Config::parse(
- "default-config.kdl",
+impl Config {
+ pub fn load_default() -> Self {
+ let res = Config::parse(
+ Path::new("default-config.kdl"),
include_str!("../../resources/default-config.kdl"),
- )
- .unwrap()
+ );
+
+ // Includes in the default config can break its parsing at runtime.
+ assert!(
+ res.includes.is_empty(),
+ "default config must not have includes",
+ );
+
+ res.config.unwrap()
+ }
+
+ pub fn load(path: &Path) -> ConfigParseResult<Self, miette::Report> {
+ let contents = match fs::read_to_string(path) {
+ Ok(x) => x,
+ Err(err) => {
+ return ConfigParseResult::from_err(
+ miette!(err).context(format!("error reading {path:?}")),
+ );
+ }
+ };
+
+ Self::parse(path, &contents).map_config_res(|res| {
+ let config = res.context("error parsing")?;
+ debug!("loaded config from {path:?}");
+ Ok(config)
+ })
+ }
+
+ pub fn parse(path: &Path, text: &str) -> ConfigParseResult<Self, ConfigIncludeError> {
+ let base = path.parent().map(Path::to_path_buf).unwrap_or_default();
+ let filename = path
+ .file_name()
+ .and_then(OsStr::to_str)
+ .unwrap_or("config.kdl");
+
+ let config = Rc::new(RefCell::new(Config::default()));
+ let includes = Rc::new(RefCell::new(Includes(Vec::new())));
+ let include_errors = Rc::new(RefCell::new(IncludeErrors(Vec::new())));
+
+ let part = knuffel::parse_with_context::<ConfigPart, knuffel::span::Span, _>(
+ filename,
+ text,
+ |ctx| {
+ ctx.set(BasePath(base.clone()));
+ ctx.set(RootBase(base));
+ ctx.set(Recursion(0));
+ ctx.set(includes.clone());
+ ctx.set(include_errors.clone());
+ ctx.set(config.clone());
+ },
+ );
+
+ let includes = includes.take().0;
+ let include_errors = include_errors.take().0;
+ let config = part
+ .map(|_| config.take())
+ .map_err(move |err| ConfigIncludeError {
+ main: err,
+ includes: include_errors,
+ });
+
+ ConfigParseResult { config, includes }
+ }
+
+ pub fn parse_mem(text: &str) -> Result<Self, ConfigIncludeError> {
+ Self::parse(Path::new("config.kdl"), text).config
}
}
impl ConfigPath {
/// Loads the config, returns an error if it doesn't exist.
- pub fn load(&self) -> miette::Result<Config> {
+ pub fn load(&self) -> ConfigParseResult<Config, miette::Report> {
let _span = tracy_client::span!("ConfigPath::load");
self.load_inner(|user_path, system_path| {
- Err(miette::miette!(
+ Err(miette!(
"no config file found; create one at {user_path:?} or {system_path:?}",
))
})
- .context("error loading config")
+ .map_config_res(|res| res.context("error loading config"))
}
/// Loads the config, or creates it if it doesn't exist.
@@ -194,7 +455,7 @@ impl ConfigPath {
///
/// If the config was created, but for some reason could not be read afterwards,
/// this may return `(Some(_), Err(_))`.
- pub fn load_or_create(&self) -> (Option<&Path>, miette::Result<Config>) {
+ pub fn load_or_create(&self) -> (Option<&Path>, ConfigParseResult<Config, miette::Report>) {
let _span = tracy_client::span!("ConfigPath::load_or_create");
let mut created_at = None;
@@ -205,7 +466,7 @@ impl ConfigPath {
.map(|()| user_path)
.with_context(|| format!("error creating config at {user_path:?}"))
})
- .context("error loading config");
+ .map_config_res(|res| res.context("error loading config"));
(created_at, result)
}
@@ -213,7 +474,7 @@ impl ConfigPath {
fn load_inner<'a>(
&'a self,
maybe_create: impl FnOnce(&'a Path, &'a Path) -> miette::Result<&'a Path>,
- ) -> miette::Result<Config> {
+ ) -> ConfigParseResult<Config, miette::Report> {
let path = match self {
ConfigPath::Explicit(path) => path.as_path(),
ConfigPath::Regular {
@@ -225,7 +486,10 @@ impl ConfigPath {
} else if system_path.exists() {
system_path.as_path()
} else {
- maybe_create(user_path.as_path(), system_path.as_path())?
+ match maybe_create(user_path.as_path(), system_path.as_path()) {
+ Ok(x) => x,
+ Err(err) => return ConfigParseResult::from_err(miette!(err)),
+ }
}
}
};
@@ -274,19 +538,19 @@ mod tests {
#[test]
fn can_create_default_config() {
- let _ = Config::default();
+ let _ = Config::load_default();
}
#[test]
fn default_repeat_params() {
- let config = Config::parse("config.kdl", "").unwrap();
+ let config = Config::parse_mem("").unwrap();
assert_eq!(config.input.keyboard.repeat_delay, 600);
assert_eq!(config.input.keyboard.repeat_rate, 25);
}
#[track_caller]
fn do_parse(text: &str) -> Config {
- Config::parse("test.kdl", text)
+ Config::parse_mem(text)
.map_err(miette::Report::new)
.unwrap()
}
@@ -809,237 +1073,209 @@ mod tests {
command: "qs -c ~/source/qs/MyAwesomeShell",
},
],
- layout: LayoutPart {
- focus_ring: Some(
- BorderRule {
- off: false,
- on: false,
- width: Some(
- FloatOrInt(
- 5.0,
- ),
- ),
- active_color: Some(
- Color {
- r: 0.0,
- g: 0.39215687,
- b: 0.78431374,
+ layout: Layout {
+ focus_ring: FocusRing {
+ off: false,
+ width: 5.0,
+ active_color: Color {
+ r: 0.0,
+ g: 0.39215687,
+ b: 0.78431374,
+ a: 1.0,
+ },
+ inactive_color: Color {
+ r: 1.0,
+ g: 0.78431374,
+ b: 0.39215687,
+ a: 0.0,
+ },
+ urgent_color: Color {
+ r: 0.60784316,
+ g: 0.0,
+ b: 0.0,
+ a: 1.0,
+ },
+ active_gradient: Some(
+ Gradient {
+ from: Color {
+ r: 0.039215688,
+ g: 0.078431375,
+ b: 0.11764706,
a: 1.0,
},
- ),
- inactive_color: Some(
- Color {
- r: 1.0,
- g: 0.78431374,
- b: 0.39215687,
- a: 0.0,
+ to: Color {
+ r: 0.0,
+ g: 0.5019608,
+ b: 1.0,
+ a: 1.0,
},
- ),
- urgent_color: None,
- active_gradient: Some(
- Gradient {
- from: Color {
- r: 0.039215688,
- g: 0.078431375,
- b: 0.11764706,
- a: 1.0,
- },
- to: Color {
- r: 0.0,
- g: 0.5019608,
- b: 1.0,
- a: 1.0,
- },
- angle: 180,
- relative_to: WorkspaceView,
- in_: GradientInterpolation {
- color_space: Srgb,
- hue_interpolation: Shorter,
- },
+ angle: 180,
+ relative_to: WorkspaceView,
+ in_: GradientInterpolation {
+ color_space: Srgb,
+ hue_interpolation: Shorter,
},
- ),
- inactive_gradient: None,
- urgent_gradient: None,
+ },
+ ),
+ inactive_gradient: None,
+ urgent_gradient: None,
+ },
+ border: Border {
+ off: false,
+ width: 3.0,
+ active_color: Color {
+ r: 1.0,
+ g: 0.78431374,
+ b: 0.49803922,
+ a: 1.0,
},
- ),
- border: Some(
- BorderRule {
- off: false,
- on: false,
- width: Some(
- FloatOrInt(
- 3.0,
- ),
+ inactive_color: Color {
+ r: 1.0,
+ g: 0.78431374,
+ b: 0.39215687,
+ a: 0.0,
+ },
+ urgent_color: Color {
+ r: 0.60784316,
+ g: 0.0,
+ b: 0.0,
+ a: 1.0,
+ },
+ active_gradient: None,
+