use std::sync::OnceLock;
use x11rb::protocol::xproto::{Atom, AtomEnum, Window};
use super::connection::{Atoms, X11Connection, X11Error};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WmCapabilities {
pub ewmh_wm_present: bool,
pub move_resize: bool,
}
impl WmCapabilities {
pub const NONE: Self = Self {
ewmh_wm_present: false,
move_resize: false,
};
pub fn supports_custom_chrome(self) -> bool {
self.ewmh_wm_present && self.move_resize
}
}
pub fn validate_wm_check(root_value: Option<u32>, child_value: Option<u32>) -> Option<Window> {
match root_value {
Some(win) if win != 0 && child_value == Some(win) => Some(win),
_ => None,
}
}
pub fn supports_atom(supported: &[Atom], atom: Atom) -> bool {
supported.contains(&atom)
}
fn probe_uncached() -> Result<WmCapabilities, X11Error> {
let conn = X11Connection::open()?;
let atoms = conn.atoms();
let root_check = conn
.get_property_full(
conn.root(),
atoms.net_supporting_wm_check,
AtomEnum::WINDOW.into(),
)?
.and_then(|value| value.as_u32());
let child_check = match root_check {
Some(win) => conn
.get_property_full(win, atoms.net_supporting_wm_check, AtomEnum::WINDOW.into())
.ok()
.flatten()
.and_then(|value| value.as_u32()),
None => None,
};
let ewmh_wm_present = validate_wm_check(root_check, child_check).is_some();
if !ewmh_wm_present {
return Ok(WmCapabilities::NONE);
}
let supported = conn
.get_property_full(conn.root(), atoms.net_supported, AtomEnum::ATOM.into())?
.map(|value| value.as_u32s())
.unwrap_or_default();
Ok(WmCapabilities {
ewmh_wm_present,
move_resize: supports_atom(&supported, atoms.net_wm_moveresize),
})
}
pub fn capabilities() -> WmCapabilities {
static CACHE: OnceLock<WmCapabilities> = OnceLock::new();
*CACHE.get_or_init(|| {
use crate::window_system::{WindowSystem, active_window_system};
if active_window_system() != WindowSystem::X11 {
return WmCapabilities::NONE;
}
match probe_uncached() {
Ok(caps) => caps,
Err(err) => {
eprintln!(
"teksilo-platform: X11 window-manager probe failed ({err}); \
keeping native window decorations"
);
WmCapabilities::NONE
}
}
})
}
pub fn mark_skip_taskbar(window: Window) -> Result<(), X11Error> {
add_net_wm_state(window, |atoms| atoms.net_wm_state_skip_taskbar)
}
#[doc(hidden)]
pub fn mark_modal(window: Window) -> Result<(), X11Error> {
add_net_wm_state(window, |atoms| atoms.net_wm_state_modal)
}
fn add_net_wm_state(window: Window, which: fn(&Atoms) -> Atom) -> Result<(), X11Error> {
use x11rb::protocol::xproto::PropMode;
use x11rb::wrapper::ConnectionExt as _;
let conn = X11Connection::open()?;
let atoms = conn.atoms();
let atom = which(atoms);
if read_net_wm_state(&conn, window)?.contains(&atom) {
return Ok(());
}
conn.conn()
.change_property32(
PropMode::APPEND,
window,
atoms.net_wm_state,
AtomEnum::ATOM,
&[atom],
)?
.check()?;
Ok(())
}
pub fn net_wm_state(window: Window) -> Result<Vec<Atom>, X11Error> {
read_net_wm_state(&X11Connection::open()?, window)
}
pub fn transient_for(window: Window) -> Result<Option<Window>, X11Error> {
let conn = X11Connection::open()?;
Ok(conn
.get_property_full(
window,
AtomEnum::WM_TRANSIENT_FOR.into(),
AtomEnum::WINDOW.into(),
)?
.and_then(|value| value.as_u32()))
}
pub fn set_transient_for(window: Window, parent: Window) -> Result<(), X11Error> {
use x11rb::protocol::xproto::PropMode;
use x11rb::wrapper::ConnectionExt as _;
let conn = X11Connection::open()?;
conn.conn()
.change_property32(
PropMode::REPLACE,
window,
AtomEnum::WM_TRANSIENT_FOR,
AtomEnum::WINDOW,
&[parent],
)?
.check()?;
Ok(())
}
pub fn is_dialog_type(window: Window) -> Result<bool, X11Error> {
let conn = X11Connection::open()?;
let types = conn
.get_property_full(
window,
conn.atoms().net_wm_window_type,
AtomEnum::ATOM.into(),
)?
.map(|value| value.as_u32s())
.unwrap_or_default();
Ok(types.contains(&conn.atoms().net_wm_window_type_dialog))
}
fn read_net_wm_state(conn: &X11Connection, window: Window) -> Result<Vec<Atom>, X11Error> {
Ok(conn
.get_property_full(window, conn.atoms().net_wm_state, AtomEnum::ATOM.into())?
.map(|value| value.as_u32s())
.unwrap_or_default())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn wm_check_needs_the_child_to_point_at_itself() {
assert_eq!(validate_wm_check(Some(0x200), Some(0x200)), Some(0x200));
}
#[test]
fn stale_wm_check_is_rejected() {
assert_eq!(validate_wm_check(Some(0x200), None), None);
assert_eq!(validate_wm_check(Some(0x200), Some(0x999)), None);
}
#[test]
fn absent_wm_check_means_no_window_manager() {
assert_eq!(validate_wm_check(None, None), None);
assert_eq!(validate_wm_check(Some(0), Some(0)), None);
}
#[test]
fn custom_chrome_needs_both_a_wm_and_move_resize() {
assert!(
WmCapabilities {
ewmh_wm_present: true,
move_resize: true
}
.supports_custom_chrome()
);
assert!(
!WmCapabilities {
ewmh_wm_present: true,
move_resize: false
}
.supports_custom_chrome()
);
assert!(
!WmCapabilities {
ewmh_wm_present: false,
move_resize: true
}
.supports_custom_chrome()
);
assert!(!WmCapabilities::NONE.supports_custom_chrome());
}
#[test]
fn supported_atom_lookup() {
assert!(supports_atom(&[10, 20, 30], 20));
assert!(!supports_atom(&[10, 30], 20));
assert!(!supports_atom(&[], 20));
}
#[test]
#[ignore = "requires a live X server; run with --ignored"]
fn x11_skip_taskbar_adds_the_state_once_and_keeps_the_others() {
use x11rb::connection::Connection as _;
use x11rb::protocol::xproto::{ConnectionExt as _, CreateWindowAux, WindowClass};
let conn = X11Connection::open().expect("connect to $DISPLAY");
let atoms = conn.atoms().clone();
let raw = conn.conn();
let window = raw.generate_id().expect("allocate an XID");
raw.create_window(
x11rb::COPY_DEPTH_FROM_PARENT,
window,
conn.root(),
0,
0,
16,
16,
0,
WindowClass::INPUT_OUTPUT,
x11rb::COPY_FROM_PARENT,
&CreateWindowAux::new(),
)
.expect("send CreateWindow")
.check()
.expect("create an unmapped window");
let above = raw
.intern_atom(false, b"_NET_WM_STATE_ABOVE")
.expect("send InternAtom")
.reply()
.expect("intern _NET_WM_STATE_ABOVE")
.atom;
conn.set_property32(window, atoms.net_wm_state, AtomEnum::ATOM.into(), &[above])
.expect("give the window a state of its own");
mark_skip_taskbar(window).expect("mark the window");
mark_skip_taskbar(window).expect("mark it again");
assert_eq!(
net_wm_state(window).expect("read _NET_WM_STATE"),
vec![above, atoms.net_wm_state_skip_taskbar],
"the window's own state is kept and the taskbar state is added once"
);
let _ = raw.destroy_window(window).map(|cookie| cookie.check());
}
#[test]
#[ignore = "requires a live X server; run with --ignored"]
fn x11_probe_against_a_live_server() {
let caps = probe_uncached().expect("connect to $DISPLAY");
eprintln!("probed window manager: {caps:?}");
assert!(
caps.ewmh_wm_present,
"no EWMH window manager found on $DISPLAY — custom chrome would be refused"
);
assert!(
caps.move_resize,
"_NET_WM_MOVERESIZE missing from _NET_SUPPORTED — custom chrome would be refused"
);
}
}