app.window - Window Management API
List, query, move, resize, and control macOS application windows.
Permission: L2 sensitive, requires system Accessibility permission. The system will prompt for authorization on first use.
Methods
app.window.list(opts?)
Get the list of all visible windows.
Parameters:
opts(table, optional):app(string) - filter by application name (supports partial matching)
Returns: array<table> - each item contains:
title(string) - window titleapp(string) - application namebundleId(string) - application Bundle IDpid(number) - process IDx(number) - X coordinate of the top-left corner (points)y(number) - Y coordinate of the top-left corner (points)width(number) - window width (points)height(number) - window height (points)isMinimized(boolean) - whether the window is minimizedisFullscreen(boolean) - whether the window is in fullscreen mode
-- Get all windows
local windows = app.window.list()
for _, w in ipairs(windows) do
app.log.info(w.app .. ": " .. w.title)
end
-- Get only Safari windows
local safariWindows = app.window.list({app = "Safari"})
app.window.focused()
Get the currently focused (frontmost) window.
Returns: table|nil - window information (same fields as list() items), or nil if no window is focused
local win = app.window.focused()
if win then
app.log.info("Focused window: " .. win.title .. " (" .. win.app .. ")")
end
app.window.move(window, x, y)
Move a window to the specified position.
Parameters:
window- window identifier, supports three forms:{app = "Safari", title = "Window Title"}- match by app name and title{pid = 1234, title = "Window Title"}- match by process ID and title"Window Title"- match by title only (uses first match)
x(number) - target X coordinate (points)y(number) - target Y coordinate (points)
Returns: boolean, error
local ok, err = app.window.move({app = "Safari"}, 0, 0)
if not ok then
app.log.error("Move failed: " .. (err or ""))
end
-- Using process ID
app.window.move({pid = 1234, title = "index.html"}, 100, 100)
-- Match by title only
app.window.move("Untitled", 200, 200)
app.window.resize(window, w, h)
Resize a window.
Parameters:
window- window identifier (same asmove())w(number) - target width (points)h(number) - target height (points)
Returns: boolean, error
local ok, err = app.window.resize({app = "Finder"}, 800, 600)
app.window.setFrame(window, x, y, w, h)
Set both the position and size of a window in one call.
Parameters:
window- window identifier (same asmove())x(number) - X coordinatey(number) - Y coordinatew(number) - widthh(number) - height
Returns: boolean, error
-- Set Safari to the left half of the screen
local screen = app.screen.mainScreen()
local ok, err = app.window.setFrame(
{app = "Safari"},
0, 0,
screen.width / 2, screen.height
)
app.window.minimize(window)
Minimize a window.
Parameters:
window- window identifier (same asmove())
Returns: boolean, error
app.window.minimize({app = "Safari"})
app.window.maximize(window)
Maximize a window (zoom to fill available screen area).
Parameters:
window- window identifier (same asmove())
Returns: boolean, error
app.window.maximize({app = "Safari"})
app.window.close(window)
Close a window.
Parameters:
window- window identifier (same asmove())
Returns: boolean, error
app.window.close({app = "TextEdit", title = "Untitled"})
app.window.focus(window)
Bring a window to the front and focus it.
Parameters:
window- window identifier (same asmove())
Returns: boolean, error
app.window.focus({app = "Finder"})
AX Elements (1.3.0)
The app.window.element.* namespace exposes the Accessibility element tree — use it to read control state, locate buttons/text fields, simulate clicks, and write values. Shares the window permission and Accessibility system authorization.
An element handle looks like {__ax_element_id, __type = "AXElement", role, title, identifier, value, children}. Lua can read the fields directly, but must pass the handle back to click/value/setValue as-is.
app.window.element.tree(window, opts?)
Recursively read the AX element tree for a window (since 1.3.0).
Parameters:
window- window descriptor (same asapp.window.move())opts(table, optional)maxDepth(number) - recursion depth (default 10)maxChildren(number) - children limit per level (default 200)
Returns: handle - root node with nested children array
local tree = app.window.element.tree({app = "Calculator"})
for _, btn in ipairs(tree.children) do
app.log.info(btn.role .. " / " .. btn.title)
end
app.window.element.find(window, filter)
Search the element tree by filter, returning matching handles (since 1.3.0).
Parameters:
window- window descriptorfilter(table)role(string, optional) - exact AXRole match (e.g."AXButton")title(string, optional) - title contains substring (case-insensitive)identifier(string, optional) - exact AXIdentifier matchlimit(number, optional) - max results (default 50)maxDepth(number, optional) - search depth (default 15)
Returns: array<handle>, err
local buttons = app.window.element.find({app = "Calculator"}, {
role = "AXButton", title = "1"
})
app.window.element.click(handle)
Perform the AXPress action on the element (since 1.3.0).
local ok = app.window.element.click(buttons[1])
app.window.element.value(handle)
Read the element’s AXValue (since 1.3.0). CGPoint/CGSize/CGRect/CFRange are converted to tables; other values stay as strings or numbers.
local v = app.window.element.value(handle)
app.window.element.setValue(handle, value)
Write the element’s AXValue (since 1.3.0). Primarily useful for text fields, sliders, and other writable controls.
app.window.element.setValue(inputField, "hello")
Description
- All window operations require system Accessibility permission, which can be granted in System Settings → Privacy & Security → Accessibility
- The
titlefield in window identifiers supports partial matching; if multiple windows match, the first match is used maximize()is equivalent to double-clicking the title bar zoom button, not the same as entering fullscreen mode- The coordinate system has its origin at the top-left corner of the main display, with positive directions going right and down (points, not pixels)
- Minimized windows cannot be operated on via
move()/resize(); callfocus()first to restore them
Examples
Organize Window Layout
function MyPlugin:handleOrganizeWindows(context)
local screen = app.screen.mainScreen()
local w = screen.width / 2
local h = screen.height
-- Safari on the left half
app.window.setFrame({app = "Safari"}, 0, 0, w, h)
-- VS Code on the right half
app.window.setFrame({app = "Code"}, w, 0, w, h)
app.notification.show("Windows Organized", "Left: Safari, Right: VS Code")
end
List All Window Information
function MyPlugin:handleListWindows(context)
local windows = app.window.list()
local lines = {}
for _, win in ipairs(windows) do
local size = string.format("%dx%d @ (%d,%d)", win.width, win.height, win.x, win.y)
table.insert(lines, string.format("[%s] %s — %s", win.app, win.title, size))
end
local result = table.concat(lines, "\n")
app.dialog.alert("Windows (" .. #windows .. ")", result)
end