diff --git a/Cargo.lock b/Cargo.lock index 47a08ab94..dc98ce9fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4690,6 +4690,7 @@ dependencies = [ "cocoa", "core-foundation 0.7.0", "core-graphics 0.19.2", + "dirs-next", "euclid", "filedescriptor", "gl_generator", diff --git a/docs/changelog.md b/docs/changelog.md index 5842e59b0..19dbea09c 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -94,6 +94,7 @@ brief notes about them may accumulate here. * Unix: adjust font-config patterns to also match "dual spacing" fonts such as [Iosevka Term](https://typeof.net/Iosevka/). Thanks to [Leiser](https://github.com/leiserfg)! [#446](https://github.com/wez/wezterm/pull/446) * New: Added [alternate_buffer_wheel_scroll_speed](config/lua/config/alternate_buffer_wheel_scroll_speed.md) option to control how many cursor key presses are generated by the mouse wheel when the alternate screen is active. The new default for this is a faster-than-previous-releases 3 lines per wheel tick. [#432](https://github.com/wez/wezterm/issues/432) * macOS: Dead Keys are now processed even when `use_ime=false`. [More details in the docs](config/keys.md#macos-left-and-right-option-key). [#410](https://github.com/wez/wezterm/issues/410). +* X11: attempt to load cursors from the XCursor.theme resource specified on the root window [#524](https://github.com/wez/wezterm/issues/524) ### 20201101-103216-403d002d diff --git a/get-deps b/get-deps index a72db67a2..fe3fb1061 100755 --- a/get-deps +++ b/get-deps @@ -23,6 +23,7 @@ if test -e /etc/centos-release || test -e /etc/fedora-release; then wayland-devel \ mesa-libEGL-devel \ xcb-util-keysyms-devel \ + xcb-util-image-devel \ xcb-util-wm-devel \ rpm-build \ redhat-lsb-core diff --git a/window/Cargo.toml b/window/Cargo.toml index 2968b081b..800753113 100644 --- a/window/Cargo.toml +++ b/window/Cargo.toml @@ -51,10 +51,11 @@ clipboard-win = "2.2" shared_library = "0.1" [target.'cfg(all(unix, not(target_os = "macos")))'.dependencies] +dirs-next = "2.0" filedescriptor = { version="0.7", path = "../filedescriptor" } x11 = {version ="2.18", features = ["xlib_xcb"]} -xcb = {version="0.9", features=["shm", "xkb", "xlib_xcb"]} -xcb-util = { features = [ "cursor", "icccm", "ewmh", "keysyms", "shm"], version = "0.3" } +xcb = {version="0.9", features=["render", "shm", "xkb", "xlib_xcb"]} +xcb-util = { features = [ "cursor", "image", "icccm", "ewmh", "keysyms", "shm"], version = "0.3" } xkbcommon = { version = "0.5", features = ["x11", "wayland"], git="https://github.com/wez/xkbcommon-rs.git", rev="01a0a0cd5663405e6e4abb1ad3add9add1496f58"} mio = "0.6" libc = "0.2" diff --git a/window/src/os/x11/connection.rs b/window/src/os/x11/connection.rs index 580e78169..fa474ef38 100644 --- a/window/src/os/x11/connection.rs +++ b/window/src/os/x11/connection.rs @@ -29,6 +29,7 @@ pub struct XConnection { pub atom_targets: xcb::Atom, pub atom_clipboard: xcb::Atom, keysyms: *mut xcb_key_symbols_t, + pub(crate) xrm: HashMap, pub(crate) windows: RefCell>>>, should_terminate: RefCell, pub(crate) shm_available: bool, @@ -391,11 +392,15 @@ impl XConnection { let root = screen.root(); + let xrm = + crate::x11::xrm::parse_root_resource_manager(&conn, root).unwrap_or(HashMap::new()); + let conn = XConnection { conn, cursor_font_id, screen_num, root, + xrm, atom_protocols, atom_clipboard, atom_delete, diff --git a/window/src/os/x11/cursor.rs b/window/src/os/x11/cursor.rs index 3491aaa3e..a14f7c206 100644 --- a/window/src/os/x11/cursor.rs +++ b/window/src/os/x11/cursor.rs @@ -1,6 +1,12 @@ use crate::x11::XConnection; use crate::MouseCursor; +use anyhow::{ensure, Context}; use std::collections::HashMap; +use std::convert::TryInto; +use std::ffi::OsString; +use std::io::prelude::*; +use std::io::SeekFrom; +use std::path::PathBuf; use std::rc::{Rc, Weak}; use xcb::ffi::xcb_cursor_t; @@ -21,14 +27,113 @@ pub struct CursorInfo { cursors: HashMap, XcbCursor>, cursor: Option, conn: Weak, + size: Option, + theme: Option, + icon_path: Vec, + pict_format_id: Option, +} + +fn icon_path() -> Vec { + let path = std::env::var_os("XCURSOR_PATH").unwrap_or_else(|| { + OsString::from("~/.icons:/usr/share/icons:/usr/share/pixmaps:/usr/X11R6/lib/X11/icons") + }); + + fn tilde_expand(p: PathBuf) -> PathBuf { + match p.to_str() { + Some(s) => { + if s.starts_with("~/") { + if let Some(home) = dirs_next::home_dir() { + home.join(&s[2..]) + } else { + p.into() + } + } else { + p.into() + } + } + None => p.into(), + } + } + + std::env::split_paths(&path).map(tilde_expand).collect() +} + +fn cursor_size(map: &HashMap) -> u32 { + if let Ok(size) = std::env::var("XCURSOR_SIZE") { + if let Ok(size) = size.parse::() { + return size; + } + } + + if let Some(size) = map.get("Xcursor.size") { + if let Ok(size) = size.parse::() { + return size; + } + } + + if let Some(dpi) = map.get("Xft.dpi") { + if let Ok(dpi) = dpi.parse::() { + return dpi * 16 / 72; + } + } + + // Probably a good default? + 24 } impl CursorInfo { - pub fn new(conn: Weak) -> Self { + pub fn new(conn: &Rc) -> Self { + let mut size = None; + let mut theme = None; + let mut pict_format_id = None; + // If we know the theme to use, then we need the render extension + // if we are to be able to load the cursor + let has_render = unsafe { + conn.get_extension_data(&mut xcb::ffi::render::xcb_render_id) + .map_or(false, |ext| ext.present()) + }; + if has_render { + if let Ok(vers) = xcb::render::query_version( + conn.conn(), + xcb::ffi::render::XCB_RENDER_MAJOR_VERSION, + xcb::ffi::render::XCB_RENDER_MINOR_VERSION, + ) + .get_reply() + { + // 0.5 and later have the required support + if (vers.major_version(), vers.minor_version()) >= (0, 5) { + size.replace(cursor_size(&conn.xrm)); + theme = conn.xrm.get("Xcursor.theme").cloned(); + + // Locate the Pictformat corresponding to ARGB32 + if let Ok(formats) = xcb::render::query_pict_formats(conn.conn()).get_reply() { + for fmt in formats.formats() { + if fmt.depth() == 32 { + let direct = fmt.direct(); + if direct.alpha_shift() == 24 + && direct.red_shift() == 16 + && direct.green_shift() == 8 + && direct.blue_shift() == 0 + { + pict_format_id.replace(fmt.id()); + break; + } + } + } + } + } + } + } + let icon_path = icon_path(); + Self { cursors: HashMap::new(), cursor: None, - conn, + conn: Rc::downgrade(conn), + size, + theme, + icon_path, + pict_format_id, } } @@ -49,43 +154,10 @@ impl CursorInfo { let cursor_id = match self.cursors.get(&cursor) { Some(cursor) => cursor.id, - None => { - let id_no = match cursor.unwrap_or(MouseCursor::Arrow) { - // `/usr/include/X11/cursorfont.h` - // - MouseCursor::Arrow => xcb_util::cursor::TOP_LEFT_ARROW, - MouseCursor::Hand => xcb_util::cursor::HAND1, - MouseCursor::Text => xcb_util::cursor::XTERM, - MouseCursor::SizeUpDown => xcb_util::cursor::SB_V_DOUBLE_ARROW, - MouseCursor::SizeLeftRight => xcb_util::cursor::SB_H_DOUBLE_ARROW, - }; - - let cursor_id: xcb::ffi::xcb_cursor_t = conn.generate_id(); - xcb::create_glyph_cursor( - &conn, - cursor_id, - conn.cursor_font_id, - conn.cursor_font_id, - id_no, - id_no + 1, - 0xffff, - 0xffff, - 0xffff, - 0, - 0, - 0, - ); - - self.cursors.insert( - cursor, - XcbCursor { - id: cursor_id, - conn: Rc::downgrade(&conn), - }, - ); - - cursor_id - } + None => match self.load_themed(&conn, cursor) { + Some(c) => c, + None => self.load_basic(&conn, cursor), + }, }; xcb::change_window_attributes(&conn, window_id, &[(xcb::ffi::XCB_CW_CURSOR, cursor_id)]); @@ -94,4 +166,309 @@ impl CursorInfo { Ok(()) } + + fn load_themed(&mut self, conn: &Rc, cursor: Option) -> Option { + let theme = self.theme.as_ref()?; + if self.pict_format_id.is_none() { + return None; + } + + let name = match cursor.unwrap_or(MouseCursor::Arrow) { + MouseCursor::Arrow => "top_left_arrow", + MouseCursor::Hand => "hand2", + MouseCursor::Text => "xterm", + MouseCursor::SizeUpDown => "sb_v_double_arrow", + MouseCursor::SizeLeftRight => "sb_h_double_arrow", + }; + + for dir in &self.icon_path { + let candidate = dir.join(theme).join("cursors").join(name); + if let Ok(file) = std::fs::File::open(&candidate) { + match self.parse_cursor_file(conn, file) { + Ok(cursor_id) => { + self.cursors.insert( + cursor, + XcbCursor { + id: cursor_id, + conn: Rc::downgrade(&conn), + }, + ); + + return Some(cursor_id); + } + Err(err) => log::error!("{:#}", err), + } + } + } + None + } + + fn load_basic(&mut self, conn: &Rc, cursor: Option) -> u32 { + let id_no = match cursor.unwrap_or(MouseCursor::Arrow) { + // `/usr/include/X11/cursorfont.h` + // + MouseCursor::Arrow => xcb_util::cursor::TOP_LEFT_ARROW, + MouseCursor::Hand => xcb_util::cursor::HAND1, + MouseCursor::Text => xcb_util::cursor::XTERM, + MouseCursor::SizeUpDown => xcb_util::cursor::SB_V_DOUBLE_ARROW, + MouseCursor::SizeLeftRight => xcb_util::cursor::SB_H_DOUBLE_ARROW, + }; + + let cursor_id: xcb::ffi::xcb_cursor_t = conn.generate_id(); + xcb::create_glyph_cursor( + &conn, + cursor_id, + conn.cursor_font_id, + conn.cursor_font_id, + id_no, + id_no + 1, + 0xffff, + 0xffff, + 0xffff, + 0, + 0, + 0, + ); + + self.cursors.insert( + cursor, + XcbCursor { + id: cursor_id, + conn: Rc::downgrade(&conn), + }, + ); + + cursor_id + } + + fn parse_cursor_file( + &self, + conn: &Rc, + mut file: std::fs::File, + ) -> anyhow::Result { + /* See: + * + * Cursor files start with a header. The header + * contains a magic number, a version number and a + * table of contents which has type and offset information + * for the remaining tables in the file. + * + * File minor versions increment for compatible changes + * File major versions increment for incompatible changes (never, we hope) + * + * Chunks of the same type are always upward compatible. Incompatible + * changes are made with new chunk types; the old data can remain under + * the old type. Upward compatible changes can add header data as the + * header lengths are specified in the file. + * + * File: + * FileHeader + * LISTofChunk + * + * FileHeader: + * CARD32 magic magic number + * CARD32 header bytes in file header + * CARD32 version file version + * CARD32 ntoc number of toc entries + * LISTofFileToc toc table of contents + * + * FileToc: + * CARD32 type entry type + * CARD32 subtype entry subtype (size for images) + * CARD32 position absolute file position + */ + + #[derive(Debug)] + struct FileHeader { + magic: u32, + header: u32, + version: u32, + ntoc: u32, + } + const MAGIC: u32 = 0x72756358; + const IMAGE_TYPE: u32 = 0xfffd0002; + + #[derive(Debug)] + struct Toc { + type_: u32, + subtype: u32, + position: u32, + } + + /// Read a u32 that is stored in little endian format, + /// return in host byte order + fn read_u32(r: &mut dyn Read) -> anyhow::Result { + let mut u32buf = [0u8; 4]; + r.read_exact(&mut u32buf)?; + Ok(u32::from_le_bytes(u32buf)) + } + + let header = FileHeader { + magic: read_u32(&mut file)?, + header: read_u32(&mut file)?, + version: read_u32(&mut file)?, + ntoc: read_u32(&mut file)?, + }; + ensure!( + header.magic == MAGIC, + "magic number doesn't match 0x{:x} != expected 0x{:x}", + header.magic, + MAGIC + ); + + let mut toc = vec![]; + for _ in 0..header.ntoc { + toc.push(Toc { + type_: read_u32(&mut file)?, + subtype: read_u32(&mut file)?, + position: read_u32(&mut file)?, + }); + } + + ensure!(!toc.is_empty(), "no images are present"); + + let size = self.size.unwrap_or(24) as isize; + let mut best = None; + for item in &toc { + if item.type_ != IMAGE_TYPE { + continue; + } + let distance = ((item.subtype as isize) - size).abs(); + match best.take() { + None => { + best.replace((item, distance)); + } + Some((other_item, other_dist)) => { + best.replace(if distance < other_dist { + (item, distance) + } else { + (other_item, other_dist) + }); + } + } + } + + let item = best + .take() + .ok_or_else(|| anyhow::anyhow!("no matching images"))? + .0; + + file.seek(SeekFrom::Start(item.position.into()))?; + + let _chunk_header = read_u32(&mut file)?; + let chunk_type = read_u32(&mut file)?; + let chunk_subtype = read_u32(&mut file)?; + let _chunk_version = read_u32(&mut file)?; + + ensure!( + chunk_type == item.type_, + "chunk_type {:x} != item.type_ {:x}", + chunk_type, + item.type_ + ); + ensure!( + chunk_subtype == item.subtype, + "chunk_subtype {:x} != item.subtype {:x}", + chunk_subtype, + item.subtype + ); + + let width = read_u32(&mut file)?; + let height = read_u32(&mut file)?; + let xhot = read_u32(&mut file)?; + let yhot = read_u32(&mut file)?; + let _delay = read_u32(&mut file)?; + + let num_pixels = (width as usize) * (height as usize); + ensure!( + num_pixels < u32::max_value() as usize, + "cursor image is larger than fits in u32" + ); + + let mut pixels = vec![0u8; num_pixels * 4]; + file.read_exact(&mut pixels)?; + + // The data is all little endian; convert to host order + for chunk in pixels.chunks_exact_mut(4) { + let mut data = [0u8; 4]; + data.copy_from_slice(chunk); + let le = u32::from_le_bytes(data); + data = le.to_le_bytes(); + chunk.copy_from_slice(&data); + } + + let image = unsafe { + xcb_util::ffi::image::xcb_image_create_native( + conn.conn().get_raw_conn(), + width.try_into()?, + height.try_into()?, + xcb::xproto::IMAGE_FORMAT_Z_PIXMAP, + 32, + std::ptr::null_mut(), + pixels.len() as u32, + pixels.as_mut_ptr(), + ) + }; + ensure!(!image.is_null(), "failed to create native image"); + + let pixmap = conn.generate_id(); + xcb::xproto::create_pixmap_checked( + conn, + 32, + pixmap, + conn.root, + width as u16, + height as u16, + ) + .request_check() + .context("create_pixmap")?; + + let gc = conn.generate_id(); + xcb::create_gc(conn.conn(), gc, pixmap, &[]); + + unsafe { + xcb_util::ffi::image::xcb_image_put( + conn.conn().get_raw_conn(), + pixmap, + gc, + image, + 0, + 0, + 0, + ) + }; + + xcb::free_gc(conn.conn(), gc); + + let pic = conn.generate_id(); + xcb::render::create_picture_checked( + conn.conn(), + pic, + pixmap, + self.pict_format_id.unwrap(), + &[], + ) + .request_check() + .context("create_picture")?; + + xcb::xproto::free_pixmap(conn.conn(), pixmap); + + let cursor_id: xcb::ffi::xcb_cursor_t = conn.generate_id(); + xcb::render::create_cursor_checked( + conn.conn(), + cursor_id, + pic, + xhot.try_into()?, + yhot.try_into()?, + ) + .request_check() + .context("create_cursor")?; + + xcb::render::free_picture(conn.conn(), pic); + unsafe { + xcb_util::ffi::image::xcb_image_destroy(image); + } + + Ok(cursor_id) + } } diff --git a/window/src/os/x11/mod.rs b/window/src/os/x11/mod.rs index 1a1dc3ffd..281575c6c 100644 --- a/window/src/os/x11/mod.rs +++ b/window/src/os/x11/mod.rs @@ -4,6 +4,7 @@ pub mod connection; pub mod cursor; pub mod keyboard; pub mod window; +pub mod xrm; pub use self::window::*; pub use bitmap::*; diff --git a/window/src/os/x11/window.rs b/window/src/os/x11/window.rs index e2019c6db..461707466 100644 --- a/window/src/os/x11/window.rs +++ b/window/src/os/x11/window.rs @@ -718,7 +718,7 @@ impl XWindow { expose: VecDeque::new(), paint_all: true, copy_and_paste: CopyAndPaste::default(), - cursors: CursorInfo::new(Rc::downgrade(&conn)), + cursors: CursorInfo::new(&conn), gl_state: None, config: Arc::clone(&config), })) diff --git a/window/src/os/x11/xrm.rs b/window/src/os/x11/xrm.rs new file mode 100644 index 000000000..e44ba3613 --- /dev/null +++ b/window/src/os/x11/xrm.rs @@ -0,0 +1,34 @@ +use std::collections::HashMap; + +/// Parses: +/// $ xprop -root | grep RESOURCE_MANAGER +/// RESOURCE_MANAGER(STRING) = "Xft.dpi:\t96\nXft.hinting:\t1\nXft.hintstyle:\thintslight\nXft.antialias:\t1\nXft.rgba:\tnone\nXcursor.size:\t24\nXcursor.theme:\tAdwaita\n" +pub fn parse_root_resource_manager( + conn: &xcb::Connection, + root: xcb::xproto::Window, +) -> anyhow::Result> { + let reply = xcb::xproto::get_property( + conn, + false, + root, + xcb::ffi::XCB_ATOM_RESOURCE_MANAGER, + xcb::xproto::ATOM_STRING, + 0, + 1024 * 1024, + ) + .get_reply()?; + + let text = String::from_utf8_lossy(reply.value::()); + let mut map = HashMap::new(); + for line in text.split('\n') { + if let Some(colon) = line.find(':') { + let (key, value) = line.split_at(colon); + let key = key.trim(); + let value = value[1..].trim(); + + map.insert(key.to_string(), value.to_string()); + } + } + + Ok(map) +}