Use libdisplay-info for make/model/serial parsing, implement throughout

This commit is contained in:
Ivan Molodetskikh
2024-09-03 12:13:04 +03:00
parent 4b7c16b04a
commit f0157e03e7
22 changed files with 528 additions and 197 deletions

View File

@@ -34,7 +34,7 @@ jobs:
- name: Install dependencies
run: |
apt-get update -y
apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev
apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libdisplay-info-dev
- uses: dtolnay/rust-toolchain@stable
@@ -85,7 +85,7 @@ jobs:
- name: Install dependencies
run: |
apt-get update -y
apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libadwaita-1-dev
apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libadwaita-1-dev libdisplay-info-dev
- uses: dtolnay/rust-toolchain@stable
@@ -110,7 +110,7 @@ jobs:
- name: Install dependencies
run: |
apt-get update -y
apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libadwaita-1-dev
apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libadwaita-1-dev libdisplay-info-dev
- uses: dtolnay/rust-toolchain@1.77.0
@@ -134,7 +134,7 @@ jobs:
- name: Install dependencies
run: |
apt-get update -y
apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libadwaita-1-dev
apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libadwaita-1-dev libdisplay-info-dev
- uses: dtolnay/rust-toolchain@stable
with:
@@ -172,7 +172,7 @@ jobs:
- name: Install dependencies
run: |
sudo dnf update -y
sudo dnf install -y cargo gcc libudev-devel libgbm-devel libxkbcommon-devel wayland-devel libinput-devel dbus-devel systemd-devel libseat-devel pipewire-devel pango-devel cairo-gobject-devel clang libadwaita-devel
sudo dnf install -y cargo gcc libudev-devel libgbm-devel libxkbcommon-devel wayland-devel libinput-devel dbus-devel systemd-devel libseat-devel pipewire-devel pango-devel cairo-gobject-devel clang libadwaita-devel libdisplay-info-devel
- uses: Swatinem/rust-cache@v2
- run: cargo build --all

31
Cargo.lock generated
View File

@@ -1942,6 +1942,36 @@ version = "0.2.155"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
[[package]]
name = "libdisplay-info"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb6dd47a677df2378a8bb88d08a593f51e8dddf4b61d2db5f2ceb35e67f9389d"
dependencies = [
"bitflags 2.6.0",
"libc",
"libdisplay-info-derive",
"libdisplay-info-sys",
"thiserror",
]
[[package]]
name = "libdisplay-info-derive"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea1cd31036b732a546d845f9485c56b1b606b5e476b0821c680dd66c8cd6fcee"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.72",
]
[[package]]
name = "libdisplay-info-sys"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea8cec1fa7872b621f40c756bc1304b1a975461282e250b0e76737b037c0c236"
[[package]]
name = "libloading"
version = "0.8.5"
@@ -2238,6 +2268,7 @@ dependencies = [
"k9",
"keyframe",
"libc",
"libdisplay-info",
"log",
"niri-config",
"niri-ipc",

View File

@@ -59,6 +59,7 @@ glam = "0.28.0"
input = { version = "0.9.0", features = ["libinput_1_21"] }
keyframe = { version = "1.1.1", default-features = false }
libc = "0.2.155"
libdisplay-info = "0.1.0"
log = { version = "0.4.22", features = ["max_level_trace", "release_max_level_debug"] }
niri-config = { version = "0.1.8", path = "niri-config" }
niri-ipc = { version = "0.1.8", path = "niri-ipc", features = ["clap"] }

View File

@@ -359,6 +359,14 @@ impl Default for Output {
}
}
#[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)]
@@ -1720,14 +1728,95 @@ impl FromIterator<Output> for Outputs {
}
impl Outputs {
pub fn find(&self, name: &str) -> Option<&Output> {
self.0.iter().find(|o| o.name.eq_ignore_ascii_case(name))
pub fn find(&self, name: &OutputName) -> Option<&Output> {
self.0.iter().find(|o| name.matches(&o.name))
}
pub fn find_mut(&mut self, name: &str) -> Option<&mut Output> {
self.0
.iter_mut()
.find(|o| o.name.eq_ignore_ascii_case(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 {
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
}
}
@@ -3351,4 +3440,70 @@ mod tests {
assert_eq!(config.input.keyboard.repeat_delay, 600);
assert_eq!(config.input.keyboard.repeat_rate, 25);
}
#[test]
fn test_output_name_match() {
fn check(
target: &str,
connector: &str,
make: Option<&str>,
model: Option<&str>,
serial: Option<&str>,
) -> bool {
let name = 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()),
};
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));
}
}

View File

