Skip to content

sunsided/miniscreenshot

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

21 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

miniscreenshot

A pluggable, multi-crate Rust workspace for taking screenshots of windowed applications or the entire desktop.


Crate overview

Crate Description
miniscreenshot CoreScreenshot 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)

Design goals

  • 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 separate wgpu/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 via XGetImage with an MIT-SHM fast path, and XDG Desktop Portal via ashpd (GNOME, KDE, Flatpak, Snap).

Quick start

Core crate

[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

Desktop umbrella (just take a screenshot)

[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.

softbuffer backend

[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();

softbuffer + winit pairing

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.

wgpu backend

[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_SRC and cannot be captured directly. Render your scene into an offscreen texture created with RENDER_ATTACHMENT | COPY_SRC, capture that, then blit it to the surface to present. See the wgpu_scene_screenshot example. From an async render loop, wrap the (blocking) capture call in tokio::task::spawn_blocking.

Wayland system screenshot

[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 return WaylandCaptureError::NoScreencopyManager. Use miniscreenshot-portal instead on these compositors.

X11 system screenshot

[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 ($DISPLAY set). Uses MIT-SHM when available for a fast-path capture; otherwise falls back to a plain XGetImage transfer over the wire.

Screenshot portal (GNOME / KDE / Flatpak)

[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_DIR and a portal implementation (xdg-desktop-portal + a backend such as xdg-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 of miniscreenshot-wayland.

MCP integration

[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/mcp

The 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.

Capture your game's frame (wgpu)

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.

Custom configuration

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?;

Async capture (CaptureAsync)

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?;

Connecting a coding agent

Point your MCP-capable agent at the running server. With Claude Code:

claude mcp add --transport http screenshot http://127.0.0.1:8731/mcp

The 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.

minifb (prototyping window)

[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");
}

Capture trait

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();
}

Closures as Capture

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);

MultiCapture

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();
    }
}

Optional features

# Winit (for softbuffer + winit integration)
miniscreenshot-softbuffer = { version = "0.3", features = ["winit"] }

Portal features

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.


Output formats

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)

Examples

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:build

Build and run all headless examples:

task examples

License

Licensed under either of Apache License 2.0 or MIT License at your option.

About

A pluggable, multi-crate Rust workspace for taking screenshots of windowed applications or the entire desktop

Topics

Resources

Stars

Watchers

Forks

Contributors

Languages