Wren Linux

wren-linux / bin / wallpaper

#!/bin/bash
# wallpaper: written by Wren Linux parts/wallpaper.sh
# Your own picture on the desktop: the newest JPEG or PNG in
# ~/Pictures/wren/desktop becomes the wallpaper; with none there, the default: a
# wren for the theme in use, so a theme switch (it reloads sway) swaps the wren.
# A background of your own in your sway config ("output * bg") stays instead.
# sway runs it at every start and reload (parts/wallpaper.sh).
#   wallpaper           put the picture up (or nothing, if there is none)
#   wallpaper next      the next picture in ~/Pictures/Wallpapers (Super+]),
#   wallpaper previous  or the one before (Super+[), up at once and kept
#   wallpaper pick      choose one from ~/Pictures/Wallpapers by name (Super+Shift+w),
#                       or "0  No wallpaper"
#   wallpaper none      no wallpaper (Super+\): the theme's plain background again
#   wallpaper status    which picture is used, and which files aren't, and why
# A picture chosen with next, previous or pick is copied into the desktop
# folder as chosen.jpg (or .png), so it is the newest there and stays after a
# reboot; a picture you copy in yourself later wins over it. "No wallpaper"
# stays the same way: until you choose one, or copy a newer picture in, or
# wrenctl remove wallpapers ends it (extras/wallpapers.sh).
set -uo pipefail

DIR=${WREN_WALLPAPER_DIR:-$HOME/Pictures/wren/desktop}
COLL=${WREN_WALLPAPERS:-$HOME/Pictures/Wallpapers}
CACHE=${XDG_CACHE_HOME:-$HOME/.cache}/wren
# the collection's file last chosen, for next and previous ("none": no
# wallpaper, since this file's time)
CURRENT=$CACHE/chosen-wallpaper
MAX_BYTES=$((50 * 1024 * 1024))
DEFAULTS=$HOME/.local/share/wren/wallpapers

# why_not FILE: empty if FILE can be the wallpaper, else the reason
why_not() {
    local f=$1 magic
    case ${f,,} in
        *.jpg | *.jpeg | *.png) ;;
        *.webp) echo "a WebP picture: only JPEG and PNG can be wallpapers (convert it, or save a JPEG)"; return ;;
        *) echo "not a .jpg, .jpeg or .png"; return ;;
    esac
    [ -f "$f" ] && [ -r "$f" ] || { echo "can't be read"; return; }
    [ "$(stat -L -c %s -- "$f")" -le "$MAX_BYTES" ] || { echo "bigger than 50 MB"; return; }
    # what the file really is, by its first bytes, not its name
    magic=$(head -c 8 -- "$f" | od -An -tx1 | tr -d ' \n')
    case $magic in
        89504e470d0a1a0a) ;;
        ffd8ff*) ;;
        *) echo "not really a JPEG or PNG picture" ;;
    esac
}

# newest: the newest picture in DIR that can be used
newest() {
    local f best=
    for f in "$DIR"/*; do
        [ -e "$f" ] || continue
        [ -z "$(why_not "$f")" ] || continue
        if [ -z "$best" ] || [ "$f" -nt "$best" ]; then best=$f; fi
    done
    [ -n "$best" ] && echo "$best"
}

# default_picture: the wren for the theme in use, unless your sway config sets a
# background of its own, or the defaults aren't here
default_picture() {
    local theme f
    if grep -qsE '^[[:space:]]*output[[:space:]].*[[:space:]]bg[[:space:]]' "$HOME/.config/sway/config"; then return 1; fi
    theme=$(head -c 16 "${XDG_CONFIG_HOME:-$HOME/.config}/wren/theme" 2>/dev/null)
    [ "$theme" = light ] || theme=dark
    f=$DEFAULTS/default-$theme.jpg
    [ -f "$f" ] && echo "$f"
}

# shown TEXT: TEXT without control characters (a file name can hold escape codes)
shown() { printf '%s' "$1" | tr -d '\000-\037\177'; }

# collection: the usable pictures in COLL, one a line, by name
collection() {
    local f
    for f in "$COLL"/*; do
        [ -e "$f" ] || continue
        case ${f##*/} in *$'\n'*) continue ;; esac
        [ -z "$(why_not "$f")" ] && printf '%s\n' "$f"
    done
}

