Skip to main content
Version: 2.1 (dev)

Screenshots

This page explains how SomeWM captures screenshots through root.content() and how the interactive selection mode works.

The Capture Pipeline​

All screenshots flow through the same C function: root.content(). This is the Wayland equivalent of reading the X11 root window pixmap.

X11 (AwesomeWM)​

On X11, the root window is a composited pixmap managed by the X Server:

root.content() → cairo_xcb_surface_create() → Done

One function call. The X Server already has a composited framebuffer.

Wayland (SomeWM)​

On Wayland, the compositor owns all buffers. There is no pre-composited root pixmap to read from. SomeWM must manually composite every visible element onto a new Cairo surface:

root.content()
├─ Create Cairo surface at layout dimensions
├─ Paint wallpaper
├─ For each scene buffer (client windows):
│ ├─ Read pixels from GPU texture (or SHM buffer)
│ └─ Composite at logical position with scaling
├─ Composite widgets (drawins, titlebars)
└─ Return surface as lightuserdata

The result is a Cairo image surface at logical resolution (matching root.size()). This is the coordinate system Lua operates in.

HiDPI and Buffer Scaling​

On HiDPI displays, client windows render at physical resolution but occupy logical space. A client at logical size 800x600 might have a 1600x1200 pixel buffer (at 2x scale).

The scene buffer compositor handles this by:

  1. Reading the full buffer->width x buffer->height texture (physical pixels)
  2. Scaling down to dst_width x dst_height (logical pixels) when painting

This produces a screenshot where every pixel corresponds 1:1 to a logical coordinate, which is what awful.screenshot expects for cropping.

Widget Surfaces​

Widget drawable surfaces (drawins, titlebars) use a different mechanism. Their Cairo surfaces have cairo_surface_set_device_scale() applied, so Cairo handles the physical-to-logical scaling automatically during compositing. No explicit scaling is needed.

Interactive Selection Mode​

awful.screenshot({ interactive = true }) uses a three-phase pipeline:

Phase 1: Capture​

When refresh() is called, root.content() captures the entire desktop into a Cairo surface. This happens before the overlay is shown, so the overlay itself is not captured.

Phase 2: Overlay​

start_snipping() creates a fullscreen wibox (the "frame") containing:

  • An imagebox showing the captured surface as a background
  • A selection widget (red rectangle) that follows the mouse

The user sees the captured image and draws a selection rectangle on top of it. Mouse coordinates from the mousegrabber are in logical space, matching the captured surface.

Phase 3: Crop​

When the user releases the mouse button, accept() crops the pre-captured surface using the selection coordinates:

-- Inside accept()
crop_shot(selected_geometry.surface, selected_geometry)
-- selected_geometry = {x, y, width, height} from mouse coordinates

The crop operates on the original high-quality capture. The overlay was only for visual feedback.

Coordinate Spaces​

Everything in the screenshot pipeline uses logical coordinates:

ComponentCoordinate Space
root.size()Logical (layout bounding box)
root.content() surfaceLogical pixels
Mouse position (mousegrabber)Logical (Wayland compositor coords)
Frame wibox dimensionsLogical
Selection rectangleLogical
crop_shot() source and targetLogical

This consistency is what makes the crop coordinates match the captured content.

See Also​