From e172d4fbb3fc1d15c4e9eed58b8d68ce9cbb7e72 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Fri, 26 Jun 2026 10:56:59 +0300 Subject: [PATCH] wayland: idle-inhibit while focused and a content-type hint Signed-off-by: NotAShelf Change-Id: Ib25e27fc913c3af009e85496412002366a6a6964 --- doc/beer.toml.5.scd | 5 ++++ src/config.rs | 4 ++++ src/wayland/handlers.rs | 53 +++++++++++++++++++++++++++++++++++++++++ src/wayland/mod.rs | 45 ++++++++++++++++++++++++++++++++++ 4 files changed, 107 insertions(+) diff --git a/doc/beer.toml.5.scd b/doc/beer.toml.5.scd index 15acd1e..9317efd 100644 --- a/doc/beer.toml.5.scd +++ b/doc/beer.toml.5.scd @@ -39,6 +39,11 @@ channel). built-in set (whitespace and common punctuation, keeping _.-/:~\__ inside words). +*idle-inhibit* = _bool_ + While the window is focused, ask the compositor not to blank the screen or + start the screensaver (idle-inhibit-v1). A backgrounded window stops + inhibiting. Default _false_. + # [colors] *foreground* = _color_, *background* = _color_ diff --git a/src/config.rs b/src/config.rs index fcdcccb..fbff91a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -154,6 +154,9 @@ pub struct Main { /// Characters that break a word for double-click selection. Empty/unset /// keeps the built-in default. pub word_delimiters: Option, + /// Hold an idle inhibitor while the window is focused, so the compositor + /// does not blank the screen or start the screensaver. Default off. + pub idle_inhibit: bool, } impl Default for Main { @@ -167,6 +170,7 @@ impl Default for Main { pad_x: 2, pad_y: 2, word_delimiters: None, + idle_inhibit: false, } } } diff --git a/src/wayland/handlers.rs b/src/wayland/handlers.rs index 7761442..4ca5233 100644 --- a/src/wayland/handlers.rs +++ b/src/wayland/handlers.rs @@ -70,6 +70,7 @@ impl WindowHandler for App { } } self.focused = configure.is_activated(); + self.sync_idle_inhibit(); if self.session.is_none() { self.spawn_session(); } else { @@ -188,6 +189,7 @@ impl KeyboardHandler for App { self.activate_keyboard(keyboard); self.serial = serial; self.focused = true; + self.sync_idle_inhibit(); self.report_focus(true); self.needs_draw = true; } @@ -201,6 +203,7 @@ impl KeyboardHandler for App { _: u32, ) { self.focused = false; + self.sync_idle_inhibit(); // Drop held-key state so a key released while unfocused can't leak a // stale kitty release event later. self.keys_down.clear(); @@ -634,6 +637,56 @@ impl Dispatch for App { } } +// idle-inhibit and content-type are likewise raw protocol objects; none of them +// emit events we act on. +impl Dispatch for App { + fn event( + _: &mut Self, + _: &ZwpIdleInhibitManagerV1, + _: ::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + } +} + +impl Dispatch for App { + fn event( + _: &mut Self, + _: &ZwpIdleInhibitorV1, + _: zwp_idle_inhibitor_v1::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + } +} + +impl Dispatch for App { + fn event( + _: &mut Self, + _: &WpContentTypeManagerV1, + _: ::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + } +} + +impl Dispatch for App { + fn event( + _: &mut Self, + _: &WpContentTypeV1, + _: wp_content_type_v1::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + } +} + impl ActivationHandler for App { type RequestData = RequestData; diff --git a/src/wayland/mod.rs b/src/wayland/mod.rs index 5e6301b..a20c7e7 100644 --- a/src/wayland/mod.rs +++ b/src/wayland/mod.rs @@ -84,10 +84,18 @@ use wayland_client::{ wl_surface, }, }; +use wayland_protocols::wp::content_type::v1::client::{ + wp_content_type_manager_v1::WpContentTypeManagerV1, + wp_content_type_v1::{self, WpContentTypeV1}, +}; use wayland_protocols::wp::fractional_scale::v1::client::{ wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1, wp_fractional_scale_v1::{self, WpFractionalScaleV1}, }; +use wayland_protocols::wp::idle_inhibit::zv1::client::{ + zwp_idle_inhibit_manager_v1::ZwpIdleInhibitManagerV1, + zwp_idle_inhibitor_v1::{self, ZwpIdleInhibitorV1}, +}; use wayland_protocols::wp::text_input::zv3::client::{ zwp_text_input_manager_v3::ZwpTextInputManagerV3, zwp_text_input_v3::{self, ContentHint, ContentPurpose, ZwpTextInputV3}, @@ -181,6 +189,14 @@ pub fn run(config: Config, config_path: Option) -> anyhow::R }); let text_input_manager = bind_global::(&globals, &qh); let activation = ActivationState::bind(&globals, &qh).ok(); + let idle_inhibit_manager = bind_global::(&globals, &qh); + // Tag the surface as plain content (a terminal is none of photo/video/game) + // so the compositor applies no media-specific treatment. Applies on commit. + let content_type = bind_global::(&globals, &qh) + .map(|mgr| mgr.get_surface_content_type(window.wl_surface(), &qh, ())); + if let Some(ct) = &content_type { + ct.set_content_type(wp_content_type_v1::Type::None); + } // First commit with no buffer kicks off the initial configure. window.commit(); @@ -219,6 +235,9 @@ pub fn run(config: Config, config_path: Option) -> anyhow::R cursor_shape_manager, text_input_manager, activation, + idle_inhibit_manager, + idle_inhibitor: None, + content_type, preedit: String::new(), ime_preedit_pending: String::new(), ime_commit_pending: String::new(), @@ -397,6 +416,17 @@ struct App { text_input_manager: Option, /// xdg-activation, used to request attention on an urgent bell. activation: Option, + /// idle-inhibit-v1 manager; an inhibitor is held while focused when the + /// `[main] idle-inhibit` config is on, so the screen does not blank. + idle_inhibit_manager: Option, + idle_inhibitor: Option, + /// content-type-v1 hint object. Set once at startup; held only so the + /// object (and thus the hint) outlives construction. + #[allow( + dead_code, + reason = "kept alive to preserve the surface content-type hint" + )] + content_type: Option, /// Committed IME preedit string shown inline at the cursor while composing. preedit: String, /// Preedit/commit accumulated since the last text-input `done`. @@ -1409,6 +1439,21 @@ impl App { } } + /// Create or drop the idle inhibitor to match `[main] idle-inhibit` and the + /// current focus: inhibit only while focused, so a backgrounded terminal + /// still lets the screen blank. Idempotent; called on every focus change. + fn sync_idle_inhibit(&mut self) { + let want = self.config.main.idle_inhibit && self.focused; + if want && self.idle_inhibitor.is_none() { + if let Some(mgr) = &self.idle_inhibit_manager { + self.idle_inhibitor = + Some(mgr.create_inhibitor(self.window.wl_surface(), &self.qh, ())); + } + } else if !want && let Some(inhibitor) = self.idle_inhibitor.take() { + inhibitor.destroy(); + } + } + /// Write bytes to the PTY master, logging on failure. fn write_to_pty(&mut self, bytes: &[u8]) { if let Some(session) = self.session.as_mut()