Wren Linux

wren-linux / install-extras.sh

#!/bin/bash
# Pick optional programs from a list and install them (lib/checklist.sh): arrow
# keys move (round the ends), Space or a click checks a box, the wheel scrolls,
# Enter or a click on Install installs.
# program is a script in extras/. install-base.sh asks right after you agree to
# the base install and installs what you picked once the base is done; you can
# run this again whenever you like. Nothing is checked to begin with.
# The list comes in groups by what the programs are for, from each extra's
# "# category:" line, and in each group the most secure first, from its
# "# security:" line, shown as three circles: most (walled off from your files or
# network, or it protects you), some (everyday programs from signed sources, run
# as you) and least (it lowers protection).
#   bash install-extras.sh               the list, then installs what you check
#   bash install-extras.sh --list        only print what is available
#   bash install-extras.sh --choose F    the list only; writes the names you checked to file F
#   bash install-extras.sh --install F   installs the names in file F (no list)
#   bash install-extras.sh --remove      a list of what is installed; removes what
#                                        you check (each extra's --remove), after asking
# ~/.config/wren/preselect (one name a line, left by the install stick, e.g.
# make-install-iso.sh --simplesway) ticks those in the first list shown; it
# is then deleted, so later lists start empty again.
. "$(dirname -- "${BASH_SOURCE[0]}")/lib.sh"
. "$WREN_DIR/lib/checklist.sh"

# The groups, in the order they come: CATEGORY<TAB>HEADING
CATEGORIES=$'web\tBROWSE THE WEB
talk\tTALK AND SHARE
money\tMONEY
files\tPASSWORDS AND PRIVATE FILES
media\tDOCUMENTS AND MEDIA
systems\tTOR, WHONIX AND OTHER SYSTEMS
devices\tNETWORK AND DEVICES
computer\tTHIS COMPUTER
dev\tFOR DEVELOPERS'
# category_rank CATEGORY: where its group comes (one not in the list: with this computer's)
category_rank() { awk -F'\t' -v c="$1" '$1 == c { print NR; f = 1 } END { if (!f) print 8 }' <<<"$CATEGORIES"; }
category_heading() { awk -F'\t' -v c="$1" '$1 == c { print $2; f = 1 } END { if (!f) print "THIS COMPUTER" }' <<<"$CATEGORIES"; }
# tier_rank TIER: most secure first (an extra without the line counts as some)
tier_rank() { case $1 in most) echo 1 ;; least) echo 3 ;; *) echo 2 ;; esac; }
# How secure, as three circles, filled for each step; where a terminal can't show them (the
# Linux console's font, a language setting without UTF-8), stars
if [[ ${LC_ALL:-${LC_CTYPE:-${LANG:-}}} =~ [Uu][Tt][Ff]-?8 ]] && [ "${TERM:-}" != linux ]; then
    DOTS_MOST='●●●' DOTS_SOME='●●○' DOTS_LEAST='●○○'
else
    DOTS_MOST='***' DOTS_SOME='**-' DOTS_LEAST='*--'
fi
dots() { case $1 in most) echo "$DOTS_MOST" ;; least) echo "$DOTS_LEAST" ;; *) echo "$DOTS_SOME" ;; esac; }
LEGEND="$DOTS_MOST walled off, or it protects you    $DOTS_SOME runs as you    $DOTS_LEAST lowers protection"

