use std::sync::{atomic::AtomicPtr, Arc, Mutex}; use x11rb::{ connect, connection::Connection, protocol::{ xproto::{ self, Atom, AtomEnum, ChangeWindowAttributesAux, ClientMessageData, ClientMessageEvent, ConnectionExt, EventMask, Window, }, Event, }, rust_connection::RustConnection, }; use crate::{Bounds, UnderlayEvent}; const WM_NAME_BUFSIZE: u32 = 512; static TARGET: AtomicPtr>> = AtomicPtr::new(std::ptr::null_mut()); struct Target { title: String, wid: Window, is_focused: bool, is_destroyed: bool, is_fullscreen: bool, } struct AtomCollection { net_active_window: Atom, net_wm_name: Atom, utf8_string: Atom, net_wm_state: Atom, net_wm_state_fullscreen: Atom, // net_wm_state_skip_taskbar: Atom, // net_wm_state_skip_pager: Atom, } impl AtomCollection { fn new(conn: &RustConnection) -> Option { Some(Self { net_active_window: get_atom(conn, "_NET_ACTIVE_WINDOW")?, net_wm_name: get_atom(conn, "_NET_WM_NAME")?, utf8_string: get_atom(conn, "UTF8_STRING")?, net_wm_state: get_atom(conn, "_NET_WM_STATE")?, net_wm_state_fullscreen: get_atom(conn, "_NET_WM_STATE_FULLSCREEN")?, // net_wm_state_skip_taskbar: Self::_get(conn, "_NET_WM_STATE_SKIP_TASKBAR")?, // net_wm_state_skip_pager: Self::_get(conn, "_NET_WM_STATE_SKIP_PAGER")?, }) } } fn get_atom>(conn: &RustConnection, name: T) -> Option { Some( conn.intern_atom(false, name.into().as_bytes()) .ok()? .reply() .ok()? .atom, ) } //Not very pretty and a bit wasteful making a new connection and getting the atom every time.. pub fn focus_target() { unsafe { if let Ok(target) = (*TARGET.load(std::sync::atomic::Ordering::SeqCst)).lock() { if *target != x11rb::NONE { if let Ok((conn, _screen_num)) = connect(None) { let net_active_window = get_atom(&conn, "_NET_ACTIVE_WINDOW").unwrap(); let event = ClientMessageEvent { response_type: xproto::CLIENT_MESSAGE_EVENT, format: 32, sequence: 0, window: *target, type_: net_active_window, data: ClientMessageData::from([1, x11rb::CURRENT_TIME, 0, 0, 0]), }; if let Ok(cookie) = conn.send_event( false, conn.setup().roots[_screen_num].root, EventMask::SUBSTRUCTURE_NOTIFY | EventMask::SUBSTRUCTURE_REDIRECT, event, ) { if let Err(e) = cookie.check() { log::error!("Error focusing target: {e:?}"); } } conn.flush().ok(); } } } } } struct Runtime { conn: RustConnection, root: Window, target: Target, atoms: AtomCollection, underlay_tx: std::sync::mpsc::Sender, active: Window, } impl Runtime { fn get_active_window(&self) -> Option { let cookie = self .conn .get_property( false, self.root, self.atoms.net_active_window, AtomEnum::WINDOW, 0, 1, ) .ok()?; let reply = cookie.reply().ok()?; let mut value32_iter = reply.value32()?; value32_iter.next() } ///Using this instead to allow setting the globally accessible target too for focus_target. fn set_target(&mut self, wid: Window) { self.target.wid = wid; unsafe { if let Ok(mut target) = (*TARGET.load(std::sync::atomic::Ordering::Relaxed)).lock() { *target = wid; } } } fn get_title(&self, wid: Window) -> Option { if wid == x11rb::NONE { return None; } let cookie = self .conn .get_property( false, wid, self.atoms.net_wm_name, self.atoms.utf8_string, 0, WM_NAME_BUFSIZE, ) .ok()?; let reply = cookie.reply().ok()?; let data = reply.value8()?.collect(); String::from_utf8(data).ok() } fn get_content_bounds(&self, wid: Window) -> Option { let geometry_cookie = self.conn.get_geometry(wid).ok()?; let geometry_reply = geometry_cookie.reply().ok()?; let translation_cookie = self.conn.translate_coordinates(wid, self.root, 0, 0).ok()?; let translation_reply = translation_cookie.reply().ok()?; Some(Bounds { x: translation_reply.dst_x as i32, y: translation_reply.dst_y as i32, width: geometry_reply.width as i32, height: geometry_reply.height as i32, }) } fn is_fullscreen_window(&self, wid: Window) -> Option { let cookie = self .conn .get_property(false, wid, self.atoms.net_wm_state, AtomEnum::ATOM, 0, 0) .ok()?; let reply = cookie.reply().ok()?; let len = reply.value32()?.count(); let cookie = self .conn .get_property( false, wid, self.atoms.net_wm_state, AtomEnum::ATOM, 0, len as u32, ) .ok()?; let reply = cookie.reply().ok()?; let mut is_fullscreen = false; for state in reply.value32()? { is_fullscreen = state == self.atoms.net_wm_state_fullscreen; } Some(is_fullscreen) } fn handle_moveresize_xevent(&self) { if let Some(bounds) = self.get_content_bounds(self.target.wid) { self.underlay_tx .send(UnderlayEvent::MoveResize(bounds)) .ok(); } } fn handle_fullscreen_xevent(&mut self) { if let Some(is_fullscreen) = self.is_fullscreen_window(self.target.wid) { if self.target.is_fullscreen != is_fullscreen { self.target.is_fullscreen = is_fullscreen; self.underlay_tx .send(UnderlayEvent::X11FullscreenEvent { is_fullscreen }) .ok(); } } } fn check_and_handle_window(&mut self, wid: Window) { if self.target.wid != x11rb::NONE { if self.target.wid == wid { if !self.target.is_focused { self.target.is_focused = true; self.underlay_tx.send(UnderlayEvent::Focus).ok(); } return; } if self.target.is_focused { self.target.is_focused = false; self.underlay_tx.send(UnderlayEvent::Blur).ok(); } if self.target.is_destroyed { self.set_target(x11rb::NONE); self.target.is_destroyed = false; self.underlay_tx.send(UnderlayEvent::Detach).ok(); } } log::trace!( "got title: {}", self.get_title(wid).unwrap_or("ERROR".into()) ); match self.get_title(wid) { Some(title) => { if title != self.target.title { return; } } _ => return, } log::trace!("Found target!"); if self.target.wid != x11rb::NONE { self.conn .change_window_attributes( self.target.wid, &ChangeWindowAttributesAux::new().event_mask(EventMask::NO_EVENT), ) .ok(); } self.set_target(wid); self.conn .change_window_attributes( wid, &ChangeWindowAttributesAux::new() .event_mask(EventMask::PROPERTY_CHANGE | EventMask::STRUCTURE_NOTIFY), ) .ok(); if let Some(is_fullscreen) = self.is_fullscreen_window(self.target.wid) { if let Some(bounds) = self.get_content_bounds(self.target.wid) { if is_fullscreen != self.target.is_fullscreen { self.target.is_fullscreen = is_fullscreen; } self.underlay_tx .send(UnderlayEvent::Attach { bounds, has_access: None, is_fullscreen: Some(is_fullscreen), }) .ok(); self.target.is_focused = true; self.underlay_tx.send(UnderlayEvent::Focus).ok(); //Early return to not unset target wid return; } } self.set_target(x11rb::NONE); } fn handle_event(&mut self, event: Event) { match event { Event::DestroyNotify(event) => { log::trace!("Got DestroyNotify: {event:?}"); if event.window == self.target.wid { self.target.is_destroyed = true; self.check_and_handle_window(x11rb::NONE); } } Event::ConfigureNotify(event) => { log::trace!("Got ConfigureNotify: {event:?}"); if event.window == self.target.wid { self.handle_moveresize_xevent(); } } Event::PropertyNotify(event) => { log::trace!("Got PropertyNotify: {event:?}"); if event.window == self.root && event.atom == self.atoms.net_active_window { let old = self.active; self.active = self.get_active_window().unwrap_or(x11rb::NONE); if old != self.target.wid { self.conn .change_window_attributes( old, &ChangeWindowAttributesAux::new().event_mask(EventMask::NO_EVENT), ) .ok(); } if self.active != x11rb::NONE && self.active != self.target.wid { self.conn .change_window_attributes( self.active, &ChangeWindowAttributesAux::new() .event_mask(EventMask::PROPERTY_CHANGE), ) .ok(); } self.check_and_handle_window(self.active); } else if event.window == self.target.wid && event.atom == self.atoms.net_wm_state { self.handle_fullscreen_xevent(); } else if event.window == self.active && event.atom == self.atoms.net_wm_name { self.check_and_handle_window(self.active); } } _ => (), } } } pub fn init( window_title: String, tx: std::sync::mpsc::Sender, ) -> Result<(), String> { std::thread::spawn(move || { let (conn, screen_num) = match connect(None) { Ok(conn) => conn, Err(_) => { log::error!("Could not connect"); return; } }; let root = conn.setup().roots[screen_num].root; let atoms = match AtomCollection::new(&conn) { Some(atoms) => atoms, None => { log::error!("Could not initialize atoms"); return; } }; conn.change_window_attributes( root, &ChangeWindowAttributesAux::new().event_mask(EventMask::PROPERTY_CHANGE), ) .ok(); let mut runtime = Runtime { conn, root, target: Target { title: window_title, wid: x11rb::NONE, is_focused: false, is_destroyed: false, is_fullscreen: false, }, atoms, underlay_tx: tx, active: x11rb::NONE, }; runtime.active = runtime.get_active_window().unwrap_or(x11rb::NONE); if runtime.active != x11rb::NONE { runtime .conn .change_window_attributes( runtime.active, &ChangeWindowAttributesAux::new().event_mask(EventMask::PROPERTY_CHANGE), ) .ok(); runtime.check_and_handle_window(runtime.active); } runtime.conn.flush().ok(); TARGET.store( Box::into_raw(Box::new(Arc::new(Mutex::new(x11rb::NONE)))), std::sync::atomic::Ordering::Relaxed, ); log::trace!("Initialized, listning for events"); while let Ok(event) = runtime.conn.wait_for_event() { log::trace!("Got event"); runtime.handle_event(event); runtime.conn.flush().ok(); } }); Ok(()) }