adding init program
This commit is contained in:
148
cmd/streamdeck-init/yaml.go
Normal file
148
cmd/streamdeck-init/yaml.go
Normal file
@@ -0,0 +1,148 @@
|
||||
// yaml.go — Appends key entries to config.yaml without destroying comments.
|
||||
//
|
||||
// ## Why raw text append instead of yaml.Marshal?
|
||||
//
|
||||
// yaml.v3's Marshal destroys comments, reorders map keys, and normalises
|
||||
// formatting. Users keep the default config's comments (key layout diagram,
|
||||
// examples, etc.), so we must preserve them. The approach:
|
||||
//
|
||||
// - Parse config.yaml with config.Load() for DATA (which keys exist, icons_dir, etc.)
|
||||
// - Write new keys by appending raw YAML text to the end of the file
|
||||
//
|
||||
// This works because keys: is always the last top-level block in config.yaml,
|
||||
// and YAML maps are unordered, so appending a new 2-space-indented key entry
|
||||
// to the end of the file is valid.
|
||||
//
|
||||
// ## Three cases handled by appendKeyToConfig
|
||||
//
|
||||
// Case A: "keys: {}" — the empty map literal from the default config generator.
|
||||
//
|
||||
// Replace {} with a newline, then insert the key block. This is the most
|
||||
// common case on a fresh install.
|
||||
//
|
||||
// Case B: "keys:" with existing children — the normal case after keys exist.
|
||||
//
|
||||
// Append the new key block to the end of the file. It lands inside the
|
||||
// keys: block because it's indented with 2 spaces.
|
||||
//
|
||||
// Case C: no "keys:" block at all — a malformed or minimal config.
|
||||
//
|
||||
// Append "keys:\n" then the key block.
|
||||
//
|
||||
// ## Expanding
|
||||
//
|
||||
// To support toggle keys (icon_true/icon_false + poll block), add those fields
|
||||
// to keyEntry and extend renderKeySnippet() to emit the extra YAML lines.
|
||||
//
|
||||
// To support deleting or editing existing keys, you'd need a different strategy
|
||||
// (e.g. yaml.v3 node-level manipulation). That's not in scope for the current
|
||||
// append-only tool.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// keyEntry holds the data collected from the TUI forms, ready to be rendered
|
||||
// as a YAML snippet and appended to config.yaml.
|
||||
//
|
||||
// Two mutually exclusive modes:
|
||||
// - Module key: Module + Function + Params are set, Command is empty
|
||||
// - Command key: Command is set, Module/Function/Params are empty
|
||||
type keyEntry struct {
|
||||
Index int // key slot (0-31)
|
||||
Icon string // icon filename relative to icons_dir
|
||||
Module string // module name from modules.yaml (e.g. "slack")
|
||||
Function string // function name within the module (e.g. "set_status")
|
||||
Params map[string]string // param overrides (merged with module defaults at runtime)
|
||||
Command string // raw shell command (non-module keys only)
|
||||
}
|
||||
|
||||
// appendKeyToConfig reads config.yaml, appends a key entry as raw YAML text,
|
||||
// and writes the file back. Preserves all existing content including comments.
|
||||
func appendKeyToConfig(path string, entry keyEntry) error {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read config: %w", err)
|
||||
}
|
||||
|
||||
content := string(data)
|
||||
snippet := renderKeySnippet(entry)
|
||||
|
||||
// Case A: keys: {} — empty map literal from the default config generator.
|
||||
// Replace the {} with a newline so the snippet becomes the first child.
|
||||
emptyKeys := regexp.MustCompile(`(?m)^keys:\s*\{\}\s*$`)
|
||||
if emptyKeys.MatchString(content) {
|
||||
content = emptyKeys.ReplaceAllString(content, "keys:\n"+snippet)
|
||||
return os.WriteFile(path, []byte(content), 0644)
|
||||
}
|
||||
|
||||
// Case B: keys: exists with children — append snippet to end of file.
|
||||
// The 2-space indent on the snippet places it inside the keys: block.
|
||||
hasKeys := regexp.MustCompile(`(?m)^keys:\s*$`)
|
||||
if hasKeys.MatchString(content) {
|
||||
if !strings.HasSuffix(content, "\n") {
|
||||
content += "\n"
|
||||
}
|
||||
content += snippet
|
||||
return os.WriteFile(path, []byte(content), 0644)
|
||||
}
|
||||
|
||||
// Case C: no keys: block at all — append both the block header and snippet.
|
||||
if !strings.HasSuffix(content, "\n") {
|
||||
content += "\n"
|
||||
}
|
||||
content += "\nkeys:\n" + snippet
|
||||
return os.WriteFile(path, []byte(content), 0644)
|
||||
}
|
||||
|
||||
// renderKeySnippet formats a keyEntry as an indented YAML block (2-space indent
|
||||
// for the key index, 4-space for fields, 6-space for params).
|
||||
//
|
||||
// Module key output:
|
||||
//
|
||||
// 3:
|
||||
// icon: status.png
|
||||
// module: slack
|
||||
// function: set_status
|
||||
// params:
|
||||
// emoji: ":brb:"
|
||||
// text: "BRB"
|
||||
//
|
||||
// Command key output:
|
||||
//
|
||||
// 3:
|
||||
// icon: firefox.png
|
||||
// command: "open -a Firefox"
|
||||
func renderKeySnippet(e keyEntry) string {
|
||||
var b strings.Builder
|
||||
|
||||
fmt.Fprintf(&b, " %d:\n", e.Index)
|
||||
fmt.Fprintf(&b, " icon: %s\n", e.Icon)
|
||||
|
||||
if e.Module != "" {
|
||||
fmt.Fprintf(&b, " module: %s\n", e.Module)
|
||||
fmt.Fprintf(&b, " function: %s\n", e.Function)
|
||||
if len(e.Params) > 0 {
|
||||
b.WriteString(" params:\n")
|
||||
// Sort param keys for deterministic, diffable output.
|
||||
keys := make([]string, 0, len(e.Params))
|
||||
for k := range e.Params {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
fmt.Fprintf(&b, " %s: \"%s\"\n", k, e.Params[k])
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(&b, " command: \"%s\"\n", e.Command)
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
Reference in New Issue
Block a user