# One line per extra: NAME<TAB>SUMMARY<TAB>installed(1|0)<TAB>TIER<TAB>CATEGORY, by group,
# the most secure first in each, then by name
mapfile -t EXTRAS < <(
    for f in "$WREN_DIR"/extras/*.sh; do
        name=$(basename -- "$f" .sh)
        summary=$(sed -n 's/^# summary: //p' "$f" | head -1)
        tier=$(sed -n 's/^# security: //p' "$f" | head -1)
        case $tier in most | some | least) ;; *) tier=some ;; esac
        cat=$(sed -n 's/^# category: //p' "$f" | head -1)
        installed=0
        if [ -n "$(script_found "$f")" ]; then installed=1; fi
        # A retired extra (update.sh offers its --remove): listed only while you
        # still have it, so it can be taken out here too, never to install
        if grep -q '^# retired: ' "$f"; then
            [ "$installed" = 1 ] || continue
            summary="(retired) ${summary:-$name}"
        fi
        printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$(category_rank "$cat")" "$(tier_rank "$tier")" "$name" "${summary:-$name}" "$installed" "$tier" "$cat"
    done | sort -t$'\t' -k1,1n -k2,2n -k3,3 | cut -f3-
)
# The name column: as wide as the longest name, so the circles and summaries line up
NAMEW=16
for line in "${EXTRAS[@]}"; do
    name=${line%%$'\t'*}
    [ ${#name} -le "$NAMEW" ] || NAMEW=${#name}
done

if [ "${1:-}" = --list ]; then
    last=
    echo "$LEGEND"
    for line in "${EXTRAS[@]}"; do
        IFS=$'\t' read -r name summary installed tier cat <<<"$line"
        if [ "$cat" != "$last" ]; then
            printf '\n%s\n' "$(category_heading "$cat")"
            last=$cat
        fi
        printf "  %-${NAMEW}s  %s  %s%s\n" "$name" "$(dots "$tier")" "$summary" "$([ "$installed" = 1 ] && echo '  [installed]')"
    done
    exit 0
fi

mode=both file=
PRESELECT=$HOME/.config/wren/preselect
# What the list says, and its buttons (--remove changes them)
TITLE="Optional extras" OK=Install CANCEL="No extras"
TEXT="$LEGEND\nArrow keys or the wheel move; Space or a click checks a box; / or Tab searches.\n* = already installed (checking it runs it again, to repair or update).\nInstall puts the checked ones in; No extras finishes without any."
case "${1:-}" in
    --remove)
        mode=remove TITLE="Remove apps" OK=Remove CANCEL=Cancel
        TEXT="These are installed. Check the ones to remove (each keeps your own data, and says so).\n$LEGEND\nArrow keys or the wheel move; Space or a click checks a box."
        # Only what is installed
        mapfile -t EXTRAS < <(printf '%s\n' "${EXTRAS[@]}" | awk -F'\t' '$3 == 1')
        if [ ${#EXTRAS[@]} -eq 0 ]; then note "No extras are installed, so there is nothing to remove."; exit 0; fi ;;
    --choose) mode=choose file=${2:?"usage: install-extras.sh --choose FILE"} ;;
    --install) mode=install file=${2:?"usage: install-extras.sh --install FILE"} ;;
esac

chosen=()
if [ "$mode" != install ] && ! { : </dev/tty; } 2>/dev/null; then
    note "There is no terminal to ask on. Pick extras later with: bash install-extras.sh"
    [ "$mode" != choose ] || : >"$file"
    exit 0
fi


# checklist_items: the list's lines, three words each (TAG ITEM OFF), with a
# heading line before each group (~CATEGORY: checking one does nothing)
checklist_items() {
    local line name summary installed tier cat last=''
    for line in "${EXTRAS[@]}"; do
        IFS=$'\t' read -r name summary installed tier cat <<<"$line"
        if [ "$cat" != "$last" ]; then
            ITEMS+=("~$cat" "== $(category_heading "$cat") ==" OFF)
            last=$cat
        fi
        if [ "$installed" = 1 ] && [ "$mode" != remove ]; then summary="$(dots "$tier") * $summary"; else summary="$(dots "$tier")   $summary"; fi
        if [ "$mode" != remove ] && grep -qxF -- "$name" "$PRESELECT" 2>/dev/null; then
            ITEMS+=("$name" "$summary" ON)
        else
            ITEMS+=("$name" "$summary" OFF)
        fi
    done
}

# pick_with_list: the check boxes (lib/checklist.sh), into chosen
pick_with_list() {
    ITEMS=()
    checklist_items
    CL_TITLE=$TITLE CL_TEXT=$TEXT CL_OK=$OK CL_CANCEL=$CANCEL CL_TAGW=$((NAMEW + 1))
    checklist || chosen=()
}

# Where the list can't be drawn (no terminal type): numbers typed at a prompt
pick_with_numbers() {
    local i=0 line name summary installed tier cat last='' reply n
    echo "$TITLE:"
    echo "  $LEGEND"
    for line in "${EXTRAS[@]}"; do
        IFS=$'\t' read -r name summary installed tier cat <<<"$line"
        if [ "$cat" != "$last" ]; then
            printf '  %s\n' "$(category_heading "$cat")"
            last=$cat
        fi
        i=$((i + 1))
        printf "  %2d) %-${NAMEW}s  %s  %s%s\n" "$i" "$name" "$(dots "$tier")" "$summary" "$([ "$installed" = 1 ] && echo ' [installed]')"
    done
    tty_read reply "Numbers to $(tr '[:upper:]' '[:lower:]' <<<"$OK"), separated by spaces (Enter for none): "
    for n in $reply; do
        if [[ $n =~ ^[0-9]+$ ]] && [ "$n" -ge 1 ] && [ "$n" -le ${#EXTRAS[@]} ]; then
            chosen+=("${EXTRAS[n - 1]%%$'\t'*}")
        fi
    done
}

if [ "$mode" = install ]; then
    # the names were picked earlier (--choose): one per line
    mapfile -t chosen < <(grep . "$file" 2>/dev/null || true)
else
    say "$TITLE"
    if [ -n "${TERM:-}" ] && [ "$TERM" != dumb ]; then
        pick_with_list
    else
        pick_with_numbers
    fi
    # ticked once, by the stick: from now on the list starts empty
    [ "$mode" = remove ] || rm -f "$PRESELECT"
fi

if [ "$mode" = choose ]; then
    : >"$file"
    if [ ${#chosen[@]} -gt 0 ]; then printf '%s\n' "${chosen[@]}" >"$file"; fi
    if [ ${#chosen[@]} -eq 0 ]; then
        note "No extras chosen. Add some later with: wrenctl --apps"
    else
        note "To install after the base: ${chosen[*]}"
    fi
    exit 0
fi

if [ ${#chosen[@]} -eq 0 ]; then
    note "No extras chosen. Add some later with: wrenctl --apps"
    exit 0
fi

if [ "$mode" = remove ]; then
    # (a name not in the list is no extra that is installed: left alone)
    keep=()
    for name in "${chosen[@]}"; do
        printf '%s\n' "${EXTRAS[@]}" | cut -f1 | grep -qxF -- "$name" && keep+=("$name")
    done
    chosen=("${keep[@]}")
    [ ${#chosen[@]} -gt 0 ] || { note "Nothing removed."; exit 0; }
    if ! ask "Remove ${chosen[*]}? Each keeps your own data (it says where)." n; then
        note "Nothing removed."
        exit 0
    fi
    failed=()
    for name in "${chosen[@]}"; do
        say "Removing: $name"
        bash "$WREN_DIR/extras/$name.sh" --remove || failed+=("$name")
    done
    say "Removing done"
    if [ ${#failed[@]} -gt 0 ]; then
        warn "These didn't finish: ${failed[*]}. Try one again with: bash extras/NAME.sh --remove"
        pause_if_asked
        exit 1
    fi
    pause_if_asked
    exit 0
fi

failed=()
for name in "${chosen[@]}"; do
    [ -f "$WREN_DIR/extras/$name.sh" ] || continue
    say "Extra: $name"
    bash "$WREN_DIR/extras/$name.sh" || failed+=("$name")
done

say "Extras done"
if [ ${#failed[@]} -gt 0 ]; then
    warn "These failed: ${failed[*]}. Run one again with: bash extras/NAME.sh"
    pause_if_asked
    exit 1
fi
pause_if_asked