Random Unsplash Image widget screenshot

Script

Random Unsplash Image

by Brett Terpstra

Fetch a random photo from Unsplash (optionally filtered by keyword) and display it in a Terminal Widget with --image.

Requirements

Demo apps are rate-limited (about 50 requests/hour). A 15-minute schedule uses 4 requests/hour per job — fine for a couple of widgets; don’t poll every minute.

1. Create an Unsplash Access Key

  1. Sign in (or register) at unsplash.com/developers.
  2. Click Your appsNew Application.
  3. Accept the API terms and create an app (any descriptive name is fine, e.g. “Terminal Widget”).
  4. Open the app and copy the Access Key (not the Secret Key).
    The Secret is only needed for OAuth user login; this recipe uses the Access Key alone.
  5. Store the key only in your local config file — never in the script or a public gist.

2. Create unsplash.env

mkdir -p ~/.config/terminal-widget
chmod 700 ~/.config/terminal-widget

Create ~/.config/terminal-widget/unsplash.env:

# Required
export UNSPLASH_ACCESS_KEY='your_access_key_here'

# Optional defaults when the script is run with no arguments
# export UNSPLASH_TARGET='unsplash'
# export UNSPLASH_QUERY='color splash'   # or omit / use "-" for fully random
# export UNSPLASH_ORIENTATION='landscape'  # landscape | portrait | squarish

# Optional: path to the CLI if it is not on PATH
# export TERMINAL_WIDGET='/opt/homebrew/bin/terminal-widget'

Lock it down:

chmod 600 ~/.config/terminal-widget/unsplash.env

Alternate config path: set UNSPLASH_ENV=/path/to/your.env before running the script.

3. Add a widget target

In Terminal Widget, create a target for the image. Defaults:

Target ID Purpose

| unsplash | Default when no CLI args / env overrides are set | For keyword-specific widgets, use distinct targets such as unsplash-color-splash or unsplash-ocean so each LaunchAgent can update its own widget.

4. Install and run

Save the script below as ~/bin/unsplash-random.sh (or wherever you keep widget scripts), then:

chmod +x ~/bin/unsplash-random.sh

Run once to verify:

~/bin/unsplash-random.sh
# or with an explicit target + keyword:
~/bin/unsplash-random.sh unsplash-color-splash "color splash" landscape

Add a Desktop / Notification Center widget and choose that target.

5. Schedule updates

Enable the launchd section on this recipe page (suggested interval: 15 minutes). That installs a basic agent that runs the script with no arguments, using your unsplash.env defaults.

Customize the LaunchAgent (target + keyword)

After you create the plist from the launchd section, edit ProgramArguments so the script receives target, keyword, and orientation. Example:

<key>ProgramArguments</key>
<array>
  <string>/bin/bash</string>
  <string>/Users/YOU/bin/unsplash-random.sh</string>
  <string>unsplash-color-splash</string>
  <string>color splash</string>
  <string>landscape</string>
</array>
Argument Meaning
1 — target Terminal Widget target name
2 — keyword Search string, or - for fully random
3 — orientation landscape, portrait, or squarish

Reload after editing (or use LaunchControl to edit and restart the job):

launchctl bootout "gui/$(id -u)/com.terminalwidget.unsplash-random" 2>/dev/null || true
launchctl bootstrap "gui/$(id -u)" ~/Library/LaunchAgents/com.terminalwidget.unsplash-random.plist

(Use the Label from your plist if it differs.)

Multiple widgets with different keywords

  1. Duplicate the LaunchAgent plist.
  2. Give it a unique Label (and unique log paths).
  3. Change the three script arguments (target + keyword + orientation).
  4. Create a matching widget target in Terminal Widget.
  5. Bootstrap the new plist.

Example second job: target unsplash-ocean, keyword ocean, orientation landscape.

Environment variables reference

Variable Required Default Description
UNSPLASH_ACCESS_KEY Yes Access Key from your Unsplash application
UNSPLASH_TARGET No unsplash Widget target when arg 1 is omitted
UNSPLASH_QUERY No - (random) Search keyword when arg 2 is omitted
UNSPLASH_ORIENTATION No landscape Orientation when arg 3 is omitted
UNSPLASH_ENV No ~/.config/terminal-widget/unsplash.env Config file path
TERMINAL_WIDGET No terminal-widget Path to the CLI

Troubleshooting

