package main import ( "errors" "fmt" "sort" "strings" "netdome.biz/paul/qmk-rgb/internal/via" ) // zoneAll is the zone name that means every channel the keyboard reports. const zoneAll = "all" // resolveZoneName maps a zone argument to the channels it may mean, without // opening the keyboard. A display name of the connected board wins over a // subsystem name, and a value that would mean two channels is refused rather // than resolved to one of them. // // The argument is one name or several, comma separated, and the channels come // back in channel order however they were written, so a caller sees the same // order for every spelling of the same selection. // // An empty argument means every channel and is reported as a nil slice, which is // how a command that only reads reports the whole keyboard. func resolveZoneName(name string, display map[uint16]string, alternatives map[uint16][]string) ([]via.Channel, error) { if name == "" { return nil, nil } parts := strings.Split(name, ",") var channels []via.Channel for _, part := range parts { part = strings.TrimSpace(part) if part == "" { return nil, fmt.Errorf("zone %q names an empty channel: write several channels comma separated, like side,logo", name) } // all means every channel, so naming it next to one more channel is the // same request as naming it alone: the union is the whole keyboard. if strings.EqualFold(part, zoneAll) { return nil, nil } found, err := resolveOneZoneName(part, display, alternatives) if errors.Is(err, errNotAZoneName) { return nil, unknownZoneError(part, name) } if err != nil { return nil, err } channels = append(channels, found...) } return sortChannels(dedupeChannels(channels)), nil } // errNotAZoneName says the argument is not a name this board or this tool knows. // The caller turns it into the message, because the message has to quote the // whole list when there is one: "unknown zone" in a list of four is otherwise a // claim about a word the user never wrote on its own. var errNotAZoneName = errors.New("not a zone name") // unknownZoneError names the part that could not be placed, the list it came // from, and the two vocabularies that would have placed it. func unknownZoneError(part, whole string) error { quoted := fmt.Sprintf("unknown zone %q", part) if part != whole { quoted += fmt.Sprintf(" in %q", whole) } return fmt.Errorf("%s: use a channel name such as rgb_matrix, the name this keyboard's definition gives it, or %s for all of them", quoted, zoneAll) } // resolveOneZoneName maps a single channel name to the channels it may mean. func resolveOneZoneName(name string, display map[uint16]string, alternatives map[uint16][]string) ([]via.Channel, error) { if channels := dedupeChannels(append(displayNameChannels(display, name), alternateNameChannels(alternatives, name)...)); len(channels) > 1 { return nil, fmt.Errorf("zone %q matches several channels of this keyboard; its definition gives the same name to more than one", name) } else if len(channels) == 1 { return channels, nil } for c := via.Channel(1); c <= via.AssignedChannelMax; c++ { if c.Subsystem() == name { return []via.Channel{c}, nil } } return nil, errNotAZoneName } // displayNameChannels returns every channel the board names displayName. The // comparison ignores case, because a definition writes "Backlight" where a user // types "backlight", and the two mean the same channel. func displayNameChannels(display map[uint16]string, displayName string) []via.Channel { var channels []via.Channel for number, name := range display { if strings.EqualFold(name, displayName) { channels = append(channels, via.Channel(number)) } } sort.Slice(channels, func(i, j int) bool { return channels[i] < channels[j] }) return channels } // alternateNameChannels returns the channels a name reaches through the // alternatives a channel carries, which is how a QMK subsystem name keeps // working for a board whose definition calls that channel something else. func alternateNameChannels(alternatives map[uint16][]string, displayName string) []via.Channel { var channels []via.Channel for number, names := range alternatives { for _, name := range names { if strings.EqualFold(name, displayName) { channels = append(channels, via.Channel(number)) break } } } sort.Slice(channels, func(i, j int) bool { return channels[i] < channels[j] }) return channels } // displayNameConflicts rejects a board whose display name is also the subsystem // name of a different channel the keyboard actually has. A name that means two // channels is a silent retarget, so the tool refuses it. // // Presence is only known after the probe, which is why this is checked here // rather than when the file is read: a static check would reject the Impact 80, // which calls channel 3 "backlight" while channel 1 is absent. func displayNameConflicts(display map[uint16]string, present []via.Channel) error { names := make([]string, 0, len(display)) for _, name := range display { names = append(names, name) } sort.Strings(names) for _, name := range names { named := via.Channel(0) for _, ch := range displayNameChannels(display, name) { named = ch break } for _, other := range present { if other != named && other.Subsystem() == name { return fmt.Errorf("channel %d is named %q, which is also the subsystem name of channel %d", named, name, other) } } } return nil } // channelName is how a channel is printed: the board's display name when it has // one, otherwise the QMK subsystem name. func channelName(c via.Channel, display map[uint16]string) string { if name, ok := display[uint16(c)]; ok { return name } return c.Subsystem() } // sortChannels puts channels in channel order, which is the order the tool // reports them in everywhere else. A list of names is written in whatever order // suits the user; the channels it resolves to still come back in channel order, // so a script sees one order for every spelling of the same selection. func sortChannels(channels []via.Channel) []via.Channel { if len(channels) < 2 { return channels } sort.Slice(channels, func(i, j int) bool { return channels[i] < channels[j] }) return channels } // dedupeChannels drops repeats, keeping the order. A name can reach the same // channel twice — once as the definition's label and once as the subsystem name // it replaces — and that is one channel, not a conflict. func dedupeChannels(channels []via.Channel) []via.Channel { seen := make(map[via.Channel]bool, len(channels)) out := make([]via.Channel, 0, len(channels)) for _, ch := range channels { if seen[ch] { continue } seen[ch] = true out = append(out, ch) } return out }