Files
macropad-itprotech/install.sh
Bjorn Blomberg 80c6a5da1c Add the ITPROTECH MK series (8808) as a third pad family
Confirmed on the MK-09 (8808:660a, 9 keys): the vendor's .NET configurator
was decompiled for the wire format and every frame was then written to a
real pad and read back. Keys, media keys and multikey sequences are
supported; lighting from the command line.

The firmware acts on every other output report, so each report is sent
twice (as the vendor software does) and every write is verified by reading
the key back. The app now keeps one state file per pad and starts with a
known layout for a known USB id, and the editor asks the backend whether a
binding fits before offering to write it. The actions listener and the
udev rule cover every supported vendor.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-23 19:24:30 +02:00

258 lines
11 KiB
Bash
Executable File

#!/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"
# One line per pad family the app supports. CH57x (1189), SayoDevice (8089),
# ITPROTECH MK series (8808). Keep in step with macropad_gui/backends.
RULE_LINES=(
'KERNEL=="hidraw*", ATTRS{idVendor}=="1189", TAG+="uaccess"'
'KERNEL=="hidraw*", ATTRS{idVendor}=="8089", TAG+="uaccess"'
'KERNEL=="hidraw*", ATTRS{idVendor}=="8808", 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/tty; } 2>/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 <<EOF
${O} ┌────────────────┐${X}
${O} │ ◯ ◯ │${X} ${B}MacroPad${X}
${O} │ ▢ ▢ ▢ │${X} Set up your 12 key, 2 dial macro pad on Linux.
${O} │ ▢ ▢ ${X}▣${O} │${X}
${O} └────────────────┘${X} ${D}github.com/${REPO}${X}
EOF
[ "$(id -u)" -ne 0 ] || die "Run this as your normal user, not with sudo. It will ask for your password itself when it needs it."
command -v python3 >/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" <<EOF
#!/bin/sh
# Opens MacroPad. Anything after 'macropad' is passed on, e.g. 'macropad doctor'.
cd "$APP" && exec "$PY" -m macropad_gui "\$@"
EOF
chmod +x "$BIN/macropad"
cp "$APP/packaging/macropad.svg" "$ICON"
cat > "$DESKTOP" <<EOF
[Desktop Entry]
Type=Application
Name=MacroPad
GenericName=Macro pad configurator
Comment=Change what your macro pad's keys and dials do
Exec=$BIN/macropad
Icon=$ICON
Terminal=false
Categories=Utility;Settings;HardwareSettings;
Keywords=macro;keyboard;keypad;knob;dial;shortcut;hid;
StartupWMClass=macropad
EOF
command -v update-desktop-database >/dev/null && update-desktop-database -q "$(dirname "$DESKTOP")" 2>/dev/null || true
command -v kbuildsycoca6 >/dev/null && kbuildsycoca6 >/dev/null 2>&1 || true
# Plasma caches icon lookups; drop the cache so the new icon shows straight away.
rm -f "${XDG_CACHE_HOME:-$HOME/.cache}/icon-cache.kcache" 2>/dev/null || true
ok "MacroPad is in your app menu"
# If the user turned background actions on and they're running, restart them
# so they pick up the new code. Identified by the listener's own lock file and
# checked against /proc, never by matching command lines, which would also hit
# an editor or a grep that happens to mention it.
ACTIONS_LOCK="${XDG_RUNTIME_DIR:-$HOME/.cache}/macropad-actions.lock"
ACTIONS_AUTOSTART="${XDG_CONFIG_HOME:-$HOME/.config}/autostart/macropad-actions.desktop"
if [ -f "$ACTIONS_AUTOSTART" ] && [ -f "$ACTIONS_LOCK" ]; then
pid="$(cat "$ACTIONS_LOCK" 2>/dev/null || true)"
case "$pid" in ''|*[!0-9]*) pid="" ;; esac
if [ -n "$pid" ] && tr '\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null \
| grep -q "macropad.*actions"; then
kill "$pid" 2>/dev/null || true
sleep 0.5
setsid -f "$BIN/macropad" actions \
>>"${XDG_RUNTIME_DIR:-$HOME/.cache}/macropad-actions.log" 2>&1 < /dev/null || true
ok "restarted background actions"
fi
fi
# -------------------------------------------------------------- 4. permission
step "Letting the app talk to the pad"
have_all_rules() {
[ -f "$RULE" ] || return 1
for line in "${RULE_LINES[@]}"; do grep -qF "$line" "$RULE" || return 1; done
}
if have_all_rules; 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 the macro pads it supports." \
"# Must sort before 73-seat-late.rules, which is where uaccess takes effect." \
"${RULE_LINES[@]}" | 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|8089|8808)"' "$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