#!/bin/sh
# alloy-backdrop — the desktop background: this machine's own verbs and keys.
#
# Runs as a shop surface on the wlr-layer-shell background layer, started from
# the sway config. A running Alloy machine otherwise ships no keybinding
# reference at all: the only other copy is docs/manual/05-keybindings.md, and no
# COPY puts docs/ into the image. So an empty workspace shows the reference
# instead of a flat colour.
#
# BOTH LISTS ARE DERIVED, never transcribed. The verbs come from `alloy --help`
# and the keys from the sway config this session actually loaded. A second copy
# of either would be a list to maintain, and the one that rots is the one
# nobody reads because it is behind their windows.
#
# A SCRIPT RATHER THAN A VERB, and that is the reason: `alloy backdrop` would
# appear in the verb list this panel prints. usr/bin is where the session's own
# helpers already live (alloy-menu, alloy-clipmenu, alloy-layer-notice).
#
# IT TAKES NO INPUT. shop's `--layer background` sets an empty input region and
# no keyboard interactivity, so clicks and keys pass through to whatever is
# actually focused. Nothing here should ever want a keypress.
#
# NO COLOUR OF ITS OWN. Only SGR bold and dim, which shop resolves out of the
# theme named in ~/.config/shop/config.toml. `alloy theme apply` swaps that file
# for its .night sibling, so the backdrop follows the desktop without knowing a
# palette exists (docs/TOKENS.md: no hex outside the theme files).
#
# IT MUST NOT EXIT. shop closes when the program it runs finishes, and a closed
# background surface is a black screen, not a flat one. So this parks after
# drawing, and redraws on SIGWINCH: the surface is sized to the output, and
# `alloy display --reconcile` runs at login, so the first size this sees is
# often not the one it keeps.

set -u

CONFIG="${ALLOY_BACKDROP_CONFIG:-$HOME/.config/sway/config}"
ALLOY="${ALLOY_BACKDROP_ALLOY:-alloy}"

# The verbs, from the binary rather than from a list here. clap indents each
# command by two spaces under "Commands:" and ends the block with a blank line.
# `help` is clap's own and is dropped: it is not something the machine does.
verbs() {
    "$ALLOY" --help 2>/dev/null | awk '
        /^Commands:/ { inside = 1; next }
        inside && /^[[:space:]]*$/ { exit }
        inside && /^  [a-z]/ {
            name = $1
            if (name == "help") next
            $1 = ""
            sub(/^[[:space:]]+/, "")
            print name "\t" $0
        }
    '
}

# The keys, from the config the session loaded. Variables are resolved from the
# file's own `set` lines, so $mod reads as the key someone presses rather than
# as sway's spelling of it, and $term reads as the terminal this image ships.
# Reading only the main config on purpose: the drop-ins under config.d are the
# machine's and the user's, and a backdrop that printed someone's private binds
# onto the desktop would be a surprise.
keys() {
    [ -r "$CONFIG" ] || return 0
    awk '
        # Collect `set $name value` first; sway allows use before definition,
        # so substitution waits until the whole file is read.
        /^set[[:space:]]+\$/ {
            name = $2
            $1 = ""; $2 = ""
            sub(/^[[:space:]]+/, "")
            vars[name] = $0
            next
        }
        # A `mode "resize" { ... }` block scopes its binds to that mode. They
        # are not reachable from the desktop the way the others are, so they
        # carry the mode on the chord: an untagged `h` printed beside the
        # global binds would read as Super+h, which does something else.
        /^mode[[:space:]]+"/ {
            mode = $2
            gsub(/"/, "", mode)
            next
        }
        mode != "" && /^}/ { mode = ""; next }
        /^[[:space:]]*bindsym[[:space:]]/ {
            line = $0
            sub(/^[[:space:]]*bindsym[[:space:]]+/, "", line)
            # `--release` and friends are modifiers on the bind, not the chord.
            while (line ~ /^--[a-z-]+[[:space:]]/) {
                sub(/^--[a-z-]+[[:space:]]+/, "", line)
            }
            split(line, part, /[[:space:]]+/)
            chord = part[1]
            action = line
            sub(/^[^[:space:]]+[[:space:]]+/, "", action)
            # `exec` is how sway spells "run a command" and is on most of these
            # lines; printing it on every row costs width and says nothing.
            sub(/^exec[[:space:]]+/, "", action)
            if (mode != "") chord = mode " " chord
            binds[++n] = chord "\t" action
        }
        END {
            for (i = 1; i <= n; i++) {
                line = binds[i]
                for (name in vars) {
                    gsub("\\" name, vars[name], line)
                }
                # Mod4 is what sway calls it and Super is what is printed on
                # the key, which is the whole point of resolving the variable.
                gsub(/Mod4/, "Super", line)
                gsub(/Mod1/, "Alt", line)
                print line
            }
        }
    ' "$CONFIG"
}

