This guide sends mouse and keyboard input to a box by screen coordinates, captures the screen, and uses the clipboard. It works with any application. On a web page, act on elements by name instead: see Control modes.
Coordinates
Coordinates are device pixels on one screen. (0, 0) is the top-left corner.
- Get a point from the most recent full-size screenshot.
- Do not calculate a point from a scaled or cropped capture.
- Take a new screenshot after a person, another process, or a new browser tab changes the screen.
- A screenshot does not show the pointer. Use
cursorto get its position.
Mouse
| Operation | CLI | MCP | Rust |
|---|---|---|---|
| Move | mouse <box> move X Y |
batch step move |
move_to(at) |
| Click | mouse <box> click X Y [button] |
click |
click(at, button) |
| Double click | mouse <box> click X Y --double |
click with double |
double_click(at, button) |
| Drag | mouse <box> drag X1 Y1 X2 Y2 |
drag |
drag(from, to, button) |
| Path | mouse <box> path X1 Y1 X2 Y2 … |
batch step draw |
drag_along(from, &steps, button, &held) |
| Button down | mouse <box> down [X Y] |
mouse_down |
button_down(at, button) |
| Button up | mouse <box> up [X Y] |
mouse_up |
button_up(at, button) |
| Pointer position | mouse <box> at |
cursor |
cursor() |
button is left (default), right, or middle.
In Rust, move_along, drag_along, button_down, button_up, key_down, and key_up are methods of the Desktop trait. Import it with use holm::Desktop as _;.
holm mouse "$BOX" move 640 400
holm mouse "$BOX" click 640 400
holm mouse "$BOX" click 640 400 right
holm mouse "$BOX" click 640 400 left --double
holm mouse "$BOX" drag 100 100 400 300computer.move_to((640, 400)).await?;
computer.click((640, 400), Button::Left).await?;
computer.double_click((640, 400), Button::Left).await?;
computer.drag((100, 100), (400, 300), Button::Left).await?;A drag is one press, one move, and one release. A path is one press, a move through each point, and one release, so it draws one stroke with corners. A drag for each part of the line draws separate strokes.
holm mouse "$BOX" path 100 100 300 100 300 300 100 300Modifier keys
Hold modifier keys during a click, drag, or path with --held. Pressing a key in a separate command does not work, because that key is released when its command ends.
holm mouse "$BOX" click 640 400 left --held shift
holm mouse "$BOX" drag 100 100 400 300 left --held ctrlcomputer.click_with((640, 400), Button::Left, &[Held::Shift]).await?;
computer.drag_with((100, 100), (400, 300), Button::Left, &[Held::Ctrl]).await?;MCP: held: ["shift"] on click or drag. The modifiers are shift, ctrl, alt, and super.
Motion
The pointer jumps to a point unless you ask for motion:
| Motion | Movement |
|---|---|
instant (default) |
Jumps. |
smooth |
Moves on a straight line. |
human |
Moves on a curve, over 100 to 700 ms by distance. The same seed gives the same curve. |
Use human for a page that watches pointer movement, or for a person who watches the screen.
holm mouse "$BOX" move 640 400 --smooth
holm mouse "$BOX" click 640 400 left --human --seed 42MCP: motion: "human" and seed: 42 on click, drag, mouse_down, and the page tools.
use holm::{Desktop as _, Motion, motion};
let steps = motion::path(Point::new(100, 100), Point::new(400, 300), Motion::Human, 42);
computer.move_along(&steps).await?;Hold a button
Use down and up only when a drag cannot do the task, for example when you must wait for something before you release:
holm mouse "$BOX" down 400 400 left --hold 10
holm mouse "$BOX" move 900 600 --smooth
holm mouse "$BOX" up 900 600 leftWith no point, down and up act at the pointer. A move between them drags.
The server releases a button that stays down:
- After
--holdseconds (hold_secondsin MCP,hold_msin REST). Default 10 seconds, maximum 60. - When a person takes control.
- At the end of a batch, unless the step gave a hold time. The result names each released button.
down and up need a server. They do not work with --local.
Keyboard
| Operation | CLI | MCP | Rust |
|---|---|---|---|
| Type text | keyboard <box> type TEXT |
type_text |
type_text(text) |
| Press keys | keyboard <box> press KEYS… |
press_key |
press(keys) |
| Key down | keyboard <box> down KEY |
key_down |
key_down(key) |
| Key up | keyboard <box> up KEY |
key_up |
key_up(key) |
Typing goes to the element that has the keyboard focus. Click the field, or focus it, first.
holm keyboard "$BOX" type "hello"
holm keyboard "$BOX" press enter
holm keyboard "$BOX" press ctrl+lcomputer.type_text("hello").await?;
computer.press("enter").await?;
computer.press("ctrl+l").await?;Slow typing
Some inputs act on each key and lose characters that arrive at full speed. Add a delay between keys. 20 to 50 ms is usually enough.
holm keyboard "$BOX" type "one key at a time" --delay 30computer.type_text("one key at a time").every(Duration::from_millis(30)).await?;MCP: delay_ms: 30 on type_text. REST: delay_ms on a type action.
Key names
A key is a name or a combination: enter, tab, escape, up, pagedown, ctrl+a, cmd+shift+p. Common other names also work, such as esc, return, pgdn, cmd, and win.
Hold a modifier across keys
A combination releases its modifiers at the end. To keep a modifier down across several keys, give all the keys in one command with --held:
holm keyboard "$BOX" press tab tab tab --held altcomputer.press(["tab", "tab", "tab"]).holding([Held::Alt]).await?;MCP: press_key with chord: "tab", then: ["tab", "tab"], and held: ["alt"].
This goes to the third window. Three separate alt+tab presses go only to the second.
Hold a key
down keeps one key down until up. Everything typed or clicked while it is down carries it:
holm keyboard "$BOX" down shift --hold 20
holm mouse "$BOX" click 300 200
holm mouse "$BOX" click 300 400
holm keyboard "$BOX" up shiftdown takes one key, not a combination. The server releases the key at the same times as a button.
Scroll
scroll turns the mouse wheel at a point, in notches. It moves what is under the point, so it reaches a list or a sidebar without a click.
holm mouse "$BOX" scroll 640 400 down 3
holm mouse "$BOX" scroll down
holm mouse "$BOX" scroll 640 400 5 -2With no point, the CLI scrolls at the center of the screen. With no count, it scrolls 3 notches. The signed form takes DY then DX: positive is down and right.
computer.scroll((640, 400), Delta::down(3)).await?;
computer.scroll((640, 400), Delta::right(3)).await?;MCP: scroll with x, y, dy, and dx.
On a web page, scroll_page (MCP) moves the page by pixels and returns the new position. See Control modes.
Wait for the screen
After an action that draws, such as opening a menu, wait until the screen stops changing before you take a screenshot:
holm wait "$BOX" --settle 400 --within 10000computer.wait_until_still(Duration::from_millis(400), Duration::from_secs(10)).await?;MCP: wait_until_still. REST: a wait_still action.
The screen must not change for settle (default 400 ms). The wait stops at within (default 10 seconds). A screen with an animation reaches the limit. On a web page, wait_for is better: it waits for the element you need.
Screenshots
| Capture | CLI | MCP (screenshot) |
Rust |
|---|---|---|---|
| Full screen | screenshot <box> out.png |
no parameters | screenshot() |
| One window | --window ID |
window |
capture(&Shot::window(id)) |
| A rectangle | --at X,Y --size WxH |
x, y, width, height |
capture(&Shot::region(rect)) |
| Smaller | --scale PERCENT |
scale |
.scaled(percent) |
| With the pointer | --pointer |
pointer |
Shot { pointer: true, .. } |
holm screenshot "$BOX" full.png
holm screenshot "$BOX" region.png --at 100,80 --size 400x300
holm screenshot "$BOX" window.png --window 42
holm screenshot "$BOX" small.png --scale 50let full = computer.screenshot().await?;
let region = computer
.capture(&Shot::region(Rect::new(Point::new(100, 80), 400, 300)))
.await?;
let small = computer.capture(&Shot::of(Of::Screen).scaled(50)).await?;Get window IDs from holm window <box> list or the MCP window tool with op: list. A window capture uses the window's position when the capture runs.
A scaled image uses fewer bytes, but do not calculate click points from it.
MCP tools that change the screen return a frame. Give have_frame with the last frame's hash: if the screen did not change, the result says unchanged and sends no image.
Clipboard
Each screen has two selections:
| Selection | Filled by | Pasted by |
|---|---|---|
clipboard (default) |
Copy | Paste (ctrl+v) |
primary |
Selecting text | A middle click |
holm clip "$BOX" "ready to paste"
holm clip "$BOX"
holm clip "$BOX" "middle-click paste" --primary
holm clip "$BOX" --primarycomputer.set_clipboard("ready to paste").await?;
let text = computer.clipboard().await?;
computer.set_selection(Selection::Primary, "middle-click paste").await?;MCP: clipboard with text to set it, or without text to read it, and selection. REST: GET and PUT /v1/boxes/{id}/screens/{screen}/clipboard.
Setting the clipboard and pasting is faster than typing long text. From Rust, clipboard_bytes and set_clipboard_bytes move other types, such as image/png.
Batches
A batch runs several steps in one call. The box holds the screen for all the steps, so no other client can act between them, and one result comes back at the end. Use a batch when you know the steps in advance: a form, a drawing, or a key held across clicks.
CLI
holm batch reads a file, or standard input, with a list of REST actions:
[
{ "type": "click", "at": { "x": 200, "y": 150 } },
{ "type": "type", "text": "hello", "delay_ms": 20 },
{ "type": "press", "chord": "enter" },
{ "type": "wait_still", "settle_ms": 400 }
]holm batch "$BOX" steps.json --settle 400The file can also be an object with an actions list. --keep-going runs the remaining steps after a step is refused.
MCP
batch takes actions, each with a tool name and that tool's arguments:
{
"box_id": "box_…",
"actions": [
{ "tool": "mouse_down", "arguments": { "x": 100, "y": 100 } },
{ "tool": "move", "arguments": { "x": 200, "y": 150, "pause_ms": 40 } },
{ "tool": "move", "arguments": { "x": 300, "y": 100, "pause_ms": 40 } },
{ "tool": "mouse_up", "arguments": {} }
],
"keep_going": true
}move and draw are steps only in a batch. draw takes through, a list of {x, y}. pause_ms on move waits after the move. A drawing program needs about 40 ms to see each point.
REST
POST /v1/boxes/{id}/screens/{screen}/actions takes the same list as the CLI, under actions. See Actions for every action type.
Buttons and keys at the end
A button or key that is still down when the batch ends is released, and the result names it in released or released_keys. A step that gives a hold time keeps it down until that time, and the result lists it in holding or holding_keys.