Files
streamdeck-go/install.sh

505 lines
19 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# streamdeck-go installer — Linux and macOS
# Handles dotfiles-aware installation with interactive prompts.
set -euo pipefail
BINARY="streamdeck-go"
XDG_CONFIG="${HOME}/.config"
DEFAULT_DOTFILES="${HOME}/dotfiles"
# ── OS detection ───────────────────────────────────────────────────────────────
OS="$(uname -s)"
IS_MAC=false
[[ "$OS" == "Darwin" ]] && IS_MAC=true
if $IS_MAC; then
BIN_DIR="${HOME}/go/bin"
LAUNCHAGENTS_DIR="${HOME}/Library/LaunchAgents"
PLIST_LABEL="com.woodarddigital.streamdeck-go"
PLIST_DST="${LAUNCHAGENTS_DIR}/${PLIST_LABEL}.plist"
else
BIN_DIR="${HOME}/.local/bin"
SYSTEMD_USER="${HOME}/.config/systemd/user"
UDEV_RULE="/etc/udev/rules.d/99-streamdeck.rules"
fi
# ── Colours ────────────────────────────────────────────────────────────────────
if [[ -t 1 ]]; then
BOLD='\033[1m'; DIM='\033[2m'
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
BLUE='\033[0;34m'; CYAN='\033[0;36m'; MAGENTA='\033[0;35m'
NC='\033[0m'
else
BOLD=''; DIM=''; RED=''; GREEN=''; YELLOW=''
BLUE=''; CYAN=''; MAGENTA=''; NC=''
fi
CHECK="${GREEN}${NC}"
CROSS="${RED}${NC}"
ARROW="${CYAN}${NC}"
WARN="${YELLOW}${NC}"
DOT="${DIM}·${NC}"
# ── Helpers ────────────────────────────────────────────────────────────────────
nl() { echo ""; }
step() { echo -e " ${ARROW} $1"; }
ok() { echo -e " ${CHECK} $1"; }
warn() { echo -e " ${WARN} $1"; }
info() { echo -e " ${DOT} $1"; }
error() { echo -e " ${CROSS} ${RED}$1${NC}"; }
dim() { echo -e " ${DIM}$1${NC}"; }
prompt_yn() {
local msg="$1" default="${2:-y}"
local opts; [[ $default == "y" ]] && opts="Y/n" || opts="y/N"
echo -ne " ${ARROW} ${msg} ${DIM}[${opts}]${NC} "
read -r _answer </dev/tty
_answer="${_answer:-$default}"
[[ $_answer =~ ^[Yy] ]]
}
prompt_input() {
local msg="$1" default="$2"
echo -ne " ${ARROW} ${msg} ${DIM}[${default}]${NC}: " >/dev/tty
read -r _input </dev/tty
echo "${_input:-$default}"
}
pick_directory() {
local default="$1"
local result=""
if command -v fzf &>/dev/null; then
echo -e "\n ${DIM}arrow keys to navigate · enter to select · ctrl-c to type path manually${NC}\n" >/dev/tty
result=$(
find "$HOME" -maxdepth 4 -type d 2>/dev/null | sort |
fzf --height=50% \
--reverse \
--border=rounded \
--prompt=" " \
--header=" Select dotfiles directory" \
--preview="ls -1 {} 2>/dev/null | head -30" \
--preview-window="right:35%:border-left" \
--query="dotfiles" \
--bind="ctrl-c:abort"
) || result=""
fi
if [[ -z "$result" ]]; then
echo -ne " ${ARROW} Path to dotfiles repo ${DIM}[${default}]${NC}: " >/dev/tty
read -r result </dev/tty
result="${result:-$default}"
fi
echo "$result"
}
abspath() {
echo "${1/#\~/$HOME}"
}
# Portable realpath: macOS ships without readlink -f unless coreutils is installed.
realpath_portable() {
local path="$1"
if command -v realpath &>/dev/null; then
realpath "$path"
elif command -v python3 &>/dev/null; then
python3 -c "import os,sys; print(os.path.realpath(sys.argv[1]))" "$path"
else
echo "${path/#\~/$HOME}"
fi
}
# Portable install with parent dir creation.
# macOS install(1) lacks -D; use explicit mkdir -p instead.
install_file() {
local mode="$1" src="$2" dst="$3"
mkdir -p "$(dirname "$dst")"
install -m "$mode" "$src" "$dst"
}
# ── Dependency detection ───────────────────────────────────────────────────────
detect_pkg_manager() {
if $IS_MAC; then echo "brew"
elif command -v pacman &>/dev/null; then echo "pacman"
elif command -v apt &>/dev/null; then echo "apt"
elif command -v dnf &>/dev/null; then echo "dnf"
elif command -v zypper &>/dev/null; then echo "zypper"
else echo "unknown"
fi
}
hidapi_present() {
if $IS_MAC; then
# On macOS hidapi is a formula; check pkg-config or the dylib directly.
pkg-config --exists hidapi 2>/dev/null ||
[ -f /usr/local/lib/libhidapi.dylib ] ||
[ -f /opt/homebrew/lib/libhidapi.dylib ]
else
pkg-config --exists hidapi-hidraw 2>/dev/null ||
pkg-config --exists hidapi 2>/dev/null ||
ldconfig -p 2>/dev/null | grep -q libhidapi
fi
}
install_pkg() {
local pm="$1"; shift
local pkgs=("$@")
case "$pm" in
brew) brew install "${pkgs[@]}" ;;
pacman) sudo pacman -S --needed --noconfirm "${pkgs[@]}" ;;
apt) sudo apt-get install -y "${pkgs[@]}" ;;
dnf) sudo dnf install -y "${pkgs[@]}" ;;
zypper) sudo zypper install -y "${pkgs[@]}" ;;
esac
}
# ── Header ─────────────────────────────────────────────────────────────────────
nl
echo -e " ${BOLD}${CYAN}streamdeck-go${NC} ${DIM}installer${NC}"
echo -e " ${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
nl
# ── 1. System dependencies ─────────────────────────────────────────────────────
step "Checking system dependencies..."
PM="$(detect_pkg_manager)"
# Go
if ! command -v go &>/dev/null; then
warn "Go not found."
if [[ "$PM" == "unknown" ]]; then
error "Could not detect a package manager — install Go manually: https://go.dev/dl/"
exit 1
fi
if prompt_yn "Install Go now?" "y"; then
case "$PM" in
brew) install_pkg brew go ;;
pacman) install_pkg pacman go ;;
apt) install_pkg apt golang-go ;;
dnf) install_pkg dnf golang ;;
zypper) install_pkg zypper go ;;
esac
ok "Go installed: $(go version)"
else
error "Go is required to build — aborting."
exit 1
fi
else
ok "Go found: $(go version | awk '{print $3}')"
fi
# hidapi
if hidapi_present; then
ok "hidapi found"
else
warn "hidapi not found (required for USB HID communication)."
if [[ "$PM" == "unknown" ]]; then
warn "Could not detect a package manager — install hidapi manually, then re-run."
if $IS_MAC; then
warn " macOS: brew install hidapi"
else
warn " Arch: sudo pacman -S hidapi"
warn " Debian: sudo apt install libhidapi-dev libhidapi-hidraw0"
warn " Fedora: sudo dnf install hidapi hidapi-devel"
fi
exit 1
fi
if prompt_yn "Install hidapi now?" "y"; then
case "$PM" in
brew) install_pkg brew hidapi ;;
pacman) install_pkg pacman hidapi ;;
apt) install_pkg apt libhidapi-dev libhidapi-hidraw0 ;;
dnf) install_pkg dnf hidapi hidapi-devel ;;
zypper) install_pkg zypper hidapi-devel ;;
esac
ok "hidapi installed"
else
error "hidapi is required — aborting."
exit 1
fi
fi
# obs-cmd (optional — required for OBS module)
if command -v obs-cmd &>/dev/null; then
ok "obs-cmd found"
else
info "obs-cmd not found (required for OBS Studio module)"
if prompt_yn "Install obs-cmd now?" "y"; then
OBS_CMD_VERSION=$(curl -sL "https://api.github.com/repos/grigio/obs-cmd/releases/latest" | grep '"tag_name"' | head -1 | sed 's/.*"v\(.*\)".*/\1/')
if [[ -z "$OBS_CMD_VERSION" ]]; then
warn "Could not fetch obs-cmd release — install manually: cargo install obs-cmd"
elif $IS_MAC; then
# Detect architecture — Apple Silicon vs Intel
ARCH="$(uname -m)"
if [[ "$ARCH" == "arm64" ]]; then
OBS_CMD_ASSET="obs-cmd-arm64-macos.tar.gz"
else
OBS_CMD_ASSET="obs-cmd-x64-macos.tar.gz"
fi
step "Downloading obs-cmd v${OBS_CMD_VERSION} (${ARCH})..."
curl -sL "https://github.com/grigio/obs-cmd/releases/download/v${OBS_CMD_VERSION}/${OBS_CMD_ASSET}" \
| tar xz -C /tmp
install -m 755 /tmp/obs-cmd /usr/local/bin/obs-cmd
rm -f /tmp/obs-cmd
else
# Linux — x86_64 static binary
step "Downloading obs-cmd v${OBS_CMD_VERSION}..."
curl -sL "https://github.com/grigio/obs-cmd/releases/download/v${OBS_CMD_VERSION}/obs-cmd-x64-linux.tar.gz" \
| tar xz -C /tmp
sudo install -m 755 /tmp/obs-cmd /usr/local/bin/obs-cmd
rm -f /tmp/obs-cmd
fi
if command -v obs-cmd &>/dev/null; then
ok "obs-cmd installed"
else
warn "obs-cmd install may have failed — OBS module won't work until it's available"
fi
else
info "Skipped — OBS module will not work until obs-cmd is installed"
fi
fi
nl
# ── 2. Build ───────────────────────────────────────────────────────────────────
step "Building ${BOLD}${BINARY}${NC}..."
if go build -o "${BINARY}" ./cmd/streamdeck/ 2>&1; then
ok "Build complete"
else
error "Build failed — aborting."
exit 1
fi
nl
# ── 3. Device permissions ──────────────────────────────────────────────────────
if $IS_MAC; then
step "Device permissions (macOS)..."
info "macOS grants HID access via IOKit — no udev rule needed."
info "On first run the OS may show an Input Monitoring prompt; grant access if asked."
ok "No action required"
else
step "Checking udev rule..."
if [[ -f "${UDEV_RULE}" ]]; then
ok "udev rule already installed — skipping"
else
info "Installing udev rule (requires sudo):"
dim " ${UDEV_RULE}"
echo 'KERNEL=="hidraw*", ATTRS{idVendor}=="0fd9", MODE="0666"' \
| sudo tee "${UDEV_RULE}" >/dev/null
sudo udevadm control --reload
sudo udevadm trigger
ok "udev rule installed — device accessible without root"
fi
fi
nl
# ── 4. Binary ──────────────────────────────────────────────────────────────────
step "Installing binary to ${BOLD}${BIN_DIR}/${BINARY}${NC}..."
mkdir -p "${BIN_DIR}"
install -m 755 "${BINARY}" "${BIN_DIR}/${BINARY}"
ok "Binary installed"
if $IS_MAC && [[ ":$PATH:" != *":${BIN_DIR}:"* ]]; then
warn "${BIN_DIR} is not in your PATH — add it to your shell profile:"
dim " export PATH=\"\$HOME/go/bin:\$PATH\""
fi
nl
# ── 5. Dotfiles ────────────────────────────────────────────────────────────────
echo -e " ${BOLD}Config location${NC}"
echo -e " ${DIM}─────────────────────────────────────────────${NC}"
nl
info "streamdeck-go stores its config and icons in a single directory."
info "You can keep that directory inside your dotfiles repo and symlink it"
info "into ~/.config — the same pattern used by Hyprland, Waybar, etc."
nl
USE_DOTFILES=false
CONFIG_DIR=""
if prompt_yn "Use a dotfiles directory?" "y"; then
nl
DOTFILES_RAW="$(pick_directory "${DEFAULT_DOTFILES}")"
DOTFILES="$(abspath "${DOTFILES_RAW}")"
if [[ ! -d "${DOTFILES}" ]]; then
warn "Directory ${DOTFILES} does not exist — it will be created."
fi
CONFIG_DIR="${DOTFILES}/.config/streamdeck-go"
SYMLINK_TARGET="${XDG_CONFIG}/streamdeck-go"
nl
echo -e " ${DIM}─────────────────────────────────────────────${NC}"
info "${BOLD}Will create:${NC}"
dim " ${CONFIG_DIR}/"
dim " ${CONFIG_DIR}/config.yaml ${DIM}(if absent)${NC}"
dim " ${CONFIG_DIR}/icons/"
nl
info "${BOLD}Will symlink:${NC}"
dim " ${SYMLINK_TARGET}"
dim " └─▶ ${CONFIG_DIR}"
echo -e " ${DIM}─────────────────────────────────────────────${NC}"
nl
if ! prompt_yn "Confirm?" "y"; then
nl
warn "Aborted — nothing written."
exit 0
fi
USE_DOTFILES=true
else
nl
CONFIG_DIR="${XDG_CONFIG}/streamdeck-go"
info "Config will go directly in ${CONFIG_DIR}"
fi
nl
# ── 6. Create config directory ─────────────────────────────────────────────────
step "Setting up config directory..."
mkdir -p "${CONFIG_DIR}/icons"
ok "Created ${CONFIG_DIR}/icons/"
if [[ ! -f "${CONFIG_DIR}/config.yaml" ]]; then
install -m 644 config.example.yaml "${CONFIG_DIR}/config.yaml"
ok "Default config written to config.yaml"
else
ok "config.yaml already exists — not overwritten"
fi
# Always install/update modules.yaml — this is a registry of available module
# definitions, not user data. User customisations go in config.yaml (params
# overrides per key). Keeping modules.yaml current ensures new modules (OBS,
# Slack, etc.) are available immediately after upgrade.
install -m 644 modules.example.yaml "${CONFIG_DIR}/modules.yaml"
ok "modules.yaml installed (updated to latest)"
# Copy bundled icons (never overwrite existing ones the user may have customised).
if [[ -d "icons" ]]; then
copied=0
for f in icons/*; do
[[ -f "$f" ]] || continue
dest="${CONFIG_DIR}/icons/$(basename "$f")"
if [[ ! -f "$dest" ]]; then
cp "$f" "$dest"
(( copied++ )) || true
fi
done
if (( copied > 0 )); then
ok "Copied ${copied} bundled icon(s) to ${CONFIG_DIR}/icons/"
else
ok "Bundled icons already present — not overwritten"
fi
fi
# ── 7. Symlink (dotfiles mode only) ───────────────────────────────────────────
if [[ "${USE_DOTFILES}" == "true" ]]; then
nl
step "Creating symlink..."
SYMLINK_TARGET="${XDG_CONFIG}/streamdeck-go"
if [[ -L "${SYMLINK_TARGET}" ]]; then
existing="$(realpath_portable "${SYMLINK_TARGET}")"
if [[ "${existing}" == "${CONFIG_DIR}" ]]; then
ok "Symlink already correct — skipping"
else
warn "Symlink exists but points to ${existing}"
if prompt_yn "Replace it?" "y"; then
rm "${SYMLINK_TARGET}"
ln -s "${CONFIG_DIR}" "${SYMLINK_TARGET}"
ok "Symlink updated → ${CONFIG_DIR}"
else
warn "Symlink not updated — you may need to fix this manually."
fi
fi
elif [[ -d "${SYMLINK_TARGET}" ]]; then
warn "${SYMLINK_TARGET} is a real directory (not a symlink)."
info "Move its contents to ${CONFIG_DIR} first, then re-run install."
warn "Skipping symlink — config will work from ${CONFIG_DIR} but is not linked."
else
mkdir -p "${XDG_CONFIG}"
ln -s "${CONFIG_DIR}" "${SYMLINK_TARGET}"
ok "Symlinked ${SYMLINK_TARGET}${CONFIG_DIR}"
fi
fi
# ── 8. Service ─────────────────────────────────────────────────────────────────
nl
if $IS_MAC; then
step "Installing launchd agent..."
mkdir -p "${LAUNCHAGENTS_DIR}"
LOG_PATH="${HOME}/Library/Logs/streamdeck-go.log"
# Substitute binary path and log path into the plist template.
sed \
-e "s|STREAMDECK_BINARY_PATH|${BIN_DIR}/${BINARY}|g" \
-e "s|STREAMDECK_LOG_PATH|${LOG_PATH}|g" \
launchd/com.woodarddigital.streamdeck-go.plist \
> "${PLIST_DST}"
# Unload first in case an old version is already loaded.
launchctl unload "${PLIST_DST}" 2>/dev/null || true
launchctl load "${PLIST_DST}"
ok "launchd agent loaded — will start at login"
else
step "Installing systemd user service..."
mkdir -p "${SYSTEMD_USER}"
install_file 644 systemd/streamdeck-go.service "${SYSTEMD_USER}/streamdeck-go.service"
systemctl --user daemon-reload
systemctl --user enable --now streamdeck-go.service
ok "Service enabled and started"
fi
# ── 9. Watchdog ────────────────────────────────────────────────────────────────
nl
step "Installing watchdog (USB unplug/replug recovery)..."
if $IS_MAC; then
WATCHDOG_BIN="${BIN_DIR}/streamdeck-go-watchdog"
WATCHDOG_PLIST_LABEL="com.woodarddigital.streamdeck-go-watchdog"
WATCHDOG_PLIST="${LAUNCHAGENTS_DIR}/${WATCHDOG_PLIST_LABEL}.plist"
WATCHDOG_LOG="${HOME}/Library/Logs/streamdeck-go-watchdog.log"
install -m 755 systemd/streamdeck-go-watchdog.sh "${WATCHDOG_BIN}"
sed \
-e "s|STREAMDECK_WATCHDOG_PATH|${WATCHDOG_BIN}|g" \
-e "s|STREAMDECK_WATCHDOG_LOG_PATH|${WATCHDOG_LOG}|g" \
launchd/com.woodarddigital.streamdeck-go-watchdog.plist \
> "${WATCHDOG_PLIST}"
launchctl bootout "gui/$(id -u)/${WATCHDOG_PLIST_LABEL}" 2>/dev/null || true
launchctl bootstrap "gui/$(id -u)" "${WATCHDOG_PLIST}"
ok "Watchdog loaded — fires every 30s"
else
install_file 755 systemd/streamdeck-go-watchdog.sh "${BIN_DIR}/streamdeck-go-watchdog"
install_file 644 systemd/streamdeck-go-watchdog.service "${SYSTEMD_USER}/streamdeck-go-watchdog.service"
install_file 644 systemd/streamdeck-go-watchdog.timer "${SYSTEMD_USER}/streamdeck-go-watchdog.timer"
systemctl --user daemon-reload
systemctl --user enable --now streamdeck-go-watchdog.timer
ok "Watchdog timer enabled — fires every 30s"
fi
# ── Done ───────────────────────────────────────────────────────────────────────
nl
echo -e " ${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e " ${GREEN}${BOLD}All done.${NC}"
nl
if [[ "${USE_DOTFILES}" == "true" ]]; then
info "Your config lives in your dotfiles repo:"
dim " ${CONFIG_DIR}/config.yaml"
dim " ${CONFIG_DIR}/icons/"
else
info "Your config:"
dim " ${CONFIG_DIR}/config.yaml"
dim " ${CONFIG_DIR}/icons/"
fi
nl
info "Edit and save config.yaml — the deck reloads automatically."
info "For privileged commands (suspend, reboot, etc): ${BOLD}make install-helper${NC}"
nl
if $IS_MAC; then
info "Logs: ${DIM}tail -f ~/Library/Logs/streamdeck-go.log${NC}"
else
info "Logs: ${DIM}journalctl --user -u streamdeck-go -f${NC}"
fi
nl