Basic window capturing
- Capture current window - First step before implementing "click-to-select" window - `fmt` and `clippy` fixes
This commit is contained in:
parent
60fe3bf12c
commit
31900b4a17
40
src/core.rs
40
src/core.rs
@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,28 +48,36 @@ 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.
|
||||||
@ -92,11 +102,11 @@ pub fn capture_rect(ctx: &XContext, x: u16, y: u16, w: u16, h: u16) -> anyhow::R
|
|||||||
Ok(out)
|
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
|
/// `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)?;
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user