SPB Git

Repository Public Research preview

applescript-recipes.md 8.1 KB

# AppleScript recipes

Each file in scripts/recipes/ is a complete program for osascript, tested against Safari on a local site. Values are passed as arguments, never pasted into source. Typical sequence:

bash
W=$(osascript scripts/recipes/open-window.applescript "https://example.org/")
osascript scripts/recipes/wait-for-load.applescript "$W" 1 30
osascript scripts/recipes/get-text.applescript "$W" 1
osascript scripts/recipes/fill-field.applescript "$W" 1 'input[name="q"]' "tide tables"
osascript scripts/recipes/click.applescript "$W" 1 'button[type="submit"]'
osascript scripts/recipes/wait-for-load.applescript "$W" 1 30
osascript scripts/recipes/fetch-base64.applescript "$W" 1 /export.csv | base64 -D > export.csv
screencapture -x -o -l "$W" page.png        # the window id is the system window number
osascript scripts/recipes/close-window.applescript "$W"

Keep the window id from the first step and use it everywhere. Wait after every step that navigates. fill-field and click print not-found when the selector matches nothing: check it.

# open-window.applescript

applescript
-- Open a URL in a NEW Safari window and print that window's id.
-- Usage: osascript open-window.applescript "https://example.com/"
-- Keep the id: it is stable for the life of the window, unlike "window 1" or "front window",
-- which change whenever the user clicks another window.
on run argv
	tell application "Safari"
		make new document with properties {URL:(item 1 of argv)}
		return id of window 1
	end tell
end run

# wait-for-load.applescript

applescript
-- Wait until a tab has finished navigating. Prints "loaded" or "timeout".
-- Usage: osascript wait-for-load.applescript <window-id> <tab-index> [timeout-seconds]
-- Safari sets "URL of tab" to the destination as soon as a navigation starts, while the old
-- document is still the one that answers JavaScript. The page is ready when the document's own
-- URL has caught up AND readyState is "complete" — checking readyState alone reports the OLD page.
on run argv
	set w to (item 1 of argv) as integer
	set t to (item 2 of argv) as integer
	set limit to 30
	if (count of argv) > 2 then set limit to (item 3 of argv) as integer
	set deadline to (current date) + limit
	set calm to 0
	repeat while (current date) < deadline
		try
			tell application "Safari"
				set tabURL to URL of tab t of window id w
				set state to do JavaScript "location.href + '\\n' + document.readyState" in tab t of window id w
			end tell
			if state is (tabURL & linefeed & "complete") then
				set calm to calm + 1
				if calm ≥ 2 then return "loaded"
			else
				set calm to 0
			end if
		on error
			set calm to 0 -- the tab is between documents: try again
		end try
		delay 0.2
	end repeat
	return "timeout"
end run

# list-tabs.applescript

applescript
-- List the tabs of one window: index, URL and title, tab-separated, one per line.
-- Usage: osascript list-tabs.applescript <window-id>
on run argv
	set w to (item 1 of argv) as integer
	set out to ""
	-- Inside a Safari "tell" block the word "tab" means a browser tab, not the tab character:
	-- name the separator outside it.
	set sep to character id 9
	tell application "Safari"
		repeat with t in tabs of window id w
			set u to URL of t
			if u is missing value then set u to ""
			set n to name of t
			if n is missing value then set n to ""
			set out to out & (index of t) & sep & u & sep & n & (character id 10)
		end repeat
	end tell
	return out
end run

# run-js.applescript

applescript
-- Run JavaScript in a tab and print the result.
-- Usage: osascript run-js.applescript <window-id> <tab-index> '<javascript>'
-- The code travels as an argument, so it needs no AppleScript escaping.
-- Return a string (JSON.stringify(...)): objects, undefined and promises come back empty.
on run argv
	set w to (item 1 of argv) as integer
	set t to (item 2 of argv) as integer
	tell application "Safari"
		set r to do JavaScript (item 3 of argv) in tab t of window id w
	end tell
	try
		return r
	on error
		return ""
	end try
end run

# get-text.applescript

