2026-04-14 05:43:30 -06:00
2026-04-14 05:43:30 -06:00
2026-03-15 10:10:13 -06:00
2026-03-15 10:10:13 -06:00
2026-04-14 05:43:30 -06:00
2026-04-13 08:11:19 -06:00
2026-04-08 20:26:09 -06:00
2026-03-15 10:10:13 -06:00
2026-04-13 07:19:49 -06:00
2026-04-13 12:45:41 -06:00
2026-04-13 12:45:41 -06:00
2026-04-13 08:11:19 -06:00
2026-04-13 08:11:19 -06:00
2026-04-13 12:45:41 -06:00
2026-04-14 05:43:30 -06:00
2026-03-15 10:10:13 -06:00
2026-04-13 12:45:41 -06:00
2026-04-13 12:45:41 -06:00

streamdeck-go

Important

This is a sloppy utility that's designed to just work. 406.fail

A lightweight, dotfile-style controller for the Elgato Stream Deck XL on Linux and macOS. No Elgato software required — communicates directly with the device over USB HID.


Features

  • Configure keys with a single YAML file (designed for dotfiles compatibility)

  • PNG, JPEG, and SVG icons, automatically scaled to key size

  • Animated GIF support — frames pre-encoded at startup, cycled at the GIF's native rate

  • Runs any shell command on key press

  • Status/toggle keys — poll any shell command on an interval, swap icons based on output; icon updates on press

  • Live config reload — save your config and the deck updates instantly, no restart needed

  • Privileged commands — run whitelisted root/admin commands; Linux uses a root helper daemon, macOS uses the native admin auth dialog

  • Automatic reconnect — survives USB unplug, KVM switches, and suspend/resume

  • Runs as a systemd user service (Linux) or launchd agent (macOS), starts automatically at login

  • No Stream Deck app, no Node.js, no Electron

  • Modules — define reusable, parameterised commands in modules.yaml with Go templates; secrets stay in env vars, config stays in dotfiles. First built-in example: Slack (status, presence, snooze). See Modules.

  • Interactive config builder — TUI tool (streamdeck-init) that walks you through key setup: pick a slot, pick a module/function, customize params, choose an icon. No YAML editing required. See Config builder.

Planned: text/label overlays on keys, multi-page layouts, AUR package — see Roadmap


Architecture

streamdeck-go/
├── cmd/
│   ├── streamdeck/
│   │   └── main.go              # Entry point, config watcher, event loop
│   └── streamdeck-helper/
│       └── main.go              # Privileged helper daemon (Linux only)
├── internal/
│   ├── config/
│   │   └── config.go            # YAML parsing with XDG-aware defaults
│   ├── device/
│   │   └── streamdeck.go        # USB HID communication, image encoding, button reads
│   └── modules/
│       └── modules.go           # Module registry, template resolution, env/expiry helpers
├── systemd/
│   └── streamdeck-go.service    # systemd user service (Linux)
├── launchd/
│   └── com.woodarddigital.streamdeck-go.plist  # launchd agent (macOS)
├── config.example.yaml          # Starter config (copied to ~/.config on install)
├── modules.example.yaml         # Example modules file (Slack integration)
├── Makefile                     # build / install / uninstall
├── go.mod
└── go.sum

How it works

~/.config/streamdeck-go/config.yaml
~/.config/streamdeck-go/modules.yaml   (optional)
    │
    ├── fsnotify watcher ──── file saved? ──▶ cancel ctx → reload both → restart run()
    │
    └──▶ run(ctx, registry)
              │
              ├── module resolution ──▶ key has module/function?
              │                            └──▶ registry.Resolve() → rendered shell command
              │                                 (templates expanded, env vars resolved)
              │
              ├── static icon ──▶ device.SetKeyImage()   (scale → flip → JPEG → HID)
              │
              ├── animated GIF ──▶ device.EncodeFrame()  (pre-encode all frames once)
              │                        └──▶ goroutine/key: loop frames, sleep per-frame delay
              │                             (cancelled via ctx on reload)
              │
              ├── poll goroutine/key ──▶ exec poll command every interval
              │                            └──▶ match in stdout? swap icon_true / icon_false
              │                                 (also triggered immediately on button press)
              │
              └── event loop ──▶ device.ReadButtons()    (250 ms timeout, checks ctx)
                                      └──▶ key-down: exec.Command("sh", "-c", command)

