#!/bin/bash
# wren-learn: written by Wren Linux parts/wren-menu.sh
# Learn bash by reading Wren Linux itself: every bash script it is made of, the
# smallest first, with a box to check off each one you have read and understood.
# At the top, the two best places to learn bash from: online, Greg's BashGuide;
# here, bash's own manual (man bash; the whole Bash Reference Manual with
# bash-doc). Your progress is kept in ~/.local/share/wren/bash-learning.progress
# (one script per line), in your home folder, so a backup of it keeps it.
#   wren-learn            the list: arrows (or j/k) move, Space checks, Enter
#                         or a click opens (scripts read-only), n the next
#                         unread, / searches, q or Esc leaves
#   wren-learn --list     the list as text: [x] or [ ], lines, the script
#   wren-learn --toggle FILE   check or uncheck one (FILE as --list shows it)
set -uo pipefail

if [ -n "${WREN_DIR:-}" ]; then
    DIR=$WREN_DIR
elif [ -f "${WREN_SYSTEM_DIR:-/usr/share/wren}/lib.sh" ]; then
    DIR=${WREN_SYSTEM_DIR:-/usr/share/wren}
else
    DIR=$HOME/wren
fi
[ -f "$DIR/lib.sh" ] || { echo "wren-learn: Wren Linux's folder isn't here ($DIR); set WREN_DIR." >&2; exit 1; }
PROGRESS=${WREN_LEARN_PROGRESS:-${XDG_DATA_HOME:-$HOME/.local/share}/wren/bash-learning.progress}
ONLINE=https://mywiki.wooledge.org/BashGuide
# here: the Bash Reference Manual, if bash-doc put it here; else bash's manual page
LOCAL_HTML=/usr/share/doc/bash/bashref.html