# Terminal size. `stty size` answers from the pty shop gave us; the fallback is
# a small sane grid rather than an error, because a backdrop that refused to
# draw would leave the screen black.
#
# ALLOY_BACKDROP_SIZE overrides it as "<rows> <cols>". That exists so the layout
# can be asserted without allocating a pty: the test that matters is that every
# bind reaches the panel at any width, and a test harness has no terminal.
size() {
    if [ -n "${ALLOY_BACKDROP_SIZE:-}" ]; then
        printf '%s\n' "$ALLOY_BACKDROP_SIZE"
        return
    fi
    s=$(stty size 2>/dev/null) || s=""
    case "$s" in
        [0-9]*' '[0-9]*) printf '%s\n' "$s" ;;
        *) printf '%s\n' "24 80" ;;
    esac
}

render() {
    set -- $(size)
    rows=$1
    cols=$2

    printf '\033[2J\033[H'

    { verbs | sed 's/^/V\t/'; keys | sed 's/^/K\t/'; } | awk \
        -v rows="$rows" -v cols="$cols" -F '\t' '
        function spaces(n,   out) {
            out = ""
            while (n-- > 0) out = out " "
            return out
        }
        # Flow a section into as many columns as the width allows, filling each
        # column top to bottom so the reading order down a column is the order
        # the source had.
        #
        # Padding is computed from the VISIBLE length, which is why the styled
        # cell is assembled here rather than handed in ready-made: length()
        # counts the SGR bytes, so padding a styled string aligns the escapes
        # and not the glyphs.
        function columns(key, val, count, kw, width, label,   per, cn, r, c, idx, line, cell, vis, gap) {
            if (count == 0) return
            print ""
            gap = 3
            cn = int((cols - 2 + gap) / (width + gap))
            if (cn < 1) cn = 1
            per = int((count + cn - 1) / cn)
            printf "\033[1m  %s\033[0m\n", label
            for (r = 0; r < per; r++) {
                line = ""
                for (c = 0; c < cn; c++) {
                    idx = c * per + r + 1
                    if (idx > count) continue
                    if (c > 0) line = line spaces(gap)
                    cell = "\033[1m" key[idx] "\033[0m" spaces(kw - length(key[idx])) \
                           " \033[2m" val[idx] "\033[0m"
                    vis = kw + 1 + length(val[idx])
                    line = line cell spaces(width - vis)
                }
                sub(/[[:space:]]+$/, "", line)
                print "  " line
            }
        }
        {
            if ($1 == "V") {
                vn++
                vkey[vn] = $2
                vval[vn] = $3
                if (length($2) > vw) vw = length($2)
            } else {
                kn++
                kkey[kn] = $2
                kval[kn] = $3
                if (length($2) > kw) kw = length($2)
            }
        }
        END {
            for (i = 1; i <= vn; i++) {
                vlen = vw + 1 + length(vval[i])
                if (vlen > vwidth) vwidth = vlen
            }
            for (i = 1; i <= kn; i++) {
                klen = kw + 1 + length(kval[i])
                if (klen > kwidth) kwidth = klen
            }
            columns(vkey, vval, vn, vw, vwidth, "alloy")
            columns(kkey, kval, kn, kw, kwidth, "keys")
        }
    '
}

case "${1:-}" in
    --once)
        # Draw and leave. What `alloy-backdrop --once | less` gives a person who
        # wants the reference in front of them, and what lets the panel be
        # asserted without a compositor or a pty.
        render
        exit 0
        ;;
    "") ;;
    *)
        echo "alloy-backdrop: expected --once or no argument" >&2
        exit 2
        ;;
esac

render
trap 'render' WINCH

# Park. `wait` is what makes the trap prompt rather than waiting out the sleep,
# so a resize redraws now instead of up to a day later.
while :; do
    sleep 86400 &
    wait $! 2>/dev/null || true
done
