#!/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