# scripts: "LINES PATH" for every bash (or sh) script, smallest first; PATH
# from Wren Linux's folder. A script is a .sh file, or a file whose first line
# runs bash or sh.
scripts() {
    local f first
    while IFS= read -r -d '' f; do
        case $f in *.sh) ;; *)
            IFS= read -r first <"$f" 2>/dev/null || continue
            [[ $first =~ ^#!(/usr)?/bin/(env\ )?(ba)?sh([[:space:]]|$) ]] || continue ;;
        esac
        printf '%s %s\n' "$(wc -l <"$f")" "${f#"$DIR"/}"
    done < <(find "$DIR" -path "$DIR/.git" -prune -o -type f -print0 2>/dev/null) | sort -n -k1,1 -k2,2
}

# READ: the scripts read, from the progress file (read once, not on every key)
declare -A READ=()
if [ -f "$PROGRESS" ]; then
    while IFS= read -r l; do [ -z "$l" ] || READ[$l]=1; done <"$PROGRESS"
fi
checked() { [ -n "${READ[$1]:-}" ]; }
# toggle FILE: in the progress file, or out of it
toggle() {
    mkdir -p "${PROGRESS%/*}"
    if checked "$1"; then
        grep -vxF -- "$1" "$PROGRESS" >"$PROGRESS.new" || true
        mv -f "$PROGRESS.new" "$PROGRESS"
        unset 'READ[$1]'
    else
        printf '%s\n' "$1" >>"$PROGRESS"
        READ[$1]=1
    fi
}

mapfile -t ALL < <(scripts)
[ ${#ALL[@]} -gt 0 ] || { echo "wren-learn: no scripts found in $DIR" >&2; exit 1; }
# NUM: each script's place in the list, 1 for the smallest (kept while searching)
declare -A NUM=()
for i in "${!ALL[@]}"; do NUM[${ALL[i]#* }]=$((i + 1)); done
# row LINE: "  12  parts/x.sh  (34 lines)", for "LINES PATH"
row() { printf '%4s  %s  (%s lines)' "${NUM[${1#* }]}" "${1#* }" "${1%% *}"; }

case ${1:-} in
    --list)
        for l in "${ALL[@]}"; do
            p=${l#* }
            if checked "$p"; then m='[x]'; else m='[ ]'; fi
            printf '%s %s\n' "$m" "$(row "$l")"
        done
        exit 0 ;;
    --toggle)
        [ -n "${2:-}" ] || { echo "usage: wren-learn --toggle FILE" >&2; exit 2; }
        found=
        for l in "${ALL[@]}"; do [ "${l#* }" != "$2" ] || found=1; done
        [ -n "$found" ] || { echo "wren-learn: $2 isn't one of Wren Linux's scripts (wren-learn --list)" >&2; exit 1; }
        toggle "$2"
        exit 0 ;;
    "") ;;
    *) sed -n '2,/^set -uo/{ /^set -uo/d; s/^# \{0,1\}//; p }' "$0"; exit 2 ;;
esac

# ---- the screen ----
# A green check mark where the terminal shows UTF-8, else a green *
case ${LC_ALL:-${LC_CTYPE:-${LANG:-}}} in *[Uu][Tt][Ff]-8* | *[Uu][Tt][Ff]8*) MARK='✓' ;; *) MARK='*' ;; esac
GREEN=$'\e[32m' BOLD=$'\e[1m' DIM=$'\e[2m' REV=$'\e[7m' OFF=$'\e[0m'
filter='' sel=0 top=0 view=()
# rows: the two references first, then the scripts the search finds
rebuild() {
    local l
    view=(online local)
    for l in "${ALL[@]}"; do
        [[ -z $filter || ${l#* } == *"$filter"* ]] && view+=("$l")
    done
    [ "$sel" -lt ${#view[@]} ] || sel=$((${#view[@]} - 1))
}

draw() {
    local rows cols height i r line p n out
    read -r rows cols < <(stty size 2>/dev/null || echo "24 80")
    height=$((rows - 4))
    [ "$height" -ge 1 ] || height=1
    [ "$sel" -ge "$top" ] || top=$sel
    [ "$sel" -lt $((top + height)) ] || top=$((sel - height + 1))
    n=0
    for p in "${!READ[@]}"; do [ -z "${NUM[$p]:-}" ] || n=$((n + 1)); done
    # all in one write, over the old screen (clearing it first makes it flicker)
    out=$(printf '\e[H%sLearn bash with Wren Linux%s  %s of %s read (%s%%)%s\n' "$BOLD" "$OFF" "$n" "${#ALL[@]}" $((n * 100 / ${#ALL[@]})) \
        "${filter:+  search: $filter}")$'\e[K\n'
    out+=$(printf '%sThe smallest first. Space: read it. Enter or a click: open. n: next unread. /: search. q: leave.%s' "$DIM" "$OFF")$'\e[K\n\e[K\n'
    for ((i = 0; i < height && top + i < ${#view[@]}; i++)); do
        r=${view[top + i]}
        case $r in
            online) line="  >  Online: Greg's BashGuide, the best place to learn bash ($ONLINE)" ;;
            local)
                if [ -f "$LOCAL_HTML" ] && command -v w3m >/dev/null; then line="  >  Here: the Bash Reference Manual ($LOCAL_HTML)"
                elif [ -f "$LOCAL_HTML" ]; then line="  >  Here: bash's own manual (man bash; the whole Reference Manual with w3m: wrenctl install w3m)"
                else line="  >  Here: bash's own manual (man bash; the whole Reference Manual: sudo apt install bash-doc)"; fi ;;
            *)
                p=${r#* }
                if checked "$p"; then line="[${GREEN}${MARK}${OFF}] "; else line="[ ] "; fi
                line+=$(row "$r") ;;
        esac
        # (cut to the window; the color codes don't take room)
        if [ $((top + i)) -eq "$sel" ]; then out+="$REV${line:0:cols+9}$OFF"$'\e[K\n'
        else out+="${line:0:cols+9}"$'\e[K\n'; fi
    done
    printf '%s\e[J' "$out"
}

# open ROW: a reference in your browser or pager; a script read-only, in
# vim's read-only view or less (never to be changed by accident). The online
# one opens through sway, so it stays open after this window closes: Wren
# Linux's firefox (the sandboxed one, when it is set up), else xdg-open.
open_row() {
    local r=$1 b
    if [ "$r" = online ] && [ -n "${SWAYSOCK:-}" ]; then
        for b in firefox xdg-open; do
            command -v "$b" >/dev/null && swaymsg -q exec -- "$b" "$ONLINE" >/dev/null 2>&1 && return
        done
    fi
    # bash's manual, in sway: a terminal window of its own beside the list, on the workspace you
    # are on, so the list stays as it was. (sway runs it through sh: only fixed words go in.)
    if [ "$r" = local ] && [ -n "${SWAYSOCK:-}" ] && command -v foot >/dev/null; then
        b='man bash'
        if [ -f "$LOCAL_HTML" ] && command -v w3m >/dev/null; then b="w3m $LOCAL_HTML"; fi
        # shellcheck disable=SC2086  # the words of the command, one an argument
        swaymsg -q exec -- foot --log-level=error --app-id=wren-learn-manual $b >/dev/null 2>&1 && return
    fi
    printf '\e[?1000l\e[?1006l\e[?25h\e[?1049l'
    stty "$saved" 2>/dev/null
    case $r in
        online) printf '%s\n\nNo browser to open it in here: open that address in one.\nPress Enter to go back to the list. ' "$ONLINE"; read -r _ ;;
        local)
            if [ -f "$LOCAL_HTML" ] && command -v w3m >/dev/null; then w3m "$LOCAL_HTML"
            else man bash; fi ;;
        *)
            if command -v view >/dev/null; then view -- "$DIR/${r#* }"
            else less -- "$DIR/${r#* }"; fi ;;
    esac
    screen_on
}
screen_on() { printf '\e[?1049h\e[?25l\e[?1000h\e[?1006h'; stty -echo -icanon min 1 2>/dev/null; }
saved=$(stty -g 2>/dev/null)
trap 'printf "\e[?1000l\e[?1006l\e[?25h\e[?1049l"; stty "$saved" 2>/dev/null' EXIT
trap 'exit 130' INT TERM HUP
screen_on
rebuild
while :; do
    draw
    IFS= read -rsn1 k || exit 0
    case $k in
        $'\e')
            # an arrow key or a mouse click, or Esc alone
            if IFS= read -rsn1 -t 0.05 k2; then
                if [ "$k2" = '[' ]; then
                    IFS= read -rsn1 -t 0.05 k3
                    case $k3 in
                        A) k=up ;; B) k=down ;; 5) read -rsn1 -t 0.05 _; k=pgup ;; 6) read -rsn1 -t 0.05 _; k=pgdn ;;
                        '<')
                            m=''
                            while IFS= read -rsn1 -t 0.05 c && [[ $c != [Mm] ]]; do m+=$c; done
                            k=other
                            if [ "$c" = M ] && [[ $m =~ ^0\;([0-9]{1,4})\;([0-9]{1,4})$ ]]; then
                                i=$((top + BASH_REMATCH[2] - 4))
                                if [ "$i" -ge 0 ] && [ "$i" -lt ${#view[@]} ]; then sel=$i; k=enter; fi
                            elif [[ $m =~ ^64\; ]]; then k=up
                            elif [[ $m =~ ^65\; ]]; then k=down; fi ;;
                        *) k=other ;;
                    esac
                else k=other; fi
            else exit 0; fi ;;
        '') k=enter ;;
    esac
    case $k in
        up | k) [ "$sel" -le 0 ] || sel=$((sel - 1)) ;;
        down | j) [ "$sel" -ge $((${#view[@]} - 1)) ] || sel=$((sel + 1)) ;;
        pgup) sel=$((sel - 10)); [ "$sel" -ge 0 ] || sel=0 ;;
        pgdn) sel=$((sel + 10)); [ "$sel" -lt ${#view[@]} ] || sel=$((${#view[@]} - 1)) ;;
        ' ') case ${view[sel]} in online | local) ;; *) toggle "${view[sel]#* }" ;; esac ;;
        enter) open_row "${view[sel]}" ;;
        n)
            for ((i = sel + 1; i < ${#view[@]}; i++)); do
                case ${view[i]} in online | local) continue ;; esac
                checked "${view[i]#* }" || { sel=$i; break; }
            done ;;
        /)
            printf '\e[%d;1H\e[K/' "$(($(stty size 2>/dev/null | cut -d' ' -f1) + 0))"
            stty echo icanon 2>/dev/null
            IFS= read -r filter || filter=''
            stty -echo -icanon min 1 2>/dev/null
            sel=0 top=0
            rebuild ;;
        q) exit 0 ;;
    esac
done
