package argvpb // This is where the actual autocomplete happens // lots of the fun magic is in here import ( "errors" "fmt" "os" "strings" "time" "go.wit.com/lib/cobol" "go.wit.com/lib/config" "go.wit.com/lib/env" durationpb "google.golang.org/protobuf/types/known/durationpb" ) // This function behaves oddly. It works in several different ways. It can: // - Initialize a protobuf and returns it to the application // - Help a user generate shell completion support & os.Exit // - figures out what to print to Stdout & Stderr and then does os.Exit() func Autocomplete() *Argv { // parses os.Args into the protobuf me.pb.parseOsArgs() // initializes the lib/env library env.Init(me.pb.AppInfo.APPNAME, me.pb.AppInfo.VERSION, cobol.Time(me.pb.AppInfo.BUILDTIME), me.pb.Real, GoodExit, BadExit) // open the argv cache history file to figure out the timing examineArgvHistory() // try to register bash args for go-args // arg.Register(&ArgvBash) // todo: figure this out if me.guiFunc != nil { // register gui args me.guiFunc() // fmt.Println("gui init") } else { // fmt.Println("no gui init") } // user is trying to setup bash or zsh autocomplete // --bash or --zsh is the first os.Args if me.setupAuto { // --bash or --zsh was passed. try to configure bash-completion MakeAutocompleteFiles(me.pb.AppInfo.APPNAME) // never forget to exit here or you will hate yourself and the choices you have made saveAndExit() } env.SetGlobal("argv", "real", fmt.Sprintf("%v", me.pb.Real)) for _, a := range me.pb.Real { if strings.HasPrefix(a, "--") { env.SetGlobal("argv", a, "true") } } if !me.isAuto { // not autocompleting. return to the application // save the pb & history // savePB() // me.pp = arg.MustParse(dest) me.Err = errors.Join(me.Err, me.mustParseFunc()) fmt.Fprintf(Stderr, "did mustParseFunc(). heading to forge. err(%v)\n", me.Err) doStdoutStderr() return me.pb } fmt.Fprintf(Stderr, "heading to autocomplete. err(%v)\n", me.Err) prepareStdout() // print to Stdout & Stderr saveAndExit() // never gets here return me.pb } // doAutocomplete(): print what is needed to Stdout & Stderr, then exit // // everything sent to STDOUT and STDERR matters past this point // any "junk" output, hidden fmt(), print() or log() statements are bad func prepareStdout() { if me.debug { // add an initial debug line me.pb.PrintDebug(me.last.GetCmd()) } if strings.HasPrefix(me.pb.Partial, "'--argv") { fmt.Fprintf(Stdout, " --argvdebug --argvhelp") me.pb.Stderr += fmt.Sprintln("argv override") me.debug = true return } // highjack "--gui" if len(me.pb.Real) > 1 { lastarg := me.pb.Real[len(me.pb.Real)-1] // this is a work in progress if lastarg == "--gui" { me.pb.Debugf("DEBUG: real=(%v) found --gui", me.pb.Real) PB.Stdout += " andlabs gogui" return } else { // me.pb.Debugf("DEBUG: NO MATCH last='%s' found key '%s' = %s", last, key, val) } } me.pb.HelpCounter = me.last.HelpCounter fmt.Fprintf(Stderr, "got to HelpCounter check err(%v)\n", me.Err) if me.pb.Fast { if me.last.Fast { me.pb.HelpCounter = me.last.HelpCounter + 1 } else { me.pb.HelpCounter = 0 } } else { errors.Join(me.Err, me.autoFunc()) // run the autocomplete function the user made for their application } fmt.Fprintf(Stderr, "got to the end err(%v)\n", me.Err) return } func savePB() { // save now. this is near the end probably me.all.Append(me.pb) npb := new(Argv) // npb.Uuid = uuid.New().String() me.all.Append(npb) errors.Join(me.Err, me.all.Save()) } func doStdoutStderr() { if me.debug { me.all.PrintHistory(me.last.GetCmd()) fmt.Fprintf(Stderr, "debug=true pb.Stdout (%v)\n", PB.Stdout) } else { if me.pb.Fast { if me.pb.HelpCounter < 3 { fmt.Fprintf(Stderr, "help counter < 3\n") } } } PrintStderr() PrintStdout() } func saveAndExit() { savePB() doStdoutStderr() os.Exit(0) } // sets me.last // computes me.pb.Duration func examineArgvHistory() { me.all = NewArgvs() // loads the argv autocomplete history file me.Err = config.ForceCreateCacheDirPB(me.all, "argv", me.pb.AppInfo.APPNAME) if me.Err != nil { // there is no history. // ALWAYS KEEP THESE LINES AND THE panic() // They are needed to debug autocomplete. // // This code is only executed when the user is hitting tab in the shell, // so this panic() is safe and can never be triggered by normal program execution. // me.debug = true me.pb.Stderr += fmt.Sprintf("config.CreateCacheDirPB() err(%v)\n", me.Err) doStdoutStderr() panic("argvpb.Load() history file failed") } if me.debug { // use this if you are having trouble debugging this code // me.all.PrintHistory("EARLY") // doStdoutStderr() } // roll the autocomplete file maxsize := 17 trim := 10 if me.all.Len() > maxsize { me.pb.Debugf("DEBUG: trim() history is over 17 len=%d vs new=%d", me.all.Len(), me.all.Len()-trim) me.pb.Debugf("DEBUG: trim() history is over 17 len=%d vs new=%d", me.all.Len(), me.all.Len()-trim) me.pb.Debugf("DEBUG: trim() history is over 17 len=%d vs new=%d", me.all.Len(), me.all.Len()-trim) me.all.Argvs = me.all.Argvs[me.all.Len()-trim:] // newall.Autos = me.all.Autos[0:10] } // drops nil entries from the history // even gets dup uuids. don't have time to figure this out now // need to launch the WIT private cloud product // todo: figure out why nil values are happening // a facinating PITA this is counter := 0 uuidmap := make(map[string]*Argv) for pb := range me.all.IterAll() { counter += 1 if pb.Ctime == nil { me.all.Delete(pb) continue } if pb.Uuid == "" { me.all.Delete(pb) continue } if _, ok := uuidmap[pb.Uuid]; ok { // already have this uuid, keep the oldest one me.all.Delete(pb) continue } uuidmap[pb.Uuid] = pb // if me.debug { // hist := fmt.Sprintf("HISTNIL(%d)", counter) // pb.PrintDebugNew(hist, "too soon") // } } // only have nil values in the .pb file. just die for now if me.all.Len() == 0 { me.debug = true fmt.Fprintf(Stderr, "examineArgvHistory() empty file %s\n", me.all.Filename) doStdoutStderr() saveAndExit() } // finally safe to get the last history entry me.last = me.all.Argvs[me.all.Len()-1] // compute the duration since the last time dur := time.Since(me.last.Ctime.AsTime()) me.pb.Duration = durationpb.New(dur) if me.pb.Duration.AsDuration() < time.Millisecond*200 { me.pb.Fast = true if me.last.Fast { // do the smart something here if me.pb.GetCmd() == me.last.GetCmd() { // turn on debugging if duration < 200 milliseconds } else { // reset help counter me.pb.HelpCounter = 0 } } } }