Why eensh exists
What a screenshot does not tell an agent, and the one design decision that fixes it.
Every screenshot tool ever written answers one question: what do the pixels look like? That is the right question for a human, who can see the result and reason about it. It is close to useless for a program, which receives a rectangle of bytes and has to decide what to do next.
The gap is not the image. It is everything the image does not say: where those pixels were, when they were taken, how long the capture took, and whether the screen had finished drawing.
Five things that hurt when a program takes screenshots
What a program gets
A file, a pixel size, and the silent assumption that the size is the screen's size.
What a program needs
Two sizes — source and image — and an explicit transform between them.
1. Coordinate ambiguity. A resized screenshot has two different widths: the screen's and the image's. Most tools report one and let the caller infer the other. An agent that guesses wrong clicks the wrong place, and the failure is silent.
2. Presentation welded to capture. If the capture path writes a file, that path can never support comparison, multiple crops from one capture, or a persistent service. The format decision has been made before anyone knows what the caller wants.
3. “Something went wrong.” Not actionable. An agent needs to know whether to retry, fix the region, wait, or give up — and those are four different errors.
4. Timing. sleep(2) and hope is the standard way to wait for a UI. It is wrong in both directions: too short and you capture a half-drawn menu, too long and you have wasted two seconds on every interaction.
5. One frame cannot show motion. A single screenshot cannot distinguish a spinner from a freeze, or a settled screen from a paused one.
The decision that answers all five
eensh is built on one rule, and almost everything else follows from it:
Capture into a raw frame, and render late.
A capture produces pixels and nothing else — no PNG, no JPEG, no file, no base64. Every later stage consumes that raw frame and decides what to do with it:
- captureReads the pixels from X11 into a raw Frame. No encoder runs.
- compareFrame A and Frame B become counts, a fraction, and a bounding box. Still no encoder.
- observeRepeats capture until the screen changes, then until it stops. Encodes once, at the end.
- sessionKeeps raw frames in a bounded history, so later requests read from memory instead of the display.
- presentationChooses the size, format, and quality of what is sent. Runs after everything above.
Because the boundary is a raw frame, the same comparison primitive works on a live capture, on a decoded file, inside a high-frequency observation loop, and across a process boundary — without being rewritten four times.
It is also why later requirements never disturbed earlier ones. Regions, temporal policy, and payload budgets were all added in a layer that runs after capture. None of them changed how a frame is taken.
The second rule: never guess on the caller's behalf
Where a choice could be made silently, eensh states it instead. This shows up in five places, and each one is a decision you would otherwise have to reverse-engineer:
- The transform is always present, even when it is the identity mapping. A caller never has to check whether a field exists.
- Errors carry a class and a status, so “retry” and “your region is wrong” are distinguishable without parsing a message.
- Regions are never clipped silently. A rectangle that does not fit is an error, not a surprise.
- Applied settings are reported separately from requested ones, so a payload budget can never quietly change what you received.
- A timeout is an outcome, not an error — a different exit status entirely, so the two cannot be confused.
What it is not
It is worth being explicit, because the restraint is deliberate. There is no OCR, no object detection, no automatic region discovery, no connected-component segmentation, no XDamage, no shared-memory capture, no optical flow, and no push streaming.
It does not look inside a frame. It knows where a rectangle is and what you asked for — nothing about what is depicted. That is what keeps its behaviour predictable from the request alone, and it is the subject of what is deliberately absent.
Where to go next
If any of the five problems above sounded familiar, the fastest route is a single capture with every part read back to you. If you have used scrot or import before — tools that write an image file and nothing else — then the part that matters most is the coordinate transform, because that is where the two models differ.
eensh capture --display :1 --json --base64