diff --git a/src/core.rs b/src/core.rs index 131c312..54f9a82 100644 --- a/src/core.rs +++ b/src/core.rs @@ -14,7 +14,9 @@ use std::fs::File; pub struct XContext { pub conn: RustConnection, pub root: u32, + #[allow(dead_code)] pub width: u16, + #[allow(dead_code)] pub height: u16, } @@ -46,28 +48,36 @@ impl XContext { } } -/// Capture a rectangular part of the screen. +/// Capture a rectangular part of the screen. /// -/// `ctx` – connection + screen info -/// `x,y` – top-left corner of the rectangle +/// `ctxt` – connection + screen info\ +/// `x_coord,y_coord` – top-left corner of the rectangle\ /// `w,h` – size of the rectangle /// /// Returns: a `Vec` containing RGBA pixel data. -pub fn capture_rect(ctx: &XContext, x: u16, y: u16, w: u16, h: u16) -> anyhow::Result> { +pub fn capture_rect( + ctxt: &XContext, + top_left_x: u16, + top_left_y: u16, + width: u16, + height: u16, +) -> anyhow::Result> { // X11 uses the number 2 for "ZPixmap", which means: return full pixel data. 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. // This returns X11's native pixel format (usually B,G,R,unused). - let reply = ctx + let reply = ctxt .conn .get_image( format_zpixmap.into(), - ctx.root, - x as i16, - y as i16, - w, - h, + ctxt.root, + x_coord, + y_coord, + width, + height, u32::MAX, )? .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. // 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) // into the common RGBA format that PNG expects. @@ -92,11 +102,11 @@ pub fn capture_rect(ctx: &XContext, x: u16, y: u16, w: u16, h: u16) -> anyhow::R Ok(out) } -/// Save an RGBA pixel buffer as a PNG file. +/// Save an RGBA pixel buffer as a PNG file. /// -/// `path` – where to write the file -/// `width` – image width in pixels -/// `height` – image height in pixels +/// `path` – where to write the file\ +/// `width` – image width in pixels\ +/// `height` – image height in pixels\ /// `rgba` – pixel data in RGBA format pub fn save_png(path: &str, width: u16, height: u16, rgba: &[u8]) -> anyhow::Result<()> { let file = File::create(path)?; diff --git a/src/window.rs b/src/window.rs index 244f744..18c3193 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1,3 +1,79 @@ -fn main() { - println!("Hello"); +mod core; + +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 { + 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 { + 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 { + let geom = ctx.conn.get_geometry(win)?.reply()?; + Ok(geom) }