From b428f4861a9fce66da4707b05a740598f90ac889 Mon Sep 17 00:00:00 2001 From: Levi Woodard Date: Sun, 15 Mar 2026 10:36:44 -0600 Subject: [PATCH] Fix HID timeout/EINTR errors and relative icon paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Treat "timeout" and "interrupted system call" from ReadWithTimeout as non-fatal — hidraw on Linux returns errors instead of (0, nil) for both, causing false device-dead detection and reconnect loops - Resolve icons_dir relative to the config file when the path is not absolute, so the service finds icons regardless of working directory - Installer now copies bundled icons to the config dir on first install --- install.sh | 53 +++++++++++++++++++++++++++++++++-- internal/config/config.go | 6 ++++ internal/device/streamdeck.go | 7 +++++ 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/install.sh b/install.sh index be5a5bf..9c040e8 100755 --- a/install.sh +++ b/install.sh @@ -46,12 +46,43 @@ 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}: " + echo -ne " ${ARROW} ${msg} ${DIM}[${default}]${NC}: " >/dev/tty read -r _input /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 + + # 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 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 diff --git a/internal/config/config.go b/internal/config/config.go index 2c30680..70be297 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -49,5 +49,11 @@ func Load(path string) (*Config, error) { return nil, fmt.Errorf("parse config: %w", err) } + // Resolve relative icons_dir against the config file's directory so the + // binary works regardless of the working directory (e.g. as a systemd service). + if !filepath.IsAbs(cfg.IconsDir) { + cfg.IconsDir = filepath.Join(filepath.Dir(path), cfg.IconsDir) + } + return cfg, nil } diff --git a/internal/device/streamdeck.go b/internal/device/streamdeck.go index 658ee7d..922d764 100644 --- a/internal/device/streamdeck.go +++ b/internal/device/streamdeck.go @@ -7,6 +7,7 @@ import ( "image" "image/jpeg" _ "image/png" + "strings" "sync" "github.com/sstallion/go-hid" @@ -152,6 +153,12 @@ func (sd *StreamDeck) ReadButtons() ([]bool, error) { data := make([]byte, readReportSize) n, err := sd.dev.ReadWithTimeout(data, 250) if err != nil { + // hidraw on Linux returns errors rather than (0, nil) for non-fatal + // conditions: timeout waiting for data, or EINTR (signal interrupted). + msg := strings.ToLower(err.Error()) + if strings.Contains(msg, "timeout") || strings.Contains(msg, "interrupted") { + return nil, nil + } return nil, err } if n == 0 {