Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
idle: name the clients holding an inhibitor
`ccectl idle status` said inhibited=true and nothing more, so a display
that never darkened could not be traced to the app keeping it on. The
inhibit manager now visits every inhibitor (not just the first) and
hands the idle manager a deduplicated name per holder: a window's app
id, its title when it set none, `layer:<namespace>` for a layer
surface, and the bare kind for the lock surface, shell surfaces and X11
override-redirects. The status line gains inhibited_by="a,b" after
inhibited=, and the log names the set at info level when it changes.
Timers still rearm only when the flag flips.
Verified in a shadow with mpv: inhibited_by="mpv" while playing, empty
once it exits, with matching log lines.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
src/cce_ctl.rs | 2 +-
src/server/idle.rs | 38 ++++++++++++++++----
src/server/idle_inhibit_manager.rs | 72 +++++++++++++++++++++++++++++++-------
3 files changed, 92 insertions(+), 20 deletions(-)
diff --git a/src/cce_ctl.rs b/src/cce_ctl.rs
index 36781b11..bea4e2e0 100644
--- a/src/cce_ctl.rs
+++ b/src/cce_ctl.rs
@@ -64,7 +64,7 @@ fn usage(name: &str, to_stderr: bool) {
print(" overview # toggle; the exit lands on the focused window, not the pointer");
print(" windows [--json] # list windows; --json emits one JSON object per line");
print(" outputs [--json] # list outputs: mode, scale, logical size, physical mm, px/mm, and where the mm came from");
- print(" idle [status] # idle timeouts: configured seconds, idle time, inhibited/displays_off/sleeping");
+ print(" idle [status] # idle timeouts: configured seconds, idle time, inhibited and by whom (app ids), displays_off/sleeping");
print(" idle wake|sleep|display on|display off # act now: wake darkened outputs, run the sleep command, darken/wake outputs");
print(" idle timeouts <display_off_s> <sleep_s> # set the timeouts live (0 = off); config.kdl `idle { }` on reload");
print(" status-hide-mode [true|false]");
diff --git a/src/server/idle.rs b/src/server/idle.rs
index 57fcc01f..e8e0c8b6 100644
--- a/src/server/idle.rs
+++ b/src/server/idle.rs
@@ -61,6 +61,10 @@ pub struct IdleManager {
sleep_command: Option<String>,
/// An idle-inhibitor is active: timers are held disarmed.
inhibited: bool,
+ /// Who holds one, by app id (a window) or surface kind, deduplicated,
+ /// in creation order; empty when nobody does. `Option` for the same
+ /// reason as `sleep_command`: null-niche safe under the zeroed init.
+ inhibitors: Option<Vec<String>>,
/// The display timeout fired and outputs were darkened by us.
displays_off: bool,
/// The sleep command was spawned; cleared by the next activity.
@@ -103,6 +107,7 @@ impl IdleManager {
self.sleep_ms = 0;
self.sleep_command = None;
self.inhibited = false;
+ self.inhibitors = None;
self.displays_off = false;
self.sleeping = false;
self.armed_at_ms = 0;
@@ -165,15 +170,33 @@ impl IdleManager {
self.rearm(changed);
}
- /// From `IdleInhibitManager::check_active`: an inhibitor appeared or
- /// the last one went away.
- pub unsafe fn set_inhibited(&mut self, inhibited: bool) {
- if self.inhibited == inhibited {
+ /// From `IdleInhibitManager::check_active`: the set of clients holding
+ /// an inhibitor changed. The names are what `ccectl idle status` and the
+ /// log report, so a display that never darkens can be traced to the app
+ /// keeping it on rather than to a bare `inhibited=true`.
+ pub unsafe fn set_inhibitors(&mut self, names: Vec<String>) {
+ if self.inhibitors.as_deref().unwrap_or(&[]) == names.as_slice() {
return;
}
+ let inhibited = !names.is_empty();
+ if inhibited {
+ log::info!("idle: inhibited by {}", names.join(", "));
+ } else {
+ log::info!("idle: no inhibitors left");
+ }
+ self.inhibitors = Some(names);
+ let flipped = self.inhibited != inhibited;
self.inhibited = inhibited;
- log::debug!("idle: inhibited={}", inhibited);
- self.rearm(true);
+ // A change of holder while still inhibited leaves the timers as they
+ // are: disarmed. Only the flag flipping rearms.
+ if flipped {
+ self.rearm(true);
+ }
+ }
+
+ /// The inhibitor names as the status line prints them.
+ fn inhibited_by(&self) -> String {
+ self.inhibitors.as_deref().unwrap_or(&[]).join(",")
}
/// Arm (or disarm, when inhibited or unconfigured) both timers from
@@ -258,12 +281,13 @@ impl IdleManager {
pub fn status(&self) -> String {
let idle_s = now_ms().saturating_sub(self.last_activity_ms) / 1000;
format!(
- "display_off={}s sleep={}s command={:?} idle={}s inhibited={} displays_off={} sleeping={}\n",
+ "display_off={}s sleep={}s command={:?} idle={}s inhibited={} inhibited_by={:?} displays_off={} sleeping={}\n",
self.display_off_ms / 1000,
self.sleep_ms / 1000,
self.sleep_command(),
idle_s,
self.inhibited,
+ self.inhibited_by(),
self.displays_off,
self.sleeping
)
diff --git a/src/server/idle_inhibit_manager.rs b/src/server/idle_inhibit_manager.rs
index 84cc1357..2c33633f 100644
--- a/src/server/idle_inhibit_manager.rs
+++ b/src/server/idle_inhibit_manager.rs
@@ -46,36 +46,84 @@ impl IdleInhibitManager {
}
}
+ /// Recount the live inhibitors and hand the idle manager the list of who
+ /// holds one, so `ccectl idle status` can name them. Every inhibitor is
+ /// visited (no early exit): two apps holding one are both reported.
pub unsafe fn check_active(&self) {
- let mut inhibited = false;
-
+ let mut names: Vec<String> = Vec::new();
+
let inhibitors_head = &self.inhibitors as *const ffi::wl_list as *mut crate::server::WlList;
let mut curr = (*inhibitors_head).next;
while curr != inhibitors_head {
let next = (*curr).next;
let inhibitor = crate::container_of!(curr, IdleInhibitor, link);
-
+
let surface = (*(*inhibitor).wlr_inhibitor).surface;
if let Some(node_data) = SceneNodeData::from_surface(surface) {
- match node_data.data {
- crate::scene_node_data::SceneNodeDataVal::Window(_) |
- crate::scene_node_data::SceneNodeDataVal::ShellSurface(_) |
- crate::scene_node_data::SceneNodeDataVal::LockSurface(_) |
- crate::scene_node_data::SceneNodeDataVal::LayerSurface(_) |
- crate::scene_node_data::SceneNodeDataVal::OverrideRedirect(_) => {
- inhibited = true;
- break;
+ use crate::scene_node_data::SceneNodeDataVal::*;
+ let name = match node_data.data {
+ Window(w) => inhibitor_name(
+ "window",
+ (*w).get_app_id_string().as_deref(),
+ (*w).get_title_string().as_deref(),
+ ),
+ ShellSurface(_) => inhibitor_name("shell-surface", None, None),
+ LockSurface(_) => inhibitor_name("lock-surface", None, None),
+ LayerSurface(l) => {
+ let ns = (*(*l).wlr_layer_surface).namespace;
+ let ns = (!ns.is_null())
+ .then(|| std::ffi::CStr::from_ptr(ns).to_string_lossy().into_owned());
+ inhibitor_name("layer", ns.as_deref(), None)
}
+ OverrideRedirect(_) => inhibitor_name("xwayland-override-redirect", None, None),
+ };
+ if !names.contains(&name) {
+ names.push(name);
}
}
curr = next;
}
+ let inhibited = !names.is_empty();
let notifier = (*self.server).input_manager.idle_notifier;
if !notifier.is_null() {
ffi::wlr_idle_notifier_v1_set_inhibited(notifier, inhibited);
}
- (*self.server).idle.set_inhibited(inhibited);
+ (*self.server).idle.set_inhibitors(names);
+ }
+}
+
+/// What the status line calls one inhibitor: a window's app id, its title
+/// when the client never set one, and the bare kind when it set neither; a
+/// layer surface shows as `layer:<namespace>`. Kinds are stable words, so a
+/// script can tell a browser holding the display from the lock screen.
+pub fn inhibitor_name(kind: &str, id: Option<&str>, title: Option<&str>) -> String {
+ let id = id.filter(|s| !s.is_empty());
+ let title = title.filter(|s| !s.is_empty());
+ match (kind, id, title) {
+ ("window", Some(id), _) => id.to_string(),
+ ("window", None, Some(title)) => format!("window:{}", title),
+ (kind, Some(id), _) => format!("{}:{}", kind, id),
+ (kind, None, _) => kind.to_string(),
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::inhibitor_name;
+
+ #[test]
+ fn a_window_is_its_app_id_then_its_title_then_the_kind() {
+ assert_eq!(inhibitor_name("window", Some("cce-browser"), Some("Video")), "cce-browser");
+ assert_eq!(inhibitor_name("window", None, Some("Video")), "window:Video");
+ assert_eq!(inhibitor_name("window", Some(""), None), "window");
+ }
+
+ #[test]
+ fn other_surfaces_are_their_kind_with_an_optional_id() {
+ assert_eq!(inhibitor_name("layer", Some("bar"), None), "layer:bar");
+ assert_eq!(inhibitor_name("layer", None, None), "layer");
+ assert_eq!(inhibitor_name("lock-surface", None, None), "lock-surface");
}
}