diff options
| author | Ivan Molodetskikh <yalterz@gmail.com> | 2025-09-27 11:20:43 +0300 |
|---|---|---|
| committer | Ivan Molodetskikh <yalterz@gmail.com> | 2025-10-02 09:38:17 +0300 |
| commit | b3ae3adbb77c4111366cb59b80d757f361c70237 (patch) | |
| tree | af60f8397ae9971b3ab2889c0925c705abe02a25 | |
| parent | 264289cd41069bd2c85c523a1b0c687eab0a89d7 (diff) | |
| download | niri-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.rs | 10 | ||||
| -rw-r--r-- | niri-config/src/error.rs | 99 | ||||
| -rw-r--r-- | niri-config/src/lib.rs | 883 | ||||
| -rw-r--r-- | niri-config/src/misc.rs | 11 | ||||
| -rw-r--r-- | niri-config/tests/wiki-parses.rs | 4 | ||||
| -rw-r--r-- | src/layer/mapped.rs | 4 | ||||
| -rw-r--r-- | src/layout/mod.rs | 2 | ||||
| -rw-r--r-- | src/layout/tests.rs | 10 | ||||
| -rw-r--r-- | src/main.rs | 13 | ||||
| -rw-r--r-- | src/tests/animations.rs | 4 | ||||
| -rw-r--r-- | src/tests/floating.rs | 2 | ||||
| -rw-r--r-- | src/tests/window_opening.rs | 4 | ||||
| -rw-r--r-- | src/ui/hotkey_overlay.rs | 2 | ||||
| -rw-r--r-- | src/utils/mod.rs | 2 | ||||
| -rw-r--r-- | src/utils/watcher.rs | 2 |
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, + |
