Tool sequences for tasks that take several calls. Each recipe lists the calls in order, what each should return, a final check, and where it tends to go wrong. See tools for each tool's contract.
The tool behavior described here comes from the code. How each app lays out its UI, such as button titles, roles and which process owns a panel, was not observed live and is marked (unverified). When the observed UI differs, follow what the inspection shows.
Conventions:
- inspect means
desktop_inspectwith thepidandwindow_idof the window'stargetfromdesktop_windows. Each completed input action returns a fresh observation, so use itssnapshot_idand element IDs for the next action. Snapshots are single-use. - app target means
{pid, process_start_identity_decimal}fromdesktop_apps. Window target means thetargetobject of adesktop_windowsrow. - After
refusedorunknown, observe again before acting. Never repeat input blindly. - Shell commands are fine for files: checking that a file exists,
cp,hdiutil. They are no substitute for the desktop tools on UI steps.
If the app has File > Export as PDF…, as Preview, Pages, Safari and Notes do, use it: it opens a Save panel directly and skips steps 2 to 5.
desktop_menuwith the app target andpath: ["File", "Print…"]. Menu titles are exact, including the…character; read them withdesktop_menusif unsure. Expectcompleted.desktop_waitwithuntil: "dialog_appears"andpid. Expectmatched. The print panel is a sheet or a window of the app.- inspect the window that holds the panel. Find the PDF menu button at the bottom left (unverified: role
AXMenuButton, titlePDF). desktop_clickthe PDF button. The fresh observation lists the open menu's items first.desktop_element_actionwithaction: "AXPick"on Save as PDF (ordesktop_clickit). A Save panel opens.desktop_dialogwith the app target. Expectis_file_dialogtrue. Use its resolved window target for the next calls.desktop_dialog_answerwithpath: "/Users/me/Desktop/out.pdf". Expectcompleted: the panel reads back the folder and name.desktop_dialog_answerwithbutton: "Save".
Verify: test -s /Users/me/Desktop/out.pdf in a shell, or desktop_wait with until: "dialog_gone" and then check the file. Printing to PDF writes the file a moment after the panel closes.
Goes wrong:
- Chrome, Firefox and Electron apps use their own print preview (unverified). Choose Save as PDF as the destination there instead of the PDF menu.
- More than one dialog open in the app makes
desktop_dialogrefuse; pass the window target. - An existing file at the path makes the panel ask to replace it. Read it with
desktop_dialogand answer deliberately.
An app's File > Open… (⌘O), or its "add a project" or "choose folder" button, shows an Open panel. In a sandboxed app the panel's content is drawn by the open and save panel service, a separate process, while the app lists the panel as its own window.
- Open the panel:
desktop_menuwith the app target andpath: ["File", "Open…"], or click the app's own button. Expectcompleted. desktop_waitwithuntil: "dialog_appears"andpid. Expectmatched.desktop_dialogwith the app target. Expectis_file_dialogtrue, the panel's buttons such as Open and Cancel, and its window target. Use that window target for the next calls.desktop_dialog_answerwithpath: "/Users/me/code/project". This is foreground: it activates the app and types the path into Go to Folder. Expectcompletedonce the panel reads back the folder; an Open panel's file choice isunknown, so read the panel again withdesktop_dialog.desktop_dialog_answerwithbutton: "Open", or the panel's default button's title (some apps name it Choose or Add).
Verify: desktop_wait with until: "dialog_gone", then check the app shows the file or folder.
Goes wrong:
- The panel service's own window is listed under its process, not the app's, with availability
open_save_panel_service. Act on the app's window with the same title and bounds instead. - Background keys don't reach the panel's content, so
desktop_key(such as ⇧⌘G for Go to Folder) and typing are refused. Clicks do work in the background: inspect the app's panel window anddesktop_clicka sidebar location or a row, then press Open withdesktop_dialog_answer. - The panel's sidebar can fill much of an inspection;
desktop_dialoglists the buttons and text fields directly.
- Select what the color applies to: for text in TextEdit,
desktop_selectthe text. desktop_menuwithpath: ["Format", "Font", "Show Colors"], or["Format", "Show Colors"]depending on the app (unverified per app).desktop_waitwithuntil: "window_appears", the app'spidandtitle: "Colors". The panel belongs to the app's own process.- inspect the Colors window. Click the Color Sliders tab in its toolbar (unverified: an
AXRadioButtonorAXButton). Then choose RGB Sliders from the pop-up button, bydesktop_clickanddesktop_element_actionAXPick. desktop_typethe Hex Color # text field (unverified label) withtext: "FF8000". Expectcompletedwith the value read back.desktop_keywithkey: "enter", using the snapshot from step 5's result.
Verify: inspect the panel again and check the hex field's value. Whether the selection took the color can only be seen in the app. Inspect it with ocr off and compare the screenshot, or read an attribute the app exposes (unverified).
Goes wrong:
- The panel applies the color to the current selection or the key window's first responder. If focus moved, the color goes elsewhere or nowhere.
- The panel's layout differs per tab and macOS version.
- Setting the hex field without pressing Enter may not apply it.
- Select the text, as for colors.
desktop_menuwithpath: ["Format", "Font", "Show Fonts"].desktop_waitwithuntil: "window_appears",pidandtitle: "Fonts".- inspect the Fonts window. The family list is long, so either:
- type into the panel's search field to filter it (unverified: an
AXSearchFieldorAXTextField); or - inspect with
scopeset to the family table to list every row.
- type into the panel's search field to filter it (unverified: an
desktop_clickthe family row, then the typeface row, each from the latest observation.desktop_typethe size field withtext: "18", thendesktop_keyenter.
Verify: inspect the panel and check that the family and typeface rows show selected: true and that the size field shows the value. The text itself carries no readable font through desktop_read.
Goes wrong:
- Clicking a row the panel scrolled away from: scope or scroll first, with
desktop_element_actionAXScrollToVisiblewhen the row is listed. - Apps with their own font pop-up in a toolbar or inspector, such as Pages, ignore the Fonts panel for some text. Use their pop-up like any pop-up button.
The simplest verifiable route is the shell: hdiutil attach, then ditto the app into /Applications, then hdiutil detach. The desktop tools are needed for the first-launch prompt either way.
Through Finder:
desktop_openwithitem: {path: "/Users/me/Downloads/App.dmg"}. macOS may verify the image first.desktop_waitwithuntil: "window_appears",bundle_id: "com.apple.finder"andtitle_containsset to the volume name. Allowtimeout_msup to 45000 for a large image.- inspect the volume window. Find the app icon and the Applications alias (unverified: the image's own layout).
desktop_dragwithin that window,fromthe app icon's pointtothe alias's point. It's a background drag in one window.desktop_waitwithuntil: "dialog_gone"and the Finder pid, for the copy progress window (unverified that progress shows as a dialog). Or poll the file withtest -d /Applications/App.app.desktop_launchwithapplication: {path: "/Applications/App.app"}.- The Gatekeeper prompt ("App is an app downloaded from the Internet…") belongs to a system agent, not the app (unverified: CoreServicesUIAgent).
desktop_notificationslists system alerts undersystem_alerts. Read it withdesktop_dialog, using the owner's app target fromdesktop_apps, then press Open withdesktop_dialog_answer. - Eject:
desktop_menuon Finder with["File", "Eject \"Volume\""](unverified title), orhdiutil detachin a shell.
Verify: desktop_wait with until: "app_running" and the app's bundle_id.
Goes wrong:
- Replacing an existing app, or installing a
.pkg, asks for an administrator password. Password entry is not supported, so a person must do it. - If the drag result is
unknown, check the file before dragging again. - The prompt can appear after a delay, or not at all for notarized apps that were already approved. Wait for it with a timeout and treat
timed_outas "no prompt".
A shell cp is simpler. Use this when the copy must go through Finder.
desktop_focusthe destination window target,desktop_clickan empty part of its file list, then inspect it.desktop_insertwithformat: "files"and thepaths. It pastes them with Command-V and restores the clipboard once every name is in the folder.
To copy files picked in Finder instead, desktop_click the first one and the rest with modifiers: ["command"] (foreground, unknown; inspect to check the selection), then desktop_key c with command, and desktop_key v with command in the destination. Add option to move instead of copy.
Verify: the result is completed with copy: "started". Large files may still be copying; ls the destination to check sizes.
Goes wrong:
- A name that already exists is refused with
PASTE_NAME_EXISTSbefore anything is pasted. A manual Command-V would open a Replace / Keep Both / Stop dialog instead. - A drag between Finder windows also copies, but its result is always
unknown.
A shell mv is simpler and can be checked. Finder shows a name written into a file's name field through Accessibility without renaming the file, so desktop_set_cell, desktop_type and desktop_set_value refuse it with CELL_NOT_EDITABLE. In Finder itself the rename is foreground input (unverified):
desktop_focusthe Finder window target,desktop_clickthe file's row, then inspect.desktop_keyenterto edit the name, then inspect.desktop_insertthe new name anddesktop_keyenter.
Verify: ls the folder. The row's text alone doesn't prove the rename.
Goes wrong:
- A name already in the folder makes Finder ask to use another name.
- With the window in the background, the name editor doesn't hold the window's keyboard focus and the insert is refused.
desktop_open or desktop_launch is better when the target is known. Use Spotlight only for what needs its search.
desktop_system_keywithkey: "space"andmodifiers: ["command"]. The result isunknown; nothing reads back a system shortcut.desktop_appswithquery: "Spotlight", thendesktop_windowswith its pid, to find the search window (unverified: on recent macOS the field may belong to another process).desktop_apps'system.focusalso names where keyboard focus is now.- inspect the Spotlight window.
desktop_insertthe query; the field is the focused control. desktop_waitwithuntil: "text_contains"on that window, for a result line.desktop_keywithkey: "enter".
Verify: desktop_wait with until: "app_frontmost" and the expected bundle_id, or window_appears for a document.
Goes wrong:
- Command-Space can be rebound, for example to switch input sources. If no Spotlight window appears, press Escape with
desktop_system_keyand usedesktop_openinstead. - The top hit can change while typing. Read the results before pressing Enter.
desktop_input_sourcewith no arguments lists the enabled sources byid,nameandkind, withcurrent.desktop_input_sourcewithselectset to the wantedid, such ascom.apple.keylayout.French. It reads the source back.- Type, then select the
beforesource again; the switch affects every app.
Verify: the result is completed, and current equals the requested ID.
Goes wrong:
- An unlisted source isn't enabled; the user adds it in System Settings > Keyboard > Text Input.
- An input method's mode may reach the app only after its next focus change. Inspect before typing.
unknownmeans no readback within a second. List again rather than selecting again.desktop_keymaps characters throughkeyboard_layout, so switch before typing with it.
desktop_menuswith the app target andpath: ["File"]. This shows whether Share is a submenu of services or a single Share… command.- With a submenu, run
desktop_menuwithpath: ["File", "Share", "Mail"](the service's exact title). It opens that service directly. - With Share…, run
desktop_menuwithpath: ["File", "Share…"]. A popover with service icons opens.created_windowsmay be empty, because the popover can be hosted by another process (unverified). Find it withdesktop_screen, which lists the windows on the display front to back, then inspect that window anddesktop_clickthe service.
Verify: the service's own window. For Mail, use desktop_wait with until: "window_appears" and bundle_id: "com.apple.mail"; for AirDrop, the AirDrop sheet.
Goes wrong:
- Apps differ in where Share lives: in File, in a toolbar button, or in a context menu. A toolbar Share button opens the same popover as Share….
- The popover closes when focus moves elsewhere. Act on it right after opening it.
To nudge the level, desktop_system_key with volume_up, volume_down, mute, brightness_up or brightness_down. Each press is one step and the result is unknown; mute toggles, so don't repeat it blindly. For an exact level, use Control Center:
desktop_menubar_items, thendesktop_menubar_clickthe item owned bycom.apple.controlcenter. Pick it by owner, not title.- inspect the window the click opened. Find the Sound or Display slider (unverified:
AXSlider, titles vary); its current value shows the range, 0 to 1 or 0 to 100. desktop_set_valuethe slider. Expectcompletedwith the value read back.
Goes wrong:
- Control Center closes when another app takes focus, which makes the snapshot stale. Reopen it and inspect again.
- Some modules open a detail view first, such as Display with an external monitor: click it, then inspect again.
There is no pinch or rotate gesture. Use the app's own command: it acts on an exact target, and you can read the result back.
desktop_menuswith the app target, and look for zoom and rotate items in View, and rotate items in Tools or Image. Common items are Zoom In, Zoom Out, Actual Size and Zoom to Fit in Preview, Safari, Maps and Photos, and Rotate Left and Rotate Right in Preview and Photos (unverified: titles and menus vary by app and macOS version). Each item shows its shortcut.desktop_menuwith the app target and that item's exactpath. Expectcompleted. Repeat it to zoom further, observing between steps.- If the app has no menu item, use the shortcut on the window: inspect, then
desktop_keywith=andcommandto zoom in,-andcommandto zoom out, and0andcommandfor actual size, against that snapshot. In Preview,lwithcommandrotates left andrwithcommandrotates right (unverified). Passrepeatto press a key several times against one snapshot.
Verify: inspect the window. The zoom level often shows in a toolbar control, in the window title or in a status bar, and a rotated image changes its bounds. A menu item that became disabled, such as Zoom In at the largest size, means there is nothing further to do.
Goes wrong:
- Some apps use Command-Shift-= or Command-+ for Zoom In. Read the shortcut from
desktop_menusrather than guessing it. - Command-scroll and Control-scroll zoom only in some apps and only with the feature turned on, so they aren't a general substitute.
- Maps and other canvas views may not report the zoom level anywhere. Compare screenshots with
desktop_inspectin pixels mode instead. - Safari's View > Zoom In zooms the page. Pinch zoom in Safari magnifies the view instead, and no tool reproduces that.
desktop_openwithitem: {url: "https://example.com/form"}and optionallyapplication: {bundle_id: "com.apple.Safari"}.desktop_waitwithuntil: "window_appears", the browser'sbundle_idandtitle_containsset to part of the page title. Thendesktop_waitwithuntil: "element_appears", that window'spidandwindow, and the first field aselement: {role: "AXTextField", title: "Email"}(the field's label as the page exposes it). Its result includes an inspectionsnapshot_idwith the matching element IDs.- If the inspection is
truncated, inspect again withscopeset to the page'sAXWebAreaelement. - For each text field:
desktop_clickthe field. The fresh observation reports it as focused.desktop_typethe field with that observation'ssnapshot_idand element ID. A web field is typed into with key events, so the page seesinputandchange. Expectcompleted; the field is read back.
- For a
<select>:desktop_clickit, thendesktop_element_actionAXPickon the option, ordesktop_clickit. In Chrome and other Chromium browsers, pick with onedesktop_keycall instead: pick from a native select in a browser. - For a checkbox:
desktop_clickit, ordesktop_set_valuetrue(unverified per site: some custom checkboxes ignore Accessibility values). - For a multi-line
<textarea>: click it,desktop_keyawithcommandto select its text, thendesktop_insertwithroute: "paste". Typed text can't contain newlines. desktop_clickthe submit button.
Verify: desktop_wait with until: "text_contains" and the confirmation text, or window_appears with the next page's title. Then desktop_read the page if the outcome needs details.
Goes wrong:
desktop_typeon a web field that isn't focused is refused before any input, with a hint to click it first. Clicking is never done implicitly.- Typed web text is at most 1,000 UTF-16 code units with no newlines or tabs.
- An
unknownresult means the readback didn't match. Autocomplete or input masks can reformat the text, such as phone numbers. Inspect before retrying. - Password fields refuse
desktop_typewithSECURE_FIELD_REQUIRES_FILL; fill them withdesktop_fill_password, preferably from a Keychain item. - An autocomplete popup can take focus after typing. Press
escapewithdesktop_keybefore the next click if it covers the next field.
A <select> shows in Accessibility as an AXPopUpButton. In Chrome and other Chromium browsers, its menu closes between tool calls, so clicking an item or AXPick from a later snapshot is refused, often with SNAPSHOT_STALE. desktop_set_value doesn't apply either: a pop-up button has no settable value. Pick in one desktop_key call instead. Background keys reach Chrome, so the browser can stay behind other apps.
- inspect the browser window. Note the select's current value and check that it has keyboard focus. If not,
desktop_keytabto it, ordesktop_clickit, which also opens its menu. - Find the options. If the menu is open, the inspection lists them as
AXMenuandAXMenuItem, the highlighted one marked selected; if not,desktop_keydownopens it, and the fresh observation lists them. n is the target option's index minus the current option's index. - If the menu is still open, as right after a click, send step 4's keys at once without the first Down. Otherwise it may already have closed by itself, or close it without changing the value with
desktop_keyescape; the select keeps focus. desktop_keywithkeys: [{key: "down"}, …n × {key: "down"}, {key: "enter"}], all in one call. The first Down opens the menu, the next n move the highlight, and Enter picks it.
Verify: inspect the window. The AXPopUpButton's value reads back as the chosen option.
Goes wrong:
- The value didn't change: inspect to check that the select still has focus. As a fallback,
desktop_activatethe browser, inspect, and send the keys again. - Separators and disabled items in the menu are skipped by Down, so count only the items that can be chosen (unverified).
- When n is negative, send that many
upin place of the Downs. - A sequence takes at most 16 keys, so it reaches at most 14 options away.
- Outside Chromium browsers, as in native apps,
desktop_clickthe pop-up button, thendesktop_clickorAXPickthe item from the next observation.
- inspect the browser window. If the inspection is
truncated, inspect again withscopeset to the page'sAXWebAreaelement. - Find the element that holds the text, such as an
AXStaticTextorAXHeading. Pick the smallest one that contains all of it. desktop_selectthat element withtextset to the exact text, andprefixorsuffixif it occurs more than once. Expectcompletedwithselectionread back.desktop_keywithkey: "c",modifiers: ["command"]and the snapshot from step 3's result.desktop_clipboard_read.
Verify: the clipboard text equals the requested text. If you only need the text, desktop_read the window instead; it changes neither the selection nor the clipboard.
Goes wrong:
- Text that spans several elements, such as a paragraph with a link inside, can't be selected in one call. Select within one element, or use
desktop_read. - A long element can be refused when the host can't find the range within its step limit. Select within a smaller element.
unknownmeans the page's selection didn't read back as the requested range, for example because a script changed it. Inspect before retrying.- Some pages block copying, or put other text on the clipboard. Check the clipboard.
desktop_set_cell writes cells that hold a field or a checkbox. Spreadsheet cells such as Numbers' and Excel's usually show only text and are refused, so edit them in place (unverified: which apps expose cell controls).
- inspect the window with
scopeset to the table, thendesktop_readwithtable: trueand that scope. Find the row and column. desktop_clickthe cell'sAXCellelement from that inspection, a child of the row. A single click selects the cell; typing replaces its content.desktop_insertwith the value androute: "type". Expectunknown, since nothing reads the cell during editing.desktop_keywithkey: "enter"to commit. In Numbers and Excel, Return moves to the cell below.
Verify: inspect with scope set to the table again and desktop_read it. The cell shows the value as the app formats it, such as 1,000 for 1000.
Goes wrong:
- A double click edits the existing content instead of replacing it.
- Typing
=starts a formula. The app reformats values it reads as dates or numbers. escapecancels an edit that hasn't been committed.