Adding Slack modules
This commit is contained in:
301
README.md
301
README.md
@@ -21,6 +21,8 @@ No Elgato software required — communicates directly with the device over USB H
|
||||
- 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](#modules).
|
||||
|
||||
**Planned:** text/label overlays on keys, multi-page layouts, AUR package — see [Roadmap](#roadmap)
|
||||
|
||||
---
|
||||
@@ -37,13 +39,16 @@ streamdeck-go/
|
||||
├── internal/
|
||||
│ ├── config/
|
||||
│ │ └── config.go # YAML parsing with XDG-aware defaults
|
||||
│ └── device/
|
||||
│ └── streamdeck.go # USB HID communication, image encoding, button reads
|
||||
│ ├── 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
|
||||
@@ -53,10 +58,15 @@ streamdeck-go/
|
||||
|
||||
```
|
||||
~/.config/streamdeck-go/config.yaml
|
||||
~/.config/streamdeck-go/modules.yaml (optional)
|
||||
│
|
||||
├── fsnotify watcher ──── file saved? ──▶ cancel ctx → reload config → restart run()
|
||||
├── fsnotify watcher ──── file saved? ──▶ cancel ctx → reload both → restart run()
|
||||
│
|
||||
└──▶ run(ctx)
|
||||
└──▶ 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)
|
||||
│
|
||||
@@ -391,6 +401,94 @@ keys:
|
||||
|
||||
---
|
||||
|
||||
### 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:
|
||||
|
||||
```yaml
|
||||
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:**
|
||||
|
||||
```yaml
|
||||
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
|
||||
|
||||
```yaml
|
||||
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`:
|
||||
|
||||
```yaml
|
||||
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:
|
||||
|
||||
```yaml
|
||||
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.
|
||||
@@ -404,7 +502,7 @@ keys:
|
||||
command: ghostty # Linux
|
||||
1:
|
||||
icon: terminal.png
|
||||
command: open -a Terminal # macOS — or: open -a iTerm
|
||||
command: open -a Ghostty # macOS — or: open -a Terminal, open -a iTerm
|
||||
```
|
||||
|
||||
**SSH:**
|
||||
@@ -506,6 +604,199 @@ commands:
|
||||
|
||||
---
|
||||
|
||||
### 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
|
||||
|
||||
```yaml
|
||||
# ~/.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`](modules.example.yaml) in the repo root.
|
||||
|
||||
#### Using modules in config.yaml
|
||||
|
||||
Reference a module function instead of writing inline commands:
|
||||
|
||||
```yaml
|
||||
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:
|
||||
|
||||
```yaml
|
||||
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](https://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.):
|
||||
|
||||
```bash
|
||||
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:
|
||||
```xml
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>SLACK_TOKEN</key>
|
||||
<string>xoxp-your-token-here</string>
|
||||
</dict>
|
||||
```
|
||||
|
||||
**Linux** — import into the systemd user environment:
|
||||
```bash
|
||||
systemctl --user import-environment SLACK_TOKEN
|
||||
```
|
||||
|
||||
Or add an `Environment=` line via `systemctl --user edit streamdeck-go`:
|
||||
```ini
|
||||
[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.
|
||||
|
||||
---
|
||||
|
||||
### Supported icon formats
|
||||
|
||||
| Format | Notes |
|
||||
|
||||
Reference in New Issue
Block a user