Symptom Likely cause
Widget shows “UNSPLASH_ACCESS_KEY not set” Missing or empty unsplash.env, or wrong UNSPLASH_ENV path
Widget shows “API request failed” / rate errors Demo limit (50/hour); space out jobs or request a production upgrade from Unsplash
Image never changes Same keyword pool is small, or LaunchAgent isn’t running — check /tmp/com.terminalwidget.…err
terminal-widget not found under launchd Set TERMINAL_WIDGET=/opt/homebrew/bin/terminal-widget in unsplash.env, or add Homebrew to the plist EnvironmentVariables PATH
Tap does nothing See Debugging widget taps below

Debugging widget taps

run-command runs inside the sandboxed Terminal Widget app as /bin/zsh -lc '…'. That process:

  • Uses a container HOME (not your real home directory)
  • Cannot execute files under ~/Scripts or /opt/homebrew/bin (operation not permitted)
  • Can write under the App Group

So a tap that runs this Unsplash script directly will fail. Prefer run-shortcut (below).

To confirm a tap is reaching the app, temporarily set:

terminal-widget --target YOUR_TARGET \
  --action-kind run-command \
  --action-value 'echo tapped-$(date) >>"$HOME/../../../../../Group Containers/group.brettterpstra.TerminalWidget/logs/tap-probe.log"'

(or patch the payload action to echo tapped >>"/Users/YOU/Library/Group Containers/group.brettterpstra.TerminalWidget/logs/tap-probe.log"), click the widget, then:

tail -f ~/Library/Group\ Containers/group.brettterpstra.TerminalWidget/logs/tap-probe.log