applescript
-- Print the visible text of a tab.
-- Usage: osascript get-text.applescript <window-id> <tab-index>
on run argv
	set w to (item 1 of argv) as integer
	set t to (item 2 of argv) as integer
	tell application "Safari"
		return do JavaScript "document.body ? document.body.innerText : ''" in tab t of window id w
	end tell
end run

# fill-field.applescript

applescript
-- Set the value of a form field so that the page's own code notices.
-- Usage: osascript fill-field.applescript <window-id> <tab-index> '<css-selector>' '<value>'
-- Assigning el.value alone is ignored by React, Vue and similar frameworks: they patch the
-- property. Calling the native setter and then firing "input" and "change" works everywhere.
-- Prints "ok", or "not-found".
on run argv
	set w to (item 1 of argv) as integer
	set t to (item 2 of argv) as integer
	set js to "(function(sel, val){var el=document.querySelector(sel); if(!el) return 'not-found';" & ¬
		"var proto = el.tagName==='TEXTAREA' ? HTMLTextAreaElement.prototype : el.tagName==='SELECT' ? HTMLSelectElement.prototype : HTMLInputElement.prototype;" & ¬
		"Object.getOwnPropertyDescriptor(proto,'value').set.call(el, val);" & ¬
		"el.dispatchEvent(new Event('input',{bubbles:true})); el.dispatchEvent(new Event('change',{bubbles:true})); return 'ok';})"
	-- JSON.stringify-style quoting of the two arguments is done in JavaScript, not here.
	set call to js & "(decodeURIComponent('" & my encode(item 3 of argv) & "'), decodeURIComponent('" & my encode(item 4 of argv) & "'))"
	tell application "Safari"
		return do JavaScript call in tab t of window id w
	end tell
end run

-- Percent-encode every byte, so no character of the value can break out of the JavaScript string.
on encode(s)
	return do shell script "/usr/bin/python3 -c 'import sys,urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=\"\"))' " & quoted form of s
end encode

# click.applescript

applescript
-- Click the first element matching a CSS selector. Prints "ok" or "not-found".
-- Usage: osascript click.applescript <window-id> <tab-index> '<css-selector>'
-- el.click() runs the page's handlers and follows links. It does not move the real pointer.
on run argv
	set w to (item 1 of argv) as integer
	set t to (item 2 of argv) as integer
	set sel to do shell script "/usr/bin/python3 -c 'import sys,urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=\"\"))' " & quoted form of (item 3 of argv)
	tell application "Safari"
		return do JavaScript "(function(s){var el=document.querySelector(s); if(!el) return 'not-found'; el.scrollIntoView({block:'center'}); el.click(); return 'ok';})(decodeURIComponent('" & sel & "'))" in tab t of window id w
	end tell
end run

# fetch-base64.applescript

applescript
-- Download a URL from inside the page — with the site's cookies and sign-in — and print it as
-- base64. Pipe the output into a file:
--   osascript fetch-base64.applescript <window-id> <tab-index> '/path/or/url' | base64 -D > file.bin
-- Prints "HTTP <status>" on stderr-like first line when the server refuses. Same-origin URLs only
-- (the browser's own rules). Suited to files up to a few megabytes; larger ones need chunking.
on run argv
	set w to (item 1 of argv) as integer
	set t to (item 2 of argv) as integer
	set u to do shell script "/usr/bin/python3 -c 'import sys,urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=\"\"))' " & quoted form of (item 3 of argv)
	-- do JavaScript cannot wait for a promise, so the request is synchronous.
	set js to "(function(u){var x=new XMLHttpRequest(); x.open('GET',u,false); x.overrideMimeType('text/plain; charset=x-user-defined'); x.send();" & ¬
		"if(x.status<200||x.status>=300) return 'HTTP '+x.status; var s=x.responseText, b=''; for(var i=0;i<s.length;i++) b+=String.fromCharCode(s.charCodeAt(i)&255); return btoa(b);})(decodeURIComponent('" & u & "'))"
	tell application "Safari"
		return do JavaScript js in tab t of window id w
	end tell
end run

# close-window.applescript

applescript
-- Close a window by id. Usage: osascript close-window.applescript <window-id>
on run argv
	tell application "Safari" to close window id ((item 1 of argv) as integer)
end run
Files main
Menu

Theme