Wren Linux

wren-linux / bin / theme

#!/bin/bash
# theme: written by Wren Linux parts/theme.sh
# Wren Linux's colors, light or dark, from one place:
#   theme                 which theme is set (dark unless you chose light)
#   theme light | dark    switch to it (the Wren menu's Theme line too)
#   theme toggle          the other one
#   theme get NAME        one of its colors (e.g. LOCK), for the lock screen
#   theme env             all of them as NAME='value' lines, for Wren Linux's
#                         commands (wren-menu, nmtui, powermenu, wallpaper)
#   theme apply           write the current theme's colors again
# Switching writes the bar's colors, the plain background (no wallpaper) and
# the window borders in a sway snippet, foot's [colors], mako's colors and the launcher's
# (Super+d), GTK apps' light or dark (settings.ini, and the color-scheme
# setting libadwaita apps read), then reloads sway. The sandboxed Firefox and
# incognito are told at their next start (bin/firefox, bin/incognito). Windows open now keep their colors until they
# open again. The text console (tmux) stays dark. Only Wren Linux's own files are
# changed: a foot.ini or mako config you wrote yourself is left alone.
# The colors are data/themes/dark and light, read strictly: NAME=colour lines
# (rrggbb or rrggbbaa), lists of them, nothing else.
set -uo pipefail

STATE=${XDG_CONFIG_HOME:-$HOME/.config}/wren/theme
THEMES=$HOME/.local/share/wren/themes
SWAY_CONF=$HOME/.config/sway/config
SNIPPET=$HOME/.config/sway/config.d/theme.conf
FOOT=$HOME/.config/foot/foot.ini
MAKO=$HOME/.config/mako/config
NEEDED=(DESKTOP_BG BAR_BG BAR_TEXT BAR_INACTIVE_TEXT LOCK NOTIFY_BG NOTIFY_TEXT NOTIFY_BORDER
    WMENU_BG WMENU_TEXT MENU_FG MENU_BG MENU_REGULAR MENU_BRIGHT NEWT_TEXT NEWT_REVERSED NEWT_DIM
    FOOT_FG FOOT_BG FOOT_REGULAR FOOT_BRIGHT FOOT_SEL_FG FOOT_SEL_BG
    BORDER_ACTIVE BORDER_TEXT BORDER_INACTIVE BORDER_INACTIVE_TEXT)
declare -A C

die() { echo "theme: $*" >&2; exit 1; }

# current: the theme set (dark if none, or if the file says something else)
current() {
    local t
    t=$(head -c 16 "$STATE" 2>/dev/null)
    case $t in light | dark) echo "$t" ;; *) echo dark ;; esac
}

# load NAME: its colors into C; fails on a missing file, a line that isn't a
# color, or a color missing
load() {
    local f=$THEMES/$1 line key val k
    [ -f "$f" ] || return 1
    while IFS= read -r line || [ -n "$line" ]; do
        case $line in '' | '#'*) continue ;; esac
        [[ $line =~ ^([A-Z_]+)=([0-9a-fA-F]{6}([0-9a-fA-F]{2})?( [0-9a-fA-F]{6}([0-9a-fA-F]{2})?)*)$ ]] || return 1
        key=${BASH_REMATCH[1]} val=${BASH_REMATCH[2]}
        C[$key]=${val,,}
    done <"$f"
    for k in "${NEEDED[@]}"; do [ -n "${C[$k]:-}" ] || return 1; done
    for k in MENU_REGULAR MENU_BRIGHT FOOT_REGULAR FOOT_BRIGHT; do
        [ "$(wc -w <<<"${C[$k]}")" = 8 ] || return 1
    done
}

# snippet: the bar's colors, the plain background and the window borders, for
# sway. Borders: 1 pixel on floating windows (the menu, pop-ups), the active
# one in the theme's color; none on tiled windows. sway gives default_floating_border
# only to windows that open floating, so the pop-ups a rule floats get theirs by
# app_id, and Super+Shift+space sets the border as it floats or tiles a window.
# This file, not install-base.sh's, because update.sh runs parts/theme.sh again
# when this changes. (Your own sway file after this one changes them:
# config.d/zz-mine.conf, say.)
snippet() {
    local a=${C[BORDER_ACTIVE]} at=${C[BORDER_TEXT]} i=${C[BORDER_INACTIVE]} it=${C[BORDER_INACTIVE_TEXT]}
    cat <<EOF
# Wren Linux's theme ($1): written by bin/theme; "theme light" or "theme dark" rewrites it
output * bg #${C[DESKTOP_BG]} solid_color
bar bar-0 colors {
    background #${C[BAR_BG]}
    statusline #${C[BAR_TEXT]}
    inactive_workspace #00000000 #00000000 #${C[BAR_INACTIVE_TEXT]}
}
default_border none
default_floating_border pixel 1
for_window [app_id="^wren-(keys|run|nmtui|timer)$"] border pixel 1
# (--no-warn: sway's own config binds these keys first; without it, sway shows a warning bar at every reload)
bindsym --no-warn \$mod+Shift+space floating toggle; [con_id=__focused__ floating] border pixel 1; [con_id=__focused__ tiling] border none
client.focused #$a #$a #$at #$a #$a
client.focused_inactive #$i #$i #$it #$i #$i
client.unfocused #$i #$i #$it #$i #$i
EOF
}

