Make output modes configurable

This commit is contained in:
Ivan Molodetskikh
2023-10-03 08:35:24 +04:00
parent ef76b1df04
commit bb3fbe2e83
3 changed files with 156 additions and 10 deletions
+8
View File
@@ -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.
+71 -10
View File
@@ -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:?}");
// 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.,
);
if m.mode_type().contains(ModeTypeFlags::PREFERRED) {
// Pick the highest refresh rate.
if mode
.map(|curr| curr.vrefresh() < m.vrefresh())
.unwrap_or(true)
{
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:?}");
+77
View File
@@ -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());
}
}