//! Port of apprt/gtk/winproto/x11.zig: X11 window protocol implementation. //! //! Uses Xlib (through the `x11` crate, sharing GDK's `Display*`) exactly like //! the Zig code; all raw FFI is confined to small `unsafe` blocks with SAFETY //! comments. #![allow(unsafe_code)] use std::{ ffi::{c_int, c_long, c_uchar, c_uint, c_ulong, c_void}, rc::Rc, }; use gdk4_x11::{X11Display, X11Surface, x11::xlib}; use ghostty_input::Mods; use gtk::{gdk, glib, prelude::*}; use super::{Error, Result, WindowHost}; // XKB bits we need that the `x11` crate doesn't expose (XKBlib.h / XKB.h). const XKB_MAJOR_VERSION: c_int = 1; const XKB_MINOR_VERSION: c_int = 0; const XKB_USE_CORE_KBD: c_uint = 0x0100; const XKB_STATE_NOTIFY: c_int = 2; const QUEUED_AFTER_READING: c_int = 1; const XKB_MODIFIER_STATE_MASK: c_ulong = 1 << 0; #[link(name = "X11")] unsafe extern "C" { fn XkbQueryExtension( dpy: *mut c_void, opcode_rtrn: *mut c_int, event_rtrn: *mut c_int, error_rtrn: *mut c_int, major_in_out: *mut c_int, minor_in_out: *mut c_int, ) -> c_int; fn XkbSelectEventDetails( dpy: *mut c_void, device_spec: c_uint, event_type: c_uint, bits_to_change: c_ulong, values_for_bits: c_ulong, ) -> c_int; } /// Leading part of `XkbStateNotifyEvent` (XKBlib.h) up to `lookup_mods`. #[repr(C)] #[allow(dead_code)] struct XkbStateNotifyEvent { type_: c_int, serial: c_ulong, send_event: c_int, display: *mut xlib::Display, time: c_ulong, xkb_type: c_int, device: c_int, changed: c_uint, group: c_int, base_group: c_int, latched_group: c_int, locked_group: c_int, mods: c_uint, base_mods: c_uint, latched_mods: c_uint, locked_mods: c_uint, compat_state: c_int, grab_mods: c_uchar, compat_grab_mods: c_uchar, lookup_mods: c_uchar, } #[derive(Debug, Clone, Copy)] struct Atoms { kde_blur: xlib::Atom, motif_wm_hints: xlib::Atom, } impl Atoms { fn new(display: &X11Display) -> Atoms { Atoms { kde_blur: gdk4_x11::x11_get_xatom_by_name_for_display( display, "_KDE_NET_WM_BLUR_BEHIND_REGION", ), motif_wm_hints: gdk4_x11::x11_get_xatom_by_name_for_display(display, "_MOTIF_WM_HINTS"), } } } /// X11 application-wide state. pub struct App { xl: Rc, display: *mut xlib::Display, base_event_code: c_int, atoms: Atoms, } impl App { /// Returns `Ok(None)` if the display isn't X11. pub fn init( gdk_display: &gdk::Display, app_id: &str, config: &ghostty_config::config::Config, ) -> Result> { let Some(x11_display) = gdk_display.downcast_ref::() else { return Ok(None); }; let xl = xlib::Xlib::open().map_err(|e| { log::error!("failed to load libX11: {e}"); Error::XkbInitialization })?; // SAFETY: the display is a live X11 display; the returned pointer is // owned by GDK and valid for the lifetime of the display (the whole // application). let xlib_display = unsafe { x11_display.xdisplay() }; let program_name = match &config.x11_instance_name { Some(pn) => pn.clone(), None if cfg!(debug_assertions) => "ghostty-debug".to_string(), None => "ghostty".to_string(), }; // Set the X11 window class property (WM_CLASS); the instance name is // derived from g_set_prgname. Results in e.g. // WM_CLASS(STRING) = "ghostty", "com.mitchellh.ghostty" glib::set_prgname(Some(program_name.as_str())); x11_display.set_program_class(app_id); log::debug!("Xkb.init: initializing Xkb"); let mut opcode: c_int = 0; let mut base_event_code: c_int = 0; let mut base_error_code: c_int = 0; let mut major = XKB_MAJOR_VERSION; let mut minor = XKB_MINOR_VERSION; // SAFETY: valid display pointer, valid out-pointers to locals. let ok = unsafe { XkbQueryExtension( xlib_display.cast(), &raw mut opcode, &raw mut base_event_code, &raw mut base_error_code, &raw mut major, &raw mut minor, ) }; if ok == 0 { log::error!( "Fatal: error initializing Xkb extension: error executing XkbQueryExtension" ); return Err(Error::XkbInitialization); } // SAFETY: valid display pointer. let ok = unsafe { XkbSelectEventDetails( xlib_display.cast(), XKB_USE_CORE_KBD, XKB_STATE_NOTIFY as c_uint, XKB_MODIFIER_STATE_MASK, XKB_MODIFIER_STATE_MASK, ) }; if ok == 0 { log::error!( "Fatal: error initializing Xkb extension: error executing XkbSelectEventDetails" ); return Err(Error::XkbInitialization); } Ok(Some(App { xl: Rc::new(xl), display: xlib_display, base_event_code, atoms: Atoms::new(x11_display), })) } /// Checks for an immediate pending XKB state update event and returns the /// keyboard state based on it. Necessary because the standard GTK X11 API /// does not include the currently pressed key in the modifier snapshot /// (paraphrased from Firefox/Gecko widget/gtk/nsGtkKeyUtils.cpp). /// /// Returns `None` if there is no such event; callers fall back to the GDK /// modifier state. pub fn event_mods(&self, _: Option<&gdk::Device>, _: gdk::ModifierType) -> Option { // SAFETY: `display` is valid for the application's lifetime; XEvent is // plain-old-data, zero-initialised, and filled by XPeekEvent. let lookup_mods = unsafe { if (self.xl.XEventsQueued)(self.display, QUEUED_AFTER_READING) == 0 { return None; } let mut next: xlib::XEvent = std::mem::zeroed(); (self.xl.XPeekEvent)(self.display, &raw mut next); if next.type_ != self.base_event_code { return None; } // XkbStateNotifyEvent is a prefix-compatible view of the XEvent // union (which is at least as large and aligned as a long). let ev = &*std::ptr::from_ref(&next).cast::(); if ev.xkb_type != XKB_STATE_NOTIFY { return None; } ev.lookup_mods }; let lookup = c_uint::from(lookup_mods); let mut mods = Mods::default(); log::debug!("X11: found extra XkbStateNotify event w/lookup_mods: {lookup_mods:b}"); if lookup & xlib::ShiftMask != 0 { mods.shift = true; } if lookup & xlib::ControlMask != 0 { mods.ctrl = true; } if lookup & xlib::Mod1Mask != 0 { mods.alt = true; } if lookup & xlib::Mod4Mask != 0 { mods.super_ = true; } if lookup & xlib::LockMask != 0 { mods.caps_lock = true; } Some(mods) } #[allow(clippy::unused_self)] pub fn supports_quick_terminal(&self) -> bool { log::warn!("quick terminal is not yet supported on X11"); false } #[allow(clippy::unused_self, clippy::unnecessary_wraps)] pub fn init_quick_terminal(&mut self, _: >k::Window) -> Result<()> { Ok(()) } } /// `_KDE_NET_WM_BLUR_BEHIND_REGION` payload: x, y, width, height (32-bit /// format properties are arrays of `long`). #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] #[repr(C)] struct Region { x: c_long, y: c_long, width: c_long, height: c_long, } /// See Xm/MwmUtil.h, packaged with the Motif Window Manager. All fields are /// `long`-sized in the 32-bit format. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] #[repr(C)] struct MotifWmHints { flags: c_ulong, functions: c_ulong, decorations: c_ulong, input_mode: c_long, status: c_ulong, } impl MotifWmHints { const FLAG_DECORATIONS: c_ulong = 1 << 1; const DECORATIONS_ALL: c_ulong = 1 << 0; } /// X11 per-window state. pub struct Window { xl: Rc, display: *mut xlib::Display, atoms: Atoms, host: Rc, x11_surface: X11Surface, blur_region: Region, // Cache last applied values to avoid redundant X11 property updates, which // cause visual glitches with some WMs (ghostty-org/ghostty#8075). last_applied_blur_region: Option, last_applied_decoration_hints: Option, } impl Window { pub fn init(app: &App, host: Rc) -> Result { let win = host.gtk_window().ok_or(Error::NotX11Surface)?; let surface = super::native_surface(&win).ok_or(Error::NotX11Surface)?; let x11_surface = surface .downcast::() .map_err(|_| Error::NotX11Surface)?; Ok(Window { xl: Rc::clone(&app.xl), display: app.display, atoms: app.atoms, host, x11_surface, blur_region: Region::default(), last_applied_blur_region: None, last_applied_decoration_hints: None, }) } pub fn resize_event(&mut self) -> Result<()> { // The blur region must update with window resizes self.sync_blur() } pub fn sync_appearance(&mut self) -> Result<()> { // The user could have toggled between CSDs and SSDs, therefore we need // to recalculate the blur region offset. if let Some(win) = self.host.gtk_window() { let (mut x, mut y) = win.upcast_ref::().surface_transform(); // Transform surface coordinates to device coordinates. let scale = f64::from(win.scale_factor()); x *= scale; y *= scale; self.blur_region = Region { x: x as c_long, y: y as c_long, ..Region::default() }; } if let Err(err) = self.sync_blur() { log::error!("failed to synchronize blur={err}"); } if let Err(err) = self.sync_decorations() { log::error!("failed to synchronize decorations={err}"); } Ok(()) } pub fn client_side_decoration_enabled(&self) -> bool { use ghostty_config::types_b::WindowDecoration as D; match self.host.window_decoration() { D::Auto | D::Client => true, D::Server | D::None => false, } } fn sync_blur(&mut self) -> Result<()> { // FIXME (upstream): doesn't factor in rounded corners on Adwaita. let Some(win) = self.host.gtk_window() else { return Ok(()); }; let Some(config) = self.host.config() else { return Ok(()); }; // When blur is disabled, remove the property if it was previously set let blur = config.background_blur; if !blur.enabled() { if self.last_applied_blur_region.is_some() { self.delete_property(self.atoms.kde_blur)?; self.last_applied_blur_region = None; } return Ok(()); } // Transform surface coordinates to device coordinates. let scale = win.scale_factor(); self.blur_region.width = c_long::from(win.width() * scale); self.blur_region.height = c_long::from(win.height() * scale); // Only update X11 properties when the blur region actually changes if self.last_applied_blur_region == Some(self.blur_region) { return Ok(()); } log::debug!( "set blur={blur:?}, window xid={}, region={:?}", self.x11_surface.xid(), self.blur_region ); let region = self.blur_region; self.change_property(self.atoms.kde_blur, xlib::XA_CARDINAL, ®ion)?; self.last_applied_blur_region = Some(region); Ok(()) } fn sync_decorations(&mut self) -> Result<()> { use ghostty_config::types_b::WindowDecoration as D; let mut hints = match self.get_window_property::( self.atoms.motif_wm_hints, self.atoms.motif_wm_hints, ) { Ok(h) => h, // already default-initialised, so this is fine Err(Error::PropertyNotFound) => MotifWmHints::default(), Err(e) => return Err(e), }; hints.flags |= MotifWmHints::FLAG_DECORATIONS; if matches!(self.host.window_decoration(), D::Server) { hints.decorations |= MotifWmHints::DECORATIONS_ALL; } else { hints.decorations &= !MotifWmHints::DECORATIONS_ALL; } // Only update decoration hints when they actually change if self.last_applied_decoration_hints == Some(hints) { return Ok(()); } self.change_property(self.atoms.motif_wm_hints, self.atoms.motif_wm_hints, &hints)?; self.last_applied_decoration_hints = Some(hints); Ok(()) } pub fn add_subprocess_env(&mut self, env: &mut Vec<(String, String)>) -> Result<()> { env.push(("WINDOWID".to_string(), self.x11_surface.xid().to_string())); Ok(()) } #[allow(clippy::unnecessary_wraps)] pub fn set_urgent(&mut self, urgent: bool) -> Result<()> { self.x11_surface.set_urgency_hint(urgent); Ok(()) } /// `XGetWindowProperty` for a 32-format property that is an array of /// `long`s whose layout matches `T` (`T` is a `repr(C)` struct of longs). fn get_window_property(&self, name: xlib::Atom, typ: xlib::Atom) -> Result { let mut actual_type: xlib::Atom = 0; let mut actual_format: c_int = 0; let mut nitems: c_ulong = 0; let mut bytes_after: c_ulong = 0; let mut prop: *mut c_uchar = std::ptr::null_mut(); // SAFETY: valid display and window id; all out-pointers refer to locals. let code = unsafe { (self.xl.XGetWindowProperty)( self.display, self.x11_surface.xid(), name, 0, c_long::MAX, xlib::False, typ, &raw mut actual_type, &raw mut actual_format, &raw mut nitems, &raw mut bytes_after, &raw mut prop, ) }; if code != c_int::from(xlib::Success) { return Err(Error::RequestFailed); } if actual_type == 0 { return Err(Error::PropertyNotFound); } let result = if typ != actual_type { Err(Error::PropertyTypeMismatch) } else if actual_format != 32 { Err(Error::PropertyFormatMismatch) } else if (nitems as usize) * std::mem::size_of::() < std::mem::size_of::() || prop.is_null() { Err(Error::RequestFailed) } else { // SAFETY: Xlib returns 32-format data as an array of `long` // (suitably aligned), at least size_of::() bytes of which we // just verified; T is a repr(C) aggregate of longs. Ok(unsafe { prop.cast::().read() }) }; if !prop.is_null() { // SAFETY: allocated by Xlib. unsafe { (self.xl.XFree)(prop.cast()) }; } result } fn change_property(&self, name: xlib::Atom, typ: xlib::Atom, value: &T) -> Result<()> { let nelements = std::mem::size_of::() / std::mem::size_of::(); // SAFETY: valid display/window; `value` points to `nelements` longs // (T is a repr(C) aggregate of longs); data is copied by Xlib. let status = unsafe { (self.xl.XChangeProperty)( self.display, self.x11_surface.xid(), name, typ, 32, xlib::PropModeReplace, std::ptr::from_ref(value).cast::(), nelements as c_int, ) }; // Xlib alternates between error values (0 = success) and booleans // (1 = success); the Zig code treats 0 as failure here. if status == 0 { return Err(Error::RequestFailed); } Ok(()) } fn delete_property(&self, name: xlib::Atom) -> Result<()> { // SAFETY: valid display and window id. let status = unsafe { (self.xl.XDeleteProperty)(self.display, self.x11_surface.xid(), name) }; if status == 0 { return Err(Error::RequestFailed); } Ok(()) } } #[cfg(test)] mod tests { use super::*; #[test] fn property_layouts_are_long_arrays() { let l = std::mem::size_of::(); assert_eq!(std::mem::size_of::(), 4 * l); assert_eq!(std::mem::size_of::(), 5 * l); } }