Improve startup time, fiddle with completion

This commit is contained in:
kayos@tcp.direct 2023-02-06 08:16:36 -08:00
parent edfa970e26
commit 73a0e7b85a
Signed by: kayos
GPG Key ID: 4B841471B4BEE979
9 changed files with 213 additions and 75 deletions

@ -9,11 +9,10 @@ import (
"time" "time"
cli "git.tcp.direct/Mirrors/go-prompt" cli "git.tcp.direct/Mirrors/go-prompt"
"github.com/davecgh/go-spew/spew"
tui "github.com/manifoldco/promptui" tui "github.com/manifoldco/promptui"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/davecgh/go-spew/spew"
"github.com/google/shlex" "github.com/google/shlex"
"git.tcp.direct/kayos/ziggs/internal/common" "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} 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"} debuglevels := map[zerolog.Level]string{zerolog.InfoLevel: "info", zerolog.DebugLevel: "debug", zerolog.TraceLevel: "trace"}
if len(args) < 2 { if len(args) < 2 {
println("current debug level: " + debuglevels[log.GetLevel()]) log.Info().Msgf("current debug level: %s", debuglevels[log.GetLevel()])
return return
} }
if newlevel, ok := levelsdebug[args[1]]; ok { if newlevel, ok := levelsdebug[args[1]]; ok {
zerolog.SetGlobalLevel(newlevel) zerolog.SetGlobalLevel(newlevel)
} else { return
println("invalid argument: " + args[1])
} }
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": case "help":
if len(args) < 2 { if len(args) < 2 {
getHelp("") getHelp("")
@ -90,13 +101,6 @@ func executor(cmd string) {
getHelp(args[len(args)-1]) getHelp(args[len(args)-1])
case "clear": case "clear":
print("\033[H\033[2J") print("\033[H\033[2J")
case "debugcli":
if extraDebug {
extraDebug = false
} else {
extraDebug = true
}
spew.Dump(suggestions)
default: default:
if len(args) == 0 { if len(args) == 0 {
return return
@ -213,9 +217,12 @@ func saveHist() {
func StartCLI() { func StartCLI() {
log = config.GetLogger() log = config.GetLogger()
processBridges() processBridges()
grpmap := ziggy.GetGroupMap() go func() {
processGroups(grpmap) processGroups(ziggy.GetGroupMap())
processLights() processLights(ziggy.GetLightMap())
processScenes(ziggy.GetSceneMap())
}()
buildTime, _ := common.Version()
prompt = cli.New( prompt = cli.New(
executor, executor,
completer, completer,
@ -235,8 +242,7 @@ func StartCLI() {
} }
return fmt.Sprintf("ziggs[%s] %s ", sel.String(), bulb), true return fmt.Sprintf("ziggs[%s] %s ", sel.String(), bulb), true
}), }),
cli.OptionTitle("ziggs"), cli.OptionTitle("ziggs - built "+buildTime),
// cli.OptionCompletionOnDown(),
) )
prompt.Run() prompt.Run()

@ -2,8 +2,11 @@ package cli
import ( import (
"strings" "strings"
"sync"
cli "git.tcp.direct/Mirrors/go-prompt" cli "git.tcp.direct/Mirrors/go-prompt"
"github.com/davecgh/go-spew/spew"
"github.com/google/shlex"
) )
const ( const (
@ -16,51 +19,71 @@ type completion struct {
cli.Suggest cli.Suggest
inner *ziggsCommand inner *ziggsCommand
requires map[int]map[string]bool requires map[int]map[string]bool
callback func([]string) bool
isAlias bool isAlias bool
root bool root bool
} }
func (c completion) qualifies(line string) 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 return true
} case len(args) < len(c.requires):
verbose(red + "len(args) < len(c.requires)" + rst)
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)
}
return false return false
} case len(args)-2 > len(c.requires):
if len(args)-2 > len(c.requires) { verbose(red + "len(args)-2 > len(c.requires)" + rst)
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)
}
return false return false
default:
//
} }
var count = 0 var count = 0
for i, a := range args { for i, a := range args {
i++ i++
if _, ok := c.requires[i][a]; ok { if _, ok := c.requires[i][a]; ok {
if extraDebug { verbose("%v%s: found %s (count++) %v", grn, c.Text, a, rst)
log.Trace().Msgf("%v%s: found %s%v", grn, c.Text, a, rst)
}
count++ count++
} }
} }
if extraDebug && !(count >= len(c.requires)) { ok := count >= len(c.requires)
log.Trace().Msgf("%v%s: count(%d) < len(c.requires)(%d)", red, c.Text, 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
} }
var suggestions map[int]map[string]*completion return c.callback(args)
}
var (
suggestions map[int]map[string]*completion
suggestionMutex = &sync.RWMutex{}
)
func init() { func init() {
Commands["ls"] = newZiggsCommand(cmdList, "list all lights, groups, scenes, rules, and schedules", 0) Commands["ls"] = newZiggsCommand(cmdList, "list all lights, groups, scenes, rules, and schedules", 0)
@ -91,10 +114,15 @@ func init() {
} }
func initCompletion() { func initCompletion() {
suggestionMutex.Lock()
defer suggestionMutex.Unlock()
suggestions = make(map[int]map[string]*completion) suggestions = make(map[int]map[string]*completion)
suggestions[0] = make(map[string]*completion) suggestions[0] = make(map[string]*completion)
suggestions[1] = make(map[string]*completion) suggestions[1] = make(map[string]*completion)
suggestions[2] = 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: "lights"}, inner: Commands["lights"]},
{Suggest: cli.Suggest{Text: "groups"}, inner: Commands["groups"]}, {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: "dump"}, inner: Commands["dump"]},
{Suggest: cli.Suggest{Text: "load"}, inner: Commands["load"]}, {Suggest: cli.Suggest{Text: "load"}, inner: Commands["load"]},
{Suggest: cli.Suggest{Text: "use", Description: "select bridge to perform actions on"}}, {Suggest: cli.Suggest{Text: "use", Description: "select bridge to perform actions on"}},
{Suggest: cli.Suggest{Text: "exit", Description: "exit ziggs"}}, {Suggest: cli.Suggest{Text: "exit", Description: "exit ziggs"}},
*/ */
@ -156,16 +183,13 @@ func initCompletion() {
} }
func completer(in cli.Document) []cli.Suggest { 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 var head = len(infields) - 1
if head < 0 { if head < 0 {
head = 0 head = 0
} }
if head == 1 {
head = 1
}
if head > 0 && in.LastKeyStroke() == ' ' { if head > 0 && in.LastKeyStroke() == ' ' {
head++ head++
} }
@ -174,11 +198,15 @@ func completer(in cli.Document) []cli.Suggest {
log.Trace().Int("head", head).Msgf("completing %v", infields) log.Trace().Int("head", head).Msgf("completing %v", infields)
} }
var sugs []cli.Suggest var sugs []cli.Suggest
suggestionMutex.RLock()
defer suggestionMutex.RUnlock()
for _, sug := range suggestions[head] { for _, sug := range suggestions[head] {
if sug.qualifies(c) { if !sug.qualifies(c) {
if in.GetWordBeforeCursor() != "" && strings.HasPrefix(sug.Text, in.GetWordBeforeCursor()) { continue
sugs = append(sugs, sug.Suggest)
} }
if in.TextBeforeCursor() != "" && strings.Contains(strings.ToLower(sug.Text),
strings.ToLower(strings.TrimSpace(in.GetWordBeforeCursorWithSpace()))) {
sugs = append(sugs, sug.Suggest)
} }
} }
return sugs return sugs

