From 5a125a59933cfc3c6a1772dae9a7480bad8a09d4 Mon Sep 17 00:00:00 2001 From: Candifloss Date: Sun, 7 Dec 2025 16:47:30 +0530 Subject: [PATCH] Full-screen screenshot - Working `bazooka-full` binary from `fullscreen.rs` module --- .gitignore | 7 ++++ Cargo.toml | 26 ++++++++++++ src/core.rs | 104 ++++++++++++++++++++++++++++++++++++++++++++++ src/fullscreen.rs | 38 +++++++++++++++++ src/region.rs | 3 ++ src/window.rs | 3 ++ 6 files changed, 181 insertions(+) create mode 100644 Cargo.toml create mode 100644 src/core.rs create mode 100644 src/fullscreen.rs create mode 100644 src/region.rs create mode 100644 src/window.rs diff --git a/.gitignore b/.gitignore index ab951f8..b548918 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,10 @@ Cargo.lock # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + +# Added by me +test/ + +# Added by cargo + +/target diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..14dde68 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "bazooka" +version = "0.1.0" +edition = "2024" +description = "Experimental minimalist screenshot tool for x11" +authors = ["candifloss "] +readme = "README.md" + +[[bin]] +name = "bazooka-full" +path = "src/fullscreen.rs" + +[[bin]] +name = "bazooka-win" +path = "src/window.rs" + +[[bin]] +name = "bazooka-reg" +path = "src/region.rs" + +[dependencies] +anyhow = "1.0.100" +chrono = { version = "0.4.42", features = ["clock"] } +dirs = "6.0.0" +png = "0.18.0" +x11rb = "0.13.2" diff --git a/src/core.rs b/src/core.rs new file mode 100644 index 0000000..0b717a8 --- /dev/null +++ b/src/core.rs @@ -0,0 +1,104 @@ +use x11rb::connection::Connection; +use x11rb::protocol::xproto::ConnectionExt; +use x11rb::rust_connection::RustConnection; + +use png::{BitDepth, ColorType, Encoder}; +use std::fs::File; + +/// Holds an open X11 connection and basic screen geometry. +/// All screenshot binaries share this context. +pub struct XContext { + pub conn: RustConnection, + pub root: u32, // The root window of the screen (represents the whole desktop) + pub width: u16, // Screen width in pixels + pub height: u16, // Screen height in pixels +} + +impl XContext { + pub fn new() -> anyhow::Result { + // Establish a connection to the running X server. + // `screen_num` selects which screen we are connected to (multi-head setups). + let (conn, screen_num) = RustConnection::connect(None)?; + + // Extract the geometry information from the X11 setup. + // These fields are Copy, so borrowing ends immediately. + let screen = &conn.setup().roots[screen_num]; + let root = screen.root; + let width = screen.width_in_pixels; + let height = screen.height_in_pixels; + + Ok(Self { + conn, + root, + width, + height, + }) + } +} + +/// Capture the *entire* root window (full screen) into an RGBA8 buffer. +/// +/// X11 provides the `GetImage` request, which returns raw pixel data from a drawable. +/// We request the `ZPixmap` format, which returns packed pixel values. +/// +/// Many x11rb code generators include a named constant for `ZPixmap`. +/// If not available, its protocol value is simply `2`. +pub fn capture_root_image(ctx: &XContext) -> anyhow::Result> { + // ZPixmap corresponds to the X11 format that returns full pixel values. + // Using a literal here avoids dependency on generated constant names. + let format_zpixmap: u8 = 2; + + // Send the GetImage request for the entire screen. + let reply = ctx + .conn + .get_image( + format_zpixmap.into(), // ZPixmap format + ctx.root, // The root window (whole desktop) + 0, // X origin + 0, // Y origin + ctx.width, // Width to capture + ctx.height, // Height to capture + u32::MAX, // Plane mask: capture all pixel planes + )? + .reply()?; + + let data = reply.data; + let depth = reply.depth; + + // Modern X11 desktops typically use depth 24 (RGB) or 32 (RGB with padding). + if depth != 24 && depth != 32 { + anyhow::bail!("Unsupported X11 depth: {depth}"); + } + + // X11 typically stores pixels in little-endian B,G,R,(unused) format. + // We convert it to standard RGBA because PNG expects that. + let mut out = Vec::with_capacity((ctx.width as usize) * (ctx.height as usize) * 4); + + for chunk in data.chunks_exact(4) { + let b = chunk[0]; + let g = chunk[1]; + let r = chunk[2]; + out.extend_from_slice(&[r, g, b, 0xFF]); // Always opaque + } + + Ok(out) +} + +/// Save a raw RGBA8 buffer as a PNG image at `path`. +/// +/// The png crate handles all compression and file formatting. +/// We simply specify: +/// - RGBA layout +/// - 8 bits per channel +pub fn save_png(path: &str, width: u16, height: u16, rgba: &[u8]) -> anyhow::Result<()> { + let file = File::create(path)?; + let mut encoder = Encoder::new(file, width.into(), height.into()); + encoder.set_color(ColorType::Rgba); + encoder.set_depth(BitDepth::Eight); + + let mut writer = encoder.write_header()?; + writer.write_image_data(rgba)?; + writer.finish()?; + + Ok(()) +} diff --git a/src/fullscreen.rs b/src/fullscreen.rs new file mode 100644 index 0000000..4bcfd5a --- /dev/null +++ b/src/fullscreen.rs @@ -0,0 +1,38 @@ +mod core; + +use chrono::Local; +use core::{XContext, capture_root_image, save_png}; +use std::fs; + +fn main() -> anyhow::Result<()> { + // Establish connection to X11 and gather screen info. + let ctx = XContext::new()?; + + // Capture the entire screen as an RGBA8 buffer. + let img = capture_root_image(&ctx)?; + + // Build an output path: + // ~/Pictures/Screenshots/screenshot_YYYYMMDDHHMMSS.png + // + // Using timestamps avoids overwriting previous screenshots, + // and matches the behavior of many modern screenshot tools. + 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"); + + // Ensure the directory exists + fs::create_dir_all(&path)?; + + path.push(format!("screenshot_{timestamp}.png")); + + // Convert PathBuf → &str (safe because it’s UTF-8 on all Linux systems). + let path_str = path.to_string_lossy(); + + // Write the PNG file to disk. + save_png(&path_str, ctx.width, ctx.height, &img)?; + + println!("Saved {path_str}"); + Ok(()) +} diff --git a/src/region.rs b/src/region.rs new file mode 100644 index 0000000..244f744 --- /dev/null +++ b/src/region.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello"); +} diff --git a/src/window.rs b/src/window.rs new file mode 100644 index 0000000..244f744 --- /dev/null +++ b/src/window.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello"); +}