diff --git a/src/main.rs b/src/main.rs index d40c1ca..85c0088 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,12 @@ use anyhow::Result; use std::thread; use std::time::Duration; -use x11rb::wrapper::ConnectionExt as _; -use x11rb::{ - connection::Connection, - protocol::xproto::{ - AtomEnum, ChangeWindowAttributesAux, CloseDown, ConnectionExt, CreateGCAux, EventMask, - ImageFormat, PropMode, - }, - rust_connection::RustConnection, -}; + mod args; mod config; mod settings; mod wallpaper; +mod x11; fn main() -> Result<()> { // Parse CLI arguments @@ -25,154 +18,25 @@ fn main() -> Result<()> { // Resolve wallpaper settings let wallpaper_settings = settings::resolve_wallpaper(&args, config)?; - // Connect to the running graphical session, the X11 server. - let (conn, screen_num) = RustConnection::connect(None)?; - // Select the current screen. - let screen = &conn.setup().roots[screen_num]; - // Find the "root window," i.e., the desktop where you set the background. - let root = screen.root; - - // Short delay to avoid race conditions that can cause the WM to overwrite the wallpaper. - thread::sleep(Duration::from_millis(200)); - - // Ensure X11 resources created by this process remain valid after exit. - // To keep the wallpaper persistent after `icing` exits. - conn.set_close_down_mode(CloseDown::RETAIN_PERMANENT)?; - - // Atom (property) used to track ownership of the current wallpaper pixmap. - // This is read to find and clean up the previous wallpaper owner. - let atom_eset = conn.intern_atom(false, b"ESETROOT_PMAP_ID")?.reply()?.atom; - - // Remove any previously registered wallpaper pixmap. - // This prevents resource leaks and forces consumers to refresh. - if let Ok(reply) = conn - // Look for previous background image pixmap. - .get_property(false, root, atom_eset, AtomEnum::PIXMAP, 0, 1)? - .reply() - // Get any existing old one, if it exists. - && let Some(old) = reply.value32().and_then(|mut v| v.next()) - { - // Kill the client that owns the old wallpaper pixmap. - // Allows X server to free old pixmap and forces clients to refresh if cached. - conn.kill_client(old)?; - } - - // Get screen dimensions and color depth - let width = screen.width_in_pixels; - let height = screen.height_in_pixels; - let depth = screen.root_depth; - - // Create a pixmap (image) matching the screen size and depth. - // This pixmap will become the desktop background. - let pixmap = conn.generate_id()?; - conn.create_pixmap(depth, pixmap, root, width, height)?; + // Connect to X11 + let session = x11::X11Session::connect()?; // Prepare wallpaper image (scaling + pixel format conversion) let prepared = wallpaper::prepare_wallpaper( &wallpaper_settings.path, wallpaper_settings.mode, - width.into(), - height.into(), + session.width.into(), // screen width + session.height.into(), // screen height )?; - // Create a Graphics Context (GC). - // A GC is required by X11 for image uploads, even though most of its - // drawing settings are unused when using put_image. - let gc = conn.generate_id()?; - conn.create_gc(gc, pixmap, &CreateGCAux::new())?; + // Clean up any previous wallpaper + session.cleanup_previous_wallpaper()?; - // Upload the image pixels into the pixmap. - // This writes the final wallpaper image into server memory. - // No scaling or color conversion happens here; the data must already be correct. - conn.put_image( - ImageFormat::Z_PIXMAP, - pixmap, // drawable: Destination pixmap (wallpaper image) - gc, // Graphics Context (required by X11) - prepared.width as u16, - prepared.height as u16, // width/height: size of the uploaded image region in pixels - prepared.offset_x as i16, - prepared.offset_y as i16, // dst_x/dst_y: Destination offset inside the pixmap (top-left corner) - 0, // left_pad: legacy bitmap padding, always 0 for modern images - depth, // depth: must match the root window’s depth (usually 24 or 32) - &prepared.pixels, // data: raw BGRA/BGRX pixel buffer - )?; + // Upload image to X11 + let pixmap = session.upload_wallpaper(&prepared)?; - // Grab and lock the X server to prevent other clients from observing or reacting to intermediate state. - // Prevents the WM or other apps from acting (repainting, caching, etc.) while the background is being updated. - conn.grab_server()?; - - // Temporarily suppress root window event notifications to the WM until the process is done. - // Prevent the WM from reacting mid-update, reading outdated data, repainting the background, or ignoring the final background. - conn.change_window_attributes( - root, - &ChangeWindowAttributesAux::new().event_mask(EventMask::NO_EVENT), - )?; - - // Core operation: assign the pixmap as the root window's background. Aka set the wallpaper. - conn.change_window_attributes( - root, - &ChangeWindowAttributesAux::new().background_pixmap(pixmap), - )?; - - // Request the root window to redraw itself using its current background pixmap. - // "Clear area": Historical X11 terminology stuck without name change. - // Original meaning: "Erase drawn content on top, restore the window's (historically plain) background." - // Current meaning, practically: "Fill the region using the window's current background (color or pixmap)." - conn.clear_area( - false, // Exposures: false. Do not generate expose events (redraw requests) for clients; avoids waking other clients during the update. - root, // Window: The desktop window (root). - 0, 0, // x,y: Start at the top-left corner. - 0, 0, // Width, height: 0 means "the entire window." - )?; - - // Atoms (properties) used by WMs, compositors, and client apps (e.g., for transparency effects) to locate and reuse the wallpaper pixmap. - // There's no standard. Different apps rely on different properties: `_XROOTPMAP_ID`, `ESETROOT_PMAP_ID`, & `_XSETROOT_ID`. Better use all 3. - let atom_root = conn.intern_atom(false, b"_XROOTPMAP_ID")?.reply()?.atom; - let atom_setroot = conn.intern_atom(false, b"_XSETROOT_ID")?.reply()?.atom; - - // Set root window properties that publish the wallpaper pixmap location to other clients. - conn.change_property32( - PropMode::REPLACE, - root, - atom_root, // _XROOTPMAP_ID: For discovery. - AtomEnum::PIXMAP, - &[pixmap], - )?; - conn.change_property32( - PropMode::REPLACE, - root, - atom_eset, // ESETROOT_PMAP_ID: For discovery and ownership / lifecycle. - AtomEnum::PIXMAP, - &[pixmap], - )?; - conn.change_property32( - PropMode::REPLACE, - root, - atom_setroot, // _XSETROOT_ID: For discovery, but legacy. - AtomEnum::PIXMAP, - &[pixmap], - )?; - - // Restore root window event notifications required by the window manager. - // Without these, the WM cannot track window creation, layout changes, or property updates, which breaks normal window management. - conn.change_window_attributes( - root, - &ChangeWindowAttributesAux::new().event_mask( - // Allows the WM to control and manage child windows (positioning, tiling, etc.). - EventMask::SUBSTRUCTURE_REDIRECT - // Notifies the WM about changes to managed windows (open, close, move, etc.). - | EventMask::SUBSTRUCTURE_NOTIFY - // Notifies listeners when root window properties change (like wallpaper). This is mainly what was disabled earlier. - | EventMask::PROPERTY_CHANGE, - ), - )?; - - // Release the server lock and allow normal processing to resume, since the background is set. - conn.ungrab_server()?; - - // Send all required updates in queue to X11, refresh immediately. - // Ensure the requests are sent and everything's done. - conn.flush()?; + // Apply it as the root background + session.apply_wallpaper(pixmap)?; // Short delay to ensure the server processes all requests before `icing` exits. thread::sleep(Duration::from_millis(50)); diff --git a/src/wallpaper.rs b/src/wallpaper.rs index 9344889..175934d 100644 --- a/src/wallpaper.rs +++ b/src/wallpaper.rs @@ -39,7 +39,7 @@ pub struct PreparedWallpaper { /// /// Image libraries produce RGBA, but X11 `TrueColor` visuals typically use /// BGRX/BGRA byte order on little-endian systems. -/// Swap red and blue channels to match the server’s native pixel layout. +/// Swap red and blue channels to match the server's native pixel layout. /// Without this conversion, colors appear incorrect. fn rgba_to_bgra(mut pixels: Vec) -> Vec { for px in pixels.chunks_exact_mut(4) { @@ -127,9 +127,9 @@ pub fn prepare_wallpaper( .with_context(|| format!("Failed to open image: {}", image_path.display()))? .to_rgba8(); - // Scaling mode + // Scaling mode let prepared = match mode { - ScalingMode::Fill => prepare_fill(&img, screen_width, screen_height), // Default + ScalingMode::Fill => prepare_fill(&img, screen_width, screen_height), // Default ScalingMode::Stretch => prepare_stretch(&img, screen_width, screen_height), }; diff --git a/src/x11.rs b/src/x11.rs new file mode 100644 index 0000000..55bcf35 --- /dev/null +++ b/src/x11.rs @@ -0,0 +1,196 @@ +use anyhow::Result; +use std::{thread, time::Duration}; + +use x11rb::{ + connection::Connection, + protocol::xproto::{ + AtomEnum, ChangeWindowAttributesAux, CloseDown, ConnectionExt, CreateGCAux, EventMask, + ImageFormat, PropMode, + }, + rust_connection::RustConnection, + wrapper::ConnectionExt as _, +}; + +use crate::wallpaper::PreparedWallpaper; + +/// Thin wrapper around an X11 root-window session. +/// +/// Owns the X11 connection and all data needed to +/// safely update the root window background. +pub struct X11Session { + conn: RustConnection, + pub root: u32, + pub depth: u8, + pub width: u16, + pub height: u16, + atom_eset: u32, +} + +impl X11Session { + /// Connect to X11 and initialize the root window session. + pub fn connect() -> Result { + // Connect to the running graphical session, the X11 server. + let (conn, screen_num) = RustConnection::connect(None)?; + // Select the current screen. + let screen = &conn.setup().roots[screen_num]; + + // Copy what you need out of `screen` (To avoid dangling reference) + // Find the "root window," i.e., the desktop where you set the background. + let root = screen.root; + // Get screen dimensions and color depth + let depth = screen.root_depth; + let width = screen.width_in_pixels; + let height = screen.height_in_pixels; + + // Short delay to avoid race conditions that can cause the WM to overwrite the wallpaper. + thread::sleep(Duration::from_millis(200)); + + // Ensure X11 resources created by this process remain valid after exit. + // To keep the wallpaper persistent after `icing` exits. + conn.set_close_down_mode(CloseDown::RETAIN_PERMANENT)?; + + // Atom (property) used to track ownership of the current wallpaper pixmap. + // This is read to find and clean up the previous wallpaper owner. + let atom_eset = conn.intern_atom(false, b"ESETROOT_PMAP_ID")?.reply()?.atom; + + Ok(Self { + conn, + root, + depth, + width, + height, + atom_eset, + }) + } + + /// Remove any previously registered wallpaper pixmap. + /// + /// Prevents resource leaks and forces clients to refresh. + pub fn cleanup_previous_wallpaper(&self) -> Result<()> { + if let Ok(reply) = self + .conn + // Look for previous background image pixmap. + .get_property(false, self.root, self.atom_eset, AtomEnum::PIXMAP, 0, 1)? + .reply() + // Get any existing old one, if it exists. + && let Some(old) = reply.value32().and_then(|mut v| v.next()) + { + // Kill the client that owns the old wallpaper pixmap. + // Allows X server to free old pixmap and forces clients to refresh if cached. + self.conn.kill_client(old)?; + } + + Ok(()) + } + + /// Upload prepared image data into a new pixmap. + pub fn upload_wallpaper(&self, prepared: &PreparedWallpaper) -> Result { + // Create a pixmap (image) matching the screen size and depth. + // This pixmap will become the desktop background. + let pixmap = self.conn.generate_id()?; + self.conn + .create_pixmap(self.depth, pixmap, self.root, self.width, self.height)?; + + // Create a Graphics Context (GC). + // A GC is required by X11 for image uploads, even though most of its + // drawing settings are unused when using put_image. + let gc = self.conn.generate_id()?; + self.conn.create_gc(gc, pixmap, &CreateGCAux::new())?; + + // Upload the image pixels into the pixmap. + // This writes the final wallpaper image into server memory. + // No scaling or color conversion happens here; the data must already be correct. + self.conn.put_image( + ImageFormat::Z_PIXMAP, + pixmap, // drawable: Destination pixmap (wallpaper image) + gc, // Graphics Context (required by X11) + prepared.width as u16, + prepared.height as u16, // width/height: size of the uploaded image region in pixels + prepared.offset_x as i16, + prepared.offset_y as i16, // dst_x/dst_y: Destination offset inside the pixmap (top-left corner) + 0, // left_pad: legacy bitmap padding, always 0 for modern images + self.depth, // depth: must match the root window's depth (usually 24 or 32) + &prepared.pixels, // data: raw BGRA/BGRX pixel buffer + )?; + + Ok(pixmap) + } + + /// Apply the pixmap as the root window background and notify clients. + pub fn apply_wallpaper(&self, pixmap: u32) -> Result<()> { + // Grab and lock the X server to prevent other clients from observing or reacting to intermediate state. + // Prevents the WM or other apps from acting (repainting, caching, etc.) while the background is being updated. + self.conn.grab_server()?; + + // Temporarily suppress root window event notifications to the WM until the process is done. + // Prevent the WM from reacting mid-update, reading outdated data, repainting the background, or ignoring the final background. + self.conn.change_window_attributes( + self.root, + &ChangeWindowAttributesAux::new().event_mask(EventMask::NO_EVENT), + )?; + + // Core operation: assign the pixmap as the root window's background. Aka set the wallpaper. + self.conn.change_window_attributes( + self.root, + &ChangeWindowAttributesAux::new().background_pixmap(pixmap), + )?; + + // Request the root window to redraw itself using its current background pixmap. + // "Clear area": Historical X11 terminology stuck without name change. + // Original meaning: "Erase drawn content on top, restore the window's (historically plain) background." + // Current meaning, practically: "Fill the region using the window's current background (color or pixmap)." + self.conn.clear_area( + false, // Exposures: false. Do not generate expose events (redraw requests) for clients; avoids waking other clients during the update. + self.root, // Window: The desktop window (root). + 0, 0, // x,y: Start at the top-left corner. + 0, 0, // Width, height: 0 means "the entire window." + )?; + + // Atoms (properties) used by WMs, compositors, and client apps (e.g., for transparency effects) to locate and reuse the wallpaper pixmap. + // There's no standard. Different apps rely on different properties: `_XROOTPMAP_ID`, `ESETROOT_PMAP_ID`, & `_XSETROOT_ID`. Better use all 3. + let atom_root = self + .conn + .intern_atom(false, b"_XROOTPMAP_ID")? + .reply()? + .atom; + let atom_setroot = self.conn.intern_atom(false, b"_XSETROOT_ID")?.reply()?.atom; + + // Set root window properties that publish the wallpaper pixmap location to other clients. + for atom in [ + atom_root, // _XROOTPMAP_ID: For discovery. + self.atom_eset, // ESETROOT_PMAP_ID: For discovery and ownership / lifecycle. + atom_setroot, // _XSETROOT_ID: For discovery, but legacy. + ] { + self.conn.change_property32( + PropMode::REPLACE, + self.root, + atom, + AtomEnum::PIXMAP, + &[pixmap], + )?; + } + + // Restore root window event notifications required by the window manager. + // Without these, the WM cannot track window creation, layout changes, or property updates, which breaks normal window management. + self.conn.change_window_attributes( + self.root, + &ChangeWindowAttributesAux::new().event_mask( + // Allows the WM to control and manage child windows (positioning, tiling, etc.). + EventMask::SUBSTRUCTURE_REDIRECT + // Notifies the WM about changes to managed windows (open, close, move, etc.). + | EventMask::SUBSTRUCTURE_NOTIFY + // Notifies listeners when root window properties change (like wallpaper). This is mainly what was disabled earlier. + | EventMask::PROPERTY_CHANGE, + ), + )?; + + // Release the server lock and allow normal processing to resume, since the background is set. + self.conn.ungrab_server()?; + + // Send all required updates in queue to X11, refresh immediately. + // Ensure the requests are sent and everything's done. + self.conn.flush()?; + + Ok(()) + } +}