@@ -71,7 +71,7 @@ pub enum Response {
Version(String),
/// Information about connected outputs.
///
/// Map from connector name to output info.
/// Map from output name to output info.
Outputs(HashMap<String, Output>),
/// Information about workspaces.
Workspaces(Vec<Workspace>),
@@ -466,6 +466,8 @@ pub struct Output {
pub make: String,
/// Textual description of the model.
pub model: String,
/// Serial of the output, if known.
pub serial: Option<String>,
/// Physical width and height of the output in millimeters, if known.
pub physical_size: Option<(u32, u32)>,
/// Available modes for the output.

View File

@@ -5,7 +5,7 @@ use niri::layout::workspace::ColumnWidth;
use niri::layout::{LayoutElement as _, Options};
use niri::render_helpers::RenderTarget;
use niri::utils::get_monotonic_time;
use niri_config::{Color, FloatOrInt};
use niri_config::{Color, FloatOrInt, OutputName};
use smithay::backend::renderer::element::RenderElement;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::desktop::layer_map_for_output;
@@ -41,6 +41,12 @@ impl Layout {
refresh: 60000,
});
output.change_current_state(mode, None, None, None);
output.user_data().insert_if_missing(|| OutputName {
connector: String::new(),
make: None,
model: None,
serial: None,
});
let options = Options {
focus_ring: niri_config::FocusRing {

View File

@@ -68,6 +68,7 @@ BuildRequires: pkgconfig(libinput)
BuildRequires: pkgconfig(dbus-1)
BuildRequires: pkgconfig(systemd)
BuildRequires: pkgconfig(libseat)
BuildRequires: pkgconfig(libdisplay-info)
BuildRequires: pipewire-devel
BuildRequires: pango-devel
BuildRequires: cairo-gobject-devel

View File

@@ -174,6 +174,14 @@ impl Backend {
}
}
pub fn tty_checked(&mut self) -> Option<&mut Tty> {
if let Self::Tty(v) = self {
Some(v)
} else {
None
}
}
pub fn tty(&mut self) -> &mut Tty {
if let Self::Tty(v) = self {
v

View File

@@ -4,7 +4,6 @@ use std::fmt::Write;
use std::iter::zip;
use std::num::NonZeroU64;
use std::os::fd::AsFd;
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::path::Path;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
@@ -14,7 +13,7 @@ use std::{io, mem};
use anyhow::{anyhow, bail, ensure, Context};
use bytemuck::cast_slice_mut;
use libc::dev_t;
use niri_config::Config;
use niri_config::{Config, OutputName};
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::allocator::format::FormatSet;
use smithay::backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice};
@@ -52,7 +51,6 @@ use smithay::wayland::drm_lease::{
DrmLease, DrmLeaseBuilder, DrmLeaseRequest, DrmLeaseState, LeaseRejected,
};
use smithay_drm_extras::drm_scanner::{DrmScanEvent, DrmScanner};
use smithay_drm_extras::edid::EdidInfo;
use wayland_protocols::wp::linux_dmabuf::zv1::server::zwp_linux_dmabuf_feedback_v1::TrancheFlags;
use wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
@@ -178,7 +176,7 @@ struct TtyOutputState {
}
struct Surface {
name: String,
name: OutputName,
compositor: GbmDrmCompositor,
connector: connector::Handle,
dmabuf_feedback: Option<SurfaceDmabufFeedback>,
@@ -440,7 +438,7 @@ impl Tty {
continue;
};
let Some(output_state) = niri.output_state.get_mut(&output) else {
error!("missing state for output {:?}", surface.name);
error!("missing state for output {:?}", surface.name.connector);
continue;
};
@@ -746,26 +744,22 @@ impl Tty {
connector: connector::Info,
crtc: crtc::Handle,
) -> anyhow::Result<()> {
let output_name = format!(
"{}-{}",
connector.interface().as_str(),
connector.interface_id(),
);
debug!("connecting connector: {output_name}");
let connector_name = format_connector_name(&connector);
debug!("connecting connector: {connector_name}");
let device = self.devices.get_mut(&node).context("missing device")?;
let output_name = make_output_name(&device.drm, connector.handle(), connector_name.clone());
let non_desktop = find_drm_property(&device.drm, connector.handle(), "non-desktop")
.and_then(|(_, info, value)| info.value_type().convert_value(value).as_boolean())
.unwrap_or(false);
if non_desktop {
debug!("output is non desktop");
let description = get_edid_info(&device.drm, connector.handle())
.map(|info| truncate_to_nul(info.model))
.unwrap_or_else(|| "Unknown".into());
let description = output_name.format_description();
if let Some(lease_state) = &mut device.drm_lease_state {
lease_state.add_connector::<State>(connector.handle(), output_name, description);
lease_state.add_connector::<State>(connector.handle(), connector_name, description);
}
device
.non_desktop_connectors
@@ -881,22 +875,13 @@ impl Tty {
// Update the output mode.
let (physical_width, physical_height) = connector.size().unwrap_or((0, 0));
let (make, model) = get_edid_info(&device.drm, connector.handle())
.map(|info| {
(
truncate_to_nul(info.manufacturer),
truncate_to_nul(info.model),
)
})
.unwrap_or_else(|| ("Unknown".into(), "Unknown".into()));
let output = Output::new(
output_name.clone(),
connector_name.clone(),
PhysicalProperties {
size: (physical_width as i32, physical_height as i32).into(),
subpixel: connector.subpixel().into(),
model,
make,
model: output_name.model.as_deref().unwrap_or("Unknown").to_owned(),
make: output_name.make.as_deref().unwrap_or("Unknown").to_owned(),
},
);
@@ -907,6 +892,7 @@ impl Tty {
output
.user_data()
.insert_if_missing(|| TtyOutputState { node, crtc });
output.user_data().insert_if_missing(|| output_name.clone());
let mut planes = surface.planes().clone();
@@ -993,17 +979,18 @@ impl Tty {
}
let vblank_frame_name =
tracy_client::FrameName::new_leak(format!("vblank on {output_name}"));
let time_since_presentation_plot_name =
tracy_client::PlotName::new_leak(format!("{output_name} time since presentation, ms"));
tracy_client::FrameName::new_leak(format!("vblank on {connector_name}"));
let time_since_presentation_plot_name = tracy_client::PlotName::new_leak(format!(
"{connector_name} time since presentation, ms"
));
let presentation_misprediction_plot_name = tracy_client::PlotName::new_leak(format!(
"{output_name} presentation misprediction, ms"
"{connector_name} presentation misprediction, ms"
));
let sequence_delta_plot_name =
tracy_client::PlotName::new_leak(format!("{output_name} sequence delta"));
tracy_client::PlotName::new_leak(format!("{connector_name} sequence delta"));
let surface = Surface {
name: output_name.clone(),
name: output_name,
connector: connector.handle(),
compositor,
dmabuf_feedback,
@@ -1066,7 +1053,7 @@ impl Tty {
return;
};
debug!("disconnecting connector: {:?}", surface.name);
debug!("disconnecting connector: {:?}", surface.name.connector);
let output = niri
.global_space
@@ -1108,7 +1095,7 @@ impl Tty {
// Finish the Tracy frame, if any.
drop(surface.vblank_frame.take());
let name = &surface.name;
let name = &surface.name.connector;
trace!("vblank on {name} {meta:?}");
span.emit_text(name);
@@ -1311,7 +1298,7 @@ impl Tty {
return rv;
};
span.emit_text(&surface.name);
span.emit_text(&surface.name.connector);
if !device.drm.is_active() {
warn!("device is inactive");
@@ -1526,22 +1513,10 @@ impl Tty {
for (node, device) in &self.devices {
for (connector, crtc) in device.drm_scanner.crtcs() {
let name = format!(
"{}-{}",
connector.interface().as_str(),
connector.interface_id(),
);
let connector_name = format_connector_name(connector);
let physical_size = connector.size();
let (make, model) = get_edid_info(&device.drm, connector.handle())
.map(|info| {
(
truncate_to_nul(info.manufacturer),
truncate_to_nul(info.model),
)
})
.unwrap_or_else(|| ("Unknown".into(), "Unknown".into()));
let output_name =
make_output_name(&device.drm, connector.handle(), connector_name.clone());
let surface = device.surfaces.get(&crtc);
let current_crtc_mode = surface.map(|surface| surface.compositor.pending_mode());
@@ -1589,9 +1564,10 @@ impl Tty {
.map(logical_output);
let ipc_output = niri_ipc::Output {
name,
make,
model,
name: connector_name,
make: output_name.make.unwrap_or_else(|| "Unknown".into()),
model: output_name.model.unwrap_or_else(|| "Unknown".into()),
serial: output_name.serial,
physical_size,
modes,
current_mode,
@@ -1736,7 +1712,7 @@ impl Tty {
continue;
};
let Some(output_state) = niri.output_state.get_mut(&output) else {
error!("missing state for output {:?}", surface.name);
error!("missing state for output {:?}", surface.name.connector);
continue;
};
@@ -1759,7 +1735,7 @@ impl Tty {
warn!(
"output {:?}: configured mode {}x{}{} could not be found, \
falling back to preferred",
surface.name,
surface.name.connector,
target.width,
target.height,
if let Some(refresh) = target.refresh {
@@ -1770,7 +1746,10 @@ impl Tty {
);
}
debug!("output {:?}: picking mode: {mode:?}", surface.name);
debug!(
"output {:?}: picking mode: {mode:?}",
surface.name.connector
);
if let Err(err) = surface.compositor.use_mode(mode) {
warn!("error changing mode: {err:?}");
continue;
@@ -1796,12 +1775,8 @@ impl Tty {
continue;
}
let output_name = format!(
"{}-{}",
connector.interface().as_str(),
connector.interface_id(),
);
let connector_name = format_connector_name(connector);
let output_name = make_output_name(&device.drm, connector.handle(), connector_name);
let config = self
.config
.borrow()
@@ -1845,6 +1820,30 @@ impl Tty {
pub fn get_device_from_node(&mut self, node: DrmNode) -> Option<&mut OutputDevice> {
self.devices.get_mut(&node)
}
pub fn disconnected_connector_name_by_name_match(&self, target: &str) -> Option<OutputName> {
for device in self.devices.values() {
for (connector, crtc) in device.drm_scanner.crtcs() {
// Check if connected.
if connector.state() != connector::State::Connected {
continue;
}
// Check if already enabled.
if device.surfaces.contains_key(&crtc) {
continue;
}
let connector_name = format_connector_name(connector);
let output_name = make_output_name(&device.drm, connector.handle(), connector_name);
if output_name.matches(target) {
return Some(output_name);
}
}
}
None
}
}
impl GammaProps {
@@ -2277,23 +2276,21 @@ fn pick_mode(
mode.map(|m| (*m, fallback))
}
fn truncate_to_nul(mut s: String) -> String {
if let Some(index) = s.find('\0') {
s.truncate(index);
}
s
}
fn get_edid_info(device: &DrmDevice, connector: connector::Handle) -> Option<EdidInfo> {
match catch_unwind(AssertUnwindSafe(move || {
EdidInfo::for_connector(device, connector)
})) {
Ok(info) => info,
Err(err) => {
warn!("edid-rs panicked: {err:?}");
None
}
}
fn get_edid_info(
device: &DrmDevice,
connector: connector::Handle,
) -> anyhow::Result<libdisplay_info::info::Info> {
let (_, info, value) =
find_drm_property(device, connector, "EDID").context("no EDID property")?;
let blob = info
.value_type()
.convert_value(value)
.as_blob()
.context("EDID was not blob type")?;
let data = device
.get_property_blob(blob)
.context("error getting EDID blob value")?;
libdisplay_info::info::Info::parse_edid(&data).context("error parsing EDID")
}
fn set_max_bpc(device: &DrmDevice, connector: connector::Handle, bpc: u64) -> anyhow::Result<u64> {
@@ -2415,41 +2412,47 @@ fn try_to_change_vrr(
match set_vrr_enabled(device, crtc, enable_vrr) {
Ok(enabled) => {
if enabled != enable_vrr {
warn!("output {:?}: failed {} VRR", surface.name, word);
warn!("output {:?}: failed {} VRR", surface.name.connector, word);
}
surface.vrr_enabled = enabled;
output_state.frame_clock.set_vrr(enabled);
}
Err(err) => {
warn!("output {:?}: error {} VRR: {err:?}", surface.name, word);
warn!(
"output {:?}: error {} VRR: {err:?}",
surface.name.connector, word
);
}
}
} else if enable_vrr {
warn!(
"output {:?}: cannot enable VRR because connector is not vrr_capable",
surface.name
surface.name.connector
);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn format_connector_name(connector: &connector::Info) -> String {
format!(
"{}-{}",
connector.interface().as_str(),
connector.interface_id(),
)
}
#[track_caller]
fn check(input: &str, expected: &str) {
let input = String::from(input);
assert_eq!(truncate_to_nul(input), expected);
}
#[test]
fn truncate_to_nul_works() {
check("", "");
check("qwer", "qwer");
check("abc\0def", "abc");
check("\0as", "");
check("a\0\0\0b", "a");
check("bb😁\0cc", "bb😁");
fn make_output_name(
device: &DrmDevice,
connector: connector::Handle,
connector_name: String,
) -> OutputName {
let info = get_edid_info(device, connector)
.map_err(|err| warn!("error getting EDID info for {connector_name}: {err:?}"))
.ok();
OutputName {
connector: connector_name,
make: info.as_ref().and_then(|info| info.make()),
model: info.as_ref().and_then(|info| info.model()),
serial: info.as_ref().and_then(|info| info.serial()),
}
}

View File

@@ -5,7 +5,7 @@ use std::rc::Rc;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use niri_config::Config;
use niri_config::{Config, OutputName};
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::renderer::damage::OutputDamageTracker;
use smithay::backend::renderer::gles::GlesRenderer;
@@ -59,6 +59,13 @@ impl Winit {
output.change_current_state(Some(mode), None, None, None);
output.set_preferred(mode);
output.user_data().insert_if_missing(|| OutputName {
connector: "winit".to_string(),
make: Some("Smithay".to_string()),
model: Some("Winit".to_string()),
serial: None,
});
let physical_properties = output.physical_properties();
let ipc_outputs = Arc::new(Mutex::new(HashMap::from([(
OutputId::next(),
@@ -66,6 +73,7 @@ impl Winit {
name: output.name(),
make: physical_properties.make,
model: physical_properties.model,
serial: None,
physical_size: None,
modes: vec![niri_ipc::Mode {
width: backend.window_size().w.clamp(0, u16::MAX as i32) as u16,

View File

@@ -64,18 +64,13 @@ impl DisplayConfig {
// Loosely matches the check in Mutter.
let c = &output.name;
let is_laptop_panel = matches!(c.get(..4), Some("eDP-" | "LVDS" | "DSI-"));
// FIXME: use proper serial when we have libdisplay-info.
// A serial is required for correct session restore by xdp-gnome.
let serial = c.clone();
let display_name = make_display_name(output, is_laptop_panel);
let mut properties = HashMap::new();
if is_laptop_panel {
properties.insert(
String::from("display-name"),
OwnedValue::from(zvariant::Str::from_static("Built-in display")),
);
}
properties.insert(
String::from("display-name"),
OwnedValue::from(zvariant::Str::from(display_name)),
);
properties.insert(
String::from("is-builtin"),
OwnedValue::from(is_laptop_panel),
@@ -111,8 +106,16 @@ impl DisplayConfig {
.properties
.insert(String::from("is-current"), OwnedValue::from(true));
let connector = c.clone();
let model = output.model.clone();
let make = output.make.clone();
// Serial is used for session restore, so fall back to the connector name if it's
// not available.
let serial = output.serial.as_ref().unwrap_or(&connector).clone();
let monitor = Monitor {
names: (c.clone(), String::new(), String::new(), serial),
names: (connector, make, model, serial),
modes,
properties,
};
@@ -144,15 +147,8 @@ impl DisplayConfig {
})
.collect();
// Sort the built-in monitor first, then by connector name.
monitors.sort_unstable_by(|a, b| {
let a_is_builtin = a.0.properties.contains_key("display-name");
let b_is_builtin = b.0.properties.contains_key("display-name");
a_is_builtin
.cmp(&b_is_builtin)
.reverse()
.then_with(|| a.0.names.0.cmp(&b.0.names.0))
});
// Sort by connector.
monitors.sort_unstable_by(|a, b| a.0.names.0.cmp(&b.0.names.0));
let (monitors, logical_monitors) = monitors.into_iter().unzip();
let properties = HashMap::from([(String::from("layout-mode"), OwnedValue::from(1u32))]);
@@ -183,3 +179,48 @@ impl Start for DisplayConfig {
Ok(conn)
}
}
// Adapted from Mutter.
fn make_display_name(output: &niri_ipc::Output, is_laptop_panel: bool) -> String {
if is_laptop_panel {
return String::from("Built-in display");
}
let make = &output.make;
let model = &output.model;
if let Some(diagonal) = output.physical_size.map(|(width_mm, height_mm)| {
let diagonal = f64::hypot(f64::from(width_mm), f64::from(height_mm)) / 25.4;
format_diagonal(diagonal)
}) {
format!("{make} {diagonal}")
} else if model != "Unknown" {
format!("{make} {model}")
} else {
make.clone()
}
}
fn format_diagonal(diagonal_inches: f64) -> String {
let known = [12.1, 13.3, 15.6];
if let Some(d) = known.iter().find(|d| (*d - diagonal_inches).abs() < 0.1) {
format!("{d:.1}")
} else {
format!("{}", diagonal_inches.round() as u32)
}
}
#[cfg(test)]
mod tests {
use k9::snapshot;
use super::*;
#[test]
fn test_format_diagonal() {
snapshot!(format_diagonal(12.11), "12.1″");
snapshot!(format_diagonal(13.28), "13.3″");
snapshot!(format_diagonal(15.6), "15.6″");
snapshot!(format_diagonal(23.2), "23″");
snapshot!(format_diagonal(24.8), "25″");
}
}

View File

@@ -41,7 +41,7 @@ use crate::input::DOUBLE_CLICK_TIME;
use crate::layout::workspace::ColumnWidth;
use crate::niri::{PopupGrabState, State};
use crate::utils::transaction::Transaction;
use crate::utils::{get_monotonic_time, send_scale_transform, ResizeEdge};
use crate::utils::{get_monotonic_time, output_matches_name, send_scale_transform, ResizeEdge};
use crate::window::{InitialConfigureState, ResolvedWindowRules, Unmapped, WindowRef};
impl XdgShellHandler for State {
@@ -668,7 +668,12 @@ impl State {
rules
.open_on_output
.as_deref()
.and_then(|name| self.niri.output_by_name.get(name))
.and_then(|name| {
self.niri
.global_space
.outputs()
.find(|output| output_matches_name(output, name))
})
.and_then(|o| self.niri.layout.monitor_for_output(o))
});

View File

@@ -122,8 +122,8 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
let mut outputs = outputs.into_iter().collect::<Vec<_>>();
outputs.sort_unstable_by(|a, b| a.0.cmp(&b.0));
for (connector, output) in outputs.into_iter() {
print_output(connector, output)?;
for (_name, output) in outputs.into_iter() {
print_output(output)?;
println!();
}
}
@@ -207,7 +207,7 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
}
if let Some(output) = output {
print_output(output.name.clone(), output)?;
print_output(output)?;
} else {
println!("No output is focused.");
}
@@ -364,11 +364,12 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
Ok(())
}
fn print_output(connector: String, output: Output) -> anyhow::Result<()> {
fn print_output(output: Output) -> anyhow::Result<()> {
let Output {
name,
make,
model,
serial,
physical_size,
modes,
current_mode,
@@ -377,7 +378,8 @@ fn print_output(connector: String, output: Output) -> anyhow::Result<()> {
logical,
} = output;
println!(r#"Output "{connector}" ({make} - {model} - {name})"#);
let serial = serial.as_deref().unwrap_or("Unknown");
println!(r#"Output "{make} {model} {serial}" ({name})"#);
if let Some(current) = current_mode {
let mode = *modes

View File

@@ -13,6 +13,7 @@ use calloop::io::Async;
use directories::BaseDirs;
use futures_util::io::{AsyncReadExt, BufReader};
use futures_util::{select_biased, AsyncBufReadExt, AsyncWrite, AsyncWriteExt, FutureExt as _};
use niri_config::OutputName;
use niri_ipc::state::{EventStreamState, EventStreamStatePart as _};
use niri_ipc::{Event, KeyboardLayouts, OutputConfigChanged, Reply, Request, Response, Workspace};
use smithay::input::keyboard::XkbContextHandler;
@@ -296,7 +297,7 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply {
let ipc_outputs = ctx.ipc_outputs.lock().unwrap();
let found = ipc_outputs
.values()
.any(|o| o.name.eq_ignore_ascii_case(&output));
.any(|o| OutputName::from_ipc_output(o).matches(&output));
let response = if found {
OutputConfigChanged::Applied
} else {

View File

@@ -54,7 +54,7 @@ use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderEleme
use crate::render_helpers::texture::TextureBuffer;
use crate::render_helpers::{BakedBuffer, RenderTarget, SplitElements};
use crate::utils::transaction::{Transaction, TransactionBlocker};
use crate::utils::{output_size, round_logical_in_physical_max1, ResizeEdge};
use crate::utils::{output_matches_name, output_size, round_logical_in_physical_max1, ResizeEdge};
use crate::window::ResolvedWindowRules;
pub mod closing_window;
@@ -344,8 +344,6 @@ impl<W: LayoutElement> Layout<W> {
}
pub fn add_output(&mut self, output: Output) {
let id = OutputId::new(&output);
self.monitor_set = match mem::take(&mut self.monitor_set) {
MonitorSet::Normal {
mut monitors,
@@ -358,7 +356,7 @@ impl<W: LayoutElement> Layout<W> {
let mut workspaces = vec![];
for i in (0..primary.workspaces.len()).rev() {
if primary.workspaces[i].original_output == id {
if primary.workspaces[i].original_output.matches(&output) {
let ws = primary.workspaces.remove(i);
// FIXME: this can be coded in a way that the workspace switch won't be
@@ -1722,18 +1720,16 @@ impl<W: LayoutElement> Layout<W> {
assert!(after_idx < monitor.workspaces.len());
}
let monitor_id = OutputId::new(&monitor.output);
if idx == primary_idx {
for ws in &monitor.workspaces {
if ws.original_output == monitor_id {
if ws.original_output.matches(&monitor.output) {
// This is the primary monitor's own workspace.
continue;
}
let own_monitor_exists = monitors
.iter()
.any(|m| OutputId::new(&m.output) == ws.original_output);
.any(|m| ws.original_output.matches(&m.output));
assert!(
!own_monitor_exists,
"primary monitor cannot have workspaces for which their own monitor exists"
@@ -1744,7 +1740,7 @@ impl<W: LayoutElement> Layout<W> {
monitor
.workspaces
.iter()
.any(|workspace| workspace.original_output == monitor_id),
.any(|workspace| workspace.original_output.matches(&monitor.output)),
"secondary monitor must not have any non-own workspaces"
);
}
@@ -1881,7 +1877,7 @@ impl<W: LayoutElement> Layout<W> {
.map(|name| {
monitors
.iter_mut()
.position(|monitor| monitor.output_name().eq_ignore_ascii_case(name))
.position(|monitor| output_matches_name(&monitor.output, name))
.unwrap_or(*primary_idx)
})
.unwrap_or(*active_monitor_idx);
@@ -2556,7 +2552,7 @@ impl<W: LayoutElement> Default for MonitorSet<W> {
mod tests {
use std::cell::Cell;
use niri_config::{FloatOrInt, WorkspaceName};
use niri_config::{FloatOrInt, OutputName, WorkspaceName};
use proptest::prelude::*;
use proptest_derive::Arbitrary;
use smithay::output::{Mode, PhysicalProperties, Subpixel};
@@ -2967,7 +2963,7 @@ mod tests {
}
let output = Output::new(
name,
name.clone(),
PhysicalProperties {
size: Size::from((1280, 720)),
subpixel: Subpixel::Unknown,
@@ -2984,6 +2980,12 @@ mod tests {
None,
None,
);
output.user_data().insert_if_missing(|| OutputName {
connector: name,
make: None,
model: None,
serial: None,
});
layout.add_output(output.clone());
}
Op::AddScaledOutput { id, scale } => {
@@ -2993,7 +2995,7 @@ mod tests {
}
let output = Output::new(
name,
name.clone(),
PhysicalProperties {
size: Size::from((1280, 720)),
subpixel: Subpixel::Unknown,
@@ -3010,6 +3012,12 @@ mod tests {
Some(smithay::output::Scale::Fractional(scale)),
None,
);
output.user_data().insert_if_missing(|| OutputName {
connector: name,
make: None,
model: None,
serial: None,
});
layout.add_output(output.clone());
}
Op::RemoveOutput(id) => {

View File

@@ -3,7 +3,9 @@ use std::iter::{self, zip};
use std::rc::Rc;
use std::time::Duration;
use niri_config::{CenterFocusedColumn, PresetWidth, Struts, Workspace as WorkspaceConfig};
use niri_config::{
CenterFocusedColumn, OutputName, PresetWidth, Struts, Workspace as WorkspaceConfig,
};
use niri_ipc::SizeChange;
use ordered_float::NotNan;
use smithay::backend::renderer::gles::GlesRenderer;
@@ -117,9 +119,16 @@ pub struct Workspace<W: LayoutElement> {
id: WorkspaceId,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone)]
pub struct OutputId(String);
impl OutputId {
pub fn matches(&self, output: &Output) -> bool {
let output_name = output.user_data().get::<OutputName>().unwrap();
output_name.matches(&self.0)
}
}
static WORKSPACE_ID_COUNTER: IdCounter = IdCounter::new();
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
@@ -274,7 +283,8 @@ struct TileData {
impl OutputId {
pub fn new(output: &Output) -> Self {
Self(output.name())
let output_name = output.user_data().get::<OutputName>().unwrap();
Self(output_name.format_make_model_serial_or_connector())
}
}
@@ -401,8 +411,8 @@ impl<W: LayoutElement> Workspace<W> {
) -> Self {
let original_output = OutputId(
config
.clone()
.and_then(|c| c.open_on_output)
.as_ref()
.and_then(|c| c.open_on_output.clone())
.unwrap_or_default(),
);
@@ -559,6 +569,11 @@ impl<W: LayoutElement> Workspace<W> {
self.output = output;
if let Some(output) = &self.output {
// Normalize original output: possibly replace connector with make/model/serial.
if self.original_output.matches(output) {
self.original_output = OutputId::new(output);
}
let scale = output.current_scale();
let transform = output.current_transform();
let working_area = compute_working_area(output, self.options.struts);

View File

@@ -13,7 +13,7 @@ use _server_decoration::server::org_kde_kwin_server_decoration_manager::Mode as
use anyhow::{bail, ensure, Context};
use calloop::futures::Scheduler;
use niri_config::{
Config, FloatOrInt, Key, Modifiers, PreviewRender, TrackLayout, WorkspaceReference,
Config, FloatOrInt, Key, Modifiers, OutputName, PreviewRender, TrackLayout, WorkspaceReference,
DEFAULT_BACKGROUND_COLOR,
};
use smithay::backend::allocator::Fourcc;
@@ -142,7 +142,7 @@ use crate::utils::scale::{closest_representable_scale, guess_monitor_scale};
use crate::utils::spawning::CHILD_ENV;
use crate::utils::{
center, center_f64, get_monotonic_time, ipc_transform_to_smithay, logical_output,
make_screenshot_path, output_size, send_scale_transform, write_png_rgba8,
make_screenshot_path, output_matches_name, output_size, send_scale_transform, write_png_rgba8,
};
use crate::window::{InitialConfigureState, Mapped, ResolvedWindowRules, Unmapped, WindowRef};
use crate::{animation, niri_render_elements};
@@ -198,7 +198,6 @@ pub struct Niri {
pub blocker_cleared_rx: Receiver<Client>,
pub output_state: HashMap<Output, OutputState>,
pub output_by_name: HashMap<String, Output>,
// When false, we're idling with monitors powered off.
pub monitors_active: bool,
@@ -1121,9 +1120,9 @@ impl State {
let mut recolored_outputs = vec![];
for output in self.niri.global_space.outputs() {
let name = output.name();
let name = output.user_data().get::<OutputName>().unwrap();
let config = self.niri.config.borrow_mut();
let config = config.outputs.find(&name);
let config = config.outputs.find(name);
let scale = config
.and_then(|c| c.scale)
@@ -1139,7 +1138,7 @@ impl State {
.map(|c| ipc_transform_to_smithay(c.transform))
.unwrap_or(Transform::Normal);
// FIXME: fix winit damage on other transforms.
if name == "winit" {
if name.connector == "winit" {
transform = Transform::Flipped180;
}
@@ -1193,11 +1192,36 @@ impl State {
pub fn apply_transient_output_config(&mut self, name: &str, action: niri_ipc::OutputAction) {
{
// Try hard to find the output config section corresponding to the output set by the
// user. Since if we add a new section and some existing section also matches the
// output, then our new section won't do anything.
let temp;
let match_name = if let Some(output) = self.niri.output_by_name_match(name) {
output.user_data().get::<OutputName>().unwrap()
} else if let Some(output_name) = self
.backend
.tty_checked()
.and_then(|tty| tty.disconnected_connector_name_by_name_match(name))
{
temp = output_name;
&temp
} else {
// Even if name is "make model serial", matching will work fine this way.
temp = OutputName {
connector: name.to_owned(),
make: None,
model: None,
serial: None,
};
&temp
};
let mut config = self.niri.config.borrow_mut();
let config = if let Some(config) = config.outputs.find_mut(name) {
let config = if let Some(config) = config.outputs.find_mut(match_name) {
config
} else {
config.outputs.0.push(niri_config::Output {
// Save name as set by the user.
name: String::from(name),
..Default::default()
});
@@ -1759,7 +1783,6 @@ impl Niri {
layout,
global_space: Space::default(),
output_state: HashMap::new(),
output_by_name: HashMap::new(),
unmapped_windows: HashMap::new(),
root_surface: HashMap::new(),
dmabuf_pre_commit_hook: HashMap::new(),
@@ -1892,13 +1915,13 @@ impl Niri {
let config = self.config.borrow();
let mut outputs = vec![];
for output in self.global_space.outputs().chain(new_output) {
let name = output.name();
let name = output.user_data().get::<OutputName>().unwrap();
let position = self.global_space.output_geometry(output).map(|geo| geo.loc);
let config = config.outputs.find(&name).and_then(|c| c.position);
let config = config.outputs.find(name).and_then(|c| c.position);
outputs.push(Data {
output: output.clone(),
name,
name: name.connector.clone(),
position,
config,
});
@@ -1996,10 +2019,10 @@ impl Niri {
pub fn add_output(&mut self, output: Output, refresh_interval: Option<Duration>, vrr: bool) {
let global = output.create_global::<State>(&self.display_handle);
let name = output.name();
let name = output.user_data().get::<OutputName>().unwrap();
let config = self.config.borrow();
let c = config.outputs.find(&name);
let c = config.outputs.find(name);
let scale = c.and_then(|c| c.scale).map(|s| s.0).unwrap_or_else(|| {
let size_mm = output.physical_properties().size;
let resolution = output.current_mode().unwrap().size;
@@ -2018,7 +2041,7 @@ impl Niri {
background_color[3] = 1.;
// FIXME: fix winit damage on other transforms.
if name == "winit" {
if name.connector == "winit" {
transform = Transform::Flipped180;
}
drop(config);
@@ -2058,8 +2081,6 @@ impl Niri {
};
let rv = self.output_state.insert(output.clone(), state);
assert!(rv.is_none(), "output was already tracked");
let rv = self.output_by_name.insert(name, output.clone());
assert!(rv.is_none(), "output was already tracked");
// Must be last since it will call queue_redraw(output) which needs things to be filled-in.
self.reposition_outputs(Some(&output));
@@ -2076,7 +2097,6 @@ impl Niri {
self.gamma_control_manager_state.output_removed(output);
let state = self.output_state.remove(output).unwrap();
self.output_by_name.remove(&output.name()).unwrap();
match state.redraw_state {
RedrawState::Idle => (),
@@ -2412,13 +2432,6 @@ impl Niri {
.cloned()
}
pub fn output_by_name(&self, name: &str) -> Option<Output> {
self.global_space
.outputs()
.find(|output| output.name().eq_ignore_ascii_case(name))
.cloned()
}
pub fn find_output_and_workspace_index(
&self,
workspace_reference: WorkspaceReference,
@@ -2465,17 +2478,23 @@ impl Niri {
pub fn output_for_tablet(&self) -> Option<&Output> {
let config = self.config.borrow();
let map_to_output = config.input.tablet.map_to_output.as_ref();
map_to_output.and_then(|name| self.output_by_name.get(name))
map_to_output.and_then(|name| self.output_by_name_match(name))
}
pub fn output_for_touch(&self) -> Option<&Output> {
let config = self.config.borrow();
let map_to_output = config.input.touch.map_to_output.as_ref();
map_to_output
.and_then(|name| self.output_by_name.get(name))
.and_then(|name| self.output_by_name_match(name))
.or_else(|| self.global_space.outputs().next())
}
pub fn output_by_name_match(&self, target: &str) -> Option<&Output> {
self.global_space
.outputs()
.find(|output| output_matches_name(output, target))
}
pub fn output_for_root(&self, root: &WlSurface) -> Option<&Output> {
// Check the main layout.
let win_out = self.layout.find_window_and_output(root);
@@ -3177,11 +3196,13 @@ impl Niri {
pub fn refresh_on_demand_vrr(&mut self, backend: &mut Backend, output: &Output) {
let _span = tracy_client::span!("Niri::refresh_on_demand_vrr");
let name = output.user_data().get::<OutputName>().unwrap();
let Some(on_demand) = self
.config
.borrow()
.outputs
.find(&output.name())
.find(name)
.map(|output| output.is_vrr_on_demand())
else {
warn!("error getting output config for {}", output.name());

View File

@@ -3,7 +3,7 @@ use std::collections::HashMap;
use std::iter::zip;
use std::mem;
use niri_config::{FloatOrInt, Vrr};
use niri_config::{FloatOrInt, OutputName, Vrr};
use niri_ipc::Transform;
use smithay::reexports::wayland_protocols_wlr::output_management::v1::server::{
zwlr_output_configuration_head_v1, zwlr_output_configuration_v1, zwlr_output_head_v1,
@@ -403,12 +403,13 @@ where
return;
}
Entry::Vacant(entry) => {
let name = OutputName::from_ipc_output(current_config);
let mut config = g_state
.current_config
.find(&current_config.name)
.find(&name)
.cloned()
.unwrap_or_else(|| niri_config::Output {
name: current_config.name.clone(),
name: name.format_make_model_serial_or_connector(),
..Default::default()
});
config.off = false;
@@ -452,12 +453,13 @@ where
);
}
Entry::Vacant(entry) => {
let name = OutputName::from_ipc_output(current_config);
let mut config = g_state
.current_config
.find(&current_config.name)
.find(&name)
.cloned()
.unwrap_or_else(|| niri_config::Output {
name: current_config.name.clone(),
name: name.format_make_model_serial_or_connector(),
..Default::default()
});
config.off = true;
@@ -841,6 +843,7 @@ fn send_new_head<D>(
.unwrap();
client_data.manager.head(&new_head);
new_head.name(conf.name.clone());
// Format matches what Output::new() does internally.
new_head.description(format!("{} - {} - {}", conf.make, conf.model, conf.name));
if let Some((width, height)) = conf.physical_size {
if let (Ok(a), Ok(b)) = (width.try_into(), height.try_into()) {
@@ -877,6 +880,11 @@ fn send_new_head<D>(
if new_head.version() >= zwlr_output_head_v1::EVT_MODEL_SINCE {
new_head.model(conf.model.clone());
}
if new_head.version() >= zwlr_output_head_v1::EVT_SERIAL_NUMBER_SINCE {
if let Some(serial) = &conf.serial {
new_head.serial_number(serial.clone());
}
}
if new_head.version() >= zwlr_output_head_v1::EVT_ADAPTIVE_SYNC_SINCE {
new_head.adaptive_sync(match conf.vrr_enabled {

View File

@@ -10,7 +10,7 @@ use anyhow::{ensure, Context};
use bitflags::bitflags;
use directories::UserDirs;
use git_version::git_version;
use niri_config::Config;
use niri_config::{Config, OutputName};
use smithay::input::pointer::CursorIcon;
use smithay::output::{self, Output};
use smithay::reexports::rustix::time::{clock_gettime, ClockId};
@@ -216,6 +216,11 @@ pub fn write_png_rgba8(
writer.write_image_data(pixels)
}
pub fn output_matches_name(output: &Output, target: &str) -> bool {
let name = output.user_data().get::<OutputName>().unwrap();
name.matches(target)
}
#[cfg(feature = "dbus")]
pub fn show_screenshot_notification(image_path: Option<PathBuf>) {
let mut notification = notify_rust::Notification::new();

View File

@@ -8,7 +8,7 @@ You can declare named workspaces at the top level of the config:
workspace "browser"
workspace "chat" {
open-on-output "DP-2"
open-on-output "Some Company CoolMonitor 1234"
}
```

View File

@@ -19,14 +19,22 @@ output "eDP-1" {
output "HDMI-A-1" {
// ...settings for HDMI-A-1...
}
output "Some Company CoolMonitor 1234" {
// ...settings for CoolMonitor...
}
```
Outputs are matched by connector name (i.e. `eDP-1`, `HDMI-A-1`) which you can find by running `niri msg outputs`.
Outputs are matched by connector name (i.e. `eDP-1`, `HDMI-A-1`), or by monitor manufacturer, model, and serial, separated by a single space each.
You can find all of these by running `niri msg outputs`.
Usually, the built-in monitor in laptops will be called `eDP-1`.
Matching by output manufacturer and model is planned, but blocked on Smithay adopting libdisplay-info instead of edid-rs.
<sup>Since: 0.1.6</sup> The output name is case-insensitive.
<sup>Since: 0.1.9</sup> Outputs can be matched by manufacturer, model, and serial.
Before, they could be matched only by the connector name.
### `off`
This flag turns off that output entirely.

View File

@@ -37,7 +37,7 @@ window-rule {
// Properties that apply once upon window opening.
default-column-width { proportion 0.75; }
open-on-output "eDP-1"
open-on-output "Some Company CoolMonitor 1234"
open-on-workspace "chat"
open-maximized true
open-fullscreen true
@@ -252,6 +252,8 @@ window-rule {
exclude app-id=r#"^org\.telegram\.desktop$"# title="^Media viewer$"
open-on-output "HDMI-A-1"
// Or:
// open-on-output "Some Company CoolMonitor 1234"
}
```