#!/bin/bash
# settings [--time-zone ZONE] [--color-scheme light|dark]
#     [--keyboard-layout LAYOUT [--keyboard-variant VARIANT]] [--locale ll_CC]...
#
# Makes the desktop match the computer it is viewed from. The desktop add-on's
# copy of the desktop image's desktop-settings.sh. The viewer calls this
# through PUT /api/settings each time it connects, with what the browser says
# about that computer. Only the
# options given are changed, and each is left alone when it already matches.
#
#   --time-zone      The workspace's time zone (/etc/localtime, which every
#                    program reads now that the image sets no TZ), and the
#                    panel clock's, which caches the zone it started with.
#   --color-scheme   The light or dark variant of the GTK theme in use, and of
#                    the panel. A theme with no such variant is left as it is.
#   --keyboard-*     The X keyboard layout. The viewer sends each character as
#                    a keysym, so text comes out right under any layout; with
#                    the viewer's own layout, a character such as ö is on its
#                    real key instead of a spare keycode the VNC server
#                    borrows for it, which shortcuts and some apps do not see.
#   --locale         Date and time formats: the panel clock's 12- or 24-hour
#                    time at once, and LC_TIME, LC_PAPER and LC_MEASUREMENT for
#                    apps from the desktop's next start (libexec/session
#                    reads them). Numbers, sorting and messages stay en_US: a
#                    decimal comma breaks scripts and agents parsing output.
#                    Given more than once, the first locale glibc has is used:
#                    a browser in en-SE offers sv_SE after en_SE, which glibc
#                    lacks.
#
# Exit codes are read by devgrail-desktop (localapi.go): 2 when a value is
# malformed or the desktop does not have it (every other value is still
# applied), 3 when the session is not up within about 15 seconds.
set -euo pipefail

time_zone='' color_scheme='' keyboard_layout='' keyboard_variant='' locale_name=''
locales=()
while [ $# -gt 0 ]; do
    [ $# -ge 2 ] || { echo "missing value for $1" >&2; exit 2; }
    case "$1" in
        --time-zone) time_zone="$2" ;;
        --color-scheme) color_scheme="$2" ;;
        --keyboard-layout) keyboard_layout="$2" ;;
        --keyboard-variant) keyboard_variant="$2" ;;
        --locale) locales+=("$2") ;;
        *) echo "unknown option: $1" >&2; exit 2 ;;
    esac
    shift 2
done

# The same shapes devgrail-desktop checks. They keep every value a plain name:
# no "..", no leading slash, nothing a path or a command line reads specially.
malformed() {
    echo "$1" >&2
    exit 2
}
if [ -n "$time_zone" ] && ! [[ $time_zone =~ ^[A-Za-z0-9_+-]+(/[A-Za-z0-9_+-]+)*$ ]]; then
    malformed "malformed time zone: $time_zone"
fi
case "$color_scheme" in '' | light | dark) ;; *) malformed "unknown color scheme: $color_scheme" ;; esac
if [ -n "$keyboard_layout" ] && ! [[ $keyboard_layout =~ ^[a-z]{2,8}$ ]]; then
    malformed "malformed keyboard layout: $keyboard_layout"
fi
if [ -n "$keyboard_variant" ] && ! [[ -n $keyboard_layout && $keyboard_variant =~ ^[a-z0-9_]{1,32}$ ]]; then
    malformed "malformed keyboard variant: $keyboard_variant"
fi
for candidate in "${locales[@]}"; do
    [[ $candidate =~ ^[a-z]{2,3}_[A-Z]{2}$ ]] || malformed "malformed locale: $candidate"
done

# A value this desktop does not have is skipped and reported at the end, so a
# locale glibc lacks (en_SE, say) does not also cost the time zone and theme.
missing=()
if [ -n "$time_zone" ] && [ ! -f "/usr/share/zoneinfo/$time_zone" ]; then
    missing+=("the desktop has no time zone $time_zone")
    time_zone=''
fi
if [ -n "$keyboard_layout" ]; then
    symbols="/usr/share/X11/xkb/symbols/$keyboard_layout"
    if [ ! -f "$symbols" ]; then
        missing+=("the desktop has no keyboard layout $keyboard_layout")
        keyboard_layout='' keyboard_variant=''
    elif [ -n "$keyboard_variant" ] && ! grep -q "xkb_symbols \"$keyboard_variant\"" "$symbols"; then
        missing+=("the desktop has no keyboard layout $keyboard_layout($keyboard_variant)")
        keyboard_layout='' keyboard_variant=''
    fi
