Screenshot
Screenshot takes a picture of the screen, a window or a region you drag out, lets you mark it up, and saves it where you and your agents can find it. Every mark stays editable until you save: the captured pixels are never changed, and what is saved is exactly what you see.
Taking a screenshot
| Key | Takes |
|---|---|
| a region: drag out a rectangle with the crosshair; a click or Enter takes the whole screen, Escape cancels | |
| Shift+Print | the whole screen |
| Alt+Print | the window you were working in |
The keys work everywhere on the desktop. They open Screenshot, or hand the shot to the one already open, and Screenshot hides itself while it takes the picture. If the window holds annotations you have not saved, it comes forward and asks first. Nothing you drew is ever thrown away by a key press.
In the window, Take screenshot (Ctrl+N) takes another shot in the mode the toolbar shows: region, full screen or window. For a window shot, choose the window from the list beside the mode buttons; opening the list reads the windows again. With more than one display, the same place offers the output: the one under the pointer, or a named one (also Capture › Output). The Capture menu also has a delay (3, 5 or 10 seconds) and Include pointer, which is off unless you turn it on. For a region, the status line says "Drag to select an area · Esc to cancel" as the window hides; compd then outlines the screen until you drag. A left drag selects, Escape or the right button cancels, and a click without a drag keeps the selection waiting. If you select nothing before compd's limit, the window comes back saying "Nothing was selected". Whatever happens, a shot you asked for brings the window back on top and focused. Escape or Capture › Cancel stops a shot that is waiting out its delay. A second Print while a shot is being taken is answered "already being taken" in the window's status line.
Marking it up
The tool bar on the left holds the tools. Each has a key you press with nothing else held:
| Tool | Key | Does |
|---|---|---|
| Select and move | V | click an annotation to select it; drag it to move it |
| Crop | C | drag out the part to keep (Edit › Reset crop undoes it) |
| Arrow | A | drag from the tail to the point |
| Line | L | drag |
| Rectangle | R | drag a box |
| Ellipse | E | drag a box |
| Pen | P | draw freehand |
| Highlighter | H | draw a translucent stroke |
| Redact | X | drag a box; it is filled solid, so nothing shows through |
| Text | T | type the text in the bar at the top, then click or drag |
| Numbered marker | N | click; markers count up 1, 2, 3… |
The bar at the top sets the ink (red, amber, green, teal, black or white), the stroke width, and for text and markers the text size. Changing the ink or width while an annotation is selected changes that annotation too. In a narrow window, the controls that don't fit are left out of the bar; Annotate still has them all.
Widths and text sizes are what you see: a Medium line is the same
thickness on screen whether the shot is a 4K screen shown scaled down or a
small region shown full size. Each annotation keeps the size it was drawn
at in the image's own pixels, so zooming later makes it bigger or smaller
with the picture, and changing only the ink never changes its width. To
an agent, width and size are in the image's pixels (up to 512 and 1024).
Delete or Backspace removes the selected annotation, and Escape deselects it. Undo is Ctrl+Z and Redo is Ctrl+Shift+Z or Ctrl+Y. Ctrl with + and − zooms, Ctrl+0 fits the picture to the window, the wheel scrolls, and a middle-button drag pans.
Saving
Once there is a picture, Copy and Save buttons sit at the right end of the bar at the top; Save's tip names the folder it saves to.
- Save (Ctrl+S) writes the picture straight into
Pictures/Screenshots, no questions asked, as
20261012_193927.png(the local date and time you saved, asYYYYMMDD_HHMMSS, the form KDE's Spectacle can be set to use). If that name is taken, the new file gets_2,_3and so on (20261012_193927_2.png). Screenshot never overwrites a file. The status line then says "Saved to ~/Pictures/Screenshots/…" with the file's name. - Save As… (Ctrl+Shift+S) asks for a path, proposing the name Save would use in Pictures/Screenshots (created if missing). It refuses one that already exists.
- Copy image (Ctrl+C) puts the marked-up picture on the clipboard.
The Pictures folder is the one your desktop names
(XDG_PICTURES_DIR in ~/.config/user-dirs.dirs), or ~/Pictures.
Saved PNGs are created like any file an application saves: your umask
decides who else may read them.
The raw shot compd takes is kept, until you save, in Screenshot's own
private folder (~/.local/state/mixos/apps/screenshot/captures, readable
only by you). It is deleted once the shot is saved, replaced by another,
or closed, and when a capture is cancelled or fails. On start, Screenshot
also prunes that folder to the newest 20 raw shots and nothing older than
7 days. The window and headless instances share that folder.
Handing a shot to an agent
You don't need to tell an agent where a shot is. Press Print, mark it up, press Ctrl+S, and ask the agent to look at your latest screenshot. The agent finds it in two ways:
screenshot.recentlists the newest saved shots: path, size, whether they carry annotations, and when each was saved.screenshot.savedis published on every save with the same fields, for an agent that is listening.
-- The newest saved screenshot's path.
$body = json_encode({limit: 1})
send screenshot screenshot.recent body=$body
if $rc != 0 then raise("SCREENSHOT", to_string($result)) end
$recent = if type($result) == "string" then json_parse($result) else $result end
print $recent.shots[0].path
The answer names the folder too:
{folder, shots: [{path, width, height, annotated, saved_at}]}, newest
first.
For agents
Screenshot answers on the Bus as screenshot (schema screenshot.v1).
| Verb | Arguments | Answer |
|---|---|---|
screenshot.ping, screenshot.info | liveness and the compositor served (comp); the document, its source and last save, the capture, the job and the UI state | |
screenshot.capture | mode (screen, window, region), output, window {id, generation}, cursor, delay (0–10) | a new document, once it is ready; any answer, refusals included, carries restore_failed: true if the window could not be shown again; refused while unsaved annotations exist |
screenshot.take | mode, window, comp | as Print Screen: starts at once, or brings the window forward and asks about unsaved work at once; refused (COMPOSITOR) when comp names another compositor |
screenshot.cancel | cancels a capture during its delay | |
screenshot.open | path | opens an image; refused while unsaved annotations exist |
screenshot.annotate (deprecated) | kind, points, colour, width, and text, size, number for text and markers | the document plus the new annotation's id |
screenshot.move, screenshot.delete (deprecated) | id (and dx, dy) | the document |
screenshot.crop (deprecated) | crop {x, y, width, height}, or null | the document |
screenshot.undo, screenshot.redo (deprecated) | the document, including when history is empty | |
screenshot.save | saves into the screenshots folder as YYYYMMDD_HHMMSS.png (_2, _3… when taken); publishes screenshot.saved | |
screenshot.export | path | Save As to a new file |
screenshot.recent | limit (1–100) | the newest saved shots |
screenshot.select | tool, mode, window, output | the UI state |
screenshot.commands | every command with parameter schemas, labels, shortcuts and enablement | |
screenshot.execute | id, optional params | runs a command; answers {executed, result, status, dialog, job} |
screenshot.theme | scheme, mode | this window's look |
screenshot.show, screenshot.quit | brings the window forward; closes it (refused with unsaved work) |
Coordinates are pixels of the captured image, whatever the zoom. Like any
program you run, an agent on the Bus acts as you: screenshot.open reads
any image you can read, and screenshot.export writes a new PNG wherever
you can write (never over an existing file). An agent
is refused rather than allowed to discard a person's unsaved annotations
(UNSAVED), and is refused while a dialog is open (DIALOG) or another job
runs (BUSY).
Parameterised document edits
The headless annotation engine owns edit.annotate (the Shape body),
edit.move {id, dx, dy}, edit.delete {id}, edit.crop {crop} (null resets),
and nullary edit.undo / edit.redo. Dispatch through
screenshot.execute {id, params}; its reply includes the command's result
alongside status, dialog and job. Annotate returns document info plus the
new object's id; the other edits return document info. Unknown fields in
the execute body are refused with ARGUMENT.
Pointer strokes, text, number markers, completed object drags, crops, menus,
shortcuts and Bus calls all reach the same command::Registry::execute.
A drag commits on release, making one undo step and one executed event.
Delete selection is the nullary shell command edit.delete_selected; it
dispatches edit.delete with the selected id. Keymap overrides that named
the old nullary edit.delete must move to edit.delete_selected.
Uncrop dispatches edit.crop. The shell's dialog/job checks compose with
the engine's enablement.
The six screenshot.annotate, screenshot.move, screenshot.delete,
screenshot.crop, screenshot.undo and screenshot.redo verbs remain
deprecated aliases of execute and preserve their old reply bodies and
argument error codes at document limits. Alias undo/redo with empty history
return the document without executing or publishing an event; the command
and screenshot.execute remain disabled. Undo/redo use the shared
command::History; its byte budget counts undo and redo snapshots. Raster
workers receive a fresh history without old edits.
The window is drivable like every MixOS app, including its menus: see
driving an app (screenshot.ui.*, screenshot.window). The
verbs are registered in docs/docs/spec/bus/verbs.conf.mix. Each command run
from a menu, a key, the toolbar or screenshot.execute is published on
screenshot.command.executed v2 with accepted params and returned result,
including successful edits reached by hand or through the deprecated aliases
(executed commands).
Edit › Delete and Edit › Uncrop each publish two executed events: first the
inner edit.delete / edit.crop with parameters, then the nullary wrapper
edit.delete_selected / edit.uncrop. The wrapper records the gesture;
the inner command records the document edit.
screenshot --headless runs the same verbs with no window, for agents and
tests, registered under the headless names above so Print Screen never
lands in an instance nobody can see. It never asks anything: work that would need a
question is refused.
From the command line
screenshot [IMAGE] open the window (or bring it forward)
screenshot --capture region take a shot, as Print does
screenshot --capture window --window ID:GEN
screenshot --headless the Bus service alone (screenshot-headless)
screenshot --comp comp-nested capture through another compositor
There is one window, and one headless instance, per compositor:
| Compositor | Window | Headless |
|---|---|---|
comp (the desktop's own) | screenshot | screenshot-headless |
another, e.g. comp-nested | screenshot-comp-nested | screenshot-headless-comp-nested |
| a name too long to fit | screenshot--<8 hex> | screenshot-headless--<8 hex> |
The hex is a stable hash of the compositor's name; a service name is at
most 31 bytes, and a window name never starts with screenshot-headless.
A second launch, or Print Screen in a compositor, hands its request to
that compositor's window, so a shot is always taken through the
compositor whose key was pressed; before handing over, the launch checks
that the running instance serves the same compositor (comp in
screenshot.ping) and refuses otherwise, and the request itself names the
compositor so the receiver refuses it too. A running window from an older
build that does not report comp refuses every hand-over: close it and
launch again. --comp and --service take Bus
service names: 2 to 31 of a-z, 0-9 and -, starting with a letter.
An agent's capture (screenshot.capture) hides the window and shows it
again without taking the focus or switching workspace; a person's capture
brings the window forward. Showing the window again is retried for a few
seconds; if it still fails, the shot is kept, the answer carries
restore_failed: true and the status line says so. Bring the window back
with Print Screen, screenshot.show or the taskbar; nothing restores it
later on its own.
Source and tests
The source is
apps/screenshot
(app id dev.mixos.screenshot):
crates/annotation: the headless engine (document, painter, viewport, screenshots folder, ledger).src/: the egui shell (appstate and rules,commandsregistry,captureover compd,jobs,view,shell,headless). The instance names per compositor are insrc/names.rs.
Test it on a build worker:
cargo test -p screenshot -p annotation
tests/native_bus.rs runs the headless binary against an in-process
broker and a fixture compositor; tests/snapshot.rs renders the window
offscreen (regenerate with UPDATE_SNAPSHOTS=1 and look at the images);
tests/registry.rs checks the verb registry.
Notes
- Copy image goes through the desktop's X11 clipboard (XWayland), so native Wayland applications may not see it yet. Save, then paste the file, is the reliable path for now.
- Screenshot replaces Cap, the earlier screenshot app; see the decision.
Changes from Cap
- Print, Shift+Print and Alt+Print take shots from anywhere.
- Ctrl+S saves straight into Pictures/Screenshots; agents find saves with
screenshot.recentandscreenshot.saved. - Stroke width and text size are steps (thin to heavy, small to huge) rather than free values; numbered markers are discs.
- Open and Save As take a typed path rather than a file browser.
- The pointer is left out of shots unless you include it, for people and agents alike.
- A region selection may take up to 70 seconds (Cap allowed 38).
- Raw shots are deleted when no longer needed, and the folder is pruned.