Basic window capturing

- Capture current window
- First step before implementing "click-to-select" window
- `fmt` and `clippy` fixes
This commit is contained in:
Candifloss 2025-12-08 15:19:04 +05:30
parent 60fe3bf12c
commit 31900b4a17
2 changed files with 103 additions and 17 deletions

View File

@ -14,7 +14,9 @@ use std::fs::File;
pub struct XContext { pub struct XContext {
pub conn: RustConnection, pub conn: RustConnection,
pub root: u32, pub root: u32,
#[allow(dead_code)]
pub width: u16, pub width: u16,
#[allow(dead_code)]
pub height: u16, pub height: u16,
} }
@ -48,26 +50,34 @@ impl XContext {
/// Capture a rectangular part of the screen. /// Capture a rectangular part of the screen.
/// ///
/// `ctx` connection + screen info /// `ctxt` connection + screen info\
/// `x,y` top-left corner of the rectangle /// `x_coord,y_coord` top-left corner of the rectangle\
/// `w,h` size of the rectangle /// `w,h` size of the rectangle
/// ///
/// Returns: a `Vec<u8>` containing RGBA pixel data. /// Returns: a `Vec<u8>` containing RGBA pixel data.
pub fn capture_rect(ctx: &XContext, x: u16, y: u16, w: u16, h: u16) -> anyhow::Result<Vec<u8>> { pub fn capture_rect(
ctxt: &XContext,
top_left_x: u16,
top_left_y: u16,
width: u16,
height: u16,
) -> anyhow::Result<Vec<u8>> {
// X11 uses the number 2 for "ZPixmap", which means: return full pixel data. // X11 uses the number 2 for "ZPixmap", which means: return full pixel data.
let format_zpixmap: u8 = 2; let format_zpixmap: u8 = 2;
let x_coord = i16::try_from(top_left_x).unwrap_or(0);
let y_coord = i16::try_from(top_left_y).unwrap_or(0);
// Ask X11 to send us the raw pixels in the requested rectangle. // Ask X11 to send us the raw pixels in the requested rectangle.
// This returns X11's native pixel format (usually B,G,R,unused). // This returns X11's native pixel format (usually B,G,R,unused).
let reply = ctx let reply = ctxt
.conn .conn
.get_image( .get_image(
format_zpixmap.into(), format_zpixmap.into(),
ctx.root, ctxt.root,
x as i16, x_coord,
y as i16, y_coord,
w, width,
h, height,
u32::MAX, u32::MAX,
)? )?
.reply()?; .reply()?;
@ -76,7 +86,7 @@ pub fn capture_rect(ctx: &XContext, x: u16, y: u16, w: u16, h: u16) -> anyhow::R
// Prepare a buffer for RGBA output. // Prepare a buffer for RGBA output.
// Each pixel = 4 bytes (R, G, B, A). // Each pixel = 4 bytes (R, G, B, A).
let mut out = Vec::with_capacity((w as usize) * (h as usize) * 4); let mut out = Vec::with_capacity((width as usize) * (height as usize) * 4);
// Convert each incoming pixel from X11's format (B, G, R, X) // Convert each incoming pixel from X11's format (B, G, R, X)
// into the common RGBA format that PNG expects. // into the common RGBA format that PNG expects.
@ -94,9 +104,9 @@ pub fn capture_rect(ctx: &XContext, x: u16, y: u16, w: u16, h: u16) -> anyhow::R
/// Save an RGBA pixel buffer as a PNG file. /// Save an RGBA pixel buffer as a PNG file.
/// ///
/// `path` where to write the file /// `path` where to write the file\
/// `width` image width in pixels /// `width` image width in pixels\
/// `height` image height in pixels /// `height` image height in pixels\
/// `rgba` pixel data in RGBA format /// `rgba` pixel data in RGBA format
pub fn save_png(path: &str, width: u16, height: u16, rgba: &[u8]) -> anyhow::Result<()> { pub fn save_png(path: &str, width: u16, height: u16, rgba: &[u8]) -> anyhow::Result<()> {
let file = File::create(path)?; let file = File::create(path)?;

View File

@ -1,3 +1,79 @@
fn main() { mod core;
println!("Hello");
use core::{XContext, capture_rect, save_png};
use chrono::Local;
use std::fs;
use x11rb::protocol::xproto::{AtomEnum, ConnectionExt, GetGeometryReply, Window};
fn main() -> anyhow::Result<()> {
// Connect to X11 and load screen info.
let ctx = XContext::new()?;
// Try to get the currently active window using EWMH (_NET_ACTIVE_WINDOW).
let window = get_active_window(&ctx).unwrap_or_else(|| {
eprintln!("Could not get active window. Falling back to input focus.");
get_focused_window(&ctx).unwrap_or(ctx.root)
});
// Read the size and position of that window.
let geom = get_window_geometry(&ctx, window)?;
let x = u16::try_from(geom.x.max(0)).unwrap();
let y = u16::try_from(geom.y.max(0)).unwrap();
// Capture only the window rectangle.
let img = capture_rect(&ctx, x, y, geom.width, geom.height)?;
// Build output path: ~/Pictures/Screenshots/window_TIMESTAMP.png
let timestamp = Local::now().format("%Y%m%d%H%M%S").to_string();
let mut path = dirs::home_dir().unwrap_or_else(|| ".".into());
path.push("Pictures");
path.push("Screenshots");
fs::create_dir_all(&path)?;
path.push(format!("window_{timestamp}.png"));
let path_str = path.to_string_lossy();
save_png(&path_str, geom.width, geom.height, &img)?;
println!("Saved {path_str}");
Ok(())
}
/// Ask the window manager for the active window using the EWMH protocol.
///
/// This works on most modern Linux desktops.
/// Returns None if the WM does not support this.
fn get_active_window(ctx: &XContext) -> Option<Window> {
let atom = ctx
.conn
.intern_atom(false, b"_NET_ACTIVE_WINDOW")
.ok()?
.reply()
.ok()?
.atom;
let reply = ctx
.conn
.get_property(false, ctx.root, atom, AtomEnum::WINDOW, 0, 1)
.ok()?
.reply()
.ok()?;
// The active window ID is stored as a 32-bit value.
reply.value32().and_then(|mut iter| iter.next())
}
/// Fallback method: get the window that currently has keyboard focus.
fn get_focused_window(ctx: &XContext) -> Option<Window> {
let reply = ctx.conn.get_input_focus().ok()?.reply().ok()?;
Some(reply.focus)
}
/// Read x, y, width, height of a window.
fn get_window_geometry(ctx: &XContext, win: Window) -> anyhow::Result<GetGeometryReply> {
let geom = ctx.conn.get_geometry(win)?.reply()?;
Ok(geom)
} }