mod atoms;
pub mod ffi;
pub mod keys;
pub mod selection;
#[cfg(feature = "x11-e2e")]
pub mod e2e;
use core::ffi::{c_int, c_void};
use core::ptr::{self, NonNull};
use core::time::Duration;
use std::collections::VecDeque;
use crcbl_core::input::DeviceId;
use crcbl_core::{EventTime, KeyCode, Pool};
use crate::linux::xkb;
use crate::{
ClipboardContent, ClipboardOffer, ClipboardRequestId, CloseReply, CursorIcon, DisplayMode,
LogicalSize, MimeType, MonitorId, MonitorInfo, PhysicalPoint, PhysicalRect, PhysicalSize,
PointerMode, ReceivedMime, Shell, ShellBackend, ShellCaps, ShellError, ShellEvent,
SizeConstraints, WindowConfiguration, WindowDesc, WindowId, WindowState,
};
use atoms::Atoms;
use ffi::{Connection, Lib};
use keys::{SizeHints, WmStateAction};
use selection::{Delivery, Read, Step, Write};
use xdnd::Drag;
const KEYBOARD_DEVICE: DeviceId = DeviceId(1);
const POINTER_DEVICE: DeviceId = DeviceId(2);
const DEFAULT_SCALE: f64 = 1.0;
const BASE_DPI: f64 = 96.0;
const TRANSFER_POLL: Duration = Duration::from_millis(50);
const LOCK_RECENTRE_FRACTION: f64 = 0.25;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct TimeBase {
epoch_nanos: u64,
server_origin_nanos: Option<i128>,
high_water_millis: u64,
}
impl TimeBase {
fn now() -> Self {
Self {
epoch_nanos: ffi::monotonic_nanos(),
server_origin_nanos: None,
high_water_millis: 0,
}
}
fn align(&mut self, elapsed: Duration) {
let now = ffi::monotonic_nanos();
self.epoch_nanos =
now.saturating_sub(u64::try_from(elapsed.as_nanos()).unwrap_or(u64::MAX));
}
fn widen(high_water_millis: u64, server_millis: u32) -> u64 {
const WRAP: u64 = 1 << 32;
let low = u64::from(server_millis);
let candidate = (high_water_millis & !(WRAP - 1)) | low;
if candidate + WRAP / 2 < high_water_millis {
candidate + WRAP
} else {
candidate
}
}
fn event_time(&mut self, server_millis: u32) -> EventTime {
self.event_time_at(ffi::monotonic_nanos(), server_millis)
}
fn event_time_at(&mut self, now_nanos: u64, server_millis: u32) -> EventTime {
let widened = Self::widen(self.high_water_millis, server_millis);
self.high_water_millis = self.high_water_millis.max(widened);
let server_nanos = i128::from(widened) * 1_000_000;
let origin = *self
.server_origin_nanos
.get_or_insert_with(|| i128::from(now_nanos) - server_nanos);
let since_epoch = origin + server_nanos - i128::from(self.epoch_nanos);
EventTime::from_duration(Duration::from_nanos(
u64::try_from(since_epoch.max(0)).unwrap_or(u64::MAX),
))
}
}
#[derive(Clone, Copy, Debug)]
struct Argb {
visual: u32,
colormap: u32,
}
struct Conn {
lib: &'static Lib,
ext: ffi::Ext,
connection: NonNull<Connection>,
fd: c_int,
root: u32,
root_visual: u32,
screen_size: PhysicalSize,
atoms: Atoms,
max_property_bytes: usize,
randr_event_base: Option<u8>,
xi_opcode: Option<u8>,
shape: Option<&'static ffi::ShapeLib>,
argb: Option<Argb>,
main_device: Option<u64>,
detectable_auto_repeat: bool,
}
impl core::fmt::Debug for Conn {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Conn")
.field("root", &self.root)
.field("randr", &self.randr_event_base.is_some())
.field("xinput2", &self.xi_opcode.is_some())
.finish()
}
}
impl Conn {
fn raw(&self) -> *mut Connection {
self.connection.as_ptr()
}
fn flush(&self) {
unsafe { (self.lib.flush)(self.raw()) };
}
fn generate_id(&self) -> u32 {
unsafe { (self.lib.generate_id)(self.raw()) }
}
fn has_error(&self) -> Option<String> {
let code = unsafe { (self.lib.connection_has_error)(self.raw()) };
if code == 0 {
return None;
}
Some(match code {
1 => "the connection was closed by the server".to_string(),
2 => "the server does not support an extension we required".to_string(),
3 => "out of memory".to_string(),
4 => "the server hit its request length limit".to_string(),
5 => "cannot parse the display string".to_string(),
6 => "cannot open the display".to_string(),
7 => "the display is invalid".to_string(),
other => format!("libxcb error {other}"),
})
}
fn set_property(&self, window: u32, property: u32, type_: u32, format: u8, data: &[u8]) {
let elements = match format {
32 => data.len() / 4,
16 => data.len() / 2,
_ => data.len(),
};
unsafe {
(self.lib.change_property)(
self.raw(),
ffi::value::PROP_MODE_REPLACE,
window,
property,
type_,
format,
u32::try_from(elements).unwrap_or(u32::MAX),
data.as_ptr().cast::<c_void>(),
);
}
}
fn get_property(&self, window: u32, property: u32) -> Option<(u32, u8, Vec<u8>)> {
const CHUNK_WORDS: u32 = 1 << 20;
let mut bytes = Vec::new();
let mut offset = 0u32;
let mut type_;
let mut format;
loop {
let reply = unsafe {
let cookie = (self.lib.get_property)(
self.raw(),
0,
window,
property,
0,
offset,
CHUNK_WORDS,
);
(self.lib.get_property_reply)(self.raw(), cookie, ptr::null_mut())
};
if reply.is_null() {
return None;
}
let (reply_type, reply_format, bytes_after, value) = unsafe {
let length = (self.lib.get_property_value_length)(reply);
let data = (self.lib.get_property_value)(reply);
let value = if length > 0 && !data.is_null() {
core::slice::from_raw_parts(data.cast::<u8>(), length as usize).to_vec()
} else {
Vec::new()
};
((*reply).type_, (*reply).format, (*reply).bytes_after, value)
};
unsafe { ffi::free_reply(reply) };
if reply_type == 0 {
return Some((0, reply_format, bytes));
}
type_ = reply_type;
format = reply_format;
let read = value.len();
if bytes.len().saturating_add(read) > selection::MAX_BYTES {
crcbl_core::log::warn!(
"property {property} on window {window:#x} is larger than \
{} bytes; treating it as absent",
selection::MAX_BYTES
);
return None;
}
bytes.extend_from_slice(&value);
if bytes_after == 0 || read == 0 {
break;
}
offset += u32::try_from(read / 4).unwrap_or(0).max(1);
}
Some((type_, format, bytes))
}
fn delete_property(&self, window: u32, property: u32) {
unsafe { (self.lib.delete_property)(self.raw(), window, property) };
}
fn get_property_words(&self, window: u32, property: u32) -> Vec<u32> {
let Some((_, format, bytes)) = self.get_property(window, property) else {
return Vec::new();
};
if format != 32 {
return Vec::new();
}
bytes
.as_chunks::<4>()
.0
.iter()
.map(|word| u32::from_ne_bytes([word[0], word[1], word[2], word[3]]))
.collect()
}
fn send_event<T>(&self, window: u32, mask: u32, event: &T) {
const {
assert!(
size_of::<T>() == 32,
"xcb_send_event always reads 32 bytes, so the event must be exactly that large"
);
}
unsafe {
(self.lib.send_event)(
self.raw(),
0,
window,
mask,
ptr::from_ref(event).cast::<core::ffi::c_char>(),
);
}
}
}
pub(super) fn read_wire<T: Copy>(raw: &[u8]) -> Option<T> {
if raw.len() < size_of::<T>() {
crcbl_core::log::debug!(
"an X11 event of {} bytes is too short for a {}-byte wire struct",
raw.len(),
size_of::<T>()
);
return None;
}
let mut value = core::mem::MaybeUninit::<T>::uninit();
unsafe {
core::ptr::copy_nonoverlapping(
raw.as_ptr(),
value.as_mut_ptr().cast::<u8>(),
size_of::<T>(),
);
Some(value.assume_init())
}
}
#[derive(Debug)]
struct XWindow {
id: u32,
title: String,
requested_size: LogicalSize,
requested_mode: DisplayMode,
requested_constraints: SizeConstraints,
resizable: bool,
configuration: Option<WindowConfiguration>,
pending: Option<WindowConfiguration>,
last_size: Option<PhysicalSize>,
effective_fullscreen: bool,
effective_monitor: Option<MonitorId>,
pointer_mode: PointerMode,
cursor: Option<Option<CursorIcon>>,
pointer_inside: bool,
focused: bool,
visible: bool,
mapped: bool,
map_requested: bool,
close_pending: bool,
transparent: bool,
popup: Option<crate::PopupDesc>,
panel: Option<panel::Panel>,
accept_drops: bool,
}
pub struct X11Shell {
conn: Conn,
windows: Pool<XWindow>,
monitors: Vec<MonitorInfo>,
monitor_ids: Vec<(u32, MonitorId)>,
next_monitor_id: u32,
next_request_id: u32,
scale_factor: f64,
keymap: Option<xkb::Keymap>,
held: Vec<u8>,
last_server_time: u32,
timestamp_probe_seen: bool,
reads: Vec<Read>,
writes: Vec<Write>,
offers: Vec<(MimeType, Vec<u8>)>,
owner_window: u32,
owner_time: u32,
drag: Option<Drag>,
blank_cursor: u32,
cursors: cursor::Cursors,
queue: VecDeque<ShellEvent>,
time: TimeBase,
caps: ShellCaps,
has_window_manager: bool,
watching_root: bool,
undrained_events: bool,
wake: Option<std::sync::Arc<crate::linux::wake::WakePipe>>,
frames: crate::frame::FrameRequests,
lost: Option<String>,
#[cfg(feature = "accesskit")]
access: crate::linux::access::Adapters,
}
impl core::fmt::Debug for X11Shell {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("X11Shell")
.field("windows", &self.windows.len())
.field("monitors", &self.monitors.len())
.field("window_manager", &self.has_window_manager)
.field("transfers", &(self.reads.len() + self.writes.len()))
.field("queued_events", &self.queue.len())
.field("connected", &self.lost.is_none())
.finish()
}
}
impl X11Shell {
pub fn open() -> Result<Self, ShellError> {
let lib = ffi::load().map_err(|detail| ShellError::Connect {
backend: ShellBackend::X11,
detail: detail.to_string(),
})?;
let mut screen_index: c_int = 0;
let connection = unsafe { (lib.connect)(ptr::null(), &raw mut screen_index) };
let Some(connection) = NonNull::new(connection) else {
return Err(ShellError::Connect {
backend: ShellBackend::X11,
detail: "xcb_connect returned null".to_string(),
});
};
let error = unsafe { (lib.connection_has_error)(connection.as_ptr()) };
if error != 0 {
unsafe { (lib.disconnect)(connection.as_ptr()) };
return Err(ShellError::Connect {
backend: ShellBackend::X11,
detail: match std::env::var("DISPLAY") {
Ok(display) => format!("no X server at DISPLAY={display} (libxcb {error})"),
Err(_) => "DISPLAY is not set".to_string(),
},
});
}
let (screen, root, root_visual, screen_size, max_request_length) = unsafe {
let setup = (lib.get_setup)(connection.as_ptr());
if setup.is_null() {
(lib.disconnect)(connection.as_ptr());
return Err(ShellError::Connect {
backend: ShellBackend::X11,
detail: "the server sent no setup reply".to_string(),
});
}
let mut iterator = (lib.setup_roots_iterator)(setup);
for _ in 0..screen_index.max(0) {
if iterator.rem <= 1 {
break;
}
(lib.screen_next)(&raw mut iterator);
}
if iterator.data.is_null() {
(lib.disconnect)(connection.as_ptr());
return Err(ShellError::Connect {
backend: ShellBackend::X11,
detail: "the server has no screens".to_string(),
});
}
let screen = *iterator.data;
(
iterator.data,
screen.root,
screen.root_visual,
PhysicalSize::new(
u32::from(screen.width_in_pixels),
u32::from(screen.height_in_pixels),
),
(*setup).maximum_request_length,
)
};
let fd = unsafe { (lib.get_file_descriptor)(connection.as_ptr()) };
let atom_table = unsafe { Atoms::intern(lib, connection.as_ptr()) };
let mut conn = Conn {
lib,
ext: ffi::load_ext(),
connection,
fd,
root,
root_visual,
screen_size,
atoms: atom_table,
max_property_bytes: selection::max_property_bytes(max_request_length),
randr_event_base: None,
xi_opcode: None,
shape: None,
argb: None,
main_device: None,
detectable_auto_repeat: false,
};
conn.randr_event_base = conn.setup_randr();
conn.xi_opcode = conn.setup_xinput2();
conn.shape = conn.setup_shape();
conn.argb = conn.setup_argb(screen, screen_index);
conn.main_device = conn.dri3_device();
conn.detectable_auto_repeat = conn.setup_detectable_auto_repeat();
let keymap = unsafe { xkb::Keymap::from_x11_device(conn.raw().cast::<c_void>()) };
let has_window_manager = conn.detect_window_manager();
let scale_factor = conn.read_xft_scale();
let cursors = unsafe { cursor::Cursors::open(conn.ext.cursor, conn.raw(), screen) };
let mut shell = Self {
conn,
windows: Pool::new(),
monitors: Vec::new(),
monitor_ids: Vec::new(),
next_monitor_id: 1,
next_request_id: 1,
scale_factor,
keymap,
held: Vec::new(),
last_server_time: ffi::value::CURRENT_TIME,
timestamp_probe_seen: false,
reads: Vec::new(),
writes: Vec::new(),
offers: Vec::new(),
owner_window: 0,
owner_time: ffi::value::CURRENT_TIME,
drag: None,
blank_cursor: 0,
cursors,
queue: VecDeque::new(),
time: TimeBase::now(),
caps: ShellCaps::empty(),
has_window_manager,
watching_root: false,
undrained_events: false,
wake: None,
frames: crate::frame::FrameRequests::default(),
lost: None,
#[cfg(feature = "accesskit")]
access: crate::linux::access::Adapters::default(),
};
shell.monitors = shell.enumerate_monitors();
shell.caps = shell.latch_caps();
shell.conn.flush();
Ok(shell)
}
fn latch_caps(&self) -> ShellCaps {
let mut caps = ShellCaps::MULTI_WINDOW
| ShellCaps::EVENT_WAIT
| ShellCaps::CLIPBOARD
| ShellCaps::WINDOW_POSITION
| ShellCaps::POINTER_WARP
| ShellCaps::POINTER_LOCK
| ShellCaps::POINTER_CONFINE
| ShellCaps::FRACTIONAL_SCALE;
caps.set(ShellCaps::CURSOR_SHAPE, self.cursors.applies_shapes());
caps.set(ShellCaps::INPUT_REGION, self.conn.shape.is_some());
caps.set(ShellCaps::TRANSPARENT, self.conn.argb.is_some());
caps.insert(ShellCaps::POPUP | ShellCaps::POPUP_REPOSITION);
caps.insert(ShellCaps::PANEL);
caps.set(ShellCaps::PANEL_RESERVE, self.has_window_manager);
caps.set(ShellCaps::ASPECT_HINT_HONORED, self.has_window_manager);
caps.set(ShellCaps::SERVER_DECORATIONS, self.has_window_manager);
caps.set(ShellCaps::RAW_POINTER_MOTION, self.conn.xi_opcode.is_some());
caps.set(
ShellCaps::TEXT_COMPOSE,
xkb::x11_available() && self.keymap.is_some(),
);
caps.insert(ShellCaps::DRAG_DROP);
#[cfg(feature = "accesskit")]
caps.insert(ShellCaps::ACCESSIBILITY);
caps.remove(ShellCaps::HW_UPSCALE);
caps
}
fn window(&self, window: WindowId) -> Result<&XWindow, ShellError> {
self.windows
.get(window.cast())
.ok_or_else(|| ShellError::invalid_window(window))
}
fn window_mut(&mut self, window: WindowId) -> Result<&mut XWindow, ShellError> {
self.windows
.get_mut(window.cast())
.ok_or_else(|| ShellError::invalid_window(window))
}
fn forget_selection_state(&mut self, window: WindowId, xid: u32) {
let orphaned: Vec<Delivery> = self
.reads
.iter()
.filter(|read| read.window == window)
.map(|read| read.delivery.clone())
.collect();
self.reads.retain(|read| read.window != window);
for delivery in &orphaned {
self.abandon_drop(xid, delivery);
}
self.forget_drag(window);
if self.owner_window == xid {
self.conn
.set_selection_owner(ffi::value::NONE, self.last_server_time);
self.offers.clear();
self.writes.clear();
self.owner_window = 0;
self.owner_time = ffi::value::CURRENT_TIME;
}
}
fn grab_holder(&self) -> Option<WindowId> {
self.windows
.iter()
.find(|(_, state)| state.pointer_mode.is_captured())
.map(|(handle, _)| handle.cast())
}
fn window_by_xid(&self, xid: u32) -> Option<WindowId> {
self.windows
.iter()
.find(|(_, window)| window.id == xid)
.map(|(handle, _)| handle.cast())
}
fn unsupported(what: &'static str) -> ShellError {
ShellError::Unsupported {
backend: ShellBackend::X11,
what,
}
}
pub fn align_time_base(&mut self, elapsed: Duration) {
self.time.align(elapsed);
}
}
mod connect;
mod cursor;
mod input;
mod monitors;
mod panel;
mod popup;
mod shell;
mod window;
mod xdnd;
mod xselection;
#[cfg(test)]
mod tests {
use super::*;
fn base_at(uptime_nanos: u64) -> TimeBase {
TimeBase {
epoch_nanos: uptime_nanos,
server_origin_nanos: None,
high_water_millis: 0,
}
}
#[test]
fn the_server_clock_is_calibrated_on_the_first_event_and_not_before() {
let mut base = base_at(9_000_000_000_000);
assert_eq!(base.server_origin_nanos, None, "nothing to calibrate from");
let first = base.event_time_at(9_003_000_000_000, 4_000_000);
assert!(
base.server_origin_nanos.is_some(),
"the first timestamp calibrates"
);
assert_eq!(
first.as_duration(),
Duration::from_secs(3),
"the first event reads as now, not as the server's uptime"
);
}
#[test]
fn a_server_clock_ahead_of_this_machines_uptime_still_calibrates() {
let mut base = base_at(49_000_000_000);
let first = base.event_time_at(49_500_000_000, 4_000_000);
assert_eq!(
first.as_duration(),
Duration::from_millis(500),
"calibrated, not forwarded: {first:?}"
);
assert!(
base.server_origin_nanos.expect("calibrated") < 0,
"millisecond zero predates this machine's boot"
);
}
#[test]
fn an_event_stamped_before_the_epoch_reads_as_the_epoch() {
let mut base = TimeBase {
epoch_nanos: 10_000_000_000,
server_origin_nanos: Some(0),
high_water_millis: 0,
};
let early = base.event_time_at(10_000_000_000, 1_000);
assert_eq!(early.as_duration(), Duration::ZERO);
}
#[test]
fn durations_between_two_events_survive_calibration_exactly() {
let mut base = TimeBase {
epoch_nanos: 0,
server_origin_nanos: Some(0),
high_water_millis: 0,
};
let pressed = base.event_time(1_000);
let released = base.event_time(1_180);
assert_eq!(
released.saturating_since(pressed),
Duration::from_millis(180),
"the offset cancels"
);
}
#[test]
fn a_thirty_two_bit_server_clock_wrap_does_not_go_backwards() {
const WRAP: u64 = 1 << 32;
let before = TimeBase::widen(WRAP - 1_000, (WRAP - 1_000) as u32);
assert_eq!(before, WRAP - 1_000);
let after = TimeBase::widen(WRAP - 1_000, 500);
assert_eq!(after, WRAP + 500, "the wrap is widened, not misread");
assert!(after > before, "time never goes backwards across a wrap");
assert_eq!(TimeBase::widen(10_000, 9_990), 9_990);
}
#[test]
fn the_two_input_devices_are_distinct_and_not_the_unknown_one() {
assert_ne!(KEYBOARD_DEVICE, POINTER_DEVICE);
assert_ne!(KEYBOARD_DEVICE, DeviceId::UNKNOWN);
assert_ne!(POINTER_DEVICE, DeviceId::UNKNOWN);
}
}