summaryrefslogtreecommitdiff
path: root/internal/appconfig/config_validate.go
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-02 13:28:46 +0200
committerPaul Buetow <paul@buetow.org>2026-03-02 13:28:46 +0200
commit10406467650942b780e5de462d5103431c5a951e (patch)
tree2ef1050fe60916915f39c3eea655fdafe8241d3a /internal/appconfig/config_validate.go
parent8735394dae4266bea638b20b5d327ce366a608a1 (diff)
appconfig: split config module and decompose oversized funcs (task 406)
Diffstat (limited to 'internal/appconfig/config_validate.go')
-rw-r--r--internal/appconfig/config_validate.go98
1 files changed, 98 insertions, 0 deletions
diff --git a/internal/appconfig/config_validate.go b/internal/appconfig/config_validate.go
new file mode 100644
index 0000000..f5e698f
--- /dev/null
+++ b/internal/appconfig/config_validate.go
@@ -0,0 +1,98 @@
+package appconfig
+
+import (
+ "fmt"
+ "strings"
+)
+
+// Validate checks custom actions and tmux settings for duplicates and consistency.
+func (a App) Validate() error {
+ if err := validateCustomActions(a.CustomActions); err != nil {
+ return err
+ }
+ return validateTmuxCustomMenuHotkey(a.TmuxCustomMenuHotkey)
+}
+
+func validateCustomActions(actions []CustomAction) error {
+ seenID := make(map[string]struct{})
+ seenHK := make(map[string]struct{})
+ for _, action := range actions {
+ if err := validateCustomAction(action, seenID, seenHK); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func validateCustomAction(action CustomAction, seenID, seenHK map[string]struct{}) error {
+ id := strings.ToLower(strings.TrimSpace(action.ID))
+ if id == "" {
+ return fmt.Errorf("config: custom action missing required field id")
+ }
+ if _, exists := seenID[id]; exists {
+ return fmt.Errorf("config: duplicate custom action id: %s", action.ID)
+ }
+ seenID[id] = struct{}{}
+
+ if strings.TrimSpace(action.Title) == "" {
+ return fmt.Errorf("config: custom action %s missing required field title", action.ID)
+ }
+ if err := validateCustomScope(action); err != nil {
+ return err
+ }
+ if err := validateCustomInstructionOrUser(action); err != nil {
+ return err
+ }
+ return validateCustomHotkey(action, seenHK)
+}
+
+func validateCustomScope(action CustomAction) error {
+ scope := strings.TrimSpace(action.Scope)
+ if scope == "" || scope == "selection" || scope == "diagnostics" {
+ return nil
+ }
+ return fmt.Errorf("config: custom action %s has invalid scope: %s", action.ID, action.Scope)
+}
+
+func validateCustomInstructionOrUser(action CustomAction) error {
+ hasInstr := strings.TrimSpace(action.Instruction) != ""
+ hasUser := strings.TrimSpace(action.User) != ""
+ if hasInstr && hasUser {
+ return fmt.Errorf("config: custom action %s must set either instruction or user, not both", action.ID)
+ }
+ if !hasInstr && !hasUser {
+ return fmt.Errorf("config: custom action %s requires instruction or user", action.ID)
+ }
+ return nil
+}
+
+func validateCustomHotkey(action CustomAction, seenHK map[string]struct{}) error {
+ hk := strings.TrimSpace(action.Hotkey)
+ if hk == "" {
+ return nil
+ }
+ if len([]rune(hk)) != 1 {
+ return fmt.Errorf("config: custom action %s hotkey must be a single character", action.ID)
+ }
+ lower := strings.ToLower(hk)
+ if _, exists := seenHK[lower]; exists {
+ return fmt.Errorf("config: duplicate custom action hotkey: %s", hk)
+ }
+ seenHK[lower] = struct{}{}
+ return nil
+}
+
+func validateTmuxCustomMenuHotkey(hotkey string) error {
+ hk := strings.TrimSpace(hotkey)
+ if hk == "" {
+ return nil
+ }
+ if len([]rune(hk)) != 1 {
+ return fmt.Errorf("config: invalid tmux.custom_menu_hotkey: %s", hk)
+ }
+ switch strings.ToLower(hk) {
+ case "r", "i", "c", "t", "p", "s":
+ return fmt.Errorf("config: invalid tmux.custom_menu_hotkey: %s (clashes with built-in)", hk)
+ }
+ return nil
+}