A key with `folder: NAME` swaps the deck to the keys defined under `folders.NAME`. Every folder shows a back key (slot 0 by default, drawn with a built-in chevron) that pops back to the parent; folders nest. The daemon now runs one page at a time: per-key setup moved into setupKey(), openPage() draws a page under its own context, and page.stop() tears it down before the next page is drawn (and on exit, so device death no longer waits on uncancelled goroutines). streamdeck-ctl gains `key set -folder`, reports kind "folder", lists every folder in status, and refuses to `key press` a navigation-only key. The Omarchy grid shows a folder glyph. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ARLa4moGb154fzc9TevgV
136 lines
5.0 KiB
Go
136 lines
5.0 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// PollConfig defines how to poll for the current state of a toggle key.
|
|
type PollConfig struct {
|
|
Command string `yaml:"command"` // shell command whose output is checked
|
|
Interval string `yaml:"interval"` // how often to poll, e.g. "2s" (default: "2s")
|
|
Match string `yaml:"match"` // substring to find in output → "on" state; omit to use exit code 0
|
|
|
|
// Module-based alternative to Command: resolved to a shell string at startup.
|
|
Module string `yaml:"module"`
|
|
Function string `yaml:"function"`
|
|
Params map[string]string `yaml:"params"`
|
|
}
|
|
|
|
// KeyConfig defines what a single Stream Deck key does.
|
|
type KeyConfig struct {
|
|
Icon string `yaml:"icon"` // filename relative to icons_dir (regular keys)
|
|
Text string `yaml:"text"` // text overlay on the key (auto-sized, supports newlines)
|
|
TextColor string `yaml:"text_color"` // text color: "white" (default), "black", "red", "blue", or hex "#RRGGBB"
|
|
Command string `yaml:"command"` // shell command to run on press
|
|
|
|
// Dynamic text: periodically run TextCommand and render its stdout as the
|
|
// key's text overlay. Refresh is the interval (default 1s, floor 250ms).
|
|
// Text (above) is the static fallback / initial value.
|
|
TextCommand string `yaml:"text_command"` // shell command whose stdout becomes the overlay text
|
|
Refresh string `yaml:"refresh"` // how often to re-run text_command, e.g. "1s" (default: "1s")
|
|
|
|
// Toggle/status keys: show different icons based on polled state.
|
|
IconTrue string `yaml:"icon_true"` // icon when poll match is true
|
|
IconFalse string `yaml:"icon_false"` // icon when poll match is false
|
|
Poll *PollConfig `yaml:"poll"`
|
|
|
|
// Module-based alternative to Command: resolved to a shell string at startup.
|
|
Module string `yaml:"module"`
|
|
Function string `yaml:"function"`
|
|
Params map[string]string `yaml:"params"`
|
|
|
|
// Folder key: pressing it swaps the deck to the named folder's keys
|
|
// (defined under the top-level `folders:` block). Folders nest — a folder's
|
|
// key may itself have a folder — and every folder shows a back key.
|
|
Folder string `yaml:"folder"`
|
|
}
|
|
|
|
// BackConfig customises the back key every folder shows. All fields are
|
|
// optional: the default is key 0 drawn with a built-in arrow and "Back" label.
|
|
type BackConfig struct {
|
|
Key *int `yaml:"key"` // slot index for the back key (default 0)
|
|
Icon string `yaml:"icon"` // icon filename relative to icons_dir (default: built-in arrow)
|
|
Text string `yaml:"text"` // label (default "Back")
|
|
TextColor string `yaml:"text_color"` // same values as KeyConfig.TextColor
|
|
}
|
|
|
|
// FolderConfig is a sub-page of keys shown while a folder is open.
|
|
type FolderConfig struct {
|
|
Back BackConfig `yaml:"back"`
|
|
Keys map[int]KeyConfig `yaml:"keys"`
|
|
}
|
|
|
|
// BackKey returns the slot index the folder's back key occupies.
|
|
func (f FolderConfig) BackKey() int {
|
|
if f.Back.Key != nil {
|
|
return *f.Back.Key
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// DefaultConfigPath returns the default config file path, respecting XDG_CONFIG_HOME.
|
|
func DefaultConfigPath() string {
|
|
base := os.Getenv("XDG_CONFIG_HOME")
|
|
if base == "" {
|
|
home, _ := os.UserHomeDir()
|
|
base = filepath.Join(home, ".config")
|
|
}
|
|
return filepath.Join(base, "streamdeck-go", "config.yaml")
|
|
}
|
|
|
|
// ModulesPath returns the path to the modules.yaml file next to the given config file.
|
|
func ModulesPath(cfgPath string) string {
|
|
return filepath.Join(filepath.Dir(cfgPath), "modules.yaml")
|
|
}
|
|
|
|
// Config is the top-level structure of the YAML config file.
|
|
type Config struct {
|
|
IconsDir string `yaml:"icons_dir"`
|
|
Brightness int `yaml:"brightness"`
|
|
Device DeviceConfig `yaml:"device"`
|
|
Keys map[int]KeyConfig `yaml:"keys"`
|
|
// Folders are sub-pages opened by keys with a `folder:` field. The root
|
|
// page is Keys; a folder's keys replace it until its back key is pressed.
|
|
Folders map[string]FolderConfig `yaml:"folders"`
|
|
}
|
|
|
|
// DeviceConfig allows overriding USB IDs (defaults work for Stream Deck XL v2).
|
|
type DeviceConfig struct {
|
|
VendorID uint16 `yaml:"vendor_id"`
|
|
ProductID uint16 `yaml:"product_id"`
|
|
}
|
|
|
|
// Load reads and parses a YAML config file, applying sane defaults.
|
|
// icons_dir defaults to an "icons" folder next to the config file.
|
|
func Load(path string) (*Config, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read config %q: %w", path, err)
|
|
}
|
|
|
|
cfg := &Config{
|
|
IconsDir: filepath.Join(filepath.Dir(path), "icons"),
|
|
Brightness: 70,
|
|
Device: DeviceConfig{
|
|
VendorID: 0x0fd9,
|
|
ProductID: 0x00ba, // Stream Deck XL v2
|
|
},
|
|
}
|
|
|
|
if err := yaml.Unmarshal(data, cfg); err != nil {
|
|
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
|
|
}
|