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>
258 lines
11 KiB
Bash
Executable File
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
|