Abstract and encapsulated x11 logic into module
- Isolate x11 code into `x11.rs` - `main()` is now just an orchestrator of logic
This commit is contained in:
parent
c2fac51705
commit
efbf5dc7f4
160
src/main.rs
160
src/main.rs
@ -1,19 +1,12 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::Duration;
|
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 args;
|
||||||
mod config;
|
mod config;
|
||||||
mod settings;
|
mod settings;
|
||||||
mod wallpaper;
|
mod wallpaper;
|
||||||
|
mod x11;
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
// Parse CLI arguments
|
// Parse CLI arguments
|
||||||
@ -25,154 +18,25 @@ fn main() -> Result<()> {
|
|||||||
// Resolve wallpaper settings
|
// Resolve wallpaper settings
|
||||||
let wallpaper_settings = settings::resolve_wallpaper(&args, config)?;
|
let wallpaper_settings = settings::resolve_wallpaper(&args, config)?;
|
||||||
|
|
||||||
// Connect to the running graphical session, the X11 server.
|
// Connect to X11
|
||||||
let (conn, screen_num) = RustConnection::connect(None)?;
|
let session = x11::X11Session::connect()?;
|
||||||
// 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)?;
|
|
||||||
|
|
||||||
// Prepare wallpaper image (scaling + pixel format conversion)
|
// Prepare wallpaper image (scaling + pixel format conversion)
|
||||||
let prepared = wallpaper::prepare_wallpaper(
|
let prepared = wallpaper::prepare_wallpaper(
|
||||||
&wallpaper_settings.path,
|
&wallpaper_settings.path,
|
||||||
wallpaper_settings.mode,
|
wallpaper_settings.mode,
|
||||||
width.into(),
|
session.width.into(), // screen width
|
||||||
height.into(),
|
session.height.into(), // screen height
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Create a Graphics Context (GC).
|
// Clean up any previous wallpaper
|
||||||
// A GC is required by X11 for image uploads, even though most of its
|
session.cleanup_previous_wallpaper()?;
|
||||||
// drawing settings are unused when using put_image.
|
|
||||||
let gc = conn.generate_id()?;
|
|
||||||
conn.create_gc(gc, pixmap, &CreateGCAux::new())?;
|
|
||||||
|
|
||||||
// Upload the image pixels into the pixmap.
|
// Upload image to X11
|
||||||
// This writes the final wallpaper image into server memory.
|
let pixmap = session.upload_wallpaper(&prepared)?;
|
||||||
// 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
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Grab and lock the X server to prevent other clients from observing or reacting to intermediate state.
|
// Apply it as the root background
|
||||||
// Prevents the WM or other apps from acting (repainting, caching, etc.) while the background is being updated.
|
session.apply_wallpaper(pixmap)?;
|
||||||
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()?;
|
|
||||||
|
|
||||||
// Short delay to ensure the server processes all requests before `icing` exits.
|
// Short delay to ensure the server processes all requests before `icing` exits.
|
||||||
thread::sleep(Duration::from_millis(50));
|
thread::sleep(Duration::from_millis(50));
|
||||||
|
|||||||
@ -39,7 +39,7 @@ pub struct PreparedWallpaper {
|
|||||||
///
|
///
|
||||||
/// Image libraries produce RGBA, but X11 `TrueColor` visuals typically use
|
/// Image libraries produce RGBA, but X11 `TrueColor` visuals typically use
|
||||||
/// BGRX/BGRA byte order on little-endian systems.
|
/// 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.
|
/// Without this conversion, colors appear incorrect.
|
||||||
fn rgba_to_bgra(mut pixels: Vec<u8>) -> Vec<u8> {
|
fn rgba_to_bgra(mut pixels: Vec<u8>) -> Vec<u8> {
|
||||||
for px in pixels.chunks_exact_mut(4) {
|
for px in pixels.chunks_exact_mut(4) {
|
||||||
|
|||||||
196
src/x11.rs
Normal file
196
src/x11.rs
Normal file
@ -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<Self> {
|
||||||
|
// 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<u32> {
|
||||||
|
// 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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user