#!/usr/bin/env bash # Shared setup flow for Zenbook Duo desktop backends. # Desktop-specific wrappers declare only their backend dependency deltas and then # call run_duo_setup. DUO_COMMON_DNF_PACKAGES=(usbutils iio-sensor-proxy systemd) DUO_COMMON_APT_PACKAGES=(usbutils iio-sensor-proxy systemd) DUO_COMMON_PACMAN_PACKAGES=(usbutils iio-sensor-proxy systemd) DUO_COMMON_MANUAL_DEPENDENCIES_HINT="usbutils, iio-sensor-proxy, systemd" # Rust runtime and UI share one Tauri crate; build these before install-rust-runtime.sh. DUO_COMMON_DNF_BUILD_PACKAGES=( gcc gcc-c++ make pkgconf-pkg-config openssl-devel gtk3-devel webkit2gtk4.1-devel librsvg2-devel ) DUO_COMMON_APT_BUILD_PACKAGES=( build-essential pkg-config libssl-dev libglib2.0-dev libgtk-3-dev libwebkit2gtk-4.1-dev librsvg2-dev ) DUO_COMMON_PACMAN_BUILD_PACKAGES=( base-devel pkgconf openssl gtk3 webkit2gtk-4.1 librsvg ) # Keep these defaults aligned with DuoSettings::default in the Rust runtime and # DEFAULT_DUO_SETTINGS in the frontend. DUO_DEFAULT_BACKLIGHT=0 DUO_DEFAULT_SCALE=1.66 DUO_DEFAULT_USB_MEDIA_REMAP_ENABLED=true DUO_DEFAULT_START_ON_BOOT_MINIMIZED=false DUO_INSTALLER_MARKS_SETUP_COMPLETED=true DUO_SUDOERS_DROPIN_PATH="/etc/sudoers.d/zenbook-duo" build_duo_setup_package_lists() { DNF_DESKTOP_PACKAGES=("${DNF_DESKTOP_PACKAGES[@]:-}") APT_DESKTOP_PACKAGES=("${APT_DESKTOP_PACKAGES[@]:-}") PACMAN_DESKTOP_PACKAGES=("${PACMAN_DESKTOP_PACKAGES[@]:-}") DNF_PACKAGES=("${DUO_COMMON_DNF_PACKAGES[@]}" "${DNF_DESKTOP_PACKAGES[@]}") APT_PACKAGES=("${DUO_COMMON_APT_PACKAGES[@]}" "${APT_DESKTOP_PACKAGES[@]}") PACMAN_PACKAGES=("${DUO_COMMON_PACMAN_PACKAGES[@]}" "${PACMAN_DESKTOP_PACKAGES[@]}") if [ -n "${MANUAL_DESKTOP_DEPENDENCIES_HINT:-}" ]; then MANUAL_DEPENDENCIES_HINT="${DUO_COMMON_MANUAL_DEPENDENCIES_HINT}, ${MANUAL_DESKTOP_DEPENDENCIES_HINT}" else MANUAL_DEPENDENCIES_HINT="${DUO_COMMON_MANUAL_DEPENDENCIES_HINT}" fi } run_duo_setup() { : "${SETUP_SCRIPT_NAME:?SETUP_SCRIPT_NAME is required}" DEFAULT_BACKLIGHT="${DEFAULT_BACKLIGHT:-${DUO_DEFAULT_BACKLIGHT}}" DEFAULT_SCALE="${DEFAULT_SCALE:-${DUO_DEFAULT_SCALE}}" USB_MEDIA_REMAP_ENABLED="${USB_MEDIA_REMAP_ENABLED:-${DUO_DEFAULT_USB_MEDIA_REMAP_ENABLED}}" START_ON_BOOT_MINIMIZED="${START_ON_BOOT_MINIMIZED:-${DUO_DEFAULT_START_ON_BOOT_MINIMIZED}}" build_duo_setup_package_lists parse_duo_setup_args "$@" resolve_duo_target_user migrate_duo_sudoers prompt_duo_defaults install_duo_dependencies install_duo_build_dependencies configure_duo_sudoers configure_duo_input_access write_duo_settings_defaults install_duo_rust_runtime echo "Install complete." } parse_duo_setup_args() { while [ "$#" -gt 0 ]; do case "$1" in --usb-media-remap) USB_MEDIA_REMAP_ENABLED=true shift ;; --no-usb-media-remap) USB_MEDIA_REMAP_ENABLED=false shift ;; *) # Unknown flag/arg - ignore to keep backwards compatibility. shift ;; esac done } resolve_duo_target_user() { TARGET_USER="${USER:-}" if [ -z "${TARGET_USER}" ]; then TARGET_USER="$(id -un 2>/dev/null || true)" fi if [ "${EUID}" = "0" ]; then if [ -n "${SUDO_USER:-}" ] && [ "${SUDO_USER}" != "root" ]; then TARGET_USER="${SUDO_USER}" else echo "ERROR: ${SETUP_SCRIPT_NAME} must be run from a real user session." echo "Run: ./${SETUP_SCRIPT_NAME}" exit 1 fi fi TARGET_UID="$(id -u "${TARGET_USER}" 2>/dev/null || true)" TARGET_HOME="$(getent passwd "${TARGET_USER}" 2>/dev/null | cut -d: -f6)" if [ -z "${TARGET_UID}" ] || [ -z "${TARGET_HOME}" ]; then echo "ERROR: failed to resolve TARGET_USER=${TARGET_USER}" exit 1 fi } duo_prompt() { local prompt="$1" local reply_var="$2" local value="" if [ -r /dev/tty ]; then read -r -p "${prompt}" value /dev/null 2>&1; then sudo dnf install -y "${DNF_PACKAGES[@]}" elif command -v apt >/dev/null 2>&1; then sudo apt install -y "${APT_PACKAGES[@]}" elif command -v pacman >/dev/null 2>&1; then sudo pacman -S --needed --noconfirm "${PACMAN_PACKAGES[@]}" else echo "Unsupported package manager. Please install dependencies manually:" echo " ${MANUAL_DEPENDENCIES_HINT}" exit 1 fi } install_duo_build_dependencies() { echo "Installing Rust/Tauri build dependencies..." if command -v dnf >/dev/null 2>&1; then sudo dnf install -y "${DUO_COMMON_DNF_BUILD_PACKAGES[@]}" sudo dnf install -y libappindicator-gtk3-devel \ || sudo dnf install -y libappindicator3-devel \ || sudo dnf install -y ayatana-appindicator3-devel \ || echo "WARN: Could not install an AppIndicator *-devel package." >&2 elif command -v apt >/dev/null 2>&1; then sudo apt install -y "${DUO_COMMON_APT_BUILD_PACKAGES[@]}" sudo apt install -y libayatana-appindicator3-dev \ || sudo apt install -y libappindicator3-dev \ || echo "WARN: Could not install an AppIndicator *-dev package." >&2 elif command -v pacman >/dev/null 2>&1; then sudo pacman -S --needed --noconfirm "${DUO_COMMON_PACMAN_BUILD_PACKAGES[@]}" sudo pacman -S --needed --noconfirm libayatana-appindicator \ || echo "WARN: Could not install libayatana-appindicator." >&2 else echo "WARN: Could not install build dependencies automatically." >&2 echo "Install GTK/WebKit dev packages manually before the Rust runtime build." >&2 fi } remove_legacy_duo_sudoers_entry() { if sudo grep -q "/usr/bin/tee /sys/class/backlight/.*/brightness" /etc/sudoers 2>/dev/null; then sudo sed -i '\|/usr/bin/tee /sys/class/backlight/.*/brightness|d' /etc/sudoers fi } write_duo_sudoers_dropin() { if [ -z "${TARGET_USER}" ]; then return 0 fi local backlight_entries=() local backlight_dir for backlight_dir in /sys/class/backlight/*; do [ -e "${backlight_dir}/brightness" ] || continue backlight_entries+=("${TARGET_USER} ALL=NOPASSWD:/usr/bin/tee ${backlight_dir}/brightness") done if [ "${#backlight_entries[@]}" -eq 0 ]; then echo "WARN: No backlight brightness paths found; skipping sudoers drop-in." >&2 return 0 fi { echo "# Managed by Zenbook Duo installer." printf '%s\n' "${backlight_entries[@]}" } | sudo tee "${DUO_SUDOERS_DROPIN_PATH}" >/dev/null sudo chmod 0440 "${DUO_SUDOERS_DROPIN_PATH}" } migrate_duo_sudoers() { remove_legacy_duo_sudoers_entry } configure_duo_sudoers() { write_duo_sudoers_dropin } configure_duo_input_access() { if [ -n "${TARGET_USER}" ] && ! groups "${TARGET_USER}" | grep -q '\binput\b'; then sudo usermod -aG input "${TARGET_USER}" echo "Added ${TARGET_USER} to input group (logout/login required for full effect)" fi echo 'SUBSYSTEM=="input", ATTRS{name}=="*ASUS Zenbook Duo Keyboard", TAG+="uaccess", MODE="0660", GROUP="input"' | sudo tee /etc/udev/rules.d/90-zenbook-duo-keyboard.rules >/dev/null # Do not install a hwdb remap for KEYBOARD_KEY_7003*: it overrides the USB Fn layer. sudo rm -f /etc/udev/hwdb.d/90-zenbook-duo-keyboard.hwdb sudo systemd-hwdb update sudo udevadm trigger } write_duo_settings_defaults() { local python3="" python3="$(command -v python3 || true)" [ -n "${python3}" ] || return 0 [ -n "${TARGET_HOME}" ] || return 0 local config_dir="${TARGET_HOME}/.config/zenbook-duo" local settings_file="${config_dir}/settings.json" local start_on_boot_minimized="${START_ON_BOOT_MINIMIZED:-${DUO_DEFAULT_START_ON_BOOT_MINIMIZED}}" local python_cmd=("${python3}") if [ "${TARGET_USER}" != "${USER:-}" ] || [ "${EUID}" = "0" ]; then sudo -u "${TARGET_USER}" mkdir -p "${config_dir}" 2>/dev/null || true python_cmd=(sudo -u "${TARGET_USER}" "${python3}") else mkdir -p "${config_dir}" 2>/dev/null || true fi "${python_cmd[@]}" - "${settings_file}" "${DEFAULT_BACKLIGHT}" "${DEFAULT_SCALE}" "${USB_MEDIA_REMAP_ENABLED}" "${start_on_boot_minimized}" "${DUO_INSTALLER_MARKS_SETUP_COMPLETED}" <<'PY' import json import sys from pathlib import Path settings_file = Path(sys.argv[1]) default_backlight = int(sys.argv[2]) default_scale = float(sys.argv[3]) usb_media_remap_enabled = sys.argv[4].strip().lower() in ("1", "true", "yes", "y", "on") start_on_boot_minimized = sys.argv[5].strip().lower() in ("1", "true", "yes", "y", "on") setup_completed = sys.argv[6].strip().lower() in ("1", "true", "yes", "y", "on") settings_file.parent.mkdir(parents=True, exist_ok=True) data = {} if settings_file.exists(): try: loaded = json.loads(settings_file.read_text()) if isinstance(loaded, dict): data = loaded except Exception: data = {} data.setdefault("autoDualScreen", True) data.setdefault("syncBrightness", True) data.setdefault("theme", "system") data.setdefault("invertSensorRotation", False) data["defaultBacklight"] = default_backlight data["defaultScale"] = default_scale data["usbMediaRemapEnabled"] = usb_media_remap_enabled data["startOnBootMinimized"] = start_on_boot_minimized data["setupCompleted"] = setup_completed settings_file.write_text(json.dumps(data, indent=2) + "\n") PY } install_duo_rust_runtime() { local args=() case "${DUO_RUNTIME_MODE:-}" in build-only) args+=(--build-only) echo "Building Rust runtime..." ;; install-only) args+=(--install-only) echo "Installing Rust runtime..." ;; *) echo "Building and installing Rust runtime..." ;; esac "${DUO_SETUP_DIR}/install-rust-runtime.sh" "${args[@]}" }