The grigio/obs-cmd v1.0.0 release ships Linux builds as obs-cmd-x64-linux.tar.gz; the old obs-cmd-v-x86_64-unknown-linux-musl.tar.gz name no longer exists and curl was piping a 404 page into tar.
478 lines
17 KiB
Bash
Executable File
478 lines
17 KiB
Bash
Executable File
#!/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
|
||
|
||
# ── 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
|