use crate::{Config, Desktop, dependencies::is_installed, exec_cmd};
use std::{fs, path::PathBuf, process::Command};
pub fn detect_monitors(config: &Config) -> Vec<String> {
let mut monitors = Vec::new();
match config.desktop {
Desktop::Niri if is_installed("niri") => {
if let Ok(out) = Command::new("niri").args(["msg", "outputs"]).output() {
monitors = parse_niri(&String::from_utf8_lossy(&out.stdout));
}
}
Desktop::Hyprland if is_installed("hyprctl") => {
if let Ok(out) = Command::new("hyprctl").arg("monitors").output() {
monitors = parse_hyprland(&String::from_utf8_lossy(&out.stdout));
}
}
Desktop::Xfce if is_installed("xfconf-query") => {
let mut cmd = Command::new("xfconf-query");
cmd.args([
"--channel",
"xfce4-desktop",
"--property",
"/backdrop",
"--list",
]);
if let Ok(out) = exec_cmd(&mut cmd, config.verbose, "get_xfce") {
monitors = parse_xfce(&String::from_utf8_lossy(&out.stdout));
}
}
_ => {}
}
if !monitors.is_empty() {
return monitors;
}
if is_installed("wlr-randr")
&& let Ok(out) = Command::new("wlr-randr").output()
{
monitors = parse_wlr_randr(&String::from_utf8_lossy(&out.stdout));
if !monitors.is_empty() {
return monitors;
}
}
monitors = detect_drm_monitors();
if !monitors.is_empty() {
return monitors;
}
if config.verbose {
println!("All monitor detection methods failed. Using defaults.");
}
vec!["DP-1".to_string(), "DP-2".to_string()]
}
pub fn parse_niri(stdout: &str) -> Vec<String> {
stdout
.lines()
.filter(|line| line.starts_with("Output"))
.filter_map(|line| {
let start = line.rfind('(')?;
let end = line.rfind(')')?;
if start < end {
Some(line[start + 1..end].to_string())
} else {
None
}
})
.collect()
}
pub fn parse_hyprland(stdout: &str) -> Vec<String> {
stdout
.lines()
.filter(|line| line.starts_with("Monitor"))
.filter_map(|line| line.split_whitespace().nth(1).map(String::from))
.collect()
}
pub fn parse_wlr_randr(stdout: &str) -> Vec<String> {
stdout
.lines()
.filter(|line| !line.starts_with(' ') && !line.is_empty())
.filter_map(|line| line.split_whitespace().next().map(String::from))
.collect()
}
pub fn parse_xfce(stdout: &str) -> Vec<String> {
let words = ["screen0", "workspace0", "last-image"];
stdout
.trim()
.split(['\n', ' '])
.filter(|out| words.iter().all(|w| out.contains(w)))
.map(String::from)
.collect()
}
fn detect_drm_monitors() -> Vec<String> {
let mut monitors = Vec::new();
let drm_path = PathBuf::from("/sys/class/drm");
if let Ok(entries) = fs::read_dir(drm_path) {
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
if name.starts_with("card") && name.contains('-') {
let status_path = entry.path().join("status");
if let Ok(status) = fs::read_to_string(status_path)
&& status.trim() == "connected"
&& let Some(idx) = name.find('-')
{
monitors.push(name[idx + 1..].to_string());
}
}
}
}
monitors
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_monitor_parsers() {
let expected = vec!["DP-1", "DP-2"];
let niri_mock = "\
Output eDP-1 (DP-1)
Mode: 1920x1080
Output HDMI-A-1 (DP-2)
Mode: 1920x1080";
assert_eq!(parse_niri(niri_mock), expected);
let hypr_mock = "\
Monitor DP-1 (ID 0):
1920x1080@60.00000
Monitor DP-2 (ID 1):
1920x1080@60.00000";
assert_eq!(parse_hyprland(hypr_mock), expected);
let wlr_mock = "\
DP-1 \"Manufacturer X\"
Position: 0,0
DP-2 \"Manufacturer Y\"
Position: 1920,0";
assert_eq!(parse_wlr_randr(wlr_mock), expected);
let xfce_mock = "\
/backdrop/screen0/monitorDP-1/workspace0/last-image
/backdrop/screen0/monitorDP-2/workspace0/last-image
/backdrop/screen0/monitorDP-1/workspace0/color-style";
let xfce_expected = vec![
"/backdrop/screen0/monitorDP-1/workspace0/last-image",
"/backdrop/screen0/monitorDP-2/workspace0/last-image",
];
assert_eq!(parse_xfce(xfce_mock), xfce_expected);
}
}