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:
+110
-24
@@ -38,6 +38,7 @@ type RawRule struct {
|
|||||||
type rawConfig struct {
|
type rawConfig struct {
|
||||||
Sounds map[string]SoundOverride `toml:"sounds"`
|
Sounds map[string]SoundOverride `toml:"sounds"`
|
||||||
Rules []RawRule `toml:"rules"`
|
Rules []RawRule `toml:"rules"`
|
||||||
|
Groups map[string]string `toml:"groups"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadResult is the return type from Load(). It carries the merged FreqConfig map,
|
// 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.
|
// Add auto-freq entries BEFORE merge so that [sounds.X] overrides for user classes apply.
|
||||||
addAutoFreqEntries(freqCfgs, userRules, autoClasses)
|
addAutoFreqEntries(freqCfgs, userRules, autoClasses)
|
||||||
merge(freqCfgs, raw.Sounds)
|
merge(freqCfgs, raw.Sounds)
|
||||||
|
applyGroupOverrides(freqCfgs, raw.Groups)
|
||||||
|
|
||||||
return LoadResult{
|
return LoadResult{
|
||||||
FreqCfgs: freqCfgs,
|
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.
|
// PrintConfig returns the effective configuration as commented TOML output.
|
||||||
// The output includes a header with source path and generation date, an optional
|
// 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
|
// [[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{}
|
builtinSet := map[classify.TrafficClass]bool{}
|
||||||
for _, cls := range classify.AllClasses() {
|
for _, cls := range classify.AllClasses() {
|
||||||
builtinSet[cls] = true
|
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
|
var userClasses []string
|
||||||
for cls := range result.FreqCfgs {
|
for cls := range result.FreqCfgs {
|
||||||
if !builtinSet[cls] {
|
if !builtinSet[cls] {
|
||||||
@@ -308,28 +404,18 @@ func PrintConfig(result LoadResult) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
sort.Strings(userClasses)
|
sort.Strings(userClasses)
|
||||||
|
if len(userClasses) > 0 {
|
||||||
// Emit built-in classes first
|
fmt.Fprintf(&sb, "# User-defined\n\n")
|
||||||
for _, cls := range classify.AllClasses() {
|
for _, clsStr := range userClasses {
|
||||||
cfg := result.FreqCfgs[cls]
|
cls := classify.TrafficClass(clsStr)
|
||||||
annotation := classAnnotation(cls, cfg, result.AutoClasses)
|
cfg := result.FreqCfgs[cls]
|
||||||
fmt.Fprintf(&sb, "# %s -- %.1f Hz (%s)\n", string(cls), cfg.BaseHz, annotation)
|
annotation := classAnnotation(cls, cfg, result.AutoClasses)
|
||||||
fmt.Fprintf(&sb, "[sounds.%s]\n", string(cls))
|
fmt.Fprintf(&sb, "# %s -- %.1f Hz (%s)\n", clsStr, cfg.BaseHz, annotation)
|
||||||
fmt.Fprintf(&sb, "frequency = %.1f\n", cfg.BaseHz)
|
fmt.Fprintf(&sb, "[sounds.%s]\n", clsStr)
|
||||||
fmt.Fprintf(&sb, "waveform = %q\n", waveformString(cfg.WaveformType))
|
fmt.Fprintf(&sb, "frequency = %.1f\n", cfg.BaseHz)
|
||||||
fmt.Fprintf(&sb, "\n")
|
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")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return sb.String()
|
return sb.String()
|
||||||
|
|||||||
Reference in New Issue
Block a user