summaryrefslogtreecommitdiff
path: root/internal/config/config.go
blob: 036d081787ca396d220271ce3e9d6a1295980f14 (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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
package config

import (
	"bufio"
	"fmt"
	"os"
	"path/filepath"
	"strconv"
	"strings"

	"codeberg.org/snonux/loadbars/internal/constants"
)

// Config holds all loadbars configuration (file + CLI).
// Defaults match the Perl Shared.pm %C.
type Config struct {
	Hosts      []string // Each entry is "host" or "host:user"
	Title      string
	BarWidth   int
	CPUAverage int
	Extended   bool
	HasAgent   bool
	Height     int
	MaxWidth   int
	NetAverage int
	NetLink    string
	ShowAvgLine bool
	ShowCores   bool
	ShowMem     bool
	ShowNet     bool
	SSHOpts    string
	Cluster    string
}

// Default returns a Config with default values.
func Default() Config {
	return Config{
		BarWidth:   1200,
		CPUAverage: 10,
		Extended:   false,
		HasAgent:   false,
		Height:     150,
		MaxWidth:   1900,
		NetAverage: 15,
		NetLink:    "gbit",
		ShowCores:  false,
		ShowMem:    false,
		ShowNet:    false,
	}
}

// ConfFilePath returns the full path to the config file (~/.loadbarsrc).
func ConfFilePath() (string, error) {
	home, err := os.UserHomeDir()
	if err != nil {
		return "", fmt.Errorf("home dir: %w", err)
	}
	return filepath.Join(home, constants.ConfFile), nil
}

// Load reads config from the config file and merges into c. Unknown keys are ignored.
func (c *Config) Load() error {
	path, err := ConfFilePath()
	if err != nil {
		return err
	}
	f, err := os.Open(path)
	if err != nil {
		if os.IsNotExist(err) {
			return nil
		}
		return fmt.Errorf("open config: %w", err)
	}
	defer f.Close()
	return c.parseReader(f)
}

// Write saves the current config to the config file (excluding title).
func (c *Config) Write() error {
	path, err := ConfFilePath()
	if err != nil {
		return err
	}
	f, err := os.Create(path)
	if err != nil {
		return fmt.Errorf("create config: %w", err)
	}
	defer f.Close()
	return c.writeTo(f)
}

// GetClusterHosts resolves a cluster name from /etc/clusters into a list of hosts.
func GetClusterHosts(cluster string) ([]string, error) {
	return GetClusterHostsFromFile(cluster, constants.CSSHConfFile)
}

// GetClusterHostsFromFile resolves a cluster from a clusters file (for testing or custom path).
// Supports recursive cluster references with cycle detection.
func GetClusterHostsFromFile(cluster, path string) ([]string, error) {
	return getClusterHostsRec(cluster, path, 1, nil)
}

func (c *Config) parseReader(f *os.File) error {
	validKeys := map[string]bool{
		"title": true, "barwidth": true, "cpuaverage": true, "extended": true,
		"hasagent": true, "height": true, "maxwidth": true, "netaverage": true,
		"netlink": true, "showcores": true, "showmem": true,
		"showavgline": true, "shownet": true, "sshopts": true, "cluster": true,
	}
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if idx := strings.Index(line, "#"); idx >= 0 {
			line = strings.TrimSpace(line[:idx])
		}
		if line == "" {
			continue
		}
		parts := strings.SplitN(line, "=", 2)
		if len(parts) != 2 {
			continue
		}
		key := strings.TrimSpace(parts[0])
		val := strings.TrimSpace(parts[1])
		if !validKeys[key] {
			continue
		}
		c.set(key, val)
	}
	return scanner.Err()
}

func (c *Config) set(key, val string) {
	switch key {
	case "title":
		c.Title = val
	case "barwidth":
		if n, err := strconv.Atoi(val); err == nil {
			c.BarWidth = n
		}
	case "cpuaverage":
		if n, err := strconv.Atoi(val); err == nil {
			c.CPUAverage = n
		}
	case "extended":
		c.Extended = parseBool(val)
	case "hasagent":
		c.HasAgent = parseBool(val)
	case "height":
		if n, err := strconv.Atoi(val); err == nil {
			c.Height = n
		}
	case "maxwidth":
		if n, err := strconv.Atoi(val); err == nil {
			c.MaxWidth = n
		}
	case "netaverage":
		if n, err := strconv.Atoi(val); err == nil {
			c.NetAverage = n
		}
	case "netlink":
		c.NetLink = val
	case "showavgline":
		c.ShowAvgLine = parseBool(val)
	case "showcores":
		c.ShowCores = parseBool(val)
	case "showmem":
		c.ShowMem = parseBool(val)
	case "shownet":
		c.ShowNet = parseBool(val)
	case "sshopts":
		c.SSHOpts = val
	case "cluster":
		c.Cluster = val
	}
}

func parseBool(s string) bool {
	s = strings.TrimSpace(strings.ToLower(s))
	return s == "1" || s == "true" || s == "yes"
}

func (c *Config) writeTo(f *os.File) error {
	w := bufio.NewWriter(f)
	writeInt := func(key string, v int) { fmt.Fprintf(w, "%s=%d\n", key, v) }
	writeStr := func(key, v string) { fmt.Fprintf(w, "%s=%s\n", key, v) }
	writeBool := func(key string, v bool) {
		val := "0"
		if v {
			val = "1"
		}
		fmt.Fprintf(w, "%s=%s\n", key, val)
	}
	writeInt("barwidth", c.BarWidth)
	writeInt("cpuaverage", c.CPUAverage)
	writeBool("extended", c.Extended)
	writeBool("hasagent", c.HasAgent)
	writeInt("height", c.Height)
	writeInt("maxwidth", c.MaxWidth)
	writeInt("netaverage", c.NetAverage)
	writeStr("netlink", c.NetLink)
	writeBool("showavgline", c.ShowAvgLine)
	writeBool("showcores", c.ShowCores)
	writeBool("showmem", c.ShowMem)
	writeBool("shownet", c.ShowNet)
	writeStr("sshopts", c.SSHOpts)
	writeStr("cluster", c.Cluster)
	return w.Flush()
}

func getClusterHostsRec(cluster, path string, depth int, seen map[string]bool) ([]string, error) {
	if depth > constants.CSSHMaxRecursion {
		return nil, fmt.Errorf("cluster recursion limit reached in %s (possible cycle)", path)
	}
	if seen == nil {
		seen = make(map[string]bool)
	}
	if seen[cluster] {
		return nil, fmt.Errorf("cluster cycle detected: %s", cluster)
	}

	f, err := os.Open(path)
	if err != nil {
		return nil, fmt.Errorf("open %s: %w", path, err)
	}
	defer f.Close()

	var line string
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		ln := strings.TrimSpace(scanner.Text())
		if ln == "" || strings.HasPrefix(ln, "#") {
			continue
		}
		fields := strings.Fields(ln)
		if len(fields) >= 1 && fields[0] == cluster {
			if len(fields) > 1 {
				line = strings.Join(fields[1:], " ")
			}
			break
		}
	}
	if err := scanner.Err(); err != nil {
		return nil, err
	}

	if line == "" {
		return []string{cluster}, nil
	}

	seen[cluster] = true
	defer delete(seen, cluster)

	var out []string
	for _, part := range strings.Fields(line) {
		hosts, err := getClusterHostsRec(part, path, depth+1, seen)
		if err != nil {
			return nil, err
		}
		out = append(out, hosts...)
	}
	return out, nil
}