holm

Windows and screens

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" active

Each 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" close

Use 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 10

The 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.txt

app 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.mp4
  • start fails if the screen is already recording.
  • stop stops ffmpeg and copies the file to the host. With no file name, it writes recording.mp4.
  • The file in the box is /tmp/holm/recording-<screen>.mp4. The next start on the same screen deletes it.
  • On X11, if the box has the audio feature, 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.