wayland: idle-inhibit while focused and a content-type hint

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ib25e27fc913c3af009e85496412002366a6a6964
This commit is contained in:
raf 2026-06-26 10:56:59 +03:00
commit e172d4fbb3
No known key found for this signature in database
GPG key ID: 29D95B64378DB4BF
4 changed files with 107 additions and 0 deletions

View file

@ -39,6 +39,11 @@ channel).
built-in set (whitespace and common punctuation, keeping _.-/:~\__ inside built-in set (whitespace and common punctuation, keeping _.-/:~\__ inside
words). 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] # [colors]
*foreground* = _color_, *background* = _color_ *foreground* = _color_, *background* = _color_

View file

@ -154,6 +154,9 @@ pub struct Main {
/// Characters that break a word for double-click selection. Empty/unset /// Characters that break a word for double-click selection. Empty/unset
/// keeps the built-in default. /// keeps the built-in default.
pub word_delimiters: Option<String>, pub word_delimiters: Option<String>,
/// 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 { impl Default for Main {
@ -167,6 +170,7 @@ impl Default for Main {
pad_x: 2, pad_x: 2,
pad_y: 2, pad_y: 2,
word_delimiters: None, word_delimiters: None,
idle_inhibit: false,
} }
} }
} }

View file

@ -70,6 +70,7 @@ impl WindowHandler for App {
} }
} }
self.focused = configure.is_activated(); self.focused = configure.is_activated();
self.sync_idle_inhibit();
if self.session.is_none() { if self.session.is_none() {
self.spawn_session(); self.spawn_session();
} else { } else {
@ -188,6 +189,7 @@ impl KeyboardHandler for App {
self.activate_keyboard(keyboard); self.activate_keyboard(keyboard);
self.serial = serial; self.serial = serial;
self.focused = true; self.focused = true;
self.sync_idle_inhibit();
self.report_focus(true); self.report_focus(true);
self.needs_draw = true; self.needs_draw = true;
} }
@ -201,6 +203,7 @@ impl KeyboardHandler for App {
_: u32, _: u32,
) { ) {
self.focused = false; self.focused = false;
self.sync_idle_inhibit();
// Drop held-key state so a key released while unfocused can't leak a // Drop held-key state so a key released while unfocused can't leak a
// stale kitty release event later. // stale kitty release event later.
self.keys_down.clear(); self.keys_down.clear();
@ -634,6 +637,56 @@ impl Dispatch<WpViewport, ()> for App {
} }
} }
// idle-inhibit and content-type are likewise raw protocol objects; none of them
// emit events we act on.
impl Dispatch<ZwpIdleInhibitManagerV1, ()> for App {
fn event(
_: &mut Self,
_: &ZwpIdleInhibitManagerV1,
_: <ZwpIdleInhibitManagerV1 as Proxy>::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
}
}
impl Dispatch<ZwpIdleInhibitorV1, ()> for App {
fn event(
_: &mut Self,
_: &ZwpIdleInhibitorV1,
_: zwp_idle_inhibitor_v1::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
}
}
impl Dispatch<WpContentTypeManagerV1, ()> for App {
fn event(
_: &mut Self,
_: &WpContentTypeManagerV1,
_: <WpContentTypeManagerV1 as Proxy>::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
}
}
impl Dispatch<WpContentTypeV1, ()> for App {
fn event(
_: &mut Self,
_: &WpContentTypeV1,
_: wp_content_type_v1::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
}
}
impl ActivationHandler for App { impl ActivationHandler for App {
type RequestData = RequestData; type RequestData = RequestData;

View file

@ -84,10 +84,18 @@ use wayland_client::{
wl_surface, 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::{ use wayland_protocols::wp::fractional_scale::v1::client::{
wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1, wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1,
wp_fractional_scale_v1::{self, WpFractionalScaleV1}, 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::{ use wayland_protocols::wp::text_input::zv3::client::{
zwp_text_input_manager_v3::ZwpTextInputManagerV3, zwp_text_input_manager_v3::ZwpTextInputManagerV3,
zwp_text_input_v3::{self, ContentHint, ContentPurpose, ZwpTextInputV3}, zwp_text_input_v3::{self, ContentHint, ContentPurpose, ZwpTextInputV3},
@ -181,6 +189,14 @@ pub fn run(config: Config, config_path: Option<std::path::PathBuf>) -> anyhow::R
}); });
let text_input_manager = bind_global::<ZwpTextInputManagerV3>(&globals, &qh); let text_input_manager = bind_global::<ZwpTextInputManagerV3>(&globals, &qh);
let activation = ActivationState::bind(&globals, &qh).ok(); let activation = ActivationState::bind(&globals, &qh).ok();
let idle_inhibit_manager = bind_global::<ZwpIdleInhibitManagerV1>(&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::<WpContentTypeManagerV1>(&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. // First commit with no buffer kicks off the initial configure.
window.commit(); window.commit();
@ -219,6 +235,9 @@ pub fn run(config: Config, config_path: Option<std::path::PathBuf>) -> anyhow::R
cursor_shape_manager, cursor_shape_manager,
text_input_manager, text_input_manager,
activation, activation,
idle_inhibit_manager,
idle_inhibitor: None,
content_type,
preedit: String::new(), preedit: String::new(),
ime_preedit_pending: String::new(), ime_preedit_pending: String::new(),
ime_commit_pending: String::new(), ime_commit_pending: String::new(),
@ -397,6 +416,17 @@ struct App {
text_input_manager: Option<ZwpTextInputManagerV3>, text_input_manager: Option<ZwpTextInputManagerV3>,
/// xdg-activation, used to request attention on an urgent bell. /// xdg-activation, used to request attention on an urgent bell.
activation: Option<ActivationState>, activation: Option<ActivationState>,
/// 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<ZwpIdleInhibitManagerV1>,
idle_inhibitor: Option<ZwpIdleInhibitorV1>,
/// 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<WpContentTypeV1>,
/// Committed IME preedit string shown inline at the cursor while composing. /// Committed IME preedit string shown inline at the cursor while composing.
preedit: String, preedit: String,
/// Preedit/commit accumulated since the last text-input `done`. /// 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. /// Write bytes to the PTY master, logging on failure.
fn write_to_pty(&mut self, bytes: &[u8]) { fn write_to_pty(&mut self, bytes: &[u8]) {
if let Some(session) = self.session.as_mut() if let Some(session) = self.session.as_mut()