HID output reports are mutex-guarded so concurrent animation goroutines never interleave partial image data across keys.


Dependencies

Go packages

Package Purpose
github.com/sstallion/go-hid Bindings for libhidapi — USB HID read/write
github.com/fsnotify/fsnotify Config file watching for live reload
golang.org/x/image Bi-linear image scaling
gopkg.in/yaml.v3 YAML config parsing
github.com/srwiley/oksvg SVG rasterisation
Go stdlib image/gif, image/jpeg, image/png Image decoding and JPEG encoding

System library

libhidapi must be present at runtime:

Platform Command
macOS brew install hidapi
Arch / Manjaro sudo pacman -S hidapi
Debian / Ubuntu sudo apt install libhidapi-hidraw0
Fedora / RHEL sudo dnf install hidapi
openSUSE sudo zypper install libhidapi-hidraw0

Installation

macOS

# Prerequisites
brew install go hidapi

git clone https://github.com/WoodardDigital/streamdeck-go
cd streamdeck-go
make install

The binary installs to ~/go/bin/streamdeck-go. Make sure ~/go/bin is in your PATH:

# Add to ~/.zshrc or ~/.bash_profile if not already present:
export PATH="$HOME/go/bin:$PATH"

A launchd agent is installed to ~/Library/LaunchAgents/ and started automatically. No sudo required for the main binary.

For privileged commands (sleep, reboot, etc.) see Privileged commands — macOS.


An interactive installer that handles everything, including optional dotfiles directory integration.

# Prerequisites — Arch example; adjust for your distro (see table above)
sudo pacman -S go hidapi

git clone https://github.com/WoodardDigital/streamdeck-go
cd streamdeck-go
make install

The installer walks you through the whole setup:

   Building streamdeck-go...
  ✓ Build complete

   Checking udev rule...
  ✓ udev rule already installed — skipping

  Config location
  ─────────────────────────────────────────────

  · streamdeck-go stores its config and icons in a single directory.
  · You can keep that directory inside your dotfiles repo and symlink it
  · into ~/.config — the same pattern used by Hyprland, Waybar, etc.

   Use a dotfiles directory? [Y/n]
   Path to dotfiles repo [~/dotfiles]:

  · Will create:
  ·   ~/dotfiles/.config/streamdeck-go/
  ·   ~/dotfiles/.config/streamdeck-go/config.yaml
  ·   ~/dotfiles/.config/streamdeck-go/icons/

  · Will symlink:
  ·   ~/.config/streamdeck-go
  ·   └─▶ ~/dotfiles/.config/streamdeck-go

   Confirm? [Y/n]

The resulting structure inside your dotfiles repo mirrors everything else in .config:

~/dotfiles/
└── .config/
    ├── hypr/
    ├── waybar/
    └── streamdeck-go/       ← lives here, symlinked to ~/.config/streamdeck-go
        ├── config.yaml
        └── icons/

Linux — manual / dev setup

1. udev rule (one-time):

echo 'KERNEL=="hidraw*", ATTRS{idVendor}=="0fd9", MODE="0666"' \
  | sudo tee /etc/udev/rules.d/99-streamdeck.rules
sudo udevadm control --reload
sudo udevadm trigger

2. Build and run:

git clone https://github.com/WoodardDigital/streamdeck-go
cd streamdeck-go

cp config.example.yaml config.yaml
go run ./cmd/streamdeck/

# Or point at any config file:
go run ./cmd/streamdeck/ -config ~/.config/streamdeck-go/config.yaml

When no -config flag is given the binary checks ~/.config/streamdeck-go/config.yaml first (respecting $XDG_CONFIG_HOME). The repo's config.yaml is gitignored.


AUR (Arch Linux)

AUR package coming soon. Until then, use make install above.


Service management

macOS (launchd)

# Status
launchctl list | grep streamdeck

# Stop / start
launchctl unload ~/Library/LaunchAgents/com.woodarddigital.streamdeck-go.plist
launchctl load   ~/Library/LaunchAgents/com.woodarddigital.streamdeck-go.plist

# Logs (persistent across reboots)
tail -f ~/Library/Logs/streamdeck-go.log

The agent starts automatically at login and restarts if the process exits. Sleep/wake is handled by the reconnect loop in the binary — no extra config needed.

To use a custom config path, edit the installed plist at ~/Library/LaunchAgents/com.woodarddigital.streamdeck-go.plist and add -config /path/to/config.yaml to the ProgramArguments array, then reload.

