summaryrefslogtreecommitdiff
path: root/internal/appconfig/config_validate.go
blob: 19cfcd065200caab39886cf6c89b8d6a7f1dc85a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
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
}