mirror of
https://github.com/niri-wm/niri.git
synced 2026-06-23 02:05:33 +07:00
Make output modes configurable
This commit is contained in:
@@ -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
@@ -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:?}");
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user