wren-linux / bin / wren-learn
#!/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