feat: DPMS fallback via wlr-randr for non-CEC desktop monitors

This commit is contained in:
Mitchell R 2026-05-10 22:46:30 +02:00
parent cbb1683c5d
commit c0704be343
2 changed files with 80 additions and 28 deletions

View file

@ -1,44 +1,49 @@
//! CEC (HDMI Consumer Electronics Control) — manages display power state //! Display power management — CEC for TVs, DPMS (wlr-randr/xset) for monitors.
//! via `cec-ctl` subprocess. v4l-utils package provides cec-ctl on Pi5.
//! //!
//! Commands: //! Tries CEC first (works for HDMI TVs).
//! - standby: tell TV to sleep //! Falls back to compositor-level output disable for desktop monitors that
//! - image-view-on: wake TV //! don't speak CEC.
//! - is-active-source: query state
use std::process::Command; use std::process::Command;
use tracing::{info, warn}; use tracing::{info, warn};
const CEC_DEVICE: &str = "/dev/cec0"; const CEC_DEVICE: &str = "/dev/cec0";
/// Send CEC standby (sleep) to all connected devices. /// Put the display to sleep — try CEC first, fall back to compositor DPMS.
pub fn standby() -> bool { pub fn standby() {
info!("cec: standby"); info!("power: standby");
if !cec_standby() {
wlr_output_off();
}
}
/// Wake the display — try CEC first, fall back to compositor DPMS.
pub fn wake() {
info!("power: wake");
if !cec_wake() {
wlr_output_on();
}
}
fn cec_standby() -> bool {
run_cec(&["--standby", "--to", "0"]) run_cec(&["--standby", "--to", "0"])
} }
/// Send CEC image-view-on (wake) to TV. fn cec_wake() -> bool {
pub fn wake() -> bool {
info!("cec: wake");
run_cec(&["--image-view-on", "--to", "0"]) run_cec(&["--image-view-on", "--to", "0"])
} }
/// Switch HDMI input on TV to this device.
pub fn become_active_source() -> bool {
info!("cec: become active source");
run_cec(&["--active-source", "phys-addr=0.0.0.0"])
}
fn run_cec(args: &[&str]) -> bool { fn run_cec(args: &[&str]) -> bool {
match Command::new("cec-ctl") if !std::path::Path::new(CEC_DEVICE).exists() {
.arg("-d") return false;
.arg(CEC_DEVICE) }
.args(args) match Command::new("cec-ctl").arg("-d").arg(CEC_DEVICE).args(args).output() {
.output() Ok(out) if out.status.success() => {
{ info!("cec: ok");
Ok(out) if out.status.success() => true, true
}
Ok(out) => { Ok(out) => {
warn!("cec-ctl failed: {}", String::from_utf8_lossy(&out.stderr)); warn!("cec failed: {}", String::from_utf8_lossy(&out.stderr));
false false
} }
Err(e) => { Err(e) => {
@ -47,3 +52,50 @@ fn run_cec(args: &[&str]) -> bool {
} }
} }
} }
/// Turn off all outputs via wlr-randr (Wayland compositors: labwc, wayfire, sway).
fn wlr_output_off() {
// Get list of outputs
let outputs = list_outputs();
if outputs.is_empty() {
warn!("dpms: no outputs found");
return;
}
for output in outputs {
match Command::new("wlr-randr")
.args(["--output", &output, "--off"])
.output()
{
Ok(out) if out.status.success() => info!("dpms: {output} off"),
Ok(out) => warn!("dpms off {output} failed: {}", String::from_utf8_lossy(&out.stderr)),
Err(e) => warn!("wlr-randr unavailable: {e}"),
}
}
}
fn wlr_output_on() {
let outputs = list_outputs();
for output in outputs {
match Command::new("wlr-randr")
.args(["--output", &output, "--on"])
.output()
{
Ok(out) if out.status.success() => info!("dpms: {output} on"),
Ok(out) => warn!("dpms on {output} failed: {}", String::from_utf8_lossy(&out.stderr)),
Err(e) => warn!("wlr-randr unavailable: {e}"),
}
}
}
fn list_outputs() -> Vec<String> {
let out = match Command::new("wlr-randr").output() {
Ok(out) => out,
Err(_) => return Vec::new(),
};
let text = String::from_utf8_lossy(&out.stdout);
text.lines()
.filter(|l| !l.starts_with(' ') && !l.is_empty())
.map(|l| l.split_whitespace().next().unwrap_or("").to_string())
.filter(|s| !s.is_empty())
.collect()
}

View file

@ -89,8 +89,8 @@ fn activate(app: &Application) {
let bundle = server::fetch_bundle(&server_for_reload, &key_for_reload); let bundle = server::fetch_bundle(&server_for_reload, &key_for_reload);
let _ = tx_for_reload.send(WorkerMsg::RenderBundle(bundle)); let _ = tx_for_reload.send(WorkerMsg::RenderBundle(bundle));
} }
ServerMsg::Standby => { cec::standby(); } ServerMsg::Standby => cec::standby(),
ServerMsg::Wake => { cec::wake(); } ServerMsg::Wake => cec::wake(),
} }
} }
}); });