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
This commit is contained in:
2026-03-27 16:35:10 +01:00
parent b9bcde4f46
commit 374282e5de
+97 -11
View File
@@ -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,23 +318,34 @@ 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)
var userClasses []string
for cls := range result.FreqCfgs {
if !builtinSet[cls] {
userClasses = append(userClasses, string(cls))
// 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
})
}
}
sort.Strings(userClasses)
// Emit built-in classes first
for _, cls := range classify.AllClasses() {
// 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)
@@ -319,8 +354,58 @@ func PrintConfig(result LoadResult) string {
fmt.Fprintf(&sb, "waveform = %q\n", waveformString(cfg.WaveformType))
fmt.Fprintf(&sb, "\n")
}
}
// Emit user-defined classes sorted alphabetically
// 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] {
userClasses = append(userClasses, string(cls))
}
}
sort.Strings(userClasses)
if len(userClasses) > 0 {
fmt.Fprintf(&sb, "# User-defined\n\n")
for _, clsStr := range userClasses {
cls := classify.TrafficClass(clsStr)
cfg := result.FreqCfgs[cls]
@@ -331,6 +416,7 @@ func PrintConfig(result LoadResult) string {
fmt.Fprintf(&sb, "waveform = %q\n", waveformString(cfg.WaveformType))
fmt.Fprintf(&sb, "\n")
}
}
return sb.String()
}