wren-linux / bin / wren-menu
#!/bin/bash
# wren-menu: written by Wren Linux parts/wren-menu.sh
# The menu behind the "wren" item in the bar. A click opens a black-and-gray
# terminal in a floating, centered window. Its first screen says what you can do:
# Browse the web, Browse privately (Tor Browser, Whonix), Chat (Signal, SimpleX),
# My files, Write a note, Passwords, Terminal, More apps (Get more apps, the
# others you have, Remove apps), Wi-Fi (Choose a network, on or off, Sign-in
# page), Privacy (Bluetooth, Microphone, Webcam, Vault, Secure DNS), Settings
# (Theme, Font size, Startup programs), Is my computer OK? (Update, Status, Back
# up now), Help & keyboard shortcuts, About this computer, and Lock, sleep, shut
# down (Lock, Sleep, Log out, Restart, Shut down).
# Each shows only what is here. Arrow keys (or j/k) and Enter choose; so
# does a click. Esc goes back from a list, and closes the first screen, the
# install list and the pager. What you choose runs in that same window, and the
# window closes when it ends (the install and update screens wait for a key
# first, if they did something you may want to read). Install, Remove, Update and
# Status need the Wren Linux folder (~/wren, or WREN_DIR).
# wren-menu open the window
# wren-menu --tui the menu itself, in the terminal you are in
# wren-menu --pager FILE show FILE: arrows, Space and b scroll, Esc closes
# Where Wren Linux is: WREN_DIR if you set it; else the root-owned copy the
# Wren Linux package installs, which no program running as you can change; else
# your own copy, ~/wren
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
KEYS_FILE=$HOME/.local/share/wren/keys.txt
[ -f "$KEYS_FILE" ] || KEYS_FILE=$DIR/data/keys.txt
# The terminal's colors, all gray: black screen, light gray text (in the light
# theme the other way round: bin/theme). The eight normal and eight bright
# colors programs ask for (red warnings, green headings...) come out as
# shades, so nothing is colored.
gray_options() {
local i=0 c MENU_FG=d0d0d0 MENU_BG=000000
local MENU_REGULAR="000000 9a9a9a b8b8b8 c8c8c8 8a8a8a a8a8a8 bcbcbc d0d0d0"
local MENU_BRIGHT="606060 aaaaaa d8d8d8 e0e0e0 9a9a9a b8b8b8 d0d0d0 ffffff"
# (theme env prints only checked colors: NAME='rrggbb ...')
eval "$("$HOME/.local/bin/theme" env 2>/dev/null | grep -E "^MENU_(FG|BG|REGULAR|BRIGHT)='[0-9a-f ]+'\$")"
printf '%s\n' -o "colors.foreground=$MENU_FG" -o "colors.background=$MENU_BG" \
-o "colors.selection-foreground=$MENU_BG" -o "colors.selection-background=$MENU_FG" -o colors.alpha=0.85
for c in $MENU_REGULAR; do
printf '%s\n' -o "colors.regular$i=$c"
i=$((i + 1))
done
i=0
for c in $MENU_BRIGHT; do
printf '%s\n' -o "colors.bright$i=$c"
i=$((i + 1))
done
}
if [ -z "$1" ]; then
mapfile -t gray < <(gray_options)
# --log-level=error: foot warns about a protocol sway lacks (the window icon), harmlessly
# scrollback.multiplier=1: one step of the mouse wheel is one arrow key in
# full-screen lists (the install checklist), not three
exec foot --log-level=error --app-id=wren-run -o scrollback.multiplier=1.0 "${gray[@]}" "$(readlink -f -- "$0")" --tui
fi
# ---- inside the terminal ----
export LC_ALL=C.UTF-8 # so that cutting a line to the window's width counts characters
saved=$(stty -g 2>/dev/null)
# How long to wait for the rest of an arrow key or mouse report after an Esc
# byte. Nothing follows a plain Esc, so this is also how long Esc takes to close.
ESC_WAIT=${WREN_ESC_WAIT:-0.1}
size=
REDRAW=
KEY=
CX=0 CY=0
screen_on() { printf '\e[?1049h\e[?25l\e[?1000h\e[?1006h'; stty -echo 2>/dev/null; }
screen_off() {
printf '\e[?1000l\e[?1006l\e[?25h\e[?1049l'
[ -z "$saved" ] || stty "$saved" 2>/dev/null
}
trap screen_off EXIT
trap 'exit 130' INT TERM HUP
# settle: a new window starts at a small default size and sway stretches it a
# moment later. Drawing at once would show the menu off to one side and then
# jump, so wait (at most about half a second) until the size has stopped
# changing for a fraction of a second.
settle() {
local prev cur same=0 i
prev=$(stty size 2>/dev/null)
for ((i = 0; i < 20; i++)); do
sleep 0.03
cur=$(stty size 2>/dev/null)
if [ "$cur" = "$prev" ]; then
same=$((same + 1))
[ "$same" -lt 4 ] || return 0
else
same=0
prev=$cur
fi
done
}
# on_idle: the window is a different size from when it was drawn (a new window
# starts small and sway stretches it a moment later): draw it again
on_idle() {
[ -z "$REDRAW" ] || [ "$(stty size 2>/dev/null || echo "24 80")" = "$size" ] || "$REDRAW"
}
# waiting: the menu is about to wait for a key. Only when
# tools/test-wren-menu.sh asks (WREN_MENU_TEST_READY=1) it says so, with
# a terminal code that terminals ignore: the test then knows each key has been
# handled, instead of guessing how long that takes on a busy machine.
waiting() { [ -z "${WREN_MENU_TEST_READY:-}" ] || printf '\e]9999;wren-ready\a'; }
# wait_enter: for "Press Enter to go back" and the like
wait_enter() {
waiting
IFS= read -r _ </dev/tty || true
}
# read_key: waits for one key or mouse report and sets KEY to: up down pgup pgdn
# home end esc enter space click wheelup wheeldown other, or the character typed.
# A click also sets CX and CY (column and row, counted from 1).
read_key() {
local c rc k2 k3 k4 report btn
waiting
while :; do
IFS= read -rsn1 -t 0.1 c
rc=$?
[ "$rc" -ne 0 ] || break
[ "$rc" -gt 128 ] || exit 0 # nothing more to read: the window is gone
on_idle
done
case $c in
'') KEY=enter ;;
' ') KEY=space ;;
$'\e')
KEY=esc # nothing follows a plain Esc; an arrow or a mouse report follows at once
IFS= read -rsn1 -t "$ESC_WAIT" k2 || return 0
[ "$k2" = '[' ] || [ "$k2" = O ] || return 0
IFS= read -rsn1 -t "$ESC_WAIT" k3 || return 0
case $k3 in
A) KEY=up ;;
B) KEY=down ;;
H) KEY=home ;;
F) KEY=end ;;
[0-9])
IFS= read -rsn1 -t "$ESC_WAIT" k4 || return 0 # the ~ that ends it
case $k3 in
1 | 7) KEY=home ;;
4 | 8) KEY=end ;;
5) KEY=pgup ;;
6) KEY=pgdn ;;
*) KEY=other ;;
esac ;;
'<')
# mouse: button;column;row then M (press) or m (release)
report=
while IFS= read -rsn1 -t "$ESC_WAIT" c; do
if [ "$c" = M ] || [ "$c" = m ]; then break; fi
report+=$c
done
KEY=other
# three numbers, or nothing: bash arithmetic on CY would
# run whatever else came in (pasted text can look like this)
[[ $report =~ ^[0-9]{1,4}\;[0-9]{1,4}\;[0-9]{1,4}$ ]] || return 0
IFS=';' read -r btn CX CY <<<"$report"
if [ "$c" = M ]; then
case $btn in
0) KEY=click ;;
64) KEY=wheelup ;;
65) KEY=wheeldown ;;
esac
fi ;;
*) KEY=other ;;
esac ;;
*) KEY=$c ;;
esac
}
# ---- the pager ----
lines=()
top=0
page=20
# wl-copy, for About's c (WREN_MENU_WLCOPY: a test's own; empty when there is none)
WLCOPY=${WREN_MENU_WLCOPY-$(command -v wl-copy || true)}
pager_draw() {
local rows cols i end maxtop
size=$(stty size 2>/dev/null || echo "24 80")
read -r rows cols <<<"$size"
page=$((rows - 1))
[ "$page" -ge 1 ] || page=1
maxtop=$((${#lines[@]} - page))
[ "$maxtop" -ge 0 ] || maxtop=0
[ "$top" -le "$maxtop" ] || top=$maxtop
[ "$top" -ge 0 ] || top=0
end=$((top + page))
[ "$end" -le "${#lines[@]}" ] || end=${#lines[@]}
printf '\e[H\e[2J'
for ((i = top; i < end; i++)); do printf '%s\e[0m\n' "${lines[i]:0:cols}"; done
printf '\e[%d;1H\e[2mEsc %s. Arrows, Space and b scroll. (%d-%d of %d)\e[0m' \
"$rows" "$([ -n "$PAGER_BACK" ] && echo 'goes back' || echo closes)" $((top + 1)) "$end" "${#lines[@]}"
}
# pager [back]: shows `lines`; Esc (or q) ends the program, or with back (from the menu: the
# keys, Status) goes back to it
PAGER_BACK=
pager() {
top=0 PAGER_BACK=${1:-}
REDRAW=pager_draw
while :; do
pager_draw
read_key
case $KEY in
esc | q)
[ -n "$PAGER_BACK" ] || exit 0
REDRAW=draw PAGER_BACK=
return 0 ;;
down | j | enter) top=$((top + 1)) ;;
up | k) top=$((top - 1)) ;;
space | pgdn | f) top=$((top + page)) ;;
pgup | b) top=$((top - page)) ;;
home | g) top=0 ;;
end | G) top=$((${#lines[@]} + 1000000)) ;; # pager_draw brings it back to the last page
wheeldown) top=$((top + 3)) ;;
wheelup) top=$((top - 3)) ;;
esac
done
}
if [ "$1" = --pager ]; then
[ -r "${2:-}" ] || { echo "wren-menu: can't read ${2:-a file}" >&2; exit 1; }
mapfile -t lines <"$2"
screen_on
settle
pager
fi
[ "$1" = --tui ] || { echo "usage: wren-menu [--tui | --pager FILE]" >&2; exit 2; }
# ---- the menu ----
# The first screen: what you open most, then the lists, in words a newcomer
# would look for. ids say what an entry does; items are what it shows. Esc, or Back, goes from a
# list to the first screen.
# Wi-Fi, Bluetooth and the microphone are listed only when this computer has them
# (bin/radios), with their state; choosing one flips it and the menu stays open.
RADIOS=$HOME/.local/bin/radios
# web_browser: the browser links open in (mimeapps.list), or the first one
# installed; text_editor: Mousepad, if installed. Each prints its command, or nothing.
# (WREN_MENU_BROWSERS and WREN_MENU_EDITORS: a test's own lists)
BROWSERS=${WREN_MENU_BROWSERS-/usr/local/bin/firefox /usr/bin/firefox-esr /usr/local/bin/mullvad-browser /usr/bin/mullvad-browser}
EDITORS=${WREN_MENU_EDITORS-$HOME/.local/bin/mousepad /usr/bin/mousepad}
web_browser() {
local app c
app=$({ sed -n 's|^x-scheme-handler/https=||p' "$HOME/.config/mimeapps.list" 2>/dev/null || true; } | head -1)
for c in $BROWSERS; do
[ -x "$c" ] || continue
case $app:${c##*/} in
wren-firefox.desktop:firefox) echo "$c"; return ;;
esac
done
for c in $BROWSERS; do [ -x "$c" ] && { echo "$c"; return; }; done
return 0
}
text_editor() {
local c
for c in $EDITORS; do [ -x "$c" ] && { echo "$c"; return; }; done
return 0
}
# app NAME...: the first of these commands that is installed, from APPS (where
# Wren Linux and Debian put them; WREN_MENU_APPS: a test's own list)
APPS=${WREN_MENU_APPS-/usr/local/bin/tor-browser /usr/local/bin/tor-browser-unsafe /usr/bin/torbrowser-launcher $HOME/.local/bin/whonix $HOME/.local/bin/gimp $HOME/.local/bin/sparrow $HOME/.local/bin/feather /usr/bin/electrum /usr/bin/thunar /usr/bin/keepassxc /usr/bin/signal-desktop $HOME/.local/bin/simplex $HOME/.local/bin/claude $HOME/.local/bin/opencode-vm $HOME/.local/bin/vault $HOME/.local/bin/backup $HOME/.local/bin/wren-autostart $HOME/.local/bin/theme $HOME/.local/bin/secure-dns /usr/local/bin/wifi-signin}
app() {
local want c
for want; do
for c in $APPS; do [ "${c##*/}" = "$want" ] && [ -x "$c" ] && { echo "$c"; return; }; done
done
return 0
}
# claude_cmd: Claude Code in a window of its own (claude asks which, if more than one)
claude_cmd() {
local c
c=$(app claude)
[ -z "$c" ] || echo "foot --title=Claude -D $HOME $c"
}
IN_SWAY=
TOR_CMD='' TBU_CMD='' WHONIX_CMD='' GIMP_CMD='' SPARROW_CMD='' FEATHER_CMD='' ELECTRUM_CMD='' BROWSER='' EDITOR_CMD='' FILES_CMD='' PASSWORDS_CMD='' CLAUDE_CMD='' SIGNAL_CMD='' SIMPLEX_CMD='' OPENCODE_CMD='' SIGNIN_CMD='' THEME_CMD=''
# (a terminal window, as Super+Enter opens: for a mouse, or where the keys don't
# arrive: sway in a VM's window, whose computer's own sway takes Super first)
if [ -n "${SWAYSOCK:-}" ] && command -v swaymsg >/dev/null; then
IN_SWAY=1
BROWSER=$(web_browser) EDITOR_CMD=$(text_editor)
FILES_CMD=$(app thunar) PASSWORDS_CMD=$(app keepassxc) CLAUDE_CMD=$(claude_cmd)
SIGNAL_CMD=$(app signal-desktop) SIMPLEX_CMD=$(app simplex)
TOR_CMD=$(app tor-browser torbrowser-launcher) TBU_CMD=$(app tor-browser-unsafe) WHONIX_CMD=$(app whonix) GIMP_CMD=$(app gimp)
SPARROW_CMD=$(app sparrow) FEATHER_CMD=$(app feather) ELECTRUM_CMD=$(app electrum)
# A throwaway window for a Wi-Fi sign-in page (it switches secure DNS off
# while it is open, and on again after)
SIGNIN_CMD=$(app wifi-signin)
fi
BACKUP_CMD=$(app backup) VAULT_CMD=$(app vault) SECDNS_CMD=$(app secure-dns) AUTOSTART_CMD=$(app wren-autostart)
# The theme (light or dark), inside sway: the console stays dark
if [ -n "${SWAYSOCK:-}${WAYLAND_DISPLAY:-}" ]; then THEME_CMD=$(app theme); fi
# Text size, on a text console only (console-only mode): there is no Terminal
# there, and the menu runs in the console itself
FONT_CMD=
if [ -z "${SWAYSOCK:-}" ] && [ -z "${WAYLAND_DISPLAY:-}" ] && [ -x "$HOME/.local/bin/console-font" ]; then
FONT_CMD=$HOME/.local/bin/console-font
fi
HAS_UPDATE=
if [ -f "$DIR/update.sh" ]; then HAS_UPDATE=1; fi
# (WREN_MENU_NMTUI: a test's own; empty when NetworkManager isn't here)
NMTUI_CMD=${WREN_MENU_NMTUI-$(command -v nmtui || true)}
WIFI_SCRIPT=
if [ -f "$DIR/wifi.sh" ]; then WIFI_SCRIPT=1; fi
# radio_ids: all of them; wifi goes in the Wi-Fi list, the others in Settings
radio_ids=() HAS_WIFI='' other_radios=()
if [ -x "$RADIOS" ]; then
mapfile -t radio_ids < <("$RADIOS" list 2>/dev/null | awk '{ print $1 }')
fi
for r in "${radio_ids[@]}"; do
if [ "$r" = wifi ]; then HAS_WIFI=1; else other_radios+=("$r"); fi
done
# Icons on the first screen, only where they come out as pictures: in a
# window (a text console has no color emoji) and with a font that has every
# one of them. Without one, a terminal shows an empty box for each, so then
# there are none. Each is a wide (two-column) emoji, so the lines stay even.
# (WREN_MENU_ICONS=1 or 0: a test's own)
declare -A ICON=([browser]=๐ [private]=๐ง
[chat]=๐ฌ [files]=๐ [editor]=๐ [passwords]=๐
[terminal]=๐ป [apps]=โ [network]=๐ถ [privacy]=๐ [settings]=๐จ [system]=๐ฉบ [help]=โ [about]=๐ [power]=๐)
ICONS=${WREN_MENU_ICONS-}
if [ -z "$ICONS" ]; then
ICONS=0
if [ -n "${SWAYSOCK:-}${WAYLAND_DISPLAY:-}" ] && command -v fc-list >/dev/null; then
codes=$(for c in "${ICON[@]}"; do printf '%x ' "'$c"; done)
[ -z "$(fc-list ":charset=${codes% }" family 2>/dev/null)" ] || ICONS=1
fi
fi
# entry ID TEXT [SHOW]: an entry for this screen; with a third argument (a
# command found above), only if that isn't empty
entry() {
[ $# -lt 3 ] || [ -n "$3" ] || return 0
ids+=("$1")
items+=("$2")
if [ "$ICONS" = 1 ] && [ "$level" = top ]; then icons+=("${ICON[$1]:-}"); else icons+=(""); fi
}
# build: the entries of the screen in level
build() {
ids=() items=() icons=()
case $level in
top)
# Named by what you want to do, not by the program: the program's
# name shows in the list it opens. (No browser, no line: a host may
# do without one on purpose; More apps > Get more apps has them.)
entry browser "Browse the web" "$BROWSER"
entry private "Browse privately" "$TOR_CMD$WHONIX_CMD"
entry chat Chat "$SIGNAL_CMD$SIMPLEX_CMD"
entry files "My files" "$FILES_CMD"
entry editor "Write a note" "$EDITOR_CMD"
# Inside sway, this one stays even before it is installed, to say how
if [ -n "$PASSWORDS_CMD" ]; then entry passwords "Passwords"
elif [ -n "$IN_SWAY" ] && [ -n "$HAS_UPDATE" ]; then entry passwords "Passwords: install a manager"; fi
entry terminal Terminal "$IN_SWAY"
entry apps "More apps" "$HAS_UPDATE$GIMP_CMD$SPARROW_CMD$FEATHER_CMD$ELECTRUM_CMD$CLAUDE_CMD$OPENCODE_CMD"
entry network Wi-Fi "$HAS_WIFI$NMTUI_CMD$WIFI_SCRIPT$SIGNIN_CMD"
entry privacy Privacy "${other_radios[*]}$VAULT_CMD$SECDNS_CMD"
entry settings Settings "$THEME_CMD$FONT_CMD$AUTOSTART_CMD"
entry system "Is my computer OK?" "$HAS_UPDATE$BACKUP_CMD"
entry help "Help & keyboard shortcuts"
entry about "About this computer"
entry power "Lock, sleep, shut down" ;;
private)
entry tor "Tor Browser" "$TOR_CMD"
entry tor-unsafe "Tor Browser (Safer)" "$TBU_CMD"
entry whonix Whonix "$WHONIX_CMD"
entry back Back ;;
chat)
entry signal Signal "$SIGNAL_CMD"
entry simplex SimpleX "$SIMPLEX_CMD"
entry back Back ;;
apps)
entry apps-install "Get more apps" "$HAS_UPDATE"
entry gimp GIMP "$GIMP_CMD"
entry sparrow Sparrow "$SPARROW_CMD"
entry feather Feather "$FEATHER_CMD"
entry electrum Electrum "$ELECTRUM_CMD"
entry claude Claude "$CLAUDE_CMD"
entry remove "Remove apps" "$HAS_UPDATE"
entry back Back ;;
network)
entry choose "Choose a network" "$NMTUI_CMD$WIFI_SCRIPT"
entry wifi "" "$HAS_WIFI"
entry signin "Sign-in page" "$SIGNIN_CMD"
entry back Back ;;
privacy)
for r in "${other_radios[@]}"; do entry "$r" ""; done
# the vault's own switch: closed, it asks for the vault's password
entry vault "" "$VAULT_CMD"
entry securedns "" "$SECDNS_CMD"
entry back Back ;;
settings)
entry theme "" "$THEME_CMD"
entry font "Font size" "$FONT_CMD"
entry autostart "Startup programs" "$AUTOSTART_CMD"
entry back Back ;;
system)
entry update Update "$HAS_UPDATE"
entry status Status "$HAS_UPDATE"
entry backup "Back up now" "$BACKUP_CMD"
entry back Back ;;
power)
entry lock Lock "$IN_SWAY"
entry suspend Sleep
entry logout "Log out" "$IN_SWAY"
entry reboot Restart
entry shutdown "Shut down"
entry back Back ;;
esac
n=${#ids[@]}
refresh_items
}
# go LEVEL: to that screen, the cursor on its first entry (or, back on the first
# screen, on the list you came from)
go() {
local from=$level i
level=$1
build
sel=0
for i in "${!ids[@]}"; do
if [ "${ids[i]}" = "$from" ]; then sel=$i; fi
done
}
# radio_label NAME: what the entry shows, e.g. "Wi-Fi: on"
radio_label() {
local name state
if [ "$1" = theme ]; then
state=$("$THEME_CMD" 2>/dev/null)
state=${state#Theme: }
printf 'Theme: %s' "${state^}"
return
fi
if [ "$1" = securedns ]; then
printf 'Secure DNS: %s' "$("$SECDNS_CMD" status 2>/dev/null || echo unknown)"
return
fi
if [ "$1" = vault ]; then
state=$("$VAULT_CMD" status 2>/dev/null)
printf 'Vault: %s' "${state%% *}"
return
fi
case $1 in wifi) name=Wi-Fi ;; bluetooth) name=Bluetooth ;; mic) name=Microphone ;; camera) name=Webcam ;; esac
state=$("$RADIOS" "$1" status 2>/dev/null)
case $state in
blocked) state="off (hardware switch)" ;;
"") state="unknown" ;;
esac
printf '%s: %s' "$name" "$state"
}
# privacy_label: the first screen's line: what matters at a glance, whatever
# is in the list ("Privacy: mic off, camera off, DNS on")
privacy_label() {
local r said=''
for r in "${radio_ids[@]}"; do
case $r in
mic) said+=", mic $("$RADIOS" mic status 2>/dev/null)" ;;
camera) said+=", camera $("$RADIOS" camera status 2>/dev/null)" ;;
esac
done
if [ -n "$SECDNS_CMD" ]; then said+=", DNS $("$SECDNS_CMD" status 2>/dev/null || echo unknown)"; fi
printf 'Privacy%s' "${said:+: ${said#, }}"
}
refresh_items() {
local i
for i in "${!ids[@]}"; do
case ${ids[i]} in
privacy) items[i]=$(privacy_label) ;;
wifi | bluetooth | mic | camera | vault | theme | securedns) items[i]=$(radio_label "${ids[i]}") ;;
esac
done
}
level=top
sel=0
width=44
first_row=1 first_col=1
build
# level_title: where you are, after the title
level_title() {
case $level in
top) ;;
private) echo " > Browse privately" ;;
apps) echo " > More apps" ;;
network) echo " > Wi-Fi" ;;
system) echo " > Is my computer OK?" ;;
*) echo " > ${level^}" ;;
esac
}
# draw: the title, the choices (the chosen one reversed), and a hint, centered.
# The padding is counted here: printf's would count bytes, and an icon is
# four of them but two columns wide.
draw() {
local rows cols top left i y text pad
size=$(stty size 2>/dev/null || echo "24 80")
read -r rows cols <<<"$size"
top=$(((rows - n - 4) / 2 + 1))
[ "$top" -ge 1 ] || top=1
left=$(((cols - width) / 2 + 1))
[ "$left" -ge 1 ] || left=1
first_row=$((top + 2))
first_col=$left
printf '\e[2J\e[%d;%dH\e[1mWren Linux%s\e[0m' "$top" "$left" "$(level_title)"
for i in "${!items[@]}"; do
y=$((first_row + i))
text=${items[i]}
if [ -n "${icons[i]}" ]; then text="${icons[i]} $text"; fi
pad=$((width - 2 - ${#text} - ${#icons[i]}))
[ "$pad" -ge 0 ] || pad=0
if [ "$i" -eq "$sel" ]; then
printf '\e[%d;%dH\e[7m %s%*s \e[0m' "$y" "$left" "$text" "$pad" ""
else
printf '\e[%d;%dH %s%*s ' "$y" "$left" "$text" "$pad" ""
fi
done
if [ "$level" = top ]; then
printf '\e[%d;%dH\e[2mEnter or click; Esc closes\e[0m' $((first_row + n + 1)) "$left"
else
printf '\e[%d;%dH\e[2mEnter or click; Space switches; Esc goes back\e[0m' $((first_row + n + 1)) "$left"
fi
}
# about_fields: About this computer, one "Label<Tab>value" line each, an
# empty line between the groups. Only what is safe to show anyone, as a
# screenshot asking for help: no serial number, MAC or IP address, network,
# computer or user name. (WREN_ETC and WREN_PROC: a test's own /etc and /proc)
about_fields() {
local etc=${WREN_ETC:-/etc} proc=${WREN_PROC:-/proc} v os cpu total avail root disk
v=$(git -C "$DIR" describe --always --dirty 2>/dev/null) ||
v=$(dpkg-query -W -f '${Version}' wren-linux 2>/dev/null) || v=unknown
os=$(. "$etc/os-release" 2>/dev/null && echo "${PRETTY_NAME:-}")
# (without the (R) and (TM) marks, which only make the line longer)
cpu=$(sed -n 's/^model name[[:space:]]*: *//p' "$proc/cpuinfo" 2>/dev/null | head -1 | sed 's/([RT]M\{0,1\})//g; s/ */ /g')
total=$(awk '$1 == "MemTotal:" { print $2 }' "$proc/meminfo" 2>/dev/null)
avail=$(awk '$1 == "MemAvailable:" { print $2 }' "$proc/meminfo" 2>/dev/null)
disk=$(df -h --output=avail,size / 2>/dev/null | awk 'NR == 2 { print $1 " free of " $2 }')
printf 'Wren Linux\t%s\n' "$v"
printf 'System\t%s\n' "${os:-unknown}"
printf 'Kernel\t%s\n' "$(uname -r 2>/dev/null || echo unknown)"
printf 'Processor\t%s\n' "${cpu:-unknown}"
printf 'Threads\t%s\n' "$(nproc 2>/dev/null || echo unknown)"
if [ -n "$total" ] && [ -n "$avail" ]; then
awk -v t="$total" -v a="$avail" 'BEGIN { printf "Memory\t%.1f GB (%.1f GB in use)\n", t / 1048576, (t - a) / 1048576 }'
else
printf 'Memory\tunknown\n'
fi
printf 'Disk\t%s\n\n' "${disk:-unknown}"
# as status.sh checks them: any dm-crypt layer under /; ufw on and enabled
root=$(findmnt -no SOURCE / 2>/dev/null | sed 's/\[.*//')
if [ -z "$root" ]; then
printf 'Disk encrypted\tunknown\n'
elif lsblk -s -no TYPE "$root" 2>/dev/null | grep -qx crypt; then
printf 'Disk encrypted\tyes\n'
else
printf 'Disk encrypted\tno\n'
fi
if systemctl is-active --quiet ufw 2>/dev/null && grep -qsx 'ENABLED=yes' "$etc/ufw/ufw.conf"; then
printf 'Firewall\ton\n'
else
printf 'Firewall\toff\n'
fi
}
ABOUT_LABEL=16 # the label column: the longest label (Disk encrypted) and two spaces
# about_text: the same as plain text, for c to copy
about_text() {
local label value
printf 'About this computer\n\n'
while IFS=$'\t' read -r label value; do
if [ -z "$label" ]; then echo; else printf ' %-*s%s\n' "$ABOUT_LABEL" "$label" "$value"; fi
done < <(about_fields)
}
# about_draw: the fields in a block centered in the window: the title, a rule,
# the labels dim and the values bright, a rule between the groups, the keys
about_draw() {
local rows cols w=0 h top left y label value line text rule
size=$(stty size 2>/dev/null || echo "24 80")
read -r rows cols <<<"$size"
for line in "${about[@]}"; do
value=${line#*$'\t'}
[ $((ABOUT_LABEL + ${#value})) -le "$w" ] || w=$((ABOUT_LABEL + ${#value}))
done
[ "$w" -ge 36 ] || w=36
[ "$w" -le $((cols - 2)) ] || w=$((cols - 2))
[ "$w" -ge 1 ] || w=1
h=$((${#about[@]} + 6))
top=$(((rows - h) / 2 + 1))
[ "$top" -ge 1 ] || top=1
left=$(((cols - w) / 2 + 1))
[ "$left" -ge 1 ] || left=1
printf -v rule '%*s' "$w" ''
rule=${rule// /โ}
text="About this computer"
printf '\e[2J\e[%d;%dH\e[1m%s\e[0m' "$top" $((left + (w - ${#text}) / 2)) "$text"
printf '\e[%d;%dH\e[2m%s\e[0m' $((top + 1)) "$left" "$rule"
y=$((top + 2))
for line in "${about[@]}"; do
if [ -z "$line" ]; then
printf '\e[%d;%dH\e[2m%s\e[0m' "$y" "$left" "$rule"
else
label=${line%%$'\t'*} value=${line#*$'\t'}
value=${value:0:$((w - ABOUT_LABEL > 0 ? w - ABOUT_LABEL : 0))}
printf '\e[%d;%dH\e[2m%-*s\e[0m\e[1m%s\e[0m' "$y" "$left" "$ABOUT_LABEL" "$label" "$value"
fi
y=$((y + 1))
done
printf '\e[%d;%dH\e[2m%s\e[0m' "$y" "$left" "$rule"
if [ -n "$copied" ]; then
text="Copied. Esc goes back."
elif [ -n "$WLCOPY" ]; then
text="c copies it (safe to share) ยท Esc goes back"
else
text="Nothing here says who you are ยท Esc goes back"
fi
printf '\e[%d;%dH\e[2m%s\e[0m' $((y + 2)) $((left + (w - ${#text}) / 2 > 0 ? left + (w - ${#text}) / 2 : 1)) "$text"
}
# about_screen: shows it until Esc or q, which go back to the menu
about_screen() {
local copied=''
mapfile -t about < <(about_fields)
REDRAW=about_draw
while :; do
about_draw
read_key
case $KEY in
esc | q) REDRAW=draw; return 0 ;;
c) if [ -n "$WLCOPY" ] && about_text | "$WLCOPY" 2>/dev/null; then copied=1; fi ;;
esac
done
}
# run_choice: runs the chosen entry in this window
run_choice() {
local out
case ${ids[sel]} in
private | chat | apps | network | privacy | settings | system | power)
go "${ids[sel]}"
return 0 ;;
back)
go top
return 0 ;;
autostart)
# The startup watch (wren-watch) warns about any change to this
# list. One you just made here is offered to it as yours, as wrenctl
# does; one that was there before you opened it is not (it may not
# have been you), and the bar's "startup changed" shows it.
local watch=${WREN_WATCH_CMD:-/usr/local/bin/wren-watch} rel=.config/wren/autostart before='' after='' pending=''
if [ -x "$watch" ]; then
before=$("$watch" manifest 2>/dev/null | awk -v r="$rel" '$NF == r')
! grep -qxF "$rel" "${WREN_WATCH_RUN:-/run/wren-watch}/$(id -un)" 2>/dev/null || pending=1
fi
screen_off
printf '\e[2J\e[1;1H'
"$AUTOSTART_CMD" edit
if [ -x "$watch" ]; then after=$("$watch" manifest 2>/dev/null | awk -v r="$rel" '$NF == r'); fi
if [ "$before" != "$after" ] && [ -n "$pending" ]; then
printf '\e[2J\e[1;1HYour startup list had changed before you opened it, so it is not taken as\nyours here: look at "startup changed" in the bar.\n\nPress Enter to go back. '
wait_enter
elif [ "$before" != "$after" ]; then
printf '\e[2J\e[1;1HTake it as yours, so the bar doesn'"'"'t warn about your own change?\n(sudo asks your password; Enter: yes, n: no) '
waiting
IFS= read -r out </dev/tty || out=n
case $out in [nN]*) ;; *) printf '\n'; sudo "$watch" accept "$rel" || { printf '\nNot taken. Press Enter to go back. '; wait_enter; } ;; esac
fi
screen_on
settle
return 0 ;;
wifi | bluetooth | mic | camera)
"$RADIOS" "${ids[sel]}" toggle >/dev/null 2>&1
refresh_items
return 0 ;;
securedns)
# Switching on checks that names resolve (up to 20 seconds), and
# either way it says what to do next: shown on this window's screen
screen_off
printf '\e[2J\e[1;1HSwitching secure DNS...\n\n'
"$SECDNS_CMD" toggle || true
printf '\nPress Enter to go back. '
wait_enter
screen_on
settle
refresh_items
return 0 ;;
theme)
# (this window keeps its colors; the next ones open in the new theme)
"$THEME_CMD" toggle >/dev/null 2>&1
refresh_items
return 0 ;;
terminal)
swaymsg exec foot >/dev/null 2>&1
exit 0 ;;
browser)
# the sandboxed Firefox, already open: to its window, on whichever workspace it is
# (bin/firefox-session names it wren-firefox; a profile's or Firefox as you have
# other names: sway's criteria are regular expressions, so anchored, or a profile's
# wren-firefox-NAME would match too), not a second one
if [ "${BROWSER##*/}" = firefox ] && swaymsg -q '[app_id="^wren-firefox$"] focus' >/dev/null 2>&1; then exit 0; fi
swaymsg exec "$BROWSER" >/dev/null 2>&1
exit 0 ;;
passwords)
local cmd=$PASSWORDS_CMD
if [ -z "$cmd" ]; then
# not installed yet: the list to install it from
ids[sel]=apps-install
run_choice
return
fi
# already open: to its window, on whichever workspace it is (sway's
# focus fails when no window has that app id), not a second one
swaymsg -q '[app_id="org.keepassxc.KeePassXC"] focus' >/dev/null 2>&1 ||
swaymsg exec "$cmd" >/dev/null 2>&1
exit 0 ;;
files | signal | simplex | claude | signin | tor | whonix | gimp | sparrow | feather | electrum)
local cmd
case ${ids[sel]} in
signin) cmd=$SIGNIN_CMD ;;
files) cmd=$FILES_CMD ;;
signal) cmd=$SIGNAL_CMD ;;
simplex) cmd=$SIMPLEX_CMD ;; claude) cmd=$CLAUDE_CMD ;;
tor) cmd=$TOR_CMD ;; tor-unsafe) cmd=$TBU_CMD ;; whonix) cmd=$WHONIX_CMD ;; gimp) cmd=$GIMP_CMD ;;
sparrow) cmd=$SPARROW_CMD ;; feather) cmd=$FEATHER_CMD ;; electrum) cmd=$ELECTRUM_CMD ;;
esac
swaymsg exec "$cmd" >/dev/null 2>&1
exit 0 ;;
choose)
# nmtui draws in this window; without NetworkManager, wifi.sh asks here
screen_off
printf '\e[2J\e[1;1H'
if [ -n "$NMTUI_CMD" ]; then
"$NMTUI_CMD" || true
else
(cd "$DIR" && bash wifi.sh) || true
printf '\nPress Enter to go back. '
wait_enter
fi
screen_on
settle
refresh_items
return 0 ;;
# As bin/powermenu does; the lock through sway, so it outlives this window
lock)
if [ -x "$HOME/.local/bin/wren-lock" ]; then swaymsg exec "$HOME/.local/bin/wren-lock" >/dev/null 2>&1
else swaymsg exec "swaylock -f -c 000000" >/dev/null 2>&1; fi
exit 0 ;;
suspend) systemctl suspend; exit 0 ;;
logout) swaymsg exit >/dev/null 2>&1; exit 0 ;;
reboot) systemctl reboot; exit 0 ;;
shutdown) systemctl poweroff; exit 0 ;;
vault)
if "$VAULT_CMD" status 2>/dev/null | grep -q '^open'; then
"$VAULT_CMD" close >/dev/null 2>&1
else
# It asks for the vault's password: on this window's own screen
screen_off
printf '\e[2J\e[1;1HOpening the vault (for an hour; vault open --for 2h in a terminal for longer).\n\n'
"$VAULT_CMD" open || true
printf '\nPress Enter to go back. '
wait_enter
screen_on
settle
fi
refresh_items
return 0 ;;
backup)
screen_off
printf '\e[2J\e[1;1HBacking up your home folder.\n\n'
"$BACKUP_CMD" || printf '\nThe backup stopped with an error (see above).\n'
printf '\nPress Enter to go back. '
wait_enter
screen_on
settle ;;
editor)
swaymsg exec "$EDITOR_CMD" >/dev/null 2>&1
exit 0 ;;
font)
# console-font lists the sizes and asks; leave the menu's screen
# for it, then come back to the menu
screen_off
printf '\e[2J\e[1;1H'
"$FONT_CMD"
printf '\nPress Enter to go back. '
wait_enter
screen_on
settle ;;
help)
# the sheet without the lines for things that are not installed
mapfile -t lines < <("$HOME/.local/bin/keys" --text 2>/dev/null || cat "$KEYS_FILE")
pager back ;;
about) about_screen ;;
status)
REDRAW=
printf '\e[2J\e[1;1HLooking at what is set up...'
out=$(cd "$DIR" && bash status.sh 2>&1)
mapfile -t lines <<<"$out"
pager back ;;
apps-install | remove | update)
trap - EXIT
screen_off
cd "$DIR" || exit 1
export WREN_PAUSE=1
# The scripts wait for a key at their end (pause_if_asked), which
# marks this file; one that stops on an error before that would
# close the window at once, so this waits instead
WREN_PAUSED_FILE=$(mktemp -u "${TMPDIR:-/tmp}/wren-paused.XXXXXX")
export WREN_PAUSED_FILE
local rc log=
# Busy: a click on Wren Linux in the bar now brings this window to the
# front instead of closing it (bin/sway-status), so nothing is cut off
local busy=${XDG_RUNTIME_DIR:-/tmp}/wren-menu-busy
echo $$ >"$busy"
if [ "${ids[sel]}" = update ]; then
# What Update prints is also kept, for a look afterwards
log=${XDG_STATE_HOME:-$HOME/.local/state}/wren/menu-update.log
mkdir -p "${log%/*}"
# (through wrenctl, which takes what its run changed in your startup files
# as Wren Linux's, so they don't show as "startup changed")
WREN_DIR=$DIR bash bin/wrenctl update 2>&1 | tee "$log"
rc=${PIPESTATUS[0]}
else
if [ "${ids[sel]}" = remove ]; then WREN_DIR=$DIR bash bin/wrenctl apps --remove; else WREN_DIR=$DIR bash bin/wrenctl apps; fi
rc=$?
fi
if [ "$rc" -ne 0 ] && [ ! -e "$WREN_PAUSED_FILE" ]; then
printf '\nIt stopped with an error (status %s).%s\nPress Enter to close. ' \
"$rc" "${log:+ What it printed is in $log}" >&2
wait_enter
fi
rm -f "$WREN_PAUSED_FILE" "$busy"
exit "$rc" ;;
esac
}
screen_on
settle
REDRAW=draw
while :; do
draw
read_key
case $KEY in
enter) run_choice ;;
# Space flips a switch (Wi-Fi, Bluetooth, microphone, webcam, theme,
# secure DNS); on anything else it does nothing, so it never opens a program
space) case ${ids[sel]} in wifi | bluetooth | mic | camera | theme | securedns) run_choice ;; esac ;;
down | j) sel=$(((sel + 1) % n)) ;;
up | k) sel=$(((sel + n - 1) % n)) ;;
wheeldown) sel=$(((sel + 1) % n)) ;;
wheelup) sel=$(((sel + n - 1) % n)) ;;
esc | q)
# a list goes back to the first screen, which closes
[ "$level" != top ] || exit 0
go top ;;
click)
i=$((CY - first_row))
if [ "$i" -ge 0 ] && [ "$i" -lt "$n" ] && [ "$CX" -ge "$first_col" ] && [ "$CX" -lt $((first_col + width)) ]; then
sel=$i
run_choice
fi ;;
esac
done