Also check the Terminal Widget menu bar app status panel for the last opened URL (terminalwidget://action?target=…).

  1. In Shortcuts, create Unsplash Widget Refresh with a Run Shell Script action:
/Users/YOU/Scripts/widget-unsplash.sh unsplash-color-splash "color splash" landscape
  1. Run it once and allow any prompts.
  2. Point the widget at the Shortcut instead of run-command:
terminal-widget --target unsplash-color-splash \
  --action-kind run-shortcut \
  --action-value 'Unsplash Widget Refresh'

(Re-apply after each image update, or bake --action-kind run-shortcut into the script.)

Script (random-unsplash.sh)

#!/usr/bin/env bash
# Random Unsplash photo → Terminal Widget (--image).
# Config: ~/.config/terminal-widget/unsplash.env (see recipe).
#
# Usage:
#   unsplash-random.sh [TARGET] [KEYWORD|-] [ORIENTATION]
#
# Args override env defaults. Use "-" for KEYWORD to mean fully random.
# Duplicate a LaunchAgent and change TARGET + KEYWORD for multiple widgets.
#
# Tap refresh: set UNSPLASH_TAP_SHORTCUT in unsplash.env to a Shortcuts
# name that runs this script. Do not use run-command — the sandboxed app
# cannot exec ~/Scripts or Homebrew binaries.
set -euo pipefail

# launchd / fdautil often have a minimal PATH
export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin${PATH:+:$PATH}"

UNSPLASH_ENV="${UNSPLASH_ENV:-$HOME/.config/terminal-widget/unsplash.env}"
if [[ -f "$UNSPLASH_ENV" ]]; then
	# shellcheck source=/dev/null
	source "$UNSPLASH_ENV"
fi

if [[ -z "${TERMINAL_WIDGET:-}" ]]; then
	if [[ -x /opt/homebrew/bin/terminal-widget ]]; then
		TW=/opt/homebrew/bin/terminal-widget
	elif [[ -x /usr/local/bin/terminal-widget ]]; then
		TW=/usr/local/bin/terminal-widget
	else
		TW=terminal-widget
	fi
else
	TW="$TERMINAL_WIDGET"
fi

TARGET="${1:-${UNSPLASH_TARGET:-unsplash}}"
QUERY="${2:-${UNSPLASH_QUERY:--}}"
ORIENTATION="${3:-${UNSPLASH_ORIENTATION:-landscape}}"
[[ "$QUERY" == "-" ]] && QUERY=""
[[ "$ORIENTATION" == "-" ]] && ORIENTATION=""

widget_error() {
	local msg="$1"
	echo "Unsplash: $msg" >&2
	if command -v "$TW" >/dev/null 2>&1; then
		"$TW" --target "$TARGET" \
			--icon exclamationmark.triangle.fill \
			--background "#422006" \
			--foreground "#fde68a" \
			--text "Unsplash: $msg" || true
	fi
}

if [[ -z "${UNSPLASH_ACCESS_KEY:-}" ]]; then
	widget_error "UNSPLASH_ACCESS_KEY not set"
	exit 1
fi

for cmd in jq curl; do
	if ! command -v "$cmd" >/dev/null 2>&1; then
		widget_error "$cmd required"
		exit 1
	fi
done

if ! command -v "$TW" >/dev/null 2>&1; then
	widget_error "terminal-widget not found (set TERMINAL_WIDGET)"
	exit 1
fi

API="https://api.unsplash.com/photos/random"
PARAMS=()
[[ -n "$QUERY" ]] && PARAMS+=("query=$(printf %s "$QUERY" | jq -sRr @uri)")
[[ -n "$ORIENTATION" ]] && PARAMS+=("orientation=$(printf %s "$ORIENTATION" | jq -sRr @uri)")
if ((${#PARAMS[@]})); then
	API+="?$(IFS='&'; echo "${PARAMS[*]}")"
fi

if ! JSON=$(curl -fsS \
	-H "Authorization: Client-ID ${UNSPLASH_ACCESS_KEY}" \
	-H "Accept-Version: v1" \
	"$API"); then
	widget_error "API request failed"
	exit 1
fi

IMAGE_URL=$(echo "$JSON" | jq -r '.urls.regular // .urls.small // empty')
if [[ -z "$IMAGE_URL" || "$IMAGE_URL" == "null" ]]; then
	ERR=$(echo "$JSON" | jq -r '.errors[0] // .message // "no image url"' 2>/dev/null || echo "no image url")
	widget_error "$ERR"
	exit 1
fi

# Unsplash guideline: trigger download_location when displaying a photo
DOWNLOAD_LOC=$(echo "$JSON" | jq -r '.links.download_location // empty')
if [[ -n "$DOWNLOAD_LOC" && "$DOWNLOAD_LOC" != "null" ]]; then
	curl -fsS -H "Authorization: Client-ID ${UNSPLASH_ACCESS_KEY}" "$DOWNLOAD_LOC" >/dev/null || true
fi

ACTION_ARGS=()
if [[ -n "${UNSPLASH_TAP_SHORTCUT:-}" ]]; then
	ACTION_ARGS=(--action-kind run-shortcut --action-value "$UNSPLASH_TAP_SHORTCUT")
fi

"$TW" \
	--target "$TARGET" \
	--image "$IMAGE_URL" \
	--padding fill \
	"${ACTION_ARGS[@]}"

Download script

Running in the background with launchd

Suggested interval: 15 minutes (StartInterval = 900 seconds).

Save the script from this recipe to ~/bin/random-unsplash-image.sh, then create a Launch Agent plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>com.terminalwidget.random-unsplash-image</string>
  <key>ProgramArguments</key>
  <array>
    <string>/bin/bash</string>
    <string>~/bin/random-unsplash-image.sh</string>
  </array>
  <key>RunAtLoad</key>
  <true/>
  <key>StartInterval</key>
  <integer>900</integer>
  <key>StandardOutPath</key>
  <string>/tmp/com.terminalwidget.random-unsplash-image.log</string>
  <key>StandardErrorPath</key>
  <string>/tmp/com.terminalwidget.random-unsplash-image.err</string>
</dict>
</plist>

From Terminal:

mkdir -p ~/bin ~/Library/LaunchAgents
# Save your script to ~/bin/random-unsplash-image.sh
cat > ~/Library/LaunchAgents/com.terminalwidget.random-unsplash-image.plist <<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>com.terminalwidget.random-unsplash-image</string>
  <key>ProgramArguments</key>
  <array>
    <string>/bin/bash</string>
    <string>~/bin/random-unsplash-image.sh</string>
  </array>
  <key>RunAtLoad</key>
  <true/>
  <key>StartInterval</key>
  <integer>900</integer>
  <key>StandardOutPath</key>
  <string>/tmp/com.terminalwidget.random-unsplash-image.log</string>
  <key>StandardErrorPath</key>
  <string>/tmp/com.terminalwidget.random-unsplash-image.err</string>
</dict>
</plist>
EOF
launchctl bootstrap "gui/$(id -u)" ~/Library/LaunchAgents/com.terminalwidget.random-unsplash-image.plist

For a GUI editor and troubleshooting, see LaunchControl from soma-zone.

← All recipes