Linux (systemd)

systemctl --user status streamdeck-go      # check if running
systemctl --user restart streamdeck-go     # restart manually
systemctl --user stop streamdeck-go        # stop
journalctl --user -u streamdeck-go -f      # follow logs

To use a custom config path:

systemctl --user edit streamdeck-go

Add:

[Service]
ExecStart=
ExecStart=%h/.local/bin/streamdeck-go -config %h/.config/streamdeck-go/config.yaml

Configuration

Config lives at ~/.config/streamdeck-go/config.yaml (or wherever -config points). Editing and saving the file reloads the deck live — icons update, animations restart, no service restart required.

icons_dir: ~/.config/streamdeck-go/icons  # default; can be any path
brightness: 70                             # 0100

# USB IDs — defaults match Stream Deck XL v2
# macOS: system_profiler SPUSBDataType | grep -A5 "Stream Deck"
# Linux: lsusb | grep Elgato
device:
  vendor_id: 0x0fd9
  product_id: 0x00ba

# Keys are 0-indexed, left-to-right, top-to-bottom.
# Stream Deck XL layout (8 columns × 4 rows):
#
#  0  1  2  3  4  5  6  7
#  8  9 10 11 12 13 14 15
# 16 17 18 19 20 21 22 23
# 24 25 26 27 28 29 30 31

keys:
  0:
    icon: ghostty.png     # PNG, JPEG, GIF, or SVG — relative to icons_dir
    command: ghostty
  1:
    icon: firefox.png
    command: open -a Firefox    # macOS; use "firefox" on Linux
  8:
    icon: loading.gif     # animated — cycles at the GIF's native frame rate
    command: ""

Status / toggle keys

A key can poll any shell command on an interval and show one of two icons based on the result. Pressing the button runs command as usual, and the icon re-checks ~400 ms later so it reflects the new state immediately.

keys:
  3:
    command: pactl set-source-mute @DEFAULT_SOURCE@ toggle
    icon_true:  mic-muted.png   # shown when poll output contains match
    icon_false: mic-active.png  # shown when poll output does not contain match
    poll:
      command:  pactl get-source-mute @DEFAULT_SOURCE@
      interval: 2s              # how often to check (default: 2s)
      match: "yes"              # substring to find in stdout → true

How matching works:

match set? True condition False condition
Yes stdout contains the string stdout does not contain it
No (omitted) command exits 0 command exits non-zero

macOS examples:

# Mute microphone (macOS — requires SwitchAudioSource or similar)
3:
  command: "osascript -e 'set volume input muted not (input muted of (get volume settings))'"
  icon_true:  mic-muted.png
  icon_false: mic-active.png
  poll:
    command: "osascript -e 'input muted of (get volume settings)'"
    interval: 2s
    match: "true"

# VPN toggle (macOS)
4:
  command: "osascript -e 'tell application \"Tunnelblick\" to connect \"My VPN\"'"
  icon_true:  vpn-on.png
  icon_false: vpn-off.png
  poll:
    command: "scutil --nc status \"My VPN\""
    interval: 5s
    match: "Connected"

Linux examples:

# VPN status (exit-code match — no match string needed)
4:
  command: nmcli connection up my-vpn
  icon_true:  vpn-on.png
  icon_false: vpn-off.png
  poll:
    command: nmcli connection show --active my-vpn
    interval: 5s

# Systemd service toggle
5:
  command: systemctl --user toggle my-service
  icon_true:  service-running.png
  icon_false: service-stopped.png
  poll:
    command:  systemctl --user is-active my-service
    interval: 3s

# Speaker mute
6:
  command: pactl set-sink-mute @DEFAULT_SINK@ toggle
  icon_true:  speaker-muted.png
  icon_false: speaker-on.png
  poll:
    command:  pactl get-sink-mute @DEFAULT_SINK@
    interval: 2s
    match: "yes"

Launching applications

On Linux, GUI apps are typically launched by their binary name (firefox, ghostty, nautilus). On macOS, apps live in /Applications/ as .app bundles and need to be opened differently.

macOS — open -a

The open -a command launches (or focuses) a macOS application by name:

keys:
  0:
    icon: firefox.png
    command: "open -a Firefox"
  1:
    icon: slack.png
    command: "open -a Slack"
  2:
    icon: ghostty.png
    command: "open -a Ghostty"
  3:
    icon: finder.png
    command: "open -a Finder ~/Documents"     # open Finder to a specific folder
  4:
    icon: vscode.png
    command: "open -a 'Visual Studio Code'"   # quote names with spaces

