Adding Slack modules

This commit is contained in:
lwoodard
2026-04-13 11:27:18 -06:00
parent 1d9f0b519b
commit 639a08a808
5 changed files with 522 additions and 9 deletions

View File

@@ -13,6 +13,11 @@ 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.
@@ -24,6 +29,16 @@ type KeyConfig struct {
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"`
}
// 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.

117
internal/modules/modules.go Normal file
View File

@@ -0,0 +1,117 @@
package modules
import (
"bytes"
"errors"
"fmt"
"os"
"os/exec"
"strconv"
"text/template"
"time"
"gopkg.in/yaml.v3"
)
// FunctionDef is a single callable function within a module.
type FunctionDef struct {
Exec string `yaml:"exec"` // Go text/template that renders to a shell command
Params map[string]string `yaml:"params"` // default parameter values
}
// ModuleDef maps function names to their definitions.
type ModuleDef map[string]FunctionDef
// Registry holds all loaded modules.
type Registry struct {
Modules map[string]ModuleDef `yaml:"modules"`
}
// LoadRegistry reads a modules.yaml file.
// A missing file is not an error — returns an empty Registry so callers don't need to
// special-case systems without a modules.yaml.
func LoadRegistry(path string) (*Registry, error) {
data, err := os.ReadFile(path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return &Registry{}, nil
}
return nil, fmt.Errorf("read modules %q: %w", path, err)
}
var r Registry
if err := yaml.Unmarshal(data, &r); err != nil {
return nil, fmt.Errorf("parse modules %q: %w", path, err)
}
return &r, nil
}
// Resolve looks up module/function, merges params (defaults overridden by overrides),
// and renders the exec template. Returns the shell command string ready for sh -c.
func (r *Registry) Resolve(module, function string, overrides map[string]string) (string, error) {
if r == nil || r.Modules == nil {
return "", fmt.Errorf("no modules loaded")
}
mod, ok := r.Modules[module]
if !ok {
return "", fmt.Errorf("unknown module %q", module)
}
fn, ok := mod[function]
if !ok {
return "", fmt.Errorf("module %q has no function %q", module, function)
}
// Merge: start with defaults, apply overrides on top.
params := make(map[string]string, len(fn.Params)+len(overrides))
for k, v := range fn.Params {
params[k] = v
}
for k, v := range overrides {
params[k] = v
}
tmpl, err := template.New("exec").Funcs(funcMap()).Parse(fn.Exec)
if err != nil {
return "", fmt.Errorf("module %q function %q: parse template: %w", module, function, err)
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, params); err != nil {
return "", fmt.Errorf("module %q function %q: render template: %w", module, function, err)
}
return buf.String(), nil
}
// Execute resolves the module/function and runs it via sh -c.
func (r *Registry) Execute(module, function string, overrides map[string]string) error {
shell, err := r.Resolve(module, function, overrides)
if err != nil {
return err
}
c := exec.Command("sh", "-c", shell)
c.Stdout = os.Stdout
c.Stderr = os.Stderr
return c.Run()
}
// funcMap returns the template helper functions available in exec templates.
func funcMap() template.FuncMap {
return template.FuncMap{
// env returns the value of an environment variable.
// Use this to keep secrets (tokens, passwords) out of modules.yaml.
"env": os.Getenv,
// expiry converts a duration string like "+1h" to a Unix epoch timestamp string.
// Pass "" or "0" to get "0" (no expiry). Supports Go duration syntax (e.g. "30m", "2h").
// Note: time.ParseDuration does not support days (d) or weeks (w).
"expiry": func(s string) string {
if s == "" || s == "0" {
return "0"
}
d, err := time.ParseDuration(s)
if err != nil {
return "0"
}
return strconv.FormatInt(time.Now().Add(d).Unix(), 10)
},
}
}