Adding Slack modules
This commit is contained in:
@@ -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
117
internal/modules/modules.go
Normal 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)
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user