diff --git a/internal/cli/cli.go b/internal/cli/cli.go index e3a9c1a..c7c9a41 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -9,11 +9,10 @@ import ( "time" cli "git.tcp.direct/Mirrors/go-prompt" + "github.com/davecgh/go-spew/spew" tui "github.com/manifoldco/promptui" "github.com/rs/zerolog" - "github.com/davecgh/go-spew/spew" - "github.com/google/shlex" "git.tcp.direct/kayos/ziggs/internal/common" @@ -74,14 +73,26 @@ func executor(cmd string) { levelsdebug := map[string]zerolog.Level{"info": zerolog.InfoLevel, "debug": zerolog.DebugLevel, "trace": zerolog.TraceLevel} debuglevels := map[zerolog.Level]string{zerolog.InfoLevel: "info", zerolog.DebugLevel: "debug", zerolog.TraceLevel: "trace"} if len(args) < 2 { - println("current debug level: " + debuglevels[log.GetLevel()]) + log.Info().Msgf("current debug level: %s", debuglevels[log.GetLevel()]) return } if newlevel, ok := levelsdebug[args[1]]; ok { zerolog.SetGlobalLevel(newlevel) - } else { - println("invalid argument: " + args[1]) + return } + if args[1] == "debugcli" || args[1] == "cli" { + if extraDebug { + extraDebug = false + log.Info().Msg("disabled cli debug") + } else { + extraDebug = true + log.Info().Msgf("dumping suggestions") + spew.Dump(suggestions) + log.Info().Msg("enabled cli debug") + } + return + } + return case "help": if len(args) < 2 { getHelp("") @@ -90,13 +101,6 @@ func executor(cmd string) { getHelp(args[len(args)-1]) case "clear": print("\033[H\033[2J") - case "debugcli": - if extraDebug { - extraDebug = false - } else { - extraDebug = true - } - spew.Dump(suggestions) default: if len(args) == 0 { return @@ -213,9 +217,12 @@ func saveHist() { func StartCLI() { log = config.GetLogger() processBridges() - grpmap := ziggy.GetGroupMap() - processGroups(grpmap) - processLights() + go func() { + processGroups(ziggy.GetGroupMap()) + processLights(ziggy.GetLightMap()) + processScenes(ziggy.GetSceneMap()) + }() + buildTime, _ := common.Version() prompt = cli.New( executor, completer, @@ -235,8 +242,7 @@ func StartCLI() { } return fmt.Sprintf("ziggs[%s] %s ", sel.String(), bulb), true }), - cli.OptionTitle("ziggs"), - // cli.OptionCompletionOnDown(), + cli.OptionTitle("ziggs - built "+buildTime), ) prompt.Run() diff --git a/internal/cli/completer.go b/internal/cli/completer.go index c267f3a..f2082e9 100644 --- a/internal/cli/completer.go +++ b/internal/cli/completer.go @@ -2,8 +2,11 @@ package cli import ( "strings" + "sync" cli "git.tcp.direct/Mirrors/go-prompt" + "github.com/davecgh/go-spew/spew" + "github.com/google/shlex" ) const ( @@ -16,51 +19,71 @@ type completion struct { cli.Suggest inner *ziggsCommand requires map[int]map[string]bool + callback func([]string) bool isAlias bool root bool } func (c completion) qualifies(line string) bool { - args := strings.Fields(line) + args, err := shlex.Split(line) + if err != nil { + log.Warn().Err(err).Msg("shlex.Split failed") + return false + } - if len(args) <= 1 && c.root { + verbose := func(msg string, args ...interface{}) { + if !extraDebug { + return + } + log.Trace().Caller(1). + Int("len(args)", len(args)). + Int("len(c.requires)", len(c.requires)).Msgf(msg, args...) + } + + if extraDebug { + spew.Dump(args) + } + + switch { + case len(args) <= 1 && c.root: + verbose("%v%s: len(args) <= 1 && c.root", grn, c.Text) return true - } - - if len(args) < len(c.requires) { - if extraDebug { - log.Trace().Int("len(args)", len(args)).Int("len(c.requires)", len(c.requires)). - Msg(red + "len(args) < len(c.requires)" + rst) - } + case len(args) < len(c.requires): + verbose(red + "len(args) < len(c.requires)" + rst) return false - } - if len(args)-2 > len(c.requires) { - if extraDebug { - log.Trace().Int("len(args)-2", len(args)-2).Int("len(c.requires)", len(c.requires)). - Msg(red + "len(args)-2 > len(c.requires)" + rst) - } + case len(args)-2 > len(c.requires): + verbose(red + "len(args)-2 > len(c.requires)" + rst) return false + default: + // } var count = 0 for i, a := range args { i++ if _, ok := c.requires[i][a]; ok { - if extraDebug { - log.Trace().Msgf("%v%s: found %s%v", grn, c.Text, a, rst) - } + verbose("%v%s: found %s (count++) %v", grn, c.Text, a, rst) count++ } } - if extraDebug && !(count >= len(c.requires)) { - log.Trace().Msgf("%v%s: count(%d) < len(c.requires)(%d)", red, c.Text, count, len(c.requires)) + ok := count >= len(c.requires) + if !ok { + verbose("%v%s: count(%d) < len(c.requires)(%d)", red, c.Text, count, len(c.requires)) + return false } - return count >= len(c.requires) + if c.callback == nil { + return true + } + + return c.callback(args) } -var suggestions map[int]map[string]*completion +var ( + suggestions map[int]map[string]*completion + suggestionMutex = &sync.RWMutex{} +) func init() { Commands["ls"] = newZiggsCommand(cmdList, "list all lights, groups, scenes, rules, and schedules", 0) @@ -91,10 +114,15 @@ func init() { } func initCompletion() { + suggestionMutex.Lock() + defer suggestionMutex.Unlock() + suggestions = make(map[int]map[string]*completion) suggestions[0] = make(map[string]*completion) suggestions[1] = make(map[string]*completion) suggestions[2] = make(map[string]*completion) + suggestions[3] = make(map[string]*completion) + suggestions[4] = make(map[string]*completion) /* {Suggest: cli.Suggest{Text: "lights"}, inner: Commands["lights"]}, {Suggest: cli.Suggest{Text: "groups"}, inner: Commands["groups"]}, @@ -111,7 +139,6 @@ func initCompletion() { {Suggest: cli.Suggest{Text: "dump"}, inner: Commands["dump"]}, {Suggest: cli.Suggest{Text: "load"}, inner: Commands["load"]}, {Suggest: cli.Suggest{Text: "use", Description: "select bridge to perform actions on"}}, - {Suggest: cli.Suggest{Text: "exit", Description: "exit ziggs"}}, */ @@ -156,16 +183,13 @@ func initCompletion() { } func completer(in cli.Document) []cli.Suggest { - c := in.CurrentLine() + c := in.Text - infields := strings.Fields(c) + infields, _ := shlex.Split(c) var head = len(infields) - 1 if head < 0 { head = 0 } - if head == 1 { - head = 1 - } if head > 0 && in.LastKeyStroke() == ' ' { head++ } @@ -174,11 +198,15 @@ func completer(in cli.Document) []cli.Suggest { log.Trace().Int("head", head).Msgf("completing %v", infields) } var sugs []cli.Suggest + suggestionMutex.RLock() + defer suggestionMutex.RUnlock() for _, sug := range suggestions[head] { - if sug.qualifies(c) { - if in.GetWordBeforeCursor() != "" && strings.HasPrefix(sug.Text, in.GetWordBeforeCursor()) { - sugs = append(sugs, sug.Suggest) - } + if !sug.qualifies(c) { + continue + } + if in.TextBeforeCursor() != "" && strings.Contains(strings.ToLower(sug.Text), + strings.ToLower(strings.TrimSpace(in.GetWordBeforeCursorWithSpace()))) { + sugs = append(sugs, sug.Suggest) } } return sugs diff --git a/internal/cli/get.go b/internal/cli/get.go index 27691b0..8e1a129 100644 --- a/internal/cli/get.go +++ b/internal/cli/get.go @@ -21,7 +21,7 @@ func cmdGet(bridge *ziggy.Bridge, args []string) error { } var ( - groupMap map[string]*huego.Group + groupMap map[string]*ziggy.HueGroup lightMap map[string]*ziggy.HueLight currentState *huego.State argHead = -1 diff --git a/internal/cli/help.go b/internal/cli/help.go index 0a9ed5e..2392d1d 100644 --- a/internal/cli/help.go +++ b/internal/cli/help.go @@ -10,6 +10,9 @@ import ( var tabber = tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', tabwriter.AlignRight) func getHelp(target string) { + suggestionMutex.RLock() + defer suggestionMutex.RUnlock() + if target != "" && target != "meta" { for _, su := range suggestions[0] { if strings.Contains(strings.ToLower(su.Text), strings.ToLower(target)) { diff --git a/internal/cli/process.go b/internal/cli/process.go index 0c273d1..d72c18b 100644 --- a/internal/cli/process.go +++ b/internal/cli/process.go @@ -2,38 +2,88 @@ package cli import ( cli "git.tcp.direct/Mirrors/go-prompt" - "github.com/amimof/huego" "git.tcp.direct/kayos/ziggs/internal/ziggy" ) -func processGroups(grps map[string]*huego.Group) { +func processGroups(grps map[string]*ziggy.HueGroup) { for grp, g := range grps { suffix := "" if g.Type != "" { suffix = " (" + g.Type + ")" } - + suggestionMutex.Lock() suggestions[2][grp] = &completion{ Suggest: cli.Suggest{ Text: grp, Description: "Group" + suffix, }, requires: map[int]map[string]bool{ - 1: {"set": true, "s": true, "delete": true, "d": true}, + 1: {"set": true, "s": true, "delete": true, "d": true, "get": true, "dump": true}, 2: {"group": true, "g": true}, }, root: false, } + suggestionMutex.Unlock() } } -func processLights() { - for lt, l := range ziggy.GetLightMap() { +func processScenes(scns map[string]*ziggy.HueScene) { + for scn, s := range scns { + suffix := "" + if s.Type != "" { + suffix = " (" + s.Type + ")" + } + suggestionMutex.Lock() + suggestions[4][scn] = &completion{ + Suggest: cli.Suggest{ + Text: scn, + Description: "Scene" + suffix, + }, + requires: map[int]map[string]bool{ + 1: {"set": true, "s": true, "delete": true, "d": true, "get": true, "dump": true}, + 2: {"group": true, "g": true, "scene": true, "s": true, "light": true, "l": true}, + 4: {"scene": true, "s": true}, + }, + callback: func(args []string) bool { + if extraDebug { + log.Trace().Msgf("Checking if scene %s belongs to group %s, their group is %s", + s.Name, args[3], s.Group) + } + if len(args) < 4 { + return false + } + delGetDumpOnly := args[1] == "scene" || args[1] == "s" + switch { + case delGetDumpOnly && args[3] == "scene" || args[3] == "s": + return false + case delGetDumpOnly && args[0] == "set": + return false + case args[1] == "group" || args[1] == "g": + if extraDebug { + log.Trace().Msgf("Checking if group %s is %s", args[3], s.Group) + } + if args[3] == s.Group { + return true + } + default: + return false + } + return false + }, + root: false, + } + suggestionMutex.Unlock() + } +} + +func processLights(lghts map[string]*ziggy.HueLight) { + for lt, l := range lghts { suffix := "" if l.Type != "" { suffix = " (" + l.Type + ")" } + suggestionMutex.Lock() suggestions[2][lt] = &completion{ Suggest: cli.Suggest{ Text: lt, @@ -45,11 +95,13 @@ func processLights() { }, root: false, } + suggestionMutex.Unlock() } } func processBridges() { for brd, b := range ziggy.Lucifer.Bridges { + suggestionMutex.Lock() suggestions[1]["bridge"] = &completion{ Suggest: cli.Suggest{ Text: brd, @@ -58,5 +110,6 @@ func processBridges() { requires: map[int]map[string]bool{0: {"use": true, "u": true}}, root: false, } + suggestionMutex.Unlock() } } diff --git a/internal/cli/set.go b/internal/cli/set.go index 9301d5a..33b5622 100644 --- a/internal/cli/set.go +++ b/internal/cli/set.go @@ -27,9 +27,11 @@ type cmdTarget interface { Effect(string) error } +var ErrNotEnoughArguments = errors.New("not enough arguments") + func cmdSet(bridge *ziggy.Bridge, args []string) error { if len(args) < 3 { - return errors.New("not enough arguments") + return ErrNotEnoughArguments } type ( @@ -37,7 +39,7 @@ func cmdSet(bridge *ziggy.Bridge, args []string) error { ) var ( - groupMap map[string]*huego.Group + groupMap map[string]*ziggy.HueGroup lightMap map[string]*ziggy.HueLight actions []action currentState *huego.State @@ -141,7 +143,7 @@ func cmdSet(bridge *ziggy.Bridge, args []string) error { }) case "hue", "h": if len(args) == argHead-1 { - return errors.New("not enough arguments") + return ErrNotEnoughArguments } argHead++ newHue, numErr := strconv.Atoi(strings.TrimSpace(args[argHead])) @@ -157,7 +159,7 @@ func cmdSet(bridge *ziggy.Bridge, args []string) error { }) case "saturation", "sat": if len(args) == argHead-1 { - return errors.New("not enough arguments") + return ErrNotEnoughArguments } argHead++ newSat, numErr := strconv.Atoi(strings.TrimSpace(args[argHead])) @@ -173,7 +175,7 @@ func cmdSet(bridge *ziggy.Bridge, args []string) error { }) case "effect", "e": if len(args) == argHead-1 { - return errors.New("not enough arguments") + return ErrNotEnoughArguments } argHead++ newEffect := strings.TrimSpace(args[argHead]) @@ -186,7 +188,7 @@ func cmdSet(bridge *ziggy.Bridge, args []string) error { }) case "temperature", "temp": if len(args) == argHead-1 { - return errors.New("not enough arguments") + return ErrNotEnoughArguments } argHead++ newTemp, numErr := strconv.Atoi(strings.TrimSpace(args[argHead])) @@ -222,11 +224,11 @@ func cmdSet(bridge *ziggy.Bridge, args []string) error { return nil case "scene", "sc": if len(args) == argHead-1 { - return errors.New("not enough arguments") + return ErrNotEnoughArguments } argHead++ if argHead > len(args)-1 { - return errors.New("not enough arguments") + return ErrNotEnoughArguments } targetScene := strings.TrimSpace(args[argHead]) actions = append(actions, func() error { @@ -251,7 +253,7 @@ func cmdSet(bridge *ziggy.Bridge, args []string) error { if target == nil { return errors.New("no target specified") } - tg, tgok := target.(*huego.Group) + tg, tgok := target.(*ziggy.HueGroup) tl, tlok := target.(*ziggy.HueLight) switch { case tgok: diff --git a/internal/ziggy/lights.go b/internal/ziggy/lights.go index 536d0dc..4a92620 100644 --- a/internal/ziggy/lights.go +++ b/internal/ziggy/lights.go @@ -63,6 +63,18 @@ type HueLight struct { controller *Bridge } +type HueGroup struct { + *huego.Group + scenes map[string]*HueScene + controller *Bridge +} + +type HueScene struct { + *huego.Scene + controller *Bridge + group *HueGroup +} + func (hl *HueLight) Scene(s string) error { return hl.Scene(s) } diff --git a/internal/ziggy/multiplexor.go b/internal/ziggy/multiplexor.go index cdb90c9..8e6c254 100644 --- a/internal/ziggy/multiplexor.go +++ b/internal/ziggy/multiplexor.go @@ -1,6 +1,6 @@ package ziggy -import "github.com/amimof/huego" +import "strconv" // Multiplex is all of the lights (all of the lights). // I'll see myself out. @@ -32,8 +32,8 @@ func GetLightMap() map[string]*HueLight { return lightMap } -func GetGroupMap() map[string]*huego.Group { - var groupMap = make(map[string]*huego.Group) +func GetGroupMap() map[string]*HueGroup { + var groupMap = make(map[string]*HueGroup) for _, c := range Lucifer.Bridges { gs, err := c.GetGroups() if err != nil { @@ -50,14 +50,16 @@ func GetGroupMap() map[string]*huego.Group { log.Warn().Msgf("duplicate group name %s on bridge %s - please rename", g.Name, c.ID) continue } - groupMap[g.Name] = group + hg := &HueGroup{Group: group, controller: c} + groupMap[g.Name] = hg + groupMap[strconv.Itoa(g.ID)] = hg } } return groupMap } -func GetSceneMap() map[string]*huego.Scene { - var sceneMap = make(map[string]*huego.Scene) +func GetSceneMap() map[string]*HueScene { + var sceneMap = make(map[string]*HueScene) for _, c := range Lucifer.Bridges { scs, err := c.GetScenes() if err != nil { @@ -70,11 +72,14 @@ func GetSceneMap() map[string]*huego.Scene { log.Warn().Msgf("failed to get pointer for scene %s on bridge %s: %v", s.Name, c.ID, gerr) continue } - if _, ok := sceneMap[s.Name]; ok { + if _, ok := sceneMap[s.Name]; !ok { + sceneMap[s.Name] = &HueScene{Scene: group, controller: c} + continue + } + if _, ok := sceneMap[s.Name+"-2"]; ok { log.Warn().Msgf("duplicate scene name %s on bridge %s - please rename", s.Name, c.ID) continue } - sceneMap[s.Name] = group } } return sceneMap diff --git a/internal/ziggy/util.go b/internal/ziggy/util.go index 7087d8b..784a5aa 100644 --- a/internal/ziggy/util.go +++ b/internal/ziggy/util.go @@ -23,7 +23,7 @@ func (c *Bridge) FindLight(input string) (light *HueLight, err error) { return &HueLight{Light: l, controller: c}, nil } -func (c *Bridge) FindGroup(input string) (light *huego.Group, err error) { +func (c *Bridge) FindGroup(input string) (light *HueGroup, err error) { var groupID int if groupID, err = strconv.Atoi(input); err != nil { targ, ok := GetGroupMap()[input] @@ -32,5 +32,34 @@ func (c *Bridge) FindGroup(input string) (light *huego.Group, err error) { } return targ, nil } - return c.GetGroup(groupID) + var hg *huego.Group + if hg, err = c.GetGroup(groupID); err != nil { + return nil, err + } + + return &HueGroup{Group: hg, controller: c}, nil +} + +func (hg *HueGroup) Scenes() ([]*HueScene, error) { + scenes, err := hg.controller.GetScenes() + if err != nil { + return nil, err + } + var ret []*HueScene + for _, s := range scenes { + intID, err := strconv.Atoi(s.Group) + if err != nil { + log.Warn().Msgf("unable to parse group ID from scene %s: %v", s.Name, err) + } + if intID != hg.ID { + continue + } + s, err := hg.controller.GetScene(s.ID) + if err != nil { + log.Warn().Msgf("unable to get scene pointer for scene %s: %v", s.Name, err) + return nil, err + } + ret = append(ret, &HueScene{Scene: s, controller: hg.controller}) + } + return ret, nil }