In practice6 min read

Replacing sleep()

Wait for the screen to change, then wait for it to stop changing.

sleep(2) 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 in a test suite. The fix is not a better constant — it is having the tool watch the screen and return when it has settled, instead of guessing how long that takes.

bash
eensh observe --display :1 --stable-for 300ms --timeout 10s --json --base64

Three observation commands, and how they differ

wait-change, wait-stable and observe differ in exactly one way: what each frame is compared against. Getting that backwards returns a confident answer to the wrong question, which is worse than an error.

CommandComparesQuestion
wait-changeEach frame against a fixed baselineHas the visible state changed?
wait-stableConsecutive framesHas it stopped changing?
observeBaseline, then consecutiveWhat did it settle into?
  1. capture baseline
  2. compare against baseline
  3. changed!
  4. compare consecutive
  5. still
  6. return frame

Why the comparison target matters

A fixed baseline notices gradual change. Imagine a progress bar filling one percent at a time. Each step moves far fewer pixels than the area threshold, so no single consecutive comparison sees a meaningful change. But every frame differs from the original, and cumulative drift eventually crosses the threshold. Compare against a fixed baseline and you notice; compare consecutively and you wait forever.

Consecutive comparison notices stillness. The mirror image. A fixed baseline tells you the screen looks different from how it started — it cannot tell you whether it is still moving. For that you need to know whether the last two frames differed, which is a question only consecutive comparison answers.

Why observe waits past the first change

The first frame that differs from the baseline is usually not the state you want. It is a half-drawn menu, an animation step, or a layout that has not finished laying out. Returning it would be technically correct and practically useless.

So observe switches modes: a fixed baseline until a change is seen, then consecutive comparison until the scene holds still for --stable-for. The result is the state the transition settled into, which is almost always what a caller meant.

The options worth setting

OptionDefaultMeaning
--stable-for300msHow long the scene must hold still. Too short and you catch a pause between animation frames; too long and every observation pays for it.
--timeout5sTotal deadline. Exceeding it exits 100, which is an outcome rather than an error.
--interval100msTarget cadence between samples. Sampling is scheduled from a fixed origin, so a slow capture cannot accumulate drift.
--area-threshold0.005Smallest changed fraction treated as meaningful. Note this is stricter than diff's default of 0.0, deliberately — see below.

Durations need a unit

100ms, 300ms, 1s, 5s. A bare 5 is rejected rather than guessed at, because --timeout 5 is ambiguous between five seconds and five milliseconds — and a silently wrong timeout is worse than a parse error.

Next: what a single observation still cannot tell you — motion from stillness, and why one frame is never enough.