#!/usr/bin/env bash # MacroPad installer. # # curl -fsSL https://raw.githubusercontent.com/Probler-Yt/MacroPad/main/install.sh | bash # # or, from a downloaded copy: ./install.sh # # Safe to run again at any time: it updates in place and keeps your bindings. # Everything goes in your home folder except one small permission file in # /etc/udev/rules.d, which is the only step that asks for your password. set -euo pipefail NEED_REPLUG="" REPO="Probler-Yt/MacroPad" BRANCH="main" DATA="${XDG_DATA_HOME:-$HOME/.local/share}" HOME_DIR="$DATA/macropad" # app + optional private Python APP="$HOME_DIR/app" BIN="$HOME/.local/bin" DESKTOP="$DATA/applications/macropad.desktop" ICON="$DATA/icons/hicolor/scalable/apps/macropad.svg" RULE="/etc/udev/rules.d/60-macropad.rules" RULE_LINE='KERNEL=="hidraw*", ATTRS{idVendor}=="1189", ATTRS{idProduct}=="8840", TAG+="uaccess"' if [ -t 1 ]; then O=$'\e[38;5;208m'; G=$'\e[32m'; R=$'\e[31m'; B=$'\e[1m'; D=$'\e[2m'; X=$'\e[0m' else O=""; G=""; R=""; B=""; D=""; X="" fi say() { printf '%s\n' "$*"; } step() { printf '\n%s==>%s %s%s%s\n' "$O" "$X" "$B" "$*" "$X"; } ok() { printf ' %sok%s %s\n' "$G" "$X" "$*"; } die() { printf '\n%sStopped:%s %s\n' "$R" "$X" "$*" >&2; exit 1; } # When piped from curl, stdin is this script, so questions read the terminal. ask() { local answer="" if { exec 3/dev/null; then read -r -p " $1 [Y/n] " answer <&3 || true exec 3<&- fi case "$answer" in [nN]*) return 1 ;; *) return 0 ;; esac } cat </dev/null || die "Python 3 isn't installed. Install it with your distro's software centre, then run this again." python3 -c 'import sys; sys.exit(sys.version_info < (3, 9))' \ || die "Python 3.9 or newer is needed. You have $(python3 -V)." . /etc/os-release 2>/dev/null || true FAMILY=" ${ID:-} ${ID_LIKE:-} " # -------------------------------------------------------------- 1. files step "Getting the app" SRC="" HERE="$(cd "$(dirname "${BASH_SOURCE[0]:-.}")" 2>/dev/null && pwd || true)" if [ -n "$HERE" ] && [ -d "$HERE/macropad_gui" ] && [ -f "$HERE/macropad.py" ]; then SRC="$HERE" ok "using the copy in $SRC" else command -v curl >/dev/null || die "curl is needed to download the app." TMP="$(mktemp -d)" trap 'rm -rf "$TMP"' EXIT curl -fsSL "https://github.com/${REPO}/archive/refs/heads/${BRANCH}.tar.gz" \ | tar -xz -C "$TMP" || die "Couldn't download the app. Check your internet connection." SRC="$(find "$TMP" -mindepth 1 -maxdepth 1 -type d | head -n1)" [ -d "$SRC/macropad_gui" ] || die "The download didn't contain the app. Please report this at github.com/${REPO}/issues" ok "downloaded the latest version" fi mkdir -p "$HOME_DIR" rm -rf "$APP.new" mkdir -p "$APP.new" for f in macropad.py macropad-probe.py macropad-capture.py macropad-handshake.py \ macropad_gui packaging install.sh uninstall.sh LICENSE README.md; do if [ -e "$SRC/$f" ]; then cp -r "$SRC/$f" "$APP.new/"; fi done find "$APP.new" -name '__pycache__' -type d -prune -exec rm -rf {} + rm -rf "$APP.old" if [ -d "$APP" ]; then mv "$APP" "$APP.old"; fi mv "$APP.new" "$APP" rm -rf "$APP.old" ok "installed to $APP" # -------------------------------------------------------------- 2. Qt step "Checking for Qt, the toolkit that draws the window" PY="" if python3 -c 'import PySide6.QtWidgets' 2>/dev/null; then PY="$(command -v python3)" ok "already installed" elif command -v pacman >/dev/null && [[ "$FAMILY" == *" arch "* ]] && [ ! -e /run/ostree-booted ]; then say " Your system can install it directly (package: pyside6)." if ask "Install it now? This asks for your password."; then sudo pacman -S --needed --noconfirm pyside6 \ && python3 -c 'import PySide6.QtWidgets' && PY="$(command -v python3)" \ && ok "installed pyside6" fi fi if [ -z "$PY" ]; then # Anything else gets a private copy, so nothing system-wide changes. say " Setting up a private copy of Qt just for this app (about 100 MB, one time)." VENV="$HOME_DIR/venv" if [ ! -x "$VENV/bin/python" ]; then if ! python3 -m venv "$VENV" 2>/dev/null; then rm -rf "$VENV" if command -v apt-get >/dev/null; then say " Python needs its 'venv' add-on first. This asks for your password." sudo apt-get install -y python3-venv >/dev/null python3 -m venv "$VENV" || die "Couldn't create the private Python." else die "Couldn't create a private Python. Install your distro's python3-venv package and run this again." fi fi fi "$VENV/bin/python" -m pip install --quiet --upgrade pip "$VENV/bin/python" -m pip install --quiet --upgrade PySide6-Essentials \ || die "Couldn't download Qt. Check your internet connection and run this again." PY="$VENV/bin/python" ok "Qt ready" # Qt's X11 backend needs one small system library some distros leave out. if [ "${XDG_SESSION_TYPE:-}" = "x11" ]; then if command -v apt-get >/dev/null && ! ldconfig -p | grep -q libxcb-cursor.so.0; then sudo apt-get install -y libxcb-cursor0 >/dev/null && ok "added libxcb-cursor0" elif command -v dnf >/dev/null && [ ! -e /run/ostree-booted ] && ! ldconfig -p | grep -q libxcb-cursor.so.0; then sudo dnf install -y xcb-util-cursor >/dev/null && ok "added xcb-util-cursor" fi fi fi # -------------------------------------------------------------- 3. launcher step "Adding MacroPad to your app menu" mkdir -p "$BIN" "$(dirname "$DESKTOP")" "$(dirname "$ICON")" cat > "$BIN/macropad" < "$DESKTOP" </dev/null && update-desktop-database -q "$(dirname "$DESKTOP")" 2>/dev/null || true command -v kbuildsycoca6 >/dev/null && kbuildsycoca6 >/dev/null 2>&1 || true ok "MacroPad is in your app menu" # -------------------------------------------------------------- 4. permission step "Letting the app talk to the pad" if [ -f "$RULE" ] && grep -qF "$RULE_LINE" "$RULE"; then ok "permission rule already in place" else say " Linux doesn't let apps write to USB devices by default. This adds one" say " small rule so MacroPad can, for whoever is logged in at the computer." say " ${D}It writes: $RULE${X}" if ask "Add it now? This asks for your password."; then sudo mkdir -p "$(dirname "$RULE")" printf '%s\n' "# MacroPad: let the logged-in user configure 1189:8840 macro pads." \ "# Must sort before 73-seat-late.rules, which is where uaccess takes effect." \ "$RULE_LINE" | sudo tee "$RULE" >/dev/null if command -v udevadm >/dev/null; then sudo udevadm control --reload-rules || true sudo udevadm trigger --subsystem-match=hidraw || true fi ok "rule added" NEED_REPLUG=1 else say " Skipped. The app will show you how to add it later." fi fi # A rule named 99-something (or with no number) is read too late to work. for f in /etc/udev/rules.d/*.rules; do [ -e "$f" ] || continue [ "$f" = "$RULE" ] && continue if grep -qiE 'idVendor\}=="1189"' "$f" 2>/dev/null && grep -q uaccess "$f"; then base="$(basename "$f")" if [[ ! "$base" < "73-seat-late.rules" ]]; then say " ${D}Note: $f also mentions this pad but is read too late to do" say " anything. It's harmless. Remove it with: sudo rm $f${X}" fi fi done # -------------------------------------------------------------- done printf '\n%s%sAll done.%s\n\n' "$G" "$B" "$X" n=1 if [ -n "$NEED_REPLUG" ]; then say " ${B}$n.${X} Unplug your macro pad and plug it back in." n=$((n + 1)) fi say " ${B}$n.${X} Open ${B}MacroPad${X} from your app menu." say " ${D}(or type${X} macropad ${D}in a terminal)${X}" say "" say " To update later, run the same command again." say " To remove it: ${D}$APP/uninstall.sh${X}" say "" case ":$PATH:" in *":$BIN:"*) ;; *) say " ${D}Tip: $BIN isn't on your PATH, so the 'macropad' command won't work" say " in a terminal yet. The app menu entry works regardless.${X}" say "" ;; esac