diff options
| -rw-r--r-- | resources/default-config.kdl | 8 | ||||
| -rw-r--r-- | src/backend/tty.rs | 83 | ||||
| -rw-r--r-- | src/config.rs | 77 |
3 files changed, 157 insertions, 11 deletions
diff --git a/resources/default-config.kdl b/resources/default-config.kdl index 69867533..b68bd2a3 100644 --- a/resources/default-config.kdl +++ b/resources/default-config.kdl @@ -34,6 +34,14 @@ input { // Scale is a floating-point number, but at the moment only integer values work. scale 2.0 + // Resolution and, optionally, refresh rate of the output. + // The format is "<width>x<height>" or "<width>x<height>@<refresh rate>". + // If the refresh rate is omitted, niri will pick the highest refresh rate + // for the resolution. + // If the mode is omitted altogether or is invalid, niri will pick one automatically. + // All valid modes are listed in niri's debug output when an output is connected. + mode "1920x1080@144" + // Position of the output in the global coordinate space. // This affects directional monitor actions like "focus-monitor-left", and cursor movement. // The cursor can only move between directly adjacent outputs. diff --git a/src/backend/tty.rs b/src/backend/tty.rs index dc08bbe5..f6a40500 100644 --- a/src/backend/tty.rs +++ b/src/backend/tty.rs @@ -434,20 +434,81 @@ impl Tty { .as_mut() .context("missing output device")?; - let mut mode = connector.modes().get(0); - connector.modes().iter().for_each(|m| { - trace!("mode: {m:?}"); - - if m.mode_type().contains(ModeTypeFlags::PREFERRED) { - // Pick the highest refresh rate. - if mode - .map(|curr| curr.vrefresh() < m.vrefresh()) - .unwrap_or(true) - { + // FIXME: print modes here until we have a better way to list all modes. + for m in connector.modes() { + let wl_mode = Mode::from(*m); + debug!( + "mode: {}x{}@{:.3}", + m.size().0, + m.size().1, + wl_mode.refresh as f64 / 1000., + ); + + trace!("{m:?}"); + } + + let mut mode = None; + + if let Some(target) = &config.mode { + let refresh = target.refresh.map(|r| (r * 1000.).round() as i32); + + for m in connector.modes() { + if m.size() != (target.width, target.height) { + continue; + } + + if let Some(refresh) = refresh { + // If refresh is set, only pick modes with matching refresh. + let wl_mode = Mode::from(*m); + if wl_mode.refresh == refresh { + mode = Some(m); + } + } else if let Some(curr) = mode { + // If refresh isn't set, pick the mode with the highest refresh. + if curr.vrefresh() < m.vrefresh() { + mode = Some(m); + } + } else { mode = Some(m); } } - }); + + if mode.is_none() { + warn!( + "configured mode {}x{}{} could not be found, falling back to preferred", + target.width, + target.height, + if let Some(refresh) = target.refresh { + format!("@{refresh}") + } else { + String::new() + }, + ); + } + } + + if mode.is_none() { + // Pick a preferred mode. + for m in connector.modes() { + if !m.mode_type().contains(ModeTypeFlags::PREFERRED) { + continue; + } + + if let Some(curr) = mode { + if curr.vrefresh() < m.vrefresh() { + mode = Some(m); + } + } else { + mode = Some(m); + } + } + } + + if mode.is_none() { + // Last attempt. + mode = connector.modes().get(0); + } + let mode = mode.ok_or_else(|| anyhow!("no mode"))?; debug!("picking mode: {mode:?}"); diff --git a/src/config.rs b/src/config.rs index 6bf31323..7ff7e338 100644 --- a/src/config.rs +++ b/src/config.rs @@ -81,6 +81,8 @@ pub struct Output { pub scale: f64, #[knuffel(child)] pub position: Option<Position>, + #[knuffel(child, unwrap(argument, str))] + pub mode: Option<Mode>, } impl Default for Output { @@ -89,6 +91,7 @@ impl Default for Output { name: String::new(), scale: 1., position: None, + mode: None, } } } @@ -101,6 +104,13 @@ pub struct Position { pub y: i32, } +#[derive(Debug, Clone, PartialEq)] +pub struct Mode { + pub width: u16, + pub height: u16, + pub refresh: Option<f64>, +} + #[derive(knuffel::Decode, Debug, Clone, PartialEq, Eq)] pub struct SpawnAtStartup { #[knuffel(arguments)] @@ -303,6 +313,41 @@ impl Default for Config { } } +impl FromStr for Mode { + type Err = miette::Error; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + let Some((width, rest)) = s.split_once('x') else { + return Err(miette!("no 'x' separator found")); + }; + + let (height, refresh) = match rest.split_once('@') { + Some((height, refresh)) => (height, Some(refresh)), + None => (rest, None), + }; + + let width = width + .parse() + .into_diagnostic() + .context("error parsing width")?; + let height = height + .parse() + .into_diagnostic() + .context("error parsing height")?; + let refresh = refresh + .map(str::parse) + .transpose() + .into_diagnostic() + .context("error parsing refresh rate")?; + + Ok(Self { + width, + height, + refresh, + }) + } +} + impl FromStr for Key { type Err = miette::Error; @@ -377,6 +422,7 @@ mod tests { output "eDP-1" { scale 2.0 position x=10 y=20 + mode "1920x1080@144" } spawn-at-startup "alacritty" "-e" "fish" @@ -428,6 +474,11 @@ mod tests { name: "eDP-1".to_owned(), scale: 2., position: Some(Position { x: 10, y: 20 }), + mode: Some(Mode { + width: 1920, + height: 1080, + refresh: Some(144.), + }), }], spawn_at_startup: vec![SpawnAtStartup { command: vec!["alacritty".to_owned(), "-e".to_owned(), "fish".to_owned()], @@ -509,4 +560,30 @@ mod tests { fn can_create_default_config() { let _ = Config::default(); } + + #[test] + fn parse_mode() { + assert_eq!( + "2560x1600@165.004".parse::<Mode>().unwrap(), + Mode { + width: 2560, + height: 1600, + refresh: Some(165.004), + }, + ); + + assert_eq!( + "1920x1080".parse::<Mode>().unwrap(), + Mode { + width: 1920, + height: 1080, + refresh: None, + }, + ); + + assert!("1920".parse::<Mode>().is_err()); + assert!("1920x".parse::<Mode>().is_err()); + assert!("1920x1080@".parse::<Mode>().is_err()); + assert!("1920x1080@60Hz".parse::<Mode>().is_err()); + } } |
