wren-linux / lib / core.sh
# shellcheck shell=bash
# lib/core.sh: messages, prompts, and small checks. Sourced by lib.sh.
BIN="$HOME/.local/bin"
# Where saved downloads are kept (offline/: the Wi-Fi packages, Tor Browser,
# Whonix, Tails, the install image): in the Wren Linux folder when it has them
# already (a stick, your own copy) or you can write there; from the package's
# read-only copy in /usr/share/wren, in your own ~/.local/share/wren/offline
if [ -d "$WREN_DIR/offline" ] || [ -w "$WREN_DIR" ]; then
OFFLINE_DIR=$WREN_DIR/offline
else
OFFLINE_DIR=${XDG_DATA_HOME:-$HOME/.local/share}/wren/offline
fi
# (WREN_SUDO: a test points this at a pretend sudo)
if [ "$(id -u)" -eq 0 ]; then SUDO=; else SUDO=${WREN_SUDO:-sudo}; fi
say() { printf '\033[1;32m==>\033[0m %s\n' "$*"; }
note() { printf ' %s\n' "$*"; }
warn() { printf '\033[1;33m!!\033[0m %s\n' "$*" >&2; }
die() { printf '\033[1;31mxx\033[0m %s\n' "$*" >&2; exit 1; }
# tty_read VAR "Prompt": reads a line from the terminal into VAR, or
# leaves it empty when there is no terminal. The prompt goes to stderr,
# so only the check for a terminal is silenced, never the prompt.
tty_read() {
local _line=
# An unattended run (install-base.sh installing its default apps) never
# prompts: it leaves the answer empty, so ask() falls back to its default.
if [ -n "${WREN_NONINTERACTIVE:-}" ]; then
printf -v "$1" '%s' ''
return
fi
if [ -n "${WREN_TEST_ANSWERS:-}" ]; then
# For tools/test-bridges.sh: answers come from a file, one per line
[ -e /proc/self/fd/8 ] || exec 8<"$WREN_TEST_ANSWERS"
printf '%s\n' "$2" >&2
IFS= read -r _line <&8 || _line=
elif { : </dev/tty; } 2>/dev/null; then
IFS= read -r -p "$2" _line </dev/tty || _line=
fi
# A pasted answer comes wrapped in the terminal's paste markers (ESC[200~ before, ESC[201~
# after: "bracketed paste", which bash's own prompt asks for and plain read doesn't take
# out). They'd be part of the answer: a bridge line or an address starting ^[[200~.
_line=${_line//$'\e[200~'/}
_line=${_line//$'\e[201~'/}
printf -v "$1" '%s' "$_line"
}
# pause_if_asked: when a script was started from the Wren menu
# (WREN_PAUSE=1), its window closes the moment the script ends, so wait for
# Esc (or Enter) first. Called at the end of what the user may want to read, not when
# nothing was done. Does nothing in a terminal of your own.
pause_if_asked() {
[ -n "${WREN_PAUSE:-}" ] && [ -z "${WREN_NONINTERACTIVE:-}" ] || return 0
# (the Wren menu then knows the window already waited)
[ -z "${WREN_PAUSED_FILE:-}" ] || : >"$WREN_PAUSED_FILE" 2>/dev/null || true
{ : </dev/tty; } 2>/dev/null || return 0
local k more
printf '\nPress Esc to close. ' >&2
while IFS= read -rsn1 k </dev/tty; do
case $k in
'' | q | Q) break ;;
$'\e')
# a plain Esc closes; an arrow key (Esc plus more) does not
if IFS= read -rsn1 -t "${WREN_ESC_WAIT:-0.1}" more </dev/tty; then
while IFS= read -rsn1 -t "${WREN_ESC_WAIT:-0.1}" more </dev/tty; do :; done
else
break
fi ;;
esac
done
printf '\n' >&2
}
# ask "Question?" [y|n]: default answer is the second argument
ask() {
local reply def=${2:-n} hint='[y/N]'
[ "$def" = y ] && hint='[Y/n]'
# (indented as note's lines are, so a question sits with what it is about)
tty_read reply " $1 $hint "
[[ ${reply:-$def} =~ ^[Yy] ]]
}
# choose VAR "Question" DEFAULT OPTION...: one of the OPTIONs into VAR, typed
# in full or by its first letter; Enter (or no terminal to ask on) gives
# DEFAULT. Anything else asks again. The OPTIONs start with different letters.
choose() {
local _var=$1 _q=$2 _def=$3 _reply _o _hint=""
shift 3
for _o; do _hint+="${_hint:+ / }$_o"; done
while :; do
tty_read _reply " $_q $_hint (Enter: $_def) "
_reply=${_reply,,}
if [ -z "$_reply" ]; then
printf -v "$_var" '%s' "$_def"
return
fi
for _o; do
if [ "$_reply" = "$_o" ] || [ "$_reply" = "${_o:0:1}" ]; then
printf -v "$_var" '%s' "$_o"
return
fi
done
note "Type one of: $_hint"
done
}
# auto_or_ask VARNAME "Question?" [y|n]: for prompts install-base.sh needs to
# answer for you when it installs its default apps unattended. If VARNAME is
# set to yes/no (1/y/yes or 0/n/no) that is the answer, with no prompt;
# otherwise it just asks (and an unattended run with VARNAME unset takes the
# default, because tty_read returns empty). A person running the script by
# hand is always asked.
auto_or_ask() {
case ${!1:-} in
1 | y | Y | yes | YES) return 0 ;;
0 | n | N | no | NO) return 1 ;;
esac
shift
ask "$@"
}
# What this computer has. A part that only makes sense with some hardware asks
# first (skip_unless), so a desktop does not get laptop software. WREN_SYS
# is where /sys is, for tests.
SYS=${WREN_SYS:-/sys}
has_backlight() { compgen -G "$SYS/class/backlight/*" >/dev/null; }
has_battery() { compgen -G "$SYS/class/power_supply/BAT*" >/dev/null; }
has_power_profiles() { has_battery || [ -e "$SYS/firmware/acpi/platform_profile" ]; }
has_wifi() { compgen -G "$SYS/class/net/*/wireless" >/dev/null || compgen -G "$SYS/class/ieee80211/*" >/dev/null; }
has_bluetooth() { compgen -G "$SYS/class/bluetooth/*" >/dev/null; }
# skip_unless CHECK "what is missing": when install-base.sh runs a part
# (WREN_FROM_BASE=1) and CHECK finds no such hardware, say so and stop with
# success. Run by hand, a part always installs: you have said what you want.
skip_unless() {
[ -n "${WREN_FROM_BASE:-}" ] || return 0
if "$1"; then return 0; fi
note "Skipped: $2 Install it later with: bash $(basename -- "$(dirname -- "$0")")/$(basename -- "$0")"
exit 0
}
# For scripts that only touch your home folder
not_root() {
[ "$(id -u)" -ne 0 ] || die "Run this as your normal user, not root."
}
# old_install: this computer was set up before Wren Linux had its name
# (debsway): its settings, firewall rules and sudo rules are under the old
# names, and the scripts of today would set up second copies next to them
old_install() { [ -d "$HOME/.config/debsway" ] || [ -d "$HOME/.local/share/debsway" ]; }
# Desktop scripts run as the normal user and use sudo for system changes
need_user() {
[ "$(id -u)" -ne 0 ] ||
die "Run this as your normal user, not root. It uses sudo when it needs to."
if old_install && [ -z "${WREN_ALLOW_OLD_INSTALL:-}" ]; then
warn "This computer was set up as debsway, before it was called Wren Linux: its settings,"
warn "firewall rules and sudo rules are under the old names, and this would set up second"
warn "copies next to them. Reinstall instead: back up your home, make an install stick with"
die "bash $WREN_DIR/prep/make-install-iso.sh, and install from it. (WREN_ALLOW_OLD_INSTALL=1 runs it anyway.)"
fi
command -v sudo >/dev/null ||
die "sudo isn't installed. Log in as root and run: bash $WREN_DIR/prep/setup-sudo.sh $(id -un)"
# Two different failures: a user who is not allowed to use sudo at all, and one
# who is but typed the password wrong. A typo shouldn't end a first install:
# it asks whether to try again (yes unless you say no), where there is someone
# to ask, a few rounds at most; nothing has been changed yet either way
local round=1
until $SUDO -v; do
id -nG | tr ' ' '\n' | grep -qxE 'sudo|wheel|admin' ||
die "Your user can't use sudo. Log in as root and run: bash $WREN_DIR/prep/setup-sudo.sh $(id -un)"
warn "sudo didn't accept the password (three tries). Your own password, the one you log in with."
if [ -n "${WREN_NONINTERACTIVE:-}" ] || [ "$round" -ge 5 ] ||
{ [ -z "${WREN_TEST_ANSWERS:-}" ] && ! { : </dev/tty; } 2>/dev/null; } ||
! ask "Try again?" y; then
die "Stopped before changing anything. Run this again and type your own password."
fi
round=$((round + 1))
done
}
# sudo_keepalive: asks for the sudo password once, now, and keeps the permission
# fresh in the background until the script ends, so a long run is not stopped
# part way by a second password question. Returns 1 if the password was not
# accepted (after sudo's own three tries); the caller should then stop.
sudo_keepalive() {
[ -z "$SUDO" ] || $SUDO -v || return 1
[ -n "$SUDO" ] || return 0
# (its own messages go nowhere: when the script ends, the exit trap below stops
# its sleep, and the shell would say "Terminated" about that, after all is done)
(
# ends by itself when the script that started it is gone
while kill -0 "$$" 2>/dev/null; do
$SUDO -n true 2>/dev/null || exit 0
sleep 50
done
) 2>/dev/null &
SUDO_KEEPALIVE_PID=$!
trap '[ -z "${SUDO_KEEPALIVE_PID:-}" ] || { pkill -P "$SUDO_KEEPALIVE_PID" 2>/dev/null; kill "$SUDO_KEEPALIVE_PID" 2>/dev/null; }' EXIT
}
# print_manual [PAGE]: what --manual prints. The "## MANUAL INSTALL" section of
# docs/extras/PAGE.md (by default the running extra's name), without its code fences, with
# each @VAR@ filled in from a variable of the same name (pins.sh, or the
# script's own; an array gives its items). Changes nothing and needs no sudo.
print_manual() {
local doc line out name m
doc="$WREN_DIR/docs/extras/${1:-$(basename -- "$0" .sh)}.md"
grep -qx '## MANUAL INSTALL' "$doc" 2>/dev/null ||
die "There are no manual steps in $doc."
while IFS= read -r line; do
out=
while [[ $line =~ @([A-Z][A-Z0-9_]*)@ ]]; do
m=${BASH_REMATCH[0]} name=${BASH_REMATCH[1]}
out+=${line%%"$m"*}
if declare -p "$name" >/dev/null 2>&1; then
declare -n ref=$name
out+="${ref[*]}"
unset -n ref
else
out+=$m
fi
line=${line#*"$m"}
done
printf '%s\n' "$out$line"
done < <(awk '$0 == "## MANUAL INSTALL" { on = 1; next } on && /^## / { exit } on && !/^```/' "$doc")
}
# script_help SCRIPT: what SCRIPT's comment at the top says (each script's own description,
# its options and what each does), without the lines Wren Linux reads for itself
script_help() {
sed -n '2,/^[^#]/{ /^#/!d; /^# *\(summary\|security\|status\|files\|retired\|shellcheck\|part\):/d; /^# *shellcheck /d; s/^# \{0,1\}//; p; }' "$1"
# what every extra's --remove and --manual do, for one that takes them
local rm='(= --remove \]|^[[:space:]]*--remove\))' man='(= --manual \]|^[[:space:]]*--manual\))'
if grep -qE "$rm|$man" "$1"; then
echo
! grep -qE "$rm" "$1" || echo " --remove undo what it set up (wrenctl remove NAME does the same)"
! grep -qE "$man" "$1" || echo " --manual print the steps to do it by hand, without this script (wrenctl manual NAME)"
fi
return 0
}
is_installed() {
dpkg-query -W -f='${Status}' "$1" 2>/dev/null | grep -q 'install ok installed'
}
# Your own sound server, PipeWire, starts at login only with the sound extra
# (extras/pipewire-audio.sh). Debian starts it for every user once its packages are in,
# and the sandboxes (incognito, Firefox) install them for a sound server of their own,
# with no sound card. Without the extra: nothing plays (their sound comes through
# yours), nothing asks about sound, and no program gets a microphone through it.
USER_AUDIO_UNITS="pipewire.socket pipewire.service pipewire-pulse.socket pipewire-pulse.service wireplumber.service filter-chain.service"
USER_AUDIO_DIR=${WREN_USER_UNIT_DIR:-/usr/lib/systemd/user}
user_audio_wanted() { is_installed pipewire-audio; }
# user_audio_sync: PipeWire on for everyone with the extra, off without it (the units this
# Debian has: systemctl stops at the first it doesn't know)
user_audio_sync() {
local u units=()
for u in $USER_AUDIO_UNITS; do [ -e "$USER_AUDIO_DIR/$u" ] && units+=("$u"); done
[ ${#units[@]} -gt 0 ] || return 0
if user_audio_wanted; then
$SUDO systemctl --global enable pipewire.socket pipewire-pulse.socket wireplumber.service >/dev/null 2>&1 || true
else
$SUDO systemctl --global disable "${units[@]}" >/dev/null 2>&1 || true
systemctl --user disable --now "${units[@]}" >/dev/null 2>&1 || true
fi
}
have_internet() { getent hosts deb.debian.org >/dev/null 2>&1; }
# For a script that writes a startup file itself, outside wrenctl's runs (which do this
# for every file their run changed): watch_before, before writing it, then
# watch_take FILE..., and the startup watch (parts/startup-watch.sh) takes those files
# as Wren Linux's, so they don't show as "startup changed". A file that was listed
# before is left out: that change is for you to look at (wren-watch).
WATCH_CMD=${WREN_WATCH_CMD:-/usr/local/bin/wren-watch}
watch_before() { WATCH_PENDING=$(cat "${WREN_WATCH_RUN:-/run/wren-watch}/$(id -un)" 2>/dev/null || true); }
watch_take() {
local f take=()
[ -x "$WATCH_CMD" ] || return 0
for f in "$@"; do
f=${f#"$HOME"/}
if grep -qxF -- "$f" <<<"${WATCH_PENDING:-}"; then
# shellcheck disable=SC2088 # a literal ~, as wren-watch lists the files
warn "~/$f had changed before this: not taken as Wren Linux's (look: wren-watch)"
else
take+=("$f")
fi
done
[ ${#take[@]} -gt 0 ] || return 0
$SUDO "$WATCH_CMD" accept "${take[@]}" >/dev/null ||
warn "Not taken as Wren Linux's; they show as \"startup changed\" until: wren-watch accept"
}
# install_bin NAME: puts bin/NAME into ~/.local/bin. A different file there
# that Wren Linux didn't write is kept as NAME.bak.
install_bin() {
local src="$WREN_DIR/bin/$1" dest="$BIN/$1"
if [ -f "$dest" ] && ! cmp -s "$src" "$dest" && ! grep -qis 'written by Wren Linux' "$dest"; then
cp "$dest" "$dest.bak"
note "Old $dest kept as $dest.bak"
fi
install -D -m 0755 "$src" "$dest"
note "Installed $dest"
}
# printable: standard input without control characters (an escape sequence that
# steers the terminal, a carriage return that writes over a line, a backspace...),
# tabs and line breaks kept, and without the 8-bit ones in their UTF-8 form
# (U+0080-U+009F: some terminals obey those too). For text from outside, before
# it reaches the terminal: a commit message from the server, a Wi-Fi network's name.
printable() { LC_ALL=C sed 's/\xc2[\x80-\x9f]//g' | LC_ALL=C tr -d '\000-\010\013-\037\177'; }
# The wrapper that keeps a sandbox user off X11's abstract socket (see
# bin/no-abstract-sockets). One copy, root's, shared by the sandboxes; the last
# one to be removed takes it away.
NO_ABSTRACT=/usr/local/libexec/wren-no-abstract-sockets
install_no_abstract() {
sudo install -d -o root -g root -m 0755 /usr/local/libexec
sudo install -o root -g root -m 0755 "$WREN_DIR/bin/no-abstract-sockets" "$NO_ABSTRACT"
}
remove_no_abstract() {
local s
for s in wallet-session incognito-session claude-session claude-vm-session opencode-vm-session firefox-session onion-site-session yt-dlp-session distro-vm-session browser-session; do
[ ! -e "/usr/local/libexec/$s" ] || return 0
done
# (a media viewer given a web address; this one is yours, in ~/.local/bin)
[ ! -e "$BIN/media-sandbox" ] || return 0
sudo rm -f "$NO_ABSTRACT"
}
# The start-up steps every sandbox's command shares (see bin/wren-sandbox): one copy, root's,
# sourced by firefox, incognito, tor-browser and mullvad-browser. The last of them to be removed
# takes it away.
SANDBOX_LIB=/usr/local/libexec/wren-sandbox
install_sandbox_lib() {
sudo install -d -o root -g root -m 0755 /usr/local/libexec
sudo install -o root -g root -m 0755 "$WREN_DIR/bin/wren-sandbox" "$SANDBOX_LIB"
}
remove_sandbox_lib() {
local c
for c in /usr/local/bin/firefox /usr/local/bin/incognito /usr/local/bin/browser-sandbox; do
[ ! -e "$c" ] || return 0
done
sudo rm -f "$SANDBOX_LIB"
}
# The check that a sandbox's firewall really holds (see bin/sandbox-net-check).
# One copy, root's, shared by the sandboxes; the last one to be removed takes it away.
NET_CHECK=/usr/local/libexec/wren-sandbox-net-check
install_net_check() {
sudo install -d -o root -g root -m 0755 /usr/local/libexec
sudo install -o root -g root -m 0755 "$WREN_DIR/bin/sandbox-net-check" "$NET_CHECK"
}
remove_net_check() {
local s
for s in incognito-session claude-session claude-vm-session opencode-vm-session firefox-session onion-site-session yt-dlp-session browser-session; do
[ ! -e "/usr/local/libexec/$s" ] || return 0
done
sudo rm -f "$NET_CHECK"
}
# remove_wayland_restrict: takes wayland-restrict away once nothing uses it (call
# it after the script's own command is gone)
remove_wayland_restrict() {
local c
for c in /usr/local/bin/wallet /usr/local/bin/firefox /usr/local/bin/incognito /usr/local/bin/browser-sandbox "$BIN/media-sandbox"; do
[ ! -e "$c" ] || return 0
done
remove_bin wayland-restrict
}
# remove_bin NAME: removes ~/.local/bin/NAME if Wren Linux wrote it
remove_bin() {
if grep -qis 'written by Wren Linux' "$BIN/$1"; then rm -f "$BIN/$1"; fi
}
# script_found SCRIPT: what is set up of the part or extra SCRIPT, one per
# line, from its "# status:" line: a list of apt:PACKAGE, opt:APP (unpacked
# into ~/.local/opt), bin:COMMAND (in ~/.local/bin), file:PATH and gone:PACKAGE
# (for a script that removes it: done once it isn't installed). Nothing
# is printed if it isn't set up; one thing is enough.
script_found() {
local tok what
for tok in $(sed -n 's/^# status: //p' "$1" | head -1); do
what=${tok#*:}
case $tok in
apt:*) if is_installed "$what"; then echo "$what"; fi ;;
gone:*) if ! is_installed "$what"; then echo "no $what"; fi ;;
opt:*) if [ -f "$HOME/.local/opt/$what/.wren-version" ]; then
echo "$what $(cat "$HOME/.local/opt/$what/.wren-version")"; fi ;;
bin:*) if [ -e "$BIN/$what" ] || [ -L "$BIN/$what" ]; then echo "$what"; fi ;;
file:*) what=${what/#\~/$HOME}
if [ -e "$what" ]; then echo "${what##*/}"; fi ;;
esac
done
}
# profile_autostart sway|console: what a login on a text console starts (sway on
# tty1, or tmux on any tty), kept in ~/.config/wren/login and acted on by one
# block in ~/.profile. The block loops: when the program ends it checks the file
# again, so switching the mode (extras/console-only.sh) can end sway or tmux and
# start the other without logging out. If the file is unchanged, ending the
# program logs out, as before. WREN_LOGIN_LOOP=1 marks a session started by
# the block (an older block, which exec'd sway, doesn't set it).
profile_autostart() {
local profile="$HOME/.profile" mode="$HOME/.config/wren/login"
touch "$profile"
sed -i -e '/^# >>> Wren Linux autostart >>>$/,/^# <<< Wren Linux autostart <<<$/d' \
-e '/^# >>> Wren Linux console >>>$/,/^# <<< Wren Linux console <<<$/d' "$profile"
mkdir -p "${mode%/*}"
echo "$1" >"$mode"
cat >>"$profile" <<'BLOCK'
# >>> Wren Linux autostart >>>
# What a text console login starts: ~/.config/wren/login says sway (tty1) or
# console (tmux, any tty). When it ends and that file has changed, the other starts;
# otherwise tmux's end logs out, and sway's leaves you at the shell, after your password:
# a sway that ended, by itself or not, may have been locked.
case "$(tty 2>/dev/null)" in
/dev/tty[0-9]*)
if [ -z "${WAYLAND_DISPLAY:-}" ] && [ -z "${TMUX:-}" ]; then
export WREN_LOGIN_LOOP=1
while :; do
m=$(cat "$HOME/.config/wren/login" 2>/dev/null); m=${m:-sway}
if [ "$m" = console ]; then
# bigger text on the console; ~/.config/wren/console-font names another
setfont "$(cat "$HOME/.config/wren/console-font" 2>/dev/null || echo Uni3-Terminus28x14)" 2>/dev/null
rm -f "$HOME/.config/wren/console-redraw"
tmux new-session -A -s main
# console-font leaves this to say: detached on purpose, come back with the new font
[ ! -e "$HOME/.config/wren/console-redraw" ] || continue
elif [ "$(tty)" = /dev/tty1 ]; then
# A sway that didn't end by itself (a crash, out of memory, killed) may
# have been locked, so nothing starts again without your password, even
# with autologin. In this boot only: restarting needs what it always did.
r=$HOME/.local/state/wren/sway-running
b=$(cat /proc/sys/kernel/random/boot_id 2>/dev/null)
if [ -n "$b" ] && [ "$(cat "$r" 2>/dev/null)" = "$b" ]; then
trap '' INT QUIT TSTP
printf '\nsway stopped unexpectedly, perhaps while locked, and its windows are gone.\nYour password starts it again.\n'
until su -c true "$(id -un)"; do sleep 1; done
trap - INT QUIT TSTP
rm -f "$r"
# it can't start at all: you need the shell, to see why
if [ -n "${q:-}" ]; then
printf '\nsway keeps failing. Start it again: sway Log out: exit\n'
break
fi
fi
mkdir -p "${r%/*}"
printf '%s\n' "$b" >"$r"
s=$(date +%s)
rc=0
env XDG_CURRENT_DESKTOP=sway sway || rc=$?
if [ "$rc" != 0 ]; then
q=
[ "$(($(date +%s) - s))" -ge 10 ] || q=1
continue
fi
rm -f "$r"
else
break
fi
n=$(cat "$HOME/.config/wren/login" 2>/dev/null)
if [ "${n:-sway}" = "$m" ]; then
# Leaving tmux (its last window) logs out. Leaving sway leaves
# you here, at the shell: with autologin, logging out would
# only log you in again and start sway
[ "$m" = sway ] || exit
# Your password first: it may have ended while locked (a program or a key
# that makes it exit), and this shell is already you
trap '' INT QUIT TSTP
printf '\nsway has ended. Your password opens the shell.\n'
until su -c true "$(id -un)"; do sleep 1; done
trap - INT QUIT TSTP
printf 'Start it again: sway Log out: exit\n'
break
fi
done
fi ;;
esac
# <<< Wren Linux autostart <<<
BLOCK
}
# login_mode: sway or console, as profile_autostart last set it
login_mode() {
local m
m=$(cat "$HOME/.config/wren/login" 2>/dev/null)
echo "${m:-sway}"
}