adding mac support mainly

This commit is contained in:
lwoodard
2026-04-13 08:11:19 -06:00
parent 9c681e482e
commit 0f5a136764
9 changed files with 657 additions and 190 deletions

View File

@@ -1,15 +1,28 @@
#!/usr/bin/env bash
# streamdeck-go installer
# streamdeck-go installer — Linux and macOS
# Handles dotfiles-aware installation with interactive prompts.
set -euo pipefail
BINARY="streamdeck-go"
BIN_DIR="${HOME}/.local/bin"
SYSTEMD_USER="${HOME}/.config/systemd/user"
UDEV_RULE="/etc/udev/rules.d/99-streamdeck.rules"
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'
@@ -46,7 +59,6 @@ prompt_yn() {
}
prompt_input() {
# echo-ne goes to /dev/tty so it isn't captured when called inside $()
local msg="$1" default="$2"
echo -ne " ${ARROW} ${msg} ${DIM}[${default}]${NC}: " >/dev/tty
read -r _input </dev/tty
@@ -73,7 +85,6 @@ pick_directory() {
) || result=""
fi
# fzf not available or ctrl-c pressed — fall back to plain text input
if [[ -z "$result" ]]; then
echo -ne " ${ARROW} Path to dotfiles repo ${DIM}[${default}]${NC}: " >/dev/tty
read -r result </dev/tty
@@ -84,33 +95,59 @@ pick_directory() {
}
abspath() {
# Expand ~, resolve to absolute path without requiring it to exist yet.
local p="${1/#\~/$HOME}"
echo "$p"
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 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"
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
}
# Returns 0 if hidapi dev headers are present (needed for cgo build).
hidapi_present() {
pkg-config --exists hidapi-hidraw 2>/dev/null ||
pkg-config --exists hidapi 2>/dev/null ||
ldconfig -p 2>/dev/null | grep -q libhidapi
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[@]}" ;;
@@ -137,6 +174,7 @@ if ! command -v go &>/dev/null; then
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 ;;
@@ -151,20 +189,25 @@ else
ok "Go found: $(go version | awk '{print $3}')"
fi
# hidapi (C library + dev headers required for cgo)
# 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."
warn " Arch: sudo pacman -S hidapi"
warn " Debian: sudo apt install libhidapi-dev libhidapi-hidraw0"
warn " Fedora: sudo dnf install hidapi-devel"
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 ;;
@@ -188,26 +231,37 @@ else
fi
nl
# ── 3. udev rule ───────────────────────────────────────────────────────────────
step "Checking udev rule..."
if [[ -f "${UDEV_RULE}" ]]; then
ok "udev rule already installed — skipping"
# ── 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
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"
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 -Dm755 "${BINARY}" "${BIN_DIR}/${BINARY}"
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 ────────────────────────────────────────────────────────────────
@@ -231,7 +285,7 @@ if prompt_yn "Use a dotfiles directory?" "y"; then
warn "Directory ${DOTFILES} does not exist — it will be created."
fi
CONFIG_DIR="${DOTFILES}/streamdeck-go/.config/streamdeck-go"
CONFIG_DIR="${DOTFILES}/.config/streamdeck-go"
SYMLINK_TARGET="${XDG_CONFIG}/streamdeck-go"
nl
@@ -268,7 +322,7 @@ mkdir -p "${CONFIG_DIR}/icons"
ok "Created ${CONFIG_DIR}/icons/"
if [[ ! -f "${CONFIG_DIR}/config.yaml" ]]; then
install -Dm644 config.example.yaml "${CONFIG_DIR}/config.yaml"
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"
@@ -299,7 +353,7 @@ if [[ "${USE_DOTFILES}" == "true" ]]; then
SYMLINK_TARGET="${XDG_CONFIG}/streamdeck-go"
if [[ -L "${SYMLINK_TARGET}" ]]; then
existing="$(readlink -f "${SYMLINK_TARGET}")"
existing="$(realpath_portable "${SYMLINK_TARGET}")"
if [[ "${existing}" == "${CONFIG_DIR}" ]]; then
ok "Symlink already correct — skipping"
else
@@ -323,14 +377,30 @@ if [[ "${USE_DOTFILES}" == "true" ]]; then
fi
fi
# ── 8. Systemd user service ────────────────────────────────────────────────────
# ── 8. Service ─────────────────────────────────────────────────────────────────
nl
step "Installing systemd user service..."
mkdir -p "${SYSTEMD_USER}"
install -Dm644 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"
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
@@ -352,5 +422,9 @@ nl
info "Edit and save config.yaml — the deck reloads automatically."
info "For privileged commands (suspend, reboot, etc): ${BOLD}make install-helper${NC}"
nl
info "Logs: ${DIM}journalctl --user -u streamdeck-go -f${NC}"
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