aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--resources/default-config.kdl8
-rw-r--r--src/backend/tty.rs83
-rw-r--r--src/config.rs77
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());
+ }
}