# label FILE: what to call it: its title from CREDITS.txt, after its source
# ("Webb: Stars brewing in a nebulous cauldron"), else its tidied file name
label() {
    local base=${1##*/} src="" title
    # (the name on a line of its own, its title under it; or, from an install from before,
    # all of it on one line: "NAME: TITLE. Credit: ...")
    title=$(awk -v n="$base" -v b="$base: " '
        $0 == n { getline; if (sub(/^ *Title: */, "")) print; exit }
        index($0, b) == 1 { print substr($0, length(b) + 1); exit }' "$COLL/CREDITS.txt" 2>/dev/null)
    title=${title%%. Credit:*}
    case $base in
        nasa_*) src=NASA ;; webb_*) src=Webb ;;
        earth_*) src="Earth Observatory" ;; budgie_*) src=Budgie ;;
    esac
    if [ -z "$title" ]; then
        title=${base%.*}
        [ -z "$src" ] || title=${title#*_}
        title=${title//[_-]/ }
    fi
    shown "${src:+$src: }$title"
}

# apply: the newest picture in DIR goes up, else the default
apply() {
    local pic ext link old
    if pic=$(newest); then
        # "no wallpaper" was chosen after this picture: the plain background stays
        if [ "$(cat "$CURRENT" 2>/dev/null)" = none ] && [ ! "$pic" -nt "$CURRENT" ]; then return 0; fi
    else
        [ "$(cat "$CURRENT" 2>/dev/null)" != none ] || return 0
        pic=$(default_picture) || return 0
    fi
    # sway reads the path inside a command line, where a quote or a ; in a file
    # name would end it: hand sway a link with a name made of digits only. The
    # name changes with the picture, so sway starts the new one.
    case $CACHE in *[\"\'\\\;]* | *$'\n'*) echo "wallpaper: can't use the folder $CACHE (odd characters in its name)" >&2; return 1 ;; esac
    mkdir -p "$CACHE"
    ext=${pic##*.}
    ext=${ext,,}
    [ "$ext" = jpeg ] && ext=jpg
    link="$CACHE/wallpaper-$(stat -L -c '%i-%Y' -- "$pic").$ext"
    # (only the links: wallpaper- and digits)
    for old in "$CACHE"/wallpaper-[0-9]*; do
        [ "$old" = "$link" ] || rm -f -- "$old"
    done
    ln -sfn -- "$pic" "$link"
    swaymsg -q "output * bg \"$link\" fill"
}

# use FILE: FILE, from the collection, becomes the wallpaper and stays
use() {
    local ext=${1##*.}
    ext=${ext,,}
    [ "$ext" = jpeg ] && ext=jpg
    mkdir -p "$DIR" "$CACHE"
    cp -- "$1" "$DIR/.chosen.part" || return 1
    rm -f -- "$DIR/chosen.jpg" "$DIR/chosen.png"
    mv -f -- "$DIR/.chosen.part" "$DIR/chosen.$ext"
    printf '%s\n' "${1##*/}" >"$CURRENT"
    apply
    # its name, for a moment, if notifications are set up
    if command -v notify-send >/dev/null; then notify-send -t 3000 Wallpaper "$(label "$1")" 2>/dev/null || true; fi
}

# step +1|-1: the next or previous picture in the collection
step() {
    local pics=() cur i n=0
    mapfile -t pics < <(collection)
    n=${#pics[@]}
    [ "$n" -gt 0 ] || { echo "wallpaper: no pictures in $COLL (wrenctl install wallpapers fetches some)" >&2; return 1; }
    cur=$(cat "$CURRENT" 2>/dev/null)
    i=-1
    for ((k = 0; k < n; k++)); do [ "${pics[k]##*/}" = "$cur" ] && i=$k; done
    if [ "$i" -lt 0 ]; then
        # nothing chosen yet: start at the first (or, going back, the last)
        if [ "$1" -gt 0 ]; then i=0; else i=$((n - 1)); fi
    else
        i=$(((i + $1 + n) % n))
    fi
    use "${pics[i]}"
}

# pick: a list of the collection by name, newest first, in Wren Linux's wmenu look
# none: no wallpaper: the chosen picture goes, and sway reloads its own
# background (its config's "output * bg" line, or its plain one); pictures of
# your own in the desktop folder stay, but don't go up until one is newer
none() {
    mkdir -p "$CACHE"
    rm -f -- "$DIR/chosen.jpg" "$DIR/chosen.png"
    echo none >"$CURRENT"
    swaymsg -q reload
    if command -v notify-send >/dev/null; then notify-send -t 3000 Wallpaper "No wallpaper" 2>/dev/null || true; fi
}

pick() {
    local pics=() f sel num bg fg
    # the theme's menu colors (bin/theme), else black and white
    bg=$("$HOME/.local/bin/theme" get WMENU_BG 2>/dev/null) || bg=000000
    fg=$("$HOME/.local/bin/theme" get WMENU_TEXT 2>/dev/null) || fg=ffffff
    mapfile -t pics < <(collection | while IFS= read -r f; do printf '%s\t%s\n' "$(stat -c %Y -- "$f")" "$f"; done | sort -rn | cut -f2-)
    # each line starts with its number, which says which picture it is (0: none)
    sel=$({ echo "0  No wallpaper"; for i in "${!pics[@]}"; do printf '%d  %s\n' "$((i + 1))" "$(label "${pics[i]}")"; done; } |
        wmenu -i -l 15 -p "Wallpaper:" -f 'monospace 12' -N "$bg" -n "$fg" -M "$bg" -m "$fg" -S "$fg" -s "$bg") || return 0
    num=${sel%% *}
    [[ $num =~ ^[0-9]{1,6}$ ]] && [ "$num" -le ${#pics[@]} ] || return 0
    if [ "$num" -eq 0 ]; then none; else use "${pics[num - 1]}"; fi
}

case ${1:-} in
    status)
        if [ "$(cat "$CURRENT" 2>/dev/null)" = none ] && { ! pic=$(newest) || [ ! "$pic" -nt "$CURRENT" ]; }; then
            echo "Wallpaper: none (the theme's plain background; Super+] or Super+Shift+w chooses one)"
        elif pic=$(newest); then
            if [ "${pic##*/}" = chosen.jpg ] || [ "${pic##*/}" = chosen.png ]; then
                cur=$(cat "$CURRENT" 2>/dev/null)
                if [ -n "$cur" ] && [ -f "$COLL/$cur" ]; then
                    echo "Wallpaper: $(label "$COLL/$cur") ($(shown "$cur"), from $COLL)"
                else
                    echo "Wallpaper: ${pic##*/} (from $DIR)"
                fi
            else
                echo "Wallpaper: $(shown "${pic##*/}") (from $DIR)"
            fi
        else
            if pic=$(default_picture); then
                pic=${pic##*/default-}
                echo "Wallpaper: the default wren (${pic%.jpg}), with no picture in $DIR"
            else
                echo "Wallpaper: the default (no picture in $DIR)"
            fi
        fi
        for d in "$DIR" "$COLL"; do
            for f in "$d"/*; do
                [ -f "$f" ] || continue
                case ${f##*/} in *.txt) continue ;; esac
                why=$(why_not "$f")
                if [ -n "$why" ]; then printf '  not used: %s: %s\n' "$(shown "${f##*/}")" "$why"; fi
            done
        done
        exit 0 ;;
    next) step 1 ;;
    previous | prev) step -1 ;;
    pick) pick ;;
    none) none ;;
    "") apply ;;
    *) echo "usage: wallpaper [next | previous | pick | none | status]" >&2; exit 2 ;;
esac