diff options
| author | Ivan Molodetskikh <yalterz@gmail.com> | 2025-08-27 10:10:50 +0300 |
|---|---|---|
| committer | Ivan Molodetskikh <yalterz@gmail.com> | 2025-08-27 10:43:57 +0300 |
| commit | 19c576cfd88fa58c8f3e8123933e8a13ba6da3fa (patch) | |
| tree | 9625b7c1d72c5205aab546dc6589e108a0ec875c | |
| parent | ab52bb9521faa897727d5eff278fc4310390ff07 (diff) | |
| download | niri-19c576cfd88fa58c8f3e8123933e8a13ba6da3fa.tar.gz niri-19c576cfd88fa58c8f3e8123933e8a13ba6da3fa.tar.bz2 niri-19c576cfd88fa58c8f3e8123933e8a13ba6da3fa.zip | |
config: Extract output
| -rw-r--r-- | niri-config/src/lib.rs | 353 | ||||
| -rw-r--r-- | niri-config/src/output.rs | 358 |
2 files changed, 361 insertions, 350 deletions
diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index c33fe372..35789be2 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -14,8 +14,7 @@ use bitflags::bitflags; use knuffel::errors::DecodeError; use miette::{miette, Context, IntoDiagnostic}; use niri_ipc::{ - ColumnDisplay, ConfiguredMode, LayoutSwitchTarget, PositionChange, SizeChange, Transform, - WorkspaceReferenceArg, + ColumnDisplay, LayoutSwitchTarget, PositionChange, SizeChange, WorkspaceReferenceArg, }; use smithay::backend::renderer::Color32F; use smithay::input::keyboard::keysyms::KEY_NoSymbol; @@ -28,6 +27,7 @@ pub const DEFAULT_BACKDROP_COLOR: Color = Color::from_array_unpremul([0.15, 0.15 pub mod animations; pub mod input; pub mod layer_rule; +pub mod output; pub mod utils; pub mod window_rule; @@ -36,6 +36,7 @@ pub use crate::animations::{ }; pub use crate::input::{Input, ModKey, ScrollMethod, TrackLayout, WarpMouseToFocusMode, Xkb}; pub use crate::layer_rule::LayerRule; +pub use crate::output::{Output, OutputName, Outputs, Position, Vrr}; pub use crate::window_rule::{FloatingPosition, RelativeTo, WindowRule}; #[derive(knuffel::Decode, Debug, PartialEq)] @@ -107,86 +108,6 @@ pub enum CenterFocusedColumn { #[derive(Debug, Clone, Copy, PartialEq)] pub struct Percent(pub f64); -#[derive(Debug, Default, Clone, PartialEq)] -pub struct Outputs(pub Vec<Output>); - -#[derive(knuffel::Decode, Debug, Clone, PartialEq)] -pub struct Output { - #[knuffel(child)] - pub off: bool, - #[knuffel(argument)] - pub name: String, - #[knuffel(child, unwrap(argument))] - pub scale: Option<FloatOrInt<0, 10>>, - #[knuffel(child, unwrap(argument, str), default = Transform::Normal)] - pub transform: Transform, - #[knuffel(child)] - pub position: Option<Position>, - #[knuffel(child, unwrap(argument, str))] - pub mode: Option<ConfiguredMode>, - #[knuffel(child)] - pub variable_refresh_rate: Option<Vrr>, - #[knuffel(child)] - pub focus_at_startup: bool, - #[knuffel(child)] - pub background_color: Option<Color>, - #[knuffel(child)] - pub backdrop_color: Option<Color>, -} - -impl Output { - pub fn is_vrr_always_on(&self) -> bool { - self.variable_refresh_rate == Some(Vrr { on_demand: false }) - } - - pub fn is_vrr_on_demand(&self) -> bool { - self.variable_refresh_rate == Some(Vrr { on_demand: true }) - } - - pub fn is_vrr_always_off(&self) -> bool { - self.variable_refresh_rate.is_none() - } -} - -impl Default for Output { - fn default() -> Self { - Self { - off: false, - focus_at_startup: false, - name: String::new(), - scale: None, - transform: Transform::Normal, - position: None, - mode: None, - variable_refresh_rate: None, - background_color: None, - backdrop_color: None, - } - } -} - -#[derive(Debug, Clone)] -pub struct OutputName { - pub connector: String, - pub make: Option<String>, - pub model: Option<String>, - pub serial: Option<String>, -} - -#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq, Eq)] -pub struct Position { - #[knuffel(property)] - pub x: i32, - #[knuffel(property)] - pub y: i32, -} - -#[derive(knuffel::Decode, Debug, Clone, PartialEq, Default)] -pub struct Vrr { - #[knuffel(property, default = false)] - pub on_demand: bool, -} - // MIN and MAX generics are only used during parsing to check the value. #[derive(Debug, Default, Clone, Copy, PartialEq)] pub struct FloatOrInt<const MIN: i32, const MAX: i32>(pub f64); @@ -2263,129 +2184,6 @@ fn expect_only_children<S>( } } -impl FromIterator<Output> for Outputs { - fn from_iter<T: IntoIterator<Item = Output>>(iter: T) -> Self { - Self(Vec::from_iter(iter)) - } -} - -impl Outputs { - pub fn find(&self, name: &OutputName) -> Option<&Output> { - self.0.iter().find(|o| name.matches(&o.name)) - } - - pub fn find_mut(&mut self, name: &OutputName) -> Option<&mut Output> { - self.0.iter_mut().find(|o| name.matches(&o.name)) - } -} - -impl OutputName { - pub fn from_ipc_output(output: &niri_ipc::Output) -> Self { - Self { - connector: output.name.clone(), - make: (output.make != "Unknown").then(|| output.make.clone()), - model: (output.model != "Unknown").then(|| output.model.clone()), - serial: output.serial.clone(), - } - } - - /// Returns an output description matching what Smithay's `Output::new()` does. - pub fn format_description(&self) -> String { - format!( - "{} - {} - {}", - self.make.as_deref().unwrap_or("Unknown"), - self.model.as_deref().unwrap_or("Unknown"), - self.connector, - ) - } - - /// Returns an output name that will match by make/model/serial or, if they are missing, by - /// connector. - pub fn format_make_model_serial_or_connector(&self) -> String { - if self.make.is_none() && self.model.is_none() && self.serial.is_none() { - self.connector.to_string() - } else { - self.format_make_model_serial() - } - } - - pub fn format_make_model_serial(&self) -> String { - let make = self.make.as_deref().unwrap_or("Unknown"); - let model = self.model.as_deref().unwrap_or("Unknown"); - let serial = self.serial.as_deref().unwrap_or("Unknown"); - format!("{make} {model} {serial}") - } - - pub fn matches(&self, target: &str) -> bool { - // Match by connector. - if target.eq_ignore_ascii_case(&self.connector) { - return true; - } - - // If no other fields are available, don't try to match by them. - // - // This is used by niri msg output. - if self.make.is_none() && self.model.is_none() && self.serial.is_none() { - return false; - } - - // Match by "make model serial" with Unknown if something is missing. - let make = self.make.as_deref().unwrap_or("Unknown"); - let model = self.model.as_deref().unwrap_or("Unknown"); - let serial = self.serial.as_deref().unwrap_or("Unknown"); - - let Some(target_make) = target.get(..make.len()) else { - return false; - }; - let rest = &target[make.len()..]; - if !target_make.eq_ignore_ascii_case(make) { - return false; - } - if !rest.starts_with(' ') { - return false; - } - let rest = &rest[1..]; - - let Some(target_model) = rest.get(..model.len()) else { - return false; - }; - let rest = &rest[model.len()..]; - if !target_model.eq_ignore_ascii_case(model) { - return false; - } - if !rest.starts_with(' ') { - return false; - } - - let rest = &rest[1..]; - if !rest.eq_ignore_ascii_case(serial) { - return false; - } - - true - } - - // Similar in spirit to Ord, but I don't want to derive Eq to avoid mistakes (you should use - // `Self::match`, not Eq). - pub fn compare(&self, other: &Self) -> std::cmp::Ordering { - let self_missing_mms = self.make.is_none() && self.model.is_none() && self.serial.is_none(); - let other_missing_mms = - other.make.is_none() && other.model.is_none() && other.serial.is_none(); - - match (self_missing_mms, other_missing_mms) { - (true, true) => self.connector.cmp(&other.connector), - (true, false) => std::cmp::Ordering::Greater, - (false, true) => std::cmp::Ordering::Less, - (false, false) => self - .make - .cmp(&other.make) - .then_with(|| self.model.cmp(&other.model)) - .then_with(|| self.serial.cmp(&other.serial)) - .then_with(|| self.connector.cmp(&other.connector)), - } - } -} - impl<S> knuffel::Decode<S> for DefaultPresetSize where S: knuffel::traits::ErrorSpan, @@ -4602,32 +4400,6 @@ mod tests { } #[test] - fn parse_mode() { - assert_eq!( - "2560x1600@165.004".parse::<ConfiguredMode>().unwrap(), - ConfiguredMode { - width: 2560, - height: 1600, - refresh: Some(165.004), - }, - ); - - assert_eq!( - "1920x1080".parse::<ConfiguredMode>().unwrap(), - ConfiguredMode { - width: 1920, - height: 1080, - refresh: None, - }, - ); - - assert!("1920".parse::<ConfiguredMode>().is_err()); - assert!("1920x".parse::<ConfiguredMode>().is_err()); - assert!("1920x1080@".parse::<ConfiguredMode>().is_err()); - assert!("1920x1080@60Hz".parse::<ConfiguredMode>().is_err()); - } - - #[test] fn parse_size_change() { assert_eq!( "10".parse::<SizeChange>().unwrap(), @@ -4795,125 +4567,6 @@ mod tests { assert_eq!(config.input.keyboard.repeat_rate, 25); } - fn make_output_name( - connector: &str, - make: Option<&str>, - model: Option<&str>, - serial: Option<&str>, - ) -> OutputName { - OutputName { - connector: connector.to_string(), - make: make.map(|x| x.to_string()), - model: model.map(|x| x.to_string()), - serial: serial.map(|x| x.to_string()), - } - } - - #[test] - fn test_output_name_match() { - fn check( - target: &str, - connector: &str, - make: Option<&str>, - model: Option<&str>, - serial: Option<&str>, - ) -> bool { - let name = make_output_name(connector, make, model, serial); - name.matches(target) - } - - assert!(check("dp-2", "DP-2", None, None, None)); - assert!(!check("dp-1", "DP-2", None, None, None)); - assert!(check("dp-2", "DP-2", Some("a"), Some("b"), Some("c"))); - assert!(check( - "some company some monitor 1234", - "DP-2", - Some("Some Company"), - Some("Some Monitor"), - Some("1234") - )); - assert!(!check( - "some other company some monitor 1234", - "DP-2", - Some("Some Company"), - Some("Some Monitor"), - Some("1234") - )); - assert!(!check( - "make model serial ", - "DP-2", - Some("make"), - Some("model"), - Some("serial") - )); - assert!(check( - "make serial", - "DP-2", - Some("make"), - Some(""), - Some("serial") - )); - assert!(check( - "make model unknown", - "DP-2", - Some("Make"), - Some("Model"), - None - )); - assert!(check( - "unknown unknown serial", - "DP-2", - None, - None, - Some("Serial") - )); - assert!(!check("unknown unknown unknown", "DP-2", None, None, None)); - } - - #[test] - fn test_output_name_sorting() { - let mut names = vec![ - make_output_name("DP-2", None, None, None), - make_output_name("DP-1", None, None, None), - make_output_name("DP-3", Some("B"), Some("A"), Some("A")), - make_output_name("DP-3", Some("A"), Some("B"), Some("A")), - make_output_name("DP-3", Some("A"), Some("A"), Some("B")), - make_output_name("DP-3", None, Some("A"), Some("A")), - make_output_name("DP-3", Some("A"), None, Some("A")), - make_output_name("DP-3", Some("A"), Some("A"), None), - make_output_name("DP-5", Some("A"), Some("A"), Some("A")), - make_output_name("DP-4", Some("A"), Some("A"), Some("A")), - ]; - names.sort_by(|a, b| a.compare(b)); - let names = names - .into_iter() - .map(|name| { - format!( - "{} | {}", - name.format_make_model_serial_or_connector(), - name.connector, - ) - }) - .collect::<Vec<_>>(); - assert_debug_snapshot!( - names, - @r#" - [ - "Unknown A A | DP-3", - "A Unknown A | DP-3", - "A A Unknown | DP-3", - "A A A | DP-4", - "A A A | DP-5", - "A A B | DP-3", - "A B A | DP-3", - "B A A | DP-3", - "DP-1 | DP-1", - "DP-2 | DP-2", - ] - "# - ); - } - #[test] fn test_border_rule_on_off_merging() { fn is_on(config: &str, rules: &[&str]) -> String { diff --git a/niri-config/src/output.rs b/niri-config/src/output.rs new file mode 100644 index 00000000..9b12aa7b --- /dev/null +++ b/niri-config/src/output.rs @@ -0,0 +1,358 @@ +use niri_ipc::{ConfiguredMode, Transform}; + +use crate::{Color, FloatOrInt}; + +#[derive(Debug, Default, Clone, PartialEq)] +pub struct Outputs(pub Vec<Output>); + +#[derive(knuffel::Decode, Debug, Clone, PartialEq)] +pub struct Output { + #[knuffel(child)] + pub off: bool, + #[knuffel(argument)] + pub name: String, + #[knuffel(child, unwrap(argument))] + pub scale: Option<FloatOrInt<0, 10>>, + #[knuffel(child, unwrap(argument, str), default = Transform::Normal)] + pub transform: Transform, + #[knuffel(child)] + pub position: Option<Position>, + #[knuffel(child, unwrap(argument, str))] + pub mode: Option<ConfiguredMode>, + #[knuffel(child)] + pub variable_refresh_rate: Option<Vrr>, + #[knuffel(child)] + pub focus_at_startup: bool, + #[knuffel(child)] + pub background_color: Option<Color>, + #[knuffel(child)] + pub backdrop_color: Option<Color>, +} + +impl Output { + pub fn is_vrr_always_on(&self) -> bool { + self.variable_refresh_rate == Some(Vrr { on_demand: false }) + } + + pub fn is_vrr_on_demand(&self) -> bool { + self.variable_refresh_rate == Some(Vrr { on_demand: true }) + } + + pub fn is_vrr_always_off(&self) -> bool { + self.variable_refresh_rate.is_none() + } +} + +impl Default for Output { + fn default() -> Self { + Self { + off: false, + focus_at_startup: false, + name: String::new(), + scale: None, + transform: Transform::Normal, + position: None, + mode: None, + variable_refresh_rate: None, + background_color: None, + backdrop_color: None, + } + } +} + +#[derive(Debug, Clone)] +pub struct OutputName { + pub connector: String, + pub make: Option<String>, + pub model: Option<String>, + pub serial: Option<String>, +} + +#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq, Eq)] +pub struct Position { + #[knuffel(property)] + pub x: i32, + #[knuffel(property)] + pub y: i32, +} + +#[derive(knuffel::Decode, Debug, Clone, PartialEq, Default)] +pub struct Vrr { + #[knuffel(property, default = false)] + pub on_demand: bool, +} + +impl FromIterator<Output> for Outputs { + fn from_iter<T: IntoIterator<Item = Output>>(iter: T) -> Self { + Self(Vec::from_iter(iter)) + } +} + +impl Outputs { + pub fn find(&self, name: &OutputName) -> Option<&Output> { + self.0.iter().find(|o| name.matches(&o.name)) + } + + pub fn find_mut(&mut self, name: &OutputName) -> Option<&mut Output> { + self.0.iter_mut().find(|o| name.matches(&o.name)) + } +} + +impl OutputName { + pub fn from_ipc_output(output: &niri_ipc::Output) -> Self { + Self { + connector: output.name.clone(), + make: (output.make != "Unknown").then(|| output.make.clone()), + model: (output.model != "Unknown").then(|| output.model.clone()), + serial: output.serial.clone(), + } + } + + /// Returns an output description matching what Smithay's `Output::new()` does. + pub fn format_description(&self) -> String { + format!( + "{} - {} - {}", + self.make.as_deref().unwrap_or("Unknown"), + self.model.as_deref().unwrap_or("Unknown"), + self.connector, + ) + } + + /// Returns an output name that will match by make/model/serial or, if they are missing, by + /// connector. + pub fn format_make_model_serial_or_connector(&self) -> String { + if self.make.is_none() && self.model.is_none() && self.serial.is_none() { + self.connector.to_string() + } else { + self.format_make_model_serial() + } + } + + pub fn format_make_model_serial(&self) -> String { + let make = self.make.as_deref().unwrap_or("Unknown"); + let model = self.model.as_deref().unwrap_or("Unknown"); + let serial = self.serial.as_deref().unwrap_or("Unknown"); + format!("{make} {model} {serial}") + } + + pub fn matches(&self, target: &str) -> bool { + // Match by connector. + if target.eq_ignore_ascii_case(&self.connector) { + return true; + } + + // If no other fields are available, don't try to match by them. + // + // This is used by niri msg output. + if self.make.is_none() && self.model.is_none() && self.serial.is_none() { + return false; + } + + // Match by "make model serial" with Unknown if something is missing. + let make = self.make.as_deref().unwrap_or("Unknown"); + let model = self.model.as_deref().unwrap_or("Unknown"); + let serial = self.serial.as_deref().unwrap_or("Unknown"); + + let Some(target_make) = target.get(..make.len()) else { + return false; + }; + let rest = &target[make.len()..]; + if !target_make.eq_ignore_ascii_case(make) { + return false; + } + if !rest.starts_with(' ') { + return false; + } + let rest = &rest[1..]; + + let Some(target_model) = rest.get(..model.len()) else { + return false; + }; + let rest = &rest[model.len()..]; + if !target_model.eq_ignore_ascii_case(model) { + return false; + } + if !rest.starts_with(' ') { + return false; + } + + let rest = &rest[1..]; + if !rest.eq_ignore_ascii_case(serial) { + return false; + } + + true + } + + // Similar in spirit to Ord, but I don't want to derive Eq to avoid mistakes (you should use + // `Self::match`, not Eq). + pub fn compare(&self, other: &Self) -> std::cmp::Ordering { + let self_missing_mms = self.make.is_none() && self.model.is_none() && self.serial.is_none(); + let other_missing_mms = + other.make.is_none() && other.model.is_none() && other.serial.is_none(); + + match (self_missing_mms, other_missing_mms) { + (true, true) => self.connector.cmp(&other.connector), + (true, false) => std::cmp::Ordering::Greater, + (false, true) => std::cmp::Ordering::Less, + (false, false) => self + .make + .cmp(&other.make) + .then_with(|| self.model.cmp(&other.model)) + .then_with(|| self.serial.cmp(&other.serial)) + .then_with(|| self.connector.cmp(&other.connector)), + } + } +} + +#[cfg(test)] +mod tests { + use insta::assert_debug_snapshot; + + use super::*; + + #[test] + fn parse_mode() { + assert_eq!( + "2560x1600@165.004".parse::<ConfiguredMode>().unwrap(), + ConfiguredMode { + width: 2560, + height: 1600, + refresh: Some(165.004), + }, + ); + + assert_eq!( + "1920x1080".parse::<ConfiguredMode>().unwrap(), + ConfiguredMode { + width: 1920, + height: 1080, + refresh: None, + }, + ); + + assert!("1920".parse::<ConfiguredMode>().is_err()); + assert!("1920x".parse::<ConfiguredMode>().is_err()); + assert!("1920x1080@".parse::<ConfiguredMode>().is_err()); + assert!("1920x1080@60Hz".parse::<ConfiguredMode>().is_err()); + } + + fn make_output_name( + connector: &str, + make: Option<&str>, + model: Option<&str>, + serial: Option<&str>, + ) -> OutputName { + OutputName { + connector: connector.to_string(), + make: make.map(|x| x.to_string()), + model: model.map(|x| x.to_string()), + serial: serial.map(|x| x.to_string()), + } + } + + #[test] + fn test_output_name_match() { + fn check( + target: &str, + connector: &str, + make: Option<&str>, + model: Option<&str>, + serial: Option<&str>, + ) -> bool { + let name = make_output_name(connector, make, model, serial); + name.matches(target) + } + + assert!(check("dp-2", "DP-2", None, None, None)); + assert!(!check("dp-1", "DP-2", None, None, None)); + assert!(check("dp-2", "DP-2", Some("a"), Some("b"), Some("c"))); + assert!(check( + "some company some monitor 1234", + "DP-2", + Some("Some Company"), + Some("Some Monitor"), + Some("1234") + )); + assert!(!check( + "some other company some monitor 1234", + "DP-2", + Some("Some Company"), + Some("Some Monitor"), + Some("1234") + )); + assert!(!check( + "make model serial ", + "DP-2", + Some("make"), + Some("model"), + Some("serial") + )); + assert!(check( + "make serial", + "DP-2", + Some("make"), + Some(""), + Some("serial") + )); + assert!(check( + "make model unknown", + "DP-2", + Some("Make"), + Some("Model"), + None + )); + assert!(check( + "unknown unknown serial", + "DP-2", + None, + None, + Some("Serial") + )); + assert!(!check("unknown unknown unknown", "DP-2", None, None, None)); + } + + #[test] + fn test_output_name_sorting() { + let mut names = vec![ + make_output_name("DP-2", None, None, None), + make_output_name("DP-1", None, None, None), + make_output_name("DP-3", Some("B"), Some("A"), Some("A")), + make_output_name("DP-3", Some("A"), Some("B"), Some("A")), + make_output_name("DP-3", Some("A"), Some("A"), Some("B")), + make_output_name("DP-3", None, Some("A"), Some("A")), + make_output_name("DP-3", Some("A"), None, Some("A")), + make_output_name("DP-3", Some("A"), Some("A"), None), + make_output_name("DP-5", Some("A"), Some("A"), Some("A")), + make_output_name("DP-4", Some("A"), Some("A"), Some("A")), + ]; + names.sort_by(|a, b| a.compare(b)); + let names = names + .into_iter() + .map(|name| { + format!( + "{} | {}", + name.format_make_model_serial_or_connector(), + name.connector, + ) + }) + .collect::<Vec<_>>(); + assert_debug_snapshot!( + names, + @r#" + [ + "Unknown A A | DP-3", + "A Unknown A | DP-3", + "A A Unknown | DP-3", + "A A A | DP-4", + "A A A | DP-5", + "A A B | DP-3", + "A B A | DP-3", + "B A A | DP-3", + "DP-1 | DP-1", + "DP-2 | DP-2", + ] + "# + ); + } +} |