How open -a works:

  • If the app is already running, it brings it to the front (no duplicate launched).
  • If the app is not running, it launches it.
  • The app name matches what's in /Applications/ minus the .app extension.
  • To find the exact name: ls /Applications/ or osascript -e 'tell application "System Events" to get name of every process whose background only is false'

Opening files and URLs:

keys:
  5:
    icon: project.png
    command: "open -a 'Visual Studio Code' ~/Projects/myproject"   # open a folder in VS Code
  6:
    icon: notes.png
    command: "open ~/Documents/notes.txt"      # opens in default app for .txt
  7:
    icon: github.png
    command: "open https://github.com"         # opens in default browser

Linux — direct binary

keys:
  0:
    icon: firefox.png
    command: firefox
  1:
    icon: slack.png
    command: slack
  2:
    icon: ghostty.png
    command: ghostty
  3:
    icon: files.png
    command: nautilus ~/Documents

Most Linux desktop apps can also be launched with their .desktop file via gtk-launch:

keys:
  4:
    icon: vscode.png
    command: "gtk-launch code"    # uses the .desktop file name (without .desktop)

Cross-platform keys

If you use the same config on both platforms, you can use a shell one-liner:

keys:
  0:
    icon: browser.png
    command: "if [ \"$(uname)\" = \"Darwin\" ]; then open -a Firefox; else firefox; fi"

Terminal & SSH commands

Any shell command works — including launching terminals and SSH sessions.

Open a terminal:

keys:
  0:
    icon: ghostty.png
    command: ghostty               # Linux
  1:
    icon: terminal.png
    command: open -a Ghostty       # macOS — or: open -a Terminal, open -a iTerm

SSH:

keys:
  5:
    icon: homeserver.png
    command: "ghostty -e ssh user@homeserver"          # Linux
  6:
    icon: homeserver.png
    command: "open -a Terminal ssh://user@homeserver"  # macOS

SSH agent & the service

The service starts before your shell environment is fully loaded, so SSH_AUTH_SOCK may not be set. Fix:

Linux:

systemctl --user import-environment SSH_AUTH_SOCK

macOS — add to ~/Library/LaunchAgents/com.woodarddigital.streamdeck-go.plist inside the <dict> block:

<key>EnvironmentVariables</key>
<dict>
    <key>SSH_AUTH_SOCK</key>
    <string>/private/tmp/com.apple.launchd.XXXXX/Listeners</string>
</dict>

Run echo $SSH_AUTH_SOCK in a terminal to find the correct path.

Common terminal flags:

Terminal Flag to run a command
ghostty ghostty -e <cmd>
alacritty alacritty -e <cmd>
kitty kitty <cmd>
wezterm wezterm start -- <cmd>
Terminal.app open -a Terminal <script>
iTerm2 open -a iTerm <script>

Privileged commands

Use the priv: prefix to run commands that require elevated privileges.

keys:
  30:
    icon: suspend.png
    command: "priv:suspend"

Privileged commands — macOS

No root daemon required. The main process reads ~/.config/streamdeck-go/privileged.yaml and runs the command via osascript, which shows the standard macOS admin authentication dialog.

Setup:

make install-helper   # copies privileged.yaml to ~/.config/streamdeck-go/

Edit ~/.config/streamdeck-go/privileged.yaml:

commands:
  suspend:  "pmset sleepnow"
  reboot:   "shutdown -r now"
  poweroff: "shutdown -h now"

Privileged commands — Linux

A root helper daemon (streamdeck-go-helper) validates commands against a root-owned whitelist at /etc/streamdeck-go/privileged.yaml before running them. The main daemon communicates with it over a Unix socket.

make install-helper   # creates streamdeck group, installs helper + system service

Edit /etc/streamdeck-go/privileged.yaml (as root):

commands:
  suspend:  "systemctl suspend"
  reboot:   "systemctl reboot"
  poweroff: "systemctl poweroff"

Modules

Modules let you define reusable commands in a separate modules.yaml file instead of inlining shell commands in your config. This is especially useful for API-driven integrations like Slack where commands are long curl calls with tokens and JSON payloads.

How it works

  1. Create ~/.config/streamdeck-go/modules.yaml (next to your config.yaml).
  2. Define modules with named functions. Each function has an exec template and optional default params.
  3. Reference them in config.yaml with module, function, and optional params overrides.

