aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--niri-config/src/lib.rs353
-rw-r--r--niri-config/src/output.rs358
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",
+ ]
+ "#
+ );
+ }
+}