| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184 |
- 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
- }
|