@ -21,7 +21,7 @@ func cmdGet(bridge *ziggy.Bridge, args []string) error {
} }
var ( var (
groupMap map[string]*huego.Group groupMap map[string]*ziggy.HueGroup
lightMap map[string]*ziggy.HueLight lightMap map[string]*ziggy.HueLight
currentState *huego.State currentState *huego.State
argHead = -1 argHead = -1

@ -10,6 +10,9 @@ import (
var tabber = tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', tabwriter.AlignRight) var tabber = tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', tabwriter.AlignRight)
func getHelp(target string) { func getHelp(target string) {
suggestionMutex.RLock()
defer suggestionMutex.RUnlock()
if target != "" && target != "meta" { if target != "" && target != "meta" {
for _, su := range suggestions[0] { for _, su := range suggestions[0] {
if strings.Contains(strings.ToLower(su.Text), strings.ToLower(target)) { if strings.Contains(strings.ToLower(su.Text), strings.ToLower(target)) {

@ -2,38 +2,88 @@ package cli
import ( import (
cli "git.tcp.direct/Mirrors/go-prompt" cli "git.tcp.direct/Mirrors/go-prompt"
"github.com/amimof/huego"
"git.tcp.direct/kayos/ziggs/internal/ziggy" "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 { for grp, g := range grps {
suffix := "" suffix := ""
if g.Type != "" { if g.Type != "" {
suffix = " (" + g.Type + ")" suffix = " (" + g.Type + ")"
} }
suggestionMutex.Lock()
suggestions[2][grp] = &completion{ suggestions[2][grp] = &completion{
Suggest: cli.Suggest{ Suggest: cli.Suggest{
Text: grp, Text: grp,
Description: "Group" + suffix, Description: "Group" + suffix,
}, },
requires: map[int]map[string]bool{ 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}, 2: {"group": true, "g": true},
}, },
root: false, root: false,
} }
suggestionMutex.Unlock()
} }
} }
func processLights() { func processScenes(scns map[string]*ziggy.HueScene) {
for lt, l := range ziggy.GetLightMap() { 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 := "" suffix := ""
if l.Type != "" { if l.Type != "" {
suffix = " (" + l.Type + ")" suffix = " (" + l.Type + ")"
} }
suggestionMutex.Lock()
suggestions[2][lt] = &completion{ suggestions[2][lt] = &completion{
Suggest: cli.Suggest{ Suggest: cli.Suggest{
Text: lt, Text: lt,
@ -45,11 +95,13 @@ func processLights() {
}, },
root: false, root: false,
} }
suggestionMutex.Unlock()
} }
} }
func processBridges() { func processBridges() {
for brd, b := range ziggy.Lucifer.Bridges { for brd, b := range ziggy.Lucifer.Bridges {
suggestionMutex.Lock()
suggestions[1]["bridge"] = &completion{ suggestions[1]["bridge"] = &completion{
Suggest: cli.Suggest{ Suggest: cli.Suggest{
Text: brd, Text: brd,
@ -58,5 +110,6 @@ func processBridges() {
requires: map[int]map[string]bool{0: {"use": true, "u": true}}, requires: map[int]map[string]bool{0: {"use": true, "u": true}},
root: false, root: false,
} }
suggestionMutex.Unlock()
} }
} }

@ -27,9 +27,11 @@ type cmdTarget interface {
Effect(string) error Effect(string) error
} }
var ErrNotEnoughArguments = errors.New("not enough arguments")
func cmdSet(bridge *ziggy.Bridge, args []string) error { func cmdSet(bridge *ziggy.Bridge, args []string) error {
if len(args) < 3 { if len(args) < 3 {
return errors.New("not enough arguments") return ErrNotEnoughArguments
} }
type ( type (
@ -37,7 +39,7 @@ func cmdSet(bridge *ziggy.Bridge, args []string) error {
) )
var ( var (
groupMap map[string]*huego.Group groupMap map[string]*ziggy.HueGroup
lightMap map[string]*ziggy.HueLight lightMap map[string]*ziggy.HueLight
actions []action actions []action
currentState *huego.State currentState *huego.State
@ -141,7 +143,7 @@ func cmdSet(bridge *ziggy.Bridge, args []string) error {
}) })
case "hue", "h": case "hue", "h":
if len(args) == argHead-1 { if len(args) == argHead-1 {
return errors.New("not enough arguments") return ErrNotEnoughArguments
} }
argHead++ argHead++
newHue, numErr := strconv.Atoi(strings.TrimSpace(args[argHead])) newHue, numErr := strconv.Atoi(strings.TrimSpace(args[argHead]))
@ -157,7 +159,7 @@ func cmdSet(bridge *ziggy.Bridge, args []string) error {
}) })
case "saturation", "sat": case "saturation", "sat":
if len(args) == argHead-1 { if len(args) == argHead-1 {
return errors.New("not enough arguments") return ErrNotEnoughArguments
} }
argHead++ argHead++
newSat, numErr := strconv.Atoi(strings.TrimSpace(args[argHead])) newSat, numErr := strconv.Atoi(strings.TrimSpace(args[argHead]))
@ -173,7 +175,7 @@ func cmdSet(bridge *ziggy.Bridge, args []string) error {
}) })
case "effect", "e": case "effect", "e":
if len(args) == argHead-1 { if len(args) == argHead-1 {
return errors.New("not enough arguments") return ErrNotEnoughArguments
} }
argHead++ argHead++
newEffect := strings.TrimSpace(args[argHead]) newEffect := strings.TrimSpace(args[argHead])
@ -186,7 +188,7 @@ func cmdSet(bridge *ziggy.Bridge, args []string) error {
}) })
case "temperature", "temp": case "temperature", "temp":
if len(args) == argHead-1 { if len(args) == argHead-1 {
return errors.New("not enough arguments") return ErrNotEnoughArguments
} }
argHead++ argHead++
newTemp, numErr := strconv.Atoi(strings.TrimSpace(args[argHead])) newTemp, numErr := strconv.Atoi(strings.TrimSpace(args[argHead]))
@ -222,11 +224,11 @@ func cmdSet(bridge *ziggy.Bridge, args []string) error {
return nil return nil
case "scene", "sc": case "scene", "sc":
if len(args) == argHead-1 { if len(args) == argHead-1 {
return errors.New("not enough arguments") return ErrNotEnoughArguments
} }
argHead++ argHead++
if argHead > len(args)-1 { if argHead > len(args)-1 {
return errors.New("not enough arguments") return ErrNotEnoughArguments
} }
targetScene := strings.TrimSpace(args[argHead]) targetScene := strings.TrimSpace(args[argHead])
actions = append(actions, func() error { actions = append(actions, func() error {
@ -251,7 +253,7 @@ func cmdSet(bridge *ziggy.Bridge, args []string) error {
if target == nil { if target == nil {
return errors.New("no target specified") return errors.New("no target specified")
} }
tg, tgok := target.(*huego.Group) tg, tgok := target.(*ziggy.HueGroup)
tl, tlok := target.(*ziggy.HueLight) tl, tlok := target.(*ziggy.HueLight)
switch { switch {
case tgok: case tgok:

@ -63,6 +63,18 @@ type HueLight struct {
controller *Bridge 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 { func (hl *HueLight) Scene(s string) error {
return hl.Scene(s) return hl.Scene(s)
} }

@ -1,6 +1,6 @@
package ziggy package ziggy
import "github.com/amimof/huego" import "strconv"
// Multiplex is all of the lights (all of the lights). // Multiplex is all of the lights (all of the lights).
// I'll see myself out. // I'll see myself out.
@ -32,8 +32,8 @@ func GetLightMap() map[string]*HueLight {
return lightMap return lightMap
} }
func GetGroupMap() map[string]*huego.Group { func GetGroupMap() map[string]*HueGroup {
var groupMap = make(map[string]*huego.Group) var groupMap = make(map[string]*HueGroup)
for _, c := range Lucifer.Bridges { for _, c := range Lucifer.Bridges {
gs, err := c.GetGroups() gs, err := c.GetGroups()
if err != nil { 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) log.Warn().Msgf("duplicate group name %s on bridge %s - please rename", g.Name, c.ID)
continue continue
} }
groupMap[g.Name] = group hg := &HueGroup{Group: group, controller: c}
groupMap[g.Name] = hg
groupMap[strconv.Itoa(g.ID)] = hg
} }
} }
return groupMap return groupMap
} }
func GetSceneMap() map[string]*huego.Scene { func GetSceneMap() map[string]*HueScene {
var sceneMap = make(map[string]*huego.Scene) var sceneMap = make(map[string]*HueScene)
for _, c := range Lucifer.Bridges { for _, c := range Lucifer.Bridges {
scs, err := c.GetScenes() scs, err := c.GetScenes()
if err != nil { 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) log.Warn().Msgf("failed to get pointer for scene %s on bridge %s: %v", s.Name, c.ID, gerr)
continue 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) log.Warn().Msgf("duplicate scene name %s on bridge %s - please rename", s.Name, c.ID)
continue continue
} }
sceneMap[s.Name] = group
} }
} }
return sceneMap return sceneMap

@ -23,7 +23,7 @@ func (c *Bridge) FindLight(input string) (light *HueLight, err error) {
return &HueLight{Light: l, controller: c}, nil 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 var groupID int
if groupID, err = strconv.Atoi(input); err != nil { if groupID, err = strconv.Atoi(input); err != nil {
targ, ok := GetGroupMap()[input] targ, ok := GetGroupMap()[input]
@ -32,5 +32,34 @@ func (c *Bridge) FindGroup(input string) (light *huego.Group, err error) {
} }
return targ, nil 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
} }