#!/bin/sh # morning-masterpiece-wallpaper: set today's Morning Masterpiece artwork as the # desktop wallpaper and keep it fresh. https://morningmasterpiece.com # # Copyright (c) 2026 Morning Masterpiece. Released under the MIT License. # # Plain POSIX sh for Linux and macOS. Needs curl or wget, nothing else. # Privacy: the only network requests are HTTPS GETs for today.json and the # wallpaper image on morningmasterpiece.com. Nothing is collected or sent. VERSION=1.0.0 PROG=morning-masterpiece-wallpaper SITE=https://morningmasterpiece.com MEDIA_PREFIX=https://morningmasterpiece.com/ DEFAULT_API_URL=https://morningmasterpiece.com/api/today.json USER_AGENT="$PROG/$VERSION" LAUNCHD_LABEL=com.morningmasterpiece.wallpaper UNIT=morning-masterpiece-wallpaper BACKENDS="auto omarchy hyprpaper swaybg sway gnome cinnamon mate kde xfce feh macos custom none" # ---------------------------------------------------------------- output --- QUIET=0 DRY_RUN=0 say() { [ "$QUIET" = 1 ] || printf '%s\n' "$*"; } note() { printf '%s\n' "$*"; } warn() { printf '%s: %s\n' "$PROG" "$*" >&2; } have() { command -v "$1" >/dev/null 2>&1; } # Run a command, or only print it with --dry-run. run() { if [ "$DRY_RUN" = 1 ]; then printf 'dry-run: %s\n' "$*" return 0 fi "$@" } usage() { cat </dev/null) in Darwin) echo macos ;; Linux) echo linux ;; *BSD | DragonFly) echo bsd ;; *) echo other ;; esac } init_paths() { OS=$(detect_os) if [ "$OS" = macos ]; then _cache_default="$HOME/Library/Caches/$PROG" else _cache_default="${XDG_CACHE_HOME:-$HOME/.cache}/$PROG" fi CACHE_DIR=${MM_WALLPAPER_CACHE_DIR:-$_cache_default} CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/$PROG" CONFIG_FILE=${MM_WALLPAPER_CONFIG:-$CONFIG_DIR/config} BIN_DIR=${MM_WALLPAPER_BIN_DIR:-$HOME/.local/bin} SYSTEMD_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user" LAUNCHD_PLIST="$HOME/Library/LaunchAgents/$LAUNCHD_LABEL.plist" STATE_FILE="$CACHE_DIR/state" META_FILE="$CACHE_DIR/today.json" API_URL=${MM_WALLPAPER_API_URL:-$DEFAULT_API_URL} } # Paths end up in hyprctl/swaymsg/systemd/cron syntax; keep them boring. NL=' ' path_is_safe() { case $1 in *[\"\'\\%,]* | *"$NL"*) return 1 ;; esac return 0 } # -------------------------------------------------------------- settings --- to_bool() { case $(printf '%s' "$1" | tr '[:upper:]' '[:lower:]') in 1 | yes | true | on) echo 1 ;; 0 | no | false | off) echo 0 ;; *) return 1 ;; esac } set_defaults() { S_BACKEND=auto S_MODE=fill S_VARIANT=desktop S_NOTIFY=1 S_KEEP=7 S_COMMAND= } # Assign one setting; prints an error and returns 1 if the value is invalid. set_setting() { _key=$1 _val=$2 _src=$3 case $_key in backend) case " $BACKENDS " in *" $_val "*) S_BACKEND=$_val ;; *) warn "$_src: unknown backend '$_val' (choose from: $BACKENDS)" && return 1 ;; esac ;; mode) case $_val in fill | fit | stretch | center | tile) S_MODE=$_val ;; *) warn "$_src: unknown mode '$_val' (fill, fit, stretch, center, tile)" && return 1 ;; esac ;; variant) case $_val in desktop | phone) S_VARIANT=$_val ;; *) warn "$_src: variant must be desktop or phone" && return 1 ;; esac ;; notify) S_NOTIFY=$(to_bool "$_val") || { warn "$_src: notify must be yes or no" return 1 } ;; keep) case $_val in '' | *[!0-9]*) warn "$_src: keep must be a number" && return 1 ;; esac if [ "$_val" -lt 1 ] || [ "$_val" -gt 365 ]; then warn "$_src: keep must be between 1 and 365" return 1 fi S_KEEP=$_val ;; command) S_COMMAND=$_val ;; *) warn "$_src: unknown setting '$_key'" && return 1 ;; esac return 0 } # Config file: "key = value" lines, '#' comments. Parsed, never sourced. load_config() { [ -f "$CONFIG_FILE" ] || return 0 _cfg_ok=0 _lineno=0 while IFS= read -r _line || [ -n "$_line" ]; do _lineno=$((_lineno + 1)) _line=$(printf '%s' "$_line" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') case $_line in '' | '#'*) continue ;; *=*) ;; *) warn "$CONFIG_FILE:$_lineno: expected key = value" _cfg_ok=1 continue ;; esac _k=$(printf '%s' "${_line%%=*}" | sed 's/[[:space:]]*$//') _v=$(printf '%s' "${_line#*=}" | sed 's/^[[:space:]]*//') case $_v in \"*\") _v=${_v#\"} && _v=${_v%\"} ;; esac set_setting "$_k" "$_v" "$CONFIG_FILE:$_lineno" || _cfg_ok=1 done <"$CONFIG_FILE" return $_cfg_ok } load_env_settings() { _env_ok=0 [ -z "${MM_WALLPAPER_BACKEND:-}" ] || set_setting backend "$MM_WALLPAPER_BACKEND" MM_WALLPAPER_BACKEND || _env_ok=1 [ -z "${MM_WALLPAPER_MODE:-}" ] || set_setting mode "$MM_WALLPAPER_MODE" MM_WALLPAPER_MODE || _env_ok=1 [ -z "${MM_WALLPAPER_VARIANT:-}" ] || set_setting variant "$MM_WALLPAPER_VARIANT" MM_WALLPAPER_VARIANT || _env_ok=1 [ -z "${MM_WALLPAPER_NOTIFY:-}" ] || set_setting notify "$MM_WALLPAPER_NOTIFY" MM_WALLPAPER_NOTIFY || _env_ok=1 [ -z "${MM_WALLPAPER_KEEP:-}" ] || set_setting keep "$MM_WALLPAPER_KEEP" MM_WALLPAPER_KEEP || _env_ok=1 [ -z "${MM_WALLPAPER_COMMAND:-}" ] || S_COMMAND=$MM_WALLPAPER_COMMAND return $_env_ok } # Rewrite (or append) one key in the config file. config_set() { mkdir -p "$(dirname "$CONFIG_FILE")" || return 1 _tmp="$CONFIG_FILE.tmp.$$" if [ -f "$CONFIG_FILE" ]; then grep -v "^[[:space:]]*$1[[:space:]]*=" "$CONFIG_FILE" >"$_tmp" else printf '# %s configuration (see README)\n' "$PROG" >"$_tmp" fi printf '%s = %s\n' "$1" "$2" >>"$_tmp" && mv -f "$_tmp" "$CONFIG_FILE" } # ------------------------------------------------------------------ json --- # Minimal JSON reader: prints the scalar at a dotted path ("artwork.title", # "images.web.w640", "list[0]"). Exit 1 if missing/null, 2 if malformed. # Output is printf %b-ready: \u escapes become \0ooo octal UTF-8 bytes, so # it works the same in every awk regardless of locale. # shellcheck disable=SC2016 # awk program, not shell JSON_AWK=' function skip( c) { while (pos <= len) { c = substr(s, pos, 1) if (c == " " || c == "\t" || c == "\n" || c == "\r") pos++ else break } } function fail() { bad = 1; pos = len + 2 } function hexv(h, i, c, v) { if (length(h) != 4) return -1 v = 0; h = tolower(h) for (i = 1; i <= 4; i++) { c = index("0123456789abcdef", substr(h, i, 1)) if (!c) return -1 v = v * 16 + c - 1 } return v } function oct(b) { return sprintf("\\0%o", b) } function enc(cp) { if (cp < 32 || cp == 127) return (cp == 9 || cp == 10 || cp == 13) ? " " : "" if (cp == 92) return "\\\\" if (cp < 128) return sprintf("%c", cp) if (cp < 160) return "" if (cp < 2048) return oct(192 + int(cp / 64)) oct(128 + cp % 64) if (cp < 65536) return oct(224 + int(cp / 4096)) oct(128 + int(cp / 64) % 64) oct(128 + cp % 64) return oct(240 + int(cp / 262144)) oct(128 + int(cp / 4096) % 64) oct(128 + int(cp / 64) % 64) oct(128 + cp % 64) } function str( out, rest, c, e, cp, lo) { pos++; out = "" while (pos <= len) { rest = substr(s, pos) if (!match(rest, /["\\]/)) break out = out substr(rest, 1, RSTART - 1) pos += RSTART - 1 if (substr(s, pos, 1) == "\"") { pos++; return out } e = substr(s, pos + 1, 1); pos += 2 if (e == "u") { cp = hexv(substr(s, pos, 4)) if (cp < 0) { fail(); return "" } pos += 4 if (cp >= 55296 && cp < 56320 && substr(s, pos, 2) == "\\u") { lo = hexv(substr(s, pos + 2, 4)) if (lo >= 56320 && lo < 57344) { cp = 65536 + (cp - 55296) * 1024 + (lo - 56320); pos += 6 } } if (cp >= 55296 && cp < 57344) cp = 65533 out = out enc(cp) } else if (e == "n" || e == "t" || e == "r") out = out " " else if (e == "b" || e == "f") out = out "" else if (e == "\\") out = out "\\\\" else if (e == "\"" || e == "/") out = out e else { fail(); return "" } } fail(); return "" } function val(path, c, k, i, v) { skip(); c = substr(s, pos, 1) if (c == "{") { pos++; skip() if (substr(s, pos, 1) == "}") { pos++; return } while (pos <= len) { skip() if (substr(s, pos, 1) != "\"") { fail(); return } k = str(); skip() if (substr(s, pos, 1) != ":") { fail(); return } pos++ val(path == "" ? k : (path "." k)) skip(); c = substr(s, pos, 1); pos++ if (c == "}") return if (c != ",") { fail(); return } } fail(); return } if (c == "[") { pos++; skip(); i = 0 if (substr(s, pos, 1) == "]") { pos++; return } while (pos <= len) { val(path "[" i "]"); i++ skip(); c = substr(s, pos, 1); pos++ if (c == "]") return if (c != ",") { fail(); return } } fail(); return } if (c == "\"") { v = str() if (path == want) { found = 1; out = v } return } if (match(substr(s, pos), /^[-+.0-9eEa-z]+/)) { v = substr(s, pos, RLENGTH); pos += RLENGTH if (v != "null" && v != "true" && v != "false" && v !~ /^-?[0-9]/) { fail(); return } if (path == want) { if (v == "null") found = 0; else { found = 1; out = v } } return } fail() } { s = s $0 "\n" } END { len = length(s); pos = 1 val(""); skip() if (bad || pos <= len) exit 2 if (!found) exit 1 printf "%s", out exit 0 }' json_get() { _jv=$(LC_ALL=C awk -v want="$2" "$JSON_AWK" "$1") || return $? printf '%b' "$_jv" } # Strip control characters so remote text cannot mess with the terminal. clean_text() { printf '%s' "$1" | tr '\n\r\t' ' ' | LC_ALL=C tr -d '\000-\037\177' } # ------------------------------------------------------------------ urls --- # Only https://morningmasterpiece.com/... with ordinary URL characters. url_allowed() { case $1 in "$MEDIA_PREFIX"*) ;; *) return 1 ;; esac [ ${#1} -le 2048 ] || return 1 [ -z "$(printf '%s' "$1" | LC_ALL=C tr -d 'A-Za-z0-9._~:/?#@!$&()*+,;=%-')" ] } # Add utm_source=wallpaper, replacing an existing utm_source and keeping # the fragment at the end. add_utm() { printf '%s\n' "$1" | awk '{ url = $0; frag = "" i = index(url, "#") if (i) { frag = substr(url, i); url = substr(url, 1, i - 1) } q = index(url, "?") if (!q) { print url "?utm_source=wallpaper" frag; exit } base = substr(url, 1, q - 1); n = split(substr(url, q + 1), parts, "&") out = ""; found = 0 for (k = 1; k <= n; k++) { p = parts[k] if (p == "") continue if (p ~ /^utm_source(=|$)/) { if (found) continue; p = "utm_source=wallpaper"; found = 1 } out = out (out == "" ? "" : "&") p } if (!found) out = out (out == "" ? "" : "&") "utm_source=wallpaper" print base "?" out frag }' } # file:// URI with percent-encoding (works in any awk and locale). file_uri() { printf 'file://' printf '%s' "$1" | od -An -tx1 -v | LC_ALL=C awk ' function dec(h, i, v) { v = 0; for (i = 1; i <= 2; i++) v = v * 16 + index("0123456789abcdef", substr(h, i, 1)) - 1; return v } { for (i = 1; i <= NF; i++) { n = dec($i); c = (n < 128) ? sprintf("%c", n) : "" if (c != "" && c ~ /^[A-Za-z0-9._~\/-]$/) printf "%s", c; else printf "%%%s", toupper($i) } }' } # --------------------------------------------------------------- network --- # http_get URL OUTFILE MAX_SECONDS http_get() { if have curl; then curl -fs --proto =https --connect-timeout 10 --max-time "$3" -A "$USER_AGENT" -o "$2" "$1" elif have wget; then # Like curl above: never follow redirects (busybox wget lacks the flag). if wget --help 2>&1 | grep -q -- --max-redirect; then wget -q -T "$3" -U "$USER_AGENT" --max-redirect=0 -O "$2" "$1" else wget -q -T "$3" -U "$USER_AGENT" -O "$2" "$1" fi else return 127 fi } fetch_json() { case $API_URL in https://*) ;; *) warn "MM_WALLPAPER_API_URL must be an https:// URL" return 1 ;; esac http_get "$API_URL" "$1" 20 _rc=$? if [ $_rc = 127 ]; then warn "neither curl nor wget is installed" fi [ $_rc = 0 ] && [ -s "$1" ] } is_jpeg() { [ -f "$1" ] && [ -s "$1" ] || return 1 _size=$(wc -c <"$1" | tr -d ' ') [ "$_size" -ge 1024 ] || return 1 [ "$(od -An -tx1 -N3 "$1" | tr -d ' \n')" = ffd8ff ] } # Download to a temp file, check it really is a JPEG, then rename atomically. download_image() { _part="$2.part.$$" TMP_FILES="$TMP_FILES $_part" if ! http_get "$1" "$_part" 180; then rm -f "$_part" return 1 fi if ! is_jpeg "$_part"; then warn "downloaded file is not a JPEG image; ignoring it" rm -f "$_part" return 1 fi mv -f "$_part" "$2" } # ------------------------------------------------------------- metadata --- # Read today.json into M_* variables. Returns 1 if it is unusable. read_meta() { M_DATE=$(json_get "$1" publish_date) || return 1 case $M_DATE in [0-9][0-9][0-9][0-9]-[0-1][0-9]-[0-3][0-9]) ;; *) return 1 ;; esac M_SLUG=$(json_get "$1" slug | tr '[:upper:]' '[:lower:]' | LC_ALL=C tr -cd 'a-z0-9-' | cut -c1-80) [ -n "$M_SLUG" ] || M_SLUG=artwork M_TITLE=$(clean_text "$(json_get "$1" artwork.title)") M_ARTIST=$(clean_text "$(json_get "$1" artwork.artist)") M_YEAR=$(clean_text "$(json_get "$1" artwork.date)") M_MEDIUM=$(clean_text "$(json_get "$1" artwork.medium)") M_MUSEUM=$(clean_text "$(json_get "$1" artwork.museum)") M_CREDIT=$(clean_text "$(json_get "$1" artwork.credit_line)") M_STORY=$(clean_text "$(json_get "$1" story_title)") M_COLOR=$(json_get "$1" artwork.color) case $M_COLOR in \#[0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f]) ;; *) M_COLOR= ;; esac M_PAGE=$(json_get "$1" url) url_allowed "$M_PAGE" || M_PAGE="$SITE/$M_DATE" return 0 } # First allowed image URL for the variant: wallpaper, download, then large. pick_image_url() { for _p in "images.wallpaper_$2" "images.downloads.wallpaper_$2" images.large; do _u=$(json_get "$1" "$_p") || continue if url_allowed "$_u"; then printf '%s' "$_u" return 0 fi warn "ignoring image URL outside $MEDIA_PREFIX ($_p)" done return 1 } credit_line() { _c=${M_TITLE:-Untitled} [ -z "$M_ARTIST" ] || _c="$_c — $M_ARTIST" [ -z "$M_MUSEUM" ] || _c="$_c ($M_MUSEUM)" printf '%s' "$_c" } # ---------------------------------------------------------------- state --- load_state() { st_publish_date='' st_slug='' st_variant='' st_image='' st_applied_image='' st_applied_backend='' st_applied_session='' st_omarchy_prev='' [ -f "$STATE_FILE" ] || return 0 while IFS='=' read -r _k _v; do case $_k in publish_date) st_publish_date=$_v ;; slug) st_slug=$_v ;; variant) st_variant=$_v ;; image) st_image=$_v ;; applied_image) st_applied_image=$_v ;; applied_backend) st_applied_backend=$_v ;; applied_session) st_applied_session=$_v ;; omarchy_prev) st_omarchy_prev=$_v ;; esac done <"$STATE_FILE" } save_state() { [ "$DRY_RUN" = 1 ] && return 0 _t="$STATE_FILE.tmp.$$" { printf 'version=%s\n' "$VERSION" printf 'publish_date=%s\n' "$st_publish_date" printf 'slug=%s\n' "$st_slug" printf 'variant=%s\n' "$st_variant" printf 'image=%s\n' "$st_image" printf 'applied_image=%s\n' "$st_applied_image" printf 'applied_backend=%s\n' "$st_applied_backend" printf 'applied_session=%s\n' "$st_applied_session" printf 'omarchy_prev=%s\n' "$st_omarchy_prev" } >"$_t" && mv -f "$_t" "$STATE_FILE" } # Identifies the current boot + compositor instance. Non-persistent # backends (swaybg, hyprpaper, feh, ...) are re-applied when it changes. session_key() { printf '%s|%s|%s' "$(boot_id)" "${HYPRLAND_INSTANCE_SIGNATURE:-}" "${SWAYSOCK:-}" } LOCK_HELD=0 TMP_FILES= cleanup() { # shellcheck disable=SC2086 # list of our own temp paths, no spaces [ -z "$TMP_FILES" ] || rm -f $TMP_FILES [ "$LOCK_HELD" = 1 ] && rm -rf "$CACHE_DIR/.lock" LOCK_HELD=0 } boot_id() { if [ -r /proc/sys/kernel/random/boot_id ]; then cat /proc/sys/kernel/random/boot_id elif [ "$OS" = macos ]; then sysctl -n kern.boottime 2>/dev/null | sed 's/[^0-9]*\([0-9]*\).*/\1/' fi } # The lock is a directory holding "pid boot-id". It counts as stale if that # process is gone, the machine rebooted since, or it is older than 15 min # (a run never takes that long). acquire_lock() { _lock="$CACHE_DIR/.lock" if ! mkdir "$_lock" 2>/dev/null; then _owner=$(cat "$_lock/pid" 2>/dev/null) _pid=${_owner%% *} _lboot=${_owner#* } _old=$(find "$_lock" -prune -mmin +15 2>/dev/null) if [ -z "$_old" ]; then # A lock without a pid may be brand new. [ -z "$_owner" ] && return 1 if [ "$_lboot" = "$(boot_id)" ] && kill -0 "$_pid" 2>/dev/null; then return 1 fi fi # Move the stale lock away first so only one run can take over. mv "$_lock" "$_lock.stale.$$" 2>/dev/null && rm -rf "$_lock.stale.$$" mkdir "$_lock" 2>/dev/null || return 1 fi LOCK_HELD=1 printf '%s %s\n' $$ "$(boot_id)" >"$_lock/pid" } # Cached image names, newest first: YYYY-MM-DD_slug_variant.jpg cached_images() { for _f in "$CACHE_DIR"/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]_*_desktop.jpg \ "$CACHE_DIR"/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]_*_phone.jpg; do _f=${_f##*/} case $_f in *'*'* | *[!a-z0-9_.-]*) continue ;; esac [ -f "$CACHE_DIR/$_f" ] && printf '%s\n' "$_f" done | sort -r } # Keep the newest S_KEEP images (never the one currently in use). prune_cache() { [ "$DRY_RUN" = 1 ] && return 0 _n=0 for _f in $(cached_images); do _n=$((_n + 1)) [ $_n -le "$S_KEEP" ] && continue [ "$CACHE_DIR/$_f" = "$st_applied_image" ] && continue [ "$CACHE_DIR/$_f" = "$st_image" ] && continue rm -f "$CACHE_DIR/$_f" done } # ----------------------------------------------------------- environment --- # A systemd/cron job may miss the session variables; find them ourselves. discover_env() { [ "$OS" = macos ] && return 0 if [ -z "${XDG_RUNTIME_DIR:-}" ] && [ -d "/run/user/$(id -u)" ]; then XDG_RUNTIME_DIR="/run/user/$(id -u)" export XDG_RUNTIME_DIR fi _rt=${XDG_RUNTIME_DIR:-/nonexistent} if [ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ] && [ -S "$_rt/bus" ]; then DBUS_SESSION_BUS_ADDRESS="unix:path=$_rt/bus" export DBUS_SESSION_BUS_ADDRESS fi if [ -z "${HYPRLAND_INSTANCE_SIGNATURE:-}" ]; then for _d in "$_rt/hypr" /tmp/hypr; do [ -d "$_d" ] || continue # Newest first; the names are Hyprland instance signatures. # shellcheck disable=SC2045 for _sig in $(ls -1t "$_d" 2>/dev/null); do if [ -S "$_d/$_sig/.socket.sock" ]; then HYPRLAND_INSTANCE_SIGNATURE=$_sig export HYPRLAND_INSTANCE_SIGNATURE break 2 fi done done fi if [ -z "${SWAYSOCK:-}" ]; then # shellcheck disable=SC2045 # newest first; names are sway-ipc.UID.PID.sock for _s in $(ls -1t "$_rt"/sway-ipc.*.sock 2>/dev/null); do if [ -S "$_s" ]; then SWAYSOCK=$_s export SWAYSOCK break fi done fi if [ -z "${WAYLAND_DISPLAY:-}" ]; then # shellcheck disable=SC2010 # newest first; names are wayland-N for _s in $(ls -1t "$_rt" 2>/dev/null | grep '^wayland-[0-9]*$'); do if [ -S "$_rt/$_s" ]; then WAYLAND_DISPLAY=$_s export WAYLAND_DISPLAY break fi done fi if [ -z "${WAYLAND_DISPLAY:-}" ] && [ -z "${DISPLAY:-}" ] && [ -S /tmp/.X11-unix/X0 ]; then DISPLAY=:0 export DISPLAY fi } has_session() { [ "$OS" = macos ] && return 0 [ -n "${WAYLAND_DISPLAY:-}${DISPLAY:-}${HYPRLAND_INSTANCE_SIGNATURE:-}${SWAYSOCK:-}" ] } running() { have pgrep && pgrep -u "$(id -u)" -x "$1" >/dev/null 2>&1 } # ------------------------------------------------------------- backends --- find_omarchy_bg_set() { OMARCHY_BG_SET= if have omarchy-theme-bg-set; then OMARCHY_BG_SET=$(command -v omarchy-theme-bg-set) elif [ -x "${OMARCHY_PATH:-$HOME/.local/share/omarchy}/bin/omarchy-theme-bg-set" ]; then OMARCHY_BG_SET="${OMARCHY_PATH:-$HOME/.local/share/omarchy}/bin/omarchy-theme-bg-set" fi [ -n "$OMARCHY_BG_SET" ] } # Omarchy 4 keeps the link in ~/.local/state, Omarchy 3 in ~/.config. omarchy_link() { for _l in "$HOME/.local/state/omarchy/current/background" "$HOME/.config/omarchy/current/background"; do if [ -L "$_l" ]; then printf '%s' "$_l" return 0 fi done return 1 } backend_available() { MISSING= case $1 in omarchy) find_omarchy_bg_set && return 0 if [ -d "$HOME/.config/omarchy/current" ] && have swaybg; then return 0; fi MISSING="omarchy-theme-bg-set" ;; hyprpaper) if ! have hyprctl; then MISSING=hyprctl elif running hyprpaper || have hyprpaper; then return 0 else MISSING=hyprpaper fi ;; swaybg) have swaybg && return 0 || MISSING=swaybg ;; sway) have swaymsg && return 0 || MISSING=swaymsg ;; gnome | cinnamon | mate) have gsettings && return 0 || MISSING=gsettings ;; kde) have plasma-apply-wallpaperimage && return 0 || MISSING=plasma-apply-wallpaperimage ;; xfce) have xfconf-query && return 0 || MISSING=xfconf-query ;; feh) have feh && return 0 || MISSING=feh ;; macos) have osascript && return 0 || MISSING=osascript ;; custom) [ -n "$S_COMMAND" ] && return 0 || MISSING="command setting" ;; none) return 0 ;; esac return 1 } try_backend() { case "$TRIED" in *"$1 (needs"*) return 1 ;; esac if backend_available "$1"; then BACKEND=$1 return 0 fi TRIED="${TRIED:+$TRIED, }$1 (needs $MISSING)" return 1 } desk_has() { case ":$DESK:" in *"$1"*) return 0 ;; esac return 1 } # Without XDG_CURRENT_DESKTOP (e.g. in a timer job), look at processes. guess_desktop() { running plasmashell && echo kde && return running gnome-shell && echo gnome && return running cinnamon && echo x-cinnamon && return running mate-session && echo mate && return running xfdesktop && echo xfce && return running Hyprland && echo hyprland && return running sway && echo sway && return echo unknown } # Sets BACKEND. Returns 0 on success, 1 if none works (TRIED lists them), # 4 if there is no graphical session at all. resolve_backend() { BACKEND= TRIED= if [ "$S_BACKEND" != auto ]; then try_backend "$S_BACKEND" return fi if [ "$OS" = macos ]; then try_backend macos return fi has_session || return 4 DESK=$(printf '%s' "${XDG_CURRENT_DESKTOP:-}:${XDG_SESSION_DESKTOP:-}:${DESKTOP_SESSION:-}" | tr '[:upper:]' '[:lower:]') [ "$DESK" = "::" ] && DESK=$(guess_desktop) if [ -n "${HYPRLAND_INSTANCE_SIGNATURE:-}" ] || desk_has hyprland; then try_backend omarchy && return 0 # Prefer whichever wallpaper daemon is already running. if running hyprpaper; then try_backend hyprpaper && return 0; fi if running swaybg; then try_backend swaybg && return 0; fi try_backend hyprpaper && return 0 try_backend swaybg && return 0 fi if [ -n "${SWAYSOCK:-}" ] || desk_has sway; then try_backend sway && return 0 fi if desk_has kde || desk_has plasma; then try_backend kde && return 0; fi if desk_has cinnamon; then try_backend cinnamon && return 0; fi if desk_has mate; then try_backend mate && return 0; fi if desk_has xfce; then try_backend xfce && return 0; fi if desk_has gnome || desk_has unity || desk_has budgie || desk_has pantheon || desk_has ubuntu; then try_backend gnome && return 0 fi if [ -n "${WAYLAND_DISPLAY:-}" ]; then try_backend swaybg && return 0; fi if [ -n "${DISPLAY:-}" ]; then try_backend feh && return 0; fi return 1 } # Settings that live on in the desktop's own config survive a re-login. backend_persistent() { case $1 in gnome | cinnamon | mate | kde | xfce | omarchy | macos | none) return 0 ;; esac return 1 } # Start a long-running helper (swaybg, hyprpaper) outside our process # group, and outside the systemd service cgroup when run from the timer. launch_detached() { if [ "$DRY_RUN" = 1 ]; then printf 'dry-run: %s &\n' "$*" return 0 fi if [ -n "${INVOCATION_ID:-}" ] && have systemd-run; then set -- systemd-run --user --scope --quiet -- "$@" fi if have setsid; then nohup setsid "$@" >/dev/null 2>&1 /dev/null 2>&1 /dev/null || true fi run gsettings set "$1" picture-options "$_opt" || true if [ -n "$M_COLOR" ] && { [ "$S_MODE" = fit ] || [ "$S_MODE" = center ]; }; then run gsettings set "$1" primary-color "$M_COLOR" || true run gsettings set "$1" color-shading-type solid || true fi } apply_mate() { case $S_MODE in fit) _opt=scaled ;; stretch) _opt=stretched ;; center) _opt=centered ;; tile) _opt=wallpaper ;; *) _opt=zoom ;; esac run gsettings set org.mate.background picture-filename "$1" || return 1 run gsettings set org.mate.background picture-options "$_opt" || true } apply_kde() { case $S_MODE in fit) _fm=preserveAspectFit ;; stretch) _fm=stretch ;; center) _fm=pad ;; *) _fm=preserveAspectCrop ;; esac # --fill-mode arrived in Plasma 6; retry without it on older versions. if [ "$DRY_RUN" = 1 ]; then run plasma-apply-wallpaperimage --fill-mode "$_fm" "$1" return 0 fi plasma-apply-wallpaperimage --fill-mode "$_fm" "$1" >/dev/null 2>&1 && return 0 plasma-apply-wallpaperimage "$1" } apply_xfce() { case $S_MODE in fit) _st=4 ;; stretch) _st=3 ;; center) _st=1 ;; tile) _st=2 ;; *) _st=5 ;; esac _props=$(xfconf-query -c xfce4-desktop -l 2>/dev/null | grep '/last-image$') if [ -z "$_props" ]; then warn "xfce: no backdrop found; open Desktop settings once, then retry" return 1 fi for _p in $_props; do run xfconf-query -c xfce4-desktop -p "$_p" -s "$1" || return 1 run xfconf-query -c xfce4-desktop -p "${_p%/last-image}/image-style" -n -t int -s "$_st" 2>/dev/null || true done } apply_sway() { case $S_MODE in fit | stretch | center | tile) _m=$S_MODE ;; *) _m=fill ;; esac _cmd="output * bg \"$1\" $_m" [ -z "$M_COLOR" ] || _cmd="$_cmd $M_COLOR" if [ "$DRY_RUN" = 1 ]; then run swaymsg "$_cmd" return 0 fi swaymsg "$_cmd" >/dev/null } swaybg_pids() { have pgrep && pgrep -u "$(id -u)" -x swaybg } apply_swaybg() { # image mode [colour] _old=$(swaybg_pids | tr '\n' ' ') if [ -n "${3:-}" ]; then launch_detached swaybg -i "$1" -m "$2" -c "$3" else launch_detached swaybg -i "$1" -m "$2" fi [ "$DRY_RUN" = 1 ] && return 0 # Start the new swaybg first, then stop the old one: no black flash. _started=0 _i=0 while [ $_i -lt 3 ] && [ $_started = 0 ]; do sleep 1 for _pid in $(swaybg_pids); do case " $_old " in *" $_pid "*) ;; *) _started=1 ;; esac done _i=$((_i + 1)) done if [ $_started = 0 ]; then warn "swaybg did not start" return 1 fi # shellcheck disable=SC2086 # list of pids [ -z "$_old" ] || kill $_old 2>/dev/null return 0 } hyprctl_ok() { _out=$("$@" 2>&1) _rc=$? _out=$(printf '%s' "$_out" | tr -d '\n\r' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') [ $_rc = 0 ] && { [ -z "$_out" ] || [ "$_out" = ok ]; } && return 0 [ -z "$_out" ] || warn "hyprpaper: $_out" return 1 } apply_hyprpaper() { case $S_MODE in fit | center) _fit=contain _pre=contain: ;; stretch) _fit=stretch _pre= ;; tile) _fit=tile _pre=tile: ;; *) _fit=cover _pre= ;; esac if ! running hyprpaper && [ "$DRY_RUN" != 1 ]; then # Right after login hyprpaper may still be starting. _i=0 while [ $_i -lt 5 ] && ! running hyprpaper; do sleep 1 _i=$((_i + 1)) done if ! running hyprpaper; then have hyprpaper || { warn "hyprpaper is not running or installed" return 1 } say "Starting hyprpaper..." launch_detached hyprpaper sleep 2 fi fi _mons=$(hyprctl monitors 2>/dev/null | sed -n 's/^Monitor \([^ ]*\) (ID.*/\1/p') if [ "$DRY_RUN" = 1 ]; then for _m in "" $_mons; do run hyprctl hyprpaper wallpaper "$_m,$1,$_fit"; done return 0 fi # hyprpaper >= 0.8: "monitor, path, fit_mode", no preload. The empty # monitor is the fallback for screens plugged in later. # Success is decided by the named monitors; the fallback is best effort # unless no monitor could be listed. _ok=1 [ -n "$_mons" ] || hyprctl_ok hyprctl hyprpaper wallpaper ",$1,$_fit" || _ok=0 for _m in $_mons; do hyprctl_ok hyprctl hyprpaper wallpaper "$_m,$1,$_fit" || _ok=0 done if [ -n "$_mons" ] && [ $_ok = 1 ]; then hyprctl hyprpaper wallpaper ",$1,$_fit" >/dev/null 2>&1 fi [ $_ok = 1 ] && return 0 # hyprpaper 0.7: preload, then "monitor,[contain:|tile:]path", unload. say "Retrying with the hyprpaper 0.7 commands..." hyprctl_ok hyprctl hyprpaper preload "$1" || return 1 _ok=1 [ -n "$_mons" ] || hyprctl_ok hyprctl hyprpaper wallpaper ",$_pre$1" || _ok=0 for _m in $_mons; do hyprctl_ok hyprctl hyprpaper wallpaper "$_m,$_pre$1" || _ok=0 done [ -z "$_mons" ] || hyprctl hyprpaper wallpaper ",$_pre$1" >/dev/null 2>&1 hyprctl hyprpaper unload unused >/dev/null 2>&1 [ $_ok = 1 ] } # Is $1 inside the cache dir? (Omarchy stores realpath'd targets.) in_cache() { case $1 in "$CACHE_DIR"/*) return 0 ;; "$(cd "$CACHE_DIR" 2>/dev/null && pwd -P)"/*) return 0 ;; esac return 1 } remember_omarchy_prev() { _link=$(omarchy_link) || return 0 _target=$(readlink "$_link") in_cache "$_target" || st_omarchy_prev=$_target } # Use Omarchy's own command so theme switching keeps working. apply_omarchy() { remember_omarchy_prev if find_omarchy_bg_set; then run env PATH="$(dirname "$OMARCHY_BG_SET"):$PATH" "$OMARCHY_BG_SET" "$1" return fi # Omarchy before omarchy-theme-bg-set: link + swaybg, like it does itself. _link="$HOME/.config/omarchy/current/background" run ln -nsf "$1" "$_link" || return 1 apply_swaybg "$_link" fill } apply_feh() { case $S_MODE in fit) _o=--bg-max ;; stretch) _o=--bg-scale ;; center) _o=--bg-center ;; tile) _o=--bg-tile ;; *) _o=--bg-fill ;; esac if [ -n "$M_COLOR" ]; then run feh --image-bg "$M_COLOR" "$_o" "$1" else run feh "$_o" "$1" fi } # NSWorkspace sets the picture on every screen without the Automation # (System Events) permission prompt, which a launchd job can't answer. MACOS_JXA='function run(argv) { ObjC.import("AppKit"); var url = $.NSURL.fileURLWithPath(argv[0]); var ws = $.NSWorkspace.sharedWorkspace; var screens = $.NSScreen.screens; if (screens.count < 1) throw new Error("no screens"); for (var i = 0; i < screens.count; i++) { if (!ws.setDesktopImageURLForScreenOptionsError(url, screens.objectAtIndex(i), $.NSDictionary.alloc.init, null)) throw new Error("setDesktopImageURL failed"); } }' apply_macos() { if [ "$DRY_RUN" = 1 ]; then run osascript -l JavaScript -e "(NSWorkspace setDesktopImageURL on every screen)" "$1" return 0 fi osascript -l JavaScript -e "$MACOS_JXA" "$1" >/dev/null 2>&1 && return 0 osascript -e 'on run argv' \ -e 'tell application "System Events" to tell every desktop to set picture to (item 1 of argv)' \ -e 'end run' "$1" >/dev/null } apply_wallpaper() { case $BACKEND in omarchy) apply_omarchy "$1" ;; hyprpaper) apply_hyprpaper "$1" ;; swaybg) apply_swaybg "$1" "$S_MODE" "$M_COLOR" ;; sway) apply_sway "$1" ;; gnome) apply_gnome_like org.gnome.desktop.background "$1" ;; cinnamon) apply_gnome_like org.cinnamon.desktop.background "$1" ;; mate) apply_mate "$1" ;; kde) apply_kde "$1" ;; xfce) apply_xfce "$1" ;; feh) apply_feh "$1" ;; macos) apply_macos "$1" ;; custom) run sh -c "$S_COMMAND" "$PROG" "$1" ;; none) return 0 ;; *) return 1 ;; esac } notify_credit() { [ "$S_NOTIFY" = 1 ] || return 0 _body=$(credit_line) if [ "$DRY_RUN" = 1 ]; then printf 'dry-run: notify "%s"\n' "$_body" return 0 fi if [ "$OS" = macos ]; then osascript -e 'on run argv' -e 'display notification (item 2 of argv) with title (item 1 of argv)' \ -e 'end run' "Morning Masterpiece" "$_body" >/dev/null 2>&1 || true elif have notify-send; then # Most notification daemons read the body as markup. _body=$(printf '%s' "$_body" | sed 's/&/\&/g; s//\>/g') notify-send -a "Morning Masterpiece" -i "$1" "Morning Masterpiece" "$_body" >/dev/null 2>&1 || true fi return 0 } # Apply IMAGE if needed (new image, --force, other backend, new session). # Returns 0 on success or nothing to do, 3 if no backend works. maybe_apply() { _img=$1 _is_new=$2 resolve_backend _rb=$? if [ $_rb = 4 ]; then say "No graphical session found; the image is cached at $_img." return 0 fi if [ $_rb != 0 ]; then warn "could not set the wallpaper: no working backend found." warn "tried: ${TRIED:-none}" warn "pick one with --backend or 'backend = ...' in $CONFIG_FILE (see README)" return 3 fi _session=$(session_key) _need=0 [ "$_is_new" = 1 ] && _need=1 [ "$FORCE" = 1 ] && _need=1 [ "$st_applied_image" != "$_img" ] && _need=1 [ "$st_applied_backend" != "$BACKEND" ] && _need=1 if ! backend_persistent "$BACKEND" && [ "$st_applied_session" != "$_session" ]; then _need=1 fi if [ $_need = 0 ]; then say "Wallpaper is already set ($BACKEND)." return 0 fi if ! apply_wallpaper "$_img"; then warn "the $BACKEND backend failed to set the wallpaper" return 3 fi st_applied_image=$_img st_applied_backend=$BACKEND st_applied_session=$_session save_state if [ "$DRY_RUN" = 1 ]; then note "Dry run: the wallpaper would be set with $BACKEND." elif [ "$BACKEND" = none ]; then note "Artwork saved to $_img (backend: none)." else note "Wallpaper set with $BACKEND: $(credit_line)" fi [ "$_is_new" = 1 ] && notify_credit "$_img" return 0 } # ------------------------------------------------------------- commands --- newest_cached_image() { if [ -n "$st_image" ] && is_jpeg "$st_image"; then printf '%s' "$st_image" return 0 fi for _f in $(cached_images); do if is_jpeg "$CACHE_DIR/$_f"; then printf '%s' "$CACHE_DIR/$_f" return 0 fi done return 1 } # Offline or broken response: keep (and if needed re-apply) the cached image. fallback_to_cache() { note "$1; keeping the cached wallpaper." _img=$(newest_cached_image) || { note "No artwork cached yet; will try again on the next run." return 0 } if ! { [ -f "$META_FILE" ] && read_meta "$META_FILE"; } || [ "${_img##*/}" = "${_img##*/"$M_DATE"_}" ]; then M_TITLE='' M_ARTIST='' M_MUSEUM='' M_COLOR='' fi maybe_apply "$_img" 0 } cmd_update() { path_is_safe "$CACHE_DIR" || { warn "unsupported characters in cache path: $CACHE_DIR" return 2 } if [ "$DRY_RUN" != 1 ] && ! mkdir -p "$CACHE_DIR" 2>/dev/null; then warn "cannot create $CACHE_DIR" return 2 fi if [ "$DRY_RUN" != 1 ] && ! acquire_lock; then say "Another run is in progress; nothing to do." return 0 fi discover_env load_state if [ "$DRY_RUN" = 1 ]; then _json=$(mktemp "${TMPDIR:-/tmp}/$PROG.XXXXXX") || return 2 else _json="$CACHE_DIR/.today.json.$$" fi TMP_FILES="$TMP_FILES $_json" if ! fetch_json "$_json"; then fallback_to_cache "Could not reach morningmasterpiece.com" return fi if ! read_meta "$_json"; then fallback_to_cache "today.json from morningmasterpiece.com was not usable" return fi _url=$(pick_image_url "$_json" "$S_VARIANT") || { fallback_to_cache "today.json has no usable $S_VARIANT image" return } _img="$CACHE_DIR/${M_DATE}_${M_SLUG}_$S_VARIANT.jpg" _new=0 if [ "$FORCE" = 1 ] || ! is_jpeg "$_img"; then say "New artwork for $M_DATE: $(credit_line)" if [ "$DRY_RUN" = 1 ]; then printf 'dry-run: download %s -> %s\n' "$_url" "$_img" elif ! download_image "$_url" "$_img"; then fallback_to_cache "Could not download today's image" return fi _new=1 else say "Artwork unchanged ($M_DATE, $M_SLUG): no download needed." fi [ "$DRY_RUN" = 1 ] || mv -f "$_json" "$META_FILE" st_publish_date=$M_DATE st_slug=$M_SLUG st_variant=$S_VARIANT st_image=$_img save_state maybe_apply "$_img" "$_new" _rc=$? prune_cache return $_rc } # Today's metadata: live if possible, otherwise the cached copy. load_any_meta() { META_SOURCE=live _j=$(mktemp "${TMPDIR:-/tmp}/$PROG.XXXXXX") || return 1 TMP_FILES="$TMP_FILES $_j" if fetch_json "$_j" && read_meta "$_j"; then return 0 fi META_SOURCE=cached [ -f "$META_FILE" ] && read_meta "$META_FILE" } cmd_info() { if ! load_any_meta; then warn "could not fetch today's artwork and nothing is cached yet" return 1 fi [ "$META_SOURCE" = cached ] && note "(offline: showing the last cached artwork)" note "Morning Masterpiece for $M_DATE" note "Title: ${M_TITLE:-Untitled}" [ -z "$M_ARTIST" ] || note "Artist: $M_ARTIST" [ -z "$M_YEAR" ] || note "Date: $M_YEAR" [ -z "$M_MEDIUM" ] || note "Medium: $M_MEDIUM" [ -z "$M_MUSEUM" ] || note "Museum: $M_MUSEUM" [ -z "$M_CREDIT" ] || note "Credit: $M_CREDIT" [ -z "$M_STORY" ] || note "Story: $M_STORY" note "Page: $(add_utm "$M_PAGE")" } cmd_open() { if load_any_meta; then _link=$(add_utm "$M_PAGE") else _link=$(add_utm "$SITE/") fi discover_env if [ "$OS" = macos ] && have open; then run open "$_link" elif [ "$DRY_RUN" = 1 ]; then run xdg-open "$_link" elif have xdg-open; then xdg-open "$_link" >/dev/null 2>&1 & else note "Open this link in your browser: $_link" return 0 fi say "Opening $_link" } systemd_quote() { printf '"%s"' "$(printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/%/%%/g; s/\$/$$/g')" } render_service() { cat </\>/g' } render_plist() { # program minute cat < Label $LAUNCHD_LABEL ProgramArguments /bin/sh $(xml_escape "$1") update --quiet RunAtLoad StartCalendarInterval Minute $2 StartInterval 10800 ProcessType Background StandardOutPath $(xml_escape "$HOME/Library/Logs/$PROG.log") StandardErrorPath $(xml_escape "$HOME/Library/Logs/$PROG.log") EOF } cron_line() { # program minute printf '%s * * * * /bin/sh %s update --quiet >/dev/null 2>&1 # %s\n' "$2" "'$1'" "$PROG" } # Spread scheduled checks over the hour so not everyone asks at once. random_minute() { awk 'BEGIN { srand(); print int(rand() * 60) % 60 }' } systemd_user_ok() { have systemctl && systemctl --user show-environment >/dev/null 2>&1 } write_file() { # path content if [ "$DRY_RUN" = 1 ]; then printf 'dry-run: write %s\n' "$1" return 0 fi mkdir -p "$(dirname "$1")" && printf '%s\n' "$2" >"$1" } install_systemd() { write_file "$SYSTEMD_DIR/$UNIT.service" "$(render_service "$1")" || return 1 write_file "$SYSTEMD_DIR/$UNIT.timer" "$(render_timer)" || return 1 run systemctl --user daemon-reload || return 1 run systemctl --user enable "$UNIT.service" >/dev/null 2>&1 || true run systemctl --user enable --now "$UNIT.timer" || return 1 note "Scheduled: systemd user timer $UNIT.timer (hourly, and at login)." if ! systemctl --user show-environment 2>/dev/null | grep -qE '^(WAYLAND_DISPLAY|DISPLAY)='; then note "Note: your systemd user session has no WAYLAND_DISPLAY/DISPLAY yet." note "The timer finds the session itself, but if it ever fails, add this to" note "your session startup: systemctl --user import-environment WAYLAND_DISPLAY DISPLAY HYPRLAND_INSTANCE_SIGNATURE XDG_CURRENT_DESKTOP" fi } install_launchd() { write_file "$LAUNCHD_PLIST" "$(render_plist "$1" "$(random_minute)")" || return 1 run mkdir -p "$HOME/Library/Logs" _dom="gui/$(id -u)" run launchctl bootout "$_dom/$LAUNCHD_LABEL" >/dev/null 2>&1 || true if ! run launchctl bootstrap "$_dom" "$LAUNCHD_PLIST" 2>/dev/null; then run launchctl load -w "$LAUNCHD_PLIST" || return 1 fi note "Scheduled: launchd agent $LAUNCHD_LABEL (hourly, at login and after wake)." } install_cron() { _line=$(cron_line "$1" "$(random_minute)") if [ "$DRY_RUN" = 1 ]; then printf 'dry-run: add crontab line: %s\n' "$_line" return 0 fi # Read first: some crontab versions truncate before the reader finishes. _cur=$(crontab -l 2>/dev/null | grep -v "# $PROG\$") if [ -n "$_cur" ]; then printf '%s\n%s\n' "$_cur" "$_line" | crontab - || return 1 else printf '%s\n' "$_line" | crontab - || return 1 fi note "Scheduled: hourly cron job (no systemd user session found)." note "Tip: also run '$PROG update' from your session autostart." } abs_path() { (cd "$(dirname "$1")" 2>/dev/null && printf '%s/%s' "$(pwd -P)" "$(basename "$1")") } cmd_install() { _self=$(abs_path "$SELF") if [ ! -f "$_self" ] || ! grep -q '^PROG=morning-masterpiece-wallpaper$' "$_self"; then warn "cannot find the program file to install." warn "download it first, then run: sh ./$PROG install" return 2 fi path_is_safe "$BIN_DIR" || { warn "unsupported characters in $BIN_DIR" return 2 } _bin="$BIN_DIR/$PROG" if [ "$(abs_path "$_bin")" != "$_self" ]; then run mkdir -p "$BIN_DIR" || return 2 if [ "$DRY_RUN" = 1 ]; then printf 'dry-run: copy %s -> %s\n' "$_self" "$_bin" else if ! { cp "$_self" "$_bin.tmp.$$" && chmod 755 "$_bin.tmp.$$" && mv -f "$_bin.tmp.$$" "$_bin"; }; then rm -f "$_bin.tmp.$$" warn "could not copy the program to $_bin" return 2 fi fi fi note "Installed $_bin" # Options given to install become the defaults for scheduled runs. if [ "$DRY_RUN" != 1 ]; then [ -z "$O_BACKEND" ] || config_set backend "$O_BACKEND" [ -z "$O_MODE" ] || config_set mode "$O_MODE" [ -z "$O_VARIANT" ] || config_set variant "$O_VARIANT" [ -z "$O_NOTIFY" ] || config_set notify "$O_NOTIFY" fi cmd_update _rc=$? if [ "$OS" = macos ]; then install_launchd "$_bin" || { warn "could not register the launchd agent" return 2 } elif systemd_user_ok; then install_systemd "$_bin" || { warn "could not enable the systemd user timer" return 2 } elif have crontab; then install_cron "$_bin" || { warn "could not install the cron job" return 2 } else warn "no scheduler found (systemd user session, launchd or cron)." warn "run '$_bin update' from your session autostart instead." fi case ":$PATH:" in *":$BIN_DIR:"*) ;; *) note "Note: $BIN_DIR is not in your PATH; add it to run '$PROG' directly." ;; esac return $_rc } # Put Omarchy back on its previous background if ours is still showing. restore_omarchy() { _link=$(omarchy_link) || return 0 _target=$(readlink "$_link") in_cache "$_target" || return 0 if [ -n "$st_omarchy_prev" ] && [ -f "$st_omarchy_prev" ] && find_omarchy_bg_set; then run env PATH="$(dirname "$OMARCHY_BG_SET"):$PATH" "$OMARCHY_BG_SET" "$st_omarchy_prev" && note "Restored your previous Omarchy background." elif have omarchy-theme-bg-next; then run omarchy-theme-bg-next && note "Switched Omarchy back to a theme background." fi } cmd_uninstall() { discover_env load_state if [ "$OS" = macos ]; then run launchctl bootout "gui/$(id -u)/$LAUNCHD_LABEL" >/dev/null 2>&1 || run launchctl unload "$LAUNCHD_PLIST" >/dev/null 2>&1 [ -f "$LAUNCHD_PLIST" ] && run rm -f "$LAUNCHD_PLIST" && note "Removed launchd agent $LAUNCHD_LABEL" else if have systemctl; then run systemctl --user disable --now "$UNIT.timer" "$UNIT.service" >/dev/null 2>&1 fi if [ -f "$SYSTEMD_DIR/$UNIT.timer" ] || [ -f "$SYSTEMD_DIR/$UNIT.service" ]; then run rm -f "$SYSTEMD_DIR/$UNIT.timer" "$SYSTEMD_DIR/$UNIT.service" have systemctl && run systemctl --user daemon-reload >/dev/null 2>&1 note "Removed systemd user timer $UNIT.timer" fi if have crontab && crontab -l 2>/dev/null | grep -q "# $PROG\$"; then if [ "$DRY_RUN" = 1 ]; then printf 'dry-run: remove crontab line\n' else _cur=$(crontab -l 2>/dev/null | grep -v "# $PROG\$") if [ -n "$_cur" ]; then printf '%s\n' "$_cur" | crontab - else crontab -r 2>/dev/null || printf '' | crontab - fi fi note "Removed cron job" fi restore_omarchy fi if [ -f "$BIN_DIR/$PROG" ] && grep -q '^PROG=morning-masterpiece-wallpaper$' "$BIN_DIR/$PROG"; then run rm -f "$BIN_DIR/$PROG" && note "Removed $BIN_DIR/$PROG" fi if [ "$PURGE" = 1 ]; then run rm -rf "$CACHE_DIR" "$CONFIG_DIR" note "Removed cache ($CACHE_DIR) and config ($CONFIG_DIR)." case $st_applied_backend in '' | none | omarchy) ;; *) backend_persistent "$st_applied_backend" && note "Your desktop ($st_applied_backend) still points at the deleted image; pick a new wallpaper in its settings." ;; esac else note "Kept cached images in $CACHE_DIR (use --purge to delete them)." fi note "Uninstalled. Your desktop settings were otherwise left as they are." } cmd_status() { discover_env load_state note "Morning Masterpiece wallpaper $VERSION ($OS)" resolve_backend case $? in 0) note "Backend: $BACKEND${S_BACKEND:+ (setting: $S_BACKEND)}" ;; 4) note "Backend: no graphical session detected" ;; *) note "Backend: none available (tried: ${TRIED:-none})" ;; esac _notify=on [ "$S_NOTIFY" = 1 ] || _notify=off note "Mode: $S_MODE, $S_VARIANT image, notifications $_notify, keep $S_KEEP" if [ -f "$CONFIG_FILE" ]; then note "Config: $CONFIG_FILE"; else note "Config: none ($CONFIG_FILE)"; fi note "Cache: $CACHE_DIR ($(cached_images | wc -l | tr -d ' ') images)" if [ -n "$st_publish_date" ]; then note "Artwork: $st_publish_date $st_slug${st_applied_backend:+ (applied with $st_applied_backend)}" fi _sched="not installed" if [ "$OS" = macos ]; then [ -f "$LAUNCHD_PLIST" ] && _sched="launchd agent $LAUNCHD_LABEL" elif [ -f "$SYSTEMD_DIR/$UNIT.timer" ]; then _sched="systemd user timer ($(systemctl --user is-active "$UNIT.timer" 2>/dev/null || echo unknown))" elif have crontab && crontab -l 2>/dev/null | grep -q "# $PROG\$"; then _sched="cron" fi note "Schedule: $_sched" } # ------------------------------------------------------------------ main --- main() { SELF=$0 CMD= FORCE=0 PURGE=0 O_BACKEND='' O_MODE='' O_VARIANT='' O_NOTIFY='' while [ $# -gt 0 ]; do case $1 in -f | --force) FORCE=1 ;; -b | --backend) [ $# -ge 2 ] || { warn "$1 needs a value" && return 2; } O_BACKEND=$2 shift ;; --backend=*) O_BACKEND=${1#*=} ;; -m | --mode) [ $# -ge 2 ] || { warn "$1 needs a value" && return 2; } O_MODE=$2 shift ;; --mode=*) O_MODE=${1#*=} ;; --phone) O_VARIANT=phone ;; --desktop) O_VARIANT=desktop ;; --notify) O_NOTIFY=yes ;; --no-notify) O_NOTIFY=no ;; -n | --dry-run) DRY_RUN=1 ;; -q | --quiet) QUIET=1 ;; --purge) PURGE=1 ;; -h | --help | help) usage return 0 ;; -V | --version | version) echo "$PROG $VERSION" return 0 ;; --info) CMD=info ;; update | install | uninstall | info | open | status) if [ -n "$CMD" ]; then warn "only one command at a time" return 2 fi CMD=$1 ;; *) warn "unknown argument: $1 (see --help)" return 2 ;; esac shift done [ -n "$CMD" ] || CMD=update init_paths set_defaults _ok=0 load_config || _ok=2 load_env_settings || _ok=2 [ -z "$O_BACKEND" ] || set_setting backend "$O_BACKEND" --backend || _ok=2 [ -z "$O_MODE" ] || set_setting mode "$O_MODE" --mode || _ok=2 [ -z "$O_VARIANT" ] || set_setting variant "$O_VARIANT" "--$O_VARIANT" || _ok=2 [ -z "$O_NOTIFY" ] || set_setting notify "$O_NOTIFY" --notify || _ok=2 [ $_ok = 0 ] || return 2 trap cleanup EXIT trap 'exit 130' INT TERM HUP case $CMD in update) cmd_update ;; install) cmd_install ;; uninstall) cmd_uninstall ;; info) cmd_info ;; open) cmd_open ;; status) cmd_status ;; esac } # Tests source this file with MM_WALLPAPER_LIB=1 to call the functions. if [ "${MM_WALLPAPER_LIB:-}" != 1 ]; then main "$@" exit $? fi