The daemon watches modules.yaml for changes alongside config.yaml — edits to either file trigger a live reload.

Module file format

# ~/.config/streamdeck-go/modules.yaml

modules:
  slack:
    set_status:
      params:
        emoji: ":speech_balloon:"
        text: "In a meeting"
        expiry: "1h"
      exec: |
        curl -s -X POST https://slack.com/api/users.profile.set \
          -H "Authorization: Bearer {{env "SLACK_TOKEN"}}" \
          -H "Content-Type: application/json" \
          -d '{"profile":{"status_emoji":"{{.emoji}}","status_text":"{{.text}}","status_expiration":{{expiry .expiry}}}}'

    clear_status:
      exec: |
        curl -s -X POST https://slack.com/api/users.profile.set \
          -H "Authorization: Bearer {{env "SLACK_TOKEN"}}" \
          -H "Content-Type: application/json" \
          -d '{"profile":{"status_emoji":"","status_text":"","status_expiration":0}}'

    set_presence:
      params:
        presence: "away"
      exec: |
        curl -s -X POST https://slack.com/api/users.setPresence \
          -H "Authorization: Bearer {{env "SLACK_TOKEN"}}" \
          -H "Content-Type: application/json" \
          -d '{"presence":"{{.presence}}"}'

    snooze:
      params:
        minutes: "60"
      exec: |
        curl -s -X POST https://slack.com/api/dnd.setSnooze \
          -H "Authorization: Bearer {{env "SLACK_TOKEN"}}" \
          -H "Content-Type: application/json" \
          -d '{"num_minutes":{{.minutes}}}'

    end_snooze:
      exec: |
        curl -s -X POST https://slack.com/api/dnd.endSnooze \
          -H "Authorization: Bearer {{env "SLACK_TOKEN"}}"

A full example is included at modules.example.yaml in the repo root.

Using modules in config.yaml

Reference a module function instead of writing inline commands:

keys:
  # Set Slack status to "In a meeting" for 1 hour (uses module defaults)
  10:
    icon: meeting.png
    module: slack
    function: set_status

  # Override default params — different emoji, text, and duration
  11:
    icon: lunch.png
    module: slack
    function: set_status
    params:
      emoji: ":knife_fork_plate:"
      text: "Out to lunch"
      expiry: "30m"

  # Clear Slack status
  12:
    icon: clear-status.png
    module: slack
    function: clear_status

  # Go away / come back
  13:
    icon: away.png
    module: slack
    function: set_presence
    params:
      presence: "away"

  14:
    icon: active.png
    module: slack
    function: set_presence
    params:
      presence: "auto"

  # Snooze notifications for 60 minutes
  15:
    icon: snooze.png
    module: slack
    function: snooze

  # End snooze
  16:
    icon: unsnooze.png
    module: slack
    function: end_snooze

Poll commands also support modules — use module, function, and params inside the poll block:

keys:
  17:
    icon_true: dnd-on.png
    icon_false: dnd-off.png
    module: slack
    function: snooze
    poll:
      module: slack
      function: check_dnd
      interval: 10s
      match: "snooze_enabled.*true"

Template helpers

Two helpers are available in exec templates:

Helper Usage Description
env {{env "VAR_NAME"}} Returns the value of an environment variable
expiry {{expiry .duration}} Converts a Go duration string (e.g. "1h", "30m") to a Unix epoch timestamp. "0" or "" returns "0" (no expiry)

Secrets and tokens

Tokens and secrets must never go in modules.yaml or config.yaml. Use the {{env "VAR_NAME"}} template helper to read them from environment variables at runtime.

Setting up your Slack token:

  1. Create a Slack app at api.slack.com/apps with these OAuth scopes:

    • users.profile:write — set/clear status
    • users:write — set presence (away/auto)
    • dnd:write — snooze/unsnooze notifications
  2. Install the app to your workspace and copy the User OAuth Token (xoxp-...).

  3. Export the token in your shell profile (~/.zshrc, ~/.bash_profile, etc.):

    export SLACK_TOKEN="xoxp-your-token-here"
    
  4. Make sure the service can see the variable:

    macOS — add to ~/Library/LaunchAgents/com.woodarddigital.streamdeck-go.plist inside the <dict> block:

    <key>EnvironmentVariables</key>
    <dict>
        <key>SLACK_TOKEN</key>
        <string>xoxp-your-token-here</string>
    </dict>
    

    Linux — import into the systemd user environment:

    systemctl --user import-environment SLACK_TOKEN
    

    Or add an Environment= line via systemctl --user edit streamdeck-go:

    [Service]
    Environment=SLACK_TOKEN=xoxp-your-token-here
    

