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