This guide works with the windows on a desktop, opens applications, uses more than one screen, changes the wallpaper, and records the screen.
Windows
List windows
holm window "$BOX" list
holm window "$BOX" activeEach window has an id, title, class, position (at), width, and height. Use the id in later commands. active returns the window that receives keyboard input.
Focus, close, and arrange
holm window "$BOX" "$WIN" focus
holm window "$BOX" "$WIN" move 120 90
holm window "$BOX" "$WIN" size 800 600
holm window "$BOX" "$WIN" max
holm window "$BOX" "$WIN" min
holm window "$BOX" "$WIN" restore
holm window "$BOX" "$WIN" closeUse an id from list. close asks the application to close the window, and the application can ask a question first.
Move, size, and state changes return the final geometry. The window manager can change a request, for example to keep a window on the screen or to respect the application's size limits. On Wayland, minimize moves the window to the sway scratchpad.
Wait for a window
After an action that opens a window, wait for it. Do not use a fixed delay.
holm window "$BOX" wait Mousepad --within 10The command returns when a window of that class appears and stops moving. Use the class, not the title: a title often changes with the open document.
The CLI takes --within in seconds. REST and MCP take within_ms in milliseconds. The default is 30 seconds.
The same steps in other interfaces
| Task | MCP (window tool) |
REST | Rust (computer.primary()) |
|---|---|---|---|
| List | op: "list" |
GET …/screens/{n}/windows |
windows() |
| Active | op: "active" |
GET …/screens/{n}/windows/active |
active_window() |
| Wait | op: "wait", class, within_ms |
POST …/screens/{n}/windows/wait |
wait_for_window(class, within) |
| Focus | op: "focus", window |
POST …/windows/{id}/focus |
focus(id) |
| Close | op: "close", window |
DELETE …/windows/{id} |
close_window(id) |
| Arrange | op: "arrange", window, how, x, y, width, height |
POST …/windows/{id}/arrange |
arrange(id, Arrange) |
REST arrange bodies are in the REST reference.
let screen = computer.primary();
let window = screen.wait_for_window("Mousepad", Duration::from_secs(10)).await?;
screen.arrange(&window.id, Arrange::Size { width: 800, height: 600 }).await?;
screen.arrange(&window.id, Arrange::At { to: Point::new(120, 90) }).await?;
screen.arrange(&window.id, Arrange::Maximise).await?;
screen.focus(&window.id).await?;Applications
Open an application that the box was launched with:
BOX=$(holm new --app text-editor)
holm app "$BOX" text-editor
holm app "$BOX" text-editor /tmp/notes.txtapp returns when the application's window has appeared and stopped changing, so the next screenshot shows it ready. Arguments after the name go to the application, such as a file to open.
| Interface | How |
|---|---|
| CLI | holm app <box> <name> [args…], and holm apps for the names |
| MCP | open_app with app and args, and list_apps for the names |
| REST | The launch action in a batch: {"type": "launch", "app": "text-editor", "args": []} |
| Rust | screen.launch(&Launch { command, class, settle, within }) with the command and window class |
To install applications, see Configure a box.
More than one screen
A box can have up to eight screens. Ask for them when you create the box:
BOX=$(holm new --screens 2)Each screen has its own display, browser, clipboard, and viewer. Screen 0 is the primary screen.
| Interface | Screens it can use |
|---|---|
| REST | Any screen of the box: the {screen} part of each /screens/{screen}/… path. |
| Rust | Any screen, up to eight: computer.screen(ScreenId(1)). |
| CLI | Screen 0 only. |
| MCP | Screen 0 only. |
curl -s "$BASE/v1/boxes/$BOX/screens/1/actions" \
-H 'content-type: application/json' \
-d '{"actions": [{"type": "open_url", "url": "https://example.org"}], "want": ["frame"]}'REST refuses a screen number that is not less than the box's screens value.
let second = computer.screen(ScreenId(1)).await?;
second.open_url("https://example.org").await?;
let png = second.screenshot().await?;In Rust, a screen after screen 0 starts when you first ask for it. screen() leases the screen to your process, so a second caller is refused, not given the same screen.
Wallpaper
Only the Rust API can change the wallpaper:
let image = std::fs::read("background.png")?;
computer.set_wallpaper(&image).await?;
computer.screen(ScreenId(1)).await?.set_wallpaper(&image).await?;Give the bytes of an image file. An empty image is refused.
Record the screen
Recording needs the video feature, which adds ffmpeg. A box has it by default. The recording is an MP4 file in the box, so the frames do not go over the network while it records.
BOX=$(holm new)
holm record "$BOX" start --fps 12
holm record "$BOX" status
holm record "$BOX" stop screen.mp4startfails if the screen is already recording.stopstops ffmpeg and copies the file to the host. With no file name, it writesrecording.mp4.- The file in the box is
/tmp/holm/recording-<screen>.mp4. The nextstarton the same screen deletes it. - On X11, if the box has the
audiofeature, the recording includes the screen's sound. On Wayland, the recording has no sound, and the frames come from repeated screenshots. - The default is 12 frames per second.
| Interface | Start | Stop | Get the file |
|---|---|---|---|
| CLI | record start --fps N |
record stop [file] |
stop copies it. |
| MCP | record with op: "start", fps |
record with op: "stop" |
Not possible. The tool gives the path in the box, for the person to collect. |
| REST | POST …/screens/{n}/recording with {"fps": 12} |
DELETE …/screens/{n}/recording |
GET /v1/boxes/{id}/files?path=… returns it as base64. |
| Rust | start_recording(Some(12)) |
stop_recording() returns the path |
download(path, local) |
MCP read_file refuses files that are not UTF-8 text, so an MCP agent cannot read the video. Give the path to the person.
computer.start_recording(Some(12)).await?;
// drive the box
let inside = computer.stop_recording().await?;
computer.download(&inside, "screen.mp4").await?;Rust also has record(duration, path), which records for a fixed time. It uses X11 capture, so it does not work on Wayland. Use start_recording and stop_recording on Wayland.
For an animated GIF made from screenshots, see examples/recording.rs.