Security notes:

  • The modules.yaml and config.yaml files can safely live in your dotfiles repo — they contain no secrets.
  • On Linux, the systemd override file (~/.config/systemd/user/streamdeck-go.service.d/override.conf) is user-readable only (mode 600). Still, prefer import-environment over hardcoding tokens in unit files when possible.
  • On macOS, launchd plist files in ~/Library/LaunchAgents/ are user-readable only by default.
  • Never commit tokens to git. Add .env files to .gitignore if you use one.

Config builder

Instead of editing YAML by hand, use the interactive TUI to configure keys:

make build-init
./streamdeck-init

Or with a custom config path:

./streamdeck-init -config ~/dotfiles/.config/streamdeck-go/config.yaml

The tool shows your 8x4 key grid, walks you through each step, and appends the result to your config.yaml:

  Stream Deck XL — 8×4 grid

  ╭─────────╮╭─────────╮╭─────────╮╭─────────╮╭─────────╮╭─────────╮╭─────────╮╭─────────╮
  │Rootshell││ lights  ││set_stat ││  WHH    ││Dumpster ││  [5]    ││  [6]    ││  [7]    │
  ╰─────────╯╰─────────╯╰─────────╯╰─────────╯╰─────────╯╰─────────╯╰─────────╯╰─────────╯
  ...

  [n] = free   dim = occupied

  ? Pick a key slot
  > 5  (free)
    6  (free)
    7  (free)
    ...

  ? What should this key do?
  > Module function (Slack, etc.)
    Shell command

  ? Pick a module
  > slack

  ? Pick a function from slack
  > set_status
    clear_status
    set_presence
    go_offline
    snooze
    end_snooze

  ? emoji
  > :coffee:

  ? text
  > Coffee break

  ? expiry
  > 30m

  ? Pick an icon from ~/.config/streamdeck-go/icons
  > coffee.png

  Summary:
    Key:      5
    Icon:     coffee.png
    Module:   slack
    Function: set_status
    Params:
      emoji: :coffee:
      text: Coffee break
      expiry: 30m

  ? Write this key to config? Yes
  ✓ Key 5 added to config.

  ? Add another key? No
  ✓ Done — the daemon will auto-reload your config.

On a fresh install with no config files, the tool creates config.yaml and modules.yaml automatically.


Supported icon formats

Format Notes
PNG Recommended for static icons
JPEG Good for photos / complex images
GIF Animated — all frames pre-encoded at startup, cycled in a background goroutine
SVG Rasterised to 96×96 at startup via oksvg

Icons are scaled to 96×96 px using bi-linear filtering. The XL renders images mirrored, so they are pre-flipped before sending — your icons will appear the right way round.


Supported Devices

Model Product ID Keys Linux macOS
Stream Deck XL v2 0x00ba 32 tested tested
Stream Deck XL v1 0x006c 32 untested untested
Stream Deck MK.2 0x006d 15 untested untested

To find your device's product ID:

  • macOS: system_profiler SPUSBDataType | grep -A5 "Stream Deck"
  • Linux: lsusb | grep Elgato

To add a model, edit the models map in internal/device/streamdeck.go.


Roadmap

Text / label overlay on icons

Render dynamic text directly onto a key image at runtime — useful for showing live state like volume level, a clock, a counter, or the current git branch.

Example config (proposed):

keys:
  4:
    icon: volume.png
    label: "$(pactl get-sink-volume @DEFAULT_SINK@ | awk '{print $5}')"
    label_position: bottom   # top | center | bottom
    refresh: 5s

Multi-page layouts

Support more than 32 actions by organising keys into named pages. A designated key switches between pages.

Example config (proposed):

pages:
  default:
    0:
      icon: apps.png
      command: "page:apps"
    1:
      icon: firefox.png
      command: firefox

  apps:
    0:
      icon: back.png
      command: "page:default"
    1:
      icon: ghostty.png
      command: ghostty

AUR package

yay -S streamdeck-go   # coming soon
Description
Personal Streamdeck utility for Omarchy | 406.fail
Readme 7.4 MiB
Languages
Go 66.6%
Shell 23%
Makefile 10.4%