Full-screen screenshot
- Working `bazooka-full` binary from `fullscreen.rs` module
This commit is contained in:
parent
0c0715339e
commit
5a125a5993
7
.gitignore
vendored
7
.gitignore
vendored
@ -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
|
||||
|
||||
26
Cargo.toml
Normal file
26
Cargo.toml
Normal file
@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "bazooka"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
description = "Experimental minimalist screenshot tool for x11"
|
||||
authors = ["candifloss <candifloss.cc>"]
|
||||
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"
|
||||
104
src/core.rs
Normal file
104
src/core.rs
Normal file
@ -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<Self> {
|
||||
// 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<Vec<u8>> {
|
||||
// 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(())
|
||||
}
|
||||
38
src/fullscreen.rs
Normal file
38
src/fullscreen.rs
Normal file
@ -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(())
|
||||
}
|
||||
3
src/region.rs
Normal file
3
src/region.rs
Normal file
@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
println!("Hello");
|
||||
}
|
||||
3
src/window.rs
Normal file
3
src/window.rs
Normal file
@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
println!("Hello");
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user