# foot_colors: foot.ini's [colors] section
foot_colors() {
    local i=0 c
    printf '[colors]\nalpha=1.0\nforeground=%s\nbackground=%s\n' "${C[FOOT_FG]}" "${C[FOOT_BG]}"
    for c in ${C[FOOT_REGULAR]}; do printf 'regular%d=%s\n' "$i" "$c"; i=$((i + 1)); done
    i=0
    for c in ${C[FOOT_BRIGHT]}; do printf 'bright%d=%s\n' "$i" "$c"; i=$((i + 1)); done
    printf 'selection-foreground=%s\nselection-background=%s\n' "${C[FOOT_SEL_FG]}" "${C[FOOT_SEL_BG]}"
}

# gtk LIGHT|DARK: GTK 3 and 4 apps (Mousepad, Thunar...) prefer that: one line
# in each settings.ini, the rest kept; and the desktop's color-scheme
# setting, which libadwaita apps read (when gsettings is there)
gtk() {
    local dark=false f d
    [ "$1" = dark ] && dark=true
    for d in gtk-3.0 gtk-4.0; do
        f=${XDG_CONFIG_HOME:-$HOME/.config}/$d/settings.ini
        mkdir -p "${f%/*}"
        [ -f "$f" ] || printf '[Settings]\n' >"$f"
        grep -q '^\[Settings\]' "$f" || printf '\n[Settings]\n' >>"$f"
        if grep -q '^gtk-application-prefer-dark-theme=' "$f"; then
            sed -i "s/^gtk-application-prefer-dark-theme=.*/gtk-application-prefer-dark-theme=$dark/" "$f"
        else
            sed -i "/^\[Settings\]/a gtk-application-prefer-dark-theme=$dark" "$f"
        fi
    done
    if command -v gsettings >/dev/null; then
        gsettings set org.gnome.desktop.interface color-scheme "prefer-$1" 2>/dev/null || true
        # Mousepad: Wren Linux's scheme to match (extras/mousepad.sh), while it
        # is one of Wren Linux's two; one you chose yourself stays
        if gsettings list-schemas 2>/dev/null | grep -qx org.xfce.mousepad.preferences.view; then
            case $(gsettings get org.xfce.mousepad.preferences.view color-scheme 2>/dev/null) in
                "'wren-dark'" | "'wren-light'")
                    gsettings set org.xfce.mousepad.preferences.view color-scheme "wren-$1" 2>/dev/null || true ;;
            esac
        fi
    fi
}

# apply: the current theme's colors into Wren Linux's files, then sway reloads
apply() {
    local t tmp
    t=$(current)
    load "$t" || die "the $t theme's colors ($THEMES/$t) can't be read; run wrenctl part theme"
    mkdir -p "${SNIPPET%/*}"
    snippet "$t" >"$SNIPPET.new" && mv -f "$SNIPPET.new" "$SNIPPET"
    # foot: only Wren Linux's foot.ini; its [colors] section, up to the next section
    if grep -qs 'Written by Wren Linux' "$FOOT"; then
        tmp=$(mktemp)
        awk -v colors="$(foot_colors)" '
            /^\[colors\]/ { print colors; skip = 1; next }
            /^\[/ { skip = 0 }
            !skip { print }' "$FOOT" >"$tmp" && cat "$tmp" >"$FOOT"
        rm -f "$tmp"
    fi
    # mako: only Wren Linux's config
    if grep -qsxF '# Written by Wren Linux parts/mako.sh' "$MAKO"; then
        sed -i -E '/^(background-color|text-color|border-color)=/d' "$MAKO"
        printf 'background-color=#%s\ntext-color=#%s\nborder-color=#%s\n' \
            "${C[NOTIFY_BG]}" "${C[NOTIFY_TEXT]}" "${C[NOTIFY_BORDER]}" >>"$MAKO"
        if pgrep -x mako >/dev/null; then makoctl reload >/dev/null 2>&1 || true; fi
    fi
    # the launcher (Super+d): only the line Wren Linux styled (lib/sway.sh)
    if [ -f "$SWAY_CONF" ]; then
        sed -i -E "s/^(set \\\$menu wmenu-run -f 'monospace 12') -N [0-9a-f]{6} -n [0-9a-f]{6} -M [0-9a-f]{6} -m [0-9a-f]{6} -S [0-9a-f]{6} -s [0-9a-f]{6}\$/\\1 -N ${C[WMENU_BG]} -n ${C[WMENU_TEXT]} -M ${C[WMENU_BG]} -m ${C[WMENU_TEXT]} -S ${C[WMENU_TEXT]} -s ${C[WMENU_BG]}/" "$SWAY_CONF"
    fi
    gtk "$t"
    if [ -n "${SWAYSOCK:-}" ]; then swaymsg -q reload 2>/dev/null || true; fi
}

case ${1:-} in
    "" | status)
        echo "Theme: $(current)" ;;
    light | dark)
        load "$1" || die "the $1 theme's colors ($THEMES/$1) can't be read; run wrenctl part theme"
        mkdir -p "${STATE%/*}"
        echo "$1" >"$STATE"
        apply
        if command -v notify-send >/dev/null; then notify-send -t 3000 Theme "${1^}: new windows open in it" 2>/dev/null || true; fi ;;
    toggle)
        if [ "$(current)" = dark ]; then exec "$0" light; else exec "$0" dark; fi ;;
    get)
        # one color, or nothing and failure: never an empty or odd value (the
        # lock screen falls back to black on failure)
        [[ ${2:-} =~ ^[A-Z_]+$ ]] || exit 2
        load "$(current)" || exit 1
        [ -n "${C[$2]:-}" ] || exit 1
        echo "${C[$2]}" ;;
    env)
        load "$(current)" || exit 1
        for k in "${NEEDED[@]}"; do printf "%s='%s'\n" "$k" "${C[$k]}"; done ;;
    apply) apply ;;
    *) echo "usage: theme [light | dark | toggle | get NAME | env | apply]" >&2; exit 2 ;;
esac