package config import ( "fmt" "regexp" "sort" "strings" ) // MultiConfig controls netssh multi. type MultiConfig struct { // Multiplexer is auto, tmux, or zellij; empty means auto. Multiplexer string `mapstructure:"multiplexer"` // PanesPerWindow splits larger host lists into more windows or tabs; 0 = 9. PanesPerWindow int `mapstructure:"panes_per_window"` // SyncKey and SoloKey are tmux prefix keys for broadcast to all panes and // for excluding the current pane; empty = default (S, e), "none" = off. SyncKey string `mapstructure:"sync_key"` SoloKey string `mapstructure:"solo_key"` } // Default tmux keys: unbound in a stock tmux (E would be "spread evenly"). const ( DefaultSyncKey = "S" DefaultSoloKey = "e" ) // Keys returns the tmux keys to bind; "" for a disabled one. func (m MultiConfig) Keys() (sync, solo string) { pick := func(key, fallback string) string { switch key { case "": return fallback case "none": return "" } return key } return pick(m.SyncKey, DefaultSyncKey), pick(m.SoloKey, DefaultSoloKey) } var tmuxKey = regexp.MustCompile(`^([CMS]-)*([A-Za-z0-9]|F[0-9]{1,2}|[-_=+!@#$%^&*,./?<>\[\]]|Tab|Space|Enter|BSpace|Up|Down|Left|Right|Home|End|PPage|NPage)$`) // DefaultPanesPerWindow keeps panes readable on a normal screen (3×3). const DefaultPanesPerWindow = 9 // ValidateMulti checks groups and the multi settings. func (c *Config) ValidateMulti() error { switch c.Multi.Multiplexer { case "", "auto", "tmux", "zellij": default: return fmt.Errorf("multi.multiplexer must be auto, tmux, or zellij") } if c.Multi.PanesPerWindow < 0 || c.Multi.PanesPerWindow > 64 { return fmt.Errorf("multi.panes_per_window must be between 1 and 64") } for name, key := range map[string]string{"sync_key": c.Multi.SyncKey, "solo_key": c.Multi.SoloKey} { if key != "" && key != "none" && !tmuxKey.MatchString(key) { return fmt.Errorf("multi.%s %q is not a tmux key (e.g. S, e, C-s, F5) or none", name, key) } } if sync, solo := c.Multi.Keys(); sync != "" && sync == solo { return fmt.Errorf("multi.sync_key and multi.solo_key must differ") } names := make([]string, 0, len(c.Groups)) for name := range c.Groups { names = append(names, name) } sort.Strings(names) for _, name := range names { if !instanceName.MatchString(name) { return fmt.Errorf("groups: name %q must contain only letters, digits, underscores, or hyphens", name) } if len(c.Groups[name]) == 0 { return fmt.Errorf("groups.%s: needs at least one host or selector", name) } for _, selector := range c.Groups[name] { if strings.TrimSpace(selector) == "" || strings.ContainsAny(selector, " \t\n") || strings.HasPrefix(selector, "-") { return fmt.Errorf("groups.%s: invalid entry %q", name, selector) } } } return nil } // PanesPerWindowOrDefault returns the configured value or the default. func (m MultiConfig) PanesPerWindowOrDefault() int { if m.PanesPerWindow > 0 { return m.PanesPerWindow } return DefaultPanesPerWindow }