fi
for candidate in "${locales[@]}"; do
    if grep -qx "$candidate.UTF-8 UTF-8" /usr/share/i18n/SUPPORTED; then
        locale_name="$candidate"
        break
    fi
done
if [ ${#locales[@]} -gt 0 ] && [ -z "$locale_name" ]; then
    names="$(printf '%s or ' "${locales[@]}")"
    missing+=("the desktop has no locale ${names% or }")
fi

# The workspace-wide parts, which need root. The dev user's passwordless sudo
# is part of the image's contract.
if [ -n "$time_zone" ] && [ "$(readlink /etc/localtime || true)" != "/usr/share/zoneinfo/$time_zone" ]; then
    sudo -n ln -sfn "/usr/share/zoneinfo/$time_zone" /etc/localtime
    printf '%s\n' "$time_zone" | sudo -n tee /etc/timezone >/dev/null
fi
if [ -n "$locale_name" ]; then
    # Not grep -q: under pipefail, locale -a can die of SIGPIPE when it quits.
    if ! locale -a | grep -x "$locale_name.utf8" >/dev/null; then
        sudo -n locale-gen "$locale_name.UTF-8" >/dev/null
    fi
    formats_file="$HOME/.config/devgrail/desktop-locale"
    mkdir -p "$(dirname "$formats_file")"
    printf '%s\n' "$locale_name" > "$formats_file"
fi

# The session's parts, over its private D-Bus, as in libexec/scale.
bus_file="$HOME/.cache/devgrail/desktop-bus"
: "${DISPLAY:?the app sets DISPLAY}"
ready=''
for _ in $(seq 1 30); do
    if [ -s "$bus_file" ]; then
        DBUS_SESSION_BUS_ADDRESS="$(cat "$bus_file")"
        export DBUS_SESSION_BUS_ADDRESS
        if xfconf-query -c xsettings -p /Net/ThemeName >/dev/null 2>&1; then
            ready=1
            break
        fi
    fi
    sleep 0.5
done
if [ -z "$ready" ]; then
    echo 'the desktop session is not running' >&2
    exit 3
fi

if [ -n "$color_scheme" ]; then
    theme="$(xfconf-query -c xsettings -p /Net/ThemeName)"
    base="${theme%-dark}"
    wanted="$base"
    [ "$color_scheme" = light ] || wanted="$base-dark"
    if [ "$wanted" != "$theme" ] && { [ -d "/usr/share/themes/$wanted" ] || [ -d "$HOME/.themes/$wanted" ]; }; then
        xfconf-query -c xsettings -p /Net/ThemeName -s "$wanted"
    fi
    dark=false
    [ "$color_scheme" = light ] || dark=true
    xfconf-query -c xfce4-panel -p /panels/dark-mode -n -t bool -s "$dark"
fi

if [ -n "$time_zone" ] || [ -n "$locale_name" ]; then
    clock_format=''
    if [ -n "$locale_name" ]; then
        # The locale's own time format says whether its clock is 12-hour.
        case "$(env -u LC_ALL LANG=C.UTF-8 LC_TIME="$locale_name.UTF-8" locale t_fmt)" in
            *%p* | *%r* | *%I* | *%l*) clock_format='%l:%M %p' ;;
            *) clock_format='%R' ;;
        esac
    fi
    for plugin in $(xfconf-query -c xfce4-panel -l | grep -E '^/plugins/plugin-[0-9]+$'); do
        [ "$(xfconf-query -c xfce4-panel -p "$plugin")" = clock ] || continue
        if [ -n "$time_zone" ]; then
            xfconf-query -c xfce4-panel -p "$plugin/timezone" -n -t string -s "$time_zone"
        fi
        if [ -n "$clock_format" ]; then
            # Only the panel's default and the two this sets: a format the
            # user chose stays.
            current="$(xfconf-query -c xfce4-panel -p "$plugin/digital-format" 2>/dev/null || true)"
            case "$current" in
                '' | '%R' | '%l:%M %p')
                    xfconf-query -c xfce4-panel -p "$plugin/digital-format" -n -t string -s "$clock_format"
                    ;;
            esac
        fi
    done
fi

if [ -n "$keyboard_layout" ]; then
    setxkbmap -layout "$keyboard_layout" -variant "$keyboard_variant"
fi

if [ ${#missing[@]} -gt 0 ]; then
    message="$(printf '%s; ' "${missing[@]}")"
    echo "${message%; }" >&2
    exit 2
fi
