mirror of
https://github.com/niri-wm/niri.git
synced 2026-06-24 02:01:18 +07:00
Partially implement config includes
Subsequent commits will add merging for all leftover sections.
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
+422
-195
@@ -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:?}"))?;
|
||||
// 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>);
|
||||
|
||||
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)
|
||||
// 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;
|
||||
}};
|
||||
}
|
||||
|
||||
pub fn resolve_layout(&self) -> Layout {
|
||||
let mut rv = Layout::from_part(&self.layout);
|
||||
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.
|
||||
// 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.
|
||||
// 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).
|
||||
// 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;
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rv
|
||||
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;
|
||||
}
|
||||
|
||||
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()),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,33 +1073,28 @@ mod tests {
|
||||
command: "qs -c ~/source/qs/MyAwesomeShell",
|
||||
},
|
||||
],
|
||||
layout: LayoutPart {
|
||||
focus_ring: Some(
|
||||
BorderRule {
|
||||
layout: Layout {
|
||||
focus_ring: FocusRing {
|
||||
off: false,
|
||||
on: false,
|
||||
width: Some(
|
||||
FloatOrInt(
|
||||
5.0,
|
||||
),
|
||||
),
|
||||
active_color: Some(
|
||||
Color {
|
||||
width: 5.0,
|
||||
active_color: Color {
|
||||
r: 0.0,
|
||||
g: 0.39215687,
|
||||
b: 0.78431374,
|
||||
a: 1.0,
|
||||
},
|
||||
),
|
||||
inactive_color: Some(
|
||||
Color {
|
||||
inactive_color: Color {
|
||||
r: 1.0,
|
||||
g: 0.78431374,
|
||||
b: 0.39215687,
|
||||
a: 0.0,
|
||||
},
|
||||
),
|
||||
urgent_color: None,
|
||||
urgent_color: Color {
|
||||
r: 0.60784316,
|
||||
g: 0.0,
|
||||
b: 0.0,
|
||||
a: 1.0,
|
||||
},
|
||||
active_gradient: Some(
|
||||
Gradient {
|
||||
from: Color {
|
||||
@@ -861,37 +1120,34 @@ mod tests {
|
||||
inactive_gradient: None,
|
||||
urgent_gradient: None,
|
||||
},
|
||||
),
|
||||
border: Some(
|
||||
BorderRule {
|
||||
border: Border {
|
||||
off: false,
|
||||
on: false,
|
||||
width: Some(
|
||||
FloatOrInt(
|
||||
3.0,
|
||||
),
|
||||
),
|
||||
active_color: None,
|
||||
inactive_color: Some(
|
||||
Color {
|
||||
width: 3.0,
|
||||
active_color: Color {
|
||||
r: 1.0,
|
||||
g: 0.78431374,
|
||||
b: 0.49803922,
|
||||
a: 1.0,
|
||||
},
|
||||
inactive_color: Color {
|
||||
r: 1.0,
|
||||
g: 0.78431374,
|
||||
b: 0.39215687,
|
||||
a: 0.0,
|
||||
},
|
||||
),
|
||||
urgent_color: None,
|
||||
urgent_color: Color {
|
||||
r: 0.60784316,
|
||||
g: 0.0,
|
||||
b: 0.0,
|
||||
a: 1.0,
|
||||
},
|
||||
active_gradient: None,
|
||||
inactive_gradient: None,
|
||||
urgent_gradient: None,
|
||||
},
|
||||
),
|
||||
shadow: Some(
|
||||
ShadowRule {
|
||||
off: false,
|
||||
shadow: Shadow {
|
||||
on: false,
|
||||
offset: Some(
|
||||
ShadowOffset {
|
||||
offset: ShadowOffset {
|
||||
x: FloatOrInt(
|
||||
10.0,
|
||||
),
|
||||
@@ -899,32 +1155,31 @@ mod tests {
|
||||
-20.0,
|
||||
),
|
||||
},
|
||||
),
|
||||
softness: None,
|
||||
spread: None,
|
||||
draw_behind_window: None,
|
||||
color: None,
|
||||
softness: 30.0,
|
||||
spread: 5.0,
|
||||
draw_behind_window: false,
|
||||
color: Color {
|
||||
r: 0.0,
|
||||
g: 0.0,
|
||||
b: 0.0,
|
||||
a: 0.46666667,
|
||||
},
|
||||
inactive_color: None,
|
||||
},
|
||||
),
|
||||
tab_indicator: Some(
|
||||
TabIndicatorPart {
|
||||
tab_indicator: TabIndicator {
|
||||
off: false,
|
||||
on: false,
|
||||
hide_when_single_tab: None,
|
||||
place_within_column: None,
|
||||
gap: None,
|
||||
width: Some(
|
||||
FloatOrInt(
|
||||
10.0,
|
||||
hide_when_single_tab: false,
|
||||
place_within_column: false,
|
||||
gap: 5.0,
|
||||
width: 10.0,
|
||||
length: TabIndicatorLength {
|
||||
total_proportion: Some(
|
||||
0.5,
|
||||
),
|
||||
),
|
||||
length: None,
|
||||
position: Some(
|
||||
Top,
|
||||
),
|
||||
gaps_between_tabs: None,
|
||||
corner_radius: None,
|
||||
},
|
||||
position: Top,
|
||||
gaps_between_tabs: 0.0,
|
||||
corner_radius: 0.0,
|
||||
active_color: None,
|
||||
inactive_color: None,
|
||||
urgent_color: None,
|
||||
@@ -932,19 +1187,14 @@ mod tests {
|
||||
inactive_gradient: None,
|
||||
urgent_gradient: None,
|
||||
},
|
||||
),
|
||||
insert_hint: Some(
|
||||
InsertHintPart {
|
||||
insert_hint: InsertHint {
|
||||
off: false,
|
||||
on: false,
|
||||
color: Some(
|
||||
Color {
|
||||
color: Color {
|
||||
r: 1.0,
|
||||
g: 0.78431374,
|
||||
b: 0.49803922,
|
||||
a: 1.0,
|
||||
},
|
||||
),
|
||||
gradient: Some(
|
||||
Gradient {
|
||||
from: Color {
|
||||
@@ -968,9 +1218,7 @@ mod tests {
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
preset_column_widths: Some(
|
||||
[
|
||||
preset_column_widths: [
|
||||
Proportion(
|
||||
0.25,
|
||||
),
|
||||
@@ -984,18 +1232,12 @@ mod tests {
|
||||
1280,
|
||||
),
|
||||
],
|
||||
),
|
||||
default_column_width: Some(
|
||||
DefaultPresetSize(
|
||||
Some(
|
||||
Proportion(
|
||||
0.25,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
preset_window_heights: Some(
|
||||
[
|
||||
preset_window_heights: [
|
||||
Proportion(
|
||||
0.25,
|
||||
),
|
||||
@@ -1009,22 +1251,12 @@ mod tests {
|
||||
1280,
|
||||
),
|
||||
],
|
||||
),
|
||||
center_focused_column: Some(
|
||||
OnOverflow,
|
||||
),
|
||||
always_center_single_column: None,
|
||||
empty_workspace_above_first: None,
|
||||
default_column_display: Some(
|
||||
Tabbed,
|
||||
),
|
||||
gaps: Some(
|
||||
FloatOrInt(
|
||||
8.0,
|
||||
),
|
||||
),
|
||||
struts: Some(
|
||||
Struts {
|
||||
center_focused_column: OnOverflow,
|
||||
always_center_single_column: false,
|
||||
empty_workspace_above_first: false,
|
||||
default_column_display: Tabbed,
|
||||
gaps: 8.0,
|
||||
struts: Struts {
|
||||
left: FloatOrInt(
|
||||
1.0,
|
||||
),
|
||||
@@ -1038,8 +1270,12 @@ mod tests {
|
||||
0.0,
|
||||
),
|
||||
},
|
||||
),
|
||||
background_color: None,
|
||||
background_color: Color {
|
||||
r: 0.25,
|
||||
g: 0.25,
|
||||
b: 0.25,
|
||||
a: 1.0,
|
||||
},
|
||||
},
|
||||
prefer_no_csd: true,
|
||||
cursor: Cursor {
|
||||
@@ -1050,9 +1286,11 @@ mod tests {
|
||||
3000,
|
||||
),
|
||||
},
|
||||
screenshot_path: Some(
|
||||
screenshot_path: ScreenshotPath(
|
||||
Some(
|
||||
"~/Screenshots/screenshot.png",
|
||||
),
|
||||
),
|
||||
clipboard: Clipboard {
|
||||
disable_primary: true,
|
||||
},
|
||||
@@ -1855,31 +2093,14 @@ mod tests {
|
||||
// We try to write the config defaults in such a way that empty sections (and an empty
|
||||
// config) give the same outcome as the default config bundled with niri. This test
|
||||
// verifies the actual differences between the two.
|
||||
let mut default_config = Config::default();
|
||||
let empty_config = Config::parse("empty.kdl", "").unwrap();
|
||||
let mut default_config = Config::load_default();
|
||||
let empty_config = Config::parse_mem("").unwrap();
|
||||
|
||||
// Some notable omissions: the default config has some window rules, and an empty config
|
||||
// will not have any binds. Clear them out so they don't spam the diff.
|
||||
default_config.window_rules.clear();
|
||||
default_config.binds.0.clear();
|
||||
|
||||
let default_layout = default_config.resolve_layout();
|
||||
let empty_layout = empty_config.resolve_layout();
|
||||
default_config.layout = Default::default();
|
||||
assert_snapshot!(
|
||||
diff_lines(
|
||||
&format!("{empty_layout:#?}"),
|
||||
&format!("{default_layout:#?}")
|
||||
),
|
||||
@r"
|
||||
- 0.3333333333333333,
|
||||
+ 0.33333,
|
||||
|
||||
- 0.6666666666666666,
|
||||
+ 0.66667,
|
||||
",
|
||||
);
|
||||
|
||||
assert_snapshot!(
|
||||
diff_lines(
|
||||
&format!("{empty_config:#?}"),
|
||||
@@ -1903,6 +2124,12 @@ mod tests {
|
||||
+ ],
|
||||
+ },
|
||||
+ ],
|
||||
|
||||
- 0.3333333333333333,
|
||||
+ 0.33333,
|
||||
|
||||
- 0.6666666666666666,
|
||||
+ 0.66667,
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,6 +36,17 @@ impl Default for Cursor {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(knuffel::Decode, Debug, Clone, PartialEq)]
|
||||
pub struct ScreenshotPath(#[knuffel(argument)] pub Option<String>);
|
||||
|
||||
impl Default for ScreenshotPath {
|
||||
fn default() -> Self {
|
||||
Self(Some(String::from(
|
||||
"~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png",
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct HotkeyOverlay {
|
||||
#[knuffel(child)]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
struct KdlCodeBlock {
|
||||
filename: String,
|
||||
@@ -84,7 +84,7 @@ fn wiki_docs_parses() {
|
||||
must_fail,
|
||||
} in code_blocks
|
||||
{
|
||||
if let Err(error) = niri_config::Config::parse(&filename, &code) {
|
||||
if let Err(error) = niri_config::Config::parse(Path::new(&filename), &code).config {
|
||||
if !must_fail {
|
||||
errors.push(format!(
|
||||
"Error parsing wiki KDL code block at {}:{}: {:?}",
|
||||
|
||||
+2
-2
@@ -59,7 +59,7 @@ impl MappedLayer {
|
||||
clock: Clock,
|
||||
config: &Config,
|
||||
) -> Self {
|
||||
let mut shadow_config = config.resolve_layout().shadow;
|
||||
let mut shadow_config = config.layout.shadow;
|
||||
// Shadows for layer surfaces need to be explicitly enabled.
|
||||
shadow_config.on = false;
|
||||
shadow_config.merge_with(&rules.shadow);
|
||||
@@ -76,7 +76,7 @@ impl MappedLayer {
|
||||
}
|
||||
|
||||
pub fn update_config(&mut self, config: &Config) {
|
||||
let mut shadow_config = config.resolve_layout().shadow;
|
||||
let mut shadow_config = config.layout.shadow;
|
||||
// Shadows for layer surfaces need to be explicitly enabled.
|
||||
shadow_config.on = false;
|
||||
shadow_config.merge_with(&self.rules.shadow);
|
||||
|
||||
+1
-1
@@ -576,7 +576,7 @@ impl HitType {
|
||||
impl Options {
|
||||
fn from_config(config: &Config) -> Self {
|
||||
Self {
|
||||
layout: config.resolve_layout(),
|
||||
layout: config.layout.clone(),
|
||||
animations: config.animations.clone(),
|
||||
gestures: config.gestures,
|
||||
overview: config.overview,
|
||||
|
||||
+5
-5
@@ -2357,9 +2357,9 @@ fn removing_all_outputs_preserves_empty_named_workspaces() {
|
||||
#[test]
|
||||
fn config_change_updates_cached_sizes() {
|
||||
let mut config = Config::default();
|
||||
let border = config.layout.border.get_or_insert_with(Default::default);
|
||||
let border = &mut config.layout.border;
|
||||
border.off = false;
|
||||
border.width = Some(FloatOrInt(2.));
|
||||
border.width = 2.;
|
||||
|
||||
let mut layout = Layout::new(Clock::default(), &config);
|
||||
|
||||
@@ -2371,7 +2371,7 @@ fn config_change_updates_cached_sizes() {
|
||||
}
|
||||
.apply(&mut layout);
|
||||
|
||||
config.layout.border.as_mut().unwrap().width = Some(FloatOrInt(4.));
|
||||
config.layout.border.width = 4.;
|
||||
layout.update_config(&config);
|
||||
|
||||
layout.verify_invariants();
|
||||
@@ -2380,7 +2380,7 @@ fn config_change_updates_cached_sizes() {
|
||||
#[test]
|
||||
fn preset_height_change_removes_preset() {
|
||||
let mut config = Config::default();
|
||||
config.layout.preset_window_heights = Some(vec![PresetSize::Fixed(1), PresetSize::Fixed(2)]);
|
||||
config.layout.preset_window_heights = vec![PresetSize::Fixed(1), PresetSize::Fixed(2)];
|
||||
|
||||
let mut layout = Layout::new(Clock::default(), &config);
|
||||
|
||||
@@ -2401,7 +2401,7 @@ fn preset_height_change_removes_preset() {
|
||||
}
|
||||
|
||||
// Leave only one.
|
||||
config.layout.preset_window_heights = Some(vec![PresetSize::Fixed(1)]);
|
||||
config.layout.preset_window_heights = vec![PresetSize::Fixed(1)];
|
||||
|
||||
layout.update_config(&config);
|
||||
|
||||
|
||||
+7
-6
@@ -24,7 +24,7 @@ use niri::utils::spawning::{
|
||||
REMOVE_ENV_RUST_BACKTRACE, REMOVE_ENV_RUST_LIB_BACKTRACE,
|
||||
};
|
||||
use niri::utils::{cause_panic, version, watcher, xwayland, IS_SYSTEMD_SERVICE};
|
||||
use niri_config::ConfigPath;
|
||||
use niri_config::{Config, ConfigPath};
|
||||
use niri_ipc::socket::SOCKET_PATH_ENV;
|
||||
use portable_atomic::Ordering;
|
||||
use sd_notify::NotifyState;
|
||||
@@ -101,7 +101,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
Sub::Validate { config } => {
|
||||
tracy_client::Client::start();
|
||||
|
||||
config_path(config).load()?;
|
||||
config_path(config).load().config?;
|
||||
info!("config is valid");
|
||||
return Ok(());
|
||||
}
|
||||
@@ -148,10 +148,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let config_path = config_path(cli.config);
|
||||
env::remove_var("NIRI_CONFIG");
|
||||
let (config_created_at, config_load_result) = config_path.load_or_create();
|
||||
let config_errored = config_load_result.is_err();
|
||||
let mut config = config_load_result
|
||||
.map_err(|err| warn!("{err:?}"))
|
||||
.unwrap_or_default();
|
||||
let config_errored = config_load_result.config.is_err();
|
||||
let mut config = config_load_result.config.unwrap_or_else(|err| {
|
||||
warn!("{err:?}");
|
||||
Config::load_default()
|
||||
});
|
||||
|
||||
let spawn_at_startup = mem::take(&mut config.spawn_at_startup);
|
||||
let spawn_sh_at_startup = mem::take(&mut config.spawn_sh_at_startup);
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::time::Duration;
|
||||
|
||||
use insta::assert_snapshot;
|
||||
use niri_config::animations::{Curve, EasingParams, Kind};
|
||||
use niri_config::{Config, FloatOrInt};
|
||||
use niri_config::Config;
|
||||
use niri_ipc::SizeChange;
|
||||
use smithay::utils::{Point, Size};
|
||||
use wayland_client::protocol::wl_surface::WlSurface;
|
||||
@@ -74,7 +74,7 @@ fn set_up() -> Fixture {
|
||||
});
|
||||
|
||||
let mut config = Config::default();
|
||||
config.layout.gaps = Some(FloatOrInt(0.0));
|
||||
config.layout.gaps = 0.0;
|
||||
config.animations.window_resize.anim.kind = LINEAR;
|
||||
config.animations.window_movement.0.kind = LINEAR;
|
||||
|
||||
|
||||
@@ -795,7 +795,7 @@ window-rule {
|
||||
max-width 300
|
||||
}
|
||||
"##;
|
||||
let config = Config::parse("test.kdl", config).unwrap();
|
||||
let config = Config::parse_mem(config).unwrap();
|
||||
let mut f = Fixture::with_config(config);
|
||||
f.add_output(1, (1920, 1080));
|
||||
f.add_output(2, (1280, 720));
|
||||
|
||||
@@ -281,7 +281,7 @@ window-rule {{
|
||||
|
||||
snapshot_desc.push(format!("config:{config}"));
|
||||
|
||||
let config = Config::parse("config.kdl", &config).unwrap();
|
||||
let config = Config::parse_mem(&config).unwrap();
|
||||
|
||||
let mut f = Fixture::with_config(config);
|
||||
f.add_output(1, (1280, 720));
|
||||
@@ -574,7 +574,7 @@ layout {
|
||||
|
||||
snapshot_desc.push(format!("config:{config}"));
|
||||
|
||||
let config = Config::parse("config.kdl", &config).unwrap();
|
||||
let config = Config::parse_mem(&config).unwrap();
|
||||
|
||||
let mut f = Fixture::with_config(config);
|
||||
f.add_output(1, (1280, 720));
|
||||
|
||||
@@ -607,7 +607,7 @@ mod tests {
|
||||
|
||||
#[track_caller]
|
||||
fn check(config: &str, action: Action) -> String {
|
||||
let config = Config::parse("test.kdl", config).unwrap();
|
||||
let config = Config::parse_mem(config).unwrap();
|
||||
if let Some((key, title)) = format_bind(&config.binds.0, &action) {
|
||||
let key = key.map(|key| key_name(false, ModKey::Super, &key));
|
||||
let key = key.as_deref().unwrap_or("(not bound)");
|
||||
|
||||
+1
-1
@@ -213,7 +213,7 @@ pub fn expand_home(path: &Path) -> anyhow::Result<Option<PathBuf>> {
|
||||
}
|
||||
|
||||
pub fn make_screenshot_path(config: &Config) -> anyhow::Result<Option<PathBuf>> {
|
||||
let Some(path) = &config.screenshot_path else {
|
||||
let Some(path) = &config.screenshot_path.0 else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ pub fn setup(state: &mut State, config_path: &ConfigPath) {
|
||||
// Parsing the config actually takes > 20 ms on my beefy machine, so let's do it on the
|
||||
// watcher thread.
|
||||
let process = |path: &ConfigPath| {
|
||||
path.load().map_err(|err| {
|
||||
path.load().config.map_err(|err| {
|
||||
warn!("{err:?}");
|
||||
})
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user