package cli import ( "fmt" "os" "os/exec" "reflect" "strconv" "strings" "time" ) // InputFromEditor retrieves user input by opening an editor (one specified by // $EDITOR or 'vim' if $EDITOR is not set) on a temporary file. Once the editor // closes, the contents of the file are read and the file is deleted. If the // contents of the file exactly match the value of placeholder (no edits to the // file were made), then an empty string is returned. Otherwise, the contents // are returned. Example patten: message.*.md func InputFromEditor(placeholder string, pattern string) (string, error) { msgFile, _ := os.CreateTemp("/tmp", pattern) defer os.Remove(msgFile.Name()) os.WriteFile(msgFile.Name(), []byte(placeholder), os.ModeAppend) editor := os.Getenv("EDITOR") if editor == "" { editor = "vim" // default to vim if no EDITOR env variable } execCmd := exec.Command(editor, msgFile.Name()) execCmd.Stdin = os.Stdin execCmd.Stdout = os.Stdout execCmd.Stderr = os.Stderr if err := execCmd.Run(); err != nil { return "", err } bytes, _ := os.ReadFile(msgFile.Name()) content := string(bytes) if placeholder != "" { if content == placeholder { return "", nil } // strip placeholder if content begins with it if strings.HasPrefix(content, placeholder) { content = content[len(placeholder):] } } return strings.Trim(content, "\n \t"), nil } // humanTimeElapsedSince returns a human-friendly "in the past" representation // of the given duration. func humanTimeElapsedSince(d time.Duration) string { seconds := d.Seconds() minutes := seconds / 60 hours := minutes / 60 days := hours / 24 weeks := days / 7 months := days / 30 years := days / 365 switch { case seconds < 60: return "seconds ago" case minutes < 2: return "1 minute ago" case minutes < 60: return fmt.Sprintf("%d minutes ago", int64(minutes)) case hours < 2: return "1 hour ago" case hours < 24: return fmt.Sprintf("%d hours ago", int64(hours)) case days < 2: return "1 day ago" case days < 7: return fmt.Sprintf("%d days ago", int64(days)) case weeks < 2: return "1 week ago" case weeks <= 4: return fmt.Sprintf("%d weeks ago", int64(weeks)) case months < 2: return "1 month ago" case months < 12: return fmt.Sprintf("%d months ago", int64(months)) case years < 2: return "1 year ago" default: return fmt.Sprintf("%d years ago", int64(years)) } } // SetStructDefaultValues checks for any nil ptr fields within the passed // struct, and sets the values of those fields to the value that is contained // within their "default" struct tag. Handles setting string, int, and bool // values. Returns whether any changes were made to the struct. func SetStructDefaults(data interface{}) bool { v := reflect.ValueOf(data).Elem() changed := false for i := 0; i < v.NumField(); i++ { field := v.Field(i) // Check if we can set the field value if !field.CanSet() { continue } // We won't bother with non-pointer fields if field.Kind() != reflect.Ptr { continue } t := field.Type() // type of pointer e := t.Elem() // type of value of pointer // Handle nested structs recursively if e.Kind() == reflect.Struct { if field.IsNil() { field.Set(reflect.New(e)) changed = true } result := SetStructDefaults(field.Interface()) if result { changed = true } continue } if !field.IsNil() { continue } // Get the "default" struct tag defaultTag := v.Type().Field(i).Tag.Get("default") if defaultTag == "" { continue } // Set nil pointer fields to their defined defaults switch e.Kind() { case reflect.String: defaultValue := defaultTag field.Set(reflect.ValueOf(&defaultValue)) case reflect.Int, reflect.Int32, reflect.Int64: intValue, _ := strconv.ParseInt(defaultTag, 10, 64) field.Set(reflect.New(e)) field.Elem().SetInt(intValue) case reflect.Bool: boolValue := defaultTag == "true" field.Set(reflect.ValueOf(&boolValue)) } changed = true } return changed }