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" timestamppb "google.golang.org/protobuf/types/known/timestamppb" ) // 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(dest any) *Argv { me = new(AutoArgs) // todo: redo this me.pb = new(Argv) // set the start time of the binary now := time.Now() me.pb.Ctime = timestamppb.New(now) // makes sure the application has the // needed functions defined, otherwise dies verifyApplication(dest) // gets APPNAME, BUILDTIME and VERSION from the application initAppname() // 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) // loads the argv autocomplete history file me.all = NewArgvs() me.Err = config.CreateCacheDirPB(me.all, "argv", me.pb.AppInfo.APPNAME) if me.Err == nil { me.all.PrintHistory() } else { // there is no history. // todo: initialize the history file // todo: check if this is automatically done already // // 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) me.pb.PrintStderr() panic("argvpb.Load() history file failed") } // 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 { savePB() // --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 { // do autocomplete and exit savePB() doAutocomplete() // todo: this never gets here. fix that saveAndExit() } // not autocompleting. return to the application // save the pb & history me.all.Clone(me.pb) errors.Join(me.Err, me.all.Save()) // me.pp = arg.MustParse(dest) me.Err = errors.Join(me.Err, me.mustParseFunc()) return me.pb } // prints what is needed to Stdout & Stderr, then exits // EVERYTHING PAST HERE IS FOR AUTOCOMPLETE // everything sent to STDOUT and STDERR matters past this point func doAutocomplete() { // set the duration since the last auto complete // find the last entry. this is dumb way to do it me.last = new(Argv) me.last.Ctime = timestamppb.New(time.Now().Add(-time.Second)) for found := range me.all.IterAll() { me.last = found } dur := time.Since(me.last.Ctime.AsTime()) me.pb.Duration = durationpb.New(dur) if me.debug { // dump debug info me.pb.PrintDebug() me.all.PrintHistory() // me.pb.PrintStderr() } // roll the autocomplete file if me.all.Len() > 15 { me.pb.Debugf("DEBUG: trim() history is over 100 len=%d vs new=%d", me.all.Len(), me.all.Len()-90) me.all.Argvs = me.all.Argvs[me.all.Len()-10:] // newall.Autos = me.all.Autos[0:10] // for _, found := range me.all.Autos[0:10] { // newall.Append(found) // } } // turn on debugging if duration < 200 milliseconds dur = me.pb.Duration.AsDuration() if dur < time.Millisecond*200 { me.debug = true me.pb.Fast = true // me.fastcmd = me.pb.GetCmd() if me.last.Fast { if me.pb.GetCmd() == me.last.GetCmd() { // do the smart something here } } } flags := []string{} for _, s := range me.pb.Real { if s == "--autodebug" { continue } if s == "--argvdebug" { me.pb.PrintStderr() continue } if strings.TrimSpace(s) == "" { // skip anything blank continue } flags = append(flags, s) } if strings.HasPrefix(me.pb.Partial, "--argv") { me.pb.SendString("--argvdebug --argvhelp") me.pb.Stderr += fmt.Sprintln("argv override") me.debug = true me.pb.PrintStderrExit() } // 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) me.pb.PrintStderr() me.pb.SendString("andlabs gogui") saveAndExit() } else { // me.pb.Debugf("DEBUG: NO MATCH last='%s' found key '%s' = %s", me.last, key, val) } } // use go-args to parse the structs so we can use them here // me.pp, err = arg.ParseFlags(flags, dest) if me.parseFlagsFunc == nil { me.pb.Stderr += fmt.Sprintln("argv.parseFlags() is nil") me.debug = true me.pb.PrintStderrExit() } if err := me.parseFlagsFunc(flags); err != nil { me.pb.Stderr += fmt.Sprintf("application parseFlags() err(%v)\n", err) me.pb.Stderr += fmt.Sprintln("argv.parseFlags() err. probably cmd doesn't really exist in struct") me.debug = true me.pb.PrintStderrExit() } if len(flags) == 0 { // error is normal if there are no command line args } else { if me.Err != nil { // users has command line arguments that won't parse with go-args me.pb.Debugf("DEBUG: Parse error: %v flags%v", me.Err, flags) } } me.pb.HelpCounter = me.last.HelpCounter if me.pb.Fast { if me.last.Fast { me.debug = true me.pb.HelpCounter = me.last.HelpCounter + 1 if me.pb.HelpCounter < 3 { me.pb.PrintStderr() } else { saveAndExit() } } else { // this means the user is pressing tab. no longer doing stderr if me.pb.GetCmd() == "" { // me.pp.WriteHelp(Stderr) me.writeHelpFunc() } else { // me.pp.WriteHelpForSubcommand(Stderr, me.pb.Cmd) me.writeHelpForSubcommandFunc(me.pb.GetCmd()) } me.pb.HelpCounter = 0 } } else { if me.autoFunc == nil { me.pb.SubCommand(me.pb.Real...) } else { me.autoFunc(me.pb) // run the autocomplete function the user made for their application } if me.debug { // TODO: // check here to see if there was any completion text sent // if not, send "reset bash newline\n" to cause bash to redraw PS1 for the user } } saveAndExit() } func savePB() { // save now. this is near the end probably me.all.Clone(me.pb) errors.Join(me.Err, me.all.Save()) } func saveAndExit() { savePB() os.Exit(0) }