mirror of
https://github.com/YaLTeR/niri.git
synced 2025-10-06 00:23:14 +02:00
Use libdisplay-info for make/model/serial parsing, implement throughout
This commit is contained in:
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@@ -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
31
Cargo.lock
generated
@@ -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",
|
||||
|
@@ -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"] }
|
||||
|
@@ -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));
|
||||
}
|
||||
}
|
||||
|
@@ -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.
|
||||
|
@@ -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 {
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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()),
|
||||
}
|
||||
}
|
||||
|
@@ -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,
|
||||
|
@@ -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″");
|
||||
}
|
||||
}
|
||||
|
@@ -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))
|
||||
});
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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 {
|
||||
|
@@ -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) => {
|
||||
|
@@ -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);
|
||||
|
75
src/niri.rs
75
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<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());
|
||||
|
@@ -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<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 {
|
||||
|
@@ -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();
|
||||
|
@@ -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"
|
||||
}
|
||||
```
|
||||
|
||||
|
@@ -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.
|
||||
|
@@ -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"
|
||||
}
|
||||
```
|
||||
|
||||
|
Reference in New Issue
Block a user