From 374282e5dec9ea79c403d8e3109a0451669f3cf6 Mon Sep 17 00:00:00 2001 From: Markus Graf Date: Fri, 27 Mar 2026 16:35:10 +0100 Subject: [PATCH] feat(11-02): add [groups] TOML support and group-ordered PrintConfig - Add Groups map[string]string field to rawConfig struct - Add applyGroupOverrides function for [groups] reassignment with stderr warning on unknown class - Add groupOrder canonical slice for section header ordering - Refactor PrintConfig to emit group headers (Infrastructure, Web, Mail, Remote Access, File Transfer, Database, Discovery, VoIP, Unknown) - Classes within each group sorted by ascending BaseHz - User-defined classes emitted under '# User-defined' section - Non-canonical group names (user-invented) emitted in sorted order after canonical groups --- config/config.go | 134 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 110 insertions(+), 24 deletions(-) diff --git a/config/config.go b/config/config.go index bbccce0..dfc6fdb 100644 --- a/config/config.go +++ b/config/config.go @@ -38,6 +38,7 @@ type RawRule struct { type rawConfig struct { Sounds map[string]SoundOverride `toml:"sounds"` Rules []RawRule `toml:"rules"` + Groups map[string]string `toml:"groups"` } // LoadResult is the return type from Load(). It carries the merged FreqConfig map, @@ -99,6 +100,7 @@ func Load(configPath string) (LoadResult, error) { // Add auto-freq entries BEFORE merge so that [sounds.X] overrides for user classes apply. addAutoFreqEntries(freqCfgs, userRules, autoClasses) merge(freqCfgs, raw.Sounds) + applyGroupOverrides(freqCfgs, raw.Groups) return LoadResult{ FreqCfgs: freqCfgs, @@ -262,6 +264,28 @@ func waveformString(wt synth.WaveformType) string { } } +// applyGroupOverrides overlays [groups] reassignments onto freqCfgs.Group in-place. +// Unknown class names produce a warning to stderr (D-09). +// Unknown group names are silently accepted -- users can invent custom groups (D-09). +func applyGroupOverrides(cfgs map[classify.TrafficClass]synth.FreqConfig, groups map[string]string) { + for className, groupName := range groups { + cls := classify.TrafficClass(className) + cfg, known := cfgs[cls] + if !known { + fmt.Fprintf(os.Stderr, "Warning: config: [groups]: unknown class %q (ignored)\n", className) + continue + } + cfg.Group = groupName + cfgs[cls] = cfg + } +} + +// groupOrder defines the canonical display order for --print-config section headers (D-04). +var groupOrder = []string{ + "Infrastructure", "Web", "Mail", "Remote Access", + "File Transfer", "Database", "Discovery", "VoIP", "Unknown", +} + // PrintConfig returns the effective configuration as commented TOML output. // The output includes a header with source path and generation date, an optional // [[rules]] section for user-defined rules, and a [sounds.*] section for all @@ -294,13 +318,85 @@ func PrintConfig(result LoadResult) string { } } - // [sounds.*] section — built-in classes first, then user-defined sorted alphabetically + // [sounds.*] section — built-in classes grouped by family, then user-defined + + // Build group -> []TrafficClass index from AllClasses() + builtinByGroup := map[string][]classify.TrafficClass{} builtinSet := map[classify.TrafficClass]bool{} for _, cls := range classify.AllClasses() { builtinSet[cls] = true + cfg := result.FreqCfgs[cls] + grp := cfg.Group + builtinByGroup[grp] = append(builtinByGroup[grp], cls) } - // Collect user-defined classes (in FreqCfgs but not in AllClasses) + // Sort each group by ascending BaseHz (D-05) using result.FreqCfgs (effective Hz, not defaults) + for grp := range builtinByGroup { + classes := builtinByGroup[grp] + sort.Slice(classes, func(i, j int) bool { + return result.FreqCfgs[classes[i]].BaseHz < result.FreqCfgs[classes[j]].BaseHz + }) + } + + // Emit built-in classes grouped with headers (D-04) + for _, grp := range groupOrder { + classes, ok := builtinByGroup[grp] + if !ok || len(classes) == 0 { + continue + } + fmt.Fprintf(&sb, "# %s\n\n", grp) + for _, cls := range classes { + cfg := result.FreqCfgs[cls] + annotation := classAnnotation(cls, cfg, result.AutoClasses) + fmt.Fprintf(&sb, "# %s -- %.1f Hz (%s)\n", string(cls), cfg.BaseHz, annotation) + fmt.Fprintf(&sb, "[sounds.%s]\n", string(cls)) + fmt.Fprintf(&sb, "frequency = %.1f\n", cfg.BaseHz) + fmt.Fprintf(&sb, "waveform = %q\n", waveformString(cfg.WaveformType)) + fmt.Fprintf(&sb, "\n") + } + } + + // Check for custom groups (from [groups] reassignment) that are not in groupOrder. + // For non-canonical group names (user-invented), collect and emit separately. + customGroups := map[string][]classify.TrafficClass{} + for _, cls := range classify.AllClasses() { + cfg := result.FreqCfgs[cls] + grp := cfg.Group + isCanonical := false + for _, cg := range groupOrder { + if grp == cg { + isCanonical = true + break + } + } + if !isCanonical { + customGroups[grp] = append(customGroups[grp], cls) + } + } + // Sort and emit custom group sections + var customGroupNames []string + for grp := range customGroups { + customGroupNames = append(customGroupNames, grp) + } + sort.Strings(customGroupNames) + for _, grp := range customGroupNames { + classes := customGroups[grp] + sort.Slice(classes, func(i, j int) bool { + return result.FreqCfgs[classes[i]].BaseHz < result.FreqCfgs[classes[j]].BaseHz + }) + fmt.Fprintf(&sb, "# %s\n\n", grp) + for _, cls := range classes { + cfg := result.FreqCfgs[cls] + annotation := classAnnotation(cls, cfg, result.AutoClasses) + fmt.Fprintf(&sb, "# %s -- %.1f Hz (%s)\n", string(cls), cfg.BaseHz, annotation) + fmt.Fprintf(&sb, "[sounds.%s]\n", string(cls)) + fmt.Fprintf(&sb, "frequency = %.1f\n", cfg.BaseHz) + fmt.Fprintf(&sb, "waveform = %q\n", waveformString(cfg.WaveformType)) + fmt.Fprintf(&sb, "\n") + } + } + + // Emit user-defined classes (in FreqCfgs but not in AllClasses) under "# User-defined" (D-06) var userClasses []string for cls := range result.FreqCfgs { if !builtinSet[cls] { @@ -308,28 +404,18 @@ func PrintConfig(result LoadResult) string { } } sort.Strings(userClasses) - - // Emit built-in classes first - for _, cls := range classify.AllClasses() { - cfg := result.FreqCfgs[cls] - annotation := classAnnotation(cls, cfg, result.AutoClasses) - fmt.Fprintf(&sb, "# %s -- %.1f Hz (%s)\n", string(cls), cfg.BaseHz, annotation) - fmt.Fprintf(&sb, "[sounds.%s]\n", string(cls)) - fmt.Fprintf(&sb, "frequency = %.1f\n", cfg.BaseHz) - fmt.Fprintf(&sb, "waveform = %q\n", waveformString(cfg.WaveformType)) - fmt.Fprintf(&sb, "\n") - } - - // Emit user-defined classes sorted alphabetically - for _, clsStr := range userClasses { - cls := classify.TrafficClass(clsStr) - cfg := result.FreqCfgs[cls] - annotation := classAnnotation(cls, cfg, result.AutoClasses) - fmt.Fprintf(&sb, "# %s -- %.1f Hz (%s)\n", clsStr, cfg.BaseHz, annotation) - fmt.Fprintf(&sb, "[sounds.%s]\n", clsStr) - fmt.Fprintf(&sb, "frequency = %.1f\n", cfg.BaseHz) - fmt.Fprintf(&sb, "waveform = %q\n", waveformString(cfg.WaveformType)) - fmt.Fprintf(&sb, "\n") + if len(userClasses) > 0 { + fmt.Fprintf(&sb, "# User-defined\n\n") + for _, clsStr := range userClasses { + cls := classify.TrafficClass(clsStr) + cfg := result.FreqCfgs[cls] + annotation := classAnnotation(cls, cfg, result.AutoClasses) + fmt.Fprintf(&sb, "# %s -- %.1f Hz (%s)\n", clsStr, cfg.BaseHz, annotation) + fmt.Fprintf(&sb, "[sounds.%s]\n", clsStr) + fmt.Fprintf(&sb, "frequency = %.1f\n", cfg.BaseHz) + fmt.Fprintf(&sb, "waveform = %q\n", waveformString(cfg.WaveformType)) + fmt.Fprintf(&sb, "\n") + } } return sb.String()