summaryrefslogtreecommitdiff
path: root/auto.Complete.go
diff options
context:
space:
mode:
Diffstat (limited to 'auto.Complete.go')
-rw-r--r--auto.Complete.go508
1 files changed, 508 insertions, 0 deletions
diff --git a/auto.Complete.go b/auto.Complete.go
new file mode 100644
index 0000000..4de01db
--- /dev/null
+++ b/auto.Complete.go
@@ -0,0 +1,508 @@
+package prep
+
+// initializes logging and command line options
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "time"
+
+ "go.wit.com/dev/alexflint/arg"
+ "go.wit.com/lib/config"
+ "go.wit.com/log"
+ durationpb "google.golang.org/protobuf/types/known/durationpb"
+ timestamppb "google.golang.org/protobuf/types/known/timestamppb"
+)
+
+// makes a bash autocomplete file for your command
+func (pb *Auto) doHandlePB() error {
+ homeDir, err := os.UserHomeDir()
+ if err != nil {
+ return err
+ }
+ basedir := filepath.Join(homeDir, ".cache/autocomplete")
+ os.MkdirAll(basedir, os.ModePerm)
+ fullname := filepath.Join(basedir, pb.Argname+".pb")
+
+ all := NewAutos()
+ var last *Auto
+ data, err := os.ReadFile(fullname)
+ if err == nil {
+ err = all.Unmarshal(data)
+ if err == nil {
+ for found := range all.IterAll() {
+ dur := time.Since(found.Ctime.AsTime())
+ pb.Duration = durationpb.New(dur)
+ // found.PrintDebug()
+ cmd := fmt.Sprintf("cmd='%s'", found.Cmd)
+ arglast := fmt.Sprintf("last='%s'", found.Last)
+ partial := fmt.Sprintf("p='%s'", found.Partial)
+ age := fmt.Sprintf("age='%-6.6s'", config.FormatDuration(dur))
+ pb.Debugf("AUTO HISTORY: %s %-18.18s %-18.18s %-12.12s argv='%v' goargs='%v'", age, cmd, arglast, partial, found.Argv, found.Goargs)
+ last = found
+ }
+ }
+ }
+
+ if all.Len() > 15 {
+ pb.Debugf("DEBUG: trim() history is over 100 len=%d vs new=%d", all.Len(), all.Len()-90)
+ all.Autos = all.Autos[all.Len()-10:]
+ // newall.Autos = all.Autos[0:10]
+ // for _, found := range all.Autos[0:10] {
+ // newall.Append(found)
+ // }
+ }
+
+ // need this for the first time the user runs autocomplete
+ if last == nil {
+ last = new(Auto)
+ }
+
+ now := time.Now()
+ pb.Ctime = timestamppb.New(now)
+ duration := time.Since(last.Ctime.AsTime())
+ all.Append(pb)
+
+ data, err = all.Marshal()
+ if err != nil {
+ return err
+ }
+
+ f, err := os.OpenFile(fullname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
+ defer f.Close()
+ if err != nil {
+ return err
+ }
+ _, err = f.Write(data)
+ pb.Debugf("WRITE DEBUG: write PB='%s' len(pb)=%d len(data)=%d dur=%v err=%v", fullname, all.Len(), len(data), duration, err)
+ return err
+}
+
+func (pb *Auto) SubCommand(cmd ...string) {
+ partial := strings.Trim(pb.Partial, "'")
+ if pb.Debug {
+ if myAuto.examples == nil {
+ pb.Debugf("WRITE DEBUG: argv.Examples() not defined")
+ // log.Fprintf(os.Stderr, "\n")
+ // log.Fprintf(os.Stderr, "examples was nil\n")
+ // log.Fprintf(os.Stderr, "\n")
+ } else {
+ log.Fprintf(os.Stderr, "\n")
+ log.Fprintf(os.Stderr, "\n")
+ log.Fprintf(os.Stderr, "Examples:\n")
+ for _, line := range strings.Split(myAuto.examples(), "\n") {
+ log.Fprintf(os.Stderr, " %s\n", line)
+ }
+ // log.Fprintf(os.Stderr, "\n")
+ }
+ myAuto.pp.WriteHelpForAutocomplete(os.Stderr, os.Stdout, partial, cmd...)
+ // myAuto.pp.GetUsageForSubcommand(os.Stdout, os.Stderr, partial, cmd)
+ // myAuto.pp.GetUsageForSubcommand(os.Stdout, nil, partial, cmd)
+ } else {
+ f, _ := os.OpenFile("/tmp/outlook", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
+ myAuto.pp.WriteHelpForAutocomplete(f, os.Stdout, partial, cmd...)
+ // myAuto.pp.GetUsageForSubcommand(os.Stdout, nil, partial, cmd)
+ }
+ os.Exit(0)
+ // SubCommand(cmd)
+}
+
+/*
+func (pb *Auto) SubCommandShow() {
+ partial := strings.Trim(pb.Partial, "'")
+ if pb.Debug {
+ myAuto.pp.WriteHelpForAutocomplete(os.Stderr, os.Stdout, partial, "show", "repo")
+ } else {
+ f, _ := os.OpenFile("/tmp/outlook", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
+ myAuto.pp.WriteHelpForAutocomplete(f, os.Stdout, partial, "show", "repo")
+ }
+ os.Exit(0)
+}
+
+func (pb *Auto) SubCommand2(cmd string, addmatch []string) {
+ partial := strings.Trim(pb.Partial, "'")
+ if pb.Debug {
+ // myAuto.pp.WriteHelpForAutocomplete(os.Stderr, os.Stdout, partial, pb.Cmd)
+ myAuto.pp.WriteHelpForAutocomplete(os.Stderr, os.Stdout, "", "")
+ // myAuto.pp.GetUsageForSubcommand(os.Stdout, os.Stderr, partial, cmd)
+ // myAuto.pp.GetUsageForSubcommand(os.Stdout, nil, partial, cmd)
+ } else {
+ f, _ := os.OpenFile("/tmp/outlook", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
+ myAuto.pp.WriteHelpForAutocomplete(f, os.Stdout, partial, pb.Cmd)
+ // myAuto.pp.GetUsageForSubcommand(os.Stdout, nil, partial, cmd)
+ }
+ os.Exit(0)
+ // SubCommand(cmd)
+}
+*/
+
+// todo: move everything to this?
+func (pb *Auto) Autocomplete3(sendthis []string) {
+ pb.Autocomplete2(strings.Join(sendthis, " "))
+}
+
+func (pb *Auto) Autocomplete2(sendthis string) {
+ dur := pb.Duration.AsDuration()
+ if dur < time.Millisecond*200 {
+ pb.Debug = true
+ /*
+ pb.Debugf("TODO: show extended help here '%s' '%s' %v dur=%v\n", pb.Arg0, pb.Arg1, pb.Argv, config.FormatDuration(dur))
+ pb.PrintDebug()
+ fmt.Println(" ")
+ */
+ if myAuto.pp == nil {
+ pb.Debugf("myAuto.pp == nil")
+ } else {
+ pb.Debugf("myAuto.pp != nil")
+ if pb.Cmd == "" {
+ myAuto.pp.WriteHelp(os.Stderr)
+ } else {
+ myAuto.pp.WriteHelpForSubcommand(os.Stderr, pb.Cmd)
+ }
+ }
+ }
+
+ parts := strings.Split(sendthis, " ")
+ var all []string
+ for _, part := range parts {
+ var found bool
+ for _, s := range os.Args {
+ if s == part {
+ found = true
+ }
+ }
+ if found {
+ continue
+ }
+ all = append(all, part)
+ }
+ fmt.Printf("%s", strings.Join(all, " "))
+ /*
+ if dur > time.Millisecond*200 {
+ if dur < time.Millisecond*800 {
+ // fmt.Println("a b")
+ fmt.Println(pb.Partial + " hello world")
+ }
+ }
+ */
+}
+
+func parseArgv(argname string) *Auto {
+ pb := new(Auto)
+ pb.Argname = argname
+ if len(os.Args) == 0 {
+ return pb
+ }
+
+ if len(os.Args) > 1 && os.Args[1] == "--bash" {
+ pb.SetupAuto = true
+ return pb
+ }
+
+ // HACK: set debug flag if --autodebug is passed
+ for _, s := range os.Args {
+ if s == "--autodebug" {
+ pb.Debug = true
+ }
+ }
+
+ // "complete -C /usr/bin/argv forge" bash ENV values:
+ // * `COMP_LINE`: A string containing the entire current command line (forge first second third forth ). This is what you are looking for.
+ // * `COMP_POINT`: A number indicating the cursor's position (index) within the COMP_LINE.
+ // * `COMP_WORDS`: An array in Bash (seen differently by Go) containing each individual word on the command line.
+ // * `COMP_CWORD`: A number indicating the index of the word the cursor is currently on within the COMP_WORDS array.
+ if len(os.Args) > 1 && os.Args[1] == pb.Argname {
+ pb.IsAuto = true
+ parts := strings.Split(os.Getenv("COMP_LINE"), " ")
+ pb.Debug = true
+ log.Fprintf(os.Stderr, "\n")
+ pb.Debugf("MATCH Partial os.Args=%v COMP_LINE=%v", os.Args, os.Getenv("COMP_LINE"))
+ // log.Fprintf(os.Stdout, "jcarr")
+ if len(parts) > 0 {
+ pb.Arg0 = parts[0]
+ }
+ pb.Arg1 = os.Args[1]
+ os.Exit(0)
+ }
+
+ if len(os.Args) > 1 && os.Args[1] == "--version" {
+ if myAuto.buildtime != nil {
+ // if binary defined buildtime() then process standard version output here
+ doVersion(pb)
+ os.Exit(0)
+ }
+ }
+ if len(os.Args) == 0 {
+ return pb
+ }
+
+ if len(os.Args) == 1 {
+ pb.Arg0 = os.Args[0]
+ return pb
+ }
+ if os.Args[1] != "--auto-complete" {
+ pb.Cmd = os.Args[1]
+ return pb
+ }
+
+ // should we do auto complete here?
+ if len(os.Args) > 1 && os.Args[1] == "--auto-complete" {
+ pb.IsAuto = true
+ pb.Arg0 = os.Args[0]
+ pb.Arg1 = os.Args[1]
+ pb.Partial = os.Args[2]
+ pb.Arg3 = os.Args[3]
+ if len(os.Args) < 5 {
+ // the user is doing autocomplete on the command itself
+ pb.Partial = ""
+ pb.Cmd = ""
+ pb.Argv = []string{""}
+ return pb
+ }
+ if pb.Partial == "''" {
+ pb.Partial = ""
+ }
+ // pb.Argv = os.Args[4:]
+ for _, s := range os.Args[4:] {
+ if s == "--autodebug" {
+ continue
+ }
+ tmp := strings.Trim(pb.Partial, "'")
+ if tmp == s {
+ // don't put pb.Partial into Argv
+ continue
+ }
+ pb.Argv = append(pb.Argv, s)
+ }
+ // set pb.Cmd to the first thing that doesn't have a '-' arg
+ for _, s := range pb.Argv {
+ if strings.HasPrefix(s, "-") {
+ continue
+ }
+ pb.Cmd = s
+ break
+ }
+ if pb.Partial == "'"+pb.Cmd+"'" {
+ // not really a command, it's just a partially inputed string from the user
+ pb.Cmd = ""
+ }
+ // try to figure out what the last argv is
+ for _, s := range pb.Argv {
+ p := fmt.Sprintf("'%s'", s)
+ if pb.Partial == p {
+ pb.Debugf("DEBUG: Last argv MATCHES Partial %s %s", s, p)
+ continue
+ } else {
+ pb.Debugf("DEBUG: Last argv DOES NOT MATCH Partial %s %s", s, p)
+ }
+ pb.Last = s
+ if strings.HasPrefix(s, "-") {
+ // skip args like -test --verbose when sending subcommands to go-args for help text
+ continue
+ }
+ pb.Goargs = append(pb.Goargs, s)
+ }
+ // if pb.Cmd == "" {
+ // pb.Cmd = strings.Join(pb.Argv, "BLAH")
+ // }
+ }
+ return pb
+}
+
+func Bash3(dest any) *Auto {
+ return Bash(dest)
+}
+
+func Bash(dest any) *Auto {
+ myAuto = new(AutoArgs)
+ findAppInfo(dest) // parses back to main() for argv info
+
+ pb := parseArgv(myAuto.appName)
+ if pb.SetupAuto {
+ // --bash was passed. try to configure bash-completion
+ doBash(myAuto.appName)
+ os.Exit(0)
+ }
+
+ myAuto.match = make(map[string]string)
+ myAuto.match["--gui"] = "andlabs gocui"
+
+ if pb.Debug {
+ // dump debug info
+ pb.PrintDebug()
+ }
+
+ pb.doHandlePB() // read in the history protobuf file
+
+ // turn on debugging if duration < 200 milliseconds
+ dur := pb.Duration.AsDuration()
+ if dur < time.Millisecond*200 {
+ pb.Debug = true
+ }
+
+ // prepart["--gui"] = "andlabs gocui"
+
+ arg.Register(&argBash)
+ flags := []string{}
+ for _, s := range pb.Argv {
+ if s == "--autodebug" {
+ continue
+ }
+ flags = append(flags, s)
+ }
+ // pb.Debug = true
+ // pb.Debugf("DEBUG: MustParse(%v)", flags)
+ var err error
+ myAuto.pp, err = arg.ParseFlags(flags, dest)
+ if err != nil {
+ pb.Debugf("DEBUG: Parse error: %v", err)
+ }
+
+ if myAuto.pp == nil {
+ pb.Debugf("DEBUG: myAuto.pp == nil after ParseFlags()")
+ } else {
+ // pb.Debugf("DEBUG: myAuto.pp is ok after ParseFlags()")
+ }
+
+ if pb.IsAuto {
+ for key, val := range myAuto.match {
+ if pb.Last == key {
+ pb.Debugf("DEBUG: last=%s found key %s = %s", pb.Last, key, val)
+ pb.Autocomplete2(val)
+ os.Exit(0)
+ } else {
+ // pb.Debugf("DEBUG: NO MATCH last='%s' found key '%s' = %s", pb.Last, key, val)
+ }
+ }
+ if myAuto.autoFunc == nil {
+ pb.SubCommand(pb.Argv...)
+ } else {
+ myAuto.autoFunc(pb) // run the autocomplete function the user made for their application
+ }
+ if pb.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
+ }
+ os.Exit(0)
+ }
+
+ arg.Register(&argBash)
+ myAuto.pp = arg.MustParse(dest)
+ return pb
+}
+
+// makes a bash autocomplete file for your command
+func doBash(argname string) {
+ fmt.Println(makeBashCompletionText2(argname))
+
+ homeDir, err := os.UserHomeDir()
+ if err != nil {
+ log.Printf("# %v\n", err)
+ os.Exit(0)
+ }
+ filename := filepath.Join(homeDir, ".local/share/bash-completion/completions", argname)
+ if config.Exists(filename) {
+ log.Fprintln(os.Stderr, "# file already exists", filename)
+ // os.Exit(0)
+ }
+ basedir, _ := filepath.Split(filename)
+ if !config.IsDir(basedir) {
+ os.MkdirAll(basedir, os.ModePerm)
+ }
+
+ if f, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644); err == nil {
+ f.Write([]byte(makeBashCompletionText2(argname)))
+ f.Close()
+ } else {
+ log.Fprintln(os.Stderr, "# open file error", filename, err)
+ }
+ os.Exit(0)
+}
+
+func (pb *Auto) Version() string {
+ return pb.getVersion()
+}
+
+func doVersion(pb *Auto) {
+ log.Info(pb.getVersion())
+ os.Exit(0)
+}
+
+func (pb *Auto) getVersion() string {
+ if myAuto.buildtime == nil {
+ return "app doesn't have argv.BuildVersion()"
+ }
+ BUILDTIME, VERSION := myAuto.buildtime()
+
+ parts := strings.Split(BUILDTIME, ".")
+ if len(parts) == 1 {
+ // The input epoch seconds
+ // epochSeconds := int64(1758646486)
+ num, err := strconv.Atoi(BUILDTIME)
+ epochSeconds := int64(num)
+ if err == nil {
+
+ // 1. Convert the epoch seconds to a time.Time object.
+ // time.Unix() creates the time in the UTC timezone by default.
+ t := time.Unix(epochSeconds, 0)
+
+ // 2. Convert the UTC time to the computer's local timezone.
+ localTime := t.Local()
+
+ // 3. Print the result. The default format is clear and includes the timezone.
+ // fmt.Println("Default format:", localTime)
+ // For a more human-friendly format, use the Format() method.
+ // Go uses a special reference time for formatting: Mon Jan 2 15:04:05 2006 MST
+ // You lay out your desired format using these specific numbers.
+ // formattedString := localTime.Format("Monday, January 2, 2006 at 3:04:05 PM (MST)")
+ // fmt.Println(" Custom format:", formattedString)
+
+ // now := time.Now()
+ // dur := time.Since(localTime)
+ // BUILDTIME = fmt.Sprintf("%s age(%v)", localTime.String(), , config.FormatDuration(time.Since(localTime)))
+ stamp := log.Sprintf("Built %s Age(%s)", localTime.Format("2006-01-02 15:04"), config.FormatDuration(time.Since(localTime)))
+ return fmt.Sprintf("%s %s %s", pb.Argname, VERSION, stamp)
+ }
+ }
+
+ return fmt.Sprintf("%s %s Built on %s", pb.Argname, VERSION, BUILDTIME)
+}
+
+func StandardVersion(ARGNAME, VERSION, BUILDTIME string) string {
+ parts := strings.Split(BUILDTIME, ".")
+ if len(parts) == 1 {
+ // The input epoch seconds
+ // epochSeconds := int64(1758646486)
+ num, err := strconv.Atoi(BUILDTIME)
+ epochSeconds := int64(num)
+ if err == nil {
+
+ // 1. Convert the epoch seconds to a time.Time object.
+ // time.Unix() creates the time in the UTC timezone by default.
+ t := time.Unix(epochSeconds, 0)
+
+ // 2. Convert the UTC time to the computer's local timezone.
+ localTime := t.Local()
+
+ // 3. Print the result. The default format is clear and includes the timezone.
+ // fmt.Println("Default format:", localTime)
+ // For a more human-friendly format, use the Format() method.
+ // Go uses a special reference time for formatting: Mon Jan 2 15:04:05 2006 MST
+ // You lay out your desired format using these specific numbers.
+ // formattedString := localTime.Format("Monday, January 2, 2006 at 3:04:05 PM (MST)")
+ // fmt.Println(" Custom format:", formattedString)
+
+ // now := time.Now()
+ // dur := time.Since(localTime)
+ // BUILDTIME = fmt.Sprintf("%s age(%v)", localTime.String(), , config.FormatDuration(time.Since(localTime)))
+ stamp := log.Sprintf("Built %s Age(%s)", localTime.Format("2006-01-02 15:04"), config.FormatDuration(time.Since(localTime)))
+ return fmt.Sprintf("%s %s %s", ARGNAME, VERSION, stamp)
+ }
+ }
+
+ return fmt.Sprintf("%s %s Built on %s", ARGNAME, VERSION, BUILDTIME)
+}