From f0157e03e72714264e684295fac226e2046f0b38 Mon Sep 17 00:00:00 2001 From: Ivan Molodetskikh Date: Tue, 3 Sep 2024 12:13:04 +0300 Subject: Use libdisplay-info for make/model/serial parsing, implement throughout --- .github/workflows/ci.yml | 10 +- Cargo.lock | 31 +++++ Cargo.toml | 1 + niri-config/src/lib.rs | 167 +++++++++++++++++++++++++- niri-ipc/src/lib.rs | 4 +- niri-visual-tests/src/cases/layout.rs | 8 +- niri.spec.rpkg | 1 + src/backend/mod.rs | 8 ++ src/backend/tty.rs | 203 ++++++++++++++++---------------- src/backend/winit.rs | 10 +- src/dbus/mutter_display_config.rs | 81 +++++++++---- src/handlers/xdg_shell.rs | 9 +- src/ipc/client.rs | 12 +- src/ipc/server.rs | 3 +- src/layout/mod.rs | 34 ++++-- src/layout/workspace.rs | 25 +++- src/niri.rs | 75 +++++++----- src/protocols/output_management.rs | 18 ++- src/utils/mod.rs | 7 +- wiki/Configuration:-Named-Workspaces.md | 2 +- wiki/Configuration:-Outputs.md | 12 +- wiki/Configuration:-Window-Rules.md | 4 +- 22 files changed, 528 insertions(+), 197 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6e6e4544..8710ae34 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/Cargo.lock b/Cargo.lock index 02cb81ea..131a0c75 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index 6262d052..e8a4574b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] } diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index 9092cd21..bee1288f 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -359,6 +359,14 @@ impl Default for Output { } } +#[derive(Debug, Clone)] +pub struct OutputName { + pub connector: String, + pub make: Option, + pub model: Option, + pub serial: Option, +} + #[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq, Eq)] pub struct Position { #[knuffel(property)] @@ -1720,14 +1728,95 @@ impl FromIterator 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)); + } } diff --git a/niri-ipc/src/lib.rs b/niri-ipc/src/lib.rs index 22a1a63b..32377bd6 100644 --- a/niri-ipc/src/lib.rs +++ b/niri-ipc/src/lib.rs @@ -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), /// Information about workspaces. Workspaces(Vec), @@ -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, /// Physical width and height of the output in millimeters, if known. pub physical_size: Option<(u32, u32)>, /// Available modes for the output. diff --git a/niri-visual-tests/src/cases/layout.rs b/niri-visual-tests/src/cases/layout.rs index 5a1a0ae4..8dc32154 100644 --- a/niri-visual-tests/src/cases/layout.rs +++ b/niri-visual-tests/src/cases/layout.rs @@ -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 { diff --git a/niri.spec.rpkg b/niri.spec.rpkg index 0e1b8fc9..71ef9494 100644 --- a/niri.spec.rpkg +++ b/niri.spec.rpkg @@ -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 diff --git a/src/backend/mod.rs b/src/backend/mod.rs index bbacf1cf..2213b8bd 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -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 diff --git a/src/backend/tty.rs b/src/backend/tty.rs index 17b16eec..b4c892a8 100644 --- a/src/backend/tty.rs +++ b/src/backend/tty.rs @@ -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, @@ -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::(connector.handle(), output_name, description); + lease_state.add_connector::(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 { + 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 { - 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 { + 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 { @@ -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::*; - - #[track_caller] - fn check(input: &str, expected: &str) { - let input = String::from(input); - assert_eq!(truncate_to_nul(input), expected); - } +fn format_connector_name(connector: &connector::Info) -> String { + format!( + "{}-{}", + connector.interface().as_str(), + connector.interface_id(), + ) +} - #[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()), } } diff --git a/src/backend/winit.rs b/src/backend/winit.rs index 61744e5e..500215f7 100644 --- a/src/backend/winit.rs +++ b/src/backend/winit.rs @@ -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, diff --git a/src/dbus/mutter_display_config.rs b/src/dbus/mutter_display_config.rs index 146174f8..e1d12870 100644 --- a/src/dbus/mutter_display_config.rs +++ b/src/dbus/mutter_display_config.rs @@ -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″"); + } +} diff --git a/src/handlers/xdg_shell.rs b/src/handlers/xdg_shell.rs index 2486dd7a..48c798aa 100644 --- a/src/handlers/xdg_shell.rs +++ b/src/handlers/xdg_shell.rs @@ -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)) }); diff --git a/src/ipc/client.rs b/src/ipc/client.rs index 0f4d6228..1b44a987 100644 --- a/src/ipc/client.rs +++ b/src/ipc/client.rs @@ -122,8 +122,8 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> { let mut outputs = outputs.into_iter().collect::>(); 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 diff --git a/src/ipc/server.rs b/src/ipc/server.rs index 6990cd41..aeb0fcf0 100644 --- a/src/ipc/server.rs +++ b/src/ipc/server.rs @@ -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 { diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 9a9e084d..734512dd 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -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 Layout { } 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 Layout { 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 Layout { 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 Layout { 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 Layout { .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 Default for MonitorSet { 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) => { diff --git a/src/layout/workspace.rs b/src/layout/workspace.rs index f85ec622..a2dcf460 100644 --- a/src/layout/workspace.rs +++ b/src/layout/workspace.rs @@ -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 { 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::().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::().unwrap(); + Self(output_name.format_make_model_serial_or_connector()) } } @@ -401,8 +411,8 @@ impl Workspace { ) -> 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 Workspace { 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); diff --git a/src/niri.rs b/src/niri.rs index edd56466..4295a039 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -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, pub output_state: HashMap, - pub output_by_name: HashMap, // 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::().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::().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::().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, vrr: bool) { let global = output.create_global::(&self.display_handle); - let name = output.name(); + let name = output.user_data().get::().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 { - 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::().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()); diff --git a/src/protocols/output_management.rs b/src/protocols/output_management.rs index 3839bdb4..4f1ac8c0 100644 --- a/src/protocols/output_management.rs +++ b/src/protocols/output_management.rs @@ -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(¤t_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(¤t_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( .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( 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 { diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 5fb1d163..d5201532 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -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::().unwrap(); + name.matches(target) +} + #[cfg(feature = "dbus")] pub fn show_screenshot_notification(image_path: Option) { let mut notification = notify_rust::Notification::new(); diff --git a/wiki/Configuration:-Named-Workspaces.md b/wiki/Configuration:-Named-Workspaces.md index 2e6da060..bb7ff388 100644 --- a/wiki/Configuration:-Named-Workspaces.md +++ b/wiki/Configuration:-Named-Workspaces.md @@ -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" } ``` diff --git a/wiki/Configuration:-Outputs.md b/wiki/Configuration:-Outputs.md index f7b3adf8..59bf9182 100644 --- a/wiki/Configuration:-Outputs.md +++ b/wiki/Configuration:-Outputs.md @@ -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. Since: 0.1.6 The output name is case-insensitive. +Since: 0.1.9 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. diff --git a/wiki/Configuration:-Window-Rules.md b/wiki/Configuration:-Window-Rules.md index b13b4661..c52bef61 100644 --- a/wiki/Configuration:-Window-Rules.md +++ b/wiki/Configuration:-Window-Rules.md @@ -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" } ``` -- cgit