A pluggable, multi-crate Rust workspace for taking screenshots of windowed applications or the entire desktop.
| Crate | Description |
|---|---|
miniscreenshot |
Core — Screenshot type, PNG / PPM / PGM encoding, Capture / CaptureAsync / MultiCapture traits |
miniscreenshot-softbuffer |
softbuffer integration + re-export. Enable the winit feature to re-export winit alongside softbuffer. |
miniscreenshot-wgpu |
wgpu texture readback + re-export |
miniscreenshot-wayland |
Wayland wlr-screencopy-v1 system capture + re-exports |
miniscreenshot-x11 |
X11 (XGetImage / MIT-SHM) system capture + re-exports |
miniscreenshot-portal |
XDG Desktop Portal (ashpd) system capture; works on GNOME, KDE, wlroots, and inside Flatpak/Snap |
miniscreenshot-skia |
skia-safe re-export + surface screenshot helper |
miniscreenshot-vello |
vello re-export + pixel readback support |
miniscreenshot-minifb |
minifb re-export + pixel buffer screenshot helper |
miniscreenshot-desktop |
Umbrella — auto-selects Wayland → X11 → Portal for "just take a screenshot" |
miniscreenshot-mcp |
MCP server — serves a screenshot tool over the Model Context Protocol (streamable HTTP) |
- Pluggable — each rendering backend is a separate crate. Applications depend only on what they use.
- No version conflicts — every driver crate re-exports its underlying
library (e.g.,
miniscreenshot_wgpu::wgpu). Depending on a driver crate is sufficient; no separatewgpu/winit/… dependency required. - Low-friction output formats — PNG (default), PPM and PGM are supported out of the box. Format is inferred from the file extension.
- System screenshots — Linux Wayland via
zwlr_screencopy_manager_v1(wlroots-based compositors: Sway, Hyprland, …), X11 viaXGetImagewith an MIT-SHM fast path, and XDG Desktop Portal viaashpd(GNOME, KDE, Flatpak, Snap).
[dependencies]
miniscreenshot = "0.3"use miniscreenshot::Screenshot;
// Build from raw RGBA8 pixel data
let data = vec![255u8, 0, 0, 255]; // 1×1 red pixel
let shot = Screenshot::from_rgba(1, 1, data);
// Save — format inferred from extension (.png / .ppm / .pgm)
shot.save("screenshot.png").unwrap();
// Or let a "screenshot key" handler name the file for you:
// writes screenshots/screenshot-YYYYMMDD-HHMMSS-mmm.png, returns the path.
let path = shot.save_in_dir_timestamped("screenshots").unwrap();
// Or encode to bytes explicitly
let png_bytes: Vec<u8> = shot.encode_png().unwrap();
let ppm_bytes: Vec<u8> = shot.encode_ppm(); // lossless, trivial format
let pgm_bytes: Vec<u8> = shot.encode_pgm(); // grayscale[dependencies]
miniscreenshot-desktop = "0.3"use miniscreenshot_desktop::take;
let shot = take().expect("screenshot");
shot.save("desktop.png").unwrap();The take() function auto-selects the best backend: Wayland → X11 → Portal.
[dependencies]
miniscreenshot-softbuffer = "0.3"use miniscreenshot_softbuffer::{softbuffer, capture};
// softbuffer stores pixels as u32 XRGB8888 values
let pixels: &[u32] = /* buffer.deref() from softbuffer */ &[];
let shot = capture(pixels, width, height);
shot.save("screenshot.png").unwrap();When you want to create a softbuffer::Surface from a winit::Window, enable
the winit feature. This re-exports winit alongside softbuffer at the same
version, avoiding dependency conflicts.
[dependencies]
miniscreenshot-softbuffer = { version = "0.3", features = ["winit"] }use miniscreenshot_softbuffer::winit::window::Window;
use miniscreenshot_softbuffer::softbuffer;
use std::rc::Rc;
// Rc<Window> implements the raw-handle traits softbuffer needs.
let window: Rc<Window> = /* create window */;
let ctx = softbuffer::Context::new(window.clone()).unwrap();
let surface = softbuffer::Surface::new(&ctx, window.clone()).unwrap();See the softbuffer_winit_scene_screenshot example for a complete demo.
[dependencies]
miniscreenshot-wgpu = "0.3"use miniscreenshot_wgpu::{wgpu, capture};
// `texture` must have been created with TextureUsages::COPY_SRC
let shot = capture(&device, &queue, &texture).unwrap();
shot.save("screenshot.png").unwrap();Capturing a frame you present: the swapchain/surface texture is acquired without
COPY_SRCand cannot be captured directly. Render your scene into an offscreen texture created withRENDER_ATTACHMENT | COPY_SRC,capturethat, then blit it to the surface to present. See thewgpu_scene_screenshotexample. From an async render loop, wrap the (blocking)capturecall intokio::task::spawn_blocking.
[dependencies]
miniscreenshot-wayland = "0.3"use miniscreenshot_wayland::WaylandCapture;
let mut cap = WaylandCapture::connect().expect("connect to Wayland");
println!("{} output(s) found", cap.output_count());
// Capture first monitor
let shot = cap.capture_output(0).expect("capture");
shot.save("screenshot.png").unwrap();
// Or capture all monitors at once
let shots = cap.capture_all().expect("capture all");Compositor requirements: Requires a Wayland compositor that implements
zwlr_screencopy_manager_v1(wlroots-based — Sway, Hyprland, weston, cage, labwc, …). GNOME-on-Wayland and KWin do not implement this protocol and will returnWaylandCaptureError::NoScreencopyManager. Useminiscreenshot-portalinstead on these compositors.
[dependencies]
miniscreenshot-x11 = "0.3"use miniscreenshot_x11::X11Capture;
let mut cap = X11Capture::connect().expect("connect to X11");
println!("{} screen(s) found", cap.screen_count());
// Capture first screen
let shot = cap.capture_screen(0).expect("capture");
shot.save("screenshot.png").unwrap();
// Or capture all screens at once
let shots = cap.capture_all().expect("capture all");Server requirements: Requires a reachable X11 server (
$DISPLAYset). Uses MIT-SHM when available for a fast-path capture; otherwise falls back to a plainXGetImagetransfer over the wire.
[dependencies]
miniscreenshot-portal = "0.3"Blocking usage (default):
use miniscreenshot_portal::PortalCapture;
let mut cap = PortalCapture::connect().expect("connect to portal");
let shot = cap.capture_interactive().expect("capture");
shot.save("screenshot.png").unwrap();Async usage:
[dependencies]
miniscreenshot-portal = { version = "0.3", default-features = false, features = ["tokio"] }use miniscreenshot_portal::PortalCapture;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut cap = PortalCapture::connect_async().await;
let shot = cap.capture_interactive_async().await?;
shot.save("screenshot.png")?;
Ok(())
}Portal requirements: Requires a running desktop session with
$XDG_RUNTIME_DIRand a portal implementation (xdg-desktop-portal+ a backend such asxdg-desktop-portal-gnome,-kde,-wlr, or-gtk). GNOME always shows a confirmation dialog; KDE and wlroots may or may not depending on backend policy. Works inside Flatpak and Snap sandboxes. Use this crate on GNOME or KWin instead ofminiscreenshot-wayland.
[dependencies]
miniscreenshot-mcp = "0.3"Embed it in your running game or editor so a coding agent can inspect the live
frame. Serve any Capture (or CaptureAsync) implementor over streamable HTTP:
use miniscreenshot_mcp::ScreenshotServer;
// `capture` can be any `Capture` — a closure over your wgpu frame, or a
// desktop / wayland / x11 / portal backend.
let capture = miniscreenshot_desktop::take;
let server = ScreenshotServer::new(capture);
server.serve().await?; // serves on http://127.0.0.1:8731/mcpThe server is meant to live inside the long-running process you want to inspect; the agent connects to it over HTTP. (That is why the transport is HTTP rather than stdio: a stdio server would be a fresh subprocess that cannot see your already-running game.)
It exposes a single screenshot tool. An agent can call it with no path to
just see the current frame inline:
{ "max_dimension": 768 }or with a path to also save it to disk:
{ "path": "/tmp/screenshot.png", "include_image": false }| Argument | Type | Default | Description |
|---|---|---|---|
path |
string |
(none) | Where to save the screenshot. Omit to return the image inline only — nothing is written to disk. |
format |
"png" | "ppm" | "pgm" |
null |
Explicit format override for the saved file (extension is inferred otherwise) |
include_image |
bool |
false |
Also return the image inline as base64 ImageContent. Forced on when path is omitted. |
max_dimension |
int |
1568 |
Cap the inline image's longest side (aspect preserved) to keep the response small. 0 sends it full-resolution. The saved file is always full-resolution. |
To serve your rendered frame rather than the desktop, give the server a
WgpuFrameTarget. It owns clones of your
wgpu device/queue and a swappable "current frame" texture, so it satisfies
Capture + Send + 'static and can live inside the server while your render loop
keeps drawing:
use miniscreenshot_mcp::ScreenshotServer;
use miniscreenshot_wgpu::WgpuFrameTarget;
// Render into an offscreen texture created with RENDER_ATTACHMENT | COPY_SRC.
let target = WgpuFrameTarget::new(device.clone(), queue.clone());
// Hand one clone to the server (runs on its own task)…
let server = ScreenshotServer::new(target.clone());
tokio::spawn(server.serve());
// …and publish your persistent offscreen texture once. A capture reads its
// *current* contents, so you only re-publish when you recreate it (on resize).
target.set_frame(frame_texture.clone());
loop {
// render_scene(&mut encoder, &view); // draws into frame_texture in place
queue.submit([encoder.finish()]);
}set_frame publishes the texture handle, not a snapshot — so publishing once
is enough; the loop keeps drawing into it and a capture reads the latest
contents. Calling it every frame is harmless (just a mutex + Arc bump) but
unnecessary. wgpu serializes GPU work on the queue, so the readback observes a
complete frame. See the wgpu_game_mcp_server example for a runnable headless
version.
use miniscreenshot_mcp::{ScreenshotServer, ServerConfig};
let config = ServerConfig {
ip: "0.0.0.0".parse().unwrap(),
port: 9000,
path: "/mcp".into(),
allowed_root: Some("/tmp/screenshots".into()), // path traversal guard
..Default::default()
};
let server = ScreenshotServer::with_config(capture, config);
server.serve().await?;For async-native backends like the portal with its async feature:
use miniscreenshot_mcp::AsyncScreenshotServer;
use miniscreenshot_portal::PortalCapture;
let capture = PortalCapture::connect_async().await;
let server = AsyncScreenshotServer::new(capture);
server.serve().await?;Point your MCP-capable agent at the running server. With Claude Code:
claude mcp add --transport http screenshot http://127.0.0.1:8731/mcpThe agent can then call the screenshot tool to see your game's current frame
as you iterate — pass a path when you also want the capture saved to disk.
[dependencies]
miniscreenshot-minifb = "0.3"use miniscreenshot_minifb::capture;
// minifb stores pixels as u32 in 0RGB8888 format
let pixels: &[u32] = /* buffer passed to Window::update_with_buffer() */ &[];
let shot = capture(pixels, width as u32, height as u32).unwrap();
shot.save("screenshot.png").unwrap();Full window example:
use miniscreenshot_minifb::minifb;
use miniscreenshot_minifb::capture;
fn main() {
let (width, height) = (640, 480);
let mut buffer = vec![0u32; width * height];
// Fill buffer with content...
for y in 0..height {
for x in 0..width {
let idx = y * width + x;
buffer[idx] = ((x as u32) << 16) | ((y as u32) << 8) | ((x ^ y) as u32 & 0xFF);
}
}
let mut window = minifb::Window::new(
"Demo",
width,
height,
minifb::WindowOptions::default(),
)
.expect("failed to create window");
window.update_with_buffer(&buffer, width, height).unwrap();
// Capture the displayed buffer as a Screenshot
let shot = capture(&buffer, width as u32, height as u32).unwrap();
shot.save("screenshot.png").unwrap();
println!("saved screenshot");
}All system-capture driver crates (-wayland, -x11, -portal) implement
the core Capture trait, making backends interchangeable:
use miniscreenshot::Capture;
fn take_and_save<C: Capture>(cap: &mut C)
where C::Error: std::fmt::Debug
{
let shot = cap.capture().unwrap();
shot.save("output.png").unwrap();
}A blanket implementation allows any FnMut() -> Result<Screenshot, E> to be
used as a Capture. This means free functions like miniscreenshot_wgpu::capture
can be used directly:
use miniscreenshot::Capture;
fn take_and_save<C: Capture>(cap: &mut C)
where C::Error: std::fmt::Debug
{
let shot = cap.capture().unwrap();
shot.save("output.png").unwrap();
}
// Usage with a wgpu closure:
let mut cap = || miniscreenshot_wgpu::capture(&device, &queue, &texture);
take_and_save(&mut cap);For backends with multiple outputs (monitors), the MultiCapture super-trait
provides source_count(), capture_index(), and capture_all():
use miniscreenshot::{Capture, MultiCapture};
fn capture_all_screens<C: MultiCapture>(cap: &mut C)
where C::Error: std::fmt::Debug
{
println!("{} screen(s) found", cap.source_count());
let shots = cap.capture_all().unwrap();
for (i, shot) in shots.iter().enumerate() {
shot.save(&format!("screen_{i}.png")).unwrap();
}
}# Winit (for softbuffer + winit integration)
miniscreenshot-softbuffer = { version = "0.3", features = ["winit"] }miniscreenshot-portal exposes runtime and API-surface features. Enabling
a runtime (tokio or async-io) automatically enables the async API surface.
# Default: tokio runtime + blocking API + async API
miniscreenshot-portal = "0.3"
# Async-only with tokio (no blocking convenience methods)
miniscreenshot-portal = { version = "0.3", default-features = false, features = ["tokio"] }
# Async-only with async-io
miniscreenshot-portal = { version = "0.3", default-features = false, features = ["async-io"] }The tokio and async-io runtime features are mutually exclusive. The
blocking API-surface feature is independent. The async API surface is
implied by whichever runtime you select, but can also be enabled standalone
if you want to provide your own executor.
| Format | Method | Notes |
|---|---|---|
| PNG | encode_png() / save("file.png") |
Lossless, widely supported |
| PPM | encode_ppm() / save("file.ppm") |
Binary P6, trivial to parse |
| PGM | encode_pgm() / save("file.pgm") |
Binary P5 grayscale (BT.601 luma) |
Each crate ships with a self-contained examples/<crate_short>_scene_screenshot.rs that
renders a scene (or synthesises a buffer) and saves a PNG.
| Crate | Command | Headless? |
|---|---|---|
miniscreenshot (core) |
cargo run -p miniscreenshot --example core_scene_screenshot |
Yes |
miniscreenshot-softbuffer |
cargo run -p miniscreenshot-softbuffer --example softbuffer_scene_screenshot |
Yes |
miniscreenshot-softbuffer (winit) |
cargo run -p miniscreenshot-softbuffer --example softbuffer_winit_scene_screenshot --features winit |
No (needs a display) |
miniscreenshot-wgpu |
cargo run -p miniscreenshot-wgpu --example wgpu_scene_screenshot |
Yes |
miniscreenshot-wayland |
cargo run -p miniscreenshot-wayland --example wayland_scene_screenshot |
No (needs wlroots-based Wayland compositor) |
miniscreenshot-x11 |
cargo run -p miniscreenshot-x11 --example x11_scene_screenshot |
No (needs $DISPLAY / X11 server) |
miniscreenshot-portal |
cargo run -p miniscreenshot-portal --example portal_scene_screenshot |
No (needs desktop session with portal) |
miniscreenshot-portal (async) |
cargo run -p miniscreenshot-portal --example portal_async_scene_screenshot --features async |
No (needs desktop session with portal) |
miniscreenshot-mcp (desktop) |
cargo run -p miniscreenshot-mcp --example desktop_mcp_server |
No (needs desktop session) |
miniscreenshot-mcp (portal async) |
cargo run -p miniscreenshot-mcp --example portal_async_mcp_server |
No (needs desktop session with portal) |
miniscreenshot-mcp (wgpu game) |
cargo run -p miniscreenshot-mcp --example wgpu_game_mcp_server |
No (runs a server; needs a GPU) |
miniscreenshot-skia |
cargo run -p miniscreenshot-skia --example skia_scene_screenshot |
Yes |
miniscreenshot-vello |
cargo run -p miniscreenshot-vello --example vello_scene_screenshot |
Yes |
miniscreenshot-minifb |
cargo run -p miniscreenshot-minifb --example minifb_scene_screenshot |
Yes |
Build all examples at once:
task examples:buildBuild and run all headless examples:
task examplesLicensed under either of Apache License 2.0 or MIT License at your option.